@typicalday/firegraph 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/README.md +93 -90
  2. package/bin/firegraph.mjs +21 -7
  3. package/dist/backend-U-MLShlg.d.ts +97 -0
  4. package/dist/backend-np4gEVhB.d.cts +97 -0
  5. package/dist/backend.cjs.map +1 -1
  6. package/dist/backend.d.cts +7 -6
  7. package/dist/backend.d.ts +7 -6
  8. package/dist/backend.js +1 -1
  9. package/dist/backend.js.map +1 -1
  10. package/dist/{chunk-EVUM6ORB.js → chunk-6SB34IPQ.js} +76 -8
  11. package/dist/chunk-6SB34IPQ.js.map +1 -0
  12. package/dist/{chunk-SU4FNLC3.js → chunk-EEKWRX5E.js} +1 -1
  13. package/dist/{chunk-SU4FNLC3.js.map → chunk-EEKWRX5E.js.map} +1 -1
  14. package/dist/{chunk-YLGXLEUE.js → chunk-GJVVRTQT.js} +5 -14
  15. package/dist/chunk-GJVVRTQT.js.map +1 -0
  16. package/dist/{chunk-GLOVWKQH.js → chunk-R7CRGYY4.js} +1 -1
  17. package/dist/{chunk-GLOVWKQH.js.map → chunk-R7CRGYY4.js.map} +1 -1
  18. package/dist/{do-sqlite.cjs → cloudflare/index.cjs} +1659 -1422
  19. package/dist/cloudflare/index.cjs.map +1 -0
  20. package/dist/cloudflare/index.d.cts +529 -0
  21. package/dist/cloudflare/index.d.ts +529 -0
  22. package/dist/cloudflare/index.js +934 -0
  23. package/dist/cloudflare/index.js.map +1 -0
  24. package/dist/codegen/index.cjs +4 -13
  25. package/dist/codegen/index.cjs.map +1 -1
  26. package/dist/codegen/index.d.cts +1 -1
  27. package/dist/codegen/index.d.ts +1 -1
  28. package/dist/codegen/index.js +1 -1
  29. package/dist/index.cjs +144 -132
  30. package/dist/index.cjs.map +1 -1
  31. package/dist/index.d.cts +116 -27
  32. package/dist/index.d.ts +116 -27
  33. package/dist/index.js +77 -123
  34. package/dist/index.js.map +1 -1
  35. package/dist/query-client/index.cjs.map +1 -1
  36. package/dist/query-client/index.js +1 -1
  37. package/dist/{scope-path-BtajqNK5.d.ts → scope-path-B1G3YiA7.d.cts} +5 -100
  38. package/dist/{scope-path-D2mNENJ-.d.cts → scope-path-B1G3YiA7.d.ts} +5 -100
  39. package/dist/{types-DfWVTsMn.d.ts → types-BGWxcpI_.d.cts} +92 -1
  40. package/dist/{types-DfWVTsMn.d.cts → types-BGWxcpI_.d.ts} +92 -1
  41. package/package.json +13 -17
  42. package/dist/chunk-EVUM6ORB.js.map +0 -1
  43. package/dist/chunk-SZ6W4VAS.js +0 -701
  44. package/dist/chunk-SZ6W4VAS.js.map +0 -1
  45. package/dist/chunk-YLGXLEUE.js.map +0 -1
  46. package/dist/d1.cjs +0 -2421
  47. package/dist/d1.cjs.map +0 -1
  48. package/dist/d1.d.cts +0 -54
  49. package/dist/d1.d.ts +0 -54
  50. package/dist/d1.js +0 -76
  51. package/dist/d1.js.map +0 -1
  52. package/dist/do-sqlite.cjs.map +0 -1
  53. package/dist/do-sqlite.d.cts +0 -41
  54. package/dist/do-sqlite.d.ts +0 -41
  55. package/dist/do-sqlite.js +0 -79
  56. package/dist/do-sqlite.js.map +0 -1
  57. package/dist/editor/client/assets/index-Bq2bfzeY.js +0 -411
  58. package/dist/editor/client/assets/index-CJ4m_EOL.css +0 -1
  59. package/dist/editor/client/index.html +0 -16
  60. package/dist/editor/server/index.mjs +0 -51511
package/README.md CHANGED
@@ -90,6 +90,7 @@ const g = createGraphClient(db, 'graph', { registry });
90
90
  ```
91
91
 
92
92
  **Parameters:**
93
+
93
94
  - `db` — A `Firestore` instance from `@google-cloud/firestore`
94
95
  - `collectionPath` — Firestore collection path for all graph data
95
96
  - `options.registry` — Optional `GraphRegistry` for schema validation
@@ -207,10 +208,10 @@ const result = await createTraversal(g, tourId)
207
208
  })
208
209
  .run({ maxReads: 200, returnIntermediates: true });
209
210
 
210
- result.nodes; // StoredGraphRecord[] — edges from the final hop
211
- result.hops; // HopResult[] — per-hop breakdown
211
+ result.nodes; // StoredGraphRecord[] — edges from the final hop
212
+ result.hops; // HopResult[] — per-hop breakdown
212
213
  result.totalReads; // number — Firestore reads consumed
213
- result.truncated; // boolean — true if budget was hit
214
+ result.truncated; // boolean — true if budget was hit
214
215
  ```
215
216
 
216
217
  `createTraversal` accepts a `GraphClient` or `GraphReader`. When passed a `GraphClient`, cross-graph hops via `targetGraph` are supported (see [Cross-Graph Edges](#cross-graph-edges)).
@@ -233,33 +234,30 @@ const result = await createTraversal(g, riderId)
233
234
 
234
235
  ```typescript
235
236
  await g.runTransaction(async (tx) => {
236
- const result = await createTraversal(tx, tourId)
237
- .follow('hasDeparture')
238
- .follow('hasRider')
239
- .run();
237
+ const result = await createTraversal(tx, tourId).follow('hasDeparture').follow('hasRider').run();
240
238
  // Use result to make transactional writes...
241
239
  });
242
240
  ```
243
241
 
244
242
  #### Hop Options
245
243
 
246
- | Option | Type | Default | Description |
247
- |--------|------|---------|-------------|
248
- | `direction` | `'forward' \| 'reverse'` | `'forward'` | Edge direction |
249
- | `aType` | `string` | — | Filter source node type |
250
- | `bType` | `string` | — | Filter target node type |
251
- | `limit` | `number` | `10` | Max edges per source node |
252
- | `orderBy` | `{ field, direction? }` | — | Firestore-level ordering |
253
- | `filter` | `(edge) => boolean` | — | In-memory post-filter |
254
- | `targetGraph` | `string` | — | Subgraph to cross into (forward only). See [Cross-Graph Edges](#cross-graph-edges) |
244
+ | Option | Type | Default | Description |
245
+ | ------------- | ------------------------ | ----------- | ---------------------------------------------------------------------------------- |
246
+ | `direction` | `'forward' \| 'reverse'` | `'forward'` | Edge direction |
247
+ | `aType` | `string` | — | Filter source node type |
248
+ | `bType` | `string` | — | Filter target node type |
249
+ | `limit` | `number` | `10` | Max edges per source node |
250
+ | `orderBy` | `{ field, direction? }` | — | Firestore-level ordering |
251
+ | `filter` | `(edge) => boolean` | — | In-memory post-filter |
252
+ | `targetGraph` | `string` | — | Subgraph to cross into (forward only). See [Cross-Graph Edges](#cross-graph-edges) |
255
253
 
256
254
  #### Run Options
257
255
 
258
- | Option | Type | Default | Description |
259
- |--------|------|---------|-------------|
260
- | `maxReads` | `number` | `100` | Total Firestore read budget |
261
- | `concurrency` | `number` | `5` | Max parallel queries per hop |
262
- | `returnIntermediates` | `boolean` | `false` | Include edges from all hops |
256
+ | Option | Type | Default | Description |
257
+ | --------------------- | --------- | ------- | ---------------------------- |
258
+ | `maxReads` | `number` | `100` | Total Firestore read budget |
259
+ | `concurrency` | `number` | `5` | Max parallel queries per hop |
260
+ | `returnIntermediates` | `boolean` | `false` | Include edges from all hops |
263
261
 
264
262
  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.
265
263
 
@@ -363,7 +361,7 @@ const registry = createRegistry([
363
361
  axbType: 'is',
364
362
  bType: 'tour',
365
363
  jsonSchema: tourSchemaV2,
366
- migrations, // version derived as max(toVersion) = 2
364
+ migrations, // version derived as max(toVersion) = 2
367
365
  migrationWriteBack: 'eager',
368
366
  },
369
367
  ]);
@@ -386,11 +384,11 @@ const tour = await g.getNode(tourId);
386
384
 
387
385
  Write-back controls whether migrated data is persisted back to Firestore after a read-triggered migration:
388
386
 
389
- | Mode | Behavior |
390
- |------|----------|
391
- | `'off'` | In-memory only; Firestore document unchanged (default) |
392
- | `'eager'` | Fire-and-forget write after read; inline update in transactions |
393
- | `'background'` | Same as eager but errors are swallowed with a `console.warn` |
387
+ | Mode | Behavior |
388
+ | -------------- | --------------------------------------------------------------- |
389
+ | `'off'` | In-memory only; Firestore document unchanged (default) |
390
+ | `'eager'` | Fire-and-forget write after read; inline update in transactions |
391
+ | `'background'` | Same as eager but errors are swallowed with a `console.warn` |
394
392
 
395
393
  Resolution order: `entry.migrationWriteBack > client.migrationWriteBack > 'off'`
396
394
 
@@ -402,11 +400,15 @@ const g = createGraphClient(db, 'graph', {
402
400
  });
403
401
 
404
402
  // Entry-level override (takes priority)
405
- createRegistry([{
406
- aType: 'tour', axbType: 'is', bType: 'tour',
407
- migrations,
408
- migrationWriteBack: 'eager',
409
- }]);
403
+ createRegistry([
404
+ {
405
+ aType: 'tour',
406
+ axbType: 'is',
407
+ bType: 'tour',
408
+ migrations,
409
+ migrationWriteBack: 'eager',
410
+ },
411
+ ]);
410
412
  ```
411
413
 
412
414
  #### Dynamic Registry Migrations
@@ -415,9 +417,7 @@ In dynamic mode, migrations are stored as source code strings:
415
417
 
416
418
  ```typescript
417
419
  await g.defineNodeType('tour', tourSchema, 'A tour', {
418
- migrations: [
419
- { fromVersion: 0, toVersion: 1, up: '(d) => ({ ...d, status: "draft" })' },
420
- ],
420
+ migrations: [{ fromVersion: 0, toVersion: 1, up: '(d) => ({ ...d, status: "draft" })' }],
421
421
  migrationWriteBack: 'eager',
422
422
  });
423
423
  await g.reloadRegistry();
@@ -431,7 +431,9 @@ For custom sandboxing, pass `migrationSandbox` to `createGraphClient()`:
431
431
  const g = createGraphClient(db, 'graph', {
432
432
  registryMode: { mode: 'dynamic' },
433
433
  migrationSandbox: (source) => {
434
- const compartment = new Compartment({ /* endowments */ });
434
+ const compartment = new Compartment({
435
+ /* endowments */
436
+ });
435
437
  return compartment.evaluate(source);
436
438
  },
437
439
  });
@@ -507,14 +509,14 @@ await g.putNode('memory', generateId(), {}); // throws RegistryScopeError
507
509
 
508
510
  **Pattern syntax:**
509
511
 
510
- | Pattern | Matches |
511
- |---------|---------|
512
- | `root` | Top-level collection only |
513
- | `memories` | Exact subgraph name |
514
- | `workspace/tasks` | Exact path |
515
- | `*/memories` | `*` matches one segment |
516
- | `**/memories` | `**` matches zero or more segments |
517
- | `**` | Everything including root |
512
+ | Pattern | Matches |
513
+ | ----------------- | ---------------------------------- |
514
+ | `root` | Top-level collection only |
515
+ | `memories` | Exact subgraph name |
516
+ | `workspace/tasks` | Exact path |
517
+ | `*/memories` | `*` matches one segment |
518
+ | `**/memories` | `**` matches zero or more segments |
519
+ | `**` | Everything including root |
518
520
 
519
521
  Omitting `allowedIn` (or passing an empty array) means the type is allowed everywhere.
520
522
 
@@ -592,9 +594,7 @@ await workflow.putNode('agent', agentId, { name: 'Backend Dev' });
592
594
  await workflow.putEdge('task', taskId, 'assignedTo', 'agent', agentId, { role: 'lead' });
593
595
 
594
596
  // Forward traversal: task → agents (automatically crosses into workflow subgraph)
595
- const result = await createTraversal(g, taskId, registry)
596
- .follow('assignedTo')
597
- .run();
597
+ const result = await createTraversal(g, taskId, registry).follow('assignedTo').run();
598
598
  // result.nodes contains the agent edges from the workflow subgraph
599
599
  ```
600
600
 
@@ -632,9 +632,7 @@ You can override the registry's `targetGraph` on a per-hop basis:
632
632
 
633
633
  ```typescript
634
634
  // Use 'team' subgraph instead of registry's default
635
- const result = await createTraversal(g, taskId)
636
- .follow('assignedTo', { targetGraph: 'team' })
637
- .run();
635
+ const result = await createTraversal(g, taskId).follow('assignedTo', { targetGraph: 'team' }).run();
638
636
  ```
639
637
 
640
638
  Resolution priority: explicit hop `targetGraph` > registry `targetGraph` > no cross-graph.
@@ -647,7 +645,7 @@ For cross-cutting reads across all subgraphs, use `findEdgesGlobal`:
647
645
  // Find all 'assignedTo' edges across all 'workflow' subgraphs in the database
648
646
  const allAssignments = await g.findEdgesGlobal(
649
647
  { axbType: 'assignedTo', allowCollectionScan: true },
650
- 'workflow', // collection name to query across
648
+ 'workflow', // collection name to query across
651
649
  );
652
650
  ```
653
651
 
@@ -659,17 +657,13 @@ Each hop resolves its reader from the root client. If hop 1 crosses into a subgr
659
657
 
660
658
  ```typescript
661
659
  // This traversal finds agents in the workflow subgraph
662
- const agents = await createTraversal(g, taskId, registry)
663
- .follow('assignedTo')
664
- .run();
660
+ const agents = await createTraversal(g, taskId, registry).follow('assignedTo').run();
665
661
 
666
662
  // To continue traversing within the workflow subgraph,
667
663
  // create a new traversal from the subgraph client
668
664
  const workflow = g.subgraph(taskId, 'workflow');
669
665
  for (const agent of agents.nodes) {
670
- const mentees = await createTraversal(workflow, agent.bUid)
671
- .follow('mentors')
672
- .run();
666
+ const mentees = await createTraversal(workflow, agent.bUid).follow('mentors').run();
673
667
  }
674
668
  ```
675
669
 
@@ -695,19 +689,19 @@ const id = generateId(); // 21-char URL-safe nanoid
695
689
 
696
690
  All errors extend `FiregraphError` with a `code` property:
697
691
 
698
- | Error Class | Code | When |
699
- |------------|------|------|
700
- | `FiregraphError` | varies | Base class |
701
- | `NodeNotFoundError` | `NODE_NOT_FOUND` | Node lookup fails (not thrown by `getNode` — it returns `null`) |
702
- | `EdgeNotFoundError` | `EDGE_NOT_FOUND` | Edge lookup fails |
703
- | `ValidationError` | `VALIDATION_ERROR` | Schema validation fails (registry + Zod) |
704
- | `RegistryViolationError` | `REGISTRY_VIOLATION` | Triple not registered |
705
- | `RegistryScopeError` | `REGISTRY_SCOPE` | Type not allowed at this subgraph scope |
706
- | `MigrationError` | `MIGRATION_ERROR` | Migration function fails or chain is incomplete |
707
- | `DynamicRegistryError` | `DYNAMIC_REGISTRY_ERROR` | Dynamic registry misconfiguration or misuse |
708
- | `InvalidQueryError` | `INVALID_QUERY` | `findEdges` called with no filters |
709
- | `QuerySafetyError` | `QUERY_SAFETY` | Query would cause a full collection scan |
710
- | `TraversalError` | `TRAVERSAL_ERROR` | `run()` called with zero hops |
692
+ | Error Class | Code | When |
693
+ | ------------------------ | ------------------------ | --------------------------------------------------------------- |
694
+ | `FiregraphError` | varies | Base class |
695
+ | `NodeNotFoundError` | `NODE_NOT_FOUND` | Node lookup fails (not thrown by `getNode` — it returns `null`) |
696
+ | `EdgeNotFoundError` | `EDGE_NOT_FOUND` | Edge lookup fails |
697
+ | `ValidationError` | `VALIDATION_ERROR` | Schema validation fails (registry + Zod) |
698
+ | `RegistryViolationError` | `REGISTRY_VIOLATION` | Triple not registered |
699
+ | `RegistryScopeError` | `REGISTRY_SCOPE` | Type not allowed at this subgraph scope |
700
+ | `MigrationError` | `MIGRATION_ERROR` | Migration function fails or chain is incomplete |
701
+ | `DynamicRegistryError` | `DYNAMIC_REGISTRY_ERROR` | Dynamic registry misconfiguration or misuse |
702
+ | `InvalidQueryError` | `INVALID_QUERY` | `findEdges` called with no filters |
703
+ | `QuerySafetyError` | `QUERY_SAFETY` | Query would cause a full collection scan |
704
+ | `TraversalError` | `TRAVERSAL_ERROR` | `run()` called with zero hops |
711
705
 
712
706
  ```typescript
713
707
  import { FiregraphError, ValidationError } from 'firegraph';
@@ -716,7 +710,7 @@ try {
716
710
  await g.putNode('tour', generateId(), { name: 123 });
717
711
  } catch (err) {
718
712
  if (err instanceof ValidationError) {
719
- console.error(err.code); // 'VALIDATION_ERROR'
713
+ console.error(err.code); // 'VALIDATION_ERROR'
720
714
  console.error(err.details); // Zod error details
721
715
  }
722
716
  }
@@ -748,9 +742,9 @@ import type {
748
742
  GraphClientOptions,
749
743
 
750
744
  // Registry
751
- RegistryEntry, // includes targetGraph, allowedIn
752
- GraphRegistry, // includes lookupByAxbType
753
- EdgeTopology, // includes targetGraph
745
+ RegistryEntry, // includes targetGraph, allowedIn
746
+ GraphRegistry, // includes lookupByAxbType
747
+ EdgeTopology, // includes targetGraph
754
748
 
755
749
  // Dynamic Registry
756
750
  DynamicGraphClient,
@@ -766,7 +760,7 @@ import type {
766
760
  MigrationWriteBack,
767
761
 
768
762
  // Traversal
769
- HopDefinition, // includes targetGraph
763
+ HopDefinition, // includes targetGraph
770
764
  TraversalOptions,
771
765
  HopResult,
772
766
  TraversalResult,
@@ -784,17 +778,17 @@ import type {
784
778
 
785
779
  All data lives in one Firestore collection. Each document has these fields:
786
780
 
787
- | Field | Type | Description |
788
- |-------|------|-------------|
789
- | `aType` | string | Source node type |
790
- | `aUid` | string | Source node ID |
791
- | `axbType` | string | Relationship type (`is` for nodes) |
792
- | `bType` | string | Target node type |
793
- | `bUid` | string | Target node ID |
794
- | `data` | object | User payload |
795
- | `v` | number? | Schema version (derived from `max(toVersion)` of migrations; set when entry has migrations) |
796
- | `createdAt` | Timestamp | Server-set on create |
797
- | `updatedAt` | Timestamp | Server-set on create/update |
781
+ | Field | Type | Description |
782
+ | ----------- | --------- | ------------------------------------------------------------------------------------------- |
783
+ | `aType` | string | Source node type |
784
+ | `aUid` | string | Source node ID |
785
+ | `axbType` | string | Relationship type (`is` for nodes) |
786
+ | `bType` | string | Target node type |
787
+ | `bUid` | string | Target node ID |
788
+ | `data` | object | User payload |
789
+ | `v` | number? | Schema version (derived from `max(toVersion)` of migrations; set when entry has migrations) |
790
+ | `createdAt` | Timestamp | Server-set on create |
791
+ | `updatedAt` | Timestamp | Server-set on create/update |
798
792
 
799
793
  ### Query Planning
800
794
 
@@ -840,12 +834,13 @@ Uses the Firestore Pipeline API (`db.pipeline()`). This is the recommended mode
840
834
 
841
835
  Uses standard Firestore queries (`.where().get()`). Use only if you understand the limitations:
842
836
 
843
- | Firestore Edition | With `data.*` Filters | Risk |
844
- |---|---|---|
845
- | Enterprise | Full collection scan (no index needed) | High billing on large collections |
846
- | Standard | Fails without composite index | Query errors for unindexed fields |
837
+ | Firestore Edition | With `data.*` Filters | Risk |
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 |
847
841
 
848
842
  Standard mode is appropriate for:
843
+
849
844
  - **Emulator** — the emulator doesn't support pipelines, so firegraph auto-falls back to standard mode when `FIRESTORE_EMULATOR_HOST` is set
850
845
  - **Small datasets** where full scans are acceptable
851
846
  - Projects that manage their own composite indexes
@@ -882,6 +877,14 @@ pnpm test:emulator # Full test suite against Firestore emulator
882
877
 
883
878
  Requires Node.js 18+.
884
879
 
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
+
885
888
  ## License
886
889
 
887
890
  MIT
package/bin/firegraph.mjs CHANGED
@@ -36,7 +36,9 @@ if (subcommand === 'editor') {
36
36
  try {
37
37
  execSync('npm run build:editor', { cwd: pkgDir, stdio: 'inherit' });
38
38
  } catch {
39
- console.error('Failed to build editor. Run "npm run build:editor" manually in the firegraph package directory.');
39
+ console.error(
40
+ 'Failed to build editor. Run "npm run build:editor" manually in the firegraph package directory.',
41
+ );
40
42
  process.exit(1);
41
43
  }
42
44
  }
@@ -93,16 +95,20 @@ if (subcommand === 'editor') {
93
95
  const outPath = args.out || null;
94
96
 
95
97
  const distIndex = path.join(__dirname, '..', 'dist', 'index.js');
96
- const { generateIndexConfig, discoverEntities } = await import(distIndex);
98
+ const { generateIndexConfig, discoverEntities, createRegistry } = await import(distIndex);
97
99
 
98
100
  try {
99
101
  let entities = undefined;
102
+ let registryEntries = undefined;
100
103
  if (entitiesDir) {
101
104
  const { result, warnings } = discoverEntities(entitiesDir);
102
105
  for (const w of warnings) {
103
106
  console.warn(` warning: ${w.message}`);
104
107
  }
105
108
  entities = result;
109
+ // Build a registry so per-entity `indexes` (from meta.json) reach
110
+ // the generator via RegistryEntry.indexes.
111
+ registryEntries = createRegistry(result).entries();
106
112
  const nodeCount = result.nodes.size;
107
113
  const edgeCount = result.edges.size;
108
114
  if (nodeCount > 0 || edgeCount > 0) {
@@ -110,7 +116,7 @@ if (subcommand === 'editor') {
110
116
  }
111
117
  }
112
118
 
113
- const config = generateIndexConfig(collection, entities);
119
+ const config = generateIndexConfig(collection, { entities, registryEntries });
114
120
  const output = JSON.stringify(config, null, 2) + '\n';
115
121
 
116
122
  if (outPath) {
@@ -136,7 +142,9 @@ if (subcommand === 'editor') {
136
142
  console.log(' indexes Generate recommended Firestore index definitions');
137
143
  console.log('');
138
144
  console.log(' Editor options:');
139
- console.log(' --config <path> Path to firegraph.config.ts (default: auto-discover in cwd)');
145
+ console.log(
146
+ ' --config <path> Path to firegraph.config.ts (default: auto-discover in cwd)',
147
+ );
140
148
  console.log(' --entities <path> Path to entities directory');
141
149
  console.log(' --project <id> GCP project ID (default: auto-detect via ADC)');
142
150
  console.log(' --collection <path> Firestore collection path (default: graph)');
@@ -152,7 +160,9 @@ if (subcommand === 'editor') {
152
160
  console.log(' --out <path> Output file path (default: stdout)');
153
161
  console.log('');
154
162
  console.log(' Indexes options:');
155
- console.log(' --entities <path> Path to entities directory (adds per-entity data field indexes)');
163
+ console.log(
164
+ ' --entities <path> Path to entities directory (adds per-entity data field indexes)',
165
+ );
156
166
  console.log(' --collection <name> Firestore collection name (default: graph)');
157
167
  console.log(' --out <path> Output file path (default: stdout)');
158
168
  console.log('');
@@ -161,12 +171,16 @@ if (subcommand === 'editor') {
161
171
  console.log(' flags every time. CLI flags override config file values.');
162
172
  console.log('');
163
173
  console.log(' Examples:');
164
- console.log(' npx firegraph editor # uses firegraph.config.ts');
174
+ console.log(
175
+ ' npx firegraph editor # uses firegraph.config.ts',
176
+ );
165
177
  console.log(' npx firegraph editor --config ./custom-config.ts # explicit config file');
166
178
  console.log(' npx firegraph editor --entities ./entities # per-entity convention');
167
179
  console.log(' npx firegraph codegen --entities ./entities # types to stdout');
168
180
  console.log(' npx firegraph codegen --entities ./entities --out src/generated/types.ts');
169
- console.log(' npx firegraph indexes # 4 base indexes to stdout');
181
+ console.log(
182
+ ' npx firegraph indexes # 4 base indexes to stdout',
183
+ );
170
184
  console.log(' npx firegraph indexes --entities ./entities --out firestore.indexes.json');
171
185
  console.log('');
172
186
  } else {
@@ -0,0 +1,97 @@
1
+ import { S as StoredGraphRecord, i as QueryFilter, z as QueryOptions, d as GraphReader, m as BulkOptions, C as CascadeResult, F as FindEdgesParams, o as BulkResult } from './types-BGWxcpI_.js';
2
+
3
+ /**
4
+ * Backend abstraction for firegraph.
5
+ *
6
+ * `StorageBackend` is the single interface every storage driver implements.
7
+ * The Firestore backend wraps `@google-cloud/firestore`; the SQLite backend
8
+ * (shared by D1 and Durable Object SQLite) uses a parameterized SQL executor.
9
+ *
10
+ * `GraphClientImpl` and friends depend only on this interface — they have
11
+ * no direct knowledge of Firestore or SQLite.
12
+ */
13
+
14
+ /**
15
+ * Per-record write payload — backend-agnostic. Timestamps are not present;
16
+ * the backend supplies them via `serverTimestamp()` placeholders that it
17
+ * itself resolves at commit time.
18
+ */
19
+ interface WritableRecord {
20
+ aType: string;
21
+ aUid: string;
22
+ axbType: string;
23
+ bType: string;
24
+ bUid: string;
25
+ data: Record<string, unknown>;
26
+ /** Schema version (set by the writer when registry has migrations). */
27
+ v?: number;
28
+ }
29
+ /**
30
+ * Patch shape for `updateDoc`. Captures the two patterns that exist today:
31
+ * - `dataFields`: shallow merge under `data` (used by `updateNode`)
32
+ * - `replaceData`: full data replacement (used by migration write-back)
33
+ * - `v`: optional schema-version stamp
34
+ *
35
+ * `updatedAt` is always set by the backend.
36
+ */
37
+ interface UpdatePayload {
38
+ dataFields?: Record<string, unknown>;
39
+ replaceData?: Record<string, unknown>;
40
+ v?: number;
41
+ }
42
+ /**
43
+ * Read/write transaction adapter. Mirrors Firestore's transaction semantics:
44
+ * reads are snapshot-consistent; writes are issued inside the transaction
45
+ * and a rejection from any write aborts the surrounding `runTransaction`.
46
+ *
47
+ * Writes return `Promise<void>` so SQL drivers can surface row-level errors
48
+ * (constraint violations, malformed JSON paths) rather than swallowing them.
49
+ * Firestore implementations can resolve synchronously since the underlying
50
+ * `Transaction.set/update/delete` calls are themselves synchronous buffers.
51
+ */
52
+ interface TransactionBackend {
53
+ getDoc(docId: string): Promise<StoredGraphRecord | null>;
54
+ query(filters: QueryFilter[], options?: QueryOptions): Promise<StoredGraphRecord[]>;
55
+ setDoc(docId: string, record: WritableRecord): Promise<void>;
56
+ updateDoc(docId: string, update: UpdatePayload): Promise<void>;
57
+ deleteDoc(docId: string): Promise<void>;
58
+ }
59
+ /**
60
+ * Atomic multi-write batch.
61
+ */
62
+ interface BatchBackend {
63
+ setDoc(docId: string, record: WritableRecord): void;
64
+ updateDoc(docId: string, update: UpdatePayload): void;
65
+ deleteDoc(docId: string): void;
66
+ commit(): Promise<void>;
67
+ }
68
+ /**
69
+ * The single storage abstraction.
70
+ *
71
+ * Each backend instance is scoped to a "graph location" — for Firestore
72
+ * that's a collection path; for SQLite it's a (table, scopePath) pair.
73
+ * `subgraph()` returns a child backend bound to a nested location.
74
+ */
75
+ interface StorageBackend {
76
+ /** Backend-internal location identifier (collection path or table name). */
77
+ readonly collectionPath: string;
78
+ /** Subgraph scope (empty string for root). */
79
+ readonly scopePath: string;
80
+ getDoc(docId: string): Promise<StoredGraphRecord | null>;
81
+ query(filters: QueryFilter[], options?: QueryOptions): Promise<StoredGraphRecord[]>;
82
+ setDoc(docId: string, record: WritableRecord): Promise<void>;
83
+ updateDoc(docId: string, update: UpdatePayload): Promise<void>;
84
+ deleteDoc(docId: string): Promise<void>;
85
+ runTransaction<T>(fn: (tx: TransactionBackend) => Promise<T>): Promise<T>;
86
+ createBatch(): BatchBackend;
87
+ subgraph(parentNodeUid: string, name: string): StorageBackend;
88
+ removeNodeCascade(uid: string, reader: GraphReader, options?: BulkOptions): Promise<CascadeResult>;
89
+ bulkRemoveEdges(params: FindEdgesParams, reader: GraphReader, options?: BulkOptions): Promise<BulkResult>;
90
+ /**
91
+ * Find edges across all subgraphs sharing a given collection name.
92
+ * Optional — backends that can't support this should throw a clear error.
93
+ */
94
+ findEdgesGlobal?(params: FindEdgesParams, collectionName?: string): Promise<StoredGraphRecord[]>;
95
+ }
96
+
97
+ export type { BatchBackend as B, StorageBackend as S, TransactionBackend as T, UpdatePayload as U, WritableRecord as W };
@@ -0,0 +1,97 @@
1
+ import { S as StoredGraphRecord, i as QueryFilter, z as QueryOptions, d as GraphReader, m as BulkOptions, C as CascadeResult, F as FindEdgesParams, o as BulkResult } from './types-BGWxcpI_.cjs';
2
+
3
+ /**
4
+ * Backend abstraction for firegraph.
5
+ *
6
+ * `StorageBackend` is the single interface every storage driver implements.
7
+ * The Firestore backend wraps `@google-cloud/firestore`; the SQLite backend
8
+ * (shared by D1 and Durable Object SQLite) uses a parameterized SQL executor.
9
+ *
10
+ * `GraphClientImpl` and friends depend only on this interface — they have
11
+ * no direct knowledge of Firestore or SQLite.
12
+ */
13
+
14
+ /**
15
+ * Per-record write payload — backend-agnostic. Timestamps are not present;
16
+ * the backend supplies them via `serverTimestamp()` placeholders that it
17
+ * itself resolves at commit time.
18
+ */
19
+ interface WritableRecord {
20
+ aType: string;
21
+ aUid: string;
22
+ axbType: string;
23
+ bType: string;
24
+ bUid: string;
25
+ data: Record<string, unknown>;
26
+ /** Schema version (set by the writer when registry has migrations). */
27
+ v?: number;
28
+ }
29
+ /**
30
+ * Patch shape for `updateDoc`. Captures the two patterns that exist today:
31
+ * - `dataFields`: shallow merge under `data` (used by `updateNode`)
32
+ * - `replaceData`: full data replacement (used by migration write-back)
33
+ * - `v`: optional schema-version stamp
34
+ *
35
+ * `updatedAt` is always set by the backend.
36
+ */
37
+ interface UpdatePayload {
38
+ dataFields?: Record<string, unknown>;
39
+ replaceData?: Record<string, unknown>;
40
+ v?: number;
41
+ }
42
+ /**
43
+ * Read/write transaction adapter. Mirrors Firestore's transaction semantics:
44
+ * reads are snapshot-consistent; writes are issued inside the transaction
45
+ * and a rejection from any write aborts the surrounding `runTransaction`.
46
+ *
47
+ * Writes return `Promise<void>` so SQL drivers can surface row-level errors
48
+ * (constraint violations, malformed JSON paths) rather than swallowing them.
49
+ * Firestore implementations can resolve synchronously since the underlying
50
+ * `Transaction.set/update/delete` calls are themselves synchronous buffers.
51
+ */
52
+ interface TransactionBackend {
53
+ getDoc(docId: string): Promise<StoredGraphRecord | null>;
54
+ query(filters: QueryFilter[], options?: QueryOptions): Promise<StoredGraphRecord[]>;
55
+ setDoc(docId: string, record: WritableRecord): Promise<void>;
56
+ updateDoc(docId: string, update: UpdatePayload): Promise<void>;
57
+ deleteDoc(docId: string): Promise<void>;
58
+ }
59
+ /**
60
+ * Atomic multi-write batch.
61
+ */
62
+ interface BatchBackend {
63
+ setDoc(docId: string, record: WritableRecord): void;
64
+ updateDoc(docId: string, update: UpdatePayload): void;
65
+ deleteDoc(docId: string): void;
66
+ commit(): Promise<void>;
67
+ }
68
+ /**
69
+ * The single storage abstraction.
70
+ *
71
+ * Each backend instance is scoped to a "graph location" — for Firestore
72
+ * that's a collection path; for SQLite it's a (table, scopePath) pair.
73
+ * `subgraph()` returns a child backend bound to a nested location.
74
+ */
75
+ interface StorageBackend {
76
+ /** Backend-internal location identifier (collection path or table name). */
77
+ readonly collectionPath: string;
78
+ /** Subgraph scope (empty string for root). */
79
+ readonly scopePath: string;
80
+ getDoc(docId: string): Promise<StoredGraphRecord | null>;
81
+ query(filters: QueryFilter[], options?: QueryOptions): Promise<StoredGraphRecord[]>;
82
+ setDoc(docId: string, record: WritableRecord): Promise<void>;
83
+ updateDoc(docId: string, update: UpdatePayload): Promise<void>;
84
+ deleteDoc(docId: string): Promise<void>;
85
+ runTransaction<T>(fn: (tx: TransactionBackend) => Promise<T>): Promise<T>;
86
+ createBatch(): BatchBackend;
87
+ subgraph(parentNodeUid: string, name: string): StorageBackend;
88
+ removeNodeCascade(uid: string, reader: GraphReader, options?: BulkOptions): Promise<CascadeResult>;
89
+ bulkRemoveEdges(params: FindEdgesParams, reader: GraphReader, options?: BulkOptions): Promise<BulkResult>;
90
+ /**
91
+ * Find edges across all subgraphs sharing a given collection name.
92
+ * Optional — backends that can't support this should throw a clear error.
93
+ */
94
+ findEdgesGlobal?(params: FindEdgesParams, collectionName?: string): Promise<StoredGraphRecord[]>;
95
+ }
96
+
97
+ export type { BatchBackend as B, StorageBackend as S, TransactionBackend as T, UpdatePayload as U, WritableRecord as W };