Key Concepts
1. NodeFragment: a Node
Section titled “1. NodeFragment: a Node”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 {}}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.
3. GraphView: Composing Fragments
Section titled “3. GraphView: Composing Fragments”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.

