agrestio / agrest Goto Github PK
View Code? Open in Web Editor NEWServer-side Java REST Framework for easy access to data graphs from various backends
Home Page: https://agrest.io
License: Apache License 2.0
Server-side Java REST Framework for easy access to data graphs from various backends
Home Page: https://agrest.io
License: Apache License 2.0
operator : String
The operator to use to compare the property to this Filter's value
Possible values are:
<
<=
=
>=
>
!=
in
like
The in operator expects this filter's value to be an array and matches values that are present in that array.
The like operator matches values that contain this filter's value as a substring.
Currently LR supports "group" as an object array: group:[{"property":"blabla","direction":"ASC"}, ...].
It'll be nice to have "group" as an object supported. Example: group:{"property":"blabla","direction":"ASC"}
The motivation for this is the following problem that happened when trying to "unrelate" all objects in a relationship:
Found this issue in 1.3 - 1..N:
service.unrelate(E.class, id, _E.REL);
Results in Exception:
CommonsJdbcEventLogger: *** error.
java.sql.BatchUpdateException: Column 'E_ID' cannot be null
at com.mysql.jdbc.PreparedStatement.executeBatchSerially(PreparedStatement.java:2054)
....
at org.apache.cayenne.access.DataContext.commitChanges(DataContext.java:676)
at com.nhl.link.rest.runtime.cayenne.CayenneDao.unrelate(CayenneDao.java:166)
at com.nhl.link.rest.runtime.EntityDaoLinkRestService.unrelate(EntityDaoLinkRestService.java:119)
Side effect of this feature will be that "unrelate" will stop deleting objects
When recursively compiling constraints from annotations, we can get into the endless loop, resulting in a stack overflow. We must account for the fact that Cayenne model is a graph , not a tree and keep track of processed entities as we are doing the compilation.
javax.ws.rs.ProcessingException: Server-side request processing failed with an error.
at org.glassfish.jersey.test.inmemory.InMemoryConnector$InMemoryResponseWriter.failure(InMemoryConnector.java:168)
at org.glassfish.jersey.server.ServerRuntime$Responder.process(ServerRuntime.java:433)
at org.glassfish.jersey.server.ServerRuntime$1.run(ServerRuntime.java:265)
at org.glassfish.jersey.internal.Errors$1.call(Errors.java:271)
at org.glassfish.jersey.internal.Errors$1.call(Errors.java:267)
at org.glassfish.jersey.internal.Errors.process(Errors.java:315)
at org.glassfish.jersey.internal.Errors.process(Errors.java:297)
at org.glassfish.jersey.internal.Errors.process(Errors.java:267)
at org.glassfish.jersey.process.internal.RequestScope.runInScope(RequestScope.java:320)
at org.glassfish.jersey.server.ServerRuntime.process(ServerRuntime.java:236)
at org.glassfish.jersey.server.ApplicationHandler.handle(ApplicationHandler.java:1028)
at org.glassfish.jersey.test.inmemory.InMemoryConnector.apply(InMemoryConnector.java:278)
at org.glassfish.jersey.client.ClientRuntime.invoke(ClientRuntime.java:225)
at org.glassfish.jersey.client.JerseyInvocation$1.call(JerseyInvocation.java:655)
at org.glassfish.jersey.client.JerseyInvocation$1.call(JerseyInvocation.java:652)
at org.glassfish.jersey.internal.Errors.process(Errors.java:315)
at org.glassfish.jersey.internal.Errors.process(Errors.java:297)
at org.glassfish.jersey.internal.Errors.process(Errors.java:228)
at org.glassfish.jersey.process.internal.RequestScope.runInScope(RequestScope.java:424)
at org.glassfish.jersey.client.JerseyInvocation.invoke(JerseyInvocation.java:652)
at org.glassfish.jersey.client.JerseyInvocation$Builder.method(JerseyInvocation.java:387)
at org.glassfish.jersey.client.JerseyInvocation$Builder.get(JerseyInvocation.java:291)
at com.nhl.link.rest.incontainer.GET_Constraints_Test.test_Annotated(GET_Constraints_Test.java:64)
...
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:390)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:197)
Caused by: java.lang.StackOverflowError
at org.apache.cayenne.di.Key.(Key.java:49)
at org.apache.cayenne.di.Key.get(Key.java:37)
at org.apache.cayenne.di.spi.DefaultInjector.getProvider(DefaultInjector.java:141)
at org.apache.cayenne.di.spi.DefaultInjector.getInstance(DefaultInjector.java:131)
at org.apache.cayenne.configuration.CayenneRuntime.getChannel(CayenneRuntime.java:165)
at com.nhl.link.rest.runtime.cayenne.CayennePersister.entityResolver(CayennePersister.java:34)
at com.nhl.link.rest.runtime.constraints.ConstraintsHandler.compile(ConstraintsHandler.java:112)
at com.nhl.link.rest.runtime.constraints.ConstraintsHandler.compileRead(ConstraintsHandler.java:102)
at com.nhl.link.rest.runtime.constraints.ConstraintsHandler.compile(ConstraintsHandler.java:132)
at com.nhl.link.rest.runtime.constraints.ConstraintsHandler.compileRead(ConstraintsHandler.java:102)
at com.nhl.link.rest.runtime.constraints.ConstraintsHandler.compile(ConstraintsHandler.java:132)
at com.nhl.link.rest.runtime.constraints.ConstraintsHandler.compileRead(ConstraintsHandler.java:102)
at com.nhl.link.rest.runtime.constraints.ConstraintsHandler.compile(ConstraintsHandler.java:132)
at com.nhl.link.rest.runtime.constraints.ConstraintsHandler.compileRead(ConstraintsHandler.java:102)
at com.nhl.link.rest.runtime.constraints.ConstraintsHandler.compile(ConstraintsHandler.java:132)
...
at com.nhl.link.rest.runtime.constraints.ConstraintsHandler.compileRead(ConstraintsHandler.java:102)
at com.nhl.link.rest.runtime.constraints.ConstraintsHandler.compile(ConstraintsHandler.java:132)
at com.nhl.link.rest.runtime.constraints.ConstraintsHandler.compileRead(ConstraintsHandler.java:102)
at com.nhl.link.rest.runtime.constraints.ConstraintsHandler.compile(ConstraintsHandler.java:132)
at com.nhl.link.rest.runtime.constraints.ConstraintsHandler.compileRead(ConstraintsHandler.java:102)
at com.nhl.link.rest.runtime.constraints.ConstraintsHandler.compile(ConstraintsHandler.java:132)
at com.nhl.link.rest.runtime.constraints.ConstraintsHandler.compileRead(ConstraintsHandler.java:102)
at com.nhl.link.rest.runtime.constraints.ConstraintsHandler.compile(ConstraintsHandler.java:132)
at com.nhl.link.rest.runtime.constraints.ConstraintsHandler.compileRead(ConstraintsHandler.java:102)
at com.nhl.link.rest.runtime.constraints.ConstraintsHandler.compile(ConstraintsHandler.java:132)
at com.nhl.link.rest.runtime.constraints.ConstraintsHandler.compileRead(ConstraintsHandler.java:102)
at com.nhl.link.rest.runtime.constraints.ConstraintsHandler.compile(ConstraintsHandler.java:132)
at com.nhl.link.rest.runtime.constraints.ConstraintsHandler.compileRead(ConstraintsHandler.java:102)
at com.nhl.link.rest.runtime.constraints.ConstraintsHandler.compile(ConstraintsHandler.java:132)
at com.nhl.link.rest.runtime.constraints.ConstraintsHandler.compileRead(ConstraintsHandler.java:102)
at com.nhl.link.rest.runtime.constraints.ConstraintsHandler.compile(ConstraintsHandler.java:132)
at com.nhl.link.rest.runtime.constraints.ConstraintsHandler.compileRead(ConstraintsHandler.java:102)
Define LinkRestAdapter interface - a generic LR extension mechanism that would allow to customize a bunch of services at once. This would allow Sencha specific extensions, but also pretty much any kind of customization that LR DI allows. Here is a proposed API, allowing to contribute to both internal Injector and JAX RS Feature:
public interface LinkRestAdapter {
void contributeToRuntime(Binder binder);
void contributeToJaxRs(Collection<Feature> features);
}
This is a sencha thing, so it should be in the sencha adapter. Default to-one encoder must not include FK
Currently DataResponseConfig is created by calling ILinkRestService and the instance is then passed to SelectBuilder. This only makes possible to configure it once and outside of SelectBuilder. Per #14 LinkRest needs to create its own DataResponseConfig for relationship requests. I guess it makes sense to keep a single instance of DataResponseConfig inside SelectBuilder created lazily. Then it can be initially configured for a given request by LinkRest, and users can customize it as needed.
So....
ILinkRestService.newConfig // will be removed
SelectBuilder.withConfig // will be removed
SelectBuilder<T> withEntity(EntityConfigBuilder builder); // will be added
SelectBuilder<T> fetchOffset(int offset); // will be added
SelectBuilder<T> fetchLimit(int limit); // will be added
Prebuilt entity configurations can still be stored and reused as EntityConfigBuilder instances.
This is a massive refactoring of the insert/update flow that gives us consistent bulk requests with optional idempotent behavior.
Since we have so many intersecting options (insert,update, insert-or-update , idempotent or not, with implied parent relationship or without one), we need a way for the users to build a backend request they need in each specific situation. I came up with an interface like that:
ILinkRestService:
<T> CreateOrUpdateBuilder<T> update(Class<T> type);
<T> CreateOrUpdateBuilder<T> create(Class<T> type);
<T> CreateOrUpdateBuilder<T> createOrUpdate(Class<T> type);
/**
* Returns a CreateOrUpdateBuilder that would perform an idempotent
* create-or-update operation on the request objects. The operation will
* fail if it can't be executed as idempotent. The condition is usually that
* all object's ID should be passed explicitly in request or can be implied
* from a relationship. Otherwise the server will have no way of mapping
* update data to an existing object and the update can't be idempotent.
*/
<T> CreateOrUpdateBuilder<T> idempotentCreateOrUpdate(Class<T> type);
CreateOrUpdateBuilder:
/**
* A builder for create (insert) or update operations for a single entity type.
*/
public interface CreateOrUpdateBuilder<T> {
/**
* Set an explicit id for the update. In this case only a single object is
* allowed in the update.
*/
CreateOrUpdateBuilder<T> id(Object id);
/**
* Sets up a relationship clause for all objects in this update.
*/
CreateOrUpdateBuilder<T> parent(Class<?> parentType, Object parentId, String relationshipFromParent);
/**
* Sets up a relationship clause for all objects in this update.
*/
CreateOrUpdateBuilder<T> parent(Class<?> parentType, Object parentId, Property<T> relationshipFromParent);
/**
* Sets up a relationship clause for all objects in this update.
*/
CreateOrUpdateBuilder<T> toManyParent(Class<?> parentType, Object parentId,
Property<? extends Collection<T>> relationshipFromParent);
UpdateResponse<T> process(String entityData);
}
This will effectively deprecate current "insert" and "update" operations (the are a special case of the builder) and remove all the recent related/relatedNew API (which is hopefully not widely used yet).
Some of the examples are shown under #40 ... "Location:" header to point to the updated resource is a REST-ful way to show the client where to get the latest data, and makes returning full representation in the body optional.
Allow TreeConstraints to be applied to insert/update requests ... 2 separate configurations will define permissions to read and modify attributes and relationships. E.g.:
CreateOrUpdateBuilder
Currently various LinkRest operations that are using ObjectMapper are done per-object. If we are updating/deleting large collections , this is going to result in visible performance impact. Need to batch all relationship ops.
We need a pluggable mechanism for semi-opaque ID encoding/decoding similar to cayenne-lifecycle IdCoder [1]. It should be compatible with the current implicit ID encoding. I.e. single column IDs should render as is. Multi-column should be concatenated with separator. Unlike IdCoder we don't need to include entity name in the encoded id. Also we probably need to implement some kind of escaping scheme to tell separators from ID bodies.
'filter' is an ExtJS style key that supports basic criteria filtering. We need to improve it to support full set of Ext options:
This is a broad refactoring of EntityConfig and DataResponseConfig. Both are gone. Instead we are using TreeConstraints and SizeConstraints objects.
Minor... A code like this servicing PUT:
service.idempotentCreateOrUpdate(E2.class)
.toManyParent(E1.class, id, _E1.E2S)
.mapper(ByKeyAndParentObjectMapper.byKeyAndParent(_E2.NAME))
.with(uriInfo)
.readConstraints(cosntraints).process(data);
I run the same PUT request 3 times. Each request contains 2 E2s. First request generates 2 INSERTs (correct), second and third requests run 2 UPDATEs, updating E2's parent PK - to the same parent ID. This still results in correct data, but UPDATE is not needed and only hits the DB for no good reason.
Expected: no UPDATEs.
Even with fluent API mapping constraints is an ugly task. Wouldn't it be cool if we can map default constraints with annotations on entity classes.
Currently updates are done Sencha-style - the response of POST or PUT (but not DELETE) is always a full representation - our normal LinkRest collection. A more "purist" REST response would look like something like this for a single object:
HTTP/1.1 201 Created
Location: http://example.org/linkrest/myentity/123
HTTP/1.1 200 Ok
Location: http://example.org/linkrest/myentity/123
And this for collection:
HTTP/1.1 200 Ok
Location: http://example.org/linkrest/myentity/
I.e. the body is optional and normally omitted, and "Location" header is added that points to a modified resource. This should probably be the default, and returning full representation - a feature of the SenchaAdapter.
While we are not ready to supply the location header just yet (LinkRest has no notion of the resource URI mappings) , we can at least suppress the collection and let the caller to figure out the Location URI.
With the latest refactoring done around 1.7 to support full sync operations, we've lost a guaranteed order of items on response from POST/PUT. This seems important to Ext/Sencha bulk update code. So will need to ensure the ordering is preserved. If it is not possible to achieve within a general code, this can be made a feature of Sencha adapter.
Cleanup/rename/add setters and getters so that APIs for Entity and EntityConfig aligh with each other.
Right now a Cayenne ValidationException results in 500 response and the exception being exposed to the client. E.g.:
HTTP/1.1 500 Internal Server Error
Content-Length: 210
Content-Type: application/json
Vary: Accept-Encoding
Server: Jetty(8.1.4.v20120524)
{"success":false,
"message":"CayenneRuntimeException [v.3.2.M2.27fc609 May 15 2014 11:40:10]
Validation failures: Validation failure for foo.Entity.property:
\"property\" is required."}
What we can do here is create a ExceptionMapper and a MessageBodyWriter. This will bridge Cayenne validation layer with REST.
Ran into a situation of 1..N relationship with unique target collection, where target uniqueness for a given source is determined by some column. I.e.:
create table target (
id PK,
source_id FK,
type
);
So (source_id, type) are unique. Client does not know target id, but knows source ID and type. Link-rest gets confused when doing the following idempotent operation:
service.idempotentCreateOrUpdate(Target.class)
.toManyParent(Source.class, id, _Source.TARGETS).process(data);
PUT:
{"type":"t1"}
{"success":false,"message":"Request is not idempotent. At least one update has no id"}
Same request with POST instead of PUT (createOrUpdate) results in new object created every time. I guess one way to solve this is to allow passing custom ObjectLocator (currently a private interface in CayenneCreateOrUpdateBuilder) to the builder.
When selecting by id, our code explicitly checks the size of the result, and returns 404 on 0 objects, and 500 on > 1 objects. But sometimes we'd like to select a single object based on a set of criteria that are not id (e.g. matching on a non-id unique column, etc.). To handle this case we will add SelectBuilder.selectOne method that will behave similar to the current selection by Id, no matter the query.
IMetadataService currently returns null for bad entities. Most its users would check for null and throw an exception.. Types of exceptions vary. Sometimes they don't check for null, and this will result in an NPE downstream. So make sure IMetadataService consistently throws LinkRestException/BAD_REQUEST.
Note that this is not a huge deal as till now LinkRest API operated based on Java classes for all root entities, ignoring the possibility of specifying an entity specified dynamically. Although with relationships support (#14) it becomes more important to validate metadata names.
Upgrade dependencies to the latest version:
This is a more generic version of #2 . Essentially every protocol request parameter can be defined on the server , either globally or per request with the goal of either simplify the client's life, or more likely, restrict the client to a certain set of options.
This config object might mirror the structure of DataResponse, and merged into DataResponse according to the merge policy ("intersection", "union").
Updating many-to-many relationship N ... NM ... M. Join table NM has its own meaningful columns so it can't be flattened. NM has a compound PK made of FKs to N and M. An update JSON may look like this:
{ "m":1,"n":2}
{ "m":1} # if "n" is propagated from parent.
I.e. there's no explicit ID, but instead there are propagated IDs which can come from parent of the request and/or from relationship keys.
We need to make request chain steps customizable ... there should be an ability to include/exclude request keys defined in the protocol at https://github.com/nhl/link-rest/wiki/LinkRest-Protocol per stack (and maybe later per request). For now this will be achieved by making the following services injectable:
IFilterProcessor
ISortProcessor
ITreeProcessor
Will also split "group", "groupDir" and "filter" keys to SenchaAdapter.
For entities that exclude most attributes on the server, a default request with no includes or excludes results in a bunch of complaints in the logs:
IntersectConfigMerger: Attribute not allowed, removing: bla
IntersectConfigMerger: Attribute not allowed, removing: foo
Would be nice to only complain about explicit includes that were denied. This also hints that merging after creating a default request filled with all entity attributes may be a suboptimal approach.
This feature is needed to properly handle Sencha requests. For new objects Sencha generates phantom IDs (e.g. "id" : "MyEntity-123", but this customizable - http://docs.sencha.com/extjs/5.0/apidocs/#!/api/Ext.data.identifier.Generator ) . And LinkRest 1.3 can mistake those for real DB ids. So ideally we need an update pipeline filter that would match the ID patterns and replace them with nulls.
We'll use this opportunity to start work on SenchaAdapter , implementing LinkRestAdapter (see #24 ) to provide Sencha-specific bindings.
We are supporting includes on the client, that can be arbitrarily nested. Would be cool to give server a way to restrict that ability per request, as there are clearly security and performance implications. So here is what's suggested:
A proper HTTP response code for the cases when an object is created is 201, not 200
Would be nice to be able to define runtime-scoped policy on request constraints and return Select and CreateOrUpdate builders preconfigured with constraints.
We need to handle all schemas supported by Cayenne:
java.lang.IllegalArgumentException: PK is not a number: ObjectId:....
at com.nhl.link.rest.encoder.NumericObjectIdEncoder.encodeNonNullObject(NumericObjectIdEncoder.java:38)
at com.nhl.link.rest.encoder.AbstractEncoder.encode(AbstractEncoder.java:19)
at com.nhl.link.rest.property.PropertyBuilder.encode(PropertyBuilder.java:60)
at com.nhl.link.rest.encoder.EntityToOneEncoder.encode(EntityToOneEncoder.java:26)
at com.nhl.link.rest.property.PropertyBuilder.encode(PropertyBuilder.java:60)
at com.nhl.link.rest.encoder.EntityEncoder.encodeNonNullObject(EntityEncoder.java:29)
at com.nhl.link.rest.encoder.AbstractEncoder.encode(AbstractEncoder.java:19)
at com.nhl.link.rest.encoder.RootListEncoder.encode(RootListEncoder.java:97)
at com.nhl.link.rest.encoder.RootListEncoder.encode(RootListEncoder.java:59)
at com.nhl.link.rest.DataResponse.writeData(DataResponse.java:153)
at com.nhl.link.rest.provider.DataResponseWriter.writeData(DataResponseWriter.java:23)
at com.nhl.link.rest.provider.DataResponseWriter.writeData(DataResponseWriter.java:13)
at com.nhl.link.rest.provider.BaseResponseWriter$1.generateJSON(BaseResponseWriter.java:50)
PATCH is absolutely critical for any generic REST API to scale. We should investigate and start supporting RFC6902 (JSON Patch).
Also a useful link on the topic: http://kingsfleet.blogspot.be/2014/02/transparent-patch-support-in-jax-rs-20.html
ExtJS 5 serializes grouper into an object (as opposed to an array with a single object). LR SortProcessor needs to be able to accept a single sorter represented by a single object.
Not sure if we are going to expose this as a standalone feature for root objects, but #18 requires same-entity batch update support for relationships. This task is to track code changes needed for this to happen.
Allow to clone DataResponseConfig, and its underlying EntityConfig. This would allow to reuse entity templates and tweak configs for special operations.
Add missing builder API to EntityConfig:
paths(Property<?>...)
paths(String...)
and(Expression e) // rename from 'andQualifier'
or(Expression e)
Originally we used NoRolesEntityAuthorizationEncoderFilter as a superclass for authorization filters. Though the code in it is not related to authorization at all. In fact the only thing it does is applying the filter to a specific entity. So will need to rename it to EntityEncoderFilter and rename the abstract methods accordingly.
Not used and makes no sense withing the context of LinkRest.. A holdover from the times past
It would be a good improvement for this annotations.
The current IEncoderService API is too fancy for its own good:
<T> DataResponse<T> makeEncoder(DataResponse<T> response)
This won't handle UpdateResponse pass-through; this does an implicit assignment of encoder to response. So let's simplify it and make more reusable (though a little more verbose) :
Encoder makeEncoder(DataResponse<?> response);
"Client" prefix creates confusion in the server code.
TreeConstraints is a proper thing to constrain individual requests. Not so much when declaring per-entity constraints. The later form a graph, not a tree and using Tree structure causes issues like #36 . So this is a complete redesign of default constraints using different classes.
With #38 CreateOrUpdateBuilder will be used to delete data , so createOrUpdate is no longer a proper name. So rename to UpdateBuilder. While this not an ideal name, it is more generic than CreateOrUpdateBuilder and actually matches UpdateResponse.
ILinkRestService has these methods:
- forSelectRelated(Class<?> root, Object sourceId, Property<T> relationship);
- forSelectRelated(Class<?> root, Object sourceId, String relationship);
As our experience in 1.3 shows, specifying relationships should better be done inside the builder. So deprecate these methods and add a method to SelectBuilder similar to CreateOrUpdateBuilder:
- parent(Class<?> parentType, Object parentId, Property<T> relationshipFromParent);
- parent(Class<?> parentType, Object parentId, String relationshipFromParent);
Will rename SelectBuilder.withEntity to SelectBuilder.constraints to be consistent with the upcoming readConstraints/writeConstraints of CreateOrUpdateBuilder per #7.
This will support for relationships to LinkRest. Below we'll describe typical scenarios that will be handled, rather than the Java API that handles them. Based on the new API one may handle two types of resources - relationship ("collection") resources and relationship target resources.
Here the resource is the entire relationship collection defined by the source entity, source object id and relationship name. Even if relationship is to-one, it is treated as a LinkRest data collection.
// Return a list of related entities for a give source
GET /foo/{sourceid}/{relationship}
// create new target and relate to source...
// target data passed in the body
POST /foo/{sourceid}/{relationship}
// Break a relationship between source and all its target objects.
// If target objects can't exist without the source, each target object is deleted
DELETE /foo/{sourceid}/{relationship}
Here the resource is a relationship between two existing objects - source and target.
// create a relationship between existing source and target
// unless it already exists.
// additionally target data is updated from the body entity
// this is intended as an idempotent operation, hence - PUT.
// but of course for now LinkRest is purely a Java framework
// so mapping its backend to REST can be done in any possible way
PUT /foo/{sourceid}/{relationship}/{targetid}
// Break a relationship between source and specific target.
// Target may also be deleted if it can't exist without a source
DELETE /foo/{sourceid}/{relationship}/{targetid}
See if we can genericize TreeConstraint . It is rooted in an entity after all, and it should be rather helpful to ensure we are applying the write one to a given request chain
recent "createOrUpdate" functionality in LinkRestService doesn't go far enough. It doesn't allow to build a true PUT request on a collection (e.g. "update the state of 1..N"). It is half way there, but deleting items is not supported. Need to add another Java operation (preliminary name: "idempotentFullSync") that would treat incoming data as a full collection representation and hence would delete server items not in the collection.
There's synergy with #29 - once we switch to multi-object locators, it will be easy to implement delete.
IRelationshipMapper currently has a notion of the special FK column named as "relationship_id". We should limit this to SenchaAdapter, with default implementation using straight "relationship" for the property name.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.