@typicalday/firegraph 0.11.2 → 0.12.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 (44) hide show
  1. package/README.md +38 -5
  2. package/dist/backend-BsR0lnFL.d.ts +200 -0
  3. package/dist/backend-Ct-fLlkG.d.cts +200 -0
  4. package/dist/backend.cjs +143 -2
  5. package/dist/backend.cjs.map +1 -1
  6. package/dist/backend.d.cts +3 -3
  7. package/dist/backend.d.ts +3 -3
  8. package/dist/backend.js +13 -4
  9. package/dist/backend.js.map +1 -1
  10. package/dist/chunk-AWW4MUJ5.js +245 -0
  11. package/dist/chunk-AWW4MUJ5.js.map +1 -0
  12. package/dist/{chunk-5753Y42M.js → chunk-C2QMD7RY.js} +6 -10
  13. package/dist/chunk-C2QMD7RY.js.map +1 -0
  14. package/dist/chunk-EQJUUVFG.js +14 -0
  15. package/dist/chunk-EQJUUVFG.js.map +1 -0
  16. package/dist/{chunk-NJSOD64C.js → chunk-HONQY4HF.js} +80 -17
  17. package/dist/chunk-HONQY4HF.js.map +1 -0
  18. package/dist/cloudflare/index.cjs +458 -73
  19. package/dist/cloudflare/index.cjs.map +1 -1
  20. package/dist/cloudflare/index.d.cts +8 -5
  21. package/dist/cloudflare/index.d.ts +8 -5
  22. package/dist/cloudflare/index.js +234 -56
  23. package/dist/cloudflare/index.js.map +1 -1
  24. package/dist/codegen/index.d.cts +1 -1
  25. package/dist/codegen/index.d.ts +1 -1
  26. package/dist/index.cjs +271 -36
  27. package/dist/index.cjs.map +1 -1
  28. package/dist/index.d.cts +21 -69
  29. package/dist/index.d.ts +21 -69
  30. package/dist/index.js +58 -28
  31. package/dist/index.js.map +1 -1
  32. package/dist/registry-B1qsVL0E.d.cts +64 -0
  33. package/dist/registry-Fi074zVa.d.ts +64 -0
  34. package/dist/{serialization-ZZ7RSDRX.js → serialization-OE2PFZMY.js} +6 -4
  35. package/dist/{types-BGWxcpI_.d.cts → types-DxYLy8Ol.d.cts} +36 -2
  36. package/dist/{types-BGWxcpI_.d.ts → types-DxYLy8Ol.d.ts} +36 -2
  37. package/package.json +1 -1
  38. package/dist/backend-U-MLShlg.d.ts +0 -97
  39. package/dist/backend-np4gEVhB.d.cts +0 -97
  40. package/dist/chunk-5753Y42M.js.map +0 -1
  41. package/dist/chunk-NJSOD64C.js.map +0 -1
  42. package/dist/chunk-R7CRGYY4.js +0 -94
  43. package/dist/chunk-R7CRGYY4.js.map +0 -1
  44. /package/dist/{serialization-ZZ7RSDRX.js.map → serialization-OE2PFZMY.js.map} +0 -0
@@ -30,6 +30,21 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
30
30
  ));
31
31
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
32
32
 
33
+ // src/internal/serialization-tag.ts
34
+ function isTaggedValue(value) {
35
+ if (value === null || typeof value !== "object") return false;
36
+ const tag = value[SERIALIZATION_TAG];
37
+ return typeof tag === "string" && KNOWN_TYPES.has(tag);
38
+ }
39
+ var SERIALIZATION_TAG, KNOWN_TYPES;
40
+ var init_serialization_tag = __esm({
41
+ "src/internal/serialization-tag.ts"() {
42
+ "use strict";
43
+ SERIALIZATION_TAG = "__firegraph_ser__";
44
+ KNOWN_TYPES = /* @__PURE__ */ new Set(["Timestamp", "GeoPoint", "VectorValue", "DocumentReference"]);
45
+ }
46
+ });
47
+
33
48
  // src/serialization.ts
34
49
  var serialization_exports = {};
35
50
  __export(serialization_exports, {
@@ -38,11 +53,6 @@ __export(serialization_exports, {
38
53
  isTaggedValue: () => isTaggedValue,
39
54
  serializeFirestoreTypes: () => serializeFirestoreTypes
40
55
  });
41
- function isTaggedValue(value) {
42
- if (value === null || typeof value !== "object") return false;
43
- const tag = value[SERIALIZATION_TAG];
44
- return typeof tag === "string" && KNOWN_TYPES.has(tag);
45
- }
46
56
  function isTimestamp(value) {
47
57
  return value instanceof import_firestore.Timestamp;
48
58
  }
@@ -143,13 +153,13 @@ function deserializeValue(value, db) {
143
153
  }
144
154
  return result;
145
155
  }
146
- var import_firestore, SERIALIZATION_TAG, KNOWN_TYPES, _docRefWarned;
156
+ var import_firestore, _docRefWarned;
147
157
  var init_serialization = __esm({
148
158
  "src/serialization.ts"() {
149
159
  "use strict";
150
160
  import_firestore = require("@google-cloud/firestore");
151
- SERIALIZATION_TAG = "__firegraph_ser__";
152
- KNOWN_TYPES = /* @__PURE__ */ new Set(["Timestamp", "GeoPoint", "VectorValue", "DocumentReference"]);
161
+ init_serialization_tag();
162
+ init_serialization_tag();
153
163
  _docRefWarned = false;
154
164
  }
155
165
  });
@@ -159,9 +169,15 @@ var cloudflare_exports = {};
159
169
  __export(cloudflare_exports, {
160
170
  DORPCBackend: () => DORPCBackend,
161
171
  FiregraphDO: () => FiregraphDO,
172
+ META_EDGE_TYPE: () => META_EDGE_TYPE,
173
+ META_NODE_TYPE: () => META_NODE_TYPE,
162
174
  buildDOSchemaStatements: () => buildDOSchemaStatements,
163
175
  createDOClient: () => createDOClient,
164
- createSiblingClient: () => createSiblingClient
176
+ createMergedRegistry: () => createMergedRegistry,
177
+ createRegistry: () => createRegistry,
178
+ createSiblingClient: () => createSiblingClient,
179
+ deleteField: () => deleteField,
180
+ generateId: () => generateId
165
181
  });
166
182
  module.exports = __toCommonJS(cloudflare_exports);
167
183
 
@@ -234,6 +250,296 @@ var BUILTIN_FIELDS = /* @__PURE__ */ new Set([
234
250
  ]);
235
251
  var SHARD_SEPARATOR = ":";
236
252
 
253
+ // src/internal/sqlite-data-ops.ts
254
+ var FIRESTORE_TYPE_NAMES = /* @__PURE__ */ new Set([
255
+ "Timestamp",
256
+ "GeoPoint",
257
+ "VectorValue",
258
+ "DocumentReference",
259
+ "FieldValue"
260
+ ]);
261
+ function isFirestoreSpecialType(value) {
262
+ const ctorName = value.constructor?.name;
263
+ if (ctorName && FIRESTORE_TYPE_NAMES.has(ctorName)) return ctorName;
264
+ return null;
265
+ }
266
+ var JSON_PATH_KEY_RE = /^[A-Za-z_][A-Za-z0-9_-]*$/;
267
+ function validateJsonPathKey(key, backendLabel) {
268
+ if (key.length === 0) {
269
+ throw new FiregraphError(
270
+ `${backendLabel}: empty JSON path component is not allowed`,
271
+ "INVALID_QUERY"
272
+ );
273
+ }
274
+ if (!JSON_PATH_KEY_RE.test(key)) {
275
+ throw new FiregraphError(
276
+ `${backendLabel}: data field path component "${key}" is not a safe JSON-path identifier. Allowed pattern: /^[A-Za-z_][A-Za-z0-9_-]*$/. Use replaceNode/replaceEdge (full-data overwrite) for keys with reserved characters (whitespace, dots, brackets, quotes, etc.).`,
277
+ "INVALID_QUERY"
278
+ );
279
+ }
280
+ }
281
+ function jsonBind(value, backendLabel) {
282
+ if (value === void 0) return "null";
283
+ if (value !== null && typeof value === "object") {
284
+ const firestoreType = isFirestoreSpecialType(value);
285
+ if (firestoreType) {
286
+ throw new FiregraphError(
287
+ `${backendLabel} cannot persist a Firestore ${firestoreType} value. Convert to a primitive before writing (e.g. \`ts.toMillis()\` for Timestamp).`,
288
+ "INVALID_ARGUMENT"
289
+ );
290
+ }
291
+ }
292
+ return JSON.stringify(value);
293
+ }
294
+ function compileDataOpsExpr(ops, base, params, backendLabel) {
295
+ if (ops.length === 0) return null;
296
+ const deletes = [];
297
+ const sets = [];
298
+ for (const op of ops) (op.delete ? deletes : sets).push(op);
299
+ let expr = base;
300
+ if (deletes.length > 0) {
301
+ const placeholders = deletes.map(() => "?").join(", ");
302
+ expr = `json_remove(${expr}, ${placeholders})`;
303
+ for (const op of deletes) {
304
+ for (const seg of op.path) validateJsonPathKey(seg, backendLabel);
305
+ params.push(`$.${op.path.join(".")}`);
306
+ }
307
+ }
308
+ if (sets.length > 0) {
309
+ const pieces = sets.map(() => "?, json(?)").join(", ");
310
+ expr = `json_set(${expr}, ${pieces})`;
311
+ for (const op of sets) {
312
+ for (const seg of op.path) validateJsonPathKey(seg, backendLabel);
313
+ params.push(`$.${op.path.join(".")}`);
314
+ params.push(jsonBind(op.value, backendLabel));
315
+ }
316
+ }
317
+ return expr;
318
+ }
319
+
320
+ // src/internal/sqlite-payload-guard.ts
321
+ init_serialization_tag();
322
+
323
+ // src/internal/write-plan.ts
324
+ init_serialization_tag();
325
+ var DELETE_FIELD = /* @__PURE__ */ Symbol.for("firegraph.deleteField");
326
+ function deleteField() {
327
+ return DELETE_FIELD;
328
+ }
329
+ function isDeleteSentinel(value) {
330
+ return value === DELETE_FIELD;
331
+ }
332
+ var FIRESTORE_TERMINAL_CTOR = /* @__PURE__ */ new Set([
333
+ "Timestamp",
334
+ "GeoPoint",
335
+ "VectorValue",
336
+ "DocumentReference",
337
+ "FieldValue",
338
+ "NumericIncrementTransform",
339
+ "ArrayUnionTransform",
340
+ "ArrayRemoveTransform",
341
+ "ServerTimestampTransform",
342
+ "DeleteTransform"
343
+ ]);
344
+ function isTerminalValue(value) {
345
+ if (value === null) return true;
346
+ const t = typeof value;
347
+ if (t !== "object") return true;
348
+ if (Array.isArray(value)) return true;
349
+ if (isTaggedValue(value)) return true;
350
+ const proto = Object.getPrototypeOf(value);
351
+ if (proto === null || proto === Object.prototype) return false;
352
+ const ctor = value.constructor;
353
+ if (ctor && typeof ctor.name === "string" && FIRESTORE_TERMINAL_CTOR.has(ctor.name)) return true;
354
+ return true;
355
+ }
356
+ var SAFE_KEY_RE = /^[A-Za-z_][A-Za-z0-9_-]*$/;
357
+ function assertUpdatePayloadExclusive(update) {
358
+ if (update.replaceData !== void 0 && update.dataOps !== void 0) {
359
+ throw new Error(
360
+ "firegraph: UpdatePayload cannot specify both `replaceData` and `dataOps`. Use one or the other \u2014 `replaceData` is the migration-write-back form, `dataOps` is the standard partial-update form."
361
+ );
362
+ }
363
+ }
364
+ function assertNoDeleteSentinels(data, callerLabel) {
365
+ walkForDeleteSentinels(data, [], { kind: "root" }, ({ path }) => {
366
+ const where = path.length === 0 ? "<root>" : path.map((p) => JSON.stringify(p)).join(" > ");
367
+ throw new Error(
368
+ `firegraph: ${callerLabel} payload contains a deleteField() sentinel at ${where}. deleteField() is only valid inside updateNode/updateEdge \u2014 full-data writes (put*, replace*) cannot delete individual fields. Use updateNode with a deleteField() value, or omit the field from the replace payload.`
369
+ );
370
+ });
371
+ }
372
+ function walkForDeleteSentinels(node, path, parent, visit) {
373
+ if (node === null || node === void 0) return;
374
+ if (isDeleteSentinel(node)) {
375
+ visit({ path, parent });
376
+ return;
377
+ }
378
+ if (typeof node !== "object") return;
379
+ if (isTaggedValue(node)) return;
380
+ if (Array.isArray(node)) {
381
+ for (let i = 0; i < node.length; i++) {
382
+ walkForDeleteSentinels(node[i], [...path, String(i)], { kind: "array", index: i }, visit);
383
+ }
384
+ return;
385
+ }
386
+ const proto = Object.getPrototypeOf(node);
387
+ if (proto !== null && proto !== Object.prototype) return;
388
+ const obj = node;
389
+ for (const key of Object.keys(obj)) {
390
+ walkForDeleteSentinels(obj[key], [...path, key], { kind: "object" }, visit);
391
+ }
392
+ }
393
+ function assertSafePath(path) {
394
+ for (const seg of path) {
395
+ if (!SAFE_KEY_RE.test(seg)) {
396
+ throw new Error(
397
+ `firegraph: unsafe object key ${JSON.stringify(seg)} at path ${path.map((p) => JSON.stringify(p)).join(" > ")}. Keys used inside update payloads must match /^[A-Za-z_][A-Za-z0-9_-]*$/ so they can be embedded safely in SQLite JSON paths.`
398
+ );
399
+ }
400
+ }
401
+ }
402
+ function flattenPatch(data) {
403
+ const ops = [];
404
+ walk(data, [], ops);
405
+ return ops;
406
+ }
407
+ function assertNoDeleteSentinelsInArrayValue(arr, arrayPath) {
408
+ walkForDeleteSentinels(arr, arrayPath, { kind: "root" }, ({ parent }) => {
409
+ const arrayPathStr = arrayPath.length === 0 ? "<root>" : arrayPath.map((p) => JSON.stringify(p)).join(" > ");
410
+ if (parent.kind === "array") {
411
+ throw new Error(
412
+ `firegraph: deleteField() sentinel at index ${parent.index} inside an array at path ${arrayPathStr}. Arrays are terminal in update payloads (replaced as a unit), so the sentinel would be silently dropped by JSON serialization. To remove the field entirely, pass deleteField() in place of the whole array.`
413
+ );
414
+ }
415
+ throw new Error(
416
+ `firegraph: deleteField() sentinel inside an array element at path ${arrayPathStr}. Arrays are terminal in update payloads \u2014 the sentinel would be silently dropped by JSON serialization.`
417
+ );
418
+ });
419
+ }
420
+ function walk(node, path, out) {
421
+ if (node === void 0) return;
422
+ if (isDeleteSentinel(node)) {
423
+ if (path.length === 0) {
424
+ throw new Error("firegraph: deleteField() cannot be the entire update payload.");
425
+ }
426
+ assertSafePath(path);
427
+ out.push({ path: [...path], value: void 0, delete: true });
428
+ return;
429
+ }
430
+ if (isTerminalValue(node)) {
431
+ if (path.length === 0) {
432
+ throw new Error(
433
+ "firegraph: update payload must be a plain object. Got " + (node === null ? "null" : Array.isArray(node) ? "array" : typeof node) + "."
434
+ );
435
+ }
436
+ if (Array.isArray(node)) {
437
+ assertNoDeleteSentinelsInArrayValue(node, path);
438
+ }
439
+ assertSafePath(path);
440
+ out.push({ path: [...path], value: node, delete: false });
441
+ return;
442
+ }
443
+ const obj = node;
444
+ const keys = Object.keys(obj);
445
+ if (keys.length === 0) {
446
+ if (path.length > 0) {
447
+ assertSafePath(path);
448
+ out.push({ path: [...path], value: {}, delete: false });
449
+ }
450
+ return;
451
+ }
452
+ for (const key of keys) {
453
+ if (key === SERIALIZATION_TAG) {
454
+ const where = path.length === 0 ? "<root>" : path.map((p) => JSON.stringify(p)).join(" > ");
455
+ throw new Error(
456
+ `firegraph: update payload contains a literal \`${SERIALIZATION_TAG}\` key at ${where}. That key is reserved for firegraph's serialization envelope and cannot appear on a plain object in user data. Use a different field name, or pass a recognized tagged value through replaceNode/replaceEdge instead.`
457
+ );
458
+ }
459
+ walk(obj[key], [...path, key], out);
460
+ }
461
+ }
462
+
463
+ // src/internal/sqlite-payload-guard.ts
464
+ var FIRESTORE_TYPE_NAMES2 = /* @__PURE__ */ new Set([
465
+ "Timestamp",
466
+ "GeoPoint",
467
+ "VectorValue",
468
+ "DocumentReference",
469
+ "FieldValue"
470
+ ]);
471
+ function assertJsonSafePayload(data, label) {
472
+ walk2(data, [], label);
473
+ }
474
+ function walk2(node, path, label) {
475
+ if (node === null || node === void 0) return;
476
+ if (isDeleteSentinel(node)) {
477
+ throw new FiregraphError(
478
+ `${label} backend cannot persist a deleteField() sentinel inside a full-data payload (replaceNode/replaceEdge or first-insert). The sentinel is only valid inside an updateNode/updateEdge dataOps patch. Path: ${formatPath(path)}.`,
479
+ "INVALID_ARGUMENT"
480
+ );
481
+ }
482
+ const t = typeof node;
483
+ if (t === "symbol" || t === "function") {
484
+ throw new FiregraphError(
485
+ `${label} backend cannot persist a value of type ${t}. JSON.stringify drops it silently. Path: ${formatPath(path)}.`,
486
+ "INVALID_ARGUMENT"
487
+ );
488
+ }
489
+ if (t === "bigint") {
490
+ throw new FiregraphError(
491
+ `${label} backend cannot persist a value of type bigint. JSON.stringify cannot serialize this type (throws TypeError). Path: ${formatPath(path)}.`,
492
+ "INVALID_ARGUMENT"
493
+ );
494
+ }
495
+ if (t !== "object") return;
496
+ if (Array.isArray(node)) {
497
+ for (let i = 0; i < node.length; i++) {
498
+ walk2(node[i], [...path, String(i)], label);
499
+ }
500
+ return;
501
+ }
502
+ const obj = node;
503
+ if (Object.prototype.hasOwnProperty.call(obj, SERIALIZATION_TAG)) {
504
+ const tagValue = obj[SERIALIZATION_TAG];
505
+ throw new FiregraphError(
506
+ `${label} backend cannot persist an object with a \`${SERIALIZATION_TAG}\` key (value: ${formatTagValue(tagValue)}). Recognised tags are valid only on the Firestore backend (migration-sandbox output); a literal \`${SERIALIZATION_TAG}\` field in user data is reserved and not allowed. Path: ${formatPath(path)}.`,
507
+ "INVALID_ARGUMENT"
508
+ );
509
+ }
510
+ const proto = Object.getPrototypeOf(node);
511
+ if (proto !== null && proto !== Object.prototype) {
512
+ const ctor = node.constructor;
513
+ const ctorName = ctor && typeof ctor.name === "string" ? ctor.name : "<anonymous>";
514
+ if (FIRESTORE_TYPE_NAMES2.has(ctorName)) {
515
+ throw new FiregraphError(
516
+ `${label} backend cannot persist a Firestore ${ctorName} value. Convert to a primitive before writing (e.g. \`ts.toMillis()\` for Timestamp, \`{lat,lng}\` for GeoPoint). Path: ${formatPath(path)}.`,
517
+ "INVALID_ARGUMENT"
518
+ );
519
+ }
520
+ if (node instanceof Date) return;
521
+ throw new FiregraphError(
522
+ `${label} backend cannot persist a class instance of type ${ctorName}. Only plain objects, arrays, and primitives round-trip safely through JSON storage. Path: ${formatPath(path)}.`,
523
+ "INVALID_ARGUMENT"
524
+ );
525
+ }
526
+ for (const key of Object.keys(obj)) {
527
+ walk2(obj[key], [...path, key], label);
528
+ }
529
+ }
530
+ function formatPath(path) {
531
+ return path.length === 0 ? "<root>" : path.map((p) => JSON.stringify(p)).join(" > ");
532
+ }
533
+ function formatTagValue(value) {
534
+ if (value === null) return "null";
535
+ if (value === void 0) return "undefined";
536
+ if (typeof value === "string") return JSON.stringify(value);
537
+ if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
538
+ return String(value);
539
+ }
540
+ return typeof value;
541
+ }
542
+
237
543
  // src/timestamp.ts
238
544
  var GraphTimestampImpl = class _GraphTimestampImpl {
239
545
  constructor(seconds, nanoseconds) {
@@ -273,7 +579,7 @@ var DEFAULT_CORE_INDEXES = Object.freeze([
273
579
 
274
580
  // src/internal/sqlite-index-ddl.ts
275
581
  var IDENT_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
276
- var JSON_PATH_KEY_RE = /^[A-Za-z_][A-Za-z0-9_-]*$/;
582
+ var JSON_PATH_KEY_RE2 = /^[A-Za-z_][A-Za-z0-9_-]*$/;
277
583
  function quoteIdent(name) {
278
584
  if (!IDENT_RE.test(name)) {
279
585
  throw new FiregraphError(
@@ -321,7 +627,7 @@ function compileFieldExpr(path, fieldToColumn) {
321
627
  const suffix = path.slice(5);
322
628
  const parts = suffix.split(".");
323
629
  for (const part of parts) {
324
- if (!JSON_PATH_KEY_RE.test(part)) {
630
+ if (!JSON_PATH_KEY_RE2.test(part)) {
325
631
  throw new FiregraphError(
326
632
  `IndexSpec data path "${path}" has invalid component "${part}". Each component must match /^[A-Za-z_][A-Za-z0-9_-]*$/.`,
327
633
  "INVALID_INDEX"
@@ -416,6 +722,8 @@ function buildDOSchemaStatements(table, options = {}) {
416
722
  }
417
723
 
418
724
  // src/cloudflare/sql.ts
725
+ var DO_BACKEND_LABEL = "DO SQLite";
726
+ var DO_BACKEND_ERR_LABEL = "DO SQLite backend";
419
727
  function compileFieldRef(field) {
420
728
  const column = DO_FIELD_TO_COLUMN[field];
421
729
  if (column) {
@@ -424,7 +732,7 @@ function compileFieldRef(field) {
424
732
  if (field.startsWith("data.")) {
425
733
  const suffix = field.slice(5);
426
734
  for (const part of suffix.split(".")) {
427
- validateJsonPathKey(part);
735
+ validateJsonPathKey(part, DO_BACKEND_ERR_LABEL);
428
736
  }
429
737
  return { expr: `json_extract("data", '$.${suffix}')` };
430
738
  }
@@ -436,18 +744,6 @@ function compileFieldRef(field) {
436
744
  "INVALID_QUERY"
437
745
  );
438
746
  }
439
- var FIRESTORE_TYPE_NAMES = /* @__PURE__ */ new Set([
440
- "Timestamp",
441
- "GeoPoint",
442
- "VectorValue",
443
- "DocumentReference",
444
- "FieldValue"
445
- ]);
446
- function isFirestoreSpecialType(value) {
447
- const ctorName = value.constructor?.name;
448
- if (ctorName && FIRESTORE_TYPE_NAMES.has(ctorName)) return ctorName;
449
- return null;
450
- }
451
747
  function bindValue(value) {
452
748
  if (value === null || value === void 0) return null;
453
749
  if (typeof value === "string" || typeof value === "number" || typeof value === "bigint" || typeof value === "boolean") {
@@ -466,21 +762,6 @@ function bindValue(value) {
466
762
  }
467
763
  return String(value);
468
764
  }
469
- var JSON_PATH_KEY_RE2 = /^[A-Za-z_][A-Za-z0-9_-]*$/;
470
- function validateJsonPathKey(key) {
471
- if (key.length === 0) {
472
- throw new FiregraphError(
473
- "DO SQLite backend: empty JSON path component is not allowed",
474
- "INVALID_QUERY"
475
- );
476
- }
477
- if (!JSON_PATH_KEY_RE2.test(key)) {
478
- throw new FiregraphError(
479
- `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.).`,
480
- "INVALID_QUERY"
481
- );
482
- }
483
- }
484
765
  function compileFilter(filter, params) {
485
766
  const { expr } = compileFieldRef(filter.field);
486
767
  switch (filter.op) {
@@ -567,11 +848,27 @@ function compileDOSelectByDocId(table, docId) {
567
848
  params: [docId]
568
849
  };
569
850
  }
570
- function compileDOSet(table, docId, record, nowMillis) {
571
- const sql = `INSERT OR REPLACE INTO ${quoteDOIdent(table)} (
572
- doc_id, a_type, a_uid, axb_type, b_type, b_uid, data, v, created_at, updated_at
573
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;
574
- const params = [
851
+ function compileDOSet(table, docId, record, nowMillis, mode) {
852
+ assertJsonSafePayload(record.data, DO_BACKEND_LABEL);
853
+ if (mode === "replace") {
854
+ const sql2 = `INSERT OR REPLACE INTO ${quoteDOIdent(table)} (
855
+ doc_id, a_type, a_uid, axb_type, b_type, b_uid, data, v, created_at, updated_at
856
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;
857
+ const params = [
858
+ docId,
859
+ record.aType,
860
+ record.aUid,
861
+ record.axbType,
862
+ record.bType,
863
+ record.bUid,
864
+ JSON.stringify(record.data ?? {}),
865
+ record.v ?? null,
866
+ nowMillis,
867
+ nowMillis
868
+ ];
869
+ return { sql: sql2, params };
870
+ }
871
+ const insertParams = [
575
872
  docId,
576
873
  record.aType,
577
874
  record.aUid,
@@ -583,22 +880,44 @@ function compileDOSet(table, docId, record, nowMillis) {
583
880
  nowMillis,
584
881
  nowMillis
585
882
  ];
586
- return { sql, params };
883
+ const ops = flattenPatch(record.data ?? {});
884
+ const updateParams = [];
885
+ const dataExpr = compileDataOpsExpr(ops, `COALESCE("data", '{}')`, updateParams, DO_BACKEND_ERR_LABEL) ?? `COALESCE("data", '{}')`;
886
+ const sql = `INSERT INTO ${quoteDOIdent(table)} (
887
+ doc_id, a_type, a_uid, axb_type, b_type, b_uid, data, v, created_at, updated_at
888
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
889
+ ON CONFLICT(doc_id) DO UPDATE SET
890
+ "a_type" = excluded."a_type",
891
+ "a_uid" = excluded."a_uid",
892
+ "axb_type" = excluded."axb_type",
893
+ "b_type" = excluded."b_type",
894
+ "b_uid" = excluded."b_uid",
895
+ "data" = ${dataExpr},
896
+ "v" = COALESCE(excluded."v", "v"),
897
+ "created_at" = excluded."created_at",
898
+ "updated_at" = excluded."updated_at"`;
899
+ return { sql, params: [...insertParams, ...updateParams] };
587
900
  }
588
901
  function compileDOUpdate(table, docId, update, nowMillis) {
902
+ assertUpdatePayloadExclusive(update);
589
903
  const setClauses = [];
590
904
  const params = [];
591
905
  if (update.replaceData) {
906
+ assertJsonSafePayload(update.replaceData, DO_BACKEND_LABEL);
592
907
  setClauses.push(`"data" = ?`);
593
908
  params.push(JSON.stringify(update.replaceData));
594
- } else if (update.dataFields && Object.keys(update.dataFields).length > 0) {
595
- const entries = Object.entries(update.dataFields);
596
- const pathArgs = entries.map(() => `?, ?`).join(", ");
597
- setClauses.push(`"data" = json_set(COALESCE("data", '{}'), ${pathArgs})`);
598
- for (const [k, v] of entries) {
599
- validateJsonPathKey(k);
600
- params.push(`$.${k}`);
601
- params.push(bindValue(v));
909
+ } else if (update.dataOps && update.dataOps.length > 0) {
910
+ for (const op of update.dataOps) {
911
+ if (!op.delete) assertJsonSafePayload(op.value, DO_BACKEND_LABEL);
912
+ }
913
+ const expr = compileDataOpsExpr(
914
+ update.dataOps,
915
+ `COALESCE("data", '{}')`,
916
+ params,
917
+ DO_BACKEND_ERR_LABEL
918
+ );
919
+ if (expr !== null) {
920
+ setClauses.push(`"data" = ${expr}`);
602
921
  }
603
922
  }
604
923
  if (update.v !== void 0) {
@@ -694,8 +1013,8 @@ var DORPCBatchBackend = class {
694
1013
  this.getStub = getStub;
695
1014
  }
696
1015
  ops = [];
697
- setDoc(docId, record) {
698
- this.ops.push({ kind: "set", docId, record });
1016
+ setDoc(docId, record, mode) {
1017
+ this.ops.push({ kind: "set", docId, record, mode });
699
1018
  }
700
1019
  updateDoc(docId, update) {
701
1020
  this.ops.push({ kind: "update", docId, update });
@@ -745,8 +1064,8 @@ var DORPCBackend = class _DORPCBackend {
745
1064
  return wires.map(hydrateDORecord);
746
1065
  }
747
1066
  // --- Writes ---
748
- async setDoc(docId, record) {
749
- return this.stub._fgSetDoc(docId, record);
1067
+ async setDoc(docId, record, mode) {
1068
+ return this.stub._fgSetDoc(docId, record, mode);
750
1069
  }
751
1070
  async updateDoc(docId, update) {
752
1071
  return this.stub._fgUpdateDoc(docId, update);
@@ -875,6 +1194,19 @@ var GraphBatchImpl = class {
875
1194
  this.scopePath = scopePath;
876
1195
  }
877
1196
  async putNode(aType, uid, data) {
1197
+ this.writeNode(aType, uid, data, "merge");
1198
+ }
1199
+ async putEdge(aType, aUid, axbType, bType, bUid, data) {
1200
+ this.writeEdge(aType, aUid, axbType, bType, bUid, data, "merge");
1201
+ }
1202
+ async replaceNode(aType, uid, data) {
1203
+ this.writeNode(aType, uid, data, "replace");
1204
+ }
1205
+ async replaceEdge(aType, aUid, axbType, bType, bUid, data) {
1206
+ this.writeEdge(aType, aUid, axbType, bType, bUid, data, "replace");
1207
+ }
1208
+ writeNode(aType, uid, data, mode) {
1209
+ assertNoDeleteSentinels(data, mode === "replace" ? "replaceNode" : "putNode");
878
1210
  if (this.registry) {
879
1211
  this.registry.validate(aType, NODE_RELATION, aType, data, this.scopePath);
880
1212
  }
@@ -886,9 +1218,10 @@ var GraphBatchImpl = class {
886
1218
  record.v = entry.schemaVersion;
887
1219
  }
888
1220
  }
889
- this.backend.setDoc(docId, record);
1221
+ this.backend.setDoc(docId, record, mode);
890
1222
  }
891
- async putEdge(aType, aUid, axbType, bType, bUid, data) {
1223
+ writeEdge(aType, aUid, axbType, bType, bUid, data, mode) {
1224
+ assertNoDeleteSentinels(data, mode === "replace" ? "replaceEdge" : "putEdge");
892
1225
  if (this.registry) {
893
1226
  this.registry.validate(aType, axbType, bType, data, this.scopePath);
894
1227
  }
@@ -900,11 +1233,15 @@ var GraphBatchImpl = class {
900
1233
  record.v = entry.schemaVersion;
901
1234
  }
902
1235
  }
903
- this.backend.setDoc(docId, record);
1236
+ this.backend.setDoc(docId, record, mode);
904
1237
  }
905
1238
  async updateNode(uid, data) {
906
1239
  const docId = computeNodeDocId(uid);
907
- this.backend.updateDoc(docId, { dataFields: data });
1240
+ this.backend.updateDoc(docId, { dataOps: flattenPatch(data) });
1241
+ }
1242
+ async updateEdge(aUid, axbType, bUid, data) {
1243
+ const docId = computeEdgeDocId(aUid, axbType, bUid);
1244
+ this.backend.updateDoc(docId, { dataOps: flattenPatch(data) });
908
1245
  }
909
1246
  async removeNode(uid) {
910
1247
  const docId = computeNodeDocId(uid);
@@ -1853,6 +2190,19 @@ var GraphTransactionImpl = class {
1853
2190
  return results.map((r) => r.record);
1854
2191
  }
1855
2192
  async putNode(aType, uid, data) {
2193
+ await this.writeNode(aType, uid, data, "merge");
2194
+ }
2195
+ async putEdge(aType, aUid, axbType, bType, bUid, data) {
2196
+ await this.writeEdge(aType, aUid, axbType, bType, bUid, data, "merge");
2197
+ }
2198
+ async replaceNode(aType, uid, data) {
2199
+ await this.writeNode(aType, uid, data, "replace");
2200
+ }
2201
+ async replaceEdge(aType, aUid, axbType, bType, bUid, data) {
2202
+ await this.writeEdge(aType, aUid, axbType, bType, bUid, data, "replace");
2203
+ }
2204
+ async writeNode(aType, uid, data, mode) {
2205
+ assertNoDeleteSentinels(data, mode === "replace" ? "replaceNode" : "putNode");
1856
2206
  if (this.registry) {
1857
2207
  this.registry.validate(aType, NODE_RELATION, aType, data, this.scopePath);
1858
2208
  }
@@ -1864,9 +2214,10 @@ var GraphTransactionImpl = class {
1864
2214
  record.v = entry.schemaVersion;
1865
2215
  }
1866
2216
  }
1867
- await this.backend.setDoc(docId, record);
2217
+ await this.backend.setDoc(docId, record, mode);
1868
2218
  }
1869
- async putEdge(aType, aUid, axbType, bType, bUid, data) {
2219
+ async writeEdge(aType, aUid, axbType, bType, bUid, data, mode) {
2220
+ assertNoDeleteSentinels(data, mode === "replace" ? "replaceEdge" : "putEdge");
1870
2221
  if (this.registry) {
1871
2222
  this.registry.validate(aType, axbType, bType, data, this.scopePath);
1872
2223
  }
@@ -1878,11 +2229,15 @@ var GraphTransactionImpl = class {
1878
2229
  record.v = entry.schemaVersion;
1879
2230
  }
1880
2231
  }
1881
- await this.backend.setDoc(docId, record);
2232
+ await this.backend.setDoc(docId, record, mode);
1882
2233
  }
1883
2234
  async updateNode(uid, data) {
1884
2235
  const docId = computeNodeDocId(uid);
1885
- await this.backend.updateDoc(docId, { dataFields: data });
2236
+ await this.backend.updateDoc(docId, { dataOps: flattenPatch(data) });
2237
+ }
2238
+ async updateEdge(aUid, axbType, bUid, data) {
2239
+ const docId = computeEdgeDocId(aUid, axbType, bUid);
2240
+ await this.backend.updateDoc(docId, { dataOps: flattenPatch(data) });
1886
2241
  }
1887
2242
  async removeNode(uid) {
1888
2243
  const docId = computeNodeDocId(uid);
@@ -2081,6 +2436,19 @@ var GraphClientImpl = class _GraphClientImpl {
2081
2436
  // GraphWriter
2082
2437
  // ---------------------------------------------------------------------------
2083
2438
  async putNode(aType, uid, data) {
2439
+ await this.writeNode(aType, uid, data, "merge");
2440
+ }
2441
+ async putEdge(aType, aUid, axbType, bType, bUid, data) {
2442
+ await this.writeEdge(aType, aUid, axbType, bType, bUid, data, "merge");
2443
+ }
2444
+ async replaceNode(aType, uid, data) {
2445
+ await this.writeNode(aType, uid, data, "replace");
2446
+ }
2447
+ async replaceEdge(aType, aUid, axbType, bType, bUid, data) {
2448
+ await this.writeEdge(aType, aUid, axbType, bType, bUid, data, "replace");
2449
+ }
2450
+ async writeNode(aType, uid, data, mode) {
2451
+ assertNoDeleteSentinels(data, mode === "replace" ? "replaceNode" : "putNode");
2084
2452
  const registry = this.getRegistryForType(aType);
2085
2453
  if (registry) {
2086
2454
  registry.validate(aType, NODE_RELATION, aType, data, this.backend.scopePath);
@@ -2094,9 +2462,10 @@ var GraphClientImpl = class _GraphClientImpl {
2094
2462
  record.v = entry.schemaVersion;
2095
2463
  }
2096
2464
  }
2097
- await backend.setDoc(docId, record);
2465
+ await backend.setDoc(docId, record, mode);
2098
2466
  }
2099
- async putEdge(aType, aUid, axbType, bType, bUid, data) {
2467
+ async writeEdge(aType, aUid, axbType, bType, bUid, data, mode) {
2468
+ assertNoDeleteSentinels(data, mode === "replace" ? "replaceEdge" : "putEdge");
2100
2469
  const registry = this.getRegistryForType(aType);
2101
2470
  if (registry) {
2102
2471
  registry.validate(aType, axbType, bType, data, this.backend.scopePath);
@@ -2110,11 +2479,15 @@ var GraphClientImpl = class _GraphClientImpl {
2110
2479
  record.v = entry.schemaVersion;
2111
2480
  }
2112
2481
  }
2113
- await backend.setDoc(docId, record);
2482
+ await backend.setDoc(docId, record, mode);
2114
2483
  }
2115
2484
  async updateNode(uid, data) {
2116
2485
  const docId = computeNodeDocId(uid);
2117
- await this.backend.updateDoc(docId, { dataFields: data });
2486
+ await this.backend.updateDoc(docId, { dataOps: flattenPatch(data) });
2487
+ }
2488
+ async updateEdge(aUid, axbType, bUid, data) {
2489
+ const docId = computeEdgeDocId(aUid, axbType, bUid);
2490
+ await this.backend.updateDoc(docId, { dataOps: flattenPatch(data) });
2118
2491
  }
2119
2492
  async removeNode(uid) {
2120
2493
  const docId = computeNodeDocId(uid);
@@ -2495,8 +2868,8 @@ var FiregraphDO = class extends import_cloudflare_workers.DurableObject {
2495
2868
  // ---------------------------------------------------------------------------
2496
2869
  // RPC: writes
2497
2870
  // ---------------------------------------------------------------------------
2498
- async _fgSetDoc(docId, record) {
2499
- const stmt = compileDOSet(this.table, docId, record, Date.now());
2871
+ async _fgSetDoc(docId, record, mode) {
2872
+ const stmt = compileDOSet(this.table, docId, record, Date.now(), mode);
2500
2873
  this.execRun(stmt);
2501
2874
  }
2502
2875
  async _fgUpdateDoc(docId, update) {
@@ -2526,7 +2899,7 @@ var FiregraphDO = class extends import_cloudflare_workers.DurableObject {
2526
2899
  const statements = ops.map((op) => {
2527
2900
  switch (op.kind) {
2528
2901
  case "set":
2529
- return compileDOSet(this.table, op.docId, op.record, now);
2902
+ return compileDOSet(this.table, op.docId, op.record, now, op.mode);
2530
2903
  case "update":
2531
2904
  return compileDOUpdate(this.table, op.docId, op.update, now);
2532
2905
  case "delete":
@@ -2672,12 +3045,24 @@ var FiregraphDO = class extends import_cloudflare_workers.DurableObject {
2672
3045
  this.state.storage.sql.exec(stmt.sql, ...stmt.params).toArray();
2673
3046
  }
2674
3047
  };
3048
+
3049
+ // src/id.ts
3050
+ var import_nanoid = require("nanoid");
3051
+ function generateId() {
3052
+ return (0, import_nanoid.nanoid)();
3053
+ }
2675
3054
  // Annotate the CommonJS export names for ESM import in node:
2676
3055
  0 && (module.exports = {
2677
3056
  DORPCBackend,
2678
3057
  FiregraphDO,
3058
+ META_EDGE_TYPE,
3059
+ META_NODE_TYPE,
2679
3060
  buildDOSchemaStatements,
2680
3061
  createDOClient,
2681
- createSiblingClient
3062
+ createMergedRegistry,
3063
+ createRegistry,
3064
+ createSiblingClient,
3065
+ deleteField,
3066
+ generateId
2682
3067
  });
2683
3068
  //# sourceMappingURL=index.cjs.map