@typicalday/firegraph 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  } from "./chunk-KFA7G37W.js";
8
8
 
9
9
  // src/client.ts
10
- import { FieldValue as FieldValue4 } from "@google-cloud/firestore";
10
+ import { FieldValue as FieldValue5 } from "@google-cloud/firestore";
11
11
 
12
12
  // src/docid.ts
13
13
  import { createHash } from "crypto";
@@ -135,6 +135,12 @@ var RegistryScopeError = class extends FiregraphError {
135
135
  this.name = "RegistryScopeError";
136
136
  }
137
137
  };
138
+ var MigrationError = class extends FiregraphError {
139
+ constructor(message) {
140
+ super(message, "MIGRATION_ERROR");
141
+ this.name = "MigrationError";
142
+ }
143
+ };
138
144
 
139
145
  // src/query.ts
140
146
  function buildEdgeQueryPlan(params) {
@@ -325,7 +331,7 @@ function createPipelineQueryAdapter(db, collectionPath) {
325
331
  }
326
332
 
327
333
  // src/transaction.ts
328
- import { FieldValue as FieldValue2 } from "@google-cloud/firestore";
334
+ import { FieldValue as FieldValue3 } from "@google-cloud/firestore";
329
335
 
330
336
  // src/query-safety.ts
331
337
  var SAFE_INDEX_PATTERNS = [
@@ -375,24 +381,248 @@ function analyzeQuerySafety(filters) {
375
381
  };
376
382
  }
377
383
 
384
+ // src/serialization.ts
385
+ import { Timestamp, GeoPoint, FieldValue as FieldValue2 } from "@google-cloud/firestore";
386
+ var SERIALIZATION_TAG = "__firegraph_ser__";
387
+ var KNOWN_TYPES = /* @__PURE__ */ new Set(["Timestamp", "GeoPoint", "VectorValue", "DocumentReference"]);
388
+ var _docRefWarned = false;
389
+ function isTaggedValue(value) {
390
+ if (value === null || typeof value !== "object") return false;
391
+ const tag = value[SERIALIZATION_TAG];
392
+ return typeof tag === "string" && KNOWN_TYPES.has(tag);
393
+ }
394
+ function isTimestamp(value) {
395
+ return value instanceof Timestamp;
396
+ }
397
+ function isGeoPoint(value) {
398
+ return value instanceof GeoPoint;
399
+ }
400
+ function isDocumentReference(value) {
401
+ if (value === null || typeof value !== "object") return false;
402
+ const v = value;
403
+ return typeof v.path === "string" && v.firestore !== void 0 && typeof v.id === "string" && v.constructor?.name === "DocumentReference";
404
+ }
405
+ function isVectorValue(value) {
406
+ if (value === null || typeof value !== "object") return false;
407
+ const v = value;
408
+ return v.constructor?.name === "VectorValue" && Array.isArray(v._values);
409
+ }
410
+ function serializeFirestoreTypes(data) {
411
+ return serializeValue(data);
412
+ }
413
+ function serializeValue(value) {
414
+ if (value === null || value === void 0) return value;
415
+ if (typeof value !== "object") return value;
416
+ if (isTimestamp(value)) {
417
+ return { [SERIALIZATION_TAG]: "Timestamp", seconds: value.seconds, nanoseconds: value.nanoseconds };
418
+ }
419
+ if (isGeoPoint(value)) {
420
+ return { [SERIALIZATION_TAG]: "GeoPoint", latitude: value.latitude, longitude: value.longitude };
421
+ }
422
+ if (isDocumentReference(value)) {
423
+ return { [SERIALIZATION_TAG]: "DocumentReference", path: value.path };
424
+ }
425
+ if (isVectorValue(value)) {
426
+ const v = value;
427
+ const values = typeof v.toArray === "function" ? v.toArray() : v._values;
428
+ return { [SERIALIZATION_TAG]: "VectorValue", values: [...values] };
429
+ }
430
+ if (Array.isArray(value)) {
431
+ return value.map(serializeValue);
432
+ }
433
+ const result = {};
434
+ for (const key of Object.keys(value)) {
435
+ result[key] = serializeValue(value[key]);
436
+ }
437
+ return result;
438
+ }
439
+ function deserializeFirestoreTypes(data, db) {
440
+ return deserializeValue(data, db);
441
+ }
442
+ function deserializeValue(value, db) {
443
+ if (value === null || value === void 0) return value;
444
+ if (typeof value !== "object") return value;
445
+ if (isTimestamp(value) || isGeoPoint(value) || isDocumentReference(value) || isVectorValue(value)) {
446
+ return value;
447
+ }
448
+ if (Array.isArray(value)) {
449
+ return value.map((v) => deserializeValue(v, db));
450
+ }
451
+ const obj = value;
452
+ if (isTaggedValue(obj)) {
453
+ const tag = obj[SERIALIZATION_TAG];
454
+ switch (tag) {
455
+ case "Timestamp":
456
+ if (typeof obj.seconds !== "number" || typeof obj.nanoseconds !== "number") return obj;
457
+ return new Timestamp(obj.seconds, obj.nanoseconds);
458
+ case "GeoPoint":
459
+ if (typeof obj.latitude !== "number" || typeof obj.longitude !== "number") return obj;
460
+ return new GeoPoint(obj.latitude, obj.longitude);
461
+ case "VectorValue":
462
+ if (!Array.isArray(obj.values)) return obj;
463
+ return FieldValue2.vector(obj.values);
464
+ case "DocumentReference":
465
+ if (typeof obj.path !== "string") return obj;
466
+ if (db) {
467
+ return db.doc(obj.path);
468
+ }
469
+ if (!_docRefWarned) {
470
+ _docRefWarned = true;
471
+ console.warn(
472
+ "[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."
473
+ );
474
+ }
475
+ return obj;
476
+ default:
477
+ return obj;
478
+ }
479
+ }
480
+ const result = {};
481
+ for (const key of Object.keys(obj)) {
482
+ result[key] = deserializeValue(obj[key], db);
483
+ }
484
+ return result;
485
+ }
486
+
487
+ // src/migration.ts
488
+ async function applyMigrationChain(data, currentVersion, targetVersion, migrations) {
489
+ const sorted = [...migrations].sort((a, b) => a.fromVersion - b.fromVersion);
490
+ let result = { ...data };
491
+ let version = currentVersion;
492
+ for (const step of sorted) {
493
+ if (step.fromVersion === version) {
494
+ try {
495
+ result = await step.up(result);
496
+ } catch (err) {
497
+ if (err instanceof MigrationError) throw err;
498
+ throw new MigrationError(
499
+ `Migration from v${step.fromVersion} to v${step.toVersion} failed: ${err.message}`
500
+ );
501
+ }
502
+ if (!result || typeof result !== "object") {
503
+ throw new MigrationError(
504
+ `Migration from v${step.fromVersion} to v${step.toVersion} returned invalid data (expected object)`
505
+ );
506
+ }
507
+ version = step.toVersion;
508
+ }
509
+ }
510
+ if (version !== targetVersion) {
511
+ throw new MigrationError(
512
+ `Incomplete migration chain: reached v${version} but target is v${targetVersion}`
513
+ );
514
+ }
515
+ return result;
516
+ }
517
+ function validateMigrationChain(migrations, label) {
518
+ if (migrations.length === 0) return;
519
+ const seen = /* @__PURE__ */ new Set();
520
+ for (const step of migrations) {
521
+ if (step.toVersion <= step.fromVersion) {
522
+ throw new MigrationError(
523
+ `${label}: migration step has toVersion (${step.toVersion}) <= fromVersion (${step.fromVersion})`
524
+ );
525
+ }
526
+ if (seen.has(step.fromVersion)) {
527
+ throw new MigrationError(
528
+ `${label}: duplicate migration step for fromVersion ${step.fromVersion}`
529
+ );
530
+ }
531
+ seen.add(step.fromVersion);
532
+ }
533
+ const sorted = [...migrations].sort((a, b) => a.fromVersion - b.fromVersion);
534
+ const targetVersion = Math.max(...migrations.map((m) => m.toVersion));
535
+ let version = 0;
536
+ for (const step of sorted) {
537
+ if (step.fromVersion === version) {
538
+ version = step.toVersion;
539
+ } else if (step.fromVersion > version) {
540
+ throw new MigrationError(
541
+ `${label}: migration chain has a gap \u2014 no step covers v${version} \u2192 v${step.fromVersion}`
542
+ );
543
+ }
544
+ }
545
+ if (version !== targetVersion) {
546
+ throw new MigrationError(
547
+ `${label}: migration chain does not reach v${targetVersion} (stuck at v${version})`
548
+ );
549
+ }
550
+ }
551
+ async function migrateRecord(record, registry, globalWriteBack = "off") {
552
+ const entry = registry.lookup(record.aType, record.axbType, record.bType);
553
+ if (!entry?.migrations?.length || !entry.schemaVersion) {
554
+ return { record, migrated: false, writeBack: "off" };
555
+ }
556
+ const currentVersion = record.v ?? 0;
557
+ if (currentVersion >= entry.schemaVersion) {
558
+ return { record, migrated: false, writeBack: "off" };
559
+ }
560
+ const migratedData = await applyMigrationChain(
561
+ record.data,
562
+ currentVersion,
563
+ entry.schemaVersion,
564
+ entry.migrations
565
+ );
566
+ const writeBack = entry.migrationWriteBack ?? globalWriteBack ?? "off";
567
+ return {
568
+ record: { ...record, data: migratedData, v: entry.schemaVersion },
569
+ migrated: true,
570
+ writeBack
571
+ };
572
+ }
573
+ async function migrateRecords(records, registry, globalWriteBack = "off") {
574
+ return Promise.all(
575
+ records.map((r) => migrateRecord(r, registry, globalWriteBack))
576
+ );
577
+ }
578
+
378
579
  // src/transaction.ts
379
580
  var GraphTransactionImpl = class {
380
- constructor(adapter, registry, scanProtection = "error", scopePath = "") {
581
+ constructor(adapter, registry, scanProtection = "error", scopePath = "", globalWriteBack = "off", db) {
381
582
  this.adapter = adapter;
382
583
  this.registry = registry;
383
584
  this.scanProtection = scanProtection;
384
585
  this.scopePath = scopePath;
586
+ this.globalWriteBack = globalWriteBack;
587
+ this.db = db;
385
588
  }
386
589
  async getNode(uid) {
387
590
  const docId = computeNodeDocId(uid);
388
- return this.adapter.getDoc(docId);
591
+ const record = await this.adapter.getDoc(docId);
592
+ if (!record || !this.registry) return record;
593
+ const result = await migrateRecord(record, this.registry, this.globalWriteBack);
594
+ if (result.migrated && result.writeBack !== "off") {
595
+ const update = {
596
+ data: deserializeFirestoreTypes(result.record.data, this.db),
597
+ updatedAt: FieldValue3.serverTimestamp()
598
+ };
599
+ if (result.record.v !== void 0) {
600
+ update.v = result.record.v;
601
+ }
602
+ this.adapter.updateDoc(docId, update);
603
+ }
604
+ return result.record;
389
605
  }
390
606
  async getEdge(aUid, axbType, bUid) {
391
607
  const docId = computeEdgeDocId(aUid, axbType, bUid);
392
- return this.adapter.getDoc(docId);
608
+ const record = await this.adapter.getDoc(docId);
609
+ if (!record || !this.registry) return record;
610
+ const result = await migrateRecord(record, this.registry, this.globalWriteBack);
611
+ if (result.migrated && result.writeBack !== "off") {
612
+ const update = {
613
+ data: deserializeFirestoreTypes(result.record.data, this.db),
614
+ updatedAt: FieldValue3.serverTimestamp()
615
+ };
616
+ if (result.record.v !== void 0) {
617
+ update.v = result.record.v;
618
+ }
619
+ this.adapter.updateDoc(docId, update);
620
+ }
621
+ return result.record;
393
622
  }
394
623
  async edgeExists(aUid, axbType, bUid) {
395
- const record = await this.getEdge(aUid, axbType, bUid);
624
+ const docId = computeEdgeDocId(aUid, axbType, bUid);
625
+ const record = await this.adapter.getDoc(docId);
396
626
  return record !== null;
397
627
  }
398
628
  checkQuerySafety(filters, allowCollectionScan) {
@@ -406,21 +636,45 @@ var GraphTransactionImpl = class {
406
636
  }
407
637
  async findEdges(params) {
408
638
  const plan = buildEdgeQueryPlan(params);
639
+ let records;
409
640
  if (plan.strategy === "get") {
410
641
  const record = await this.adapter.getDoc(plan.docId);
411
- return record ? [record] : [];
642
+ records = record ? [record] : [];
643
+ } else {
644
+ this.checkQuerySafety(plan.filters, params.allowCollectionScan);
645
+ records = await this.adapter.query(plan.filters, plan.options);
412
646
  }
413
- this.checkQuerySafety(plan.filters, params.allowCollectionScan);
414
- return this.adapter.query(plan.filters, plan.options);
647
+ return this.applyMigrations(records);
415
648
  }
416
649
  async findNodes(params) {
417
650
  const plan = buildNodeQueryPlan(params);
651
+ let records;
418
652
  if (plan.strategy === "get") {
419
653
  const record = await this.adapter.getDoc(plan.docId);
420
- return record ? [record] : [];
654
+ records = record ? [record] : [];
655
+ } else {
656
+ this.checkQuerySafety(plan.filters, params.allowCollectionScan);
657
+ records = await this.adapter.query(plan.filters, plan.options);
658
+ }
659
+ return this.applyMigrations(records);
660
+ }
661
+ async applyMigrations(records) {
662
+ if (!this.registry || records.length === 0) return records;
663
+ const results = await migrateRecords(records, this.registry, this.globalWriteBack);
664
+ for (const result of results) {
665
+ if (result.migrated && result.writeBack !== "off") {
666
+ const docId = result.record.axbType === NODE_RELATION ? computeNodeDocId(result.record.aUid) : computeEdgeDocId(result.record.aUid, result.record.axbType, result.record.bUid);
667
+ const update = {
668
+ data: deserializeFirestoreTypes(result.record.data, this.db),
669
+ updatedAt: FieldValue3.serverTimestamp()
670
+ };
671
+ if (result.record.v !== void 0) {
672
+ update.v = result.record.v;
673
+ }
674
+ this.adapter.updateDoc(docId, update);
675
+ }
421
676
  }
422
- this.checkQuerySafety(plan.filters, params.allowCollectionScan);
423
- return this.adapter.query(plan.filters, plan.options);
677
+ return results.map((r) => r.record);
424
678
  }
425
679
  async putNode(aType, uid, data) {
426
680
  if (this.registry) {
@@ -428,6 +682,12 @@ var GraphTransactionImpl = class {
428
682
  }
429
683
  const docId = computeNodeDocId(uid);
430
684
  const record = buildNodeRecord(aType, uid, data);
685
+ if (this.registry) {
686
+ const entry = this.registry.lookup(aType, NODE_RELATION, aType);
687
+ if (entry?.schemaVersion && entry.schemaVersion > 0) {
688
+ record.v = entry.schemaVersion;
689
+ }
690
+ }
431
691
  this.adapter.setDoc(docId, record);
432
692
  }
433
693
  async putEdge(aType, aUid, axbType, bType, bUid, data) {
@@ -436,13 +696,19 @@ var GraphTransactionImpl = class {
436
696
  }
437
697
  const docId = computeEdgeDocId(aUid, axbType, bUid);
438
698
  const record = buildEdgeRecord(aType, aUid, axbType, bType, bUid, data);
699
+ if (this.registry) {
700
+ const entry = this.registry.lookup(aType, axbType, bType);
701
+ if (entry?.schemaVersion && entry.schemaVersion > 0) {
702
+ record.v = entry.schemaVersion;
703
+ }
704
+ }
439
705
  this.adapter.setDoc(docId, record);
440
706
  }
441
707
  async updateNode(uid, data) {
442
708
  const docId = computeNodeDocId(uid);
443
709
  this.adapter.updateDoc(docId, {
444
710
  ...data,
445
- updatedAt: FieldValue2.serverTimestamp()
711
+ updatedAt: FieldValue3.serverTimestamp()
446
712
  });
447
713
  }
448
714
  async removeNode(uid) {
@@ -456,7 +722,7 @@ var GraphTransactionImpl = class {
456
722
  };
457
723
 
458
724
  // src/batch.ts
459
- import { FieldValue as FieldValue3 } from "@google-cloud/firestore";
725
+ import { FieldValue as FieldValue4 } from "@google-cloud/firestore";
460
726
  var GraphBatchImpl = class {
461
727
  constructor(adapter, registry, scopePath = "") {
462
728
  this.adapter = adapter;
@@ -469,6 +735,12 @@ var GraphBatchImpl = class {
469
735
  }
470
736
  const docId = computeNodeDocId(uid);
471
737
  const record = buildNodeRecord(aType, uid, data);
738
+ if (this.registry) {
739
+ const entry = this.registry.lookup(aType, NODE_RELATION, aType);
740
+ if (entry?.schemaVersion && entry.schemaVersion > 0) {
741
+ record.v = entry.schemaVersion;
742
+ }
743
+ }
472
744
  this.adapter.setDoc(docId, record);
473
745
  }
474
746
  async putEdge(aType, aUid, axbType, bType, bUid, data) {
@@ -477,13 +749,19 @@ var GraphBatchImpl = class {
477
749
  }
478
750
  const docId = computeEdgeDocId(aUid, axbType, bUid);
479
751
  const record = buildEdgeRecord(aType, aUid, axbType, bType, bUid, data);
752
+ if (this.registry) {
753
+ const entry = this.registry.lookup(aType, axbType, bType);
754
+ if (entry?.schemaVersion && entry.schemaVersion > 0) {
755
+ record.v = entry.schemaVersion;
756
+ }
757
+ }
480
758
  this.adapter.setDoc(docId, record);
481
759
  }
482
760
  async updateNode(uid, data) {
483
761
  const docId = computeNodeDocId(uid);
484
762
  this.adapter.updateDoc(docId, {
485
763
  ...data,
486
- updatedAt: FieldValue3.serverTimestamp()
764
+ updatedAt: FieldValue4.serverTimestamp()
487
765
  });
488
766
  }
489
767
  async removeNode(uid) {
@@ -642,7 +920,7 @@ async function removeNodeCascade(db, collectionPath, reader, uid, options) {
642
920
  }
643
921
 
644
922
  // src/dynamic-registry.ts
645
- import { createHash as createHash2 } from "crypto";
923
+ import { createHash as createHash3 } from "crypto";
646
924
 
647
925
  // src/json-schema.ts
648
926
  import Ajv from "ajv";
@@ -791,6 +1069,13 @@ function createRegistry(input) {
791
1069
  `Entry (${entry.aType}) -[${entry.axbType}]-> (${entry.bType}) has invalid targetGraph "${entry.targetGraph}" \u2014 must be a single segment (no "/")`
792
1070
  );
793
1071
  }
1072
+ if (entry.migrations?.length) {
1073
+ const label = `Entry (${entry.aType}) -[${entry.axbType}]-> (${entry.bType})`;
1074
+ validateMigrationChain(entry.migrations, label);
1075
+ entry.schemaVersion = Math.max(...entry.migrations.map((m) => m.toVersion));
1076
+ } else {
1077
+ entry.schemaVersion = void 0;
1078
+ }
794
1079
  const key = tripleKey(entry.aType, entry.axbType, entry.bType);
795
1080
  const validator = entry.jsonSchema ? compileSchema(entry.jsonSchema, `(${entry.aType}) -[${entry.axbType}]-> (${entry.bType})`) : void 0;
796
1081
  map.set(key, { entry, validate: validator });
@@ -892,7 +1177,9 @@ function discoveryToEntries(discovery) {
892
1177
  description: entity.description,
893
1178
  titleField: entity.titleField,
894
1179
  subtitleField: entity.subtitleField,
895
- allowedIn: entity.allowedIn
1180
+ allowedIn: entity.allowedIn,
1181
+ migrations: entity.migrations,
1182
+ migrationWriteBack: entity.migrationWriteBack
896
1183
  });
897
1184
  }
898
1185
  for (const [axbType, entity] of discovery.edges) {
@@ -918,7 +1205,9 @@ function discoveryToEntries(discovery) {
918
1205
  titleField: entity.titleField,
919
1206
  subtitleField: entity.subtitleField,
920
1207
  allowedIn: entity.allowedIn,
921
- targetGraph: resolvedTargetGraph
1208
+ targetGraph: resolvedTargetGraph,
1209
+ migrations: entity.migrations,
1210
+ migrationWriteBack: entity.migrationWriteBack
922
1211
  });
923
1212
  }
924
1213
  }
@@ -926,9 +1215,258 @@ function discoveryToEntries(discovery) {
926
1215
  return entries;
927
1216
  }
928
1217
 
1218
+ // src/sandbox.ts
1219
+ import { Worker } from "worker_threads";
1220
+ import { createHash as createHash2 } from "crypto";
1221
+ var _worker = null;
1222
+ var _requestId = 0;
1223
+ var _pending = /* @__PURE__ */ new Map();
1224
+ var WORKER_SOURCE = [
1225
+ `'use strict';`,
1226
+ `var _wt = require('node:worker_threads');`,
1227
+ `var _mod = require('node:module');`,
1228
+ `var _crypto = require('node:crypto');`,
1229
+ `var parentPort = _wt.parentPort;`,
1230
+ `var workerData = _wt.workerData;`,
1231
+ ``,
1232
+ `// Load SES using the parent module's resolution context`,
1233
+ `var esmRequire = _mod.createRequire(workerData.parentUrl);`,
1234
+ `esmRequire('ses');`,
1235
+ ``,
1236
+ `lockdown({`,
1237
+ ` errorTaming: 'unsafe',`,
1238
+ ` consoleTaming: 'unsafe',`,
1239
+ ` evalTaming: 'safe-eval',`,
1240
+ ` overrideTaming: 'moderate',`,
1241
+ ` stackFiltering: 'verbose'`,
1242
+ `});`,
1243
+ ``,
1244
+ `// Defense-in-depth: verify lockdown() actually hardened JSON.`,
1245
+ `if (!Object.isFrozen(JSON)) {`,
1246
+ ` throw new Error('SES lockdown failed: JSON is not frozen');`,
1247
+ `}`,
1248
+ ``,
1249
+ `var cache = new Map();`,
1250
+ ``,
1251
+ `function hashSource(s) {`,
1252
+ ` return _crypto.createHash('sha256').update(s).digest('hex');`,
1253
+ `}`,
1254
+ ``,
1255
+ `function buildWrapper(source) {`,
1256
+ ` return '(function() {' +`,
1257
+ ` ' var fn = (' + source + ');\\n' +`,
1258
+ ` ' if (typeof fn !== "function") return null;\\n' +`,
1259
+ ` ' return function(jsonIn) {\\n' +`,
1260
+ ` ' var data = JSON.parse(jsonIn);\\n' +`,
1261
+ ` ' var result = fn(data);\\n' +`,
1262
+ ` ' if (result !== null && typeof result === "object" && typeof result.then === "function") {\\n' +`,
1263
+ ` ' return result.then(function(r) { return JSON.stringify(r); });\\n' +`,
1264
+ ` ' }\\n' +`,
1265
+ ` ' return JSON.stringify(result);\\n' +`,
1266
+ ` ' };\\n' +`,
1267
+ ` '})()';`,
1268
+ `}`,
1269
+ ``,
1270
+ `function compileSource(source) {`,
1271
+ ` var key = hashSource(source);`,
1272
+ ` var cached = cache.get(key);`,
1273
+ ` if (cached) return cached;`,
1274
+ ``,
1275
+ ` var compartmentFn;`,
1276
+ ` try {`,
1277
+ ` var c = new Compartment({ JSON: JSON });`,
1278
+ ` compartmentFn = c.evaluate(buildWrapper(source));`,
1279
+ ` } catch (err) {`,
1280
+ ` throw new Error('Failed to compile migration source: ' + (err.message || String(err)));`,
1281
+ ` }`,
1282
+ ``,
1283
+ ` if (typeof compartmentFn !== 'function') {`,
1284
+ ` throw new Error('Migration source did not produce a function: ' + source.slice(0, 80));`,
1285
+ ` }`,
1286
+ ``,
1287
+ ` cache.set(key, compartmentFn);`,
1288
+ ` return compartmentFn;`,
1289
+ `}`,
1290
+ ``,
1291
+ `parentPort.on('message', function(msg) {`,
1292
+ ` var id = msg.id;`,
1293
+ ` try {`,
1294
+ ` if (msg.type === 'compile') {`,
1295
+ ` compileSource(msg.source);`,
1296
+ ` parentPort.postMessage({ id: id, type: 'compiled' });`,
1297
+ ` return;`,
1298
+ ` }`,
1299
+ ` if (msg.type === 'execute') {`,
1300
+ ` var fn = compileSource(msg.source);`,
1301
+ ` var raw;`,
1302
+ ` try {`,
1303
+ ` raw = fn(msg.jsonData);`,
1304
+ ` } catch (err) {`,
1305
+ ` parentPort.postMessage({ id: id, type: 'error', message: 'Migration function threw: ' + (err.message || String(err)) });`,
1306
+ ` return;`,
1307
+ ` }`,
1308
+ ` if (raw !== null && typeof raw === 'object' && typeof raw.then === 'function') {`,
1309
+ ` raw.then(`,
1310
+ ` function(jsonResult) {`,
1311
+ ` if (jsonResult === undefined || jsonResult === null) {`,
1312
+ ` parentPort.postMessage({ id: id, type: 'error', message: 'Migration returned a non-JSON-serializable value' });`,
1313
+ ` } else {`,
1314
+ ` parentPort.postMessage({ id: id, type: 'result', jsonResult: jsonResult });`,
1315
+ ` }`,
1316
+ ` },`,
1317
+ ` function(err) {`,
1318
+ ` parentPort.postMessage({ id: id, type: 'error', message: 'Async migration function threw: ' + (err.message || String(err)) });`,
1319
+ ` }`,
1320
+ ` );`,
1321
+ ` return;`,
1322
+ ` }`,
1323
+ ` if (raw === undefined || raw === null) {`,
1324
+ ` parentPort.postMessage({ id: id, type: 'error', message: 'Migration returned a non-JSON-serializable value' });`,
1325
+ ` } else {`,
1326
+ ` parentPort.postMessage({ id: id, type: 'result', jsonResult: raw });`,
1327
+ ` }`,
1328
+ ` }`,
1329
+ ` } catch (err) {`,
1330
+ ` parentPort.postMessage({ id: id, type: 'error', message: err.message || String(err) });`,
1331
+ ` }`,
1332
+ `});`
1333
+ ].join("\n");
1334
+ function ensureWorker() {
1335
+ if (_worker) return _worker;
1336
+ _worker = new Worker(WORKER_SOURCE, {
1337
+ eval: true,
1338
+ workerData: { parentUrl: import.meta.url }
1339
+ });
1340
+ _worker.unref();
1341
+ _worker.on("message", (msg) => {
1342
+ if (msg.id === void 0) return;
1343
+ const pending = _pending.get(msg.id);
1344
+ if (!pending) return;
1345
+ _pending.delete(msg.id);
1346
+ if (msg.type === "error") {
1347
+ pending.reject(new MigrationError(msg.message ?? "Unknown sandbox error"));
1348
+ } else {
1349
+ pending.resolve(msg);
1350
+ }
1351
+ });
1352
+ _worker.on("error", (err) => {
1353
+ for (const [, p] of _pending) {
1354
+ p.reject(new MigrationError(`Sandbox worker error: ${err.message}`));
1355
+ }
1356
+ _pending.clear();
1357
+ _worker = null;
1358
+ });
1359
+ _worker.on("exit", (code) => {
1360
+ if (_pending.size > 0) {
1361
+ for (const [, p] of _pending) {
1362
+ p.reject(new MigrationError(`Sandbox worker exited with code ${code}`));
1363
+ }
1364
+ _pending.clear();
1365
+ }
1366
+ _worker = null;
1367
+ });
1368
+ return _worker;
1369
+ }
1370
+ function sendToWorker(msg) {
1371
+ const worker = ensureWorker();
1372
+ if (_requestId >= Number.MAX_SAFE_INTEGER) _requestId = 0;
1373
+ const id = ++_requestId;
1374
+ return new Promise((resolve2, reject) => {
1375
+ _pending.set(id, { resolve: resolve2, reject });
1376
+ worker.postMessage({ ...msg, id });
1377
+ });
1378
+ }
1379
+ var compiledCache = /* @__PURE__ */ new WeakMap();
1380
+ function getExecutorCache(executor) {
1381
+ let cache = compiledCache.get(executor);
1382
+ if (!cache) {
1383
+ cache = /* @__PURE__ */ new Map();
1384
+ compiledCache.set(executor, cache);
1385
+ }
1386
+ return cache;
1387
+ }
1388
+ function hashSource(source) {
1389
+ return createHash2("sha256").update(source).digest("hex");
1390
+ }
1391
+ function defaultExecutor(source) {
1392
+ ensureWorker();
1393
+ return ((data) => {
1394
+ const jsonData = JSON.stringify(serializeFirestoreTypes(data));
1395
+ return sendToWorker({ type: "execute", source, jsonData }).then(
1396
+ (response) => {
1397
+ if (response.jsonResult === void 0 || response.jsonResult === null) {
1398
+ throw new MigrationError("Migration returned a non-JSON-serializable value");
1399
+ }
1400
+ try {
1401
+ return deserializeFirestoreTypes(JSON.parse(response.jsonResult));
1402
+ } catch {
1403
+ throw new MigrationError("Migration returned a non-JSON-serializable value");
1404
+ }
1405
+ }
1406
+ );
1407
+ });
1408
+ }
1409
+ async function precompileSource(source, executor) {
1410
+ if (executor && executor !== defaultExecutor) {
1411
+ try {
1412
+ executor(source);
1413
+ } catch (err) {
1414
+ if (err instanceof MigrationError) throw err;
1415
+ throw new MigrationError(
1416
+ `Failed to compile migration source: ${err.message}`
1417
+ );
1418
+ }
1419
+ return;
1420
+ }
1421
+ await sendToWorker({ type: "compile", source });
1422
+ }
1423
+ function compileMigrationFn(source, executor = defaultExecutor) {
1424
+ const cache = getExecutorCache(executor);
1425
+ const key = hashSource(source);
1426
+ const cached = cache.get(key);
1427
+ if (cached) return cached;
1428
+ try {
1429
+ const fn = executor(source);
1430
+ cache.set(key, fn);
1431
+ return fn;
1432
+ } catch (err) {
1433
+ if (err instanceof MigrationError) throw err;
1434
+ throw new MigrationError(
1435
+ `Failed to compile migration source: ${err.message}`
1436
+ );
1437
+ }
1438
+ }
1439
+ function compileMigrations(stored, executor) {
1440
+ return stored.map((step) => ({
1441
+ fromVersion: step.fromVersion,
1442
+ toVersion: step.toVersion,
1443
+ up: compileMigrationFn(step.up, executor)
1444
+ }));
1445
+ }
1446
+ async function destroySandboxWorker() {
1447
+ if (!_worker) return;
1448
+ const w = _worker;
1449
+ _worker = null;
1450
+ for (const [, p] of _pending) {
1451
+ p.reject(new MigrationError("Sandbox worker terminated"));
1452
+ }
1453
+ _pending.clear();
1454
+ await w.terminate();
1455
+ }
1456
+
929
1457
  // src/dynamic-registry.ts
930
1458
  var META_NODE_TYPE = "nodeType";
931
1459
  var META_EDGE_TYPE = "edgeType";
1460
+ var STORED_MIGRATION_STEP_SCHEMA = {
1461
+ type: "object",
1462
+ required: ["fromVersion", "toVersion", "up"],
1463
+ properties: {
1464
+ fromVersion: { type: "integer", minimum: 0 },
1465
+ toVersion: { type: "integer", minimum: 1 },
1466
+ up: { type: "string", minLength: 1 }
1467
+ },
1468
+ additionalProperties: false
1469
+ };
932
1470
  var NODE_TYPE_SCHEMA = {
933
1471
  type: "object",
934
1472
  required: ["name", "jsonSchema"],
@@ -940,7 +1478,10 @@ var NODE_TYPE_SCHEMA = {
940
1478
  subtitleField: { type: "string" },
941
1479
  viewTemplate: { type: "string" },
942
1480
  viewCss: { type: "string" },
943
- allowedIn: { type: "array", items: { type: "string", minLength: 1 } }
1481
+ allowedIn: { type: "array", items: { type: "string", minLength: 1 } },
1482
+ schemaVersion: { type: "integer", minimum: 0 },
1483
+ migrations: { type: "array", items: STORED_MIGRATION_STEP_SCHEMA },
1484
+ migrationWriteBack: { type: "string", enum: ["off", "eager", "background"] }
944
1485
  },
945
1486
  additionalProperties: false
946
1487
  };
@@ -969,7 +1510,10 @@ var EDGE_TYPE_SCHEMA = {
969
1510
  viewTemplate: { type: "string" },
970
1511
  viewCss: { type: "string" },
971
1512
  allowedIn: { type: "array", items: { type: "string", minLength: 1 } },
972
- targetGraph: { type: "string", minLength: 1, pattern: "^[^/]+$" }
1513
+ targetGraph: { type: "string", minLength: 1, pattern: "^[^/]+$" },
1514
+ schemaVersion: { type: "integer", minimum: 0 },
1515
+ migrations: { type: "array", items: STORED_MIGRATION_STEP_SCHEMA },
1516
+ migrationWriteBack: { type: "string", enum: ["off", "eager", "background"] }
973
1517
  },
974
1518
  additionalProperties: false
975
1519
  };
@@ -993,15 +1537,33 @@ function createBootstrapRegistry() {
993
1537
  return createRegistry([...BOOTSTRAP_ENTRIES]);
994
1538
  }
995
1539
  function generateDeterministicUid(metaType, name) {
996
- const hash = createHash2("sha256").update(`${metaType}:${name}`).digest("base64url");
1540
+ const hash = createHash3("sha256").update(`${metaType}:${name}`).digest("base64url");
997
1541
  return hash.slice(0, 21);
998
1542
  }
999
- async function createRegistryFromGraph(reader) {
1543
+ async function createRegistryFromGraph(reader, executor) {
1000
1544
  const [nodeTypes, edgeTypes] = await Promise.all([
1001
1545
  reader.findNodes({ aType: META_NODE_TYPE }),
1002
1546
  reader.findNodes({ aType: META_EDGE_TYPE })
1003
1547
  ]);
1004
1548
  const entries = [...BOOTSTRAP_ENTRIES];
1549
+ const prevalidations = [];
1550
+ for (const record of nodeTypes) {
1551
+ const data = record.data;
1552
+ if (data.migrations) {
1553
+ for (const m of data.migrations) {
1554
+ prevalidations.push(precompileSource(m.up, executor));
1555
+ }
1556
+ }
1557
+ }
1558
+ for (const record of edgeTypes) {
1559
+ const data = record.data;
1560
+ if (data.migrations) {
1561
+ for (const m of data.migrations) {
1562
+ prevalidations.push(precompileSource(m.up, executor));
1563
+ }
1564
+ }
1565
+ }
1566
+ await Promise.all(prevalidations);
1005
1567
  for (const record of nodeTypes) {
1006
1568
  const data = record.data;
1007
1569
  entries.push({
@@ -1012,13 +1574,16 @@ async function createRegistryFromGraph(reader) {
1012
1574
  description: data.description,
1013
1575
  titleField: data.titleField,
1014
1576
  subtitleField: data.subtitleField,
1015
- allowedIn: data.allowedIn
1577
+ allowedIn: data.allowedIn,
1578
+ migrations: data.migrations ? compileMigrations(data.migrations, executor) : void 0,
1579
+ migrationWriteBack: data.migrationWriteBack
1016
1580
  });
1017
1581
  }
1018
1582
  for (const record of edgeTypes) {
1019
1583
  const data = record.data;
1020
1584
  const fromTypes = Array.isArray(data.from) ? data.from : [data.from];
1021
1585
  const toTypes = Array.isArray(data.to) ? data.to : [data.to];
1586
+ const compiledMigrations = data.migrations ? compileMigrations(data.migrations, executor) : void 0;
1022
1587
  for (const aType of fromTypes) {
1023
1588
  for (const bType of toTypes) {
1024
1589
  entries.push({
@@ -1031,7 +1596,9 @@ async function createRegistryFromGraph(reader) {
1031
1596
  titleField: data.titleField,
1032
1597
  subtitleField: data.subtitleField,
1033
1598
  allowedIn: data.allowedIn,
1034
- targetGraph: data.targetGraph
1599
+ targetGraph: data.targetGraph,
1600
+ migrations: compiledMigrations,
1601
+ migrationWriteBack: data.migrationWriteBack
1035
1602
  });
1036
1603
  }
1037
1604
  }
@@ -1047,6 +1614,8 @@ var GraphClientImpl = class _GraphClientImpl {
1047
1614
  this.db = db;
1048
1615
  this.scopePath = scopePath;
1049
1616
  this.adapter = createFirestoreAdapter(db, collectionPath);
1617
+ this.globalWriteBack = options?.migrationWriteBack ?? "off";
1618
+ this.migrationSandbox = options?.migrationSandbox;
1050
1619
  if (options?.registryMode) {
1051
1620
  this.dynamicConfig = options.registryMode;
1052
1621
  this.bootstrapRegistry = createBootstrapRegistry();
@@ -1098,6 +1667,9 @@ var GraphClientImpl = class _GraphClientImpl {
1098
1667
  metaPipelineAdapter;
1099
1668
  // Subgraph scope tracking
1100
1669
  scopePath;
1670
+ // Migration settings
1671
+ globalWriteBack;
1672
+ migrationSandbox;
1101
1673
  // ---------------------------------------------------------------------------
1102
1674
  // Registry routing
1103
1675
  // ---------------------------------------------------------------------------
@@ -1167,37 +1739,114 @@ var GraphClientImpl = class _GraphClientImpl {
1167
1739
  console.warn(`[firegraph] Query safety warning: ${result.reason}`);
1168
1740
  }
1169
1741
  // ---------------------------------------------------------------------------
1742
+ // Migration helpers
1743
+ // ---------------------------------------------------------------------------
1744
+ /**
1745
+ * Apply migration to a single record. Returns the (possibly migrated)
1746
+ * record and triggers write-back if applicable.
1747
+ */
1748
+ async applyMigration(record, docId) {
1749
+ const registry = this.getCombinedRegistry();
1750
+ if (!registry) return record;
1751
+ const result = await migrateRecord(record, registry, this.globalWriteBack);
1752
+ if (result.migrated) {
1753
+ this.handleWriteBack(result, docId);
1754
+ }
1755
+ return result.record;
1756
+ }
1757
+ /**
1758
+ * Apply migrations to an array of records. Returns all records
1759
+ * (migrated where applicable) and triggers write-backs.
1760
+ */
1761
+ async applyMigrations(records) {
1762
+ const registry = this.getCombinedRegistry();
1763
+ if (!registry || records.length === 0) return records;
1764
+ const results = await migrateRecords(records, registry, this.globalWriteBack);
1765
+ for (const result of results) {
1766
+ if (result.migrated) {
1767
+ const docId = result.record.axbType === NODE_RELATION ? computeNodeDocId(result.record.aUid) : computeEdgeDocId(result.record.aUid, result.record.axbType, result.record.bUid);
1768
+ this.handleWriteBack(result, docId);
1769
+ }
1770
+ }
1771
+ return results.map((r) => r.record);
1772
+ }
1773
+ /**
1774
+ * Handle write-back for a migrated record based on the resolved mode.
1775
+ *
1776
+ * Both `'eager'` and `'background'` are fire-and-forget (not awaited by
1777
+ * the caller). The difference is logging level on failure:
1778
+ * - `eager`: logs an error via `console.error`
1779
+ * - `background`: logs a warning via `console.warn`
1780
+ *
1781
+ * For truly synchronous write-back guarantees, use transactions — the
1782
+ * `GraphTransactionImpl` performs write-back inline within the transaction.
1783
+ */
1784
+ handleWriteBack(result, docId) {
1785
+ if (result.writeBack === "off") return;
1786
+ const doWriteBack = async () => {
1787
+ try {
1788
+ const update = {
1789
+ data: deserializeFirestoreTypes(result.record.data, this.db),
1790
+ updatedAt: FieldValue5.serverTimestamp()
1791
+ };
1792
+ if (result.record.v !== void 0) {
1793
+ update.v = result.record.v;
1794
+ }
1795
+ await this.adapter.updateDoc(docId, update);
1796
+ } catch (err) {
1797
+ const msg = `[firegraph] Migration write-back failed for ${docId}: ${err.message}`;
1798
+ if (result.writeBack === "eager") {
1799
+ console.error(msg);
1800
+ } else {
1801
+ console.warn(msg);
1802
+ }
1803
+ }
1804
+ };
1805
+ void doWriteBack();
1806
+ }
1807
+ // ---------------------------------------------------------------------------
1170
1808
  // GraphReader
1171
1809
  // ---------------------------------------------------------------------------
1172
1810
  async getNode(uid) {
1173
1811
  const docId = computeNodeDocId(uid);
1174
- return this.adapter.getDoc(docId);
1812
+ const record = await this.adapter.getDoc(docId);
1813
+ if (!record) return null;
1814
+ return this.applyMigration(record, docId);
1175
1815
  }
1176
1816
  async getEdge(aUid, axbType, bUid) {
1177
1817
  const docId = computeEdgeDocId(aUid, axbType, bUid);
1178
- return this.adapter.getDoc(docId);
1818
+ const record = await this.adapter.getDoc(docId);
1819
+ if (!record) return null;
1820
+ return this.applyMigration(record, docId);
1179
1821
  }
1180
1822
  async edgeExists(aUid, axbType, bUid) {
1181
- const record = await this.getEdge(aUid, axbType, bUid);
1823
+ const docId = computeEdgeDocId(aUid, axbType, bUid);
1824
+ const record = await this.adapter.getDoc(docId);
1182
1825
  return record !== null;
1183
1826
  }
1184
1827
  async findEdges(params) {
1185
1828
  const plan = buildEdgeQueryPlan(params);
1829
+ let records;
1186
1830
  if (plan.strategy === "get") {
1187
1831
  const record = await this.adapter.getDoc(plan.docId);
1188
- return record ? [record] : [];
1832
+ records = record ? [record] : [];
1833
+ } else {
1834
+ this.checkQuerySafety(plan.filters, params.allowCollectionScan);
1835
+ records = await this.executeQuery(plan.filters, plan.options);
1189
1836
  }
1190
- this.checkQuerySafety(plan.filters, params.allowCollectionScan);
1191
- return this.executeQuery(plan.filters, plan.options);
1837
+ return this.applyMigrations(records);
1192
1838
  }
1193
1839
  async findNodes(params) {
1194
1840
  const plan = buildNodeQueryPlan(params);
1841
+ let records;
1195
1842
  if (plan.strategy === "get") {
1196
1843
  const record = await this.adapter.getDoc(plan.docId);
1197
- return record ? [record] : [];
1844
+ records = record ? [record] : [];
1845
+ } else {
1846
+ this.checkQuerySafety(plan.filters, params.allowCollectionScan);
1847
+ records = await this.executeQuery(plan.filters, plan.options);
1198
1848
  }
1199
- this.checkQuerySafety(plan.filters, params.allowCollectionScan);
1200
- return this.executeQuery(plan.filters, plan.options);
1849
+ return this.applyMigrations(records);
1201
1850
  }
1202
1851
  // ---------------------------------------------------------------------------
1203
1852
  // GraphWriter
@@ -1210,6 +1859,12 @@ var GraphClientImpl = class _GraphClientImpl {
1210
1859
  const adapter = this.getAdapterForType(aType);
1211
1860
  const docId = computeNodeDocId(uid);
1212
1861
  const record = buildNodeRecord(aType, uid, data);
1862
+ if (registry) {
1863
+ const entry = registry.lookup(aType, NODE_RELATION, aType);
1864
+ if (entry?.schemaVersion && entry.schemaVersion > 0) {
1865
+ record.v = entry.schemaVersion;
1866
+ }
1867
+ }
1213
1868
  await adapter.setDoc(docId, record);
1214
1869
  }
1215
1870
  async putEdge(aType, aUid, axbType, bType, bUid, data) {
@@ -1220,13 +1875,19 @@ var GraphClientImpl = class _GraphClientImpl {
1220
1875
  const adapter = this.getAdapterForType(aType);
1221
1876
  const docId = computeEdgeDocId(aUid, axbType, bUid);
1222
1877
  const record = buildEdgeRecord(aType, aUid, axbType, bType, bUid, data);
1878
+ if (registry) {
1879
+ const entry = registry.lookup(aType, axbType, bType);
1880
+ if (entry?.schemaVersion && entry.schemaVersion > 0) {
1881
+ record.v = entry.schemaVersion;
1882
+ }
1883
+ }
1223
1884
  await adapter.setDoc(docId, record);
1224
1885
  }
1225
1886
  async updateNode(uid, data) {
1226
1887
  const docId = computeNodeDocId(uid);
1227
1888
  await this.adapter.updateDoc(docId, {
1228
1889
  ...data,
1229
- updatedAt: FieldValue4.serverTimestamp()
1890
+ updatedAt: FieldValue5.serverTimestamp()
1230
1891
  });
1231
1892
  }
1232
1893
  async removeNode(uid) {
@@ -1247,7 +1908,7 @@ var GraphClientImpl = class _GraphClientImpl {
1247
1908
  this.adapter.collectionPath,
1248
1909
  firestoreTx
1249
1910
  );
1250
- const graphTx = new GraphTransactionImpl(adapter, this.getCombinedRegistry(), this.scanProtection, this.scopePath);
1911
+ const graphTx = new GraphTransactionImpl(adapter, this.getCombinedRegistry(), this.scanProtection, this.scopePath, this.globalWriteBack, this.db);
1251
1912
  return fn(graphTx);
1252
1913
  });
1253
1914
  }
@@ -1279,7 +1940,9 @@ var GraphClientImpl = class _GraphClientImpl {
1279
1940
  {
1280
1941
  registry: this.getCombinedRegistry(),
1281
1942
  queryMode: this.queryMode === "pipeline" ? "pipeline" : "standard",
1282
- scanProtection: this.scanProtection
1943
+ scanProtection: this.scanProtection,
1944
+ migrationWriteBack: this.globalWriteBack,
1945
+ migrationSandbox: this.migrationSandbox
1283
1946
  },
1284
1947
  newScopePath
1285
1948
  );
@@ -1309,7 +1972,8 @@ var GraphClientImpl = class _GraphClientImpl {
1309
1972
  q = q.limit(plan.options.limit);
1310
1973
  }
1311
1974
  const snap = await q.get();
1312
- return snap.docs.map((doc) => doc.data());
1975
+ const records = snap.docs.map((doc) => doc.data());
1976
+ return this.applyMigrations(records);
1313
1977
  }
1314
1978
  // ---------------------------------------------------------------------------
1315
1979
  // Bulk operations
@@ -1347,6 +2011,10 @@ var GraphClientImpl = class _GraphClientImpl {
1347
2011
  if (options?.viewTemplate !== void 0) data.viewTemplate = options.viewTemplate;
1348
2012
  if (options?.viewCss !== void 0) data.viewCss = options.viewCss;
1349
2013
  if (options?.allowedIn !== void 0) data.allowedIn = options.allowedIn;
2014
+ if (options?.migrationWriteBack !== void 0) data.migrationWriteBack = options.migrationWriteBack;
2015
+ if (options?.migrations !== void 0) {
2016
+ data.migrations = await this.serializeMigrations(options.migrations);
2017
+ }
1350
2018
  await this.putNode(META_NODE_TYPE, uid, data);
1351
2019
  }
1352
2020
  async defineEdgeType(name, topology, jsonSchema, description, options) {
@@ -1388,6 +2056,10 @@ var GraphClientImpl = class _GraphClientImpl {
1388
2056
  if (options?.viewTemplate !== void 0) data.viewTemplate = options.viewTemplate;
1389
2057
  if (options?.viewCss !== void 0) data.viewCss = options.viewCss;
1390
2058
  if (options?.allowedIn !== void 0) data.allowedIn = options.allowedIn;
2059
+ if (options?.migrationWriteBack !== void 0) data.migrationWriteBack = options.migrationWriteBack;
2060
+ if (options?.migrations !== void 0) {
2061
+ data.migrations = await this.serializeMigrations(options.migrations);
2062
+ }
1391
2063
  await this.putNode(META_EDGE_TYPE, uid, data);
1392
2064
  }
1393
2065
  async reloadRegistry() {
@@ -1397,13 +2069,28 @@ var GraphClientImpl = class _GraphClientImpl {
1397
2069
  );
1398
2070
  }
1399
2071
  const reader = this.createMetaReader();
1400
- const dynamicOnly = await createRegistryFromGraph(reader);
2072
+ const dynamicOnly = await createRegistryFromGraph(reader, this.migrationSandbox);
1401
2073
  if (this.staticRegistry) {
1402
2074
  this.dynamicRegistry = createMergedRegistry(this.staticRegistry, dynamicOnly);
1403
2075
  } else {
1404
2076
  this.dynamicRegistry = dynamicOnly;
1405
2077
  }
1406
2078
  }
2079
+ /**
2080
+ * Serialize migration steps for storage in Firestore.
2081
+ * Function objects are converted via `.toString()`; strings are stored as-is.
2082
+ * Each migration is validated at define-time by pre-compiling in the sandbox.
2083
+ */
2084
+ async serializeMigrations(migrations) {
2085
+ const result = migrations.map((m) => {
2086
+ const source = typeof m.up === "function" ? m.up.toString() : m.up;
2087
+ return { fromVersion: m.fromVersion, toVersion: m.toVersion, up: source };
2088
+ });
2089
+ await Promise.all(
2090
+ result.map((m) => precompileSource(m.up, this.migrationSandbox))
2091
+ );
2092
+ return result;
2093
+ }
1407
2094
  /**
1408
2095
  * Create a GraphReader for the meta-collection.
1409
2096
  * If meta-collection is the same as main collection, returns `this`.
@@ -1841,11 +2528,39 @@ function findViewsFile(dir) {
1841
2528
  }
1842
2529
  return void 0;
1843
2530
  }
2531
+ var MIGRATION_EXTENSIONS = [".ts", ".js", ".mts", ".mjs"];
2532
+ function findMigrationsFile(dir) {
2533
+ for (const ext of MIGRATION_EXTENSIONS) {
2534
+ const candidate = join(dir, `migrations${ext}`);
2535
+ if (existsSync(candidate)) return candidate;
2536
+ }
2537
+ return void 0;
2538
+ }
2539
+ function loadMigrations(filePath, entityLabel) {
2540
+ try {
2541
+ const jiti = getJiti();
2542
+ const mod = jiti(filePath);
2543
+ const migrations = mod && typeof mod === "object" && "default" in mod ? mod.default : mod;
2544
+ if (!Array.isArray(migrations)) {
2545
+ throw new DiscoveryError(
2546
+ `Migrations file ${filePath} for ${entityLabel} must default-export an array of MigrationStep.`
2547
+ );
2548
+ }
2549
+ return migrations;
2550
+ } catch (err) {
2551
+ if (err instanceof DiscoveryError) throw err;
2552
+ throw new DiscoveryError(
2553
+ `Failed to load migrations ${filePath} for ${entityLabel}: ${err.message}`
2554
+ );
2555
+ }
2556
+ }
1844
2557
  function loadNodeEntity(dir, name) {
1845
2558
  const schema = loadSchema(dir, `node type "${name}"`);
1846
2559
  const meta = readJsonIfExists(join(dir, "meta.json"));
1847
2560
  const sampleData = readJsonIfExists(join(dir, "sample.json"));
1848
2561
  const viewsPath = findViewsFile(dir);
2562
+ const migrationsPath = findMigrationsFile(dir);
2563
+ const migrations = migrationsPath ? loadMigrations(migrationsPath, `node type "${name}"`) : void 0;
1849
2564
  return {
1850
2565
  kind: "node",
1851
2566
  name,
@@ -1856,7 +2571,9 @@ function loadNodeEntity(dir, name) {
1856
2571
  viewDefaults: meta?.viewDefaults,
1857
2572
  viewsPath,
1858
2573
  sampleData,
1859
- allowedIn: meta?.allowedIn
2574
+ allowedIn: meta?.allowedIn,
2575
+ migrations,
2576
+ migrationWriteBack: meta?.migrationWriteBack
1860
2577
  };
1861
2578
  }
1862
2579
  function loadEdgeEntity(dir, name) {
@@ -1881,6 +2598,8 @@ function loadEdgeEntity(dir, name) {
1881
2598
  const meta = readJsonIfExists(join(dir, "meta.json"));
1882
2599
  const sampleData = readJsonIfExists(join(dir, "sample.json"));
1883
2600
  const viewsPath = findViewsFile(dir);
2601
+ const migrationsPath = findMigrationsFile(dir);
2602
+ const migrations = migrationsPath ? loadMigrations(migrationsPath, `edge type "${name}"`) : void 0;
1884
2603
  return {
1885
2604
  kind: "edge",
1886
2605
  name,
@@ -1893,7 +2612,9 @@ function loadEdgeEntity(dir, name) {
1893
2612
  viewsPath,
1894
2613
  sampleData,
1895
2614
  allowedIn: meta?.allowedIn,
1896
- targetGraph: topology.targetGraph ?? meta?.targetGraph
2615
+ targetGraph: topology.targetGraph ?? meta?.targetGraph,
2616
+ migrations,
2617
+ migrationWriteBack: meta?.migrationWriteBack
1897
2618
  };
1898
2619
  }
1899
2620
  function getSubdirectories(dir) {
@@ -2084,6 +2805,7 @@ export {
2084
2805
  InvalidQueryError,
2085
2806
  META_EDGE_TYPE,
2086
2807
  META_NODE_TYPE,
2808
+ MigrationError,
2087
2809
  NODE_TYPE_SCHEMA,
2088
2810
  NodeNotFoundError,
2089
2811
  QueryClient,
@@ -2091,13 +2813,17 @@ export {
2091
2813
  QuerySafetyError,
2092
2814
  RegistryScopeError,
2093
2815
  RegistryViolationError,
2816
+ SERIALIZATION_TAG,
2094
2817
  TraversalError,
2095
2818
  ValidationError,
2096
2819
  analyzeQuerySafety,
2820
+ applyMigrationChain,
2097
2821
  buildEdgeQueryPlan,
2098
2822
  buildEdgeRecord,
2099
2823
  buildNodeQueryPlan,
2100
2824
  buildNodeRecord,
2825
+ compileMigrationFn,
2826
+ compileMigrations,
2101
2827
  compileSchema,
2102
2828
  computeEdgeDocId,
2103
2829
  computeNodeDocId,
@@ -2107,18 +2833,27 @@ export {
2107
2833
  createRegistry,
2108
2834
  createRegistryFromGraph,
2109
2835
  createTraversal,
2836
+ defaultExecutor,
2110
2837
  defineConfig,
2111
2838
  defineViews,
2839
+ deserializeFirestoreTypes,
2840
+ destroySandboxWorker,
2112
2841
  discoverEntities,
2113
2842
  generateDeterministicUid,
2114
2843
  generateId,
2115
2844
  generateIndexConfig,
2116
2845
  generateTypes,
2117
2846
  isAncestorUid,
2847
+ isTaggedValue,
2118
2848
  jsonSchemaToFieldMeta,
2119
2849
  matchScope,
2120
2850
  matchScopeAny,
2851
+ migrateRecord,
2852
+ migrateRecords,
2853
+ precompileSource,
2121
2854
  resolveAncestorCollection,
2122
- resolveView
2855
+ resolveView,
2856
+ serializeFirestoreTypes,
2857
+ validateMigrationChain
2123
2858
  };
2124
2859
  //# sourceMappingURL=index.js.map