Neo4j, Amazon Neptune, Apache TinkerPop (Gremlin) and Dgraph are reachable
through the same model API as the SQL dialects. findAll, create,
update, destroy, where operators, associations and include all work —
the graph dialects implement the Dialect contract and compile those calls into
Cypher, Gremlin or Dgraph DQL. You do not hand-write graph queries for standard
operations.This is different from the NoSQL stores, which expose each
engine's own native API instead of the model layer.| Dialect name | Class | Language |
|---|
neo4j | Neo4jGraphDialect | Cypher |
gremlin | GremlinGraphDialect | Gremlin (TinkerPop) |
neptune | GremlinGraphDialect | Gremlin (Amazon Neptune) |
dgraph | DgraphGraphDialect | Dgraph DQL + JSON mutations |
All four are registered with the shared DialectRegistry when the package
loads, so dialect: 'neo4j' resolves like any built-in.import { Prorm, DataTypes } from 'ts-prorm-orm';
const prorm = new Prorm({
dialect: 'neo4j',
host: 'localhost', port: 7687,
username: 'neo4j', password: process.env.NEO4J_PASSWORD!,
});
await prorm.connect();
const User = prorm.define('User', {
name: { type: DataTypes.STRING(100) },
age: { type: DataTypes.INTEGER() },
});
A model's name becomes the node label; its attributes become node
properties.What the model calls compile to#
Same call, three languages. Each tab shows what the dialect compiles, with the bound parameters it sends alongside:import { Op } from 'ts-prorm-orm';
const adults = await User.findAll({
where: { age: { [Op.gte]: 30 }, name: { [Op.ne]: 'Bob' } },
order: [['age', 'DESC']],
limit: 10,
});
MATCH (n:User)
WHERE (n.age >= $p0 AND n.name <> $p1)
RETURN n
ORDER BY n.age DESC
LIMIT 10
// params: { p0: 30, p1: 'Bob' }
query find($p0: int, $p1: string) {
result(func: type(User), orderdesc: age, first: 10) @filter((ge(age, $p0) AND NOT eq(name, $p1))) {
uid
dgraph.type
expand(_all_)
}
}
# vars: { $p0: 30, $p1: 'Bob' }
Values are always bound parameters ($p0, b0), never interpolated into
the statement.The write operations, on Neo4j:| Call | Cypher |
|---|
User.create({ name: 'Ada', age: 36 }) | CREATE (n:User $props0) RETURN n |
User.update({ age: 37 }, { where: { name: 'Ada' } }) | MATCH (n:User) WHERE n.name = $p0 SET n += $p1 RETURN n |
User.destroy({ where: { name: 'Ada' } }) | MATCH (n:User) WHERE n.name = $p0 DETACH DELETE n |
User.upsert({ … }) | MERGE |
User.increment('views') | SET n.views = n.views + … |
DETACH DELETE is the right default: deleting a node without detaching its
relationships is an error in Neo4j, and orphan edges are never what you wanted.Associations are edges#
| Association | Edge |
|---|
hasMany / hasOne | outbound edge from the source node |
belongsTo | inbound edge |
include | a traversal — a graph walk, not a join |
User.hasMany(Post, { as: 'posts' });
await User.findAll({ include: [{ model: Post, as: 'posts' }] });
This is the case where a graph database earns its keep: a multi-hop traversal
costs the graph engine far less than the equivalent chain of SQL joins.Traversals and edges, side by side#
Beyond the model calls, every graph dialect has relate() (create an edge between matched nodes) and traverse() (walk edges from matched nodes, one to n hops). The same spec compiles to each engine:import { Neo4jGraphDialect } from 'ts-prorm-orm';
const g = prorm.getDialectInstance() as Neo4jGraphDialect;
// Ada follows user 7, with a property on the edge
await g.relate({
fromLabel: 'User', fromWhere: { id: 42 },
edgeLabel: 'FOLLOWS',
toLabel: 'User', toWhere: { id: 7 },
props: { since: 2024 },
});
// Active users within two FOLLOWS hops of Ada
const reachable = await g.traverse({
label: 'User', where: { id: 42 },
edgeLabel: 'FOLLOWS', direction: 'out',
targetLabel: 'User', targetWhere: { active: true },
minDepth: 1, maxDepth: 2,
limit: 20,
});
// relate
MATCH (a:User) WHERE a.id = $p0
MATCH (b:User) WHERE b.id = $p1
MERGE (a)-[r:FOLLOWS]->(b)
SET r += $p2
RETURN r
// traverse
MATCH (n:User)
WHERE n.id = $p0
MATCH (n)-[:FOLLOWS*1..2]->(m:User)
WHERE m.active = $p1
RETURN DISTINCT m
LIMIT 20
# relate: a JSON mutation after resolving both ends by query
{ from(func: type(User)) @filter(eq(id, 42)) { uid } to(func: type(User)) @filter(eq(id, 7)) { uid } }
# traverse
query traverse($p0: int, $p1: bool) {
result(func: type(User), first: 20) @filter(eq(id, $p0)) {
uid
FOLLOWS @filter(eq(active, $p1)) {
uid
expand(_all_)
}
}
}
Note the differences the tabs expose: Cypher MERGEs the edge, so relating twice is idempotent, while Gremlin's addE creates a second edge. Dgraph follows one hop per nested block, so maxDepth beyond 1 needs @recurse in raw DQL.Indexes and constraints#
const g = prorm.getDialectInstance()!;
g.compileCreateIndex('User', ['email'], 'idx_email');
// CREATE INDEX idx_email IF NOT EXISTS FOR (n:User) ON (n.email)
Node constraints (uniqueness, existence, node key) compile through
compileCreateConstraint / compileDropConstraint, and compileShowLabels()
lists the labels in the graph.What throws#
Relational-only features raise a typed GraphCapabilityError rather than
producing something meaningless:"materialized views" is not supported by the neo4j graph dialect
That covers partitions, materialized views, row-level security, stored
procedures, foreign-data wrappers and arbitrary SQL joins. Catch it the way you
catch UnsupportedSchemaObjectError:import { GraphCapabilityError } from 'ts-prorm-orm';
try {
await prorm.createMaterializedView({ name: 'x', query: '…' });
} catch (e) {
if (e instanceof GraphCapabilityError) { /* pick another approach */ }
}
Dropping to the native language#
For traversals the model API cannot express — variable-length paths, shortest
path, PageRank — write the query yourself:const { rows } = await prorm.query(`
MATCH path = shortestPath((a:User {id: $from})-[:FOLLOWS*..6]-(b:User {id: $to}))
RETURN length(path) AS hops
`, { replacements: { from, to } });
queryStream() is available too, for traversals that return more than fits in
memory — see Streaming.Choosing between this and a store adapter#
| You want | Use |
|---|
Models, associations, include, hooks, validation over a graph | these dialects |
| The engine's own API — Cypher builders, Gremlin bytecode, native bulk loaders | the Neo4j / Dgraph / TigerGraph stores |
| Relational data with occasional graph queries | a SQL dialect with recursive CTEs |
Modified at 2026-10-04 00:40:16