@typicalday/firegraph 0.8.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/dist/backend-73p5Blx7.d.cts +97 -0
  2. package/dist/backend-BrqFkbid.d.ts +97 -0
  3. package/dist/backend.cjs +222 -0
  4. package/dist/backend.cjs.map +1 -0
  5. package/dist/backend.d.cts +122 -0
  6. package/dist/backend.d.ts +122 -0
  7. package/dist/backend.js +136 -0
  8. package/dist/backend.js.map +1 -0
  9. package/dist/{chunk-6OQW5OKO.js → chunk-5753Y42M.js} +12 -4
  10. package/dist/chunk-5753Y42M.js.map +1 -0
  11. package/dist/{chunk-YUXOALMR.js → chunk-LZOIQHYN.js} +69 -92
  12. package/dist/chunk-LZOIQHYN.js.map +1 -0
  13. package/dist/chunk-R7CRGYY4.js +94 -0
  14. package/dist/chunk-R7CRGYY4.js.map +1 -0
  15. package/dist/{chunk-KFA7G37W.js → chunk-SU4FNLC3.js} +32 -30
  16. package/dist/chunk-SU4FNLC3.js.map +1 -0
  17. package/dist/chunk-TYYPRVIE.js +57 -0
  18. package/dist/chunk-TYYPRVIE.js.map +1 -0
  19. package/dist/{do-sqlite.cjs → cloudflare/index.cjs} +1538 -1420
  20. package/dist/cloudflare/index.cjs.map +1 -0
  21. package/dist/cloudflare/index.d.cts +454 -0
  22. package/dist/cloudflare/index.d.ts +454 -0
  23. package/dist/cloudflare/index.js +822 -0
  24. package/dist/cloudflare/index.js.map +1 -0
  25. package/dist/codegen/index.d.cts +1 -1
  26. package/dist/codegen/index.d.ts +1 -1
  27. package/dist/editor/client/assets/index-Bq2bfzeY.js +411 -0
  28. package/dist/editor/client/index.html +1 -1
  29. package/dist/editor/server/index.mjs +6481 -6327
  30. package/dist/index.cjs +165 -44
  31. package/dist/index.cjs.map +1 -1
  32. package/dist/index.d.cts +14 -138
  33. package/dist/index.d.ts +14 -138
  34. package/dist/index.js +31 -22
  35. package/dist/index.js.map +1 -1
  36. package/dist/query-client/index.cjs +30 -28
  37. package/dist/query-client/index.cjs.map +1 -1
  38. package/dist/query-client/index.d.cts +2 -2
  39. package/dist/query-client/index.d.ts +2 -2
  40. package/dist/query-client/index.js +1 -1
  41. package/dist/react.cjs +0 -1
  42. package/dist/react.cjs.map +1 -1
  43. package/dist/react.js +0 -1
  44. package/dist/react.js.map +1 -1
  45. package/dist/scope-path-B1G3YiA7.d.cts +139 -0
  46. package/dist/scope-path-B1G3YiA7.d.ts +139 -0
  47. package/dist/{serialization-C6JNNOCS.js → serialization-ZZ7RSDRX.js} +2 -2
  48. package/dist/svelte.cjs +0 -2
  49. package/dist/svelte.cjs.map +1 -1
  50. package/dist/svelte.js +0 -2
  51. package/dist/svelte.js.map +1 -1
  52. package/dist/{types-BVtx9zLv.d.cts → types-DOemdlVA.d.cts} +20 -2
  53. package/dist/{types-BVtx9zLv.d.ts → types-DOemdlVA.d.ts} +20 -2
  54. package/package.json +39 -40
  55. package/dist/chunk-6OQW5OKO.js.map +0 -1
  56. package/dist/chunk-KFA7G37W.js.map +0 -1
  57. package/dist/chunk-WOAJRVHD.js +0 -699
  58. package/dist/chunk-WOAJRVHD.js.map +0 -1
  59. package/dist/chunk-YUXOALMR.js.map +0 -1
  60. package/dist/d1.cjs +0 -2416
  61. package/dist/d1.cjs.map +0 -1
  62. package/dist/d1.d.cts +0 -54
  63. package/dist/d1.d.ts +0 -54
  64. package/dist/d1.js +0 -75
  65. package/dist/d1.js.map +0 -1
  66. package/dist/do-sqlite.cjs.map +0 -1
  67. package/dist/do-sqlite.d.cts +0 -41
  68. package/dist/do-sqlite.d.ts +0 -41
  69. package/dist/do-sqlite.js +0 -78
  70. package/dist/do-sqlite.js.map +0 -1
  71. package/dist/editor/client/assets/index-tyFcX6qG.js +0 -411
  72. /package/dist/{serialization-C6JNNOCS.js.map → serialization-ZZ7RSDRX.js.map} +0 -0
@@ -66,10 +66,18 @@ function serializeValue(value) {
66
66
  if (value === null || value === void 0) return value;
67
67
  if (typeof value !== "object") return value;
68
68
  if (isTimestamp(value)) {
69
- return { [SERIALIZATION_TAG]: "Timestamp", seconds: value.seconds, nanoseconds: value.nanoseconds };
69
+ return {
70
+ [SERIALIZATION_TAG]: "Timestamp",
71
+ seconds: value.seconds,
72
+ nanoseconds: value.nanoseconds
73
+ };
70
74
  }
71
75
  if (isGeoPoint(value)) {
72
- return { [SERIALIZATION_TAG]: "GeoPoint", latitude: value.latitude, longitude: value.longitude };
76
+ return {
77
+ [SERIALIZATION_TAG]: "GeoPoint",
78
+ latitude: value.latitude,
79
+ longitude: value.longitude
80
+ };
73
81
  }
74
82
  if (isDocumentReference(value)) {
75
83
  return { [SERIALIZATION_TAG]: "DocumentReference", path: value.path };
@@ -146,101 +154,15 @@ var init_serialization = __esm({
146
154
  }
147
155
  });
148
156
 
149
- // src/do-sqlite.ts
150
- var do_sqlite_exports = {};
151
- __export(do_sqlite_exports, {
152
- createDOSqliteGraphClient: () => createDOSqliteGraphClient
157
+ // src/cloudflare/index.ts
158
+ var cloudflare_exports = {};
159
+ __export(cloudflare_exports, {
160
+ DORPCBackend: () => DORPCBackend,
161
+ FiregraphDO: () => FiregraphDO,
162
+ createDOClient: () => createDOClient,
163
+ createSiblingClient: () => createSiblingClient
153
164
  });
154
- module.exports = __toCommonJS(do_sqlite_exports);
155
-
156
- // src/docid.ts
157
- var import_node_crypto = require("crypto");
158
-
159
- // src/internal/constants.ts
160
- var NODE_RELATION = "is";
161
- var DEFAULT_QUERY_LIMIT = 500;
162
- var BUILTIN_FIELDS = /* @__PURE__ */ new Set([
163
- "aType",
164
- "aUid",
165
- "axbType",
166
- "bType",
167
- "bUid",
168
- "createdAt",
169
- "updatedAt"
170
- ]);
171
- var SHARD_SEPARATOR = ":";
172
-
173
- // src/docid.ts
174
- function computeNodeDocId(uid) {
175
- return uid;
176
- }
177
- function computeEdgeDocId(aUid, axbType, bUid) {
178
- const composite = `${aUid}${SHARD_SEPARATOR}${axbType}${SHARD_SEPARATOR}${bUid}`;
179
- const hash = (0, import_node_crypto.createHash)("sha256").update(composite).digest("hex");
180
- const shard = hash[0];
181
- return `${shard}${SHARD_SEPARATOR}${aUid}${SHARD_SEPARATOR}${axbType}${SHARD_SEPARATOR}${bUid}`;
182
- }
183
-
184
- // src/batch.ts
185
- function buildWritableNodeRecord(aType, uid, data) {
186
- return { aType, aUid: uid, axbType: NODE_RELATION, bType: aType, bUid: uid, data };
187
- }
188
- function buildWritableEdgeRecord(aType, aUid, axbType, bType, bUid, data) {
189
- return { aType, aUid, axbType, bType, bUid, data };
190
- }
191
- var GraphBatchImpl = class {
192
- constructor(backend, registry, scopePath = "") {
193
- this.backend = backend;
194
- this.registry = registry;
195
- this.scopePath = scopePath;
196
- }
197
- async putNode(aType, uid, data) {
198
- if (this.registry) {
199
- this.registry.validate(aType, NODE_RELATION, aType, data, this.scopePath);
200
- }
201
- const docId = computeNodeDocId(uid);
202
- const record = buildWritableNodeRecord(aType, uid, data);
203
- if (this.registry) {
204
- const entry = this.registry.lookup(aType, NODE_RELATION, aType);
205
- if (entry?.schemaVersion && entry.schemaVersion > 0) {
206
- record.v = entry.schemaVersion;
207
- }
208
- }
209
- this.backend.setDoc(docId, record);
210
- }
211
- async putEdge(aType, aUid, axbType, bType, bUid, data) {
212
- if (this.registry) {
213
- this.registry.validate(aType, axbType, bType, data, this.scopePath);
214
- }
215
- const docId = computeEdgeDocId(aUid, axbType, bUid);
216
- const record = buildWritableEdgeRecord(aType, aUid, axbType, bType, bUid, data);
217
- if (this.registry) {
218
- const entry = this.registry.lookup(aType, axbType, bType);
219
- if (entry?.schemaVersion && entry.schemaVersion > 0) {
220
- record.v = entry.schemaVersion;
221
- }
222
- }
223
- this.backend.setDoc(docId, record);
224
- }
225
- async updateNode(uid, data) {
226
- const docId = computeNodeDocId(uid);
227
- this.backend.updateDoc(docId, { dataFields: data });
228
- }
229
- async removeNode(uid) {
230
- const docId = computeNodeDocId(uid);
231
- this.backend.deleteDoc(docId);
232
- }
233
- async removeEdge(aUid, axbType, bUid) {
234
- const docId = computeEdgeDocId(aUid, axbType, bUid);
235
- this.backend.deleteDoc(docId);
236
- }
237
- async commit() {
238
- await this.backend.commit();
239
- }
240
- };
241
-
242
- // src/dynamic-registry.ts
243
- var import_node_crypto3 = require("crypto");
165
+ module.exports = __toCommonJS(cloudflare_exports);
244
166
 
245
167
  // src/errors.ts
246
168
  var FiregraphError = class extends Error {
@@ -259,10 +181,7 @@ var ValidationError = class extends FiregraphError {
259
181
  };
260
182
  var RegistryViolationError = class extends FiregraphError {
261
183
  constructor(aType, axbType, bType) {
262
- super(
263
- `Unregistered triple: (${aType}) -[${axbType}]-> (${bType})`,
264
- "REGISTRY_VIOLATION"
265
- );
184
+ super(`Unregistered triple: (${aType}) -[${axbType}]-> (${bType})`, "REGISTRY_VIOLATION");
266
185
  this.name = "RegistryViolationError";
267
186
  }
268
187
  };
@@ -300,237 +219,852 @@ var MigrationError = class extends FiregraphError {
300
219
  }
301
220
  };
302
221
 
303
- // src/json-schema.ts
304
- var import_ajv = __toESM(require("ajv"), 1);
305
- var import_ajv_formats = __toESM(require("ajv-formats"), 1);
306
- var ajv = new import_ajv.default({ allErrors: true, strict: false });
307
- (0, import_ajv_formats.default)(ajv);
308
- function compileSchema(schema, label) {
309
- const validate = ajv.compile(schema);
310
- return (data) => {
311
- if (!validate(data)) {
312
- const errors = validate.errors ?? [];
313
- const messages = errors.map((err) => `${err.instancePath || "/"}${err.message ? ": " + err.message : ""}`).join("; ");
314
- throw new ValidationError(
315
- `Data validation failed${label ? " for " + label : ""}: ${messages}`,
316
- errors
317
- );
318
- }
319
- };
320
- }
222
+ // src/internal/constants.ts
223
+ var NODE_RELATION = "is";
224
+ var DEFAULT_QUERY_LIMIT = 500;
225
+ var BUILTIN_FIELDS = /* @__PURE__ */ new Set([
226
+ "aType",
227
+ "aUid",
228
+ "axbType",
229
+ "bType",
230
+ "bUid",
231
+ "createdAt",
232
+ "updatedAt"
233
+ ]);
234
+ var SHARD_SEPARATOR = ":";
321
235
 
322
- // src/migration.ts
323
- async function applyMigrationChain(data, currentVersion, targetVersion, migrations) {
324
- const sorted = [...migrations].sort((a, b) => a.fromVersion - b.fromVersion);
325
- let result = { ...data };
326
- let version = currentVersion;
327
- for (const step of sorted) {
328
- if (step.fromVersion === version) {
329
- try {
330
- result = await step.up(result);
331
- } catch (err) {
332
- if (err instanceof MigrationError) throw err;
333
- throw new MigrationError(
334
- `Migration from v${step.fromVersion} to v${step.toVersion} failed: ${err.message}`
335
- );
336
- }
337
- if (!result || typeof result !== "object") {
338
- throw new MigrationError(
339
- `Migration from v${step.fromVersion} to v${step.toVersion} returned invalid data (expected object)`
340
- );
341
- }
342
- version = step.toVersion;
343
- }
236
+ // src/timestamp.ts
237
+ var GraphTimestampImpl = class _GraphTimestampImpl {
238
+ constructor(seconds, nanoseconds) {
239
+ this.seconds = seconds;
240
+ this.nanoseconds = nanoseconds;
344
241
  }
345
- if (version !== targetVersion) {
346
- throw new MigrationError(
347
- `Incomplete migration chain: reached v${version} but target is v${targetVersion}`
348
- );
242
+ toDate() {
243
+ return new Date(this.toMillis());
349
244
  }
350
- return result;
351
- }
352
- function validateMigrationChain(migrations, label) {
353
- if (migrations.length === 0) return;
354
- const seen = /* @__PURE__ */ new Set();
355
- for (const step of migrations) {
356
- if (step.toVersion <= step.fromVersion) {
357
- throw new MigrationError(
358
- `${label}: migration step has toVersion (${step.toVersion}) <= fromVersion (${step.fromVersion})`
359
- );
360
- }
361
- if (seen.has(step.fromVersion)) {
362
- throw new MigrationError(
363
- `${label}: duplicate migration step for fromVersion ${step.fromVersion}`
364
- );
365
- }
366
- seen.add(step.fromVersion);
245
+ toMillis() {
246
+ return this.seconds * 1e3 + Math.floor(this.nanoseconds / 1e6);
367
247
  }
368
- const sorted = [...migrations].sort((a, b) => a.fromVersion - b.fromVersion);
369
- const targetVersion = Math.max(...migrations.map((m) => m.toVersion));
370
- let version = 0;
371
- for (const step of sorted) {
372
- if (step.fromVersion === version) {
373
- version = step.toVersion;
374
- } else if (step.fromVersion > version) {
375
- throw new MigrationError(
376
- `${label}: migration chain has a gap \u2014 no step covers v${version} \u2192 v${step.fromVersion}`
377
- );
378
- }
248
+ toJSON() {
249
+ return { seconds: this.seconds, nanoseconds: this.nanoseconds };
379
250
  }
380
- if (version !== targetVersion) {
381
- throw new MigrationError(
382
- `${label}: migration chain does not reach v${targetVersion} (stuck at v${version})`
383
- );
251
+ static fromMillis(ms) {
252
+ const seconds = Math.floor(ms / 1e3);
253
+ const nanoseconds = (ms - seconds * 1e3) * 1e6;
254
+ return new _GraphTimestampImpl(seconds, nanoseconds);
384
255
  }
385
- }
386
- async function migrateRecord(record, registry, globalWriteBack = "off") {
387
- const entry = registry.lookup(record.aType, record.axbType, record.bType);
388
- if (!entry?.migrations?.length || !entry.schemaVersion) {
389
- return { record, migrated: false, writeBack: "off" };
256
+ static now() {
257
+ return _GraphTimestampImpl.fromMillis(Date.now());
390
258
  }
391
- const currentVersion = record.v ?? 0;
392
- if (currentVersion >= entry.schemaVersion) {
393
- return { record, migrated: false, writeBack: "off" };
259
+ };
260
+
261
+ // src/cloudflare/schema.ts
262
+ var DO_FIELD_TO_COLUMN = {
263
+ aType: "a_type",
264
+ aUid: "a_uid",
265
+ axbType: "axb_type",
266
+ bType: "b_type",
267
+ bUid: "b_uid",
268
+ v: "v",
269
+ createdAt: "created_at",
270
+ updatedAt: "updated_at"
271
+ };
272
+ var IDENT_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
273
+ function validateDOTableName(name) {
274
+ if (!IDENT_RE.test(name)) {
275
+ throw new Error(`Invalid SQL identifier: ${name}. Must match /^[A-Za-z_][A-Za-z0-9_]*$/.`);
394
276
  }
395
- const migratedData = await applyMigrationChain(
396
- record.data,
397
- currentVersion,
398
- entry.schemaVersion,
399
- entry.migrations
400
- );
401
- const writeBack = entry.migrationWriteBack ?? globalWriteBack ?? "off";
402
- return {
403
- record: { ...record, data: migratedData, v: entry.schemaVersion },
404
- migrated: true,
405
- writeBack
406
- };
407
277
  }
408
- async function migrateRecords(records, registry, globalWriteBack = "off") {
409
- return Promise.all(
410
- records.map((r) => migrateRecord(r, registry, globalWriteBack))
411
- );
278
+ function quoteDOIdent(name) {
279
+ validateDOTableName(name);
280
+ return `"${name}"`;
281
+ }
282
+ function buildDOSchemaStatements(table) {
283
+ const t = quoteDOIdent(table);
284
+ return [
285
+ `CREATE TABLE IF NOT EXISTS ${t} (
286
+ doc_id TEXT NOT NULL PRIMARY KEY,
287
+ a_type TEXT NOT NULL,
288
+ a_uid TEXT NOT NULL,
289
+ axb_type TEXT NOT NULL,
290
+ b_type TEXT NOT NULL,
291
+ b_uid TEXT NOT NULL,
292
+ data TEXT NOT NULL,
293
+ v INTEGER,
294
+ created_at INTEGER NOT NULL,
295
+ updated_at INTEGER NOT NULL
296
+ )`,
297
+ `CREATE INDEX IF NOT EXISTS ${quoteDOIdent(`${table}_idx_a_uid`)} ON ${t}(a_uid)`,
298
+ `CREATE INDEX IF NOT EXISTS ${quoteDOIdent(`${table}_idx_b_uid`)} ON ${t}(b_uid)`,
299
+ `CREATE INDEX IF NOT EXISTS ${quoteDOIdent(`${table}_idx_axb_type_b_uid`)} ON ${t}(axb_type, b_uid)`,
300
+ `CREATE INDEX IF NOT EXISTS ${quoteDOIdent(`${table}_idx_a_type`)} ON ${t}(a_type)`,
301
+ `CREATE INDEX IF NOT EXISTS ${quoteDOIdent(`${table}_idx_b_type`)} ON ${t}(b_type)`
302
+ ];
412
303
  }
413
304
 
414
- // src/scope.ts
415
- function matchScope(scopePath, pattern) {
416
- if (pattern === "root") return scopePath === "";
417
- if (pattern === "**") return true;
418
- const pathSegments = scopePath === "" ? [] : scopePath.split("/");
419
- const patternSegments = pattern.split("/");
420
- return matchSegments(pathSegments, 0, patternSegments, 0);
421
- }
422
- function matchScopeAny(scopePath, patterns) {
423
- if (!patterns || patterns.length === 0) return true;
424
- return patterns.some((p) => matchScope(scopePath, p));
425
- }
426
- function matchSegments(path, pi, pattern, qi) {
427
- if (pi === path.length && qi === pattern.length) return true;
428
- if (qi === pattern.length) return false;
429
- const seg = pattern[qi];
430
- if (seg === "**") {
431
- if (qi === pattern.length - 1) return true;
432
- for (let skip = 0; skip <= path.length - pi; skip++) {
433
- if (matchSegments(path, pi + skip, pattern, qi + 1)) return true;
434
- }
435
- return false;
305
+ // src/cloudflare/sql.ts
306
+ function compileFieldRef(field) {
307
+ const column = DO_FIELD_TO_COLUMN[field];
308
+ if (column) {
309
+ return { expr: quoteDOIdent(column) };
436
310
  }
437
- if (pi === path.length) return false;
438
- if (seg === "*") {
439
- return matchSegments(path, pi + 1, pattern, qi + 1);
311
+ if (field.startsWith("data.")) {
312
+ const key = field.slice(5);
313
+ validateJsonPathKey(key);
314
+ return { expr: 'json_extract("data", ?)', pathParam: `$.${key}` };
440
315
  }
441
- if (path[pi] === seg) {
442
- return matchSegments(path, pi + 1, pattern, qi + 1);
316
+ if (field === "data") {
317
+ return { expr: 'json_extract("data", ?)', pathParam: "$" };
443
318
  }
444
- return false;
445
- }
446
-
447
- // src/registry.ts
448
- function tripleKey(aType, axbType, bType) {
449
- return `${aType}:${axbType}:${bType}`;
319
+ throw new FiregraphError(
320
+ `DO SQLite backend cannot resolve filter field: ${field}`,
321
+ "INVALID_QUERY"
322
+ );
450
323
  }
451
- function tripleKeyFor(e) {
452
- return tripleKey(e.aType, e.axbType, e.bType);
324
+ var FIRESTORE_TYPE_NAMES = /* @__PURE__ */ new Set([
325
+ "Timestamp",
326
+ "GeoPoint",
327
+ "VectorValue",
328
+ "DocumentReference",
329
+ "FieldValue"
330
+ ]);
331
+ function isFirestoreSpecialType(value) {
332
+ const ctorName = value.constructor?.name;
333
+ if (ctorName && FIRESTORE_TYPE_NAMES.has(ctorName)) return ctorName;
334
+ return null;
453
335
  }
454
- function createRegistry(input) {
455
- const map = /* @__PURE__ */ new Map();
456
- let entries;
457
- if (Array.isArray(input)) {
458
- entries = input;
459
- } else {
460
- entries = discoveryToEntries(input);
336
+ function bindValue(value) {
337
+ if (value === null || value === void 0) return null;
338
+ if (typeof value === "string" || typeof value === "number" || typeof value === "bigint" || typeof value === "boolean") {
339
+ return value;
461
340
  }
462
- const entryList = Object.freeze([...entries]);
463
- for (const entry of entries) {
464
- if (entry.targetGraph && entry.targetGraph.includes("/")) {
465
- throw new ValidationError(
466
- `Entry (${entry.aType}) -[${entry.axbType}]-> (${entry.bType}) has invalid targetGraph "${entry.targetGraph}" \u2014 must be a single segment (no "/")`
341
+ if (value instanceof Date) return value.getTime();
342
+ if (typeof value === "object") {
343
+ const firestoreType = isFirestoreSpecialType(value);
344
+ if (firestoreType) {
345
+ throw new FiregraphError(
346
+ `DO SQLite backend cannot bind a Firestore ${firestoreType} value \u2014 JSON serialization would silently drop fields and the resulting bind would never match a stored row. Convert to a primitive (e.g. \`ts.toMillis()\` for Timestamp) before filtering or updating.`,
347
+ "INVALID_QUERY"
467
348
  );
468
349
  }
469
- if (entry.migrations?.length) {
470
- const label = `Entry (${entry.aType}) -[${entry.axbType}]-> (${entry.bType})`;
471
- validateMigrationChain(entry.migrations, label);
472
- entry.schemaVersion = Math.max(...entry.migrations.map((m) => m.toVersion));
473
- } else {
474
- entry.schemaVersion = void 0;
475
- }
476
- const key = tripleKey(entry.aType, entry.axbType, entry.bType);
477
- const validator = entry.jsonSchema ? compileSchema(entry.jsonSchema, `(${entry.aType}) -[${entry.axbType}]-> (${entry.bType})`) : void 0;
478
- map.set(key, { entry, validate: validator });
350
+ return JSON.stringify(value);
479
351
  }
480
- const axbIndex = /* @__PURE__ */ new Map();
481
- const axbBuild = /* @__PURE__ */ new Map();
482
- for (const entry of entries) {
483
- const existing = axbBuild.get(entry.axbType);
484
- if (existing) {
485
- existing.push(entry);
486
- } else {
487
- axbBuild.set(entry.axbType, [entry]);
352
+ return String(value);
353
+ }
354
+ var JSON_PATH_KEY_RE = /^[A-Za-z_][A-Za-z0-9_-]*$/;
355
+ function validateJsonPathKey(key) {
356
+ if (key.length === 0) {
357
+ throw new FiregraphError(
358
+ "DO SQLite backend: empty JSON path component is not allowed",
359
+ "INVALID_QUERY"
360
+ );
361
+ }
362
+ if (!JSON_PATH_KEY_RE.test(key)) {
363
+ throw new FiregraphError(
364
+ `DO SQLite backend: data field path component "${key}" is not a safe JSON-path identifier. Allowed pattern: /^[A-Za-z_][A-Za-z0-9_-]*$/. Use replaceData (full-data overwrite) for keys with reserved characters (whitespace, dots, brackets, quotes, etc.).`,
365
+ "INVALID_QUERY"
366
+ );
367
+ }
368
+ }
369
+ function compileFilter(filter, params) {
370
+ const { expr, pathParam } = compileFieldRef(filter.field);
371
+ if (pathParam !== void 0) params.push(pathParam);
372
+ switch (filter.op) {
373
+ case "==":
374
+ params.push(bindValue(filter.value));
375
+ return `${expr} = ?`;
376
+ case "!=":
377
+ params.push(bindValue(filter.value));
378
+ return `${expr} != ?`;
379
+ case "<":
380
+ params.push(bindValue(filter.value));
381
+ return `${expr} < ?`;
382
+ case "<=":
383
+ params.push(bindValue(filter.value));
384
+ return `${expr} <= ?`;
385
+ case ">":
386
+ params.push(bindValue(filter.value));
387
+ return `${expr} > ?`;
388
+ case ">=":
389
+ params.push(bindValue(filter.value));
390
+ return `${expr} >= ?`;
391
+ case "in": {
392
+ const values = asArray(filter.value, "in");
393
+ const placeholders = values.map(() => "?").join(", ");
394
+ for (const v of values) params.push(bindValue(v));
395
+ return `${expr} IN (${placeholders})`;
396
+ }
397
+ case "not-in": {
398
+ const values = asArray(filter.value, "not-in");
399
+ const placeholders = values.map(() => "?").join(", ");
400
+ for (const v of values) params.push(bindValue(v));
401
+ return `${expr} NOT IN (${placeholders})`;
402
+ }
403
+ case "array-contains": {
404
+ params.push(bindValue(filter.value));
405
+ return `EXISTS (SELECT 1 FROM json_each(${expr}) WHERE value = ?)`;
406
+ }
407
+ case "array-contains-any": {
408
+ const values = asArray(filter.value, "array-contains-any");
409
+ const placeholders = values.map(() => "?").join(", ");
410
+ for (const v of values) params.push(bindValue(v));
411
+ return `EXISTS (SELECT 1 FROM json_each(${expr}) WHERE value IN (${placeholders}))`;
488
412
  }
413
+ default:
414
+ throw new FiregraphError(
415
+ `DO SQLite backend does not support filter operator: ${String(filter.op)}`,
416
+ "INVALID_QUERY"
417
+ );
489
418
  }
490
- for (const [key, arr] of axbBuild) {
491
- axbIndex.set(key, Object.freeze(arr));
419
+ }
420
+ function asArray(value, op) {
421
+ if (!Array.isArray(value) || value.length === 0) {
422
+ throw new FiregraphError(`Operator "${op}" requires a non-empty array value`, "INVALID_QUERY");
423
+ }
424
+ return value;
425
+ }
426
+ function compileOrderBy(options, params) {
427
+ if (!options?.orderBy) return "";
428
+ const { field, direction } = options.orderBy;
429
+ const { expr, pathParam } = compileFieldRef(field);
430
+ if (pathParam !== void 0) params.push(pathParam);
431
+ const dir = direction === "desc" ? "DESC" : "ASC";
432
+ return ` ORDER BY ${expr} ${dir}`;
433
+ }
434
+ function compileLimit(options, params) {
435
+ if (options?.limit === void 0) return "";
436
+ params.push(options.limit);
437
+ return ` LIMIT ?`;
438
+ }
439
+ function compileDOSelect(table, filters, options) {
440
+ const params = [];
441
+ const conditions = [];
442
+ for (const f of filters) {
443
+ conditions.push(compileFilter(f, params));
492
444
  }
445
+ const where = conditions.length > 0 ? ` WHERE ${conditions.join(" AND ")}` : "";
446
+ let sql = `SELECT * FROM ${quoteDOIdent(table)}${where}`;
447
+ sql += compileOrderBy(options, params);
448
+ sql += compileLimit(options, params);
449
+ return { sql, params };
450
+ }
451
+ function compileDOSelectByDocId(table, docId) {
493
452
  return {
494
- lookup(aType, axbType, bType) {
495
- return map.get(tripleKey(aType, axbType, bType))?.entry;
496
- },
497
- lookupByAxbType(axbType) {
498
- return axbIndex.get(axbType) ?? [];
499
- },
500
- validate(aType, axbType, bType, data, scopePath) {
501
- const rec = map.get(tripleKey(aType, axbType, bType));
502
- if (!rec) {
503
- throw new RegistryViolationError(aType, axbType, bType);
504
- }
505
- if (scopePath !== void 0 && rec.entry.allowedIn && rec.entry.allowedIn.length > 0) {
506
- if (!matchScopeAny(scopePath, rec.entry.allowedIn)) {
507
- throw new RegistryScopeError(aType, axbType, bType, scopePath, rec.entry.allowedIn);
508
- }
509
- }
510
- if (rec.validate) {
511
- try {
512
- rec.validate(data);
513
- } catch (err) {
514
- if (err instanceof ValidationError) throw err;
515
- throw new ValidationError(
516
- `Data validation failed for (${aType}) -[${axbType}]-> (${bType})`,
517
- err
518
- );
519
- }
520
- }
521
- },
522
- entries() {
523
- return entryList;
453
+ sql: `SELECT * FROM ${quoteDOIdent(table)} WHERE "doc_id" = ? LIMIT 1`,
454
+ params: [docId]
455
+ };
456
+ }
457
+ function compileDOSet(table, docId, record, nowMillis) {
458
+ const sql = `INSERT OR REPLACE INTO ${quoteDOIdent(table)} (
459
+ doc_id, a_type, a_uid, axb_type, b_type, b_uid, data, v, created_at, updated_at
460
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;
461
+ const params = [
462
+ docId,
463
+ record.aType,
464
+ record.aUid,
465
+ record.axbType,
466
+ record.bType,
467
+ record.bUid,
468
+ JSON.stringify(record.data ?? {}),
469
+ record.v ?? null,
470
+ nowMillis,
471
+ nowMillis
472
+ ];
473
+ return { sql, params };
474
+ }
475
+ function compileDOUpdate(table, docId, update, nowMillis) {
476
+ const setClauses = [];
477
+ const params = [];
478
+ if (update.replaceData) {
479
+ setClauses.push(`"data" = ?`);
480
+ params.push(JSON.stringify(update.replaceData));
481
+ } else if (update.dataFields && Object.keys(update.dataFields).length > 0) {
482
+ const entries = Object.entries(update.dataFields);
483
+ const pathArgs = entries.map(() => `?, ?`).join(", ");
484
+ setClauses.push(`"data" = json_set(COALESCE("data", '{}'), ${pathArgs})`);
485
+ for (const [k, v] of entries) {
486
+ validateJsonPathKey(k);
487
+ params.push(`$.${k}`);
488
+ params.push(bindValue(v));
524
489
  }
490
+ }
491
+ if (update.v !== void 0) {
492
+ setClauses.push(`"v" = ?`);
493
+ params.push(update.v);
494
+ }
495
+ setClauses.push(`"updated_at" = ?`);
496
+ params.push(nowMillis);
497
+ params.push(docId);
498
+ return {
499
+ sql: `UPDATE ${quoteDOIdent(table)} SET ${setClauses.join(", ")} WHERE "doc_id" = ?`,
500
+ params
525
501
  };
526
502
  }
527
- function createMergedRegistry(base, extension) {
528
- const baseKeys = new Set(base.entries().map(tripleKeyFor));
503
+ function compileDODelete(table, docId) {
529
504
  return {
530
- lookup(aType, axbType, bType) {
531
- return base.lookup(aType, axbType, bType) ?? extension.lookup(aType, axbType, bType);
532
- },
533
- lookupByAxbType(axbType) {
505
+ sql: `DELETE FROM ${quoteDOIdent(table)} WHERE "doc_id" = ?`,
506
+ params: [docId]
507
+ };
508
+ }
509
+ function compileDODeleteAll(table) {
510
+ return {
511
+ sql: `DELETE FROM ${quoteDOIdent(table)}`,
512
+ params: []
513
+ };
514
+ }
515
+ function rowToDORecord(row) {
516
+ const dataString = row.data;
517
+ const data = dataString ? JSON.parse(dataString) : {};
518
+ const createdAtMs = toMillis(row.created_at);
519
+ const updatedAtMs = toMillis(row.updated_at);
520
+ const record = {
521
+ aType: row.a_type,
522
+ aUid: row.a_uid,
523
+ axbType: row.axb_type,
524
+ bType: row.b_type,
525
+ bUid: row.b_uid,
526
+ data,
527
+ createdAtMs,
528
+ updatedAtMs
529
+ };
530
+ if (row.v !== null && row.v !== void 0) {
531
+ record.v = Number(row.v);
532
+ }
533
+ return record;
534
+ }
535
+ function hydrateDORecord(wire) {
536
+ const record = {
537
+ aType: wire.aType,
538
+ aUid: wire.aUid,
539
+ axbType: wire.axbType,
540
+ bType: wire.bType,
541
+ bUid: wire.bUid,
542
+ data: wire.data,
543
+ createdAt: GraphTimestampImpl.fromMillis(wire.createdAtMs),
544
+ updatedAt: GraphTimestampImpl.fromMillis(wire.updatedAtMs)
545
+ };
546
+ if (wire.v !== void 0) {
547
+ record.v = wire.v;
548
+ }
549
+ return record;
550
+ }
551
+ function toMillis(value) {
552
+ if (typeof value === "number") return value;
553
+ if (typeof value === "bigint") return Number(value);
554
+ if (typeof value === "string") {
555
+ const n = Number(value);
556
+ if (Number.isFinite(n)) return n;
557
+ }
558
+ throw new FiregraphError(
559
+ `DO SQLite row has non-numeric timestamp column: ${typeof value} (${String(value)})`,
560
+ "INVALID_QUERY"
561
+ );
562
+ }
563
+
564
+ // src/cloudflare/backend.ts
565
+ function validateSegment(value, label) {
566
+ if (!value || value.includes("/")) {
567
+ throw new FiregraphError(
568
+ `Invalid ${label} for subgraph: "${value}". Must be non-empty and not contain "/".`,
569
+ "INVALID_SUBGRAPH"
570
+ );
571
+ }
572
+ }
573
+ function transactionsUnsupported() {
574
+ return new FiregraphError(
575
+ "Interactive transactions are not supported by the Cloudflare DO backend. Use `batch()` for atomic multi-write commits, or restructure the read-then-conditional-write as an explicit read \u2192 decide \u2192 batch sequence.",
576
+ "UNSUPPORTED_OPERATION"
577
+ );
578
+ }
579
+ var DORPCBatchBackend = class {
580
+ constructor(getStub) {
581
+ this.getStub = getStub;
582
+ }
583
+ ops = [];
584
+ setDoc(docId, record) {
585
+ this.ops.push({ kind: "set", docId, record });
586
+ }
587
+ updateDoc(docId, update) {
588
+ this.ops.push({ kind: "update", docId, update });
589
+ }
590
+ deleteDoc(docId) {
591
+ this.ops.push({ kind: "delete", docId });
592
+ }
593
+ async commit() {
594
+ if (this.ops.length === 0) return;
595
+ const ops = this.ops.slice();
596
+ this.ops.length = 0;
597
+ await this.getStub()._fgBatch(ops);
598
+ }
599
+ };
600
+ var DORPCBackend = class _DORPCBackend {
601
+ collectionPath = "firegraph";
602
+ scopePath;
603
+ /** @internal */
604
+ storageKey;
605
+ /** @internal */
606
+ namespace;
607
+ registryAccessor;
608
+ /** @internal — see `DORPCBackendOptions.makeSiblingClient` for the union-type rationale. */
609
+ makeSiblingClient;
610
+ cachedStub = null;
611
+ constructor(namespace, options) {
612
+ this.namespace = namespace;
613
+ this.scopePath = options.scopePath ?? "";
614
+ this.storageKey = options.storageKey;
615
+ this.registryAccessor = options.registryAccessor;
616
+ this.makeSiblingClient = options.makeSiblingClient;
617
+ }
618
+ get stub() {
619
+ if (!this.cachedStub) {
620
+ const id = this.namespace.idFromName(this.storageKey);
621
+ this.cachedStub = this.namespace.get(id);
622
+ }
623
+ return this.cachedStub;
624
+ }
625
+ // --- Reads ---
626
+ async getDoc(docId) {
627
+ const wire = await this.stub._fgGetDoc(docId);
628
+ return wire ? hydrateDORecord(wire) : null;
629
+ }
630
+ async query(filters, options) {
631
+ const wires = await this.stub._fgQuery(filters, options);
632
+ return wires.map(hydrateDORecord);
633
+ }
634
+ // --- Writes ---
635
+ async setDoc(docId, record) {
636
+ return this.stub._fgSetDoc(docId, record);
637
+ }
638
+ async updateDoc(docId, update) {
639
+ return this.stub._fgUpdateDoc(docId, update);
640
+ }
641
+ async deleteDoc(docId) {
642
+ return this.stub._fgDeleteDoc(docId);
643
+ }
644
+ // --- Transactions / batches ---
645
+ async runTransaction(_fn) {
646
+ void _fn;
647
+ throw transactionsUnsupported();
648
+ }
649
+ createBatch() {
650
+ return new DORPCBatchBackend(() => this.stub);
651
+ }
652
+ // --- Subgraphs ---
653
+ subgraph(parentNodeUid, name) {
654
+ validateSegment(parentNodeUid, "parentNodeUid");
655
+ validateSegment(name, "subgraph name");
656
+ const newStorageKey = `${this.storageKey}/${parentNodeUid}/${name}`;
657
+ const newScopePath = this.scopePath ? `${this.scopePath}/${name}` : name;
658
+ return new _DORPCBackend(this.namespace, {
659
+ scopePath: newScopePath,
660
+ storageKey: newStorageKey,
661
+ // Subgraph backends share the same live registry accessor so a cascade
662
+ // invoked on a subgraph client still fans out correctly. The sibling
663
+ // factory is also carried forward so `createSiblingClient` works from
664
+ // any subgraph client in the chain.
665
+ registryAccessor: this.registryAccessor,
666
+ makeSiblingClient: this.makeSiblingClient
667
+ });
668
+ }
669
+ // --- Cascade & bulk ---
670
+ async removeNodeCascade(uid, reader, options) {
671
+ const shouldDeleteSubgraphs = options?.deleteSubcollections !== false;
672
+ const registry = this.registryAccessor?.();
673
+ if (shouldDeleteSubgraphs && registry) {
674
+ const node = await reader.getNode(uid);
675
+ if (node) {
676
+ const topology = registry.getSubgraphTopology(node.aType);
677
+ for (const entry of topology) {
678
+ const target = entry.targetGraph;
679
+ const childBackend = this.subgraph(uid, target);
680
+ await childBackend.destroyRecursively(registry);
681
+ }
682
+ }
683
+ }
684
+ return this.stub._fgRemoveNodeCascade(uid);
685
+ }
686
+ async bulkRemoveEdges(params, _reader, options) {
687
+ void _reader;
688
+ return this.stub._fgBulkRemoveEdges(params, options);
689
+ }
690
+ // --- Cross-scope queries ---
691
+ //
692
+ // `findEdgesGlobal` is deliberately NOT defined on this class. The
693
+ // GraphClient checks for its presence before running query planning and
694
+ // throws `UNSUPPORTED_OPERATION` when absent, giving the caller an
695
+ // immediate, accurate error. Defining the method with a throwing body
696
+ // would only surface the same error AFTER `checkQuerySafety` had already
697
+ // fired — and for scan-unsafe calls that results in a misleading
698
+ // `QuerySafetyError` ("add filters like aUid+axbType") when no filter
699
+ // combination would actually make the call work on this backend. See the
700
+ // "What's not supported" section in `createDOClient` for the design
701
+ // rationale (no collection-group index across DOs).
702
+ // --- Destroy helpers ---
703
+ /**
704
+ * Wipe this DO's storage. The DO itself can't be deleted — its ID
705
+ * persists forever — but its rows can be emptied, which is what the
706
+ * cascade walk does on every descendant subgraph DO.
707
+ *
708
+ * Exposed on the concrete class (not `StorageBackend`) so generic
709
+ * backend code doesn't reach for it.
710
+ */
711
+ async destroy() {
712
+ await this.stub._fgDestroy();
713
+ }
714
+ /**
715
+ * Tear down every descendant subgraph DO, then wipe this DO's own rows.
716
+ *
717
+ * Invoked by cross-DO cascade: for each node in this DO we enumerate the
718
+ * subgraph topology and recurse into child DOs depth-first before
719
+ * wiping the current DO. The current DO's own rows are destroyed last so
720
+ * that a partial failure mid-recursion leaves the caller's reader able
721
+ * to discover what's still present.
722
+ *
723
+ * @internal
724
+ */
725
+ async destroyRecursively(registry) {
726
+ const nodes = await this.query([{ field: "axbType", op: "==", value: NODE_RELATION }]);
727
+ for (const node of nodes) {
728
+ const topology = registry.getSubgraphTopology(node.aType);
729
+ for (const entry of topology) {
730
+ const target = entry.targetGraph;
731
+ const childBackend = this.subgraph(node.aUid, target);
732
+ await childBackend.destroyRecursively(registry);
733
+ }
734
+ }
735
+ await this.destroy();
736
+ }
737
+ };
738
+
739
+ // src/docid.ts
740
+ var import_node_crypto = require("crypto");
741
+ function computeNodeDocId(uid) {
742
+ return uid;
743
+ }
744
+ function computeEdgeDocId(aUid, axbType, bUid) {
745
+ const composite = `${aUid}${SHARD_SEPARATOR}${axbType}${SHARD_SEPARATOR}${bUid}`;
746
+ const hash = (0, import_node_crypto.createHash)("sha256").update(composite).digest("hex");
747
+ const shard = hash[0];
748
+ return `${shard}${SHARD_SEPARATOR}${aUid}${SHARD_SEPARATOR}${axbType}${SHARD_SEPARATOR}${bUid}`;
749
+ }
750
+
751
+ // src/batch.ts
752
+ function buildWritableNodeRecord(aType, uid, data) {
753
+ return { aType, aUid: uid, axbType: NODE_RELATION, bType: aType, bUid: uid, data };
754
+ }
755
+ function buildWritableEdgeRecord(aType, aUid, axbType, bType, bUid, data) {
756
+ return { aType, aUid, axbType, bType, bUid, data };
757
+ }
758
+ var GraphBatchImpl = class {
759
+ constructor(backend, registry, scopePath = "") {
760
+ this.backend = backend;
761
+ this.registry = registry;
762
+ this.scopePath = scopePath;
763
+ }
764
+ async putNode(aType, uid, data) {
765
+ if (this.registry) {
766
+ this.registry.validate(aType, NODE_RELATION, aType, data, this.scopePath);
767
+ }
768
+ const docId = computeNodeDocId(uid);
769
+ const record = buildWritableNodeRecord(aType, uid, data);
770
+ if (this.registry) {
771
+ const entry = this.registry.lookup(aType, NODE_RELATION, aType);
772
+ if (entry?.schemaVersion && entry.schemaVersion > 0) {
773
+ record.v = entry.schemaVersion;
774
+ }
775
+ }
776
+ this.backend.setDoc(docId, record);
777
+ }
778
+ async putEdge(aType, aUid, axbType, bType, bUid, data) {
779
+ if (this.registry) {
780
+ this.registry.validate(aType, axbType, bType, data, this.scopePath);
781
+ }
782
+ const docId = computeEdgeDocId(aUid, axbType, bUid);
783
+ const record = buildWritableEdgeRecord(aType, aUid, axbType, bType, bUid, data);
784
+ if (this.registry) {
785
+ const entry = this.registry.lookup(aType, axbType, bType);
786
+ if (entry?.schemaVersion && entry.schemaVersion > 0) {
787
+ record.v = entry.schemaVersion;
788
+ }
789
+ }
790
+ this.backend.setDoc(docId, record);
791
+ }
792
+ async updateNode(uid, data) {
793
+ const docId = computeNodeDocId(uid);
794
+ this.backend.updateDoc(docId, { dataFields: data });
795
+ }
796
+ async removeNode(uid) {
797
+ const docId = computeNodeDocId(uid);
798
+ this.backend.deleteDoc(docId);
799
+ }
800
+ async removeEdge(aUid, axbType, bUid) {
801
+ const docId = computeEdgeDocId(aUid, axbType, bUid);
802
+ this.backend.deleteDoc(docId);
803
+ }
804
+ async commit() {
805
+ await this.backend.commit();
806
+ }
807
+ };
808
+
809
+ // src/dynamic-registry.ts
810
+ var import_node_crypto3 = require("crypto");
811
+
812
+ // src/json-schema.ts
813
+ var import_ajv = __toESM(require("ajv"), 1);
814
+ var import_ajv_formats = __toESM(require("ajv-formats"), 1);
815
+ var ajv = new import_ajv.default({ allErrors: true, strict: false });
816
+ (0, import_ajv_formats.default)(ajv);
817
+ function compileSchema(schema, label) {
818
+ const validate = ajv.compile(schema);
819
+ return (data) => {
820
+ if (!validate(data)) {
821
+ const errors = validate.errors ?? [];
822
+ const messages = errors.map((err) => `${err.instancePath || "/"}${err.message ? ": " + err.message : ""}`).join("; ");
823
+ throw new ValidationError(
824
+ `Data validation failed${label ? " for " + label : ""}: ${messages}`,
825
+ errors
826
+ );
827
+ }
828
+ };
829
+ }
830
+
831
+ // src/migration.ts
832
+ async function applyMigrationChain(data, currentVersion, targetVersion, migrations) {
833
+ const sorted = [...migrations].sort((a, b) => a.fromVersion - b.fromVersion);
834
+ let result = { ...data };
835
+ let version = currentVersion;
836
+ for (const step of sorted) {
837
+ if (step.fromVersion === version) {
838
+ try {
839
+ result = await step.up(result);
840
+ } catch (err) {
841
+ if (err instanceof MigrationError) throw err;
842
+ throw new MigrationError(
843
+ `Migration from v${step.fromVersion} to v${step.toVersion} failed: ${err.message}`
844
+ );
845
+ }
846
+ if (!result || typeof result !== "object") {
847
+ throw new MigrationError(
848
+ `Migration from v${step.fromVersion} to v${step.toVersion} returned invalid data (expected object)`
849
+ );
850
+ }
851
+ version = step.toVersion;
852
+ }
853
+ }
854
+ if (version !== targetVersion) {
855
+ throw new MigrationError(
856
+ `Incomplete migration chain: reached v${version} but target is v${targetVersion}`
857
+ );
858
+ }
859
+ return result;
860
+ }
861
+ function validateMigrationChain(migrations, label) {
862
+ if (migrations.length === 0) return;
863
+ const seen = /* @__PURE__ */ new Set();
864
+ for (const step of migrations) {
865
+ if (step.toVersion <= step.fromVersion) {
866
+ throw new MigrationError(
867
+ `${label}: migration step has toVersion (${step.toVersion}) <= fromVersion (${step.fromVersion})`
868
+ );
869
+ }
870
+ if (seen.has(step.fromVersion)) {
871
+ throw new MigrationError(
872
+ `${label}: duplicate migration step for fromVersion ${step.fromVersion}`
873
+ );
874
+ }
875
+ seen.add(step.fromVersion);
876
+ }
877
+ const sorted = [...migrations].sort((a, b) => a.fromVersion - b.fromVersion);
878
+ const targetVersion = Math.max(...migrations.map((m) => m.toVersion));
879
+ let version = 0;
880
+ for (const step of sorted) {
881
+ if (step.fromVersion === version) {
882
+ version = step.toVersion;
883
+ } else if (step.fromVersion > version) {
884
+ throw new MigrationError(
885
+ `${label}: migration chain has a gap \u2014 no step covers v${version} \u2192 v${step.fromVersion}`
886
+ );
887
+ }
888
+ }
889
+ if (version !== targetVersion) {
890
+ throw new MigrationError(
891
+ `${label}: migration chain does not reach v${targetVersion} (stuck at v${version})`
892
+ );
893
+ }
894
+ }
895
+ async function migrateRecord(record, registry, globalWriteBack = "off") {
896
+ const entry = registry.lookup(record.aType, record.axbType, record.bType);
897
+ if (!entry?.migrations?.length || !entry.schemaVersion) {
898
+ return { record, migrated: false, writeBack: "off" };
899
+ }
900
+ const currentVersion = record.v ?? 0;
901
+ if (currentVersion >= entry.schemaVersion) {
902
+ return { record, migrated: false, writeBack: "off" };
903
+ }
904
+ const migratedData = await applyMigrationChain(
905
+ record.data,
906
+ currentVersion,
907
+ entry.schemaVersion,
908
+ entry.migrations
909
+ );
910
+ const writeBack = entry.migrationWriteBack ?? globalWriteBack ?? "off";
911
+ return {
912
+ record: { ...record, data: migratedData, v: entry.schemaVersion },
913
+ migrated: true,
914
+ writeBack
915
+ };
916
+ }
917
+ async function migrateRecords(records, registry, globalWriteBack = "off") {
918
+ return Promise.all(
919
+ records.map((r) => migrateRecord(r, registry, globalWriteBack))
920
+ );
921
+ }
922
+
923
+ // src/scope.ts
924
+ function matchScope(scopePath, pattern) {
925
+ if (pattern === "root") return scopePath === "";
926
+ if (pattern === "**") return true;
927
+ const pathSegments = scopePath === "" ? [] : scopePath.split("/");
928
+ const patternSegments = pattern.split("/");
929
+ return matchSegments(pathSegments, 0, patternSegments, 0);
930
+ }
931
+ function matchScopeAny(scopePath, patterns) {
932
+ if (!patterns || patterns.length === 0) return true;
933
+ return patterns.some((p) => matchScope(scopePath, p));
934
+ }
935
+ function matchSegments(path, pi, pattern, qi) {
936
+ if (pi === path.length && qi === pattern.length) return true;
937
+ if (qi === pattern.length) return false;
938
+ const seg = pattern[qi];
939
+ if (seg === "**") {
940
+ if (qi === pattern.length - 1) return true;
941
+ for (let skip = 0; skip <= path.length - pi; skip++) {
942
+ if (matchSegments(path, pi + skip, pattern, qi + 1)) return true;
943
+ }
944
+ return false;
945
+ }
946
+ if (pi === path.length) return false;
947
+ if (seg === "*") {
948
+ return matchSegments(path, pi + 1, pattern, qi + 1);
949
+ }
950
+ if (path[pi] === seg) {
951
+ return matchSegments(path, pi + 1, pattern, qi + 1);
952
+ }
953
+ return false;
954
+ }
955
+
956
+ // src/registry.ts
957
+ function tripleKey(aType, axbType, bType) {
958
+ return `${aType}:${axbType}:${bType}`;
959
+ }
960
+ function tripleKeyFor(e) {
961
+ return tripleKey(e.aType, e.axbType, e.bType);
962
+ }
963
+ function createRegistry(input) {
964
+ const map = /* @__PURE__ */ new Map();
965
+ let entries;
966
+ if (Array.isArray(input)) {
967
+ entries = input;
968
+ } else {
969
+ entries = discoveryToEntries(input);
970
+ }
971
+ const entryList = Object.freeze([...entries]);
972
+ for (const entry of entries) {
973
+ if (entry.targetGraph && entry.targetGraph.includes("/")) {
974
+ throw new ValidationError(
975
+ `Entry (${entry.aType}) -[${entry.axbType}]-> (${entry.bType}) has invalid targetGraph "${entry.targetGraph}" \u2014 must be a single segment (no "/")`
976
+ );
977
+ }
978
+ if (entry.migrations?.length) {
979
+ const label = `Entry (${entry.aType}) -[${entry.axbType}]-> (${entry.bType})`;
980
+ validateMigrationChain(entry.migrations, label);
981
+ entry.schemaVersion = Math.max(...entry.migrations.map((m) => m.toVersion));
982
+ } else {
983
+ entry.schemaVersion = void 0;
984
+ }
985
+ const key = tripleKey(entry.aType, entry.axbType, entry.bType);
986
+ const validator = entry.jsonSchema ? compileSchema(entry.jsonSchema, `(${entry.aType}) -[${entry.axbType}]-> (${entry.bType})`) : void 0;
987
+ map.set(key, { entry, validate: validator });
988
+ }
989
+ const axbIndex = /* @__PURE__ */ new Map();
990
+ const axbBuild = /* @__PURE__ */ new Map();
991
+ for (const entry of entries) {
992
+ const existing = axbBuild.get(entry.axbType);
993
+ if (existing) {
994
+ existing.push(entry);
995
+ } else {
996
+ axbBuild.set(entry.axbType, [entry]);
997
+ }
998
+ }
999
+ for (const [key, arr] of axbBuild) {
1000
+ axbIndex.set(key, Object.freeze(arr));
1001
+ }
1002
+ const topologyIndex = /* @__PURE__ */ new Map();
1003
+ const topologyBuild = /* @__PURE__ */ new Map();
1004
+ const topologySeen = /* @__PURE__ */ new Map();
1005
+ for (const entry of entries) {
1006
+ if (!entry.targetGraph) continue;
1007
+ let seen = topologySeen.get(entry.aType);
1008
+ if (!seen) {
1009
+ seen = /* @__PURE__ */ new Set();
1010
+ topologySeen.set(entry.aType, seen);
1011
+ }
1012
+ if (seen.has(entry.targetGraph)) continue;
1013
+ seen.add(entry.targetGraph);
1014
+ const existing = topologyBuild.get(entry.aType);
1015
+ if (existing) {
1016
+ existing.push(entry);
1017
+ } else {
1018
+ topologyBuild.set(entry.aType, [entry]);
1019
+ }
1020
+ }
1021
+ for (const [key, arr] of topologyBuild) {
1022
+ topologyIndex.set(key, Object.freeze(arr));
1023
+ }
1024
+ return {
1025
+ lookup(aType, axbType, bType) {
1026
+ return map.get(tripleKey(aType, axbType, bType))?.entry;
1027
+ },
1028
+ lookupByAxbType(axbType) {
1029
+ return axbIndex.get(axbType) ?? [];
1030
+ },
1031
+ getSubgraphTopology(aType) {
1032
+ return topologyIndex.get(aType) ?? [];
1033
+ },
1034
+ validate(aType, axbType, bType, data, scopePath) {
1035
+ const rec = map.get(tripleKey(aType, axbType, bType));
1036
+ if (!rec) {
1037
+ throw new RegistryViolationError(aType, axbType, bType);
1038
+ }
1039
+ if (scopePath !== void 0 && rec.entry.allowedIn && rec.entry.allowedIn.length > 0) {
1040
+ if (!matchScopeAny(scopePath, rec.entry.allowedIn)) {
1041
+ throw new RegistryScopeError(aType, axbType, bType, scopePath, rec.entry.allowedIn);
1042
+ }
1043
+ }
1044
+ if (rec.validate) {
1045
+ try {
1046
+ rec.validate(data);
1047
+ } catch (err) {
1048
+ if (err instanceof ValidationError) throw err;
1049
+ throw new ValidationError(
1050
+ `Data validation failed for (${aType}) -[${axbType}]-> (${bType})`,
1051
+ err
1052
+ );
1053
+ }
1054
+ }
1055
+ },
1056
+ entries() {
1057
+ return entryList;
1058
+ }
1059
+ };
1060
+ }
1061
+ function createMergedRegistry(base, extension) {
1062
+ const baseKeys = new Set(base.entries().map(tripleKeyFor));
1063
+ return {
1064
+ lookup(aType, axbType, bType) {
1065
+ return base.lookup(aType, axbType, bType) ?? extension.lookup(aType, axbType, bType);
1066
+ },
1067
+ lookupByAxbType(axbType) {
534
1068
  const baseResults = base.lookupByAxbType(axbType);
535
1069
  const extResults = extension.lookupByAxbType(axbType);
536
1070
  if (extResults.length === 0) return baseResults;
@@ -544,6 +1078,21 @@ function createMergedRegistry(base, extension) {
544
1078
  }
545
1079
  return Object.freeze(merged);
546
1080
  },
1081
+ getSubgraphTopology(aType) {
1082
+ const baseResults = base.getSubgraphTopology(aType);
1083
+ const extResults = extension.getSubgraphTopology(aType);
1084
+ if (extResults.length === 0) return baseResults;
1085
+ if (baseResults.length === 0) return extResults;
1086
+ const seen = new Set(baseResults.map((e) => e.targetGraph));
1087
+ const merged = [...baseResults];
1088
+ for (const entry of extResults) {
1089
+ if (!seen.has(entry.targetGraph)) {
1090
+ seen.add(entry.targetGraph);
1091
+ merged.push(entry);
1092
+ }
1093
+ }
1094
+ return Object.freeze(merged);
1095
+ },
547
1096
  validate(aType, axbType, bType, data, scopePath) {
548
1097
  if (baseKeys.has(tripleKey(aType, axbType, bType))) {
549
1098
  return base.validate(aType, axbType, bType, data, scopePath);
@@ -1168,263 +1717,46 @@ var GraphTransactionImpl = class {
1168
1717
  return this.applyMigrations(records);
1169
1718
  }
1170
1719
  async applyMigrations(records) {
1171
- if (!this.registry || records.length === 0) return records;
1172
- const results = await migrateRecords(records, this.registry, this.globalWriteBack);
1173
- for (const result of results) {
1174
- if (result.migrated && result.writeBack !== "off") {
1175
- const docId = result.record.axbType === NODE_RELATION ? computeNodeDocId(result.record.aUid) : computeEdgeDocId(result.record.aUid, result.record.axbType, result.record.bUid);
1176
- await this.backend.updateDoc(docId, {
1177
- replaceData: result.record.data,
1178
- v: result.record.v
1179
- });
1180
- }
1181
- }
1182
- return results.map((r) => r.record);
1183
- }
1184
- async putNode(aType, uid, data) {
1185
- if (this.registry) {
1186
- this.registry.validate(aType, NODE_RELATION, aType, data, this.scopePath);
1187
- }
1188
- const docId = computeNodeDocId(uid);
1189
- const record = buildWritableNodeRecord2(aType, uid, data);
1190
- if (this.registry) {
1191
- const entry = this.registry.lookup(aType, NODE_RELATION, aType);
1192
- if (entry?.schemaVersion && entry.schemaVersion > 0) {
1193
- record.v = entry.schemaVersion;
1194
- }
1195
- }
1196
- await this.backend.setDoc(docId, record);
1197
- }
1198
- async putEdge(aType, aUid, axbType, bType, bUid, data) {
1199
- if (this.registry) {
1200
- this.registry.validate(aType, axbType, bType, data, this.scopePath);
1201
- }
1202
- const docId = computeEdgeDocId(aUid, axbType, bUid);
1203
- const record = buildWritableEdgeRecord2(aType, aUid, axbType, bType, bUid, data);
1204
- if (this.registry) {
1205
- const entry = this.registry.lookup(aType, axbType, bType);
1206
- if (entry?.schemaVersion && entry.schemaVersion > 0) {
1207
- record.v = entry.schemaVersion;
1208
- }
1209
- }
1210
- await this.backend.setDoc(docId, record);
1211
- }
1212
- async updateNode(uid, data) {
1213
- const docId = computeNodeDocId(uid);
1214
- await this.backend.updateDoc(docId, { dataFields: data });
1215
- }
1216
- async removeNode(uid) {
1217
- const docId = computeNodeDocId(uid);
1218
- await this.backend.deleteDoc(docId);
1219
- }
1220
- async removeEdge(aUid, axbType, bUid) {
1221
- const docId = computeEdgeDocId(aUid, axbType, bUid);
1222
- await this.backend.deleteDoc(docId);
1223
- }
1224
- };
1225
-
1226
- // src/client.ts
1227
- var RESERVED_TYPE_NAMES = /* @__PURE__ */ new Set([META_NODE_TYPE, META_EDGE_TYPE]);
1228
- function buildWritableNodeRecord3(aType, uid, data) {
1229
- return { aType, aUid: uid, axbType: NODE_RELATION, bType: aType, bUid: uid, data };
1230
- }
1231
- function buildWritableEdgeRecord3(aType, aUid, axbType, bType, bUid, data) {
1232
- return { aType, aUid, axbType, bType, bUid, data };
1233
- }
1234
- var GraphClientImpl = class _GraphClientImpl {
1235
- constructor(backend, options, metaBackend) {
1236
- this.backend = backend;
1237
- this.globalWriteBack = options?.migrationWriteBack ?? "off";
1238
- this.migrationSandbox = options?.migrationSandbox;
1239
- if (options?.registryMode) {
1240
- this.dynamicConfig = options.registryMode;
1241
- this.bootstrapRegistry = createBootstrapRegistry();
1242
- if (options.registry) {
1243
- this.staticRegistry = options.registry;
1244
- }
1245
- this.metaBackend = metaBackend;
1246
- } else {
1247
- this.staticRegistry = options?.registry;
1248
- }
1249
- this.scanProtection = options?.scanProtection ?? "error";
1250
- }
1251
- scanProtection;
1252
- // Static mode
1253
- staticRegistry;
1254
- // Dynamic mode
1255
- dynamicConfig;
1256
- bootstrapRegistry;
1257
- dynamicRegistry;
1258
- metaBackend;
1259
- // Migration settings
1260
- globalWriteBack;
1261
- migrationSandbox;
1262
- // ---------------------------------------------------------------------------
1263
- // Backend access (exposed for traversal helpers and subgraph cloning)
1264
- // ---------------------------------------------------------------------------
1265
- /** @internal */
1266
- getBackend() {
1267
- return this.backend;
1268
- }
1269
- // ---------------------------------------------------------------------------
1270
- // Registry routing
1271
- // ---------------------------------------------------------------------------
1272
- getRegistryForType(aType) {
1273
- if (!this.dynamicConfig) return this.staticRegistry;
1274
- if (aType === META_NODE_TYPE || aType === META_EDGE_TYPE) {
1275
- return this.bootstrapRegistry;
1276
- }
1277
- return this.dynamicRegistry ?? this.staticRegistry ?? this.bootstrapRegistry;
1278
- }
1279
- getBackendForType(aType) {
1280
- if (this.metaBackend && (aType === META_NODE_TYPE || aType === META_EDGE_TYPE)) {
1281
- return this.metaBackend;
1282
- }
1283
- return this.backend;
1284
- }
1285
- getCombinedRegistry() {
1286
- if (!this.dynamicConfig) return this.staticRegistry;
1287
- return this.dynamicRegistry ?? this.staticRegistry ?? this.bootstrapRegistry;
1288
- }
1289
- // ---------------------------------------------------------------------------
1290
- // Query safety
1291
- // ---------------------------------------------------------------------------
1292
- checkQuerySafety(filters, allowCollectionScan) {
1293
- if (allowCollectionScan || this.scanProtection === "off") return;
1294
- const result = analyzeQuerySafety(filters);
1295
- if (result.safe) return;
1296
- if (this.scanProtection === "error") {
1297
- throw new QuerySafetyError(result.reason);
1298
- }
1299
- console.warn(`[firegraph] Query safety warning: ${result.reason}`);
1300
- }
1301
- // ---------------------------------------------------------------------------
1302
- // Migration helpers
1303
- // ---------------------------------------------------------------------------
1304
- async applyMigration(record, docId) {
1305
- const registry = this.getCombinedRegistry();
1306
- if (!registry) return record;
1307
- const result = await migrateRecord(record, registry, this.globalWriteBack);
1308
- if (result.migrated) {
1309
- this.handleWriteBack(result, docId);
1310
- }
1311
- return result.record;
1312
- }
1313
- async applyMigrations(records) {
1314
- const registry = this.getCombinedRegistry();
1315
- if (!registry || records.length === 0) return records;
1316
- const results = await migrateRecords(records, registry, this.globalWriteBack);
1317
- for (const result of results) {
1318
- if (result.migrated) {
1319
- const docId = result.record.axbType === NODE_RELATION ? computeNodeDocId(result.record.aUid) : computeEdgeDocId(result.record.aUid, result.record.axbType, result.record.bUid);
1320
- this.handleWriteBack(result, docId);
1321
- }
1322
- }
1323
- return results.map((r) => r.record);
1324
- }
1325
- /**
1326
- * Fire-and-forget write-back for a migrated record. Both `'eager'` and
1327
- * `'background'` are non-blocking; the difference is the log level on
1328
- * failure. For synchronous write-back, use a transaction — see
1329
- * `GraphTransactionImpl`.
1330
- */
1331
- handleWriteBack(result, docId) {
1332
- if (result.writeBack === "off") return;
1333
- const doWriteBack = async () => {
1334
- try {
1335
- await this.backend.updateDoc(docId, {
1336
- replaceData: result.record.data,
1337
- v: result.record.v
1338
- });
1339
- } catch (err) {
1340
- const msg = `[firegraph] Migration write-back failed for ${docId}: ${err.message}`;
1341
- if (result.writeBack === "eager") {
1342
- console.error(msg);
1343
- } else {
1344
- console.warn(msg);
1345
- }
1346
- }
1347
- };
1348
- void doWriteBack();
1349
- }
1350
- // ---------------------------------------------------------------------------
1351
- // GraphReader
1352
- // ---------------------------------------------------------------------------
1353
- async getNode(uid) {
1354
- const docId = computeNodeDocId(uid);
1355
- const record = await this.backend.getDoc(docId);
1356
- if (!record) return null;
1357
- return this.applyMigration(record, docId);
1358
- }
1359
- async getEdge(aUid, axbType, bUid) {
1360
- const docId = computeEdgeDocId(aUid, axbType, bUid);
1361
- const record = await this.backend.getDoc(docId);
1362
- if (!record) return null;
1363
- return this.applyMigration(record, docId);
1364
- }
1365
- async edgeExists(aUid, axbType, bUid) {
1366
- const docId = computeEdgeDocId(aUid, axbType, bUid);
1367
- const record = await this.backend.getDoc(docId);
1368
- return record !== null;
1369
- }
1370
- async findEdges(params) {
1371
- const plan = buildEdgeQueryPlan(params);
1372
- let records;
1373
- if (plan.strategy === "get") {
1374
- const record = await this.backend.getDoc(plan.docId);
1375
- records = record ? [record] : [];
1376
- } else {
1377
- this.checkQuerySafety(plan.filters, params.allowCollectionScan);
1378
- records = await this.backend.query(plan.filters, plan.options);
1379
- }
1380
- return this.applyMigrations(records);
1381
- }
1382
- async findNodes(params) {
1383
- const plan = buildNodeQueryPlan(params);
1384
- let records;
1385
- if (plan.strategy === "get") {
1386
- const record = await this.backend.getDoc(plan.docId);
1387
- records = record ? [record] : [];
1388
- } else {
1389
- this.checkQuerySafety(plan.filters, params.allowCollectionScan);
1390
- records = await this.backend.query(plan.filters, plan.options);
1720
+ if (!this.registry || records.length === 0) return records;
1721
+ const results = await migrateRecords(records, this.registry, this.globalWriteBack);
1722
+ for (const result of results) {
1723
+ if (result.migrated && result.writeBack !== "off") {
1724
+ const docId = result.record.axbType === NODE_RELATION ? computeNodeDocId(result.record.aUid) : computeEdgeDocId(result.record.aUid, result.record.axbType, result.record.bUid);
1725
+ await this.backend.updateDoc(docId, {
1726
+ replaceData: result.record.data,
1727
+ v: result.record.v
1728
+ });
1729
+ }
1391
1730
  }
1392
- return this.applyMigrations(records);
1731
+ return results.map((r) => r.record);
1393
1732
  }
1394
- // ---------------------------------------------------------------------------
1395
- // GraphWriter
1396
- // ---------------------------------------------------------------------------
1397
1733
  async putNode(aType, uid, data) {
1398
- const registry = this.getRegistryForType(aType);
1399
- if (registry) {
1400
- registry.validate(aType, NODE_RELATION, aType, data, this.backend.scopePath);
1734
+ if (this.registry) {
1735
+ this.registry.validate(aType, NODE_RELATION, aType, data, this.scopePath);
1401
1736
  }
1402
- const backend = this.getBackendForType(aType);
1403
1737
  const docId = computeNodeDocId(uid);
1404
- const record = buildWritableNodeRecord3(aType, uid, data);
1405
- if (registry) {
1406
- const entry = registry.lookup(aType, NODE_RELATION, aType);
1738
+ const record = buildWritableNodeRecord2(aType, uid, data);
1739
+ if (this.registry) {
1740
+ const entry = this.registry.lookup(aType, NODE_RELATION, aType);
1407
1741
  if (entry?.schemaVersion && entry.schemaVersion > 0) {
1408
1742
  record.v = entry.schemaVersion;
1409
1743
  }
1410
1744
  }
1411
- await backend.setDoc(docId, record);
1745
+ await this.backend.setDoc(docId, record);
1412
1746
  }
1413
1747
  async putEdge(aType, aUid, axbType, bType, bUid, data) {
1414
- const registry = this.getRegistryForType(aType);
1415
- if (registry) {
1416
- registry.validate(aType, axbType, bType, data, this.backend.scopePath);
1748
+ if (this.registry) {
1749
+ this.registry.validate(aType, axbType, bType, data, this.scopePath);
1417
1750
  }
1418
- const backend = this.getBackendForType(aType);
1419
1751
  const docId = computeEdgeDocId(aUid, axbType, bUid);
1420
- const record = buildWritableEdgeRecord3(aType, aUid, axbType, bType, bUid, data);
1421
- if (registry) {
1422
- const entry = registry.lookup(aType, axbType, bType);
1752
+ const record = buildWritableEdgeRecord2(aType, aUid, axbType, bType, bUid, data);
1753
+ if (this.registry) {
1754
+ const entry = this.registry.lookup(aType, axbType, bType);
1423
1755
  if (entry?.schemaVersion && entry.schemaVersion > 0) {
1424
1756
  record.v = entry.schemaVersion;
1425
1757
  }
1426
1758
  }
1427
- await backend.setDoc(docId, record);
1759
+ await this.backend.setDoc(docId, record);
1428
1760
  }
1429
1761
  async updateNode(uid, data) {
1430
1762
  const docId = computeNodeDocId(uid);
@@ -1438,982 +1770,768 @@ var GraphClientImpl = class _GraphClientImpl {
1438
1770
  const docId = computeEdgeDocId(aUid, axbType, bUid);
1439
1771
  await this.backend.deleteDoc(docId);
1440
1772
  }
1441
- // ---------------------------------------------------------------------------
1442
- // Transactions & Batches
1443
- // ---------------------------------------------------------------------------
1444
- async runTransaction(fn) {
1445
- return this.backend.runTransaction(async (txBackend) => {
1446
- const graphTx = new GraphTransactionImpl(
1447
- txBackend,
1448
- this.getCombinedRegistry(),
1449
- this.scanProtection,
1450
- this.backend.scopePath,
1451
- this.globalWriteBack
1452
- );
1453
- return fn(graphTx);
1454
- });
1455
- }
1456
- batch() {
1457
- return new GraphBatchImpl(
1458
- this.backend.createBatch(),
1459
- this.getCombinedRegistry(),
1460
- this.backend.scopePath
1461
- );
1462
- }
1463
- // ---------------------------------------------------------------------------
1464
- // Subgraph
1465
- // ---------------------------------------------------------------------------
1466
- subgraph(parentNodeUid, name = "graph") {
1467
- if (!parentNodeUid || parentNodeUid.includes("/")) {
1468
- throw new FiregraphError(
1469
- `Invalid parentNodeUid for subgraph: "${parentNodeUid}". Must be a non-empty string without "/".`,
1470
- "INVALID_SUBGRAPH"
1471
- );
1472
- }
1473
- if (name.includes("/")) {
1474
- throw new FiregraphError(
1475
- `Subgraph name must not contain "/": got "${name}". Use chained .subgraph() calls for nested subgraphs.`,
1476
- "INVALID_SUBGRAPH"
1477
- );
1478
- }
1479
- const childBackend = this.backend.subgraph(parentNodeUid, name);
1480
- return new _GraphClientImpl(
1481
- childBackend,
1482
- {
1483
- registry: this.getCombinedRegistry(),
1484
- scanProtection: this.scanProtection,
1485
- migrationWriteBack: this.globalWriteBack,
1486
- migrationSandbox: this.migrationSandbox
1773
+ };
1774
+
1775
+ // src/client.ts
1776
+ var RESERVED_TYPE_NAMES = /* @__PURE__ */ new Set([META_NODE_TYPE, META_EDGE_TYPE]);
1777
+ function buildWritableNodeRecord3(aType, uid, data) {
1778
+ return { aType, aUid: uid, axbType: NODE_RELATION, bType: aType, bUid: uid, data };
1779
+ }
1780
+ function buildWritableEdgeRecord3(aType, aUid, axbType, bType, bUid, data) {
1781
+ return { aType, aUid, axbType, bType, bUid, data };
1782
+ }
1783
+ var GraphClientImpl = class _GraphClientImpl {
1784
+ constructor(backend, options, metaBackend) {
1785
+ this.backend = backend;
1786
+ this.globalWriteBack = options?.migrationWriteBack ?? "off";
1787
+ this.migrationSandbox = options?.migrationSandbox;
1788
+ if (options?.registryMode) {
1789
+ this.dynamicConfig = options.registryMode;
1790
+ this.bootstrapRegistry = createBootstrapRegistry();
1791
+ if (options.registry) {
1792
+ this.staticRegistry = options.registry;
1487
1793
  }
1488
- // Subgraphs do not have meta-backends; meta lives only at the root.
1489
- );
1490
- }
1491
- // ---------------------------------------------------------------------------
1492
- // Collection group query
1493
- // ---------------------------------------------------------------------------
1494
- async findEdgesGlobal(params, collectionName) {
1495
- if (!this.backend.findEdgesGlobal) {
1496
- throw new FiregraphError(
1497
- "findEdgesGlobal() is not supported by the current storage backend.",
1498
- "UNSUPPORTED_OPERATION"
1499
- );
1500
- }
1501
- const plan = buildEdgeQueryPlan(params);
1502
- if (plan.strategy === "get") {
1503
- throw new FiregraphError(
1504
- "findEdgesGlobal() requires a query, not a direct document lookup. Omit one of aUid/axbType/bUid to force a query strategy.",
1505
- "INVALID_QUERY"
1506
- );
1794
+ this.metaBackend = metaBackend;
1795
+ } else {
1796
+ this.staticRegistry = options?.registry;
1507
1797
  }
1508
- this.checkQuerySafety(plan.filters, params.allowCollectionScan);
1509
- const records = await this.backend.findEdgesGlobal(params, collectionName);
1510
- return this.applyMigrations(records);
1798
+ this.scanProtection = options?.scanProtection ?? "error";
1511
1799
  }
1800
+ scanProtection;
1801
+ // Static mode
1802
+ staticRegistry;
1803
+ // Dynamic mode
1804
+ dynamicConfig;
1805
+ bootstrapRegistry;
1806
+ dynamicRegistry;
1807
+ metaBackend;
1808
+ // Migration settings
1809
+ globalWriteBack;
1810
+ migrationSandbox;
1512
1811
  // ---------------------------------------------------------------------------
1513
- // Bulk operations
1812
+ // Backend access (exposed for traversal helpers and subgraph cloning)
1514
1813
  // ---------------------------------------------------------------------------
1515
- async removeNodeCascade(uid, options) {
1516
- return this.backend.removeNodeCascade(uid, this, options);
1814
+ /** @internal */
1815
+ getBackend() {
1816
+ return this.backend;
1517
1817
  }
1518
- async bulkRemoveEdges(params, options) {
1519
- return this.backend.bulkRemoveEdges(params, this, options);
1818
+ /**
1819
+ * Snapshot of the currently-effective registry. Returns the merged view
1820
+ * used for domain-type validation and migration — in dynamic mode this is
1821
+ * `dynamicRegistry ?? staticRegistry ?? bootstrapRegistry`, so callers see
1822
+ * updates after `reloadRegistry()` without having to re-resolve anything.
1823
+ *
1824
+ * Exposed for backends that need topology access during bulk operations
1825
+ * (e.g. the Cloudflare DO backend's cross-DO cascade). Not part of the
1826
+ * public `GraphClient` surface.
1827
+ *
1828
+ * @internal
1829
+ */
1830
+ getRegistrySnapshot() {
1831
+ return this.getCombinedRegistry();
1520
1832
  }
1521
1833
  // ---------------------------------------------------------------------------
1522
- // Dynamic registry methods
1834
+ // Registry routing
1523
1835
  // ---------------------------------------------------------------------------
1524
- async defineNodeType(name, jsonSchema, description, options) {
1525
- if (!this.dynamicConfig) {
1526
- throw new DynamicRegistryError(
1527
- 'defineNodeType() is only available in dynamic registry mode. Pass registryMode: { mode: "dynamic" } to createGraphClient().'
1528
- );
1529
- }
1530
- if (RESERVED_TYPE_NAMES.has(name)) {
1531
- throw new DynamicRegistryError(
1532
- `Cannot define type "${name}": this name is reserved for the meta-registry.`
1533
- );
1534
- }
1535
- if (this.staticRegistry?.lookup(name, NODE_RELATION, name)) {
1536
- throw new DynamicRegistryError(
1537
- `Cannot define node type "${name}": already defined in the static registry.`
1538
- );
1539
- }
1540
- const uid = generateDeterministicUid(META_NODE_TYPE, name);
1541
- const data = { name, jsonSchema };
1542
- if (description !== void 0) data.description = description;
1543
- if (options?.titleField !== void 0) data.titleField = options.titleField;
1544
- if (options?.subtitleField !== void 0) data.subtitleField = options.subtitleField;
1545
- if (options?.viewTemplate !== void 0) data.viewTemplate = options.viewTemplate;
1546
- if (options?.viewCss !== void 0) data.viewCss = options.viewCss;
1547
- if (options?.allowedIn !== void 0) data.allowedIn = options.allowedIn;
1548
- if (options?.migrationWriteBack !== void 0)
1549
- data.migrationWriteBack = options.migrationWriteBack;
1550
- if (options?.migrations !== void 0) {
1551
- data.migrations = await this.serializeMigrations(options.migrations);
1836
+ getRegistryForType(aType) {
1837
+ if (!this.dynamicConfig) return this.staticRegistry;
1838
+ if (aType === META_NODE_TYPE || aType === META_EDGE_TYPE) {
1839
+ return this.bootstrapRegistry;
1552
1840
  }
1553
- await this.putNode(META_NODE_TYPE, uid, data);
1841
+ return this.dynamicRegistry ?? this.staticRegistry ?? this.bootstrapRegistry;
1554
1842
  }
1555
- async defineEdgeType(name, topology, jsonSchema, description, options) {
1556
- if (!this.dynamicConfig) {
1557
- throw new DynamicRegistryError(
1558
- 'defineEdgeType() is only available in dynamic registry mode. Pass registryMode: { mode: "dynamic" } to createGraphClient().'
1559
- );
1560
- }
1561
- if (RESERVED_TYPE_NAMES.has(name)) {
1562
- throw new DynamicRegistryError(
1563
- `Cannot define type "${name}": this name is reserved for the meta-registry.`
1564
- );
1565
- }
1566
- if (this.staticRegistry) {
1567
- const fromTypes = Array.isArray(topology.from) ? topology.from : [topology.from];
1568
- const toTypes = Array.isArray(topology.to) ? topology.to : [topology.to];
1569
- for (const aType of fromTypes) {
1570
- for (const bType of toTypes) {
1571
- if (this.staticRegistry.lookup(aType, name, bType)) {
1572
- throw new DynamicRegistryError(
1573
- `Cannot define edge type "${name}" for (${aType}) -> (${bType}): already defined in the static registry.`
1574
- );
1575
- }
1576
- }
1577
- }
1578
- }
1579
- const uid = generateDeterministicUid(META_EDGE_TYPE, name);
1580
- const data = {
1581
- name,
1582
- from: topology.from,
1583
- to: topology.to
1584
- };
1585
- if (jsonSchema !== void 0) data.jsonSchema = jsonSchema;
1586
- if (topology.inverseLabel !== void 0) data.inverseLabel = topology.inverseLabel;
1587
- if (topology.targetGraph !== void 0) data.targetGraph = topology.targetGraph;
1588
- if (description !== void 0) data.description = description;
1589
- if (options?.titleField !== void 0) data.titleField = options.titleField;
1590
- if (options?.subtitleField !== void 0) data.subtitleField = options.subtitleField;
1591
- if (options?.viewTemplate !== void 0) data.viewTemplate = options.viewTemplate;
1592
- if (options?.viewCss !== void 0) data.viewCss = options.viewCss;
1593
- if (options?.allowedIn !== void 0) data.allowedIn = options.allowedIn;
1594
- if (options?.migrationWriteBack !== void 0)
1595
- data.migrationWriteBack = options.migrationWriteBack;
1596
- if (options?.migrations !== void 0) {
1597
- data.migrations = await this.serializeMigrations(options.migrations);
1843
+ getBackendForType(aType) {
1844
+ if (this.metaBackend && (aType === META_NODE_TYPE || aType === META_EDGE_TYPE)) {
1845
+ return this.metaBackend;
1598
1846
  }
1599
- await this.putNode(META_EDGE_TYPE, uid, data);
1847
+ return this.backend;
1600
1848
  }
1601
- async reloadRegistry() {
1602
- if (!this.dynamicConfig) {
1603
- throw new DynamicRegistryError(
1604
- 'reloadRegistry() is only available in dynamic registry mode. Pass registryMode: { mode: "dynamic" } to createGraphClient().'
1605
- );
1849
+ getCombinedRegistry() {
1850
+ if (!this.dynamicConfig) return this.staticRegistry;
1851
+ return this.dynamicRegistry ?? this.staticRegistry ?? this.bootstrapRegistry;
1852
+ }
1853
+ // ---------------------------------------------------------------------------
1854
+ // Query safety
1855
+ // ---------------------------------------------------------------------------
1856
+ checkQuerySafety(filters, allowCollectionScan) {
1857
+ if (allowCollectionScan || this.scanProtection === "off") return;
1858
+ const result = analyzeQuerySafety(filters);
1859
+ if (result.safe) return;
1860
+ if (this.scanProtection === "error") {
1861
+ throw new QuerySafetyError(result.reason);
1606
1862
  }
1607
- const reader = this.createMetaReader();
1608
- const dynamicOnly = await createRegistryFromGraph(reader, this.migrationSandbox);
1609
- if (this.staticRegistry) {
1610
- this.dynamicRegistry = createMergedRegistry(this.staticRegistry, dynamicOnly);
1611
- } else {
1612
- this.dynamicRegistry = dynamicOnly;
1863
+ console.warn(`[firegraph] Query safety warning: ${result.reason}`);
1864
+ }
1865
+ // ---------------------------------------------------------------------------
1866
+ // Migration helpers
1867
+ // ---------------------------------------------------------------------------
1868
+ async applyMigration(record, docId) {
1869
+ const registry = this.getCombinedRegistry();
1870
+ if (!registry) return record;
1871
+ const result = await migrateRecord(record, registry, this.globalWriteBack);
1872
+ if (result.migrated) {
1873
+ this.handleWriteBack(result, docId);
1613
1874
  }
1875
+ return result.record;
1614
1876
  }
1615
- async serializeMigrations(migrations) {
1616
- const result = migrations.map((m) => {
1617
- const source = typeof m.up === "function" ? m.up.toString() : m.up;
1618
- return { fromVersion: m.fromVersion, toVersion: m.toVersion, up: source };
1619
- });
1620
- await Promise.all(result.map((m) => precompileSource(m.up, this.migrationSandbox)));
1621
- return result;
1877
+ async applyMigrations(records) {
1878
+ const registry = this.getCombinedRegistry();
1879
+ if (!registry || records.length === 0) return records;
1880
+ const results = await migrateRecords(records, registry, this.globalWriteBack);
1881
+ for (const result of results) {
1882
+ if (result.migrated) {
1883
+ const docId = result.record.axbType === NODE_RELATION ? computeNodeDocId(result.record.aUid) : computeEdgeDocId(result.record.aUid, result.record.axbType, result.record.bUid);
1884
+ this.handleWriteBack(result, docId);
1885
+ }
1886
+ }
1887
+ return results.map((r) => r.record);
1622
1888
  }
1623
1889
  /**
1624
- * Build a `GraphReader` over the meta-backend. If meta lives in the same
1625
- * collection as the main backend, `this` is returned directly.
1890
+ * Fire-and-forget write-back for a migrated record. Both `'eager'` and
1891
+ * `'background'` are non-blocking; the difference is the log level on
1892
+ * failure. For synchronous write-back, use a transaction — see
1893
+ * `GraphTransactionImpl`.
1626
1894
  */
1627
- createMetaReader() {
1628
- if (!this.metaBackend) return this;
1629
- const backend = this.metaBackend;
1630
- const executeMetaQuery = (filters, options) => backend.query(filters, options);
1631
- return {
1632
- async getNode(uid) {
1633
- return backend.getDoc(computeNodeDocId(uid));
1634
- },
1635
- async getEdge(aUid, axbType, bUid) {
1636
- return backend.getDoc(computeEdgeDocId(aUid, axbType, bUid));
1637
- },
1638
- async edgeExists(aUid, axbType, bUid) {
1639
- const record = await backend.getDoc(computeEdgeDocId(aUid, axbType, bUid));
1640
- return record !== null;
1641
- },
1642
- async findEdges(params) {
1643
- const plan = buildEdgeQueryPlan(params);
1644
- if (plan.strategy === "get") {
1645
- const record = await backend.getDoc(plan.docId);
1646
- return record ? [record] : [];
1647
- }
1648
- return executeMetaQuery(plan.filters, plan.options);
1649
- },
1650
- async findNodes(params) {
1651
- const plan = buildNodeQueryPlan(params);
1652
- if (plan.strategy === "get") {
1653
- const record = await backend.getDoc(plan.docId);
1654
- return record ? [record] : [];
1895
+ handleWriteBack(result, docId) {
1896
+ if (result.writeBack === "off") return;
1897
+ const doWriteBack = async () => {
1898
+ try {
1899
+ await this.backend.updateDoc(docId, {
1900
+ replaceData: result.record.data,
1901
+ v: result.record.v
1902
+ });
1903
+ } catch (err) {
1904
+ const msg = `[firegraph] Migration write-back failed for ${docId}: ${err.message}`;
1905
+ if (result.writeBack === "eager") {
1906
+ console.error(msg);
1907
+ } else {
1908
+ console.warn(msg);
1655
1909
  }
1656
- return executeMetaQuery(plan.filters, plan.options);
1657
1910
  }
1658
1911
  };
1912
+ void doWriteBack();
1659
1913
  }
1660
- };
1661
- function createGraphClientFromBackend(backend, options, metaBackend) {
1662
- return new GraphClientImpl(backend, options, metaBackend);
1663
- }
1664
-
1665
- // src/timestamp.ts
1666
- var GraphTimestampImpl = class _GraphTimestampImpl {
1667
- constructor(seconds, nanoseconds) {
1668
- this.seconds = seconds;
1669
- this.nanoseconds = nanoseconds;
1670
- }
1671
- toDate() {
1672
- return new Date(this.toMillis());
1673
- }
1674
- toMillis() {
1675
- return this.seconds * 1e3 + Math.floor(this.nanoseconds / 1e6);
1914
+ // ---------------------------------------------------------------------------
1915
+ // GraphReader
1916
+ // ---------------------------------------------------------------------------
1917
+ async getNode(uid) {
1918
+ const docId = computeNodeDocId(uid);
1919
+ const record = await this.backend.getDoc(docId);
1920
+ if (!record) return null;
1921
+ return this.applyMigration(record, docId);
1676
1922
  }
1677
- toJSON() {
1678
- return { seconds: this.seconds, nanoseconds: this.nanoseconds };
1923
+ async getEdge(aUid, axbType, bUid) {
1924
+ const docId = computeEdgeDocId(aUid, axbType, bUid);
1925
+ const record = await this.backend.getDoc(docId);
1926
+ if (!record) return null;
1927
+ return this.applyMigration(record, docId);
1679
1928
  }
1680
- static fromMillis(ms) {
1681
- const seconds = Math.floor(ms / 1e3);
1682
- const nanoseconds = (ms - seconds * 1e3) * 1e6;
1683
- return new _GraphTimestampImpl(seconds, nanoseconds);
1929
+ async edgeExists(aUid, axbType, bUid) {
1930
+ const docId = computeEdgeDocId(aUid, axbType, bUid);
1931
+ const record = await this.backend.getDoc(docId);
1932
+ return record !== null;
1684
1933
  }
1685
- static now() {
1686
- return _GraphTimestampImpl.fromMillis(Date.now());
1934
+ async findEdges(params) {
1935
+ const plan = buildEdgeQueryPlan(params);
1936
+ let records;
1937
+ if (plan.strategy === "get") {
1938
+ const record = await this.backend.getDoc(plan.docId);
1939
+ records = record ? [record] : [];
1940
+ } else {
1941
+ this.checkQuerySafety(plan.filters, params.allowCollectionScan);
1942
+ records = await this.backend.query(plan.filters, plan.options);
1943
+ }
1944
+ return this.applyMigrations(records);
1687
1945
  }
1688
- };
1689
-
1690
- // src/internal/sqlite-schema.ts
1691
- var FIELD_TO_COLUMN = {
1692
- aType: "a_type",
1693
- aUid: "a_uid",
1694
- axbType: "axb_type",
1695
- bType: "b_type",
1696
- bUid: "b_uid",
1697
- v: "v",
1698
- createdAt: "created_at",
1699
- updatedAt: "updated_at"
1700
- };
1701
- function buildSchemaStatements(table) {
1702
- const t = quoteIdent(table);
1703
- return [
1704
- `CREATE TABLE IF NOT EXISTS ${t} (
1705
- doc_id TEXT NOT NULL,
1706
- scope TEXT NOT NULL DEFAULT '',
1707
- a_type TEXT NOT NULL,
1708
- a_uid TEXT NOT NULL,
1709
- axb_type TEXT NOT NULL,
1710
- b_type TEXT NOT NULL,
1711
- b_uid TEXT NOT NULL,
1712
- data TEXT NOT NULL,
1713
- v INTEGER,
1714
- created_at INTEGER NOT NULL,
1715
- updated_at INTEGER NOT NULL,
1716
- PRIMARY KEY (scope, doc_id)
1717
- )`,
1718
- `CREATE INDEX IF NOT EXISTS ${quoteIdent(`${table}_idx_scope_a_uid`)} ON ${t}(scope, a_uid)`,
1719
- `CREATE INDEX IF NOT EXISTS ${quoteIdent(`${table}_idx_scope_b_uid`)} ON ${t}(scope, b_uid)`,
1720
- `CREATE INDEX IF NOT EXISTS ${quoteIdent(`${table}_idx_scope_axb_type_b_uid`)} ON ${t}(scope, axb_type, b_uid)`,
1721
- `CREATE INDEX IF NOT EXISTS ${quoteIdent(`${table}_idx_scope_a_type`)} ON ${t}(scope, a_type)`,
1722
- `CREATE INDEX IF NOT EXISTS ${quoteIdent(`${table}_idx_scope_b_type`)} ON ${t}(scope, b_type)`,
1723
- `CREATE INDEX IF NOT EXISTS ${quoteIdent(`${table}_idx_doc_id`)} ON ${t}(doc_id)`
1724
- ];
1725
- }
1726
- function quoteIdent(name) {
1727
- validateTableName(name);
1728
- return `"${name}"`;
1729
- }
1730
- function validateTableName(name) {
1731
- if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
1732
- throw new Error(`Invalid SQL identifier: ${name}. Must match /^[A-Za-z_][A-Za-z0-9_]*$/.`);
1946
+ async findNodes(params) {
1947
+ const plan = buildNodeQueryPlan(params);
1948
+ let records;
1949
+ if (plan.strategy === "get") {
1950
+ const record = await this.backend.getDoc(plan.docId);
1951
+ records = record ? [record] : [];
1952
+ } else {
1953
+ this.checkQuerySafety(plan.filters, params.allowCollectionScan);
1954
+ records = await this.backend.query(plan.filters, plan.options);
1955
+ }
1956
+ return this.applyMigrations(records);
1733
1957
  }
1734
- }
1735
-
1736
- // src/internal/sqlite-sql.ts
1737
- function compileFieldRef(field) {
1738
- const column = FIELD_TO_COLUMN[field];
1739
- if (column) {
1740
- return { expr: quoteIdent(column) };
1958
+ // ---------------------------------------------------------------------------
1959
+ // GraphWriter
1960
+ // ---------------------------------------------------------------------------
1961
+ async putNode(aType, uid, data) {
1962
+ const registry = this.getRegistryForType(aType);
1963
+ if (registry) {
1964
+ registry.validate(aType, NODE_RELATION, aType, data, this.backend.scopePath);
1965
+ }
1966
+ const backend = this.getBackendForType(aType);
1967
+ const docId = computeNodeDocId(uid);
1968
+ const record = buildWritableNodeRecord3(aType, uid, data);
1969
+ if (registry) {
1970
+ const entry = registry.lookup(aType, NODE_RELATION, aType);
1971
+ if (entry?.schemaVersion && entry.schemaVersion > 0) {
1972
+ record.v = entry.schemaVersion;
1973
+ }
1974
+ }
1975
+ await backend.setDoc(docId, record);
1741
1976
  }
1742
- if (field.startsWith("data.")) {
1743
- const key = field.slice(5);
1744
- validateJsonPathKey(key);
1745
- return { expr: 'json_extract("data", ?)', pathParam: `$.${key}` };
1977
+ async putEdge(aType, aUid, axbType, bType, bUid, data) {
1978
+ const registry = this.getRegistryForType(aType);
1979
+ if (registry) {
1980
+ registry.validate(aType, axbType, bType, data, this.backend.scopePath);
1981
+ }
1982
+ const backend = this.getBackendForType(aType);
1983
+ const docId = computeEdgeDocId(aUid, axbType, bUid);
1984
+ const record = buildWritableEdgeRecord3(aType, aUid, axbType, bType, bUid, data);
1985
+ if (registry) {
1986
+ const entry = registry.lookup(aType, axbType, bType);
1987
+ if (entry?.schemaVersion && entry.schemaVersion > 0) {
1988
+ record.v = entry.schemaVersion;
1989
+ }
1990
+ }
1991
+ await backend.setDoc(docId, record);
1746
1992
  }
1747
- if (field === "data") {
1748
- return { expr: 'json_extract("data", ?)', pathParam: "$" };
1993
+ async updateNode(uid, data) {
1994
+ const docId = computeNodeDocId(uid);
1995
+ await this.backend.updateDoc(docId, { dataFields: data });
1749
1996
  }
1750
- throw new FiregraphError(`SQLite backend cannot resolve filter field: ${field}`, "INVALID_QUERY");
1751
- }
1752
- var FIRESTORE_TYPE_NAMES = /* @__PURE__ */ new Set([
1753
- "Timestamp",
1754
- "GeoPoint",
1755
- "VectorValue",
1756
- "DocumentReference",
1757
- "FieldValue"
1758
- ]);
1759
- function isFirestoreSpecialType(value) {
1760
- const ctorName = value.constructor?.name;
1761
- if (ctorName && FIRESTORE_TYPE_NAMES.has(ctorName)) return ctorName;
1762
- return null;
1763
- }
1764
- function bindValue(value) {
1765
- if (value === null || value === void 0) return null;
1766
- if (typeof value === "string" || typeof value === "number" || typeof value === "bigint" || typeof value === "boolean") {
1767
- return value;
1997
+ async removeNode(uid) {
1998
+ const docId = computeNodeDocId(uid);
1999
+ await this.backend.deleteDoc(docId);
1768
2000
  }
1769
- if (value instanceof Date) return value.getTime();
1770
- if (typeof value === "object") {
1771
- const firestoreType = isFirestoreSpecialType(value);
1772
- if (firestoreType) {
1773
- throw new FiregraphError(
1774
- `SQLite backend cannot bind a Firestore ${firestoreType} value \u2014 JSON serialization would silently drop fields and the resulting bind would never match a stored row. Convert to a primitive (e.g. \`ts.toMillis()\` for Timestamp) before filtering or updating.`,
1775
- "INVALID_QUERY"
1776
- );
1777
- }
1778
- return JSON.stringify(value);
2001
+ async removeEdge(aUid, axbType, bUid) {
2002
+ const docId = computeEdgeDocId(aUid, axbType, bUid);
2003
+ await this.backend.deleteDoc(docId);
1779
2004
  }
1780
- return String(value);
1781
- }
1782
- var JSON_PATH_KEY_RE = /^[A-Za-z_][A-Za-z0-9_-]*$/;
1783
- function validateJsonPathKey(key) {
1784
- if (key.length === 0) {
1785
- throw new FiregraphError(
1786
- "SQLite backend: empty JSON path component is not allowed",
1787
- "INVALID_QUERY"
1788
- );
2005
+ // ---------------------------------------------------------------------------
2006
+ // Transactions & Batches
2007
+ // ---------------------------------------------------------------------------
2008
+ async runTransaction(fn) {
2009
+ return this.backend.runTransaction(async (txBackend) => {
2010
+ const graphTx = new GraphTransactionImpl(
2011
+ txBackend,
2012
+ this.getCombinedRegistry(),
2013
+ this.scanProtection,
2014
+ this.backend.scopePath,
2015
+ this.globalWriteBack
2016
+ );
2017
+ return fn(graphTx);
2018
+ });
1789
2019
  }
1790
- if (!JSON_PATH_KEY_RE.test(key)) {
1791
- throw new FiregraphError(
1792
- `SQLite backend: data field path component "${key}" is not a safe JSON-path identifier. Allowed pattern: /^[A-Za-z_][A-Za-z0-9_-]*$/. Use replaceData (full-data overwrite) for keys with reserved characters (whitespace, dots, brackets, quotes, etc.).`,
1793
- "INVALID_QUERY"
2020
+ batch() {
2021
+ return new GraphBatchImpl(
2022
+ this.backend.createBatch(),
2023
+ this.getCombinedRegistry(),
2024
+ this.backend.scopePath
1794
2025
  );
1795
2026
  }
1796
- }
1797
- function compileFilter(filter, params) {
1798
- const { expr, pathParam } = compileFieldRef(filter.field);
1799
- if (pathParam !== void 0) params.push(pathParam);
1800
- switch (filter.op) {
1801
- case "==":
1802
- params.push(bindValue(filter.value));
1803
- return `${expr} = ?`;
1804
- case "!=":
1805
- params.push(bindValue(filter.value));
1806
- return `${expr} != ?`;
1807
- case "<":
1808
- params.push(bindValue(filter.value));
1809
- return `${expr} < ?`;
1810
- case "<=":
1811
- params.push(bindValue(filter.value));
1812
- return `${expr} <= ?`;
1813
- case ">":
1814
- params.push(bindValue(filter.value));
1815
- return `${expr} > ?`;
1816
- case ">=":
1817
- params.push(bindValue(filter.value));
1818
- return `${expr} >= ?`;
1819
- case "in": {
1820
- const values = asArray(filter.value, "in");
1821
- const placeholders = values.map(() => "?").join(", ");
1822
- for (const v of values) params.push(bindValue(v));
1823
- return `${expr} IN (${placeholders})`;
1824
- }
1825
- case "not-in": {
1826
- const values = asArray(filter.value, "not-in");
1827
- const placeholders = values.map(() => "?").join(", ");
1828
- for (const v of values) params.push(bindValue(v));
1829
- return `${expr} NOT IN (${placeholders})`;
2027
+ // ---------------------------------------------------------------------------
2028
+ // Subgraph
2029
+ // ---------------------------------------------------------------------------
2030
+ subgraph(parentNodeUid, name = "graph") {
2031
+ if (!parentNodeUid || parentNodeUid.includes("/")) {
2032
+ throw new FiregraphError(
2033
+ `Invalid parentNodeUid for subgraph: "${parentNodeUid}". Must be a non-empty string without "/".`,
2034
+ "INVALID_SUBGRAPH"
2035
+ );
1830
2036
  }
1831
- case "array-contains": {
1832
- params.push(bindValue(filter.value));
1833
- return `EXISTS (SELECT 1 FROM json_each(${expr}) WHERE value = ?)`;
2037
+ if (name.includes("/")) {
2038
+ throw new FiregraphError(
2039
+ `Subgraph name must not contain "/": got "${name}". Use chained .subgraph() calls for nested subgraphs.`,
2040
+ "INVALID_SUBGRAPH"
2041
+ );
1834
2042
  }
1835
- case "array-contains-any": {
1836
- const values = asArray(filter.value, "array-contains-any");
1837
- const placeholders = values.map(() => "?").join(", ");
1838
- for (const v of values) params.push(bindValue(v));
1839
- return `EXISTS (SELECT 1 FROM json_each(${expr}) WHERE value IN (${placeholders}))`;
2043
+ const childBackend = this.backend.subgraph(parentNodeUid, name);
2044
+ return new _GraphClientImpl(
2045
+ childBackend,
2046
+ {
2047
+ registry: this.getCombinedRegistry(),
2048
+ scanProtection: this.scanProtection,
2049
+ migrationWriteBack: this.globalWriteBack,
2050
+ migrationSandbox: this.migrationSandbox
2051
+ }
2052
+ // Subgraphs do not have meta-backends; meta lives only at the root.
2053
+ );
2054
+ }
2055
+ // ---------------------------------------------------------------------------
2056
+ // Collection group query
2057
+ // ---------------------------------------------------------------------------
2058
+ async findEdgesGlobal(params, collectionName) {
2059
+ if (!this.backend.findEdgesGlobal) {
2060
+ throw new FiregraphError(
2061
+ "findEdgesGlobal() is not supported by the current storage backend.",
2062
+ "UNSUPPORTED_OPERATION"
2063
+ );
1840
2064
  }
1841
- default:
2065
+ const plan = buildEdgeQueryPlan(params);
2066
+ if (plan.strategy === "get") {
1842
2067
  throw new FiregraphError(
1843
- `SQLite backend does not support filter operator: ${String(filter.op)}`,
2068
+ "findEdgesGlobal() requires a query, not a direct document lookup. Omit one of aUid/axbType/bUid to force a query strategy.",
1844
2069
  "INVALID_QUERY"
1845
2070
  );
2071
+ }
2072
+ this.checkQuerySafety(plan.filters, params.allowCollectionScan);
2073
+ const records = await this.backend.findEdgesGlobal(params, collectionName);
2074
+ return this.applyMigrations(records);
1846
2075
  }
1847
- }
1848
- function asArray(value, op) {
1849
- if (!Array.isArray(value) || value.length === 0) {
1850
- throw new FiregraphError(`Operator "${op}" requires a non-empty array value`, "INVALID_QUERY");
1851
- }
1852
- return value;
1853
- }
1854
- function compileOrderBy(options, params) {
1855
- if (!options?.orderBy) return "";
1856
- const { field, direction } = options.orderBy;
1857
- const { expr, pathParam } = compileFieldRef(field);
1858
- if (pathParam !== void 0) params.push(pathParam);
1859
- const dir = direction === "desc" ? "DESC" : "ASC";
1860
- return ` ORDER BY ${expr} ${dir}`;
1861
- }
1862
- function compileLimit(options, params) {
1863
- if (options?.limit === void 0) return "";
1864
- params.push(options.limit);
1865
- return ` LIMIT ?`;
1866
- }
1867
- function compileSelect(table, scope, filters, options) {
1868
- const params = [];
1869
- const conditions = ['"scope" = ?'];
1870
- params.push(scope);
1871
- for (const f of filters) {
1872
- conditions.push(compileFilter(f, params));
2076
+ // ---------------------------------------------------------------------------
2077
+ // Bulk operations
2078
+ // ---------------------------------------------------------------------------
2079
+ async removeNodeCascade(uid, options) {
2080
+ return this.backend.removeNodeCascade(uid, this, options);
1873
2081
  }
1874
- let sql = `SELECT * FROM ${quoteIdent(table)} WHERE ${conditions.join(" AND ")}`;
1875
- const orderClause = compileOrderBy(options, params);
1876
- sql += orderClause;
1877
- sql += compileLimit(options, params);
1878
- return { sql, params };
1879
- }
1880
- function compileSelectGlobal(table, filters, options, scopeNameFilter) {
1881
- if (filters.length === 0) {
1882
- throw new FiregraphError(
1883
- "compileSelectGlobal requires at least one filter \u2014 refusing to issue an unbounded SELECT.",
1884
- "INVALID_QUERY"
1885
- );
2082
+ async bulkRemoveEdges(params, options) {
2083
+ return this.backend.bulkRemoveEdges(params, this, options);
1886
2084
  }
1887
- const params = [];
1888
- const conditions = [];
1889
- if (scopeNameFilter) {
1890
- if (scopeNameFilter.isRoot) {
1891
- conditions.push(`"scope" = ?`);
1892
- params.push("");
1893
- } else {
1894
- conditions.push(`"scope" LIKE ? ESCAPE '\\'`);
1895
- params.push(`%/${escapeLike(scopeNameFilter.name)}`);
2085
+ // ---------------------------------------------------------------------------
2086
+ // Dynamic registry methods
2087
+ // ---------------------------------------------------------------------------
2088
+ async defineNodeType(name, jsonSchema, description, options) {
2089
+ if (!this.dynamicConfig) {
2090
+ throw new DynamicRegistryError(
2091
+ 'defineNodeType() is only available in dynamic registry mode. Pass registryMode: { mode: "dynamic" } to createGraphClient().'
2092
+ );
1896
2093
  }
2094
+ if (RESERVED_TYPE_NAMES.has(name)) {
2095
+ throw new DynamicRegistryError(
2096
+ `Cannot define type "${name}": this name is reserved for the meta-registry.`
2097
+ );
2098
+ }
2099
+ if (this.staticRegistry?.lookup(name, NODE_RELATION, name)) {
2100
+ throw new DynamicRegistryError(
2101
+ `Cannot define node type "${name}": already defined in the static registry.`
2102
+ );
2103
+ }
2104
+ const uid = generateDeterministicUid(META_NODE_TYPE, name);
2105
+ const data = { name, jsonSchema };
2106
+ if (description !== void 0) data.description = description;
2107
+ if (options?.titleField !== void 0) data.titleField = options.titleField;
2108
+ if (options?.subtitleField !== void 0) data.subtitleField = options.subtitleField;
2109
+ if (options?.viewTemplate !== void 0) data.viewTemplate = options.viewTemplate;
2110
+ if (options?.viewCss !== void 0) data.viewCss = options.viewCss;
2111
+ if (options?.allowedIn !== void 0) data.allowedIn = options.allowedIn;
2112
+ if (options?.migrationWriteBack !== void 0)
2113
+ data.migrationWriteBack = options.migrationWriteBack;
2114
+ if (options?.migrations !== void 0) {
2115
+ data.migrations = await this.serializeMigrations(options.migrations);
2116
+ }
2117
+ await this.putNode(META_NODE_TYPE, uid, data);
1897
2118
  }
1898
- for (const f of filters) {
1899
- conditions.push(compileFilter(f, params));
1900
- }
1901
- const sql = `SELECT * FROM ${quoteIdent(table)} WHERE ${conditions.join(" AND ")}` + compileOrderBy(options, params) + compileLimit(options, params);
1902
- return { sql, params };
1903
- }
1904
- function compileSelectByDocId(table, scope, docId) {
1905
- return {
1906
- sql: `SELECT * FROM ${quoteIdent(table)} WHERE "scope" = ? AND "doc_id" = ? LIMIT 1`,
1907
- params: [scope, docId]
1908
- };
1909
- }
1910
- function compileSet(table, scope, docId, record, nowMillis) {
1911
- const sql = `INSERT OR REPLACE INTO ${quoteIdent(table)} (
1912
- doc_id, scope, a_type, a_uid, axb_type, b_type, b_uid, data, v, created_at, updated_at
1913
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;
1914
- const params = [
1915
- docId,
1916
- scope,
1917
- record.aType,
1918
- record.aUid,
1919
- record.axbType,
1920
- record.bType,
1921
- record.bUid,
1922
- JSON.stringify(record.data ?? {}),
1923
- record.v ?? null,
1924
- nowMillis,
1925
- nowMillis
1926
- ];
1927
- return { sql, params };
1928
- }
1929
- function compileUpdate(table, scope, docId, update, nowMillis) {
1930
- const setClauses = [];
1931
- const params = [];
1932
- if (update.replaceData) {
1933
- setClauses.push(`"data" = ?`);
1934
- params.push(JSON.stringify(update.replaceData));
1935
- } else if (update.dataFields && Object.keys(update.dataFields).length > 0) {
1936
- const entries = Object.entries(update.dataFields);
1937
- const pathArgs = entries.map(() => `?, ?`).join(", ");
1938
- setClauses.push(`"data" = json_set(COALESCE("data", '{}'), ${pathArgs})`);
1939
- for (const [k, v] of entries) {
1940
- validateJsonPathKey(k);
1941
- params.push(`$.${k}`);
1942
- params.push(bindValue(v));
2119
+ async defineEdgeType(name, topology, jsonSchema, description, options) {
2120
+ if (!this.dynamicConfig) {
2121
+ throw new DynamicRegistryError(
2122
+ 'defineEdgeType() is only available in dynamic registry mode. Pass registryMode: { mode: "dynamic" } to createGraphClient().'
2123
+ );
2124
+ }
2125
+ if (RESERVED_TYPE_NAMES.has(name)) {
2126
+ throw new DynamicRegistryError(
2127
+ `Cannot define type "${name}": this name is reserved for the meta-registry.`
2128
+ );
2129
+ }
2130
+ if (this.staticRegistry) {
2131
+ const fromTypes = Array.isArray(topology.from) ? topology.from : [topology.from];
2132
+ const toTypes = Array.isArray(topology.to) ? topology.to : [topology.to];
2133
+ for (const aType of fromTypes) {
2134
+ for (const bType of toTypes) {
2135
+ if (this.staticRegistry.lookup(aType, name, bType)) {
2136
+ throw new DynamicRegistryError(
2137
+ `Cannot define edge type "${name}" for (${aType}) -> (${bType}): already defined in the static registry.`
2138
+ );
2139
+ }
2140
+ }
2141
+ }
2142
+ }
2143
+ const uid = generateDeterministicUid(META_EDGE_TYPE, name);
2144
+ const data = {
2145
+ name,
2146
+ from: topology.from,
2147
+ to: topology.to
2148
+ };
2149
+ if (jsonSchema !== void 0) data.jsonSchema = jsonSchema;
2150
+ if (topology.inverseLabel !== void 0) data.inverseLabel = topology.inverseLabel;
2151
+ if (topology.targetGraph !== void 0) data.targetGraph = topology.targetGraph;
2152
+ if (description !== void 0) data.description = description;
2153
+ if (options?.titleField !== void 0) data.titleField = options.titleField;
2154
+ if (options?.subtitleField !== void 0) data.subtitleField = options.subtitleField;
2155
+ if (options?.viewTemplate !== void 0) data.viewTemplate = options.viewTemplate;
2156
+ if (options?.viewCss !== void 0) data.viewCss = options.viewCss;
2157
+ if (options?.allowedIn !== void 0) data.allowedIn = options.allowedIn;
2158
+ if (options?.migrationWriteBack !== void 0)
2159
+ data.migrationWriteBack = options.migrationWriteBack;
2160
+ if (options?.migrations !== void 0) {
2161
+ data.migrations = await this.serializeMigrations(options.migrations);
1943
2162
  }
2163
+ await this.putNode(META_EDGE_TYPE, uid, data);
1944
2164
  }
1945
- if (update.v !== void 0) {
1946
- setClauses.push(`"v" = ?`);
1947
- params.push(update.v);
2165
+ async reloadRegistry() {
2166
+ if (!this.dynamicConfig) {
2167
+ throw new DynamicRegistryError(
2168
+ 'reloadRegistry() is only available in dynamic registry mode. Pass registryMode: { mode: "dynamic" } to createGraphClient().'
2169
+ );
2170
+ }
2171
+ const reader = this.createMetaReader();
2172
+ const dynamicOnly = await createRegistryFromGraph(reader, this.migrationSandbox);
2173
+ if (this.staticRegistry) {
2174
+ this.dynamicRegistry = createMergedRegistry(this.staticRegistry, dynamicOnly);
2175
+ } else {
2176
+ this.dynamicRegistry = dynamicOnly;
2177
+ }
1948
2178
  }
1949
- setClauses.push(`"updated_at" = ?`);
1950
- params.push(nowMillis);
1951
- params.push(scope, docId);
1952
- return {
1953
- sql: `UPDATE ${quoteIdent(table)} SET ${setClauses.join(", ")} WHERE "scope" = ? AND "doc_id" = ?`,
1954
- params
1955
- };
1956
- }
1957
- function compileDelete(table, scope, docId) {
1958
- return {
1959
- sql: `DELETE FROM ${quoteIdent(table)} WHERE "scope" = ? AND "doc_id" = ?`,
1960
- params: [scope, docId]
1961
- };
1962
- }
1963
- function compileDeleteScopePrefix(table, scopePrefix) {
1964
- const escaped = escapeLike(scopePrefix);
1965
- return {
1966
- sql: `DELETE FROM ${quoteIdent(table)} WHERE "scope" LIKE ? ESCAPE '\\'`,
1967
- params: [`${escaped}/%`]
1968
- };
1969
- }
1970
- function compileCountScopePrefix(table, scopePrefix) {
1971
- const escaped = escapeLike(scopePrefix);
1972
- return {
1973
- sql: `SELECT COUNT(*) AS n FROM ${quoteIdent(table)} WHERE "scope" LIKE ? ESCAPE '\\'`,
1974
- params: [`${escaped}/%`]
1975
- };
1976
- }
1977
- function escapeLike(value) {
1978
- return value.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
1979
- }
1980
- function rowToRecord(row) {
1981
- const dataString = row.data;
1982
- const data = dataString ? JSON.parse(dataString) : {};
1983
- const createdMs = toMillis(row.created_at);
1984
- const updatedMs = toMillis(row.updated_at);
1985
- const record = {
1986
- aType: row.a_type,
1987
- aUid: row.a_uid,
1988
- axbType: row.axb_type,
1989
- bType: row.b_type,
1990
- bUid: row.b_uid,
1991
- data,
1992
- createdAt: GraphTimestampImpl.fromMillis(createdMs),
1993
- updatedAt: GraphTimestampImpl.fromMillis(updatedMs)
1994
- };
1995
- if (row.v !== null && row.v !== void 0) {
1996
- record.v = Number(row.v);
2179
+ async serializeMigrations(migrations) {
2180
+ const result = migrations.map((m) => {
2181
+ const source = typeof m.up === "function" ? m.up.toString() : m.up;
2182
+ return { fromVersion: m.fromVersion, toVersion: m.toVersion, up: source };
2183
+ });
2184
+ await Promise.all(result.map((m) => precompileSource(m.up, this.migrationSandbox)));
2185
+ return result;
1997
2186
  }
1998
- return record;
1999
- }
2000
- function toMillis(value) {
2001
- if (typeof value === "number") return value;
2002
- if (typeof value === "bigint") return Number(value);
2003
- if (typeof value === "string") return Number(value);
2004
- return 0;
2187
+ /**
2188
+ * Build a `GraphReader` over the meta-backend. If meta lives in the same
2189
+ * collection as the main backend, `this` is returned directly.
2190
+ */
2191
+ createMetaReader() {
2192
+ if (!this.metaBackend) return this;
2193
+ const backend = this.metaBackend;
2194
+ const executeMetaQuery = (filters, options) => backend.query(filters, options);
2195
+ return {
2196
+ async getNode(uid) {
2197
+ return backend.getDoc(computeNodeDocId(uid));
2198
+ },
2199
+ async getEdge(aUid, axbType, bUid) {
2200
+ return backend.getDoc(computeEdgeDocId(aUid, axbType, bUid));
2201
+ },
2202
+ async edgeExists(aUid, axbType, bUid) {
2203
+ const record = await backend.getDoc(computeEdgeDocId(aUid, axbType, bUid));
2204
+ return record !== null;
2205
+ },
2206
+ async findEdges(params) {
2207
+ const plan = buildEdgeQueryPlan(params);
2208
+ if (plan.strategy === "get") {
2209
+ const record = await backend.getDoc(plan.docId);
2210
+ return record ? [record] : [];
2211
+ }
2212
+ return executeMetaQuery(plan.filters, plan.options);
2213
+ },
2214
+ async findNodes(params) {
2215
+ const plan = buildNodeQueryPlan(params);
2216
+ if (plan.strategy === "get") {
2217
+ const record = await backend.getDoc(plan.docId);
2218
+ return record ? [record] : [];
2219
+ }
2220
+ return executeMetaQuery(plan.filters, plan.options);
2221
+ }
2222
+ };
2223
+ }
2224
+ };
2225
+ function createGraphClientFromBackend(backend, options, metaBackend) {
2226
+ return new GraphClientImpl(backend, options, metaBackend);
2005
2227
  }
2006
2228
 
2007
- // src/internal/sqlite-backend.ts
2008
- var DEFAULT_MAX_RETRIES = 3;
2009
- var BASE_RETRY_DELAY_MS = 200;
2010
- var MAX_RETRY_DELAY_MS = 5e3;
2011
- function sleep(ms) {
2012
- return new Promise((resolve) => setTimeout(resolve, ms));
2013
- }
2014
- function minDefined(a, b) {
2015
- if (a === void 0) return b;
2016
- if (b === void 0) return a;
2017
- return Math.min(a, b);
2018
- }
2019
- function chunkStatements(statements, maxStatements, maxParams) {
2020
- const stmtCap = maxStatements && maxStatements > 0 && Number.isFinite(maxStatements) ? Math.floor(maxStatements) : Infinity;
2021
- const paramCap = maxParams && maxParams > 0 && Number.isFinite(maxParams) ? Math.floor(maxParams) : Infinity;
2022
- if (stmtCap === Infinity && paramCap === Infinity) {
2023
- return [statements];
2024
- }
2025
- const chunks = [];
2026
- let current = [];
2027
- let currentParamCount = 0;
2028
- for (const stmt of statements) {
2029
- const stmtParams = stmt.params.length;
2030
- const wouldExceedStmt = current.length + 1 > stmtCap;
2031
- const wouldExceedParam = currentParamCount + stmtParams > paramCap;
2032
- if (current.length > 0 && (wouldExceedStmt || wouldExceedParam)) {
2033
- chunks.push(current);
2034
- current = [];
2035
- currentParamCount = 0;
2036
- }
2037
- current.push(stmt);
2038
- currentParamCount += stmtParams;
2039
- }
2040
- if (current.length > 0) chunks.push(current);
2041
- return chunks;
2042
- }
2043
- var SqliteTransactionBackendImpl = class {
2044
- constructor(tx, tableName, storageScope) {
2045
- this.tx = tx;
2046
- this.tableName = tableName;
2047
- this.storageScope = storageScope;
2048
- }
2049
- async getDoc(docId) {
2050
- const stmt = compileSelectByDocId(this.tableName, this.storageScope, docId);
2051
- const rows = await this.tx.all(stmt.sql, stmt.params);
2052
- return rows.length === 0 ? null : rowToRecord(rows[0]);
2053
- }
2054
- async query(filters, options) {
2055
- const stmt = compileSelect(this.tableName, this.storageScope, filters, options);
2056
- const rows = await this.tx.all(stmt.sql, stmt.params);
2057
- return rows.map(rowToRecord);
2229
+ // src/cloudflare/client.ts
2230
+ function createDOClient(namespace, rootKey, options = {}) {
2231
+ if (!rootKey || typeof rootKey !== "string") {
2232
+ throw new FiregraphError(
2233
+ `createDOClient: rootKey must be a non-empty string, got ${JSON.stringify(rootKey)}.`,
2234
+ "INVALID_ARGUMENT"
2235
+ );
2058
2236
  }
2059
- async setDoc(docId, record) {
2060
- const stmt = compileSet(this.tableName, this.storageScope, docId, record, Date.now());
2061
- await this.tx.run(stmt.sql, stmt.params);
2237
+ if (rootKey.includes("/")) {
2238
+ throw new FiregraphError(
2239
+ `createDOClient: rootKey must not contain "/". Got: "${rootKey}".`,
2240
+ "INVALID_ARGUMENT"
2241
+ );
2062
2242
  }
2063
- async updateDoc(docId, update) {
2064
- const stmt = compileUpdate(this.tableName, this.storageScope, docId, update, Date.now());
2065
- const sqlWithReturning = `${stmt.sql} RETURNING "doc_id"`;
2066
- const rows = await this.tx.all(sqlWithReturning, stmt.params);
2067
- if (rows.length === 0) {
2243
+ let client;
2244
+ const registryAccessor = () => {
2245
+ if (!client) {
2068
2246
  throw new FiregraphError(
2069
- `updateDoc: no document found for doc_id=${docId} (scope=${this.storageScope})`,
2070
- "NOT_FOUND"
2247
+ "createDOClient: registryAccessor fired before the client was assigned. This indicates a programming error in the DO backend \u2014 the accessor must only be invoked lazily from `removeNodeCascade`, never synchronously from the `DORPCBackend` constructor.",
2248
+ "INTERNAL"
2071
2249
  );
2072
2250
  }
2251
+ return client.getRegistrySnapshot();
2252
+ };
2253
+ const siblingOptions = { ...options };
2254
+ const makeSiblingClient = (siblingRootKey) => createDOClient(namespace, siblingRootKey, siblingOptions);
2255
+ const backend = new DORPCBackend(namespace, {
2256
+ scopePath: "",
2257
+ storageKey: rootKey,
2258
+ registryAccessor,
2259
+ makeSiblingClient
2260
+ });
2261
+ let metaBackend;
2262
+ if (options.registryMode?.collection) {
2263
+ const metaKey = options.registryMode.collection;
2264
+ if (metaKey.includes("/")) {
2265
+ throw new FiregraphError(
2266
+ `createDOClient: registryMode.collection must not contain "/". Got: "${metaKey}".`,
2267
+ "INVALID_ARGUMENT"
2268
+ );
2269
+ }
2270
+ if (metaKey !== rootKey) {
2271
+ metaBackend = new DORPCBackend(namespace, {
2272
+ scopePath: "",
2273
+ storageKey: metaKey,
2274
+ // Meta backend shares the accessor so its own `removeNodeCascade`
2275
+ // (unlikely, but safe) would also see the live registry. Sibling
2276
+ // factory is carried for consistency; there's no user-facing path
2277
+ // that creates a sibling from the meta backend, but it costs
2278
+ // nothing to keep the two backends in sync.
2279
+ registryAccessor,
2280
+ makeSiblingClient
2281
+ });
2282
+ }
2073
2283
  }
2074
- async deleteDoc(docId) {
2075
- const stmt = compileDelete(this.tableName, this.storageScope, docId);
2076
- await this.tx.run(stmt.sql, stmt.params);
2077
- }
2078
- };
2079
- var SqliteBatchBackendImpl = class {
2080
- constructor(executor, tableName, storageScope) {
2081
- this.executor = executor;
2082
- this.tableName = tableName;
2083
- this.storageScope = storageScope;
2084
- }
2085
- statements = [];
2086
- setDoc(docId, record) {
2087
- this.statements.push(compileSet(this.tableName, this.storageScope, docId, record, Date.now()));
2088
- }
2089
- updateDoc(docId, update) {
2090
- this.statements.push(
2091
- compileUpdate(this.tableName, this.storageScope, docId, update, Date.now())
2284
+ client = createGraphClientFromBackend(backend, options, metaBackend);
2285
+ return client;
2286
+ }
2287
+ function createSiblingClient(client, siblingRootKey) {
2288
+ if (!siblingRootKey || typeof siblingRootKey !== "string") {
2289
+ throw new FiregraphError(
2290
+ `createSiblingClient: siblingRootKey must be a non-empty string, got ${JSON.stringify(siblingRootKey)}.`,
2291
+ "INVALID_ARGUMENT"
2092
2292
  );
2093
2293
  }
2094
- deleteDoc(docId) {
2095
- this.statements.push(compileDelete(this.tableName, this.storageScope, docId));
2294
+ if (siblingRootKey.includes("/")) {
2295
+ throw new FiregraphError(
2296
+ `createSiblingClient: siblingRootKey must not contain "/". Got: "${siblingRootKey}".`,
2297
+ "INVALID_ARGUMENT"
2298
+ );
2096
2299
  }
2097
- async commit() {
2098
- if (this.statements.length === 0) return;
2099
- await this.executor.batch(this.statements);
2100
- this.statements.length = 0;
2300
+ const impl = client;
2301
+ const backend = typeof impl.getBackend === "function" ? impl.getBackend() : void 0;
2302
+ const maker = backend && backend.makeSiblingClient;
2303
+ if (typeof maker !== "function") {
2304
+ throw new FiregraphError(
2305
+ "createSiblingClient: the provided client is not backed by a DO client produced by `createDOClient`. Sibling construction is only available for DO-backed clients.",
2306
+ "UNSUPPORTED_OPERATION"
2307
+ );
2101
2308
  }
2309
+ return maker(siblingRootKey);
2310
+ }
2311
+
2312
+ // src/cloudflare/do.ts
2313
+ var DEFAULT_OPTIONS = {
2314
+ table: "firegraph",
2315
+ autoMigrate: true
2102
2316
  };
2103
- var SqliteBackendImpl = class _SqliteBackendImpl {
2104
- constructor(executor, tableName, storageScope, scopePath) {
2105
- this.executor = executor;
2106
- this.collectionPath = tableName;
2107
- this.storageScope = storageScope;
2108
- this.scopePath = scopePath;
2317
+ var FiregraphDO = class {
2318
+ /** @internal — exposed for subclass access, not part of the public RPC. */
2319
+ ctx;
2320
+ /** @internal — exposed for subclass access; opaque to this class. */
2321
+ env;
2322
+ /** @internal — table name used by every compiled statement. */
2323
+ table;
2324
+ constructor(ctx, env, options = {}) {
2325
+ this.ctx = ctx;
2326
+ this.env = env;
2327
+ const table = options.table ?? DEFAULT_OPTIONS.table;
2328
+ validateDOTableName(table);
2329
+ this.table = table;
2330
+ const autoMigrate = options.autoMigrate ?? DEFAULT_OPTIONS.autoMigrate;
2331
+ if (autoMigrate) {
2332
+ void this.ctx.blockConcurrencyWhile(async () => {
2333
+ this.runSchema();
2334
+ });
2335
+ }
2109
2336
  }
2110
- /** Logical table name (returned through `collectionPath` for parity with Firestore). */
2111
- collectionPath;
2112
- scopePath;
2113
- /** Materialized storage scope (interleaved parent UIDs + subgraph names). */
2114
- storageScope;
2115
- // --- Reads ---
2116
- async getDoc(docId) {
2117
- const stmt = compileSelectByDocId(this.collectionPath, this.storageScope, docId);
2118
- const rows = await this.executor.all(stmt.sql, stmt.params);
2119
- return rows.length === 0 ? null : rowToRecord(rows[0]);
2337
+ // ---------------------------------------------------------------------------
2338
+ // RPC: reads
2339
+ //
2340
+ // Method names are prefixed `_fg` so user subclasses can add their own RPC
2341
+ // methods without name collisions. The client-side backend in
2342
+ // `src/cloudflare/backend.ts` calls these directly on the DO stub.
2343
+ // ---------------------------------------------------------------------------
2344
+ async _fgGetDoc(docId) {
2345
+ const stmt = compileDOSelectByDocId(this.table, docId);
2346
+ const rows = this.execAll(stmt);
2347
+ return rows.length === 0 ? null : rowToDORecord(rows[0]);
2120
2348
  }
2121
- async query(filters, options) {
2122
- const stmt = compileSelect(this.collectionPath, this.storageScope, filters, options);
2123
- const rows = await this.executor.all(stmt.sql, stmt.params);
2124
- return rows.map(rowToRecord);
2349
+ async _fgQuery(filters, options) {
2350
+ const stmt = compileDOSelect(this.table, filters, options);
2351
+ const rows = this.execAll(stmt);
2352
+ return rows.map(rowToDORecord);
2125
2353
  }
2126
- // --- Writes ---
2127
- async setDoc(docId, record) {
2128
- const stmt = compileSet(this.collectionPath, this.storageScope, docId, record, Date.now());
2129
- await this.executor.run(stmt.sql, stmt.params);
2354
+ // ---------------------------------------------------------------------------
2355
+ // RPC: writes
2356
+ // ---------------------------------------------------------------------------
2357
+ async _fgSetDoc(docId, record) {
2358
+ const stmt = compileDOSet(this.table, docId, record, Date.now());
2359
+ this.execRun(stmt);
2130
2360
  }
2131
- async updateDoc(docId, update) {
2132
- const stmt = compileUpdate(this.collectionPath, this.storageScope, docId, update, Date.now());
2361
+ async _fgUpdateDoc(docId, update) {
2362
+ const stmt = compileDOUpdate(this.table, docId, update, Date.now());
2133
2363
  const sqlWithReturning = `${stmt.sql} RETURNING "doc_id"`;
2134
- const rows = await this.executor.all(sqlWithReturning, stmt.params);
2364
+ const rows = this.ctx.storage.sql.exec(sqlWithReturning, ...stmt.params).toArray();
2135
2365
  if (rows.length === 0) {
2136
- throw new FiregraphError(
2137
- `updateDoc: no document found for doc_id=${docId} (scope=${this.storageScope})`,
2138
- "NOT_FOUND"
2139
- );
2366
+ throw new FiregraphError(`updateDoc: no document found for doc_id=${docId}`, "NOT_FOUND");
2140
2367
  }
2141
2368
  }
2142
- async deleteDoc(docId) {
2143
- const stmt = compileDelete(this.collectionPath, this.storageScope, docId);
2144
- await this.executor.run(stmt.sql, stmt.params);
2369
+ async _fgDeleteDoc(docId) {
2370
+ const stmt = compileDODelete(this.table, docId);
2371
+ this.execRun(stmt);
2145
2372
  }
2146
- // --- Transactions / Batches ---
2147
- async runTransaction(fn) {
2148
- if (!this.executor.transaction) {
2149
- throw new FiregraphError(
2150
- "Interactive transactions are not supported by this SQLite driver. D1 in particular has no read-then-conditional-write transactions; use a Durable Object SQLite client instead, or rewrite the code path as a batch().",
2151
- "UNSUPPORTED_OPERATION"
2152
- );
2153
- }
2154
- return this.executor.transaction(async (tx) => {
2155
- const txBackend = new SqliteTransactionBackendImpl(
2156
- tx,
2157
- this.collectionPath,
2158
- this.storageScope
2159
- );
2160
- return fn(txBackend);
2373
+ // ---------------------------------------------------------------------------
2374
+ // RPC: batch
2375
+ // ---------------------------------------------------------------------------
2376
+ /**
2377
+ * Execute a list of write ops atomically. DO SQLite's `transactionSync`
2378
+ * provides real atomicity — either every statement commits or none do.
2379
+ * No statement-count cap applies (contrast with D1's ~100-statement batch
2380
+ * limit), so the caller can submit as many ops as they like in one call.
2381
+ */
2382
+ async _fgBatch(ops) {
2383
+ if (ops.length === 0) return;
2384
+ const now = Date.now();
2385
+ const statements = ops.map((op) => {
2386
+ switch (op.kind) {
2387
+ case "set":
2388
+ return compileDOSet(this.table, op.docId, op.record, now);
2389
+ case "update":
2390
+ return compileDOUpdate(this.table, op.docId, op.update, now);
2391
+ case "delete":
2392
+ return compileDODelete(this.table, op.docId);
2393
+ }
2394
+ });
2395
+ this.ctx.storage.transactionSync(() => {
2396
+ for (const stmt of statements) {
2397
+ this.ctx.storage.sql.exec(stmt.sql, ...stmt.params).toArray();
2398
+ }
2161
2399
  });
2162
2400
  }
2163
- createBatch() {
2164
- return new SqliteBatchBackendImpl(this.executor, this.collectionPath, this.storageScope);
2165
- }
2166
- // --- Subgraphs ---
2167
- subgraph(parentNodeUid, name) {
2168
- if (!parentNodeUid || parentNodeUid.includes("/")) {
2169
- throw new FiregraphError(
2170
- `Invalid parentNodeUid for subgraph: "${parentNodeUid}". Must be a non-empty string without "/".`,
2171
- "INVALID_SUBGRAPH"
2172
- );
2173
- }
2174
- if (!name || name.includes("/")) {
2175
- throw new FiregraphError(
2176
- `Subgraph name must not contain "/" and must be non-empty: got "${name}". Use chained .subgraph() calls for nested subgraphs.`,
2177
- "INVALID_SUBGRAPH"
2178
- );
2179
- }
2180
- const newStorageScope = this.storageScope ? `${this.storageScope}/${parentNodeUid}/${name}` : `${parentNodeUid}/${name}`;
2181
- const newScope = this.scopePath ? `${this.scopePath}/${name}` : name;
2182
- return new _SqliteBackendImpl(this.executor, this.collectionPath, newStorageScope, newScope);
2183
- }
2184
- // --- Cascade & bulk ---
2185
- async removeNodeCascade(uid, reader, options) {
2186
- const [outgoingRaw, incomingRaw] = await Promise.all([
2187
- reader.findEdges({ aUid: uid, allowCollectionScan: true, limit: 0 }),
2188
- reader.findEdges({ bUid: uid, allowCollectionScan: true, limit: 0 })
2189
- ]);
2401
+ // ---------------------------------------------------------------------------
2402
+ // RPC: cascade + bulk (local DO only)
2403
+ //
2404
+ // These cascade *within this DO*. Subgraph DOs (nested under this node) are
2405
+ // not reachable from here — the client-side `DORPCBackend.removeNodeCascade`
2406
+ // consults the registry topology to discover descendant subgraph DOs and
2407
+ // fans out explicit `_fgDestroy` calls to each before invoking this method.
2408
+ // Without that topology the DO has no way to enumerate its children.
2409
+ // ---------------------------------------------------------------------------
2410
+ async _fgRemoveNodeCascade(uid) {
2411
+ const outgoingStmt = compileDOSelect(this.table, [{ field: "aUid", op: "==", value: uid }]);
2412
+ const incomingStmt = compileDOSelect(this.table, [{ field: "bUid", op: "==", value: uid }]);
2413
+ const outgoingRows = this.execAll(outgoingStmt);
2414
+ const incomingRows = this.execAll(incomingStmt);
2190
2415
  const seen = /* @__PURE__ */ new Set();
2191
2416
  const edgeDocIds = [];
2192
- for (const edge of [...outgoingRaw, ...incomingRaw]) {
2193
- if (edge.axbType === NODE_RELATION) continue;
2194
- const docId = computeEdgeDocId(edge.aUid, edge.axbType, edge.bUid);
2417
+ let nodeExists = false;
2418
+ for (const row of [...outgoingRows, ...incomingRows]) {
2419
+ const axbType = row.axb_type;
2420
+ const aUid = row.a_uid;
2421
+ const bUid = row.b_uid;
2422
+ if (axbType === NODE_RELATION && aUid === bUid) {
2423
+ nodeExists = true;
2424
+ continue;
2425
+ }
2426
+ const docId = computeEdgeDocId(aUid, axbType, bUid);
2195
2427
  if (!seen.has(docId)) {
2196
2428
  seen.add(docId);
2197
2429
  edgeDocIds.push(docId);
2198
2430
  }
2199
2431
  }
2200
- const nodeDocId = computeNodeDocId(uid);
2201
- const shouldDeleteSubgraphs = options?.deleteSubcollections !== false;
2202
- let subgraphRowCount = 0;
2203
- if (shouldDeleteSubgraphs) {
2204
- const prefix = this.storageScope ? `${this.storageScope}/${uid}` : uid;
2205
- const countStmt = compileCountScopePrefix(this.collectionPath, prefix);
2206
- const countRows = await this.executor.all(countStmt.sql, countStmt.params);
2207
- const first = countRows[0];
2208
- const n = first?.n;
2209
- subgraphRowCount = typeof n === "bigint" ? Number(n) : Number(n ?? 0);
2210
- }
2211
- const writeStatements = edgeDocIds.map(
2212
- (id) => compileDelete(this.collectionPath, this.storageScope, id)
2213
- );
2214
- writeStatements.push(compileDelete(this.collectionPath, this.storageScope, nodeDocId));
2215
- if (shouldDeleteSubgraphs) {
2216
- const prefix = this.storageScope ? `${this.storageScope}/${uid}` : uid;
2217
- writeStatements.push(compileDeleteScopePrefix(this.collectionPath, prefix));
2218
- }
2219
- const {
2220
- deleted: stmtDeleted,
2221
- batches,
2222
- errors
2223
- } = await this.executeChunkedBatches(writeStatements, options);
2224
- const allOk = errors.length === 0;
2225
- const edgesDeleted = allOk ? edgeDocIds.length : 0;
2226
- const nodeDeleted = allOk;
2227
- const prefixStatementContribution = shouldDeleteSubgraphs && allOk ? 1 : 0;
2228
- const deleted = stmtDeleted - prefixStatementContribution + (allOk ? subgraphRowCount : 0);
2229
- return { deleted, batches, errors, edgesDeleted, nodeDeleted };
2230
- }
2231
- async bulkRemoveEdges(params, reader, options) {
2232
- const effectiveParams = params.limit !== void 0 ? { ...params, allowCollectionScan: params.allowCollectionScan ?? true } : { ...params, limit: 0, allowCollectionScan: params.allowCollectionScan ?? true };
2233
- const edges = await reader.findEdges(effectiveParams);
2234
- const docIds = edges.map((e) => computeEdgeDocId(e.aUid, e.axbType, e.bUid));
2235
- if (docIds.length === 0) {
2236
- return { deleted: 0, batches: 0, errors: [] };
2432
+ const statements = edgeDocIds.map((id) => compileDODelete(this.table, id));
2433
+ if (nodeExists) {
2434
+ statements.push(compileDODelete(this.table, computeNodeDocId(uid)));
2237
2435
  }
2238
- const statements = docIds.map(
2239
- (id) => compileDelete(this.collectionPath, this.storageScope, id)
2240
- );
2241
- return this.executeChunkedBatches(statements, options);
2242
- }
2243
- /**
2244
- * Submit `statements` to the executor as one or more `batch()` calls,
2245
- * chunking by `executor.maxBatchSize` (e.g. D1's ~100-statement cap).
2246
- * Drivers that don't advertise a cap submit everything in one batch,
2247
- * preserving cross-batch atomicity.
2248
- *
2249
- * Each chunk is retried with exponential backoff up to `maxRetries`
2250
- * (default 3) before being recorded in `errors`. The loop continues past
2251
- * a permanently failed chunk so the caller still gets partial progress
2252
- * visibility — to halt on first failure, set `maxRetries: 0` and check
2253
- * `result.errors.length` after the call.
2254
- *
2255
- * Returns `BulkResult`-shaped fields. `deleted` reflects only the
2256
- * statement count of *successfully committed* batches — a prefix-delete
2257
- * statement contributes 1 to that total even though it may match many
2258
- * rows; `removeNodeCascade` patches that up with a pre-counted row total.
2259
- *
2260
- * **Atomicity caveat (D1):** when chunking kicks in, atomicity is lost
2261
- * across chunk boundaries — one chunk may commit while a later one fails.
2262
- * `removeNodeCascade` is idempotent (deleting the same docs again is a
2263
- * no-op) so a caller can simply retry on partial failure. `bulkRemoveEdges`
2264
- * is also idempotent for the same reason. DO SQLite leaves `maxBatchSize`
2265
- * unset, so everything funnels through one atomic `transactionSync` and
2266
- * this caveat does not apply.
2267
- */
2268
- async executeChunkedBatches(statements, options) {
2269
2436
  if (statements.length === 0) {
2270
- return { deleted: 0, batches: 0, errors: [] };
2437
+ return {
2438
+ deleted: 0,
2439
+ batches: 0,
2440
+ errors: [],
2441
+ edgesDeleted: 0,
2442
+ nodeDeleted: false
2443
+ };
2271
2444
  }
2272
- const maxRetries = options?.maxRetries ?? DEFAULT_MAX_RETRIES;
2273
- const callerBatchSize = options?.batchSize;
2274
- const stmtCap = minDefined(callerBatchSize, this.executor.maxBatchSize);
2275
- const chunks = chunkStatements(statements, stmtCap, this.executor.maxBatchParams);
2276
- const errors = [];
2277
- let deleted = 0;
2278
- let batches = 0;
2279
- const totalBatches = chunks.length;
2280
- const driverParamCap = this.executor.maxBatchParams;
2281
- for (let batchIndex = 0; batchIndex < chunks.length; batchIndex++) {
2282
- const chunk = chunks[batchIndex];
2283
- const isUnretriableOversize = chunk.length === 1 && driverParamCap !== void 0 && chunk[0].params.length > driverParamCap;
2284
- let committed = false;
2285
- let lastError = null;
2286
- const effectiveRetries = isUnretriableOversize ? 0 : maxRetries;
2287
- for (let attempt = 0; attempt <= effectiveRetries; attempt++) {
2288
- try {
2289
- await this.executor.batch(chunk);
2290
- committed = true;
2291
- break;
2292
- } catch (err) {
2293
- lastError = err instanceof Error ? err : new Error(String(err));
2294
- if (attempt < effectiveRetries) {
2295
- const delay = Math.min(BASE_RETRY_DELAY_MS * Math.pow(2, attempt), MAX_RETRY_DELAY_MS);
2296
- await sleep(delay);
2297
- }
2445
+ try {
2446
+ this.ctx.storage.transactionSync(() => {
2447
+ for (const stmt of statements) {
2448
+ this.ctx.storage.sql.exec(stmt.sql, ...stmt.params).toArray();
2298
2449
  }
2299
- }
2300
- if (committed) {
2301
- deleted += chunk.length;
2302
- batches += 1;
2303
- } else if (lastError) {
2304
- errors.push({
2305
- batchIndex,
2306
- error: lastError,
2307
- operationCount: chunk.length
2308
- });
2309
- }
2310
- if (options?.onProgress) {
2311
- options.onProgress({
2312
- completedBatches: batches,
2313
- totalBatches,
2314
- deletedSoFar: deleted
2315
- });
2316
- }
2450
+ });
2451
+ return {
2452
+ deleted: statements.length,
2453
+ batches: 1,
2454
+ errors: [],
2455
+ edgesDeleted: edgeDocIds.length,
2456
+ nodeDeleted: nodeExists
2457
+ };
2458
+ } catch (err) {
2459
+ const error = err instanceof Error ? err : new Error(String(err));
2460
+ return {
2461
+ deleted: 0,
2462
+ batches: 0,
2463
+ errors: [{ batchIndex: 0, error, operationCount: statements.length }],
2464
+ edgesDeleted: 0,
2465
+ nodeDeleted: false
2466
+ };
2317
2467
  }
2318
- return { deleted, batches, errors };
2319
2468
  }
2320
- // --- Cross-scope (collection group) ---
2321
- async findEdgesGlobal(params, collectionName) {
2469
+ async _fgBulkRemoveEdges(params, _options) {
2322
2470
  const plan = buildEdgeQueryPlan(params);
2471
+ let docIds;
2323
2472
  if (plan.strategy === "get") {
2324
- throw new FiregraphError(
2325
- "findEdgesGlobal() requires a query, not a direct document lookup. Omit one of aUid/axbType/bUid to force a query strategy.",
2326
- "INVALID_QUERY"
2473
+ const existsStmt = compileDOSelectByDocId(this.table, plan.docId);
2474
+ const rows = this.execAll(existsStmt);
2475
+ docIds = rows.length > 0 ? [plan.docId] : [];
2476
+ } else {
2477
+ const selectStmt = compileDOSelect(this.table, plan.filters, plan.options);
2478
+ const rows = this.execAll(selectStmt);
2479
+ docIds = rows.map(
2480
+ (row) => computeEdgeDocId(row.a_uid, row.axb_type, row.b_uid)
2327
2481
  );
2328
2482
  }
2329
- const name = collectionName ?? this.collectionPath;
2330
- const scopeNameFilter = {
2331
- name,
2332
- isRoot: name === this.collectionPath
2333
- };
2334
- const stmt = compileSelectGlobal(
2335
- this.collectionPath,
2336
- plan.filters,
2337
- plan.options,
2338
- scopeNameFilter
2339
- );
2340
- const rows = await this.executor.all(stmt.sql, stmt.params);
2341
- return rows.map(rowToRecord);
2342
- }
2343
- };
2344
- function createSqliteBackend(executor, tableName, options = {}) {
2345
- const storageScope = options.storageScope ?? "";
2346
- const scopePath = options.scopePath ?? "";
2347
- return new SqliteBackendImpl(executor, tableName, storageScope, scopePath);
2348
- }
2349
-
2350
- // src/do-sqlite.ts
2351
- var DOSqliteExecutor = class {
2352
- constructor(storage) {
2353
- this.storage = storage;
2354
- }
2355
- async all(sql, params) {
2356
- return this.storage.sql.exec(sql, ...params).toArray();
2357
- }
2358
- async run(sql, params) {
2359
- this.storage.sql.exec(sql, ...params).toArray();
2360
- }
2361
- async batch(statements) {
2362
- if (statements.length === 0) return;
2363
- this.storage.transactionSync(() => {
2364
- for (const s of statements) {
2365
- this.storage.sql.exec(s.sql, ...s.params).toArray();
2366
- }
2367
- });
2368
- }
2369
- async transaction(fn) {
2370
- this.storage.sql.exec("BEGIN IMMEDIATE").toArray();
2483
+ if (docIds.length === 0) {
2484
+ return { deleted: 0, batches: 0, errors: [] };
2485
+ }
2486
+ const deleteStmts = docIds.map((id) => compileDODelete(this.table, id));
2371
2487
  try {
2372
- const txExec = {
2373
- all: async (sql, params) => this.storage.sql.exec(sql, ...params).toArray(),
2374
- run: async (sql, params) => {
2375
- this.storage.sql.exec(sql, ...params).toArray();
2488
+ this.ctx.storage.transactionSync(() => {
2489
+ for (const stmt of deleteStmts) {
2490
+ this.ctx.storage.sql.exec(stmt.sql, ...stmt.params).toArray();
2376
2491
  }
2377
- };
2378
- const result = await fn(txExec);
2379
- this.storage.sql.exec("COMMIT").toArray();
2380
- return result;
2492
+ });
2493
+ return { deleted: deleteStmts.length, batches: 1, errors: [] };
2381
2494
  } catch (err) {
2382
- this.storage.sql.exec("ROLLBACK").toArray();
2383
- throw err;
2495
+ const error = err instanceof Error ? err : new Error(String(err));
2496
+ return {
2497
+ deleted: 0,
2498
+ batches: 0,
2499
+ errors: [{ batchIndex: 0, error, operationCount: deleteStmts.length }]
2500
+ };
2384
2501
  }
2385
2502
  }
2386
- };
2387
- function ensureSchema(storage, table) {
2388
- const statements = buildSchemaStatements(table);
2389
- for (const sql of statements) {
2390
- storage.sql.exec(sql).toArray();
2391
- }
2392
- }
2393
- function createDOSqliteGraphClient(storage, options = {}) {
2394
- const table = options.table ?? "firegraph";
2395
- validateTableName(table);
2396
- if (options.autoMigrate !== false) {
2397
- ensureSchema(storage, table);
2398
- }
2399
- const executor = new DOSqliteExecutor(storage);
2400
- const backend = createSqliteBackend(executor, table);
2401
- const { table: _t, autoMigrate: _m, ...clientOptions } = options;
2402
- void _t;
2403
- void _m;
2404
- let metaBackend;
2405
- if (clientOptions.registryMode && typeof clientOptions.registryMode === "object" && clientOptions.registryMode.collection && clientOptions.registryMode.collection !== table) {
2406
- const metaTable = clientOptions.registryMode.collection;
2407
- validateTableName(metaTable);
2408
- if (options.autoMigrate !== false) {
2409
- ensureSchema(storage, metaTable);
2503
+ // ---------------------------------------------------------------------------
2504
+ // RPC: admin
2505
+ // ---------------------------------------------------------------------------
2506
+ /**
2507
+ * Wipe every row. Called by the client when tearing down a subgraph DO as
2508
+ * part of cascade — the DO itself can't be destroyed (DO IDs persist
2509
+ * forever), but its storage can be emptied.
2510
+ */
2511
+ async _fgDestroy() {
2512
+ const stmt = compileDODeleteAll(this.table);
2513
+ this.execRun(stmt);
2514
+ }
2515
+ // ---------------------------------------------------------------------------
2516
+ // Internals
2517
+ // ---------------------------------------------------------------------------
2518
+ runSchema() {
2519
+ for (const sql of buildDOSchemaStatements(this.table)) {
2520
+ this.ctx.storage.sql.exec(sql).toArray();
2410
2521
  }
2411
- metaBackend = createSqliteBackend(executor, metaTable);
2412
2522
  }
2413
- return createGraphClientFromBackend(backend, clientOptions, metaBackend);
2414
- }
2523
+ execAll(stmt) {
2524
+ return this.ctx.storage.sql.exec(stmt.sql, ...stmt.params).toArray();
2525
+ }
2526
+ execRun(stmt) {
2527
+ this.ctx.storage.sql.exec(stmt.sql, ...stmt.params).toArray();
2528
+ }
2529
+ };
2415
2530
  // Annotate the CommonJS export names for ESM import in node:
2416
2531
  0 && (module.exports = {
2417
- createDOSqliteGraphClient
2532
+ DORPCBackend,
2533
+ FiregraphDO,
2534
+ createDOClient,
2535
+ createSiblingClient
2418
2536
  });
2419
- //# sourceMappingURL=do-sqlite.cjs.map
2537
+ //# sourceMappingURL=index.cjs.map