@typicalday/firegraph 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-6OQW5OKO.js +110 -0
- package/dist/chunk-6OQW5OKO.js.map +1 -0
- package/dist/chunk-WOAJRVHD.js +699 -0
- package/dist/chunk-WOAJRVHD.js.map +1 -0
- package/dist/chunk-YUXOALMR.js +1653 -0
- package/dist/chunk-YUXOALMR.js.map +1 -0
- package/dist/codegen/index.d.cts +25 -1
- package/dist/codegen/index.d.ts +25 -1
- package/dist/d1.cjs +2416 -0
- package/dist/d1.cjs.map +1 -0
- package/dist/d1.d.cts +54 -0
- package/dist/d1.d.ts +54 -0
- package/dist/d1.js +75 -0
- package/dist/d1.js.map +1 -0
- package/dist/do-sqlite.cjs +2419 -0
- package/dist/do-sqlite.cjs.map +1 -0
- package/dist/do-sqlite.d.cts +41 -0
- package/dist/do-sqlite.d.ts +41 -0
- package/dist/do-sqlite.js +78 -0
- package/dist/do-sqlite.js.map +1 -0
- package/dist/editor/client/assets/{index-BIwgcRWJ.js → index-tyFcX6qG.js} +1 -1
- package/dist/editor/client/index.html +1 -1
- package/dist/editor/server/index.mjs +2054 -1984
- package/dist/index.cjs +2845 -2744
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +345 -233
- package/dist/index.d.ts +345 -233
- package/dist/index.js +710 -2295
- package/dist/index.js.map +1 -1
- package/dist/serialization-C6JNNOCS.js +13 -0
- package/dist/serialization-C6JNNOCS.js.map +1 -0
- package/dist/{index-B9aodfYD.d.cts → types-BVtx9zLv.d.cts} +26 -25
- package/dist/{index-B9aodfYD.d.ts → types-BVtx9zLv.d.ts} +26 -25
- package/package.json +39 -2
|
@@ -11,6 +11,9 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
|
|
|
11
11
|
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
12
12
|
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
13
13
|
});
|
|
14
|
+
var __esm = (fn, res) => function __init() {
|
|
15
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
16
|
+
};
|
|
14
17
|
var __commonJS = (cb, mod) => function __require2() {
|
|
15
18
|
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
16
19
|
};
|
|
@@ -29633,6 +29636,122 @@ var require_dist = __commonJS({
|
|
|
29633
29636
|
}
|
|
29634
29637
|
});
|
|
29635
29638
|
|
|
29639
|
+
// src/serialization.ts
|
|
29640
|
+
var serialization_exports = {};
|
|
29641
|
+
__export(serialization_exports, {
|
|
29642
|
+
SERIALIZATION_TAG: () => SERIALIZATION_TAG,
|
|
29643
|
+
deserializeFirestoreTypes: () => deserializeFirestoreTypes,
|
|
29644
|
+
isTaggedValue: () => isTaggedValue,
|
|
29645
|
+
serializeFirestoreTypes: () => serializeFirestoreTypes
|
|
29646
|
+
});
|
|
29647
|
+
import { Timestamp, GeoPoint, FieldValue } from "@google-cloud/firestore";
|
|
29648
|
+
function isTaggedValue(value) {
|
|
29649
|
+
if (value === null || typeof value !== "object") return false;
|
|
29650
|
+
const tag = value[SERIALIZATION_TAG];
|
|
29651
|
+
return typeof tag === "string" && KNOWN_TYPES.has(tag);
|
|
29652
|
+
}
|
|
29653
|
+
function isTimestamp(value) {
|
|
29654
|
+
return value instanceof Timestamp;
|
|
29655
|
+
}
|
|
29656
|
+
function isGeoPoint(value) {
|
|
29657
|
+
return value instanceof GeoPoint;
|
|
29658
|
+
}
|
|
29659
|
+
function isDocumentReference(value) {
|
|
29660
|
+
if (value === null || typeof value !== "object") return false;
|
|
29661
|
+
const v = value;
|
|
29662
|
+
return typeof v.path === "string" && v.firestore !== void 0 && typeof v.id === "string" && v.constructor?.name === "DocumentReference";
|
|
29663
|
+
}
|
|
29664
|
+
function isVectorValue(value) {
|
|
29665
|
+
if (value === null || typeof value !== "object") return false;
|
|
29666
|
+
const v = value;
|
|
29667
|
+
return v.constructor?.name === "VectorValue" && Array.isArray(v._values);
|
|
29668
|
+
}
|
|
29669
|
+
function serializeFirestoreTypes(data) {
|
|
29670
|
+
return serializeValue(data);
|
|
29671
|
+
}
|
|
29672
|
+
function serializeValue(value) {
|
|
29673
|
+
if (value === null || value === void 0) return value;
|
|
29674
|
+
if (typeof value !== "object") return value;
|
|
29675
|
+
if (isTimestamp(value)) {
|
|
29676
|
+
return { [SERIALIZATION_TAG]: "Timestamp", seconds: value.seconds, nanoseconds: value.nanoseconds };
|
|
29677
|
+
}
|
|
29678
|
+
if (isGeoPoint(value)) {
|
|
29679
|
+
return { [SERIALIZATION_TAG]: "GeoPoint", latitude: value.latitude, longitude: value.longitude };
|
|
29680
|
+
}
|
|
29681
|
+
if (isDocumentReference(value)) {
|
|
29682
|
+
return { [SERIALIZATION_TAG]: "DocumentReference", path: value.path };
|
|
29683
|
+
}
|
|
29684
|
+
if (isVectorValue(value)) {
|
|
29685
|
+
const v = value;
|
|
29686
|
+
const values = typeof v.toArray === "function" ? v.toArray() : v._values;
|
|
29687
|
+
return { [SERIALIZATION_TAG]: "VectorValue", values: [...values] };
|
|
29688
|
+
}
|
|
29689
|
+
if (Array.isArray(value)) {
|
|
29690
|
+
return value.map(serializeValue);
|
|
29691
|
+
}
|
|
29692
|
+
const result = {};
|
|
29693
|
+
for (const key of Object.keys(value)) {
|
|
29694
|
+
result[key] = serializeValue(value[key]);
|
|
29695
|
+
}
|
|
29696
|
+
return result;
|
|
29697
|
+
}
|
|
29698
|
+
function deserializeFirestoreTypes(data, db2) {
|
|
29699
|
+
return deserializeValue(data, db2);
|
|
29700
|
+
}
|
|
29701
|
+
function deserializeValue(value, db2) {
|
|
29702
|
+
if (value === null || value === void 0) return value;
|
|
29703
|
+
if (typeof value !== "object") return value;
|
|
29704
|
+
if (isTimestamp(value) || isGeoPoint(value) || isDocumentReference(value) || isVectorValue(value)) {
|
|
29705
|
+
return value;
|
|
29706
|
+
}
|
|
29707
|
+
if (Array.isArray(value)) {
|
|
29708
|
+
return value.map((v) => deserializeValue(v, db2));
|
|
29709
|
+
}
|
|
29710
|
+
const obj = value;
|
|
29711
|
+
if (isTaggedValue(obj)) {
|
|
29712
|
+
const tag = obj[SERIALIZATION_TAG];
|
|
29713
|
+
switch (tag) {
|
|
29714
|
+
case "Timestamp":
|
|
29715
|
+
if (typeof obj.seconds !== "number" || typeof obj.nanoseconds !== "number") return obj;
|
|
29716
|
+
return new Timestamp(obj.seconds, obj.nanoseconds);
|
|
29717
|
+
case "GeoPoint":
|
|
29718
|
+
if (typeof obj.latitude !== "number" || typeof obj.longitude !== "number") return obj;
|
|
29719
|
+
return new GeoPoint(obj.latitude, obj.longitude);
|
|
29720
|
+
case "VectorValue":
|
|
29721
|
+
if (!Array.isArray(obj.values)) return obj;
|
|
29722
|
+
return FieldValue.vector(obj.values);
|
|
29723
|
+
case "DocumentReference":
|
|
29724
|
+
if (typeof obj.path !== "string") return obj;
|
|
29725
|
+
if (db2) {
|
|
29726
|
+
return db2.doc(obj.path);
|
|
29727
|
+
}
|
|
29728
|
+
if (!_docRefWarned) {
|
|
29729
|
+
_docRefWarned = true;
|
|
29730
|
+
console.warn(
|
|
29731
|
+
"[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."
|
|
29732
|
+
);
|
|
29733
|
+
}
|
|
29734
|
+
return obj;
|
|
29735
|
+
default:
|
|
29736
|
+
return obj;
|
|
29737
|
+
}
|
|
29738
|
+
}
|
|
29739
|
+
const result = {};
|
|
29740
|
+
for (const key of Object.keys(obj)) {
|
|
29741
|
+
result[key] = deserializeValue(obj[key], db2);
|
|
29742
|
+
}
|
|
29743
|
+
return result;
|
|
29744
|
+
}
|
|
29745
|
+
var SERIALIZATION_TAG, KNOWN_TYPES, _docRefWarned;
|
|
29746
|
+
var init_serialization = __esm({
|
|
29747
|
+
"src/serialization.ts"() {
|
|
29748
|
+
"use strict";
|
|
29749
|
+
SERIALIZATION_TAG = "__firegraph_ser__";
|
|
29750
|
+
KNOWN_TYPES = /* @__PURE__ */ new Set(["Timestamp", "GeoPoint", "VectorValue", "DocumentReference"]);
|
|
29751
|
+
_docRefWarned = false;
|
|
29752
|
+
}
|
|
29753
|
+
});
|
|
29754
|
+
|
|
29636
29755
|
// editor/server/index.ts
|
|
29637
29756
|
var import_express = __toESM(require_express2(), 1);
|
|
29638
29757
|
var import_cors = __toESM(require_lib3(), 1);
|
|
@@ -29640,9 +29759,6 @@ import path4 from "path";
|
|
|
29640
29759
|
import { fileURLToPath } from "url";
|
|
29641
29760
|
import { Firestore } from "@google-cloud/firestore";
|
|
29642
29761
|
|
|
29643
|
-
// src/client.ts
|
|
29644
|
-
import { FieldValue as FieldValue5 } from "@google-cloud/firestore";
|
|
29645
|
-
|
|
29646
29762
|
// src/docid.ts
|
|
29647
29763
|
import { createHash } from "node:crypto";
|
|
29648
29764
|
|
|
@@ -29671,34 +29787,66 @@ function computeEdgeDocId(aUid, axbType, bUid) {
|
|
|
29671
29787
|
return `${shard}${SHARD_SEPARATOR}${aUid}${SHARD_SEPARATOR}${axbType}${SHARD_SEPARATOR}${bUid}`;
|
|
29672
29788
|
}
|
|
29673
29789
|
|
|
29674
|
-
// src/
|
|
29675
|
-
|
|
29676
|
-
|
|
29677
|
-
const now = FieldValue.serverTimestamp();
|
|
29678
|
-
return {
|
|
29679
|
-
aType,
|
|
29680
|
-
aUid: uid,
|
|
29681
|
-
axbType: NODE_RELATION,
|
|
29682
|
-
bType: aType,
|
|
29683
|
-
bUid: uid,
|
|
29684
|
-
data,
|
|
29685
|
-
createdAt: now,
|
|
29686
|
-
updatedAt: now
|
|
29687
|
-
};
|
|
29790
|
+
// src/batch.ts
|
|
29791
|
+
function buildWritableNodeRecord(aType, uid, data) {
|
|
29792
|
+
return { aType, aUid: uid, axbType: NODE_RELATION, bType: aType, bUid: uid, data };
|
|
29688
29793
|
}
|
|
29689
|
-
function
|
|
29690
|
-
|
|
29691
|
-
return {
|
|
29692
|
-
aType,
|
|
29693
|
-
aUid,
|
|
29694
|
-
axbType,
|
|
29695
|
-
bType,
|
|
29696
|
-
bUid,
|
|
29697
|
-
data,
|
|
29698
|
-
createdAt: now,
|
|
29699
|
-
updatedAt: now
|
|
29700
|
-
};
|
|
29794
|
+
function buildWritableEdgeRecord(aType, aUid, axbType, bType, bUid, data) {
|
|
29795
|
+
return { aType, aUid, axbType, bType, bUid, data };
|
|
29701
29796
|
}
|
|
29797
|
+
var GraphBatchImpl = class {
|
|
29798
|
+
constructor(backend, registry2, scopePath = "") {
|
|
29799
|
+
this.backend = backend;
|
|
29800
|
+
this.registry = registry2;
|
|
29801
|
+
this.scopePath = scopePath;
|
|
29802
|
+
}
|
|
29803
|
+
async putNode(aType, uid, data) {
|
|
29804
|
+
if (this.registry) {
|
|
29805
|
+
this.registry.validate(aType, NODE_RELATION, aType, data, this.scopePath);
|
|
29806
|
+
}
|
|
29807
|
+
const docId = computeNodeDocId(uid);
|
|
29808
|
+
const record2 = buildWritableNodeRecord(aType, uid, data);
|
|
29809
|
+
if (this.registry) {
|
|
29810
|
+
const entry = this.registry.lookup(aType, NODE_RELATION, aType);
|
|
29811
|
+
if (entry?.schemaVersion && entry.schemaVersion > 0) {
|
|
29812
|
+
record2.v = entry.schemaVersion;
|
|
29813
|
+
}
|
|
29814
|
+
}
|
|
29815
|
+
this.backend.setDoc(docId, record2);
|
|
29816
|
+
}
|
|
29817
|
+
async putEdge(aType, aUid, axbType, bType, bUid, data) {
|
|
29818
|
+
if (this.registry) {
|
|
29819
|
+
this.registry.validate(aType, axbType, bType, data, this.scopePath);
|
|
29820
|
+
}
|
|
29821
|
+
const docId = computeEdgeDocId(aUid, axbType, bUid);
|
|
29822
|
+
const record2 = buildWritableEdgeRecord(aType, aUid, axbType, bType, bUid, data);
|
|
29823
|
+
if (this.registry) {
|
|
29824
|
+
const entry = this.registry.lookup(aType, axbType, bType);
|
|
29825
|
+
if (entry?.schemaVersion && entry.schemaVersion > 0) {
|
|
29826
|
+
record2.v = entry.schemaVersion;
|
|
29827
|
+
}
|
|
29828
|
+
}
|
|
29829
|
+
this.backend.setDoc(docId, record2);
|
|
29830
|
+
}
|
|
29831
|
+
async updateNode(uid, data) {
|
|
29832
|
+
const docId = computeNodeDocId(uid);
|
|
29833
|
+
this.backend.updateDoc(docId, { dataFields: data });
|
|
29834
|
+
}
|
|
29835
|
+
async removeNode(uid) {
|
|
29836
|
+
const docId = computeNodeDocId(uid);
|
|
29837
|
+
this.backend.deleteDoc(docId);
|
|
29838
|
+
}
|
|
29839
|
+
async removeEdge(aUid, axbType, bUid) {
|
|
29840
|
+
const docId = computeEdgeDocId(aUid, axbType, bUid);
|
|
29841
|
+
this.backend.deleteDoc(docId);
|
|
29842
|
+
}
|
|
29843
|
+
async commit() {
|
|
29844
|
+
await this.backend.commit();
|
|
29845
|
+
}
|
|
29846
|
+
};
|
|
29847
|
+
|
|
29848
|
+
// src/dynamic-registry.ts
|
|
29849
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
29702
29850
|
|
|
29703
29851
|
// src/errors.ts
|
|
29704
29852
|
var FiregraphError = class extends Error {
|
|
@@ -29758,487 +29906,912 @@ var MigrationError = class extends FiregraphError {
|
|
|
29758
29906
|
}
|
|
29759
29907
|
};
|
|
29760
29908
|
|
|
29761
|
-
// src/
|
|
29762
|
-
|
|
29763
|
-
|
|
29764
|
-
|
|
29765
|
-
|
|
29909
|
+
// src/json-schema.ts
|
|
29910
|
+
var import_ajv = __toESM(require_ajv(), 1);
|
|
29911
|
+
var import_ajv_formats = __toESM(require_dist(), 1);
|
|
29912
|
+
var ajv = new import_ajv.default({ allErrors: true, strict: false });
|
|
29913
|
+
(0, import_ajv_formats.default)(ajv);
|
|
29914
|
+
function compileSchema(schema, label) {
|
|
29915
|
+
const validate = ajv.compile(schema);
|
|
29916
|
+
return (data) => {
|
|
29917
|
+
if (!validate(data)) {
|
|
29918
|
+
const errors = validate.errors ?? [];
|
|
29919
|
+
const messages = errors.map((err) => `${err.instancePath || "/"}${err.message ? ": " + err.message : ""}`).join("; ");
|
|
29920
|
+
throw new ValidationError(
|
|
29921
|
+
`Data validation failed${label ? " for " + label : ""}: ${messages}`,
|
|
29922
|
+
errors
|
|
29923
|
+
);
|
|
29924
|
+
}
|
|
29925
|
+
};
|
|
29926
|
+
}
|
|
29927
|
+
function jsonSchemaToFieldMeta(schema) {
|
|
29928
|
+
if (!schema || schema.type !== "object" || !schema.properties) return [];
|
|
29929
|
+
const requiredSet = new Set(
|
|
29930
|
+
Array.isArray(schema.required) ? schema.required : []
|
|
29931
|
+
);
|
|
29932
|
+
return Object.entries(schema.properties).map(
|
|
29933
|
+
([name, prop]) => propertyToFieldMeta(name, prop, requiredSet.has(name))
|
|
29934
|
+
);
|
|
29935
|
+
}
|
|
29936
|
+
function propertyToFieldMeta(name, prop, required2) {
|
|
29937
|
+
if (!prop) return { name, type: "unknown", required: required2 };
|
|
29938
|
+
if (Array.isArray(prop.enum)) {
|
|
29939
|
+
return {
|
|
29940
|
+
name,
|
|
29941
|
+
type: "enum",
|
|
29942
|
+
required: required2,
|
|
29943
|
+
enumValues: prop.enum,
|
|
29944
|
+
description: prop.description
|
|
29945
|
+
};
|
|
29766
29946
|
}
|
|
29767
|
-
|
|
29768
|
-
|
|
29769
|
-
|
|
29770
|
-
|
|
29771
|
-
|
|
29772
|
-
if (bUid) filters.push({ field: "bUid", op: "==", value: bUid });
|
|
29773
|
-
if (params.where) {
|
|
29774
|
-
for (const clause of params.where) {
|
|
29775
|
-
const field = BUILTIN_FIELDS.has(clause.field) ? clause.field : clause.field.startsWith("data.") ? clause.field : `data.${clause.field}`;
|
|
29776
|
-
filters.push({ field, op: clause.op, value: clause.value });
|
|
29947
|
+
if (Array.isArray(prop.oneOf) || Array.isArray(prop.anyOf)) {
|
|
29948
|
+
const variants = prop.oneOf ?? prop.anyOf;
|
|
29949
|
+
const nonNull = variants.filter((v) => v.type !== "null");
|
|
29950
|
+
if (nonNull.length === 1) {
|
|
29951
|
+
return propertyToFieldMeta(name, nonNull[0], false);
|
|
29777
29952
|
}
|
|
29953
|
+
return { name, type: "unknown", required: required2, description: prop.description };
|
|
29778
29954
|
}
|
|
29779
|
-
|
|
29780
|
-
|
|
29955
|
+
const type = prop.type;
|
|
29956
|
+
if (type === "string") {
|
|
29957
|
+
return {
|
|
29958
|
+
name,
|
|
29959
|
+
type: "string",
|
|
29960
|
+
required: required2,
|
|
29961
|
+
minLength: prop.minLength,
|
|
29962
|
+
maxLength: prop.maxLength,
|
|
29963
|
+
pattern: prop.pattern,
|
|
29964
|
+
description: prop.description
|
|
29965
|
+
};
|
|
29781
29966
|
}
|
|
29782
|
-
|
|
29783
|
-
|
|
29784
|
-
|
|
29785
|
-
|
|
29786
|
-
|
|
29787
|
-
|
|
29788
|
-
|
|
29789
|
-
|
|
29790
|
-
|
|
29791
|
-
|
|
29792
|
-
for (const clause of params.where) {
|
|
29793
|
-
const field = BUILTIN_FIELDS.has(clause.field) ? clause.field : clause.field.startsWith("data.") ? clause.field : `data.${clause.field}`;
|
|
29794
|
-
filters.push({ field, op: clause.op, value: clause.value });
|
|
29795
|
-
}
|
|
29967
|
+
if (type === "number" || type === "integer") {
|
|
29968
|
+
return {
|
|
29969
|
+
name,
|
|
29970
|
+
type: "number",
|
|
29971
|
+
required: required2,
|
|
29972
|
+
min: prop.minimum,
|
|
29973
|
+
max: prop.maximum,
|
|
29974
|
+
isInt: type === "integer" ? true : void 0,
|
|
29975
|
+
description: prop.description
|
|
29976
|
+
};
|
|
29796
29977
|
}
|
|
29797
|
-
|
|
29798
|
-
|
|
29978
|
+
if (type === "boolean") {
|
|
29979
|
+
return { name, type: "boolean", required: required2, description: prop.description };
|
|
29980
|
+
}
|
|
29981
|
+
if (type === "array") {
|
|
29982
|
+
const itemMeta = prop.items ? propertyToFieldMeta("item", prop.items, true) : void 0;
|
|
29983
|
+
return {
|
|
29984
|
+
name,
|
|
29985
|
+
type: "array",
|
|
29986
|
+
required: required2,
|
|
29987
|
+
itemMeta,
|
|
29988
|
+
description: prop.description
|
|
29989
|
+
};
|
|
29990
|
+
}
|
|
29991
|
+
if (type === "object") {
|
|
29992
|
+
return {
|
|
29993
|
+
name,
|
|
29994
|
+
type: "object",
|
|
29995
|
+
required: required2,
|
|
29996
|
+
fields: jsonSchemaToFieldMeta(prop),
|
|
29997
|
+
description: prop.description
|
|
29998
|
+
};
|
|
29999
|
+
}
|
|
30000
|
+
return { name, type: "unknown", required: required2, description: prop.description };
|
|
29799
30001
|
}
|
|
29800
30002
|
|
|
29801
|
-
// src/
|
|
29802
|
-
function
|
|
29803
|
-
const
|
|
29804
|
-
|
|
29805
|
-
|
|
29806
|
-
|
|
29807
|
-
|
|
29808
|
-
|
|
29809
|
-
|
|
29810
|
-
|
|
29811
|
-
|
|
29812
|
-
|
|
29813
|
-
|
|
29814
|
-
|
|
29815
|
-
await collectionRef.doc(docId).update(data);
|
|
29816
|
-
},
|
|
29817
|
-
async deleteDoc(docId) {
|
|
29818
|
-
await collectionRef.doc(docId).delete();
|
|
29819
|
-
},
|
|
29820
|
-
async query(filters, options) {
|
|
29821
|
-
let q = collectionRef;
|
|
29822
|
-
for (const f of filters) {
|
|
29823
|
-
q = q.where(f.field, f.op, f.value);
|
|
29824
|
-
}
|
|
29825
|
-
if (options?.orderBy) {
|
|
29826
|
-
q = q.orderBy(options.orderBy.field, options.orderBy.direction ?? "asc");
|
|
30003
|
+
// src/migration.ts
|
|
30004
|
+
async function applyMigrationChain(data, currentVersion, targetVersion, migrations) {
|
|
30005
|
+
const sorted = [...migrations].sort((a, b) => a.fromVersion - b.fromVersion);
|
|
30006
|
+
let result = { ...data };
|
|
30007
|
+
let version2 = currentVersion;
|
|
30008
|
+
for (const step of sorted) {
|
|
30009
|
+
if (step.fromVersion === version2) {
|
|
30010
|
+
try {
|
|
30011
|
+
result = await step.up(result);
|
|
30012
|
+
} catch (err) {
|
|
30013
|
+
if (err instanceof MigrationError) throw err;
|
|
30014
|
+
throw new MigrationError(
|
|
30015
|
+
`Migration from v${step.fromVersion} to v${step.toVersion} failed: ${err.message}`
|
|
30016
|
+
);
|
|
29827
30017
|
}
|
|
29828
|
-
if (
|
|
29829
|
-
|
|
30018
|
+
if (!result || typeof result !== "object") {
|
|
30019
|
+
throw new MigrationError(
|
|
30020
|
+
`Migration from v${step.fromVersion} to v${step.toVersion} returned invalid data (expected object)`
|
|
30021
|
+
);
|
|
29830
30022
|
}
|
|
29831
|
-
|
|
29832
|
-
return snap.docs.map((doc) => doc.data());
|
|
30023
|
+
version2 = step.toVersion;
|
|
29833
30024
|
}
|
|
29834
|
-
}
|
|
30025
|
+
}
|
|
30026
|
+
if (version2 !== targetVersion) {
|
|
30027
|
+
throw new MigrationError(
|
|
30028
|
+
`Incomplete migration chain: reached v${version2} but target is v${targetVersion}`
|
|
30029
|
+
);
|
|
30030
|
+
}
|
|
30031
|
+
return result;
|
|
29835
30032
|
}
|
|
29836
|
-
function
|
|
29837
|
-
|
|
29838
|
-
|
|
29839
|
-
|
|
29840
|
-
|
|
29841
|
-
|
|
29842
|
-
|
|
29843
|
-
|
|
29844
|
-
setDoc(docId, data) {
|
|
29845
|
-
tx.set(collectionRef.doc(docId), data);
|
|
29846
|
-
},
|
|
29847
|
-
updateDoc(docId, data) {
|
|
29848
|
-
tx.update(collectionRef.doc(docId), data);
|
|
29849
|
-
},
|
|
29850
|
-
deleteDoc(docId) {
|
|
29851
|
-
tx.delete(collectionRef.doc(docId));
|
|
29852
|
-
},
|
|
29853
|
-
async query(filters, options) {
|
|
29854
|
-
let q = collectionRef;
|
|
29855
|
-
for (const f of filters) {
|
|
29856
|
-
q = q.where(f.field, f.op, f.value);
|
|
29857
|
-
}
|
|
29858
|
-
if (options?.orderBy) {
|
|
29859
|
-
q = q.orderBy(options.orderBy.field, options.orderBy.direction ?? "asc");
|
|
29860
|
-
}
|
|
29861
|
-
if (options?.limit !== void 0) {
|
|
29862
|
-
q = q.limit(options.limit);
|
|
29863
|
-
}
|
|
29864
|
-
const snap = await tx.get(q);
|
|
29865
|
-
return snap.docs.map((doc) => doc.data());
|
|
30033
|
+
function validateMigrationChain(migrations, label) {
|
|
30034
|
+
if (migrations.length === 0) return;
|
|
30035
|
+
const seen = /* @__PURE__ */ new Set();
|
|
30036
|
+
for (const step of migrations) {
|
|
30037
|
+
if (step.toVersion <= step.fromVersion) {
|
|
30038
|
+
throw new MigrationError(
|
|
30039
|
+
`${label}: migration step has toVersion (${step.toVersion}) <= fromVersion (${step.fromVersion})`
|
|
30040
|
+
);
|
|
29866
30041
|
}
|
|
29867
|
-
|
|
29868
|
-
|
|
29869
|
-
|
|
29870
|
-
|
|
29871
|
-
const batch = db2.batch();
|
|
29872
|
-
return {
|
|
29873
|
-
setDoc(docId, data) {
|
|
29874
|
-
batch.set(collectionRef.doc(docId), data);
|
|
29875
|
-
},
|
|
29876
|
-
updateDoc(docId, data) {
|
|
29877
|
-
batch.update(collectionRef.doc(docId), data);
|
|
29878
|
-
},
|
|
29879
|
-
deleteDoc(docId) {
|
|
29880
|
-
batch.delete(collectionRef.doc(docId));
|
|
29881
|
-
},
|
|
29882
|
-
async commit() {
|
|
29883
|
-
await batch.commit();
|
|
30042
|
+
if (seen.has(step.fromVersion)) {
|
|
30043
|
+
throw new MigrationError(
|
|
30044
|
+
`${label}: duplicate migration step for fromVersion ${step.fromVersion}`
|
|
30045
|
+
);
|
|
29884
30046
|
}
|
|
29885
|
-
|
|
29886
|
-
}
|
|
29887
|
-
|
|
29888
|
-
// src/internal/pipeline-adapter.ts
|
|
29889
|
-
var _Pipelines = null;
|
|
29890
|
-
async function getPipelines() {
|
|
29891
|
-
if (!_Pipelines) {
|
|
29892
|
-
const mod = await import("@google-cloud/firestore");
|
|
29893
|
-
_Pipelines = mod.Pipelines;
|
|
30047
|
+
seen.add(step.fromVersion);
|
|
29894
30048
|
}
|
|
29895
|
-
|
|
29896
|
-
|
|
29897
|
-
|
|
29898
|
-
const
|
|
29899
|
-
|
|
29900
|
-
|
|
29901
|
-
|
|
29902
|
-
|
|
29903
|
-
|
|
29904
|
-
|
|
29905
|
-
|
|
29906
|
-
|
|
29907
|
-
|
|
29908
|
-
|
|
29909
|
-
|
|
29910
|
-
|
|
29911
|
-
return P.greaterThanOrEqual(fieldName, value);
|
|
29912
|
-
case "in":
|
|
29913
|
-
return P.equalAny(fieldName, value);
|
|
29914
|
-
case "not-in":
|
|
29915
|
-
return P.notEqualAny(fieldName, value);
|
|
29916
|
-
case "array-contains":
|
|
29917
|
-
return P.arrayContains(fieldName, value);
|
|
29918
|
-
case "array-contains-any":
|
|
29919
|
-
return P.arrayContainsAny(fieldName, value);
|
|
29920
|
-
default:
|
|
29921
|
-
throw new Error(`Unsupported filter op for pipeline mode: ${op}`);
|
|
30049
|
+
const sorted = [...migrations].sort((a, b) => a.fromVersion - b.fromVersion);
|
|
30050
|
+
const targetVersion = Math.max(...migrations.map((m) => m.toVersion));
|
|
30051
|
+
let version2 = 0;
|
|
30052
|
+
for (const step of sorted) {
|
|
30053
|
+
if (step.fromVersion === version2) {
|
|
30054
|
+
version2 = step.toVersion;
|
|
30055
|
+
} else if (step.fromVersion > version2) {
|
|
30056
|
+
throw new MigrationError(
|
|
30057
|
+
`${label}: migration chain has a gap \u2014 no step covers v${version2} \u2192 v${step.fromVersion}`
|
|
30058
|
+
);
|
|
30059
|
+
}
|
|
30060
|
+
}
|
|
30061
|
+
if (version2 !== targetVersion) {
|
|
30062
|
+
throw new MigrationError(
|
|
30063
|
+
`${label}: migration chain does not reach v${targetVersion} (stuck at v${version2})`
|
|
30064
|
+
);
|
|
29922
30065
|
}
|
|
29923
30066
|
}
|
|
29924
|
-
function
|
|
30067
|
+
async function migrateRecord(record2, registry2, globalWriteBack = "off") {
|
|
30068
|
+
const entry = registry2.lookup(record2.aType, record2.axbType, record2.bType);
|
|
30069
|
+
if (!entry?.migrations?.length || !entry.schemaVersion) {
|
|
30070
|
+
return { record: record2, migrated: false, writeBack: "off" };
|
|
30071
|
+
}
|
|
30072
|
+
const currentVersion = record2.v ?? 0;
|
|
30073
|
+
if (currentVersion >= entry.schemaVersion) {
|
|
30074
|
+
return { record: record2, migrated: false, writeBack: "off" };
|
|
30075
|
+
}
|
|
30076
|
+
const migratedData = await applyMigrationChain(
|
|
30077
|
+
record2.data,
|
|
30078
|
+
currentVersion,
|
|
30079
|
+
entry.schemaVersion,
|
|
30080
|
+
entry.migrations
|
|
30081
|
+
);
|
|
30082
|
+
const writeBack = entry.migrationWriteBack ?? globalWriteBack ?? "off";
|
|
29925
30083
|
return {
|
|
29926
|
-
|
|
29927
|
-
|
|
29928
|
-
|
|
29929
|
-
if (filters.length === 1) {
|
|
29930
|
-
pipeline = pipeline.where(buildFilterExpression(P, filters[0]));
|
|
29931
|
-
} else if (filters.length > 1) {
|
|
29932
|
-
const [first, second, ...rest] = filters.map((f) => buildFilterExpression(P, f));
|
|
29933
|
-
pipeline = pipeline.where(P.and(first, second, ...rest));
|
|
29934
|
-
}
|
|
29935
|
-
if (options?.orderBy) {
|
|
29936
|
-
const f = P.field(options.orderBy.field);
|
|
29937
|
-
const ordering = options.orderBy.direction === "desc" ? f.descending() : f.ascending();
|
|
29938
|
-
pipeline = pipeline.sort(ordering);
|
|
29939
|
-
}
|
|
29940
|
-
if (options?.limit !== void 0) {
|
|
29941
|
-
pipeline = pipeline.limit(options.limit);
|
|
29942
|
-
}
|
|
29943
|
-
const snap = await pipeline.execute();
|
|
29944
|
-
return snap.results.map((r) => r.data());
|
|
29945
|
-
}
|
|
30084
|
+
record: { ...record2, data: migratedData, v: entry.schemaVersion },
|
|
30085
|
+
migrated: true,
|
|
30086
|
+
writeBack
|
|
29946
30087
|
};
|
|
29947
30088
|
}
|
|
30089
|
+
async function migrateRecords(records, registry2, globalWriteBack = "off") {
|
|
30090
|
+
return Promise.all(
|
|
30091
|
+
records.map((r) => migrateRecord(r, registry2, globalWriteBack))
|
|
30092
|
+
);
|
|
30093
|
+
}
|
|
29948
30094
|
|
|
29949
|
-
// src/
|
|
29950
|
-
|
|
29951
|
-
|
|
29952
|
-
|
|
29953
|
-
|
|
29954
|
-
|
|
29955
|
-
|
|
29956
|
-
|
|
29957
|
-
|
|
29958
|
-
|
|
29959
|
-
|
|
29960
|
-
|
|
29961
|
-
|
|
29962
|
-
|
|
29963
|
-
|
|
29964
|
-
|
|
29965
|
-
|
|
29966
|
-
|
|
29967
|
-
|
|
29968
|
-
|
|
29969
|
-
for (const pattern of SAFE_INDEX_PATTERNS) {
|
|
29970
|
-
let matched = true;
|
|
29971
|
-
for (const field of pattern) {
|
|
29972
|
-
if (!builtinFieldsPresent.has(field)) {
|
|
29973
|
-
matched = false;
|
|
29974
|
-
break;
|
|
29975
|
-
}
|
|
29976
|
-
}
|
|
29977
|
-
if (matched) {
|
|
29978
|
-
return { safe: true };
|
|
30095
|
+
// src/scope.ts
|
|
30096
|
+
function matchScope(scopePath, pattern) {
|
|
30097
|
+
if (pattern === "root") return scopePath === "";
|
|
30098
|
+
if (pattern === "**") return true;
|
|
30099
|
+
const pathSegments = scopePath === "" ? [] : scopePath.split("/");
|
|
30100
|
+
const patternSegments = pattern.split("/");
|
|
30101
|
+
return matchSegments(pathSegments, 0, patternSegments, 0);
|
|
30102
|
+
}
|
|
30103
|
+
function matchScopeAny(scopePath, patterns) {
|
|
30104
|
+
if (!patterns || patterns.length === 0) return true;
|
|
30105
|
+
return patterns.some((p) => matchScope(scopePath, p));
|
|
30106
|
+
}
|
|
30107
|
+
function matchSegments(path5, pi, pattern, qi) {
|
|
30108
|
+
if (pi === path5.length && qi === pattern.length) return true;
|
|
30109
|
+
if (qi === pattern.length) return false;
|
|
30110
|
+
const seg = pattern[qi];
|
|
30111
|
+
if (seg === "**") {
|
|
30112
|
+
if (qi === pattern.length - 1) return true;
|
|
30113
|
+
for (let skip = 0; skip <= path5.length - pi; skip++) {
|
|
30114
|
+
if (matchSegments(path5, pi + skip, pattern, qi + 1)) return true;
|
|
29979
30115
|
}
|
|
30116
|
+
return false;
|
|
29980
30117
|
}
|
|
29981
|
-
|
|
29982
|
-
if (
|
|
29983
|
-
return
|
|
29984
|
-
safe: false,
|
|
29985
|
-
reason: "Query filters only use data.* fields with no builtin field constraints. This requires a full collection scan. Add aType, aUid, axbType, bType, or bUid filters, or set allowCollectionScan: true."
|
|
29986
|
-
};
|
|
30118
|
+
if (pi === path5.length) return false;
|
|
30119
|
+
if (seg === "*") {
|
|
30120
|
+
return matchSegments(path5, pi + 1, pattern, qi + 1);
|
|
29987
30121
|
}
|
|
29988
|
-
if (
|
|
29989
|
-
return
|
|
29990
|
-
safe: false,
|
|
29991
|
-
reason: `Query filters on [${presentFields.join(", ")}] do not match any indexed pattern. data.* filters without an indexed base require a full collection scan. Safe patterns: (aUid + axbType), (axbType + bUid), (aType + axbType), (axbType + bType). Set allowCollectionScan: true to override.`
|
|
29992
|
-
};
|
|
30122
|
+
if (path5[pi] === seg) {
|
|
30123
|
+
return matchSegments(path5, pi + 1, pattern, qi + 1);
|
|
29993
30124
|
}
|
|
29994
|
-
return
|
|
29995
|
-
safe: false,
|
|
29996
|
-
reason: `Query filters on [${presentFields.join(", ")}] do not match any indexed pattern. This may cause a full collection scan on Firestore Enterprise. Safe patterns: (aUid + axbType), (axbType + bUid), (aType + axbType), (axbType + bType). Set allowCollectionScan: true to override.`
|
|
29997
|
-
};
|
|
30125
|
+
return false;
|
|
29998
30126
|
}
|
|
29999
30127
|
|
|
30000
|
-
// src/
|
|
30001
|
-
|
|
30002
|
-
|
|
30003
|
-
var KNOWN_TYPES = /* @__PURE__ */ new Set(["Timestamp", "GeoPoint", "VectorValue", "DocumentReference"]);
|
|
30004
|
-
var _docRefWarned = false;
|
|
30005
|
-
function isTaggedValue(value) {
|
|
30006
|
-
if (value === null || typeof value !== "object") return false;
|
|
30007
|
-
const tag = value[SERIALIZATION_TAG];
|
|
30008
|
-
return typeof tag === "string" && KNOWN_TYPES.has(tag);
|
|
30009
|
-
}
|
|
30010
|
-
function isTimestamp(value) {
|
|
30011
|
-
return value instanceof Timestamp;
|
|
30012
|
-
}
|
|
30013
|
-
function isGeoPoint(value) {
|
|
30014
|
-
return value instanceof GeoPoint;
|
|
30015
|
-
}
|
|
30016
|
-
function isDocumentReference(value) {
|
|
30017
|
-
if (value === null || typeof value !== "object") return false;
|
|
30018
|
-
const v = value;
|
|
30019
|
-
return typeof v.path === "string" && v.firestore !== void 0 && typeof v.id === "string" && v.constructor?.name === "DocumentReference";
|
|
30020
|
-
}
|
|
30021
|
-
function isVectorValue(value) {
|
|
30022
|
-
if (value === null || typeof value !== "object") return false;
|
|
30023
|
-
const v = value;
|
|
30024
|
-
return v.constructor?.name === "VectorValue" && Array.isArray(v._values);
|
|
30128
|
+
// src/registry.ts
|
|
30129
|
+
function tripleKey(aType, axbType, bType) {
|
|
30130
|
+
return `${aType}:${axbType}:${bType}`;
|
|
30025
30131
|
}
|
|
30026
|
-
function
|
|
30027
|
-
return
|
|
30132
|
+
function tripleKeyFor(e) {
|
|
30133
|
+
return tripleKey(e.aType, e.axbType, e.bType);
|
|
30028
30134
|
}
|
|
30029
|
-
function
|
|
30030
|
-
|
|
30031
|
-
|
|
30032
|
-
if (
|
|
30033
|
-
|
|
30034
|
-
}
|
|
30035
|
-
|
|
30036
|
-
return { [SERIALIZATION_TAG]: "GeoPoint", latitude: value.latitude, longitude: value.longitude };
|
|
30037
|
-
}
|
|
30038
|
-
if (isDocumentReference(value)) {
|
|
30039
|
-
return { [SERIALIZATION_TAG]: "DocumentReference", path: value.path };
|
|
30040
|
-
}
|
|
30041
|
-
if (isVectorValue(value)) {
|
|
30042
|
-
const v = value;
|
|
30043
|
-
const values = typeof v.toArray === "function" ? v.toArray() : v._values;
|
|
30044
|
-
return { [SERIALIZATION_TAG]: "VectorValue", values: [...values] };
|
|
30045
|
-
}
|
|
30046
|
-
if (Array.isArray(value)) {
|
|
30047
|
-
return value.map(serializeValue);
|
|
30135
|
+
function createRegistry(input) {
|
|
30136
|
+
const map2 = /* @__PURE__ */ new Map();
|
|
30137
|
+
let entries;
|
|
30138
|
+
if (Array.isArray(input)) {
|
|
30139
|
+
entries = input;
|
|
30140
|
+
} else {
|
|
30141
|
+
entries = discoveryToEntries(input);
|
|
30048
30142
|
}
|
|
30049
|
-
const
|
|
30050
|
-
for (const
|
|
30051
|
-
|
|
30143
|
+
const entryList = Object.freeze([...entries]);
|
|
30144
|
+
for (const entry of entries) {
|
|
30145
|
+
if (entry.targetGraph && entry.targetGraph.includes("/")) {
|
|
30146
|
+
throw new ValidationError(
|
|
30147
|
+
`Entry (${entry.aType}) -[${entry.axbType}]-> (${entry.bType}) has invalid targetGraph "${entry.targetGraph}" \u2014 must be a single segment (no "/")`
|
|
30148
|
+
);
|
|
30149
|
+
}
|
|
30150
|
+
if (entry.migrations?.length) {
|
|
30151
|
+
const label = `Entry (${entry.aType}) -[${entry.axbType}]-> (${entry.bType})`;
|
|
30152
|
+
validateMigrationChain(entry.migrations, label);
|
|
30153
|
+
entry.schemaVersion = Math.max(...entry.migrations.map((m) => m.toVersion));
|
|
30154
|
+
} else {
|
|
30155
|
+
entry.schemaVersion = void 0;
|
|
30156
|
+
}
|
|
30157
|
+
const key = tripleKey(entry.aType, entry.axbType, entry.bType);
|
|
30158
|
+
const validator = entry.jsonSchema ? compileSchema(entry.jsonSchema, `(${entry.aType}) -[${entry.axbType}]-> (${entry.bType})`) : void 0;
|
|
30159
|
+
map2.set(key, { entry, validate: validator });
|
|
30052
30160
|
}
|
|
30053
|
-
|
|
30054
|
-
|
|
30055
|
-
|
|
30056
|
-
|
|
30057
|
-
|
|
30058
|
-
|
|
30059
|
-
|
|
30060
|
-
|
|
30061
|
-
|
|
30062
|
-
return value;
|
|
30161
|
+
const axbIndex = /* @__PURE__ */ new Map();
|
|
30162
|
+
const axbBuild = /* @__PURE__ */ new Map();
|
|
30163
|
+
for (const entry of entries) {
|
|
30164
|
+
const existing = axbBuild.get(entry.axbType);
|
|
30165
|
+
if (existing) {
|
|
30166
|
+
existing.push(entry);
|
|
30167
|
+
} else {
|
|
30168
|
+
axbBuild.set(entry.axbType, [entry]);
|
|
30169
|
+
}
|
|
30063
30170
|
}
|
|
30064
|
-
|
|
30065
|
-
|
|
30171
|
+
for (const [key, arr] of axbBuild) {
|
|
30172
|
+
axbIndex.set(key, Object.freeze(arr));
|
|
30066
30173
|
}
|
|
30067
|
-
|
|
30068
|
-
|
|
30069
|
-
|
|
30070
|
-
|
|
30071
|
-
|
|
30072
|
-
|
|
30073
|
-
|
|
30074
|
-
|
|
30075
|
-
|
|
30076
|
-
|
|
30077
|
-
|
|
30078
|
-
|
|
30079
|
-
|
|
30080
|
-
|
|
30081
|
-
|
|
30082
|
-
if (db2) {
|
|
30083
|
-
return db2.doc(obj.path);
|
|
30174
|
+
return {
|
|
30175
|
+
lookup(aType, axbType, bType) {
|
|
30176
|
+
return map2.get(tripleKey(aType, axbType, bType))?.entry;
|
|
30177
|
+
},
|
|
30178
|
+
lookupByAxbType(axbType) {
|
|
30179
|
+
return axbIndex.get(axbType) ?? [];
|
|
30180
|
+
},
|
|
30181
|
+
validate(aType, axbType, bType, data, scopePath) {
|
|
30182
|
+
const rec = map2.get(tripleKey(aType, axbType, bType));
|
|
30183
|
+
if (!rec) {
|
|
30184
|
+
throw new RegistryViolationError(aType, axbType, bType);
|
|
30185
|
+
}
|
|
30186
|
+
if (scopePath !== void 0 && rec.entry.allowedIn && rec.entry.allowedIn.length > 0) {
|
|
30187
|
+
if (!matchScopeAny(scopePath, rec.entry.allowedIn)) {
|
|
30188
|
+
throw new RegistryScopeError(aType, axbType, bType, scopePath, rec.entry.allowedIn);
|
|
30084
30189
|
}
|
|
30085
|
-
|
|
30086
|
-
|
|
30087
|
-
|
|
30088
|
-
|
|
30190
|
+
}
|
|
30191
|
+
if (rec.validate) {
|
|
30192
|
+
try {
|
|
30193
|
+
rec.validate(data);
|
|
30194
|
+
} catch (err) {
|
|
30195
|
+
if (err instanceof ValidationError) throw err;
|
|
30196
|
+
throw new ValidationError(
|
|
30197
|
+
`Data validation failed for (${aType}) -[${axbType}]-> (${bType})`,
|
|
30198
|
+
err
|
|
30089
30199
|
);
|
|
30090
30200
|
}
|
|
30091
|
-
|
|
30092
|
-
|
|
30093
|
-
|
|
30201
|
+
}
|
|
30202
|
+
},
|
|
30203
|
+
entries() {
|
|
30204
|
+
return entryList;
|
|
30094
30205
|
}
|
|
30095
|
-
}
|
|
30096
|
-
const result = {};
|
|
30097
|
-
for (const key of Object.keys(obj)) {
|
|
30098
|
-
result[key] = deserializeValue(obj[key], db2);
|
|
30099
|
-
}
|
|
30100
|
-
return result;
|
|
30206
|
+
};
|
|
30101
30207
|
}
|
|
30102
|
-
|
|
30103
|
-
|
|
30104
|
-
|
|
30105
|
-
|
|
30106
|
-
|
|
30107
|
-
|
|
30108
|
-
|
|
30109
|
-
|
|
30110
|
-
|
|
30111
|
-
|
|
30112
|
-
|
|
30113
|
-
|
|
30114
|
-
|
|
30115
|
-
|
|
30116
|
-
)
|
|
30208
|
+
function createMergedRegistry(base, extension) {
|
|
30209
|
+
const baseKeys = new Set(base.entries().map(tripleKeyFor));
|
|
30210
|
+
return {
|
|
30211
|
+
lookup(aType, axbType, bType) {
|
|
30212
|
+
return base.lookup(aType, axbType, bType) ?? extension.lookup(aType, axbType, bType);
|
|
30213
|
+
},
|
|
30214
|
+
lookupByAxbType(axbType) {
|
|
30215
|
+
const baseResults = base.lookupByAxbType(axbType);
|
|
30216
|
+
const extResults = extension.lookupByAxbType(axbType);
|
|
30217
|
+
if (extResults.length === 0) return baseResults;
|
|
30218
|
+
if (baseResults.length === 0) return extResults;
|
|
30219
|
+
const seen = new Set(baseResults.map(tripleKeyFor));
|
|
30220
|
+
const merged = [...baseResults];
|
|
30221
|
+
for (const entry of extResults) {
|
|
30222
|
+
if (!seen.has(tripleKeyFor(entry))) {
|
|
30223
|
+
merged.push(entry);
|
|
30224
|
+
}
|
|
30117
30225
|
}
|
|
30118
|
-
|
|
30119
|
-
|
|
30120
|
-
|
|
30121
|
-
|
|
30226
|
+
return Object.freeze(merged);
|
|
30227
|
+
},
|
|
30228
|
+
validate(aType, axbType, bType, data, scopePath) {
|
|
30229
|
+
if (baseKeys.has(tripleKey(aType, axbType, bType))) {
|
|
30230
|
+
return base.validate(aType, axbType, bType, data, scopePath);
|
|
30122
30231
|
}
|
|
30123
|
-
|
|
30124
|
-
}
|
|
30125
|
-
|
|
30126
|
-
|
|
30127
|
-
|
|
30128
|
-
|
|
30129
|
-
|
|
30130
|
-
|
|
30131
|
-
|
|
30232
|
+
return extension.validate(aType, axbType, bType, data, scopePath);
|
|
30233
|
+
},
|
|
30234
|
+
entries() {
|
|
30235
|
+
const extEntries = extension.entries();
|
|
30236
|
+
if (extEntries.length === 0) return base.entries();
|
|
30237
|
+
const merged = [...base.entries()];
|
|
30238
|
+
for (const entry of extEntries) {
|
|
30239
|
+
if (!baseKeys.has(tripleKeyFor(entry))) {
|
|
30240
|
+
merged.push(entry);
|
|
30241
|
+
}
|
|
30242
|
+
}
|
|
30243
|
+
return Object.freeze(merged);
|
|
30244
|
+
}
|
|
30245
|
+
};
|
|
30132
30246
|
}
|
|
30133
|
-
function
|
|
30134
|
-
|
|
30135
|
-
const
|
|
30136
|
-
|
|
30137
|
-
|
|
30138
|
-
|
|
30139
|
-
|
|
30247
|
+
function discoveryToEntries(discovery) {
|
|
30248
|
+
const entries = [];
|
|
30249
|
+
for (const [name, entity] of discovery.nodes) {
|
|
30250
|
+
entries.push({
|
|
30251
|
+
aType: name,
|
|
30252
|
+
axbType: NODE_RELATION,
|
|
30253
|
+
bType: name,
|
|
30254
|
+
jsonSchema: entity.schema,
|
|
30255
|
+
description: entity.description,
|
|
30256
|
+
titleField: entity.titleField,
|
|
30257
|
+
subtitleField: entity.subtitleField,
|
|
30258
|
+
allowedIn: entity.allowedIn,
|
|
30259
|
+
migrations: entity.migrations,
|
|
30260
|
+
migrationWriteBack: entity.migrationWriteBack
|
|
30261
|
+
});
|
|
30262
|
+
}
|
|
30263
|
+
for (const [axbType, entity] of discovery.edges) {
|
|
30264
|
+
const topology = entity.topology;
|
|
30265
|
+
if (!topology) continue;
|
|
30266
|
+
const fromTypes = Array.isArray(topology.from) ? topology.from : [topology.from];
|
|
30267
|
+
const toTypes = Array.isArray(topology.to) ? topology.to : [topology.to];
|
|
30268
|
+
const resolvedTargetGraph = entity.targetGraph ?? topology.targetGraph;
|
|
30269
|
+
if (resolvedTargetGraph && resolvedTargetGraph.includes("/")) {
|
|
30270
|
+
throw new ValidationError(
|
|
30271
|
+
`Edge "${axbType}" has invalid targetGraph "${resolvedTargetGraph}" \u2014 must be a single segment (no "/")`
|
|
30140
30272
|
);
|
|
30141
30273
|
}
|
|
30142
|
-
|
|
30143
|
-
|
|
30144
|
-
|
|
30145
|
-
|
|
30274
|
+
for (const aType of fromTypes) {
|
|
30275
|
+
for (const bType of toTypes) {
|
|
30276
|
+
entries.push({
|
|
30277
|
+
aType,
|
|
30278
|
+
axbType,
|
|
30279
|
+
bType,
|
|
30280
|
+
jsonSchema: entity.schema,
|
|
30281
|
+
description: entity.description,
|
|
30282
|
+
inverseLabel: topology.inverseLabel,
|
|
30283
|
+
titleField: entity.titleField,
|
|
30284
|
+
subtitleField: entity.subtitleField,
|
|
30285
|
+
allowedIn: entity.allowedIn,
|
|
30286
|
+
targetGraph: resolvedTargetGraph,
|
|
30287
|
+
migrations: entity.migrations,
|
|
30288
|
+
migrationWriteBack: entity.migrationWriteBack
|
|
30289
|
+
});
|
|
30290
|
+
}
|
|
30291
|
+
}
|
|
30292
|
+
}
|
|
30293
|
+
return entries;
|
|
30294
|
+
}
|
|
30295
|
+
|
|
30296
|
+
// src/sandbox.ts
|
|
30297
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
30298
|
+
var _worker = null;
|
|
30299
|
+
var _requestId = 0;
|
|
30300
|
+
var _pending = /* @__PURE__ */ new Map();
|
|
30301
|
+
var WORKER_SOURCE = [
|
|
30302
|
+
`'use strict';`,
|
|
30303
|
+
`var _wt = require('node:worker_threads');`,
|
|
30304
|
+
`var _mod = require('node:module');`,
|
|
30305
|
+
`var _crypto = require('node:crypto');`,
|
|
30306
|
+
`var parentPort = _wt.parentPort;`,
|
|
30307
|
+
`var workerData = _wt.workerData;`,
|
|
30308
|
+
``,
|
|
30309
|
+
`// Load SES using the parent module's resolution context`,
|
|
30310
|
+
`var esmRequire = _mod.createRequire(workerData.parentUrl);`,
|
|
30311
|
+
`esmRequire('ses');`,
|
|
30312
|
+
``,
|
|
30313
|
+
`lockdown({`,
|
|
30314
|
+
` errorTaming: 'unsafe',`,
|
|
30315
|
+
` consoleTaming: 'unsafe',`,
|
|
30316
|
+
` evalTaming: 'safe-eval',`,
|
|
30317
|
+
` overrideTaming: 'moderate',`,
|
|
30318
|
+
` stackFiltering: 'verbose'`,
|
|
30319
|
+
`});`,
|
|
30320
|
+
``,
|
|
30321
|
+
`// Defense-in-depth: verify lockdown() actually hardened JSON.`,
|
|
30322
|
+
`if (!Object.isFrozen(JSON)) {`,
|
|
30323
|
+
` throw new Error('SES lockdown failed: JSON is not frozen');`,
|
|
30324
|
+
`}`,
|
|
30325
|
+
``,
|
|
30326
|
+
`var cache = new Map();`,
|
|
30327
|
+
``,
|
|
30328
|
+
`function hashSource(s) {`,
|
|
30329
|
+
` return _crypto.createHash('sha256').update(s).digest('hex');`,
|
|
30330
|
+
`}`,
|
|
30331
|
+
``,
|
|
30332
|
+
`function buildWrapper(source) {`,
|
|
30333
|
+
` return '(function() {' +`,
|
|
30334
|
+
` ' var fn = (' + source + ');\\n' +`,
|
|
30335
|
+
` ' if (typeof fn !== "function") return null;\\n' +`,
|
|
30336
|
+
` ' return function(jsonIn) {\\n' +`,
|
|
30337
|
+
` ' var data = JSON.parse(jsonIn);\\n' +`,
|
|
30338
|
+
` ' var result = fn(data);\\n' +`,
|
|
30339
|
+
` ' if (result !== null && typeof result === "object" && typeof result.then === "function") {\\n' +`,
|
|
30340
|
+
` ' return result.then(function(r) { return JSON.stringify(r); });\\n' +`,
|
|
30341
|
+
` ' }\\n' +`,
|
|
30342
|
+
` ' return JSON.stringify(result);\\n' +`,
|
|
30343
|
+
` ' };\\n' +`,
|
|
30344
|
+
` '})()';`,
|
|
30345
|
+
`}`,
|
|
30346
|
+
``,
|
|
30347
|
+
`function compileSource(source) {`,
|
|
30348
|
+
` var key = hashSource(source);`,
|
|
30349
|
+
` var cached = cache.get(key);`,
|
|
30350
|
+
` if (cached) return cached;`,
|
|
30351
|
+
``,
|
|
30352
|
+
` var compartmentFn;`,
|
|
30353
|
+
` try {`,
|
|
30354
|
+
` var c = new Compartment({ JSON: JSON });`,
|
|
30355
|
+
` compartmentFn = c.evaluate(buildWrapper(source));`,
|
|
30356
|
+
` } catch (err) {`,
|
|
30357
|
+
` throw new Error('Failed to compile migration source: ' + (err.message || String(err)));`,
|
|
30358
|
+
` }`,
|
|
30359
|
+
``,
|
|
30360
|
+
` if (typeof compartmentFn !== 'function') {`,
|
|
30361
|
+
` throw new Error('Migration source did not produce a function: ' + source.slice(0, 80));`,
|
|
30362
|
+
` }`,
|
|
30363
|
+
``,
|
|
30364
|
+
` cache.set(key, compartmentFn);`,
|
|
30365
|
+
` return compartmentFn;`,
|
|
30366
|
+
`}`,
|
|
30367
|
+
``,
|
|
30368
|
+
`parentPort.on('message', function(msg) {`,
|
|
30369
|
+
` var id = msg.id;`,
|
|
30370
|
+
` try {`,
|
|
30371
|
+
` if (msg.type === 'compile') {`,
|
|
30372
|
+
` compileSource(msg.source);`,
|
|
30373
|
+
` parentPort.postMessage({ id: id, type: 'compiled' });`,
|
|
30374
|
+
` return;`,
|
|
30375
|
+
` }`,
|
|
30376
|
+
` if (msg.type === 'execute') {`,
|
|
30377
|
+
` var fn = compileSource(msg.source);`,
|
|
30378
|
+
` var raw;`,
|
|
30379
|
+
` try {`,
|
|
30380
|
+
` raw = fn(msg.jsonData);`,
|
|
30381
|
+
` } catch (err) {`,
|
|
30382
|
+
` parentPort.postMessage({ id: id, type: 'error', message: 'Migration function threw: ' + (err.message || String(err)) });`,
|
|
30383
|
+
` return;`,
|
|
30384
|
+
` }`,
|
|
30385
|
+
` if (raw !== null && typeof raw === 'object' && typeof raw.then === 'function') {`,
|
|
30386
|
+
` raw.then(`,
|
|
30387
|
+
` function(jsonResult) {`,
|
|
30388
|
+
` if (jsonResult === undefined || jsonResult === null) {`,
|
|
30389
|
+
` parentPort.postMessage({ id: id, type: 'error', message: 'Migration returned a non-JSON-serializable value' });`,
|
|
30390
|
+
` } else {`,
|
|
30391
|
+
` parentPort.postMessage({ id: id, type: 'result', jsonResult: jsonResult });`,
|
|
30392
|
+
` }`,
|
|
30393
|
+
` },`,
|
|
30394
|
+
` function(err) {`,
|
|
30395
|
+
` parentPort.postMessage({ id: id, type: 'error', message: 'Async migration function threw: ' + (err.message || String(err)) });`,
|
|
30396
|
+
` }`,
|
|
30397
|
+
` );`,
|
|
30398
|
+
` return;`,
|
|
30399
|
+
` }`,
|
|
30400
|
+
` if (raw === undefined || raw === null) {`,
|
|
30401
|
+
` parentPort.postMessage({ id: id, type: 'error', message: 'Migration returned a non-JSON-serializable value' });`,
|
|
30402
|
+
` } else {`,
|
|
30403
|
+
` parentPort.postMessage({ id: id, type: 'result', jsonResult: raw });`,
|
|
30404
|
+
` }`,
|
|
30405
|
+
` }`,
|
|
30406
|
+
` } catch (err) {`,
|
|
30407
|
+
` parentPort.postMessage({ id: id, type: 'error', message: err.message || String(err) });`,
|
|
30408
|
+
` }`,
|
|
30409
|
+
`});`
|
|
30410
|
+
].join("\n");
|
|
30411
|
+
var _WorkerCtor = null;
|
|
30412
|
+
async function loadWorkerCtor() {
|
|
30413
|
+
if (_WorkerCtor) return _WorkerCtor;
|
|
30414
|
+
const wt = await import("node:worker_threads");
|
|
30415
|
+
_WorkerCtor = wt.Worker;
|
|
30416
|
+
return _WorkerCtor;
|
|
30417
|
+
}
|
|
30418
|
+
async function ensureWorker() {
|
|
30419
|
+
if (_worker) return _worker;
|
|
30420
|
+
const Ctor = await loadWorkerCtor();
|
|
30421
|
+
_worker = new Ctor(WORKER_SOURCE, {
|
|
30422
|
+
eval: true,
|
|
30423
|
+
workerData: { parentUrl: import.meta.url }
|
|
30424
|
+
});
|
|
30425
|
+
_worker.unref();
|
|
30426
|
+
_worker.on("message", (msg) => {
|
|
30427
|
+
if (msg.id === void 0) return;
|
|
30428
|
+
const pending = _pending.get(msg.id);
|
|
30429
|
+
if (!pending) return;
|
|
30430
|
+
_pending.delete(msg.id);
|
|
30431
|
+
if (msg.type === "error") {
|
|
30432
|
+
pending.reject(new MigrationError(msg.message ?? "Unknown sandbox error"));
|
|
30433
|
+
} else {
|
|
30434
|
+
pending.resolve(msg);
|
|
30435
|
+
}
|
|
30436
|
+
});
|
|
30437
|
+
_worker.on("error", (err) => {
|
|
30438
|
+
for (const [, p] of _pending) {
|
|
30439
|
+
p.reject(new MigrationError(`Sandbox worker error: ${err.message}`));
|
|
30440
|
+
}
|
|
30441
|
+
_pending.clear();
|
|
30442
|
+
_worker = null;
|
|
30443
|
+
});
|
|
30444
|
+
_worker.on("exit", (code) => {
|
|
30445
|
+
if (_pending.size > 0) {
|
|
30446
|
+
for (const [, p] of _pending) {
|
|
30447
|
+
p.reject(new MigrationError(`Sandbox worker exited with code ${code}`));
|
|
30448
|
+
}
|
|
30449
|
+
_pending.clear();
|
|
30450
|
+
}
|
|
30451
|
+
_worker = null;
|
|
30452
|
+
});
|
|
30453
|
+
return _worker;
|
|
30454
|
+
}
|
|
30455
|
+
async function sendToWorker(msg) {
|
|
30456
|
+
const worker = await ensureWorker();
|
|
30457
|
+
if (_requestId >= Number.MAX_SAFE_INTEGER) _requestId = 0;
|
|
30458
|
+
const id = ++_requestId;
|
|
30459
|
+
return new Promise((resolve2, reject) => {
|
|
30460
|
+
_pending.set(id, { resolve: resolve2, reject });
|
|
30461
|
+
worker.postMessage({ ...msg, id });
|
|
30462
|
+
});
|
|
30463
|
+
}
|
|
30464
|
+
var compiledCache = /* @__PURE__ */ new WeakMap();
|
|
30465
|
+
function getExecutorCache(executor) {
|
|
30466
|
+
let cache = compiledCache.get(executor);
|
|
30467
|
+
if (!cache) {
|
|
30468
|
+
cache = /* @__PURE__ */ new Map();
|
|
30469
|
+
compiledCache.set(executor, cache);
|
|
30470
|
+
}
|
|
30471
|
+
return cache;
|
|
30472
|
+
}
|
|
30473
|
+
function hashSource(source) {
|
|
30474
|
+
return createHash2("sha256").update(source).digest("hex");
|
|
30475
|
+
}
|
|
30476
|
+
var _serializationModule = null;
|
|
30477
|
+
async function loadSerialization() {
|
|
30478
|
+
if (_serializationModule) return _serializationModule;
|
|
30479
|
+
_serializationModule = await Promise.resolve().then(() => (init_serialization(), serialization_exports));
|
|
30480
|
+
return _serializationModule;
|
|
30481
|
+
}
|
|
30482
|
+
function defaultExecutor(source) {
|
|
30483
|
+
return async (data) => {
|
|
30484
|
+
const { serializeFirestoreTypes: serializeFirestoreTypes2, deserializeFirestoreTypes: deserializeFirestoreTypes2 } = await loadSerialization();
|
|
30485
|
+
const jsonData = JSON.stringify(serializeFirestoreTypes2(data));
|
|
30486
|
+
const response = await sendToWorker({ type: "execute", source, jsonData });
|
|
30487
|
+
if (response.jsonResult === void 0 || response.jsonResult === null) {
|
|
30488
|
+
throw new MigrationError("Migration returned a non-JSON-serializable value");
|
|
30489
|
+
}
|
|
30490
|
+
try {
|
|
30491
|
+
return deserializeFirestoreTypes2(JSON.parse(response.jsonResult));
|
|
30492
|
+
} catch {
|
|
30493
|
+
throw new MigrationError("Migration returned a non-JSON-serializable value");
|
|
30494
|
+
}
|
|
30495
|
+
};
|
|
30496
|
+
}
|
|
30497
|
+
async function precompileSource(source, executor) {
|
|
30498
|
+
if (executor && executor !== defaultExecutor) {
|
|
30499
|
+
try {
|
|
30500
|
+
executor(source);
|
|
30501
|
+
} catch (err) {
|
|
30502
|
+
if (err instanceof MigrationError) throw err;
|
|
30503
|
+
throw new MigrationError(`Failed to compile migration source: ${err.message}`);
|
|
30504
|
+
}
|
|
30505
|
+
return;
|
|
30506
|
+
}
|
|
30507
|
+
await sendToWorker({ type: "compile", source });
|
|
30508
|
+
}
|
|
30509
|
+
function compileMigrationFn(source, executor = defaultExecutor) {
|
|
30510
|
+
const cache = getExecutorCache(executor);
|
|
30511
|
+
const key = hashSource(source);
|
|
30512
|
+
const cached2 = cache.get(key);
|
|
30513
|
+
if (cached2) return cached2;
|
|
30514
|
+
try {
|
|
30515
|
+
const fn = executor(source);
|
|
30516
|
+
cache.set(key, fn);
|
|
30517
|
+
return fn;
|
|
30518
|
+
} catch (err) {
|
|
30519
|
+
if (err instanceof MigrationError) throw err;
|
|
30520
|
+
throw new MigrationError(`Failed to compile migration source: ${err.message}`);
|
|
30521
|
+
}
|
|
30522
|
+
}
|
|
30523
|
+
function compileMigrations(stored, executor) {
|
|
30524
|
+
return stored.map((step) => ({
|
|
30525
|
+
fromVersion: step.fromVersion,
|
|
30526
|
+
toVersion: step.toVersion,
|
|
30527
|
+
up: compileMigrationFn(step.up, executor)
|
|
30528
|
+
}));
|
|
30529
|
+
}
|
|
30530
|
+
|
|
30531
|
+
// src/dynamic-registry.ts
|
|
30532
|
+
var META_NODE_TYPE = "nodeType";
|
|
30533
|
+
var META_EDGE_TYPE = "edgeType";
|
|
30534
|
+
var STORED_MIGRATION_STEP_SCHEMA = {
|
|
30535
|
+
type: "object",
|
|
30536
|
+
required: ["fromVersion", "toVersion", "up"],
|
|
30537
|
+
properties: {
|
|
30538
|
+
fromVersion: { type: "integer", minimum: 0 },
|
|
30539
|
+
toVersion: { type: "integer", minimum: 1 },
|
|
30540
|
+
up: { type: "string", minLength: 1 }
|
|
30541
|
+
},
|
|
30542
|
+
additionalProperties: false
|
|
30543
|
+
};
|
|
30544
|
+
var NODE_TYPE_SCHEMA = {
|
|
30545
|
+
type: "object",
|
|
30546
|
+
required: ["name", "jsonSchema"],
|
|
30547
|
+
properties: {
|
|
30548
|
+
name: { type: "string", minLength: 1 },
|
|
30549
|
+
jsonSchema: { type: "object" },
|
|
30550
|
+
description: { type: "string" },
|
|
30551
|
+
titleField: { type: "string" },
|
|
30552
|
+
subtitleField: { type: "string" },
|
|
30553
|
+
viewTemplate: { type: "string" },
|
|
30554
|
+
viewCss: { type: "string" },
|
|
30555
|
+
allowedIn: { type: "array", items: { type: "string", minLength: 1 } },
|
|
30556
|
+
schemaVersion: { type: "integer", minimum: 0 },
|
|
30557
|
+
migrations: { type: "array", items: STORED_MIGRATION_STEP_SCHEMA },
|
|
30558
|
+
migrationWriteBack: { type: "string", enum: ["off", "eager", "background"] }
|
|
30559
|
+
},
|
|
30560
|
+
additionalProperties: false
|
|
30561
|
+
};
|
|
30562
|
+
var EDGE_TYPE_SCHEMA = {
|
|
30563
|
+
type: "object",
|
|
30564
|
+
required: ["name", "from", "to"],
|
|
30565
|
+
properties: {
|
|
30566
|
+
name: { type: "string", minLength: 1 },
|
|
30567
|
+
from: {
|
|
30568
|
+
oneOf: [
|
|
30569
|
+
{ type: "string", minLength: 1 },
|
|
30570
|
+
{ type: "array", items: { type: "string", minLength: 1 }, minItems: 1 }
|
|
30571
|
+
]
|
|
30572
|
+
},
|
|
30573
|
+
to: {
|
|
30574
|
+
oneOf: [
|
|
30575
|
+
{ type: "string", minLength: 1 },
|
|
30576
|
+
{ type: "array", items: { type: "string", minLength: 1 }, minItems: 1 }
|
|
30577
|
+
]
|
|
30578
|
+
},
|
|
30579
|
+
jsonSchema: { type: "object" },
|
|
30580
|
+
inverseLabel: { type: "string" },
|
|
30581
|
+
description: { type: "string" },
|
|
30582
|
+
titleField: { type: "string" },
|
|
30583
|
+
subtitleField: { type: "string" },
|
|
30584
|
+
viewTemplate: { type: "string" },
|
|
30585
|
+
viewCss: { type: "string" },
|
|
30586
|
+
allowedIn: { type: "array", items: { type: "string", minLength: 1 } },
|
|
30587
|
+
targetGraph: { type: "string", minLength: 1, pattern: "^[^/]+$" },
|
|
30588
|
+
schemaVersion: { type: "integer", minimum: 0 },
|
|
30589
|
+
migrations: { type: "array", items: STORED_MIGRATION_STEP_SCHEMA },
|
|
30590
|
+
migrationWriteBack: { type: "string", enum: ["off", "eager", "background"] }
|
|
30591
|
+
},
|
|
30592
|
+
additionalProperties: false
|
|
30593
|
+
};
|
|
30594
|
+
var BOOTSTRAP_ENTRIES = [
|
|
30595
|
+
{
|
|
30596
|
+
aType: META_NODE_TYPE,
|
|
30597
|
+
axbType: NODE_RELATION,
|
|
30598
|
+
bType: META_NODE_TYPE,
|
|
30599
|
+
jsonSchema: NODE_TYPE_SCHEMA,
|
|
30600
|
+
description: "Meta-type: defines a node type"
|
|
30601
|
+
},
|
|
30602
|
+
{
|
|
30603
|
+
aType: META_EDGE_TYPE,
|
|
30604
|
+
axbType: NODE_RELATION,
|
|
30605
|
+
bType: META_EDGE_TYPE,
|
|
30606
|
+
jsonSchema: EDGE_TYPE_SCHEMA,
|
|
30607
|
+
description: "Meta-type: defines an edge type"
|
|
30608
|
+
}
|
|
30609
|
+
];
|
|
30610
|
+
function createBootstrapRegistry() {
|
|
30611
|
+
return createRegistry([...BOOTSTRAP_ENTRIES]);
|
|
30612
|
+
}
|
|
30613
|
+
function generateDeterministicUid(metaType, name) {
|
|
30614
|
+
const hash2 = createHash3("sha256").update(`${metaType}:${name}`).digest("base64url");
|
|
30615
|
+
return hash2.slice(0, 21);
|
|
30616
|
+
}
|
|
30617
|
+
async function createRegistryFromGraph(reader, executor) {
|
|
30618
|
+
const [nodeTypes, edgeTypes] = await Promise.all([
|
|
30619
|
+
reader.findNodes({ aType: META_NODE_TYPE }),
|
|
30620
|
+
reader.findNodes({ aType: META_EDGE_TYPE })
|
|
30621
|
+
]);
|
|
30622
|
+
const entries = [...BOOTSTRAP_ENTRIES];
|
|
30623
|
+
const prevalidations = [];
|
|
30624
|
+
for (const record2 of nodeTypes) {
|
|
30625
|
+
const data = record2.data;
|
|
30626
|
+
if (data.migrations) {
|
|
30627
|
+
for (const m of data.migrations) {
|
|
30628
|
+
prevalidations.push(precompileSource(m.up, executor));
|
|
30629
|
+
}
|
|
30630
|
+
}
|
|
30631
|
+
}
|
|
30632
|
+
for (const record2 of edgeTypes) {
|
|
30633
|
+
const data = record2.data;
|
|
30634
|
+
if (data.migrations) {
|
|
30635
|
+
for (const m of data.migrations) {
|
|
30636
|
+
prevalidations.push(precompileSource(m.up, executor));
|
|
30637
|
+
}
|
|
30638
|
+
}
|
|
30639
|
+
}
|
|
30640
|
+
await Promise.all(prevalidations);
|
|
30641
|
+
for (const record2 of nodeTypes) {
|
|
30642
|
+
const data = record2.data;
|
|
30643
|
+
entries.push({
|
|
30644
|
+
aType: data.name,
|
|
30645
|
+
axbType: NODE_RELATION,
|
|
30646
|
+
bType: data.name,
|
|
30647
|
+
jsonSchema: data.jsonSchema,
|
|
30648
|
+
description: data.description,
|
|
30649
|
+
titleField: data.titleField,
|
|
30650
|
+
subtitleField: data.subtitleField,
|
|
30651
|
+
allowedIn: data.allowedIn,
|
|
30652
|
+
migrations: data.migrations ? compileMigrations(data.migrations, executor) : void 0,
|
|
30653
|
+
migrationWriteBack: data.migrationWriteBack
|
|
30654
|
+
});
|
|
30655
|
+
}
|
|
30656
|
+
for (const record2 of edgeTypes) {
|
|
30657
|
+
const data = record2.data;
|
|
30658
|
+
const fromTypes = Array.isArray(data.from) ? data.from : [data.from];
|
|
30659
|
+
const toTypes = Array.isArray(data.to) ? data.to : [data.to];
|
|
30660
|
+
const compiledMigrations = data.migrations ? compileMigrations(data.migrations, executor) : void 0;
|
|
30661
|
+
for (const aType of fromTypes) {
|
|
30662
|
+
for (const bType of toTypes) {
|
|
30663
|
+
entries.push({
|
|
30664
|
+
aType,
|
|
30665
|
+
axbType: data.name,
|
|
30666
|
+
bType,
|
|
30667
|
+
jsonSchema: data.jsonSchema,
|
|
30668
|
+
description: data.description,
|
|
30669
|
+
inverseLabel: data.inverseLabel,
|
|
30670
|
+
titleField: data.titleField,
|
|
30671
|
+
subtitleField: data.subtitleField,
|
|
30672
|
+
allowedIn: data.allowedIn,
|
|
30673
|
+
targetGraph: data.targetGraph,
|
|
30674
|
+
migrations: compiledMigrations,
|
|
30675
|
+
migrationWriteBack: data.migrationWriteBack
|
|
30676
|
+
});
|
|
30677
|
+
}
|
|
30678
|
+
}
|
|
30679
|
+
}
|
|
30680
|
+
return createRegistry(entries);
|
|
30681
|
+
}
|
|
30682
|
+
|
|
30683
|
+
// src/query.ts
|
|
30684
|
+
function buildEdgeQueryPlan(params) {
|
|
30685
|
+
const { aType, aUid, axbType, bType, bUid, limit, orderBy } = params;
|
|
30686
|
+
if (aUid && axbType && bUid && !params.where?.length) {
|
|
30687
|
+
return { strategy: "get", docId: computeEdgeDocId(aUid, axbType, bUid) };
|
|
30688
|
+
}
|
|
30689
|
+
const filters = [];
|
|
30690
|
+
if (aType) filters.push({ field: "aType", op: "==", value: aType });
|
|
30691
|
+
if (aUid) filters.push({ field: "aUid", op: "==", value: aUid });
|
|
30692
|
+
if (axbType) filters.push({ field: "axbType", op: "==", value: axbType });
|
|
30693
|
+
if (bType) filters.push({ field: "bType", op: "==", value: bType });
|
|
30694
|
+
if (bUid) filters.push({ field: "bUid", op: "==", value: bUid });
|
|
30695
|
+
if (params.where) {
|
|
30696
|
+
for (const clause of params.where) {
|
|
30697
|
+
const field = BUILTIN_FIELDS.has(clause.field) ? clause.field : clause.field.startsWith("data.") ? clause.field : `data.${clause.field}`;
|
|
30698
|
+
filters.push({ field, op: clause.op, value: clause.value });
|
|
30699
|
+
}
|
|
30700
|
+
}
|
|
30701
|
+
if (filters.length === 0) {
|
|
30702
|
+
throw new InvalidQueryError("findEdges requires at least one filter parameter");
|
|
30703
|
+
}
|
|
30704
|
+
const effectiveLimit = limit === void 0 ? DEFAULT_QUERY_LIMIT : limit || void 0;
|
|
30705
|
+
return { strategy: "query", filters, options: { limit: effectiveLimit, orderBy } };
|
|
30706
|
+
}
|
|
30707
|
+
function buildNodeQueryPlan(params) {
|
|
30708
|
+
const { aType, limit, orderBy } = params;
|
|
30709
|
+
const filters = [
|
|
30710
|
+
{ field: "aType", op: "==", value: aType },
|
|
30711
|
+
{ field: "axbType", op: "==", value: NODE_RELATION }
|
|
30712
|
+
];
|
|
30713
|
+
if (params.where) {
|
|
30714
|
+
for (const clause of params.where) {
|
|
30715
|
+
const field = BUILTIN_FIELDS.has(clause.field) ? clause.field : clause.field.startsWith("data.") ? clause.field : `data.${clause.field}`;
|
|
30716
|
+
filters.push({ field, op: clause.op, value: clause.value });
|
|
30717
|
+
}
|
|
30718
|
+
}
|
|
30719
|
+
const effectiveLimit = limit === void 0 ? DEFAULT_QUERY_LIMIT : limit || void 0;
|
|
30720
|
+
return { strategy: "query", filters, options: { limit: effectiveLimit, orderBy } };
|
|
30721
|
+
}
|
|
30722
|
+
|
|
30723
|
+
// src/query-safety.ts
|
|
30724
|
+
var SAFE_INDEX_PATTERNS = [
|
|
30725
|
+
/* @__PURE__ */ new Set(["aUid", "axbType"]),
|
|
30726
|
+
/* @__PURE__ */ new Set(["axbType", "bUid"]),
|
|
30727
|
+
/* @__PURE__ */ new Set(["aType", "axbType"]),
|
|
30728
|
+
/* @__PURE__ */ new Set(["axbType", "bType"])
|
|
30729
|
+
];
|
|
30730
|
+
function analyzeQuerySafety(filters) {
|
|
30731
|
+
const builtinFieldsPresent = /* @__PURE__ */ new Set();
|
|
30732
|
+
let hasDataFilters = false;
|
|
30733
|
+
for (const f of filters) {
|
|
30734
|
+
if (BUILTIN_FIELDS.has(f.field)) {
|
|
30735
|
+
builtinFieldsPresent.add(f.field);
|
|
30736
|
+
} else {
|
|
30737
|
+
hasDataFilters = true;
|
|
30146
30738
|
}
|
|
30147
|
-
seen.add(step.fromVersion);
|
|
30148
30739
|
}
|
|
30149
|
-
const
|
|
30150
|
-
|
|
30151
|
-
|
|
30152
|
-
|
|
30153
|
-
|
|
30154
|
-
|
|
30155
|
-
|
|
30156
|
-
|
|
30157
|
-
|
|
30158
|
-
|
|
30740
|
+
for (const pattern of SAFE_INDEX_PATTERNS) {
|
|
30741
|
+
let matched = true;
|
|
30742
|
+
for (const field of pattern) {
|
|
30743
|
+
if (!builtinFieldsPresent.has(field)) {
|
|
30744
|
+
matched = false;
|
|
30745
|
+
break;
|
|
30746
|
+
}
|
|
30747
|
+
}
|
|
30748
|
+
if (matched) {
|
|
30749
|
+
return { safe: true };
|
|
30159
30750
|
}
|
|
30160
30751
|
}
|
|
30161
|
-
|
|
30162
|
-
|
|
30163
|
-
|
|
30164
|
-
|
|
30165
|
-
|
|
30166
|
-
}
|
|
30167
|
-
async function migrateRecord(record2, registry2, globalWriteBack = "off") {
|
|
30168
|
-
const entry = registry2.lookup(record2.aType, record2.axbType, record2.bType);
|
|
30169
|
-
if (!entry?.migrations?.length || !entry.schemaVersion) {
|
|
30170
|
-
return { record: record2, migrated: false, writeBack: "off" };
|
|
30752
|
+
const presentFields = [...builtinFieldsPresent];
|
|
30753
|
+
if (presentFields.length === 0 && hasDataFilters) {
|
|
30754
|
+
return {
|
|
30755
|
+
safe: false,
|
|
30756
|
+
reason: "Query filters only use data.* fields with no builtin field constraints. This requires a full collection scan. Add aType, aUid, axbType, bType, or bUid filters, or set allowCollectionScan: true."
|
|
30757
|
+
};
|
|
30171
30758
|
}
|
|
30172
|
-
|
|
30173
|
-
|
|
30174
|
-
|
|
30759
|
+
if (hasDataFilters) {
|
|
30760
|
+
return {
|
|
30761
|
+
safe: false,
|
|
30762
|
+
reason: `Query filters on [${presentFields.join(", ")}] do not match any indexed pattern. data.* filters without an indexed base require a full collection scan. Safe patterns: (aUid + axbType), (axbType + bUid), (aType + axbType), (axbType + bType). Set allowCollectionScan: true to override.`
|
|
30763
|
+
};
|
|
30175
30764
|
}
|
|
30176
|
-
const migratedData = await applyMigrationChain(
|
|
30177
|
-
record2.data,
|
|
30178
|
-
currentVersion,
|
|
30179
|
-
entry.schemaVersion,
|
|
30180
|
-
entry.migrations
|
|
30181
|
-
);
|
|
30182
|
-
const writeBack = entry.migrationWriteBack ?? globalWriteBack ?? "off";
|
|
30183
30765
|
return {
|
|
30184
|
-
|
|
30185
|
-
|
|
30186
|
-
writeBack
|
|
30766
|
+
safe: false,
|
|
30767
|
+
reason: `Query filters on [${presentFields.join(", ")}] do not match any indexed pattern. This may cause a full collection scan on Firestore Enterprise. Safe patterns: (aUid + axbType), (axbType + bUid), (aType + axbType), (axbType + bType). Set allowCollectionScan: true to override.`
|
|
30187
30768
|
};
|
|
30188
30769
|
}
|
|
30189
|
-
async function migrateRecords(records, registry2, globalWriteBack = "off") {
|
|
30190
|
-
return Promise.all(
|
|
30191
|
-
records.map((r) => migrateRecord(r, registry2, globalWriteBack))
|
|
30192
|
-
);
|
|
30193
|
-
}
|
|
30194
30770
|
|
|
30195
30771
|
// src/transaction.ts
|
|
30772
|
+
function buildWritableNodeRecord2(aType, uid, data) {
|
|
30773
|
+
return { aType, aUid: uid, axbType: NODE_RELATION, bType: aType, bUid: uid, data };
|
|
30774
|
+
}
|
|
30775
|
+
function buildWritableEdgeRecord2(aType, aUid, axbType, bType, bUid, data) {
|
|
30776
|
+
return { aType, aUid, axbType, bType, bUid, data };
|
|
30777
|
+
}
|
|
30196
30778
|
var GraphTransactionImpl = class {
|
|
30197
|
-
constructor(
|
|
30198
|
-
this.
|
|
30779
|
+
constructor(backend, registry2, scanProtection = "error", scopePath = "", globalWriteBack = "off") {
|
|
30780
|
+
this.backend = backend;
|
|
30199
30781
|
this.registry = registry2;
|
|
30200
30782
|
this.scanProtection = scanProtection;
|
|
30201
30783
|
this.scopePath = scopePath;
|
|
30202
30784
|
this.globalWriteBack = globalWriteBack;
|
|
30203
|
-
this.db = db2;
|
|
30204
30785
|
}
|
|
30205
30786
|
async getNode(uid) {
|
|
30206
30787
|
const docId = computeNodeDocId(uid);
|
|
30207
|
-
const record2 = await this.
|
|
30788
|
+
const record2 = await this.backend.getDoc(docId);
|
|
30208
30789
|
if (!record2 || !this.registry) return record2;
|
|
30209
30790
|
const result = await migrateRecord(record2, this.registry, this.globalWriteBack);
|
|
30210
30791
|
if (result.migrated && result.writeBack !== "off") {
|
|
30211
|
-
|
|
30212
|
-
|
|
30213
|
-
|
|
30214
|
-
};
|
|
30215
|
-
if (result.record.v !== void 0) {
|
|
30216
|
-
update.v = result.record.v;
|
|
30217
|
-
}
|
|
30218
|
-
this.adapter.updateDoc(docId, update);
|
|
30792
|
+
await this.backend.updateDoc(docId, {
|
|
30793
|
+
replaceData: result.record.data,
|
|
30794
|
+
v: result.record.v
|
|
30795
|
+
});
|
|
30219
30796
|
}
|
|
30220
30797
|
return result.record;
|
|
30221
30798
|
}
|
|
30222
30799
|
async getEdge(aUid, axbType, bUid) {
|
|
30223
30800
|
const docId = computeEdgeDocId(aUid, axbType, bUid);
|
|
30224
|
-
const record2 = await this.
|
|
30801
|
+
const record2 = await this.backend.getDoc(docId);
|
|
30225
30802
|
if (!record2 || !this.registry) return record2;
|
|
30226
30803
|
const result = await migrateRecord(record2, this.registry, this.globalWriteBack);
|
|
30227
30804
|
if (result.migrated && result.writeBack !== "off") {
|
|
30228
|
-
|
|
30229
|
-
|
|
30230
|
-
|
|
30231
|
-
};
|
|
30232
|
-
if (result.record.v !== void 0) {
|
|
30233
|
-
update.v = result.record.v;
|
|
30234
|
-
}
|
|
30235
|
-
this.adapter.updateDoc(docId, update);
|
|
30805
|
+
await this.backend.updateDoc(docId, {
|
|
30806
|
+
replaceData: result.record.data,
|
|
30807
|
+
v: result.record.v
|
|
30808
|
+
});
|
|
30236
30809
|
}
|
|
30237
30810
|
return result.record;
|
|
30238
30811
|
}
|
|
30239
30812
|
async edgeExists(aUid, axbType, bUid) {
|
|
30240
30813
|
const docId = computeEdgeDocId(aUid, axbType, bUid);
|
|
30241
|
-
const record2 = await this.
|
|
30814
|
+
const record2 = await this.backend.getDoc(docId);
|
|
30242
30815
|
return record2 !== null;
|
|
30243
30816
|
}
|
|
30244
30817
|
checkQuerySafety(filters, allowCollectionScan) {
|
|
@@ -30254,11 +30827,235 @@ var GraphTransactionImpl = class {
|
|
|
30254
30827
|
const plan = buildEdgeQueryPlan(params);
|
|
30255
30828
|
let records;
|
|
30256
30829
|
if (plan.strategy === "get") {
|
|
30257
|
-
const record2 = await this.
|
|
30830
|
+
const record2 = await this.backend.getDoc(plan.docId);
|
|
30831
|
+
records = record2 ? [record2] : [];
|
|
30832
|
+
} else {
|
|
30833
|
+
this.checkQuerySafety(plan.filters, params.allowCollectionScan);
|
|
30834
|
+
records = await this.backend.query(plan.filters, plan.options);
|
|
30835
|
+
}
|
|
30836
|
+
return this.applyMigrations(records);
|
|
30837
|
+
}
|
|
30838
|
+
async findNodes(params) {
|
|
30839
|
+
const plan = buildNodeQueryPlan(params);
|
|
30840
|
+
let records;
|
|
30841
|
+
if (plan.strategy === "get") {
|
|
30842
|
+
const record2 = await this.backend.getDoc(plan.docId);
|
|
30843
|
+
records = record2 ? [record2] : [];
|
|
30844
|
+
} else {
|
|
30845
|
+
this.checkQuerySafety(plan.filters, params.allowCollectionScan);
|
|
30846
|
+
records = await this.backend.query(plan.filters, plan.options);
|
|
30847
|
+
}
|
|
30848
|
+
return this.applyMigrations(records);
|
|
30849
|
+
}
|
|
30850
|
+
async applyMigrations(records) {
|
|
30851
|
+
if (!this.registry || records.length === 0) return records;
|
|
30852
|
+
const results = await migrateRecords(records, this.registry, this.globalWriteBack);
|
|
30853
|
+
for (const result of results) {
|
|
30854
|
+
if (result.migrated && result.writeBack !== "off") {
|
|
30855
|
+
const docId = result.record.axbType === NODE_RELATION ? computeNodeDocId(result.record.aUid) : computeEdgeDocId(result.record.aUid, result.record.axbType, result.record.bUid);
|
|
30856
|
+
await this.backend.updateDoc(docId, {
|
|
30857
|
+
replaceData: result.record.data,
|
|
30858
|
+
v: result.record.v
|
|
30859
|
+
});
|
|
30860
|
+
}
|
|
30861
|
+
}
|
|
30862
|
+
return results.map((r) => r.record);
|
|
30863
|
+
}
|
|
30864
|
+
async putNode(aType, uid, data) {
|
|
30865
|
+
if (this.registry) {
|
|
30866
|
+
this.registry.validate(aType, NODE_RELATION, aType, data, this.scopePath);
|
|
30867
|
+
}
|
|
30868
|
+
const docId = computeNodeDocId(uid);
|
|
30869
|
+
const record2 = buildWritableNodeRecord2(aType, uid, data);
|
|
30870
|
+
if (this.registry) {
|
|
30871
|
+
const entry = this.registry.lookup(aType, NODE_RELATION, aType);
|
|
30872
|
+
if (entry?.schemaVersion && entry.schemaVersion > 0) {
|
|
30873
|
+
record2.v = entry.schemaVersion;
|
|
30874
|
+
}
|
|
30875
|
+
}
|
|
30876
|
+
await this.backend.setDoc(docId, record2);
|
|
30877
|
+
}
|
|
30878
|
+
async putEdge(aType, aUid, axbType, bType, bUid, data) {
|
|
30879
|
+
if (this.registry) {
|
|
30880
|
+
this.registry.validate(aType, axbType, bType, data, this.scopePath);
|
|
30881
|
+
}
|
|
30882
|
+
const docId = computeEdgeDocId(aUid, axbType, bUid);
|
|
30883
|
+
const record2 = buildWritableEdgeRecord2(aType, aUid, axbType, bType, bUid, data);
|
|
30884
|
+
if (this.registry) {
|
|
30885
|
+
const entry = this.registry.lookup(aType, axbType, bType);
|
|
30886
|
+
if (entry?.schemaVersion && entry.schemaVersion > 0) {
|
|
30887
|
+
record2.v = entry.schemaVersion;
|
|
30888
|
+
}
|
|
30889
|
+
}
|
|
30890
|
+
await this.backend.setDoc(docId, record2);
|
|
30891
|
+
}
|
|
30892
|
+
async updateNode(uid, data) {
|
|
30893
|
+
const docId = computeNodeDocId(uid);
|
|
30894
|
+
await this.backend.updateDoc(docId, { dataFields: data });
|
|
30895
|
+
}
|
|
30896
|
+
async removeNode(uid) {
|
|
30897
|
+
const docId = computeNodeDocId(uid);
|
|
30898
|
+
await this.backend.deleteDoc(docId);
|
|
30899
|
+
}
|
|
30900
|
+
async removeEdge(aUid, axbType, bUid) {
|
|
30901
|
+
const docId = computeEdgeDocId(aUid, axbType, bUid);
|
|
30902
|
+
await this.backend.deleteDoc(docId);
|
|
30903
|
+
}
|
|
30904
|
+
};
|
|
30905
|
+
|
|
30906
|
+
// src/client.ts
|
|
30907
|
+
var RESERVED_TYPE_NAMES = /* @__PURE__ */ new Set([META_NODE_TYPE, META_EDGE_TYPE]);
|
|
30908
|
+
function buildWritableNodeRecord3(aType, uid, data) {
|
|
30909
|
+
return { aType, aUid: uid, axbType: NODE_RELATION, bType: aType, bUid: uid, data };
|
|
30910
|
+
}
|
|
30911
|
+
function buildWritableEdgeRecord3(aType, aUid, axbType, bType, bUid, data) {
|
|
30912
|
+
return { aType, aUid, axbType, bType, bUid, data };
|
|
30913
|
+
}
|
|
30914
|
+
var GraphClientImpl = class _GraphClientImpl {
|
|
30915
|
+
constructor(backend, options, metaBackend) {
|
|
30916
|
+
this.backend = backend;
|
|
30917
|
+
this.globalWriteBack = options?.migrationWriteBack ?? "off";
|
|
30918
|
+
this.migrationSandbox = options?.migrationSandbox;
|
|
30919
|
+
if (options?.registryMode) {
|
|
30920
|
+
this.dynamicConfig = options.registryMode;
|
|
30921
|
+
this.bootstrapRegistry = createBootstrapRegistry();
|
|
30922
|
+
if (options.registry) {
|
|
30923
|
+
this.staticRegistry = options.registry;
|
|
30924
|
+
}
|
|
30925
|
+
this.metaBackend = metaBackend;
|
|
30926
|
+
} else {
|
|
30927
|
+
this.staticRegistry = options?.registry;
|
|
30928
|
+
}
|
|
30929
|
+
this.scanProtection = options?.scanProtection ?? "error";
|
|
30930
|
+
}
|
|
30931
|
+
scanProtection;
|
|
30932
|
+
// Static mode
|
|
30933
|
+
staticRegistry;
|
|
30934
|
+
// Dynamic mode
|
|
30935
|
+
dynamicConfig;
|
|
30936
|
+
bootstrapRegistry;
|
|
30937
|
+
dynamicRegistry;
|
|
30938
|
+
metaBackend;
|
|
30939
|
+
// Migration settings
|
|
30940
|
+
globalWriteBack;
|
|
30941
|
+
migrationSandbox;
|
|
30942
|
+
// ---------------------------------------------------------------------------
|
|
30943
|
+
// Backend access (exposed for traversal helpers and subgraph cloning)
|
|
30944
|
+
// ---------------------------------------------------------------------------
|
|
30945
|
+
/** @internal */
|
|
30946
|
+
getBackend() {
|
|
30947
|
+
return this.backend;
|
|
30948
|
+
}
|
|
30949
|
+
// ---------------------------------------------------------------------------
|
|
30950
|
+
// Registry routing
|
|
30951
|
+
// ---------------------------------------------------------------------------
|
|
30952
|
+
getRegistryForType(aType) {
|
|
30953
|
+
if (!this.dynamicConfig) return this.staticRegistry;
|
|
30954
|
+
if (aType === META_NODE_TYPE || aType === META_EDGE_TYPE) {
|
|
30955
|
+
return this.bootstrapRegistry;
|
|
30956
|
+
}
|
|
30957
|
+
return this.dynamicRegistry ?? this.staticRegistry ?? this.bootstrapRegistry;
|
|
30958
|
+
}
|
|
30959
|
+
getBackendForType(aType) {
|
|
30960
|
+
if (this.metaBackend && (aType === META_NODE_TYPE || aType === META_EDGE_TYPE)) {
|
|
30961
|
+
return this.metaBackend;
|
|
30962
|
+
}
|
|
30963
|
+
return this.backend;
|
|
30964
|
+
}
|
|
30965
|
+
getCombinedRegistry() {
|
|
30966
|
+
if (!this.dynamicConfig) return this.staticRegistry;
|
|
30967
|
+
return this.dynamicRegistry ?? this.staticRegistry ?? this.bootstrapRegistry;
|
|
30968
|
+
}
|
|
30969
|
+
// ---------------------------------------------------------------------------
|
|
30970
|
+
// Query safety
|
|
30971
|
+
// ---------------------------------------------------------------------------
|
|
30972
|
+
checkQuerySafety(filters, allowCollectionScan) {
|
|
30973
|
+
if (allowCollectionScan || this.scanProtection === "off") return;
|
|
30974
|
+
const result = analyzeQuerySafety(filters);
|
|
30975
|
+
if (result.safe) return;
|
|
30976
|
+
if (this.scanProtection === "error") {
|
|
30977
|
+
throw new QuerySafetyError(result.reason);
|
|
30978
|
+
}
|
|
30979
|
+
console.warn(`[firegraph] Query safety warning: ${result.reason}`);
|
|
30980
|
+
}
|
|
30981
|
+
// ---------------------------------------------------------------------------
|
|
30982
|
+
// Migration helpers
|
|
30983
|
+
// ---------------------------------------------------------------------------
|
|
30984
|
+
async applyMigration(record2, docId) {
|
|
30985
|
+
const registry2 = this.getCombinedRegistry();
|
|
30986
|
+
if (!registry2) return record2;
|
|
30987
|
+
const result = await migrateRecord(record2, registry2, this.globalWriteBack);
|
|
30988
|
+
if (result.migrated) {
|
|
30989
|
+
this.handleWriteBack(result, docId);
|
|
30990
|
+
}
|
|
30991
|
+
return result.record;
|
|
30992
|
+
}
|
|
30993
|
+
async applyMigrations(records) {
|
|
30994
|
+
const registry2 = this.getCombinedRegistry();
|
|
30995
|
+
if (!registry2 || records.length === 0) return records;
|
|
30996
|
+
const results = await migrateRecords(records, registry2, this.globalWriteBack);
|
|
30997
|
+
for (const result of results) {
|
|
30998
|
+
if (result.migrated) {
|
|
30999
|
+
const docId = result.record.axbType === NODE_RELATION ? computeNodeDocId(result.record.aUid) : computeEdgeDocId(result.record.aUid, result.record.axbType, result.record.bUid);
|
|
31000
|
+
this.handleWriteBack(result, docId);
|
|
31001
|
+
}
|
|
31002
|
+
}
|
|
31003
|
+
return results.map((r) => r.record);
|
|
31004
|
+
}
|
|
31005
|
+
/**
|
|
31006
|
+
* Fire-and-forget write-back for a migrated record. Both `'eager'` and
|
|
31007
|
+
* `'background'` are non-blocking; the difference is the log level on
|
|
31008
|
+
* failure. For synchronous write-back, use a transaction — see
|
|
31009
|
+
* `GraphTransactionImpl`.
|
|
31010
|
+
*/
|
|
31011
|
+
handleWriteBack(result, docId) {
|
|
31012
|
+
if (result.writeBack === "off") return;
|
|
31013
|
+
const doWriteBack = async () => {
|
|
31014
|
+
try {
|
|
31015
|
+
await this.backend.updateDoc(docId, {
|
|
31016
|
+
replaceData: result.record.data,
|
|
31017
|
+
v: result.record.v
|
|
31018
|
+
});
|
|
31019
|
+
} catch (err) {
|
|
31020
|
+
const msg = `[firegraph] Migration write-back failed for ${docId}: ${err.message}`;
|
|
31021
|
+
if (result.writeBack === "eager") {
|
|
31022
|
+
console.error(msg);
|
|
31023
|
+
} else {
|
|
31024
|
+
console.warn(msg);
|
|
31025
|
+
}
|
|
31026
|
+
}
|
|
31027
|
+
};
|
|
31028
|
+
void doWriteBack();
|
|
31029
|
+
}
|
|
31030
|
+
// ---------------------------------------------------------------------------
|
|
31031
|
+
// GraphReader
|
|
31032
|
+
// ---------------------------------------------------------------------------
|
|
31033
|
+
async getNode(uid) {
|
|
31034
|
+
const docId = computeNodeDocId(uid);
|
|
31035
|
+
const record2 = await this.backend.getDoc(docId);
|
|
31036
|
+
if (!record2) return null;
|
|
31037
|
+
return this.applyMigration(record2, docId);
|
|
31038
|
+
}
|
|
31039
|
+
async getEdge(aUid, axbType, bUid) {
|
|
31040
|
+
const docId = computeEdgeDocId(aUid, axbType, bUid);
|
|
31041
|
+
const record2 = await this.backend.getDoc(docId);
|
|
31042
|
+
if (!record2) return null;
|
|
31043
|
+
return this.applyMigration(record2, docId);
|
|
31044
|
+
}
|
|
31045
|
+
async edgeExists(aUid, axbType, bUid) {
|
|
31046
|
+
const docId = computeEdgeDocId(aUid, axbType, bUid);
|
|
31047
|
+
const record2 = await this.backend.getDoc(docId);
|
|
31048
|
+
return record2 !== null;
|
|
31049
|
+
}
|
|
31050
|
+
async findEdges(params) {
|
|
31051
|
+
const plan = buildEdgeQueryPlan(params);
|
|
31052
|
+
let records;
|
|
31053
|
+
if (plan.strategy === "get") {
|
|
31054
|
+
const record2 = await this.backend.getDoc(plan.docId);
|
|
30258
31055
|
records = record2 ? [record2] : [];
|
|
30259
31056
|
} else {
|
|
30260
31057
|
this.checkQuerySafety(plan.filters, params.allowCollectionScan);
|
|
30261
|
-
records = await this.
|
|
31058
|
+
records = await this.backend.query(plan.filters, plan.options);
|
|
30262
31059
|
}
|
|
30263
31060
|
return this.applyMigrations(records);
|
|
30264
31061
|
}
|
|
@@ -30266,1309 +31063,916 @@ var GraphTransactionImpl = class {
|
|
|
30266
31063
|
const plan = buildNodeQueryPlan(params);
|
|
30267
31064
|
let records;
|
|
30268
31065
|
if (plan.strategy === "get") {
|
|
30269
|
-
const record2 = await this.
|
|
31066
|
+
const record2 = await this.backend.getDoc(plan.docId);
|
|
30270
31067
|
records = record2 ? [record2] : [];
|
|
30271
31068
|
} else {
|
|
30272
31069
|
this.checkQuerySafety(plan.filters, params.allowCollectionScan);
|
|
30273
|
-
records = await this.
|
|
31070
|
+
records = await this.backend.query(plan.filters, plan.options);
|
|
30274
31071
|
}
|
|
30275
31072
|
return this.applyMigrations(records);
|
|
30276
31073
|
}
|
|
30277
|
-
|
|
30278
|
-
|
|
30279
|
-
|
|
30280
|
-
for (const result of results) {
|
|
30281
|
-
if (result.migrated && result.writeBack !== "off") {
|
|
30282
|
-
const docId = result.record.axbType === NODE_RELATION ? computeNodeDocId(result.record.aUid) : computeEdgeDocId(result.record.aUid, result.record.axbType, result.record.bUid);
|
|
30283
|
-
const update = {
|
|
30284
|
-
data: deserializeFirestoreTypes(result.record.data, this.db),
|
|
30285
|
-
updatedAt: FieldValue3.serverTimestamp()
|
|
30286
|
-
};
|
|
30287
|
-
if (result.record.v !== void 0) {
|
|
30288
|
-
update.v = result.record.v;
|
|
30289
|
-
}
|
|
30290
|
-
this.adapter.updateDoc(docId, update);
|
|
30291
|
-
}
|
|
30292
|
-
}
|
|
30293
|
-
return results.map((r) => r.record);
|
|
30294
|
-
}
|
|
30295
|
-
async putNode(aType, uid, data) {
|
|
30296
|
-
if (this.registry) {
|
|
30297
|
-
this.registry.validate(aType, NODE_RELATION, aType, data, this.scopePath);
|
|
30298
|
-
}
|
|
30299
|
-
const docId = computeNodeDocId(uid);
|
|
30300
|
-
const record2 = buildNodeRecord(aType, uid, data);
|
|
30301
|
-
if (this.registry) {
|
|
30302
|
-
const entry = this.registry.lookup(aType, NODE_RELATION, aType);
|
|
30303
|
-
if (entry?.schemaVersion && entry.schemaVersion > 0) {
|
|
30304
|
-
record2.v = entry.schemaVersion;
|
|
30305
|
-
}
|
|
30306
|
-
}
|
|
30307
|
-
this.adapter.setDoc(docId, record2);
|
|
30308
|
-
}
|
|
30309
|
-
async putEdge(aType, aUid, axbType, bType, bUid, data) {
|
|
30310
|
-
if (this.registry) {
|
|
30311
|
-
this.registry.validate(aType, axbType, bType, data, this.scopePath);
|
|
30312
|
-
}
|
|
30313
|
-
const docId = computeEdgeDocId(aUid, axbType, bUid);
|
|
30314
|
-
const record2 = buildEdgeRecord(aType, aUid, axbType, bType, bUid, data);
|
|
30315
|
-
if (this.registry) {
|
|
30316
|
-
const entry = this.registry.lookup(aType, axbType, bType);
|
|
30317
|
-
if (entry?.schemaVersion && entry.schemaVersion > 0) {
|
|
30318
|
-
record2.v = entry.schemaVersion;
|
|
30319
|
-
}
|
|
30320
|
-
}
|
|
30321
|
-
this.adapter.setDoc(docId, record2);
|
|
30322
|
-
}
|
|
30323
|
-
async updateNode(uid, data) {
|
|
30324
|
-
const docId = computeNodeDocId(uid);
|
|
30325
|
-
const update = {
|
|
30326
|
-
updatedAt: FieldValue3.serverTimestamp()
|
|
30327
|
-
};
|
|
30328
|
-
for (const [k, v] of Object.entries(data)) {
|
|
30329
|
-
update[`data.${k}`] = v;
|
|
30330
|
-
}
|
|
30331
|
-
this.adapter.updateDoc(docId, update);
|
|
30332
|
-
}
|
|
30333
|
-
async removeNode(uid) {
|
|
30334
|
-
const docId = computeNodeDocId(uid);
|
|
30335
|
-
this.adapter.deleteDoc(docId);
|
|
30336
|
-
}
|
|
30337
|
-
async removeEdge(aUid, axbType, bUid) {
|
|
30338
|
-
const docId = computeEdgeDocId(aUid, axbType, bUid);
|
|
30339
|
-
this.adapter.deleteDoc(docId);
|
|
30340
|
-
}
|
|
30341
|
-
};
|
|
30342
|
-
|
|
30343
|
-
// src/batch.ts
|
|
30344
|
-
import { FieldValue as FieldValue4 } from "@google-cloud/firestore";
|
|
30345
|
-
var GraphBatchImpl = class {
|
|
30346
|
-
constructor(adapter, registry2, scopePath = "") {
|
|
30347
|
-
this.adapter = adapter;
|
|
30348
|
-
this.registry = registry2;
|
|
30349
|
-
this.scopePath = scopePath;
|
|
30350
|
-
}
|
|
31074
|
+
// ---------------------------------------------------------------------------
|
|
31075
|
+
// GraphWriter
|
|
31076
|
+
// ---------------------------------------------------------------------------
|
|
30351
31077
|
async putNode(aType, uid, data) {
|
|
30352
|
-
|
|
30353
|
-
|
|
31078
|
+
const registry2 = this.getRegistryForType(aType);
|
|
31079
|
+
if (registry2) {
|
|
31080
|
+
registry2.validate(aType, NODE_RELATION, aType, data, this.backend.scopePath);
|
|
30354
31081
|
}
|
|
31082
|
+
const backend = this.getBackendForType(aType);
|
|
30355
31083
|
const docId = computeNodeDocId(uid);
|
|
30356
|
-
const record2 =
|
|
30357
|
-
if (
|
|
30358
|
-
const entry =
|
|
31084
|
+
const record2 = buildWritableNodeRecord3(aType, uid, data);
|
|
31085
|
+
if (registry2) {
|
|
31086
|
+
const entry = registry2.lookup(aType, NODE_RELATION, aType);
|
|
30359
31087
|
if (entry?.schemaVersion && entry.schemaVersion > 0) {
|
|
30360
31088
|
record2.v = entry.schemaVersion;
|
|
30361
31089
|
}
|
|
30362
31090
|
}
|
|
30363
|
-
|
|
31091
|
+
await backend.setDoc(docId, record2);
|
|
30364
31092
|
}
|
|
30365
31093
|
async putEdge(aType, aUid, axbType, bType, bUid, data) {
|
|
30366
|
-
|
|
30367
|
-
|
|
31094
|
+
const registry2 = this.getRegistryForType(aType);
|
|
31095
|
+
if (registry2) {
|
|
31096
|
+
registry2.validate(aType, axbType, bType, data, this.backend.scopePath);
|
|
30368
31097
|
}
|
|
31098
|
+
const backend = this.getBackendForType(aType);
|
|
30369
31099
|
const docId = computeEdgeDocId(aUid, axbType, bUid);
|
|
30370
|
-
const record2 =
|
|
30371
|
-
if (
|
|
30372
|
-
const entry =
|
|
31100
|
+
const record2 = buildWritableEdgeRecord3(aType, aUid, axbType, bType, bUid, data);
|
|
31101
|
+
if (registry2) {
|
|
31102
|
+
const entry = registry2.lookup(aType, axbType, bType);
|
|
30373
31103
|
if (entry?.schemaVersion && entry.schemaVersion > 0) {
|
|
30374
31104
|
record2.v = entry.schemaVersion;
|
|
30375
31105
|
}
|
|
30376
31106
|
}
|
|
30377
|
-
|
|
31107
|
+
await backend.setDoc(docId, record2);
|
|
30378
31108
|
}
|
|
30379
31109
|
async updateNode(uid, data) {
|
|
30380
31110
|
const docId = computeNodeDocId(uid);
|
|
30381
|
-
|
|
30382
|
-
updatedAt: FieldValue4.serverTimestamp()
|
|
30383
|
-
};
|
|
30384
|
-
for (const [k, v] of Object.entries(data)) {
|
|
30385
|
-
update[`data.${k}`] = v;
|
|
30386
|
-
}
|
|
30387
|
-
this.adapter.updateDoc(docId, update);
|
|
31111
|
+
await this.backend.updateDoc(docId, { dataFields: data });
|
|
30388
31112
|
}
|
|
30389
31113
|
async removeNode(uid) {
|
|
30390
31114
|
const docId = computeNodeDocId(uid);
|
|
30391
|
-
this.
|
|
31115
|
+
await this.backend.deleteDoc(docId);
|
|
30392
31116
|
}
|
|
30393
31117
|
async removeEdge(aUid, axbType, bUid) {
|
|
30394
31118
|
const docId = computeEdgeDocId(aUid, axbType, bUid);
|
|
30395
|
-
this.
|
|
30396
|
-
}
|
|
30397
|
-
async commit() {
|
|
30398
|
-
await this.adapter.commit();
|
|
30399
|
-
}
|
|
30400
|
-
};
|
|
30401
|
-
|
|
30402
|
-
// src/bulk.ts
|
|
30403
|
-
var MAX_BATCH_SIZE = 500;
|
|
30404
|
-
var DEFAULT_MAX_RETRIES = 3;
|
|
30405
|
-
var BASE_DELAY_MS = 200;
|
|
30406
|
-
function sleep(ms) {
|
|
30407
|
-
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
30408
|
-
}
|
|
30409
|
-
function chunk(arr, size) {
|
|
30410
|
-
const chunks = [];
|
|
30411
|
-
for (let i = 0; i < arr.length; i += size) {
|
|
30412
|
-
chunks.push(arr.slice(i, i + size));
|
|
30413
|
-
}
|
|
30414
|
-
return chunks;
|
|
30415
|
-
}
|
|
30416
|
-
async function bulkDeleteDocIds(db2, collectionPath, docIds, options) {
|
|
30417
|
-
if (docIds.length === 0) {
|
|
30418
|
-
return { deleted: 0, batches: 0, errors: [] };
|
|
30419
|
-
}
|
|
30420
|
-
const batchSize = Math.min(options?.batchSize ?? MAX_BATCH_SIZE, MAX_BATCH_SIZE);
|
|
30421
|
-
const maxRetries = options?.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
30422
|
-
const onProgress = options?.onProgress;
|
|
30423
|
-
const chunks = chunk(docIds, batchSize);
|
|
30424
|
-
const errors = [];
|
|
30425
|
-
let deleted = 0;
|
|
30426
|
-
let completedBatches = 0;
|
|
30427
|
-
for (let i = 0; i < chunks.length; i++) {
|
|
30428
|
-
const ids = chunks[i];
|
|
30429
|
-
let committed = false;
|
|
30430
|
-
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
30431
|
-
try {
|
|
30432
|
-
const batch = db2.batch();
|
|
30433
|
-
const collectionRef = db2.collection(collectionPath);
|
|
30434
|
-
for (const id of ids) {
|
|
30435
|
-
batch.delete(collectionRef.doc(id));
|
|
30436
|
-
}
|
|
30437
|
-
await batch.commit();
|
|
30438
|
-
committed = true;
|
|
30439
|
-
deleted += ids.length;
|
|
30440
|
-
break;
|
|
30441
|
-
} catch (err) {
|
|
30442
|
-
if (attempt < maxRetries) {
|
|
30443
|
-
const delay = BASE_DELAY_MS * Math.pow(2, attempt);
|
|
30444
|
-
await sleep(delay);
|
|
30445
|
-
} else {
|
|
30446
|
-
errors.push({
|
|
30447
|
-
batchIndex: i,
|
|
30448
|
-
error: err instanceof Error ? err : new Error(String(err)),
|
|
30449
|
-
operationCount: ids.length
|
|
30450
|
-
});
|
|
30451
|
-
}
|
|
30452
|
-
}
|
|
30453
|
-
}
|
|
30454
|
-
if (committed) {
|
|
30455
|
-
completedBatches++;
|
|
30456
|
-
}
|
|
30457
|
-
if (onProgress) {
|
|
30458
|
-
onProgress({
|
|
30459
|
-
completedBatches,
|
|
30460
|
-
totalBatches: chunks.length,
|
|
30461
|
-
deletedSoFar: deleted
|
|
30462
|
-
});
|
|
30463
|
-
}
|
|
30464
|
-
}
|
|
30465
|
-
return { deleted, batches: completedBatches, errors };
|
|
30466
|
-
}
|
|
30467
|
-
async function bulkRemoveEdges(db2, collectionPath, reader, params, options) {
|
|
30468
|
-
const effectiveParams = params.limit !== void 0 ? { ...params, allowCollectionScan: params.allowCollectionScan ?? true } : { ...params, limit: 0, allowCollectionScan: params.allowCollectionScan ?? true };
|
|
30469
|
-
const edges = await reader.findEdges(effectiveParams);
|
|
30470
|
-
const docIds = edges.map((e) => computeEdgeDocId(e.aUid, e.axbType, e.bUid));
|
|
30471
|
-
return bulkDeleteDocIds(db2, collectionPath, docIds, options);
|
|
30472
|
-
}
|
|
30473
|
-
async function deleteSubcollectionsRecursive(db2, collectionPath, docId, options) {
|
|
30474
|
-
const docRef = db2.collection(collectionPath).doc(docId);
|
|
30475
|
-
const subcollections = await docRef.listCollections();
|
|
30476
|
-
if (subcollections.length === 0) return { deleted: 0, errors: [] };
|
|
30477
|
-
let totalDeleted = 0;
|
|
30478
|
-
const allErrors = [];
|
|
30479
|
-
const subOptions = options ? { batchSize: options.batchSize, maxRetries: options.maxRetries } : void 0;
|
|
30480
|
-
for (const subCollRef of subcollections) {
|
|
30481
|
-
const subCollPath = subCollRef.path;
|
|
30482
|
-
const snapshot = await subCollRef.select().get();
|
|
30483
|
-
const subDocIds = snapshot.docs.map((d) => d.id);
|
|
30484
|
-
for (const subDocId of subDocIds) {
|
|
30485
|
-
const subResult = await deleteSubcollectionsRecursive(db2, subCollPath, subDocId, subOptions);
|
|
30486
|
-
totalDeleted += subResult.deleted;
|
|
30487
|
-
allErrors.push(...subResult.errors);
|
|
30488
|
-
}
|
|
30489
|
-
if (subDocIds.length > 0) {
|
|
30490
|
-
const result = await bulkDeleteDocIds(db2, subCollPath, subDocIds, subOptions);
|
|
30491
|
-
totalDeleted += result.deleted;
|
|
30492
|
-
allErrors.push(...result.errors);
|
|
30493
|
-
}
|
|
31119
|
+
await this.backend.deleteDoc(docId);
|
|
30494
31120
|
}
|
|
30495
|
-
|
|
30496
|
-
|
|
30497
|
-
|
|
30498
|
-
|
|
30499
|
-
|
|
30500
|
-
|
|
30501
|
-
|
|
30502
|
-
|
|
30503
|
-
|
|
30504
|
-
|
|
30505
|
-
|
|
30506
|
-
|
|
30507
|
-
|
|
30508
|
-
|
|
30509
|
-
edgeDocIdSet.add(docId);
|
|
30510
|
-
allEdges.push(edge);
|
|
30511
|
-
}
|
|
31121
|
+
// ---------------------------------------------------------------------------
|
|
31122
|
+
// Transactions & Batches
|
|
31123
|
+
// ---------------------------------------------------------------------------
|
|
31124
|
+
async runTransaction(fn) {
|
|
31125
|
+
return this.backend.runTransaction(async (txBackend) => {
|
|
31126
|
+
const graphTx = new GraphTransactionImpl(
|
|
31127
|
+
txBackend,
|
|
31128
|
+
this.getCombinedRegistry(),
|
|
31129
|
+
this.scanProtection,
|
|
31130
|
+
this.backend.scopePath,
|
|
31131
|
+
this.globalWriteBack
|
|
31132
|
+
);
|
|
31133
|
+
return fn(graphTx);
|
|
31134
|
+
});
|
|
30512
31135
|
}
|
|
30513
|
-
|
|
30514
|
-
|
|
30515
|
-
|
|
30516
|
-
|
|
30517
|
-
|
|
30518
|
-
db2,
|
|
30519
|
-
collectionPath,
|
|
30520
|
-
nodeDocId,
|
|
30521
|
-
options
|
|
31136
|
+
batch() {
|
|
31137
|
+
return new GraphBatchImpl(
|
|
31138
|
+
this.backend.createBatch(),
|
|
31139
|
+
this.getCombinedRegistry(),
|
|
31140
|
+
this.backend.scopePath
|
|
30522
31141
|
);
|
|
30523
31142
|
}
|
|
30524
|
-
|
|
30525
|
-
|
|
30526
|
-
|
|
30527
|
-
|
|
30528
|
-
|
|
30529
|
-
|
|
30530
|
-
|
|
30531
|
-
|
|
30532
|
-
const nodeChunkIndex = totalChunks - 1;
|
|
30533
|
-
const nodeDeleted = !result.errors.some((e) => e.batchIndex === nodeChunkIndex);
|
|
30534
|
-
const topLevelEdgesDeleted = nodeDeleted ? result.deleted - 1 : result.deleted;
|
|
30535
|
-
return {
|
|
30536
|
-
deleted: result.deleted + subcollectionResult.deleted,
|
|
30537
|
-
batches: result.batches,
|
|
30538
|
-
errors: [...result.errors, ...subcollectionResult.errors],
|
|
30539
|
-
edgesDeleted: topLevelEdgesDeleted,
|
|
30540
|
-
nodeDeleted
|
|
30541
|
-
};
|
|
30542
|
-
}
|
|
30543
|
-
|
|
30544
|
-
// src/dynamic-registry.ts
|
|
30545
|
-
import { createHash as createHash3 } from "node:crypto";
|
|
30546
|
-
|
|
30547
|
-
// src/json-schema.ts
|
|
30548
|
-
var import_ajv = __toESM(require_ajv(), 1);
|
|
30549
|
-
var import_ajv_formats = __toESM(require_dist(), 1);
|
|
30550
|
-
var ajv = new import_ajv.default({ allErrors: true, strict: false });
|
|
30551
|
-
(0, import_ajv_formats.default)(ajv);
|
|
30552
|
-
function compileSchema(schema, label) {
|
|
30553
|
-
const validate = ajv.compile(schema);
|
|
30554
|
-
return (data) => {
|
|
30555
|
-
if (!validate(data)) {
|
|
30556
|
-
const errors = validate.errors ?? [];
|
|
30557
|
-
const messages = errors.map((err) => `${err.instancePath || "/"}${err.message ? ": " + err.message : ""}`).join("; ");
|
|
30558
|
-
throw new ValidationError(
|
|
30559
|
-
`Data validation failed${label ? " for " + label : ""}: ${messages}`,
|
|
30560
|
-
errors
|
|
31143
|
+
// ---------------------------------------------------------------------------
|
|
31144
|
+
// Subgraph
|
|
31145
|
+
// ---------------------------------------------------------------------------
|
|
31146
|
+
subgraph(parentNodeUid, name = "graph") {
|
|
31147
|
+
if (!parentNodeUid || parentNodeUid.includes("/")) {
|
|
31148
|
+
throw new FiregraphError(
|
|
31149
|
+
`Invalid parentNodeUid for subgraph: "${parentNodeUid}". Must be a non-empty string without "/".`,
|
|
31150
|
+
"INVALID_SUBGRAPH"
|
|
30561
31151
|
);
|
|
30562
31152
|
}
|
|
30563
|
-
|
|
30564
|
-
|
|
30565
|
-
|
|
30566
|
-
|
|
30567
|
-
|
|
30568
|
-
|
|
30569
|
-
|
|
30570
|
-
|
|
30571
|
-
|
|
30572
|
-
|
|
30573
|
-
|
|
30574
|
-
|
|
30575
|
-
|
|
30576
|
-
|
|
30577
|
-
|
|
30578
|
-
|
|
30579
|
-
|
|
30580
|
-
required: required2,
|
|
30581
|
-
enumValues: prop.enum,
|
|
30582
|
-
description: prop.description
|
|
30583
|
-
};
|
|
31153
|
+
if (name.includes("/")) {
|
|
31154
|
+
throw new FiregraphError(
|
|
31155
|
+
`Subgraph name must not contain "/": got "${name}". Use chained .subgraph() calls for nested subgraphs.`,
|
|
31156
|
+
"INVALID_SUBGRAPH"
|
|
31157
|
+
);
|
|
31158
|
+
}
|
|
31159
|
+
const childBackend = this.backend.subgraph(parentNodeUid, name);
|
|
31160
|
+
return new _GraphClientImpl(
|
|
31161
|
+
childBackend,
|
|
31162
|
+
{
|
|
31163
|
+
registry: this.getCombinedRegistry(),
|
|
31164
|
+
scanProtection: this.scanProtection,
|
|
31165
|
+
migrationWriteBack: this.globalWriteBack,
|
|
31166
|
+
migrationSandbox: this.migrationSandbox
|
|
31167
|
+
}
|
|
31168
|
+
// Subgraphs do not have meta-backends; meta lives only at the root.
|
|
31169
|
+
);
|
|
30584
31170
|
}
|
|
30585
|
-
|
|
30586
|
-
|
|
30587
|
-
|
|
30588
|
-
|
|
30589
|
-
|
|
31171
|
+
// ---------------------------------------------------------------------------
|
|
31172
|
+
// Collection group query
|
|
31173
|
+
// ---------------------------------------------------------------------------
|
|
31174
|
+
async findEdgesGlobal(params, collectionName) {
|
|
31175
|
+
if (!this.backend.findEdgesGlobal) {
|
|
31176
|
+
throw new FiregraphError(
|
|
31177
|
+
"findEdgesGlobal() is not supported by the current storage backend.",
|
|
31178
|
+
"UNSUPPORTED_OPERATION"
|
|
31179
|
+
);
|
|
30590
31180
|
}
|
|
30591
|
-
|
|
31181
|
+
const plan = buildEdgeQueryPlan(params);
|
|
31182
|
+
if (plan.strategy === "get") {
|
|
31183
|
+
throw new FiregraphError(
|
|
31184
|
+
"findEdgesGlobal() requires a query, not a direct document lookup. Omit one of aUid/axbType/bUid to force a query strategy.",
|
|
31185
|
+
"INVALID_QUERY"
|
|
31186
|
+
);
|
|
31187
|
+
}
|
|
31188
|
+
this.checkQuerySafety(plan.filters, params.allowCollectionScan);
|
|
31189
|
+
const records = await this.backend.findEdgesGlobal(params, collectionName);
|
|
31190
|
+
return this.applyMigrations(records);
|
|
30592
31191
|
}
|
|
30593
|
-
|
|
30594
|
-
|
|
30595
|
-
|
|
30596
|
-
|
|
30597
|
-
|
|
30598
|
-
required: required2,
|
|
30599
|
-
minLength: prop.minLength,
|
|
30600
|
-
maxLength: prop.maxLength,
|
|
30601
|
-
pattern: prop.pattern,
|
|
30602
|
-
description: prop.description
|
|
30603
|
-
};
|
|
31192
|
+
// ---------------------------------------------------------------------------
|
|
31193
|
+
// Bulk operations
|
|
31194
|
+
// ---------------------------------------------------------------------------
|
|
31195
|
+
async removeNodeCascade(uid, options) {
|
|
31196
|
+
return this.backend.removeNodeCascade(uid, this, options);
|
|
30604
31197
|
}
|
|
30605
|
-
|
|
30606
|
-
return
|
|
30607
|
-
name,
|
|
30608
|
-
type: "number",
|
|
30609
|
-
required: required2,
|
|
30610
|
-
min: prop.minimum,
|
|
30611
|
-
max: prop.maximum,
|
|
30612
|
-
isInt: type === "integer" ? true : void 0,
|
|
30613
|
-
description: prop.description
|
|
30614
|
-
};
|
|
31198
|
+
async bulkRemoveEdges(params, options) {
|
|
31199
|
+
return this.backend.bulkRemoveEdges(params, this, options);
|
|
30615
31200
|
}
|
|
30616
|
-
|
|
30617
|
-
|
|
31201
|
+
// ---------------------------------------------------------------------------
|
|
31202
|
+
// Dynamic registry methods
|
|
31203
|
+
// ---------------------------------------------------------------------------
|
|
31204
|
+
async defineNodeType(name, jsonSchema, description, options) {
|
|
31205
|
+
if (!this.dynamicConfig) {
|
|
31206
|
+
throw new DynamicRegistryError(
|
|
31207
|
+
'defineNodeType() is only available in dynamic registry mode. Pass registryMode: { mode: "dynamic" } to createGraphClient().'
|
|
31208
|
+
);
|
|
31209
|
+
}
|
|
31210
|
+
if (RESERVED_TYPE_NAMES.has(name)) {
|
|
31211
|
+
throw new DynamicRegistryError(
|
|
31212
|
+
`Cannot define type "${name}": this name is reserved for the meta-registry.`
|
|
31213
|
+
);
|
|
31214
|
+
}
|
|
31215
|
+
if (this.staticRegistry?.lookup(name, NODE_RELATION, name)) {
|
|
31216
|
+
throw new DynamicRegistryError(
|
|
31217
|
+
`Cannot define node type "${name}": already defined in the static registry.`
|
|
31218
|
+
);
|
|
31219
|
+
}
|
|
31220
|
+
const uid = generateDeterministicUid(META_NODE_TYPE, name);
|
|
31221
|
+
const data = { name, jsonSchema };
|
|
31222
|
+
if (description !== void 0) data.description = description;
|
|
31223
|
+
if (options?.titleField !== void 0) data.titleField = options.titleField;
|
|
31224
|
+
if (options?.subtitleField !== void 0) data.subtitleField = options.subtitleField;
|
|
31225
|
+
if (options?.viewTemplate !== void 0) data.viewTemplate = options.viewTemplate;
|
|
31226
|
+
if (options?.viewCss !== void 0) data.viewCss = options.viewCss;
|
|
31227
|
+
if (options?.allowedIn !== void 0) data.allowedIn = options.allowedIn;
|
|
31228
|
+
if (options?.migrationWriteBack !== void 0)
|
|
31229
|
+
data.migrationWriteBack = options.migrationWriteBack;
|
|
31230
|
+
if (options?.migrations !== void 0) {
|
|
31231
|
+
data.migrations = await this.serializeMigrations(options.migrations);
|
|
31232
|
+
}
|
|
31233
|
+
await this.putNode(META_NODE_TYPE, uid, data);
|
|
30618
31234
|
}
|
|
30619
|
-
|
|
30620
|
-
|
|
30621
|
-
|
|
31235
|
+
async defineEdgeType(name, topology, jsonSchema, description, options) {
|
|
31236
|
+
if (!this.dynamicConfig) {
|
|
31237
|
+
throw new DynamicRegistryError(
|
|
31238
|
+
'defineEdgeType() is only available in dynamic registry mode. Pass registryMode: { mode: "dynamic" } to createGraphClient().'
|
|
31239
|
+
);
|
|
31240
|
+
}
|
|
31241
|
+
if (RESERVED_TYPE_NAMES.has(name)) {
|
|
31242
|
+
throw new DynamicRegistryError(
|
|
31243
|
+
`Cannot define type "${name}": this name is reserved for the meta-registry.`
|
|
31244
|
+
);
|
|
31245
|
+
}
|
|
31246
|
+
if (this.staticRegistry) {
|
|
31247
|
+
const fromTypes = Array.isArray(topology.from) ? topology.from : [topology.from];
|
|
31248
|
+
const toTypes = Array.isArray(topology.to) ? topology.to : [topology.to];
|
|
31249
|
+
for (const aType of fromTypes) {
|
|
31250
|
+
for (const bType of toTypes) {
|
|
31251
|
+
if (this.staticRegistry.lookup(aType, name, bType)) {
|
|
31252
|
+
throw new DynamicRegistryError(
|
|
31253
|
+
`Cannot define edge type "${name}" for (${aType}) -> (${bType}): already defined in the static registry.`
|
|
31254
|
+
);
|
|
31255
|
+
}
|
|
31256
|
+
}
|
|
31257
|
+
}
|
|
31258
|
+
}
|
|
31259
|
+
const uid = generateDeterministicUid(META_EDGE_TYPE, name);
|
|
31260
|
+
const data = {
|
|
30622
31261
|
name,
|
|
30623
|
-
|
|
30624
|
-
|
|
30625
|
-
itemMeta,
|
|
30626
|
-
description: prop.description
|
|
31262
|
+
from: topology.from,
|
|
31263
|
+
to: topology.to
|
|
30627
31264
|
};
|
|
31265
|
+
if (jsonSchema !== void 0) data.jsonSchema = jsonSchema;
|
|
31266
|
+
if (topology.inverseLabel !== void 0) data.inverseLabel = topology.inverseLabel;
|
|
31267
|
+
if (topology.targetGraph !== void 0) data.targetGraph = topology.targetGraph;
|
|
31268
|
+
if (description !== void 0) data.description = description;
|
|
31269
|
+
if (options?.titleField !== void 0) data.titleField = options.titleField;
|
|
31270
|
+
if (options?.subtitleField !== void 0) data.subtitleField = options.subtitleField;
|
|
31271
|
+
if (options?.viewTemplate !== void 0) data.viewTemplate = options.viewTemplate;
|
|
31272
|
+
if (options?.viewCss !== void 0) data.viewCss = options.viewCss;
|
|
31273
|
+
if (options?.allowedIn !== void 0) data.allowedIn = options.allowedIn;
|
|
31274
|
+
if (options?.migrationWriteBack !== void 0)
|
|
31275
|
+
data.migrationWriteBack = options.migrationWriteBack;
|
|
31276
|
+
if (options?.migrations !== void 0) {
|
|
31277
|
+
data.migrations = await this.serializeMigrations(options.migrations);
|
|
31278
|
+
}
|
|
31279
|
+
await this.putNode(META_EDGE_TYPE, uid, data);
|
|
30628
31280
|
}
|
|
30629
|
-
|
|
31281
|
+
async reloadRegistry() {
|
|
31282
|
+
if (!this.dynamicConfig) {
|
|
31283
|
+
throw new DynamicRegistryError(
|
|
31284
|
+
'reloadRegistry() is only available in dynamic registry mode. Pass registryMode: { mode: "dynamic" } to createGraphClient().'
|
|
31285
|
+
);
|
|
31286
|
+
}
|
|
31287
|
+
const reader = this.createMetaReader();
|
|
31288
|
+
const dynamicOnly = await createRegistryFromGraph(reader, this.migrationSandbox);
|
|
31289
|
+
if (this.staticRegistry) {
|
|
31290
|
+
this.dynamicRegistry = createMergedRegistry(this.staticRegistry, dynamicOnly);
|
|
31291
|
+
} else {
|
|
31292
|
+
this.dynamicRegistry = dynamicOnly;
|
|
31293
|
+
}
|
|
31294
|
+
}
|
|
31295
|
+
async serializeMigrations(migrations) {
|
|
31296
|
+
const result = migrations.map((m) => {
|
|
31297
|
+
const source = typeof m.up === "function" ? m.up.toString() : m.up;
|
|
31298
|
+
return { fromVersion: m.fromVersion, toVersion: m.toVersion, up: source };
|
|
31299
|
+
});
|
|
31300
|
+
await Promise.all(result.map((m) => precompileSource(m.up, this.migrationSandbox)));
|
|
31301
|
+
return result;
|
|
31302
|
+
}
|
|
31303
|
+
/**
|
|
31304
|
+
* Build a `GraphReader` over the meta-backend. If meta lives in the same
|
|
31305
|
+
* collection as the main backend, `this` is returned directly.
|
|
31306
|
+
*/
|
|
31307
|
+
createMetaReader() {
|
|
31308
|
+
if (!this.metaBackend) return this;
|
|
31309
|
+
const backend = this.metaBackend;
|
|
31310
|
+
const executeMetaQuery = (filters, options) => backend.query(filters, options);
|
|
30630
31311
|
return {
|
|
30631
|
-
|
|
30632
|
-
|
|
30633
|
-
|
|
30634
|
-
|
|
30635
|
-
|
|
31312
|
+
async getNode(uid) {
|
|
31313
|
+
return backend.getDoc(computeNodeDocId(uid));
|
|
31314
|
+
},
|
|
31315
|
+
async getEdge(aUid, axbType, bUid) {
|
|
31316
|
+
return backend.getDoc(computeEdgeDocId(aUid, axbType, bUid));
|
|
31317
|
+
},
|
|
31318
|
+
async edgeExists(aUid, axbType, bUid) {
|
|
31319
|
+
const record2 = await backend.getDoc(computeEdgeDocId(aUid, axbType, bUid));
|
|
31320
|
+
return record2 !== null;
|
|
31321
|
+
},
|
|
31322
|
+
async findEdges(params) {
|
|
31323
|
+
const plan = buildEdgeQueryPlan(params);
|
|
31324
|
+
if (plan.strategy === "get") {
|
|
31325
|
+
const record2 = await backend.getDoc(plan.docId);
|
|
31326
|
+
return record2 ? [record2] : [];
|
|
31327
|
+
}
|
|
31328
|
+
return executeMetaQuery(plan.filters, plan.options);
|
|
31329
|
+
},
|
|
31330
|
+
async findNodes(params) {
|
|
31331
|
+
const plan = buildNodeQueryPlan(params);
|
|
31332
|
+
if (plan.strategy === "get") {
|
|
31333
|
+
const record2 = await backend.getDoc(plan.docId);
|
|
31334
|
+
return record2 ? [record2] : [];
|
|
31335
|
+
}
|
|
31336
|
+
return executeMetaQuery(plan.filters, plan.options);
|
|
31337
|
+
}
|
|
30636
31338
|
};
|
|
30637
31339
|
}
|
|
30638
|
-
|
|
30639
|
-
}
|
|
31340
|
+
};
|
|
30640
31341
|
|
|
30641
|
-
// src/
|
|
30642
|
-
|
|
30643
|
-
|
|
30644
|
-
|
|
30645
|
-
|
|
30646
|
-
|
|
30647
|
-
|
|
30648
|
-
|
|
30649
|
-
function matchScopeAny(scopePath, patterns) {
|
|
30650
|
-
if (!patterns || patterns.length === 0) return true;
|
|
30651
|
-
return patterns.some((p) => matchScope(scopePath, p));
|
|
30652
|
-
}
|
|
30653
|
-
function matchSegments(path5, pi, pattern, qi) {
|
|
30654
|
-
if (pi === path5.length && qi === pattern.length) return true;
|
|
30655
|
-
if (qi === pattern.length) return false;
|
|
30656
|
-
const seg = pattern[qi];
|
|
30657
|
-
if (seg === "**") {
|
|
30658
|
-
if (qi === pattern.length - 1) return true;
|
|
30659
|
-
for (let skip = 0; skip <= path5.length - pi; skip++) {
|
|
30660
|
-
if (matchSegments(path5, pi + skip, pattern, qi + 1)) return true;
|
|
30661
|
-
}
|
|
30662
|
-
return false;
|
|
30663
|
-
}
|
|
30664
|
-
if (pi === path5.length) return false;
|
|
30665
|
-
if (seg === "*") {
|
|
30666
|
-
return matchSegments(path5, pi + 1, pattern, qi + 1);
|
|
31342
|
+
// src/discover.ts
|
|
31343
|
+
import { readFileSync, readdirSync, existsSync, statSync } from "node:fs";
|
|
31344
|
+
import { createRequire } from "node:module";
|
|
31345
|
+
import { join, resolve } from "node:path";
|
|
31346
|
+
var DiscoveryError = class extends FiregraphError {
|
|
31347
|
+
constructor(message) {
|
|
31348
|
+
super(message, "DISCOVERY_ERROR");
|
|
31349
|
+
this.name = "DiscoveryError";
|
|
30667
31350
|
}
|
|
30668
|
-
|
|
30669
|
-
|
|
31351
|
+
};
|
|
31352
|
+
function readJson(filePath) {
|
|
31353
|
+
try {
|
|
31354
|
+
const raw = readFileSync(filePath, "utf-8");
|
|
31355
|
+
return JSON.parse(raw);
|
|
31356
|
+
} catch (err) {
|
|
31357
|
+
const msg = err instanceof SyntaxError ? `Invalid JSON in ${filePath}: ${err.message}` : `Cannot read ${filePath}: ${err.message}`;
|
|
31358
|
+
throw new DiscoveryError(msg);
|
|
30670
31359
|
}
|
|
30671
|
-
return false;
|
|
30672
31360
|
}
|
|
30673
|
-
|
|
30674
|
-
|
|
30675
|
-
|
|
30676
|
-
return `${aType}:${axbType}:${bType}`;
|
|
31361
|
+
function readJsonIfExists(filePath) {
|
|
31362
|
+
if (!existsSync(filePath)) return void 0;
|
|
31363
|
+
return readJson(filePath);
|
|
30677
31364
|
}
|
|
30678
|
-
|
|
30679
|
-
|
|
31365
|
+
var SCHEMA_SCRIPT_EXTENSIONS = [".ts", ".js", ".mts", ".mjs"];
|
|
31366
|
+
function loadSchema(dir, entityLabel) {
|
|
31367
|
+
for (const ext of SCHEMA_SCRIPT_EXTENSIONS) {
|
|
31368
|
+
const candidate = join(dir, `schema${ext}`);
|
|
31369
|
+
if (existsSync(candidate)) {
|
|
31370
|
+
return loadSchemaModule(candidate, entityLabel);
|
|
31371
|
+
}
|
|
31372
|
+
}
|
|
31373
|
+
const jsonPath = join(dir, "schema.json");
|
|
31374
|
+
if (existsSync(jsonPath)) {
|
|
31375
|
+
return readJson(jsonPath);
|
|
31376
|
+
}
|
|
31377
|
+
throw new DiscoveryError(
|
|
31378
|
+
`Missing schema for ${entityLabel} in ${dir}. Provide a schema.ts (or .js/.mts/.mjs) or schema.json file.`
|
|
31379
|
+
);
|
|
30680
31380
|
}
|
|
30681
|
-
|
|
30682
|
-
|
|
30683
|
-
|
|
30684
|
-
|
|
30685
|
-
|
|
30686
|
-
|
|
30687
|
-
|
|
31381
|
+
var _jiti;
|
|
31382
|
+
function getJiti() {
|
|
31383
|
+
if (!_jiti) {
|
|
31384
|
+
const base = typeof __filename !== "undefined" ? __filename : import.meta.url;
|
|
31385
|
+
const esmRequire = createRequire(base);
|
|
31386
|
+
const { createJiti: createJiti2 } = esmRequire("jiti");
|
|
31387
|
+
_jiti = createJiti2(base, { interopDefault: true });
|
|
30688
31388
|
}
|
|
30689
|
-
|
|
30690
|
-
|
|
30691
|
-
|
|
30692
|
-
|
|
30693
|
-
|
|
31389
|
+
return _jiti;
|
|
31390
|
+
}
|
|
31391
|
+
function loadSchemaModule(filePath, entityLabel) {
|
|
31392
|
+
try {
|
|
31393
|
+
const jiti2 = getJiti();
|
|
31394
|
+
const mod = jiti2(filePath);
|
|
31395
|
+
const schema = mod && typeof mod === "object" && "default" in mod ? mod.default : mod;
|
|
31396
|
+
if (!schema || typeof schema !== "object") {
|
|
31397
|
+
throw new DiscoveryError(
|
|
31398
|
+
`Schema file ${filePath} for ${entityLabel} must default-export a JSON Schema object.`
|
|
30694
31399
|
);
|
|
30695
31400
|
}
|
|
30696
|
-
|
|
30697
|
-
|
|
30698
|
-
|
|
30699
|
-
|
|
30700
|
-
|
|
30701
|
-
|
|
30702
|
-
}
|
|
30703
|
-
const key = tripleKey(entry.aType, entry.axbType, entry.bType);
|
|
30704
|
-
const validator = entry.jsonSchema ? compileSchema(entry.jsonSchema, `(${entry.aType}) -[${entry.axbType}]-> (${entry.bType})`) : void 0;
|
|
30705
|
-
map2.set(key, { entry, validate: validator });
|
|
31401
|
+
return schema;
|
|
31402
|
+
} catch (err) {
|
|
31403
|
+
if (err instanceof DiscoveryError) throw err;
|
|
31404
|
+
throw new DiscoveryError(
|
|
31405
|
+
`Failed to load schema module ${filePath} for ${entityLabel}: ${err.message}`
|
|
31406
|
+
);
|
|
30706
31407
|
}
|
|
30707
|
-
|
|
30708
|
-
|
|
30709
|
-
|
|
30710
|
-
|
|
30711
|
-
|
|
30712
|
-
|
|
30713
|
-
} else {
|
|
30714
|
-
axbBuild.set(entry.axbType, [entry]);
|
|
30715
|
-
}
|
|
31408
|
+
}
|
|
31409
|
+
var VIEW_EXTENSIONS = [".ts", ".js", ".mts", ".mjs"];
|
|
31410
|
+
function findViewsFile(dir) {
|
|
31411
|
+
for (const ext of VIEW_EXTENSIONS) {
|
|
31412
|
+
const candidate = join(dir, `views${ext}`);
|
|
31413
|
+
if (existsSync(candidate)) return candidate;
|
|
30716
31414
|
}
|
|
30717
|
-
|
|
30718
|
-
|
|
31415
|
+
return void 0;
|
|
31416
|
+
}
|
|
31417
|
+
var MIGRATION_EXTENSIONS = [".ts", ".js", ".mts", ".mjs"];
|
|
31418
|
+
function findMigrationsFile(dir) {
|
|
31419
|
+
for (const ext of MIGRATION_EXTENSIONS) {
|
|
31420
|
+
const candidate = join(dir, `migrations${ext}`);
|
|
31421
|
+
if (existsSync(candidate)) return candidate;
|
|
30719
31422
|
}
|
|
30720
|
-
return
|
|
30721
|
-
|
|
30722
|
-
|
|
30723
|
-
|
|
30724
|
-
|
|
30725
|
-
|
|
30726
|
-
|
|
30727
|
-
|
|
30728
|
-
|
|
30729
|
-
|
|
30730
|
-
|
|
30731
|
-
}
|
|
30732
|
-
if (scopePath !== void 0 && rec.entry.allowedIn && rec.entry.allowedIn.length > 0) {
|
|
30733
|
-
if (!matchScopeAny(scopePath, rec.entry.allowedIn)) {
|
|
30734
|
-
throw new RegistryScopeError(aType, axbType, bType, scopePath, rec.entry.allowedIn);
|
|
30735
|
-
}
|
|
30736
|
-
}
|
|
30737
|
-
if (rec.validate) {
|
|
30738
|
-
try {
|
|
30739
|
-
rec.validate(data);
|
|
30740
|
-
} catch (err) {
|
|
30741
|
-
if (err instanceof ValidationError) throw err;
|
|
30742
|
-
throw new ValidationError(
|
|
30743
|
-
`Data validation failed for (${aType}) -[${axbType}]-> (${bType})`,
|
|
30744
|
-
err
|
|
30745
|
-
);
|
|
30746
|
-
}
|
|
30747
|
-
}
|
|
30748
|
-
},
|
|
30749
|
-
entries() {
|
|
30750
|
-
return entryList;
|
|
31423
|
+
return void 0;
|
|
31424
|
+
}
|
|
31425
|
+
function loadMigrations(filePath, entityLabel) {
|
|
31426
|
+
try {
|
|
31427
|
+
const jiti2 = getJiti();
|
|
31428
|
+
const mod = jiti2(filePath);
|
|
31429
|
+
const migrations = mod && typeof mod === "object" && "default" in mod ? mod.default : mod;
|
|
31430
|
+
if (!Array.isArray(migrations)) {
|
|
31431
|
+
throw new DiscoveryError(
|
|
31432
|
+
`Migrations file ${filePath} for ${entityLabel} must default-export an array of MigrationStep.`
|
|
31433
|
+
);
|
|
30751
31434
|
}
|
|
30752
|
-
|
|
31435
|
+
return migrations;
|
|
31436
|
+
} catch (err) {
|
|
31437
|
+
if (err instanceof DiscoveryError) throw err;
|
|
31438
|
+
throw new DiscoveryError(
|
|
31439
|
+
`Failed to load migrations ${filePath} for ${entityLabel}: ${err.message}`
|
|
31440
|
+
);
|
|
31441
|
+
}
|
|
30753
31442
|
}
|
|
30754
|
-
function
|
|
30755
|
-
const
|
|
31443
|
+
function loadNodeEntity(dir, name) {
|
|
31444
|
+
const schema = loadSchema(dir, `node type "${name}"`);
|
|
31445
|
+
const meta3 = readJsonIfExists(join(dir, "meta.json"));
|
|
31446
|
+
const sampleData = readJsonIfExists(join(dir, "sample.json"));
|
|
31447
|
+
const viewsPath = findViewsFile(dir);
|
|
31448
|
+
const migrationsPath = findMigrationsFile(dir);
|
|
31449
|
+
const migrations = migrationsPath ? loadMigrations(migrationsPath, `node type "${name}"`) : void 0;
|
|
30756
31450
|
return {
|
|
30757
|
-
|
|
30758
|
-
|
|
30759
|
-
|
|
30760
|
-
|
|
30761
|
-
|
|
30762
|
-
|
|
30763
|
-
|
|
30764
|
-
|
|
30765
|
-
|
|
30766
|
-
|
|
30767
|
-
|
|
30768
|
-
|
|
30769
|
-
merged.push(entry);
|
|
30770
|
-
}
|
|
30771
|
-
}
|
|
30772
|
-
return Object.freeze(merged);
|
|
30773
|
-
},
|
|
30774
|
-
validate(aType, axbType, bType, data, scopePath) {
|
|
30775
|
-
if (baseKeys.has(tripleKey(aType, axbType, bType))) {
|
|
30776
|
-
return base.validate(aType, axbType, bType, data, scopePath);
|
|
30777
|
-
}
|
|
30778
|
-
return extension.validate(aType, axbType, bType, data, scopePath);
|
|
30779
|
-
},
|
|
30780
|
-
entries() {
|
|
30781
|
-
const extEntries = extension.entries();
|
|
30782
|
-
if (extEntries.length === 0) return base.entries();
|
|
30783
|
-
const merged = [...base.entries()];
|
|
30784
|
-
for (const entry of extEntries) {
|
|
30785
|
-
if (!baseKeys.has(tripleKeyFor(entry))) {
|
|
30786
|
-
merged.push(entry);
|
|
30787
|
-
}
|
|
30788
|
-
}
|
|
30789
|
-
return Object.freeze(merged);
|
|
30790
|
-
}
|
|
31451
|
+
kind: "node",
|
|
31452
|
+
name,
|
|
31453
|
+
schema,
|
|
31454
|
+
description: meta3?.description,
|
|
31455
|
+
titleField: meta3?.titleField,
|
|
31456
|
+
subtitleField: meta3?.subtitleField,
|
|
31457
|
+
viewDefaults: meta3?.viewDefaults,
|
|
31458
|
+
viewsPath,
|
|
31459
|
+
sampleData,
|
|
31460
|
+
allowedIn: meta3?.allowedIn,
|
|
31461
|
+
migrations,
|
|
31462
|
+
migrationWriteBack: meta3?.migrationWriteBack
|
|
30791
31463
|
};
|
|
30792
31464
|
}
|
|
30793
|
-
function
|
|
30794
|
-
const
|
|
30795
|
-
|
|
30796
|
-
|
|
30797
|
-
|
|
30798
|
-
|
|
30799
|
-
|
|
30800
|
-
|
|
30801
|
-
|
|
30802
|
-
|
|
30803
|
-
|
|
30804
|
-
|
|
30805
|
-
|
|
30806
|
-
|
|
30807
|
-
|
|
31465
|
+
function loadEdgeEntity(dir, name) {
|
|
31466
|
+
const schema = loadSchema(dir, `edge type "${name}"`);
|
|
31467
|
+
const edgePath = join(dir, "edge.json");
|
|
31468
|
+
if (!existsSync(edgePath)) {
|
|
31469
|
+
throw new DiscoveryError(
|
|
31470
|
+
`Missing edge.json for edge type "${name}" in ${dir}. Edge entities must declare topology (from/to node types).`
|
|
31471
|
+
);
|
|
31472
|
+
}
|
|
31473
|
+
const topology = readJson(edgePath);
|
|
31474
|
+
if (!topology.from) {
|
|
31475
|
+
throw new DiscoveryError(
|
|
31476
|
+
`edge.json for "${name}" is missing required "from" field`
|
|
31477
|
+
);
|
|
31478
|
+
}
|
|
31479
|
+
if (!topology.to) {
|
|
31480
|
+
throw new DiscoveryError(
|
|
31481
|
+
`edge.json for "${name}" is missing required "to" field`
|
|
31482
|
+
);
|
|
31483
|
+
}
|
|
31484
|
+
const meta3 = readJsonIfExists(join(dir, "meta.json"));
|
|
31485
|
+
const sampleData = readJsonIfExists(join(dir, "sample.json"));
|
|
31486
|
+
const viewsPath = findViewsFile(dir);
|
|
31487
|
+
const migrationsPath = findMigrationsFile(dir);
|
|
31488
|
+
const migrations = migrationsPath ? loadMigrations(migrationsPath, `edge type "${name}"`) : void 0;
|
|
31489
|
+
return {
|
|
31490
|
+
kind: "edge",
|
|
31491
|
+
name,
|
|
31492
|
+
schema,
|
|
31493
|
+
topology,
|
|
31494
|
+
description: meta3?.description,
|
|
31495
|
+
titleField: meta3?.titleField,
|
|
31496
|
+
subtitleField: meta3?.subtitleField,
|
|
31497
|
+
viewDefaults: meta3?.viewDefaults,
|
|
31498
|
+
viewsPath,
|
|
31499
|
+
sampleData,
|
|
31500
|
+
allowedIn: meta3?.allowedIn,
|
|
31501
|
+
targetGraph: topology.targetGraph ?? meta3?.targetGraph,
|
|
31502
|
+
migrations,
|
|
31503
|
+
migrationWriteBack: meta3?.migrationWriteBack
|
|
31504
|
+
};
|
|
31505
|
+
}
|
|
31506
|
+
function getSubdirectories(dir) {
|
|
31507
|
+
if (!existsSync(dir)) return [];
|
|
31508
|
+
return readdirSync(dir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
31509
|
+
}
|
|
31510
|
+
function discoverEntities(entitiesDir) {
|
|
31511
|
+
const absDir = resolve(entitiesDir);
|
|
31512
|
+
if (!existsSync(absDir) || !statSync(absDir).isDirectory()) {
|
|
31513
|
+
throw new DiscoveryError(`Entities directory not found: ${entitiesDir}`);
|
|
30808
31514
|
}
|
|
30809
|
-
|
|
31515
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
31516
|
+
const edges = /* @__PURE__ */ new Map();
|
|
31517
|
+
const warnings = [];
|
|
31518
|
+
const nodesDir = join(absDir, "nodes");
|
|
31519
|
+
for (const name of getSubdirectories(nodesDir)) {
|
|
31520
|
+
nodes.set(name, loadNodeEntity(join(nodesDir, name), name));
|
|
31521
|
+
}
|
|
31522
|
+
const edgesDir = join(absDir, "edges");
|
|
31523
|
+
for (const name of getSubdirectories(edgesDir)) {
|
|
31524
|
+
edges.set(name, loadEdgeEntity(join(edgesDir, name), name));
|
|
31525
|
+
}
|
|
31526
|
+
const nodeNames = new Set(nodes.keys());
|
|
31527
|
+
for (const [axbType, entity] of edges) {
|
|
30810
31528
|
const topology = entity.topology;
|
|
30811
|
-
if (!topology) continue;
|
|
30812
31529
|
const fromTypes = Array.isArray(topology.from) ? topology.from : [topology.from];
|
|
30813
31530
|
const toTypes = Array.isArray(topology.to) ? topology.to : [topology.to];
|
|
30814
|
-
const
|
|
30815
|
-
|
|
30816
|
-
|
|
30817
|
-
|
|
30818
|
-
|
|
30819
|
-
}
|
|
30820
|
-
for (const aType of fromTypes) {
|
|
30821
|
-
for (const bType of toTypes) {
|
|
30822
|
-
entries.push({
|
|
30823
|
-
aType,
|
|
30824
|
-
axbType,
|
|
30825
|
-
bType,
|
|
30826
|
-
jsonSchema: entity.schema,
|
|
30827
|
-
description: entity.description,
|
|
30828
|
-
inverseLabel: topology.inverseLabel,
|
|
30829
|
-
titleField: entity.titleField,
|
|
30830
|
-
subtitleField: entity.subtitleField,
|
|
30831
|
-
allowedIn: entity.allowedIn,
|
|
30832
|
-
targetGraph: resolvedTargetGraph,
|
|
30833
|
-
migrations: entity.migrations,
|
|
30834
|
-
migrationWriteBack: entity.migrationWriteBack
|
|
31531
|
+
for (const ref of [...fromTypes, ...toTypes]) {
|
|
31532
|
+
if (!nodeNames.has(ref)) {
|
|
31533
|
+
warnings.push({
|
|
31534
|
+
code: "DANGLING_TOPOLOGY_REF",
|
|
31535
|
+
message: `Edge "${axbType}" references node type "${ref}" which was not found in the nodes directory`
|
|
30835
31536
|
});
|
|
30836
31537
|
}
|
|
30837
31538
|
}
|
|
30838
31539
|
}
|
|
30839
|
-
return
|
|
31540
|
+
return {
|
|
31541
|
+
result: { nodes, edges },
|
|
31542
|
+
warnings
|
|
31543
|
+
};
|
|
30840
31544
|
}
|
|
30841
31545
|
|
|
30842
|
-
// src/
|
|
30843
|
-
import {
|
|
30844
|
-
|
|
30845
|
-
|
|
30846
|
-
var
|
|
30847
|
-
var
|
|
30848
|
-
var
|
|
30849
|
-
|
|
30850
|
-
|
|
30851
|
-
`var _mod = require('node:module');`,
|
|
30852
|
-
`var _crypto = require('node:crypto');`,
|
|
30853
|
-
`var parentPort = _wt.parentPort;`,
|
|
30854
|
-
`var workerData = _wt.workerData;`,
|
|
30855
|
-
``,
|
|
30856
|
-
`// Load SES using the parent module's resolution context`,
|
|
30857
|
-
`var esmRequire = _mod.createRequire(workerData.parentUrl);`,
|
|
30858
|
-
`esmRequire('ses');`,
|
|
30859
|
-
``,
|
|
30860
|
-
`lockdown({`,
|
|
30861
|
-
` errorTaming: 'unsafe',`,
|
|
30862
|
-
` consoleTaming: 'unsafe',`,
|
|
30863
|
-
` evalTaming: 'safe-eval',`,
|
|
30864
|
-
` overrideTaming: 'moderate',`,
|
|
30865
|
-
` stackFiltering: 'verbose'`,
|
|
30866
|
-
`});`,
|
|
30867
|
-
``,
|
|
30868
|
-
`// Defense-in-depth: verify lockdown() actually hardened JSON.`,
|
|
30869
|
-
`if (!Object.isFrozen(JSON)) {`,
|
|
30870
|
-
` throw new Error('SES lockdown failed: JSON is not frozen');`,
|
|
30871
|
-
`}`,
|
|
30872
|
-
``,
|
|
30873
|
-
`var cache = new Map();`,
|
|
30874
|
-
``,
|
|
30875
|
-
`function hashSource(s) {`,
|
|
30876
|
-
` return _crypto.createHash('sha256').update(s).digest('hex');`,
|
|
30877
|
-
`}`,
|
|
30878
|
-
``,
|
|
30879
|
-
`function buildWrapper(source) {`,
|
|
30880
|
-
` return '(function() {' +`,
|
|
30881
|
-
` ' var fn = (' + source + ');\\n' +`,
|
|
30882
|
-
` ' if (typeof fn !== "function") return null;\\n' +`,
|
|
30883
|
-
` ' return function(jsonIn) {\\n' +`,
|
|
30884
|
-
` ' var data = JSON.parse(jsonIn);\\n' +`,
|
|
30885
|
-
` ' var result = fn(data);\\n' +`,
|
|
30886
|
-
` ' if (result !== null && typeof result === "object" && typeof result.then === "function") {\\n' +`,
|
|
30887
|
-
` ' return result.then(function(r) { return JSON.stringify(r); });\\n' +`,
|
|
30888
|
-
` ' }\\n' +`,
|
|
30889
|
-
` ' return JSON.stringify(result);\\n' +`,
|
|
30890
|
-
` ' };\\n' +`,
|
|
30891
|
-
` '})()';`,
|
|
30892
|
-
`}`,
|
|
30893
|
-
``,
|
|
30894
|
-
`function compileSource(source) {`,
|
|
30895
|
-
` var key = hashSource(source);`,
|
|
30896
|
-
` var cached = cache.get(key);`,
|
|
30897
|
-
` if (cached) return cached;`,
|
|
30898
|
-
``,
|
|
30899
|
-
` var compartmentFn;`,
|
|
30900
|
-
` try {`,
|
|
30901
|
-
` var c = new Compartment({ JSON: JSON });`,
|
|
30902
|
-
` compartmentFn = c.evaluate(buildWrapper(source));`,
|
|
30903
|
-
` } catch (err) {`,
|
|
30904
|
-
` throw new Error('Failed to compile migration source: ' + (err.message || String(err)));`,
|
|
30905
|
-
` }`,
|
|
30906
|
-
``,
|
|
30907
|
-
` if (typeof compartmentFn !== 'function') {`,
|
|
30908
|
-
` throw new Error('Migration source did not produce a function: ' + source.slice(0, 80));`,
|
|
30909
|
-
` }`,
|
|
30910
|
-
``,
|
|
30911
|
-
` cache.set(key, compartmentFn);`,
|
|
30912
|
-
` return compartmentFn;`,
|
|
30913
|
-
`}`,
|
|
30914
|
-
``,
|
|
30915
|
-
`parentPort.on('message', function(msg) {`,
|
|
30916
|
-
` var id = msg.id;`,
|
|
30917
|
-
` try {`,
|
|
30918
|
-
` if (msg.type === 'compile') {`,
|
|
30919
|
-
` compileSource(msg.source);`,
|
|
30920
|
-
` parentPort.postMessage({ id: id, type: 'compiled' });`,
|
|
30921
|
-
` return;`,
|
|
30922
|
-
` }`,
|
|
30923
|
-
` if (msg.type === 'execute') {`,
|
|
30924
|
-
` var fn = compileSource(msg.source);`,
|
|
30925
|
-
` var raw;`,
|
|
30926
|
-
` try {`,
|
|
30927
|
-
` raw = fn(msg.jsonData);`,
|
|
30928
|
-
` } catch (err) {`,
|
|
30929
|
-
` parentPort.postMessage({ id: id, type: 'error', message: 'Migration function threw: ' + (err.message || String(err)) });`,
|
|
30930
|
-
` return;`,
|
|
30931
|
-
` }`,
|
|
30932
|
-
` if (raw !== null && typeof raw === 'object' && typeof raw.then === 'function') {`,
|
|
30933
|
-
` raw.then(`,
|
|
30934
|
-
` function(jsonResult) {`,
|
|
30935
|
-
` if (jsonResult === undefined || jsonResult === null) {`,
|
|
30936
|
-
` parentPort.postMessage({ id: id, type: 'error', message: 'Migration returned a non-JSON-serializable value' });`,
|
|
30937
|
-
` } else {`,
|
|
30938
|
-
` parentPort.postMessage({ id: id, type: 'result', jsonResult: jsonResult });`,
|
|
30939
|
-
` }`,
|
|
30940
|
-
` },`,
|
|
30941
|
-
` function(err) {`,
|
|
30942
|
-
` parentPort.postMessage({ id: id, type: 'error', message: 'Async migration function threw: ' + (err.message || String(err)) });`,
|
|
30943
|
-
` }`,
|
|
30944
|
-
` );`,
|
|
30945
|
-
` return;`,
|
|
30946
|
-
` }`,
|
|
30947
|
-
` if (raw === undefined || raw === null) {`,
|
|
30948
|
-
` parentPort.postMessage({ id: id, type: 'error', message: 'Migration returned a non-JSON-serializable value' });`,
|
|
30949
|
-
` } else {`,
|
|
30950
|
-
` parentPort.postMessage({ id: id, type: 'result', jsonResult: raw });`,
|
|
30951
|
-
` }`,
|
|
30952
|
-
` }`,
|
|
30953
|
-
` } catch (err) {`,
|
|
30954
|
-
` parentPort.postMessage({ id: id, type: 'error', message: err.message || String(err) });`,
|
|
30955
|
-
` }`,
|
|
30956
|
-
`});`
|
|
30957
|
-
].join("\n");
|
|
30958
|
-
function ensureWorker() {
|
|
30959
|
-
if (_worker) return _worker;
|
|
30960
|
-
_worker = new Worker(WORKER_SOURCE, {
|
|
30961
|
-
eval: true,
|
|
30962
|
-
workerData: { parentUrl: import.meta.url }
|
|
30963
|
-
});
|
|
30964
|
-
_worker.unref();
|
|
30965
|
-
_worker.on("message", (msg) => {
|
|
30966
|
-
if (msg.id === void 0) return;
|
|
30967
|
-
const pending = _pending.get(msg.id);
|
|
30968
|
-
if (!pending) return;
|
|
30969
|
-
_pending.delete(msg.id);
|
|
30970
|
-
if (msg.type === "error") {
|
|
30971
|
-
pending.reject(new MigrationError(msg.message ?? "Unknown sandbox error"));
|
|
30972
|
-
} else {
|
|
30973
|
-
pending.resolve(msg);
|
|
30974
|
-
}
|
|
30975
|
-
});
|
|
30976
|
-
_worker.on("error", (err) => {
|
|
30977
|
-
for (const [, p] of _pending) {
|
|
30978
|
-
p.reject(new MigrationError(`Sandbox worker error: ${err.message}`));
|
|
30979
|
-
}
|
|
30980
|
-
_pending.clear();
|
|
30981
|
-
_worker = null;
|
|
30982
|
-
});
|
|
30983
|
-
_worker.on("exit", (code) => {
|
|
30984
|
-
if (_pending.size > 0) {
|
|
30985
|
-
for (const [, p] of _pending) {
|
|
30986
|
-
p.reject(new MigrationError(`Sandbox worker exited with code ${code}`));
|
|
30987
|
-
}
|
|
30988
|
-
_pending.clear();
|
|
30989
|
-
}
|
|
30990
|
-
_worker = null;
|
|
30991
|
-
});
|
|
30992
|
-
return _worker;
|
|
30993
|
-
}
|
|
30994
|
-
function sendToWorker(msg) {
|
|
30995
|
-
const worker = ensureWorker();
|
|
30996
|
-
if (_requestId >= Number.MAX_SAFE_INTEGER) _requestId = 0;
|
|
30997
|
-
const id = ++_requestId;
|
|
30998
|
-
return new Promise((resolve2, reject) => {
|
|
30999
|
-
_pending.set(id, { resolve: resolve2, reject });
|
|
31000
|
-
worker.postMessage({ ...msg, id });
|
|
31001
|
-
});
|
|
31546
|
+
// src/internal/firestore-backend.ts
|
|
31547
|
+
import { FieldValue as FieldValue2 } from "@google-cloud/firestore";
|
|
31548
|
+
|
|
31549
|
+
// src/bulk.ts
|
|
31550
|
+
var MAX_BATCH_SIZE = 500;
|
|
31551
|
+
var DEFAULT_MAX_RETRIES = 3;
|
|
31552
|
+
var BASE_DELAY_MS = 200;
|
|
31553
|
+
function sleep(ms) {
|
|
31554
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
31002
31555
|
}
|
|
31003
|
-
|
|
31004
|
-
|
|
31005
|
-
let
|
|
31006
|
-
|
|
31007
|
-
cache = /* @__PURE__ */ new Map();
|
|
31008
|
-
compiledCache.set(executor, cache);
|
|
31556
|
+
function chunk(arr, size) {
|
|
31557
|
+
const chunks = [];
|
|
31558
|
+
for (let i = 0; i < arr.length; i += size) {
|
|
31559
|
+
chunks.push(arr.slice(i, i + size));
|
|
31009
31560
|
}
|
|
31010
|
-
return
|
|
31011
|
-
}
|
|
31012
|
-
function hashSource(source) {
|
|
31013
|
-
return createHash2("sha256").update(source).digest("hex");
|
|
31561
|
+
return chunks;
|
|
31014
31562
|
}
|
|
31015
|
-
function
|
|
31016
|
-
|
|
31017
|
-
|
|
31018
|
-
|
|
31019
|
-
|
|
31020
|
-
|
|
31021
|
-
|
|
31022
|
-
|
|
31563
|
+
async function bulkDeleteDocIds(db2, collectionPath, docIds, options) {
|
|
31564
|
+
if (docIds.length === 0) {
|
|
31565
|
+
return { deleted: 0, batches: 0, errors: [] };
|
|
31566
|
+
}
|
|
31567
|
+
const batchSize = Math.min(options?.batchSize ?? MAX_BATCH_SIZE, MAX_BATCH_SIZE);
|
|
31568
|
+
const maxRetries = options?.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
31569
|
+
const onProgress = options?.onProgress;
|
|
31570
|
+
const chunks = chunk(docIds, batchSize);
|
|
31571
|
+
const errors = [];
|
|
31572
|
+
let deleted = 0;
|
|
31573
|
+
let completedBatches = 0;
|
|
31574
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
31575
|
+
const ids = chunks[i];
|
|
31576
|
+
let committed = false;
|
|
31577
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
31578
|
+
try {
|
|
31579
|
+
const batch = db2.batch();
|
|
31580
|
+
const collectionRef = db2.collection(collectionPath);
|
|
31581
|
+
for (const id of ids) {
|
|
31582
|
+
batch.delete(collectionRef.doc(id));
|
|
31023
31583
|
}
|
|
31024
|
-
|
|
31025
|
-
|
|
31026
|
-
|
|
31027
|
-
|
|
31584
|
+
await batch.commit();
|
|
31585
|
+
committed = true;
|
|
31586
|
+
deleted += ids.length;
|
|
31587
|
+
break;
|
|
31588
|
+
} catch (err) {
|
|
31589
|
+
if (attempt < maxRetries) {
|
|
31590
|
+
const delay = BASE_DELAY_MS * Math.pow(2, attempt);
|
|
31591
|
+
await sleep(delay);
|
|
31592
|
+
} else {
|
|
31593
|
+
errors.push({
|
|
31594
|
+
batchIndex: i,
|
|
31595
|
+
error: err instanceof Error ? err : new Error(String(err)),
|
|
31596
|
+
operationCount: ids.length
|
|
31597
|
+
});
|
|
31028
31598
|
}
|
|
31029
31599
|
}
|
|
31030
|
-
|
|
31031
|
-
|
|
31600
|
+
}
|
|
31601
|
+
if (committed) {
|
|
31602
|
+
completedBatches++;
|
|
31603
|
+
}
|
|
31604
|
+
if (onProgress) {
|
|
31605
|
+
onProgress({
|
|
31606
|
+
completedBatches,
|
|
31607
|
+
totalBatches: chunks.length,
|
|
31608
|
+
deletedSoFar: deleted
|
|
31609
|
+
});
|
|
31610
|
+
}
|
|
31611
|
+
}
|
|
31612
|
+
return { deleted, batches: completedBatches, errors };
|
|
31032
31613
|
}
|
|
31033
|
-
async function
|
|
31034
|
-
|
|
31035
|
-
|
|
31036
|
-
|
|
31037
|
-
|
|
31038
|
-
|
|
31039
|
-
|
|
31040
|
-
|
|
31041
|
-
|
|
31614
|
+
async function bulkRemoveEdges(db2, collectionPath, reader, params, options) {
|
|
31615
|
+
const effectiveParams = params.limit !== void 0 ? { ...params, allowCollectionScan: params.allowCollectionScan ?? true } : { ...params, limit: 0, allowCollectionScan: params.allowCollectionScan ?? true };
|
|
31616
|
+
const edges = await reader.findEdges(effectiveParams);
|
|
31617
|
+
const docIds = edges.map((e) => computeEdgeDocId(e.aUid, e.axbType, e.bUid));
|
|
31618
|
+
return bulkDeleteDocIds(db2, collectionPath, docIds, options);
|
|
31619
|
+
}
|
|
31620
|
+
async function deleteSubcollectionsRecursive(db2, collectionPath, docId, options) {
|
|
31621
|
+
const docRef = db2.collection(collectionPath).doc(docId);
|
|
31622
|
+
const subcollections = await docRef.listCollections();
|
|
31623
|
+
if (subcollections.length === 0) return { deleted: 0, errors: [] };
|
|
31624
|
+
let totalDeleted = 0;
|
|
31625
|
+
const allErrors = [];
|
|
31626
|
+
const subOptions = options ? { batchSize: options.batchSize, maxRetries: options.maxRetries } : void 0;
|
|
31627
|
+
for (const subCollRef of subcollections) {
|
|
31628
|
+
const subCollPath = subCollRef.path;
|
|
31629
|
+
const snapshot = await subCollRef.select().get();
|
|
31630
|
+
const subDocIds = snapshot.docs.map((d) => d.id);
|
|
31631
|
+
for (const subDocId of subDocIds) {
|
|
31632
|
+
const subResult = await deleteSubcollectionsRecursive(db2, subCollPath, subDocId, subOptions);
|
|
31633
|
+
totalDeleted += subResult.deleted;
|
|
31634
|
+
allErrors.push(...subResult.errors);
|
|
31635
|
+
}
|
|
31636
|
+
if (subDocIds.length > 0) {
|
|
31637
|
+
const result = await bulkDeleteDocIds(db2, subCollPath, subDocIds, subOptions);
|
|
31638
|
+
totalDeleted += result.deleted;
|
|
31639
|
+
allErrors.push(...result.errors);
|
|
31042
31640
|
}
|
|
31043
|
-
return;
|
|
31044
31641
|
}
|
|
31045
|
-
|
|
31642
|
+
return { deleted: totalDeleted, errors: allErrors };
|
|
31046
31643
|
}
|
|
31047
|
-
function
|
|
31048
|
-
const
|
|
31049
|
-
|
|
31050
|
-
|
|
31051
|
-
|
|
31052
|
-
|
|
31053
|
-
|
|
31054
|
-
|
|
31055
|
-
|
|
31056
|
-
|
|
31057
|
-
|
|
31058
|
-
|
|
31059
|
-
|
|
31644
|
+
async function removeNodeCascade(db2, collectionPath, reader, uid, options) {
|
|
31645
|
+
const [outgoingRaw, incomingRaw] = await Promise.all([
|
|
31646
|
+
reader.findEdges({ aUid: uid, allowCollectionScan: true, limit: 0 }),
|
|
31647
|
+
reader.findEdges({ bUid: uid, allowCollectionScan: true, limit: 0 })
|
|
31648
|
+
]);
|
|
31649
|
+
const outgoing = outgoingRaw.filter((e) => e.axbType !== NODE_RELATION);
|
|
31650
|
+
const incoming = incomingRaw.filter((e) => e.axbType !== NODE_RELATION);
|
|
31651
|
+
const edgeDocIdSet = /* @__PURE__ */ new Set();
|
|
31652
|
+
const allEdges = [];
|
|
31653
|
+
for (const edge of [...outgoing, ...incoming]) {
|
|
31654
|
+
const docId = computeEdgeDocId(edge.aUid, edge.axbType, edge.bUid);
|
|
31655
|
+
if (!edgeDocIdSet.has(docId)) {
|
|
31656
|
+
edgeDocIdSet.add(docId);
|
|
31657
|
+
allEdges.push(edge);
|
|
31658
|
+
}
|
|
31659
|
+
}
|
|
31660
|
+
const shouldDeleteSubcollections = options?.deleteSubcollections !== false;
|
|
31661
|
+
const nodeDocId = computeNodeDocId(uid);
|
|
31662
|
+
let subcollectionResult = { deleted: 0, errors: [] };
|
|
31663
|
+
if (shouldDeleteSubcollections) {
|
|
31664
|
+
subcollectionResult = await deleteSubcollectionsRecursive(
|
|
31665
|
+
db2,
|
|
31666
|
+
collectionPath,
|
|
31667
|
+
nodeDocId,
|
|
31668
|
+
options
|
|
31060
31669
|
);
|
|
31061
31670
|
}
|
|
31062
|
-
|
|
31063
|
-
|
|
31064
|
-
|
|
31065
|
-
|
|
31066
|
-
|
|
31067
|
-
|
|
31068
|
-
})
|
|
31671
|
+
const edgeDocIds = allEdges.map((e) => computeEdgeDocId(e.aUid, e.axbType, e.bUid));
|
|
31672
|
+
const allDocIds = [...edgeDocIds, nodeDocId];
|
|
31673
|
+
const batchSize = Math.min(options?.batchSize ?? MAX_BATCH_SIZE, MAX_BATCH_SIZE);
|
|
31674
|
+
const result = await bulkDeleteDocIds(db2, collectionPath, allDocIds, {
|
|
31675
|
+
...options,
|
|
31676
|
+
batchSize
|
|
31677
|
+
});
|
|
31678
|
+
const totalChunks = Math.ceil(allDocIds.length / batchSize);
|
|
31679
|
+
const nodeChunkIndex = totalChunks - 1;
|
|
31680
|
+
const nodeDeleted = !result.errors.some((e) => e.batchIndex === nodeChunkIndex);
|
|
31681
|
+
const topLevelEdgesDeleted = nodeDeleted ? result.deleted - 1 : result.deleted;
|
|
31682
|
+
return {
|
|
31683
|
+
deleted: result.deleted + subcollectionResult.deleted,
|
|
31684
|
+
batches: result.batches,
|
|
31685
|
+
errors: [...result.errors, ...subcollectionResult.errors],
|
|
31686
|
+
edgesDeleted: topLevelEdgesDeleted,
|
|
31687
|
+
nodeDeleted
|
|
31688
|
+
};
|
|
31069
31689
|
}
|
|
31070
31690
|
|
|
31071
|
-
// src/
|
|
31072
|
-
|
|
31073
|
-
|
|
31074
|
-
|
|
31075
|
-
|
|
31076
|
-
|
|
31077
|
-
|
|
31078
|
-
|
|
31079
|
-
|
|
31080
|
-
|
|
31081
|
-
|
|
31082
|
-
|
|
31083
|
-
};
|
|
31084
|
-
var NODE_TYPE_SCHEMA = {
|
|
31085
|
-
type: "object",
|
|
31086
|
-
required: ["name", "jsonSchema"],
|
|
31087
|
-
properties: {
|
|
31088
|
-
name: { type: "string", minLength: 1 },
|
|
31089
|
-
jsonSchema: { type: "object" },
|
|
31090
|
-
description: { type: "string" },
|
|
31091
|
-
titleField: { type: "string" },
|
|
31092
|
-
subtitleField: { type: "string" },
|
|
31093
|
-
viewTemplate: { type: "string" },
|
|
31094
|
-
viewCss: { type: "string" },
|
|
31095
|
-
allowedIn: { type: "array", items: { type: "string", minLength: 1 } },
|
|
31096
|
-
schemaVersion: { type: "integer", minimum: 0 },
|
|
31097
|
-
migrations: { type: "array", items: STORED_MIGRATION_STEP_SCHEMA },
|
|
31098
|
-
migrationWriteBack: { type: "string", enum: ["off", "eager", "background"] }
|
|
31099
|
-
},
|
|
31100
|
-
additionalProperties: false
|
|
31101
|
-
};
|
|
31102
|
-
var EDGE_TYPE_SCHEMA = {
|
|
31103
|
-
type: "object",
|
|
31104
|
-
required: ["name", "from", "to"],
|
|
31105
|
-
properties: {
|
|
31106
|
-
name: { type: "string", minLength: 1 },
|
|
31107
|
-
from: {
|
|
31108
|
-
oneOf: [
|
|
31109
|
-
{ type: "string", minLength: 1 },
|
|
31110
|
-
{ type: "array", items: { type: "string", minLength: 1 }, minItems: 1 }
|
|
31111
|
-
]
|
|
31691
|
+
// src/internal/firestore-backend.ts
|
|
31692
|
+
init_serialization();
|
|
31693
|
+
|
|
31694
|
+
// src/internal/firestore-adapter.ts
|
|
31695
|
+
function createFirestoreAdapter(db2, collectionPath) {
|
|
31696
|
+
const collectionRef = db2.collection(collectionPath);
|
|
31697
|
+
return {
|
|
31698
|
+
collectionPath,
|
|
31699
|
+
async getDoc(docId) {
|
|
31700
|
+
const snap = await collectionRef.doc(docId).get();
|
|
31701
|
+
if (!snap.exists) return null;
|
|
31702
|
+
return snap.data();
|
|
31112
31703
|
},
|
|
31113
|
-
|
|
31114
|
-
|
|
31115
|
-
{ type: "string", minLength: 1 },
|
|
31116
|
-
{ type: "array", items: { type: "string", minLength: 1 }, minItems: 1 }
|
|
31117
|
-
]
|
|
31704
|
+
async setDoc(docId, data) {
|
|
31705
|
+
await collectionRef.doc(docId).set(data);
|
|
31118
31706
|
},
|
|
31119
|
-
|
|
31120
|
-
|
|
31121
|
-
|
|
31122
|
-
|
|
31123
|
-
|
|
31124
|
-
|
|
31125
|
-
|
|
31126
|
-
|
|
31127
|
-
|
|
31128
|
-
|
|
31129
|
-
migrations: { type: "array", items: STORED_MIGRATION_STEP_SCHEMA },
|
|
31130
|
-
migrationWriteBack: { type: "string", enum: ["off", "eager", "background"] }
|
|
31131
|
-
},
|
|
31132
|
-
additionalProperties: false
|
|
31133
|
-
};
|
|
31134
|
-
var BOOTSTRAP_ENTRIES = [
|
|
31135
|
-
{
|
|
31136
|
-
aType: META_NODE_TYPE,
|
|
31137
|
-
axbType: NODE_RELATION,
|
|
31138
|
-
bType: META_NODE_TYPE,
|
|
31139
|
-
jsonSchema: NODE_TYPE_SCHEMA,
|
|
31140
|
-
description: "Meta-type: defines a node type"
|
|
31141
|
-
},
|
|
31142
|
-
{
|
|
31143
|
-
aType: META_EDGE_TYPE,
|
|
31144
|
-
axbType: NODE_RELATION,
|
|
31145
|
-
bType: META_EDGE_TYPE,
|
|
31146
|
-
jsonSchema: EDGE_TYPE_SCHEMA,
|
|
31147
|
-
description: "Meta-type: defines an edge type"
|
|
31148
|
-
}
|
|
31149
|
-
];
|
|
31150
|
-
function createBootstrapRegistry() {
|
|
31151
|
-
return createRegistry([...BOOTSTRAP_ENTRIES]);
|
|
31152
|
-
}
|
|
31153
|
-
function generateDeterministicUid(metaType, name) {
|
|
31154
|
-
const hash2 = createHash3("sha256").update(`${metaType}:${name}`).digest("base64url");
|
|
31155
|
-
return hash2.slice(0, 21);
|
|
31156
|
-
}
|
|
31157
|
-
async function createRegistryFromGraph(reader, executor) {
|
|
31158
|
-
const [nodeTypes, edgeTypes] = await Promise.all([
|
|
31159
|
-
reader.findNodes({ aType: META_NODE_TYPE }),
|
|
31160
|
-
reader.findNodes({ aType: META_EDGE_TYPE })
|
|
31161
|
-
]);
|
|
31162
|
-
const entries = [...BOOTSTRAP_ENTRIES];
|
|
31163
|
-
const prevalidations = [];
|
|
31164
|
-
for (const record2 of nodeTypes) {
|
|
31165
|
-
const data = record2.data;
|
|
31166
|
-
if (data.migrations) {
|
|
31167
|
-
for (const m of data.migrations) {
|
|
31168
|
-
prevalidations.push(precompileSource(m.up, executor));
|
|
31707
|
+
async updateDoc(docId, data) {
|
|
31708
|
+
await collectionRef.doc(docId).update(data);
|
|
31709
|
+
},
|
|
31710
|
+
async deleteDoc(docId) {
|
|
31711
|
+
await collectionRef.doc(docId).delete();
|
|
31712
|
+
},
|
|
31713
|
+
async query(filters, options) {
|
|
31714
|
+
let q = collectionRef;
|
|
31715
|
+
for (const f of filters) {
|
|
31716
|
+
q = q.where(f.field, f.op, f.value);
|
|
31169
31717
|
}
|
|
31170
|
-
|
|
31171
|
-
|
|
31172
|
-
for (const record2 of edgeTypes) {
|
|
31173
|
-
const data = record2.data;
|
|
31174
|
-
if (data.migrations) {
|
|
31175
|
-
for (const m of data.migrations) {
|
|
31176
|
-
prevalidations.push(precompileSource(m.up, executor));
|
|
31718
|
+
if (options?.orderBy) {
|
|
31719
|
+
q = q.orderBy(options.orderBy.field, options.orderBy.direction ?? "asc");
|
|
31177
31720
|
}
|
|
31178
|
-
|
|
31179
|
-
|
|
31180
|
-
await Promise.all(prevalidations);
|
|
31181
|
-
for (const record2 of nodeTypes) {
|
|
31182
|
-
const data = record2.data;
|
|
31183
|
-
entries.push({
|
|
31184
|
-
aType: data.name,
|
|
31185
|
-
axbType: NODE_RELATION,
|
|
31186
|
-
bType: data.name,
|
|
31187
|
-
jsonSchema: data.jsonSchema,
|
|
31188
|
-
description: data.description,
|
|
31189
|
-
titleField: data.titleField,
|
|
31190
|
-
subtitleField: data.subtitleField,
|
|
31191
|
-
allowedIn: data.allowedIn,
|
|
31192
|
-
migrations: data.migrations ? compileMigrations(data.migrations, executor) : void 0,
|
|
31193
|
-
migrationWriteBack: data.migrationWriteBack
|
|
31194
|
-
});
|
|
31195
|
-
}
|
|
31196
|
-
for (const record2 of edgeTypes) {
|
|
31197
|
-
const data = record2.data;
|
|
31198
|
-
const fromTypes = Array.isArray(data.from) ? data.from : [data.from];
|
|
31199
|
-
const toTypes = Array.isArray(data.to) ? data.to : [data.to];
|
|
31200
|
-
const compiledMigrations = data.migrations ? compileMigrations(data.migrations, executor) : void 0;
|
|
31201
|
-
for (const aType of fromTypes) {
|
|
31202
|
-
for (const bType of toTypes) {
|
|
31203
|
-
entries.push({
|
|
31204
|
-
aType,
|
|
31205
|
-
axbType: data.name,
|
|
31206
|
-
bType,
|
|
31207
|
-
jsonSchema: data.jsonSchema,
|
|
31208
|
-
description: data.description,
|
|
31209
|
-
inverseLabel: data.inverseLabel,
|
|
31210
|
-
titleField: data.titleField,
|
|
31211
|
-
subtitleField: data.subtitleField,
|
|
31212
|
-
allowedIn: data.allowedIn,
|
|
31213
|
-
targetGraph: data.targetGraph,
|
|
31214
|
-
migrations: compiledMigrations,
|
|
31215
|
-
migrationWriteBack: data.migrationWriteBack
|
|
31216
|
-
});
|
|
31721
|
+
if (options?.limit !== void 0) {
|
|
31722
|
+
q = q.limit(options.limit);
|
|
31217
31723
|
}
|
|
31724
|
+
const snap = await q.get();
|
|
31725
|
+
return snap.docs.map((doc) => doc.data());
|
|
31218
31726
|
}
|
|
31219
|
-
}
|
|
31220
|
-
return createRegistry(entries);
|
|
31727
|
+
};
|
|
31221
31728
|
}
|
|
31222
|
-
|
|
31223
|
-
|
|
31224
|
-
|
|
31225
|
-
|
|
31226
|
-
|
|
31227
|
-
|
|
31228
|
-
|
|
31229
|
-
|
|
31230
|
-
|
|
31231
|
-
|
|
31232
|
-
|
|
31233
|
-
|
|
31234
|
-
|
|
31235
|
-
|
|
31236
|
-
|
|
31237
|
-
|
|
31729
|
+
function createTransactionAdapter(db2, collectionPath, tx) {
|
|
31730
|
+
const collectionRef = db2.collection(collectionPath);
|
|
31731
|
+
return {
|
|
31732
|
+
async getDoc(docId) {
|
|
31733
|
+
const snap = await tx.get(collectionRef.doc(docId));
|
|
31734
|
+
if (!snap.exists) return null;
|
|
31735
|
+
return snap.data();
|
|
31736
|
+
},
|
|
31737
|
+
setDoc(docId, data) {
|
|
31738
|
+
tx.set(collectionRef.doc(docId), data);
|
|
31739
|
+
},
|
|
31740
|
+
updateDoc(docId, data) {
|
|
31741
|
+
tx.update(collectionRef.doc(docId), data);
|
|
31742
|
+
},
|
|
31743
|
+
deleteDoc(docId) {
|
|
31744
|
+
tx.delete(collectionRef.doc(docId));
|
|
31745
|
+
},
|
|
31746
|
+
async query(filters, options) {
|
|
31747
|
+
let q = collectionRef;
|
|
31748
|
+
for (const f of filters) {
|
|
31749
|
+
q = q.where(f.field, f.op, f.value);
|
|
31238
31750
|
}
|
|
31239
|
-
|
|
31240
|
-
|
|
31241
|
-
this.metaAdapter = createFirestoreAdapter(db2, metaCollectionPath);
|
|
31751
|
+
if (options?.orderBy) {
|
|
31752
|
+
q = q.orderBy(options.orderBy.field, options.orderBy.direction ?? "asc");
|
|
31242
31753
|
}
|
|
31243
|
-
|
|
31244
|
-
|
|
31245
|
-
}
|
|
31246
|
-
const requestedMode = options?.queryMode ?? "pipeline";
|
|
31247
|
-
const isEmulator = !!process.env.FIRESTORE_EMULATOR_HOST;
|
|
31248
|
-
if (isEmulator) {
|
|
31249
|
-
this.queryMode = "standard";
|
|
31250
|
-
} else {
|
|
31251
|
-
this.queryMode = requestedMode;
|
|
31252
|
-
}
|
|
31253
|
-
if (this.queryMode === "standard" && !isEmulator && requestedMode === "standard" && !_standardModeWarned) {
|
|
31254
|
-
_standardModeWarned = true;
|
|
31255
|
-
console.warn(
|
|
31256
|
-
"[firegraph] Standard query mode enabled. This is NOT recommended for production:\n - Enterprise Firestore: data.* filters cause full collection scans (high billing)\n - Standard Firestore: data.* filters without composite indexes will fail\n See: https://github.com/typicalday/firegraph#query-modes"
|
|
31257
|
-
);
|
|
31258
|
-
}
|
|
31259
|
-
this.scanProtection = options?.scanProtection ?? "error";
|
|
31260
|
-
if (this.queryMode === "pipeline") {
|
|
31261
|
-
this.pipelineAdapter = createPipelineQueryAdapter(db2, collectionPath);
|
|
31262
|
-
if (this.metaAdapter) {
|
|
31263
|
-
this.metaPipelineAdapter = createPipelineQueryAdapter(
|
|
31264
|
-
db2,
|
|
31265
|
-
options.registryMode.collection
|
|
31266
|
-
);
|
|
31754
|
+
if (options?.limit !== void 0) {
|
|
31755
|
+
q = q.limit(options.limit);
|
|
31267
31756
|
}
|
|
31757
|
+
const snap = await tx.get(q);
|
|
31758
|
+
return snap.docs.map((doc) => doc.data());
|
|
31268
31759
|
}
|
|
31269
|
-
}
|
|
31270
|
-
|
|
31271
|
-
|
|
31272
|
-
|
|
31273
|
-
|
|
31274
|
-
|
|
31275
|
-
|
|
31276
|
-
|
|
31277
|
-
|
|
31278
|
-
|
|
31279
|
-
|
|
31280
|
-
|
|
31281
|
-
|
|
31282
|
-
|
|
31283
|
-
|
|
31284
|
-
|
|
31285
|
-
|
|
31286
|
-
migrationSandbox;
|
|
31287
|
-
// ---------------------------------------------------------------------------
|
|
31288
|
-
// Registry routing
|
|
31289
|
-
// ---------------------------------------------------------------------------
|
|
31290
|
-
/**
|
|
31291
|
-
* Get the appropriate registry for validating a write to the given type.
|
|
31292
|
-
*
|
|
31293
|
-
* - Static-only mode: returns staticRegistry (or undefined if none set)
|
|
31294
|
-
* - Dynamic mode (pure or merged):
|
|
31295
|
-
* - Meta-types (nodeType, edgeType): validated against bootstrapRegistry
|
|
31296
|
-
* - Domain types: validated against dynamicRegistry (falls back to
|
|
31297
|
-
* bootstrapRegistry which rejects unknown types)
|
|
31298
|
-
* - Merged mode: dynamicRegistry is a merged wrapper (static + dynamic
|
|
31299
|
-
* extension), so static entries take priority automatically.
|
|
31300
|
-
*/
|
|
31301
|
-
getRegistryForType(aType) {
|
|
31302
|
-
if (!this.dynamicConfig) return this.staticRegistry;
|
|
31303
|
-
if (aType === META_NODE_TYPE || aType === META_EDGE_TYPE) {
|
|
31304
|
-
return this.bootstrapRegistry;
|
|
31305
|
-
}
|
|
31306
|
-
return this.dynamicRegistry ?? this.staticRegistry ?? this.bootstrapRegistry;
|
|
31307
|
-
}
|
|
31308
|
-
/**
|
|
31309
|
-
* Get the Firestore adapter for writing the given type.
|
|
31310
|
-
* Meta-types route to metaAdapter if a separate collection is configured.
|
|
31311
|
-
*/
|
|
31312
|
-
getAdapterForType(aType) {
|
|
31313
|
-
if (this.metaAdapter && (aType === META_NODE_TYPE || aType === META_EDGE_TYPE)) {
|
|
31314
|
-
return this.metaAdapter;
|
|
31315
|
-
}
|
|
31316
|
-
return this.adapter;
|
|
31317
|
-
}
|
|
31318
|
-
/**
|
|
31319
|
-
* Get the combined registry for transaction/batch context.
|
|
31320
|
-
* In static-only mode, returns staticRegistry.
|
|
31321
|
-
* In dynamic mode, returns dynamicRegistry (which includes bootstrap entries)
|
|
31322
|
-
* or falls back to staticRegistry (merged mode) or bootstrapRegistry.
|
|
31323
|
-
*/
|
|
31324
|
-
getCombinedRegistry() {
|
|
31325
|
-
if (!this.dynamicConfig) return this.staticRegistry;
|
|
31326
|
-
return this.dynamicRegistry ?? this.staticRegistry ?? this.bootstrapRegistry;
|
|
31327
|
-
}
|
|
31328
|
-
// ---------------------------------------------------------------------------
|
|
31329
|
-
// Query dispatch
|
|
31330
|
-
// ---------------------------------------------------------------------------
|
|
31331
|
-
/**
|
|
31332
|
-
* Dispatch a query to the appropriate adapter based on queryMode.
|
|
31333
|
-
* Pipeline queries use the PipelineQueryAdapter; standard queries
|
|
31334
|
-
* use the FirestoreAdapter.
|
|
31335
|
-
*/
|
|
31336
|
-
executeQuery(filters, options) {
|
|
31337
|
-
if (this.pipelineAdapter) {
|
|
31338
|
-
return this.pipelineAdapter.query(filters, options);
|
|
31339
|
-
}
|
|
31340
|
-
return this.adapter.query(filters, options);
|
|
31341
|
-
}
|
|
31342
|
-
/**
|
|
31343
|
-
* Check whether a query's filter set is safe (matches a known index pattern).
|
|
31344
|
-
* Throws QuerySafetyError or logs a warning depending on scanProtection config.
|
|
31345
|
-
*/
|
|
31346
|
-
checkQuerySafety(filters, allowCollectionScan) {
|
|
31347
|
-
if (allowCollectionScan || this.scanProtection === "off") return;
|
|
31348
|
-
const result = analyzeQuerySafety(filters);
|
|
31349
|
-
if (result.safe) return;
|
|
31350
|
-
if (this.scanProtection === "error") {
|
|
31351
|
-
throw new QuerySafetyError(result.reason);
|
|
31760
|
+
};
|
|
31761
|
+
}
|
|
31762
|
+
function createBatchAdapter(db2, collectionPath) {
|
|
31763
|
+
const collectionRef = db2.collection(collectionPath);
|
|
31764
|
+
const batch = db2.batch();
|
|
31765
|
+
return {
|
|
31766
|
+
setDoc(docId, data) {
|
|
31767
|
+
batch.set(collectionRef.doc(docId), data);
|
|
31768
|
+
},
|
|
31769
|
+
updateDoc(docId, data) {
|
|
31770
|
+
batch.update(collectionRef.doc(docId), data);
|
|
31771
|
+
},
|
|
31772
|
+
deleteDoc(docId) {
|
|
31773
|
+
batch.delete(collectionRef.doc(docId));
|
|
31774
|
+
},
|
|
31775
|
+
async commit() {
|
|
31776
|
+
await batch.commit();
|
|
31352
31777
|
}
|
|
31353
|
-
|
|
31778
|
+
};
|
|
31779
|
+
}
|
|
31780
|
+
|
|
31781
|
+
// src/internal/pipeline-adapter.ts
|
|
31782
|
+
var _Pipelines = null;
|
|
31783
|
+
async function getPipelines() {
|
|
31784
|
+
if (!_Pipelines) {
|
|
31785
|
+
const mod = await import("@google-cloud/firestore");
|
|
31786
|
+
_Pipelines = mod.Pipelines;
|
|
31354
31787
|
}
|
|
31355
|
-
|
|
31356
|
-
|
|
31357
|
-
|
|
31358
|
-
|
|
31359
|
-
|
|
31360
|
-
|
|
31361
|
-
|
|
31362
|
-
|
|
31363
|
-
|
|
31364
|
-
|
|
31365
|
-
|
|
31366
|
-
|
|
31367
|
-
|
|
31368
|
-
|
|
31369
|
-
|
|
31788
|
+
return _Pipelines;
|
|
31789
|
+
}
|
|
31790
|
+
function buildFilterExpression(P, filter) {
|
|
31791
|
+
const { field: fieldName, op, value } = filter;
|
|
31792
|
+
switch (op) {
|
|
31793
|
+
case "==":
|
|
31794
|
+
return P.equal(fieldName, value);
|
|
31795
|
+
case "!=":
|
|
31796
|
+
return P.notEqual(fieldName, value);
|
|
31797
|
+
case "<":
|
|
31798
|
+
return P.lessThan(fieldName, value);
|
|
31799
|
+
case "<=":
|
|
31800
|
+
return P.lessThanOrEqual(fieldName, value);
|
|
31801
|
+
case ">":
|
|
31802
|
+
return P.greaterThan(fieldName, value);
|
|
31803
|
+
case ">=":
|
|
31804
|
+
return P.greaterThanOrEqual(fieldName, value);
|
|
31805
|
+
case "in":
|
|
31806
|
+
return P.equalAny(fieldName, value);
|
|
31807
|
+
case "not-in":
|
|
31808
|
+
return P.notEqualAny(fieldName, value);
|
|
31809
|
+
case "array-contains":
|
|
31810
|
+
return P.arrayContains(fieldName, value);
|
|
31811
|
+
case "array-contains-any":
|
|
31812
|
+
return P.arrayContainsAny(fieldName, value);
|
|
31813
|
+
default:
|
|
31814
|
+
throw new Error(`Unsupported filter op for pipeline mode: ${op}`);
|
|
31370
31815
|
}
|
|
31371
|
-
|
|
31372
|
-
|
|
31373
|
-
|
|
31374
|
-
|
|
31375
|
-
|
|
31376
|
-
|
|
31377
|
-
|
|
31378
|
-
|
|
31379
|
-
|
|
31380
|
-
|
|
31381
|
-
|
|
31382
|
-
|
|
31816
|
+
}
|
|
31817
|
+
function createPipelineQueryAdapter(db2, collectionPath) {
|
|
31818
|
+
return {
|
|
31819
|
+
async query(filters, options) {
|
|
31820
|
+
const P = await getPipelines();
|
|
31821
|
+
let pipeline = db2.pipeline().collection(collectionPath);
|
|
31822
|
+
if (filters.length === 1) {
|
|
31823
|
+
pipeline = pipeline.where(buildFilterExpression(P, filters[0]));
|
|
31824
|
+
} else if (filters.length > 1) {
|
|
31825
|
+
const [first, second, ...rest] = filters.map((f) => buildFilterExpression(P, f));
|
|
31826
|
+
pipeline = pipeline.where(P.and(first, second, ...rest));
|
|
31827
|
+
}
|
|
31828
|
+
if (options?.orderBy) {
|
|
31829
|
+
const f = P.field(options.orderBy.field);
|
|
31830
|
+
const ordering = options.orderBy.direction === "desc" ? f.descending() : f.ascending();
|
|
31831
|
+
pipeline = pipeline.sort(ordering);
|
|
31832
|
+
}
|
|
31833
|
+
if (options?.limit !== void 0) {
|
|
31834
|
+
pipeline = pipeline.limit(options.limit);
|
|
31383
31835
|
}
|
|
31836
|
+
const snap = await pipeline.execute();
|
|
31837
|
+
return snap.results.map((r) => r.data());
|
|
31838
|
+
}
|
|
31839
|
+
};
|
|
31840
|
+
}
|
|
31841
|
+
|
|
31842
|
+
// src/internal/firestore-backend.ts
|
|
31843
|
+
function buildFirestoreUpdate(update, db2) {
|
|
31844
|
+
const out = {
|
|
31845
|
+
updatedAt: FieldValue2.serverTimestamp()
|
|
31846
|
+
};
|
|
31847
|
+
if (update.replaceData) {
|
|
31848
|
+
out.data = deserializeFirestoreTypes(update.replaceData, db2);
|
|
31849
|
+
}
|
|
31850
|
+
if (update.dataFields) {
|
|
31851
|
+
for (const [k, v] of Object.entries(update.dataFields)) {
|
|
31852
|
+
out[`data.${k}`] = v;
|
|
31384
31853
|
}
|
|
31385
|
-
return results.map((r) => r.record);
|
|
31386
31854
|
}
|
|
31387
|
-
|
|
31388
|
-
|
|
31389
|
-
*
|
|
31390
|
-
* Both `'eager'` and `'background'` are fire-and-forget (not awaited by
|
|
31391
|
-
* the caller). The difference is logging level on failure:
|
|
31392
|
-
* - `eager`: logs an error via `console.error`
|
|
31393
|
-
* - `background`: logs a warning via `console.warn`
|
|
31394
|
-
*
|
|
31395
|
-
* For truly synchronous write-back guarantees, use transactions — the
|
|
31396
|
-
* `GraphTransactionImpl` performs write-back inline within the transaction.
|
|
31397
|
-
*/
|
|
31398
|
-
handleWriteBack(result, docId) {
|
|
31399
|
-
if (result.writeBack === "off") return;
|
|
31400
|
-
const doWriteBack = async () => {
|
|
31401
|
-
try {
|
|
31402
|
-
const update = {
|
|
31403
|
-
data: deserializeFirestoreTypes(result.record.data, this.db),
|
|
31404
|
-
updatedAt: FieldValue5.serverTimestamp()
|
|
31405
|
-
};
|
|
31406
|
-
if (result.record.v !== void 0) {
|
|
31407
|
-
update.v = result.record.v;
|
|
31408
|
-
}
|
|
31409
|
-
await this.adapter.updateDoc(docId, update);
|
|
31410
|
-
} catch (err) {
|
|
31411
|
-
const msg = `[firegraph] Migration write-back failed for ${docId}: ${err.message}`;
|
|
31412
|
-
if (result.writeBack === "eager") {
|
|
31413
|
-
console.error(msg);
|
|
31414
|
-
} else {
|
|
31415
|
-
console.warn(msg);
|
|
31416
|
-
}
|
|
31417
|
-
}
|
|
31418
|
-
};
|
|
31419
|
-
void doWriteBack();
|
|
31855
|
+
if (update.v !== void 0) {
|
|
31856
|
+
out.v = update.v;
|
|
31420
31857
|
}
|
|
31421
|
-
|
|
31422
|
-
|
|
31423
|
-
|
|
31424
|
-
|
|
31425
|
-
|
|
31426
|
-
|
|
31427
|
-
|
|
31428
|
-
|
|
31858
|
+
return out;
|
|
31859
|
+
}
|
|
31860
|
+
function stampWritableRecord(record2) {
|
|
31861
|
+
const now = FieldValue2.serverTimestamp();
|
|
31862
|
+
const out = {
|
|
31863
|
+
aType: record2.aType,
|
|
31864
|
+
aUid: record2.aUid,
|
|
31865
|
+
axbType: record2.axbType,
|
|
31866
|
+
bType: record2.bType,
|
|
31867
|
+
bUid: record2.bUid,
|
|
31868
|
+
data: record2.data,
|
|
31869
|
+
createdAt: now,
|
|
31870
|
+
updatedAt: now
|
|
31871
|
+
};
|
|
31872
|
+
if (record2.v !== void 0) out.v = record2.v;
|
|
31873
|
+
return out;
|
|
31874
|
+
}
|
|
31875
|
+
var FirestoreTransactionBackend = class {
|
|
31876
|
+
constructor(adapter, db2) {
|
|
31877
|
+
this.adapter = adapter;
|
|
31878
|
+
this.db = db2;
|
|
31429
31879
|
}
|
|
31430
|
-
|
|
31431
|
-
|
|
31432
|
-
const record2 = await this.adapter.getDoc(docId);
|
|
31433
|
-
if (!record2) return null;
|
|
31434
|
-
return this.applyMigration(record2, docId);
|
|
31880
|
+
getDoc(docId) {
|
|
31881
|
+
return this.adapter.getDoc(docId);
|
|
31435
31882
|
}
|
|
31436
|
-
|
|
31437
|
-
|
|
31438
|
-
const record2 = await this.adapter.getDoc(docId);
|
|
31439
|
-
return record2 !== null;
|
|
31883
|
+
query(filters, options) {
|
|
31884
|
+
return this.adapter.query(filters, options);
|
|
31440
31885
|
}
|
|
31441
|
-
async
|
|
31442
|
-
|
|
31443
|
-
let records;
|
|
31444
|
-
if (plan.strategy === "get") {
|
|
31445
|
-
const record2 = await this.adapter.getDoc(plan.docId);
|
|
31446
|
-
records = record2 ? [record2] : [];
|
|
31447
|
-
} else {
|
|
31448
|
-
this.checkQuerySafety(plan.filters, params.allowCollectionScan);
|
|
31449
|
-
records = await this.executeQuery(plan.filters, plan.options);
|
|
31450
|
-
}
|
|
31451
|
-
return this.applyMigrations(records);
|
|
31886
|
+
async setDoc(docId, record2) {
|
|
31887
|
+
this.adapter.setDoc(docId, stampWritableRecord(record2));
|
|
31452
31888
|
}
|
|
31453
|
-
async
|
|
31454
|
-
|
|
31455
|
-
let records;
|
|
31456
|
-
if (plan.strategy === "get") {
|
|
31457
|
-
const record2 = await this.adapter.getDoc(plan.docId);
|
|
31458
|
-
records = record2 ? [record2] : [];
|
|
31459
|
-
} else {
|
|
31460
|
-
this.checkQuerySafety(plan.filters, params.allowCollectionScan);
|
|
31461
|
-
records = await this.executeQuery(plan.filters, plan.options);
|
|
31462
|
-
}
|
|
31463
|
-
return this.applyMigrations(records);
|
|
31889
|
+
async updateDoc(docId, update) {
|
|
31890
|
+
this.adapter.updateDoc(docId, buildFirestoreUpdate(update, this.db));
|
|
31464
31891
|
}
|
|
31465
|
-
|
|
31466
|
-
|
|
31467
|
-
// ---------------------------------------------------------------------------
|
|
31468
|
-
async putNode(aType, uid, data) {
|
|
31469
|
-
const registry2 = this.getRegistryForType(aType);
|
|
31470
|
-
if (registry2) {
|
|
31471
|
-
registry2.validate(aType, NODE_RELATION, aType, data, this.scopePath);
|
|
31472
|
-
}
|
|
31473
|
-
const adapter = this.getAdapterForType(aType);
|
|
31474
|
-
const docId = computeNodeDocId(uid);
|
|
31475
|
-
const record2 = buildNodeRecord(aType, uid, data);
|
|
31476
|
-
if (registry2) {
|
|
31477
|
-
const entry = registry2.lookup(aType, NODE_RELATION, aType);
|
|
31478
|
-
if (entry?.schemaVersion && entry.schemaVersion > 0) {
|
|
31479
|
-
record2.v = entry.schemaVersion;
|
|
31480
|
-
}
|
|
31481
|
-
}
|
|
31482
|
-
await adapter.setDoc(docId, record2);
|
|
31892
|
+
async deleteDoc(docId) {
|
|
31893
|
+
this.adapter.deleteDoc(docId);
|
|
31483
31894
|
}
|
|
31484
|
-
|
|
31485
|
-
|
|
31486
|
-
|
|
31487
|
-
|
|
31488
|
-
|
|
31489
|
-
|
|
31490
|
-
|
|
31491
|
-
|
|
31492
|
-
|
|
31493
|
-
|
|
31494
|
-
|
|
31495
|
-
|
|
31496
|
-
|
|
31895
|
+
};
|
|
31896
|
+
var FirestoreBatchBackend = class {
|
|
31897
|
+
constructor(adapter, db2) {
|
|
31898
|
+
this.adapter = adapter;
|
|
31899
|
+
this.db = db2;
|
|
31900
|
+
}
|
|
31901
|
+
setDoc(docId, record2) {
|
|
31902
|
+
this.adapter.setDoc(docId, stampWritableRecord(record2));
|
|
31903
|
+
}
|
|
31904
|
+
updateDoc(docId, update) {
|
|
31905
|
+
this.adapter.updateDoc(docId, buildFirestoreUpdate(update, this.db));
|
|
31906
|
+
}
|
|
31907
|
+
deleteDoc(docId) {
|
|
31908
|
+
this.adapter.deleteDoc(docId);
|
|
31909
|
+
}
|
|
31910
|
+
commit() {
|
|
31911
|
+
return this.adapter.commit();
|
|
31912
|
+
}
|
|
31913
|
+
};
|
|
31914
|
+
var FirestoreBackendImpl = class _FirestoreBackendImpl {
|
|
31915
|
+
constructor(db2, collectionPath, queryMode, scopePath) {
|
|
31916
|
+
this.db = db2;
|
|
31917
|
+
this.queryMode = queryMode;
|
|
31918
|
+
this.collectionPath = collectionPath;
|
|
31919
|
+
this.scopePath = scopePath;
|
|
31920
|
+
this.adapter = createFirestoreAdapter(db2, collectionPath);
|
|
31921
|
+
if (queryMode === "pipeline") {
|
|
31922
|
+
this.pipelineAdapter = createPipelineQueryAdapter(db2, collectionPath);
|
|
31497
31923
|
}
|
|
31498
|
-
await adapter.setDoc(docId, record2);
|
|
31499
31924
|
}
|
|
31500
|
-
|
|
31501
|
-
|
|
31502
|
-
|
|
31503
|
-
|
|
31504
|
-
|
|
31505
|
-
|
|
31506
|
-
|
|
31925
|
+
collectionPath;
|
|
31926
|
+
scopePath;
|
|
31927
|
+
adapter;
|
|
31928
|
+
pipelineAdapter;
|
|
31929
|
+
// --- Reads ---
|
|
31930
|
+
getDoc(docId) {
|
|
31931
|
+
return this.adapter.getDoc(docId);
|
|
31932
|
+
}
|
|
31933
|
+
query(filters, options) {
|
|
31934
|
+
if (this.pipelineAdapter) {
|
|
31935
|
+
return this.pipelineAdapter.query(filters, options);
|
|
31507
31936
|
}
|
|
31508
|
-
|
|
31937
|
+
return this.adapter.query(filters, options);
|
|
31509
31938
|
}
|
|
31510
|
-
|
|
31511
|
-
|
|
31512
|
-
|
|
31939
|
+
// --- Writes ---
|
|
31940
|
+
setDoc(docId, record2) {
|
|
31941
|
+
return this.adapter.setDoc(docId, stampWritableRecord(record2));
|
|
31513
31942
|
}
|
|
31514
|
-
|
|
31515
|
-
|
|
31516
|
-
await this.adapter.deleteDoc(docId);
|
|
31943
|
+
updateDoc(docId, update) {
|
|
31944
|
+
return this.adapter.updateDoc(docId, buildFirestoreUpdate(update, this.db));
|
|
31517
31945
|
}
|
|
31518
|
-
|
|
31519
|
-
|
|
31520
|
-
|
|
31521
|
-
|
|
31946
|
+
deleteDoc(docId) {
|
|
31947
|
+
return this.adapter.deleteDoc(docId);
|
|
31948
|
+
}
|
|
31949
|
+
// --- Transactions / Batches ---
|
|
31950
|
+
runTransaction(fn) {
|
|
31522
31951
|
return this.db.runTransaction(async (firestoreTx) => {
|
|
31523
|
-
const
|
|
31524
|
-
|
|
31525
|
-
this.adapter.collectionPath,
|
|
31526
|
-
firestoreTx
|
|
31527
|
-
);
|
|
31528
|
-
const graphTx = new GraphTransactionImpl(adapter, this.getCombinedRegistry(), this.scanProtection, this.scopePath, this.globalWriteBack, this.db);
|
|
31529
|
-
return fn(graphTx);
|
|
31952
|
+
const txAdapter = createTransactionAdapter(this.db, this.collectionPath, firestoreTx);
|
|
31953
|
+
return fn(new FirestoreTransactionBackend(txAdapter, this.db));
|
|
31530
31954
|
});
|
|
31531
31955
|
}
|
|
31532
|
-
|
|
31533
|
-
const
|
|
31534
|
-
return new
|
|
31956
|
+
createBatch() {
|
|
31957
|
+
const batchAdapter = createBatchAdapter(this.db, this.collectionPath);
|
|
31958
|
+
return new FirestoreBatchBackend(batchAdapter, this.db);
|
|
31535
31959
|
}
|
|
31536
|
-
//
|
|
31537
|
-
|
|
31538
|
-
|
|
31539
|
-
|
|
31540
|
-
|
|
31541
|
-
throw new FiregraphError(
|
|
31542
|
-
`Invalid parentNodeUid for subgraph: "${parentNodeUid}". Must be a non-empty string without "/".`,
|
|
31543
|
-
"INVALID_SUBGRAPH"
|
|
31544
|
-
);
|
|
31545
|
-
}
|
|
31546
|
-
if (name.includes("/")) {
|
|
31547
|
-
throw new FiregraphError(
|
|
31548
|
-
`Subgraph name must not contain "/": got "${name}". Use chained .subgraph() calls for nested subgraphs.`,
|
|
31549
|
-
"INVALID_SUBGRAPH"
|
|
31550
|
-
);
|
|
31551
|
-
}
|
|
31552
|
-
const subCollectionPath = `${this.adapter.collectionPath}/${parentNodeUid}/${name}`;
|
|
31553
|
-
const newScopePath = this.scopePath ? `${this.scopePath}/${name}` : name;
|
|
31554
|
-
return new _GraphClientImpl(
|
|
31555
|
-
this.db,
|
|
31556
|
-
subCollectionPath,
|
|
31557
|
-
{
|
|
31558
|
-
registry: this.getCombinedRegistry(),
|
|
31559
|
-
queryMode: this.queryMode === "pipeline" ? "pipeline" : "standard",
|
|
31560
|
-
scanProtection: this.scanProtection,
|
|
31561
|
-
migrationWriteBack: this.globalWriteBack,
|
|
31562
|
-
migrationSandbox: this.migrationSandbox
|
|
31563
|
-
},
|
|
31564
|
-
newScopePath
|
|
31565
|
-
);
|
|
31960
|
+
// --- Subgraphs ---
|
|
31961
|
+
subgraph(parentNodeUid, name) {
|
|
31962
|
+
const subPath = `${this.collectionPath}/${parentNodeUid}/${name}`;
|
|
31963
|
+
const newScope = this.scopePath ? `${this.scopePath}/${name}` : name;
|
|
31964
|
+
return new _FirestoreBackendImpl(this.db, subPath, this.queryMode, newScope);
|
|
31566
31965
|
}
|
|
31567
|
-
//
|
|
31568
|
-
|
|
31569
|
-
|
|
31966
|
+
// --- Cascade & bulk ---
|
|
31967
|
+
removeNodeCascade(uid, reader, options) {
|
|
31968
|
+
return removeNodeCascade(this.db, this.collectionPath, reader, uid, options);
|
|
31969
|
+
}
|
|
31970
|
+
bulkRemoveEdges(params, reader, options) {
|
|
31971
|
+
return bulkRemoveEdges(this.db, this.collectionPath, reader, params, options);
|
|
31972
|
+
}
|
|
31973
|
+
// --- Cross-collection ---
|
|
31570
31974
|
async findEdgesGlobal(params, collectionName) {
|
|
31571
|
-
const name = collectionName ?? this.
|
|
31975
|
+
const name = collectionName ?? this.collectionPath.split("/").pop();
|
|
31572
31976
|
const plan = buildEdgeQueryPlan(params);
|
|
31573
31977
|
if (plan.strategy === "get") {
|
|
31574
31978
|
throw new FiregraphError(
|
|
@@ -31576,183 +31980,47 @@ var GraphClientImpl = class _GraphClientImpl {
|
|
|
31576
31980
|
"INVALID_QUERY"
|
|
31577
31981
|
);
|
|
31578
31982
|
}
|
|
31579
|
-
this.checkQuerySafety(plan.filters, params.allowCollectionScan);
|
|
31580
31983
|
const collectionGroupRef = this.db.collectionGroup(name);
|
|
31581
31984
|
let q = collectionGroupRef;
|
|
31582
31985
|
for (const f of plan.filters) {
|
|
31583
31986
|
q = q.where(f.field, f.op, f.value);
|
|
31584
31987
|
}
|
|
31585
|
-
if (plan.options?.orderBy) {
|
|
31586
|
-
q = q.orderBy(plan.options.orderBy.field, plan.options.orderBy.direction ?? "asc");
|
|
31587
|
-
}
|
|
31588
|
-
if (plan.options?.limit !== void 0) {
|
|
31589
|
-
q = q.limit(plan.options.limit);
|
|
31590
|
-
}
|
|
31591
|
-
const snap = await q.get();
|
|
31592
|
-
|
|
31593
|
-
return this.applyMigrations(records);
|
|
31594
|
-
}
|
|
31595
|
-
// ---------------------------------------------------------------------------
|
|
31596
|
-
// Bulk operations
|
|
31597
|
-
// ---------------------------------------------------------------------------
|
|
31598
|
-
async removeNodeCascade(uid, options) {
|
|
31599
|
-
return removeNodeCascade(this.db, this.adapter.collectionPath, this, uid, options);
|
|
31600
|
-
}
|
|
31601
|
-
async bulkRemoveEdges(params, options) {
|
|
31602
|
-
return bulkRemoveEdges(this.db, this.adapter.collectionPath, this, params, options);
|
|
31603
|
-
}
|
|
31604
|
-
// ---------------------------------------------------------------------------
|
|
31605
|
-
// Dynamic registry methods
|
|
31606
|
-
// ---------------------------------------------------------------------------
|
|
31607
|
-
async defineNodeType(name, jsonSchema, description, options) {
|
|
31608
|
-
if (!this.dynamicConfig) {
|
|
31609
|
-
throw new DynamicRegistryError(
|
|
31610
|
-
'defineNodeType() is only available in dynamic registry mode. Pass registryMode: { mode: "dynamic" } to createGraphClient().'
|
|
31611
|
-
);
|
|
31612
|
-
}
|
|
31613
|
-
if (RESERVED_TYPE_NAMES.has(name)) {
|
|
31614
|
-
throw new DynamicRegistryError(
|
|
31615
|
-
`Cannot define type "${name}": this name is reserved for the meta-registry.`
|
|
31616
|
-
);
|
|
31617
|
-
}
|
|
31618
|
-
if (this.staticRegistry?.lookup(name, NODE_RELATION, name)) {
|
|
31619
|
-
throw new DynamicRegistryError(
|
|
31620
|
-
`Cannot define node type "${name}": already defined in the static registry.`
|
|
31621
|
-
);
|
|
31622
|
-
}
|
|
31623
|
-
const uid = generateDeterministicUid(META_NODE_TYPE, name);
|
|
31624
|
-
const data = { name, jsonSchema };
|
|
31625
|
-
if (description !== void 0) data.description = description;
|
|
31626
|
-
if (options?.titleField !== void 0) data.titleField = options.titleField;
|
|
31627
|
-
if (options?.subtitleField !== void 0) data.subtitleField = options.subtitleField;
|
|
31628
|
-
if (options?.viewTemplate !== void 0) data.viewTemplate = options.viewTemplate;
|
|
31629
|
-
if (options?.viewCss !== void 0) data.viewCss = options.viewCss;
|
|
31630
|
-
if (options?.allowedIn !== void 0) data.allowedIn = options.allowedIn;
|
|
31631
|
-
if (options?.migrationWriteBack !== void 0) data.migrationWriteBack = options.migrationWriteBack;
|
|
31632
|
-
if (options?.migrations !== void 0) {
|
|
31633
|
-
data.migrations = await this.serializeMigrations(options.migrations);
|
|
31634
|
-
}
|
|
31635
|
-
await this.putNode(META_NODE_TYPE, uid, data);
|
|
31636
|
-
}
|
|
31637
|
-
async defineEdgeType(name, topology, jsonSchema, description, options) {
|
|
31638
|
-
if (!this.dynamicConfig) {
|
|
31639
|
-
throw new DynamicRegistryError(
|
|
31640
|
-
'defineEdgeType() is only available in dynamic registry mode. Pass registryMode: { mode: "dynamic" } to createGraphClient().'
|
|
31641
|
-
);
|
|
31642
|
-
}
|
|
31643
|
-
if (RESERVED_TYPE_NAMES.has(name)) {
|
|
31644
|
-
throw new DynamicRegistryError(
|
|
31645
|
-
`Cannot define type "${name}": this name is reserved for the meta-registry.`
|
|
31646
|
-
);
|
|
31647
|
-
}
|
|
31648
|
-
if (this.staticRegistry) {
|
|
31649
|
-
const fromTypes = Array.isArray(topology.from) ? topology.from : [topology.from];
|
|
31650
|
-
const toTypes = Array.isArray(topology.to) ? topology.to : [topology.to];
|
|
31651
|
-
for (const aType of fromTypes) {
|
|
31652
|
-
for (const bType of toTypes) {
|
|
31653
|
-
if (this.staticRegistry.lookup(aType, name, bType)) {
|
|
31654
|
-
throw new DynamicRegistryError(
|
|
31655
|
-
`Cannot define edge type "${name}" for (${aType}) -> (${bType}): already defined in the static registry.`
|
|
31656
|
-
);
|
|
31657
|
-
}
|
|
31658
|
-
}
|
|
31659
|
-
}
|
|
31660
|
-
}
|
|
31661
|
-
const uid = generateDeterministicUid(META_EDGE_TYPE, name);
|
|
31662
|
-
const data = {
|
|
31663
|
-
name,
|
|
31664
|
-
from: topology.from,
|
|
31665
|
-
to: topology.to
|
|
31666
|
-
};
|
|
31667
|
-
if (jsonSchema !== void 0) data.jsonSchema = jsonSchema;
|
|
31668
|
-
if (topology.inverseLabel !== void 0) data.inverseLabel = topology.inverseLabel;
|
|
31669
|
-
if (topology.targetGraph !== void 0) data.targetGraph = topology.targetGraph;
|
|
31670
|
-
if (description !== void 0) data.description = description;
|
|
31671
|
-
if (options?.titleField !== void 0) data.titleField = options.titleField;
|
|
31672
|
-
if (options?.subtitleField !== void 0) data.subtitleField = options.subtitleField;
|
|
31673
|
-
if (options?.viewTemplate !== void 0) data.viewTemplate = options.viewTemplate;
|
|
31674
|
-
if (options?.viewCss !== void 0) data.viewCss = options.viewCss;
|
|
31675
|
-
if (options?.allowedIn !== void 0) data.allowedIn = options.allowedIn;
|
|
31676
|
-
if (options?.migrationWriteBack !== void 0) data.migrationWriteBack = options.migrationWriteBack;
|
|
31677
|
-
if (options?.migrations !== void 0) {
|
|
31678
|
-
data.migrations = await this.serializeMigrations(options.migrations);
|
|
31679
|
-
}
|
|
31680
|
-
await this.putNode(META_EDGE_TYPE, uid, data);
|
|
31681
|
-
}
|
|
31682
|
-
async reloadRegistry() {
|
|
31683
|
-
if (!this.dynamicConfig) {
|
|
31684
|
-
throw new DynamicRegistryError(
|
|
31685
|
-
'reloadRegistry() is only available in dynamic registry mode. Pass registryMode: { mode: "dynamic" } to createGraphClient().'
|
|
31686
|
-
);
|
|
31687
|
-
}
|
|
31688
|
-
const reader = this.createMetaReader();
|
|
31689
|
-
const dynamicOnly = await createRegistryFromGraph(reader, this.migrationSandbox);
|
|
31690
|
-
if (this.staticRegistry) {
|
|
31691
|
-
this.dynamicRegistry = createMergedRegistry(this.staticRegistry, dynamicOnly);
|
|
31692
|
-
} else {
|
|
31693
|
-
this.dynamicRegistry = dynamicOnly;
|
|
31694
|
-
}
|
|
31988
|
+
if (plan.options?.orderBy) {
|
|
31989
|
+
q = q.orderBy(plan.options.orderBy.field, plan.options.orderBy.direction ?? "asc");
|
|
31990
|
+
}
|
|
31991
|
+
if (plan.options?.limit !== void 0) {
|
|
31992
|
+
q = q.limit(plan.options.limit);
|
|
31993
|
+
}
|
|
31994
|
+
const snap = await q.get();
|
|
31995
|
+
return snap.docs.map((doc) => doc.data());
|
|
31695
31996
|
}
|
|
31696
|
-
|
|
31697
|
-
|
|
31698
|
-
|
|
31699
|
-
|
|
31700
|
-
|
|
31701
|
-
|
|
31702
|
-
|
|
31703
|
-
|
|
31704
|
-
|
|
31705
|
-
|
|
31706
|
-
|
|
31707
|
-
|
|
31997
|
+
};
|
|
31998
|
+
function createFirestoreBackend(db2, collectionPath, options = {}) {
|
|
31999
|
+
const queryMode = options.queryMode ?? "pipeline";
|
|
32000
|
+
const scopePath = options.scopePath ?? "";
|
|
32001
|
+
return new FirestoreBackendImpl(db2, collectionPath, queryMode, scopePath);
|
|
32002
|
+
}
|
|
32003
|
+
|
|
32004
|
+
// src/firestore.ts
|
|
32005
|
+
var _standardModeWarned = false;
|
|
32006
|
+
function createGraphClient(db2, collectionPath, options) {
|
|
32007
|
+
const requestedMode = options?.queryMode ?? "pipeline";
|
|
32008
|
+
const isEmulator = !!process.env.FIRESTORE_EMULATOR_HOST;
|
|
32009
|
+
const effectiveMode = isEmulator ? "standard" : requestedMode;
|
|
32010
|
+
if (effectiveMode === "standard" && !isEmulator && requestedMode === "standard" && !_standardModeWarned) {
|
|
32011
|
+
_standardModeWarned = true;
|
|
32012
|
+
console.warn(
|
|
32013
|
+
"[firegraph] Standard query mode enabled. This is NOT recommended for production:\n - Enterprise Firestore: data.* filters cause full collection scans (high billing)\n - Standard Firestore: data.* filters without composite indexes will fail\n See: https://github.com/typicalday/firegraph#query-modes"
|
|
31708
32014
|
);
|
|
31709
|
-
return result;
|
|
31710
32015
|
}
|
|
31711
|
-
|
|
31712
|
-
|
|
31713
|
-
|
|
31714
|
-
|
|
31715
|
-
|
|
31716
|
-
|
|
31717
|
-
if (!this.metaAdapter) return this;
|
|
31718
|
-
const adapter = this.metaAdapter;
|
|
31719
|
-
const pipelineAdapter = this.metaPipelineAdapter;
|
|
31720
|
-
const executeMetaQuery = (filters, options) => {
|
|
31721
|
-
if (pipelineAdapter) return pipelineAdapter.query(filters, options);
|
|
31722
|
-
return adapter.query(filters, options);
|
|
31723
|
-
};
|
|
31724
|
-
return {
|
|
31725
|
-
async getNode(uid) {
|
|
31726
|
-
return adapter.getDoc(computeNodeDocId(uid));
|
|
31727
|
-
},
|
|
31728
|
-
async getEdge(aUid, axbType, bUid) {
|
|
31729
|
-
return adapter.getDoc(computeEdgeDocId(aUid, axbType, bUid));
|
|
31730
|
-
},
|
|
31731
|
-
async edgeExists(aUid, axbType, bUid) {
|
|
31732
|
-
const record2 = await adapter.getDoc(computeEdgeDocId(aUid, axbType, bUid));
|
|
31733
|
-
return record2 !== null;
|
|
31734
|
-
},
|
|
31735
|
-
async findEdges(params) {
|
|
31736
|
-
const plan = buildEdgeQueryPlan(params);
|
|
31737
|
-
if (plan.strategy === "get") {
|
|
31738
|
-
const record2 = await adapter.getDoc(plan.docId);
|
|
31739
|
-
return record2 ? [record2] : [];
|
|
31740
|
-
}
|
|
31741
|
-
return executeMetaQuery(plan.filters, plan.options);
|
|
31742
|
-
},
|
|
31743
|
-
async findNodes(params) {
|
|
31744
|
-
const plan = buildNodeQueryPlan(params);
|
|
31745
|
-
if (plan.strategy === "get") {
|
|
31746
|
-
const record2 = await adapter.getDoc(plan.docId);
|
|
31747
|
-
return record2 ? [record2] : [];
|
|
31748
|
-
}
|
|
31749
|
-
return executeMetaQuery(plan.filters, plan.options);
|
|
31750
|
-
}
|
|
31751
|
-
};
|
|
32016
|
+
const backend = createFirestoreBackend(db2, collectionPath, { queryMode: effectiveMode });
|
|
32017
|
+
let metaBackend;
|
|
32018
|
+
if (options?.registryMode?.collection && options.registryMode.collection !== collectionPath) {
|
|
32019
|
+
metaBackend = createFirestoreBackend(db2, options.registryMode.collection, {
|
|
32020
|
+
queryMode: effectiveMode
|
|
32021
|
+
});
|
|
31752
32022
|
}
|
|
31753
|
-
|
|
31754
|
-
function createGraphClient(db2, collectionPath, options) {
|
|
31755
|
-
return new GraphClientImpl(db2, collectionPath, options);
|
|
32023
|
+
return new GraphClientImpl(backend, options, metaBackend);
|
|
31756
32024
|
}
|
|
31757
32025
|
|
|
31758
32026
|
// node_modules/.pnpm/nanoid@5.1.6/node_modules/nanoid/index.js
|
|
@@ -31790,209 +32058,11 @@ function generateId() {
|
|
|
31790
32058
|
return nanoid();
|
|
31791
32059
|
}
|
|
31792
32060
|
|
|
31793
|
-
// src/
|
|
31794
|
-
import {
|
|
31795
|
-
|
|
31796
|
-
|
|
31797
|
-
|
|
31798
|
-
constructor(message) {
|
|
31799
|
-
super(message, "DISCOVERY_ERROR");
|
|
31800
|
-
this.name = "DiscoveryError";
|
|
31801
|
-
}
|
|
31802
|
-
};
|
|
31803
|
-
function readJson(filePath) {
|
|
31804
|
-
try {
|
|
31805
|
-
const raw = readFileSync(filePath, "utf-8");
|
|
31806
|
-
return JSON.parse(raw);
|
|
31807
|
-
} catch (err) {
|
|
31808
|
-
const msg = err instanceof SyntaxError ? `Invalid JSON in ${filePath}: ${err.message}` : `Cannot read ${filePath}: ${err.message}`;
|
|
31809
|
-
throw new DiscoveryError(msg);
|
|
31810
|
-
}
|
|
31811
|
-
}
|
|
31812
|
-
function readJsonIfExists(filePath) {
|
|
31813
|
-
if (!existsSync(filePath)) return void 0;
|
|
31814
|
-
return readJson(filePath);
|
|
31815
|
-
}
|
|
31816
|
-
var SCHEMA_SCRIPT_EXTENSIONS = [".ts", ".js", ".mts", ".mjs"];
|
|
31817
|
-
function loadSchema(dir, entityLabel) {
|
|
31818
|
-
for (const ext of SCHEMA_SCRIPT_EXTENSIONS) {
|
|
31819
|
-
const candidate = join(dir, `schema${ext}`);
|
|
31820
|
-
if (existsSync(candidate)) {
|
|
31821
|
-
return loadSchemaModule(candidate, entityLabel);
|
|
31822
|
-
}
|
|
31823
|
-
}
|
|
31824
|
-
const jsonPath = join(dir, "schema.json");
|
|
31825
|
-
if (existsSync(jsonPath)) {
|
|
31826
|
-
return readJson(jsonPath);
|
|
31827
|
-
}
|
|
31828
|
-
throw new DiscoveryError(
|
|
31829
|
-
`Missing schema for ${entityLabel} in ${dir}. Provide a schema.ts (or .js/.mts/.mjs) or schema.json file.`
|
|
31830
|
-
);
|
|
31831
|
-
}
|
|
31832
|
-
var _jiti;
|
|
31833
|
-
function getJiti() {
|
|
31834
|
-
if (!_jiti) {
|
|
31835
|
-
const base = typeof __filename !== "undefined" ? __filename : import.meta.url;
|
|
31836
|
-
const esmRequire = createRequire(base);
|
|
31837
|
-
const { createJiti: createJiti2 } = esmRequire("jiti");
|
|
31838
|
-
_jiti = createJiti2(base, { interopDefault: true });
|
|
31839
|
-
}
|
|
31840
|
-
return _jiti;
|
|
31841
|
-
}
|
|
31842
|
-
function loadSchemaModule(filePath, entityLabel) {
|
|
31843
|
-
try {
|
|
31844
|
-
const jiti2 = getJiti();
|
|
31845
|
-
const mod = jiti2(filePath);
|
|
31846
|
-
const schema = mod && typeof mod === "object" && "default" in mod ? mod.default : mod;
|
|
31847
|
-
if (!schema || typeof schema !== "object") {
|
|
31848
|
-
throw new DiscoveryError(
|
|
31849
|
-
`Schema file ${filePath} for ${entityLabel} must default-export a JSON Schema object.`
|
|
31850
|
-
);
|
|
31851
|
-
}
|
|
31852
|
-
return schema;
|
|
31853
|
-
} catch (err) {
|
|
31854
|
-
if (err instanceof DiscoveryError) throw err;
|
|
31855
|
-
throw new DiscoveryError(
|
|
31856
|
-
`Failed to load schema module ${filePath} for ${entityLabel}: ${err.message}`
|
|
31857
|
-
);
|
|
31858
|
-
}
|
|
31859
|
-
}
|
|
31860
|
-
var VIEW_EXTENSIONS = [".ts", ".js", ".mts", ".mjs"];
|
|
31861
|
-
function findViewsFile(dir) {
|
|
31862
|
-
for (const ext of VIEW_EXTENSIONS) {
|
|
31863
|
-
const candidate = join(dir, `views${ext}`);
|
|
31864
|
-
if (existsSync(candidate)) return candidate;
|
|
31865
|
-
}
|
|
31866
|
-
return void 0;
|
|
31867
|
-
}
|
|
31868
|
-
var MIGRATION_EXTENSIONS = [".ts", ".js", ".mts", ".mjs"];
|
|
31869
|
-
function findMigrationsFile(dir) {
|
|
31870
|
-
for (const ext of MIGRATION_EXTENSIONS) {
|
|
31871
|
-
const candidate = join(dir, `migrations${ext}`);
|
|
31872
|
-
if (existsSync(candidate)) return candidate;
|
|
31873
|
-
}
|
|
31874
|
-
return void 0;
|
|
31875
|
-
}
|
|
31876
|
-
function loadMigrations(filePath, entityLabel) {
|
|
31877
|
-
try {
|
|
31878
|
-
const jiti2 = getJiti();
|
|
31879
|
-
const mod = jiti2(filePath);
|
|
31880
|
-
const migrations = mod && typeof mod === "object" && "default" in mod ? mod.default : mod;
|
|
31881
|
-
if (!Array.isArray(migrations)) {
|
|
31882
|
-
throw new DiscoveryError(
|
|
31883
|
-
`Migrations file ${filePath} for ${entityLabel} must default-export an array of MigrationStep.`
|
|
31884
|
-
);
|
|
31885
|
-
}
|
|
31886
|
-
return migrations;
|
|
31887
|
-
} catch (err) {
|
|
31888
|
-
if (err instanceof DiscoveryError) throw err;
|
|
31889
|
-
throw new DiscoveryError(
|
|
31890
|
-
`Failed to load migrations ${filePath} for ${entityLabel}: ${err.message}`
|
|
31891
|
-
);
|
|
31892
|
-
}
|
|
31893
|
-
}
|
|
31894
|
-
function loadNodeEntity(dir, name) {
|
|
31895
|
-
const schema = loadSchema(dir, `node type "${name}"`);
|
|
31896
|
-
const meta3 = readJsonIfExists(join(dir, "meta.json"));
|
|
31897
|
-
const sampleData = readJsonIfExists(join(dir, "sample.json"));
|
|
31898
|
-
const viewsPath = findViewsFile(dir);
|
|
31899
|
-
const migrationsPath = findMigrationsFile(dir);
|
|
31900
|
-
const migrations = migrationsPath ? loadMigrations(migrationsPath, `node type "${name}"`) : void 0;
|
|
31901
|
-
return {
|
|
31902
|
-
kind: "node",
|
|
31903
|
-
name,
|
|
31904
|
-
schema,
|
|
31905
|
-
description: meta3?.description,
|
|
31906
|
-
titleField: meta3?.titleField,
|
|
31907
|
-
subtitleField: meta3?.subtitleField,
|
|
31908
|
-
viewDefaults: meta3?.viewDefaults,
|
|
31909
|
-
viewsPath,
|
|
31910
|
-
sampleData,
|
|
31911
|
-
allowedIn: meta3?.allowedIn,
|
|
31912
|
-
migrations,
|
|
31913
|
-
migrationWriteBack: meta3?.migrationWriteBack
|
|
31914
|
-
};
|
|
31915
|
-
}
|
|
31916
|
-
function loadEdgeEntity(dir, name) {
|
|
31917
|
-
const schema = loadSchema(dir, `edge type "${name}"`);
|
|
31918
|
-
const edgePath = join(dir, "edge.json");
|
|
31919
|
-
if (!existsSync(edgePath)) {
|
|
31920
|
-
throw new DiscoveryError(
|
|
31921
|
-
`Missing edge.json for edge type "${name}" in ${dir}. Edge entities must declare topology (from/to node types).`
|
|
31922
|
-
);
|
|
31923
|
-
}
|
|
31924
|
-
const topology = readJson(edgePath);
|
|
31925
|
-
if (!topology.from) {
|
|
31926
|
-
throw new DiscoveryError(
|
|
31927
|
-
`edge.json for "${name}" is missing required "from" field`
|
|
31928
|
-
);
|
|
31929
|
-
}
|
|
31930
|
-
if (!topology.to) {
|
|
31931
|
-
throw new DiscoveryError(
|
|
31932
|
-
`edge.json for "${name}" is missing required "to" field`
|
|
31933
|
-
);
|
|
31934
|
-
}
|
|
31935
|
-
const meta3 = readJsonIfExists(join(dir, "meta.json"));
|
|
31936
|
-
const sampleData = readJsonIfExists(join(dir, "sample.json"));
|
|
31937
|
-
const viewsPath = findViewsFile(dir);
|
|
31938
|
-
const migrationsPath = findMigrationsFile(dir);
|
|
31939
|
-
const migrations = migrationsPath ? loadMigrations(migrationsPath, `edge type "${name}"`) : void 0;
|
|
31940
|
-
return {
|
|
31941
|
-
kind: "edge",
|
|
31942
|
-
name,
|
|
31943
|
-
schema,
|
|
31944
|
-
topology,
|
|
31945
|
-
description: meta3?.description,
|
|
31946
|
-
titleField: meta3?.titleField,
|
|
31947
|
-
subtitleField: meta3?.subtitleField,
|
|
31948
|
-
viewDefaults: meta3?.viewDefaults,
|
|
31949
|
-
viewsPath,
|
|
31950
|
-
sampleData,
|
|
31951
|
-
allowedIn: meta3?.allowedIn,
|
|
31952
|
-
targetGraph: topology.targetGraph ?? meta3?.targetGraph,
|
|
31953
|
-
migrations,
|
|
31954
|
-
migrationWriteBack: meta3?.migrationWriteBack
|
|
31955
|
-
};
|
|
31956
|
-
}
|
|
31957
|
-
function getSubdirectories(dir) {
|
|
31958
|
-
if (!existsSync(dir)) return [];
|
|
31959
|
-
return readdirSync(dir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
31960
|
-
}
|
|
31961
|
-
function discoverEntities(entitiesDir) {
|
|
31962
|
-
const absDir = resolve(entitiesDir);
|
|
31963
|
-
if (!existsSync(absDir) || !statSync(absDir).isDirectory()) {
|
|
31964
|
-
throw new DiscoveryError(`Entities directory not found: ${entitiesDir}`);
|
|
31965
|
-
}
|
|
31966
|
-
const nodes = /* @__PURE__ */ new Map();
|
|
31967
|
-
const edges = /* @__PURE__ */ new Map();
|
|
31968
|
-
const warnings = [];
|
|
31969
|
-
const nodesDir = join(absDir, "nodes");
|
|
31970
|
-
for (const name of getSubdirectories(nodesDir)) {
|
|
31971
|
-
nodes.set(name, loadNodeEntity(join(nodesDir, name), name));
|
|
31972
|
-
}
|
|
31973
|
-
const edgesDir = join(absDir, "edges");
|
|
31974
|
-
for (const name of getSubdirectories(edgesDir)) {
|
|
31975
|
-
edges.set(name, loadEdgeEntity(join(edgesDir, name), name));
|
|
31976
|
-
}
|
|
31977
|
-
const nodeNames = new Set(nodes.keys());
|
|
31978
|
-
for (const [axbType, entity] of edges) {
|
|
31979
|
-
const topology = entity.topology;
|
|
31980
|
-
const fromTypes = Array.isArray(topology.from) ? topology.from : [topology.from];
|
|
31981
|
-
const toTypes = Array.isArray(topology.to) ? topology.to : [topology.to];
|
|
31982
|
-
for (const ref of [...fromTypes, ...toTypes]) {
|
|
31983
|
-
if (!nodeNames.has(ref)) {
|
|
31984
|
-
warnings.push({
|
|
31985
|
-
code: "DANGLING_TOPOLOGY_REF",
|
|
31986
|
-
message: `Edge "${axbType}" references node type "${ref}" which was not found in the nodes directory`
|
|
31987
|
-
});
|
|
31988
|
-
}
|
|
31989
|
-
}
|
|
31990
|
-
}
|
|
31991
|
-
return {
|
|
31992
|
-
result: { nodes, edges },
|
|
31993
|
-
warnings
|
|
31994
|
-
};
|
|
31995
|
-
}
|
|
32061
|
+
// src/record.ts
|
|
32062
|
+
import { FieldValue as FieldValue3 } from "@google-cloud/firestore";
|
|
32063
|
+
|
|
32064
|
+
// src/index.ts
|
|
32065
|
+
init_serialization();
|
|
31996
32066
|
|
|
31997
32067
|
// editor/server/schema-introspect.ts
|
|
31998
32068
|
function introspectRegistry(registry2, dynamicNames) {
|