@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.js
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
} from "./chunk-KFA7G37W.js";
|
|
8
8
|
|
|
9
9
|
// src/client.ts
|
|
10
|
-
import { FieldValue as
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
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:
|
|
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
|
|
923
|
+
import { createHash as createHash3 } from "crypto";
|
|
646
924
|
|
|
647
925
|
// src/json-schema.ts
|
|
648
926
|
import Ajv from "ajv";
|
|
@@ -773,6 +1051,9 @@ function matchSegments(path, pi, pattern, qi) {
|
|
|
773
1051
|
function tripleKey(aType, axbType, bType) {
|
|
774
1052
|
return `${aType}:${axbType}:${bType}`;
|
|
775
1053
|
}
|
|
1054
|
+
function tripleKeyFor(e) {
|
|
1055
|
+
return tripleKey(e.aType, e.axbType, e.bType);
|
|
1056
|
+
}
|
|
776
1057
|
function createRegistry(input) {
|
|
777
1058
|
const map = /* @__PURE__ */ new Map();
|
|
778
1059
|
let entries;
|
|
@@ -788,6 +1069,13 @@ function createRegistry(input) {
|
|
|
788
1069
|
`Entry (${entry.aType}) -[${entry.axbType}]-> (${entry.bType}) has invalid targetGraph "${entry.targetGraph}" \u2014 must be a single segment (no "/")`
|
|
789
1070
|
);
|
|
790
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
|
+
}
|
|
791
1079
|
const key = tripleKey(entry.aType, entry.axbType, entry.bType);
|
|
792
1080
|
const validator = entry.jsonSchema ? compileSchema(entry.jsonSchema, `(${entry.aType}) -[${entry.axbType}]-> (${entry.bType})`) : void 0;
|
|
793
1081
|
map.set(key, { entry, validate: validator });
|
|
@@ -839,6 +1127,45 @@ function createRegistry(input) {
|
|
|
839
1127
|
}
|
|
840
1128
|
};
|
|
841
1129
|
}
|
|
1130
|
+
function createMergedRegistry(base, extension) {
|
|
1131
|
+
const baseKeys = new Set(base.entries().map(tripleKeyFor));
|
|
1132
|
+
return {
|
|
1133
|
+
lookup(aType, axbType, bType) {
|
|
1134
|
+
return base.lookup(aType, axbType, bType) ?? extension.lookup(aType, axbType, bType);
|
|
1135
|
+
},
|
|
1136
|
+
lookupByAxbType(axbType) {
|
|
1137
|
+
const baseResults = base.lookupByAxbType(axbType);
|
|
1138
|
+
const extResults = extension.lookupByAxbType(axbType);
|
|
1139
|
+
if (extResults.length === 0) return baseResults;
|
|
1140
|
+
if (baseResults.length === 0) return extResults;
|
|
1141
|
+
const seen = new Set(baseResults.map(tripleKeyFor));
|
|
1142
|
+
const merged = [...baseResults];
|
|
1143
|
+
for (const entry of extResults) {
|
|
1144
|
+
if (!seen.has(tripleKeyFor(entry))) {
|
|
1145
|
+
merged.push(entry);
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
return Object.freeze(merged);
|
|
1149
|
+
},
|
|
1150
|
+
validate(aType, axbType, bType, data, scopePath) {
|
|
1151
|
+
if (baseKeys.has(tripleKey(aType, axbType, bType))) {
|
|
1152
|
+
return base.validate(aType, axbType, bType, data, scopePath);
|
|
1153
|
+
}
|
|
1154
|
+
return extension.validate(aType, axbType, bType, data, scopePath);
|
|
1155
|
+
},
|
|
1156
|
+
entries() {
|
|
1157
|
+
const extEntries = extension.entries();
|
|
1158
|
+
if (extEntries.length === 0) return base.entries();
|
|
1159
|
+
const merged = [...base.entries()];
|
|
1160
|
+
for (const entry of extEntries) {
|
|
1161
|
+
if (!baseKeys.has(tripleKeyFor(entry))) {
|
|
1162
|
+
merged.push(entry);
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
return Object.freeze(merged);
|
|
1166
|
+
}
|
|
1167
|
+
};
|
|
1168
|
+
}
|
|
842
1169
|
function discoveryToEntries(discovery) {
|
|
843
1170
|
const entries = [];
|
|
844
1171
|
for (const [name, entity] of discovery.nodes) {
|
|
@@ -850,7 +1177,9 @@ function discoveryToEntries(discovery) {
|
|
|
850
1177
|
description: entity.description,
|
|
851
1178
|
titleField: entity.titleField,
|
|
852
1179
|
subtitleField: entity.subtitleField,
|
|
853
|
-
allowedIn: entity.allowedIn
|
|
1180
|
+
allowedIn: entity.allowedIn,
|
|
1181
|
+
migrations: entity.migrations,
|
|
1182
|
+
migrationWriteBack: entity.migrationWriteBack
|
|
854
1183
|
});
|
|
855
1184
|
}
|
|
856
1185
|
for (const [axbType, entity] of discovery.edges) {
|
|
@@ -876,7 +1205,9 @@ function discoveryToEntries(discovery) {
|
|
|
876
1205
|
titleField: entity.titleField,
|
|
877
1206
|
subtitleField: entity.subtitleField,
|
|
878
1207
|
allowedIn: entity.allowedIn,
|
|
879
|
-
targetGraph: resolvedTargetGraph
|
|
1208
|
+
targetGraph: resolvedTargetGraph,
|
|
1209
|
+
migrations: entity.migrations,
|
|
1210
|
+
migrationWriteBack: entity.migrationWriteBack
|
|
880
1211
|
});
|
|
881
1212
|
}
|
|
882
1213
|
}
|
|
@@ -884,9 +1215,258 @@ function discoveryToEntries(discovery) {
|
|
|
884
1215
|
return entries;
|
|
885
1216
|
}
|
|
886
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
|
+
|
|
887
1457
|
// src/dynamic-registry.ts
|
|
888
1458
|
var META_NODE_TYPE = "nodeType";
|
|
889
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
|
+
};
|
|
890
1470
|
var NODE_TYPE_SCHEMA = {
|
|
891
1471
|
type: "object",
|
|
892
1472
|
required: ["name", "jsonSchema"],
|
|
@@ -898,7 +1478,10 @@ var NODE_TYPE_SCHEMA = {
|
|
|
898
1478
|
subtitleField: { type: "string" },
|
|
899
1479
|
viewTemplate: { type: "string" },
|
|
900
1480
|
viewCss: { type: "string" },
|
|
901
|
-
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"] }
|
|
902
1485
|
},
|
|
903
1486
|
additionalProperties: false
|
|
904
1487
|
};
|
|
@@ -927,7 +1510,10 @@ var EDGE_TYPE_SCHEMA = {
|
|
|
927
1510
|
viewTemplate: { type: "string" },
|
|
928
1511
|
viewCss: { type: "string" },
|
|
929
1512
|
allowedIn: { type: "array", items: { type: "string", minLength: 1 } },
|
|
930
|
-
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"] }
|
|
931
1517
|
},
|
|
932
1518
|
additionalProperties: false
|
|
933
1519
|
};
|
|
@@ -951,15 +1537,33 @@ function createBootstrapRegistry() {
|
|
|
951
1537
|
return createRegistry([...BOOTSTRAP_ENTRIES]);
|
|
952
1538
|
}
|
|
953
1539
|
function generateDeterministicUid(metaType, name) {
|
|
954
|
-
const hash =
|
|
1540
|
+
const hash = createHash3("sha256").update(`${metaType}:${name}`).digest("base64url");
|
|
955
1541
|
return hash.slice(0, 21);
|
|
956
1542
|
}
|
|
957
|
-
async function createRegistryFromGraph(reader) {
|
|
1543
|
+
async function createRegistryFromGraph(reader, executor) {
|
|
958
1544
|
const [nodeTypes, edgeTypes] = await Promise.all([
|
|
959
1545
|
reader.findNodes({ aType: META_NODE_TYPE }),
|
|
960
1546
|
reader.findNodes({ aType: META_EDGE_TYPE })
|
|
961
1547
|
]);
|
|
962
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);
|
|
963
1567
|
for (const record of nodeTypes) {
|
|
964
1568
|
const data = record.data;
|
|
965
1569
|
entries.push({
|
|
@@ -970,13 +1574,16 @@ async function createRegistryFromGraph(reader) {
|
|
|
970
1574
|
description: data.description,
|
|
971
1575
|
titleField: data.titleField,
|
|
972
1576
|
subtitleField: data.subtitleField,
|
|
973
|
-
allowedIn: data.allowedIn
|
|
1577
|
+
allowedIn: data.allowedIn,
|
|
1578
|
+
migrations: data.migrations ? compileMigrations(data.migrations, executor) : void 0,
|
|
1579
|
+
migrationWriteBack: data.migrationWriteBack
|
|
974
1580
|
});
|
|
975
1581
|
}
|
|
976
1582
|
for (const record of edgeTypes) {
|
|
977
1583
|
const data = record.data;
|
|
978
1584
|
const fromTypes = Array.isArray(data.from) ? data.from : [data.from];
|
|
979
1585
|
const toTypes = Array.isArray(data.to) ? data.to : [data.to];
|
|
1586
|
+
const compiledMigrations = data.migrations ? compileMigrations(data.migrations, executor) : void 0;
|
|
980
1587
|
for (const aType of fromTypes) {
|
|
981
1588
|
for (const bType of toTypes) {
|
|
982
1589
|
entries.push({
|
|
@@ -989,7 +1596,9 @@ async function createRegistryFromGraph(reader) {
|
|
|
989
1596
|
titleField: data.titleField,
|
|
990
1597
|
subtitleField: data.subtitleField,
|
|
991
1598
|
allowedIn: data.allowedIn,
|
|
992
|
-
targetGraph: data.targetGraph
|
|
1599
|
+
targetGraph: data.targetGraph,
|
|
1600
|
+
migrations: compiledMigrations,
|
|
1601
|
+
migrationWriteBack: data.migrationWriteBack
|
|
993
1602
|
});
|
|
994
1603
|
}
|
|
995
1604
|
}
|
|
@@ -1005,14 +1614,14 @@ var GraphClientImpl = class _GraphClientImpl {
|
|
|
1005
1614
|
this.db = db;
|
|
1006
1615
|
this.scopePath = scopePath;
|
|
1007
1616
|
this.adapter = createFirestoreAdapter(db, collectionPath);
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
'Cannot provide both "registry" and "registryMode". Use "registry" for static mode or "registryMode" for dynamic mode.'
|
|
1011
|
-
);
|
|
1012
|
-
}
|
|
1617
|
+
this.globalWriteBack = options?.migrationWriteBack ?? "off";
|
|
1618
|
+
this.migrationSandbox = options?.migrationSandbox;
|
|
1013
1619
|
if (options?.registryMode) {
|
|
1014
1620
|
this.dynamicConfig = options.registryMode;
|
|
1015
1621
|
this.bootstrapRegistry = createBootstrapRegistry();
|
|
1622
|
+
if (options.registry) {
|
|
1623
|
+
this.staticRegistry = options.registry;
|
|
1624
|
+
}
|
|
1016
1625
|
const metaCollectionPath = options.registryMode.collection;
|
|
1017
1626
|
if (metaCollectionPath && metaCollectionPath !== collectionPath) {
|
|
1018
1627
|
this.metaAdapter = createFirestoreAdapter(db, metaCollectionPath);
|
|
@@ -1058,24 +1667,29 @@ var GraphClientImpl = class _GraphClientImpl {
|
|
|
1058
1667
|
metaPipelineAdapter;
|
|
1059
1668
|
// Subgraph scope tracking
|
|
1060
1669
|
scopePath;
|
|
1670
|
+
// Migration settings
|
|
1671
|
+
globalWriteBack;
|
|
1672
|
+
migrationSandbox;
|
|
1061
1673
|
// ---------------------------------------------------------------------------
|
|
1062
1674
|
// Registry routing
|
|
1063
1675
|
// ---------------------------------------------------------------------------
|
|
1064
1676
|
/**
|
|
1065
1677
|
* Get the appropriate registry for validating a write to the given type.
|
|
1066
1678
|
*
|
|
1067
|
-
* - Static mode: returns staticRegistry (or undefined if none set)
|
|
1068
|
-
* - Dynamic mode:
|
|
1679
|
+
* - Static-only mode: returns staticRegistry (or undefined if none set)
|
|
1680
|
+
* - Dynamic mode (pure or merged):
|
|
1069
1681
|
* - Meta-types (nodeType, edgeType): validated against bootstrapRegistry
|
|
1070
1682
|
* - Domain types: validated against dynamicRegistry (falls back to
|
|
1071
1683
|
* bootstrapRegistry which rejects unknown types)
|
|
1684
|
+
* - Merged mode: dynamicRegistry is a merged wrapper (static + dynamic
|
|
1685
|
+
* extension), so static entries take priority automatically.
|
|
1072
1686
|
*/
|
|
1073
1687
|
getRegistryForType(aType) {
|
|
1074
1688
|
if (!this.dynamicConfig) return this.staticRegistry;
|
|
1075
1689
|
if (aType === META_NODE_TYPE || aType === META_EDGE_TYPE) {
|
|
1076
1690
|
return this.bootstrapRegistry;
|
|
1077
1691
|
}
|
|
1078
|
-
return this.dynamicRegistry ?? this.bootstrapRegistry;
|
|
1692
|
+
return this.dynamicRegistry ?? this.staticRegistry ?? this.bootstrapRegistry;
|
|
1079
1693
|
}
|
|
1080
1694
|
/**
|
|
1081
1695
|
* Get the Firestore adapter for writing the given type.
|
|
@@ -1089,13 +1703,13 @@ var GraphClientImpl = class _GraphClientImpl {
|
|
|
1089
1703
|
}
|
|
1090
1704
|
/**
|
|
1091
1705
|
* Get the combined registry for transaction/batch context.
|
|
1092
|
-
* In static mode, returns staticRegistry.
|
|
1706
|
+
* In static-only mode, returns staticRegistry.
|
|
1093
1707
|
* In dynamic mode, returns dynamicRegistry (which includes bootstrap entries)
|
|
1094
|
-
* or
|
|
1708
|
+
* or falls back to staticRegistry (merged mode) or bootstrapRegistry.
|
|
1095
1709
|
*/
|
|
1096
1710
|
getCombinedRegistry() {
|
|
1097
1711
|
if (!this.dynamicConfig) return this.staticRegistry;
|
|
1098
|
-
return this.dynamicRegistry ?? this.bootstrapRegistry;
|
|
1712
|
+
return this.dynamicRegistry ?? this.staticRegistry ?? this.bootstrapRegistry;
|
|
1099
1713
|
}
|
|
1100
1714
|
// ---------------------------------------------------------------------------
|
|
1101
1715
|
// Query dispatch
|
|
@@ -1125,37 +1739,114 @@ var GraphClientImpl = class _GraphClientImpl {
|
|
|
1125
1739
|
console.warn(`[firegraph] Query safety warning: ${result.reason}`);
|
|
1126
1740
|
}
|
|
1127
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
|
+
// ---------------------------------------------------------------------------
|
|
1128
1808
|
// GraphReader
|
|
1129
1809
|
// ---------------------------------------------------------------------------
|
|
1130
1810
|
async getNode(uid) {
|
|
1131
1811
|
const docId = computeNodeDocId(uid);
|
|
1132
|
-
|
|
1812
|
+
const record = await this.adapter.getDoc(docId);
|
|
1813
|
+
if (!record) return null;
|
|
1814
|
+
return this.applyMigration(record, docId);
|
|
1133
1815
|
}
|
|
1134
1816
|
async getEdge(aUid, axbType, bUid) {
|
|
1135
1817
|
const docId = computeEdgeDocId(aUid, axbType, bUid);
|
|
1136
|
-
|
|
1818
|
+
const record = await this.adapter.getDoc(docId);
|
|
1819
|
+
if (!record) return null;
|
|
1820
|
+
return this.applyMigration(record, docId);
|
|
1137
1821
|
}
|
|
1138
1822
|
async edgeExists(aUid, axbType, bUid) {
|
|
1139
|
-
const
|
|
1823
|
+
const docId = computeEdgeDocId(aUid, axbType, bUid);
|
|
1824
|
+
const record = await this.adapter.getDoc(docId);
|
|
1140
1825
|
return record !== null;
|
|
1141
1826
|
}
|
|
1142
1827
|
async findEdges(params) {
|
|
1143
1828
|
const plan = buildEdgeQueryPlan(params);
|
|
1829
|
+
let records;
|
|
1144
1830
|
if (plan.strategy === "get") {
|
|
1145
1831
|
const record = await this.adapter.getDoc(plan.docId);
|
|
1146
|
-
|
|
1832
|
+
records = record ? [record] : [];
|
|
1833
|
+
} else {
|
|
1834
|
+
this.checkQuerySafety(plan.filters, params.allowCollectionScan);
|
|
1835
|
+
records = await this.executeQuery(plan.filters, plan.options);
|
|
1147
1836
|
}
|
|
1148
|
-
this.
|
|
1149
|
-
return this.executeQuery(plan.filters, plan.options);
|
|
1837
|
+
return this.applyMigrations(records);
|
|
1150
1838
|
}
|
|
1151
1839
|
async findNodes(params) {
|
|
1152
1840
|
const plan = buildNodeQueryPlan(params);
|
|
1841
|
+
let records;
|
|
1153
1842
|
if (plan.strategy === "get") {
|
|
1154
1843
|
const record = await this.adapter.getDoc(plan.docId);
|
|
1155
|
-
|
|
1844
|
+
records = record ? [record] : [];
|
|
1845
|
+
} else {
|
|
1846
|
+
this.checkQuerySafety(plan.filters, params.allowCollectionScan);
|
|
1847
|
+
records = await this.executeQuery(plan.filters, plan.options);
|
|
1156
1848
|
}
|
|
1157
|
-
this.
|
|
1158
|
-
return this.executeQuery(plan.filters, plan.options);
|
|
1849
|
+
return this.applyMigrations(records);
|
|
1159
1850
|
}
|
|
1160
1851
|
// ---------------------------------------------------------------------------
|
|
1161
1852
|
// GraphWriter
|
|
@@ -1168,6 +1859,12 @@ var GraphClientImpl = class _GraphClientImpl {
|
|
|
1168
1859
|
const adapter = this.getAdapterForType(aType);
|
|
1169
1860
|
const docId = computeNodeDocId(uid);
|
|
1170
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
|
+
}
|
|
1171
1868
|
await adapter.setDoc(docId, record);
|
|
1172
1869
|
}
|
|
1173
1870
|
async putEdge(aType, aUid, axbType, bType, bUid, data) {
|
|
@@ -1178,13 +1875,19 @@ var GraphClientImpl = class _GraphClientImpl {
|
|
|
1178
1875
|
const adapter = this.getAdapterForType(aType);
|
|
1179
1876
|
const docId = computeEdgeDocId(aUid, axbType, bUid);
|
|
1180
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
|
+
}
|
|
1181
1884
|
await adapter.setDoc(docId, record);
|
|
1182
1885
|
}
|
|
1183
1886
|
async updateNode(uid, data) {
|
|
1184
1887
|
const docId = computeNodeDocId(uid);
|
|
1185
1888
|
await this.adapter.updateDoc(docId, {
|
|
1186
1889
|
...data,
|
|
1187
|
-
updatedAt:
|
|
1890
|
+
updatedAt: FieldValue5.serverTimestamp()
|
|
1188
1891
|
});
|
|
1189
1892
|
}
|
|
1190
1893
|
async removeNode(uid) {
|
|
@@ -1205,7 +1908,7 @@ var GraphClientImpl = class _GraphClientImpl {
|
|
|
1205
1908
|
this.adapter.collectionPath,
|
|
1206
1909
|
firestoreTx
|
|
1207
1910
|
);
|
|
1208
|
-
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);
|
|
1209
1912
|
return fn(graphTx);
|
|
1210
1913
|
});
|
|
1211
1914
|
}
|
|
@@ -1237,7 +1940,9 @@ var GraphClientImpl = class _GraphClientImpl {
|
|
|
1237
1940
|
{
|
|
1238
1941
|
registry: this.getCombinedRegistry(),
|
|
1239
1942
|
queryMode: this.queryMode === "pipeline" ? "pipeline" : "standard",
|
|
1240
|
-
scanProtection: this.scanProtection
|
|
1943
|
+
scanProtection: this.scanProtection,
|
|
1944
|
+
migrationWriteBack: this.globalWriteBack,
|
|
1945
|
+
migrationSandbox: this.migrationSandbox
|
|
1241
1946
|
},
|
|
1242
1947
|
newScopePath
|
|
1243
1948
|
);
|
|
@@ -1267,7 +1972,8 @@ var GraphClientImpl = class _GraphClientImpl {
|
|
|
1267
1972
|
q = q.limit(plan.options.limit);
|
|
1268
1973
|
}
|
|
1269
1974
|
const snap = await q.get();
|
|
1270
|
-
|
|
1975
|
+
const records = snap.docs.map((doc) => doc.data());
|
|
1976
|
+
return this.applyMigrations(records);
|
|
1271
1977
|
}
|
|
1272
1978
|
// ---------------------------------------------------------------------------
|
|
1273
1979
|
// Bulk operations
|
|
@@ -1292,6 +1998,11 @@ var GraphClientImpl = class _GraphClientImpl {
|
|
|
1292
1998
|
`Cannot define type "${name}": this name is reserved for the meta-registry.`
|
|
1293
1999
|
);
|
|
1294
2000
|
}
|
|
2001
|
+
if (this.staticRegistry?.lookup(name, NODE_RELATION, name)) {
|
|
2002
|
+
throw new DynamicRegistryError(
|
|
2003
|
+
`Cannot define node type "${name}": already defined in the static registry.`
|
|
2004
|
+
);
|
|
2005
|
+
}
|
|
1295
2006
|
const uid = generateDeterministicUid(META_NODE_TYPE, name);
|
|
1296
2007
|
const data = { name, jsonSchema };
|
|
1297
2008
|
if (description !== void 0) data.description = description;
|
|
@@ -1300,6 +2011,10 @@ var GraphClientImpl = class _GraphClientImpl {
|
|
|
1300
2011
|
if (options?.viewTemplate !== void 0) data.viewTemplate = options.viewTemplate;
|
|
1301
2012
|
if (options?.viewCss !== void 0) data.viewCss = options.viewCss;
|
|
1302
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
|
+
}
|
|
1303
2018
|
await this.putNode(META_NODE_TYPE, uid, data);
|
|
1304
2019
|
}
|
|
1305
2020
|
async defineEdgeType(name, topology, jsonSchema, description, options) {
|
|
@@ -1313,6 +2028,19 @@ var GraphClientImpl = class _GraphClientImpl {
|
|
|
1313
2028
|
`Cannot define type "${name}": this name is reserved for the meta-registry.`
|
|
1314
2029
|
);
|
|
1315
2030
|
}
|
|
2031
|
+
if (this.staticRegistry) {
|
|
2032
|
+
const fromTypes = Array.isArray(topology.from) ? topology.from : [topology.from];
|
|
2033
|
+
const toTypes = Array.isArray(topology.to) ? topology.to : [topology.to];
|
|
2034
|
+
for (const aType of fromTypes) {
|
|
2035
|
+
for (const bType of toTypes) {
|
|
2036
|
+
if (this.staticRegistry.lookup(aType, name, bType)) {
|
|
2037
|
+
throw new DynamicRegistryError(
|
|
2038
|
+
`Cannot define edge type "${name}" for (${aType}) -> (${bType}): already defined in the static registry.`
|
|
2039
|
+
);
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
1316
2044
|
const uid = generateDeterministicUid(META_EDGE_TYPE, name);
|
|
1317
2045
|
const data = {
|
|
1318
2046
|
name,
|
|
@@ -1328,6 +2056,10 @@ var GraphClientImpl = class _GraphClientImpl {
|
|
|
1328
2056
|
if (options?.viewTemplate !== void 0) data.viewTemplate = options.viewTemplate;
|
|
1329
2057
|
if (options?.viewCss !== void 0) data.viewCss = options.viewCss;
|
|
1330
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
|
+
}
|
|
1331
2063
|
await this.putNode(META_EDGE_TYPE, uid, data);
|
|
1332
2064
|
}
|
|
1333
2065
|
async reloadRegistry() {
|
|
@@ -1337,7 +2069,27 @@ var GraphClientImpl = class _GraphClientImpl {
|
|
|
1337
2069
|
);
|
|
1338
2070
|
}
|
|
1339
2071
|
const reader = this.createMetaReader();
|
|
1340
|
-
|
|
2072
|
+
const dynamicOnly = await createRegistryFromGraph(reader, this.migrationSandbox);
|
|
2073
|
+
if (this.staticRegistry) {
|
|
2074
|
+
this.dynamicRegistry = createMergedRegistry(this.staticRegistry, dynamicOnly);
|
|
2075
|
+
} else {
|
|
2076
|
+
this.dynamicRegistry = dynamicOnly;
|
|
2077
|
+
}
|
|
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;
|
|
1341
2093
|
}
|
|
1342
2094
|
/**
|
|
1343
2095
|
* Create a GraphReader for the meta-collection.
|
|
@@ -1776,11 +2528,39 @@ function findViewsFile(dir) {
|
|
|
1776
2528
|
}
|
|
1777
2529
|
return void 0;
|
|
1778
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
|
+
}
|
|
1779
2557
|
function loadNodeEntity(dir, name) {
|
|
1780
2558
|
const schema = loadSchema(dir, `node type "${name}"`);
|
|
1781
2559
|
const meta = readJsonIfExists(join(dir, "meta.json"));
|
|
1782
2560
|
const sampleData = readJsonIfExists(join(dir, "sample.json"));
|
|
1783
2561
|
const viewsPath = findViewsFile(dir);
|
|
2562
|
+
const migrationsPath = findMigrationsFile(dir);
|
|
2563
|
+
const migrations = migrationsPath ? loadMigrations(migrationsPath, `node type "${name}"`) : void 0;
|
|
1784
2564
|
return {
|
|
1785
2565
|
kind: "node",
|
|
1786
2566
|
name,
|
|
@@ -1791,7 +2571,9 @@ function loadNodeEntity(dir, name) {
|
|
|
1791
2571
|
viewDefaults: meta?.viewDefaults,
|
|
1792
2572
|
viewsPath,
|
|
1793
2573
|
sampleData,
|
|
1794
|
-
allowedIn: meta?.allowedIn
|
|
2574
|
+
allowedIn: meta?.allowedIn,
|
|
2575
|
+
migrations,
|
|
2576
|
+
migrationWriteBack: meta?.migrationWriteBack
|
|
1795
2577
|
};
|
|
1796
2578
|
}
|
|
1797
2579
|
function loadEdgeEntity(dir, name) {
|
|
@@ -1816,6 +2598,8 @@ function loadEdgeEntity(dir, name) {
|
|
|
1816
2598
|
const meta = readJsonIfExists(join(dir, "meta.json"));
|
|
1817
2599
|
const sampleData = readJsonIfExists(join(dir, "sample.json"));
|
|
1818
2600
|
const viewsPath = findViewsFile(dir);
|
|
2601
|
+
const migrationsPath = findMigrationsFile(dir);
|
|
2602
|
+
const migrations = migrationsPath ? loadMigrations(migrationsPath, `edge type "${name}"`) : void 0;
|
|
1819
2603
|
return {
|
|
1820
2604
|
kind: "edge",
|
|
1821
2605
|
name,
|
|
@@ -1828,7 +2612,9 @@ function loadEdgeEntity(dir, name) {
|
|
|
1828
2612
|
viewsPath,
|
|
1829
2613
|
sampleData,
|
|
1830
2614
|
allowedIn: meta?.allowedIn,
|
|
1831
|
-
targetGraph: topology.targetGraph ?? meta?.targetGraph
|
|
2615
|
+
targetGraph: topology.targetGraph ?? meta?.targetGraph,
|
|
2616
|
+
migrations,
|
|
2617
|
+
migrationWriteBack: meta?.migrationWriteBack
|
|
1832
2618
|
};
|
|
1833
2619
|
}
|
|
1834
2620
|
function getSubdirectories(dir) {
|
|
@@ -2019,6 +2805,7 @@ export {
|
|
|
2019
2805
|
InvalidQueryError,
|
|
2020
2806
|
META_EDGE_TYPE,
|
|
2021
2807
|
META_NODE_TYPE,
|
|
2808
|
+
MigrationError,
|
|
2022
2809
|
NODE_TYPE_SCHEMA,
|
|
2023
2810
|
NodeNotFoundError,
|
|
2024
2811
|
QueryClient,
|
|
@@ -2026,33 +2813,47 @@ export {
|
|
|
2026
2813
|
QuerySafetyError,
|
|
2027
2814
|
RegistryScopeError,
|
|
2028
2815
|
RegistryViolationError,
|
|
2816
|
+
SERIALIZATION_TAG,
|
|
2029
2817
|
TraversalError,
|
|
2030
2818
|
ValidationError,
|
|
2031
2819
|
analyzeQuerySafety,
|
|
2820
|
+
applyMigrationChain,
|
|
2032
2821
|
buildEdgeQueryPlan,
|
|
2033
2822
|
buildEdgeRecord,
|
|
2034
2823
|
buildNodeQueryPlan,
|
|
2035
2824
|
buildNodeRecord,
|
|
2825
|
+
compileMigrationFn,
|
|
2826
|
+
compileMigrations,
|
|
2036
2827
|
compileSchema,
|
|
2037
2828
|
computeEdgeDocId,
|
|
2038
2829
|
computeNodeDocId,
|
|
2039
2830
|
createBootstrapRegistry,
|
|
2040
2831
|
createGraphClient,
|
|
2832
|
+
createMergedRegistry,
|
|
2041
2833
|
createRegistry,
|
|
2042
2834
|
createRegistryFromGraph,
|
|
2043
2835
|
createTraversal,
|
|
2836
|
+
defaultExecutor,
|
|
2044
2837
|
defineConfig,
|
|
2045
2838
|
defineViews,
|
|
2839
|
+
deserializeFirestoreTypes,
|
|
2840
|
+
destroySandboxWorker,
|
|
2046
2841
|
discoverEntities,
|
|
2047
2842
|
generateDeterministicUid,
|
|
2048
2843
|
generateId,
|
|
2049
2844
|
generateIndexConfig,
|
|
2050
2845
|
generateTypes,
|
|
2051
2846
|
isAncestorUid,
|
|
2847
|
+
isTaggedValue,
|
|
2052
2848
|
jsonSchemaToFieldMeta,
|
|
2053
2849
|
matchScope,
|
|
2054
2850
|
matchScopeAny,
|
|
2851
|
+
migrateRecord,
|
|
2852
|
+
migrateRecords,
|
|
2853
|
+
precompileSource,
|
|
2055
2854
|
resolveAncestorCollection,
|
|
2056
|
-
resolveView
|
|
2855
|
+
resolveView,
|
|
2856
|
+
serializeFirestoreTypes,
|
|
2857
|
+
validateMigrationChain
|
|
2057
2858
|
};
|
|
2058
2859
|
//# sourceMappingURL=index.js.map
|