Skip to content

Commit 282dcfd

Browse files
DE-1246 | nest isSmart in graph creation options and add regression tests
1 parent 8f6ffab commit 282dcfd

5 files changed

Lines changed: 421 additions & 13 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ This driver uses semantic versioning:
1616

1717
## [Unreleased]
1818

19+
### Fixed
20+
21+
- Graph creation: Send `isSmart` inside the HTTP request body's `options` object
22+
in `graph.create` and `db.createGraph`, alongside `isDisjoint` (DE-1246).
23+
The driver's options argument and returned graph metadata remain unchanged.
24+
1925
## [10.6.0] - 2026-10-14
2026

2127
### Added

‎src/graphs.ts‎

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,10 @@ export type EdgeDefinition = {
284284

285285
//#region Graph operation options
286286
/**
287-
* Option for creating a graph.
287+
* Options for creating a graph.
288+
*
289+
* SmartGraphs, EnterpriseGraphs, and SatelliteGraphs require a cluster and,
290+
* before ArangoDB 3.12.5, Enterprise Edition.
288291
*/
289292
export type CreateGraphOptions = {
290293
/**
@@ -326,27 +329,32 @@ export type CreateGraphOptions = {
326329

327330
// Extra options
328331
/**
329-
* (Enterprise Edition cluster only.) If set to `true`, the graph will be
330-
* created as a SmartGraph.
332+
* (Cluster only.) If set to `true` without a `smartGraphAttribute`, the graph
333+
* will be created as an EnterpriseGraph. Setting `smartGraphAttribute` creates
334+
* a SmartGraph and automatically enables `isSmart`.
335+
*
336+
* Has no effect when `replicationFactor` is set to `"satellite"`, which creates
337+
* a SatelliteGraph.
331338
*
332339
* Default: `false`
333340
*/
334341
isSmart?: boolean;
335342
/**
336-
* (Enterprise Edition cluster only.) Attribute containing the shard key
337-
* value to use for smart sharding.
343+
* (Cluster only.) Attribute containing the shard key value to use for smart
344+
* sharding. Setting this option creates a SmartGraph, even if `isSmart` is
345+
* omitted. Every vertex must have this attribute. Cannot be modified later.
338346
*/
339347
smartGraphAttribute?: string;
340348
/**
341-
* (Enterprise Edition cluster only.) If set to `true`, the graph will be
342-
* created as a Disjoint SmartGraph.
349+
* (Cluster only.) If set to `true`, the graph will be created as a Disjoint
350+
* SmartGraph. Only has an effect in combination with `smartGraphAttribute`
351+
* or `isSmart`.
343352
*
344353
* Default: `false`
345354
*/
346355
isDisjoint?: boolean;
347356
/**
348-
* (Enterprise Edition cluster only.) Collections to be included in a Hybrid
349-
* SmartGraph.
357+
* (Cluster only.) Collections to be included in a Hybrid SmartGraph.
350358
*/
351359
satellites?: (string | collections.ArangoCollection)[];
352360
};
@@ -1359,6 +1367,9 @@ export class Graph {
13591367
* Creates a graph with the given `edgeDefinitions` and `options` for this
13601368
* graph's name.
13611369
*
1370+
* `isSmart` and `isDisjoint` are passed directly in `options` and sent inside
1371+
* the HTTP request body's `options` object.
1372+
*
13621373
* @param edgeDefinitions - Definitions for the relations of the graph.
13631374
* @param options - Options for creating the graph.
13641375
*
@@ -1375,13 +1386,23 @@ export class Graph {
13751386
* ]);
13761387
* // graph now exists
13771388
* ```
1389+
*
1390+
* @example
1391+
* ```js
1392+
* const db = new Database();
1393+
* const graph = db.graph("enterprise-graph");
1394+
* await graph.create(
1395+
* [{ collection: "edges", from: ["vertices"], to: ["vertices"] }],
1396+
* { isSmart: true, numberOfShards: 3 }
1397+
* );
1398+
* // Creates an EnterpriseGraph in a cluster without a smartGraphAttribute.
1399+
* ```
13781400
*/
13791401
create(
13801402
edgeDefinitions: EdgeDefinitionOptions[],
13811403
options: CreateGraphOptions = {}
13821404
): Promise<GraphDescription> {
1383-
const { orphanCollections, satellites, waitForSync, isSmart, ...opts } =
1384-
options;
1405+
const { orphanCollections, satellites, waitForSync, ...opts } = options;
13851406
return this._db.request(
13861407
{
13871408
method: "POST",
@@ -1393,7 +1414,6 @@ export class Graph {
13931414
? orphanCollections.map(collections.collectionToString)
13941415
: [collections.collectionToString(orphanCollections)]),
13951416
edgeDefinitions: edgeDefinitions.map(coerceEdgeDefinition),
1396-
isSmart,
13971417
name: this._name,
13981418
options: {
13991419
...opts,

‎src/test/03-accessing-graphs.ts‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,32 @@ describe("Accessing graphs", function () {
3636
expect(graph).to.have.property("name").that.equals(name);
3737
});
3838
});
39+
describe("database.createGraph", () => {
40+
it("creates a graph and returns its handle", async () => {
41+
const name = `created_graph_${Date.now()}`;
42+
const orphan = db.collection(`${name}_orphan`);
43+
const graph = db.graph(name);
44+
try {
45+
expect(
46+
await db.createGraph(name, [], {
47+
orphanCollections: orphan,
48+
isSmart: false,
49+
isDisjoint: false,
50+
}),
51+
).to.equal(graph);
52+
await db.waitForPropagation(
53+
{ pathname: `/_api/gharial/${name}` },
54+
propagationForResourceMs,
55+
);
56+
const data = await graph.get();
57+
expect(data.name).to.equal(name);
58+
expect(data.orphanCollections).to.deep.equal([orphan.name]);
59+
} finally {
60+
if (await graph.exists()) await graph.drop(true);
61+
if (await orphan.exists()) await orphan.drop();
62+
}
63+
});
64+
});
3965
describe("database.listGraphs", () => {
4066
const vertexCollectionNames = range(2).map((i) => `vc_${Date.now()}_${i}`);
4167
const edgeCollectionNames = range(2).map((i) => `ec_${Date.now()}_${i}`);

‎src/test/16-graphs.ts‎

Lines changed: 151 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import { expect } from "chai";
22
import { Database } from "../databases.js";
3-
import { Graph } from "../graphs.js";
3+
import { isArangoError } from "../errors.js";
4+
import { Graph, type CreateGraphOptions } from "../graphs.js";
5+
import { GRAPH_NOT_FOUND } from "../lib/codes.js";
6+
import { versionStringToArangoVersionCode } from "./_arango-server-version.js";
47
import { config } from "./_config.js";
58
import {
69
clusterIntegrationTimeoutMs,
@@ -123,6 +126,153 @@ describe("Graph API", function () {
123126
expect(data).to.have.property("name", graph.name);
124127
});
125128
});
129+
describe("graph.create advanced graph options", function () {
130+
let graph: Graph;
131+
let collectionNames: string[];
132+
let sequence = 0;
133+
134+
before(async function () {
135+
const version = await system.version();
136+
const { role } = await system.request<{ role: string }>({
137+
pathname: "/_admin/server/role",
138+
});
139+
// Community Edition includes these features from 3.12.5 onward.
140+
// https://docs.arango.ai/arangodb/3.12/release-notes/version-3.12/whats-new-in-3-12/
141+
if (
142+
role !== "COORDINATOR" ||
143+
(version.license !== "enterprise" &&
144+
versionStringToArangoVersionCode(version.version) < 31205)
145+
) {
146+
// eslint-disable-next-line no-console
147+
console.warn(
148+
`Skipping advanced graph creation: ArangoDB ${version.version}, ${version.license}, role ${role} requires a cluster with smart graph support.`,
149+
);
150+
this.skip();
151+
}
152+
});
153+
154+
beforeEach(() => {
155+
const prefix = `graph_options_${Date.now()}_${sequence++}`;
156+
graph = db.graph(prefix);
157+
collectionNames = ["edges", "start", "end", "orphan"].map(
158+
(suffix) => `${prefix}_${suffix}`,
159+
);
160+
});
161+
162+
afterEach(async () => {
163+
try {
164+
await graph.drop(true);
165+
} catch (error) {
166+
if (!isArangoError(error) || error.errorNum !== GRAPH_NOT_FOUND) {
167+
throw error;
168+
}
169+
} finally {
170+
// A failed create can leave collections without a graph definition.
171+
for (const name of collectionNames) {
172+
const collection = db.collection(name);
173+
if (await collection.exists()) await collection.drop();
174+
}
175+
}
176+
});
177+
178+
const cases: {
179+
title: string;
180+
options: CreateGraphOptions;
181+
expected: Record<string, unknown>;
182+
satellite?: boolean;
183+
}[] = [
184+
{
185+
title: "creates an EnterpriseGraph without a smart attribute",
186+
options: { isSmart: true },
187+
expected: { isSmart: true, isSatellite: false, isDisjoint: false },
188+
},
189+
{
190+
title: "creates a SmartGraph with explicit isSmart",
191+
options: { isSmart: true, smartGraphAttribute: "region" },
192+
expected: {
193+
isSmart: true,
194+
isSatellite: false,
195+
smartGraphAttribute: "region",
196+
isDisjoint: false,
197+
},
198+
},
199+
{
200+
title: "creates a SmartGraph from the smart attribute alone",
201+
options: { smartGraphAttribute: "region" },
202+
expected: {
203+
isSmart: true,
204+
isSatellite: false,
205+
smartGraphAttribute: "region",
206+
isDisjoint: false,
207+
},
208+
},
209+
{
210+
title: "creates a Disjoint SmartGraph",
211+
options: {
212+
isSmart: true,
213+
smartGraphAttribute: "region",
214+
isDisjoint: true,
215+
},
216+
expected: {
217+
isSmart: true,
218+
isSatellite: false,
219+
smartGraphAttribute: "region",
220+
isDisjoint: true,
221+
},
222+
},
223+
{
224+
title: "creates a SmartGraph with a satellite vertex collection",
225+
options: { isSmart: true, smartGraphAttribute: "region" },
226+
expected: {
227+
isSmart: true,
228+
isSatellite: false,
229+
smartGraphAttribute: "region",
230+
},
231+
satellite: true,
232+
},
233+
{
234+
title: "creates a SatelliteGraph when isSmart is also true",
235+
options: { isSmart: true, replicationFactor: "satellite" },
236+
expected: { isSmart: false, isSatellite: true },
237+
},
238+
];
239+
240+
for (const { title, options, expected, satellite } of cases) {
241+
it(title, async () => {
242+
const [edges, start, end, orphan] = collectionNames;
243+
const created = await graph.create(
244+
[{ collection: edges, from: [start], to: [end] }],
245+
{
246+
...(options.replicationFactor === "satellite"
247+
? {}
248+
: { numberOfShards: 2, replicationFactor: 1 }),
249+
...options,
250+
orphanCollections: [orphan],
251+
...(satellite ? { satellites: [db.collection(end)] } : {}),
252+
},
253+
);
254+
await db.waitForPropagation(
255+
{ pathname: `/_api/gharial/${graph.name}` },
256+
propagationForResourceMs,
257+
);
258+
for (const data of [created, await graph.get()]) {
259+
expect(data).to.include({ name: graph.name, ...expected });
260+
expect(data.orphanCollections).to.include(orphan);
261+
if (!options.smartGraphAttribute) {
262+
expect(data.smartGraphAttribute).to.be.undefined;
263+
}
264+
}
265+
if (satellite) {
266+
await db.waitForPropagation(
267+
{ pathname: `/_api/collection/${end}` },
268+
propagationForResourceMs,
269+
);
270+
const properties = await db.collection(end).properties();
271+
expect(properties.replicationFactor).to.equal("satellite");
272+
}
273+
});
274+
}
275+
});
126276
describe("graph.drop", () => {
127277
let graph: Graph;
128278
let edgeCollectionNames: string[];

0 commit comments

Comments
 (0)