@typicalday/firegraph 0.4.0 → 0.6.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 CHANGED
@@ -68,7 +68,7 @@ Firegraph stores everything as **triples** in a Firestore collection (with optio
68
68
  - **Edges** are directed relationships between nodes:
69
69
  `(tour, Kj7vNq2mP9xR4wL1tY8s3) -[hasDeparture]-> (departure, Xp4nTk8qW2vR7mL9jY5a1)`
70
70
 
71
- Every record carries a `data` payload (arbitrary JSON), plus `createdAt` and `updatedAt` server timestamps.
71
+ Every record carries a `data` payload (arbitrary JSON), plus `createdAt` and `updatedAt` server timestamps. Records managed by a schema registry with migrations also carry a `v` field (schema version number, derived from `max(toVersion)` of the migrations array) on the record envelope.
72
72
 
73
73
  ### Document IDs
74
74
 
@@ -344,6 +344,110 @@ Key behaviors:
344
344
 
345
345
  Dynamic registry returns a `DynamicGraphClient` which extends `GraphClient` with `defineNodeType()`, `defineEdgeType()`, and `reloadRegistry()`. Transactions and batches also validate against the compiled dynamic registry.
346
346
 
347
+ ### Schema Versioning & Auto-Migration
348
+
349
+ Firegraph supports schema versioning with automatic migration of records on read. The schema version is derived automatically as `max(toVersion)` from the `migrations` array -- there is no separate `schemaVersion` property to set. When a record's stored version (`v`) is behind the derived version, migration functions run automatically to bring data up to the current version.
350
+
351
+ ```typescript
352
+ import { createRegistry, createGraphClient } from 'firegraph';
353
+ import type { MigrationStep } from 'firegraph';
354
+
355
+ const migrations: MigrationStep[] = [
356
+ { fromVersion: 0, toVersion: 1, up: (d) => ({ ...d, status: d.status ?? 'draft' }) },
357
+ { fromVersion: 1, toVersion: 2, up: async (d) => ({ ...d, active: true }) },
358
+ ];
359
+
360
+ const registry = createRegistry([
361
+ {
362
+ aType: 'tour',
363
+ axbType: 'is',
364
+ bType: 'tour',
365
+ jsonSchema: tourSchemaV2,
366
+ migrations, // version derived as max(toVersion) = 2
367
+ migrationWriteBack: 'eager',
368
+ },
369
+ ]);
370
+
371
+ const g = createGraphClient(db, 'graph', { registry });
372
+
373
+ // Reading a v0 record automatically migrates it to v2 in memory
374
+ const tour = await g.getNode(tourId);
375
+ // tour.v === 2, tour.data.status === 'draft', tour.data.active === true
376
+ ```
377
+
378
+ #### How It Works
379
+
380
+ - **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).
381
+ - **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.
382
+ - **Write path**: When writing via `putNode`/`putEdge`, the record is stamped with `v` equal to the derived version automatically.
383
+ - **`updateNode`**: Does not stamp `v` — it is a raw partial update without schema context. The next read re-triggers migration (which is idempotent).
384
+
385
+ #### Write-Back
386
+
387
+ Write-back controls whether migrated data is persisted back to Firestore after a read-triggered migration:
388
+
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` |
394
+
395
+ Resolution order: `entry.migrationWriteBack > client.migrationWriteBack > 'off'`
396
+
397
+ ```typescript
398
+ // Global default
399
+ const g = createGraphClient(db, 'graph', {
400
+ registry,
401
+ migrationWriteBack: 'background',
402
+ });
403
+
404
+ // Entry-level override (takes priority)
405
+ createRegistry([{
406
+ aType: 'tour', axbType: 'is', bType: 'tour',
407
+ migrations,
408
+ migrationWriteBack: 'eager',
409
+ }]);
410
+ ```
411
+
412
+ #### Dynamic Registry Migrations
413
+
414
+ In dynamic mode, migrations are stored as source code strings:
415
+
416
+ ```typescript
417
+ await g.defineNodeType('tour', tourSchema, 'A tour', {
418
+ migrations: [
419
+ { fromVersion: 0, toVersion: 1, up: '(d) => ({ ...d, status: "draft" })' },
420
+ ],
421
+ migrationWriteBack: 'eager',
422
+ });
423
+ await g.reloadRegistry();
424
+ ```
425
+
426
+ Stored migration strings must be self-contained — no `import`, `require`, or external references. Firestore special types (`Timestamp`, `GeoPoint`, `VectorValue`, `DocumentReference`) are transparently preserved through the sandbox boundary via tagged serialization. Inside the sandbox, these appear as tagged plain objects (e.g., `{ __firegraph_ser__: 'Timestamp', seconds: N, nanoseconds: N }`) that the migration can read, modify, or create. They are reconstructed into real Firestore types after the migration returns.
427
+
428
+ For custom sandboxing, pass `migrationSandbox` to `createGraphClient()`:
429
+
430
+ ```typescript
431
+ const g = createGraphClient(db, 'graph', {
432
+ registryMode: { mode: 'dynamic' },
433
+ migrationSandbox: (source) => {
434
+ const compartment = new Compartment({ /* endowments */ });
435
+ return compartment.evaluate(source);
436
+ },
437
+ });
438
+ ```
439
+
440
+ #### Entity Discovery
441
+
442
+ Place a `migrations.ts` file in the entity folder. It must default-export a `MigrationStep[]` array. Optionally set `migrationWriteBack` in `meta.json`. The schema version is derived automatically as `max(toVersion)` from the migrations array.
443
+
444
+ ```
445
+ entities/nodes/tour/
446
+ schema.json
447
+ migrations.ts # export default [{ fromVersion: 0, toVersion: 1, up: ... }]
448
+ meta.json # { "migrationWriteBack": "eager" }
449
+ ```
450
+
347
451
  ### Subgraphs
348
452
 
349
453
  Create isolated graph namespaces inside a parent node's Firestore document as subcollections. Each subgraph is a full `GraphClient` scoped to its own collection path.
@@ -599,6 +703,7 @@ All errors extend `FiregraphError` with a `code` property:
599
703
  | `ValidationError` | `VALIDATION_ERROR` | Schema validation fails (registry + Zod) |
600
704
  | `RegistryViolationError` | `REGISTRY_VIOLATION` | Triple not registered |
601
705
  | `RegistryScopeError` | `REGISTRY_SCOPE` | Type not allowed at this subgraph scope |
706
+ | `MigrationError` | `MIGRATION_ERROR` | Migration function fails or chain is incomplete |
602
707
  | `DynamicRegistryError` | `DYNAMIC_REGISTRY_ERROR` | Dynamic registry misconfiguration or misuse |
603
708
  | `InvalidQueryError` | `INVALID_QUERY` | `findEdges` called with no filters |
604
709
  | `QuerySafetyError` | `QUERY_SAFETY` | Query would cause a full collection scan |
@@ -653,6 +758,13 @@ import type {
653
758
  NodeTypeData,
654
759
  EdgeTypeData,
655
760
 
761
+ // Migration
762
+ MigrationFn,
763
+ MigrationStep,
764
+ StoredMigrationStep,
765
+ MigrationExecutor,
766
+ MigrationWriteBack,
767
+
656
768
  // Traversal
657
769
  HopDefinition, // includes targetGraph
658
770
  TraversalOptions,
@@ -680,6 +792,7 @@ All data lives in one Firestore collection. Each document has these fields:
680
792
  | `bType` | string | Target node type |
681
793
  | `bUid` | string | Target node ID |
682
794
  | `data` | object | User payload |
795
+ | `v` | number? | Schema version (derived from `max(toVersion)` of migrations; set when entry has migrations) |
683
796
  | `createdAt` | Timestamp | Server-set on create |
684
797
  | `updatedAt` | Timestamp | Server-set on create/update |
685
798
 
@@ -1,2 +1,2 @@
1
- export { l as CodegenOptions, J as generateTypes } from '../index-DR3jF5_b.cjs';
1
+ export { p as CodegenOptions, P as generateTypes } from '../index-B9aodfYD.cjs';
2
2
  import '@google-cloud/firestore';
@@ -1,2 +1,2 @@
1
- export { l as CodegenOptions, J as generateTypes } from '../index-DR3jF5_b.js';
1
+ export { p as CodegenOptions, P as generateTypes } from '../index-B9aodfYD.js';
2
2
  import '@google-cloud/firestore';