@typicalday/firegraph 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +115 -2
- package/dist/codegen/index.d.cts +1 -1
- package/dist/codegen/index.d.ts +1 -1
- package/dist/editor/server/index.mjs +831 -55
- package/dist/{index-CQkofEC_.d.cts → index-B9aodfYD.d.cts} +111 -2
- package/dist/{index-CQkofEC_.d.ts → index-B9aodfYD.d.ts} +111 -2
- package/dist/index.cjs +872 -55
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +175 -4
- package/dist/index.d.ts +175 -4
- package/dist/index.js +853 -52
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
package/dist/index.cjs
CHANGED
|
@@ -40,6 +40,7 @@ __export(index_exports, {
|
|
|
40
40
|
InvalidQueryError: () => InvalidQueryError,
|
|
41
41
|
META_EDGE_TYPE: () => META_EDGE_TYPE,
|
|
42
42
|
META_NODE_TYPE: () => META_NODE_TYPE,
|
|
43
|
+
MigrationError: () => MigrationError,
|
|
43
44
|
NODE_TYPE_SCHEMA: () => NODE_TYPE_SCHEMA,
|
|
44
45
|
NodeNotFoundError: () => NodeNotFoundError,
|
|
45
46
|
QueryClient: () => QueryClient,
|
|
@@ -47,39 +48,53 @@ __export(index_exports, {
|
|
|
47
48
|
QuerySafetyError: () => QuerySafetyError,
|
|
48
49
|
RegistryScopeError: () => RegistryScopeError,
|
|
49
50
|
RegistryViolationError: () => RegistryViolationError,
|
|
51
|
+
SERIALIZATION_TAG: () => SERIALIZATION_TAG,
|
|
50
52
|
TraversalError: () => TraversalError,
|
|
51
53
|
ValidationError: () => ValidationError,
|
|
52
54
|
analyzeQuerySafety: () => analyzeQuerySafety,
|
|
55
|
+
applyMigrationChain: () => applyMigrationChain,
|
|
53
56
|
buildEdgeQueryPlan: () => buildEdgeQueryPlan,
|
|
54
57
|
buildEdgeRecord: () => buildEdgeRecord,
|
|
55
58
|
buildNodeQueryPlan: () => buildNodeQueryPlan,
|
|
56
59
|
buildNodeRecord: () => buildNodeRecord,
|
|
60
|
+
compileMigrationFn: () => compileMigrationFn,
|
|
61
|
+
compileMigrations: () => compileMigrations,
|
|
57
62
|
compileSchema: () => compileSchema,
|
|
58
63
|
computeEdgeDocId: () => computeEdgeDocId,
|
|
59
64
|
computeNodeDocId: () => computeNodeDocId,
|
|
60
65
|
createBootstrapRegistry: () => createBootstrapRegistry,
|
|
61
66
|
createGraphClient: () => createGraphClient,
|
|
67
|
+
createMergedRegistry: () => createMergedRegistry,
|
|
62
68
|
createRegistry: () => createRegistry,
|
|
63
69
|
createRegistryFromGraph: () => createRegistryFromGraph,
|
|
64
70
|
createTraversal: () => createTraversal,
|
|
71
|
+
defaultExecutor: () => defaultExecutor,
|
|
65
72
|
defineConfig: () => defineConfig,
|
|
66
73
|
defineViews: () => defineViews,
|
|
74
|
+
deserializeFirestoreTypes: () => deserializeFirestoreTypes,
|
|
75
|
+
destroySandboxWorker: () => destroySandboxWorker,
|
|
67
76
|
discoverEntities: () => discoverEntities,
|
|
68
77
|
generateDeterministicUid: () => generateDeterministicUid,
|
|
69
78
|
generateId: () => generateId,
|
|
70
79
|
generateIndexConfig: () => generateIndexConfig,
|
|
71
80
|
generateTypes: () => generateTypes,
|
|
72
81
|
isAncestorUid: () => isAncestorUid,
|
|
82
|
+
isTaggedValue: () => isTaggedValue,
|
|
73
83
|
jsonSchemaToFieldMeta: () => jsonSchemaToFieldMeta,
|
|
74
84
|
matchScope: () => matchScope,
|
|
75
85
|
matchScopeAny: () => matchScopeAny,
|
|
86
|
+
migrateRecord: () => migrateRecord,
|
|
87
|
+
migrateRecords: () => migrateRecords,
|
|
88
|
+
precompileSource: () => precompileSource,
|
|
76
89
|
resolveAncestorCollection: () => resolveAncestorCollection,
|
|
77
|
-
resolveView: () => resolveView
|
|
90
|
+
resolveView: () => resolveView,
|
|
91
|
+
serializeFirestoreTypes: () => serializeFirestoreTypes,
|
|
92
|
+
validateMigrationChain: () => validateMigrationChain
|
|
78
93
|
});
|
|
79
94
|
module.exports = __toCommonJS(index_exports);
|
|
80
95
|
|
|
81
96
|
// src/client.ts
|
|
82
|
-
var
|
|
97
|
+
var import_firestore5 = require("@google-cloud/firestore");
|
|
83
98
|
|
|
84
99
|
// src/docid.ts
|
|
85
100
|
var import_node_crypto = require("crypto");
|
|
@@ -207,6 +222,12 @@ var RegistryScopeError = class extends FiregraphError {
|
|
|
207
222
|
this.name = "RegistryScopeError";
|
|
208
223
|
}
|
|
209
224
|
};
|
|
225
|
+
var MigrationError = class extends FiregraphError {
|
|
226
|
+
constructor(message) {
|
|
227
|
+
super(message, "MIGRATION_ERROR");
|
|
228
|
+
this.name = "MigrationError";
|
|
229
|
+
}
|
|
230
|
+
};
|
|
210
231
|
|
|
211
232
|
// src/query.ts
|
|
212
233
|
function buildEdgeQueryPlan(params) {
|
|
@@ -397,7 +418,7 @@ function createPipelineQueryAdapter(db, collectionPath) {
|
|
|
397
418
|
}
|
|
398
419
|
|
|
399
420
|
// src/transaction.ts
|
|
400
|
-
var
|
|
421
|
+
var import_firestore3 = require("@google-cloud/firestore");
|
|
401
422
|
|
|
402
423
|
// src/query-safety.ts
|
|
403
424
|
var SAFE_INDEX_PATTERNS = [
|
|
@@ -447,24 +468,248 @@ function analyzeQuerySafety(filters) {
|
|
|
447
468
|
};
|
|
448
469
|
}
|
|
449
470
|
|
|
471
|
+
// src/serialization.ts
|
|
472
|
+
var import_firestore2 = require("@google-cloud/firestore");
|
|
473
|
+
var SERIALIZATION_TAG = "__firegraph_ser__";
|
|
474
|
+
var KNOWN_TYPES = /* @__PURE__ */ new Set(["Timestamp", "GeoPoint", "VectorValue", "DocumentReference"]);
|
|
475
|
+
var _docRefWarned = false;
|
|
476
|
+
function isTaggedValue(value) {
|
|
477
|
+
if (value === null || typeof value !== "object") return false;
|
|
478
|
+
const tag = value[SERIALIZATION_TAG];
|
|
479
|
+
return typeof tag === "string" && KNOWN_TYPES.has(tag);
|
|
480
|
+
}
|
|
481
|
+
function isTimestamp(value) {
|
|
482
|
+
return value instanceof import_firestore2.Timestamp;
|
|
483
|
+
}
|
|
484
|
+
function isGeoPoint(value) {
|
|
485
|
+
return value instanceof import_firestore2.GeoPoint;
|
|
486
|
+
}
|
|
487
|
+
function isDocumentReference(value) {
|
|
488
|
+
if (value === null || typeof value !== "object") return false;
|
|
489
|
+
const v = value;
|
|
490
|
+
return typeof v.path === "string" && v.firestore !== void 0 && typeof v.id === "string" && v.constructor?.name === "DocumentReference";
|
|
491
|
+
}
|
|
492
|
+
function isVectorValue(value) {
|
|
493
|
+
if (value === null || typeof value !== "object") return false;
|
|
494
|
+
const v = value;
|
|
495
|
+
return v.constructor?.name === "VectorValue" && Array.isArray(v._values);
|
|
496
|
+
}
|
|
497
|
+
function serializeFirestoreTypes(data) {
|
|
498
|
+
return serializeValue(data);
|
|
499
|
+
}
|
|
500
|
+
function serializeValue(value) {
|
|
501
|
+
if (value === null || value === void 0) return value;
|
|
502
|
+
if (typeof value !== "object") return value;
|
|
503
|
+
if (isTimestamp(value)) {
|
|
504
|
+
return { [SERIALIZATION_TAG]: "Timestamp", seconds: value.seconds, nanoseconds: value.nanoseconds };
|
|
505
|
+
}
|
|
506
|
+
if (isGeoPoint(value)) {
|
|
507
|
+
return { [SERIALIZATION_TAG]: "GeoPoint", latitude: value.latitude, longitude: value.longitude };
|
|
508
|
+
}
|
|
509
|
+
if (isDocumentReference(value)) {
|
|
510
|
+
return { [SERIALIZATION_TAG]: "DocumentReference", path: value.path };
|
|
511
|
+
}
|
|
512
|
+
if (isVectorValue(value)) {
|
|
513
|
+
const v = value;
|
|
514
|
+
const values = typeof v.toArray === "function" ? v.toArray() : v._values;
|
|
515
|
+
return { [SERIALIZATION_TAG]: "VectorValue", values: [...values] };
|
|
516
|
+
}
|
|
517
|
+
if (Array.isArray(value)) {
|
|
518
|
+
return value.map(serializeValue);
|
|
519
|
+
}
|
|
520
|
+
const result = {};
|
|
521
|
+
for (const key of Object.keys(value)) {
|
|
522
|
+
result[key] = serializeValue(value[key]);
|
|
523
|
+
}
|
|
524
|
+
return result;
|
|
525
|
+
}
|
|
526
|
+
function deserializeFirestoreTypes(data, db) {
|
|
527
|
+
return deserializeValue(data, db);
|
|
528
|
+
}
|
|
529
|
+
function deserializeValue(value, db) {
|
|
530
|
+
if (value === null || value === void 0) return value;
|
|
531
|
+
if (typeof value !== "object") return value;
|
|
532
|
+
if (isTimestamp(value) || isGeoPoint(value) || isDocumentReference(value) || isVectorValue(value)) {
|
|
533
|
+
return value;
|
|
534
|
+
}
|
|
535
|
+
if (Array.isArray(value)) {
|
|
536
|
+
return value.map((v) => deserializeValue(v, db));
|
|
537
|
+
}
|
|
538
|
+
const obj = value;
|
|
539
|
+
if (isTaggedValue(obj)) {
|
|
540
|
+
const tag = obj[SERIALIZATION_TAG];
|
|
541
|
+
switch (tag) {
|
|
542
|
+
case "Timestamp":
|
|
543
|
+
if (typeof obj.seconds !== "number" || typeof obj.nanoseconds !== "number") return obj;
|
|
544
|
+
return new import_firestore2.Timestamp(obj.seconds, obj.nanoseconds);
|
|
545
|
+
case "GeoPoint":
|
|
546
|
+
if (typeof obj.latitude !== "number" || typeof obj.longitude !== "number") return obj;
|
|
547
|
+
return new import_firestore2.GeoPoint(obj.latitude, obj.longitude);
|
|
548
|
+
case "VectorValue":
|
|
549
|
+
if (!Array.isArray(obj.values)) return obj;
|
|
550
|
+
return import_firestore2.FieldValue.vector(obj.values);
|
|
551
|
+
case "DocumentReference":
|
|
552
|
+
if (typeof obj.path !== "string") return obj;
|
|
553
|
+
if (db) {
|
|
554
|
+
return db.doc(obj.path);
|
|
555
|
+
}
|
|
556
|
+
if (!_docRefWarned) {
|
|
557
|
+
_docRefWarned = true;
|
|
558
|
+
console.warn(
|
|
559
|
+
"[firegraph] DocumentReference encountered during migration deserialization but no Firestore instance available. The reference will remain as a tagged object with its path. Enable write-back for full reconstruction."
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
return obj;
|
|
563
|
+
default:
|
|
564
|
+
return obj;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
const result = {};
|
|
568
|
+
for (const key of Object.keys(obj)) {
|
|
569
|
+
result[key] = deserializeValue(obj[key], db);
|
|
570
|
+
}
|
|
571
|
+
return result;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// src/migration.ts
|
|
575
|
+
async function applyMigrationChain(data, currentVersion, targetVersion, migrations) {
|
|
576
|
+
const sorted = [...migrations].sort((a, b) => a.fromVersion - b.fromVersion);
|
|
577
|
+
let result = { ...data };
|
|
578
|
+
let version = currentVersion;
|
|
579
|
+
for (const step of sorted) {
|
|
580
|
+
if (step.fromVersion === version) {
|
|
581
|
+
try {
|
|
582
|
+
result = await step.up(result);
|
|
583
|
+
} catch (err) {
|
|
584
|
+
if (err instanceof MigrationError) throw err;
|
|
585
|
+
throw new MigrationError(
|
|
586
|
+
`Migration from v${step.fromVersion} to v${step.toVersion} failed: ${err.message}`
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
if (!result || typeof result !== "object") {
|
|
590
|
+
throw new MigrationError(
|
|
591
|
+
`Migration from v${step.fromVersion} to v${step.toVersion} returned invalid data (expected object)`
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
version = step.toVersion;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
if (version !== targetVersion) {
|
|
598
|
+
throw new MigrationError(
|
|
599
|
+
`Incomplete migration chain: reached v${version} but target is v${targetVersion}`
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
return result;
|
|
603
|
+
}
|
|
604
|
+
function validateMigrationChain(migrations, label) {
|
|
605
|
+
if (migrations.length === 0) return;
|
|
606
|
+
const seen = /* @__PURE__ */ new Set();
|
|
607
|
+
for (const step of migrations) {
|
|
608
|
+
if (step.toVersion <= step.fromVersion) {
|
|
609
|
+
throw new MigrationError(
|
|
610
|
+
`${label}: migration step has toVersion (${step.toVersion}) <= fromVersion (${step.fromVersion})`
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
if (seen.has(step.fromVersion)) {
|
|
614
|
+
throw new MigrationError(
|
|
615
|
+
`${label}: duplicate migration step for fromVersion ${step.fromVersion}`
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
seen.add(step.fromVersion);
|
|
619
|
+
}
|
|
620
|
+
const sorted = [...migrations].sort((a, b) => a.fromVersion - b.fromVersion);
|
|
621
|
+
const targetVersion = Math.max(...migrations.map((m) => m.toVersion));
|
|
622
|
+
let version = 0;
|
|
623
|
+
for (const step of sorted) {
|
|
624
|
+
if (step.fromVersion === version) {
|
|
625
|
+
version = step.toVersion;
|
|
626
|
+
} else if (step.fromVersion > version) {
|
|
627
|
+
throw new MigrationError(
|
|
628
|
+
`${label}: migration chain has a gap \u2014 no step covers v${version} \u2192 v${step.fromVersion}`
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
if (version !== targetVersion) {
|
|
633
|
+
throw new MigrationError(
|
|
634
|
+
`${label}: migration chain does not reach v${targetVersion} (stuck at v${version})`
|
|
635
|
+
);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
async function migrateRecord(record, registry, globalWriteBack = "off") {
|
|
639
|
+
const entry = registry.lookup(record.aType, record.axbType, record.bType);
|
|
640
|
+
if (!entry?.migrations?.length || !entry.schemaVersion) {
|
|
641
|
+
return { record, migrated: false, writeBack: "off" };
|
|
642
|
+
}
|
|
643
|
+
const currentVersion = record.v ?? 0;
|
|
644
|
+
if (currentVersion >= entry.schemaVersion) {
|
|
645
|
+
return { record, migrated: false, writeBack: "off" };
|
|
646
|
+
}
|
|
647
|
+
const migratedData = await applyMigrationChain(
|
|
648
|
+
record.data,
|
|
649
|
+
currentVersion,
|
|
650
|
+
entry.schemaVersion,
|
|
651
|
+
entry.migrations
|
|
652
|
+
);
|
|
653
|
+
const writeBack = entry.migrationWriteBack ?? globalWriteBack ?? "off";
|
|
654
|
+
return {
|
|
655
|
+
record: { ...record, data: migratedData, v: entry.schemaVersion },
|
|
656
|
+
migrated: true,
|
|
657
|
+
writeBack
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
async function migrateRecords(records, registry, globalWriteBack = "off") {
|
|
661
|
+
return Promise.all(
|
|
662
|
+
records.map((r) => migrateRecord(r, registry, globalWriteBack))
|
|
663
|
+
);
|
|
664
|
+
}
|
|
665
|
+
|
|
450
666
|
// src/transaction.ts
|
|
451
667
|
var GraphTransactionImpl = class {
|
|
452
|
-
constructor(adapter, registry, scanProtection = "error", scopePath = "") {
|
|
668
|
+
constructor(adapter, registry, scanProtection = "error", scopePath = "", globalWriteBack = "off", db) {
|
|
453
669
|
this.adapter = adapter;
|
|
454
670
|
this.registry = registry;
|
|
455
671
|
this.scanProtection = scanProtection;
|
|
456
672
|
this.scopePath = scopePath;
|
|
673
|
+
this.globalWriteBack = globalWriteBack;
|
|
674
|
+
this.db = db;
|
|
457
675
|
}
|
|
458
676
|
async getNode(uid) {
|
|
459
677
|
const docId = computeNodeDocId(uid);
|
|
460
|
-
|
|
678
|
+
const record = await this.adapter.getDoc(docId);
|
|
679
|
+
if (!record || !this.registry) return record;
|
|
680
|
+
const result = await migrateRecord(record, this.registry, this.globalWriteBack);
|
|
681
|
+
if (result.migrated && result.writeBack !== "off") {
|
|
682
|
+
const update = {
|
|
683
|
+
data: deserializeFirestoreTypes(result.record.data, this.db),
|
|
684
|
+
updatedAt: import_firestore3.FieldValue.serverTimestamp()
|
|
685
|
+
};
|
|
686
|
+
if (result.record.v !== void 0) {
|
|
687
|
+
update.v = result.record.v;
|
|
688
|
+
}
|
|
689
|
+
this.adapter.updateDoc(docId, update);
|
|
690
|
+
}
|
|
691
|
+
return result.record;
|
|
461
692
|
}
|
|
462
693
|
async getEdge(aUid, axbType, bUid) {
|
|
463
694
|
const docId = computeEdgeDocId(aUid, axbType, bUid);
|
|
464
|
-
|
|
695
|
+
const record = await this.adapter.getDoc(docId);
|
|
696
|
+
if (!record || !this.registry) return record;
|
|
697
|
+
const result = await migrateRecord(record, this.registry, this.globalWriteBack);
|
|
698
|
+
if (result.migrated && result.writeBack !== "off") {
|
|
699
|
+
const update = {
|
|
700
|
+
data: deserializeFirestoreTypes(result.record.data, this.db),
|
|
701
|
+
updatedAt: import_firestore3.FieldValue.serverTimestamp()
|
|
702
|
+
};
|
|
703
|
+
if (result.record.v !== void 0) {
|
|
704
|
+
update.v = result.record.v;
|
|
705
|
+
}
|
|
706
|
+
this.adapter.updateDoc(docId, update);
|
|
707
|
+
}
|
|
708
|
+
return result.record;
|
|
465
709
|
}
|
|
466
710
|
async edgeExists(aUid, axbType, bUid) {
|
|
467
|
-
const
|
|
711
|
+
const docId = computeEdgeDocId(aUid, axbType, bUid);
|
|
712
|
+
const record = await this.adapter.getDoc(docId);
|
|
468
713
|
return record !== null;
|
|
469
714
|
}
|
|
470
715
|
checkQuerySafety(filters, allowCollectionScan) {
|
|
@@ -478,21 +723,45 @@ var GraphTransactionImpl = class {
|
|
|
478
723
|
}
|
|
479
724
|
async findEdges(params) {
|
|
480
725
|
const plan = buildEdgeQueryPlan(params);
|
|
726
|
+
let records;
|
|
481
727
|
if (plan.strategy === "get") {
|
|
482
728
|
const record = await this.adapter.getDoc(plan.docId);
|
|
483
|
-
|
|
729
|
+
records = record ? [record] : [];
|
|
730
|
+
} else {
|
|
731
|
+
this.checkQuerySafety(plan.filters, params.allowCollectionScan);
|
|
732
|
+
records = await this.adapter.query(plan.filters, plan.options);
|
|
484
733
|
}
|
|
485
|
-
this.
|
|
486
|
-
return this.adapter.query(plan.filters, plan.options);
|
|
734
|
+
return this.applyMigrations(records);
|
|
487
735
|
}
|
|
488
736
|
async findNodes(params) {
|
|
489
737
|
const plan = buildNodeQueryPlan(params);
|
|
738
|
+
let records;
|
|
490
739
|
if (plan.strategy === "get") {
|
|
491
740
|
const record = await this.adapter.getDoc(plan.docId);
|
|
492
|
-
|
|
741
|
+
records = record ? [record] : [];
|
|
742
|
+
} else {
|
|
743
|
+
this.checkQuerySafety(plan.filters, params.allowCollectionScan);
|
|
744
|
+
records = await this.adapter.query(plan.filters, plan.options);
|
|
745
|
+
}
|
|
746
|
+
return this.applyMigrations(records);
|
|
747
|
+
}
|
|
748
|
+
async applyMigrations(records) {
|
|
749
|
+
if (!this.registry || records.length === 0) return records;
|
|
750
|
+
const results = await migrateRecords(records, this.registry, this.globalWriteBack);
|
|
751
|
+
for (const result of results) {
|
|
752
|
+
if (result.migrated && result.writeBack !== "off") {
|
|
753
|
+
const docId = result.record.axbType === NODE_RELATION ? computeNodeDocId(result.record.aUid) : computeEdgeDocId(result.record.aUid, result.record.axbType, result.record.bUid);
|
|
754
|
+
const update = {
|
|
755
|
+
data: deserializeFirestoreTypes(result.record.data, this.db),
|
|
756
|
+
updatedAt: import_firestore3.FieldValue.serverTimestamp()
|
|
757
|
+
};
|
|
758
|
+
if (result.record.v !== void 0) {
|
|
759
|
+
update.v = result.record.v;
|
|
760
|
+
}
|
|
761
|
+
this.adapter.updateDoc(docId, update);
|
|
762
|
+
}
|
|
493
763
|
}
|
|
494
|
-
|
|
495
|
-
return this.adapter.query(plan.filters, plan.options);
|
|
764
|
+
return results.map((r) => r.record);
|
|
496
765
|
}
|
|
497
766
|
async putNode(aType, uid, data) {
|
|
498
767
|
if (this.registry) {
|
|
@@ -500,6 +769,12 @@ var GraphTransactionImpl = class {
|
|
|
500
769
|
}
|
|
501
770
|
const docId = computeNodeDocId(uid);
|
|
502
771
|
const record = buildNodeRecord(aType, uid, data);
|
|
772
|
+
if (this.registry) {
|
|
773
|
+
const entry = this.registry.lookup(aType, NODE_RELATION, aType);
|
|
774
|
+
if (entry?.schemaVersion && entry.schemaVersion > 0) {
|
|
775
|
+
record.v = entry.schemaVersion;
|
|
776
|
+
}
|
|
777
|
+
}
|
|
503
778
|
this.adapter.setDoc(docId, record);
|
|
504
779
|
}
|
|
505
780
|
async putEdge(aType, aUid, axbType, bType, bUid, data) {
|
|
@@ -508,13 +783,19 @@ var GraphTransactionImpl = class {
|
|
|
508
783
|
}
|
|
509
784
|
const docId = computeEdgeDocId(aUid, axbType, bUid);
|
|
510
785
|
const record = buildEdgeRecord(aType, aUid, axbType, bType, bUid, data);
|
|
786
|
+
if (this.registry) {
|
|
787
|
+
const entry = this.registry.lookup(aType, axbType, bType);
|
|
788
|
+
if (entry?.schemaVersion && entry.schemaVersion > 0) {
|
|
789
|
+
record.v = entry.schemaVersion;
|
|
790
|
+
}
|
|
791
|
+
}
|
|
511
792
|
this.adapter.setDoc(docId, record);
|
|
512
793
|
}
|
|
513
794
|
async updateNode(uid, data) {
|
|
514
795
|
const docId = computeNodeDocId(uid);
|
|
515
796
|
this.adapter.updateDoc(docId, {
|
|
516
797
|
...data,
|
|
517
|
-
updatedAt:
|
|
798
|
+
updatedAt: import_firestore3.FieldValue.serverTimestamp()
|
|
518
799
|
});
|
|
519
800
|
}
|
|
520
801
|
async removeNode(uid) {
|
|
@@ -528,7 +809,7 @@ var GraphTransactionImpl = class {
|
|
|
528
809
|
};
|
|
529
810
|
|
|
530
811
|
// src/batch.ts
|
|
531
|
-
var
|
|
812
|
+
var import_firestore4 = require("@google-cloud/firestore");
|
|
532
813
|
var GraphBatchImpl = class {
|
|
533
814
|
constructor(adapter, registry, scopePath = "") {
|
|
534
815
|
this.adapter = adapter;
|
|
@@ -541,6 +822,12 @@ var GraphBatchImpl = class {
|
|
|
541
822
|
}
|
|
542
823
|
const docId = computeNodeDocId(uid);
|
|
543
824
|
const record = buildNodeRecord(aType, uid, data);
|
|
825
|
+
if (this.registry) {
|
|
826
|
+
const entry = this.registry.lookup(aType, NODE_RELATION, aType);
|
|
827
|
+
if (entry?.schemaVersion && entry.schemaVersion > 0) {
|
|
828
|
+
record.v = entry.schemaVersion;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
544
831
|
this.adapter.setDoc(docId, record);
|
|
545
832
|
}
|
|
546
833
|
async putEdge(aType, aUid, axbType, bType, bUid, data) {
|
|
@@ -549,13 +836,19 @@ var GraphBatchImpl = class {
|
|
|
549
836
|
}
|
|
550
837
|
const docId = computeEdgeDocId(aUid, axbType, bUid);
|
|
551
838
|
const record = buildEdgeRecord(aType, aUid, axbType, bType, bUid, data);
|
|
839
|
+
if (this.registry) {
|
|
840
|
+
const entry = this.registry.lookup(aType, axbType, bType);
|
|
841
|
+
if (entry?.schemaVersion && entry.schemaVersion > 0) {
|
|
842
|
+
record.v = entry.schemaVersion;
|
|
843
|
+
}
|
|
844
|
+
}
|
|
552
845
|
this.adapter.setDoc(docId, record);
|
|
553
846
|
}
|
|
554
847
|
async updateNode(uid, data) {
|
|
555
848
|
const docId = computeNodeDocId(uid);
|
|
556
849
|
this.adapter.updateDoc(docId, {
|
|
557
850
|
...data,
|
|
558
|
-
updatedAt:
|
|
851
|
+
updatedAt: import_firestore4.FieldValue.serverTimestamp()
|
|
559
852
|
});
|
|
560
853
|
}
|
|
561
854
|
async removeNode(uid) {
|
|
@@ -714,7 +1007,7 @@ async function removeNodeCascade(db, collectionPath, reader, uid, options) {
|
|
|
714
1007
|
}
|
|
715
1008
|
|
|
716
1009
|
// src/dynamic-registry.ts
|
|
717
|
-
var
|
|
1010
|
+
var import_node_crypto3 = require("crypto");
|
|
718
1011
|
|
|
719
1012
|
// src/json-schema.ts
|
|
720
1013
|
var import_ajv = __toESM(require("ajv"), 1);
|
|
@@ -845,6 +1138,9 @@ function matchSegments(path, pi, pattern, qi) {
|
|
|
845
1138
|
function tripleKey(aType, axbType, bType) {
|
|
846
1139
|
return `${aType}:${axbType}:${bType}`;
|
|
847
1140
|
}
|
|
1141
|
+
function tripleKeyFor(e) {
|
|
1142
|
+
return tripleKey(e.aType, e.axbType, e.bType);
|
|
1143
|
+
}
|
|
848
1144
|
function createRegistry(input) {
|
|
849
1145
|
const map = /* @__PURE__ */ new Map();
|
|
850
1146
|
let entries;
|
|
@@ -860,6 +1156,13 @@ function createRegistry(input) {
|
|
|
860
1156
|
`Entry (${entry.aType}) -[${entry.axbType}]-> (${entry.bType}) has invalid targetGraph "${entry.targetGraph}" \u2014 must be a single segment (no "/")`
|
|
861
1157
|
);
|
|
862
1158
|
}
|
|
1159
|
+
if (entry.migrations?.length) {
|
|
1160
|
+
const label = `Entry (${entry.aType}) -[${entry.axbType}]-> (${entry.bType})`;
|
|
1161
|
+
validateMigrationChain(entry.migrations, label);
|
|
1162
|
+
entry.schemaVersion = Math.max(...entry.migrations.map((m) => m.toVersion));
|
|
1163
|
+
} else {
|
|
1164
|
+
entry.schemaVersion = void 0;
|
|
1165
|
+
}
|
|
863
1166
|
const key = tripleKey(entry.aType, entry.axbType, entry.bType);
|
|
864
1167
|
const validator = entry.jsonSchema ? compileSchema(entry.jsonSchema, `(${entry.aType}) -[${entry.axbType}]-> (${entry.bType})`) : void 0;
|
|
865
1168
|
map.set(key, { entry, validate: validator });
|
|
@@ -911,6 +1214,45 @@ function createRegistry(input) {
|
|
|
911
1214
|
}
|
|
912
1215
|
};
|
|
913
1216
|
}
|
|
1217
|
+
function createMergedRegistry(base, extension) {
|
|
1218
|
+
const baseKeys = new Set(base.entries().map(tripleKeyFor));
|
|
1219
|
+
return {
|
|
1220
|
+
lookup(aType, axbType, bType) {
|
|
1221
|
+
return base.lookup(aType, axbType, bType) ?? extension.lookup(aType, axbType, bType);
|
|
1222
|
+
},
|
|
1223
|
+
lookupByAxbType(axbType) {
|
|
1224
|
+
const baseResults = base.lookupByAxbType(axbType);
|
|
1225
|
+
const extResults = extension.lookupByAxbType(axbType);
|
|
1226
|
+
if (extResults.length === 0) return baseResults;
|
|
1227
|
+
if (baseResults.length === 0) return extResults;
|
|
1228
|
+
const seen = new Set(baseResults.map(tripleKeyFor));
|
|
1229
|
+
const merged = [...baseResults];
|
|
1230
|
+
for (const entry of extResults) {
|
|
1231
|
+
if (!seen.has(tripleKeyFor(entry))) {
|
|
1232
|
+
merged.push(entry);
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
return Object.freeze(merged);
|
|
1236
|
+
},
|
|
1237
|
+
validate(aType, axbType, bType, data, scopePath) {
|
|
1238
|
+
if (baseKeys.has(tripleKey(aType, axbType, bType))) {
|
|
1239
|
+
return base.validate(aType, axbType, bType, data, scopePath);
|
|
1240
|
+
}
|
|
1241
|
+
return extension.validate(aType, axbType, bType, data, scopePath);
|
|
1242
|
+
},
|
|
1243
|
+
entries() {
|
|
1244
|
+
const extEntries = extension.entries();
|
|
1245
|
+
if (extEntries.length === 0) return base.entries();
|
|
1246
|
+
const merged = [...base.entries()];
|
|
1247
|
+
for (const entry of extEntries) {
|
|
1248
|
+
if (!baseKeys.has(tripleKeyFor(entry))) {
|
|
1249
|
+
merged.push(entry);
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
return Object.freeze(merged);
|
|
1253
|
+
}
|
|
1254
|
+
};
|
|
1255
|
+
}
|
|
914
1256
|
function discoveryToEntries(discovery) {
|
|
915
1257
|
const entries = [];
|
|
916
1258
|
for (const [name, entity] of discovery.nodes) {
|
|
@@ -922,7 +1264,9 @@ function discoveryToEntries(discovery) {
|
|
|
922
1264
|
description: entity.description,
|
|
923
1265
|
titleField: entity.titleField,
|
|
924
1266
|
subtitleField: entity.subtitleField,
|
|
925
|
-
allowedIn: entity.allowedIn
|
|
1267
|
+
allowedIn: entity.allowedIn,
|
|
1268
|
+
migrations: entity.migrations,
|
|
1269
|
+
migrationWriteBack: entity.migrationWriteBack
|
|
926
1270
|
});
|
|
927
1271
|
}
|
|
928
1272
|
for (const [axbType, entity] of discovery.edges) {
|
|
@@ -948,7 +1292,9 @@ function discoveryToEntries(discovery) {
|
|
|
948
1292
|
titleField: entity.titleField,
|
|
949
1293
|
subtitleField: entity.subtitleField,
|
|
950
1294
|
allowedIn: entity.allowedIn,
|
|
951
|
-
targetGraph: resolvedTargetGraph
|
|
1295
|
+
targetGraph: resolvedTargetGraph,
|
|
1296
|
+
migrations: entity.migrations,
|
|
1297
|
+
migrationWriteBack: entity.migrationWriteBack
|
|
952
1298
|
});
|
|
953
1299
|
}
|
|
954
1300
|
}
|
|
@@ -956,9 +1302,259 @@ function discoveryToEntries(discovery) {
|
|
|
956
1302
|
return entries;
|
|
957
1303
|
}
|
|
958
1304
|
|
|
1305
|
+
// src/sandbox.ts
|
|
1306
|
+
var import_node_worker_threads = require("worker_threads");
|
|
1307
|
+
var import_node_crypto2 = require("crypto");
|
|
1308
|
+
var import_meta = {};
|
|
1309
|
+
var _worker = null;
|
|
1310
|
+
var _requestId = 0;
|
|
1311
|
+
var _pending = /* @__PURE__ */ new Map();
|
|
1312
|
+
var WORKER_SOURCE = [
|
|
1313
|
+
`'use strict';`,
|
|
1314
|
+
`var _wt = require('node:worker_threads');`,
|
|
1315
|
+
`var _mod = require('node:module');`,
|
|
1316
|
+
`var _crypto = require('node:crypto');`,
|
|
1317
|
+
`var parentPort = _wt.parentPort;`,
|
|
1318
|
+
`var workerData = _wt.workerData;`,
|
|
1319
|
+
``,
|
|
1320
|
+
`// Load SES using the parent module's resolution context`,
|
|
1321
|
+
`var esmRequire = _mod.createRequire(workerData.parentUrl);`,
|
|
1322
|
+
`esmRequire('ses');`,
|
|
1323
|
+
``,
|
|
1324
|
+
`lockdown({`,
|
|
1325
|
+
` errorTaming: 'unsafe',`,
|
|
1326
|
+
` consoleTaming: 'unsafe',`,
|
|
1327
|
+
` evalTaming: 'safe-eval',`,
|
|
1328
|
+
` overrideTaming: 'moderate',`,
|
|
1329
|
+
` stackFiltering: 'verbose'`,
|
|
1330
|
+
`});`,
|
|
1331
|
+
``,
|
|
1332
|
+
`// Defense-in-depth: verify lockdown() actually hardened JSON.`,
|
|
1333
|
+
`if (!Object.isFrozen(JSON)) {`,
|
|
1334
|
+
` throw new Error('SES lockdown failed: JSON is not frozen');`,
|
|
1335
|
+
`}`,
|
|
1336
|
+
``,
|
|
1337
|
+
`var cache = new Map();`,
|
|
1338
|
+
``,
|
|
1339
|
+
`function hashSource(s) {`,
|
|
1340
|
+
` return _crypto.createHash('sha256').update(s).digest('hex');`,
|
|
1341
|
+
`}`,
|
|
1342
|
+
``,
|
|
1343
|
+
`function buildWrapper(source) {`,
|
|
1344
|
+
` return '(function() {' +`,
|
|
1345
|
+
` ' var fn = (' + source + ');\\n' +`,
|
|
1346
|
+
` ' if (typeof fn !== "function") return null;\\n' +`,
|
|
1347
|
+
` ' return function(jsonIn) {\\n' +`,
|
|
1348
|
+
` ' var data = JSON.parse(jsonIn);\\n' +`,
|
|
1349
|
+
` ' var result = fn(data);\\n' +`,
|
|
1350
|
+
` ' if (result !== null && typeof result === "object" && typeof result.then === "function") {\\n' +`,
|
|
1351
|
+
` ' return result.then(function(r) { return JSON.stringify(r); });\\n' +`,
|
|
1352
|
+
` ' }\\n' +`,
|
|
1353
|
+
` ' return JSON.stringify(result);\\n' +`,
|
|
1354
|
+
` ' };\\n' +`,
|
|
1355
|
+
` '})()';`,
|
|
1356
|
+
`}`,
|
|
1357
|
+
``,
|
|
1358
|
+
`function compileSource(source) {`,
|
|
1359
|
+
` var key = hashSource(source);`,
|
|
1360
|
+
` var cached = cache.get(key);`,
|
|
1361
|
+
` if (cached) return cached;`,
|
|
1362
|
+
``,
|
|
1363
|
+
` var compartmentFn;`,
|
|
1364
|
+
` try {`,
|
|
1365
|
+
` var c = new Compartment({ JSON: JSON });`,
|
|
1366
|
+
` compartmentFn = c.evaluate(buildWrapper(source));`,
|
|
1367
|
+
` } catch (err) {`,
|
|
1368
|
+
` throw new Error('Failed to compile migration source: ' + (err.message || String(err)));`,
|
|
1369
|
+
` }`,
|
|
1370
|
+
``,
|
|
1371
|
+
` if (typeof compartmentFn !== 'function') {`,
|
|
1372
|
+
` throw new Error('Migration source did not produce a function: ' + source.slice(0, 80));`,
|
|
1373
|
+
` }`,
|
|
1374
|
+
``,
|
|
1375
|
+
` cache.set(key, compartmentFn);`,
|
|
1376
|
+
` return compartmentFn;`,
|
|
1377
|
+
`}`,
|
|
1378
|
+
``,
|
|
1379
|
+
`parentPort.on('message', function(msg) {`,
|
|
1380
|
+
` var id = msg.id;`,
|
|
1381
|
+
` try {`,
|
|
1382
|
+
` if (msg.type === 'compile') {`,
|
|
1383
|
+
` compileSource(msg.source);`,
|
|
1384
|
+
` parentPort.postMessage({ id: id, type: 'compiled' });`,
|
|
1385
|
+
` return;`,
|
|
1386
|
+
` }`,
|
|
1387
|
+
` if (msg.type === 'execute') {`,
|
|
1388
|
+
` var fn = compileSource(msg.source);`,
|
|
1389
|
+
` var raw;`,
|
|
1390
|
+
` try {`,
|
|
1391
|
+
` raw = fn(msg.jsonData);`,
|
|
1392
|
+
` } catch (err) {`,
|
|
1393
|
+
` parentPort.postMessage({ id: id, type: 'error', message: 'Migration function threw: ' + (err.message || String(err)) });`,
|
|
1394
|
+
` return;`,
|
|
1395
|
+
` }`,
|
|
1396
|
+
` if (raw !== null && typeof raw === 'object' && typeof raw.then === 'function') {`,
|
|
1397
|
+
` raw.then(`,
|
|
1398
|
+
` function(jsonResult) {`,
|
|
1399
|
+
` if (jsonResult === undefined || jsonResult === null) {`,
|
|
1400
|
+
` parentPort.postMessage({ id: id, type: 'error', message: 'Migration returned a non-JSON-serializable value' });`,
|
|
1401
|
+
` } else {`,
|
|
1402
|
+
` parentPort.postMessage({ id: id, type: 'result', jsonResult: jsonResult });`,
|
|
1403
|
+
` }`,
|
|
1404
|
+
` },`,
|
|
1405
|
+
` function(err) {`,
|
|
1406
|
+
` parentPort.postMessage({ id: id, type: 'error', message: 'Async migration function threw: ' + (err.message || String(err)) });`,
|
|
1407
|
+
` }`,
|
|
1408
|
+
` );`,
|
|
1409
|
+
` return;`,
|
|
1410
|
+
` }`,
|
|
1411
|
+
` if (raw === undefined || raw === null) {`,
|
|
1412
|
+
` parentPort.postMessage({ id: id, type: 'error', message: 'Migration returned a non-JSON-serializable value' });`,
|
|
1413
|
+
` } else {`,
|
|
1414
|
+
` parentPort.postMessage({ id: id, type: 'result', jsonResult: raw });`,
|
|
1415
|
+
` }`,
|
|
1416
|
+
` }`,
|
|
1417
|
+
` } catch (err) {`,
|
|
1418
|
+
` parentPort.postMessage({ id: id, type: 'error', message: err.message || String(err) });`,
|
|
1419
|
+
` }`,
|
|
1420
|
+
`});`
|
|
1421
|
+
].join("\n");
|
|
1422
|
+
function ensureWorker() {
|
|
1423
|
+
if (_worker) return _worker;
|
|
1424
|
+
_worker = new import_node_worker_threads.Worker(WORKER_SOURCE, {
|
|
1425
|
+
eval: true,
|
|
1426
|
+
workerData: { parentUrl: import_meta.url }
|
|
1427
|
+
});
|
|
1428
|
+
_worker.unref();
|
|
1429
|
+
_worker.on("message", (msg) => {
|
|
1430
|
+
if (msg.id === void 0) return;
|
|
1431
|
+
const pending = _pending.get(msg.id);
|
|
1432
|
+
if (!pending) return;
|
|
1433
|
+
_pending.delete(msg.id);
|
|
1434
|
+
if (msg.type === "error") {
|
|
1435
|
+
pending.reject(new MigrationError(msg.message ?? "Unknown sandbox error"));
|
|
1436
|
+
} else {
|
|
1437
|
+
pending.resolve(msg);
|
|
1438
|
+
}
|
|
1439
|
+
});
|
|
1440
|
+
_worker.on("error", (err) => {
|
|
1441
|
+
for (const [, p] of _pending) {
|
|
1442
|
+
p.reject(new MigrationError(`Sandbox worker error: ${err.message}`));
|
|
1443
|
+
}
|
|
1444
|
+
_pending.clear();
|
|
1445
|
+
_worker = null;
|
|
1446
|
+
});
|
|
1447
|
+
_worker.on("exit", (code) => {
|
|
1448
|
+
if (_pending.size > 0) {
|
|
1449
|
+
for (const [, p] of _pending) {
|
|
1450
|
+
p.reject(new MigrationError(`Sandbox worker exited with code ${code}`));
|
|
1451
|
+
}
|
|
1452
|
+
_pending.clear();
|
|
1453
|
+
}
|
|
1454
|
+
_worker = null;
|
|
1455
|
+
});
|
|
1456
|
+
return _worker;
|
|
1457
|
+
}
|
|
1458
|
+
function sendToWorker(msg) {
|
|
1459
|
+
const worker = ensureWorker();
|
|
1460
|
+
if (_requestId >= Number.MAX_SAFE_INTEGER) _requestId = 0;
|
|
1461
|
+
const id = ++_requestId;
|
|
1462
|
+
return new Promise((resolve2, reject) => {
|
|
1463
|
+
_pending.set(id, { resolve: resolve2, reject });
|
|
1464
|
+
worker.postMessage({ ...msg, id });
|
|
1465
|
+
});
|
|
1466
|
+
}
|
|
1467
|
+
var compiledCache = /* @__PURE__ */ new WeakMap();
|
|
1468
|
+
function getExecutorCache(executor) {
|
|
1469
|
+
let cache = compiledCache.get(executor);
|
|
1470
|
+
if (!cache) {
|
|
1471
|
+
cache = /* @__PURE__ */ new Map();
|
|
1472
|
+
compiledCache.set(executor, cache);
|
|
1473
|
+
}
|
|
1474
|
+
return cache;
|
|
1475
|
+
}
|
|
1476
|
+
function hashSource(source) {
|
|
1477
|
+
return (0, import_node_crypto2.createHash)("sha256").update(source).digest("hex");
|
|
1478
|
+
}
|
|
1479
|
+
function defaultExecutor(source) {
|
|
1480
|
+
ensureWorker();
|
|
1481
|
+
return ((data) => {
|
|
1482
|
+
const jsonData = JSON.stringify(serializeFirestoreTypes(data));
|
|
1483
|
+
return sendToWorker({ type: "execute", source, jsonData }).then(
|
|
1484
|
+
(response) => {
|
|
1485
|
+
if (response.jsonResult === void 0 || response.jsonResult === null) {
|
|
1486
|
+
throw new MigrationError("Migration returned a non-JSON-serializable value");
|
|
1487
|
+
}
|
|
1488
|
+
try {
|
|
1489
|
+
return deserializeFirestoreTypes(JSON.parse(response.jsonResult));
|
|
1490
|
+
} catch {
|
|
1491
|
+
throw new MigrationError("Migration returned a non-JSON-serializable value");
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
);
|
|
1495
|
+
});
|
|
1496
|
+
}
|
|
1497
|
+
async function precompileSource(source, executor) {
|
|
1498
|
+
if (executor && executor !== defaultExecutor) {
|
|
1499
|
+
try {
|
|
1500
|
+
executor(source);
|
|
1501
|
+
} catch (err) {
|
|
1502
|
+
if (err instanceof MigrationError) throw err;
|
|
1503
|
+
throw new MigrationError(
|
|
1504
|
+
`Failed to compile migration source: ${err.message}`
|
|
1505
|
+
);
|
|
1506
|
+
}
|
|
1507
|
+
return;
|
|
1508
|
+
}
|
|
1509
|
+
await sendToWorker({ type: "compile", source });
|
|
1510
|
+
}
|
|
1511
|
+
function compileMigrationFn(source, executor = defaultExecutor) {
|
|
1512
|
+
const cache = getExecutorCache(executor);
|
|
1513
|
+
const key = hashSource(source);
|
|
1514
|
+
const cached = cache.get(key);
|
|
1515
|
+
if (cached) return cached;
|
|
1516
|
+
try {
|
|
1517
|
+
const fn = executor(source);
|
|
1518
|
+
cache.set(key, fn);
|
|
1519
|
+
return fn;
|
|
1520
|
+
} catch (err) {
|
|
1521
|
+
if (err instanceof MigrationError) throw err;
|
|
1522
|
+
throw new MigrationError(
|
|
1523
|
+
`Failed to compile migration source: ${err.message}`
|
|
1524
|
+
);
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
function compileMigrations(stored, executor) {
|
|
1528
|
+
return stored.map((step) => ({
|
|
1529
|
+
fromVersion: step.fromVersion,
|
|
1530
|
+
toVersion: step.toVersion,
|
|
1531
|
+
up: compileMigrationFn(step.up, executor)
|
|
1532
|
+
}));
|
|
1533
|
+
}
|
|
1534
|
+
async function destroySandboxWorker() {
|
|
1535
|
+
if (!_worker) return;
|
|
1536
|
+
const w = _worker;
|
|
1537
|
+
_worker = null;
|
|
1538
|
+
for (const [, p] of _pending) {
|
|
1539
|
+
p.reject(new MigrationError("Sandbox worker terminated"));
|
|
1540
|
+
}
|
|
1541
|
+
_pending.clear();
|
|
1542
|
+
await w.terminate();
|
|
1543
|
+
}
|
|
1544
|
+
|
|
959
1545
|
// src/dynamic-registry.ts
|
|
960
1546
|
var META_NODE_TYPE = "nodeType";
|
|
961
1547
|
var META_EDGE_TYPE = "edgeType";
|
|
1548
|
+
var STORED_MIGRATION_STEP_SCHEMA = {
|
|
1549
|
+
type: "object",
|
|
1550
|
+
required: ["fromVersion", "toVersion", "up"],
|
|
1551
|
+
properties: {
|
|
1552
|
+
fromVersion: { type: "integer", minimum: 0 },
|
|
1553
|
+
toVersion: { type: "integer", minimum: 1 },
|
|
1554
|
+
up: { type: "string", minLength: 1 }
|
|
1555
|
+
},
|
|
1556
|
+
additionalProperties: false
|
|
1557
|
+
};
|
|
962
1558
|
var NODE_TYPE_SCHEMA = {
|
|
963
1559
|
type: "object",
|
|
964
1560
|
required: ["name", "jsonSchema"],
|
|
@@ -970,7 +1566,10 @@ var NODE_TYPE_SCHEMA = {
|
|
|
970
1566
|
subtitleField: { type: "string" },
|
|
971
1567
|
viewTemplate: { type: "string" },
|
|
972
1568
|
viewCss: { type: "string" },
|
|
973
|
-
allowedIn: { type: "array", items: { type: "string", minLength: 1 } }
|
|
1569
|
+
allowedIn: { type: "array", items: { type: "string", minLength: 1 } },
|
|
1570
|
+
schemaVersion: { type: "integer", minimum: 0 },
|
|
1571
|
+
migrations: { type: "array", items: STORED_MIGRATION_STEP_SCHEMA },
|
|
1572
|
+
migrationWriteBack: { type: "string", enum: ["off", "eager", "background"] }
|
|
974
1573
|
},
|
|
975
1574
|
additionalProperties: false
|
|
976
1575
|
};
|
|
@@ -999,7 +1598,10 @@ var EDGE_TYPE_SCHEMA = {
|
|
|
999
1598
|
viewTemplate: { type: "string" },
|
|
1000
1599
|
viewCss: { type: "string" },
|
|
1001
1600
|
allowedIn: { type: "array", items: { type: "string", minLength: 1 } },
|
|
1002
|
-
targetGraph: { type: "string", minLength: 1, pattern: "^[^/]+$" }
|
|
1601
|
+
targetGraph: { type: "string", minLength: 1, pattern: "^[^/]+$" },
|
|
1602
|
+
schemaVersion: { type: "integer", minimum: 0 },
|
|
1603
|
+
migrations: { type: "array", items: STORED_MIGRATION_STEP_SCHEMA },
|
|
1604
|
+
migrationWriteBack: { type: "string", enum: ["off", "eager", "background"] }
|
|
1003
1605
|
},
|
|
1004
1606
|
additionalProperties: false
|
|
1005
1607
|
};
|
|
@@ -1023,15 +1625,33 @@ function createBootstrapRegistry() {
|
|
|
1023
1625
|
return createRegistry([...BOOTSTRAP_ENTRIES]);
|
|
1024
1626
|
}
|
|
1025
1627
|
function generateDeterministicUid(metaType, name) {
|
|
1026
|
-
const hash = (0,
|
|
1628
|
+
const hash = (0, import_node_crypto3.createHash)("sha256").update(`${metaType}:${name}`).digest("base64url");
|
|
1027
1629
|
return hash.slice(0, 21);
|
|
1028
1630
|
}
|
|
1029
|
-
async function createRegistryFromGraph(reader) {
|
|
1631
|
+
async function createRegistryFromGraph(reader, executor) {
|
|
1030
1632
|
const [nodeTypes, edgeTypes] = await Promise.all([
|
|
1031
1633
|
reader.findNodes({ aType: META_NODE_TYPE }),
|
|
1032
1634
|
reader.findNodes({ aType: META_EDGE_TYPE })
|
|
1033
1635
|
]);
|
|
1034
1636
|
const entries = [...BOOTSTRAP_ENTRIES];
|
|
1637
|
+
const prevalidations = [];
|
|
1638
|
+
for (const record of nodeTypes) {
|
|
1639
|
+
const data = record.data;
|
|
1640
|
+
if (data.migrations) {
|
|
1641
|
+
for (const m of data.migrations) {
|
|
1642
|
+
prevalidations.push(precompileSource(m.up, executor));
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
for (const record of edgeTypes) {
|
|
1647
|
+
const data = record.data;
|
|
1648
|
+
if (data.migrations) {
|
|
1649
|
+
for (const m of data.migrations) {
|
|
1650
|
+
prevalidations.push(precompileSource(m.up, executor));
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
await Promise.all(prevalidations);
|
|
1035
1655
|
for (const record of nodeTypes) {
|
|
1036
1656
|
const data = record.data;
|
|
1037
1657
|
entries.push({
|
|
@@ -1042,13 +1662,16 @@ async function createRegistryFromGraph(reader) {
|
|
|
1042
1662
|
description: data.description,
|
|
1043
1663
|
titleField: data.titleField,
|
|
1044
1664
|
subtitleField: data.subtitleField,
|
|
1045
|
-
allowedIn: data.allowedIn
|
|
1665
|
+
allowedIn: data.allowedIn,
|
|
1666
|
+
migrations: data.migrations ? compileMigrations(data.migrations, executor) : void 0,
|
|
1667
|
+
migrationWriteBack: data.migrationWriteBack
|
|
1046
1668
|
});
|
|
1047
1669
|
}
|
|
1048
1670
|
for (const record of edgeTypes) {
|
|
1049
1671
|
const data = record.data;
|
|
1050
1672
|
const fromTypes = Array.isArray(data.from) ? data.from : [data.from];
|
|
1051
1673
|
const toTypes = Array.isArray(data.to) ? data.to : [data.to];
|
|
1674
|
+
const compiledMigrations = data.migrations ? compileMigrations(data.migrations, executor) : void 0;
|
|
1052
1675
|
for (const aType of fromTypes) {
|
|
1053
1676
|
for (const bType of toTypes) {
|
|
1054
1677
|
entries.push({
|
|
@@ -1061,7 +1684,9 @@ async function createRegistryFromGraph(reader) {
|
|
|
1061
1684
|
titleField: data.titleField,
|
|
1062
1685
|
subtitleField: data.subtitleField,
|
|
1063
1686
|
allowedIn: data.allowedIn,
|
|
1064
|
-
targetGraph: data.targetGraph
|
|
1687
|
+
targetGraph: data.targetGraph,
|
|
1688
|
+
migrations: compiledMigrations,
|
|
1689
|
+
migrationWriteBack: data.migrationWriteBack
|
|
1065
1690
|
});
|
|
1066
1691
|
}
|
|
1067
1692
|
}
|
|
@@ -1077,14 +1702,14 @@ var GraphClientImpl = class _GraphClientImpl {
|
|
|
1077
1702
|
this.db = db;
|
|
1078
1703
|
this.scopePath = scopePath;
|
|
1079
1704
|
this.adapter = createFirestoreAdapter(db, collectionPath);
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
'Cannot provide both "registry" and "registryMode". Use "registry" for static mode or "registryMode" for dynamic mode.'
|
|
1083
|
-
);
|
|
1084
|
-
}
|
|
1705
|
+
this.globalWriteBack = options?.migrationWriteBack ?? "off";
|
|
1706
|
+
this.migrationSandbox = options?.migrationSandbox;
|
|
1085
1707
|
if (options?.registryMode) {
|
|
1086
1708
|
this.dynamicConfig = options.registryMode;
|
|
1087
1709
|
this.bootstrapRegistry = createBootstrapRegistry();
|
|
1710
|
+
if (options.registry) {
|
|
1711
|
+
this.staticRegistry = options.registry;
|
|
1712
|
+
}
|
|
1088
1713
|
const metaCollectionPath = options.registryMode.collection;
|
|
1089
1714
|
if (metaCollectionPath && metaCollectionPath !== collectionPath) {
|
|
1090
1715
|
this.metaAdapter = createFirestoreAdapter(db, metaCollectionPath);
|
|
@@ -1130,24 +1755,29 @@ var GraphClientImpl = class _GraphClientImpl {
|
|
|
1130
1755
|
metaPipelineAdapter;
|
|
1131
1756
|
// Subgraph scope tracking
|
|
1132
1757
|
scopePath;
|
|
1758
|
+
// Migration settings
|
|
1759
|
+
globalWriteBack;
|
|
1760
|
+
migrationSandbox;
|
|
1133
1761
|
// ---------------------------------------------------------------------------
|
|
1134
1762
|
// Registry routing
|
|
1135
1763
|
// ---------------------------------------------------------------------------
|
|
1136
1764
|
/**
|
|
1137
1765
|
* Get the appropriate registry for validating a write to the given type.
|
|
1138
1766
|
*
|
|
1139
|
-
* - Static mode: returns staticRegistry (or undefined if none set)
|
|
1140
|
-
* - Dynamic mode:
|
|
1767
|
+
* - Static-only mode: returns staticRegistry (or undefined if none set)
|
|
1768
|
+
* - Dynamic mode (pure or merged):
|
|
1141
1769
|
* - Meta-types (nodeType, edgeType): validated against bootstrapRegistry
|
|
1142
1770
|
* - Domain types: validated against dynamicRegistry (falls back to
|
|
1143
1771
|
* bootstrapRegistry which rejects unknown types)
|
|
1772
|
+
* - Merged mode: dynamicRegistry is a merged wrapper (static + dynamic
|
|
1773
|
+
* extension), so static entries take priority automatically.
|
|
1144
1774
|
*/
|
|
1145
1775
|
getRegistryForType(aType) {
|
|
1146
1776
|
if (!this.dynamicConfig) return this.staticRegistry;
|
|
1147
1777
|
if (aType === META_NODE_TYPE || aType === META_EDGE_TYPE) {
|
|
1148
1778
|
return this.bootstrapRegistry;
|
|
1149
1779
|
}
|
|
1150
|
-
return this.dynamicRegistry ?? this.bootstrapRegistry;
|
|
1780
|
+
return this.dynamicRegistry ?? this.staticRegistry ?? this.bootstrapRegistry;
|
|
1151
1781
|
}
|
|
1152
1782
|
/**
|
|
1153
1783
|
* Get the Firestore adapter for writing the given type.
|
|
@@ -1161,13 +1791,13 @@ var GraphClientImpl = class _GraphClientImpl {
|
|
|
1161
1791
|
}
|
|
1162
1792
|
/**
|
|
1163
1793
|
* Get the combined registry for transaction/batch context.
|
|
1164
|
-
* In static mode, returns staticRegistry.
|
|
1794
|
+
* In static-only mode, returns staticRegistry.
|
|
1165
1795
|
* In dynamic mode, returns dynamicRegistry (which includes bootstrap entries)
|
|
1166
|
-
* or
|
|
1796
|
+
* or falls back to staticRegistry (merged mode) or bootstrapRegistry.
|
|
1167
1797
|
*/
|
|
1168
1798
|
getCombinedRegistry() {
|
|
1169
1799
|
if (!this.dynamicConfig) return this.staticRegistry;
|
|
1170
|
-
return this.dynamicRegistry ?? this.bootstrapRegistry;
|
|
1800
|
+
return this.dynamicRegistry ?? this.staticRegistry ?? this.bootstrapRegistry;
|
|
1171
1801
|
}
|
|
1172
1802
|
// ---------------------------------------------------------------------------
|
|
1173
1803
|
// Query dispatch
|
|
@@ -1197,37 +1827,114 @@ var GraphClientImpl = class _GraphClientImpl {
|
|
|
1197
1827
|
console.warn(`[firegraph] Query safety warning: ${result.reason}`);
|
|
1198
1828
|
}
|
|
1199
1829
|
// ---------------------------------------------------------------------------
|
|
1830
|
+
// Migration helpers
|
|
1831
|
+
// ---------------------------------------------------------------------------
|
|
1832
|
+
/**
|
|
1833
|
+
* Apply migration to a single record. Returns the (possibly migrated)
|
|
1834
|
+
* record and triggers write-back if applicable.
|
|
1835
|
+
*/
|
|
1836
|
+
async applyMigration(record, docId) {
|
|
1837
|
+
const registry = this.getCombinedRegistry();
|
|
1838
|
+
if (!registry) return record;
|
|
1839
|
+
const result = await migrateRecord(record, registry, this.globalWriteBack);
|
|
1840
|
+
if (result.migrated) {
|
|
1841
|
+
this.handleWriteBack(result, docId);
|
|
1842
|
+
}
|
|
1843
|
+
return result.record;
|
|
1844
|
+
}
|
|
1845
|
+
/**
|
|
1846
|
+
* Apply migrations to an array of records. Returns all records
|
|
1847
|
+
* (migrated where applicable) and triggers write-backs.
|
|
1848
|
+
*/
|
|
1849
|
+
async applyMigrations(records) {
|
|
1850
|
+
const registry = this.getCombinedRegistry();
|
|
1851
|
+
if (!registry || records.length === 0) return records;
|
|
1852
|
+
const results = await migrateRecords(records, registry, this.globalWriteBack);
|
|
1853
|
+
for (const result of results) {
|
|
1854
|
+
if (result.migrated) {
|
|
1855
|
+
const docId = result.record.axbType === NODE_RELATION ? computeNodeDocId(result.record.aUid) : computeEdgeDocId(result.record.aUid, result.record.axbType, result.record.bUid);
|
|
1856
|
+
this.handleWriteBack(result, docId);
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
return results.map((r) => r.record);
|
|
1860
|
+
}
|
|
1861
|
+
/**
|
|
1862
|
+
* Handle write-back for a migrated record based on the resolved mode.
|
|
1863
|
+
*
|
|
1864
|
+
* Both `'eager'` and `'background'` are fire-and-forget (not awaited by
|
|
1865
|
+
* the caller). The difference is logging level on failure:
|
|
1866
|
+
* - `eager`: logs an error via `console.error`
|
|
1867
|
+
* - `background`: logs a warning via `console.warn`
|
|
1868
|
+
*
|
|
1869
|
+
* For truly synchronous write-back guarantees, use transactions — the
|
|
1870
|
+
* `GraphTransactionImpl` performs write-back inline within the transaction.
|
|
1871
|
+
*/
|
|
1872
|
+
handleWriteBack(result, docId) {
|
|
1873
|
+
if (result.writeBack === "off") return;
|
|
1874
|
+
const doWriteBack = async () => {
|
|
1875
|
+
try {
|
|
1876
|
+
const update = {
|
|
1877
|
+
data: deserializeFirestoreTypes(result.record.data, this.db),
|
|
1878
|
+
updatedAt: import_firestore5.FieldValue.serverTimestamp()
|
|
1879
|
+
};
|
|
1880
|
+
if (result.record.v !== void 0) {
|
|
1881
|
+
update.v = result.record.v;
|
|
1882
|
+
}
|
|
1883
|
+
await this.adapter.updateDoc(docId, update);
|
|
1884
|
+
} catch (err) {
|
|
1885
|
+
const msg = `[firegraph] Migration write-back failed for ${docId}: ${err.message}`;
|
|
1886
|
+
if (result.writeBack === "eager") {
|
|
1887
|
+
console.error(msg);
|
|
1888
|
+
} else {
|
|
1889
|
+
console.warn(msg);
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
};
|
|
1893
|
+
void doWriteBack();
|
|
1894
|
+
}
|
|
1895
|
+
// ---------------------------------------------------------------------------
|
|
1200
1896
|
// GraphReader
|
|
1201
1897
|
// ---------------------------------------------------------------------------
|
|
1202
1898
|
async getNode(uid) {
|
|
1203
1899
|
const docId = computeNodeDocId(uid);
|
|
1204
|
-
|
|
1900
|
+
const record = await this.adapter.getDoc(docId);
|
|
1901
|
+
if (!record) return null;
|
|
1902
|
+
return this.applyMigration(record, docId);
|
|
1205
1903
|
}
|
|
1206
1904
|
async getEdge(aUid, axbType, bUid) {
|
|
1207
1905
|
const docId = computeEdgeDocId(aUid, axbType, bUid);
|
|
1208
|
-
|
|
1906
|
+
const record = await this.adapter.getDoc(docId);
|
|
1907
|
+
if (!record) return null;
|
|
1908
|
+
return this.applyMigration(record, docId);
|
|
1209
1909
|
}
|
|
1210
1910
|
async edgeExists(aUid, axbType, bUid) {
|
|
1211
|
-
const
|
|
1911
|
+
const docId = computeEdgeDocId(aUid, axbType, bUid);
|
|
1912
|
+
const record = await this.adapter.getDoc(docId);
|
|
1212
1913
|
return record !== null;
|
|
1213
1914
|
}
|
|
1214
1915
|
async findEdges(params) {
|
|
1215
1916
|
const plan = buildEdgeQueryPlan(params);
|
|
1917
|
+
let records;
|
|
1216
1918
|
if (plan.strategy === "get") {
|
|
1217
1919
|
const record = await this.adapter.getDoc(plan.docId);
|
|
1218
|
-
|
|
1920
|
+
records = record ? [record] : [];
|
|
1921
|
+
} else {
|
|
1922
|
+
this.checkQuerySafety(plan.filters, params.allowCollectionScan);
|
|
1923
|
+
records = await this.executeQuery(plan.filters, plan.options);
|
|
1219
1924
|
}
|
|
1220
|
-
this.
|
|
1221
|
-
return this.executeQuery(plan.filters, plan.options);
|
|
1925
|
+
return this.applyMigrations(records);
|
|
1222
1926
|
}
|
|
1223
1927
|
async findNodes(params) {
|
|
1224
1928
|
const plan = buildNodeQueryPlan(params);
|
|
1929
|
+
let records;
|
|
1225
1930
|
if (plan.strategy === "get") {
|
|
1226
1931
|
const record = await this.adapter.getDoc(plan.docId);
|
|
1227
|
-
|
|
1932
|
+
records = record ? [record] : [];
|
|
1933
|
+
} else {
|
|
1934
|
+
this.checkQuerySafety(plan.filters, params.allowCollectionScan);
|
|
1935
|
+
records = await this.executeQuery(plan.filters, plan.options);
|
|
1228
1936
|
}
|
|
1229
|
-
this.
|
|
1230
|
-
return this.executeQuery(plan.filters, plan.options);
|
|
1937
|
+
return this.applyMigrations(records);
|
|
1231
1938
|
}
|
|
1232
1939
|
// ---------------------------------------------------------------------------
|
|
1233
1940
|
// GraphWriter
|
|
@@ -1240,6 +1947,12 @@ var GraphClientImpl = class _GraphClientImpl {
|
|
|
1240
1947
|
const adapter = this.getAdapterForType(aType);
|
|
1241
1948
|
const docId = computeNodeDocId(uid);
|
|
1242
1949
|
const record = buildNodeRecord(aType, uid, data);
|
|
1950
|
+
if (registry) {
|
|
1951
|
+
const entry = registry.lookup(aType, NODE_RELATION, aType);
|
|
1952
|
+
if (entry?.schemaVersion && entry.schemaVersion > 0) {
|
|
1953
|
+
record.v = entry.schemaVersion;
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1243
1956
|
await adapter.setDoc(docId, record);
|
|
1244
1957
|
}
|
|
1245
1958
|
async putEdge(aType, aUid, axbType, bType, bUid, data) {
|
|
@@ -1250,13 +1963,19 @@ var GraphClientImpl = class _GraphClientImpl {
|
|
|
1250
1963
|
const adapter = this.getAdapterForType(aType);
|
|
1251
1964
|
const docId = computeEdgeDocId(aUid, axbType, bUid);
|
|
1252
1965
|
const record = buildEdgeRecord(aType, aUid, axbType, bType, bUid, data);
|
|
1966
|
+
if (registry) {
|
|
1967
|
+
const entry = registry.lookup(aType, axbType, bType);
|
|
1968
|
+
if (entry?.schemaVersion && entry.schemaVersion > 0) {
|
|
1969
|
+
record.v = entry.schemaVersion;
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1253
1972
|
await adapter.setDoc(docId, record);
|
|
1254
1973
|
}
|
|
1255
1974
|
async updateNode(uid, data) {
|
|
1256
1975
|
const docId = computeNodeDocId(uid);
|
|
1257
1976
|
await this.adapter.updateDoc(docId, {
|
|
1258
1977
|
...data,
|
|
1259
|
-
updatedAt:
|
|
1978
|
+
updatedAt: import_firestore5.FieldValue.serverTimestamp()
|
|
1260
1979
|
});
|
|
1261
1980
|
}
|
|
1262
1981
|
async removeNode(uid) {
|
|
@@ -1277,7 +1996,7 @@ var GraphClientImpl = class _GraphClientImpl {
|
|
|
1277
1996
|
this.adapter.collectionPath,
|
|
1278
1997
|
firestoreTx
|
|
1279
1998
|
);
|
|
1280
|
-
const graphTx = new GraphTransactionImpl(adapter, this.getCombinedRegistry(), this.scanProtection, this.scopePath);
|
|
1999
|
+
const graphTx = new GraphTransactionImpl(adapter, this.getCombinedRegistry(), this.scanProtection, this.scopePath, this.globalWriteBack, this.db);
|
|
1281
2000
|
return fn(graphTx);
|
|
1282
2001
|
});
|
|
1283
2002
|
}
|
|
@@ -1309,7 +2028,9 @@ var GraphClientImpl = class _GraphClientImpl {
|
|
|
1309
2028
|
{
|
|
1310
2029
|
registry: this.getCombinedRegistry(),
|
|
1311
2030
|
queryMode: this.queryMode === "pipeline" ? "pipeline" : "standard",
|
|
1312
|
-
scanProtection: this.scanProtection
|
|
2031
|
+
scanProtection: this.scanProtection,
|
|
2032
|
+
migrationWriteBack: this.globalWriteBack,
|
|
2033
|
+
migrationSandbox: this.migrationSandbox
|
|
1313
2034
|
},
|
|
1314
2035
|
newScopePath
|
|
1315
2036
|
);
|
|
@@ -1339,7 +2060,8 @@ var GraphClientImpl = class _GraphClientImpl {
|
|
|
1339
2060
|
q = q.limit(plan.options.limit);
|
|
1340
2061
|
}
|
|
1341
2062
|
const snap = await q.get();
|
|
1342
|
-
|
|
2063
|
+
const records = snap.docs.map((doc) => doc.data());
|
|
2064
|
+
return this.applyMigrations(records);
|
|
1343
2065
|
}
|
|
1344
2066
|
// ---------------------------------------------------------------------------
|
|
1345
2067
|
// Bulk operations
|
|
@@ -1364,6 +2086,11 @@ var GraphClientImpl = class _GraphClientImpl {
|
|
|
1364
2086
|
`Cannot define type "${name}": this name is reserved for the meta-registry.`
|
|
1365
2087
|
);
|
|
1366
2088
|
}
|
|
2089
|
+
if (this.staticRegistry?.lookup(name, NODE_RELATION, name)) {
|
|
2090
|
+
throw new DynamicRegistryError(
|
|
2091
|
+
`Cannot define node type "${name}": already defined in the static registry.`
|
|
2092
|
+
);
|
|
2093
|
+
}
|
|
1367
2094
|
const uid = generateDeterministicUid(META_NODE_TYPE, name);
|
|
1368
2095
|
const data = { name, jsonSchema };
|
|
1369
2096
|
if (description !== void 0) data.description = description;
|
|
@@ -1372,6 +2099,10 @@ var GraphClientImpl = class _GraphClientImpl {
|
|
|
1372
2099
|
if (options?.viewTemplate !== void 0) data.viewTemplate = options.viewTemplate;
|
|
1373
2100
|
if (options?.viewCss !== void 0) data.viewCss = options.viewCss;
|
|
1374
2101
|
if (options?.allowedIn !== void 0) data.allowedIn = options.allowedIn;
|
|
2102
|
+
if (options?.migrationWriteBack !== void 0) data.migrationWriteBack = options.migrationWriteBack;
|
|
2103
|
+
if (options?.migrations !== void 0) {
|
|
2104
|
+
data.migrations = await this.serializeMigrations(options.migrations);
|
|
2105
|
+
}
|
|
1375
2106
|
await this.putNode(META_NODE_TYPE, uid, data);
|
|
1376
2107
|
}
|
|
1377
2108
|
async defineEdgeType(name, topology, jsonSchema, description, options) {
|
|
@@ -1385,6 +2116,19 @@ var GraphClientImpl = class _GraphClientImpl {
|
|
|
1385
2116
|
`Cannot define type "${name}": this name is reserved for the meta-registry.`
|
|
1386
2117
|
);
|
|
1387
2118
|
}
|
|
2119
|
+
if (this.staticRegistry) {
|
|
2120
|
+
const fromTypes = Array.isArray(topology.from) ? topology.from : [topology.from];
|
|
2121
|
+
const toTypes = Array.isArray(topology.to) ? topology.to : [topology.to];
|
|
2122
|
+
for (const aType of fromTypes) {
|
|
2123
|
+
for (const bType of toTypes) {
|
|
2124
|
+
if (this.staticRegistry.lookup(aType, name, bType)) {
|
|
2125
|
+
throw new DynamicRegistryError(
|
|
2126
|
+
`Cannot define edge type "${name}" for (${aType}) -> (${bType}): already defined in the static registry.`
|
|
2127
|
+
);
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
2131
|
+
}
|
|
1388
2132
|
const uid = generateDeterministicUid(META_EDGE_TYPE, name);
|
|
1389
2133
|
const data = {
|
|
1390
2134
|
name,
|
|
@@ -1400,6 +2144,10 @@ var GraphClientImpl = class _GraphClientImpl {
|
|
|
1400
2144
|
if (options?.viewTemplate !== void 0) data.viewTemplate = options.viewTemplate;
|
|
1401
2145
|
if (options?.viewCss !== void 0) data.viewCss = options.viewCss;
|
|
1402
2146
|
if (options?.allowedIn !== void 0) data.allowedIn = options.allowedIn;
|
|
2147
|
+
if (options?.migrationWriteBack !== void 0) data.migrationWriteBack = options.migrationWriteBack;
|
|
2148
|
+
if (options?.migrations !== void 0) {
|
|
2149
|
+
data.migrations = await this.serializeMigrations(options.migrations);
|
|
2150
|
+
}
|
|
1403
2151
|
await this.putNode(META_EDGE_TYPE, uid, data);
|
|
1404
2152
|
}
|
|
1405
2153
|
async reloadRegistry() {
|
|
@@ -1409,7 +2157,27 @@ var GraphClientImpl = class _GraphClientImpl {
|
|
|
1409
2157
|
);
|
|
1410
2158
|
}
|
|
1411
2159
|
const reader = this.createMetaReader();
|
|
1412
|
-
|
|
2160
|
+
const dynamicOnly = await createRegistryFromGraph(reader, this.migrationSandbox);
|
|
2161
|
+
if (this.staticRegistry) {
|
|
2162
|
+
this.dynamicRegistry = createMergedRegistry(this.staticRegistry, dynamicOnly);
|
|
2163
|
+
} else {
|
|
2164
|
+
this.dynamicRegistry = dynamicOnly;
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
/**
|
|
2168
|
+
* Serialize migration steps for storage in Firestore.
|
|
2169
|
+
* Function objects are converted via `.toString()`; strings are stored as-is.
|
|
2170
|
+
* Each migration is validated at define-time by pre-compiling in the sandbox.
|
|
2171
|
+
*/
|
|
2172
|
+
async serializeMigrations(migrations) {
|
|
2173
|
+
const result = migrations.map((m) => {
|
|
2174
|
+
const source = typeof m.up === "function" ? m.up.toString() : m.up;
|
|
2175
|
+
return { fromVersion: m.fromVersion, toVersion: m.toVersion, up: source };
|
|
2176
|
+
});
|
|
2177
|
+
await Promise.all(
|
|
2178
|
+
result.map((m) => precompileSource(m.up, this.migrationSandbox))
|
|
2179
|
+
);
|
|
2180
|
+
return result;
|
|
1413
2181
|
}
|
|
1414
2182
|
/**
|
|
1415
2183
|
* Create a GraphReader for the meta-collection.
|
|
@@ -1777,7 +2545,7 @@ function resolveView(resolverConfig, availableViewNames, context) {
|
|
|
1777
2545
|
var import_node_fs = require("fs");
|
|
1778
2546
|
var import_node_module = require("module");
|
|
1779
2547
|
var import_node_path = require("path");
|
|
1780
|
-
var
|
|
2548
|
+
var import_meta2 = {};
|
|
1781
2549
|
var DiscoveryError = class extends FiregraphError {
|
|
1782
2550
|
constructor(message) {
|
|
1783
2551
|
super(message, "DISCOVERY_ERROR");
|
|
@@ -1816,7 +2584,7 @@ function loadSchema(dir, entityLabel) {
|
|
|
1816
2584
|
var _jiti;
|
|
1817
2585
|
function getJiti() {
|
|
1818
2586
|
if (!_jiti) {
|
|
1819
|
-
const base = typeof __filename !== "undefined" ? __filename :
|
|
2587
|
+
const base = typeof __filename !== "undefined" ? __filename : import_meta2.url;
|
|
1820
2588
|
const esmRequire = (0, import_node_module.createRequire)(base);
|
|
1821
2589
|
const { createJiti } = esmRequire("jiti");
|
|
1822
2590
|
_jiti = createJiti(base, { interopDefault: true });
|
|
@@ -1849,11 +2617,39 @@ function findViewsFile(dir) {
|
|
|
1849
2617
|
}
|
|
1850
2618
|
return void 0;
|
|
1851
2619
|
}
|
|
2620
|
+
var MIGRATION_EXTENSIONS = [".ts", ".js", ".mts", ".mjs"];
|
|
2621
|
+
function findMigrationsFile(dir) {
|
|
2622
|
+
for (const ext of MIGRATION_EXTENSIONS) {
|
|
2623
|
+
const candidate = (0, import_node_path.join)(dir, `migrations${ext}`);
|
|
2624
|
+
if ((0, import_node_fs.existsSync)(candidate)) return candidate;
|
|
2625
|
+
}
|
|
2626
|
+
return void 0;
|
|
2627
|
+
}
|
|
2628
|
+
function loadMigrations(filePath, entityLabel) {
|
|
2629
|
+
try {
|
|
2630
|
+
const jiti = getJiti();
|
|
2631
|
+
const mod = jiti(filePath);
|
|
2632
|
+
const migrations = mod && typeof mod === "object" && "default" in mod ? mod.default : mod;
|
|
2633
|
+
if (!Array.isArray(migrations)) {
|
|
2634
|
+
throw new DiscoveryError(
|
|
2635
|
+
`Migrations file ${filePath} for ${entityLabel} must default-export an array of MigrationStep.`
|
|
2636
|
+
);
|
|
2637
|
+
}
|
|
2638
|
+
return migrations;
|
|
2639
|
+
} catch (err) {
|
|
2640
|
+
if (err instanceof DiscoveryError) throw err;
|
|
2641
|
+
throw new DiscoveryError(
|
|
2642
|
+
`Failed to load migrations ${filePath} for ${entityLabel}: ${err.message}`
|
|
2643
|
+
);
|
|
2644
|
+
}
|
|
2645
|
+
}
|
|
1852
2646
|
function loadNodeEntity(dir, name) {
|
|
1853
2647
|
const schema = loadSchema(dir, `node type "${name}"`);
|
|
1854
2648
|
const meta = readJsonIfExists((0, import_node_path.join)(dir, "meta.json"));
|
|
1855
2649
|
const sampleData = readJsonIfExists((0, import_node_path.join)(dir, "sample.json"));
|
|
1856
2650
|
const viewsPath = findViewsFile(dir);
|
|
2651
|
+
const migrationsPath = findMigrationsFile(dir);
|
|
2652
|
+
const migrations = migrationsPath ? loadMigrations(migrationsPath, `node type "${name}"`) : void 0;
|
|
1857
2653
|
return {
|
|
1858
2654
|
kind: "node",
|
|
1859
2655
|
name,
|
|
@@ -1864,7 +2660,9 @@ function loadNodeEntity(dir, name) {
|
|
|
1864
2660
|
viewDefaults: meta?.viewDefaults,
|
|
1865
2661
|
viewsPath,
|
|
1866
2662
|
sampleData,
|
|
1867
|
-
allowedIn: meta?.allowedIn
|
|
2663
|
+
allowedIn: meta?.allowedIn,
|
|
2664
|
+
migrations,
|
|
2665
|
+
migrationWriteBack: meta?.migrationWriteBack
|
|
1868
2666
|
};
|
|
1869
2667
|
}
|
|
1870
2668
|
function loadEdgeEntity(dir, name) {
|
|
@@ -1889,6 +2687,8 @@ function loadEdgeEntity(dir, name) {
|
|
|
1889
2687
|
const meta = readJsonIfExists((0, import_node_path.join)(dir, "meta.json"));
|
|
1890
2688
|
const sampleData = readJsonIfExists((0, import_node_path.join)(dir, "sample.json"));
|
|
1891
2689
|
const viewsPath = findViewsFile(dir);
|
|
2690
|
+
const migrationsPath = findMigrationsFile(dir);
|
|
2691
|
+
const migrations = migrationsPath ? loadMigrations(migrationsPath, `edge type "${name}"`) : void 0;
|
|
1892
2692
|
return {
|
|
1893
2693
|
kind: "edge",
|
|
1894
2694
|
name,
|
|
@@ -1901,7 +2701,9 @@ function loadEdgeEntity(dir, name) {
|
|
|
1901
2701
|
viewsPath,
|
|
1902
2702
|
sampleData,
|
|
1903
2703
|
allowedIn: meta?.allowedIn,
|
|
1904
|
-
targetGraph: topology.targetGraph ?? meta?.targetGraph
|
|
2704
|
+
targetGraph: topology.targetGraph ?? meta?.targetGraph,
|
|
2705
|
+
migrations,
|
|
2706
|
+
migrationWriteBack: meta?.migrationWriteBack
|
|
1905
2707
|
};
|
|
1906
2708
|
}
|
|
1907
2709
|
function getSubdirectories(dir) {
|
|
@@ -2412,6 +3214,7 @@ var QueryClient = class {
|
|
|
2412
3214
|
InvalidQueryError,
|
|
2413
3215
|
META_EDGE_TYPE,
|
|
2414
3216
|
META_NODE_TYPE,
|
|
3217
|
+
MigrationError,
|
|
2415
3218
|
NODE_TYPE_SCHEMA,
|
|
2416
3219
|
NodeNotFoundError,
|
|
2417
3220
|
QueryClient,
|
|
@@ -2419,33 +3222,47 @@ var QueryClient = class {
|
|
|
2419
3222
|
QuerySafetyError,
|
|
2420
3223
|
RegistryScopeError,
|
|
2421
3224
|
RegistryViolationError,
|
|
3225
|
+
SERIALIZATION_TAG,
|
|
2422
3226
|
TraversalError,
|
|
2423
3227
|
ValidationError,
|
|
2424
3228
|
analyzeQuerySafety,
|
|
3229
|
+
applyMigrationChain,
|
|
2425
3230
|
buildEdgeQueryPlan,
|
|
2426
3231
|
buildEdgeRecord,
|
|
2427
3232
|
buildNodeQueryPlan,
|
|
2428
3233
|
buildNodeRecord,
|
|
3234
|
+
compileMigrationFn,
|
|
3235
|
+
compileMigrations,
|
|
2429
3236
|
compileSchema,
|
|
2430
3237
|
computeEdgeDocId,
|
|
2431
3238
|
computeNodeDocId,
|
|
2432
3239
|
createBootstrapRegistry,
|
|
2433
3240
|
createGraphClient,
|
|
3241
|
+
createMergedRegistry,
|
|
2434
3242
|
createRegistry,
|
|
2435
3243
|
createRegistryFromGraph,
|
|
2436
3244
|
createTraversal,
|
|
3245
|
+
defaultExecutor,
|
|
2437
3246
|
defineConfig,
|
|
2438
3247
|
defineViews,
|
|
3248
|
+
deserializeFirestoreTypes,
|
|
3249
|
+
destroySandboxWorker,
|
|
2439
3250
|
discoverEntities,
|
|
2440
3251
|
generateDeterministicUid,
|
|
2441
3252
|
generateId,
|
|
2442
3253
|
generateIndexConfig,
|
|
2443
3254
|
generateTypes,
|
|
2444
3255
|
isAncestorUid,
|
|
3256
|
+
isTaggedValue,
|
|
2445
3257
|
jsonSchemaToFieldMeta,
|
|
2446
3258
|
matchScope,
|
|
2447
3259
|
matchScopeAny,
|
|
3260
|
+
migrateRecord,
|
|
3261
|
+
migrateRecords,
|
|
3262
|
+
precompileSource,
|
|
2448
3263
|
resolveAncestorCollection,
|
|
2449
|
-
resolveView
|
|
3264
|
+
resolveView,
|
|
3265
|
+
serializeFirestoreTypes,
|
|
3266
|
+
validateMigrationChain
|
|
2450
3267
|
});
|
|
2451
3268
|
//# sourceMappingURL=index.cjs.map
|