@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.
- package/README.md +40 -15
- package/dist/backend-BsR0lnFL.d.ts +200 -0
- package/dist/backend-Ct-fLlkG.d.cts +200 -0
- package/dist/backend.cjs +143 -2
- package/dist/backend.cjs.map +1 -1
- package/dist/backend.d.cts +3 -3
- package/dist/backend.d.ts +3 -3
- package/dist/backend.js +13 -4
- package/dist/backend.js.map +1 -1
- package/dist/chunk-AWW4MUJ5.js +245 -0
- package/dist/chunk-AWW4MUJ5.js.map +1 -0
- package/dist/{chunk-5753Y42M.js → chunk-C2QMD7RY.js} +6 -10
- package/dist/chunk-C2QMD7RY.js.map +1 -0
- package/dist/chunk-EQJUUVFG.js +14 -0
- package/dist/chunk-EQJUUVFG.js.map +1 -0
- package/dist/{chunk-6SB34IPQ.js → chunk-HONQY4HF.js} +100 -28
- package/dist/chunk-HONQY4HF.js.map +1 -0
- package/dist/cloudflare/index.cjs +509 -102
- package/dist/cloudflare/index.cjs.map +1 -1
- package/dist/cloudflare/index.d.cts +45 -17
- package/dist/cloudflare/index.d.ts +45 -17
- package/dist/cloudflare/index.js +265 -74
- package/dist/cloudflare/index.js.map +1 -1
- package/dist/codegen/index.d.cts +1 -1
- package/dist/codegen/index.d.ts +1 -1
- package/dist/index.cjs +291 -47
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +59 -77
- package/dist/index.d.ts +59 -77
- package/dist/index.js +58 -28
- package/dist/index.js.map +1 -1
- package/dist/registry-B1qsVL0E.d.cts +64 -0
- package/dist/registry-Fi074zVa.d.ts +64 -0
- package/dist/{serialization-ZZ7RSDRX.js → serialization-OE2PFZMY.js} +6 -4
- package/dist/{types-BGWxcpI_.d.cts → types-DxYLy8Ol.d.cts} +36 -2
- package/dist/{types-BGWxcpI_.d.ts → types-DxYLy8Ol.d.ts} +36 -2
- package/package.json +8 -3
- package/dist/backend-U-MLShlg.d.ts +0 -97
- package/dist/backend-np4gEVhB.d.cts +0 -97
- package/dist/chunk-5753Y42M.js.map +0 -1
- package/dist/chunk-6SB34IPQ.js.map +0 -1
- package/dist/chunk-R7CRGYY4.js +0 -94
- package/dist/chunk-R7CRGYY4.js.map +0 -1
- /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
|
|
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
|
-
//
|
|
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
|
|
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
|
|
381
|
-
- **`updateNode`**:
|
|
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
|
|
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); //
|
|
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 };
|