@typicalday/firegraph 0.11.2 → 0.13.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 +355 -78
- package/dist/backend-DuvHGgK1.d.cts +1897 -0
- package/dist/backend-DuvHGgK1.d.ts +1897 -0
- package/dist/backend.cjs +365 -5
- package/dist/backend.cjs.map +1 -1
- package/dist/backend.d.cts +25 -5
- package/dist/backend.d.ts +25 -5
- package/dist/backend.js +209 -7
- package/dist/backend.js.map +1 -1
- package/dist/chunk-2DHMNTV6.js +16 -0
- package/dist/chunk-2DHMNTV6.js.map +1 -0
- package/dist/chunk-4MMQ5W74.js +288 -0
- package/dist/chunk-4MMQ5W74.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-D4J7Z4FE.js +67 -0
- package/dist/chunk-D4J7Z4FE.js.map +1 -0
- package/dist/chunk-EQJUUVFG.js +14 -0
- package/dist/chunk-EQJUUVFG.js.map +1 -0
- package/dist/chunk-N5HFDWQX.js +23 -0
- package/dist/chunk-N5HFDWQX.js.map +1 -0
- package/dist/chunk-PAD7WFFU.js +573 -0
- package/dist/chunk-PAD7WFFU.js.map +1 -0
- package/dist/chunk-TK64DNVK.js +256 -0
- package/dist/chunk-TK64DNVK.js.map +1 -0
- package/dist/{chunk-NJSOD64C.js → chunk-WRTFC5NG.js} +438 -30
- package/dist/chunk-WRTFC5NG.js.map +1 -0
- package/dist/client-BKi3vk0Q.d.ts +34 -0
- package/dist/client-BrsaXtDV.d.cts +34 -0
- package/dist/cloudflare/index.cjs +1386 -74
- package/dist/cloudflare/index.cjs.map +1 -1
- package/dist/cloudflare/index.d.cts +217 -13
- package/dist/cloudflare/index.d.ts +217 -13
- package/dist/cloudflare/index.js +639 -180
- 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/errors-BRc3I_eH.d.cts +73 -0
- package/dist/errors-BRc3I_eH.d.ts +73 -0
- package/dist/firestore-enterprise/index.cjs +3877 -0
- package/dist/firestore-enterprise/index.cjs.map +1 -0
- package/dist/firestore-enterprise/index.d.cts +141 -0
- package/dist/firestore-enterprise/index.d.ts +141 -0
- package/dist/firestore-enterprise/index.js +985 -0
- package/dist/firestore-enterprise/index.js.map +1 -0
- package/dist/firestore-standard/index.cjs +3117 -0
- package/dist/firestore-standard/index.cjs.map +1 -0
- package/dist/firestore-standard/index.d.cts +49 -0
- package/dist/firestore-standard/index.d.ts +49 -0
- package/dist/firestore-standard/index.js +283 -0
- package/dist/firestore-standard/index.js.map +1 -0
- package/dist/index.cjs +809 -534
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +24 -100
- package/dist/index.d.ts +24 -100
- package/dist/index.js +184 -531
- package/dist/index.js.map +1 -1
- package/dist/registry-Bc7h6WTM.d.cts +64 -0
- package/dist/registry-C2KUPVZj.d.ts +64 -0
- package/dist/{scope-path-B1G3YiA7.d.ts → scope-path-CROFZGr9.d.cts} +1 -56
- package/dist/{scope-path-B1G3YiA7.d.cts → scope-path-CROFZGr9.d.ts} +1 -56
- package/dist/{serialization-ZZ7RSDRX.js → serialization-OE2PFZMY.js} +6 -4
- package/dist/sqlite/index.cjs +3631 -0
- package/dist/sqlite/index.cjs.map +1 -0
- package/dist/sqlite/index.d.cts +111 -0
- package/dist/sqlite/index.d.ts +111 -0
- package/dist/sqlite/index.js +1164 -0
- package/dist/sqlite/index.js.map +1 -0
- package/package.json +33 -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-NJSOD64C.js.map +0 -1
- package/dist/chunk-R7CRGYY4.js +0 -94
- package/dist/chunk-R7CRGYY4.js.map +0 -1
- package/dist/types-BGWxcpI_.d.cts +0 -736
- package/dist/types-BGWxcpI_.d.ts +0 -736
- /package/dist/{serialization-ZZ7RSDRX.js.map → serialization-OE2PFZMY.js.map} +0 -0
package/README.md
CHANGED
|
@@ -35,9 +35,11 @@ npm install -D tsup typescript
|
|
|
35
35
|
```typescript
|
|
36
36
|
import { Firestore } from '@google-cloud/firestore';
|
|
37
37
|
import { createGraphClient, generateId } from 'firegraph';
|
|
38
|
+
import { createFirestoreStandardBackend } from 'firegraph/firestore-standard';
|
|
38
39
|
|
|
39
40
|
const db = new Firestore();
|
|
40
|
-
const
|
|
41
|
+
const backend = createFirestoreStandardBackend(db, 'graph');
|
|
42
|
+
const g = createGraphClient(backend);
|
|
41
43
|
|
|
42
44
|
// Create nodes
|
|
43
45
|
const tourId = generateId();
|
|
@@ -83,34 +85,168 @@ UIDs **must** be generated via `generateId()` (21-char nanoid). Short sequential
|
|
|
83
85
|
|
|
84
86
|
```typescript
|
|
85
87
|
import { createGraphClient } from 'firegraph';
|
|
88
|
+
import { createFirestoreStandardBackend } from 'firegraph/firestore-standard';
|
|
89
|
+
// or for Enterprise Firestore (Pipelines, DML, server-side traversal, FTS, geo):
|
|
90
|
+
import { createFirestoreEnterpriseBackend } from 'firegraph/firestore-enterprise';
|
|
86
91
|
|
|
87
|
-
const
|
|
92
|
+
const backend = createFirestoreStandardBackend(db, 'graph');
|
|
93
|
+
const g = createGraphClient(backend);
|
|
88
94
|
// or with options:
|
|
89
|
-
const g = createGraphClient(
|
|
95
|
+
const g = createGraphClient(backend, { registry });
|
|
90
96
|
```
|
|
91
97
|
|
|
98
|
+
For non-Firestore backends (SQLite, Cloudflare DO, routing backend) use `createGraphClientFromBackend`, which accepts any raw `StorageBackend<C>` without requiring a named factory:
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
import { createGraphClientFromBackend } from 'firegraph';
|
|
102
|
+
const g = createGraphClientFromBackend(backend, opts, metaBackend);
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
`createGraphClientFromBackend` is a deprecated alias for `createGraphClient` — prefer `createGraphClient` directly. Both accept the same `opts` and `metaBackend` arguments.
|
|
106
|
+
|
|
92
107
|
**Parameters:**
|
|
93
108
|
|
|
94
|
-
- `
|
|
95
|
-
- `
|
|
96
|
-
- `
|
|
97
|
-
- `
|
|
109
|
+
- `backend` — A `StorageBackend<C>` from `createFirestoreStandardBackend`, `createFirestoreEnterpriseBackend`, or another backend factory
|
|
110
|
+
- `opts.registry` — Optional `GraphRegistry` for schema validation
|
|
111
|
+
- `opts.registryMode` — Optional dynamic registry config (`{ mode: 'dynamic', collection? }`). Pass alongside `opts.registry` for merged mode (static + dynamic).
|
|
112
|
+
- `opts.migrationWriteBack` — Optional global write-back mode (`'off'` | `'eager'` | `'background'`)
|
|
113
|
+
- `opts.migrationSandbox` — Optional custom migration evaluator (overrides the default SES executor)
|
|
114
|
+
- `opts.queryMode` — Optional Firestore query backend (`'pipeline'` | `'standard'`; default `'pipeline'`). Ignored by non-Firestore backends.
|
|
115
|
+
- `opts.scanProtection` — Optional full-collection-scan gate (`'off'` | `'warn'` | `'error'`; default `'error'`)
|
|
116
|
+
- `metaBackend` — Optional separate backend for meta-type storage (dynamic registry)
|
|
117
|
+
|
|
118
|
+
### Capability System
|
|
119
|
+
|
|
120
|
+
Every client exposes a `capabilities` property (a `BackendCapabilities` set) that reflects what the underlying backend supports. Use it for portable feature checks at runtime:
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
if (client.capabilities.has('query.join')) {
|
|
124
|
+
const result = await (client as JoinExtension).expand({ ... });
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
`GraphClient<C>` is a generic type — the type parameter `C` is a union of the backend's declared `Capability` strings and controls which extension methods are present on the type. `CoreGraphClient` is the unconditional base (read + write + transactions + batch + subgraph + `capabilities`). Helper functions that should accept any client should be typed to `CoreGraphClient` or `GraphReader`/`GraphWriter`.
|
|
129
|
+
|
|
130
|
+
**Capability values:**
|
|
131
|
+
|
|
132
|
+
| Capability | Methods unlocked | Backends |
|
|
133
|
+
| ----------------------------------------------------------- | ---------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
|
|
134
|
+
| `core.read` / `core.write` / `core.batch` / `core.subgraph` | `getNode`, `putNode`, `findEdges`, `batch()`, `subgraph()`, etc. | All |
|
|
135
|
+
| `core.transactions` | `runTransaction(fn)` | Firestore (both), SQLite (`better-sqlite3` only; absent on D1); **absent on Cloudflare DO** |
|
|
136
|
+
| `query.aggregate` | `aggregate(spec)` | All; `min`/`max` only on SQLite + DO (both Firestore editions reject `min`/`max` — classic `Query.aggregate` exposes only count/sum/avg) |
|
|
137
|
+
| `query.select` | `findEdgesProjected(params)` | All |
|
|
138
|
+
| `query.join` | `expand(params)` | All |
|
|
139
|
+
| `query.dml` | `bulkDelete(params)`, `bulkUpdate(params)` | Enterprise (requires `previewDml: true`), SQLite, DO |
|
|
140
|
+
| `traversal.serverSide` | `runEngineTraversal(params)` | Enterprise |
|
|
141
|
+
| `search.vector` | `findNearest(params)` | Firestore (both) |
|
|
142
|
+
| `search.fullText` | `fullTextSearch(params)` | Enterprise. **Note:** the `fields` option is not yet supported — passing a non-empty `fields` array throws `INVALID_QUERY`. |
|
|
143
|
+
| `search.geo` | `geoSearch(params)` | Enterprise |
|
|
144
|
+
| `raw.firestore` | _(reserved — no methods yet)_ | Firestore (both) |
|
|
145
|
+
| `raw.sql` | _(reserved — no methods yet)_ | SQLite |
|
|
146
|
+
| `realtime.listen` | _(reserved — no methods yet)_ | _(none currently)_ |
|
|
147
|
+
|
|
148
|
+
### Extension Methods
|
|
149
|
+
|
|
150
|
+
Methods unlocked by optional capabilities. Cast the client to the extension interface or check `client.capabilities` at runtime.
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
// query.aggregate — count / sum / avg (all backends); min / max (SQLite + DO only)
|
|
154
|
+
const stats = await (client as AggregateExtension).aggregate({
|
|
155
|
+
aType: 'tour',
|
|
156
|
+
axbType: 'is',
|
|
157
|
+
bType: 'tour',
|
|
158
|
+
ops: [
|
|
159
|
+
{ op: 'count', alias: 'total' },
|
|
160
|
+
{ op: 'avg', field: 'data.price', alias: 'avgPrice' },
|
|
161
|
+
],
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// query.select — projected edge scan
|
|
165
|
+
const names = await (client as SelectExtension).findEdgesProjected({
|
|
166
|
+
aType: 'tour',
|
|
167
|
+
axbType: 'is',
|
|
168
|
+
bType: 'tour',
|
|
169
|
+
fields: ['data.name', 'aUid'],
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// query.join — server-side expand (fan-out from a set of sources)
|
|
173
|
+
const legs = await (client as JoinExtension).expand({
|
|
174
|
+
aType: 'tour',
|
|
175
|
+
axbType: 'hasDeparture',
|
|
176
|
+
bType: 'departure',
|
|
177
|
+
sources: [{ aUid: tourId }],
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// query.dml — bulk delete / update (Enterprise opt-in; SQLite + DO always on)
|
|
181
|
+
await (client as DmlExtension).bulkDelete({
|
|
182
|
+
aType: 'tour',
|
|
183
|
+
axbType: 'hasDeparture',
|
|
184
|
+
bType: 'departure',
|
|
185
|
+
aUid: tourId,
|
|
186
|
+
});
|
|
187
|
+
await (client as DmlExtension).bulkUpdate(
|
|
188
|
+
{
|
|
189
|
+
aType: 'tour',
|
|
190
|
+
axbType: 'is',
|
|
191
|
+
bType: 'tour',
|
|
192
|
+
filters: [{ field: 'data.status', op: '==', value: 'draft' }],
|
|
193
|
+
},
|
|
194
|
+
{ 'data.status': 'archived' },
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
// traversal.serverSide — multi-hop traversal in one Pipeline round-trip (Enterprise)
|
|
198
|
+
const tree = await (client as TraversalExtension).runEngineTraversal({
|
|
199
|
+
sources: [{ aType: 'tour', aUid: tourId }],
|
|
200
|
+
hops: [{ axbType: 'hasDeparture', bType: 'departure', limitPerSource: 10 }],
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// search.vector — approximate nearest-neighbour (Firestore both editions)
|
|
204
|
+
const similar = await (client as VectorExtension).findNearest({
|
|
205
|
+
aType: 'tour',
|
|
206
|
+
axbType: 'is',
|
|
207
|
+
bType: 'tour',
|
|
208
|
+
queryVector: [0.1, 0.2, 0.3],
|
|
209
|
+
vectorField: 'data.embedding',
|
|
210
|
+
limit: 5,
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// search.fullText — full-text search (Enterprise; `fields` throws INVALID_QUERY if non-empty)
|
|
214
|
+
const results = await (client as FullTextExtension).fullTextSearch({
|
|
215
|
+
aType: 'tour',
|
|
216
|
+
axbType: 'is',
|
|
217
|
+
bType: 'tour',
|
|
218
|
+
query: 'dolomites',
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// search.geo — geospatial radius search (Enterprise)
|
|
222
|
+
const nearby = await (client as GeoExtension).geoSearch({
|
|
223
|
+
aType: 'tour',
|
|
224
|
+
axbType: 'is',
|
|
225
|
+
bType: 'tour',
|
|
226
|
+
geoField: 'data.location',
|
|
227
|
+
center: { latitude: 46.4, longitude: 11.9 },
|
|
228
|
+
radiusMeters: 50000,
|
|
229
|
+
});
|
|
230
|
+
```
|
|
98
231
|
|
|
99
232
|
### Nodes
|
|
100
233
|
|
|
101
234
|
```typescript
|
|
102
235
|
const tourId = generateId();
|
|
103
236
|
|
|
104
|
-
// Create or
|
|
237
|
+
// Create or deep-merge a node (sibling keys at any depth survive)
|
|
105
238
|
await g.putNode('tour', tourId, { name: 'Dolomites Classic' });
|
|
106
239
|
|
|
107
240
|
// Read a node
|
|
108
241
|
const node = await g.getNode(tourId);
|
|
109
242
|
// → StoredGraphRecord | null
|
|
110
243
|
|
|
111
|
-
//
|
|
244
|
+
// Partial update (deep merge into data)
|
|
112
245
|
await g.updateNode(tourId, { difficulty: 'extreme' });
|
|
113
246
|
|
|
247
|
+
// Full replace — discards every prior key not in the new payload
|
|
248
|
+
await g.replaceNode('tour', tourId, { name: 'Dolomites — 2026 Edition' });
|
|
249
|
+
|
|
114
250
|
// Delete a node
|
|
115
251
|
await g.removeNode(tourId);
|
|
116
252
|
|
|
@@ -118,12 +254,19 @@ await g.removeNode(tourId);
|
|
|
118
254
|
const tours = await g.findNodes({ aType: 'tour' });
|
|
119
255
|
```
|
|
120
256
|
|
|
257
|
+
**Write semantics (0.12+):** `putNode`/`putEdge` and `updateNode`/`updateEdge`
|
|
258
|
+
**deep-merge** by default — sibling keys at every nesting depth survive. Use
|
|
259
|
+
`replaceNode`/`replaceEdge` when you want the old "wipe and rewrite" behaviour.
|
|
260
|
+
Arrays are terminal (replaced wholesale, not element-merged); `undefined`
|
|
261
|
+
values are skipped; `null` is preserved verbatim; and the
|
|
262
|
+
[`deleteField()`](#field-deletion) sentinel removes a field at any depth.
|
|
263
|
+
|
|
121
264
|
### Edges
|
|
122
265
|
|
|
123
266
|
```typescript
|
|
124
267
|
const depId = generateId();
|
|
125
268
|
|
|
126
|
-
// Create or
|
|
269
|
+
// Create or deep-merge an edge
|
|
127
270
|
await g.putEdge('tour', tourId, 'hasDeparture', 'departure', depId, { order: 0 });
|
|
128
271
|
|
|
129
272
|
// Read a specific edge
|
|
@@ -133,10 +276,37 @@ const edge = await g.getEdge(tourId, 'hasDeparture', depId);
|
|
|
133
276
|
// Check existence
|
|
134
277
|
const exists = await g.edgeExists(tourId, 'hasDeparture', depId);
|
|
135
278
|
|
|
279
|
+
// Partial update (deep merge)
|
|
280
|
+
await g.updateEdge(tourId, 'hasDeparture', depId, { order: 5 });
|
|
281
|
+
|
|
282
|
+
// Full replace — discards every prior key not in the new payload
|
|
283
|
+
await g.replaceEdge('tour', tourId, 'hasDeparture', 'departure', depId, { order: 5 });
|
|
284
|
+
|
|
136
285
|
// Delete an edge
|
|
137
286
|
await g.removeEdge(tourId, 'hasDeparture', depId);
|
|
287
|
+
|
|
288
|
+
// Bulk delete all edges matching a filter (available on all backends)
|
|
289
|
+
const result = await g.bulkRemoveEdges({ aUid: tourId, axbType: 'hasDeparture' });
|
|
290
|
+
// → BulkResult { deleted: number, errors: BulkBatchError[] }
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
### Field Deletion
|
|
294
|
+
|
|
295
|
+
The `deleteField()` sentinel removes a field from a stored document. It works
|
|
296
|
+
across every backend (Firestore, SQLite, Cloudflare Durable Objects), so
|
|
297
|
+
calling code stays portable:
|
|
298
|
+
|
|
299
|
+
```typescript
|
|
300
|
+
import { deleteField } from 'firegraph';
|
|
301
|
+
|
|
302
|
+
await g.updateNode(tourId, {
|
|
303
|
+
meta: { deprecatedTag: deleteField() }, // removes meta.deprecatedTag
|
|
304
|
+
});
|
|
138
305
|
```
|
|
139
306
|
|
|
307
|
+
Equivalent to Firestore's `FieldValue.delete()`, but Workers-safe and
|
|
308
|
+
SQLite-aware.
|
|
309
|
+
|
|
140
310
|
### Querying Edges
|
|
141
311
|
|
|
142
312
|
`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.
|
|
@@ -253,11 +423,12 @@ await g.runTransaction(async (tx) => {
|
|
|
253
423
|
|
|
254
424
|
#### Run Options
|
|
255
425
|
|
|
256
|
-
| Option | Type
|
|
257
|
-
| --------------------- |
|
|
258
|
-
| `maxReads` | `number`
|
|
259
|
-
| `concurrency` | `number`
|
|
260
|
-
| `returnIntermediates` | `boolean`
|
|
426
|
+
| Option | Type | Default | Description |
|
|
427
|
+
| --------------------- | ---------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
428
|
+
| `maxReads` | `number` | `100` | Total read budget |
|
|
429
|
+
| `concurrency` | `number` | `5` | Max parallel queries per hop |
|
|
430
|
+
| `returnIntermediates` | `boolean` | `false` | Include edges from all hops |
|
|
431
|
+
| `engineTraversal` | `'auto' \| 'force' \| 'off'` | `'auto'` | Engine-level traversal on Enterprise backends. `'auto'` silently falls back if ineligible; `'force'` throws if unavailable; `'off'` disables |
|
|
261
432
|
|
|
262
433
|
When `filter` is set, the `limit` is applied after filtering (in-memory), so Firestore returns all matching edges and the filter + slice happens client-side.
|
|
263
434
|
|
|
@@ -267,6 +438,7 @@ Optional type validation using Zod (or any object with a `.parse()` method):
|
|
|
267
438
|
|
|
268
439
|
```typescript
|
|
269
440
|
import { createRegistry, createGraphClient } from 'firegraph';
|
|
441
|
+
import { createFirestoreStandardBackend } from 'firegraph/firestore-standard';
|
|
270
442
|
import { z } from 'zod';
|
|
271
443
|
|
|
272
444
|
const registry = createRegistry([
|
|
@@ -287,7 +459,8 @@ const registry = createRegistry([
|
|
|
287
459
|
},
|
|
288
460
|
]);
|
|
289
461
|
|
|
290
|
-
const
|
|
462
|
+
const backend = createFirestoreStandardBackend(db, 'graph');
|
|
463
|
+
const g = createGraphClient(backend, { registry });
|
|
291
464
|
|
|
292
465
|
// This validates against the registry before writing:
|
|
293
466
|
const id = generateId();
|
|
@@ -304,8 +477,10 @@ For agent-driven or runtime-extensible schemas, firegraph supports a **dynamic r
|
|
|
304
477
|
|
|
305
478
|
```typescript
|
|
306
479
|
import { createGraphClient } from 'firegraph';
|
|
480
|
+
import { createFirestoreStandardBackend } from 'firegraph/firestore-standard';
|
|
307
481
|
|
|
308
|
-
const
|
|
482
|
+
const backend = createFirestoreStandardBackend(db, 'graph');
|
|
483
|
+
const g = createGraphClient(backend, {
|
|
309
484
|
registryMode: { mode: 'dynamic' },
|
|
310
485
|
});
|
|
311
486
|
|
|
@@ -338,7 +513,7 @@ Key behaviors:
|
|
|
338
513
|
- **After `reloadRegistry()`**: Domain writes are validated against the compiled registry. Unknown types are always rejected.
|
|
339
514
|
- **Upsert semantics**: Calling `defineNodeType('tour', ...)` twice overwrites the previous definition. After reloading, the latest schema is used.
|
|
340
515
|
- **Separate collection**: Meta-nodes can be stored in a different collection via `registryMode: { mode: 'dynamic', collection: 'meta' }`.
|
|
341
|
-
- **Merged mode**:
|
|
516
|
+
- **Merged mode**: Pass both `registry` (the static side, typically built via `createRegistry` or `createMergedRegistry`) and `registryMode: { mode: 'dynamic' }`. Firegraph then merges them — static entries take priority and dynamic definitions can only add new types, never override existing ones. There is no separate `mode: 'merged'` value; merged behavior is implied by supplying both options together.
|
|
342
517
|
|
|
343
518
|
Dynamic registry returns a `DynamicGraphClient` which extends `GraphClient` with `defineNodeType()`, `defineEdgeType()`, and `reloadRegistry()`. Transactions and batches also validate against the compiled dynamic registry.
|
|
344
519
|
|
|
@@ -348,6 +523,7 @@ Firegraph supports schema versioning with automatic migration of records on read
|
|
|
348
523
|
|
|
349
524
|
```typescript
|
|
350
525
|
import { createRegistry, createGraphClient } from 'firegraph';
|
|
526
|
+
import { createFirestoreStandardBackend } from 'firegraph/firestore-standard';
|
|
351
527
|
import type { MigrationStep } from 'firegraph';
|
|
352
528
|
|
|
353
529
|
const migrations: MigrationStep[] = [
|
|
@@ -366,7 +542,8 @@ const registry = createRegistry([
|
|
|
366
542
|
},
|
|
367
543
|
]);
|
|
368
544
|
|
|
369
|
-
const
|
|
545
|
+
const backend = createFirestoreStandardBackend(db, 'graph');
|
|
546
|
+
const g = createGraphClient(backend, { registry });
|
|
370
547
|
|
|
371
548
|
// Reading a v0 record automatically migrates it to v2 in memory
|
|
372
549
|
const tour = await g.getNode(tourId);
|
|
@@ -377,8 +554,8 @@ const tour = await g.getNode(tourId);
|
|
|
377
554
|
|
|
378
555
|
- **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
556
|
- **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`**:
|
|
557
|
+
- **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.
|
|
558
|
+
- **`updateNode` / `updateEdge`**: Do not stamp `v` — they are raw partial patches without schema context. The next read re-triggers migration (which is idempotent).
|
|
382
559
|
|
|
383
560
|
#### Write-Back
|
|
384
561
|
|
|
@@ -394,7 +571,8 @@ Resolution order: `entry.migrationWriteBack > client.migrationWriteBack > 'off'`
|
|
|
394
571
|
|
|
395
572
|
```typescript
|
|
396
573
|
// Global default
|
|
397
|
-
const
|
|
574
|
+
const backend = createFirestoreStandardBackend(db, 'graph');
|
|
575
|
+
const g = createGraphClient(backend, {
|
|
398
576
|
registry,
|
|
399
577
|
migrationWriteBack: 'background',
|
|
400
578
|
});
|
|
@@ -428,7 +606,8 @@ Stored migration strings must be self-contained — no `import`, `require`, or e
|
|
|
428
606
|
For custom sandboxing, pass `migrationSandbox` to `createGraphClient()`:
|
|
429
607
|
|
|
430
608
|
```typescript
|
|
431
|
-
const
|
|
609
|
+
const backend = createFirestoreStandardBackend(db, 'graph');
|
|
610
|
+
const g = createGraphClient(backend, {
|
|
432
611
|
registryMode: { mode: 'dynamic' },
|
|
433
612
|
migrationSandbox: (source) => {
|
|
434
613
|
const compartment = new Compartment({
|
|
@@ -496,7 +675,8 @@ const registry = createRegistry([
|
|
|
496
675
|
{ aType: 'task', axbType: 'is', bType: 'task', allowedIn: ['workspace', '**/workspace'] },
|
|
497
676
|
]);
|
|
498
677
|
|
|
499
|
-
const
|
|
678
|
+
const backend = createFirestoreStandardBackend(db, 'graph');
|
|
679
|
+
const g = createGraphClient(backend, { registry });
|
|
500
680
|
|
|
501
681
|
// Agent only at root
|
|
502
682
|
await g.putNode('agent', agentId, {}); // OK
|
|
@@ -570,6 +750,7 @@ Edges that connect nodes across different subgraphs. The key rule: **edges live
|
|
|
570
750
|
|
|
571
751
|
```typescript
|
|
572
752
|
import { createGraphClient, createRegistry, createTraversal, generateId } from 'firegraph';
|
|
753
|
+
import { createFirestoreStandardBackend } from 'firegraph/firestore-standard';
|
|
573
754
|
|
|
574
755
|
// Registry declares that 'assignedTo' edges live in the 'workflow' subgraph
|
|
575
756
|
const registry = createRegistry([
|
|
@@ -578,7 +759,8 @@ const registry = createRegistry([
|
|
|
578
759
|
{ aType: 'task', axbType: 'assignedTo', bType: 'agent', targetGraph: 'workflow' },
|
|
579
760
|
]);
|
|
580
761
|
|
|
581
|
-
const
|
|
762
|
+
const backend = createFirestoreStandardBackend(db, 'graph');
|
|
763
|
+
const g = createGraphClient(backend, { registry });
|
|
582
764
|
|
|
583
765
|
// Create a task in the root graph
|
|
584
766
|
const taskId = generateId();
|
|
@@ -653,7 +835,7 @@ This uses Firestore collection group queries and requires collection group index
|
|
|
653
835
|
|
|
654
836
|
#### Multi-Hop Limitation
|
|
655
837
|
|
|
656
|
-
Each hop
|
|
838
|
+
Each hop carries its reader context forward — if hop 1 crosses into a subgraph, hop 2 stays in that subgraph. To return to the root or traverse a different subgraph, create a separate traversal from the desired client:
|
|
657
839
|
|
|
658
840
|
```typescript
|
|
659
841
|
// This traversal finds agents in the workflow subgraph
|
|
@@ -689,19 +871,22 @@ const id = generateId(); // 21-char URL-safe nanoid
|
|
|
689
871
|
|
|
690
872
|
All errors extend `FiregraphError` with a `code` property:
|
|
691
873
|
|
|
692
|
-
| Error Class
|
|
693
|
-
|
|
|
694
|
-
| `FiregraphError`
|
|
695
|
-
| `NodeNotFoundError`
|
|
696
|
-
| `EdgeNotFoundError`
|
|
697
|
-
| `ValidationError`
|
|
698
|
-
| `RegistryViolationError`
|
|
699
|
-
| `RegistryScopeError`
|
|
700
|
-
| `MigrationError`
|
|
701
|
-
| `DynamicRegistryError`
|
|
702
|
-
| `InvalidQueryError`
|
|
703
|
-
| `QuerySafetyError`
|
|
704
|
-
| `TraversalError`
|
|
874
|
+
| Error Class | Code | When |
|
|
875
|
+
| ------------------------------ | --------------------------- | ----------------------------------------------------------------------- |
|
|
876
|
+
| `FiregraphError` | varies | Base class |
|
|
877
|
+
| `NodeNotFoundError` | `NODE_NOT_FOUND` | Node lookup fails (not thrown by `getNode` — it returns `null`) |
|
|
878
|
+
| `EdgeNotFoundError` | `EDGE_NOT_FOUND` | Edge lookup fails (not thrown by `getEdge` — it returns `null`) |
|
|
879
|
+
| `ValidationError` | `VALIDATION_ERROR` | Schema validation fails (registry JSON Schema validation) |
|
|
880
|
+
| `RegistryViolationError` | `REGISTRY_VIOLATION` | Triple not registered |
|
|
881
|
+
| `RegistryScopeError` | `REGISTRY_SCOPE` | Type not allowed at this subgraph scope |
|
|
882
|
+
| `MigrationError` | `MIGRATION_ERROR` | Migration function fails or chain is incomplete |
|
|
883
|
+
| `DynamicRegistryError` | `DYNAMIC_REGISTRY_ERROR` | Dynamic registry misconfiguration or misuse |
|
|
884
|
+
| `InvalidQueryError` | `INVALID_QUERY` | `findEdges` called with no filters |
|
|
885
|
+
| `QuerySafetyError` | `QUERY_SAFETY` | Query would cause a full collection scan |
|
|
886
|
+
| `TraversalError` | `TRAVERSAL_ERROR` | `run()` called with zero hops |
|
|
887
|
+
| `CapabilityNotSupportedError` | `CAPABILITY_NOT_SUPPORTED` | Capability-gated method called on a backend that doesn't declare it |
|
|
888
|
+
| `CrossBackendTransactionError` | `CROSS_BACKEND_TRANSACTION` | `runTransaction()` attempted across backends with different storage |
|
|
889
|
+
| `DiscoveryError` | `DISCOVERY_ERROR` | Entity discovery fails (missing required files, malformed schema, etc.) |
|
|
705
890
|
|
|
706
891
|
```typescript
|
|
707
892
|
import { FiregraphError, ValidationError } from 'firegraph';
|
|
@@ -732,15 +917,49 @@ import type {
|
|
|
732
917
|
QueryPlan,
|
|
733
918
|
QueryFilter,
|
|
734
919
|
QueryOptions,
|
|
735
|
-
|
|
736
|
-
|
|
920
|
+
QueryMode,
|
|
921
|
+
ScanProtection,
|
|
922
|
+
WhereClause,
|
|
923
|
+
IndexFieldSpec,
|
|
924
|
+
IndexSpec,
|
|
925
|
+
|
|
926
|
+
// Client interfaces — CoreGraphClient is the unconditional base
|
|
927
|
+
Capability,
|
|
928
|
+
CoreGraphClient,
|
|
737
929
|
GraphReader,
|
|
738
930
|
GraphWriter,
|
|
739
|
-
GraphClient,
|
|
931
|
+
GraphClient, // generic GraphClient<C extends Capability>
|
|
740
932
|
GraphTransaction,
|
|
741
933
|
GraphBatch,
|
|
742
934
|
GraphClientOptions,
|
|
743
935
|
|
|
936
|
+
// Capability-gated extensions
|
|
937
|
+
AggregateExtension,
|
|
938
|
+
AggregateField,
|
|
939
|
+
AggregateOp,
|
|
940
|
+
AggregateResult,
|
|
941
|
+
AggregateSpec,
|
|
942
|
+
SelectExtension,
|
|
943
|
+
FindEdgesProjectedParams,
|
|
944
|
+
ProjectedRow,
|
|
945
|
+
JoinExtension,
|
|
946
|
+
ExpandParams,
|
|
947
|
+
ExpandResult,
|
|
948
|
+
DmlExtension,
|
|
949
|
+
BulkUpdatePatch,
|
|
950
|
+
BulkOptions,
|
|
951
|
+
BulkResult,
|
|
952
|
+
BulkBatchError,
|
|
953
|
+
BulkProgress,
|
|
954
|
+
VectorExtension,
|
|
955
|
+
FindNearestParams,
|
|
956
|
+
DistanceMeasure,
|
|
957
|
+
FullTextSearchExtension,
|
|
958
|
+
GeoExtension,
|
|
959
|
+
RawFirestoreExtension,
|
|
960
|
+
RawSqlExtension,
|
|
961
|
+
RealtimeListenExtension,
|
|
962
|
+
|
|
744
963
|
// Registry
|
|
745
964
|
RegistryEntry, // includes targetGraph, allowedIn
|
|
746
965
|
GraphRegistry, // includes lookupByAxbType
|
|
@@ -748,9 +967,12 @@ import type {
|
|
|
748
967
|
|
|
749
968
|
// Dynamic Registry
|
|
750
969
|
DynamicGraphClient,
|
|
970
|
+
DynamicGraphMethods,
|
|
751
971
|
DynamicRegistryConfig,
|
|
752
972
|
NodeTypeData,
|
|
753
973
|
EdgeTypeData,
|
|
974
|
+
DefineTypeOptions,
|
|
975
|
+
CascadeResult,
|
|
754
976
|
|
|
755
977
|
// Migration
|
|
756
978
|
MigrationFn,
|
|
@@ -758,6 +980,7 @@ import type {
|
|
|
758
980
|
StoredMigrationStep,
|
|
759
981
|
MigrationExecutor,
|
|
760
982
|
MigrationWriteBack,
|
|
983
|
+
MigrationResult,
|
|
761
984
|
|
|
762
985
|
// Traversal
|
|
763
986
|
HopDefinition, // includes targetGraph
|
|
@@ -768,10 +991,14 @@ import type {
|
|
|
768
991
|
|
|
769
992
|
// Entity Discovery
|
|
770
993
|
DiscoveredEntity,
|
|
771
|
-
|
|
994
|
+
DiscoverResult, // return type of discoverEntities()
|
|
995
|
+
DiscoveryResult, // { nodes: Map<...>, edges: Map<...> } — the .result field of DiscoverResult
|
|
996
|
+
DiscoveryWarning,
|
|
772
997
|
} from 'firegraph';
|
|
773
998
|
```
|
|
774
999
|
|
|
1000
|
+
> **Note:** Several types are defined in the library but not yet exported from the `'firegraph'` entry point: the parameter and result types for `fullTextSearch()`, `geoSearch()`, and `runEngineTraversal()` (`FullTextSearchParams`, `GeoSearchParams`, `GeoPointLiteral`, `EngineHopSpec`, `EngineTraversalParams`, `EngineTraversalResult`), and the extension interface `EngineTraversalExtension`. Rely on type inference or declare local `Parameters<typeof client.fullTextSearch>[0]`-style helpers until these types are promoted to the public export.
|
|
1001
|
+
|
|
775
1002
|
## How It Works
|
|
776
1003
|
|
|
777
1004
|
### Storage Layout
|
|
@@ -799,68 +1026,118 @@ When you call `findEdges`, the query planner decides the strategy:
|
|
|
799
1026
|
|
|
800
1027
|
### Traversal Execution
|
|
801
1028
|
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
3. Return final hop edges as `nodes`, all hop data in `hops`
|
|
1029
|
+
Traversal dispatches through three tiers in order:
|
|
1030
|
+
|
|
1031
|
+
1. **Engine-level** (Firestore Enterprise, `traversal.serverSide`): collapses the entire hop chain into one nested-Pipeline server-side round trip. Requires every hop to have a positive `limitPerSource`, no JS `filter` predicates, no cross-graph hops, and depth ≤ 5. Counts as `totalReads: 1`. Controlled by `engineTraversal` option (`'auto'` by default). Sets `truncated: true` on any hop whose returned edge count reaches `limitPerSource`; `result.truncated` is `true` when any hop is truncated.
|
|
1032
|
+
|
|
1033
|
+
2. **Expand fast-path** (`query.join`): one `expand()` call per hop instead of one `findEdges` per source. Counts as 1 read per hop regardless of source-set size.
|
|
1034
|
+
|
|
1035
|
+
3. **Per-source loop** (all backends): fan-out over source UIDs in parallel (bounded by semaphore). Each `findEdges` call counts as 1 read against the budget.
|
|
1036
|
+
|
|
1037
|
+
For each hop the traversal also: resolves `targetGraph` (hop override → registry → none), creates subgraph readers for cross-graph hops, applies in-memory `filter` + `limit`, deduplicates next source UIDs, and stops with `truncated = true` if the budget is exceeded.
|
|
812
1038
|
|
|
813
1039
|
## Query Modes
|
|
814
1040
|
|
|
815
|
-
Firegraph
|
|
1041
|
+
Firegraph ships two Firestore backends that you choose at construction time:
|
|
816
1042
|
|
|
817
1043
|
```typescript
|
|
818
|
-
|
|
819
|
-
|
|
1044
|
+
import { createGraphClient } from 'firegraph';
|
|
1045
|
+
import { createFirestoreStandardBackend } from 'firegraph/firestore-standard';
|
|
1046
|
+
import { createFirestoreEnterpriseBackend } from 'firegraph/firestore-enterprise';
|
|
1047
|
+
|
|
1048
|
+
// Standard — works on any Firestore project, uses classic .where().get() queries
|
|
1049
|
+
const backend = createFirestoreStandardBackend(db, 'graph');
|
|
1050
|
+
const g = createGraphClient(backend, { registry });
|
|
1051
|
+
|
|
1052
|
+
// Enterprise — uses Firestore Pipelines by default; requires Enterprise Firestore
|
|
1053
|
+
const backend = createFirestoreEnterpriseBackend(db, 'graph');
|
|
1054
|
+
const g = createGraphClient(backend, { registry });
|
|
820
1055
|
|
|
821
|
-
//
|
|
822
|
-
const
|
|
1056
|
+
// Enterprise with classic query path (e.g. to avoid full-collection scans)
|
|
1057
|
+
const backend = createFirestoreEnterpriseBackend(db, 'graph', { defaultQueryMode: 'classic' });
|
|
823
1058
|
```
|
|
824
1059
|
|
|
825
|
-
###
|
|
1060
|
+
### Standard Backend (`firegraph/firestore-standard`)
|
|
1061
|
+
|
|
1062
|
+
Uses classic Firestore queries (`.where().get()`). Works on any Firestore project (no Enterprise edition required). Limitations:
|
|
1063
|
+
|
|
1064
|
+
| `data.*` Filters | Risk |
|
|
1065
|
+
| ----------------------------- | --------------------------------- |
|
|
1066
|
+
| Fails without composite index | Query errors for unindexed fields |
|
|
826
1067
|
|
|
827
|
-
|
|
1068
|
+
Appropriate for:
|
|
1069
|
+
|
|
1070
|
+
- Any Firestore project (Standard or Enterprise edition)
|
|
1071
|
+
- **Emulator** testing — classic queries work out of the box
|
|
1072
|
+
- Projects that manage their own composite indexes
|
|
1073
|
+
|
|
1074
|
+
### Enterprise Backend (`firegraph/firestore-enterprise`)
|
|
1075
|
+
|
|
1076
|
+
Uses the Firestore Pipeline API (`db.pipeline()`) by default. Requires **Firestore Enterprise** edition.
|
|
828
1077
|
|
|
829
1078
|
- Enables queries on `data.*` fields without composite indexes
|
|
830
|
-
-
|
|
831
|
-
- Pipeline API is currently in Preview
|
|
1079
|
+
- Unlocks additional capabilities: `query.dml`, `traversal.serverSide`, `search.fullText`, `search.geo`
|
|
832
1080
|
|
|
833
|
-
|
|
1081
|
+
**Emulator auto-fallback:** when `FIRESTORE_EMULATOR_HOST` is detected, the Enterprise backend automatically switches to the classic query path (pipelines aren't supported in the emulator). No configuration needed.
|
|
834
1082
|
|
|
835
|
-
|
|
1083
|
+
**Transactions** always use the classic query path regardless of `defaultQueryMode`, because Pipeline queries are not transactionally bound.
|
|
836
1084
|
|
|
837
|
-
|
|
838
|
-
| ----------------- | -------------------------------------- | --------------------------------- |
|
|
839
|
-
| Enterprise | Full collection scan (no index needed) | High billing on large collections |
|
|
840
|
-
| Standard | Fails without composite index | Query errors for unindexed fields |
|
|
1085
|
+
### SQLite Backend (`firegraph/sqlite`)
|
|
841
1086
|
|
|
842
|
-
|
|
1087
|
+
Shared-table SQLite backend for Node.js (`better-sqlite3`) and Cloudflare D1. Supports all four core capabilities plus `query.aggregate`, `query.select`, `query.join`, and `query.dml`. Does not support `search.*`.
|
|
843
1088
|
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
1089
|
+
```typescript
|
|
1090
|
+
import { createSqliteBackend } from 'firegraph/sqlite';
|
|
1091
|
+
import { createGraphClientFromBackend } from 'firegraph';
|
|
847
1092
|
|
|
848
|
-
|
|
1093
|
+
const backend = createSqliteBackend(executor, 'graph');
|
|
1094
|
+
const g = createGraphClientFromBackend(backend, { registry });
|
|
1095
|
+
```
|
|
849
1096
|
|
|
850
|
-
|
|
1097
|
+
Note: `core.transactions` is only declared when `executor.transaction` is defined — `better-sqlite3` provides this, but Cloudflare D1 does not.
|
|
851
1098
|
|
|
852
|
-
###
|
|
1099
|
+
### Cloudflare Durable Object Backend (`firegraph/cloudflare`)
|
|
1100
|
+
|
|
1101
|
+
Runs inside a Durable Object via `state.storage.sql`. Same capability set as SQLite minus `core.transactions` (the DO's single-threaded executor cannot block on transaction callbacks) and `raw.sql` (the DO SQL surface is hidden behind RPC).
|
|
1102
|
+
|
|
1103
|
+
```typescript
|
|
1104
|
+
// In your DO class file (workerd bundle):
|
|
1105
|
+
import { FiregraphDO } from 'firegraph/cloudflare';
|
|
1106
|
+
export class MyGraphDO extends FiregraphDO {}
|
|
1107
|
+
|
|
1108
|
+
// In your backend code (Node):
|
|
1109
|
+
import { DORPCBackend, createDOClient } from 'firegraph/cloudflare';
|
|
1110
|
+
const g = createDOClient(env.MY_GRAPH, 'graph', { registry });
|
|
1111
|
+
```
|
|
1112
|
+
|
|
1113
|
+
`firegraph/cloudflare` also re-exports `createRegistry`, `createMergedRegistry`, `generateId`, `META_NODE_TYPE`, `META_EDGE_TYPE`, and `deleteField()` so workerd-bundled code can build registries without statically importing `@google-cloud/firestore`.
|
|
1114
|
+
|
|
1115
|
+
`createSiblingClient(client, siblingRootKey)` creates a peer root-level `DOGraphClient` for a sibling collection within the same Durable Object — useful when a DO hosts multiple logical graph roots.
|
|
1116
|
+
|
|
1117
|
+
### Routing Backend (`firegraph/backend`)
|
|
1118
|
+
|
|
1119
|
+
Assembles a single capability-typed backend that routes operations to the appropriate per-subgraph backend. Use when different subgraphs live in different storage systems.
|
|
1120
|
+
|
|
1121
|
+
```typescript
|
|
1122
|
+
import { createRoutingBackend } from 'firegraph/backend';
|
|
1123
|
+
import { createGraphClientFromBackend } from 'firegraph';
|
|
1124
|
+
|
|
1125
|
+
const backend = createRoutingBackend(defaultBackend, {
|
|
1126
|
+
'users/*': userBackend,
|
|
1127
|
+
});
|
|
1128
|
+
const g = createGraphClientFromBackend(backend, { registry });
|
|
1129
|
+
```
|
|
853
1130
|
|
|
854
|
-
|
|
1131
|
+
`firegraph/backend` also exports `StorageBackend`, `BackendCapabilities`, `createCapabilities`, and `intersectCapabilities` for authors implementing custom backends.
|
|
855
1132
|
|
|
856
1133
|
### Config File
|
|
857
1134
|
|
|
858
|
-
Set the
|
|
1135
|
+
Set the default backend in `firegraph.config.ts`:
|
|
859
1136
|
|
|
860
1137
|
```typescript
|
|
861
1138
|
export default defineConfig({
|
|
862
1139
|
entities: './entities',
|
|
863
|
-
queryMode: 'pipeline', //
|
|
1140
|
+
queryMode: 'pipeline', // 'pipeline' selects Enterprise, 'standard' selects Standard
|
|
864
1141
|
});
|
|
865
1142
|
```
|
|
866
1143
|
|