Skip to main content

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();
MethodWhat 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.

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
Raw composition is unscoped

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.