@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.
@@ -29324,7 +29324,7 @@ import { fileURLToPath } from "url";
29324
29324
  import { Firestore } from "@google-cloud/firestore";
29325
29325
 
29326
29326
  // src/client.ts
29327
- import { FieldValue as FieldValue4 } from "@google-cloud/firestore";
29327
+ import { FieldValue as FieldValue5 } from "@google-cloud/firestore";
29328
29328
 
29329
29329
  // src/docid.ts
29330
29330
  import { createHash } from "node:crypto";
@@ -29434,6 +29434,12 @@ var RegistryScopeError = class extends FiregraphError {
29434
29434
  this.name = "RegistryScopeError";
29435
29435
  }
29436
29436
  };
29437
+ var MigrationError = class extends FiregraphError {
29438
+ constructor(message) {
29439
+ super(message, "MIGRATION_ERROR");
29440
+ this.name = "MigrationError";
29441
+ }
29442
+ };
29437
29443
 
29438
29444
  // src/query.ts
29439
29445
  function buildEdgeQueryPlan(params) {
@@ -29624,7 +29630,7 @@ function createPipelineQueryAdapter(db2, collectionPath) {
29624
29630
  }
29625
29631
 
29626
29632
  // src/transaction.ts
29627
- import { FieldValue as FieldValue2 } from "@google-cloud/firestore";
29633
+ import { FieldValue as FieldValue3 } from "@google-cloud/firestore";
29628
29634
 
29629
29635
  // src/query-safety.ts
29630
29636
  var SAFE_INDEX_PATTERNS = [
@@ -29674,24 +29680,248 @@ function analyzeQuerySafety(filters) {
29674
29680
  };
29675
29681
  }
29676
29682
 
29683
+ // src/serialization.ts
29684
+ import { Timestamp, GeoPoint, FieldValue as FieldValue2 } from "@google-cloud/firestore";
29685
+ var SERIALIZATION_TAG = "__firegraph_ser__";
29686
+ var KNOWN_TYPES = /* @__PURE__ */ new Set(["Timestamp", "GeoPoint", "VectorValue", "DocumentReference"]);
29687
+ var _docRefWarned = false;
29688
+ function isTaggedValue(value) {
29689
+ if (value === null || typeof value !== "object") return false;
29690
+ const tag = value[SERIALIZATION_TAG];
29691
+ return typeof tag === "string" && KNOWN_TYPES.has(tag);
29692
+ }
29693
+ function isTimestamp(value) {
29694
+ return value instanceof Timestamp;
29695
+ }
29696
+ function isGeoPoint(value) {
29697
+ return value instanceof GeoPoint;
29698
+ }
29699
+ function isDocumentReference(value) {
29700
+ if (value === null || typeof value !== "object") return false;
29701
+ const v = value;
29702
+ return typeof v.path === "string" && v.firestore !== void 0 && typeof v.id === "string" && v.constructor?.name === "DocumentReference";
29703
+ }
29704
+ function isVectorValue(value) {
29705
+ if (value === null || typeof value !== "object") return false;
29706
+ const v = value;
29707
+ return v.constructor?.name === "VectorValue" && Array.isArray(v._values);
29708
+ }
29709
+ function serializeFirestoreTypes(data) {
29710
+ return serializeValue(data);
29711
+ }
29712
+ function serializeValue(value) {
29713
+ if (value === null || value === void 0) return value;
29714
+ if (typeof value !== "object") return value;
29715
+ if (isTimestamp(value)) {
29716
+ return { [SERIALIZATION_TAG]: "Timestamp", seconds: value.seconds, nanoseconds: value.nanoseconds };
29717
+ }
29718
+ if (isGeoPoint(value)) {
29719
+ return { [SERIALIZATION_TAG]: "GeoPoint", latitude: value.latitude, longitude: value.longitude };
29720
+ }
29721
+ if (isDocumentReference(value)) {
29722
+ return { [SERIALIZATION_TAG]: "DocumentReference", path: value.path };
29723
+ }
29724
+ if (isVectorValue(value)) {
29725
+ const v = value;
29726
+ const values = typeof v.toArray === "function" ? v.toArray() : v._values;
29727
+ return { [SERIALIZATION_TAG]: "VectorValue", values: [...values] };
29728
+ }
29729
+ if (Array.isArray(value)) {
29730
+ return value.map(serializeValue);
29731
+ }
29732
+ const result = {};
29733
+ for (const key of Object.keys(value)) {
29734
+ result[key] = serializeValue(value[key]);
29735
+ }
29736
+ return result;
29737
+ }
29738
+ function deserializeFirestoreTypes(data, db2) {
29739
+ return deserializeValue(data, db2);
29740
+ }
29741
+ function deserializeValue(value, db2) {
29742
+ if (value === null || value === void 0) return value;
29743
+ if (typeof value !== "object") return value;
29744
+ if (isTimestamp(value) || isGeoPoint(value) || isDocumentReference(value) || isVectorValue(value)) {
29745
+ return value;
29746
+ }
29747
+ if (Array.isArray(value)) {
29748
+ return value.map((v) => deserializeValue(v, db2));
29749
+ }
29750
+ const obj = value;
29751
+ if (isTaggedValue(obj)) {
29752
+ const tag = obj[SERIALIZATION_TAG];
29753
+ switch (tag) {
29754
+ case "Timestamp":
29755
+ if (typeof obj.seconds !== "number" || typeof obj.nanoseconds !== "number") return obj;
29756
+ return new Timestamp(obj.seconds, obj.nanoseconds);
29757
+ case "GeoPoint":
29758
+ if (typeof obj.latitude !== "number" || typeof obj.longitude !== "number") return obj;
29759
+ return new GeoPoint(obj.latitude, obj.longitude);
29760
+ case "VectorValue":
29761
+ if (!Array.isArray(obj.values)) return obj;
29762
+ return FieldValue2.vector(obj.values);
29763
+ case "DocumentReference":
29764
+ if (typeof obj.path !== "string") return obj;
29765
+ if (db2) {
29766
+ return db2.doc(obj.path);
29767
+ }
29768
+ if (!_docRefWarned) {
29769
+ _docRefWarned = true;
29770
+ console.warn(
29771
+ "[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."
29772
+ );
29773
+ }
29774
+ return obj;
29775
+ default:
29776
+ return obj;
29777
+ }
29778
+ }
29779
+ const result = {};
29780
+ for (const key of Object.keys(obj)) {
29781
+ result[key] = deserializeValue(obj[key], db2);
29782
+ }
29783
+ return result;
29784
+ }
29785
+
29786
+ // src/migration.ts
29787
+ async function applyMigrationChain(data, currentVersion, targetVersion, migrations) {
29788
+ const sorted = [...migrations].sort((a, b) => a.fromVersion - b.fromVersion);
29789
+ let result = { ...data };
29790
+ let version2 = currentVersion;
29791
+ for (const step of sorted) {
29792
+ if (step.fromVersion === version2) {
29793
+ try {
29794
+ result = await step.up(result);
29795
+ } catch (err) {
29796
+ if (err instanceof MigrationError) throw err;
29797
+ throw new MigrationError(
29798
+ `Migration from v${step.fromVersion} to v${step.toVersion} failed: ${err.message}`
29799
+ );
29800
+ }
29801
+ if (!result || typeof result !== "object") {
29802
+ throw new MigrationError(
29803
+ `Migration from v${step.fromVersion} to v${step.toVersion} returned invalid data (expected object)`
29804
+ );
29805
+ }
29806
+ version2 = step.toVersion;
29807
+ }
29808
+ }
29809
+ if (version2 !== targetVersion) {
29810
+ throw new MigrationError(
29811
+ `Incomplete migration chain: reached v${version2} but target is v${targetVersion}`
29812
+ );
29813
+ }
29814
+ return result;
29815
+ }
29816
+ function validateMigrationChain(migrations, label) {
29817
+ if (migrations.length === 0) return;
29818
+ const seen = /* @__PURE__ */ new Set();
29819
+ for (const step of migrations) {
29820
+ if (step.toVersion <= step.fromVersion) {
29821
+ throw new MigrationError(
29822
+ `${label}: migration step has toVersion (${step.toVersion}) <= fromVersion (${step.fromVersion})`
29823
+ );
29824
+ }
29825
+ if (seen.has(step.fromVersion)) {
29826
+ throw new MigrationError(
29827
+ `${label}: duplicate migration step for fromVersion ${step.fromVersion}`
29828
+ );
29829
+ }
29830
+ seen.add(step.fromVersion);
29831
+ }
29832
+ const sorted = [...migrations].sort((a, b) => a.fromVersion - b.fromVersion);
29833
+ const targetVersion = Math.max(...migrations.map((m) => m.toVersion));
29834
+ let version2 = 0;
29835
+ for (const step of sorted) {
29836
+ if (step.fromVersion === version2) {
29837
+ version2 = step.toVersion;
29838
+ } else if (step.fromVersion > version2) {
29839
+ throw new MigrationError(
29840
+ `${label}: migration chain has a gap \u2014 no step covers v${version2} \u2192 v${step.fromVersion}`
29841
+ );
29842
+ }
29843
+ }
29844
+ if (version2 !== targetVersion) {
29845
+ throw new MigrationError(
29846
+ `${label}: migration chain does not reach v${targetVersion} (stuck at v${version2})`
29847
+ );
29848
+ }
29849
+ }
29850
+ async function migrateRecord(record2, registry2, globalWriteBack = "off") {
29851
+ const entry = registry2.lookup(record2.aType, record2.axbType, record2.bType);
29852
+ if (!entry?.migrations?.length || !entry.schemaVersion) {
29853
+ return { record: record2, migrated: false, writeBack: "off" };
29854
+ }
29855
+ const currentVersion = record2.v ?? 0;
29856
+ if (currentVersion >= entry.schemaVersion) {
29857
+ return { record: record2, migrated: false, writeBack: "off" };
29858
+ }
29859
+ const migratedData = await applyMigrationChain(
29860
+ record2.data,
29861
+ currentVersion,
29862
+ entry.schemaVersion,
29863
+ entry.migrations
29864
+ );
29865
+ const writeBack = entry.migrationWriteBack ?? globalWriteBack ?? "off";
29866
+ return {
29867
+ record: { ...record2, data: migratedData, v: entry.schemaVersion },
29868
+ migrated: true,
29869
+ writeBack
29870
+ };
29871
+ }
29872
+ async function migrateRecords(records, registry2, globalWriteBack = "off") {
29873
+ return Promise.all(
29874
+ records.map((r) => migrateRecord(r, registry2, globalWriteBack))
29875
+ );
29876
+ }
29877
+
29677
29878
  // src/transaction.ts
29678
29879
  var GraphTransactionImpl = class {
29679
- constructor(adapter, registry2, scanProtection = "error", scopePath = "") {
29880
+ constructor(adapter, registry2, scanProtection = "error", scopePath = "", globalWriteBack = "off", db2) {
29680
29881
  this.adapter = adapter;
29681
29882
  this.registry = registry2;
29682
29883
  this.scanProtection = scanProtection;
29683
29884
  this.scopePath = scopePath;
29885
+ this.globalWriteBack = globalWriteBack;
29886
+ this.db = db2;
29684
29887
  }
29685
29888
  async getNode(uid) {
29686
29889
  const docId = computeNodeDocId(uid);
29687
- return this.adapter.getDoc(docId);
29890
+ const record2 = await this.adapter.getDoc(docId);
29891
+ if (!record2 || !this.registry) return record2;
29892
+ const result = await migrateRecord(record2, this.registry, this.globalWriteBack);
29893
+ if (result.migrated && result.writeBack !== "off") {
29894
+ const update = {
29895
+ data: deserializeFirestoreTypes(result.record.data, this.db),
29896
+ updatedAt: FieldValue3.serverTimestamp()
29897
+ };
29898
+ if (result.record.v !== void 0) {
29899
+ update.v = result.record.v;
29900
+ }
29901
+ this.adapter.updateDoc(docId, update);
29902
+ }
29903
+ return result.record;
29688
29904
  }
29689
29905
  async getEdge(aUid, axbType, bUid) {
29690
29906
  const docId = computeEdgeDocId(aUid, axbType, bUid);
29691
- return this.adapter.getDoc(docId);
29907
+ const record2 = await this.adapter.getDoc(docId);
29908
+ if (!record2 || !this.registry) return record2;
29909
+ const result = await migrateRecord(record2, this.registry, this.globalWriteBack);
29910
+ if (result.migrated && result.writeBack !== "off") {
29911
+ const update = {
29912
+ data: deserializeFirestoreTypes(result.record.data, this.db),
29913
+ updatedAt: FieldValue3.serverTimestamp()
29914
+ };
29915
+ if (result.record.v !== void 0) {
29916
+ update.v = result.record.v;
29917
+ }
29918
+ this.adapter.updateDoc(docId, update);
29919
+ }
29920
+ return result.record;
29692
29921
  }
29693
29922
  async edgeExists(aUid, axbType, bUid) {
29694
- const record2 = await this.getEdge(aUid, axbType, bUid);
29923
+ const docId = computeEdgeDocId(aUid, axbType, bUid);
29924
+ const record2 = await this.adapter.getDoc(docId);
29695
29925
  return record2 !== null;
29696
29926
  }
29697
29927
  checkQuerySafety(filters, allowCollectionScan) {
@@ -29705,21 +29935,45 @@ var GraphTransactionImpl = class {
29705
29935
  }
29706
29936
  async findEdges(params) {
29707
29937
  const plan = buildEdgeQueryPlan(params);
29938
+ let records;
29708
29939
  if (plan.strategy === "get") {
29709
29940
  const record2 = await this.adapter.getDoc(plan.docId);
29710
- return record2 ? [record2] : [];
29941
+ records = record2 ? [record2] : [];
29942
+ } else {
29943
+ this.checkQuerySafety(plan.filters, params.allowCollectionScan);
29944
+ records = await this.adapter.query(plan.filters, plan.options);
29711
29945
  }
29712
- this.checkQuerySafety(plan.filters, params.allowCollectionScan);
29713
- return this.adapter.query(plan.filters, plan.options);
29946
+ return this.applyMigrations(records);
29714
29947
  }
29715
29948
  async findNodes(params) {
29716
29949
  const plan = buildNodeQueryPlan(params);
29950
+ let records;
29717
29951
  if (plan.strategy === "get") {
29718
29952
  const record2 = await this.adapter.getDoc(plan.docId);
29719
- return record2 ? [record2] : [];
29953
+ records = record2 ? [record2] : [];
29954
+ } else {
29955
+ this.checkQuerySafety(plan.filters, params.allowCollectionScan);
29956
+ records = await this.adapter.query(plan.filters, plan.options);
29957
+ }
29958
+ return this.applyMigrations(records);
29959
+ }
29960
+ async applyMigrations(records) {
29961
+ if (!this.registry || records.length === 0) return records;
29962
+ const results = await migrateRecords(records, this.registry, this.globalWriteBack);
29963
+ for (const result of results) {
29964
+ if (result.migrated && result.writeBack !== "off") {
29965
+ const docId = result.record.axbType === NODE_RELATION ? computeNodeDocId(result.record.aUid) : computeEdgeDocId(result.record.aUid, result.record.axbType, result.record.bUid);
29966
+ const update = {
29967
+ data: deserializeFirestoreTypes(result.record.data, this.db),
29968
+ updatedAt: FieldValue3.serverTimestamp()
29969
+ };
29970
+ if (result.record.v !== void 0) {
29971
+ update.v = result.record.v;
29972
+ }
29973
+ this.adapter.updateDoc(docId, update);
29974
+ }
29720
29975
  }
29721
- this.checkQuerySafety(plan.filters, params.allowCollectionScan);
29722
- return this.adapter.query(plan.filters, plan.options);
29976
+ return results.map((r) => r.record);
29723
29977
  }
29724
29978
  async putNode(aType, uid, data) {
29725
29979
  if (this.registry) {
@@ -29727,6 +29981,12 @@ var GraphTransactionImpl = class {
29727
29981
  }
29728
29982
  const docId = computeNodeDocId(uid);
29729
29983
  const record2 = buildNodeRecord(aType, uid, data);
29984
+ if (this.registry) {
29985
+ const entry = this.registry.lookup(aType, NODE_RELATION, aType);
29986
+ if (entry?.schemaVersion && entry.schemaVersion > 0) {
29987
+ record2.v = entry.schemaVersion;
29988
+ }
29989
+ }
29730
29990
  this.adapter.setDoc(docId, record2);
29731
29991
  }
29732
29992
  async putEdge(aType, aUid, axbType, bType, bUid, data) {
@@ -29735,13 +29995,19 @@ var GraphTransactionImpl = class {
29735
29995
  }
29736
29996
  const docId = computeEdgeDocId(aUid, axbType, bUid);
29737
29997
  const record2 = buildEdgeRecord(aType, aUid, axbType, bType, bUid, data);
29998
+ if (this.registry) {
29999
+ const entry = this.registry.lookup(aType, axbType, bType);
30000
+ if (entry?.schemaVersion && entry.schemaVersion > 0) {
30001
+ record2.v = entry.schemaVersion;
30002
+ }
30003
+ }
29738
30004
  this.adapter.setDoc(docId, record2);
29739
30005
  }
29740
30006
  async updateNode(uid, data) {
29741
30007
  const docId = computeNodeDocId(uid);
29742
30008
  this.adapter.updateDoc(docId, {
29743
30009
  ...data,
29744
- updatedAt: FieldValue2.serverTimestamp()
30010
+ updatedAt: FieldValue3.serverTimestamp()
29745
30011
  });
29746
30012
  }
29747
30013
  async removeNode(uid) {
@@ -29755,7 +30021,7 @@ var GraphTransactionImpl = class {
29755
30021
  };
29756
30022
 
29757
30023
  // src/batch.ts
29758
- import { FieldValue as FieldValue3 } from "@google-cloud/firestore";
30024
+ import { FieldValue as FieldValue4 } from "@google-cloud/firestore";
29759
30025
  var GraphBatchImpl = class {
29760
30026
  constructor(adapter, registry2, scopePath = "") {
29761
30027
  this.adapter = adapter;
@@ -29768,6 +30034,12 @@ var GraphBatchImpl = class {
29768
30034
  }
29769
30035
  const docId = computeNodeDocId(uid);
29770
30036
  const record2 = buildNodeRecord(aType, uid, data);
30037
+ if (this.registry) {
30038
+ const entry = this.registry.lookup(aType, NODE_RELATION, aType);
30039
+ if (entry?.schemaVersion && entry.schemaVersion > 0) {
30040
+ record2.v = entry.schemaVersion;
30041
+ }
30042
+ }
29771
30043
  this.adapter.setDoc(docId, record2);
29772
30044
  }
29773
30045
  async putEdge(aType, aUid, axbType, bType, bUid, data) {
@@ -29776,13 +30048,19 @@ var GraphBatchImpl = class {
29776
30048
  }
29777
30049
  const docId = computeEdgeDocId(aUid, axbType, bUid);
29778
30050
  const record2 = buildEdgeRecord(aType, aUid, axbType, bType, bUid, data);
30051
+ if (this.registry) {
30052
+ const entry = this.registry.lookup(aType, axbType, bType);
30053
+ if (entry?.schemaVersion && entry.schemaVersion > 0) {
30054
+ record2.v = entry.schemaVersion;
30055
+ }
30056
+ }
29779
30057
  this.adapter.setDoc(docId, record2);
29780
30058
  }
29781
30059
  async updateNode(uid, data) {
29782
30060
  const docId = computeNodeDocId(uid);
29783
30061
  this.adapter.updateDoc(docId, {
29784
30062
  ...data,
29785
- updatedAt: FieldValue3.serverTimestamp()
30063
+ updatedAt: FieldValue4.serverTimestamp()
29786
30064
  });
29787
30065
  }
29788
30066
  async removeNode(uid) {
@@ -29941,7 +30219,7 @@ async function removeNodeCascade(db2, collectionPath, reader, uid, options) {
29941
30219
  }
29942
30220
 
29943
30221
  // src/dynamic-registry.ts
29944
- import { createHash as createHash2 } from "node:crypto";
30222
+ import { createHash as createHash3 } from "node:crypto";
29945
30223
 
29946
30224
  // src/json-schema.ts
29947
30225
  var import_ajv = __toESM(require_ajv(), 1);
@@ -30090,6 +30368,13 @@ function createRegistry(input) {
30090
30368
  `Entry (${entry.aType}) -[${entry.axbType}]-> (${entry.bType}) has invalid targetGraph "${entry.targetGraph}" \u2014 must be a single segment (no "/")`
30091
30369
  );
30092
30370
  }
30371
+ if (entry.migrations?.length) {
30372
+ const label = `Entry (${entry.aType}) -[${entry.axbType}]-> (${entry.bType})`;
30373
+ validateMigrationChain(entry.migrations, label);
30374
+ entry.schemaVersion = Math.max(...entry.migrations.map((m) => m.toVersion));
30375
+ } else {
30376
+ entry.schemaVersion = void 0;
30377
+ }
30093
30378
  const key = tripleKey(entry.aType, entry.axbType, entry.bType);
30094
30379
  const validator = entry.jsonSchema ? compileSchema(entry.jsonSchema, `(${entry.aType}) -[${entry.axbType}]-> (${entry.bType})`) : void 0;
30095
30380
  map2.set(key, { entry, validate: validator });
@@ -30191,7 +30476,9 @@ function discoveryToEntries(discovery) {
30191
30476
  description: entity.description,
30192
30477
  titleField: entity.titleField,
30193
30478
  subtitleField: entity.subtitleField,
30194
- allowedIn: entity.allowedIn
30479
+ allowedIn: entity.allowedIn,
30480
+ migrations: entity.migrations,
30481
+ migrationWriteBack: entity.migrationWriteBack
30195
30482
  });
30196
30483
  }
30197
30484
  for (const [axbType, entity] of discovery.edges) {
@@ -30217,7 +30504,9 @@ function discoveryToEntries(discovery) {
30217
30504
  titleField: entity.titleField,
30218
30505
  subtitleField: entity.subtitleField,
30219
30506
  allowedIn: entity.allowedIn,
30220
- targetGraph: resolvedTargetGraph
30507
+ targetGraph: resolvedTargetGraph,
30508
+ migrations: entity.migrations,
30509
+ migrationWriteBack: entity.migrationWriteBack
30221
30510
  });
30222
30511
  }
30223
30512
  }
@@ -30225,9 +30514,248 @@ function discoveryToEntries(discovery) {
30225
30514
  return entries;
30226
30515
  }
30227
30516
 
30517
+ // src/sandbox.ts
30518
+ import { Worker } from "node:worker_threads";
30519
+ import { createHash as createHash2 } from "node:crypto";
30520
+ var _worker = null;
30521
+ var _requestId = 0;
30522
+ var _pending = /* @__PURE__ */ new Map();
30523
+ var WORKER_SOURCE = [
30524
+ `'use strict';`,
30525
+ `var _wt = require('node:worker_threads');`,
30526
+ `var _mod = require('node:module');`,
30527
+ `var _crypto = require('node:crypto');`,
30528
+ `var parentPort = _wt.parentPort;`,
30529
+ `var workerData = _wt.workerData;`,
30530
+ ``,
30531
+ `// Load SES using the parent module's resolution context`,
30532
+ `var esmRequire = _mod.createRequire(workerData.parentUrl);`,
30533
+ `esmRequire('ses');`,
30534
+ ``,
30535
+ `lockdown({`,
30536
+ ` errorTaming: 'unsafe',`,
30537
+ ` consoleTaming: 'unsafe',`,
30538
+ ` evalTaming: 'safe-eval',`,
30539
+ ` overrideTaming: 'moderate',`,
30540
+ ` stackFiltering: 'verbose'`,
30541
+ `});`,
30542
+ ``,
30543
+ `// Defense-in-depth: verify lockdown() actually hardened JSON.`,
30544
+ `if (!Object.isFrozen(JSON)) {`,
30545
+ ` throw new Error('SES lockdown failed: JSON is not frozen');`,
30546
+ `}`,
30547
+ ``,
30548
+ `var cache = new Map();`,
30549
+ ``,
30550
+ `function hashSource(s) {`,
30551
+ ` return _crypto.createHash('sha256').update(s).digest('hex');`,
30552
+ `}`,
30553
+ ``,
30554
+ `function buildWrapper(source) {`,
30555
+ ` return '(function() {' +`,
30556
+ ` ' var fn = (' + source + ');\\n' +`,
30557
+ ` ' if (typeof fn !== "function") return null;\\n' +`,
30558
+ ` ' return function(jsonIn) {\\n' +`,
30559
+ ` ' var data = JSON.parse(jsonIn);\\n' +`,
30560
+ ` ' var result = fn(data);\\n' +`,
30561
+ ` ' if (result !== null && typeof result === "object" && typeof result.then === "function") {\\n' +`,
30562
+ ` ' return result.then(function(r) { return JSON.stringify(r); });\\n' +`,
30563
+ ` ' }\\n' +`,
30564
+ ` ' return JSON.stringify(result);\\n' +`,
30565
+ ` ' };\\n' +`,
30566
+ ` '})()';`,
30567
+ `}`,
30568
+ ``,
30569
+ `function compileSource(source) {`,
30570
+ ` var key = hashSource(source);`,
30571
+ ` var cached = cache.get(key);`,
30572
+ ` if (cached) return cached;`,
30573
+ ``,
30574
+ ` var compartmentFn;`,
30575
+ ` try {`,
30576
+ ` var c = new Compartment({ JSON: JSON });`,
30577
+ ` compartmentFn = c.evaluate(buildWrapper(source));`,
30578
+ ` } catch (err) {`,
30579
+ ` throw new Error('Failed to compile migration source: ' + (err.message || String(err)));`,
30580
+ ` }`,
30581
+ ``,
30582
+ ` if (typeof compartmentFn !== 'function') {`,
30583
+ ` throw new Error('Migration source did not produce a function: ' + source.slice(0, 80));`,
30584
+ ` }`,
30585
+ ``,
30586
+ ` cache.set(key, compartmentFn);`,
30587
+ ` return compartmentFn;`,
30588
+ `}`,
30589
+ ``,
30590
+ `parentPort.on('message', function(msg) {`,
30591
+ ` var id = msg.id;`,
30592
+ ` try {`,
30593
+ ` if (msg.type === 'compile') {`,
30594
+ ` compileSource(msg.source);`,
30595
+ ` parentPort.postMessage({ id: id, type: 'compiled' });`,
30596
+ ` return;`,
30597
+ ` }`,
30598
+ ` if (msg.type === 'execute') {`,
30599
+ ` var fn = compileSource(msg.source);`,
30600
+ ` var raw;`,
30601
+ ` try {`,
30602
+ ` raw = fn(msg.jsonData);`,
30603
+ ` } catch (err) {`,
30604
+ ` parentPort.postMessage({ id: id, type: 'error', message: 'Migration function threw: ' + (err.message || String(err)) });`,
30605
+ ` return;`,
30606
+ ` }`,
30607
+ ` if (raw !== null && typeof raw === 'object' && typeof raw.then === 'function') {`,
30608
+ ` raw.then(`,
30609
+ ` function(jsonResult) {`,
30610
+ ` if (jsonResult === undefined || jsonResult === null) {`,
30611
+ ` parentPort.postMessage({ id: id, type: 'error', message: 'Migration returned a non-JSON-serializable value' });`,
30612
+ ` } else {`,
30613
+ ` parentPort.postMessage({ id: id, type: 'result', jsonResult: jsonResult });`,
30614
+ ` }`,
30615
+ ` },`,
30616
+ ` function(err) {`,
30617
+ ` parentPort.postMessage({ id: id, type: 'error', message: 'Async migration function threw: ' + (err.message || String(err)) });`,
30618
+ ` }`,
30619
+ ` );`,
30620
+ ` return;`,
30621
+ ` }`,
30622
+ ` if (raw === undefined || raw === null) {`,
30623
+ ` parentPort.postMessage({ id: id, type: 'error', message: 'Migration returned a non-JSON-serializable value' });`,
30624
+ ` } else {`,
30625
+ ` parentPort.postMessage({ id: id, type: 'result', jsonResult: raw });`,
30626
+ ` }`,
30627
+ ` }`,
30628
+ ` } catch (err) {`,
30629
+ ` parentPort.postMessage({ id: id, type: 'error', message: err.message || String(err) });`,
30630
+ ` }`,
30631
+ `});`
30632
+ ].join("\n");
30633
+ function ensureWorker() {
30634
+ if (_worker) return _worker;
30635
+ _worker = new Worker(WORKER_SOURCE, {
30636
+ eval: true,
30637
+ workerData: { parentUrl: import.meta.url }
30638
+ });
30639
+ _worker.unref();
30640
+ _worker.on("message", (msg) => {
30641
+ if (msg.id === void 0) return;
30642
+ const pending = _pending.get(msg.id);
30643
+ if (!pending) return;
30644
+ _pending.delete(msg.id);
30645
+ if (msg.type === "error") {
30646
+ pending.reject(new MigrationError(msg.message ?? "Unknown sandbox error"));
30647
+ } else {
30648
+ pending.resolve(msg);
30649
+ }
30650
+ });
30651
+ _worker.on("error", (err) => {
30652
+ for (const [, p] of _pending) {
30653
+ p.reject(new MigrationError(`Sandbox worker error: ${err.message}`));
30654
+ }
30655
+ _pending.clear();
30656
+ _worker = null;
30657
+ });
30658
+ _worker.on("exit", (code) => {
30659
+ if (_pending.size > 0) {
30660
+ for (const [, p] of _pending) {
30661
+ p.reject(new MigrationError(`Sandbox worker exited with code ${code}`));
30662
+ }
30663
+ _pending.clear();
30664
+ }
30665
+ _worker = null;
30666
+ });
30667
+ return _worker;
30668
+ }
30669
+ function sendToWorker(msg) {
30670
+ const worker = ensureWorker();
30671
+ if (_requestId >= Number.MAX_SAFE_INTEGER) _requestId = 0;
30672
+ const id = ++_requestId;
30673
+ return new Promise((resolve2, reject) => {
30674
+ _pending.set(id, { resolve: resolve2, reject });
30675
+ worker.postMessage({ ...msg, id });
30676
+ });
30677
+ }
30678
+ var compiledCache = /* @__PURE__ */ new WeakMap();
30679
+ function getExecutorCache(executor) {
30680
+ let cache = compiledCache.get(executor);
30681
+ if (!cache) {
30682
+ cache = /* @__PURE__ */ new Map();
30683
+ compiledCache.set(executor, cache);
30684
+ }
30685
+ return cache;
30686
+ }
30687
+ function hashSource(source) {
30688
+ return createHash2("sha256").update(source).digest("hex");
30689
+ }
30690
+ function defaultExecutor(source) {
30691
+ ensureWorker();
30692
+ return (data) => {
30693
+ const jsonData = JSON.stringify(serializeFirestoreTypes(data));
30694
+ return sendToWorker({ type: "execute", source, jsonData }).then(
30695
+ (response) => {
30696
+ if (response.jsonResult === void 0 || response.jsonResult === null) {
30697
+ throw new MigrationError("Migration returned a non-JSON-serializable value");
30698
+ }
30699
+ try {
30700
+ return deserializeFirestoreTypes(JSON.parse(response.jsonResult));
30701
+ } catch {
30702
+ throw new MigrationError("Migration returned a non-JSON-serializable value");
30703
+ }
30704
+ }
30705
+ );
30706
+ };
30707
+ }
30708
+ async function precompileSource(source, executor) {
30709
+ if (executor && executor !== defaultExecutor) {
30710
+ try {
30711
+ executor(source);
30712
+ } catch (err) {
30713
+ if (err instanceof MigrationError) throw err;
30714
+ throw new MigrationError(
30715
+ `Failed to compile migration source: ${err.message}`
30716
+ );
30717
+ }
30718
+ return;
30719
+ }
30720
+ await sendToWorker({ type: "compile", source });
30721
+ }
30722
+ function compileMigrationFn(source, executor = defaultExecutor) {
30723
+ const cache = getExecutorCache(executor);
30724
+ const key = hashSource(source);
30725
+ const cached2 = cache.get(key);
30726
+ if (cached2) return cached2;
30727
+ try {
30728
+ const fn = executor(source);
30729
+ cache.set(key, fn);
30730
+ return fn;
30731
+ } catch (err) {
30732
+ if (err instanceof MigrationError) throw err;
30733
+ throw new MigrationError(
30734
+ `Failed to compile migration source: ${err.message}`
30735
+ );
30736
+ }
30737
+ }
30738
+ function compileMigrations(stored, executor) {
30739
+ return stored.map((step) => ({
30740
+ fromVersion: step.fromVersion,
30741
+ toVersion: step.toVersion,
30742
+ up: compileMigrationFn(step.up, executor)
30743
+ }));
30744
+ }
30745
+
30228
30746
  // src/dynamic-registry.ts
30229
30747
  var META_NODE_TYPE = "nodeType";
30230
30748
  var META_EDGE_TYPE = "edgeType";
30749
+ var STORED_MIGRATION_STEP_SCHEMA = {
30750
+ type: "object",
30751
+ required: ["fromVersion", "toVersion", "up"],
30752
+ properties: {
30753
+ fromVersion: { type: "integer", minimum: 0 },
30754
+ toVersion: { type: "integer", minimum: 1 },
30755
+ up: { type: "string", minLength: 1 }
30756
+ },
30757
+ additionalProperties: false
30758
+ };
30231
30759
  var NODE_TYPE_SCHEMA = {
30232
30760
  type: "object",
30233
30761
  required: ["name", "jsonSchema"],
@@ -30239,7 +30767,10 @@ var NODE_TYPE_SCHEMA = {
30239
30767
  subtitleField: { type: "string" },
30240
30768
  viewTemplate: { type: "string" },
30241
30769
  viewCss: { type: "string" },
30242
- allowedIn: { type: "array", items: { type: "string", minLength: 1 } }
30770
+ allowedIn: { type: "array", items: { type: "string", minLength: 1 } },
30771
+ schemaVersion: { type: "integer", minimum: 0 },
30772
+ migrations: { type: "array", items: STORED_MIGRATION_STEP_SCHEMA },
30773
+ migrationWriteBack: { type: "string", enum: ["off", "eager", "background"] }
30243
30774
  },
30244
30775
  additionalProperties: false
30245
30776
  };
@@ -30268,7 +30799,10 @@ var EDGE_TYPE_SCHEMA = {
30268
30799
  viewTemplate: { type: "string" },
30269
30800
  viewCss: { type: "string" },
30270
30801
  allowedIn: { type: "array", items: { type: "string", minLength: 1 } },
30271
- targetGraph: { type: "string", minLength: 1, pattern: "^[^/]+$" }
30802
+ targetGraph: { type: "string", minLength: 1, pattern: "^[^/]+$" },
30803
+ schemaVersion: { type: "integer", minimum: 0 },
30804
+ migrations: { type: "array", items: STORED_MIGRATION_STEP_SCHEMA },
30805
+ migrationWriteBack: { type: "string", enum: ["off", "eager", "background"] }
30272
30806
  },
30273
30807
  additionalProperties: false
30274
30808
  };
@@ -30292,15 +30826,33 @@ function createBootstrapRegistry() {
30292
30826
  return createRegistry([...BOOTSTRAP_ENTRIES]);
30293
30827
  }
30294
30828
  function generateDeterministicUid(metaType, name) {
30295
- const hash2 = createHash2("sha256").update(`${metaType}:${name}`).digest("base64url");
30829
+ const hash2 = createHash3("sha256").update(`${metaType}:${name}`).digest("base64url");
30296
30830
  return hash2.slice(0, 21);
30297
30831
  }
30298
- async function createRegistryFromGraph(reader) {
30832
+ async function createRegistryFromGraph(reader, executor) {
30299
30833
  const [nodeTypes, edgeTypes] = await Promise.all([
30300
30834
  reader.findNodes({ aType: META_NODE_TYPE }),
30301
30835
  reader.findNodes({ aType: META_EDGE_TYPE })
30302
30836
  ]);
30303
30837
  const entries = [...BOOTSTRAP_ENTRIES];
30838
+ const prevalidations = [];
30839
+ for (const record2 of nodeTypes) {
30840
+ const data = record2.data;
30841
+ if (data.migrations) {
30842
+ for (const m of data.migrations) {
30843
+ prevalidations.push(precompileSource(m.up, executor));
30844
+ }
30845
+ }
30846
+ }
30847
+ for (const record2 of edgeTypes) {
30848
+ const data = record2.data;
30849
+ if (data.migrations) {
30850
+ for (const m of data.migrations) {
30851
+ prevalidations.push(precompileSource(m.up, executor));
30852
+ }
30853
+ }
30854
+ }
30855
+ await Promise.all(prevalidations);
30304
30856
  for (const record2 of nodeTypes) {
30305
30857
  const data = record2.data;
30306
30858
  entries.push({
@@ -30311,13 +30863,16 @@ async function createRegistryFromGraph(reader) {
30311
30863
  description: data.description,
30312
30864
  titleField: data.titleField,
30313
30865
  subtitleField: data.subtitleField,
30314
- allowedIn: data.allowedIn
30866
+ allowedIn: data.allowedIn,
30867
+ migrations: data.migrations ? compileMigrations(data.migrations, executor) : void 0,
30868
+ migrationWriteBack: data.migrationWriteBack
30315
30869
  });
30316
30870
  }
30317
30871
  for (const record2 of edgeTypes) {
30318
30872
  const data = record2.data;
30319
30873
  const fromTypes = Array.isArray(data.from) ? data.from : [data.from];
30320
30874
  const toTypes = Array.isArray(data.to) ? data.to : [data.to];
30875
+ const compiledMigrations = data.migrations ? compileMigrations(data.migrations, executor) : void 0;
30321
30876
  for (const aType of fromTypes) {
30322
30877
  for (const bType of toTypes) {
30323
30878
  entries.push({
@@ -30330,7 +30885,9 @@ async function createRegistryFromGraph(reader) {
30330
30885
  titleField: data.titleField,
30331
30886
  subtitleField: data.subtitleField,
30332
30887
  allowedIn: data.allowedIn,
30333
- targetGraph: data.targetGraph
30888
+ targetGraph: data.targetGraph,
30889
+ migrations: compiledMigrations,
30890
+ migrationWriteBack: data.migrationWriteBack
30334
30891
  });
30335
30892
  }
30336
30893
  }
@@ -30346,6 +30903,8 @@ var GraphClientImpl = class _GraphClientImpl {
30346
30903
  this.db = db2;
30347
30904
  this.scopePath = scopePath;
30348
30905
  this.adapter = createFirestoreAdapter(db2, collectionPath);
30906
+ this.globalWriteBack = options?.migrationWriteBack ?? "off";
30907
+ this.migrationSandbox = options?.migrationSandbox;
30349
30908
  if (options?.registryMode) {
30350
30909
  this.dynamicConfig = options.registryMode;
30351
30910
  this.bootstrapRegistry = createBootstrapRegistry();
@@ -30397,6 +30956,9 @@ var GraphClientImpl = class _GraphClientImpl {
30397
30956
  metaPipelineAdapter;
30398
30957
  // Subgraph scope tracking
30399
30958
  scopePath;
30959
+ // Migration settings
30960
+ globalWriteBack;
30961
+ migrationSandbox;
30400
30962
  // ---------------------------------------------------------------------------
30401
30963
  // Registry routing
30402
30964
  // ---------------------------------------------------------------------------
@@ -30466,37 +31028,114 @@ var GraphClientImpl = class _GraphClientImpl {
30466
31028
  console.warn(`[firegraph] Query safety warning: ${result.reason}`);
30467
31029
  }
30468
31030
  // ---------------------------------------------------------------------------
31031
+ // Migration helpers
31032
+ // ---------------------------------------------------------------------------
31033
+ /**
31034
+ * Apply migration to a single record. Returns the (possibly migrated)
31035
+ * record and triggers write-back if applicable.
31036
+ */
31037
+ async applyMigration(record2, docId) {
31038
+ const registry2 = this.getCombinedRegistry();
31039
+ if (!registry2) return record2;
31040
+ const result = await migrateRecord(record2, registry2, this.globalWriteBack);
31041
+ if (result.migrated) {
31042
+ this.handleWriteBack(result, docId);
31043
+ }
31044
+ return result.record;
31045
+ }
31046
+ /**
31047
+ * Apply migrations to an array of records. Returns all records
31048
+ * (migrated where applicable) and triggers write-backs.
31049
+ */
31050
+ async applyMigrations(records) {
31051
+ const registry2 = this.getCombinedRegistry();
31052
+ if (!registry2 || records.length === 0) return records;
31053
+ const results = await migrateRecords(records, registry2, this.globalWriteBack);
31054
+ for (const result of results) {
31055
+ if (result.migrated) {
31056
+ const docId = result.record.axbType === NODE_RELATION ? computeNodeDocId(result.record.aUid) : computeEdgeDocId(result.record.aUid, result.record.axbType, result.record.bUid);
31057
+ this.handleWriteBack(result, docId);
31058
+ }
31059
+ }
31060
+ return results.map((r) => r.record);
31061
+ }
31062
+ /**
31063
+ * Handle write-back for a migrated record based on the resolved mode.
31064
+ *
31065
+ * Both `'eager'` and `'background'` are fire-and-forget (not awaited by
31066
+ * the caller). The difference is logging level on failure:
31067
+ * - `eager`: logs an error via `console.error`
31068
+ * - `background`: logs a warning via `console.warn`
31069
+ *
31070
+ * For truly synchronous write-back guarantees, use transactions — the
31071
+ * `GraphTransactionImpl` performs write-back inline within the transaction.
31072
+ */
31073
+ handleWriteBack(result, docId) {
31074
+ if (result.writeBack === "off") return;
31075
+ const doWriteBack = async () => {
31076
+ try {
31077
+ const update = {
31078
+ data: deserializeFirestoreTypes(result.record.data, this.db),
31079
+ updatedAt: FieldValue5.serverTimestamp()
31080
+ };
31081
+ if (result.record.v !== void 0) {
31082
+ update.v = result.record.v;
31083
+ }
31084
+ await this.adapter.updateDoc(docId, update);
31085
+ } catch (err) {
31086
+ const msg = `[firegraph] Migration write-back failed for ${docId}: ${err.message}`;
31087
+ if (result.writeBack === "eager") {
31088
+ console.error(msg);
31089
+ } else {
31090
+ console.warn(msg);
31091
+ }
31092
+ }
31093
+ };
31094
+ void doWriteBack();
31095
+ }
31096
+ // ---------------------------------------------------------------------------
30469
31097
  // GraphReader
30470
31098
  // ---------------------------------------------------------------------------
30471
31099
  async getNode(uid) {
30472
31100
  const docId = computeNodeDocId(uid);
30473
- return this.adapter.getDoc(docId);
31101
+ const record2 = await this.adapter.getDoc(docId);
31102
+ if (!record2) return null;
31103
+ return this.applyMigration(record2, docId);
30474
31104
  }
30475
31105
  async getEdge(aUid, axbType, bUid) {
30476
31106
  const docId = computeEdgeDocId(aUid, axbType, bUid);
30477
- return this.adapter.getDoc(docId);
31107
+ const record2 = await this.adapter.getDoc(docId);
31108
+ if (!record2) return null;
31109
+ return this.applyMigration(record2, docId);
30478
31110
  }
30479
31111
  async edgeExists(aUid, axbType, bUid) {
30480
- const record2 = await this.getEdge(aUid, axbType, bUid);
31112
+ const docId = computeEdgeDocId(aUid, axbType, bUid);
31113
+ const record2 = await this.adapter.getDoc(docId);
30481
31114
  return record2 !== null;
30482
31115
  }
30483
31116
  async findEdges(params) {
30484
31117
  const plan = buildEdgeQueryPlan(params);
31118
+ let records;
30485
31119
  if (plan.strategy === "get") {
30486
31120
  const record2 = await this.adapter.getDoc(plan.docId);
30487
- return record2 ? [record2] : [];
31121
+ records = record2 ? [record2] : [];
31122
+ } else {
31123
+ this.checkQuerySafety(plan.filters, params.allowCollectionScan);
31124
+ records = await this.executeQuery(plan.filters, plan.options);
30488
31125
  }
30489
- this.checkQuerySafety(plan.filters, params.allowCollectionScan);
30490
- return this.executeQuery(plan.filters, plan.options);
31126
+ return this.applyMigrations(records);
30491
31127
  }
30492
31128
  async findNodes(params) {
30493
31129
  const plan = buildNodeQueryPlan(params);
31130
+ let records;
30494
31131
  if (plan.strategy === "get") {
30495
31132
  const record2 = await this.adapter.getDoc(plan.docId);
30496
- return record2 ? [record2] : [];
31133
+ records = record2 ? [record2] : [];
31134
+ } else {
31135
+ this.checkQuerySafety(plan.filters, params.allowCollectionScan);
31136
+ records = await this.executeQuery(plan.filters, plan.options);
30497
31137
  }
30498
- this.checkQuerySafety(plan.filters, params.allowCollectionScan);
30499
- return this.executeQuery(plan.filters, plan.options);
31138
+ return this.applyMigrations(records);
30500
31139
  }
30501
31140
  // ---------------------------------------------------------------------------
30502
31141
  // GraphWriter
@@ -30509,6 +31148,12 @@ var GraphClientImpl = class _GraphClientImpl {
30509
31148
  const adapter = this.getAdapterForType(aType);
30510
31149
  const docId = computeNodeDocId(uid);
30511
31150
  const record2 = buildNodeRecord(aType, uid, data);
31151
+ if (registry2) {
31152
+ const entry = registry2.lookup(aType, NODE_RELATION, aType);
31153
+ if (entry?.schemaVersion && entry.schemaVersion > 0) {
31154
+ record2.v = entry.schemaVersion;
31155
+ }
31156
+ }
30512
31157
  await adapter.setDoc(docId, record2);
30513
31158
  }
30514
31159
  async putEdge(aType, aUid, axbType, bType, bUid, data) {
@@ -30519,13 +31164,19 @@ var GraphClientImpl = class _GraphClientImpl {
30519
31164
  const adapter = this.getAdapterForType(aType);
30520
31165
  const docId = computeEdgeDocId(aUid, axbType, bUid);
30521
31166
  const record2 = buildEdgeRecord(aType, aUid, axbType, bType, bUid, data);
31167
+ if (registry2) {
31168
+ const entry = registry2.lookup(aType, axbType, bType);
31169
+ if (entry?.schemaVersion && entry.schemaVersion > 0) {
31170
+ record2.v = entry.schemaVersion;
31171
+ }
31172
+ }
30522
31173
  await adapter.setDoc(docId, record2);
30523
31174
  }
30524
31175
  async updateNode(uid, data) {
30525
31176
  const docId = computeNodeDocId(uid);
30526
31177
  await this.adapter.updateDoc(docId, {
30527
31178
  ...data,
30528
- updatedAt: FieldValue4.serverTimestamp()
31179
+ updatedAt: FieldValue5.serverTimestamp()
30529
31180
  });
30530
31181
  }
30531
31182
  async removeNode(uid) {
@@ -30546,7 +31197,7 @@ var GraphClientImpl = class _GraphClientImpl {
30546
31197
  this.adapter.collectionPath,
30547
31198
  firestoreTx
30548
31199
  );
30549
- const graphTx = new GraphTransactionImpl(adapter, this.getCombinedRegistry(), this.scanProtection, this.scopePath);
31200
+ const graphTx = new GraphTransactionImpl(adapter, this.getCombinedRegistry(), this.scanProtection, this.scopePath, this.globalWriteBack, this.db);
30550
31201
  return fn(graphTx);
30551
31202
  });
30552
31203
  }
@@ -30578,7 +31229,9 @@ var GraphClientImpl = class _GraphClientImpl {
30578
31229
  {
30579
31230
  registry: this.getCombinedRegistry(),
30580
31231
  queryMode: this.queryMode === "pipeline" ? "pipeline" : "standard",
30581
- scanProtection: this.scanProtection
31232
+ scanProtection: this.scanProtection,
31233
+ migrationWriteBack: this.globalWriteBack,
31234
+ migrationSandbox: this.migrationSandbox
30582
31235
  },
30583
31236
  newScopePath
30584
31237
  );
@@ -30608,7 +31261,8 @@ var GraphClientImpl = class _GraphClientImpl {
30608
31261
  q = q.limit(plan.options.limit);
30609
31262
  }
30610
31263
  const snap = await q.get();
30611
- return snap.docs.map((doc) => doc.data());
31264
+ const records = snap.docs.map((doc) => doc.data());
31265
+ return this.applyMigrations(records);
30612
31266
  }
30613
31267
  // ---------------------------------------------------------------------------
30614
31268
  // Bulk operations
@@ -30646,6 +31300,10 @@ var GraphClientImpl = class _GraphClientImpl {
30646
31300
  if (options?.viewTemplate !== void 0) data.viewTemplate = options.viewTemplate;
30647
31301
  if (options?.viewCss !== void 0) data.viewCss = options.viewCss;
30648
31302
  if (options?.allowedIn !== void 0) data.allowedIn = options.allowedIn;
31303
+ if (options?.migrationWriteBack !== void 0) data.migrationWriteBack = options.migrationWriteBack;
31304
+ if (options?.migrations !== void 0) {
31305
+ data.migrations = await this.serializeMigrations(options.migrations);
31306
+ }
30649
31307
  await this.putNode(META_NODE_TYPE, uid, data);
30650
31308
  }
30651
31309
  async defineEdgeType(name, topology, jsonSchema, description, options) {
@@ -30687,6 +31345,10 @@ var GraphClientImpl = class _GraphClientImpl {
30687
31345
  if (options?.viewTemplate !== void 0) data.viewTemplate = options.viewTemplate;
30688
31346
  if (options?.viewCss !== void 0) data.viewCss = options.viewCss;
30689
31347
  if (options?.allowedIn !== void 0) data.allowedIn = options.allowedIn;
31348
+ if (options?.migrationWriteBack !== void 0) data.migrationWriteBack = options.migrationWriteBack;
31349
+ if (options?.migrations !== void 0) {
31350
+ data.migrations = await this.serializeMigrations(options.migrations);
31351
+ }
30690
31352
  await this.putNode(META_EDGE_TYPE, uid, data);
30691
31353
  }
30692
31354
  async reloadRegistry() {
@@ -30696,13 +31358,28 @@ var GraphClientImpl = class _GraphClientImpl {
30696
31358
  );
30697
31359
  }
30698
31360
  const reader = this.createMetaReader();
30699
- const dynamicOnly = await createRegistryFromGraph(reader);
31361
+ const dynamicOnly = await createRegistryFromGraph(reader, this.migrationSandbox);
30700
31362
  if (this.staticRegistry) {
30701
31363
  this.dynamicRegistry = createMergedRegistry(this.staticRegistry, dynamicOnly);
30702
31364
  } else {
30703
31365
  this.dynamicRegistry = dynamicOnly;
30704
31366
  }
30705
31367
  }
31368
+ /**
31369
+ * Serialize migration steps for storage in Firestore.
31370
+ * Function objects are converted via `.toString()`; strings are stored as-is.
31371
+ * Each migration is validated at define-time by pre-compiling in the sandbox.
31372
+ */
31373
+ async serializeMigrations(migrations) {
31374
+ const result = migrations.map((m) => {
31375
+ const source = typeof m.up === "function" ? m.up.toString() : m.up;
31376
+ return { fromVersion: m.fromVersion, toVersion: m.toVersion, up: source };
31377
+ });
31378
+ await Promise.all(
31379
+ result.map((m) => precompileSource(m.up, this.migrationSandbox))
31380
+ );
31381
+ return result;
31382
+ }
30706
31383
  /**
30707
31384
  * Create a GraphReader for the meta-collection.
30708
31385
  * If meta-collection is the same as main collection, returns `this`.
@@ -30860,11 +31537,39 @@ function findViewsFile(dir) {
30860
31537
  }
30861
31538
  return void 0;
30862
31539
  }
31540
+ var MIGRATION_EXTENSIONS = [".ts", ".js", ".mts", ".mjs"];
31541
+ function findMigrationsFile(dir) {
31542
+ for (const ext of MIGRATION_EXTENSIONS) {
31543
+ const candidate = join(dir, `migrations${ext}`);
31544
+ if (existsSync(candidate)) return candidate;
31545
+ }
31546
+ return void 0;
31547
+ }
31548
+ function loadMigrations(filePath, entityLabel) {
31549
+ try {
31550
+ const jiti2 = getJiti();
31551
+ const mod = jiti2(filePath);
31552
+ const migrations = mod && typeof mod === "object" && "default" in mod ? mod.default : mod;
31553
+ if (!Array.isArray(migrations)) {
31554
+ throw new DiscoveryError(
31555
+ `Migrations file ${filePath} for ${entityLabel} must default-export an array of MigrationStep.`
31556
+ );
31557
+ }
31558
+ return migrations;
31559
+ } catch (err) {
31560
+ if (err instanceof DiscoveryError) throw err;
31561
+ throw new DiscoveryError(
31562
+ `Failed to load migrations ${filePath} for ${entityLabel}: ${err.message}`
31563
+ );
31564
+ }
31565
+ }
30863
31566
  function loadNodeEntity(dir, name) {
30864
31567
  const schema = loadSchema(dir, `node type "${name}"`);
30865
31568
  const meta3 = readJsonIfExists(join(dir, "meta.json"));
30866
31569
  const sampleData = readJsonIfExists(join(dir, "sample.json"));
30867
31570
  const viewsPath = findViewsFile(dir);
31571
+ const migrationsPath = findMigrationsFile(dir);
31572
+ const migrations = migrationsPath ? loadMigrations(migrationsPath, `node type "${name}"`) : void 0;
30868
31573
  return {
30869
31574
  kind: "node",
30870
31575
  name,
@@ -30875,7 +31580,9 @@ function loadNodeEntity(dir, name) {
30875
31580
  viewDefaults: meta3?.viewDefaults,
30876
31581
  viewsPath,
30877
31582
  sampleData,
30878
- allowedIn: meta3?.allowedIn
31583
+ allowedIn: meta3?.allowedIn,
31584
+ migrations,
31585
+ migrationWriteBack: meta3?.migrationWriteBack
30879
31586
  };
30880
31587
  }
30881
31588
  function loadEdgeEntity(dir, name) {
@@ -30900,6 +31607,8 @@ function loadEdgeEntity(dir, name) {
30900
31607
  const meta3 = readJsonIfExists(join(dir, "meta.json"));
30901
31608
  const sampleData = readJsonIfExists(join(dir, "sample.json"));
30902
31609
  const viewsPath = findViewsFile(dir);
31610
+ const migrationsPath = findMigrationsFile(dir);
31611
+ const migrations = migrationsPath ? loadMigrations(migrationsPath, `edge type "${name}"`) : void 0;
30903
31612
  return {
30904
31613
  kind: "edge",
30905
31614
  name,
@@ -30912,7 +31621,9 @@ function loadEdgeEntity(dir, name) {
30912
31621
  viewsPath,
30913
31622
  sampleData,
30914
31623
  allowedIn: meta3?.allowedIn,
30915
- targetGraph: topology.targetGraph ?? meta3?.targetGraph
31624
+ targetGraph: topology.targetGraph ?? meta3?.targetGraph,
31625
+ migrations,
31626
+ migrationWriteBack: meta3?.migrationWriteBack
30916
31627
  };
30917
31628
  }
30918
31629
  function getSubdirectories(dir) {
@@ -34271,7 +34982,7 @@ function createExpressMiddleware(opts) {
34271
34982
  }
34272
34983
 
34273
34984
  // editor/server/trpc.ts
34274
- import { Timestamp } from "@google-cloud/firestore";
34985
+ import { Timestamp as Timestamp2 } from "@google-cloud/firestore";
34275
34986
 
34276
34987
  // editor/node_modules/.pnpm/zod@4.3.6/node_modules/zod/v4/classic/external.js
34277
34988
  var external_exports = {};
@@ -48070,7 +48781,7 @@ var NODE_RELATION2 = "is";
48070
48781
  function serializeRecord(doc) {
48071
48782
  const result = {};
48072
48783
  for (const [key, value] of Object.entries(doc)) {
48073
- if (value instanceof Timestamp) {
48784
+ if (value instanceof Timestamp2) {
48074
48785
  result[key] = value.toDate().toISOString();
48075
48786
  } else if (value && typeof value === "object" && !Array.isArray(value)) {
48076
48787
  result[key] = serializeRecord(value);
@@ -48234,7 +48945,7 @@ var appRouter = t.router({
48234
48945
  if (hasMore && docs.length > 0) {
48235
48946
  const lastDoc = docs[docs.length - 1].data();
48236
48947
  const cursorValue = lastDoc[effectiveSortBy];
48237
- nextCursor = cursorValue instanceof Timestamp ? cursorValue.toDate().toISOString() : String(cursorValue);
48948
+ nextCursor = cursorValue instanceof Timestamp2 ? cursorValue.toDate().toISOString() : String(cursorValue);
48238
48949
  }
48239
48950
  return { nodes, hasMore, nextCursor };
48240
48951
  }),
@@ -48320,7 +49031,7 @@ var appRouter = t.router({
48320
49031
  if (hasMore && docs.length > 0) {
48321
49032
  const lastDoc = docs[docs.length - 1].data();
48322
49033
  const cursorValue = effectiveSortBy.startsWith("data.") ? effectiveSortBy.split(".").reduce((obj, key) => obj?.[key], lastDoc) : lastDoc[effectiveSortBy];
48323
- nextCursor = cursorValue instanceof Timestamp ? cursorValue.toDate().toISOString() : String(cursorValue);
49034
+ nextCursor = cursorValue instanceof Timestamp2 ? cursorValue.toDate().toISOString() : String(cursorValue);
48324
49035
  }
48325
49036
  return { edges, hasMore, nextCursor };
48326
49037
  }),