@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.
@@ -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/record.ts
29675
- import { FieldValue } from "@google-cloud/firestore";
29676
- function buildNodeRecord(aType, uid, data) {
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 buildEdgeRecord(aType, aUid, axbType, bType, bUid, data) {
29690
- const now = FieldValue.serverTimestamp();
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/query.ts
29762
- function buildEdgeQueryPlan(params) {
29763
- const { aType, aUid, axbType, bType, bUid, limit, orderBy } = params;
29764
- if (aUid && axbType && bUid && !params.where?.length) {
29765
- return { strategy: "get", docId: computeEdgeDocId(aUid, axbType, bUid) };
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
- const filters = [];
29768
- if (aType) filters.push({ field: "aType", op: "==", value: aType });
29769
- if (aUid) filters.push({ field: "aUid", op: "==", value: aUid });
29770
- if (axbType) filters.push({ field: "axbType", op: "==", value: axbType });
29771
- if (bType) filters.push({ field: "bType", op: "==", value: bType });
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
- if (filters.length === 0) {
29780
- throw new InvalidQueryError("findEdges requires at least one filter parameter");
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
- const effectiveLimit = limit === void 0 ? DEFAULT_QUERY_LIMIT : limit || void 0;
29783
- return { strategy: "query", filters, options: { limit: effectiveLimit, orderBy } };
29784
- }
29785
- function buildNodeQueryPlan(params) {
29786
- const { aType, limit, orderBy } = params;
29787
- const filters = [
29788
- { field: "aType", op: "==", value: aType },
29789
- { field: "axbType", op: "==", value: NODE_RELATION }
29790
- ];
29791
- if (params.where) {
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
- const effectiveLimit = limit === void 0 ? DEFAULT_QUERY_LIMIT : limit || void 0;
29798
- return { strategy: "query", filters, options: { limit: effectiveLimit, orderBy } };
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/internal/firestore-adapter.ts
29802
- function createFirestoreAdapter(db2, collectionPath) {
29803
- const collectionRef = db2.collection(collectionPath);
29804
- return {
29805
- collectionPath,
29806
- async getDoc(docId) {
29807
- const snap = await collectionRef.doc(docId).get();
29808
- if (!snap.exists) return null;
29809
- return snap.data();
29810
- },
29811
- async setDoc(docId, data) {
29812
- await collectionRef.doc(docId).set(data);
29813
- },
29814
- async updateDoc(docId, data) {
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 (options?.limit !== void 0) {
29829
- q = q.limit(options.limit);
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
- const snap = await q.get();
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 createTransactionAdapter(db2, collectionPath, tx) {
29837
- const collectionRef = db2.collection(collectionPath);
29838
- return {
29839
- async getDoc(docId) {
29840
- const snap = await tx.get(collectionRef.doc(docId));
29841
- if (!snap.exists) return null;
29842
- return snap.data();
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
- function createBatchAdapter(db2, collectionPath) {
29870
- const collectionRef = db2.collection(collectionPath);
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
- return _Pipelines;
29896
- }
29897
- function buildFilterExpression(P, filter) {
29898
- const { field: fieldName, op, value } = filter;
29899
- switch (op) {
29900
- case "==":
29901
- return P.equal(fieldName, value);
29902
- case "!=":
29903
- return P.notEqual(fieldName, value);
29904
- case "<":
29905
- return P.lessThan(fieldName, value);
29906
- case "<=":
29907
- return P.lessThanOrEqual(fieldName, value);
29908
- case ">":
29909
- return P.greaterThan(fieldName, value);
29910
- case ">=":
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 createPipelineQueryAdapter(db2, collectionPath) {
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
- async query(filters, options) {
29927
- const P = await getPipelines();
29928
- let pipeline = db2.pipeline().collection(collectionPath);
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/transaction.ts
29950
- import { FieldValue as FieldValue3 } from "@google-cloud/firestore";
29951
-
29952
- // src/query-safety.ts
29953
- var SAFE_INDEX_PATTERNS = [
29954
- /* @__PURE__ */ new Set(["aUid", "axbType"]),
29955
- /* @__PURE__ */ new Set(["axbType", "bUid"]),
29956
- /* @__PURE__ */ new Set(["aType", "axbType"]),
29957
- /* @__PURE__ */ new Set(["axbType", "bType"])
29958
- ];
29959
- function analyzeQuerySafety(filters) {
29960
- const builtinFieldsPresent = /* @__PURE__ */ new Set();
29961
- let hasDataFilters = false;
29962
- for (const f of filters) {
29963
- if (BUILTIN_FIELDS.has(f.field)) {
29964
- builtinFieldsPresent.add(f.field);
29965
- } else {
29966
- hasDataFilters = true;
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
- const presentFields = [...builtinFieldsPresent];
29982
- if (presentFields.length === 0 && hasDataFilters) {
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 (hasDataFilters) {
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/serialization.ts
30001
- import { Timestamp, GeoPoint, FieldValue as FieldValue2 } from "@google-cloud/firestore";
30002
- var SERIALIZATION_TAG = "__firegraph_ser__";
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 serializeFirestoreTypes(data) {
30027
- return serializeValue(data);
30132
+ function tripleKeyFor(e) {
30133
+ return tripleKey(e.aType, e.axbType, e.bType);
30028
30134
  }
30029
- function serializeValue(value) {
30030
- if (value === null || value === void 0) return value;
30031
- if (typeof value !== "object") return value;
30032
- if (isTimestamp(value)) {
30033
- return { [SERIALIZATION_TAG]: "Timestamp", seconds: value.seconds, nanoseconds: value.nanoseconds };
30034
- }
30035
- if (isGeoPoint(value)) {
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 result = {};
30050
- for (const key of Object.keys(value)) {
30051
- result[key] = serializeValue(value[key]);
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
- return result;
30054
- }
30055
- function deserializeFirestoreTypes(data, db2) {
30056
- return deserializeValue(data, db2);
30057
- }
30058
- function deserializeValue(value, db2) {
30059
- if (value === null || value === void 0) return value;
30060
- if (typeof value !== "object") return value;
30061
- if (isTimestamp(value) || isGeoPoint(value) || isDocumentReference(value) || isVectorValue(value)) {
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
- if (Array.isArray(value)) {
30065
- return value.map((v) => deserializeValue(v, db2));
30171
+ for (const [key, arr] of axbBuild) {
30172
+ axbIndex.set(key, Object.freeze(arr));
30066
30173
  }
30067
- const obj = value;
30068
- if (isTaggedValue(obj)) {
30069
- const tag = obj[SERIALIZATION_TAG];
30070
- switch (tag) {
30071
- case "Timestamp":
30072
- if (typeof obj.seconds !== "number" || typeof obj.nanoseconds !== "number") return obj;
30073
- return new Timestamp(obj.seconds, obj.nanoseconds);
30074
- case "GeoPoint":
30075
- if (typeof obj.latitude !== "number" || typeof obj.longitude !== "number") return obj;
30076
- return new GeoPoint(obj.latitude, obj.longitude);
30077
- case "VectorValue":
30078
- if (!Array.isArray(obj.values)) return obj;
30079
- return FieldValue2.vector(obj.values);
30080
- case "DocumentReference":
30081
- if (typeof obj.path !== "string") return obj;
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
- if (!_docRefWarned) {
30086
- _docRefWarned = true;
30087
- console.warn(
30088
- "[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."
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
- return obj;
30092
- default:
30093
- return obj;
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
- // src/migration.ts
30104
- async function applyMigrationChain(data, currentVersion, targetVersion, migrations) {
30105
- const sorted = [...migrations].sort((a, b) => a.fromVersion - b.fromVersion);
30106
- let result = { ...data };
30107
- let version2 = currentVersion;
30108
- for (const step of sorted) {
30109
- if (step.fromVersion === version2) {
30110
- try {
30111
- result = await step.up(result);
30112
- } catch (err) {
30113
- if (err instanceof MigrationError) throw err;
30114
- throw new MigrationError(
30115
- `Migration from v${step.fromVersion} to v${step.toVersion} failed: ${err.message}`
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
- if (!result || typeof result !== "object") {
30119
- throw new MigrationError(
30120
- `Migration from v${step.fromVersion} to v${step.toVersion} returned invalid data (expected object)`
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
- version2 = step.toVersion;
30124
- }
30125
- }
30126
- if (version2 !== targetVersion) {
30127
- throw new MigrationError(
30128
- `Incomplete migration chain: reached v${version2} but target is v${targetVersion}`
30129
- );
30130
- }
30131
- return result;
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 validateMigrationChain(migrations, label) {
30134
- if (migrations.length === 0) return;
30135
- const seen = /* @__PURE__ */ new Set();
30136
- for (const step of migrations) {
30137
- if (step.toVersion <= step.fromVersion) {
30138
- throw new MigrationError(
30139
- `${label}: migration step has toVersion (${step.toVersion}) <= fromVersion (${step.fromVersion})`
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
- if (seen.has(step.fromVersion)) {
30143
- throw new MigrationError(
30144
- `${label}: duplicate migration step for fromVersion ${step.fromVersion}`
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 sorted = [...migrations].sort((a, b) => a.fromVersion - b.fromVersion);
30150
- const targetVersion = Math.max(...migrations.map((m) => m.toVersion));
30151
- let version2 = 0;
30152
- for (const step of sorted) {
30153
- if (step.fromVersion === version2) {
30154
- version2 = step.toVersion;
30155
- } else if (step.fromVersion > version2) {
30156
- throw new MigrationError(
30157
- `${label}: migration chain has a gap \u2014 no step covers v${version2} \u2192 v${step.fromVersion}`
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
- if (version2 !== targetVersion) {
30162
- throw new MigrationError(
30163
- `${label}: migration chain does not reach v${targetVersion} (stuck at v${version2})`
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
- const currentVersion = record2.v ?? 0;
30173
- if (currentVersion >= entry.schemaVersion) {
30174
- return { record: record2, migrated: false, writeBack: "off" };
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
- record: { ...record2, data: migratedData, v: entry.schemaVersion },
30185
- migrated: true,
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(adapter, registry2, scanProtection = "error", scopePath = "", globalWriteBack = "off", db2) {
30198
- this.adapter = adapter;
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.adapter.getDoc(docId);
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
- const update = {
30212
- data: deserializeFirestoreTypes(result.record.data, this.db),
30213
- updatedAt: FieldValue3.serverTimestamp()
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.adapter.getDoc(docId);
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
- const update = {
30229
- data: deserializeFirestoreTypes(result.record.data, this.db),
30230
- updatedAt: FieldValue3.serverTimestamp()
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.adapter.getDoc(docId);
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.adapter.getDoc(plan.docId);
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.adapter.query(plan.filters, plan.options);
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.adapter.getDoc(plan.docId);
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.adapter.query(plan.filters, plan.options);
31070
+ records = await this.backend.query(plan.filters, plan.options);
30274
31071
  }
30275
31072
  return this.applyMigrations(records);
30276
31073
  }
30277
- async applyMigrations(records) {
30278
- if (!this.registry || records.length === 0) return records;
30279
- const results = await migrateRecords(records, this.registry, this.globalWriteBack);
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
- if (this.registry) {
30353
- this.registry.validate(aType, NODE_RELATION, aType, data, this.scopePath);
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 = buildNodeRecord(aType, uid, data);
30357
- if (this.registry) {
30358
- const entry = this.registry.lookup(aType, NODE_RELATION, aType);
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
- this.adapter.setDoc(docId, record2);
31091
+ await backend.setDoc(docId, record2);
30364
31092
  }
30365
31093
  async putEdge(aType, aUid, axbType, bType, bUid, data) {
30366
- if (this.registry) {
30367
- this.registry.validate(aType, axbType, bType, data, this.scopePath);
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 = buildEdgeRecord(aType, aUid, axbType, bType, bUid, data);
30371
- if (this.registry) {
30372
- const entry = this.registry.lookup(aType, axbType, bType);
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
- this.adapter.setDoc(docId, record2);
31107
+ await backend.setDoc(docId, record2);
30378
31108
  }
30379
31109
  async updateNode(uid, data) {
30380
31110
  const docId = computeNodeDocId(uid);
30381
- const update = {
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.adapter.deleteDoc(docId);
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.adapter.deleteDoc(docId);
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
- return { deleted: totalDeleted, errors: allErrors };
30496
- }
30497
- async function removeNodeCascade(db2, collectionPath, reader, uid, options) {
30498
- const [outgoingRaw, incomingRaw] = await Promise.all([
30499
- reader.findEdges({ aUid: uid, allowCollectionScan: true, limit: 0 }),
30500
- reader.findEdges({ bUid: uid, allowCollectionScan: true, limit: 0 })
30501
- ]);
30502
- const outgoing = outgoingRaw.filter((e) => e.axbType !== NODE_RELATION);
30503
- const incoming = incomingRaw.filter((e) => e.axbType !== NODE_RELATION);
30504
- const edgeDocIdSet = /* @__PURE__ */ new Set();
30505
- const allEdges = [];
30506
- for (const edge of [...outgoing, ...incoming]) {
30507
- const docId = computeEdgeDocId(edge.aUid, edge.axbType, edge.bUid);
30508
- if (!edgeDocIdSet.has(docId)) {
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
- const shouldDeleteSubcollections = options?.deleteSubcollections !== false;
30514
- const nodeDocId = computeNodeDocId(uid);
30515
- let subcollectionResult = { deleted: 0, errors: [] };
30516
- if (shouldDeleteSubcollections) {
30517
- subcollectionResult = await deleteSubcollectionsRecursive(
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
- const edgeDocIds = allEdges.map((e) => computeEdgeDocId(e.aUid, e.axbType, e.bUid));
30525
- const allDocIds = [...edgeDocIds, nodeDocId];
30526
- const batchSize = Math.min(options?.batchSize ?? MAX_BATCH_SIZE, MAX_BATCH_SIZE);
30527
- const result = await bulkDeleteDocIds(db2, collectionPath, allDocIds, {
30528
- ...options,
30529
- batchSize
30530
- });
30531
- const totalChunks = Math.ceil(allDocIds.length / batchSize);
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
- function jsonSchemaToFieldMeta(schema) {
30566
- if (!schema || schema.type !== "object" || !schema.properties) return [];
30567
- const requiredSet = new Set(
30568
- Array.isArray(schema.required) ? schema.required : []
30569
- );
30570
- return Object.entries(schema.properties).map(
30571
- ([name, prop]) => propertyToFieldMeta(name, prop, requiredSet.has(name))
30572
- );
30573
- }
30574
- function propertyToFieldMeta(name, prop, required2) {
30575
- if (!prop) return { name, type: "unknown", required: required2 };
30576
- if (Array.isArray(prop.enum)) {
30577
- return {
30578
- name,
30579
- type: "enum",
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
- if (Array.isArray(prop.oneOf) || Array.isArray(prop.anyOf)) {
30586
- const variants = prop.oneOf ?? prop.anyOf;
30587
- const nonNull = variants.filter((v) => v.type !== "null");
30588
- if (nonNull.length === 1) {
30589
- return propertyToFieldMeta(name, nonNull[0], false);
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
- return { name, type: "unknown", required: required2, description: prop.description };
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
- const type = prop.type;
30594
- if (type === "string") {
30595
- return {
30596
- name,
30597
- type: "string",
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
- if (type === "number" || type === "integer") {
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
- if (type === "boolean") {
30617
- return { name, type: "boolean", required: required2, description: prop.description };
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
- if (type === "array") {
30620
- const itemMeta = prop.items ? propertyToFieldMeta("item", prop.items, true) : void 0;
30621
- return {
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
- type: "array",
30624
- required: required2,
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
- if (type === "object") {
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
- name,
30632
- type: "object",
30633
- required: required2,
30634
- fields: jsonSchemaToFieldMeta(prop),
30635
- description: prop.description
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
- return { name, type: "unknown", required: required2, description: prop.description };
30639
- }
31340
+ };
30640
31341
 
30641
- // src/scope.ts
30642
- function matchScope(scopePath, pattern) {
30643
- if (pattern === "root") return scopePath === "";
30644
- if (pattern === "**") return true;
30645
- const pathSegments = scopePath === "" ? [] : scopePath.split("/");
30646
- const patternSegments = pattern.split("/");
30647
- return matchSegments(pathSegments, 0, patternSegments, 0);
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
- if (path5[pi] === seg) {
30669
- return matchSegments(path5, pi + 1, pattern, qi + 1);
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
- // src/registry.ts
30675
- function tripleKey(aType, axbType, bType) {
30676
- return `${aType}:${axbType}:${bType}`;
31361
+ function readJsonIfExists(filePath) {
31362
+ if (!existsSync(filePath)) return void 0;
31363
+ return readJson(filePath);
30677
31364
  }
30678
- function tripleKeyFor(e) {
30679
- return tripleKey(e.aType, e.axbType, e.bType);
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
- function createRegistry(input) {
30682
- const map2 = /* @__PURE__ */ new Map();
30683
- let entries;
30684
- if (Array.isArray(input)) {
30685
- entries = input;
30686
- } else {
30687
- entries = discoveryToEntries(input);
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
- const entryList = Object.freeze([...entries]);
30690
- for (const entry of entries) {
30691
- if (entry.targetGraph && entry.targetGraph.includes("/")) {
30692
- throw new ValidationError(
30693
- `Entry (${entry.aType}) -[${entry.axbType}]-> (${entry.bType}) has invalid targetGraph "${entry.targetGraph}" \u2014 must be a single segment (no "/")`
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
- if (entry.migrations?.length) {
30697
- const label = `Entry (${entry.aType}) -[${entry.axbType}]-> (${entry.bType})`;
30698
- validateMigrationChain(entry.migrations, label);
30699
- entry.schemaVersion = Math.max(...entry.migrations.map((m) => m.toVersion));
30700
- } else {
30701
- entry.schemaVersion = void 0;
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
- const axbIndex = /* @__PURE__ */ new Map();
30708
- const axbBuild = /* @__PURE__ */ new Map();
30709
- for (const entry of entries) {
30710
- const existing = axbBuild.get(entry.axbType);
30711
- if (existing) {
30712
- existing.push(entry);
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
- for (const [key, arr] of axbBuild) {
30718
- axbIndex.set(key, Object.freeze(arr));
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
- lookup(aType, axbType, bType) {
30722
- return map2.get(tripleKey(aType, axbType, bType))?.entry;
30723
- },
30724
- lookupByAxbType(axbType) {
30725
- return axbIndex.get(axbType) ?? [];
30726
- },
30727
- validate(aType, axbType, bType, data, scopePath) {
30728
- const rec = map2.get(tripleKey(aType, axbType, bType));
30729
- if (!rec) {
30730
- throw new RegistryViolationError(aType, axbType, bType);
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 createMergedRegistry(base, extension) {
30755
- const baseKeys = new Set(base.entries().map(tripleKeyFor));
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
- lookup(aType, axbType, bType) {
30758
- return base.lookup(aType, axbType, bType) ?? extension.lookup(aType, axbType, bType);
30759
- },
30760
- lookupByAxbType(axbType) {
30761
- const baseResults = base.lookupByAxbType(axbType);
30762
- const extResults = extension.lookupByAxbType(axbType);
30763
- if (extResults.length === 0) return baseResults;
30764
- if (baseResults.length === 0) return extResults;
30765
- const seen = new Set(baseResults.map(tripleKeyFor));
30766
- const merged = [...baseResults];
30767
- for (const entry of extResults) {
30768
- if (!seen.has(tripleKeyFor(entry))) {
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 discoveryToEntries(discovery) {
30794
- const entries = [];
30795
- for (const [name, entity] of discovery.nodes) {
30796
- entries.push({
30797
- aType: name,
30798
- axbType: NODE_RELATION,
30799
- bType: name,
30800
- jsonSchema: entity.schema,
30801
- description: entity.description,
30802
- titleField: entity.titleField,
30803
- subtitleField: entity.subtitleField,
30804
- allowedIn: entity.allowedIn,
30805
- migrations: entity.migrations,
30806
- migrationWriteBack: entity.migrationWriteBack
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
- for (const [axbType, entity] of discovery.edges) {
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 resolvedTargetGraph = entity.targetGraph ?? topology.targetGraph;
30815
- if (resolvedTargetGraph && resolvedTargetGraph.includes("/")) {
30816
- throw new ValidationError(
30817
- `Edge "${axbType}" has invalid targetGraph "${resolvedTargetGraph}" \u2014 must be a single segment (no "/")`
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 entries;
31540
+ return {
31541
+ result: { nodes, edges },
31542
+ warnings
31543
+ };
30840
31544
  }
30841
31545
 
30842
- // src/sandbox.ts
30843
- import { Worker } from "node:worker_threads";
30844
- import { createHash as createHash2 } from "node:crypto";
30845
- var _worker = null;
30846
- var _requestId = 0;
30847
- var _pending = /* @__PURE__ */ new Map();
30848
- var WORKER_SOURCE = [
30849
- `'use strict';`,
30850
- `var _wt = require('node:worker_threads');`,
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
- var compiledCache = /* @__PURE__ */ new WeakMap();
31004
- function getExecutorCache(executor) {
31005
- let cache = compiledCache.get(executor);
31006
- if (!cache) {
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 cache;
31011
- }
31012
- function hashSource(source) {
31013
- return createHash2("sha256").update(source).digest("hex");
31561
+ return chunks;
31014
31562
  }
31015
- function defaultExecutor(source) {
31016
- ensureWorker();
31017
- return (data) => {
31018
- const jsonData = JSON.stringify(serializeFirestoreTypes(data));
31019
- return sendToWorker({ type: "execute", source, jsonData }).then(
31020
- (response) => {
31021
- if (response.jsonResult === void 0 || response.jsonResult === null) {
31022
- throw new MigrationError("Migration returned a non-JSON-serializable value");
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
- try {
31025
- return deserializeFirestoreTypes(JSON.parse(response.jsonResult));
31026
- } catch {
31027
- throw new MigrationError("Migration returned a non-JSON-serializable value");
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 precompileSource(source, executor) {
31034
- if (executor && executor !== defaultExecutor) {
31035
- try {
31036
- executor(source);
31037
- } catch (err) {
31038
- if (err instanceof MigrationError) throw err;
31039
- throw new MigrationError(
31040
- `Failed to compile migration source: ${err.message}`
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
- await sendToWorker({ type: "compile", source });
31642
+ return { deleted: totalDeleted, errors: allErrors };
31046
31643
  }
31047
- function compileMigrationFn(source, executor = defaultExecutor) {
31048
- const cache = getExecutorCache(executor);
31049
- const key = hashSource(source);
31050
- const cached2 = cache.get(key);
31051
- if (cached2) return cached2;
31052
- try {
31053
- const fn = executor(source);
31054
- cache.set(key, fn);
31055
- return fn;
31056
- } catch (err) {
31057
- if (err instanceof MigrationError) throw err;
31058
- throw new MigrationError(
31059
- `Failed to compile migration source: ${err.message}`
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
- function compileMigrations(stored, executor) {
31064
- return stored.map((step) => ({
31065
- fromVersion: step.fromVersion,
31066
- toVersion: step.toVersion,
31067
- up: compileMigrationFn(step.up, executor)
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/dynamic-registry.ts
31072
- var META_NODE_TYPE = "nodeType";
31073
- var META_EDGE_TYPE = "edgeType";
31074
- var STORED_MIGRATION_STEP_SCHEMA = {
31075
- type: "object",
31076
- required: ["fromVersion", "toVersion", "up"],
31077
- properties: {
31078
- fromVersion: { type: "integer", minimum: 0 },
31079
- toVersion: { type: "integer", minimum: 1 },
31080
- up: { type: "string", minLength: 1 }
31081
- },
31082
- additionalProperties: false
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
- to: {
31114
- oneOf: [
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
- jsonSchema: { type: "object" },
31120
- inverseLabel: { type: "string" },
31121
- description: { type: "string" },
31122
- titleField: { type: "string" },
31123
- subtitleField: { type: "string" },
31124
- viewTemplate: { type: "string" },
31125
- viewCss: { type: "string" },
31126
- allowedIn: { type: "array", items: { type: "string", minLength: 1 } },
31127
- targetGraph: { type: "string", minLength: 1, pattern: "^[^/]+$" },
31128
- schemaVersion: { type: "integer", minimum: 0 },
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
- // src/client.ts
31224
- var _standardModeWarned = false;
31225
- var RESERVED_TYPE_NAMES = /* @__PURE__ */ new Set([META_NODE_TYPE, META_EDGE_TYPE]);
31226
- var GraphClientImpl = class _GraphClientImpl {
31227
- constructor(db2, collectionPath, options, scopePath = "") {
31228
- this.db = db2;
31229
- this.scopePath = scopePath;
31230
- this.adapter = createFirestoreAdapter(db2, collectionPath);
31231
- this.globalWriteBack = options?.migrationWriteBack ?? "off";
31232
- this.migrationSandbox = options?.migrationSandbox;
31233
- if (options?.registryMode) {
31234
- this.dynamicConfig = options.registryMode;
31235
- this.bootstrapRegistry = createBootstrapRegistry();
31236
- if (options.registry) {
31237
- this.staticRegistry = options.registry;
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
- const metaCollectionPath = options.registryMode.collection;
31240
- if (metaCollectionPath && metaCollectionPath !== collectionPath) {
31241
- this.metaAdapter = createFirestoreAdapter(db2, metaCollectionPath);
31751
+ if (options?.orderBy) {
31752
+ q = q.orderBy(options.orderBy.field, options.orderBy.direction ?? "asc");
31242
31753
  }
31243
- } else {
31244
- this.staticRegistry = options?.registry;
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
- adapter;
31271
- pipelineAdapter;
31272
- queryMode;
31273
- scanProtection;
31274
- // Static mode
31275
- staticRegistry;
31276
- // Dynamic mode
31277
- dynamicConfig;
31278
- bootstrapRegistry;
31279
- dynamicRegistry;
31280
- metaAdapter;
31281
- metaPipelineAdapter;
31282
- // Subgraph scope tracking
31283
- scopePath;
31284
- // Migration settings
31285
- globalWriteBack;
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
- console.warn(`[firegraph] Query safety warning: ${result.reason}`);
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
- // Migration helpers
31357
- // ---------------------------------------------------------------------------
31358
- /**
31359
- * Apply migration to a single record. Returns the (possibly migrated)
31360
- * record and triggers write-back if applicable.
31361
- */
31362
- async applyMigration(record2, docId) {
31363
- const registry2 = this.getCombinedRegistry();
31364
- if (!registry2) return record2;
31365
- const result = await migrateRecord(record2, registry2, this.globalWriteBack);
31366
- if (result.migrated) {
31367
- this.handleWriteBack(result, docId);
31368
- }
31369
- return result.record;
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
- * Apply migrations to an array of records. Returns all records
31373
- * (migrated where applicable) and triggers write-backs.
31374
- */
31375
- async applyMigrations(records) {
31376
- const registry2 = this.getCombinedRegistry();
31377
- if (!registry2 || records.length === 0) return records;
31378
- const results = await migrateRecords(records, registry2, this.globalWriteBack);
31379
- for (const result of results) {
31380
- if (result.migrated) {
31381
- const docId = result.record.axbType === NODE_RELATION ? computeNodeDocId(result.record.aUid) : computeEdgeDocId(result.record.aUid, result.record.axbType, result.record.bUid);
31382
- this.handleWriteBack(result, docId);
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
- * Handle write-back for a migrated record based on the resolved mode.
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
- // GraphReader
31423
- // ---------------------------------------------------------------------------
31424
- async getNode(uid) {
31425
- const docId = computeNodeDocId(uid);
31426
- const record2 = await this.adapter.getDoc(docId);
31427
- if (!record2) return null;
31428
- return this.applyMigration(record2, docId);
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
- async getEdge(aUid, axbType, bUid) {
31431
- const docId = computeEdgeDocId(aUid, axbType, bUid);
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
- async edgeExists(aUid, axbType, bUid) {
31437
- const docId = computeEdgeDocId(aUid, axbType, bUid);
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 findEdges(params) {
31442
- const plan = buildEdgeQueryPlan(params);
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 findNodes(params) {
31454
- const plan = buildNodeQueryPlan(params);
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
- // GraphWriter
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
- async putEdge(aType, aUid, axbType, bType, bUid, data) {
31485
- const registry2 = this.getRegistryForType(aType);
31486
- if (registry2) {
31487
- registry2.validate(aType, axbType, bType, data, this.scopePath);
31488
- }
31489
- const adapter = this.getAdapterForType(aType);
31490
- const docId = computeEdgeDocId(aUid, axbType, bUid);
31491
- const record2 = buildEdgeRecord(aType, aUid, axbType, bType, bUid, data);
31492
- if (registry2) {
31493
- const entry = registry2.lookup(aType, axbType, bType);
31494
- if (entry?.schemaVersion && entry.schemaVersion > 0) {
31495
- record2.v = entry.schemaVersion;
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
- async updateNode(uid, data) {
31501
- const docId = computeNodeDocId(uid);
31502
- const update = {
31503
- updatedAt: FieldValue5.serverTimestamp()
31504
- };
31505
- for (const [k, v] of Object.entries(data)) {
31506
- update[`data.${k}`] = v;
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
- await this.adapter.updateDoc(docId, update);
31937
+ return this.adapter.query(filters, options);
31509
31938
  }
31510
- async removeNode(uid) {
31511
- const docId = computeNodeDocId(uid);
31512
- await this.adapter.deleteDoc(docId);
31939
+ // --- Writes ---
31940
+ setDoc(docId, record2) {
31941
+ return this.adapter.setDoc(docId, stampWritableRecord(record2));
31513
31942
  }
31514
- async removeEdge(aUid, axbType, bUid) {
31515
- const docId = computeEdgeDocId(aUid, axbType, bUid);
31516
- await this.adapter.deleteDoc(docId);
31943
+ updateDoc(docId, update) {
31944
+ return this.adapter.updateDoc(docId, buildFirestoreUpdate(update, this.db));
31517
31945
  }
31518
- // ---------------------------------------------------------------------------
31519
- // Transactions & Batches
31520
- // ---------------------------------------------------------------------------
31521
- async runTransaction(fn) {
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 adapter = createTransactionAdapter(
31524
- this.db,
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
- batch() {
31533
- const adapter = createBatchAdapter(this.db, this.adapter.collectionPath);
31534
- return new GraphBatchImpl(adapter, this.getCombinedRegistry(), this.scopePath);
31956
+ createBatch() {
31957
+ const batchAdapter = createBatchAdapter(this.db, this.collectionPath);
31958
+ return new FirestoreBatchBackend(batchAdapter, this.db);
31535
31959
  }
31536
- // ---------------------------------------------------------------------------
31537
- // Subgraph
31538
- // ---------------------------------------------------------------------------
31539
- subgraph(parentNodeUid, name = "graph") {
31540
- if (!parentNodeUid || parentNodeUid.includes("/")) {
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
- // Collection group query
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.adapter.collectionPath.split("/").pop();
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
- const records = snap.docs.map((doc) => doc.data());
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
- * Serialize migration steps for storage in Firestore.
31698
- * Function objects are converted via `.toString()`; strings are stored as-is.
31699
- * Each migration is validated at define-time by pre-compiling in the sandbox.
31700
- */
31701
- async serializeMigrations(migrations) {
31702
- const result = migrations.map((m) => {
31703
- const source = typeof m.up === "function" ? m.up.toString() : m.up;
31704
- return { fromVersion: m.fromVersion, toVersion: m.toVersion, up: source };
31705
- });
31706
- await Promise.all(
31707
- result.map((m) => precompileSource(m.up, this.migrationSandbox))
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
- * Create a GraphReader for the meta-collection.
31713
- * If meta-collection is the same as main collection, returns `this`.
31714
- * If separate, creates a lightweight reader wrapping the meta adapter.
31715
- */
31716
- createMetaReader() {
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/discover.ts
31794
- import { readFileSync, readdirSync, existsSync, statSync } from "node:fs";
31795
- import { createRequire } from "node:module";
31796
- import { join, resolve } from "node:path";
31797
- var DiscoveryError = class extends FiregraphError {
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) {