@typicalday/firegraph 0.11.1 → 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 +40 -15
  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-6SB34IPQ.js → chunk-HONQY4HF.js} +100 -28
  17. package/dist/chunk-HONQY4HF.js.map +1 -0
  18. package/dist/cloudflare/index.cjs +509 -102
  19. package/dist/cloudflare/index.cjs.map +1 -1
  20. package/dist/cloudflare/index.d.cts +45 -17
  21. package/dist/cloudflare/index.d.ts +45 -17
  22. package/dist/cloudflare/index.js +265 -74
  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 +291 -47
  27. package/dist/index.cjs.map +1 -1
  28. package/dist/index.d.cts +59 -77
  29. package/dist/index.d.ts +59 -77
  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 +8 -3
  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-6SB34IPQ.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
@@ -0,0 +1,245 @@
1
+ import {
2
+ SERIALIZATION_TAG,
3
+ isTaggedValue
4
+ } from "./chunk-EQJUUVFG.js";
5
+
6
+ // src/errors.ts
7
+ var FiregraphError = class extends Error {
8
+ constructor(message, code) {
9
+ super(message);
10
+ this.code = code;
11
+ this.name = "FiregraphError";
12
+ }
13
+ };
14
+ var NodeNotFoundError = class extends FiregraphError {
15
+ constructor(uid) {
16
+ super(`Node not found: ${uid}`, "NODE_NOT_FOUND");
17
+ this.name = "NodeNotFoundError";
18
+ }
19
+ };
20
+ var EdgeNotFoundError = class extends FiregraphError {
21
+ constructor(aUid, axbType, bUid) {
22
+ super(`Edge not found: ${aUid} -[${axbType}]-> ${bUid}`, "EDGE_NOT_FOUND");
23
+ this.name = "EdgeNotFoundError";
24
+ }
25
+ };
26
+ var ValidationError = class extends FiregraphError {
27
+ constructor(message, details) {
28
+ super(message, "VALIDATION_ERROR");
29
+ this.details = details;
30
+ this.name = "ValidationError";
31
+ }
32
+ };
33
+ var RegistryViolationError = class extends FiregraphError {
34
+ constructor(aType, axbType, bType) {
35
+ super(`Unregistered triple: (${aType}) -[${axbType}]-> (${bType})`, "REGISTRY_VIOLATION");
36
+ this.name = "RegistryViolationError";
37
+ }
38
+ };
39
+ var InvalidQueryError = class extends FiregraphError {
40
+ constructor(message) {
41
+ super(message, "INVALID_QUERY");
42
+ this.name = "InvalidQueryError";
43
+ }
44
+ };
45
+ var TraversalError = class extends FiregraphError {
46
+ constructor(message) {
47
+ super(message, "TRAVERSAL_ERROR");
48
+ this.name = "TraversalError";
49
+ }
50
+ };
51
+ var DynamicRegistryError = class extends FiregraphError {
52
+ constructor(message) {
53
+ super(message, "DYNAMIC_REGISTRY_ERROR");
54
+ this.name = "DynamicRegistryError";
55
+ }
56
+ };
57
+ var QuerySafetyError = class extends FiregraphError {
58
+ constructor(message) {
59
+ super(message, "QUERY_SAFETY");
60
+ this.name = "QuerySafetyError";
61
+ }
62
+ };
63
+ var RegistryScopeError = class extends FiregraphError {
64
+ constructor(aType, axbType, bType, scopePath, allowedIn) {
65
+ super(
66
+ `Type (${aType}) -[${axbType}]-> (${bType}) is not allowed at scope "${scopePath || "root"}". Allowed in: [${allowedIn.join(", ")}]`,
67
+ "REGISTRY_SCOPE"
68
+ );
69
+ this.name = "RegistryScopeError";
70
+ }
71
+ };
72
+ var MigrationError = class extends FiregraphError {
73
+ constructor(message) {
74
+ super(message, "MIGRATION_ERROR");
75
+ this.name = "MigrationError";
76
+ }
77
+ };
78
+ var CrossBackendTransactionError = class extends FiregraphError {
79
+ constructor(message) {
80
+ super(message, "CROSS_BACKEND_TRANSACTION");
81
+ this.name = "CrossBackendTransactionError";
82
+ }
83
+ };
84
+
85
+ // src/internal/write-plan.ts
86
+ var DELETE_FIELD = /* @__PURE__ */ Symbol.for("firegraph.deleteField");
87
+ function deleteField() {
88
+ return DELETE_FIELD;
89
+ }
90
+ function isDeleteSentinel(value) {
91
+ return value === DELETE_FIELD;
92
+ }
93
+ var FIRESTORE_TERMINAL_CTOR = /* @__PURE__ */ new Set([
94
+ "Timestamp",
95
+ "GeoPoint",
96
+ "VectorValue",
97
+ "DocumentReference",
98
+ "FieldValue",
99
+ "NumericIncrementTransform",
100
+ "ArrayUnionTransform",
101
+ "ArrayRemoveTransform",
102
+ "ServerTimestampTransform",
103
+ "DeleteTransform"
104
+ ]);
105
+ function isTerminalValue(value) {
106
+ if (value === null) return true;
107
+ const t = typeof value;
108
+ if (t !== "object") return true;
109
+ if (Array.isArray(value)) return true;
110
+ if (isTaggedValue(value)) return true;
111
+ const proto = Object.getPrototypeOf(value);
112
+ if (proto === null || proto === Object.prototype) return false;
113
+ const ctor = value.constructor;
114
+ if (ctor && typeof ctor.name === "string" && FIRESTORE_TERMINAL_CTOR.has(ctor.name)) return true;
115
+ return true;
116
+ }
117
+ var SAFE_KEY_RE = /^[A-Za-z_][A-Za-z0-9_-]*$/;
118
+ function assertUpdatePayloadExclusive(update) {
119
+ if (update.replaceData !== void 0 && update.dataOps !== void 0) {
120
+ throw new Error(
121
+ "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."
122
+ );
123
+ }
124
+ }
125
+ function assertNoDeleteSentinels(data, callerLabel) {
126
+ walkForDeleteSentinels(data, [], { kind: "root" }, ({ path }) => {
127
+ const where = path.length === 0 ? "<root>" : path.map((p) => JSON.stringify(p)).join(" > ");
128
+ throw new Error(
129
+ `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.`
130
+ );
131
+ });
132
+ }
133
+ function walkForDeleteSentinels(node, path, parent, visit) {
134
+ if (node === null || node === void 0) return;
135
+ if (isDeleteSentinel(node)) {
136
+ visit({ path, parent });
137
+ return;
138
+ }
139
+ if (typeof node !== "object") return;
140
+ if (isTaggedValue(node)) return;
141
+ if (Array.isArray(node)) {
142
+ for (let i = 0; i < node.length; i++) {
143
+ walkForDeleteSentinels(node[i], [...path, String(i)], { kind: "array", index: i }, visit);
144
+ }
145
+ return;
146
+ }
147
+ const proto = Object.getPrototypeOf(node);
148
+ if (proto !== null && proto !== Object.prototype) return;
149
+ const obj = node;
150
+ for (const key of Object.keys(obj)) {
151
+ walkForDeleteSentinels(obj[key], [...path, key], { kind: "object" }, visit);
152
+ }
153
+ }
154
+ function assertSafePath(path) {
155
+ for (const seg of path) {
156
+ if (!SAFE_KEY_RE.test(seg)) {
157
+ throw new Error(
158
+ `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.`
159
+ );
160
+ }
161
+ }
162
+ }
163
+ function flattenPatch(data) {
164
+ const ops = [];
165
+ walk(data, [], ops);
166
+ return ops;
167
+ }
168
+ function assertNoDeleteSentinelsInArrayValue(arr, arrayPath) {
169
+ walkForDeleteSentinels(arr, arrayPath, { kind: "root" }, ({ parent }) => {
170
+ const arrayPathStr = arrayPath.length === 0 ? "<root>" : arrayPath.map((p) => JSON.stringify(p)).join(" > ");
171
+ if (parent.kind === "array") {
172
+ throw new Error(
173
+ `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.`
174
+ );
175
+ }
176
+ throw new Error(
177
+ `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.`
178
+ );
179
+ });
180
+ }
181
+ function walk(node, path, out) {
182
+ if (node === void 0) return;
183
+ if (isDeleteSentinel(node)) {
184
+ if (path.length === 0) {
185
+ throw new Error("firegraph: deleteField() cannot be the entire update payload.");
186
+ }
187
+ assertSafePath(path);
188
+ out.push({ path: [...path], value: void 0, delete: true });
189
+ return;
190
+ }
191
+ if (isTerminalValue(node)) {
192
+ if (path.length === 0) {
193
+ throw new Error(
194
+ "firegraph: update payload must be a plain object. Got " + (node === null ? "null" : Array.isArray(node) ? "array" : typeof node) + "."
195
+ );
196
+ }
197
+ if (Array.isArray(node)) {
198
+ assertNoDeleteSentinelsInArrayValue(node, path);
199
+ }
200
+ assertSafePath(path);
201
+ out.push({ path: [...path], value: node, delete: false });
202
+ return;
203
+ }
204
+ const obj = node;
205
+ const keys = Object.keys(obj);
206
+ if (keys.length === 0) {
207
+ if (path.length > 0) {
208
+ assertSafePath(path);
209
+ out.push({ path: [...path], value: {}, delete: false });
210
+ }
211
+ return;
212
+ }
213
+ for (const key of keys) {
214
+ if (key === SERIALIZATION_TAG) {
215
+ const where = path.length === 0 ? "<root>" : path.map((p) => JSON.stringify(p)).join(" > ");
216
+ throw new Error(
217
+ `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.`
218
+ );
219
+ }
220
+ walk(obj[key], [...path, key], out);
221
+ }
222
+ }
223
+
224
+ export {
225
+ FiregraphError,
226
+ NodeNotFoundError,
227
+ EdgeNotFoundError,
228
+ ValidationError,
229
+ RegistryViolationError,
230
+ InvalidQueryError,
231
+ TraversalError,
232
+ DynamicRegistryError,
233
+ QuerySafetyError,
234
+ RegistryScopeError,
235
+ MigrationError,
236
+ CrossBackendTransactionError,
237
+ DELETE_FIELD,
238
+ deleteField,
239
+ isDeleteSentinel,
240
+ assertUpdatePayloadExclusive,
241
+ assertNoDeleteSentinels,
242
+ assertSafePath,
243
+ flattenPatch
244
+ };
245
+ //# sourceMappingURL=chunk-AWW4MUJ5.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/errors.ts","../src/internal/write-plan.ts"],"sourcesContent":["export class FiregraphError extends Error {\n constructor(\n message: string,\n public readonly code: string,\n ) {\n super(message);\n this.name = 'FiregraphError';\n }\n}\n\nexport class NodeNotFoundError extends FiregraphError {\n constructor(uid: string) {\n super(`Node not found: ${uid}`, 'NODE_NOT_FOUND');\n this.name = 'NodeNotFoundError';\n }\n}\n\nexport class EdgeNotFoundError extends FiregraphError {\n constructor(aUid: string, axbType: string, bUid: string) {\n super(`Edge not found: ${aUid} -[${axbType}]-> ${bUid}`, 'EDGE_NOT_FOUND');\n this.name = 'EdgeNotFoundError';\n }\n}\n\nexport class ValidationError extends FiregraphError {\n constructor(\n message: string,\n public readonly details?: unknown,\n ) {\n super(message, 'VALIDATION_ERROR');\n this.name = 'ValidationError';\n }\n}\n\nexport class RegistryViolationError extends FiregraphError {\n constructor(aType: string, axbType: string, bType: string) {\n super(`Unregistered triple: (${aType}) -[${axbType}]-> (${bType})`, 'REGISTRY_VIOLATION');\n this.name = 'RegistryViolationError';\n }\n}\n\nexport class InvalidQueryError extends FiregraphError {\n constructor(message: string) {\n super(message, 'INVALID_QUERY');\n this.name = 'InvalidQueryError';\n }\n}\n\nexport class TraversalError extends FiregraphError {\n constructor(message: string) {\n super(message, 'TRAVERSAL_ERROR');\n this.name = 'TraversalError';\n }\n}\n\nexport class DynamicRegistryError extends FiregraphError {\n constructor(message: string) {\n super(message, 'DYNAMIC_REGISTRY_ERROR');\n this.name = 'DynamicRegistryError';\n }\n}\n\nexport class QuerySafetyError extends FiregraphError {\n constructor(message: string) {\n super(message, 'QUERY_SAFETY');\n this.name = 'QuerySafetyError';\n }\n}\n\nexport class RegistryScopeError extends FiregraphError {\n constructor(\n aType: string,\n axbType: string,\n bType: string,\n scopePath: string,\n allowedIn: string[],\n ) {\n super(\n `Type (${aType}) -[${axbType}]-> (${bType}) is not allowed at scope \"${scopePath || 'root'}\". ` +\n `Allowed in: [${allowedIn.join(', ')}]`,\n 'REGISTRY_SCOPE',\n );\n this.name = 'RegistryScopeError';\n }\n}\n\nexport class MigrationError extends FiregraphError {\n constructor(message: string) {\n super(message, 'MIGRATION_ERROR');\n this.name = 'MigrationError';\n }\n}\n\n/**\n * Thrown when a caller tries to perform an operation that would require\n * atomicity across two physical storage backends — e.g. opening a routed\n * subgraph client from inside a transaction callback. Cross-backend\n * atomicity cannot be honoured by real-world storage engines (Firestore,\n * SQLite drivers over D1/DO/better-sqlite3, etc.), so firegraph surfaces\n * this as a typed error instead of silently confining the write to the\n * base backend.\n *\n * Normally `TransactionBackend` and `BatchBackend` don't expose `subgraph()`\n * at the type level, so this error is unreachable through well-typed code.\n * It exists as a public catchable type for app code that needs to tolerate\n * this case deliberately (e.g. dynamic code paths that bypass the type\n * system) and as future-proofing if the interface ever grows a way to\n * request a sub-scope inside a transaction.\n */\nexport class CrossBackendTransactionError extends FiregraphError {\n constructor(message: string) {\n super(message, 'CROSS_BACKEND_TRANSACTION');\n this.name = 'CrossBackendTransactionError';\n }\n}\n","/**\n * Write-plan helper — flattens partial-update payloads into a list of\n * deep-path operations every backend can execute identically.\n *\n * Background: firegraph used to ship two write semantics that quietly\n * disagreed about depth.\n * - `putNode`/`putEdge` did a full document replace.\n * - `updateNode`/`updateEdge` did a one-level shallow merge: top-level\n * keys were preserved, but nested objects were replaced wholesale.\n *\n * Both behaviours dropped sibling keys silently. The 0.12 contract is that\n * `put*` and `update*` deep-merge by default (sibling keys at any depth\n * survive); `replace*` is the explicit escape hatch.\n *\n * `flattenPatch` walks a partial-update payload and emits one\n * {@link DataPathOp} per terminal value. Plain objects recurse; arrays,\n * primitives, Firestore special types, and tagged firegraph-serialization\n * objects are terminal (replaced as a unit). `undefined` values are\n * skipped; `null` is preserved as a real `null` write; the\n * {@link DELETE_FIELD} sentinel marks a field for removal.\n *\n * The output is deliberately backend-agnostic. Each backend translates ops\n * into its native dialect:\n * - Firestore: dotted field path → `data.a.b.c` for `update()`.\n * - SQLite / DO SQLite: `json_set(data, '$.a.b.c', ?)` /\n * `json_remove(data, '$.a.b.c')`.\n */\n\nimport { isTaggedValue, SERIALIZATION_TAG } from './serialization-tag.js';\n\n// ---------------------------------------------------------------------------\n// Public sentinel\n// ---------------------------------------------------------------------------\n\n/**\n * Sentinel returned by {@link deleteField}. Treated by all backends as\n * \"remove this field from the stored document\".\n *\n * Equivalent to Firestore's `FieldValue.delete()`, but works for SQLite\n * backends too. Use inside `updateNode`/`updateEdge` payloads.\n */\nexport const DELETE_FIELD: unique symbol = Symbol.for('firegraph.deleteField');\nexport type DeleteSentinel = typeof DELETE_FIELD;\n\n/**\n * Returns the firegraph delete sentinel. Place this anywhere in an\n * `updateNode`/`updateEdge` payload to remove the corresponding field.\n *\n * ```ts\n * await client.updateNode('tour', uid, {\n * attrs: { obsoleteFlag: deleteField() },\n * });\n * ```\n */\nexport function deleteField(): DeleteSentinel {\n return DELETE_FIELD;\n}\n\n/** Type guard for the delete sentinel. */\nexport function isDeleteSentinel(value: unknown): value is DeleteSentinel {\n return value === DELETE_FIELD;\n}\n\n// ---------------------------------------------------------------------------\n// Terminal-detection helpers\n// ---------------------------------------------------------------------------\n\nconst FIRESTORE_TERMINAL_CTOR = new Set([\n 'Timestamp',\n 'GeoPoint',\n 'VectorValue',\n 'DocumentReference',\n 'FieldValue',\n 'NumericIncrementTransform',\n 'ArrayUnionTransform',\n 'ArrayRemoveTransform',\n 'ServerTimestampTransform',\n 'DeleteTransform',\n]);\n\n/**\n * Should this value be written as a single terminal op (no recursion)?\n *\n * Plain JS objects (constructor === Object, or no prototype) are recursed.\n * Everything else — arrays, primitives, class instances, Firestore special\n * types, tagged serialization payloads — is terminal.\n */\nexport function isTerminalValue(value: unknown): boolean {\n if (value === null) return true;\n const t = typeof value;\n if (t !== 'object') return true;\n if (Array.isArray(value)) return true;\n // Tagged serialization payloads carry the SERIALIZATION_TAG sentinel and\n // should be persisted whole — never split into per-field ops.\n if (isTaggedValue(value)) return true;\n const proto = Object.getPrototypeOf(value);\n if (proto === null || proto === Object.prototype) return false;\n // Class instances — Firestore types or anything else exotic.\n const ctor = (value as { constructor?: { name?: string } }).constructor;\n if (ctor && typeof ctor.name === 'string' && FIRESTORE_TERMINAL_CTOR.has(ctor.name)) return true;\n // Unknown class instance: treat as terminal. Recursing into a class\n // instance is almost always wrong (Map, Set, Date, Buffer...).\n return true;\n}\n\n// ---------------------------------------------------------------------------\n// Core type\n// ---------------------------------------------------------------------------\n\n/**\n * Single terminal write operation produced by {@link flattenPatch}.\n *\n * `path` is a non-empty array of plain object keys. `value` is the value to\n * write; ignored when `delete` is `true`. Arrays / primitives / Firestore\n * special types appear here as whole terminal values.\n */\nexport interface DataPathOp {\n path: readonly string[];\n value: unknown;\n delete: boolean;\n}\n\n// ---------------------------------------------------------------------------\n// Path-segment validation\n// ---------------------------------------------------------------------------\n\n/**\n * Object keys that are safe to embed in SQLite `json_set`/`json_remove`\n * paths. The SQLite backend uses an allowlist regex too — keep these in\n * sync (see `JSON_PATH_KEY_RE` in `internal/sqlite-sql.ts` and\n * `cloudflare/sql.ts`).\n *\n * Allows: ASCII letters, digits, `_`, `-`. Must start with a letter or\n * underscore. This rejects keys containing dots, brackets, quotes, or\n * non-ASCII characters that could break path parsing or be used to\n * inject into the path expression.\n */\nconst SAFE_KEY_RE = /^[A-Za-z_][A-Za-z0-9_-]*$/;\n\n/**\n * Mutual-exclusion guard for {@link UpdatePayload}. The two branches of the\n * shape — `dataOps` (deep-merge) and `replaceData` (full replace) — are\n * structurally incompatible: combining them would tell the backend to\n * simultaneously merge AND wipe, and the three backends disagree on which\n * wins. This helper centralises the runtime check so all three backends\n * trip the same error.\n *\n * Imported as a runtime check from `firestore-backend`, `sqlite-sql`, and\n * `cloudflare/sql`. Backend authors implementing the public `StorageBackend`\n * contract should call it too.\n */\nexport function assertUpdatePayloadExclusive(update: {\n dataOps?: unknown;\n replaceData?: unknown;\n}): void {\n if (update.replaceData !== undefined && update.dataOps !== undefined) {\n throw new Error(\n 'firegraph: UpdatePayload cannot specify both `replaceData` and `dataOps`. ' +\n 'Use one or the other — `replaceData` is the migration-write-back form, ' +\n '`dataOps` is the standard partial-update form.',\n );\n }\n}\n\n/**\n * Reject `DELETE_FIELD` sentinels in payloads where field deletion isn't a\n * meaningful operation: full-document replace (`replaceNode`/`replaceEdge`)\n * and the merge-default put surface (`putNode`/`putEdge`).\n *\n * Why both:\n * - In **replace**, the entire `data` field is overwritten. A delete\n * sentinel in that payload either silently disappears (Firestore drops\n * the Symbol during `.set()` serialization) or produces an empty SQLite\n * `json_remove` no-op, depending on backend. Either way the caller's\n * intent — \"remove field X\" — is lost. Use `updateNode` instead.\n * - In **put** (merge mode), behaviour diverges across backends today:\n * SQLite's flattenPatch emits a real delete op, but Firestore's\n * `.set(..., {merge: true})` silently drops the Symbol. Until that's\n * fixed end-to-end, the safest contract is to reject sentinels at the\n * entry point and steer callers to `updateNode`.\n *\n * The walk mirrors `flattenPatch`: plain objects recurse, everything else\n * is terminal. Tagged serialization payloads short-circuit so we don't\n * recurse into the `__firegraph_ser__` envelope.\n */\nexport function assertNoDeleteSentinels(data: unknown, callerLabel: string): void {\n walkForDeleteSentinels(data, [], { kind: 'root' }, ({ path }) => {\n const where = path.length === 0 ? '<root>' : path.map((p) => JSON.stringify(p)).join(' > ');\n throw new Error(\n `firegraph: ${callerLabel} payload contains a deleteField() sentinel at ${where}. ` +\n `deleteField() is only valid inside updateNode/updateEdge — full-data ` +\n `writes (put*, replace*) cannot delete individual fields. Use updateNode ` +\n `with a deleteField() value, or omit the field from the replace payload.`,\n );\n });\n}\n\ntype SentinelParent = { kind: 'root' } | { kind: 'object' } | { kind: 'array'; index: number };\n\nfunction walkForDeleteSentinels(\n node: unknown,\n path: readonly string[],\n parent: SentinelParent,\n visit: (ctx: { path: readonly string[]; parent: SentinelParent }) => void,\n): void {\n if (node === null || node === undefined) return;\n if (isDeleteSentinel(node)) {\n visit({ path, parent });\n return;\n }\n if (typeof node !== 'object') return;\n if (isTaggedValue(node)) return;\n if (Array.isArray(node)) {\n for (let i = 0; i < node.length; i++) {\n walkForDeleteSentinels(node[i], [...path, String(i)], { kind: 'array', index: i }, visit);\n }\n return;\n }\n const proto = Object.getPrototypeOf(node);\n if (proto !== null && proto !== Object.prototype) return;\n const obj = node as Record<string, unknown>;\n for (const key of Object.keys(obj)) {\n walkForDeleteSentinels(obj[key], [...path, key], { kind: 'object' }, visit);\n }\n}\n\n/** Throws if any path segment in the patch is unsafe for SQLite paths. */\nexport function assertSafePath(path: readonly string[]): void {\n for (const seg of path) {\n if (!SAFE_KEY_RE.test(seg)) {\n throw new Error(\n `firegraph: unsafe object key ${JSON.stringify(seg)} at path ${path\n .map((p) => JSON.stringify(p))\n .join(' > ')}. Keys used inside update payloads must match ` +\n `/^[A-Za-z_][A-Za-z0-9_-]*$/ so they can be embedded safely in ` +\n `SQLite JSON paths.`,\n );\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// flattenPatch\n// ---------------------------------------------------------------------------\n\n/**\n * Flatten a partial-update payload into a list of terminal {@link DataPathOp}s.\n *\n * Rules:\n * - Plain objects (no prototype or `Object.prototype`) recurse — each\n * key becomes another path segment.\n * - Arrays are terminal: writing `{tags: ['a']}` overwrites the whole\n * `tags` array. Element-wise array merging is intentionally NOT\n * supported — it's almost never what callers actually want, and\n * Firestore `arrayUnion`/`arrayRemove` give precise semantics when\n * they are.\n * - `undefined` values are skipped (no op generated). Use\n * {@link deleteField} if you actually want to remove a field.\n * - `null` is preserved verbatim — emits a terminal op with `value: null`.\n * - {@link DELETE_FIELD} produces an op with `delete: true`.\n * - Firestore special types and tagged serialization payloads are terminal.\n * - Class instances are terminal.\n *\n * Throws if any object key on the recursion path is unsafe (see\n * {@link assertSafePath}).\n */\nexport function flattenPatch(data: Record<string, unknown>): DataPathOp[] {\n const ops: DataPathOp[] = [];\n walk(data, [], ops);\n return ops;\n}\n\nfunction assertNoDeleteSentinelsInArrayValue(\n arr: readonly unknown[],\n arrayPath: readonly string[],\n): void {\n walkForDeleteSentinels(arr, arrayPath, { kind: 'root' }, ({ parent }) => {\n const arrayPathStr =\n arrayPath.length === 0 ? '<root>' : arrayPath.map((p) => JSON.stringify(p)).join(' > ');\n if (parent.kind === 'array') {\n throw new Error(\n `firegraph: deleteField() sentinel at index ${parent.index} inside an array at ` +\n `path ${arrayPathStr}. Arrays are ` +\n `terminal in update payloads (replaced as a unit), so the sentinel ` +\n `would be silently dropped by JSON serialization. To remove the ` +\n `field entirely, pass deleteField() in place of the whole array.`,\n );\n }\n throw new Error(\n `firegraph: deleteField() sentinel inside an array element at ` +\n `path ${arrayPathStr}. ` +\n `Arrays are terminal in update payloads — the sentinel would ` +\n `be silently dropped by JSON serialization.`,\n );\n });\n}\n\nfunction walk(node: unknown, path: string[], out: DataPathOp[]): void {\n // Caller guarantees the root is a plain object; this branch only\n // matters for recursion.\n if (node === undefined) return;\n if (isDeleteSentinel(node)) {\n if (path.length === 0) {\n throw new Error('firegraph: deleteField() cannot be the entire update payload.');\n }\n assertSafePath(path);\n out.push({ path: [...path], value: undefined, delete: true });\n return;\n }\n if (isTerminalValue(node)) {\n if (path.length === 0) {\n // `null` / array / primitive at the root is illegal — patches must\n // describe per-key changes.\n throw new Error(\n 'firegraph: update payload must be a plain object. Got ' +\n (node === null ? 'null' : Array.isArray(node) ? 'array' : typeof node) +\n '.',\n );\n }\n // A DELETE_FIELD sentinel embedded inside an array (which is terminal\n // and replaced as a unit) would silently disappear: JSON.stringify drops\n // Symbols, and Firestore's serializer does likewise. Reject loudly so\n // the divergence between \"user wrote a delete\" and \"field stayed put\"\n // can't happen.\n if (Array.isArray(node)) {\n assertNoDeleteSentinelsInArrayValue(node, path);\n }\n assertSafePath(path);\n out.push({ path: [...path], value: node, delete: false });\n return;\n }\n // Plain object: recurse into its own enumerable keys.\n const obj = node as Record<string, unknown>;\n const keys = Object.keys(obj);\n if (keys.length === 0) {\n // Empty object at non-root: emit terminal op so an empty object can\n // be written explicitly when the caller really wants one. Skip at\n // the root — no-op patches should produce no ops.\n if (path.length > 0) {\n assertSafePath(path);\n out.push({ path: [...path], value: {}, delete: false });\n }\n return;\n }\n for (const key of keys) {\n if (key === SERIALIZATION_TAG) {\n const where = path.length === 0 ? '<root>' : path.map((p) => JSON.stringify(p)).join(' > ');\n throw new Error(\n `firegraph: update payload contains a literal \\`${SERIALIZATION_TAG}\\` key at ` +\n `${where}. That key is reserved for firegraph's serialization envelope and ` +\n `cannot appear on a plain object in user data. Use a different field name, ` +\n `or pass a recognized tagged value through replaceNode/replaceEdge instead.`,\n );\n }\n walk(obj[key], [...path, key], out);\n }\n}\n"],"mappings":"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","names":[]}
@@ -1,13 +1,11 @@
1
+ import {
2
+ SERIALIZATION_TAG,
3
+ isTaggedValue
4
+ } from "./chunk-EQJUUVFG.js";
5
+
1
6
  // src/serialization.ts
2
7
  import { FieldValue, GeoPoint, Timestamp } from "@google-cloud/firestore";
3
- var SERIALIZATION_TAG = "__firegraph_ser__";
4
- var KNOWN_TYPES = /* @__PURE__ */ new Set(["Timestamp", "GeoPoint", "VectorValue", "DocumentReference"]);
5
8
  var _docRefWarned = false;
6
- function isTaggedValue(value) {
7
- if (value === null || typeof value !== "object") return false;
8
- const tag = value[SERIALIZATION_TAG];
9
- return typeof tag === "string" && KNOWN_TYPES.has(tag);
10
- }
11
9
  function isTimestamp(value) {
12
10
  return value instanceof Timestamp;
13
11
  }
@@ -110,9 +108,7 @@ function deserializeValue(value, db) {
110
108
  }
111
109
 
112
110
  export {
113
- SERIALIZATION_TAG,
114
- isTaggedValue,
115
111
  serializeFirestoreTypes,
116
112
  deserializeFirestoreTypes
117
113
  };
118
- //# sourceMappingURL=chunk-5753Y42M.js.map
114
+ //# sourceMappingURL=chunk-C2QMD7RY.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/serialization.ts"],"sourcesContent":["/**\n * Firestore-aware serialization for the sandbox migration pipeline.\n *\n * Firestore documents can contain special types (Timestamp, GeoPoint,\n * VectorValue, DocumentReference) that don't survive plain JSON\n * round-tripping. This module provides tagged serialization: Firestore\n * types are wrapped in tagged plain objects before JSON marshaling and\n * reconstructed after.\n *\n * Only used by the `defaultExecutor` sandbox path. Static migrations\n * (in-memory functions) receive raw Firestore objects directly.\n */\n\nimport type { DocumentReference, Firestore } from '@google-cloud/firestore';\nimport { FieldValue, GeoPoint, Timestamp } from '@google-cloud/firestore';\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\n// SERIALIZATION_TAG and isTaggedValue live in `internal/serialization-tag.ts`\n// so Workers-facing code (e.g. `src/internal/write-plan.ts` and the\n// `firegraph/cloudflare` bundle) can recognise tagged values without\n// pulling in `@google-cloud/firestore`. Re-exported here so callers that\n// already import from `src/serialization.ts` keep working.\nexport { isTaggedValue, SERIALIZATION_TAG } from './internal/serialization-tag.js';\nimport { isTaggedValue, SERIALIZATION_TAG } from './internal/serialization-tag.js';\n\n// One-time warning for DocumentReference deserialization without db\nlet _docRefWarned = false;\n\n// ---------------------------------------------------------------------------\n// Detection helpers\n// ---------------------------------------------------------------------------\n\nfunction isTimestamp(value: unknown): value is Timestamp {\n return value instanceof Timestamp;\n}\n\nfunction isGeoPoint(value: unknown): value is GeoPoint {\n return value instanceof GeoPoint;\n}\n\nfunction isDocumentReference(value: unknown): value is DocumentReference {\n // Duck-type check: DocumentReference has path (string) and firestore properties\n if (value === null || typeof value !== 'object') return false;\n const v = value as Record<string, unknown>;\n return (\n typeof v.path === 'string' &&\n v.firestore !== undefined &&\n typeof v.id === 'string' &&\n v.constructor?.name === 'DocumentReference'\n );\n}\n\nfunction isVectorValue(value: unknown): boolean {\n if (value === null || typeof value !== 'object') return false;\n const v = value as Record<string, unknown>;\n return (\n v.constructor?.name === 'VectorValue' && Array.isArray((v as Record<string, unknown>)._values)\n );\n}\n\n// ---------------------------------------------------------------------------\n// Serialize\n// ---------------------------------------------------------------------------\n\n/**\n * Recursively walk a data object and replace Firestore types with tagged\n * plain objects suitable for JSON serialization.\n *\n * Returns a new object tree — the input is never mutated.\n */\nexport function serializeFirestoreTypes(data: Record<string, unknown>): Record<string, unknown> {\n return serializeValue(data) as Record<string, unknown>;\n}\n\nfunction serializeValue(value: unknown): unknown {\n // Primitives\n if (value === null || value === undefined) return value;\n if (typeof value !== 'object') return value;\n\n // Firestore types (check before generic object/array)\n if (isTimestamp(value)) {\n return {\n [SERIALIZATION_TAG]: 'Timestamp',\n seconds: value.seconds,\n nanoseconds: value.nanoseconds,\n };\n }\n if (isGeoPoint(value)) {\n return {\n [SERIALIZATION_TAG]: 'GeoPoint',\n latitude: value.latitude,\n longitude: value.longitude,\n };\n }\n if (isDocumentReference(value)) {\n return { [SERIALIZATION_TAG]: 'DocumentReference', path: (value as DocumentReference).path };\n }\n if (isVectorValue(value)) {\n // Prefer toArray() (public API) over _values (private internal property)\n const v = value as Record<string, unknown>;\n const values =\n typeof v.toArray === 'function' ? (v.toArray as () => number[])() : (v._values as number[]);\n return { [SERIALIZATION_TAG]: 'VectorValue', values: [...values] };\n }\n\n // Arrays\n if (Array.isArray(value)) {\n return value.map(serializeValue);\n }\n\n // Plain objects — recurse\n const result: Record<string, unknown> = {};\n for (const key of Object.keys(value as Record<string, unknown>)) {\n result[key] = serializeValue((value as Record<string, unknown>)[key]);\n }\n return result;\n}\n\n// ---------------------------------------------------------------------------\n// Deserialize\n// ---------------------------------------------------------------------------\n\n/**\n * Recursively walk a data object and reconstruct Firestore types from\n * tagged plain objects.\n *\n * @param data - The data to deserialize (typically from JSON.parse)\n * @param db - Optional Firestore instance for DocumentReference reconstruction.\n * If not provided, tagged DocumentReferences are left as-is with a one-time warning.\n *\n * Returns a new object tree — the input is never mutated.\n */\nexport function deserializeFirestoreTypes(\n data: Record<string, unknown>,\n db?: Firestore,\n): Record<string, unknown> {\n return deserializeValue(data, db) as Record<string, unknown>;\n}\n\nfunction deserializeValue(value: unknown, db?: Firestore): unknown {\n if (value === null || value === undefined) return value;\n if (typeof value !== 'object') return value;\n\n // Short-circuit for values that are already real Firestore types.\n // This makes deserializeFirestoreTypes idempotent — safe to call on data\n // that has already been deserialized (e.g., write-back after defaultExecutor\n // already reconstructed types, or static migrations that return raw types).\n if (\n isTimestamp(value) ||\n isGeoPoint(value) ||\n isDocumentReference(value) ||\n isVectorValue(value)\n ) {\n return value;\n }\n\n // Arrays\n if (Array.isArray(value)) {\n return value.map((v) => deserializeValue(v, db));\n }\n\n const obj = value as Record<string, unknown>;\n\n // Check for tagged Firestore type\n if (isTaggedValue(obj)) {\n const tag = obj[SERIALIZATION_TAG] as string;\n\n switch (tag) {\n case 'Timestamp':\n // Validate expected fields before reconstruction\n if (typeof obj.seconds !== 'number' || typeof obj.nanoseconds !== 'number') return obj;\n return new Timestamp(obj.seconds, obj.nanoseconds);\n\n case 'GeoPoint':\n if (typeof obj.latitude !== 'number' || typeof obj.longitude !== 'number') return obj;\n return new GeoPoint(obj.latitude, obj.longitude);\n\n case 'VectorValue':\n if (!Array.isArray(obj.values)) return obj;\n return FieldValue.vector(obj.values as number[]);\n\n case 'DocumentReference':\n if (typeof obj.path !== 'string') return obj;\n if (db) {\n return db.doc(obj.path);\n }\n // No db available — leave as tagged object with one-time warning\n if (!_docRefWarned) {\n _docRefWarned = true;\n console.warn(\n '[firegraph] DocumentReference encountered during migration deserialization ' +\n 'but no Firestore instance available. The reference will remain as a tagged ' +\n 'object with its path. Enable write-back for full reconstruction.',\n );\n }\n return obj;\n\n default:\n // Unknown tag — leave as-is (forward compatibility)\n return obj;\n }\n }\n\n // Plain object — recurse\n const result: Record<string, unknown> = {};\n for (const key of Object.keys(obj)) {\n result[key] = deserializeValue(obj[key], db);\n }\n return result;\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,14 @@
1
+ // src/internal/serialization-tag.ts
2
+ var SERIALIZATION_TAG = "__firegraph_ser__";
3
+ var KNOWN_TYPES = /* @__PURE__ */ new Set(["Timestamp", "GeoPoint", "VectorValue", "DocumentReference"]);
4
+ function isTaggedValue(value) {
5
+ if (value === null || typeof value !== "object") return false;
6
+ const tag = value[SERIALIZATION_TAG];
7
+ return typeof tag === "string" && KNOWN_TYPES.has(tag);
8
+ }
9
+
10
+ export {
11
+ SERIALIZATION_TAG,
12
+ isTaggedValue
13
+ };
14
+ //# sourceMappingURL=chunk-EQJUUVFG.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/internal/serialization-tag.ts"],"sourcesContent":["/**\n * Firegraph serialization tag — split from `src/serialization.ts` so it can\n * be imported from Workers-facing code without dragging in\n * `@google-cloud/firestore`.\n *\n * The full serialization module (with Timestamp/GeoPoint round-tripping)\n * lives one folder up because the sandbox migration pipeline needs it; the\n * write-plan helper only needs to recognise tagged objects to keep them\n * terminal during patch flattening, so it imports just the tag from here.\n */\n\n/** Sentinel key used to tag serialized Firestore types. */\nexport const SERIALIZATION_TAG = '__firegraph_ser__' as const;\n\nconst KNOWN_TYPES = new Set(['Timestamp', 'GeoPoint', 'VectorValue', 'DocumentReference']);\n\n/** Check if a value is a tagged serialized Firestore type. */\nexport function isTaggedValue(value: unknown): boolean {\n if (value === null || typeof value !== 'object') return false;\n const tag = (value as Record<string, unknown>)[SERIALIZATION_TAG];\n return typeof tag === 'string' && KNOWN_TYPES.has(tag);\n}\n"],"mappings":";AAYO,IAAM,oBAAoB;AAEjC,IAAM,cAAc,oBAAI,IAAI,CAAC,aAAa,YAAY,eAAe,mBAAmB,CAAC;AAGlF,SAAS,cAAc,OAAyB;AACrD,MAAI,UAAU,QAAQ,OAAO,UAAU,SAAU,QAAO;AACxD,QAAM,MAAO,MAAkC,iBAAiB;AAChE,SAAO,OAAO,QAAQ,YAAY,YAAY,IAAI,GAAG;AACvD;","names":[]}
@@ -6,8 +6,10 @@ import {
6
6
  QuerySafetyError,
7
7
  RegistryScopeError,
8
8
  RegistryViolationError,
9
- ValidationError
10
- } from "./chunk-R7CRGYY4.js";
9
+ ValidationError,
10
+ assertNoDeleteSentinels,
11
+ flattenPatch
12
+ } from "./chunk-AWW4MUJ5.js";
11
13
 
12
14
  // src/internal/constants.ts
13
15
  var NODE_RELATION = "is";
@@ -36,23 +38,29 @@ function computeEdgeDocId(aUid, axbType, bUid) {
36
38
  }
37
39
 
38
40
  // src/json-schema.ts
39
- import Ajv from "ajv";
40
- import addFormats from "ajv-formats";
41
- var ajv = new Ajv({ allErrors: true, strict: false });
42
- addFormats(ajv);
41
+ import { Validator } from "@cfworker/json-schema";
42
+ var MAX_RENDERED_ERRORS = 20;
43
43
  function compileSchema(schema, label) {
44
- const validate = ajv.compile(schema);
44
+ const validator = new Validator(schema, "2020-12", false);
45
45
  return (data) => {
46
- if (!validate(data)) {
47
- const errors = validate.errors ?? [];
48
- const messages = errors.map((err) => `${err.instancePath || "/"}${err.message ? ": " + err.message : ""}`).join("; ");
46
+ const result = validator.validate(data);
47
+ if (!result.valid) {
48
+ const total = result.errors.length;
49
+ const head = result.errors.slice(0, MAX_RENDERED_ERRORS).map(formatError).join("; ");
50
+ const overflow = total > MAX_RENDERED_ERRORS ? ` (+${total - MAX_RENDERED_ERRORS} more)` : "";
49
51
  throw new ValidationError(
50
- `Data validation failed${label ? " for " + label : ""}: ${messages}`,
51
- errors
52
+ `Data validation failed${label ? " for " + label : ""}: ${head}${overflow}`,
53
+ result.errors
52
54
  );
53
55
  }
54
56
  };
55
57
  }
58
+ function formatError(err) {
59
+ const path = err.instanceLocation.replace(/^#/, "") || "/";
60
+ const keyword = err.keyword ? `[${err.keyword}] ` : "";
61
+ const detail = err.error ? `: ${keyword}${err.error}` : "";
62
+ return `${path}${detail}`;
63
+ }
56
64
  function jsonSchemaToFieldMeta(schema) {
57
65
  if (!schema || schema.type !== "object" || !schema.properties) return [];
58
66
  const requiredSet = new Set(Array.isArray(schema.required) ? schema.required : []);
@@ -643,7 +651,7 @@ function hashSource(source) {
643
651
  var _serializationModule = null;
644
652
  async function loadSerialization() {
645
653
  if (_serializationModule) return _serializationModule;
646
- _serializationModule = await import("./serialization-ZZ7RSDRX.js");
654
+ _serializationModule = await import("./serialization-OE2PFZMY.js");
647
655
  return _serializationModule;
648
656
  }
649
657
  function defaultExecutor(source) {
@@ -785,8 +793,11 @@ var BOOTSTRAP_ENTRIES = [
785
793
  description: "Meta-type: defines an edge type"
786
794
  }
787
795
  ];
796
+ var _bootstrapRegistry = null;
788
797
  function createBootstrapRegistry() {
789
- return createRegistry([...BOOTSTRAP_ENTRIES]);
798
+ if (_bootstrapRegistry) return _bootstrapRegistry;
799
+ _bootstrapRegistry = createRegistry([...BOOTSTRAP_ENTRIES]);
800
+ return _bootstrapRegistry;
790
801
  }
791
802
  function generateDeterministicUid(metaType, name) {
792
803
  const hash = createHash3("sha256").update(`${metaType}:${name}`).digest("base64url");
@@ -960,6 +971,19 @@ var GraphBatchImpl = class {
960
971
  this.scopePath = scopePath;
961
972
  }
962
973
  async putNode(aType, uid, data) {
974
+ this.writeNode(aType, uid, data, "merge");
975
+ }
976
+ async putEdge(aType, aUid, axbType, bType, bUid, data) {
977
+ this.writeEdge(aType, aUid, axbType, bType, bUid, data, "merge");
978
+ }
979
+ async replaceNode(aType, uid, data) {
980
+ this.writeNode(aType, uid, data, "replace");
981
+ }
982
+ async replaceEdge(aType, aUid, axbType, bType, bUid, data) {
983
+ this.writeEdge(aType, aUid, axbType, bType, bUid, data, "replace");
984
+ }
985
+ writeNode(aType, uid, data, mode) {
986
+ assertNoDeleteSentinels(data, mode === "replace" ? "replaceNode" : "putNode");
963
987
  if (this.registry) {
964
988
  this.registry.validate(aType, NODE_RELATION, aType, data, this.scopePath);
965
989
  }
@@ -971,9 +995,10 @@ var GraphBatchImpl = class {
971
995
  record.v = entry.schemaVersion;
972
996
  }
973
997
  }
974
- this.backend.setDoc(docId, record);
998
+ this.backend.setDoc(docId, record, mode);
975
999
  }
976
- async putEdge(aType, aUid, axbType, bType, bUid, data) {
1000
+ writeEdge(aType, aUid, axbType, bType, bUid, data, mode) {
1001
+ assertNoDeleteSentinels(data, mode === "replace" ? "replaceEdge" : "putEdge");
977
1002
  if (this.registry) {
978
1003
  this.registry.validate(aType, axbType, bType, data, this.scopePath);
979
1004
  }
@@ -985,11 +1010,15 @@ var GraphBatchImpl = class {
985
1010
  record.v = entry.schemaVersion;
986
1011
  }
987
1012
  }
988
- this.backend.setDoc(docId, record);
1013
+ this.backend.setDoc(docId, record, mode);
989
1014
  }
990
1015
  async updateNode(uid, data) {
991
1016
  const docId = computeNodeDocId(uid);
992
- this.backend.updateDoc(docId, { dataFields: data });
1017
+ this.backend.updateDoc(docId, { dataOps: flattenPatch(data) });
1018
+ }
1019
+ async updateEdge(aUid, axbType, bUid, data) {
1020
+ const docId = computeEdgeDocId(aUid, axbType, bUid);
1021
+ this.backend.updateDoc(docId, { dataOps: flattenPatch(data) });
993
1022
  }
994
1023
  async removeNode(uid) {
995
1024
  const docId = computeNodeDocId(uid);
@@ -1098,6 +1127,19 @@ var GraphTransactionImpl = class {
1098
1127
  return results.map((r) => r.record);
1099
1128
  }
1100
1129
  async putNode(aType, uid, data) {
1130
+ await this.writeNode(aType, uid, data, "merge");
1131
+ }
1132
+ async putEdge(aType, aUid, axbType, bType, bUid, data) {
1133
+ await this.writeEdge(aType, aUid, axbType, bType, bUid, data, "merge");
1134
+ }
1135
+ async replaceNode(aType, uid, data) {
1136
+ await this.writeNode(aType, uid, data, "replace");
1137
+ }
1138
+ async replaceEdge(aType, aUid, axbType, bType, bUid, data) {
1139
+ await this.writeEdge(aType, aUid, axbType, bType, bUid, data, "replace");
1140
+ }
1141
+ async writeNode(aType, uid, data, mode) {
1142
+ assertNoDeleteSentinels(data, mode === "replace" ? "replaceNode" : "putNode");
1101
1143
  if (this.registry) {
1102
1144
  this.registry.validate(aType, NODE_RELATION, aType, data, this.scopePath);
1103
1145
  }
@@ -1109,9 +1151,10 @@ var GraphTransactionImpl = class {
1109
1151
  record.v = entry.schemaVersion;
1110
1152
  }
1111
1153
  }
1112
- await this.backend.setDoc(docId, record);
1154
+ await this.backend.setDoc(docId, record, mode);
1113
1155
  }
1114
- async putEdge(aType, aUid, axbType, bType, bUid, data) {
1156
+ async writeEdge(aType, aUid, axbType, bType, bUid, data, mode) {
1157
+ assertNoDeleteSentinels(data, mode === "replace" ? "replaceEdge" : "putEdge");
1115
1158
  if (this.registry) {
1116
1159
  this.registry.validate(aType, axbType, bType, data, this.scopePath);
1117
1160
  }
@@ -1123,11 +1166,15 @@ var GraphTransactionImpl = class {
1123
1166
  record.v = entry.schemaVersion;
1124
1167
  }
1125
1168
  }
1126
- await this.backend.setDoc(docId, record);
1169
+ await this.backend.setDoc(docId, record, mode);
1127
1170
  }
1128
1171
  async updateNode(uid, data) {
1129
1172
  const docId = computeNodeDocId(uid);
1130
- await this.backend.updateDoc(docId, { dataFields: data });
1173
+ await this.backend.updateDoc(docId, { dataOps: flattenPatch(data) });
1174
+ }
1175
+ async updateEdge(aUid, axbType, bUid, data) {
1176
+ const docId = computeEdgeDocId(aUid, axbType, bUid);
1177
+ await this.backend.updateDoc(docId, { dataOps: flattenPatch(data) });
1131
1178
  }
1132
1179
  async removeNode(uid) {
1133
1180
  const docId = computeNodeDocId(uid);
@@ -1326,6 +1373,19 @@ var GraphClientImpl = class _GraphClientImpl {
1326
1373
  // GraphWriter
1327
1374
  // ---------------------------------------------------------------------------
1328
1375
  async putNode(aType, uid, data) {
1376
+ await this.writeNode(aType, uid, data, "merge");
1377
+ }
1378
+ async putEdge(aType, aUid, axbType, bType, bUid, data) {
1379
+ await this.writeEdge(aType, aUid, axbType, bType, bUid, data, "merge");
1380
+ }
1381
+ async replaceNode(aType, uid, data) {
1382
+ await this.writeNode(aType, uid, data, "replace");
1383
+ }
1384
+ async replaceEdge(aType, aUid, axbType, bType, bUid, data) {
1385
+ await this.writeEdge(aType, aUid, axbType, bType, bUid, data, "replace");
1386
+ }
1387
+ async writeNode(aType, uid, data, mode) {
1388
+ assertNoDeleteSentinels(data, mode === "replace" ? "replaceNode" : "putNode");
1329
1389
  const registry = this.getRegistryForType(aType);
1330
1390
  if (registry) {
1331
1391
  registry.validate(aType, NODE_RELATION, aType, data, this.backend.scopePath);
@@ -1339,9 +1399,10 @@ var GraphClientImpl = class _GraphClientImpl {
1339
1399
  record.v = entry.schemaVersion;
1340
1400
  }
1341
1401
  }
1342
- await backend.setDoc(docId, record);
1402
+ await backend.setDoc(docId, record, mode);
1343
1403
  }
1344
- async putEdge(aType, aUid, axbType, bType, bUid, data) {
1404
+ async writeEdge(aType, aUid, axbType, bType, bUid, data, mode) {
1405
+ assertNoDeleteSentinels(data, mode === "replace" ? "replaceEdge" : "putEdge");
1345
1406
  const registry = this.getRegistryForType(aType);
1346
1407
  if (registry) {
1347
1408
  registry.validate(aType, axbType, bType, data, this.backend.scopePath);
@@ -1355,11 +1416,15 @@ var GraphClientImpl = class _GraphClientImpl {
1355
1416
  record.v = entry.schemaVersion;
1356
1417
  }
1357
1418
  }
1358
- await backend.setDoc(docId, record);
1419
+ await backend.setDoc(docId, record, mode);
1359
1420
  }
1360
1421
  async updateNode(uid, data) {
1361
1422
  const docId = computeNodeDocId(uid);
1362
- await this.backend.updateDoc(docId, { dataFields: data });
1423
+ await this.backend.updateDoc(docId, { dataOps: flattenPatch(data) });
1424
+ }
1425
+ async updateEdge(aUid, axbType, bUid, data) {
1426
+ const docId = computeEdgeDocId(aUid, axbType, bUid);
1427
+ await this.backend.updateDoc(docId, { dataOps: flattenPatch(data) });
1363
1428
  }
1364
1429
  async removeNode(uid) {
1365
1430
  const docId = computeNodeDocId(uid);
@@ -1605,6 +1670,12 @@ var DEFAULT_CORE_INDEXES = Object.freeze([
1605
1670
  { fields: ["axbType", "bType"] }
1606
1671
  ]);
1607
1672
 
1673
+ // src/id.ts
1674
+ import { nanoid } from "nanoid";
1675
+ function generateId() {
1676
+ return nanoid();
1677
+ }
1678
+
1608
1679
  export {
1609
1680
  NODE_RELATION,
1610
1681
  DEFAULT_QUERY_LIMIT,
@@ -1638,6 +1709,7 @@ export {
1638
1709
  analyzeQuerySafety,
1639
1710
  GraphClientImpl,
1640
1711
  createGraphClientFromBackend,
1641
- DEFAULT_CORE_INDEXES
1712
+ DEFAULT_CORE_INDEXES,
1713
+ generateId
1642
1714
  };
1643
- //# sourceMappingURL=chunk-6SB34IPQ.js.map
1715
+ //# sourceMappingURL=chunk-HONQY4HF.js.map