@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
package/README.md CHANGED
@@ -101,16 +101,19 @@ const g = createGraphClient(db, 'graph', { registry });
101
101
  ```typescript
102
102
  const tourId = generateId();
103
103
 
104
- // Create or overwrite a node
104
+ // Create or deep-merge a node (sibling keys at any depth survive)
105
105
  await g.putNode('tour', tourId, { name: 'Dolomites Classic' });
106
106
 
107
107
  // Read a node
108
108
  const node = await g.getNode(tourId);
109
109
  // → StoredGraphRecord | null
110
110
 
111
- // Update fields (partial merge into data)
111
+ // Partial update (deep merge into data)
112
112
  await g.updateNode(tourId, { difficulty: 'extreme' });
113
113
 
114
+ // Full replace — discards every prior key not in the new payload
115
+ await g.replaceNode('tour', tourId, { name: 'Dolomites — 2026 Edition' });
116
+
114
117
  // Delete a node
115
118
  await g.removeNode(tourId);
116
119
 
@@ -118,12 +121,19 @@ await g.removeNode(tourId);
118
121
  const tours = await g.findNodes({ aType: 'tour' });
119
122
  ```
120
123
 
124
+ **Write semantics (0.12+):** `putNode`/`putEdge` and `updateNode`/`updateEdge`
125
+ **deep-merge** by default — sibling keys at every nesting depth survive. Use
126
+ `replaceNode`/`replaceEdge` when you want the old "wipe and rewrite" behaviour.
127
+ Arrays are terminal (replaced wholesale, not element-merged); `undefined`
128
+ values are skipped; `null` is preserved verbatim; and the
129
+ [`deleteField()`](#field-deletion) sentinel removes a field at any depth.
130
+
121
131
  ### Edges
122
132
 
123
133
  ```typescript
124
134
  const depId = generateId();
125
135
 
126
- // Create or overwrite an edge
136
+ // Create or deep-merge an edge
127
137
  await g.putEdge('tour', tourId, 'hasDeparture', 'departure', depId, { order: 0 });
128
138
 
129
139
  // Read a specific edge
@@ -133,10 +143,33 @@ const edge = await g.getEdge(tourId, 'hasDeparture', depId);
133
143
  // Check existence
134
144
  const exists = await g.edgeExists(tourId, 'hasDeparture', depId);
135
145
 
146
+ // Partial update (deep merge)
147
+ await g.updateEdge(tourId, 'hasDeparture', depId, { order: 5 });
148
+
149
+ // Full replace — discards every prior key not in the new payload
150
+ await g.replaceEdge('tour', tourId, 'hasDeparture', 'departure', depId, { order: 5 });
151
+
136
152
  // Delete an edge
137
153
  await g.removeEdge(tourId, 'hasDeparture', depId);
138
154
  ```
139
155
 
156
+ ### Field Deletion
157
+
158
+ The `deleteField()` sentinel removes a field from a stored document. It works
159
+ across every backend (Firestore, SQLite, Cloudflare Durable Objects), so
160
+ calling code stays portable:
161
+
162
+ ```typescript
163
+ import { deleteField } from 'firegraph';
164
+
165
+ await g.updateNode(tourId, {
166
+ meta: { deprecatedTag: deleteField() }, // removes meta.deprecatedTag
167
+ });
168
+ ```
169
+
170
+ Equivalent to Firestore's `FieldValue.delete()`, but Workers-safe and
171
+ SQLite-aware.
172
+
140
173
  ### Querying Edges
141
174
 
142
175
  `findEdges` accepts any combination of filters. When all three identifiers (`aUid`, `axbType`, `bUid`) are provided, it uses a direct document lookup instead of a query scan.
@@ -377,8 +410,8 @@ const tour = await g.getNode(tourId);
377
410
 
378
411
  - **Version storage**: The `v` field lives on the record envelope (top-level, alongside `aType`, `data`, etc.), not inside `data`. Records without `v` are treated as version 0 (legacy data).
379
412
  - **Read path**: When a record is read and its `v` is behind the derived version (`max(toVersion)` from migrations), migrations run sequentially to bring data up to the current version.
380
- - **Write path**: When writing via `putNode`/`putEdge`, the record is stamped with `v` equal to the derived version automatically.
381
- - **`updateNode`**: Does not stamp `v` — it is a raw partial update without schema context. The next read re-triggers migration (which is idempotent).
413
+ - **Write path**: When writing via `putNode`/`putEdge` (deep-merge) or `replaceNode`/`replaceEdge` (full overwrite), the record is stamped with `v` equal to the derived version automatically.
414
+ - **`updateNode` / `updateEdge`**: Do not stamp `v` — they are raw partial patches without schema context. The next read re-triggers migration (which is idempotent).
382
415
 
383
416
  #### Write-Back
384
417
 
@@ -694,7 +727,7 @@ All errors extend `FiregraphError` with a `code` property:
694
727
  | `FiregraphError` | varies | Base class |
695
728
  | `NodeNotFoundError` | `NODE_NOT_FOUND` | Node lookup fails (not thrown by `getNode` — it returns `null`) |
696
729
  | `EdgeNotFoundError` | `EDGE_NOT_FOUND` | Edge lookup fails |
697
- | `ValidationError` | `VALIDATION_ERROR` | Schema validation fails (registry + Zod) |
730
+ | `ValidationError` | `VALIDATION_ERROR` | Schema validation fails (registry JSON Schema validation) |
698
731
  | `RegistryViolationError` | `REGISTRY_VIOLATION` | Triple not registered |
699
732
  | `RegistryScopeError` | `REGISTRY_SCOPE` | Type not allowed at this subgraph scope |
700
733
  | `MigrationError` | `MIGRATION_ERROR` | Migration function fails or chain is incomplete |
@@ -711,7 +744,7 @@ try {
711
744
  } catch (err) {
712
745
  if (err instanceof ValidationError) {
713
746
  console.error(err.code); // 'VALIDATION_ERROR'
714
- console.error(err.details); // Zod error details
747
+ console.error(err.details); // OutputUnit[] from @cfworker/json-schema
715
748
  }
716
749
  }
717
750
  ```
@@ -877,14 +910,6 @@ pnpm test:emulator # Full test suite against Firestore emulator
877
910
 
878
911
  Requires Node.js 18+.
879
912
 
880
- ## Releasing
881
-
882
- Versions and npm publishes are automated via [release-please](https://github.com/googleapis/release-please).
883
- Land PRs to `main` with conventional-commit messages; release-please opens a
884
- Release PR that, when merged, tags + publishes to npm. See
885
- [docs/releasing.md](docs/releasing.md) for maintainer setup (NPM token,
886
- workflow permissions) and the full flow.
887
-
888
913
  ## License
889
914
 
890
915
  MIT
@@ -0,0 +1,200 @@
1
+ import { S as StoredGraphRecord, g as QueryFilter, z as QueryOptions, l as GraphReader, m as BulkOptions, C as CascadeResult, F as FindEdgesParams, o as BulkResult } from './types-DxYLy8Ol.js';
2
+
3
+ /**
4
+ * Write-plan helper — flattens partial-update payloads into a list of
5
+ * deep-path operations every backend can execute identically.
6
+ *
7
+ * Background: firegraph used to ship two write semantics that quietly
8
+ * disagreed about depth.
9
+ * - `putNode`/`putEdge` did a full document replace.
10
+ * - `updateNode`/`updateEdge` did a one-level shallow merge: top-level
11
+ * keys were preserved, but nested objects were replaced wholesale.
12
+ *
13
+ * Both behaviours dropped sibling keys silently. The 0.12 contract is that
14
+ * `put*` and `update*` deep-merge by default (sibling keys at any depth
15
+ * survive); `replace*` is the explicit escape hatch.
16
+ *
17
+ * `flattenPatch` walks a partial-update payload and emits one
18
+ * {@link DataPathOp} per terminal value. Plain objects recurse; arrays,
19
+ * primitives, Firestore special types, and tagged firegraph-serialization
20
+ * objects are terminal (replaced as a unit). `undefined` values are
21
+ * skipped; `null` is preserved as a real `null` write; the
22
+ * {@link DELETE_FIELD} sentinel marks a field for removal.
23
+ *
24
+ * The output is deliberately backend-agnostic. Each backend translates ops
25
+ * into its native dialect:
26
+ * - Firestore: dotted field path → `data.a.b.c` for `update()`.
27
+ * - SQLite / DO SQLite: `json_set(data, '$.a.b.c', ?)` /
28
+ * `json_remove(data, '$.a.b.c')`.
29
+ */
30
+ /**
31
+ * Sentinel returned by {@link deleteField}. Treated by all backends as
32
+ * "remove this field from the stored document".
33
+ *
34
+ * Equivalent to Firestore's `FieldValue.delete()`, but works for SQLite
35
+ * backends too. Use inside `updateNode`/`updateEdge` payloads.
36
+ */
37
+ declare const DELETE_FIELD: unique symbol;
38
+ type DeleteSentinel = typeof DELETE_FIELD;
39
+ /**
40
+ * Returns the firegraph delete sentinel. Place this anywhere in an
41
+ * `updateNode`/`updateEdge` payload to remove the corresponding field.
42
+ *
43
+ * ```ts
44
+ * await client.updateNode('tour', uid, {
45
+ * attrs: { obsoleteFlag: deleteField() },
46
+ * });
47
+ * ```
48
+ */
49
+ declare function deleteField(): DeleteSentinel;
50
+ /** Type guard for the delete sentinel. */
51
+ declare function isDeleteSentinel(value: unknown): value is DeleteSentinel;
52
+ /**
53
+ * Single terminal write operation produced by {@link flattenPatch}.
54
+ *
55
+ * `path` is a non-empty array of plain object keys. `value` is the value to
56
+ * write; ignored when `delete` is `true`. Arrays / primitives / Firestore
57
+ * special types appear here as whole terminal values.
58
+ */
59
+ interface DataPathOp {
60
+ path: readonly string[];
61
+ value: unknown;
62
+ delete: boolean;
63
+ }
64
+ /**
65
+ * Flatten a partial-update payload into a list of terminal {@link DataPathOp}s.
66
+ *
67
+ * Rules:
68
+ * - Plain objects (no prototype or `Object.prototype`) recurse — each
69
+ * key becomes another path segment.
70
+ * - Arrays are terminal: writing `{tags: ['a']}` overwrites the whole
71
+ * `tags` array. Element-wise array merging is intentionally NOT
72
+ * supported — it's almost never what callers actually want, and
73
+ * Firestore `arrayUnion`/`arrayRemove` give precise semantics when
74
+ * they are.
75
+ * - `undefined` values are skipped (no op generated). Use
76
+ * {@link deleteField} if you actually want to remove a field.
77
+ * - `null` is preserved verbatim — emits a terminal op with `value: null`.
78
+ * - {@link DELETE_FIELD} produces an op with `delete: true`.
79
+ * - Firestore special types and tagged serialization payloads are terminal.
80
+ * - Class instances are terminal.
81
+ *
82
+ * Throws if any object key on the recursion path is unsafe (see
83
+ * {@link assertSafePath}).
84
+ */
85
+ declare function flattenPatch(data: Record<string, unknown>): DataPathOp[];
86
+
87
+ /**
88
+ * Backend abstraction for firegraph.
89
+ *
90
+ * `StorageBackend` is the single interface every storage driver implements.
91
+ * The Firestore backend wraps `@google-cloud/firestore`; the SQLite backend
92
+ * (shared by D1 and Durable Object SQLite) uses a parameterized SQL executor.
93
+ *
94
+ * `GraphClientImpl` and friends depend only on this interface — they have
95
+ * no direct knowledge of Firestore or SQLite.
96
+ */
97
+
98
+ /**
99
+ * Per-record write payload — backend-agnostic. Timestamps are not present;
100
+ * the backend supplies them via `serverTimestamp()` placeholders that it
101
+ * itself resolves at commit time.
102
+ */
103
+ interface WritableRecord {
104
+ aType: string;
105
+ aUid: string;
106
+ axbType: string;
107
+ bType: string;
108
+ bUid: string;
109
+ data: Record<string, unknown>;
110
+ /** Schema version (set by the writer when registry has migrations). */
111
+ v?: number;
112
+ }
113
+ /**
114
+ * Write semantics for `setDoc`.
115
+ *
116
+ * - `'merge'` — the new contract (0.12+). Existing fields not mentioned
117
+ * in the new data survive; nested objects are recursively merged;
118
+ * arrays are replaced as a unit. This is the default for
119
+ * `putNode` / `putEdge`.
120
+ * - `'replace'` — the document is replaced wholesale, dropping any
121
+ * fields not present in the payload. This is the explicit escape
122
+ * hatch surfaced as `replaceNode` / `replaceEdge` and used by
123
+ * migration write-back.
124
+ */
125
+ type WriteMode = 'merge' | 'replace';
126
+ /**
127
+ * Patch shape for `updateDoc`.
128
+ *
129
+ * - `dataOps`: list of deep-path terminal ops produced by
130
+ * `flattenPatch()` (one op per leaf — arrays / primitives / Firestore
131
+ * special types are terminal). Used by `updateNode` / `updateEdge`.
132
+ * Sibling keys at every depth are preserved.
133
+ * - `replaceData`: full `data` replacement. Used only by the migration
134
+ * write-back path, which has already produced a complete migrated
135
+ * document.
136
+ * - `v`: optional schema-version stamp.
137
+ *
138
+ * `updatedAt` is always set by the backend.
139
+ */
140
+ interface UpdatePayload {
141
+ dataOps?: DataPathOp[];
142
+ replaceData?: Record<string, unknown>;
143
+ v?: number;
144
+ }
145
+ /**
146
+ * Read/write transaction adapter. Mirrors Firestore's transaction semantics:
147
+ * reads are snapshot-consistent; writes are issued inside the transaction
148
+ * and a rejection from any write aborts the surrounding `runTransaction`.
149
+ *
150
+ * Writes return `Promise<void>` so SQL drivers can surface row-level errors
151
+ * (constraint violations, malformed JSON paths) rather than swallowing them.
152
+ * Firestore implementations can resolve synchronously since the underlying
153
+ * `Transaction.set/update/delete` calls are themselves synchronous buffers.
154
+ */
155
+ interface TransactionBackend {
156
+ getDoc(docId: string): Promise<StoredGraphRecord | null>;
157
+ query(filters: QueryFilter[], options?: QueryOptions): Promise<StoredGraphRecord[]>;
158
+ setDoc(docId: string, record: WritableRecord, mode: WriteMode): Promise<void>;
159
+ updateDoc(docId: string, update: UpdatePayload): Promise<void>;
160
+ deleteDoc(docId: string): Promise<void>;
161
+ }
162
+ /**
163
+ * Atomic multi-write batch.
164
+ */
165
+ interface BatchBackend {
166
+ setDoc(docId: string, record: WritableRecord, mode: WriteMode): void;
167
+ updateDoc(docId: string, update: UpdatePayload): void;
168
+ deleteDoc(docId: string): void;
169
+ commit(): Promise<void>;
170
+ }
171
+ /**
172
+ * The single storage abstraction.
173
+ *
174
+ * Each backend instance is scoped to a "graph location" — for Firestore
175
+ * that's a collection path; for SQLite it's a (table, scopePath) pair.
176
+ * `subgraph()` returns a child backend bound to a nested location.
177
+ */
178
+ interface StorageBackend {
179
+ /** Backend-internal location identifier (collection path or table name). */
180
+ readonly collectionPath: string;
181
+ /** Subgraph scope (empty string for root). */
182
+ readonly scopePath: string;
183
+ getDoc(docId: string): Promise<StoredGraphRecord | null>;
184
+ query(filters: QueryFilter[], options?: QueryOptions): Promise<StoredGraphRecord[]>;
185
+ setDoc(docId: string, record: WritableRecord, mode: WriteMode): Promise<void>;
186
+ updateDoc(docId: string, update: UpdatePayload): Promise<void>;
187
+ deleteDoc(docId: string): Promise<void>;
188
+ runTransaction<T>(fn: (tx: TransactionBackend) => Promise<T>): Promise<T>;
189
+ createBatch(): BatchBackend;
190
+ subgraph(parentNodeUid: string, name: string): StorageBackend;
191
+ removeNodeCascade(uid: string, reader: GraphReader, options?: BulkOptions): Promise<CascadeResult>;
192
+ bulkRemoveEdges(params: FindEdgesParams, reader: GraphReader, options?: BulkOptions): Promise<BulkResult>;
193
+ /**
194
+ * Find edges across all subgraphs sharing a given collection name.
195
+ * Optional — backends that can't support this should throw a clear error.
196
+ */
197
+ findEdgesGlobal?(params: FindEdgesParams, collectionName?: string): Promise<StoredGraphRecord[]>;
198
+ }
199
+
200
+ export { type BatchBackend as B, DELETE_FIELD as D, type StorageBackend as S, type TransactionBackend as T, type UpdatePayload as U, type WritableRecord as W, type DataPathOp as a, type WriteMode as b, deleteField as d, flattenPatch as f, isDeleteSentinel as i };
@@ -0,0 +1,200 @@
1
+ import { S as StoredGraphRecord, g as QueryFilter, z as QueryOptions, l as GraphReader, m as BulkOptions, C as CascadeResult, F as FindEdgesParams, o as BulkResult } from './types-DxYLy8Ol.cjs';
2
+
3
+ /**
4
+ * Write-plan helper — flattens partial-update payloads into a list of
5
+ * deep-path operations every backend can execute identically.
6
+ *
7
+ * Background: firegraph used to ship two write semantics that quietly
8
+ * disagreed about depth.
9
+ * - `putNode`/`putEdge` did a full document replace.
10
+ * - `updateNode`/`updateEdge` did a one-level shallow merge: top-level
11
+ * keys were preserved, but nested objects were replaced wholesale.
12
+ *
13
+ * Both behaviours dropped sibling keys silently. The 0.12 contract is that
14
+ * `put*` and `update*` deep-merge by default (sibling keys at any depth
15
+ * survive); `replace*` is the explicit escape hatch.
16
+ *
17
+ * `flattenPatch` walks a partial-update payload and emits one
18
+ * {@link DataPathOp} per terminal value. Plain objects recurse; arrays,
19
+ * primitives, Firestore special types, and tagged firegraph-serialization
20
+ * objects are terminal (replaced as a unit). `undefined` values are
21
+ * skipped; `null` is preserved as a real `null` write; the
22
+ * {@link DELETE_FIELD} sentinel marks a field for removal.
23
+ *
24
+ * The output is deliberately backend-agnostic. Each backend translates ops
25
+ * into its native dialect:
26
+ * - Firestore: dotted field path → `data.a.b.c` for `update()`.
27
+ * - SQLite / DO SQLite: `json_set(data, '$.a.b.c', ?)` /
28
+ * `json_remove(data, '$.a.b.c')`.
29
+ */
30
+ /**
31
+ * Sentinel returned by {@link deleteField}. Treated by all backends as
32
+ * "remove this field from the stored document".
33
+ *
34
+ * Equivalent to Firestore's `FieldValue.delete()`, but works for SQLite
35
+ * backends too. Use inside `updateNode`/`updateEdge` payloads.
36
+ */
37
+ declare const DELETE_FIELD: unique symbol;
38
+ type DeleteSentinel = typeof DELETE_FIELD;
39
+ /**
40
+ * Returns the firegraph delete sentinel. Place this anywhere in an
41
+ * `updateNode`/`updateEdge` payload to remove the corresponding field.
42
+ *
43
+ * ```ts
44
+ * await client.updateNode('tour', uid, {
45
+ * attrs: { obsoleteFlag: deleteField() },
46
+ * });
47
+ * ```
48
+ */
49
+ declare function deleteField(): DeleteSentinel;
50
+ /** Type guard for the delete sentinel. */
51
+ declare function isDeleteSentinel(value: unknown): value is DeleteSentinel;
52
+ /**
53
+ * Single terminal write operation produced by {@link flattenPatch}.
54
+ *
55
+ * `path` is a non-empty array of plain object keys. `value` is the value to
56
+ * write; ignored when `delete` is `true`. Arrays / primitives / Firestore
57
+ * special types appear here as whole terminal values.
58
+ */
59
+ interface DataPathOp {
60
+ path: readonly string[];
61
+ value: unknown;
62
+ delete: boolean;
63
+ }
64
+ /**
65
+ * Flatten a partial-update payload into a list of terminal {@link DataPathOp}s.
66
+ *
67
+ * Rules:
68
+ * - Plain objects (no prototype or `Object.prototype`) recurse — each
69
+ * key becomes another path segment.
70
+ * - Arrays are terminal: writing `{tags: ['a']}` overwrites the whole
71
+ * `tags` array. Element-wise array merging is intentionally NOT
72
+ * supported — it's almost never what callers actually want, and
73
+ * Firestore `arrayUnion`/`arrayRemove` give precise semantics when
74
+ * they are.
75
+ * - `undefined` values are skipped (no op generated). Use
76
+ * {@link deleteField} if you actually want to remove a field.
77
+ * - `null` is preserved verbatim — emits a terminal op with `value: null`.
78
+ * - {@link DELETE_FIELD} produces an op with `delete: true`.
79
+ * - Firestore special types and tagged serialization payloads are terminal.
80
+ * - Class instances are terminal.
81
+ *
82
+ * Throws if any object key on the recursion path is unsafe (see
83
+ * {@link assertSafePath}).
84
+ */
85
+ declare function flattenPatch(data: Record<string, unknown>): DataPathOp[];
86
+
87
+ /**
88
+ * Backend abstraction for firegraph.
89
+ *
90
+ * `StorageBackend` is the single interface every storage driver implements.
91
+ * The Firestore backend wraps `@google-cloud/firestore`; the SQLite backend
92
+ * (shared by D1 and Durable Object SQLite) uses a parameterized SQL executor.
93
+ *
94
+ * `GraphClientImpl` and friends depend only on this interface — they have
95
+ * no direct knowledge of Firestore or SQLite.
96
+ */
97
+
98
+ /**
99
+ * Per-record write payload — backend-agnostic. Timestamps are not present;
100
+ * the backend supplies them via `serverTimestamp()` placeholders that it
101
+ * itself resolves at commit time.
102
+ */
103
+ interface WritableRecord {
104
+ aType: string;
105
+ aUid: string;
106
+ axbType: string;
107
+ bType: string;
108
+ bUid: string;
109
+ data: Record<string, unknown>;
110
+ /** Schema version (set by the writer when registry has migrations). */
111
+ v?: number;
112
+ }
113
+ /**
114
+ * Write semantics for `setDoc`.
115
+ *
116
+ * - `'merge'` — the new contract (0.12+). Existing fields not mentioned
117
+ * in the new data survive; nested objects are recursively merged;
118
+ * arrays are replaced as a unit. This is the default for
119
+ * `putNode` / `putEdge`.
120
+ * - `'replace'` — the document is replaced wholesale, dropping any
121
+ * fields not present in the payload. This is the explicit escape
122
+ * hatch surfaced as `replaceNode` / `replaceEdge` and used by
123
+ * migration write-back.
124
+ */
125
+ type WriteMode = 'merge' | 'replace';
126
+ /**
127
+ * Patch shape for `updateDoc`.
128
+ *
129
+ * - `dataOps`: list of deep-path terminal ops produced by
130
+ * `flattenPatch()` (one op per leaf — arrays / primitives / Firestore
131
+ * special types are terminal). Used by `updateNode` / `updateEdge`.
132
+ * Sibling keys at every depth are preserved.
133
+ * - `replaceData`: full `data` replacement. Used only by the migration
134
+ * write-back path, which has already produced a complete migrated
135
+ * document.
136
+ * - `v`: optional schema-version stamp.
137
+ *
138
+ * `updatedAt` is always set by the backend.
139
+ */
140
+ interface UpdatePayload {
141
+ dataOps?: DataPathOp[];
142
+ replaceData?: Record<string, unknown>;
143
+ v?: number;
144
+ }
145
+ /**
146
+ * Read/write transaction adapter. Mirrors Firestore's transaction semantics:
147
+ * reads are snapshot-consistent; writes are issued inside the transaction
148
+ * and a rejection from any write aborts the surrounding `runTransaction`.
149
+ *
150
+ * Writes return `Promise<void>` so SQL drivers can surface row-level errors
151
+ * (constraint violations, malformed JSON paths) rather than swallowing them.
152
+ * Firestore implementations can resolve synchronously since the underlying
153
+ * `Transaction.set/update/delete` calls are themselves synchronous buffers.
154
+ */
155
+ interface TransactionBackend {
156
+ getDoc(docId: string): Promise<StoredGraphRecord | null>;
157
+ query(filters: QueryFilter[], options?: QueryOptions): Promise<StoredGraphRecord[]>;
158
+ setDoc(docId: string, record: WritableRecord, mode: WriteMode): Promise<void>;
159
+ updateDoc(docId: string, update: UpdatePayload): Promise<void>;
160
+ deleteDoc(docId: string): Promise<void>;
161
+ }
162
+ /**
163
+ * Atomic multi-write batch.
164
+ */
165
+ interface BatchBackend {
166
+ setDoc(docId: string, record: WritableRecord, mode: WriteMode): void;
167
+ updateDoc(docId: string, update: UpdatePayload): void;
168
+ deleteDoc(docId: string): void;
169
+ commit(): Promise<void>;
170
+ }
171
+ /**
172
+ * The single storage abstraction.
173
+ *
174
+ * Each backend instance is scoped to a "graph location" — for Firestore
175
+ * that's a collection path; for SQLite it's a (table, scopePath) pair.
176
+ * `subgraph()` returns a child backend bound to a nested location.
177
+ */
178
+ interface StorageBackend {
179
+ /** Backend-internal location identifier (collection path or table name). */
180
+ readonly collectionPath: string;
181
+ /** Subgraph scope (empty string for root). */
182
+ readonly scopePath: string;
183
+ getDoc(docId: string): Promise<StoredGraphRecord | null>;
184
+ query(filters: QueryFilter[], options?: QueryOptions): Promise<StoredGraphRecord[]>;
185
+ setDoc(docId: string, record: WritableRecord, mode: WriteMode): Promise<void>;
186
+ updateDoc(docId: string, update: UpdatePayload): Promise<void>;
187
+ deleteDoc(docId: string): Promise<void>;
188
+ runTransaction<T>(fn: (tx: TransactionBackend) => Promise<T>): Promise<T>;
189
+ createBatch(): BatchBackend;
190
+ subgraph(parentNodeUid: string, name: string): StorageBackend;
191
+ removeNodeCascade(uid: string, reader: GraphReader, options?: BulkOptions): Promise<CascadeResult>;
192
+ bulkRemoveEdges(params: FindEdgesParams, reader: GraphReader, options?: BulkOptions): Promise<BulkResult>;
193
+ /**
194
+ * Find edges across all subgraphs sharing a given collection name.
195
+ * Optional — backends that can't support this should throw a clear error.
196
+ */
197
+ findEdgesGlobal?(params: FindEdgesParams, collectionName?: string): Promise<StoredGraphRecord[]>;
198
+ }
199
+
200
+ export { type BatchBackend as B, DELETE_FIELD as D, type StorageBackend as S, type TransactionBackend as T, type UpdatePayload as U, type WritableRecord as W, type DataPathOp as a, type WriteMode as b, deleteField as d, flattenPatch as f, isDeleteSentinel as i };