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