@typicalday/firegraph 0.7.1 → 0.9.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.
Files changed (63) hide show
  1. package/dist/backend.cjs +222 -0
  2. package/dist/backend.cjs.map +1 -0
  3. package/dist/backend.d.cts +121 -0
  4. package/dist/backend.d.ts +121 -0
  5. package/dist/backend.js +136 -0
  6. package/dist/backend.js.map +1 -0
  7. package/dist/chunk-5753Y42M.js +118 -0
  8. package/dist/chunk-5753Y42M.js.map +1 -0
  9. package/dist/chunk-EVUM6ORB.js +1575 -0
  10. package/dist/chunk-EVUM6ORB.js.map +1 -0
  11. package/dist/chunk-GLOVWKQH.js +94 -0
  12. package/dist/chunk-GLOVWKQH.js.map +1 -0
  13. package/dist/{chunk-KFA7G37W.js → chunk-SU4FNLC3.js} +32 -30
  14. package/dist/chunk-SU4FNLC3.js.map +1 -0
  15. package/dist/chunk-SZ6W4VAS.js +701 -0
  16. package/dist/chunk-SZ6W4VAS.js.map +1 -0
  17. package/dist/chunk-TYYPRVIE.js +57 -0
  18. package/dist/chunk-TYYPRVIE.js.map +1 -0
  19. package/dist/codegen/index.d.cts +25 -1
  20. package/dist/codegen/index.d.ts +25 -1
  21. package/dist/d1.cjs +2421 -0
  22. package/dist/d1.cjs.map +1 -0
  23. package/dist/d1.d.cts +54 -0
  24. package/dist/d1.d.ts +54 -0
  25. package/dist/d1.js +76 -0
  26. package/dist/d1.js.map +1 -0
  27. package/dist/do-sqlite.cjs +2424 -0
  28. package/dist/do-sqlite.cjs.map +1 -0
  29. package/dist/do-sqlite.d.cts +41 -0
  30. package/dist/do-sqlite.d.ts +41 -0
  31. package/dist/do-sqlite.js +79 -0
  32. package/dist/do-sqlite.js.map +1 -0
  33. package/dist/editor/client/assets/index-Bq2bfzeY.js +411 -0
  34. package/dist/editor/client/index.html +1 -1
  35. package/dist/editor/server/index.mjs +6524 -6355
  36. package/dist/index.cjs +2881 -2714
  37. package/dist/index.cjs.map +1 -1
  38. package/dist/index.d.cts +259 -275
  39. package/dist/index.d.ts +259 -275
  40. package/dist/index.js +728 -2304
  41. package/dist/index.js.map +1 -1
  42. package/dist/query-client/index.cjs +30 -28
  43. package/dist/query-client/index.cjs.map +1 -1
  44. package/dist/query-client/index.d.cts +2 -2
  45. package/dist/query-client/index.d.ts +2 -2
  46. package/dist/query-client/index.js +1 -1
  47. package/dist/react.cjs +0 -1
  48. package/dist/react.cjs.map +1 -1
  49. package/dist/react.js +0 -1
  50. package/dist/react.js.map +1 -1
  51. package/dist/scope-path-BtajqNK5.d.ts +234 -0
  52. package/dist/scope-path-D2mNENJ-.d.cts +234 -0
  53. package/dist/serialization-ZZ7RSDRX.js +13 -0
  54. package/dist/serialization-ZZ7RSDRX.js.map +1 -0
  55. package/dist/svelte.cjs +0 -2
  56. package/dist/svelte.cjs.map +1 -1
  57. package/dist/svelte.js +0 -2
  58. package/dist/svelte.js.map +1 -1
  59. package/dist/{index-B9aodfYD.d.ts → types-DfWVTsMn.d.cts} +28 -26
  60. package/dist/{index-B9aodfYD.d.cts → types-DfWVTsMn.d.ts} +28 -26
  61. package/package.json +35 -1
  62. package/dist/chunk-KFA7G37W.js.map +0 -1
  63. package/dist/editor/client/assets/index-tyFcX6qG.js +0 -411
package/dist/index.js CHANGED
@@ -1,838 +1,357 @@
1
+ import {
2
+ SERIALIZATION_TAG,
3
+ deserializeFirestoreTypes,
4
+ isTaggedValue,
5
+ serializeFirestoreTypes
6
+ } from "./chunk-5753Y42M.js";
7
+ import {
8
+ appendStorageScope,
9
+ isAncestorScopeUid,
10
+ parseStorageScope,
11
+ resolveAncestorScope
12
+ } from "./chunk-TYYPRVIE.js";
13
+ import {
14
+ BOOTSTRAP_ENTRIES,
15
+ DEFAULT_QUERY_LIMIT,
16
+ EDGE_TYPE_SCHEMA,
17
+ GraphClientImpl,
18
+ META_EDGE_TYPE,
19
+ META_NODE_TYPE,
20
+ NODE_RELATION,
21
+ NODE_TYPE_SCHEMA,
22
+ analyzeQuerySafety,
23
+ applyMigrationChain,
24
+ buildEdgeQueryPlan,
25
+ buildNodeQueryPlan,
26
+ compileMigrationFn,
27
+ compileMigrations,
28
+ compileSchema,
29
+ computeEdgeDocId,
30
+ computeNodeDocId,
31
+ createBootstrapRegistry,
32
+ createGraphClientFromBackend,
33
+ createMergedRegistry,
34
+ createRegistry,
35
+ createRegistryFromGraph,
36
+ defaultExecutor,
37
+ destroySandboxWorker,
38
+ generateDeterministicUid,
39
+ jsonSchemaToFieldMeta,
40
+ matchScope,
41
+ matchScopeAny,
42
+ migrateRecord,
43
+ migrateRecords,
44
+ precompileSource,
45
+ validateMigrationChain
46
+ } from "./chunk-EVUM6ORB.js";
47
+ import {
48
+ CrossBackendTransactionError,
49
+ DynamicRegistryError,
50
+ EdgeNotFoundError,
51
+ FiregraphError,
52
+ InvalidQueryError,
53
+ MigrationError,
54
+ NodeNotFoundError,
55
+ QuerySafetyError,
56
+ RegistryScopeError,
57
+ RegistryViolationError,
58
+ TraversalError,
59
+ ValidationError
60
+ } from "./chunk-GLOVWKQH.js";
1
61
  import {
2
62
  generateTypes
3
63
  } from "./chunk-YLGXLEUE.js";
4
64
  import {
5
65
  QueryClient,
6
66
  QueryClientError
7
- } from "./chunk-KFA7G37W.js";
8
-
9
- // src/client.ts
10
- import { FieldValue as FieldValue5 } from "@google-cloud/firestore";
11
-
12
- // src/docid.ts
13
- import { createHash } from "crypto";
67
+ } from "./chunk-SU4FNLC3.js";
14
68
 
15
- // src/internal/constants.ts
16
- var NODE_RELATION = "is";
17
- var DEFAULT_QUERY_LIMIT = 500;
18
- var BUILTIN_FIELDS = /* @__PURE__ */ new Set([
19
- "aType",
20
- "aUid",
21
- "axbType",
22
- "bType",
23
- "bUid",
24
- "createdAt",
25
- "updatedAt"
26
- ]);
27
- var SHARD_SEPARATOR = ":";
28
-
29
- // src/docid.ts
30
- function computeNodeDocId(uid) {
31
- return uid;
69
+ // src/config.ts
70
+ function defineConfig(config) {
71
+ return config;
32
72
  }
33
- function computeEdgeDocId(aUid, axbType, bUid) {
34
- const composite = `${aUid}${SHARD_SEPARATOR}${axbType}${SHARD_SEPARATOR}${bUid}`;
35
- const hash = createHash("sha256").update(composite).digest("hex");
36
- const shard = hash[0];
37
- return `${shard}${SHARD_SEPARATOR}${aUid}${SHARD_SEPARATOR}${axbType}${SHARD_SEPARATOR}${bUid}`;
73
+ function resolveView(resolverConfig, availableViewNames, context) {
74
+ if (!resolverConfig) return "json";
75
+ const available = new Set(availableViewNames);
76
+ if (context) {
77
+ const contextDefault = resolverConfig[context];
78
+ if (contextDefault && available.has(contextDefault)) {
79
+ return contextDefault;
80
+ }
81
+ }
82
+ if (resolverConfig.default && available.has(resolverConfig.default)) {
83
+ return resolverConfig.default;
84
+ }
85
+ return "json";
38
86
  }
39
87
 
40
- // src/record.ts
41
- import { FieldValue } from "@google-cloud/firestore";
42
- function buildNodeRecord(aType, uid, data) {
43
- const now = FieldValue.serverTimestamp();
44
- return {
45
- aType,
46
- aUid: uid,
47
- axbType: NODE_RELATION,
48
- bType: aType,
49
- bUid: uid,
50
- data,
51
- createdAt: now,
52
- updatedAt: now
53
- };
88
+ // src/cross-graph.ts
89
+ function resolveAncestorCollection(collectionPath, uid) {
90
+ const segments = collectionPath.split("/");
91
+ for (let i = 1; i < segments.length; i += 2) {
92
+ if (segments[i] === uid) {
93
+ return segments.slice(0, i).join("/");
94
+ }
95
+ }
96
+ return null;
54
97
  }
55
- function buildEdgeRecord(aType, aUid, axbType, bType, bUid, data) {
56
- const now = FieldValue.serverTimestamp();
57
- return {
58
- aType,
59
- aUid,
60
- axbType,
61
- bType,
62
- bUid,
63
- data,
64
- createdAt: now,
65
- updatedAt: now
66
- };
98
+ function isAncestorUid(collectionPath, uid) {
99
+ return resolveAncestorCollection(collectionPath, uid) !== null;
67
100
  }
68
101
 
69
- // src/errors.ts
70
- var FiregraphError = class extends Error {
71
- constructor(message, code) {
72
- super(message);
73
- this.code = code;
74
- this.name = "FiregraphError";
75
- }
76
- };
77
- var NodeNotFoundError = class extends FiregraphError {
78
- constructor(uid) {
79
- super(`Node not found: ${uid}`, "NODE_NOT_FOUND");
80
- this.name = "NodeNotFoundError";
81
- }
82
- };
83
- var EdgeNotFoundError = class extends FiregraphError {
84
- constructor(aUid, axbType, bUid) {
85
- super(`Edge not found: ${aUid} -[${axbType}]-> ${bUid}`, "EDGE_NOT_FOUND");
86
- this.name = "EdgeNotFoundError";
87
- }
88
- };
89
- var ValidationError = class extends FiregraphError {
90
- constructor(message, details) {
91
- super(message, "VALIDATION_ERROR");
92
- this.details = details;
93
- this.name = "ValidationError";
94
- }
95
- };
96
- var RegistryViolationError = class extends FiregraphError {
97
- constructor(aType, axbType, bType) {
98
- super(
99
- `Unregistered triple: (${aType}) -[${axbType}]-> (${bType})`,
100
- "REGISTRY_VIOLATION"
101
- );
102
- this.name = "RegistryViolationError";
103
- }
104
- };
105
- var InvalidQueryError = class extends FiregraphError {
106
- constructor(message) {
107
- super(message, "INVALID_QUERY");
108
- this.name = "InvalidQueryError";
109
- }
110
- };
111
- var TraversalError = class extends FiregraphError {
102
+ // src/discover.ts
103
+ import { existsSync, readdirSync, readFileSync, statSync } from "fs";
104
+ import { createRequire } from "module";
105
+ import { join, resolve } from "path";
106
+ var DiscoveryError = class extends FiregraphError {
112
107
  constructor(message) {
113
- super(message, "TRAVERSAL_ERROR");
114
- this.name = "TraversalError";
108
+ super(message, "DISCOVERY_ERROR");
109
+ this.name = "DiscoveryError";
115
110
  }
116
111
  };
117
- var DynamicRegistryError = class extends FiregraphError {
118
- constructor(message) {
119
- super(message, "DYNAMIC_REGISTRY_ERROR");
120
- this.name = "DynamicRegistryError";
112
+ function readJson(filePath) {
113
+ try {
114
+ const raw = readFileSync(filePath, "utf-8");
115
+ return JSON.parse(raw);
116
+ } catch (err) {
117
+ const msg = err instanceof SyntaxError ? `Invalid JSON in ${filePath}: ${err.message}` : `Cannot read ${filePath}: ${err.message}`;
118
+ throw new DiscoveryError(msg);
121
119
  }
122
- };
123
- var QuerySafetyError = class extends FiregraphError {
124
- constructor(message) {
125
- super(message, "QUERY_SAFETY");
126
- this.name = "QuerySafetyError";
120
+ }
121
+ function readJsonIfExists(filePath) {
122
+ if (!existsSync(filePath)) return void 0;
123
+ return readJson(filePath);
124
+ }
125
+ var SCHEMA_SCRIPT_EXTENSIONS = [".ts", ".js", ".mts", ".mjs"];
126
+ function loadSchema(dir, entityLabel) {
127
+ for (const ext of SCHEMA_SCRIPT_EXTENSIONS) {
128
+ const candidate = join(dir, `schema${ext}`);
129
+ if (existsSync(candidate)) {
130
+ return loadSchemaModule(candidate, entityLabel);
131
+ }
127
132
  }
128
- };
129
- var RegistryScopeError = class extends FiregraphError {
130
- constructor(aType, axbType, bType, scopePath, allowedIn) {
131
- super(
132
- `Type (${aType}) -[${axbType}]-> (${bType}) is not allowed at scope "${scopePath || "root"}". Allowed in: [${allowedIn.join(", ")}]`,
133
- "REGISTRY_SCOPE"
134
- );
135
- this.name = "RegistryScopeError";
133
+ const jsonPath = join(dir, "schema.json");
134
+ if (existsSync(jsonPath)) {
135
+ return readJson(jsonPath);
136
136
  }
137
- };
138
- var MigrationError = class extends FiregraphError {
139
- constructor(message) {
140
- super(message, "MIGRATION_ERROR");
141
- this.name = "MigrationError";
137
+ throw new DiscoveryError(
138
+ `Missing schema for ${entityLabel} in ${dir}. Provide a schema.ts (or .js/.mts/.mjs) or schema.json file.`
139
+ );
140
+ }
141
+ var _jiti;
142
+ function getJiti() {
143
+ if (!_jiti) {
144
+ const base = typeof __filename !== "undefined" ? __filename : import.meta.url;
145
+ const esmRequire = createRequire(base);
146
+ const { createJiti } = esmRequire("jiti");
147
+ _jiti = createJiti(base, { interopDefault: true });
142
148
  }
143
- };
144
-
145
- // src/query.ts
146
- function buildEdgeQueryPlan(params) {
147
- const { aType, aUid, axbType, bType, bUid, limit, orderBy } = params;
148
- if (aUid && axbType && bUid && !params.where?.length) {
149
- return { strategy: "get", docId: computeEdgeDocId(aUid, axbType, bUid) };
150
- }
151
- const filters = [];
152
- if (aType) filters.push({ field: "aType", op: "==", value: aType });
153
- if (aUid) filters.push({ field: "aUid", op: "==", value: aUid });
154
- if (axbType) filters.push({ field: "axbType", op: "==", value: axbType });
155
- if (bType) filters.push({ field: "bType", op: "==", value: bType });
156
- if (bUid) filters.push({ field: "bUid", op: "==", value: bUid });
157
- if (params.where) {
158
- for (const clause of params.where) {
159
- const field = BUILTIN_FIELDS.has(clause.field) ? clause.field : clause.field.startsWith("data.") ? clause.field : `data.${clause.field}`;
160
- filters.push({ field, op: clause.op, value: clause.value });
149
+ return _jiti;
150
+ }
151
+ function loadSchemaModule(filePath, entityLabel) {
152
+ try {
153
+ const jiti = getJiti();
154
+ const mod = jiti(filePath);
155
+ const schema = mod && typeof mod === "object" && "default" in mod ? mod.default : mod;
156
+ if (!schema || typeof schema !== "object") {
157
+ throw new DiscoveryError(
158
+ `Schema file ${filePath} for ${entityLabel} must default-export a JSON Schema object.`
159
+ );
161
160
  }
161
+ return schema;
162
+ } catch (err) {
163
+ if (err instanceof DiscoveryError) throw err;
164
+ throw new DiscoveryError(
165
+ `Failed to load schema module ${filePath} for ${entityLabel}: ${err.message}`
166
+ );
162
167
  }
163
- if (filters.length === 0) {
164
- throw new InvalidQueryError("findEdges requires at least one filter parameter");
168
+ }
169
+ var VIEW_EXTENSIONS = [".ts", ".js", ".mts", ".mjs"];
170
+ function findViewsFile(dir) {
171
+ for (const ext of VIEW_EXTENSIONS) {
172
+ const candidate = join(dir, `views${ext}`);
173
+ if (existsSync(candidate)) return candidate;
165
174
  }
166
- const effectiveLimit = limit === void 0 ? DEFAULT_QUERY_LIMIT : limit || void 0;
167
- return { strategy: "query", filters, options: { limit: effectiveLimit, orderBy } };
175
+ return void 0;
168
176
  }
169
- function buildNodeQueryPlan(params) {
170
- const { aType, limit, orderBy } = params;
171
- const filters = [
172
- { field: "aType", op: "==", value: aType },
173
- { field: "axbType", op: "==", value: NODE_RELATION }
174
- ];
175
- if (params.where) {
176
- for (const clause of params.where) {
177
- const field = BUILTIN_FIELDS.has(clause.field) ? clause.field : clause.field.startsWith("data.") ? clause.field : `data.${clause.field}`;
178
- filters.push({ field, op: clause.op, value: clause.value });
179
- }
177
+ var MIGRATION_EXTENSIONS = [".ts", ".js", ".mts", ".mjs"];
178
+ function findMigrationsFile(dir) {
179
+ for (const ext of MIGRATION_EXTENSIONS) {
180
+ const candidate = join(dir, `migrations${ext}`);
181
+ if (existsSync(candidate)) return candidate;
180
182
  }
181
- const effectiveLimit = limit === void 0 ? DEFAULT_QUERY_LIMIT : limit || void 0;
182
- return { strategy: "query", filters, options: { limit: effectiveLimit, orderBy } };
183
+ return void 0;
183
184
  }
184
-
185
- // src/internal/firestore-adapter.ts
186
- function createFirestoreAdapter(db, collectionPath) {
187
- const collectionRef = db.collection(collectionPath);
188
- return {
189
- collectionPath,
190
- async getDoc(docId) {
191
- const snap = await collectionRef.doc(docId).get();
192
- if (!snap.exists) return null;
193
- return snap.data();
194
- },
195
- async setDoc(docId, data) {
196
- await collectionRef.doc(docId).set(data);
197
- },
198
- async updateDoc(docId, data) {
199
- await collectionRef.doc(docId).update(data);
200
- },
201
- async deleteDoc(docId) {
202
- await collectionRef.doc(docId).delete();
203
- },
204
- async query(filters, options) {
205
- let q = collectionRef;
206
- for (const f of filters) {
207
- q = q.where(f.field, f.op, f.value);
208
- }
209
- if (options?.orderBy) {
210
- q = q.orderBy(options.orderBy.field, options.orderBy.direction ?? "asc");
211
- }
212
- if (options?.limit !== void 0) {
213
- q = q.limit(options.limit);
214
- }
215
- const snap = await q.get();
216
- return snap.docs.map((doc) => doc.data());
185
+ function loadMigrations(filePath, entityLabel) {
186
+ try {
187
+ const jiti = getJiti();
188
+ const mod = jiti(filePath);
189
+ const migrations = mod && typeof mod === "object" && "default" in mod ? mod.default : mod;
190
+ if (!Array.isArray(migrations)) {
191
+ throw new DiscoveryError(
192
+ `Migrations file ${filePath} for ${entityLabel} must default-export an array of MigrationStep.`
193
+ );
217
194
  }
218
- };
195
+ return migrations;
196
+ } catch (err) {
197
+ if (err instanceof DiscoveryError) throw err;
198
+ throw new DiscoveryError(
199
+ `Failed to load migrations ${filePath} for ${entityLabel}: ${err.message}`
200
+ );
201
+ }
219
202
  }
220
- function createTransactionAdapter(db, collectionPath, tx) {
221
- const collectionRef = db.collection(collectionPath);
203
+ function loadNodeEntity(dir, name) {
204
+ const schema = loadSchema(dir, `node type "${name}"`);
205
+ const meta = readJsonIfExists(join(dir, "meta.json"));
206
+ const sampleData = readJsonIfExists(join(dir, "sample.json"));
207
+ const viewsPath = findViewsFile(dir);
208
+ const migrationsPath = findMigrationsFile(dir);
209
+ const migrations = migrationsPath ? loadMigrations(migrationsPath, `node type "${name}"`) : void 0;
222
210
  return {
223
- async getDoc(docId) {
224
- const snap = await tx.get(collectionRef.doc(docId));
225
- if (!snap.exists) return null;
226
- return snap.data();
227
- },
228
- setDoc(docId, data) {
229
- tx.set(collectionRef.doc(docId), data);
230
- },
231
- updateDoc(docId, data) {
232
- tx.update(collectionRef.doc(docId), data);
233
- },
234
- deleteDoc(docId) {
235
- tx.delete(collectionRef.doc(docId));
236
- },
237
- async query(filters, options) {
238
- let q = collectionRef;
239
- for (const f of filters) {
240
- q = q.where(f.field, f.op, f.value);
241
- }
242
- if (options?.orderBy) {
243
- q = q.orderBy(options.orderBy.field, options.orderBy.direction ?? "asc");
244
- }
245
- if (options?.limit !== void 0) {
246
- q = q.limit(options.limit);
247
- }
248
- const snap = await tx.get(q);
249
- return snap.docs.map((doc) => doc.data());
250
- }
251
- };
252
- }
253
- function createBatchAdapter(db, collectionPath) {
254
- const collectionRef = db.collection(collectionPath);
255
- const batch = db.batch();
256
- return {
257
- setDoc(docId, data) {
258
- batch.set(collectionRef.doc(docId), data);
259
- },
260
- updateDoc(docId, data) {
261
- batch.update(collectionRef.doc(docId), data);
262
- },
263
- deleteDoc(docId) {
264
- batch.delete(collectionRef.doc(docId));
265
- },
266
- async commit() {
267
- await batch.commit();
268
- }
211
+ kind: "node",
212
+ name,
213
+ schema,
214
+ description: meta?.description,
215
+ titleField: meta?.titleField,
216
+ subtitleField: meta?.subtitleField,
217
+ viewDefaults: meta?.viewDefaults,
218
+ viewsPath,
219
+ sampleData,
220
+ allowedIn: meta?.allowedIn,
221
+ migrations,
222
+ migrationWriteBack: meta?.migrationWriteBack
269
223
  };
270
224
  }
271
-
272
- // src/internal/pipeline-adapter.ts
273
- var _Pipelines = null;
274
- async function getPipelines() {
275
- if (!_Pipelines) {
276
- const mod = await import("@google-cloud/firestore");
277
- _Pipelines = mod.Pipelines;
225
+ function loadEdgeEntity(dir, name) {
226
+ const schema = loadSchema(dir, `edge type "${name}"`);
227
+ const edgePath = join(dir, "edge.json");
228
+ if (!existsSync(edgePath)) {
229
+ throw new DiscoveryError(
230
+ `Missing edge.json for edge type "${name}" in ${dir}. Edge entities must declare topology (from/to node types).`
231
+ );
278
232
  }
279
- return _Pipelines;
280
- }
281
- function buildFilterExpression(P, filter) {
282
- const { field: fieldName, op, value } = filter;
283
- switch (op) {
284
- case "==":
285
- return P.equal(fieldName, value);
286
- case "!=":
287
- return P.notEqual(fieldName, value);
288
- case "<":
289
- return P.lessThan(fieldName, value);
290
- case "<=":
291
- return P.lessThanOrEqual(fieldName, value);
292
- case ">":
293
- return P.greaterThan(fieldName, value);
294
- case ">=":
295
- return P.greaterThanOrEqual(fieldName, value);
296
- case "in":
297
- return P.equalAny(fieldName, value);
298
- case "not-in":
299
- return P.notEqualAny(fieldName, value);
300
- case "array-contains":
301
- return P.arrayContains(fieldName, value);
302
- case "array-contains-any":
303
- return P.arrayContainsAny(fieldName, value);
304
- default:
305
- throw new Error(`Unsupported filter op for pipeline mode: ${op}`);
233
+ const topology = readJson(edgePath);
234
+ if (!topology.from) {
235
+ throw new DiscoveryError(`edge.json for "${name}" is missing required "from" field`);
306
236
  }
307
- }
308
- function createPipelineQueryAdapter(db, collectionPath) {
237
+ if (!topology.to) {
238
+ throw new DiscoveryError(`edge.json for "${name}" is missing required "to" field`);
239
+ }
240
+ const meta = readJsonIfExists(join(dir, "meta.json"));
241
+ const sampleData = readJsonIfExists(join(dir, "sample.json"));
242
+ const viewsPath = findViewsFile(dir);
243
+ const migrationsPath = findMigrationsFile(dir);
244
+ const migrations = migrationsPath ? loadMigrations(migrationsPath, `edge type "${name}"`) : void 0;
309
245
  return {
310
- async query(filters, options) {
311
- const P = await getPipelines();
312
- let pipeline = db.pipeline().collection(collectionPath);
313
- if (filters.length === 1) {
314
- pipeline = pipeline.where(buildFilterExpression(P, filters[0]));
315
- } else if (filters.length > 1) {
316
- const [first, second, ...rest] = filters.map((f) => buildFilterExpression(P, f));
317
- pipeline = pipeline.where(P.and(first, second, ...rest));
318
- }
319
- if (options?.orderBy) {
320
- const f = P.field(options.orderBy.field);
321
- const ordering = options.orderBy.direction === "desc" ? f.descending() : f.ascending();
322
- pipeline = pipeline.sort(ordering);
323
- }
324
- if (options?.limit !== void 0) {
325
- pipeline = pipeline.limit(options.limit);
326
- }
327
- const snap = await pipeline.execute();
328
- return snap.results.map((r) => r.data());
329
- }
246
+ kind: "edge",
247
+ name,
248
+ schema,
249
+ topology,
250
+ description: meta?.description,
251
+ titleField: meta?.titleField,
252
+ subtitleField: meta?.subtitleField,
253
+ viewDefaults: meta?.viewDefaults,
254
+ viewsPath,
255
+ sampleData,
256
+ allowedIn: meta?.allowedIn,
257
+ targetGraph: topology.targetGraph ?? meta?.targetGraph,
258
+ migrations,
259
+ migrationWriteBack: meta?.migrationWriteBack
330
260
  };
331
261
  }
332
-
333
- // src/transaction.ts
334
- import { FieldValue as FieldValue3 } from "@google-cloud/firestore";
335
-
336
- // src/query-safety.ts
337
- var SAFE_INDEX_PATTERNS = [
338
- /* @__PURE__ */ new Set(["aUid", "axbType"]),
339
- /* @__PURE__ */ new Set(["axbType", "bUid"]),
340
- /* @__PURE__ */ new Set(["aType", "axbType"]),
341
- /* @__PURE__ */ new Set(["axbType", "bType"])
342
- ];
343
- function analyzeQuerySafety(filters) {
344
- const builtinFieldsPresent = /* @__PURE__ */ new Set();
345
- let hasDataFilters = false;
346
- for (const f of filters) {
347
- if (BUILTIN_FIELDS.has(f.field)) {
348
- builtinFieldsPresent.add(f.field);
349
- } else {
350
- hasDataFilters = true;
351
- }
262
+ function getSubdirectories(dir) {
263
+ if (!existsSync(dir)) return [];
264
+ return readdirSync(dir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
265
+ }
266
+ function discoverEntities(entitiesDir) {
267
+ const absDir = resolve(entitiesDir);
268
+ if (!existsSync(absDir) || !statSync(absDir).isDirectory()) {
269
+ throw new DiscoveryError(`Entities directory not found: ${entitiesDir}`);
352
270
  }
353
- for (const pattern of SAFE_INDEX_PATTERNS) {
354
- let matched = true;
355
- for (const field of pattern) {
356
- if (!builtinFieldsPresent.has(field)) {
357
- matched = false;
358
- break;
359
- }
360
- }
361
- if (matched) {
362
- return { safe: true };
363
- }
271
+ const nodes = /* @__PURE__ */ new Map();
272
+ const edges = /* @__PURE__ */ new Map();
273
+ const warnings = [];
274
+ const nodesDir = join(absDir, "nodes");
275
+ for (const name of getSubdirectories(nodesDir)) {
276
+ nodes.set(name, loadNodeEntity(join(nodesDir, name), name));
364
277
  }
365
- const presentFields = [...builtinFieldsPresent];
366
- if (presentFields.length === 0 && hasDataFilters) {
367
- return {
368
- safe: false,
369
- 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."
370
- };
278
+ const edgesDir = join(absDir, "edges");
279
+ for (const name of getSubdirectories(edgesDir)) {
280
+ edges.set(name, loadEdgeEntity(join(edgesDir, name), name));
371
281
  }
372
- if (hasDataFilters) {
373
- return {
374
- safe: false,
375
- 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.`
376
- };
282
+ const nodeNames = new Set(nodes.keys());
283
+ for (const [axbType, entity] of edges) {
284
+ const topology = entity.topology;
285
+ const fromTypes = Array.isArray(topology.from) ? topology.from : [topology.from];
286
+ const toTypes = Array.isArray(topology.to) ? topology.to : [topology.to];
287
+ for (const ref of [...fromTypes, ...toTypes]) {
288
+ if (!nodeNames.has(ref)) {
289
+ warnings.push({
290
+ code: "DANGLING_TOPOLOGY_REF",
291
+ message: `Edge "${axbType}" references node type "${ref}" which was not found in the nodes directory`
292
+ });
293
+ }
294
+ }
377
295
  }
378
296
  return {
379
- safe: false,
380
- 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.`
297
+ result: { nodes, edges },
298
+ warnings
381
299
  };
382
300
  }
383
301
 
384
- // src/serialization.ts
385
- import { Timestamp, GeoPoint, FieldValue as FieldValue2 } from "@google-cloud/firestore";
386
- var SERIALIZATION_TAG = "__firegraph_ser__";
387
- var KNOWN_TYPES = /* @__PURE__ */ new Set(["Timestamp", "GeoPoint", "VectorValue", "DocumentReference"]);
388
- var _docRefWarned = false;
389
- function isTaggedValue(value) {
390
- if (value === null || typeof value !== "object") return false;
391
- const tag = value[SERIALIZATION_TAG];
392
- return typeof tag === "string" && KNOWN_TYPES.has(tag);
393
- }
394
- function isTimestamp(value) {
395
- return value instanceof Timestamp;
396
- }
397
- function isGeoPoint(value) {
398
- return value instanceof GeoPoint;
399
- }
400
- function isDocumentReference(value) {
401
- if (value === null || typeof value !== "object") return false;
402
- const v = value;
403
- return typeof v.path === "string" && v.firestore !== void 0 && typeof v.id === "string" && v.constructor?.name === "DocumentReference";
404
- }
405
- function isVectorValue(value) {
406
- if (value === null || typeof value !== "object") return false;
407
- const v = value;
408
- return v.constructor?.name === "VectorValue" && Array.isArray(v._values);
409
- }
410
- function serializeFirestoreTypes(data) {
411
- return serializeValue(data);
302
+ // src/internal/firestore-backend.ts
303
+ import { FieldValue } from "@google-cloud/firestore";
304
+
305
+ // src/bulk.ts
306
+ var MAX_BATCH_SIZE = 500;
307
+ var DEFAULT_MAX_RETRIES = 3;
308
+ var BASE_DELAY_MS = 200;
309
+ function sleep(ms) {
310
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
412
311
  }
413
- function serializeValue(value) {
414
- if (value === null || value === void 0) return value;
415
- if (typeof value !== "object") return value;
416
- if (isTimestamp(value)) {
417
- return { [SERIALIZATION_TAG]: "Timestamp", seconds: value.seconds, nanoseconds: value.nanoseconds };
418
- }
419
- if (isGeoPoint(value)) {
420
- return { [SERIALIZATION_TAG]: "GeoPoint", latitude: value.latitude, longitude: value.longitude };
421
- }
422
- if (isDocumentReference(value)) {
423
- return { [SERIALIZATION_TAG]: "DocumentReference", path: value.path };
424
- }
425
- if (isVectorValue(value)) {
426
- const v = value;
427
- const values = typeof v.toArray === "function" ? v.toArray() : v._values;
428
- return { [SERIALIZATION_TAG]: "VectorValue", values: [...values] };
429
- }
430
- if (Array.isArray(value)) {
431
- return value.map(serializeValue);
432
- }
433
- const result = {};
434
- for (const key of Object.keys(value)) {
435
- result[key] = serializeValue(value[key]);
312
+ function chunk(arr, size) {
313
+ const chunks = [];
314
+ for (let i = 0; i < arr.length; i += size) {
315
+ chunks.push(arr.slice(i, i + size));
436
316
  }
437
- return result;
438
- }
439
- function deserializeFirestoreTypes(data, db) {
440
- return deserializeValue(data, db);
317
+ return chunks;
441
318
  }
442
- function deserializeValue(value, db) {
443
- if (value === null || value === void 0) return value;
444
- if (typeof value !== "object") return value;
445
- if (isTimestamp(value) || isGeoPoint(value) || isDocumentReference(value) || isVectorValue(value)) {
446
- return value;
447
- }
448
- if (Array.isArray(value)) {
449
- return value.map((v) => deserializeValue(v, db));
450
- }
451
- const obj = value;
452
- if (isTaggedValue(obj)) {
453
- const tag = obj[SERIALIZATION_TAG];
454
- switch (tag) {
455
- case "Timestamp":
456
- if (typeof obj.seconds !== "number" || typeof obj.nanoseconds !== "number") return obj;
457
- return new Timestamp(obj.seconds, obj.nanoseconds);
458
- case "GeoPoint":
459
- if (typeof obj.latitude !== "number" || typeof obj.longitude !== "number") return obj;
460
- return new GeoPoint(obj.latitude, obj.longitude);
461
- case "VectorValue":
462
- if (!Array.isArray(obj.values)) return obj;
463
- return FieldValue2.vector(obj.values);
464
- case "DocumentReference":
465
- if (typeof obj.path !== "string") return obj;
466
- if (db) {
467
- return db.doc(obj.path);
468
- }
469
- if (!_docRefWarned) {
470
- _docRefWarned = true;
471
- console.warn(
472
- "[firegraph] DocumentReference encountered during migration deserialization but no Firestore instance available. The reference will remain as a tagged object with its path. Enable write-back for full reconstruction."
473
- );
474
- }
475
- return obj;
476
- default:
477
- return obj;
478
- }
479
- }
480
- const result = {};
481
- for (const key of Object.keys(obj)) {
482
- result[key] = deserializeValue(obj[key], db);
319
+ async function bulkDeleteDocIds(db, collectionPath, docIds, options) {
320
+ if (docIds.length === 0) {
321
+ return { deleted: 0, batches: 0, errors: [] };
483
322
  }
484
- return result;
485
- }
486
-
487
- // src/migration.ts
488
- async function applyMigrationChain(data, currentVersion, targetVersion, migrations) {
489
- const sorted = [...migrations].sort((a, b) => a.fromVersion - b.fromVersion);
490
- let result = { ...data };
491
- let version = currentVersion;
492
- for (const step of sorted) {
493
- if (step.fromVersion === version) {
323
+ const batchSize = Math.min(options?.batchSize ?? MAX_BATCH_SIZE, MAX_BATCH_SIZE);
324
+ const maxRetries = options?.maxRetries ?? DEFAULT_MAX_RETRIES;
325
+ const onProgress = options?.onProgress;
326
+ const chunks = chunk(docIds, batchSize);
327
+ const errors = [];
328
+ let deleted = 0;
329
+ let completedBatches = 0;
330
+ for (let i = 0; i < chunks.length; i++) {
331
+ const ids = chunks[i];
332
+ let committed = false;
333
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
494
334
  try {
495
- result = await step.up(result);
335
+ const batch = db.batch();
336
+ const collectionRef = db.collection(collectionPath);
337
+ for (const id of ids) {
338
+ batch.delete(collectionRef.doc(id));
339
+ }
340
+ await batch.commit();
341
+ committed = true;
342
+ deleted += ids.length;
343
+ break;
496
344
  } catch (err) {
497
- if (err instanceof MigrationError) throw err;
498
- throw new MigrationError(
499
- `Migration from v${step.fromVersion} to v${step.toVersion} failed: ${err.message}`
500
- );
501
- }
502
- if (!result || typeof result !== "object") {
503
- throw new MigrationError(
504
- `Migration from v${step.fromVersion} to v${step.toVersion} returned invalid data (expected object)`
505
- );
506
- }
507
- version = step.toVersion;
508
- }
509
- }
510
- if (version !== targetVersion) {
511
- throw new MigrationError(
512
- `Incomplete migration chain: reached v${version} but target is v${targetVersion}`
513
- );
514
- }
515
- return result;
516
- }
517
- function validateMigrationChain(migrations, label) {
518
- if (migrations.length === 0) return;
519
- const seen = /* @__PURE__ */ new Set();
520
- for (const step of migrations) {
521
- if (step.toVersion <= step.fromVersion) {
522
- throw new MigrationError(
523
- `${label}: migration step has toVersion (${step.toVersion}) <= fromVersion (${step.fromVersion})`
524
- );
525
- }
526
- if (seen.has(step.fromVersion)) {
527
- throw new MigrationError(
528
- `${label}: duplicate migration step for fromVersion ${step.fromVersion}`
529
- );
530
- }
531
- seen.add(step.fromVersion);
532
- }
533
- const sorted = [...migrations].sort((a, b) => a.fromVersion - b.fromVersion);
534
- const targetVersion = Math.max(...migrations.map((m) => m.toVersion));
535
- let version = 0;
536
- for (const step of sorted) {
537
- if (step.fromVersion === version) {
538
- version = step.toVersion;
539
- } else if (step.fromVersion > version) {
540
- throw new MigrationError(
541
- `${label}: migration chain has a gap \u2014 no step covers v${version} \u2192 v${step.fromVersion}`
542
- );
543
- }
544
- }
545
- if (version !== targetVersion) {
546
- throw new MigrationError(
547
- `${label}: migration chain does not reach v${targetVersion} (stuck at v${version})`
548
- );
549
- }
550
- }
551
- async function migrateRecord(record, registry, globalWriteBack = "off") {
552
- const entry = registry.lookup(record.aType, record.axbType, record.bType);
553
- if (!entry?.migrations?.length || !entry.schemaVersion) {
554
- return { record, migrated: false, writeBack: "off" };
555
- }
556
- const currentVersion = record.v ?? 0;
557
- if (currentVersion >= entry.schemaVersion) {
558
- return { record, migrated: false, writeBack: "off" };
559
- }
560
- const migratedData = await applyMigrationChain(
561
- record.data,
562
- currentVersion,
563
- entry.schemaVersion,
564
- entry.migrations
565
- );
566
- const writeBack = entry.migrationWriteBack ?? globalWriteBack ?? "off";
567
- return {
568
- record: { ...record, data: migratedData, v: entry.schemaVersion },
569
- migrated: true,
570
- writeBack
571
- };
572
- }
573
- async function migrateRecords(records, registry, globalWriteBack = "off") {
574
- return Promise.all(
575
- records.map((r) => migrateRecord(r, registry, globalWriteBack))
576
- );
577
- }
578
-
579
- // src/transaction.ts
580
- var GraphTransactionImpl = class {
581
- constructor(adapter, registry, scanProtection = "error", scopePath = "", globalWriteBack = "off", db) {
582
- this.adapter = adapter;
583
- this.registry = registry;
584
- this.scanProtection = scanProtection;
585
- this.scopePath = scopePath;
586
- this.globalWriteBack = globalWriteBack;
587
- this.db = db;
588
- }
589
- async getNode(uid) {
590
- const docId = computeNodeDocId(uid);
591
- const record = await this.adapter.getDoc(docId);
592
- if (!record || !this.registry) return record;
593
- const result = await migrateRecord(record, this.registry, this.globalWriteBack);
594
- if (result.migrated && result.writeBack !== "off") {
595
- const update = {
596
- data: deserializeFirestoreTypes(result.record.data, this.db),
597
- updatedAt: FieldValue3.serverTimestamp()
598
- };
599
- if (result.record.v !== void 0) {
600
- update.v = result.record.v;
601
- }
602
- this.adapter.updateDoc(docId, update);
603
- }
604
- return result.record;
605
- }
606
- async getEdge(aUid, axbType, bUid) {
607
- const docId = computeEdgeDocId(aUid, axbType, bUid);
608
- const record = await this.adapter.getDoc(docId);
609
- if (!record || !this.registry) return record;
610
- const result = await migrateRecord(record, this.registry, this.globalWriteBack);
611
- if (result.migrated && result.writeBack !== "off") {
612
- const update = {
613
- data: deserializeFirestoreTypes(result.record.data, this.db),
614
- updatedAt: FieldValue3.serverTimestamp()
615
- };
616
- if (result.record.v !== void 0) {
617
- update.v = result.record.v;
618
- }
619
- this.adapter.updateDoc(docId, update);
620
- }
621
- return result.record;
622
- }
623
- async edgeExists(aUid, axbType, bUid) {
624
- const docId = computeEdgeDocId(aUid, axbType, bUid);
625
- const record = await this.adapter.getDoc(docId);
626
- return record !== null;
627
- }
628
- checkQuerySafety(filters, allowCollectionScan) {
629
- if (allowCollectionScan || this.scanProtection === "off") return;
630
- const result = analyzeQuerySafety(filters);
631
- if (result.safe) return;
632
- if (this.scanProtection === "error") {
633
- throw new QuerySafetyError(result.reason);
634
- }
635
- console.warn(`[firegraph] Query safety warning: ${result.reason}`);
636
- }
637
- async findEdges(params) {
638
- const plan = buildEdgeQueryPlan(params);
639
- let records;
640
- if (plan.strategy === "get") {
641
- const record = await this.adapter.getDoc(plan.docId);
642
- records = record ? [record] : [];
643
- } else {
644
- this.checkQuerySafety(plan.filters, params.allowCollectionScan);
645
- records = await this.adapter.query(plan.filters, plan.options);
646
- }
647
- return this.applyMigrations(records);
648
- }
649
- async findNodes(params) {
650
- const plan = buildNodeQueryPlan(params);
651
- let records;
652
- if (plan.strategy === "get") {
653
- const record = await this.adapter.getDoc(plan.docId);
654
- records = record ? [record] : [];
655
- } else {
656
- this.checkQuerySafety(plan.filters, params.allowCollectionScan);
657
- records = await this.adapter.query(plan.filters, plan.options);
658
- }
659
- return this.applyMigrations(records);
660
- }
661
- async applyMigrations(records) {
662
- if (!this.registry || records.length === 0) return records;
663
- const results = await migrateRecords(records, this.registry, this.globalWriteBack);
664
- for (const result of results) {
665
- if (result.migrated && result.writeBack !== "off") {
666
- const docId = result.record.axbType === NODE_RELATION ? computeNodeDocId(result.record.aUid) : computeEdgeDocId(result.record.aUid, result.record.axbType, result.record.bUid);
667
- const update = {
668
- data: deserializeFirestoreTypes(result.record.data, this.db),
669
- updatedAt: FieldValue3.serverTimestamp()
670
- };
671
- if (result.record.v !== void 0) {
672
- update.v = result.record.v;
673
- }
674
- this.adapter.updateDoc(docId, update);
675
- }
676
- }
677
- return results.map((r) => r.record);
678
- }
679
- async putNode(aType, uid, data) {
680
- if (this.registry) {
681
- this.registry.validate(aType, NODE_RELATION, aType, data, this.scopePath);
682
- }
683
- const docId = computeNodeDocId(uid);
684
- const record = buildNodeRecord(aType, uid, data);
685
- if (this.registry) {
686
- const entry = this.registry.lookup(aType, NODE_RELATION, aType);
687
- if (entry?.schemaVersion && entry.schemaVersion > 0) {
688
- record.v = entry.schemaVersion;
689
- }
690
- }
691
- this.adapter.setDoc(docId, record);
692
- }
693
- async putEdge(aType, aUid, axbType, bType, bUid, data) {
694
- if (this.registry) {
695
- this.registry.validate(aType, axbType, bType, data, this.scopePath);
696
- }
697
- const docId = computeEdgeDocId(aUid, axbType, bUid);
698
- const record = buildEdgeRecord(aType, aUid, axbType, bType, bUid, data);
699
- if (this.registry) {
700
- const entry = this.registry.lookup(aType, axbType, bType);
701
- if (entry?.schemaVersion && entry.schemaVersion > 0) {
702
- record.v = entry.schemaVersion;
703
- }
704
- }
705
- this.adapter.setDoc(docId, record);
706
- }
707
- async updateNode(uid, data) {
708
- const docId = computeNodeDocId(uid);
709
- const update = {
710
- updatedAt: FieldValue3.serverTimestamp()
711
- };
712
- for (const [k, v] of Object.entries(data)) {
713
- update[`data.${k}`] = v;
714
- }
715
- this.adapter.updateDoc(docId, update);
716
- }
717
- async removeNode(uid) {
718
- const docId = computeNodeDocId(uid);
719
- this.adapter.deleteDoc(docId);
720
- }
721
- async removeEdge(aUid, axbType, bUid) {
722
- const docId = computeEdgeDocId(aUid, axbType, bUid);
723
- this.adapter.deleteDoc(docId);
724
- }
725
- };
726
-
727
- // src/batch.ts
728
- import { FieldValue as FieldValue4 } from "@google-cloud/firestore";
729
- var GraphBatchImpl = class {
730
- constructor(adapter, registry, scopePath = "") {
731
- this.adapter = adapter;
732
- this.registry = registry;
733
- this.scopePath = scopePath;
734
- }
735
- async putNode(aType, uid, data) {
736
- if (this.registry) {
737
- this.registry.validate(aType, NODE_RELATION, aType, data, this.scopePath);
738
- }
739
- const docId = computeNodeDocId(uid);
740
- const record = buildNodeRecord(aType, uid, data);
741
- if (this.registry) {
742
- const entry = this.registry.lookup(aType, NODE_RELATION, aType);
743
- if (entry?.schemaVersion && entry.schemaVersion > 0) {
744
- record.v = entry.schemaVersion;
745
- }
746
- }
747
- this.adapter.setDoc(docId, record);
748
- }
749
- async putEdge(aType, aUid, axbType, bType, bUid, data) {
750
- if (this.registry) {
751
- this.registry.validate(aType, axbType, bType, data, this.scopePath);
752
- }
753
- const docId = computeEdgeDocId(aUid, axbType, bUid);
754
- const record = buildEdgeRecord(aType, aUid, axbType, bType, bUid, data);
755
- if (this.registry) {
756
- const entry = this.registry.lookup(aType, axbType, bType);
757
- if (entry?.schemaVersion && entry.schemaVersion > 0) {
758
- record.v = entry.schemaVersion;
759
- }
760
- }
761
- this.adapter.setDoc(docId, record);
762
- }
763
- async updateNode(uid, data) {
764
- const docId = computeNodeDocId(uid);
765
- const update = {
766
- updatedAt: FieldValue4.serverTimestamp()
767
- };
768
- for (const [k, v] of Object.entries(data)) {
769
- update[`data.${k}`] = v;
770
- }
771
- this.adapter.updateDoc(docId, update);
772
- }
773
- async removeNode(uid) {
774
- const docId = computeNodeDocId(uid);
775
- this.adapter.deleteDoc(docId);
776
- }
777
- async removeEdge(aUid, axbType, bUid) {
778
- const docId = computeEdgeDocId(aUid, axbType, bUid);
779
- this.adapter.deleteDoc(docId);
780
- }
781
- async commit() {
782
- await this.adapter.commit();
783
- }
784
- };
785
-
786
- // src/bulk.ts
787
- var MAX_BATCH_SIZE = 500;
788
- var DEFAULT_MAX_RETRIES = 3;
789
- var BASE_DELAY_MS = 200;
790
- function sleep(ms) {
791
- return new Promise((resolve2) => setTimeout(resolve2, ms));
792
- }
793
- function chunk(arr, size) {
794
- const chunks = [];
795
- for (let i = 0; i < arr.length; i += size) {
796
- chunks.push(arr.slice(i, i + size));
797
- }
798
- return chunks;
799
- }
800
- async function bulkDeleteDocIds(db, collectionPath, docIds, options) {
801
- if (docIds.length === 0) {
802
- return { deleted: 0, batches: 0, errors: [] };
803
- }
804
- const batchSize = Math.min(options?.batchSize ?? MAX_BATCH_SIZE, MAX_BATCH_SIZE);
805
- const maxRetries = options?.maxRetries ?? DEFAULT_MAX_RETRIES;
806
- const onProgress = options?.onProgress;
807
- const chunks = chunk(docIds, batchSize);
808
- const errors = [];
809
- let deleted = 0;
810
- let completedBatches = 0;
811
- for (let i = 0; i < chunks.length; i++) {
812
- const ids = chunks[i];
813
- let committed = false;
814
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
815
- try {
816
- const batch = db.batch();
817
- const collectionRef = db.collection(collectionPath);
818
- for (const id of ids) {
819
- batch.delete(collectionRef.doc(id));
820
- }
821
- await batch.commit();
822
- committed = true;
823
- deleted += ids.length;
824
- break;
825
- } catch (err) {
826
- if (attempt < maxRetries) {
827
- const delay = BASE_DELAY_MS * Math.pow(2, attempt);
828
- await sleep(delay);
829
- } else {
830
- errors.push({
831
- batchIndex: i,
832
- error: err instanceof Error ? err : new Error(String(err)),
833
- operationCount: ids.length
834
- });
835
- }
345
+ if (attempt < maxRetries) {
346
+ const delay = BASE_DELAY_MS * Math.pow(2, attempt);
347
+ await sleep(delay);
348
+ } else {
349
+ errors.push({
350
+ batchIndex: i,
351
+ error: err instanceof Error ? err : new Error(String(err)),
352
+ operationCount: ids.length
353
+ });
354
+ }
836
355
  }
837
356
  }
838
357
  if (committed) {
@@ -925,1044 +444,288 @@ async function removeNodeCascade(db, collectionPath, reader, uid, options) {
925
444
  };
926
445
  }
927
446
 
928
- // src/dynamic-registry.ts
929
- import { createHash as createHash3 } from "crypto";
930
-
931
- // src/json-schema.ts
932
- import Ajv from "ajv";
933
- import addFormats from "ajv-formats";
934
- var ajv = new Ajv({ allErrors: true, strict: false });
935
- addFormats(ajv);
936
- function compileSchema(schema, label) {
937
- const validate = ajv.compile(schema);
938
- return (data) => {
939
- if (!validate(data)) {
940
- const errors = validate.errors ?? [];
941
- const messages = errors.map((err) => `${err.instancePath || "/"}${err.message ? ": " + err.message : ""}`).join("; ");
942
- throw new ValidationError(
943
- `Data validation failed${label ? " for " + label : ""}: ${messages}`,
944
- errors
945
- );
447
+ // src/internal/firestore-adapter.ts
448
+ function createFirestoreAdapter(db, collectionPath) {
449
+ const collectionRef = db.collection(collectionPath);
450
+ return {
451
+ collectionPath,
452
+ async getDoc(docId) {
453
+ const snap = await collectionRef.doc(docId).get();
454
+ if (!snap.exists) return null;
455
+ return snap.data();
456
+ },
457
+ async setDoc(docId, data) {
458
+ await collectionRef.doc(docId).set(data);
459
+ },
460
+ async updateDoc(docId, data) {
461
+ await collectionRef.doc(docId).update(data);
462
+ },
463
+ async deleteDoc(docId) {
464
+ await collectionRef.doc(docId).delete();
465
+ },
466
+ async query(filters, options) {
467
+ let q = collectionRef;
468
+ for (const f of filters) {
469
+ q = q.where(f.field, f.op, f.value);
470
+ }
471
+ if (options?.orderBy) {
472
+ q = q.orderBy(options.orderBy.field, options.orderBy.direction ?? "asc");
473
+ }
474
+ if (options?.limit !== void 0) {
475
+ q = q.limit(options.limit);
476
+ }
477
+ const snap = await q.get();
478
+ return snap.docs.map((doc) => doc.data());
946
479
  }
947
480
  };
948
481
  }
949
- function jsonSchemaToFieldMeta(schema) {
950
- if (!schema || schema.type !== "object" || !schema.properties) return [];
951
- const requiredSet = new Set(
952
- Array.isArray(schema.required) ? schema.required : []
953
- );
954
- return Object.entries(schema.properties).map(
955
- ([name, prop]) => propertyToFieldMeta(name, prop, requiredSet.has(name))
956
- );
957
- }
958
- function propertyToFieldMeta(name, prop, required) {
959
- if (!prop) return { name, type: "unknown", required };
960
- if (Array.isArray(prop.enum)) {
961
- return {
962
- name,
963
- type: "enum",
964
- required,
965
- enumValues: prop.enum,
966
- description: prop.description
967
- };
968
- }
969
- if (Array.isArray(prop.oneOf) || Array.isArray(prop.anyOf)) {
970
- const variants = prop.oneOf ?? prop.anyOf;
971
- const nonNull = variants.filter((v) => v.type !== "null");
972
- if (nonNull.length === 1) {
973
- return propertyToFieldMeta(name, nonNull[0], false);
974
- }
975
- return { name, type: "unknown", required, description: prop.description };
976
- }
977
- const type = prop.type;
978
- if (type === "string") {
979
- return {
980
- name,
981
- type: "string",
982
- required,
983
- minLength: prop.minLength,
984
- maxLength: prop.maxLength,
985
- pattern: prop.pattern,
986
- description: prop.description
987
- };
988
- }
989
- if (type === "number" || type === "integer") {
990
- return {
991
- name,
992
- type: "number",
993
- required,
994
- min: prop.minimum,
995
- max: prop.maximum,
996
- isInt: type === "integer" ? true : void 0,
997
- description: prop.description
998
- };
999
- }
1000
- if (type === "boolean") {
1001
- return { name, type: "boolean", required, description: prop.description };
1002
- }
1003
- if (type === "array") {
1004
- const itemMeta = prop.items ? propertyToFieldMeta("item", prop.items, true) : void 0;
1005
- return {
1006
- name,
1007
- type: "array",
1008
- required,
1009
- itemMeta,
1010
- description: prop.description
1011
- };
1012
- }
1013
- if (type === "object") {
1014
- return {
1015
- name,
1016
- type: "object",
1017
- required,
1018
- fields: jsonSchemaToFieldMeta(prop),
1019
- description: prop.description
1020
- };
1021
- }
1022
- return { name, type: "unknown", required, description: prop.description };
1023
- }
1024
-
1025
- // src/scope.ts
1026
- function matchScope(scopePath, pattern) {
1027
- if (pattern === "root") return scopePath === "";
1028
- if (pattern === "**") return true;
1029
- const pathSegments = scopePath === "" ? [] : scopePath.split("/");
1030
- const patternSegments = pattern.split("/");
1031
- return matchSegments(pathSegments, 0, patternSegments, 0);
1032
- }
1033
- function matchScopeAny(scopePath, patterns) {
1034
- if (!patterns || patterns.length === 0) return true;
1035
- return patterns.some((p) => matchScope(scopePath, p));
1036
- }
1037
- function matchSegments(path, pi, pattern, qi) {
1038
- if (pi === path.length && qi === pattern.length) return true;
1039
- if (qi === pattern.length) return false;
1040
- const seg = pattern[qi];
1041
- if (seg === "**") {
1042
- if (qi === pattern.length - 1) return true;
1043
- for (let skip = 0; skip <= path.length - pi; skip++) {
1044
- if (matchSegments(path, pi + skip, pattern, qi + 1)) return true;
1045
- }
1046
- return false;
1047
- }
1048
- if (pi === path.length) return false;
1049
- if (seg === "*") {
1050
- return matchSegments(path, pi + 1, pattern, qi + 1);
1051
- }
1052
- if (path[pi] === seg) {
1053
- return matchSegments(path, pi + 1, pattern, qi + 1);
1054
- }
1055
- return false;
1056
- }
1057
-
1058
- // src/registry.ts
1059
- function tripleKey(aType, axbType, bType) {
1060
- return `${aType}:${axbType}:${bType}`;
1061
- }
1062
- function tripleKeyFor(e) {
1063
- return tripleKey(e.aType, e.axbType, e.bType);
1064
- }
1065
- function createRegistry(input) {
1066
- const map = /* @__PURE__ */ new Map();
1067
- let entries;
1068
- if (Array.isArray(input)) {
1069
- entries = input;
1070
- } else {
1071
- entries = discoveryToEntries(input);
1072
- }
1073
- const entryList = Object.freeze([...entries]);
1074
- for (const entry of entries) {
1075
- if (entry.targetGraph && entry.targetGraph.includes("/")) {
1076
- throw new ValidationError(
1077
- `Entry (${entry.aType}) -[${entry.axbType}]-> (${entry.bType}) has invalid targetGraph "${entry.targetGraph}" \u2014 must be a single segment (no "/")`
1078
- );
1079
- }
1080
- if (entry.migrations?.length) {
1081
- const label = `Entry (${entry.aType}) -[${entry.axbType}]-> (${entry.bType})`;
1082
- validateMigrationChain(entry.migrations, label);
1083
- entry.schemaVersion = Math.max(...entry.migrations.map((m) => m.toVersion));
1084
- } else {
1085
- entry.schemaVersion = void 0;
1086
- }
1087
- const key = tripleKey(entry.aType, entry.axbType, entry.bType);
1088
- const validator = entry.jsonSchema ? compileSchema(entry.jsonSchema, `(${entry.aType}) -[${entry.axbType}]-> (${entry.bType})`) : void 0;
1089
- map.set(key, { entry, validate: validator });
1090
- }
1091
- const axbIndex = /* @__PURE__ */ new Map();
1092
- const axbBuild = /* @__PURE__ */ new Map();
1093
- for (const entry of entries) {
1094
- const existing = axbBuild.get(entry.axbType);
1095
- if (existing) {
1096
- existing.push(entry);
1097
- } else {
1098
- axbBuild.set(entry.axbType, [entry]);
1099
- }
1100
- }
1101
- for (const [key, arr] of axbBuild) {
1102
- axbIndex.set(key, Object.freeze(arr));
1103
- }
482
+ function createTransactionAdapter(db, collectionPath, tx) {
483
+ const collectionRef = db.collection(collectionPath);
1104
484
  return {
1105
- lookup(aType, axbType, bType) {
1106
- return map.get(tripleKey(aType, axbType, bType))?.entry;
485
+ async getDoc(docId) {
486
+ const snap = await tx.get(collectionRef.doc(docId));
487
+ if (!snap.exists) return null;
488
+ return snap.data();
489
+ },
490
+ setDoc(docId, data) {
491
+ tx.set(collectionRef.doc(docId), data);
492
+ },
493
+ updateDoc(docId, data) {
494
+ tx.update(collectionRef.doc(docId), data);
1107
495
  },
1108
- lookupByAxbType(axbType) {
1109
- return axbIndex.get(axbType) ?? [];
496
+ deleteDoc(docId) {
497
+ tx.delete(collectionRef.doc(docId));
1110
498
  },
1111
- validate(aType, axbType, bType, data, scopePath) {
1112
- const rec = map.get(tripleKey(aType, axbType, bType));
1113
- if (!rec) {
1114
- throw new RegistryViolationError(aType, axbType, bType);
499
+ async query(filters, options) {
500
+ let q = collectionRef;
501
+ for (const f of filters) {
502
+ q = q.where(f.field, f.op, f.value);
1115
503
  }
1116
- if (scopePath !== void 0 && rec.entry.allowedIn && rec.entry.allowedIn.length > 0) {
1117
- if (!matchScopeAny(scopePath, rec.entry.allowedIn)) {
1118
- throw new RegistryScopeError(aType, axbType, bType, scopePath, rec.entry.allowedIn);
1119
- }
504
+ if (options?.orderBy) {
505
+ q = q.orderBy(options.orderBy.field, options.orderBy.direction ?? "asc");
1120
506
  }
1121
- if (rec.validate) {
1122
- try {
1123
- rec.validate(data);
1124
- } catch (err) {
1125
- if (err instanceof ValidationError) throw err;
1126
- throw new ValidationError(
1127
- `Data validation failed for (${aType}) -[${axbType}]-> (${bType})`,
1128
- err
1129
- );
1130
- }
507
+ if (options?.limit !== void 0) {
508
+ q = q.limit(options.limit);
1131
509
  }
1132
- },
1133
- entries() {
1134
- return entryList;
510
+ const snap = await tx.get(q);
511
+ return snap.docs.map((doc) => doc.data());
1135
512
  }
1136
513
  };
1137
514
  }
1138
- function createMergedRegistry(base, extension) {
1139
- const baseKeys = new Set(base.entries().map(tripleKeyFor));
515
+ function createBatchAdapter(db, collectionPath) {
516
+ const collectionRef = db.collection(collectionPath);
517
+ const batch = db.batch();
1140
518
  return {
1141
- lookup(aType, axbType, bType) {
1142
- return base.lookup(aType, axbType, bType) ?? extension.lookup(aType, axbType, bType);
519
+ setDoc(docId, data) {
520
+ batch.set(collectionRef.doc(docId), data);
1143
521
  },
1144
- lookupByAxbType(axbType) {
1145
- const baseResults = base.lookupByAxbType(axbType);
1146
- const extResults = extension.lookupByAxbType(axbType);
1147
- if (extResults.length === 0) return baseResults;
1148
- if (baseResults.length === 0) return extResults;
1149
- const seen = new Set(baseResults.map(tripleKeyFor));
1150
- const merged = [...baseResults];
1151
- for (const entry of extResults) {
1152
- if (!seen.has(tripleKeyFor(entry))) {
1153
- merged.push(entry);
1154
- }
1155
- }
1156
- return Object.freeze(merged);
522
+ updateDoc(docId, data) {
523
+ batch.update(collectionRef.doc(docId), data);
1157
524
  },
1158
- validate(aType, axbType, bType, data, scopePath) {
1159
- if (baseKeys.has(tripleKey(aType, axbType, bType))) {
1160
- return base.validate(aType, axbType, bType, data, scopePath);
1161
- }
1162
- return extension.validate(aType, axbType, bType, data, scopePath);
525
+ deleteDoc(docId) {
526
+ batch.delete(collectionRef.doc(docId));
1163
527
  },
1164
- entries() {
1165
- const extEntries = extension.entries();
1166
- if (extEntries.length === 0) return base.entries();
1167
- const merged = [...base.entries()];
1168
- for (const entry of extEntries) {
1169
- if (!baseKeys.has(tripleKeyFor(entry))) {
1170
- merged.push(entry);
1171
- }
1172
- }
1173
- return Object.freeze(merged);
528
+ async commit() {
529
+ await batch.commit();
1174
530
  }
1175
531
  };
1176
532
  }
1177
- function discoveryToEntries(discovery) {
1178
- const entries = [];
1179
- for (const [name, entity] of discovery.nodes) {
1180
- entries.push({
1181
- aType: name,
1182
- axbType: NODE_RELATION,
1183
- bType: name,
1184
- jsonSchema: entity.schema,
1185
- description: entity.description,
1186
- titleField: entity.titleField,
1187
- subtitleField: entity.subtitleField,
1188
- allowedIn: entity.allowedIn,
1189
- migrations: entity.migrations,
1190
- migrationWriteBack: entity.migrationWriteBack
1191
- });
1192
- }
1193
- for (const [axbType, entity] of discovery.edges) {
1194
- const topology = entity.topology;
1195
- if (!topology) continue;
1196
- const fromTypes = Array.isArray(topology.from) ? topology.from : [topology.from];
1197
- const toTypes = Array.isArray(topology.to) ? topology.to : [topology.to];
1198
- const resolvedTargetGraph = entity.targetGraph ?? topology.targetGraph;
1199
- if (resolvedTargetGraph && resolvedTargetGraph.includes("/")) {
1200
- throw new ValidationError(
1201
- `Edge "${axbType}" has invalid targetGraph "${resolvedTargetGraph}" \u2014 must be a single segment (no "/")`
1202
- );
1203
- }
1204
- for (const aType of fromTypes) {
1205
- for (const bType of toTypes) {
1206
- entries.push({
1207
- aType,
1208
- axbType,
1209
- bType,
1210
- jsonSchema: entity.schema,
1211
- description: entity.description,
1212
- inverseLabel: topology.inverseLabel,
1213
- titleField: entity.titleField,
1214
- subtitleField: entity.subtitleField,
1215
- allowedIn: entity.allowedIn,
1216
- targetGraph: resolvedTargetGraph,
1217
- migrations: entity.migrations,
1218
- migrationWriteBack: entity.migrationWriteBack
1219
- });
1220
- }
1221
- }
1222
- }
1223
- return entries;
1224
- }
1225
533
 
1226
- // src/sandbox.ts
1227
- import { Worker } from "worker_threads";
1228
- import { createHash as createHash2 } from "crypto";
1229
- var _worker = null;
1230
- var _requestId = 0;
1231
- var _pending = /* @__PURE__ */ new Map();
1232
- var WORKER_SOURCE = [
1233
- `'use strict';`,
1234
- `var _wt = require('node:worker_threads');`,
1235
- `var _mod = require('node:module');`,
1236
- `var _crypto = require('node:crypto');`,
1237
- `var parentPort = _wt.parentPort;`,
1238
- `var workerData = _wt.workerData;`,
1239
- ``,
1240
- `// Load SES using the parent module's resolution context`,
1241
- `var esmRequire = _mod.createRequire(workerData.parentUrl);`,
1242
- `esmRequire('ses');`,
1243
- ``,
1244
- `lockdown({`,
1245
- ` errorTaming: 'unsafe',`,
1246
- ` consoleTaming: 'unsafe',`,
1247
- ` evalTaming: 'safe-eval',`,
1248
- ` overrideTaming: 'moderate',`,
1249
- ` stackFiltering: 'verbose'`,
1250
- `});`,
1251
- ``,
1252
- `// Defense-in-depth: verify lockdown() actually hardened JSON.`,
1253
- `if (!Object.isFrozen(JSON)) {`,
1254
- ` throw new Error('SES lockdown failed: JSON is not frozen');`,
1255
- `}`,
1256
- ``,
1257
- `var cache = new Map();`,
1258
- ``,
1259
- `function hashSource(s) {`,
1260
- ` return _crypto.createHash('sha256').update(s).digest('hex');`,
1261
- `}`,
1262
- ``,
1263
- `function buildWrapper(source) {`,
1264
- ` return '(function() {' +`,
1265
- ` ' var fn = (' + source + ');\\n' +`,
1266
- ` ' if (typeof fn !== "function") return null;\\n' +`,
1267
- ` ' return function(jsonIn) {\\n' +`,
1268
- ` ' var data = JSON.parse(jsonIn);\\n' +`,
1269
- ` ' var result = fn(data);\\n' +`,
1270
- ` ' if (result !== null && typeof result === "object" && typeof result.then === "function") {\\n' +`,
1271
- ` ' return result.then(function(r) { return JSON.stringify(r); });\\n' +`,
1272
- ` ' }\\n' +`,
1273
- ` ' return JSON.stringify(result);\\n' +`,
1274
- ` ' };\\n' +`,
1275
- ` '})()';`,
1276
- `}`,
1277
- ``,
1278
- `function compileSource(source) {`,
1279
- ` var key = hashSource(source);`,
1280
- ` var cached = cache.get(key);`,
1281
- ` if (cached) return cached;`,
1282
- ``,
1283
- ` var compartmentFn;`,
1284
- ` try {`,
1285
- ` var c = new Compartment({ JSON: JSON });`,
1286
- ` compartmentFn = c.evaluate(buildWrapper(source));`,
1287
- ` } catch (err) {`,
1288
- ` throw new Error('Failed to compile migration source: ' + (err.message || String(err)));`,
1289
- ` }`,
1290
- ``,
1291
- ` if (typeof compartmentFn !== 'function') {`,
1292
- ` throw new Error('Migration source did not produce a function: ' + source.slice(0, 80));`,
1293
- ` }`,
1294
- ``,
1295
- ` cache.set(key, compartmentFn);`,
1296
- ` return compartmentFn;`,
1297
- `}`,
1298
- ``,
1299
- `parentPort.on('message', function(msg) {`,
1300
- ` var id = msg.id;`,
1301
- ` try {`,
1302
- ` if (msg.type === 'compile') {`,
1303
- ` compileSource(msg.source);`,
1304
- ` parentPort.postMessage({ id: id, type: 'compiled' });`,
1305
- ` return;`,
1306
- ` }`,
1307
- ` if (msg.type === 'execute') {`,
1308
- ` var fn = compileSource(msg.source);`,
1309
- ` var raw;`,
1310
- ` try {`,
1311
- ` raw = fn(msg.jsonData);`,
1312
- ` } catch (err) {`,
1313
- ` parentPort.postMessage({ id: id, type: 'error', message: 'Migration function threw: ' + (err.message || String(err)) });`,
1314
- ` return;`,
1315
- ` }`,
1316
- ` if (raw !== null && typeof raw === 'object' && typeof raw.then === 'function') {`,
1317
- ` raw.then(`,
1318
- ` function(jsonResult) {`,
1319
- ` if (jsonResult === undefined || jsonResult === null) {`,
1320
- ` parentPort.postMessage({ id: id, type: 'error', message: 'Migration returned a non-JSON-serializable value' });`,
1321
- ` } else {`,
1322
- ` parentPort.postMessage({ id: id, type: 'result', jsonResult: jsonResult });`,
1323
- ` }`,
1324
- ` },`,
1325
- ` function(err) {`,
1326
- ` parentPort.postMessage({ id: id, type: 'error', message: 'Async migration function threw: ' + (err.message || String(err)) });`,
1327
- ` }`,
1328
- ` );`,
1329
- ` return;`,
1330
- ` }`,
1331
- ` if (raw === undefined || raw === null) {`,
1332
- ` parentPort.postMessage({ id: id, type: 'error', message: 'Migration returned a non-JSON-serializable value' });`,
1333
- ` } else {`,
1334
- ` parentPort.postMessage({ id: id, type: 'result', jsonResult: raw });`,
1335
- ` }`,
1336
- ` }`,
1337
- ` } catch (err) {`,
1338
- ` parentPort.postMessage({ id: id, type: 'error', message: err.message || String(err) });`,
1339
- ` }`,
1340
- `});`
1341
- ].join("\n");
1342
- function ensureWorker() {
1343
- if (_worker) return _worker;
1344
- _worker = new Worker(WORKER_SOURCE, {
1345
- eval: true,
1346
- workerData: { parentUrl: import.meta.url }
1347
- });
1348
- _worker.unref();
1349
- _worker.on("message", (msg) => {
1350
- if (msg.id === void 0) return;
1351
- const pending = _pending.get(msg.id);
1352
- if (!pending) return;
1353
- _pending.delete(msg.id);
1354
- if (msg.type === "error") {
1355
- pending.reject(new MigrationError(msg.message ?? "Unknown sandbox error"));
1356
- } else {
1357
- pending.resolve(msg);
1358
- }
1359
- });
1360
- _worker.on("error", (err) => {
1361
- for (const [, p] of _pending) {
1362
- p.reject(new MigrationError(`Sandbox worker error: ${err.message}`));
1363
- }
1364
- _pending.clear();
1365
- _worker = null;
1366
- });
1367
- _worker.on("exit", (code) => {
1368
- if (_pending.size > 0) {
1369
- for (const [, p] of _pending) {
1370
- p.reject(new MigrationError(`Sandbox worker exited with code ${code}`));
1371
- }
1372
- _pending.clear();
1373
- }
1374
- _worker = null;
1375
- });
1376
- return _worker;
1377
- }
1378
- function sendToWorker(msg) {
1379
- const worker = ensureWorker();
1380
- if (_requestId >= Number.MAX_SAFE_INTEGER) _requestId = 0;
1381
- const id = ++_requestId;
1382
- return new Promise((resolve2, reject) => {
1383
- _pending.set(id, { resolve: resolve2, reject });
1384
- worker.postMessage({ ...msg, id });
1385
- });
1386
- }
1387
- var compiledCache = /* @__PURE__ */ new WeakMap();
1388
- function getExecutorCache(executor) {
1389
- let cache = compiledCache.get(executor);
1390
- if (!cache) {
1391
- cache = /* @__PURE__ */ new Map();
1392
- compiledCache.set(executor, cache);
1393
- }
1394
- return cache;
534
+ // src/internal/pipeline-adapter.ts
535
+ var _Pipelines = null;
536
+ async function getPipelines() {
537
+ if (!_Pipelines) {
538
+ const mod = await import("@google-cloud/firestore");
539
+ _Pipelines = mod.Pipelines;
540
+ }
541
+ return _Pipelines;
1395
542
  }
1396
- function hashSource(source) {
1397
- return createHash2("sha256").update(source).digest("hex");
543
+ function buildFilterExpression(P, filter) {
544
+ const { field: fieldName, op, value } = filter;
545
+ switch (op) {
546
+ case "==":
547
+ return P.equal(fieldName, value);
548
+ case "!=":
549
+ return P.notEqual(fieldName, value);
550
+ case "<":
551
+ return P.lessThan(fieldName, value);
552
+ case "<=":
553
+ return P.lessThanOrEqual(fieldName, value);
554
+ case ">":
555
+ return P.greaterThan(fieldName, value);
556
+ case ">=":
557
+ return P.greaterThanOrEqual(fieldName, value);
558
+ case "in":
559
+ return P.equalAny(fieldName, value);
560
+ case "not-in":
561
+ return P.notEqualAny(fieldName, value);
562
+ case "array-contains":
563
+ return P.arrayContains(fieldName, value);
564
+ case "array-contains-any":
565
+ return P.arrayContainsAny(fieldName, value);
566
+ default:
567
+ throw new Error(`Unsupported filter op for pipeline mode: ${op}`);
568
+ }
1398
569
  }
1399
- function defaultExecutor(source) {
1400
- ensureWorker();
1401
- return ((data) => {
1402
- const jsonData = JSON.stringify(serializeFirestoreTypes(data));
1403
- return sendToWorker({ type: "execute", source, jsonData }).then(
1404
- (response) => {
1405
- if (response.jsonResult === void 0 || response.jsonResult === null) {
1406
- throw new MigrationError("Migration returned a non-JSON-serializable value");
1407
- }
1408
- try {
1409
- return deserializeFirestoreTypes(JSON.parse(response.jsonResult));
1410
- } catch {
1411
- throw new MigrationError("Migration returned a non-JSON-serializable value");
1412
- }
570
+ function createPipelineQueryAdapter(db, collectionPath) {
571
+ return {
572
+ async query(filters, options) {
573
+ const P = await getPipelines();
574
+ let pipeline = db.pipeline().collection(collectionPath);
575
+ if (filters.length === 1) {
576
+ pipeline = pipeline.where(buildFilterExpression(P, filters[0]));
577
+ } else if (filters.length > 1) {
578
+ const [first, second, ...rest] = filters.map((f) => buildFilterExpression(P, f));
579
+ pipeline = pipeline.where(P.and(first, second, ...rest));
1413
580
  }
1414
- );
1415
- });
581
+ if (options?.orderBy) {
582
+ const f = P.field(options.orderBy.field);
583
+ const ordering = options.orderBy.direction === "desc" ? f.descending() : f.ascending();
584
+ pipeline = pipeline.sort(ordering);
585
+ }
586
+ if (options?.limit !== void 0) {
587
+ pipeline = pipeline.limit(options.limit);
588
+ }
589
+ const snap = await pipeline.execute();
590
+ return snap.results.map((r) => r.data());
591
+ }
592
+ };
1416
593
  }
1417
- async function precompileSource(source, executor) {
1418
- if (executor && executor !== defaultExecutor) {
1419
- try {
1420
- executor(source);
1421
- } catch (err) {
1422
- if (err instanceof MigrationError) throw err;
1423
- throw new MigrationError(
1424
- `Failed to compile migration source: ${err.message}`
1425
- );
594
+
595
+ // src/internal/firestore-backend.ts
596
+ function buildFirestoreUpdate(update, db) {
597
+ const out = {
598
+ updatedAt: FieldValue.serverTimestamp()
599
+ };
600
+ if (update.replaceData) {
601
+ out.data = deserializeFirestoreTypes(update.replaceData, db);
602
+ }
603
+ if (update.dataFields) {
604
+ for (const [k, v] of Object.entries(update.dataFields)) {
605
+ out[`data.${k}`] = v;
1426
606
  }
1427
- return;
1428
607
  }
1429
- await sendToWorker({ type: "compile", source });
1430
- }
1431
- function compileMigrationFn(source, executor = defaultExecutor) {
1432
- const cache = getExecutorCache(executor);
1433
- const key = hashSource(source);
1434
- const cached = cache.get(key);
1435
- if (cached) return cached;
1436
- try {
1437
- const fn = executor(source);
1438
- cache.set(key, fn);
1439
- return fn;
1440
- } catch (err) {
1441
- if (err instanceof MigrationError) throw err;
1442
- throw new MigrationError(
1443
- `Failed to compile migration source: ${err.message}`
1444
- );
608
+ if (update.v !== void 0) {
609
+ out.v = update.v;
1445
610
  }
611
+ return out;
1446
612
  }
1447
- function compileMigrations(stored, executor) {
1448
- return stored.map((step) => ({
1449
- fromVersion: step.fromVersion,
1450
- toVersion: step.toVersion,
1451
- up: compileMigrationFn(step.up, executor)
1452
- }));
1453
- }
1454
- async function destroySandboxWorker() {
1455
- if (!_worker) return;
1456
- const w = _worker;
1457
- _worker = null;
1458
- for (const [, p] of _pending) {
1459
- p.reject(new MigrationError("Sandbox worker terminated"));
1460
- }
1461
- _pending.clear();
1462
- await w.terminate();
613
+ function stampWritableRecord(record) {
614
+ const now = FieldValue.serverTimestamp();
615
+ const out = {
616
+ aType: record.aType,
617
+ aUid: record.aUid,
618
+ axbType: record.axbType,
619
+ bType: record.bType,
620
+ bUid: record.bUid,
621
+ data: record.data,
622
+ createdAt: now,
623
+ updatedAt: now
624
+ };
625
+ if (record.v !== void 0) out.v = record.v;
626
+ return out;
1463
627
  }
1464
-
1465
- // src/dynamic-registry.ts
1466
- var META_NODE_TYPE = "nodeType";
1467
- var META_EDGE_TYPE = "edgeType";
1468
- var STORED_MIGRATION_STEP_SCHEMA = {
1469
- type: "object",
1470
- required: ["fromVersion", "toVersion", "up"],
1471
- properties: {
1472
- fromVersion: { type: "integer", minimum: 0 },
1473
- toVersion: { type: "integer", minimum: 1 },
1474
- up: { type: "string", minLength: 1 }
1475
- },
1476
- additionalProperties: false
1477
- };
1478
- var NODE_TYPE_SCHEMA = {
1479
- type: "object",
1480
- required: ["name", "jsonSchema"],
1481
- properties: {
1482
- name: { type: "string", minLength: 1 },
1483
- jsonSchema: { type: "object" },
1484
- description: { type: "string" },
1485
- titleField: { type: "string" },
1486
- subtitleField: { type: "string" },
1487
- viewTemplate: { type: "string" },
1488
- viewCss: { type: "string" },
1489
- allowedIn: { type: "array", items: { type: "string", minLength: 1 } },
1490
- schemaVersion: { type: "integer", minimum: 0 },
1491
- migrations: { type: "array", items: STORED_MIGRATION_STEP_SCHEMA },
1492
- migrationWriteBack: { type: "string", enum: ["off", "eager", "background"] }
1493
- },
1494
- additionalProperties: false
1495
- };
1496
- var EDGE_TYPE_SCHEMA = {
1497
- type: "object",
1498
- required: ["name", "from", "to"],
1499
- properties: {
1500
- name: { type: "string", minLength: 1 },
1501
- from: {
1502
- oneOf: [
1503
- { type: "string", minLength: 1 },
1504
- { type: "array", items: { type: "string", minLength: 1 }, minItems: 1 }
1505
- ]
1506
- },
1507
- to: {
1508
- oneOf: [
1509
- { type: "string", minLength: 1 },
1510
- { type: "array", items: { type: "string", minLength: 1 }, minItems: 1 }
1511
- ]
1512
- },
1513
- jsonSchema: { type: "object" },
1514
- inverseLabel: { type: "string" },
1515
- description: { type: "string" },
1516
- titleField: { type: "string" },
1517
- subtitleField: { type: "string" },
1518
- viewTemplate: { type: "string" },
1519
- viewCss: { type: "string" },
1520
- allowedIn: { type: "array", items: { type: "string", minLength: 1 } },
1521
- targetGraph: { type: "string", minLength: 1, pattern: "^[^/]+$" },
1522
- schemaVersion: { type: "integer", minimum: 0 },
1523
- migrations: { type: "array", items: STORED_MIGRATION_STEP_SCHEMA },
1524
- migrationWriteBack: { type: "string", enum: ["off", "eager", "background"] }
1525
- },
1526
- additionalProperties: false
628
+ var FirestoreTransactionBackend = class {
629
+ constructor(adapter, db) {
630
+ this.adapter = adapter;
631
+ this.db = db;
632
+ }
633
+ getDoc(docId) {
634
+ return this.adapter.getDoc(docId);
635
+ }
636
+ query(filters, options) {
637
+ return this.adapter.query(filters, options);
638
+ }
639
+ async setDoc(docId, record) {
640
+ this.adapter.setDoc(docId, stampWritableRecord(record));
641
+ }
642
+ async updateDoc(docId, update) {
643
+ this.adapter.updateDoc(docId, buildFirestoreUpdate(update, this.db));
644
+ }
645
+ async deleteDoc(docId) {
646
+ this.adapter.deleteDoc(docId);
647
+ }
1527
648
  };
1528
- var BOOTSTRAP_ENTRIES = [
1529
- {
1530
- aType: META_NODE_TYPE,
1531
- axbType: NODE_RELATION,
1532
- bType: META_NODE_TYPE,
1533
- jsonSchema: NODE_TYPE_SCHEMA,
1534
- description: "Meta-type: defines a node type"
1535
- },
1536
- {
1537
- aType: META_EDGE_TYPE,
1538
- axbType: NODE_RELATION,
1539
- bType: META_EDGE_TYPE,
1540
- jsonSchema: EDGE_TYPE_SCHEMA,
1541
- description: "Meta-type: defines an edge type"
649
+ var FirestoreBatchBackend = class {
650
+ constructor(adapter, db) {
651
+ this.adapter = adapter;
652
+ this.db = db;
1542
653
  }
1543
- ];
1544
- function createBootstrapRegistry() {
1545
- return createRegistry([...BOOTSTRAP_ENTRIES]);
1546
- }
1547
- function generateDeterministicUid(metaType, name) {
1548
- const hash = createHash3("sha256").update(`${metaType}:${name}`).digest("base64url");
1549
- return hash.slice(0, 21);
1550
- }
1551
- async function createRegistryFromGraph(reader, executor) {
1552
- const [nodeTypes, edgeTypes] = await Promise.all([
1553
- reader.findNodes({ aType: META_NODE_TYPE }),
1554
- reader.findNodes({ aType: META_EDGE_TYPE })
1555
- ]);
1556
- const entries = [...BOOTSTRAP_ENTRIES];
1557
- const prevalidations = [];
1558
- for (const record of nodeTypes) {
1559
- const data = record.data;
1560
- if (data.migrations) {
1561
- for (const m of data.migrations) {
1562
- prevalidations.push(precompileSource(m.up, executor));
1563
- }
1564
- }
654
+ setDoc(docId, record) {
655
+ this.adapter.setDoc(docId, stampWritableRecord(record));
1565
656
  }
1566
- for (const record of edgeTypes) {
1567
- const data = record.data;
1568
- if (data.migrations) {
1569
- for (const m of data.migrations) {
1570
- prevalidations.push(precompileSource(m.up, executor));
1571
- }
1572
- }
657
+ updateDoc(docId, update) {
658
+ this.adapter.updateDoc(docId, buildFirestoreUpdate(update, this.db));
1573
659
  }
1574
- await Promise.all(prevalidations);
1575
- for (const record of nodeTypes) {
1576
- const data = record.data;
1577
- entries.push({
1578
- aType: data.name,
1579
- axbType: NODE_RELATION,
1580
- bType: data.name,
1581
- jsonSchema: data.jsonSchema,
1582
- description: data.description,
1583
- titleField: data.titleField,
1584
- subtitleField: data.subtitleField,
1585
- allowedIn: data.allowedIn,
1586
- migrations: data.migrations ? compileMigrations(data.migrations, executor) : void 0,
1587
- migrationWriteBack: data.migrationWriteBack
1588
- });
660
+ deleteDoc(docId) {
661
+ this.adapter.deleteDoc(docId);
1589
662
  }
1590
- for (const record of edgeTypes) {
1591
- const data = record.data;
1592
- const fromTypes = Array.isArray(data.from) ? data.from : [data.from];
1593
- const toTypes = Array.isArray(data.to) ? data.to : [data.to];
1594
- const compiledMigrations = data.migrations ? compileMigrations(data.migrations, executor) : void 0;
1595
- for (const aType of fromTypes) {
1596
- for (const bType of toTypes) {
1597
- entries.push({
1598
- aType,
1599
- axbType: data.name,
1600
- bType,
1601
- jsonSchema: data.jsonSchema,
1602
- description: data.description,
1603
- inverseLabel: data.inverseLabel,
1604
- titleField: data.titleField,
1605
- subtitleField: data.subtitleField,
1606
- allowedIn: data.allowedIn,
1607
- targetGraph: data.targetGraph,
1608
- migrations: compiledMigrations,
1609
- migrationWriteBack: data.migrationWriteBack
1610
- });
1611
- }
1612
- }
663
+ commit() {
664
+ return this.adapter.commit();
1613
665
  }
1614
- return createRegistry(entries);
1615
- }
1616
-
1617
- // src/client.ts
1618
- var _standardModeWarned = false;
1619
- var RESERVED_TYPE_NAMES = /* @__PURE__ */ new Set([META_NODE_TYPE, META_EDGE_TYPE]);
1620
- var GraphClientImpl = class _GraphClientImpl {
1621
- constructor(db, collectionPath, options, scopePath = "") {
666
+ };
667
+ var FirestoreBackendImpl = class _FirestoreBackendImpl {
668
+ constructor(db, collectionPath, queryMode, scopePath) {
1622
669
  this.db = db;
670
+ this.queryMode = queryMode;
671
+ this.collectionPath = collectionPath;
1623
672
  this.scopePath = scopePath;
1624
673
  this.adapter = createFirestoreAdapter(db, collectionPath);
1625
- this.globalWriteBack = options?.migrationWriteBack ?? "off";
1626
- this.migrationSandbox = options?.migrationSandbox;
1627
- if (options?.registryMode) {
1628
- this.dynamicConfig = options.registryMode;
1629
- this.bootstrapRegistry = createBootstrapRegistry();
1630
- if (options.registry) {
1631
- this.staticRegistry = options.registry;
1632
- }
1633
- const metaCollectionPath = options.registryMode.collection;
1634
- if (metaCollectionPath && metaCollectionPath !== collectionPath) {
1635
- this.metaAdapter = createFirestoreAdapter(db, metaCollectionPath);
1636
- }
1637
- } else {
1638
- this.staticRegistry = options?.registry;
1639
- }
1640
- const requestedMode = options?.queryMode ?? "pipeline";
1641
- const isEmulator = !!process.env.FIRESTORE_EMULATOR_HOST;
1642
- if (isEmulator) {
1643
- this.queryMode = "standard";
1644
- } else {
1645
- this.queryMode = requestedMode;
1646
- }
1647
- if (this.queryMode === "standard" && !isEmulator && requestedMode === "standard" && !_standardModeWarned) {
1648
- _standardModeWarned = true;
1649
- console.warn(
1650
- "[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"
1651
- );
1652
- }
1653
- this.scanProtection = options?.scanProtection ?? "error";
1654
- if (this.queryMode === "pipeline") {
674
+ if (queryMode === "pipeline") {
1655
675
  this.pipelineAdapter = createPipelineQueryAdapter(db, collectionPath);
1656
- if (this.metaAdapter) {
1657
- this.metaPipelineAdapter = createPipelineQueryAdapter(
1658
- db,
1659
- options.registryMode.collection
1660
- );
1661
- }
1662
676
  }
1663
677
  }
678
+ collectionPath;
679
+ scopePath;
1664
680
  adapter;
1665
681
  pipelineAdapter;
1666
- queryMode;
1667
- scanProtection;
1668
- // Static mode
1669
- staticRegistry;
1670
- // Dynamic mode
1671
- dynamicConfig;
1672
- bootstrapRegistry;
1673
- dynamicRegistry;
1674
- metaAdapter;
1675
- metaPipelineAdapter;
1676
- // Subgraph scope tracking
1677
- scopePath;
1678
- // Migration settings
1679
- globalWriteBack;
1680
- migrationSandbox;
1681
- // ---------------------------------------------------------------------------
1682
- // Registry routing
1683
- // ---------------------------------------------------------------------------
1684
- /**
1685
- * Get the appropriate registry for validating a write to the given type.
1686
- *
1687
- * - Static-only mode: returns staticRegistry (or undefined if none set)
1688
- * - Dynamic mode (pure or merged):
1689
- * - Meta-types (nodeType, edgeType): validated against bootstrapRegistry
1690
- * - Domain types: validated against dynamicRegistry (falls back to
1691
- * bootstrapRegistry which rejects unknown types)
1692
- * - Merged mode: dynamicRegistry is a merged wrapper (static + dynamic
1693
- * extension), so static entries take priority automatically.
1694
- */
1695
- getRegistryForType(aType) {
1696
- if (!this.dynamicConfig) return this.staticRegistry;
1697
- if (aType === META_NODE_TYPE || aType === META_EDGE_TYPE) {
1698
- return this.bootstrapRegistry;
1699
- }
1700
- return this.dynamicRegistry ?? this.staticRegistry ?? this.bootstrapRegistry;
1701
- }
1702
- /**
1703
- * Get the Firestore adapter for writing the given type.
1704
- * Meta-types route to metaAdapter if a separate collection is configured.
1705
- */
1706
- getAdapterForType(aType) {
1707
- if (this.metaAdapter && (aType === META_NODE_TYPE || aType === META_EDGE_TYPE)) {
1708
- return this.metaAdapter;
1709
- }
1710
- return this.adapter;
1711
- }
1712
- /**
1713
- * Get the combined registry for transaction/batch context.
1714
- * In static-only mode, returns staticRegistry.
1715
- * In dynamic mode, returns dynamicRegistry (which includes bootstrap entries)
1716
- * or falls back to staticRegistry (merged mode) or bootstrapRegistry.
1717
- */
1718
- getCombinedRegistry() {
1719
- if (!this.dynamicConfig) return this.staticRegistry;
1720
- return this.dynamicRegistry ?? this.staticRegistry ?? this.bootstrapRegistry;
682
+ // --- Reads ---
683
+ getDoc(docId) {
684
+ return this.adapter.getDoc(docId);
1721
685
  }
1722
- // ---------------------------------------------------------------------------
1723
- // Query dispatch
1724
- // ---------------------------------------------------------------------------
1725
- /**
1726
- * Dispatch a query to the appropriate adapter based on queryMode.
1727
- * Pipeline queries use the PipelineQueryAdapter; standard queries
1728
- * use the FirestoreAdapter.
1729
- */
1730
- executeQuery(filters, options) {
686
+ query(filters, options) {
1731
687
  if (this.pipelineAdapter) {
1732
688
  return this.pipelineAdapter.query(filters, options);
1733
689
  }
1734
690
  return this.adapter.query(filters, options);
1735
691
  }
1736
- /**
1737
- * Check whether a query's filter set is safe (matches a known index pattern).
1738
- * Throws QuerySafetyError or logs a warning depending on scanProtection config.
1739
- */
1740
- checkQuerySafety(filters, allowCollectionScan) {
1741
- if (allowCollectionScan || this.scanProtection === "off") return;
1742
- const result = analyzeQuerySafety(filters);
1743
- if (result.safe) return;
1744
- if (this.scanProtection === "error") {
1745
- throw new QuerySafetyError(result.reason);
1746
- }
1747
- console.warn(`[firegraph] Query safety warning: ${result.reason}`);
1748
- }
1749
- // ---------------------------------------------------------------------------
1750
- // Migration helpers
1751
- // ---------------------------------------------------------------------------
1752
- /**
1753
- * Apply migration to a single record. Returns the (possibly migrated)
1754
- * record and triggers write-back if applicable.
1755
- */
1756
- async applyMigration(record, docId) {
1757
- const registry = this.getCombinedRegistry();
1758
- if (!registry) return record;
1759
- const result = await migrateRecord(record, registry, this.globalWriteBack);
1760
- if (result.migrated) {
1761
- this.handleWriteBack(result, docId);
1762
- }
1763
- return result.record;
1764
- }
1765
- /**
1766
- * Apply migrations to an array of records. Returns all records
1767
- * (migrated where applicable) and triggers write-backs.
1768
- */
1769
- async applyMigrations(records) {
1770
- const registry = this.getCombinedRegistry();
1771
- if (!registry || records.length === 0) return records;
1772
- const results = await migrateRecords(records, registry, this.globalWriteBack);
1773
- for (const result of results) {
1774
- if (result.migrated) {
1775
- const docId = result.record.axbType === NODE_RELATION ? computeNodeDocId(result.record.aUid) : computeEdgeDocId(result.record.aUid, result.record.axbType, result.record.bUid);
1776
- this.handleWriteBack(result, docId);
1777
- }
1778
- }
1779
- return results.map((r) => r.record);
1780
- }
1781
- /**
1782
- * Handle write-back for a migrated record based on the resolved mode.
1783
- *
1784
- * Both `'eager'` and `'background'` are fire-and-forget (not awaited by
1785
- * the caller). The difference is logging level on failure:
1786
- * - `eager`: logs an error via `console.error`
1787
- * - `background`: logs a warning via `console.warn`
1788
- *
1789
- * For truly synchronous write-back guarantees, use transactions — the
1790
- * `GraphTransactionImpl` performs write-back inline within the transaction.
1791
- */
1792
- handleWriteBack(result, docId) {
1793
- if (result.writeBack === "off") return;
1794
- const doWriteBack = async () => {
1795
- try {
1796
- const update = {
1797
- data: deserializeFirestoreTypes(result.record.data, this.db),
1798
- updatedAt: FieldValue5.serverTimestamp()
1799
- };
1800
- if (result.record.v !== void 0) {
1801
- update.v = result.record.v;
1802
- }
1803
- await this.adapter.updateDoc(docId, update);
1804
- } catch (err) {
1805
- const msg = `[firegraph] Migration write-back failed for ${docId}: ${err.message}`;
1806
- if (result.writeBack === "eager") {
1807
- console.error(msg);
1808
- } else {
1809
- console.warn(msg);
1810
- }
1811
- }
1812
- };
1813
- void doWriteBack();
1814
- }
1815
- // ---------------------------------------------------------------------------
1816
- // GraphReader
1817
- // ---------------------------------------------------------------------------
1818
- async getNode(uid) {
1819
- const docId = computeNodeDocId(uid);
1820
- const record = await this.adapter.getDoc(docId);
1821
- if (!record) return null;
1822
- return this.applyMigration(record, docId);
1823
- }
1824
- async getEdge(aUid, axbType, bUid) {
1825
- const docId = computeEdgeDocId(aUid, axbType, bUid);
1826
- const record = await this.adapter.getDoc(docId);
1827
- if (!record) return null;
1828
- return this.applyMigration(record, docId);
1829
- }
1830
- async edgeExists(aUid, axbType, bUid) {
1831
- const docId = computeEdgeDocId(aUid, axbType, bUid);
1832
- const record = await this.adapter.getDoc(docId);
1833
- return record !== null;
1834
- }
1835
- async findEdges(params) {
1836
- const plan = buildEdgeQueryPlan(params);
1837
- let records;
1838
- if (plan.strategy === "get") {
1839
- const record = await this.adapter.getDoc(plan.docId);
1840
- records = record ? [record] : [];
1841
- } else {
1842
- this.checkQuerySafety(plan.filters, params.allowCollectionScan);
1843
- records = await this.executeQuery(plan.filters, plan.options);
1844
- }
1845
- return this.applyMigrations(records);
1846
- }
1847
- async findNodes(params) {
1848
- const plan = buildNodeQueryPlan(params);
1849
- let records;
1850
- if (plan.strategy === "get") {
1851
- const record = await this.adapter.getDoc(plan.docId);
1852
- records = record ? [record] : [];
1853
- } else {
1854
- this.checkQuerySafety(plan.filters, params.allowCollectionScan);
1855
- records = await this.executeQuery(plan.filters, plan.options);
1856
- }
1857
- return this.applyMigrations(records);
1858
- }
1859
- // ---------------------------------------------------------------------------
1860
- // GraphWriter
1861
- // ---------------------------------------------------------------------------
1862
- async putNode(aType, uid, data) {
1863
- const registry = this.getRegistryForType(aType);
1864
- if (registry) {
1865
- registry.validate(aType, NODE_RELATION, aType, data, this.scopePath);
1866
- }
1867
- const adapter = this.getAdapterForType(aType);
1868
- const docId = computeNodeDocId(uid);
1869
- const record = buildNodeRecord(aType, uid, data);
1870
- if (registry) {
1871
- const entry = registry.lookup(aType, NODE_RELATION, aType);
1872
- if (entry?.schemaVersion && entry.schemaVersion > 0) {
1873
- record.v = entry.schemaVersion;
1874
- }
1875
- }
1876
- await adapter.setDoc(docId, record);
1877
- }
1878
- async putEdge(aType, aUid, axbType, bType, bUid, data) {
1879
- const registry = this.getRegistryForType(aType);
1880
- if (registry) {
1881
- registry.validate(aType, axbType, bType, data, this.scopePath);
1882
- }
1883
- const adapter = this.getAdapterForType(aType);
1884
- const docId = computeEdgeDocId(aUid, axbType, bUid);
1885
- const record = buildEdgeRecord(aType, aUid, axbType, bType, bUid, data);
1886
- if (registry) {
1887
- const entry = registry.lookup(aType, axbType, bType);
1888
- if (entry?.schemaVersion && entry.schemaVersion > 0) {
1889
- record.v = entry.schemaVersion;
1890
- }
1891
- }
1892
- await adapter.setDoc(docId, record);
1893
- }
1894
- async updateNode(uid, data) {
1895
- const docId = computeNodeDocId(uid);
1896
- const update = {
1897
- updatedAt: FieldValue5.serverTimestamp()
1898
- };
1899
- for (const [k, v] of Object.entries(data)) {
1900
- update[`data.${k}`] = v;
1901
- }
1902
- await this.adapter.updateDoc(docId, update);
692
+ // --- Writes ---
693
+ setDoc(docId, record) {
694
+ return this.adapter.setDoc(docId, stampWritableRecord(record));
1903
695
  }
1904
- async removeNode(uid) {
1905
- const docId = computeNodeDocId(uid);
1906
- await this.adapter.deleteDoc(docId);
696
+ updateDoc(docId, update) {
697
+ return this.adapter.updateDoc(docId, buildFirestoreUpdate(update, this.db));
1907
698
  }
1908
- async removeEdge(aUid, axbType, bUid) {
1909
- const docId = computeEdgeDocId(aUid, axbType, bUid);
1910
- await this.adapter.deleteDoc(docId);
699
+ deleteDoc(docId) {
700
+ return this.adapter.deleteDoc(docId);
1911
701
  }
1912
- // ---------------------------------------------------------------------------
1913
- // Transactions & Batches
1914
- // ---------------------------------------------------------------------------
1915
- async runTransaction(fn) {
702
+ // --- Transactions / Batches ---
703
+ runTransaction(fn) {
1916
704
  return this.db.runTransaction(async (firestoreTx) => {
1917
- const adapter = createTransactionAdapter(
1918
- this.db,
1919
- this.adapter.collectionPath,
1920
- firestoreTx
1921
- );
1922
- const graphTx = new GraphTransactionImpl(adapter, this.getCombinedRegistry(), this.scanProtection, this.scopePath, this.globalWriteBack, this.db);
1923
- return fn(graphTx);
705
+ const txAdapter = createTransactionAdapter(this.db, this.collectionPath, firestoreTx);
706
+ return fn(new FirestoreTransactionBackend(txAdapter, this.db));
1924
707
  });
1925
708
  }
1926
- batch() {
1927
- const adapter = createBatchAdapter(this.db, this.adapter.collectionPath);
1928
- return new GraphBatchImpl(adapter, this.getCombinedRegistry(), this.scopePath);
709
+ createBatch() {
710
+ const batchAdapter = createBatchAdapter(this.db, this.collectionPath);
711
+ return new FirestoreBatchBackend(batchAdapter, this.db);
1929
712
  }
1930
- // ---------------------------------------------------------------------------
1931
- // Subgraph
1932
- // ---------------------------------------------------------------------------
1933
- subgraph(parentNodeUid, name = "graph") {
1934
- if (!parentNodeUid || parentNodeUid.includes("/")) {
1935
- throw new FiregraphError(
1936
- `Invalid parentNodeUid for subgraph: "${parentNodeUid}". Must be a non-empty string without "/".`,
1937
- "INVALID_SUBGRAPH"
1938
- );
1939
- }
1940
- if (name.includes("/")) {
1941
- throw new FiregraphError(
1942
- `Subgraph name must not contain "/": got "${name}". Use chained .subgraph() calls for nested subgraphs.`,
1943
- "INVALID_SUBGRAPH"
1944
- );
1945
- }
1946
- const subCollectionPath = `${this.adapter.collectionPath}/${parentNodeUid}/${name}`;
1947
- const newScopePath = this.scopePath ? `${this.scopePath}/${name}` : name;
1948
- return new _GraphClientImpl(
1949
- this.db,
1950
- subCollectionPath,
1951
- {
1952
- registry: this.getCombinedRegistry(),
1953
- queryMode: this.queryMode === "pipeline" ? "pipeline" : "standard",
1954
- scanProtection: this.scanProtection,
1955
- migrationWriteBack: this.globalWriteBack,
1956
- migrationSandbox: this.migrationSandbox
1957
- },
1958
- newScopePath
1959
- );
713
+ // --- Subgraphs ---
714
+ subgraph(parentNodeUid, name) {
715
+ const subPath = `${this.collectionPath}/${parentNodeUid}/${name}`;
716
+ const newScope = this.scopePath ? `${this.scopePath}/${name}` : name;
717
+ return new _FirestoreBackendImpl(this.db, subPath, this.queryMode, newScope);
718
+ }
719
+ // --- Cascade & bulk ---
720
+ removeNodeCascade(uid, reader, options) {
721
+ return removeNodeCascade(this.db, this.collectionPath, reader, uid, options);
1960
722
  }
1961
- // ---------------------------------------------------------------------------
1962
- // Collection group query
1963
- // ---------------------------------------------------------------------------
723
+ bulkRemoveEdges(params, reader, options) {
724
+ return bulkRemoveEdges(this.db, this.collectionPath, reader, params, options);
725
+ }
726
+ // --- Cross-collection ---
1964
727
  async findEdgesGlobal(params, collectionName) {
1965
- const name = collectionName ?? this.adapter.collectionPath.split("/").pop();
728
+ const name = collectionName ?? this.collectionPath.split("/").pop();
1966
729
  const plan = buildEdgeQueryPlan(params);
1967
730
  if (plan.strategy === "get") {
1968
731
  throw new FiregraphError(
@@ -1970,7 +733,6 @@ var GraphClientImpl = class _GraphClientImpl {
1970
733
  "INVALID_QUERY"
1971
734
  );
1972
735
  }
1973
- this.checkQuerySafety(plan.filters, params.allowCollectionScan);
1974
736
  const collectionGroupRef = this.db.collectionGroup(name);
1975
737
  let q = collectionGroupRef;
1976
738
  for (const f of plan.filters) {
@@ -1983,170 +745,35 @@ var GraphClientImpl = class _GraphClientImpl {
1983
745
  q = q.limit(plan.options.limit);
1984
746
  }
1985
747
  const snap = await q.get();
1986
- const records = snap.docs.map((doc) => doc.data());
1987
- return this.applyMigrations(records);
1988
- }
1989
- // ---------------------------------------------------------------------------
1990
- // Bulk operations
1991
- // ---------------------------------------------------------------------------
1992
- async removeNodeCascade(uid, options) {
1993
- return removeNodeCascade(this.db, this.adapter.collectionPath, this, uid, options);
1994
- }
1995
- async bulkRemoveEdges(params, options) {
1996
- return bulkRemoveEdges(this.db, this.adapter.collectionPath, this, params, options);
1997
- }
1998
- // ---------------------------------------------------------------------------
1999
- // Dynamic registry methods
2000
- // ---------------------------------------------------------------------------
2001
- async defineNodeType(name, jsonSchema, description, options) {
2002
- if (!this.dynamicConfig) {
2003
- throw new DynamicRegistryError(
2004
- 'defineNodeType() is only available in dynamic registry mode. Pass registryMode: { mode: "dynamic" } to createGraphClient().'
2005
- );
2006
- }
2007
- if (RESERVED_TYPE_NAMES.has(name)) {
2008
- throw new DynamicRegistryError(
2009
- `Cannot define type "${name}": this name is reserved for the meta-registry.`
2010
- );
2011
- }
2012
- if (this.staticRegistry?.lookup(name, NODE_RELATION, name)) {
2013
- throw new DynamicRegistryError(
2014
- `Cannot define node type "${name}": already defined in the static registry.`
2015
- );
2016
- }
2017
- const uid = generateDeterministicUid(META_NODE_TYPE, name);
2018
- const data = { name, jsonSchema };
2019
- if (description !== void 0) data.description = description;
2020
- if (options?.titleField !== void 0) data.titleField = options.titleField;
2021
- if (options?.subtitleField !== void 0) data.subtitleField = options.subtitleField;
2022
- if (options?.viewTemplate !== void 0) data.viewTemplate = options.viewTemplate;
2023
- if (options?.viewCss !== void 0) data.viewCss = options.viewCss;
2024
- if (options?.allowedIn !== void 0) data.allowedIn = options.allowedIn;
2025
- if (options?.migrationWriteBack !== void 0) data.migrationWriteBack = options.migrationWriteBack;
2026
- if (options?.migrations !== void 0) {
2027
- data.migrations = await this.serializeMigrations(options.migrations);
2028
- }
2029
- await this.putNode(META_NODE_TYPE, uid, data);
2030
- }
2031
- async defineEdgeType(name, topology, jsonSchema, description, options) {
2032
- if (!this.dynamicConfig) {
2033
- throw new DynamicRegistryError(
2034
- 'defineEdgeType() is only available in dynamic registry mode. Pass registryMode: { mode: "dynamic" } to createGraphClient().'
2035
- );
2036
- }
2037
- if (RESERVED_TYPE_NAMES.has(name)) {
2038
- throw new DynamicRegistryError(
2039
- `Cannot define type "${name}": this name is reserved for the meta-registry.`
2040
- );
2041
- }
2042
- if (this.staticRegistry) {
2043
- const fromTypes = Array.isArray(topology.from) ? topology.from : [topology.from];
2044
- const toTypes = Array.isArray(topology.to) ? topology.to : [topology.to];
2045
- for (const aType of fromTypes) {
2046
- for (const bType of toTypes) {
2047
- if (this.staticRegistry.lookup(aType, name, bType)) {
2048
- throw new DynamicRegistryError(
2049
- `Cannot define edge type "${name}" for (${aType}) -> (${bType}): already defined in the static registry.`
2050
- );
2051
- }
2052
- }
2053
- }
2054
- }
2055
- const uid = generateDeterministicUid(META_EDGE_TYPE, name);
2056
- const data = {
2057
- name,
2058
- from: topology.from,
2059
- to: topology.to
2060
- };
2061
- if (jsonSchema !== void 0) data.jsonSchema = jsonSchema;
2062
- if (topology.inverseLabel !== void 0) data.inverseLabel = topology.inverseLabel;
2063
- if (topology.targetGraph !== void 0) data.targetGraph = topology.targetGraph;
2064
- if (description !== void 0) data.description = description;
2065
- if (options?.titleField !== void 0) data.titleField = options.titleField;
2066
- if (options?.subtitleField !== void 0) data.subtitleField = options.subtitleField;
2067
- if (options?.viewTemplate !== void 0) data.viewTemplate = options.viewTemplate;
2068
- if (options?.viewCss !== void 0) data.viewCss = options.viewCss;
2069
- if (options?.allowedIn !== void 0) data.allowedIn = options.allowedIn;
2070
- if (options?.migrationWriteBack !== void 0) data.migrationWriteBack = options.migrationWriteBack;
2071
- if (options?.migrations !== void 0) {
2072
- data.migrations = await this.serializeMigrations(options.migrations);
2073
- }
2074
- await this.putNode(META_EDGE_TYPE, uid, data);
2075
- }
2076
- async reloadRegistry() {
2077
- if (!this.dynamicConfig) {
2078
- throw new DynamicRegistryError(
2079
- 'reloadRegistry() is only available in dynamic registry mode. Pass registryMode: { mode: "dynamic" } to createGraphClient().'
2080
- );
2081
- }
2082
- const reader = this.createMetaReader();
2083
- const dynamicOnly = await createRegistryFromGraph(reader, this.migrationSandbox);
2084
- if (this.staticRegistry) {
2085
- this.dynamicRegistry = createMergedRegistry(this.staticRegistry, dynamicOnly);
2086
- } else {
2087
- this.dynamicRegistry = dynamicOnly;
2088
- }
748
+ return snap.docs.map((doc) => doc.data());
2089
749
  }
2090
- /**
2091
- * Serialize migration steps for storage in Firestore.
2092
- * Function objects are converted via `.toString()`; strings are stored as-is.
2093
- * Each migration is validated at define-time by pre-compiling in the sandbox.
2094
- */
2095
- async serializeMigrations(migrations) {
2096
- const result = migrations.map((m) => {
2097
- const source = typeof m.up === "function" ? m.up.toString() : m.up;
2098
- return { fromVersion: m.fromVersion, toVersion: m.toVersion, up: source };
2099
- });
2100
- await Promise.all(
2101
- result.map((m) => precompileSource(m.up, this.migrationSandbox))
750
+ };
751
+ function createFirestoreBackend(db, collectionPath, options = {}) {
752
+ const queryMode = options.queryMode ?? "pipeline";
753
+ const scopePath = options.scopePath ?? "";
754
+ return new FirestoreBackendImpl(db, collectionPath, queryMode, scopePath);
755
+ }
756
+
757
+ // src/firestore.ts
758
+ var _standardModeWarned = false;
759
+ function createGraphClient(db, collectionPath, options) {
760
+ const requestedMode = options?.queryMode ?? "pipeline";
761
+ const isEmulator = !!process.env.FIRESTORE_EMULATOR_HOST;
762
+ const effectiveMode = isEmulator ? "standard" : requestedMode;
763
+ if (effectiveMode === "standard" && !isEmulator && requestedMode === "standard" && !_standardModeWarned) {
764
+ _standardModeWarned = true;
765
+ console.warn(
766
+ "[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"
2102
767
  );
2103
- return result;
2104
768
  }
2105
- /**
2106
- * Create a GraphReader for the meta-collection.
2107
- * If meta-collection is the same as main collection, returns `this`.
2108
- * If separate, creates a lightweight reader wrapping the meta adapter.
2109
- */
2110
- createMetaReader() {
2111
- if (!this.metaAdapter) return this;
2112
- const adapter = this.metaAdapter;
2113
- const pipelineAdapter = this.metaPipelineAdapter;
2114
- const executeMetaQuery = (filters, options) => {
2115
- if (pipelineAdapter) return pipelineAdapter.query(filters, options);
2116
- return adapter.query(filters, options);
2117
- };
2118
- return {
2119
- async getNode(uid) {
2120
- return adapter.getDoc(computeNodeDocId(uid));
2121
- },
2122
- async getEdge(aUid, axbType, bUid) {
2123
- return adapter.getDoc(computeEdgeDocId(aUid, axbType, bUid));
2124
- },
2125
- async edgeExists(aUid, axbType, bUid) {
2126
- const record = await adapter.getDoc(computeEdgeDocId(aUid, axbType, bUid));
2127
- return record !== null;
2128
- },
2129
- async findEdges(params) {
2130
- const plan = buildEdgeQueryPlan(params);
2131
- if (plan.strategy === "get") {
2132
- const record = await adapter.getDoc(plan.docId);
2133
- return record ? [record] : [];
2134
- }
2135
- return executeMetaQuery(plan.filters, plan.options);
2136
- },
2137
- async findNodes(params) {
2138
- const plan = buildNodeQueryPlan(params);
2139
- if (plan.strategy === "get") {
2140
- const record = await adapter.getDoc(plan.docId);
2141
- return record ? [record] : [];
2142
- }
2143
- return executeMetaQuery(plan.filters, plan.options);
2144
- }
2145
- };
769
+ const backend = createFirestoreBackend(db, collectionPath, { queryMode: effectiveMode });
770
+ let metaBackend;
771
+ if (options?.registryMode?.collection && options.registryMode.collection !== collectionPath) {
772
+ metaBackend = createFirestoreBackend(db, options.registryMode.collection, {
773
+ queryMode: effectiveMode
774
+ });
2146
775
  }
2147
- };
2148
- function createGraphClient(db, collectionPath, options) {
2149
- return new GraphClientImpl(db, collectionPath, options);
776
+ return new GraphClientImpl(backend, options, metaBackend);
2150
777
  }
2151
778
 
2152
779
  // src/id.ts
@@ -2155,22 +782,175 @@ function generateId() {
2155
782
  return nanoid();
2156
783
  }
2157
784
 
2158
- // src/traverse.ts
2159
- var DEFAULT_LIMIT = 10;
2160
- var DEFAULT_MAX_READS = 100;
2161
- var DEFAULT_CONCURRENCY = 5;
2162
- var _crossGraphWarned = false;
2163
- function isGraphClient(reader) {
2164
- return "subgraph" in reader && typeof reader.subgraph === "function";
2165
- }
2166
- var Semaphore = class {
2167
- constructor(slots) {
2168
- this.slots = slots;
2169
- }
2170
- queue = [];
2171
- active = 0;
2172
- async acquire() {
2173
- if (this.active < this.slots) {
785
+ // src/indexes.ts
786
+ function baseIndexes(collection) {
787
+ return [
788
+ {
789
+ collectionGroup: collection,
790
+ queryScope: "COLLECTION",
791
+ fields: [
792
+ { fieldPath: "aUid", order: "ASCENDING" },
793
+ { fieldPath: "axbType", order: "ASCENDING" }
794
+ ]
795
+ },
796
+ {
797
+ collectionGroup: collection,
798
+ queryScope: "COLLECTION",
799
+ fields: [
800
+ { fieldPath: "axbType", order: "ASCENDING" },
801
+ { fieldPath: "bUid", order: "ASCENDING" }
802
+ ]
803
+ },
804
+ {
805
+ collectionGroup: collection,
806
+ queryScope: "COLLECTION",
807
+ fields: [
808
+ { fieldPath: "aType", order: "ASCENDING" },
809
+ { fieldPath: "axbType", order: "ASCENDING" }
810
+ ]
811
+ },
812
+ {
813
+ collectionGroup: collection,
814
+ queryScope: "COLLECTION",
815
+ fields: [
816
+ { fieldPath: "axbType", order: "ASCENDING" },
817
+ { fieldPath: "bType", order: "ASCENDING" }
818
+ ]
819
+ }
820
+ ];
821
+ }
822
+ function extractSchemaFields(schema) {
823
+ const s = schema;
824
+ if (s.type !== "object" || !s.properties) return [];
825
+ return Object.keys(s.properties);
826
+ }
827
+ function collectionGroupIndexes(collectionName) {
828
+ return [
829
+ {
830
+ collectionGroup: collectionName,
831
+ queryScope: "COLLECTION_GROUP",
832
+ fields: [
833
+ { fieldPath: "aUid", order: "ASCENDING" },
834
+ { fieldPath: "axbType", order: "ASCENDING" }
835
+ ]
836
+ },
837
+ {
838
+ collectionGroup: collectionName,
839
+ queryScope: "COLLECTION_GROUP",
840
+ fields: [
841
+ { fieldPath: "axbType", order: "ASCENDING" },
842
+ { fieldPath: "bUid", order: "ASCENDING" }
843
+ ]
844
+ },
845
+ {
846
+ collectionGroup: collectionName,
847
+ queryScope: "COLLECTION_GROUP",
848
+ fields: [
849
+ { fieldPath: "aType", order: "ASCENDING" },
850
+ { fieldPath: "axbType", order: "ASCENDING" }
851
+ ]
852
+ },
853
+ {
854
+ collectionGroup: collectionName,
855
+ queryScope: "COLLECTION_GROUP",
856
+ fields: [
857
+ { fieldPath: "axbType", order: "ASCENDING" },
858
+ { fieldPath: "bType", order: "ASCENDING" }
859
+ ]
860
+ }
861
+ ];
862
+ }
863
+ function generateIndexConfig(collection, entities, registryEntries) {
864
+ const indexes = baseIndexes(collection);
865
+ if (entities) {
866
+ for (const [, entity] of entities.nodes) {
867
+ const fields = extractSchemaFields(entity.schema);
868
+ for (const field of fields) {
869
+ indexes.push({
870
+ collectionGroup: collection,
871
+ queryScope: "COLLECTION",
872
+ fields: [
873
+ { fieldPath: "aType", order: "ASCENDING" },
874
+ { fieldPath: "axbType", order: "ASCENDING" },
875
+ { fieldPath: `data.${field}`, order: "ASCENDING" }
876
+ ]
877
+ });
878
+ }
879
+ }
880
+ for (const [, entity] of entities.edges) {
881
+ const fields = extractSchemaFields(entity.schema);
882
+ for (const field of fields) {
883
+ indexes.push({
884
+ collectionGroup: collection,
885
+ queryScope: "COLLECTION",
886
+ fields: [
887
+ { fieldPath: "aUid", order: "ASCENDING" },
888
+ { fieldPath: "axbType", order: "ASCENDING" },
889
+ { fieldPath: `data.${field}`, order: "ASCENDING" }
890
+ ]
891
+ });
892
+ }
893
+ }
894
+ }
895
+ if (registryEntries) {
896
+ const targetGraphNames = /* @__PURE__ */ new Set();
897
+ for (const entry of registryEntries) {
898
+ if (entry.targetGraph) {
899
+ targetGraphNames.add(entry.targetGraph);
900
+ }
901
+ }
902
+ for (const name of targetGraphNames) {
903
+ indexes.push(...collectionGroupIndexes(name));
904
+ }
905
+ }
906
+ return { indexes, fieldOverrides: [] };
907
+ }
908
+
909
+ // src/record.ts
910
+ import { FieldValue as FieldValue2 } from "@google-cloud/firestore";
911
+ function buildNodeRecord(aType, uid, data) {
912
+ const now = FieldValue2.serverTimestamp();
913
+ return {
914
+ aType,
915
+ aUid: uid,
916
+ axbType: NODE_RELATION,
917
+ bType: aType,
918
+ bUid: uid,
919
+ data,
920
+ createdAt: now,
921
+ updatedAt: now
922
+ };
923
+ }
924
+ function buildEdgeRecord(aType, aUid, axbType, bType, bUid, data) {
925
+ const now = FieldValue2.serverTimestamp();
926
+ return {
927
+ aType,
928
+ aUid,
929
+ axbType,
930
+ bType,
931
+ bUid,
932
+ data,
933
+ createdAt: now,
934
+ updatedAt: now
935
+ };
936
+ }
937
+
938
+ // src/traverse.ts
939
+ var DEFAULT_LIMIT = 10;
940
+ var DEFAULT_MAX_READS = 100;
941
+ var DEFAULT_CONCURRENCY = 5;
942
+ var _crossGraphWarned = false;
943
+ function isGraphClient(reader) {
944
+ return "subgraph" in reader && typeof reader.subgraph === "function";
945
+ }
946
+ var Semaphore = class {
947
+ constructor(slots) {
948
+ this.slots = slots;
949
+ }
950
+ queue = [];
951
+ active = 0;
952
+ async acquire() {
953
+ if (this.active < this.slots) {
2174
954
  this.active++;
2175
955
  return;
2176
956
  }
@@ -2359,7 +1139,6 @@ function getCustomElements() {
2359
1139
  function resilientView(ViewClass, tagName) {
2360
1140
  const g = globalThis;
2361
1141
  if (!g.HTMLElement) return ViewClass;
2362
- const Base = g.HTMLElement;
2363
1142
  const Wrapped = class extends ViewClass {
2364
1143
  connectedCallback() {
2365
1144
  try {
@@ -2444,369 +1223,9 @@ function defineViews(input) {
2444
1223
  }
2445
1224
  return { nodes, edges };
2446
1225
  }
2447
-
2448
- // src/config.ts
2449
- function defineConfig(config) {
2450
- return config;
2451
- }
2452
- function resolveView(resolverConfig, availableViewNames, context) {
2453
- if (!resolverConfig) return "json";
2454
- const available = new Set(availableViewNames);
2455
- if (context) {
2456
- const contextDefault = resolverConfig[context];
2457
- if (contextDefault && available.has(contextDefault)) {
2458
- return contextDefault;
2459
- }
2460
- }
2461
- if (resolverConfig.default && available.has(resolverConfig.default)) {
2462
- return resolverConfig.default;
2463
- }
2464
- return "json";
2465
- }
2466
-
2467
- // src/discover.ts
2468
- import { readFileSync, readdirSync, existsSync, statSync } from "fs";
2469
- import { createRequire } from "module";
2470
- import { join, resolve } from "path";
2471
- var DiscoveryError = class extends FiregraphError {
2472
- constructor(message) {
2473
- super(message, "DISCOVERY_ERROR");
2474
- this.name = "DiscoveryError";
2475
- }
2476
- };
2477
- function readJson(filePath) {
2478
- try {
2479
- const raw = readFileSync(filePath, "utf-8");
2480
- return JSON.parse(raw);
2481
- } catch (err) {
2482
- const msg = err instanceof SyntaxError ? `Invalid JSON in ${filePath}: ${err.message}` : `Cannot read ${filePath}: ${err.message}`;
2483
- throw new DiscoveryError(msg);
2484
- }
2485
- }
2486
- function readJsonIfExists(filePath) {
2487
- if (!existsSync(filePath)) return void 0;
2488
- return readJson(filePath);
2489
- }
2490
- var SCHEMA_SCRIPT_EXTENSIONS = [".ts", ".js", ".mts", ".mjs"];
2491
- function loadSchema(dir, entityLabel) {
2492
- for (const ext of SCHEMA_SCRIPT_EXTENSIONS) {
2493
- const candidate = join(dir, `schema${ext}`);
2494
- if (existsSync(candidate)) {
2495
- return loadSchemaModule(candidate, entityLabel);
2496
- }
2497
- }
2498
- const jsonPath = join(dir, "schema.json");
2499
- if (existsSync(jsonPath)) {
2500
- return readJson(jsonPath);
2501
- }
2502
- throw new DiscoveryError(
2503
- `Missing schema for ${entityLabel} in ${dir}. Provide a schema.ts (or .js/.mts/.mjs) or schema.json file.`
2504
- );
2505
- }
2506
- var _jiti;
2507
- function getJiti() {
2508
- if (!_jiti) {
2509
- const base = typeof __filename !== "undefined" ? __filename : import.meta.url;
2510
- const esmRequire = createRequire(base);
2511
- const { createJiti } = esmRequire("jiti");
2512
- _jiti = createJiti(base, { interopDefault: true });
2513
- }
2514
- return _jiti;
2515
- }
2516
- function loadSchemaModule(filePath, entityLabel) {
2517
- try {
2518
- const jiti = getJiti();
2519
- const mod = jiti(filePath);
2520
- const schema = mod && typeof mod === "object" && "default" in mod ? mod.default : mod;
2521
- if (!schema || typeof schema !== "object") {
2522
- throw new DiscoveryError(
2523
- `Schema file ${filePath} for ${entityLabel} must default-export a JSON Schema object.`
2524
- );
2525
- }
2526
- return schema;
2527
- } catch (err) {
2528
- if (err instanceof DiscoveryError) throw err;
2529
- throw new DiscoveryError(
2530
- `Failed to load schema module ${filePath} for ${entityLabel}: ${err.message}`
2531
- );
2532
- }
2533
- }
2534
- var VIEW_EXTENSIONS = [".ts", ".js", ".mts", ".mjs"];
2535
- function findViewsFile(dir) {
2536
- for (const ext of VIEW_EXTENSIONS) {
2537
- const candidate = join(dir, `views${ext}`);
2538
- if (existsSync(candidate)) return candidate;
2539
- }
2540
- return void 0;
2541
- }
2542
- var MIGRATION_EXTENSIONS = [".ts", ".js", ".mts", ".mjs"];
2543
- function findMigrationsFile(dir) {
2544
- for (const ext of MIGRATION_EXTENSIONS) {
2545
- const candidate = join(dir, `migrations${ext}`);
2546
- if (existsSync(candidate)) return candidate;
2547
- }
2548
- return void 0;
2549
- }
2550
- function loadMigrations(filePath, entityLabel) {
2551
- try {
2552
- const jiti = getJiti();
2553
- const mod = jiti(filePath);
2554
- const migrations = mod && typeof mod === "object" && "default" in mod ? mod.default : mod;
2555
- if (!Array.isArray(migrations)) {
2556
- throw new DiscoveryError(
2557
- `Migrations file ${filePath} for ${entityLabel} must default-export an array of MigrationStep.`
2558
- );
2559
- }
2560
- return migrations;
2561
- } catch (err) {
2562
- if (err instanceof DiscoveryError) throw err;
2563
- throw new DiscoveryError(
2564
- `Failed to load migrations ${filePath} for ${entityLabel}: ${err.message}`
2565
- );
2566
- }
2567
- }
2568
- function loadNodeEntity(dir, name) {
2569
- const schema = loadSchema(dir, `node type "${name}"`);
2570
- const meta = readJsonIfExists(join(dir, "meta.json"));
2571
- const sampleData = readJsonIfExists(join(dir, "sample.json"));
2572
- const viewsPath = findViewsFile(dir);
2573
- const migrationsPath = findMigrationsFile(dir);
2574
- const migrations = migrationsPath ? loadMigrations(migrationsPath, `node type "${name}"`) : void 0;
2575
- return {
2576
- kind: "node",
2577
- name,
2578
- schema,
2579
- description: meta?.description,
2580
- titleField: meta?.titleField,
2581
- subtitleField: meta?.subtitleField,
2582
- viewDefaults: meta?.viewDefaults,
2583
- viewsPath,
2584
- sampleData,
2585
- allowedIn: meta?.allowedIn,
2586
- migrations,
2587
- migrationWriteBack: meta?.migrationWriteBack
2588
- };
2589
- }
2590
- function loadEdgeEntity(dir, name) {
2591
- const schema = loadSchema(dir, `edge type "${name}"`);
2592
- const edgePath = join(dir, "edge.json");
2593
- if (!existsSync(edgePath)) {
2594
- throw new DiscoveryError(
2595
- `Missing edge.json for edge type "${name}" in ${dir}. Edge entities must declare topology (from/to node types).`
2596
- );
2597
- }
2598
- const topology = readJson(edgePath);
2599
- if (!topology.from) {
2600
- throw new DiscoveryError(
2601
- `edge.json for "${name}" is missing required "from" field`
2602
- );
2603
- }
2604
- if (!topology.to) {
2605
- throw new DiscoveryError(
2606
- `edge.json for "${name}" is missing required "to" field`
2607
- );
2608
- }
2609
- const meta = readJsonIfExists(join(dir, "meta.json"));
2610
- const sampleData = readJsonIfExists(join(dir, "sample.json"));
2611
- const viewsPath = findViewsFile(dir);
2612
- const migrationsPath = findMigrationsFile(dir);
2613
- const migrations = migrationsPath ? loadMigrations(migrationsPath, `edge type "${name}"`) : void 0;
2614
- return {
2615
- kind: "edge",
2616
- name,
2617
- schema,
2618
- topology,
2619
- description: meta?.description,
2620
- titleField: meta?.titleField,
2621
- subtitleField: meta?.subtitleField,
2622
- viewDefaults: meta?.viewDefaults,
2623
- viewsPath,
2624
- sampleData,
2625
- allowedIn: meta?.allowedIn,
2626
- targetGraph: topology.targetGraph ?? meta?.targetGraph,
2627
- migrations,
2628
- migrationWriteBack: meta?.migrationWriteBack
2629
- };
2630
- }
2631
- function getSubdirectories(dir) {
2632
- if (!existsSync(dir)) return [];
2633
- return readdirSync(dir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
2634
- }
2635
- function discoverEntities(entitiesDir) {
2636
- const absDir = resolve(entitiesDir);
2637
- if (!existsSync(absDir) || !statSync(absDir).isDirectory()) {
2638
- throw new DiscoveryError(`Entities directory not found: ${entitiesDir}`);
2639
- }
2640
- const nodes = /* @__PURE__ */ new Map();
2641
- const edges = /* @__PURE__ */ new Map();
2642
- const warnings = [];
2643
- const nodesDir = join(absDir, "nodes");
2644
- for (const name of getSubdirectories(nodesDir)) {
2645
- nodes.set(name, loadNodeEntity(join(nodesDir, name), name));
2646
- }
2647
- const edgesDir = join(absDir, "edges");
2648
- for (const name of getSubdirectories(edgesDir)) {
2649
- edges.set(name, loadEdgeEntity(join(edgesDir, name), name));
2650
- }
2651
- const nodeNames = new Set(nodes.keys());
2652
- for (const [axbType, entity] of edges) {
2653
- const topology = entity.topology;
2654
- const fromTypes = Array.isArray(topology.from) ? topology.from : [topology.from];
2655
- const toTypes = Array.isArray(topology.to) ? topology.to : [topology.to];
2656
- for (const ref of [...fromTypes, ...toTypes]) {
2657
- if (!nodeNames.has(ref)) {
2658
- warnings.push({
2659
- code: "DANGLING_TOPOLOGY_REF",
2660
- message: `Edge "${axbType}" references node type "${ref}" which was not found in the nodes directory`
2661
- });
2662
- }
2663
- }
2664
- }
2665
- return {
2666
- result: { nodes, edges },
2667
- warnings
2668
- };
2669
- }
2670
-
2671
- // src/cross-graph.ts
2672
- function resolveAncestorCollection(collectionPath, uid) {
2673
- const segments = collectionPath.split("/");
2674
- for (let i = 1; i < segments.length; i += 2) {
2675
- if (segments[i] === uid) {
2676
- return segments.slice(0, i).join("/");
2677
- }
2678
- }
2679
- return null;
2680
- }
2681
- function isAncestorUid(collectionPath, uid) {
2682
- return resolveAncestorCollection(collectionPath, uid) !== null;
2683
- }
2684
-
2685
- // src/indexes.ts
2686
- function baseIndexes(collection) {
2687
- return [
2688
- {
2689
- collectionGroup: collection,
2690
- queryScope: "COLLECTION",
2691
- fields: [
2692
- { fieldPath: "aUid", order: "ASCENDING" },
2693
- { fieldPath: "axbType", order: "ASCENDING" }
2694
- ]
2695
- },
2696
- {
2697
- collectionGroup: collection,
2698
- queryScope: "COLLECTION",
2699
- fields: [
2700
- { fieldPath: "axbType", order: "ASCENDING" },
2701
- { fieldPath: "bUid", order: "ASCENDING" }
2702
- ]
2703
- },
2704
- {
2705
- collectionGroup: collection,
2706
- queryScope: "COLLECTION",
2707
- fields: [
2708
- { fieldPath: "aType", order: "ASCENDING" },
2709
- { fieldPath: "axbType", order: "ASCENDING" }
2710
- ]
2711
- },
2712
- {
2713
- collectionGroup: collection,
2714
- queryScope: "COLLECTION",
2715
- fields: [
2716
- { fieldPath: "axbType", order: "ASCENDING" },
2717
- { fieldPath: "bType", order: "ASCENDING" }
2718
- ]
2719
- }
2720
- ];
2721
- }
2722
- function extractSchemaFields(schema) {
2723
- const s = schema;
2724
- if (s.type !== "object" || !s.properties) return [];
2725
- return Object.keys(s.properties);
2726
- }
2727
- function collectionGroupIndexes(collectionName) {
2728
- return [
2729
- {
2730
- collectionGroup: collectionName,
2731
- queryScope: "COLLECTION_GROUP",
2732
- fields: [
2733
- { fieldPath: "aUid", order: "ASCENDING" },
2734
- { fieldPath: "axbType", order: "ASCENDING" }
2735
- ]
2736
- },
2737
- {
2738
- collectionGroup: collectionName,
2739
- queryScope: "COLLECTION_GROUP",
2740
- fields: [
2741
- { fieldPath: "axbType", order: "ASCENDING" },
2742
- { fieldPath: "bUid", order: "ASCENDING" }
2743
- ]
2744
- },
2745
- {
2746
- collectionGroup: collectionName,
2747
- queryScope: "COLLECTION_GROUP",
2748
- fields: [
2749
- { fieldPath: "aType", order: "ASCENDING" },
2750
- { fieldPath: "axbType", order: "ASCENDING" }
2751
- ]
2752
- },
2753
- {
2754
- collectionGroup: collectionName,
2755
- queryScope: "COLLECTION_GROUP",
2756
- fields: [
2757
- { fieldPath: "axbType", order: "ASCENDING" },
2758
- { fieldPath: "bType", order: "ASCENDING" }
2759
- ]
2760
- }
2761
- ];
2762
- }
2763
- function generateIndexConfig(collection, entities, registryEntries) {
2764
- const indexes = baseIndexes(collection);
2765
- if (entities) {
2766
- for (const [, entity] of entities.nodes) {
2767
- const fields = extractSchemaFields(entity.schema);
2768
- for (const field of fields) {
2769
- indexes.push({
2770
- collectionGroup: collection,
2771
- queryScope: "COLLECTION",
2772
- fields: [
2773
- { fieldPath: "aType", order: "ASCENDING" },
2774
- { fieldPath: "axbType", order: "ASCENDING" },
2775
- { fieldPath: `data.${field}`, order: "ASCENDING" }
2776
- ]
2777
- });
2778
- }
2779
- }
2780
- for (const [, entity] of entities.edges) {
2781
- const fields = extractSchemaFields(entity.schema);
2782
- for (const field of fields) {
2783
- indexes.push({
2784
- collectionGroup: collection,
2785
- queryScope: "COLLECTION",
2786
- fields: [
2787
- { fieldPath: "aUid", order: "ASCENDING" },
2788
- { fieldPath: "axbType", order: "ASCENDING" },
2789
- { fieldPath: `data.${field}`, order: "ASCENDING" }
2790
- ]
2791
- });
2792
- }
2793
- }
2794
- }
2795
- if (registryEntries) {
2796
- const targetGraphNames = /* @__PURE__ */ new Set();
2797
- for (const entry of registryEntries) {
2798
- if (entry.targetGraph) {
2799
- targetGraphNames.add(entry.targetGraph);
2800
- }
2801
- }
2802
- for (const name of targetGraphNames) {
2803
- indexes.push(...collectionGroupIndexes(name));
2804
- }
2805
- }
2806
- return { indexes, fieldOverrides: [] };
2807
- }
2808
1226
  export {
2809
1227
  BOOTSTRAP_ENTRIES,
1228
+ CrossBackendTransactionError,
2810
1229
  DEFAULT_QUERY_LIMIT,
2811
1230
  DiscoveryError,
2812
1231
  DynamicRegistryError,
@@ -2828,6 +1247,7 @@ export {
2828
1247
  TraversalError,
2829
1248
  ValidationError,
2830
1249
  analyzeQuerySafety,
1250
+ appendStorageScope,
2831
1251
  applyMigrationChain,
2832
1252
  buildEdgeQueryPlan,
2833
1253
  buildEdgeRecord,
@@ -2840,6 +1260,7 @@ export {
2840
1260
  computeNodeDocId,
2841
1261
  createBootstrapRegistry,
2842
1262
  createGraphClient,
1263
+ createGraphClientFromBackend,
2843
1264
  createMergedRegistry,
2844
1265
  createRegistry,
2845
1266
  createRegistryFromGraph,
@@ -2854,6 +1275,7 @@ export {
2854
1275
  generateId,
2855
1276
  generateIndexConfig,
2856
1277
  generateTypes,
1278
+ isAncestorScopeUid,
2857
1279
  isAncestorUid,
2858
1280
  isTaggedValue,
2859
1281
  jsonSchemaToFieldMeta,
@@ -2861,8 +1283,10 @@ export {
2861
1283
  matchScopeAny,
2862
1284
  migrateRecord,
2863
1285
  migrateRecords,
1286
+ parseStorageScope,
2864
1287
  precompileSource,
2865
1288
  resolveAncestorCollection,
1289
+ resolveAncestorScope,
2866
1290
  resolveView,
2867
1291
  serializeFirestoreTypes,
2868
1292
  validateMigrationChain