A JSEG schema is created imperatively in three steps:
First, call newSchema
to create a schema builder and a type map.
let jseg = require('jseg');
let [builder, types] = jseg.newSchema();
Second, declare the abstract traits and concrete entities of objects that make up the graph. Both traits and entities may inherit other traits.
builder.trait(name, [traits...])
builder.entity(name, [traits...])
Declared types immediately appear in the type map:
builder.trait('Likeable');
builder.entity('Comment', types.Likeable);
...
New types of scalars can also be defined for the purposes of validation:
builder.scalar(name, [options])
Finally, define fields for each type. There are two classes of fields: attributes and relationships.
builder.finalize({
attributes: {...},
relationships: [...],
});
Attributes are specified on a per-type basis:
attributes: {
Comment: {
message: types.Text,
...
},
...
},
Relationships are specified as pairs of outbound relations:
relationships: [
[[types.Likeable, 'many', 'likers'],
[types.User, 'many', 'likes']],
...
],
See below for details of attribute and relationship specifications.
Attributes are fields containing "scalar" values.
Attributes are specified in the finalize
configuration map as a nested map
of type names to field names to types for those fields.
To customize the behavior of an attribute, create a custom scalar type.
Each entity in a JSEG graph has a type
and a lid
. Both are required when
putting an object the first time.
The type must be in the schema map and may be provided as either the actual type object or a string of the type's name.
The lid
, short for "Local ID", is a string uniquely identifies an entity
across all types in the graph.
Relationships are fields containing references to other entities.
Relationships are specified in the finalize
configuration map as an array
of bidirectional relationship specifications.
A bidirectional relationship specification is a pair of a relationship field specification and its reverse relationship field specification.
A relationship field specification is a triple of type, cardinality, and name. The specification may also provide a forth value: An options map. The triples may be read as the madlib "A type has cardinality field". For example: "A comment has one author" and "A user has many comments, sorted by createdAt".
[[types.Comment, 'one', 'author'],
[types.User, 'many', 'comments', {
compare: (a, b) => Math.sign(a.createdAt - b.createdAt)
}]],
Each relationship field has a cardinality of either one
or many
.
Cardinality one
specifies a field containing a singular, nullable reference.
Cardinality many
specifies a field containing a set of zero or more
references. Sets are represented as arrays.
When a relationship field option of destroy: true
is provided, destroy
operations on the source entity of the relationship will cascade, destroying
the related entity.
For cardinality many
fields, related entities are sorted by lid
by default.
To override this, provide a compare: (a, b) => ...
option. The compare
function will be used via JavaScript's normal Array sort to order the set
of related objects when querying the graph.
Attribute fields have "scalar" types.
Scalar
: Any non-null JavaScript object.
Standard JSON data is supported:
Text
: Normal JSON strings.
Bool
: Normal JSON booleans.
Num
: Normal JSON numbers.
Some common JavaScript types are also provided:
Time
: Normal JavaScript Date objects.
Two builtin types are treated specially:
Key
: Like text
, but must be non-empty and will enable lookup
.
Type
: JSEG Type objects. Must come from the graph's schema.
Custom types can be defined to provide validation and normalization, as well as to configure serialization.
A validate
function is required. It is called each time an attribute of this
type is put in to the graph. The return value of the validation function is
the value that will be stored. If an error is thrown, the value is considered
invalid; an error will be logged and the value will be discarded.
builder.scalar('Integer' {
validate: Math.round,
});
An optional serialize
function can be provided to specify how this field
should be converted to JSON. For example, the builtin Time type is defined
as follows:
builder.scalar('Time', {
validate: (x) => new Date(x),
serialize: (x) => x.toISOString(),
});