Skip to content

Key Concepts

A @NodeFragment maps one node. Every property of the object is stored, unless it is marked @GraphTransient():

@NodeFragment({ labels: ['Person'] })
export class Person {
@NodeId() uuid: string;
name: string;
bio?: string;
}

A fragment names at least one label. A subclass carries its own labels as well as its superclass’s: see Polymorphism.

Defaults for missing properties. A property the node doesn’t have keeps the class’s initializer. Two decorators handle a stored null:

@NodeFragment({ labels: ['User'] })
export class UserNode {
@NodeId() id: string;
@Default() status = 'active'; // a stored null loads as 'active'
@EmptyWhenAbsent() tags: string[]; // absent or null loads as []; an object loads as {}
}

@EmptyWhenAbsent reads the kind of collection from the type TypeScript records for the field: [] for an array, an empty Set for a set, and {} for an object or a map. A set is stored as a list, and such a field loads it back as a set. TypeScript records a list declared with a union, such as string[] | null under strict null checks, as Object, as it records a map, so name the kind for such a field, or declare it tags?: string[]:

@EmptyWhenAbsent({ kind: 'array' }) tags: string[] | null; // absent loads as [], not {}
@EmptyWhenAbsent({ kind: 'set' }) badges: Set<string> | null; // absent loads as an empty Set

A kind the declared type contradicts, and a field that holds no collection, such as a string, are refused when the model is built.

Storing a set as a list is a change from 3.2, which wrote a set as it was given: Oracle and Postgres held it as {}, and Memgraph as an empty map, without its items. A set field Drivine knows of (see below) loads such a {} as an empty set, and its next save stores it as a list. The refusal of @EmptyWhenAbsent on a field that holds no collection is new too: such a field used to load {}.

Sets. A field that holds a Set loads the list the store holds as a Set, with or without @EmptyWhenAbsent, in a fragment, a view or a relationship fragment. Drivine knows a field holds a set where TypeScript records its type as Set, which it does for a decorated field, whatever the decorator, or where the class initializes the field with a Set. A field neither decorated nor initialized leaves no type at run time, and TypeScript records a union, such as Set<string> | null, as Object: such a field loads the list as an array. Without @EmptyWhenAbsent, a set the node holds nothing for stays absent, or keeps its initializer, and a stored null loads as null.

@GraphProperty('code_set') codes: Set<string>; // decorated: loads as a Set
badges = new Set<string>(); // initialized: loads as a Set
tags: Set<string>; // neither: TypeScript records nothing, so it loads as an array

Loading a set without @EmptyWhenAbsent as a Set is a change from 3.2, which loaded it as the array the store held.

save, patch, patchAll and edges.relate all write a set as the list of its items, and a @JsonPacked one as the JSON text of that list, which is also what a where compares a packed set field with. In 3.2, patch wrote a packed set as {}, and on Postgres and Oracle so did patch and relate any set, losing its items; a where comparing a packed field with a set compared it with {}, and matched nothing.

2. RelationshipFragment: a Relationship’s Properties

Section titled “2. RelationshipFragment: a Relationship’s Properties”

A @RelationshipFragment holds the properties of a relationship, and the node it leads to:

@RelationshipFragment()
export class WorkHistory {
startDate: string; // a property of the relationship
role: string; // and another
@TargetNode() target: Organization; // the node it leads to
}

Use it for employment history, transactions, audit trails, or any relationship with data of its own.

A @GraphView composes fragments and relationships into one result. @Root marks where the query starts:

@GraphView()
export class PersonCareer {
@Root() person: Person;
@GraphRelationship({ type: 'WORKS_FOR', target: () => WorkHistory })
employmentHistory: WorkHistory[] = [];
}

@GraphRelationship takes:

Option Meaning
type The relationship type.
direction Direction.OUTGOING (the default), INCOMING or UNDIRECTED.
target The class of the related item: a fragment, a relationship fragment or a view, as a function so that classes may refer to each other in either order. It is required for a list, since TypeScript records only Array as a list’s type. For a single item it defaults to the property’s type.
required Only roots that have the relationship are loaded. TypeScript erases ?:, so a view says this rather than the type: see Required Relationships.
maxDepth For a recursive relationship, how deep to expand. Defaults to 1.

When a view refers to a class declared later in the same module, type the property with Related<T>, so the decorator metadata doesn’t read the class before it exists: @GraphRelationship({ type: 'TO_B', target: () => B }) b?: Related<B>.

4. Recursive Relationships: Hierarchies and Ontologies

Section titled “4. Recursive Relationships: Hierarchies and Ontologies”

A view whose relationship targets its own class is expanded to maxDepth levels:

@NodeFragment({ labels: ['Location'] })
export class Location {
@NodeId() uuid: string;
name: string;
type: string;
}
@GraphView()
export class LocationHierarchy {
@Root() location: Location;
@GraphRelationship({ type: 'HAS_LOCATION', target: () => LocationHierarchy, maxDepth: 3 })
subLocations: LocationHierarchy[];
}

At the last level a list is [] and a single item is undefined. To walk upwards, make another view with direction: Direction.INCOMING. A query can override the depth:

await gom.loadAll(LocationHierarchy, (q) => ({
where: q.location.type.eq('continent'),
depth: { subLocations: 5 }
}));

Cycles through other views (A → B → A) are expanded to maxDepth as well. A view nested in itself is saved, each level one relationship from the one above. A list of fragments read over several hops (maxDepth above 1 on a list of a fragment) is loaded and never written.

5. Path Traversal: Skipping Intermediate Nodes

Section titled “5. Path Traversal: Skipping Intermediate Nodes”

@GraphRelationship is one hop. @GraphPath follows several and maps only the node at the end:

@GraphView()
export class ActorDirectors {
@Root() actor: Actor;
@GraphPath([hop('ACTED_IN', Direction.OUTGOING, 'Movie'), hop('DIRECTED_BY')], { target: () => Director })
directors: Director[];
}

Each node at the end is returned once: an actor who made two movies with one director gets that director once. A hop’s label constrains the node it reaches. required: true keeps only the roots the path leads somewhere from. A path is read-only.

One hop may name an order: a property of the node it reaches, lowest first unless ascending is false. A list then holds its targets in the order of the first node of that hop that leads to each, and a single field holds the target the first such node leads to:

@GraphView()
export class SessionStarter {
@Root() session: Session;
// who wrote the earliest message
@GraphPath([hop('HAS_MESSAGE', Direction.OUTGOING, 'Message', 'sentAt'), hop('AUTHORED_BY')], { target: () => Person })
startedBy?: Person;
}

A node of the ordered hop without the property comes last, whichever way the path is read. Targets whose first nodes hold equal values come in no set order. The property should hold values of one type: every backend reads the order the same way where it does, a text of digits such as '0100' is ordered as text, and an empty text is a text, the lowest one. Where it holds texts on some nodes and numbers on others, every backend reads every text before every number lowest first, and after them highest first, as Neo4j’s ORDER BY does. A group key’s path names no order: pathKey refuses one.

6. Aggregates: Counting and Summarising Without Loading

Section titled “6. Aggregates: Counting and Summarising Without Loading”

@Count and @Aggregate compute a number for each root in the query, so you don’t load a list just to size it:

@GraphView()
export class ActorStats {
@Root() actor: Actor;
@Count('ACTED_IN') movieCount: number;
@Aggregate({ function: AggregateFunction.AVG, type: 'RATED', property: 'score' }) avgRating?: number;
@Aggregate({ function: AggregateFunction.SUM, type: 'RATED', property: 'score' }) totalRating: number;
}

SUM and COUNT over nothing are 0; AVG, MIN and MAX over nothing are undefined. Aggregates follow one hop and belong to one root. To group nodes by a key and rank the groups, use a grouped read.