compile* methods thatfindAll-shaped specs and return a { text, params } statement. ThisAqlGraphDialect, and the structuredArangoStore.ArangoStore could create collections and read documents by_from and _to is an edge collection that can be| ORM concept | ArangoDB |
|---|---|
| node label | document collection of the same name |
| edge label | edge collection of the same name |
| node | a document (_key, _id, _rev are ArangoDB's own) |
| edge | a document whose _from / _to hold document _ids |
association / include | a one-hop traversal over the edge collection |
new AqlGraphDialect({
collections: { User: 'users' }, // node label -> document collection
edgeCollections: { HAS_POSTS: 'authored' }, // edge label -> edge collection
});ArangoStore; the AQL tab is exactly what getGraphDialect() compiles for it, bind parameters included.import { Op } from 'ts-prorm-orm';
import { ArangoStore } from 'ts-prorm-orm/nosql/arangodb';
const store = new ArangoStore({ url: 'http://localhost:8529', databaseName: '_system' });
await store.connect();
// 1. filtered, sorted, paged read
const page3 = await store.matchNodes({
label: 'users',
where: { age: { [Op.gte]: 18 }, active: true },
order: [['age', 'DESC']],
limit: 10,
offset: 20,
});
// 2. one-hop include: each user with their posts
const withPosts = await store.matchNodes({
label: 'users',
where: { name: 'Ada' },
include: [{ edgeLabel: 'authored', direction: 'out', targetLabel: 'posts', as: 'posts' }],
});
// 3. multi-hop traversal
const reachable = await store.traverseNodes({
label: 'users', where: { name: 'Ada' },
edgeLabel: 'authored', direction: 'out', maxDepth: 2, targetLabel: 'posts',
});
// 4. create-or-update an edge between two matched documents
await store.relateNodes({
fromLabel: 'users', fromWhere: { _key: 'ada' },
edgeLabel: 'authored',
toLabel: 'posts', toWhere: { _key: 'p1' },
props: { at: '2026-07-05' },
});
// 5. atomic counter
await store.incrementNodes('posts', { views: 1 }, { _key: 'p1' });// 1. params: { '@c0': 'users', v0: 'age', v1: 18, v2: 'active', v3: true, v4: 'age' }
FOR doc IN @@c0
FILTER (doc[@v0] >= @v1 AND doc[@v2] == @v3)
SORT doc[@v4] DESC
LIMIT 20, 10
RETURN doc
// 2. params: { '@c0': 'users', '@c1': 'authored', v0: 'name', v1: 'Ada', v2: 'posts', v3: 'posts' }
FOR doc IN @@c0
FILTER doc[@v0] == @v1
RETURN MERGE(doc, { [@v2]: (FOR n0 IN 1..1 OUTBOUND doc @@c1 FILTER PARSE_IDENTIFIER(n0._id).collection == @v3 RETURN n0) })
// 3. params: { '@c0': 'users', '@c1': 'authored', v0: 'name', v1: 'Ada', v2: 'posts' }
WITH posts
FOR doc IN @@c0
FILTER doc[@v0] == @v1
FOR node IN 1..2 OUTBOUND doc @@c1
FILTER PARSE_IDENTIFIER(node._id).collection == @v2
RETURN DISTINCT node
// 4. params: { '@c0': 'users', '@c1': 'posts', '@c2': 'authored', v0: '_key', v1: 'ada', v2: '_key', v3: 'p1', v4: { at: '2026-07-05' } }
FOR src IN @@c0
FILTER src[@v0] == @v1
FOR dst IN @@c1
FILTER dst[@v2] == @v3
UPSERT { _from: src._id, _to: dst._id } INSERT MERGE(@v4, { _from: src._id, _to: dst._id }) UPDATE @v4 IN @@c2
RETURN NEW
// 5. params: { '@c0': 'posts', v0: '_key', v1: 'p1', v2: 'views', v3: 1 }
FOR doc IN @@c0
FILTER doc[@v0] == @v1
UPDATE doc WITH { [@v2]: (doc[@v2] == null ? 0 : doc[@v2]) + @v3 } IN @@c0
RETURN NEWstore.configureGraph({ graph: 'social' }), the traversal (3) names the graph instead of an edge collection, and the WITH prelude goes away:// params: { '@c0': 'users', v0: 'name', v1: 'Ada', v2: 'social', v3: 'posts' }
FOR doc IN @@c0
FILTER doc[@v0] == @v1
FOR node IN 1..2 OUTBOUND doc GRAPH @v2
FILTER PARSE_IDENTIFIER(node._id).collection == @v3
RETURN DISTINCT nodecompileMatch uses the first:FOR doc IN @@c0
FILTER (doc[@v0] == @v1 AND doc[@v2] >= @v3)
SORT doc[@v4] DESC
LIMIT 20, 10
RETURN doccompileTraverse uses the second:WITH posts
FOR doc IN @@c0
FILTER doc[@v0] == @v1
FOR node IN 1..3 OUTBOUND doc @@c1
FILTER PARSE_IDENTIFIER(node._id).collection == @v2
RETURN DISTINCT node_id, but youFOR selects the start documents with the fullwhere surface and hands the document itself to the traversal.include nests the second form inside the first, as a subquery:FOR doc IN @@c0
RETURN MERGE(doc, { [@v0]: (FOR n0 IN 1..1 OUTBOUND doc @@c1 RETURN n0) })… GRAPH @v0, with ArangoDB resolving the edge collections from the graphimport { ArangoStore } from 'ts-prorm-orm/nosql/arangodb';
const store = new ArangoStore({ url: 'http://localhost:8529', databaseName: '_system' });
store.configureGraph({ graph: 'social' });… OUTBOUND doc @@c1), which needs no graph to exist. The named graph is theWITH prelude in a cluster, which is why one appears above.@value versus @@collection@name and may@@name, must be a plainFOR … IN, INSERT … INTO, UPDATE/REPLACE/REMOVE/UPSERT … IN, and in@, the reference in the query carriesdoc[@v0], so not even a key is parsed as query text;KEEP/UNSET take a bound array), include{ [@v0]: … }), patch and upsert@@ parameters;@.out/in/both → OUTBOUND/INBOUND/ANY); anything else falls backOUTBOUND rather than reaching the query;min..max and LIMIT offset, count, coerced toNumber('10 REMOVE doc IN users') is NaN, and aValidationError is raised rather than a query built;WITH prelude of an anonymous-graph traversal. They[A-Za-z][A-Za-z0-9_-]*,ValidationError names the label and the call. ThesafeIdent() helper is deliberately not used on them — it rewrites_, so my-graph would silently become my_graph, arequireGraphLabel(), so a missingValidationError instead of putting undefined in the query.| Call | AQL |
|---|---|
createNode | INSERT @v0 INTO @@c0 RETURN NEW |
updateNodes | FOR doc IN @@c0 FILTER … UPDATE doc WITH @v2 IN @@c0 RETURN NEW |
upsertNode | UPSERT @v0 INSERT @v1 UPDATE @v1 IN @@c0 RETURN NEW |
incrementNodes | UPDATE doc WITH { [@v2]: (doc[@v2] == null ? 0 : doc[@v2]) + @v3 } |
deleteNodes | FOR doc IN @@c0 FILTER … REMOVE doc IN @@c0 RETURN OLD |
relateNodes | UPSERT { _from: src._id, _to: dst._id } INSERT MERGE(@v4, …) UPDATE @v4 IN @@c2 |
UPSERT … INSERT … UPDATE … IN statement: oneundefined as a value) compiles to nullOPTIONS { keepNull: false }, which is the only way AQL's UPDATE cannull instead of writing it._from/_to off the documents the filterscollection/key identifier is never assembled from caller input.UPSERT on the endpoint pair gives Cypher MERGE's semantics: re-relatingdetach is handled by the store ratherconst store = new ArangoStore({ url: 'http://localhost:8529', databaseName: '_system' });
await store.deleteNodes('users', { _key: 'k1' }, {
detach: true,
edgeCollections: ['authored'], // or configureGraph({ graph }) and let it look them up
});compile* methods emit an #ARANGO-DDL#-prefixed JSON directive thatquery() (on the dialect) or runGraphStatement() (on the store) executescreateTable() quietly skip the unique index it asked for.Op.containsOp.contains in a where renders the AQL membership test that matches itsFILTER, from the builder):| Value | AQL |
|---|---|
an element: { tags: { [Op.contains]: 'admin' } } | @v1 IN doc[@v0] |
an array: { tags: { [Op.contains]: ['a', 'b'] } } | @v1 ALL IN doc[@v0] |
a document: { prefs: { [Op.contains]: { theme: 'dark' } } } | MATCHES(doc[@v0], @v1) |
where does not know (a symbol or a $ key) is refused with aValidationError, and so is an object that mixes operators with plain keys;{ [Op.contains]: 'x' } became an equality{} and matched nothing, and beside another operator it vanished fromGraphCapabilityError, for the things AQL genuinely cannot express:compileCreateConstraint(…, 'exists')TransactionOptions carries noawait dialect.startTransaction({ collections: { write: ['users'] } } as any);compileShowLabels() lists collections via COLLECTIONS(), which reports nostore.listCollections() carries the typeimport { Op } from 'ts-prorm-orm';
import { ArangoStore } from 'ts-prorm-orm/nosql/arangodb';
interface User {
_key: string;
name: string;
age: number;
}
const store = new ArangoStore({ url: 'http://localhost:8529', databaseName: '_system', auth: { username: 'root', password: 'openSesame' } });
await store.connect();
try {
await store.createCollection('users');
await store.createCollection('posts');
await store.createEdgeCollection('authored');
await store.createNode('users', { _key: 'ada', name: 'Ada', age: 36 });
await store.createNode('posts', { _key: 'p1', title: 'Notes on the Analytical Engine' });
await store.relateNodes({
fromLabel: 'users', fromWhere: { _key: 'ada' },
edgeLabel: 'authored',
toLabel: 'posts', toWhere: { _key: 'p1' },
props: { at: new Date().toISOString() },
});
const adults = await store.matchNodes<User>({
label: 'users',
where: { age: { [Op.gte]: 18 } },
order: [['age', 'DESC']],
limit: 10,
});
const posts = await store.traverseNodes({
label: 'users', where: { name: 'Ada' },
edgeLabel: 'authored', direction: 'out', maxDepth: 2,
targetLabel: 'posts',
});
await store.updateNodes('users', { _key: 'ada' }, { age: 37 });
console.log(adults.map((u) => u.name), posts.length, await store.countNodes('users'));
// Removing a vertex leaves its edges dangling unless you detach
await store.deleteNodes('users', { _key: 'ada' }, { detach: true, edgeCollections: ['authored'] });
} finally {
for (const c of ['authored', 'posts', 'users']) await store.dropCollection(c);
await store.disconnect();
}getGraphDialect() returns the compiler if you want the statement withoutquery() with extra options:const store = new ArangoStore({ url: 'http://localhost:8529', databaseName: '_system' });
const stmt = store.getGraphDialect().compileMatch({ label: 'users', where: { name: 'Ada' } });
// { text: 'FOR doc IN @@c0\nFILTER doc[@v0] == @v1\nRETURN doc',
// params: { '@c0': 'users', v0: 'name', v1: 'Ada' } }COLLECT-based aggregation, SHORTEST_PATH, adoc[@v0], cannot use one):const store = new ArangoStore({ url: 'http://localhost:8529', databaseName: '_system' });
const rows = await store.queryAll(
`FOR v, e, p IN OUTBOUND SHORTEST_PATH @from TO @to GRAPH 'social' RETURN p`,
{ from: 'users/ada', to: 'users/grace' },
);