@typicalday/firegraph 0.4.0 → 0.5.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/dist/index.cjs CHANGED
@@ -40,6 +40,7 @@ __export(index_exports, {
40
40
  InvalidQueryError: () => InvalidQueryError,
41
41
  META_EDGE_TYPE: () => META_EDGE_TYPE,
42
42
  META_NODE_TYPE: () => META_NODE_TYPE,
43
+ MigrationError: () => MigrationError,
43
44
  NODE_TYPE_SCHEMA: () => NODE_TYPE_SCHEMA,
44
45
  NodeNotFoundError: () => NodeNotFoundError,
45
46
  QueryClient: () => QueryClient,
@@ -47,13 +48,17 @@ __export(index_exports, {
47
48
  QuerySafetyError: () => QuerySafetyError,
48
49
  RegistryScopeError: () => RegistryScopeError,
49
50
  RegistryViolationError: () => RegistryViolationError,
51
+ SERIALIZATION_TAG: () => SERIALIZATION_TAG,
50
52
  TraversalError: () => TraversalError,
51
53
  ValidationError: () => ValidationError,
52
54
  analyzeQuerySafety: () => analyzeQuerySafety,
55
+ applyMigrationChain: () => applyMigrationChain,
53
56
  buildEdgeQueryPlan: () => buildEdgeQueryPlan,
54
57
  buildEdgeRecord: () => buildEdgeRecord,
55
58
  buildNodeQueryPlan: () => buildNodeQueryPlan,
56
59
  buildNodeRecord: () => buildNodeRecord,
60
+ compileMigrationFn: () => compileMigrationFn,
61
+ compileMigrations: () => compileMigrations,
57
62
  compileSchema: () => compileSchema,
58
63
  computeEdgeDocId: () => computeEdgeDocId,
59
64
  computeNodeDocId: () => computeNodeDocId,
@@ -63,24 +68,33 @@ __export(index_exports, {
63
68
  createRegistry: () => createRegistry,
64
69
  createRegistryFromGraph: () => createRegistryFromGraph,
65
70
  createTraversal: () => createTraversal,
71
+ defaultExecutor: () => defaultExecutor,
66
72
  defineConfig: () => defineConfig,
67
73
  defineViews: () => defineViews,
74
+ deserializeFirestoreTypes: () => deserializeFirestoreTypes,
75
+ destroySandboxWorker: () => destroySandboxWorker,
68
76
  discoverEntities: () => discoverEntities,
69
77
  generateDeterministicUid: () => generateDeterministicUid,
70
78
  generateId: () => generateId,
71
79
  generateIndexConfig: () => generateIndexConfig,
72
80
  generateTypes: () => generateTypes,
73
81
  isAncestorUid: () => isAncestorUid,
82
+ isTaggedValue: () => isTaggedValue,
74
83
  jsonSchemaToFieldMeta: () => jsonSchemaToFieldMeta,
75
84
  matchScope: () => matchScope,
76
85
  matchScopeAny: () => matchScopeAny,
86
+ migrateRecord: () => migrateRecord,
87
+ migrateRecords: () => migrateRecords,
88
+ precompileSource: () => precompileSource,
77
89
  resolveAncestorCollection: () => resolveAncestorCollection,
78
- resolveView: () => resolveView
90
+ resolveView: () => resolveView,
91
+ serializeFirestoreTypes: () => serializeFirestoreTypes,
92
+ validateMigrationChain: () => validateMigrationChain
79
93
  });
80
94
  module.exports = __toCommonJS(index_exports);
81
95
 
82
96
  // src/client.ts
83
- var import_firestore4 = require("@google-cloud/firestore");
97
+ var import_firestore5 = require("@google-cloud/firestore");
84
98
 
85
99
  // src/docid.ts
86
100
  var import_node_crypto = require("crypto");
@@ -208,6 +222,12 @@ var RegistryScopeError = class extends FiregraphError {
208
222
  this.name = "RegistryScopeError";
209
223
  }
210
224
  };
225
+ var MigrationError = class extends FiregraphError {
226
+ constructor(message) {
227
+ super(message, "MIGRATION_ERROR");
228
+ this.name = "MigrationError";
229
+ }
230
+ };
211
231
 
212
232
  // src/query.ts
213
233
  function buildEdgeQueryPlan(params) {
@@ -398,7 +418,7 @@ function createPipelineQueryAdapter(db, collectionPath) {
398
418
  }
399
419
 
400
420
  // src/transaction.ts
401
- var import_firestore2 = require("@google-cloud/firestore");
421
+ var import_firestore3 = require("@google-cloud/firestore");
402
422
 
403
423
  // src/query-safety.ts
404
424
  var SAFE_INDEX_PATTERNS = [
@@ -448,24 +468,248 @@ function analyzeQuerySafety(filters) {
448
468
  };
449
469
  }
450
470
 
471
+ // src/serialization.ts
472
+ var import_firestore2 = require("@google-cloud/firestore");
473
+ var SERIALIZATION_TAG = "__firegraph_ser__";
474
+ var KNOWN_TYPES = /* @__PURE__ */ new Set(["Timestamp", "GeoPoint", "VectorValue", "DocumentReference"]);
475
+ var _docRefWarned = false;
476
+ function isTaggedValue(value) {
477
+ if (value === null || typeof value !== "object") return false;
478
+ const tag = value[SERIALIZATION_TAG];
479
+ return typeof tag === "string" && KNOWN_TYPES.has(tag);
480
+ }
481
+ function isTimestamp(value) {
482
+ return value instanceof import_firestore2.Timestamp;
483
+ }
484
+ function isGeoPoint(value) {
485
+ return value instanceof import_firestore2.GeoPoint;
486
+ }
487
+ function isDocumentReference(value) {
488
+ if (value === null || typeof value !== "object") return false;
489
+ const v = value;
490
+ return typeof v.path === "string" && v.firestore !== void 0 && typeof v.id === "string" && v.constructor?.name === "DocumentReference";
491
+ }
492
+ function isVectorValue(value) {
493
+ if (value === null || typeof value !== "object") return false;
494
+ const v = value;
495
+ return v.constructor?.name === "VectorValue" && Array.isArray(v._values);
496
+ }
497
+ function serializeFirestoreTypes(data) {
498
+ return serializeValue(data);
499
+ }
500
+ function serializeValue(value) {
501
+ if (value === null || value === void 0) return value;
502
+ if (typeof value !== "object") return value;
503
+ if (isTimestamp(value)) {
504
+ return { [SERIALIZATION_TAG]: "Timestamp", seconds: value.seconds, nanoseconds: value.nanoseconds };
505
+ }
506
+ if (isGeoPoint(value)) {
507
+ return { [SERIALIZATION_TAG]: "GeoPoint", latitude: value.latitude, longitude: value.longitude };
508
+ }
509
+ if (isDocumentReference(value)) {
510
+ return { [SERIALIZATION_TAG]: "DocumentReference", path: value.path };
511
+ }
512
+ if (isVectorValue(value)) {
513
+ const v = value;
514
+ const values = typeof v.toArray === "function" ? v.toArray() : v._values;
515
+ return { [SERIALIZATION_TAG]: "VectorValue", values: [...values] };
516
+ }
517
+ if (Array.isArray(value)) {
518
+ return value.map(serializeValue);
519
+ }
520
+ const result = {};
521
+ for (const key of Object.keys(value)) {
522
+ result[key] = serializeValue(value[key]);
523
+ }
524
+ return result;
525
+ }
526
+ function deserializeFirestoreTypes(data, db) {
527
+ return deserializeValue(data, db);
528
+ }
529
+ function deserializeValue(value, db) {
530
+ if (value === null || value === void 0) return value;
531
+ if (typeof value !== "object") return value;
532
+ if (isTimestamp(value) || isGeoPoint(value) || isDocumentReference(value) || isVectorValue(value)) {
533
+ return value;
534
+ }
535
+ if (Array.isArray(value)) {
536
+ return value.map((v) => deserializeValue(v, db));
537
+ }
538
+ const obj = value;
539
+ if (isTaggedValue(obj)) {
540
+ const tag = obj[SERIALIZATION_TAG];
541
+ switch (tag) {
542
+ case "Timestamp":
543
+ if (typeof obj.seconds !== "number" || typeof obj.nanoseconds !== "number") return obj;
544
+ return new import_firestore2.Timestamp(obj.seconds, obj.nanoseconds);
545
+ case "GeoPoint":
546
+ if (typeof obj.latitude !== "number" || typeof obj.longitude !== "number") return obj;
547
+ return new import_firestore2.GeoPoint(obj.latitude, obj.longitude);
548
+ case "VectorValue":
549
+ if (!Array.isArray(obj.values)) return obj;
550
+ return import_firestore2.FieldValue.vector(obj.values);
551
+ case "DocumentReference":
552
+ if (typeof obj.path !== "string") return obj;
553
+ if (db) {
554
+ return db.doc(obj.path);
555
+ }
556
+ if (!_docRefWarned) {
557
+ _docRefWarned = true;
558
+ console.warn(
559
+ "[firegraph] DocumentReference encountered during migration deserialization but no Firestore instance available. The reference will remain as a tagged object with its path. Enable write-back for full reconstruction."
560
+ );
561
+ }
562
+ return obj;
563
+ default:
564
+ return obj;
565
+ }
566
+ }
567
+ const result = {};
568
+ for (const key of Object.keys(obj)) {
569
+ result[key] = deserializeValue(obj[key], db);
570
+ }
571
+ return result;
572
+ }
573
+
574
+ // src/migration.ts
575
+ async function applyMigrationChain(data, currentVersion, targetVersion, migrations) {
576
+ const sorted = [...migrations].sort((a, b) => a.fromVersion - b.fromVersion);
577
+ let result = { ...data };
578
+ let version = currentVersion;
579
+ for (const step of sorted) {
580
+ if (step.fromVersion === version) {
581
+ try {
582
+ result = await step.up(result);
583
+ } catch (err) {
584
+ if (err instanceof MigrationError) throw err;
585
+ throw new MigrationError(
586
+ `Migration from v${step.fromVersion} to v${step.toVersion} failed: ${err.message}`
587
+ );
588
+ }
589
+ if (!result || typeof result !== "object") {
590
+ throw new MigrationError(
591
+ `Migration from v${step.fromVersion} to v${step.toVersion} returned invalid data (expected object)`
592
+ );
593
+ }
594
+ version = step.toVersion;
595
+ }
596
+ }
597
+ if (version !== targetVersion) {
598
+ throw new MigrationError(
599
+ `Incomplete migration chain: reached v${version} but target is v${targetVersion}`
600
+ );
601
+ }
602
+ return result;
603
+ }
604
+ function validateMigrationChain(migrations, label) {
605
+ if (migrations.length === 0) return;
606
+ const seen = /* @__PURE__ */ new Set();
607
+ for (const step of migrations) {
608
+ if (step.toVersion <= step.fromVersion) {
609
+ throw new MigrationError(
610
+ `${label}: migration step has toVersion (${step.toVersion}) <= fromVersion (${step.fromVersion})`
611
+ );
612
+ }
613
+ if (seen.has(step.fromVersion)) {
614
+ throw new MigrationError(
615
+ `${label}: duplicate migration step for fromVersion ${step.fromVersion}`
616
+ );
617
+ }
618
+ seen.add(step.fromVersion);
619
+ }
620
+ const sorted = [...migrations].sort((a, b) => a.fromVersion - b.fromVersion);
621
+ const targetVersion = Math.max(...migrations.map((m) => m.toVersion));
622
+ let version = 0;
623
+ for (const step of sorted) {
624
+ if (step.fromVersion === version) {
625
+ version = step.toVersion;
626
+ } else if (step.fromVersion > version) {
627
+ throw new MigrationError(
628
+ `${label}: migration chain has a gap \u2014 no step covers v${version} \u2192 v${step.fromVersion}`
629
+ );
630
+ }
631
+ }
632
+ if (version !== targetVersion) {
633
+ throw new MigrationError(
634
+ `${label}: migration chain does not reach v${targetVersion} (stuck at v${version})`
635
+ );
636
+ }
637
+ }
638
+ async function migrateRecord(record, registry, globalWriteBack = "off") {
639
+ const entry = registry.lookup(record.aType, record.axbType, record.bType);
640
+ if (!entry?.migrations?.length || !entry.schemaVersion) {
641
+ return { record, migrated: false, writeBack: "off" };
642
+ }
643
+ const currentVersion = record.v ?? 0;
644
+ if (currentVersion >= entry.schemaVersion) {
645
+ return { record, migrated: false, writeBack: "off" };
646
+ }
647
+ const migratedData = await applyMigrationChain(
648
+ record.data,
649
+ currentVersion,
650
+ entry.schemaVersion,
651
+ entry.migrations
652
+ );
653
+ const writeBack = entry.migrationWriteBack ?? globalWriteBack ?? "off";
654
+ return {
655
+ record: { ...record, data: migratedData, v: entry.schemaVersion },
656
+ migrated: true,
657
+ writeBack
658
+ };
659
+ }
660
+ async function migrateRecords(records, registry, globalWriteBack = "off") {
661
+ return Promise.all(
662
+ records.map((r) => migrateRecord(r, registry, globalWriteBack))
663
+ );
664
+ }
665
+
451
666
  // src/transaction.ts
452
667
  var GraphTransactionImpl = class {
453
- constructor(adapter, registry, scanProtection = "error", scopePath = "") {
668
+ constructor(adapter, registry, scanProtection = "error", scopePath = "", globalWriteBack = "off", db) {
454
669
  this.adapter = adapter;
455
670
  this.registry = registry;
456
671
  this.scanProtection = scanProtection;
457
672
  this.scopePath = scopePath;
673
+ this.globalWriteBack = globalWriteBack;
674
+ this.db = db;
458
675
  }
459
676
  async getNode(uid) {
460
677
  const docId = computeNodeDocId(uid);
461
- return this.adapter.getDoc(docId);
678
+ const record = await this.adapter.getDoc(docId);
679
+ if (!record || !this.registry) return record;
680
+ const result = await migrateRecord(record, this.registry, this.globalWriteBack);
681
+ if (result.migrated && result.writeBack !== "off") {
682
+ const update = {
683
+ data: deserializeFirestoreTypes(result.record.data, this.db),
684
+ updatedAt: import_firestore3.FieldValue.serverTimestamp()
685
+ };
686
+ if (result.record.v !== void 0) {
687
+ update.v = result.record.v;
688
+ }
689
+ this.adapter.updateDoc(docId, update);
690
+ }
691
+ return result.record;
462
692
  }
463
693
  async getEdge(aUid, axbType, bUid) {
464
694
  const docId = computeEdgeDocId(aUid, axbType, bUid);
465
- return this.adapter.getDoc(docId);
695
+ const record = await this.adapter.getDoc(docId);
696
+ if (!record || !this.registry) return record;
697
+ const result = await migrateRecord(record, this.registry, this.globalWriteBack);
698
+ if (result.migrated && result.writeBack !== "off") {
699
+ const update = {
700
+ data: deserializeFirestoreTypes(result.record.data, this.db),
701
+ updatedAt: import_firestore3.FieldValue.serverTimestamp()
702
+ };
703
+ if (result.record.v !== void 0) {
704
+ update.v = result.record.v;
705
+ }
706
+ this.adapter.updateDoc(docId, update);
707
+ }
708
+ return result.record;
466
709
  }
467
710
  async edgeExists(aUid, axbType, bUid) {
468
- const record = await this.getEdge(aUid, axbType, bUid);
711
+ const docId = computeEdgeDocId(aUid, axbType, bUid);
712
+ const record = await this.adapter.getDoc(docId);
469
713
  return record !== null;
470
714
  }
471
715
  checkQuerySafety(filters, allowCollectionScan) {
@@ -479,21 +723,45 @@ var GraphTransactionImpl = class {
479
723
  }
480
724
  async findEdges(params) {
481
725
  const plan = buildEdgeQueryPlan(params);
726
+ let records;
482
727
  if (plan.strategy === "get") {
483
728
  const record = await this.adapter.getDoc(plan.docId);
484
- return record ? [record] : [];
729
+ records = record ? [record] : [];
730
+ } else {
731
+ this.checkQuerySafety(plan.filters, params.allowCollectionScan);
732
+ records = await this.adapter.query(plan.filters, plan.options);
485
733
  }
486
- this.checkQuerySafety(plan.filters, params.allowCollectionScan);
487
- return this.adapter.query(plan.filters, plan.options);
734
+ return this.applyMigrations(records);
488
735
  }
489
736
  async findNodes(params) {
490
737
  const plan = buildNodeQueryPlan(params);
738
+ let records;
491
739
  if (plan.strategy === "get") {
492
740
  const record = await this.adapter.getDoc(plan.docId);
493
- return record ? [record] : [];
741
+ records = record ? [record] : [];
742
+ } else {
743
+ this.checkQuerySafety(plan.filters, params.allowCollectionScan);
744
+ records = await this.adapter.query(plan.filters, plan.options);
745
+ }
746
+ return this.applyMigrations(records);
747
+ }
748
+ async applyMigrations(records) {
749
+ if (!this.registry || records.length === 0) return records;
750
+ const results = await migrateRecords(records, this.registry, this.globalWriteBack);
751
+ for (const result of results) {
752
+ if (result.migrated && result.writeBack !== "off") {
753
+ const docId = result.record.axbType === NODE_RELATION ? computeNodeDocId(result.record.aUid) : computeEdgeDocId(result.record.aUid, result.record.axbType, result.record.bUid);
754
+ const update = {
755
+ data: deserializeFirestoreTypes(result.record.data, this.db),
756
+ updatedAt: import_firestore3.FieldValue.serverTimestamp()
757
+ };
758
+ if (result.record.v !== void 0) {
759
+ update.v = result.record.v;
760
+ }
761
+ this.adapter.updateDoc(docId, update);
762
+ }
494
763
  }
495
- this.checkQuerySafety(plan.filters, params.allowCollectionScan);
496
- return this.adapter.query(plan.filters, plan.options);
764
+ return results.map((r) => r.record);
497
765
  }
498
766
  async putNode(aType, uid, data) {
499
767
  if (this.registry) {
@@ -501,6 +769,12 @@ var GraphTransactionImpl = class {
501
769
  }
502
770
  const docId = computeNodeDocId(uid);
503
771
  const record = buildNodeRecord(aType, uid, data);
772
+ if (this.registry) {
773
+ const entry = this.registry.lookup(aType, NODE_RELATION, aType);
774
+ if (entry?.schemaVersion && entry.schemaVersion > 0) {
775
+ record.v = entry.schemaVersion;
776
+ }
777
+ }
504
778
  this.adapter.setDoc(docId, record);
505
779
  }
506
780
  async putEdge(aType, aUid, axbType, bType, bUid, data) {
@@ -509,13 +783,19 @@ var GraphTransactionImpl = class {
509
783
  }
510
784
  const docId = computeEdgeDocId(aUid, axbType, bUid);
511
785
  const record = buildEdgeRecord(aType, aUid, axbType, bType, bUid, data);
786
+ if (this.registry) {
787
+ const entry = this.registry.lookup(aType, axbType, bType);
788
+ if (entry?.schemaVersion && entry.schemaVersion > 0) {
789
+ record.v = entry.schemaVersion;
790
+ }
791
+ }
512
792
  this.adapter.setDoc(docId, record);
513
793
  }
514
794
  async updateNode(uid, data) {
515
795
  const docId = computeNodeDocId(uid);
516
796
  this.adapter.updateDoc(docId, {
517
797
  ...data,
518
- updatedAt: import_firestore2.FieldValue.serverTimestamp()
798
+ updatedAt: import_firestore3.FieldValue.serverTimestamp()
519
799
  });
520
800
  }
521
801
  async removeNode(uid) {
@@ -529,7 +809,7 @@ var GraphTransactionImpl = class {
529
809
  };
530
810
 
531
811
  // src/batch.ts
532
- var import_firestore3 = require("@google-cloud/firestore");
812
+ var import_firestore4 = require("@google-cloud/firestore");
533
813
  var GraphBatchImpl = class {
534
814
  constructor(adapter, registry, scopePath = "") {
535
815
  this.adapter = adapter;
@@ -542,6 +822,12 @@ var GraphBatchImpl = class {
542
822
  }
543
823
  const docId = computeNodeDocId(uid);
544
824
  const record = buildNodeRecord(aType, uid, data);
825
+ if (this.registry) {
826
+ const entry = this.registry.lookup(aType, NODE_RELATION, aType);
827
+ if (entry?.schemaVersion && entry.schemaVersion > 0) {
828
+ record.v = entry.schemaVersion;
829
+ }
830
+ }
545
831
  this.adapter.setDoc(docId, record);
546
832
  }
547
833
  async putEdge(aType, aUid, axbType, bType, bUid, data) {
@@ -550,13 +836,19 @@ var GraphBatchImpl = class {
550
836
  }
551
837
  const docId = computeEdgeDocId(aUid, axbType, bUid);
552
838
  const record = buildEdgeRecord(aType, aUid, axbType, bType, bUid, data);
839
+ if (this.registry) {
840
+ const entry = this.registry.lookup(aType, axbType, bType);
841
+ if (entry?.schemaVersion && entry.schemaVersion > 0) {
842
+ record.v = entry.schemaVersion;
843
+ }
844
+ }
553
845
  this.adapter.setDoc(docId, record);
554
846
  }
555
847
  async updateNode(uid, data) {
556
848
  const docId = computeNodeDocId(uid);
557
849
  this.adapter.updateDoc(docId, {
558
850
  ...data,
559
- updatedAt: import_firestore3.FieldValue.serverTimestamp()
851
+ updatedAt: import_firestore4.FieldValue.serverTimestamp()
560
852
  });
561
853
  }
562
854
  async removeNode(uid) {
@@ -715,7 +1007,7 @@ async function removeNodeCascade(db, collectionPath, reader, uid, options) {
715
1007
  }
716
1008
 
717
1009
  // src/dynamic-registry.ts
718
- var import_node_crypto2 = require("crypto");
1010
+ var import_node_crypto3 = require("crypto");
719
1011
 
720
1012
  // src/json-schema.ts
721
1013
  var import_ajv = __toESM(require("ajv"), 1);
@@ -864,6 +1156,13 @@ function createRegistry(input) {
864
1156
  `Entry (${entry.aType}) -[${entry.axbType}]-> (${entry.bType}) has invalid targetGraph "${entry.targetGraph}" \u2014 must be a single segment (no "/")`
865
1157
  );
866
1158
  }
1159
+ if (entry.migrations?.length) {
1160
+ const label = `Entry (${entry.aType}) -[${entry.axbType}]-> (${entry.bType})`;
1161
+ validateMigrationChain(entry.migrations, label);
1162
+ entry.schemaVersion = Math.max(...entry.migrations.map((m) => m.toVersion));
1163
+ } else {
1164
+ entry.schemaVersion = void 0;
1165
+ }
867
1166
  const key = tripleKey(entry.aType, entry.axbType, entry.bType);
868
1167
  const validator = entry.jsonSchema ? compileSchema(entry.jsonSchema, `(${entry.aType}) -[${entry.axbType}]-> (${entry.bType})`) : void 0;
869
1168
  map.set(key, { entry, validate: validator });
@@ -965,7 +1264,9 @@ function discoveryToEntries(discovery) {
965
1264
  description: entity.description,
966
1265
  titleField: entity.titleField,
967
1266
  subtitleField: entity.subtitleField,
968
- allowedIn: entity.allowedIn
1267
+ allowedIn: entity.allowedIn,
1268
+ migrations: entity.migrations,
1269
+ migrationWriteBack: entity.migrationWriteBack
969
1270
  });
970
1271
  }
971
1272
  for (const [axbType, entity] of discovery.edges) {
@@ -991,7 +1292,9 @@ function discoveryToEntries(discovery) {
991
1292
  titleField: entity.titleField,
992
1293
  subtitleField: entity.subtitleField,
993
1294
  allowedIn: entity.allowedIn,
994
- targetGraph: resolvedTargetGraph
1295
+ targetGraph: resolvedTargetGraph,
1296
+ migrations: entity.migrations,
1297
+ migrationWriteBack: entity.migrationWriteBack
995
1298
  });
996
1299
  }
997
1300
  }
@@ -999,9 +1302,259 @@ function discoveryToEntries(discovery) {
999
1302
  return entries;
1000
1303
  }
1001
1304
 
1305
+ // src/sandbox.ts
1306
+ var import_node_worker_threads = require("worker_threads");
1307
+ var import_node_crypto2 = require("crypto");
1308
+ var import_meta = {};
1309
+ var _worker = null;
1310
+ var _requestId = 0;
1311
+ var _pending = /* @__PURE__ */ new Map();
1312
+ var WORKER_SOURCE = [
1313
+ `'use strict';`,
1314
+ `var _wt = require('node:worker_threads');`,
1315
+ `var _mod = require('node:module');`,
1316
+ `var _crypto = require('node:crypto');`,
1317
+ `var parentPort = _wt.parentPort;`,
1318
+ `var workerData = _wt.workerData;`,
1319
+ ``,
1320
+ `// Load SES using the parent module's resolution context`,
1321
+ `var esmRequire = _mod.createRequire(workerData.parentUrl);`,
1322
+ `esmRequire('ses');`,
1323
+ ``,
1324
+ `lockdown({`,
1325
+ ` errorTaming: 'unsafe',`,
1326
+ ` consoleTaming: 'unsafe',`,
1327
+ ` evalTaming: 'safe-eval',`,
1328
+ ` overrideTaming: 'moderate',`,
1329
+ ` stackFiltering: 'verbose'`,
1330
+ `});`,
1331
+ ``,
1332
+ `// Defense-in-depth: verify lockdown() actually hardened JSON.`,
1333
+ `if (!Object.isFrozen(JSON)) {`,
1334
+ ` throw new Error('SES lockdown failed: JSON is not frozen');`,
1335
+ `}`,
1336
+ ``,
1337
+ `var cache = new Map();`,
1338
+ ``,
1339
+ `function hashSource(s) {`,
1340
+ ` return _crypto.createHash('sha256').update(s).digest('hex');`,
1341
+ `}`,
1342
+ ``,
1343
+ `function buildWrapper(source) {`,
1344
+ ` return '(function() {' +`,
1345
+ ` ' var fn = (' + source + ');\\n' +`,
1346
+ ` ' if (typeof fn !== "function") return null;\\n' +`,
1347
+ ` ' return function(jsonIn) {\\n' +`,
1348
+ ` ' var data = JSON.parse(jsonIn);\\n' +`,
1349
+ ` ' var result = fn(data);\\n' +`,
1350
+ ` ' if (result !== null && typeof result === "object" && typeof result.then === "function") {\\n' +`,
1351
+ ` ' return result.then(function(r) { return JSON.stringify(r); });\\n' +`,
1352
+ ` ' }\\n' +`,
1353
+ ` ' return JSON.stringify(result);\\n' +`,
1354
+ ` ' };\\n' +`,
1355
+ ` '})()';`,
1356
+ `}`,
1357
+ ``,
1358
+ `function compileSource(source) {`,
1359
+ ` var key = hashSource(source);`,
1360
+ ` var cached = cache.get(key);`,
1361
+ ` if (cached) return cached;`,
1362
+ ``,
1363
+ ` var compartmentFn;`,
1364
+ ` try {`,
1365
+ ` var c = new Compartment({ JSON: JSON });`,
1366
+ ` compartmentFn = c.evaluate(buildWrapper(source));`,
1367
+ ` } catch (err) {`,
1368
+ ` throw new Error('Failed to compile migration source: ' + (err.message || String(err)));`,
1369
+ ` }`,
1370
+ ``,
1371
+ ` if (typeof compartmentFn !== 'function') {`,
1372
+ ` throw new Error('Migration source did not produce a function: ' + source.slice(0, 80));`,
1373
+ ` }`,
1374
+ ``,
1375
+ ` cache.set(key, compartmentFn);`,
1376
+ ` return compartmentFn;`,
1377
+ `}`,
1378
+ ``,
1379
+ `parentPort.on('message', function(msg) {`,
1380
+ ` var id = msg.id;`,
1381
+ ` try {`,
1382
+ ` if (msg.type === 'compile') {`,
1383
+ ` compileSource(msg.source);`,
1384
+ ` parentPort.postMessage({ id: id, type: 'compiled' });`,
1385
+ ` return;`,
1386
+ ` }`,
1387
+ ` if (msg.type === 'execute') {`,
1388
+ ` var fn = compileSource(msg.source);`,
1389
+ ` var raw;`,
1390
+ ` try {`,
1391
+ ` raw = fn(msg.jsonData);`,
1392
+ ` } catch (err) {`,
1393
+ ` parentPort.postMessage({ id: id, type: 'error', message: 'Migration function threw: ' + (err.message || String(err)) });`,
1394
+ ` return;`,
1395
+ ` }`,
1396
+ ` if (raw !== null && typeof raw === 'object' && typeof raw.then === 'function') {`,
1397
+ ` raw.then(`,
1398
+ ` function(jsonResult) {`,
1399
+ ` if (jsonResult === undefined || jsonResult === null) {`,
1400
+ ` parentPort.postMessage({ id: id, type: 'error', message: 'Migration returned a non-JSON-serializable value' });`,
1401
+ ` } else {`,
1402
+ ` parentPort.postMessage({ id: id, type: 'result', jsonResult: jsonResult });`,
1403
+ ` }`,
1404
+ ` },`,
1405
+ ` function(err) {`,
1406
+ ` parentPort.postMessage({ id: id, type: 'error', message: 'Async migration function threw: ' + (err.message || String(err)) });`,
1407
+ ` }`,
1408
+ ` );`,
1409
+ ` return;`,
1410
+ ` }`,
1411
+ ` if (raw === undefined || raw === null) {`,
1412
+ ` parentPort.postMessage({ id: id, type: 'error', message: 'Migration returned a non-JSON-serializable value' });`,
1413
+ ` } else {`,
1414
+ ` parentPort.postMessage({ id: id, type: 'result', jsonResult: raw });`,
1415
+ ` }`,
1416
+ ` }`,
1417
+ ` } catch (err) {`,
1418
+ ` parentPort.postMessage({ id: id, type: 'error', message: err.message || String(err) });`,
1419
+ ` }`,
1420
+ `});`
1421
+ ].join("\n");
1422
+ function ensureWorker() {
1423
+ if (_worker) return _worker;
1424
+ _worker = new import_node_worker_threads.Worker(WORKER_SOURCE, {
1425
+ eval: true,
1426
+ workerData: { parentUrl: import_meta.url }
1427
+ });
1428
+ _worker.unref();
1429
+ _worker.on("message", (msg) => {
1430
+ if (msg.id === void 0) return;
1431
+ const pending = _pending.get(msg.id);
1432
+ if (!pending) return;
1433
+ _pending.delete(msg.id);
1434
+ if (msg.type === "error") {
1435
+ pending.reject(new MigrationError(msg.message ?? "Unknown sandbox error"));
1436
+ } else {
1437
+ pending.resolve(msg);
1438
+ }
1439
+ });
1440
+ _worker.on("error", (err) => {
1441
+ for (const [, p] of _pending) {
1442
+ p.reject(new MigrationError(`Sandbox worker error: ${err.message}`));
1443
+ }
1444
+ _pending.clear();
1445
+ _worker = null;
1446
+ });
1447
+ _worker.on("exit", (code) => {
1448
+ if (_pending.size > 0) {
1449
+ for (const [, p] of _pending) {
1450
+ p.reject(new MigrationError(`Sandbox worker exited with code ${code}`));
1451
+ }
1452
+ _pending.clear();
1453
+ }
1454
+ _worker = null;
1455
+ });
1456
+ return _worker;
1457
+ }
1458
+ function sendToWorker(msg) {
1459
+ const worker = ensureWorker();
1460
+ if (_requestId >= Number.MAX_SAFE_INTEGER) _requestId = 0;
1461
+ const id = ++_requestId;
1462
+ return new Promise((resolve2, reject) => {
1463
+ _pending.set(id, { resolve: resolve2, reject });
1464
+ worker.postMessage({ ...msg, id });
1465
+ });
1466
+ }
1467
+ var compiledCache = /* @__PURE__ */ new WeakMap();
1468
+ function getExecutorCache(executor) {
1469
+ let cache = compiledCache.get(executor);
1470
+ if (!cache) {
1471
+ cache = /* @__PURE__ */ new Map();
1472
+ compiledCache.set(executor, cache);
1473
+ }
1474
+ return cache;
1475
+ }
1476
+ function hashSource(source) {
1477
+ return (0, import_node_crypto2.createHash)("sha256").update(source).digest("hex");
1478
+ }
1479
+ function defaultExecutor(source) {
1480
+ ensureWorker();
1481
+ return ((data) => {
1482
+ const jsonData = JSON.stringify(serializeFirestoreTypes(data));
1483
+ return sendToWorker({ type: "execute", source, jsonData }).then(
1484
+ (response) => {
1485
+ if (response.jsonResult === void 0 || response.jsonResult === null) {
1486
+ throw new MigrationError("Migration returned a non-JSON-serializable value");
1487
+ }
1488
+ try {
1489
+ return deserializeFirestoreTypes(JSON.parse(response.jsonResult));
1490
+ } catch {
1491
+ throw new MigrationError("Migration returned a non-JSON-serializable value");
1492
+ }
1493
+ }
1494
+ );
1495
+ });
1496
+ }
1497
+ async function precompileSource(source, executor) {
1498
+ if (executor && executor !== defaultExecutor) {
1499
+ try {
1500
+ executor(source);
1501
+ } catch (err) {
1502
+ if (err instanceof MigrationError) throw err;
1503
+ throw new MigrationError(
1504
+ `Failed to compile migration source: ${err.message}`
1505
+ );
1506
+ }
1507
+ return;
1508
+ }
1509
+ await sendToWorker({ type: "compile", source });
1510
+ }
1511
+ function compileMigrationFn(source, executor = defaultExecutor) {
1512
+ const cache = getExecutorCache(executor);
1513
+ const key = hashSource(source);
1514
+ const cached = cache.get(key);
1515
+ if (cached) return cached;
1516
+ try {
1517
+ const fn = executor(source);
1518
+ cache.set(key, fn);
1519
+ return fn;
1520
+ } catch (err) {
1521
+ if (err instanceof MigrationError) throw err;
1522
+ throw new MigrationError(
1523
+ `Failed to compile migration source: ${err.message}`
1524
+ );
1525
+ }
1526
+ }
1527
+ function compileMigrations(stored, executor) {
1528
+ return stored.map((step) => ({
1529
+ fromVersion: step.fromVersion,
1530
+ toVersion: step.toVersion,
1531
+ up: compileMigrationFn(step.up, executor)
1532
+ }));
1533
+ }
1534
+ async function destroySandboxWorker() {
1535
+ if (!_worker) return;
1536
+ const w = _worker;
1537
+ _worker = null;
1538
+ for (const [, p] of _pending) {
1539
+ p.reject(new MigrationError("Sandbox worker terminated"));
1540
+ }
1541
+ _pending.clear();
1542
+ await w.terminate();
1543
+ }
1544
+
1002
1545
  // src/dynamic-registry.ts
1003
1546
  var META_NODE_TYPE = "nodeType";
1004
1547
  var META_EDGE_TYPE = "edgeType";
1548
+ var STORED_MIGRATION_STEP_SCHEMA = {
1549
+ type: "object",
1550
+ required: ["fromVersion", "toVersion", "up"],
1551
+ properties: {
1552
+ fromVersion: { type: "integer", minimum: 0 },
1553
+ toVersion: { type: "integer", minimum: 1 },
1554
+ up: { type: "string", minLength: 1 }
1555
+ },
1556
+ additionalProperties: false
1557
+ };
1005
1558
  var NODE_TYPE_SCHEMA = {
1006
1559
  type: "object",
1007
1560
  required: ["name", "jsonSchema"],
@@ -1013,7 +1566,10 @@ var NODE_TYPE_SCHEMA = {
1013
1566
  subtitleField: { type: "string" },
1014
1567
  viewTemplate: { type: "string" },
1015
1568
  viewCss: { type: "string" },
1016
- allowedIn: { type: "array", items: { type: "string", minLength: 1 } }
1569
+ allowedIn: { type: "array", items: { type: "string", minLength: 1 } },
1570
+ schemaVersion: { type: "integer", minimum: 0 },
1571
+ migrations: { type: "array", items: STORED_MIGRATION_STEP_SCHEMA },
1572
+ migrationWriteBack: { type: "string", enum: ["off", "eager", "background"] }
1017
1573
  },
1018
1574
  additionalProperties: false
1019
1575
  };
@@ -1042,7 +1598,10 @@ var EDGE_TYPE_SCHEMA = {
1042
1598
  viewTemplate: { type: "string" },
1043
1599
  viewCss: { type: "string" },
1044
1600
  allowedIn: { type: "array", items: { type: "string", minLength: 1 } },
1045
- targetGraph: { type: "string", minLength: 1, pattern: "^[^/]+$" }
1601
+ targetGraph: { type: "string", minLength: 1, pattern: "^[^/]+$" },
1602
+ schemaVersion: { type: "integer", minimum: 0 },
1603
+ migrations: { type: "array", items: STORED_MIGRATION_STEP_SCHEMA },
1604
+ migrationWriteBack: { type: "string", enum: ["off", "eager", "background"] }
1046
1605
  },
1047
1606
  additionalProperties: false
1048
1607
  };
@@ -1066,15 +1625,33 @@ function createBootstrapRegistry() {
1066
1625
  return createRegistry([...BOOTSTRAP_ENTRIES]);
1067
1626
  }
1068
1627
  function generateDeterministicUid(metaType, name) {
1069
- const hash = (0, import_node_crypto2.createHash)("sha256").update(`${metaType}:${name}`).digest("base64url");
1628
+ const hash = (0, import_node_crypto3.createHash)("sha256").update(`${metaType}:${name}`).digest("base64url");
1070
1629
  return hash.slice(0, 21);
1071
1630
  }
1072
- async function createRegistryFromGraph(reader) {
1631
+ async function createRegistryFromGraph(reader, executor) {
1073
1632
  const [nodeTypes, edgeTypes] = await Promise.all([
1074
1633
  reader.findNodes({ aType: META_NODE_TYPE }),
1075
1634
  reader.findNodes({ aType: META_EDGE_TYPE })
1076
1635
  ]);
1077
1636
  const entries = [...BOOTSTRAP_ENTRIES];
1637
+ const prevalidations = [];
1638
+ for (const record of nodeTypes) {
1639
+ const data = record.data;
1640
+ if (data.migrations) {
1641
+ for (const m of data.migrations) {
1642
+ prevalidations.push(precompileSource(m.up, executor));
1643
+ }
1644
+ }
1645
+ }
1646
+ for (const record of edgeTypes) {
1647
+ const data = record.data;
1648
+ if (data.migrations) {
1649
+ for (const m of data.migrations) {
1650
+ prevalidations.push(precompileSource(m.up, executor));
1651
+ }
1652
+ }
1653
+ }
1654
+ await Promise.all(prevalidations);
1078
1655
  for (const record of nodeTypes) {
1079
1656
  const data = record.data;
1080
1657
  entries.push({
@@ -1085,13 +1662,16 @@ async function createRegistryFromGraph(reader) {
1085
1662
  description: data.description,
1086
1663
  titleField: data.titleField,
1087
1664
  subtitleField: data.subtitleField,
1088
- allowedIn: data.allowedIn
1665
+ allowedIn: data.allowedIn,
1666
+ migrations: data.migrations ? compileMigrations(data.migrations, executor) : void 0,
1667
+ migrationWriteBack: data.migrationWriteBack
1089
1668
  });
1090
1669
  }
1091
1670
  for (const record of edgeTypes) {
1092
1671
  const data = record.data;
1093
1672
  const fromTypes = Array.isArray(data.from) ? data.from : [data.from];
1094
1673
  const toTypes = Array.isArray(data.to) ? data.to : [data.to];
1674
+ const compiledMigrations = data.migrations ? compileMigrations(data.migrations, executor) : void 0;
1095
1675
  for (const aType of fromTypes) {
1096
1676
  for (const bType of toTypes) {
1097
1677
  entries.push({
@@ -1104,7 +1684,9 @@ async function createRegistryFromGraph(reader) {
1104
1684
  titleField: data.titleField,
1105
1685
  subtitleField: data.subtitleField,
1106
1686
  allowedIn: data.allowedIn,
1107
- targetGraph: data.targetGraph
1687
+ targetGraph: data.targetGraph,
1688
+ migrations: compiledMigrations,
1689
+ migrationWriteBack: data.migrationWriteBack
1108
1690
  });
1109
1691
  }
1110
1692
  }
@@ -1120,6 +1702,8 @@ var GraphClientImpl = class _GraphClientImpl {
1120
1702
  this.db = db;
1121
1703
  this.scopePath = scopePath;
1122
1704
  this.adapter = createFirestoreAdapter(db, collectionPath);
1705
+ this.globalWriteBack = options?.migrationWriteBack ?? "off";
1706
+ this.migrationSandbox = options?.migrationSandbox;
1123
1707
  if (options?.registryMode) {
1124
1708
  this.dynamicConfig = options.registryMode;
1125
1709
  this.bootstrapRegistry = createBootstrapRegistry();
@@ -1171,6 +1755,9 @@ var GraphClientImpl = class _GraphClientImpl {
1171
1755
  metaPipelineAdapter;
1172
1756
  // Subgraph scope tracking
1173
1757
  scopePath;
1758
+ // Migration settings
1759
+ globalWriteBack;
1760
+ migrationSandbox;
1174
1761
  // ---------------------------------------------------------------------------
1175
1762
  // Registry routing
1176
1763
  // ---------------------------------------------------------------------------
@@ -1240,37 +1827,114 @@ var GraphClientImpl = class _GraphClientImpl {
1240
1827
  console.warn(`[firegraph] Query safety warning: ${result.reason}`);
1241
1828
  }
1242
1829
  // ---------------------------------------------------------------------------
1830
+ // Migration helpers
1831
+ // ---------------------------------------------------------------------------
1832
+ /**
1833
+ * Apply migration to a single record. Returns the (possibly migrated)
1834
+ * record and triggers write-back if applicable.
1835
+ */
1836
+ async applyMigration(record, docId) {
1837
+ const registry = this.getCombinedRegistry();
1838
+ if (!registry) return record;
1839
+ const result = await migrateRecord(record, registry, this.globalWriteBack);
1840
+ if (result.migrated) {
1841
+ this.handleWriteBack(result, docId);
1842
+ }
1843
+ return result.record;
1844
+ }
1845
+ /**
1846
+ * Apply migrations to an array of records. Returns all records
1847
+ * (migrated where applicable) and triggers write-backs.
1848
+ */
1849
+ async applyMigrations(records) {
1850
+ const registry = this.getCombinedRegistry();
1851
+ if (!registry || records.length === 0) return records;
1852
+ const results = await migrateRecords(records, registry, this.globalWriteBack);
1853
+ for (const result of results) {
1854
+ if (result.migrated) {
1855
+ const docId = result.record.axbType === NODE_RELATION ? computeNodeDocId(result.record.aUid) : computeEdgeDocId(result.record.aUid, result.record.axbType, result.record.bUid);
1856
+ this.handleWriteBack(result, docId);
1857
+ }
1858
+ }
1859
+ return results.map((r) => r.record);
1860
+ }
1861
+ /**
1862
+ * Handle write-back for a migrated record based on the resolved mode.
1863
+ *
1864
+ * Both `'eager'` and `'background'` are fire-and-forget (not awaited by
1865
+ * the caller). The difference is logging level on failure:
1866
+ * - `eager`: logs an error via `console.error`
1867
+ * - `background`: logs a warning via `console.warn`
1868
+ *
1869
+ * For truly synchronous write-back guarantees, use transactions — the
1870
+ * `GraphTransactionImpl` performs write-back inline within the transaction.
1871
+ */
1872
+ handleWriteBack(result, docId) {
1873
+ if (result.writeBack === "off") return;
1874
+ const doWriteBack = async () => {
1875
+ try {
1876
+ const update = {
1877
+ data: deserializeFirestoreTypes(result.record.data, this.db),
1878
+ updatedAt: import_firestore5.FieldValue.serverTimestamp()
1879
+ };
1880
+ if (result.record.v !== void 0) {
1881
+ update.v = result.record.v;
1882
+ }
1883
+ await this.adapter.updateDoc(docId, update);
1884
+ } catch (err) {
1885
+ const msg = `[firegraph] Migration write-back failed for ${docId}: ${err.message}`;
1886
+ if (result.writeBack === "eager") {
1887
+ console.error(msg);
1888
+ } else {
1889
+ console.warn(msg);
1890
+ }
1891
+ }
1892
+ };
1893
+ void doWriteBack();
1894
+ }
1895
+ // ---------------------------------------------------------------------------
1243
1896
  // GraphReader
1244
1897
  // ---------------------------------------------------------------------------
1245
1898
  async getNode(uid) {
1246
1899
  const docId = computeNodeDocId(uid);
1247
- return this.adapter.getDoc(docId);
1900
+ const record = await this.adapter.getDoc(docId);
1901
+ if (!record) return null;
1902
+ return this.applyMigration(record, docId);
1248
1903
  }
1249
1904
  async getEdge(aUid, axbType, bUid) {
1250
1905
  const docId = computeEdgeDocId(aUid, axbType, bUid);
1251
- return this.adapter.getDoc(docId);
1906
+ const record = await this.adapter.getDoc(docId);
1907
+ if (!record) return null;
1908
+ return this.applyMigration(record, docId);
1252
1909
  }
1253
1910
  async edgeExists(aUid, axbType, bUid) {
1254
- const record = await this.getEdge(aUid, axbType, bUid);
1911
+ const docId = computeEdgeDocId(aUid, axbType, bUid);
1912
+ const record = await this.adapter.getDoc(docId);
1255
1913
  return record !== null;
1256
1914
  }
1257
1915
  async findEdges(params) {
1258
1916
  const plan = buildEdgeQueryPlan(params);
1917
+ let records;
1259
1918
  if (plan.strategy === "get") {
1260
1919
  const record = await this.adapter.getDoc(plan.docId);
1261
- return record ? [record] : [];
1920
+ records = record ? [record] : [];
1921
+ } else {
1922
+ this.checkQuerySafety(plan.filters, params.allowCollectionScan);
1923
+ records = await this.executeQuery(plan.filters, plan.options);
1262
1924
  }
1263
- this.checkQuerySafety(plan.filters, params.allowCollectionScan);
1264
- return this.executeQuery(plan.filters, plan.options);
1925
+ return this.applyMigrations(records);
1265
1926
  }
1266
1927
  async findNodes(params) {
1267
1928
  const plan = buildNodeQueryPlan(params);
1929
+ let records;
1268
1930
  if (plan.strategy === "get") {
1269
1931
  const record = await this.adapter.getDoc(plan.docId);
1270
- return record ? [record] : [];
1932
+ records = record ? [record] : [];
1933
+ } else {
1934
+ this.checkQuerySafety(plan.filters, params.allowCollectionScan);
1935
+ records = await this.executeQuery(plan.filters, plan.options);
1271
1936
  }
1272
- this.checkQuerySafety(plan.filters, params.allowCollectionScan);
1273
- return this.executeQuery(plan.filters, plan.options);
1937
+ return this.applyMigrations(records);
1274
1938
  }
1275
1939
  // ---------------------------------------------------------------------------
1276
1940
  // GraphWriter
@@ -1283,6 +1947,12 @@ var GraphClientImpl = class _GraphClientImpl {
1283
1947
  const adapter = this.getAdapterForType(aType);
1284
1948
  const docId = computeNodeDocId(uid);
1285
1949
  const record = buildNodeRecord(aType, uid, data);
1950
+ if (registry) {
1951
+ const entry = registry.lookup(aType, NODE_RELATION, aType);
1952
+ if (entry?.schemaVersion && entry.schemaVersion > 0) {
1953
+ record.v = entry.schemaVersion;
1954
+ }
1955
+ }
1286
1956
  await adapter.setDoc(docId, record);
1287
1957
  }
1288
1958
  async putEdge(aType, aUid, axbType, bType, bUid, data) {
@@ -1293,13 +1963,19 @@ var GraphClientImpl = class _GraphClientImpl {
1293
1963
  const adapter = this.getAdapterForType(aType);
1294
1964
  const docId = computeEdgeDocId(aUid, axbType, bUid);
1295
1965
  const record = buildEdgeRecord(aType, aUid, axbType, bType, bUid, data);
1966
+ if (registry) {
1967
+ const entry = registry.lookup(aType, axbType, bType);
1968
+ if (entry?.schemaVersion && entry.schemaVersion > 0) {
1969
+ record.v = entry.schemaVersion;
1970
+ }
1971
+ }
1296
1972
  await adapter.setDoc(docId, record);
1297
1973
  }
1298
1974
  async updateNode(uid, data) {
1299
1975
  const docId = computeNodeDocId(uid);
1300
1976
  await this.adapter.updateDoc(docId, {
1301
1977
  ...data,
1302
- updatedAt: import_firestore4.FieldValue.serverTimestamp()
1978
+ updatedAt: import_firestore5.FieldValue.serverTimestamp()
1303
1979
  });
1304
1980
  }
1305
1981
  async removeNode(uid) {
@@ -1320,7 +1996,7 @@ var GraphClientImpl = class _GraphClientImpl {
1320
1996
  this.adapter.collectionPath,
1321
1997
  firestoreTx
1322
1998
  );
1323
- const graphTx = new GraphTransactionImpl(adapter, this.getCombinedRegistry(), this.scanProtection, this.scopePath);
1999
+ const graphTx = new GraphTransactionImpl(adapter, this.getCombinedRegistry(), this.scanProtection, this.scopePath, this.globalWriteBack, this.db);
1324
2000
  return fn(graphTx);
1325
2001
  });
1326
2002
  }
@@ -1352,7 +2028,9 @@ var GraphClientImpl = class _GraphClientImpl {
1352
2028
  {
1353
2029
  registry: this.getCombinedRegistry(),
1354
2030
  queryMode: this.queryMode === "pipeline" ? "pipeline" : "standard",
1355
- scanProtection: this.scanProtection
2031
+ scanProtection: this.scanProtection,
2032
+ migrationWriteBack: this.globalWriteBack,
2033
+ migrationSandbox: this.migrationSandbox
1356
2034
  },
1357
2035
  newScopePath
1358
2036
  );
@@ -1382,7 +2060,8 @@ var GraphClientImpl = class _GraphClientImpl {
1382
2060
  q = q.limit(plan.options.limit);
1383
2061
  }
1384
2062
  const snap = await q.get();
1385
- return snap.docs.map((doc) => doc.data());
2063
+ const records = snap.docs.map((doc) => doc.data());
2064
+ return this.applyMigrations(records);
1386
2065
  }
1387
2066
  // ---------------------------------------------------------------------------
1388
2067
  // Bulk operations
@@ -1420,6 +2099,10 @@ var GraphClientImpl = class _GraphClientImpl {
1420
2099
  if (options?.viewTemplate !== void 0) data.viewTemplate = options.viewTemplate;
1421
2100
  if (options?.viewCss !== void 0) data.viewCss = options.viewCss;
1422
2101
  if (options?.allowedIn !== void 0) data.allowedIn = options.allowedIn;
2102
+ if (options?.migrationWriteBack !== void 0) data.migrationWriteBack = options.migrationWriteBack;
2103
+ if (options?.migrations !== void 0) {
2104
+ data.migrations = await this.serializeMigrations(options.migrations);
2105
+ }
1423
2106
  await this.putNode(META_NODE_TYPE, uid, data);
1424
2107
  }
1425
2108
  async defineEdgeType(name, topology, jsonSchema, description, options) {
@@ -1461,6 +2144,10 @@ var GraphClientImpl = class _GraphClientImpl {
1461
2144
  if (options?.viewTemplate !== void 0) data.viewTemplate = options.viewTemplate;
1462
2145
  if (options?.viewCss !== void 0) data.viewCss = options.viewCss;
1463
2146
  if (options?.allowedIn !== void 0) data.allowedIn = options.allowedIn;
2147
+ if (options?.migrationWriteBack !== void 0) data.migrationWriteBack = options.migrationWriteBack;
2148
+ if (options?.migrations !== void 0) {
2149
+ data.migrations = await this.serializeMigrations(options.migrations);
2150
+ }
1464
2151
  await this.putNode(META_EDGE_TYPE, uid, data);
1465
2152
  }
1466
2153
  async reloadRegistry() {
@@ -1470,13 +2157,28 @@ var GraphClientImpl = class _GraphClientImpl {
1470
2157
  );
1471
2158
  }
1472
2159
  const reader = this.createMetaReader();
1473
- const dynamicOnly = await createRegistryFromGraph(reader);
2160
+ const dynamicOnly = await createRegistryFromGraph(reader, this.migrationSandbox);
1474
2161
  if (this.staticRegistry) {
1475
2162
  this.dynamicRegistry = createMergedRegistry(this.staticRegistry, dynamicOnly);
1476
2163
  } else {
1477
2164
  this.dynamicRegistry = dynamicOnly;
1478
2165
  }
1479
2166
  }
2167
+ /**
2168
+ * Serialize migration steps for storage in Firestore.
2169
+ * Function objects are converted via `.toString()`; strings are stored as-is.
2170
+ * Each migration is validated at define-time by pre-compiling in the sandbox.
2171
+ */
2172
+ async serializeMigrations(migrations) {
2173
+ const result = migrations.map((m) => {
2174
+ const source = typeof m.up === "function" ? m.up.toString() : m.up;
2175
+ return { fromVersion: m.fromVersion, toVersion: m.toVersion, up: source };
2176
+ });
2177
+ await Promise.all(
2178
+ result.map((m) => precompileSource(m.up, this.migrationSandbox))
2179
+ );
2180
+ return result;
2181
+ }
1480
2182
  /**
1481
2183
  * Create a GraphReader for the meta-collection.
1482
2184
  * If meta-collection is the same as main collection, returns `this`.
@@ -1843,7 +2545,7 @@ function resolveView(resolverConfig, availableViewNames, context) {
1843
2545
  var import_node_fs = require("fs");
1844
2546
  var import_node_module = require("module");
1845
2547
  var import_node_path = require("path");
1846
- var import_meta = {};
2548
+ var import_meta2 = {};
1847
2549
  var DiscoveryError = class extends FiregraphError {
1848
2550
  constructor(message) {
1849
2551
  super(message, "DISCOVERY_ERROR");
@@ -1882,7 +2584,7 @@ function loadSchema(dir, entityLabel) {
1882
2584
  var _jiti;
1883
2585
  function getJiti() {
1884
2586
  if (!_jiti) {
1885
- const base = typeof __filename !== "undefined" ? __filename : import_meta.url;
2587
+ const base = typeof __filename !== "undefined" ? __filename : import_meta2.url;
1886
2588
  const esmRequire = (0, import_node_module.createRequire)(base);
1887
2589
  const { createJiti } = esmRequire("jiti");
1888
2590
  _jiti = createJiti(base, { interopDefault: true });
@@ -1915,11 +2617,39 @@ function findViewsFile(dir) {
1915
2617
  }
1916
2618
  return void 0;
1917
2619
  }
2620
+ var MIGRATION_EXTENSIONS = [".ts", ".js", ".mts", ".mjs"];
2621
+ function findMigrationsFile(dir) {
2622
+ for (const ext of MIGRATION_EXTENSIONS) {
2623
+ const candidate = (0, import_node_path.join)(dir, `migrations${ext}`);
2624
+ if ((0, import_node_fs.existsSync)(candidate)) return candidate;
2625
+ }
2626
+ return void 0;
2627
+ }
2628
+ function loadMigrations(filePath, entityLabel) {
2629
+ try {
2630
+ const jiti = getJiti();
2631
+ const mod = jiti(filePath);
2632
+ const migrations = mod && typeof mod === "object" && "default" in mod ? mod.default : mod;
2633
+ if (!Array.isArray(migrations)) {
2634
+ throw new DiscoveryError(
2635
+ `Migrations file ${filePath} for ${entityLabel} must default-export an array of MigrationStep.`
2636
+ );
2637
+ }
2638
+ return migrations;
2639
+ } catch (err) {
2640
+ if (err instanceof DiscoveryError) throw err;
2641
+ throw new DiscoveryError(
2642
+ `Failed to load migrations ${filePath} for ${entityLabel}: ${err.message}`
2643
+ );
2644
+ }
2645
+ }
1918
2646
  function loadNodeEntity(dir, name) {
1919
2647
  const schema = loadSchema(dir, `node type "${name}"`);
1920
2648
  const meta = readJsonIfExists((0, import_node_path.join)(dir, "meta.json"));
1921
2649
  const sampleData = readJsonIfExists((0, import_node_path.join)(dir, "sample.json"));
1922
2650
  const viewsPath = findViewsFile(dir);
2651
+ const migrationsPath = findMigrationsFile(dir);
2652
+ const migrations = migrationsPath ? loadMigrations(migrationsPath, `node type "${name}"`) : void 0;
1923
2653
  return {
1924
2654
  kind: "node",
1925
2655
  name,
@@ -1930,7 +2660,9 @@ function loadNodeEntity(dir, name) {
1930
2660
  viewDefaults: meta?.viewDefaults,
1931
2661
  viewsPath,
1932
2662
  sampleData,
1933
- allowedIn: meta?.allowedIn
2663
+ allowedIn: meta?.allowedIn,
2664
+ migrations,
2665
+ migrationWriteBack: meta?.migrationWriteBack
1934
2666
  };
1935
2667
  }
1936
2668
  function loadEdgeEntity(dir, name) {
@@ -1955,6 +2687,8 @@ function loadEdgeEntity(dir, name) {
1955
2687
  const meta = readJsonIfExists((0, import_node_path.join)(dir, "meta.json"));
1956
2688
  const sampleData = readJsonIfExists((0, import_node_path.join)(dir, "sample.json"));
1957
2689
  const viewsPath = findViewsFile(dir);
2690
+ const migrationsPath = findMigrationsFile(dir);
2691
+ const migrations = migrationsPath ? loadMigrations(migrationsPath, `edge type "${name}"`) : void 0;
1958
2692
  return {
1959
2693
  kind: "edge",
1960
2694
  name,
@@ -1967,7 +2701,9 @@ function loadEdgeEntity(dir, name) {
1967
2701
  viewsPath,
1968
2702
  sampleData,
1969
2703
  allowedIn: meta?.allowedIn,
1970
- targetGraph: topology.targetGraph ?? meta?.targetGraph
2704
+ targetGraph: topology.targetGraph ?? meta?.targetGraph,
2705
+ migrations,
2706
+ migrationWriteBack: meta?.migrationWriteBack
1971
2707
  };
1972
2708
  }
1973
2709
  function getSubdirectories(dir) {
@@ -2478,6 +3214,7 @@ var QueryClient = class {
2478
3214
  InvalidQueryError,
2479
3215
  META_EDGE_TYPE,
2480
3216
  META_NODE_TYPE,
3217
+ MigrationError,
2481
3218
  NODE_TYPE_SCHEMA,
2482
3219
  NodeNotFoundError,
2483
3220
  QueryClient,
@@ -2485,13 +3222,17 @@ var QueryClient = class {
2485
3222
  QuerySafetyError,
2486
3223
  RegistryScopeError,
2487
3224
  RegistryViolationError,
3225
+ SERIALIZATION_TAG,
2488
3226
  TraversalError,
2489
3227
  ValidationError,
2490
3228
  analyzeQuerySafety,
3229
+ applyMigrationChain,
2491
3230
  buildEdgeQueryPlan,
2492
3231
  buildEdgeRecord,
2493
3232
  buildNodeQueryPlan,
2494
3233
  buildNodeRecord,
3234
+ compileMigrationFn,
3235
+ compileMigrations,
2495
3236
  compileSchema,
2496
3237
  computeEdgeDocId,
2497
3238
  computeNodeDocId,
@@ -2501,18 +3242,27 @@ var QueryClient = class {
2501
3242
  createRegistry,
2502
3243
  createRegistryFromGraph,
2503
3244
  createTraversal,
3245
+ defaultExecutor,
2504
3246
  defineConfig,
2505
3247
  defineViews,
3248
+ deserializeFirestoreTypes,
3249
+ destroySandboxWorker,
2506
3250
  discoverEntities,
2507
3251
  generateDeterministicUid,
2508
3252
  generateId,
2509
3253
  generateIndexConfig,
2510
3254
  generateTypes,
2511
3255
  isAncestorUid,
3256
+ isTaggedValue,
2512
3257
  jsonSchemaToFieldMeta,
2513
3258
  matchScope,
2514
3259
  matchScopeAny,
3260
+ migrateRecord,
3261
+ migrateRecords,
3262
+ precompileSource,
2515
3263
  resolveAncestorCollection,
2516
- resolveView
3264
+ resolveView,
3265
+ serializeFirestoreTypes,
3266
+ validateMigrationChain
2517
3267
  });
2518
3268
  //# sourceMappingURL=index.cjs.map