@typicalday/firegraph 0.12.0 → 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 +317 -73
- package/dist/backend-DuvHGgK1.d.cts +1897 -0
- package/dist/backend-DuvHGgK1.d.ts +1897 -0
- package/dist/backend.cjs +222 -3
- 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 +197 -4
- 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-D4J7Z4FE.js +67 -0
- package/dist/chunk-D4J7Z4FE.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-AWW4MUJ5.js → chunk-TK64DNVK.js} +12 -1
- package/dist/chunk-TK64DNVK.js.map +1 -0
- package/dist/{chunk-HONQY4HF.js → chunk-WRTFC5NG.js} +362 -17
- 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 +930 -3
- package/dist/cloudflare/index.cjs.map +1 -1
- package/dist/cloudflare/index.d.cts +213 -12
- package/dist/cloudflare/index.d.ts +213 -12
- package/dist/cloudflare/index.js +562 -281
- 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 +590 -550
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +9 -37
- package/dist/index.d.ts +9 -37
- package/dist/index.js +178 -555
- package/dist/index.js.map +1 -1
- package/dist/{registry-Fi074zVa.d.ts → registry-Bc7h6WTM.d.cts} +1 -1
- package/dist/{registry-B1qsVL0E.d.cts → registry-C2KUPVZj.d.ts} +1 -1
- package/dist/{scope-path-B1G3YiA7.d.cts → scope-path-CROFZGr9.d.cts} +1 -56
- package/dist/{scope-path-B1G3YiA7.d.ts → scope-path-CROFZGr9.d.ts} +1 -56
- 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-BsR0lnFL.d.ts +0 -200
- package/dist/backend-Ct-fLlkG.d.cts +0 -200
- package/dist/chunk-AWW4MUJ5.js.map +0 -1
- package/dist/chunk-HONQY4HF.js.map +0 -1
- package/dist/types-DxYLy8Ol.d.cts +0 -770
- package/dist/types-DxYLy8Ol.d.ts +0 -770
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,18 +85,149 @@ 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
|
|
|
@@ -151,6 +284,10 @@ await g.replaceEdge('tour', tourId, 'hasDeparture', 'departure', depId, { order:
|
|
|
151
284
|
|
|
152
285
|
// Delete an edge
|
|
153
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[] }
|
|
154
291
|
```
|
|
155
292
|
|
|
156
293
|
### Field Deletion
|
|
@@ -286,11 +423,12 @@ await g.runTransaction(async (tx) => {
|
|
|
286
423
|
|
|
287
424
|
#### Run Options
|
|
288
425
|
|
|
289
|
-
| Option | Type
|
|
290
|
-
| --------------------- |
|
|
291
|
-
| `maxReads` | `number`
|
|
292
|
-
| `concurrency` | `number`
|
|
293
|
-
| `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 |
|
|
294
432
|
|
|
295
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.
|
|
296
434
|
|
|
@@ -300,6 +438,7 @@ Optional type validation using Zod (or any object with a `.parse()` method):
|
|
|
300
438
|
|
|
301
439
|
```typescript
|
|
302
440
|
import { createRegistry, createGraphClient } from 'firegraph';
|
|
441
|
+
import { createFirestoreStandardBackend } from 'firegraph/firestore-standard';
|
|
303
442
|
import { z } from 'zod';
|
|
304
443
|
|
|
305
444
|
const registry = createRegistry([
|
|
@@ -320,7 +459,8 @@ const registry = createRegistry([
|
|
|
320
459
|
},
|
|
321
460
|
]);
|
|
322
461
|
|
|
323
|
-
const
|
|
462
|
+
const backend = createFirestoreStandardBackend(db, 'graph');
|
|
463
|
+
const g = createGraphClient(backend, { registry });
|
|
324
464
|
|
|
325
465
|
// This validates against the registry before writing:
|
|
326
466
|
const id = generateId();
|
|
@@ -337,8 +477,10 @@ For agent-driven or runtime-extensible schemas, firegraph supports a **dynamic r
|
|
|
337
477
|
|
|
338
478
|
```typescript
|
|
339
479
|
import { createGraphClient } from 'firegraph';
|
|
480
|
+
import { createFirestoreStandardBackend } from 'firegraph/firestore-standard';
|
|
340
481
|
|
|
341
|
-
const
|
|
482
|
+
const backend = createFirestoreStandardBackend(db, 'graph');
|
|
483
|
+
const g = createGraphClient(backend, {
|
|
342
484
|
registryMode: { mode: 'dynamic' },
|
|
343
485
|
});
|
|
344
486
|
|
|
@@ -371,7 +513,7 @@ Key behaviors:
|
|
|
371
513
|
- **After `reloadRegistry()`**: Domain writes are validated against the compiled registry. Unknown types are always rejected.
|
|
372
514
|
- **Upsert semantics**: Calling `defineNodeType('tour', ...)` twice overwrites the previous definition. After reloading, the latest schema is used.
|
|
373
515
|
- **Separate collection**: Meta-nodes can be stored in a different collection via `registryMode: { mode: 'dynamic', collection: 'meta' }`.
|
|
374
|
-
- **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.
|
|
375
517
|
|
|
376
518
|
Dynamic registry returns a `DynamicGraphClient` which extends `GraphClient` with `defineNodeType()`, `defineEdgeType()`, and `reloadRegistry()`. Transactions and batches also validate against the compiled dynamic registry.
|
|
377
519
|
|
|
@@ -381,6 +523,7 @@ Firegraph supports schema versioning with automatic migration of records on read
|
|
|
381
523
|
|
|
382
524
|
```typescript
|
|
383
525
|
import { createRegistry, createGraphClient } from 'firegraph';
|
|
526
|
+
import { createFirestoreStandardBackend } from 'firegraph/firestore-standard';
|
|
384
527
|
import type { MigrationStep } from 'firegraph';
|
|
385
528
|
|
|
386
529
|
const migrations: MigrationStep[] = [
|
|
@@ -399,7 +542,8 @@ const registry = createRegistry([
|
|
|
399
542
|
},
|
|
400
543
|
]);
|
|
401
544
|
|
|
402
|
-
const
|
|
545
|
+
const backend = createFirestoreStandardBackend(db, 'graph');
|
|
546
|
+
const g = createGraphClient(backend, { registry });
|
|
403
547
|
|
|
404
548
|
// Reading a v0 record automatically migrates it to v2 in memory
|
|
405
549
|
const tour = await g.getNode(tourId);
|
|
@@ -427,7 +571,8 @@ Resolution order: `entry.migrationWriteBack > client.migrationWriteBack > 'off'`
|
|
|
427
571
|
|
|
428
572
|
```typescript
|
|
429
573
|
// Global default
|
|
430
|
-
const
|
|
574
|
+
const backend = createFirestoreStandardBackend(db, 'graph');
|
|
575
|
+
const g = createGraphClient(backend, {
|
|
431
576
|
registry,
|
|
432
577
|
migrationWriteBack: 'background',
|
|
433
578
|
});
|
|
@@ -461,7 +606,8 @@ Stored migration strings must be self-contained — no `import`, `require`, or e
|
|
|
461
606
|
For custom sandboxing, pass `migrationSandbox` to `createGraphClient()`:
|
|
462
607
|
|
|
463
608
|
```typescript
|
|
464
|
-
const
|
|
609
|
+
const backend = createFirestoreStandardBackend(db, 'graph');
|
|
610
|
+
const g = createGraphClient(backend, {
|
|
465
611
|
registryMode: { mode: 'dynamic' },
|
|
466
612
|
migrationSandbox: (source) => {
|
|
467
613
|
const compartment = new Compartment({
|
|
@@ -529,7 +675,8 @@ const registry = createRegistry([
|
|
|
529
675
|
{ aType: 'task', axbType: 'is', bType: 'task', allowedIn: ['workspace', '**/workspace'] },
|
|
530
676
|
]);
|
|
531
677
|
|
|
532
|
-
const
|
|
678
|
+
const backend = createFirestoreStandardBackend(db, 'graph');
|
|
679
|
+
const g = createGraphClient(backend, { registry });
|
|
533
680
|
|
|
534
681
|
// Agent only at root
|
|
535
682
|
await g.putNode('agent', agentId, {}); // OK
|
|
@@ -603,6 +750,7 @@ Edges that connect nodes across different subgraphs. The key rule: **edges live
|
|
|
603
750
|
|
|
604
751
|
```typescript
|
|
605
752
|
import { createGraphClient, createRegistry, createTraversal, generateId } from 'firegraph';
|
|
753
|
+
import { createFirestoreStandardBackend } from 'firegraph/firestore-standard';
|
|
606
754
|
|
|
607
755
|
// Registry declares that 'assignedTo' edges live in the 'workflow' subgraph
|
|
608
756
|
const registry = createRegistry([
|
|
@@ -611,7 +759,8 @@ const registry = createRegistry([
|
|
|
611
759
|
{ aType: 'task', axbType: 'assignedTo', bType: 'agent', targetGraph: 'workflow' },
|
|
612
760
|
]);
|
|
613
761
|
|
|
614
|
-
const
|
|
762
|
+
const backend = createFirestoreStandardBackend(db, 'graph');
|
|
763
|
+
const g = createGraphClient(backend, { registry });
|
|
615
764
|
|
|
616
765
|
// Create a task in the root graph
|
|
617
766
|
const taskId = generateId();
|
|
@@ -686,7 +835,7 @@ This uses Firestore collection group queries and requires collection group index
|
|
|
686
835
|
|
|
687
836
|
#### Multi-Hop Limitation
|
|
688
837
|
|
|
689
|
-
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:
|
|
690
839
|
|
|
691
840
|
```typescript
|
|
692
841
|
// This traversal finds agents in the workflow subgraph
|
|
@@ -722,19 +871,22 @@ const id = generateId(); // 21-char URL-safe nanoid
|
|
|
722
871
|
|
|
723
872
|
All errors extend `FiregraphError` with a `code` property:
|
|
724
873
|
|
|
725
|
-
| Error Class
|
|
726
|
-
|
|
|
727
|
-
| `FiregraphError`
|
|
728
|
-
| `NodeNotFoundError`
|
|
729
|
-
| `EdgeNotFoundError`
|
|
730
|
-
| `ValidationError`
|
|
731
|
-
| `RegistryViolationError`
|
|
732
|
-
| `RegistryScopeError`
|
|
733
|
-
| `MigrationError`
|
|
734
|
-
| `DynamicRegistryError`
|
|
735
|
-
| `InvalidQueryError`
|
|
736
|
-
| `QuerySafetyError`
|
|
737
|
-
| `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.) |
|
|
738
890
|
|
|
739
891
|
```typescript
|
|
740
892
|
import { FiregraphError, ValidationError } from 'firegraph';
|
|
@@ -765,15 +917,49 @@ import type {
|
|
|
765
917
|
QueryPlan,
|
|
766
918
|
QueryFilter,
|
|
767
919
|
QueryOptions,
|
|
768
|
-
|
|
769
|
-
|
|
920
|
+
QueryMode,
|
|
921
|
+
ScanProtection,
|
|
922
|
+
WhereClause,
|
|
923
|
+
IndexFieldSpec,
|
|
924
|
+
IndexSpec,
|
|
925
|
+
|
|
926
|
+
// Client interfaces — CoreGraphClient is the unconditional base
|
|
927
|
+
Capability,
|
|
928
|
+
CoreGraphClient,
|
|
770
929
|
GraphReader,
|
|
771
930
|
GraphWriter,
|
|
772
|
-
GraphClient,
|
|
931
|
+
GraphClient, // generic GraphClient<C extends Capability>
|
|
773
932
|
GraphTransaction,
|
|
774
933
|
GraphBatch,
|
|
775
934
|
GraphClientOptions,
|
|
776
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
|
+
|
|
777
963
|
// Registry
|
|
778
964
|
RegistryEntry, // includes targetGraph, allowedIn
|
|
779
965
|
GraphRegistry, // includes lookupByAxbType
|
|
@@ -781,9 +967,12 @@ import type {
|
|
|
781
967
|
|
|
782
968
|
// Dynamic Registry
|
|
783
969
|
DynamicGraphClient,
|
|
970
|
+
DynamicGraphMethods,
|
|
784
971
|
DynamicRegistryConfig,
|
|
785
972
|
NodeTypeData,
|
|
786
973
|
EdgeTypeData,
|
|
974
|
+
DefineTypeOptions,
|
|
975
|
+
CascadeResult,
|
|
787
976
|
|
|
788
977
|
// Migration
|
|
789
978
|
MigrationFn,
|
|
@@ -791,6 +980,7 @@ import type {
|
|
|
791
980
|
StoredMigrationStep,
|
|
792
981
|
MigrationExecutor,
|
|
793
982
|
MigrationWriteBack,
|
|
983
|
+
MigrationResult,
|
|
794
984
|
|
|
795
985
|
// Traversal
|
|
796
986
|
HopDefinition, // includes targetGraph
|
|
@@ -801,10 +991,14 @@ import type {
|
|
|
801
991
|
|
|
802
992
|
// Entity Discovery
|
|
803
993
|
DiscoveredEntity,
|
|
804
|
-
|
|
994
|
+
DiscoverResult, // return type of discoverEntities()
|
|
995
|
+
DiscoveryResult, // { nodes: Map<...>, edges: Map<...> } — the .result field of DiscoverResult
|
|
996
|
+
DiscoveryWarning,
|
|
805
997
|
} from 'firegraph';
|
|
806
998
|
```
|
|
807
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
|
+
|
|
808
1002
|
## How It Works
|
|
809
1003
|
|
|
810
1004
|
### Storage Layout
|
|
@@ -832,68 +1026,118 @@ When you call `findEdges`, the query planner decides the strategy:
|
|
|
832
1026
|
|
|
833
1027
|
### Traversal Execution
|
|
834
1028
|
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
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.
|
|
845
1038
|
|
|
846
1039
|
## Query Modes
|
|
847
1040
|
|
|
848
|
-
Firegraph
|
|
1041
|
+
Firegraph ships two Firestore backends that you choose at construction time:
|
|
849
1042
|
|
|
850
1043
|
```typescript
|
|
851
|
-
|
|
852
|
-
|
|
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 });
|
|
853
1055
|
|
|
854
|
-
//
|
|
855
|
-
const
|
|
1056
|
+
// Enterprise with classic query path (e.g. to avoid full-collection scans)
|
|
1057
|
+
const backend = createFirestoreEnterpriseBackend(db, 'graph', { defaultQueryMode: 'classic' });
|
|
856
1058
|
```
|
|
857
1059
|
|
|
858
|
-
###
|
|
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 |
|
|
1067
|
+
|
|
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
|
|
859
1073
|
|
|
860
|
-
|
|
1074
|
+
### Enterprise Backend (`firegraph/firestore-enterprise`)
|
|
1075
|
+
|
|
1076
|
+
Uses the Firestore Pipeline API (`db.pipeline()`) by default. Requires **Firestore Enterprise** edition.
|
|
861
1077
|
|
|
862
1078
|
- Enables queries on `data.*` fields without composite indexes
|
|
863
|
-
-
|
|
864
|
-
- Pipeline API is currently in Preview
|
|
1079
|
+
- Unlocks additional capabilities: `query.dml`, `traversal.serverSide`, `search.fullText`, `search.geo`
|
|
865
1080
|
|
|
866
|
-
|
|
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.
|
|
867
1082
|
|
|
868
|
-
|
|
1083
|
+
**Transactions** always use the classic query path regardless of `defaultQueryMode`, because Pipeline queries are not transactionally bound.
|
|
869
1084
|
|
|
870
|
-
|
|
871
|
-
| ----------------- | -------------------------------------- | --------------------------------- |
|
|
872
|
-
| Enterprise | Full collection scan (no index needed) | High billing on large collections |
|
|
873
|
-
| Standard | Fails without composite index | Query errors for unindexed fields |
|
|
1085
|
+
### SQLite Backend (`firegraph/sqlite`)
|
|
874
1086
|
|
|
875
|
-
|
|
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.*`.
|
|
876
1088
|
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
1089
|
+
```typescript
|
|
1090
|
+
import { createSqliteBackend } from 'firegraph/sqlite';
|
|
1091
|
+
import { createGraphClientFromBackend } from 'firegraph';
|
|
880
1092
|
|
|
881
|
-
|
|
1093
|
+
const backend = createSqliteBackend(executor, 'graph');
|
|
1094
|
+
const g = createGraphClientFromBackend(backend, { registry });
|
|
1095
|
+
```
|
|
882
1096
|
|
|
883
|
-
|
|
1097
|
+
Note: `core.transactions` is only declared when `executor.transaction` is defined — `better-sqlite3` provides this, but Cloudflare D1 does not.
|
|
884
1098
|
|
|
885
|
-
###
|
|
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
|
+
```
|
|
886
1130
|
|
|
887
|
-
|
|
1131
|
+
`firegraph/backend` also exports `StorageBackend`, `BackendCapabilities`, `createCapabilities`, and `intersectCapabilities` for authors implementing custom backends.
|
|
888
1132
|
|
|
889
1133
|
### Config File
|
|
890
1134
|
|
|
891
|
-
Set the
|
|
1135
|
+
Set the default backend in `firegraph.config.ts`:
|
|
892
1136
|
|
|
893
1137
|
```typescript
|
|
894
1138
|
export default defineConfig({
|
|
895
1139
|
entities: './entities',
|
|
896
|
-
queryMode: 'pipeline', //
|
|
1140
|
+
queryMode: 'pipeline', // 'pipeline' selects Enterprise, 'standard' selects Standard
|
|
897
1141
|
});
|
|
898
1142
|
```
|
|
899
1143
|
|