Query builder
em.createQueryBuilder() returns a Neo4jQueryBuilder: a fluent wrapper over
@neo4j/cypher-builder that knows your entity
metadata. Labels, relationship types and directions come from the model, values are bound as named
parameters, and schema scoping is applied for you.
const movies = await em
.createQueryBuilder(Movie)
.match()
.where('released', 1999)
.orderBy('title', 'ASC')
.limit(10)
.execute();
| Method | What it does |
|---|---|
.build() | Returns the cypher string and the params map without running anything. |
.execute() / .getMany() | Runs it and returns plain rows (converted the way em.run() converts them). |
.getOne() | The first row, or null. |
.stream(options) | A cursor over the rows; rawResults: true yields driver records. |
Traversal with related()
related() extends the pattern from the current node. It takes a relationship type, a
relationship entity, or a Cypher.Relationship you built yourself:
// type, direction, target entity, alias
const { cypher, params } = em
.createQueryBuilder(Movie)
.match()
.related('ACTED_IN', 'left', Actor, 'a')
.where('title', 'The Matrix')
.return(['title'])
.build();
MATCH (this0:Movie)<-[:ACTED_IN]-(a:Actor)
WHERE this0.title = $param0
RETURN this0.title AS title
The options object carries everything the positional form cannot:
qb.match().related('ACTED_IN', {
direction: 'left', // 'left' | 'right' | 'undirected'
targetLabel: 'Actor', // or targetLabels: [...], or targetEntity: Actor
properties: { roles: ['Neo'] }, // edge properties, bound as parameters
alias: 'a',
});
qb.match().related('KNOWS', { length: { min: 1, max: 3 }, targetLabel: 'Person' });
// MATCH (this0:Movie)-[:KNOWS*1..3]->(:Person)
A relationship entity may stand in for the type, which is how edge properties get filtered with the type already known:
const Cypher = qb.getCypher();
const rel = new Cypher.Relationship();
qb.match()
.related(FriendsWith, { targetLabels: ['user'], variable: rel })
.where(Cypher.gte(rel.property('since'), new Cypher.Param(2022)));
Filtering
where() takes three shapes, and they compose:
qb.where('title', 'The Matrix'); // property = value
qb.where({ price: { $gt: 30 }, tags: { $some: { name: 'x' } } }); // a MikroORM filter
qb.where(Cypher.gt(node.property('price'), new Cypher.Param(30))); // a raw predicate
andWhere() / orWhere() (and their and() / or() aliases) chain further conditions.
whereFilter() applies a filter object explicitly. Every operator documented in
query conditions is available here.
Composition escape hatches
const Cypher = qb.getCypher(); // the underlying @neo4j/cypher-builder toolkit
qb.pattern((Cypher, node) =>
new Cypher.Pattern(node, { labels: ['Movie'] })
.related(new Cypher.Relationship(), { type: 'DIRECTED', direction: 'left' })
.to(new Cypher.Node(), { labels: ['Person'] }),
);
qb.with(['this0']); // WITH, to chain query parts
qb.call(subQb, { importVariables: '*' }); // CALL { … }
qb.call(subQb, { inTransactions: { ofRows: 1000, concurrentTransactions: 4 } });
qb.exists(pattern); // EXISTS { … } as a predicate
qb.count(pattern); // COUNT { … } as an expression
pattern(), call(), create() and merge() are raw composition, exactly as raw SQL is in
MikroORM: no schema predicate is added to what you build there. Add it
yourself if you need it.
Full-text and procedures as a source
Two heads exist that a plain MATCH cannot express — both make the query start from a procedure:
// The relevance score becomes an ordinary projectable value.
const hits = await em
.createQueryBuilder(Book, 'b')
.fullText('search', 'graph databases') // the field names the index
.where('price', 30)
.return(['title', 'score'])
.execute();
// A declared procedure as the query's source.
const rows = await em
.createQueryBuilder()
.callProcedure(QueryBooks, { indexName: 'Book_title', queryString: 'graph' })
.yield('node', 'score')
.where('score', { $gt: 0.5 })
.orderBy('score', 'DESC')
.limit(10)
.execute();
See full-text search and stored routines.
Writing
qb.create({ id: 'M1', title: 'The Matrix' });
qb.merge({ id: 'M1' }).set({ title: 'The Matrix' });
qb.match().where('id', 'M1').delete(/* detach */ true);
Schema scoping
qb.withSchema('tenant-2'); // read another schema
qb.ignoreSchema(); // read across every schema — deliberately explicit
Both are no-ops on entities that never declared schema, and may be chained in any order.