@typicalday/firegraph 0.1.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 (48) hide show
  1. package/LICENSE +27 -0
  2. package/README.md +527 -0
  3. package/bin/firegraph.mjs +129 -0
  4. package/dist/chunk-KFA7G37W.js +443 -0
  5. package/dist/chunk-KFA7G37W.js.map +1 -0
  6. package/dist/chunk-YLGXLEUE.js +47 -0
  7. package/dist/chunk-YLGXLEUE.js.map +1 -0
  8. package/dist/client-Bk2Cm6xv.d.cts +131 -0
  9. package/dist/client-Bk2Cm6xv.d.ts +131 -0
  10. package/dist/codegen/index.cjs +81 -0
  11. package/dist/codegen/index.cjs.map +1 -0
  12. package/dist/codegen/index.d.cts +2 -0
  13. package/dist/codegen/index.d.ts +2 -0
  14. package/dist/codegen/index.js +7 -0
  15. package/dist/codegen/index.js.map +1 -0
  16. package/dist/editor/client/assets/index-DJJ_b0jI.js +411 -0
  17. package/dist/editor/client/assets/index-Q0QBYrMV.css +1 -0
  18. package/dist/editor/client/index.html +16 -0
  19. package/dist/editor/server/index.mjs +49597 -0
  20. package/dist/index-CG3R68Hu.d.cts +414 -0
  21. package/dist/index-CG3R68Hu.d.ts +414 -0
  22. package/dist/index.cjs +1953 -0
  23. package/dist/index.cjs.map +1 -0
  24. package/dist/index.d.cts +186 -0
  25. package/dist/index.d.ts +186 -0
  26. package/dist/index.js +1569 -0
  27. package/dist/index.js.map +1 -0
  28. package/dist/query-client/index.cjs +484 -0
  29. package/dist/query-client/index.cjs.map +1 -0
  30. package/dist/query-client/index.d.cts +15 -0
  31. package/dist/query-client/index.d.ts +15 -0
  32. package/dist/query-client/index.js +17 -0
  33. package/dist/query-client/index.js.map +1 -0
  34. package/dist/react.cjs +85 -0
  35. package/dist/react.cjs.map +1 -0
  36. package/dist/react.d.cts +44 -0
  37. package/dist/react.d.ts +44 -0
  38. package/dist/react.js +60 -0
  39. package/dist/react.js.map +1 -0
  40. package/dist/svelte.cjs +90 -0
  41. package/dist/svelte.cjs.map +1 -0
  42. package/dist/svelte.d.cts +46 -0
  43. package/dist/svelte.d.ts +46 -0
  44. package/dist/svelte.js +65 -0
  45. package/dist/svelte.js.map +1 -0
  46. package/dist/views-DL60k0cf.d.cts +91 -0
  47. package/dist/views-DL60k0cf.d.ts +91 -0
  48. package/package.json +122 -0
package/dist/index.js ADDED
@@ -0,0 +1,1569 @@
1
+ import {
2
+ generateTypes
3
+ } from "./chunk-YLGXLEUE.js";
4
+ import {
5
+ QueryClient,
6
+ QueryClientError
7
+ } from "./chunk-KFA7G37W.js";
8
+
9
+ // src/client.ts
10
+ import { FieldValue as FieldValue4 } from "@google-cloud/firestore";
11
+
12
+ // src/docid.ts
13
+ import { createHash } from "crypto";
14
+
15
+ // src/internal/constants.ts
16
+ var NODE_RELATION = "is";
17
+ var SHARD_SEPARATOR = ":";
18
+
19
+ // src/docid.ts
20
+ function computeNodeDocId(uid) {
21
+ return uid;
22
+ }
23
+ function computeEdgeDocId(aUid, axbType, bUid) {
24
+ const composite = `${aUid}${SHARD_SEPARATOR}${axbType}${SHARD_SEPARATOR}${bUid}`;
25
+ const hash = createHash("sha256").update(composite).digest("hex");
26
+ const shard = hash[0];
27
+ return `${shard}${SHARD_SEPARATOR}${aUid}${SHARD_SEPARATOR}${axbType}${SHARD_SEPARATOR}${bUid}`;
28
+ }
29
+
30
+ // src/record.ts
31
+ import { FieldValue } from "@google-cloud/firestore";
32
+ function buildNodeRecord(aType, uid, data) {
33
+ const now = FieldValue.serverTimestamp();
34
+ return {
35
+ aType,
36
+ aUid: uid,
37
+ axbType: NODE_RELATION,
38
+ bType: aType,
39
+ bUid: uid,
40
+ data,
41
+ createdAt: now,
42
+ updatedAt: now
43
+ };
44
+ }
45
+ function buildEdgeRecord(aType, aUid, axbType, bType, bUid, data) {
46
+ const now = FieldValue.serverTimestamp();
47
+ return {
48
+ aType,
49
+ aUid,
50
+ axbType,
51
+ bType,
52
+ bUid,
53
+ data,
54
+ createdAt: now,
55
+ updatedAt: now
56
+ };
57
+ }
58
+
59
+ // src/errors.ts
60
+ var FiregraphError = class extends Error {
61
+ constructor(message, code) {
62
+ super(message);
63
+ this.code = code;
64
+ this.name = "FiregraphError";
65
+ }
66
+ };
67
+ var NodeNotFoundError = class extends FiregraphError {
68
+ constructor(uid) {
69
+ super(`Node not found: ${uid}`, "NODE_NOT_FOUND");
70
+ this.name = "NodeNotFoundError";
71
+ }
72
+ };
73
+ var EdgeNotFoundError = class extends FiregraphError {
74
+ constructor(aUid, axbType, bUid) {
75
+ super(`Edge not found: ${aUid} -[${axbType}]-> ${bUid}`, "EDGE_NOT_FOUND");
76
+ this.name = "EdgeNotFoundError";
77
+ }
78
+ };
79
+ var ValidationError = class extends FiregraphError {
80
+ constructor(message, details) {
81
+ super(message, "VALIDATION_ERROR");
82
+ this.details = details;
83
+ this.name = "ValidationError";
84
+ }
85
+ };
86
+ var RegistryViolationError = class extends FiregraphError {
87
+ constructor(aType, axbType, bType) {
88
+ super(
89
+ `Unregistered triple: (${aType}) -[${axbType}]-> (${bType})`,
90
+ "REGISTRY_VIOLATION"
91
+ );
92
+ this.name = "RegistryViolationError";
93
+ }
94
+ };
95
+ var InvalidQueryError = class extends FiregraphError {
96
+ constructor(message) {
97
+ super(message, "INVALID_QUERY");
98
+ this.name = "InvalidQueryError";
99
+ }
100
+ };
101
+ var TraversalError = class extends FiregraphError {
102
+ constructor(message) {
103
+ super(message, "TRAVERSAL_ERROR");
104
+ this.name = "TraversalError";
105
+ }
106
+ };
107
+ var DynamicRegistryError = class extends FiregraphError {
108
+ constructor(message) {
109
+ super(message, "DYNAMIC_REGISTRY_ERROR");
110
+ this.name = "DynamicRegistryError";
111
+ }
112
+ };
113
+
114
+ // src/query.ts
115
+ function buildEdgeQueryPlan(params) {
116
+ const { aType, aUid, axbType, bType, bUid, limit, orderBy } = params;
117
+ if (aUid && axbType && bUid && !params.where?.length) {
118
+ return { strategy: "get", docId: computeEdgeDocId(aUid, axbType, bUid) };
119
+ }
120
+ const filters = [];
121
+ if (aType) filters.push({ field: "aType", op: "==", value: aType });
122
+ if (aUid) filters.push({ field: "aUid", op: "==", value: aUid });
123
+ if (axbType) filters.push({ field: "axbType", op: "==", value: axbType });
124
+ if (bType) filters.push({ field: "bType", op: "==", value: bType });
125
+ if (bUid) filters.push({ field: "bUid", op: "==", value: bUid });
126
+ const builtinFields = ["aType", "aUid", "axbType", "bType", "bUid", "createdAt", "updatedAt"];
127
+ if (params.where) {
128
+ for (const clause of params.where) {
129
+ const field = builtinFields.includes(clause.field) ? clause.field : clause.field.startsWith("data.") ? clause.field : `data.${clause.field}`;
130
+ filters.push({ field, op: clause.op, value: clause.value });
131
+ }
132
+ }
133
+ if (filters.length === 0) {
134
+ throw new InvalidQueryError("findEdges requires at least one filter parameter");
135
+ }
136
+ const options = limit !== void 0 || orderBy ? { limit, orderBy } : void 0;
137
+ return { strategy: "query", filters, options };
138
+ }
139
+ function buildNodeQueryPlan(params) {
140
+ return {
141
+ strategy: "query",
142
+ filters: [
143
+ { field: "aType", op: "==", value: params.aType },
144
+ { field: "axbType", op: "==", value: NODE_RELATION }
145
+ ]
146
+ };
147
+ }
148
+
149
+ // src/internal/firestore-adapter.ts
150
+ function createFirestoreAdapter(db, collectionPath) {
151
+ const collectionRef = db.collection(collectionPath);
152
+ return {
153
+ collectionPath,
154
+ async getDoc(docId) {
155
+ const snap = await collectionRef.doc(docId).get();
156
+ if (!snap.exists) return null;
157
+ return snap.data();
158
+ },
159
+ async setDoc(docId, data) {
160
+ await collectionRef.doc(docId).set(data);
161
+ },
162
+ async updateDoc(docId, data) {
163
+ await collectionRef.doc(docId).update(data);
164
+ },
165
+ async deleteDoc(docId) {
166
+ await collectionRef.doc(docId).delete();
167
+ },
168
+ async query(filters, options) {
169
+ let q = collectionRef;
170
+ for (const f of filters) {
171
+ q = q.where(f.field, f.op, f.value);
172
+ }
173
+ if (options?.orderBy) {
174
+ q = q.orderBy(options.orderBy.field, options.orderBy.direction ?? "asc");
175
+ }
176
+ if (options?.limit !== void 0) {
177
+ q = q.limit(options.limit);
178
+ }
179
+ const snap = await q.get();
180
+ return snap.docs.map((doc) => doc.data());
181
+ }
182
+ };
183
+ }
184
+ function createTransactionAdapter(db, collectionPath, tx) {
185
+ const collectionRef = db.collection(collectionPath);
186
+ return {
187
+ async getDoc(docId) {
188
+ const snap = await tx.get(collectionRef.doc(docId));
189
+ if (!snap.exists) return null;
190
+ return snap.data();
191
+ },
192
+ setDoc(docId, data) {
193
+ tx.set(collectionRef.doc(docId), data);
194
+ },
195
+ updateDoc(docId, data) {
196
+ tx.update(collectionRef.doc(docId), data);
197
+ },
198
+ deleteDoc(docId) {
199
+ tx.delete(collectionRef.doc(docId));
200
+ },
201
+ async query(filters, options) {
202
+ let q = collectionRef;
203
+ for (const f of filters) {
204
+ q = q.where(f.field, f.op, f.value);
205
+ }
206
+ if (options?.orderBy) {
207
+ q = q.orderBy(options.orderBy.field, options.orderBy.direction ?? "asc");
208
+ }
209
+ if (options?.limit !== void 0) {
210
+ q = q.limit(options.limit);
211
+ }
212
+ const snap = await tx.get(q);
213
+ return snap.docs.map((doc) => doc.data());
214
+ }
215
+ };
216
+ }
217
+ function createBatchAdapter(db, collectionPath) {
218
+ const collectionRef = db.collection(collectionPath);
219
+ const batch = db.batch();
220
+ return {
221
+ setDoc(docId, data) {
222
+ batch.set(collectionRef.doc(docId), data);
223
+ },
224
+ updateDoc(docId, data) {
225
+ batch.update(collectionRef.doc(docId), data);
226
+ },
227
+ deleteDoc(docId) {
228
+ batch.delete(collectionRef.doc(docId));
229
+ },
230
+ async commit() {
231
+ await batch.commit();
232
+ }
233
+ };
234
+ }
235
+
236
+ // src/internal/pipeline-adapter.ts
237
+ var _Pipelines = null;
238
+ async function getPipelines() {
239
+ if (!_Pipelines) {
240
+ const mod = await import("@google-cloud/firestore");
241
+ _Pipelines = mod.Pipelines;
242
+ }
243
+ return _Pipelines;
244
+ }
245
+ function buildFilterExpression(P, filter) {
246
+ const { field: fieldName, op, value } = filter;
247
+ switch (op) {
248
+ case "==":
249
+ return P.equal(fieldName, value);
250
+ case "!=":
251
+ return P.notEqual(fieldName, value);
252
+ case "<":
253
+ return P.lessThan(fieldName, value);
254
+ case "<=":
255
+ return P.lessThanOrEqual(fieldName, value);
256
+ case ">":
257
+ return P.greaterThan(fieldName, value);
258
+ case ">=":
259
+ return P.greaterThanOrEqual(fieldName, value);
260
+ case "in":
261
+ return P.equalAny(fieldName, value);
262
+ case "not-in":
263
+ return P.notEqualAny(fieldName, value);
264
+ case "array-contains":
265
+ return P.arrayContains(fieldName, value);
266
+ case "array-contains-any":
267
+ return P.arrayContainsAny(fieldName, value);
268
+ default:
269
+ throw new Error(`Unsupported filter op for pipeline mode: ${op}`);
270
+ }
271
+ }
272
+ function createPipelineQueryAdapter(db, collectionPath) {
273
+ return {
274
+ async query(filters, options) {
275
+ const P = await getPipelines();
276
+ let pipeline = db.pipeline().collection(collectionPath);
277
+ if (filters.length === 1) {
278
+ pipeline = pipeline.where(buildFilterExpression(P, filters[0]));
279
+ } else if (filters.length > 1) {
280
+ const [first, second, ...rest] = filters.map((f) => buildFilterExpression(P, f));
281
+ pipeline = pipeline.where(P.and(first, second, ...rest));
282
+ }
283
+ if (options?.orderBy) {
284
+ const f = P.field(options.orderBy.field);
285
+ const ordering = options.orderBy.direction === "desc" ? f.descending() : f.ascending();
286
+ pipeline = pipeline.sort(ordering);
287
+ }
288
+ if (options?.limit !== void 0) {
289
+ pipeline = pipeline.limit(options.limit);
290
+ }
291
+ const snap = await pipeline.execute();
292
+ return snap.results.map((r) => r.data());
293
+ }
294
+ };
295
+ }
296
+
297
+ // src/transaction.ts
298
+ import { FieldValue as FieldValue2 } from "@google-cloud/firestore";
299
+ var GraphTransactionImpl = class {
300
+ constructor(adapter, registry) {
301
+ this.adapter = adapter;
302
+ this.registry = registry;
303
+ }
304
+ async getNode(uid) {
305
+ const docId = computeNodeDocId(uid);
306
+ return this.adapter.getDoc(docId);
307
+ }
308
+ async getEdge(aUid, axbType, bUid) {
309
+ const docId = computeEdgeDocId(aUid, axbType, bUid);
310
+ return this.adapter.getDoc(docId);
311
+ }
312
+ async edgeExists(aUid, axbType, bUid) {
313
+ const record = await this.getEdge(aUid, axbType, bUid);
314
+ return record !== null;
315
+ }
316
+ async findEdges(params) {
317
+ const plan = buildEdgeQueryPlan(params);
318
+ if (plan.strategy === "get") {
319
+ const record = await this.adapter.getDoc(plan.docId);
320
+ return record ? [record] : [];
321
+ }
322
+ return this.adapter.query(plan.filters, plan.options);
323
+ }
324
+ async findNodes(params) {
325
+ const plan = buildNodeQueryPlan(params);
326
+ if (plan.strategy === "get") {
327
+ const record = await this.adapter.getDoc(plan.docId);
328
+ return record ? [record] : [];
329
+ }
330
+ return this.adapter.query(plan.filters, plan.options);
331
+ }
332
+ async putNode(aType, uid, data) {
333
+ if (this.registry) {
334
+ this.registry.validate(aType, NODE_RELATION, aType, data);
335
+ }
336
+ const docId = computeNodeDocId(uid);
337
+ const record = buildNodeRecord(aType, uid, data);
338
+ this.adapter.setDoc(docId, record);
339
+ }
340
+ async putEdge(aType, aUid, axbType, bType, bUid, data) {
341
+ if (this.registry) {
342
+ this.registry.validate(aType, axbType, bType, data);
343
+ }
344
+ const docId = computeEdgeDocId(aUid, axbType, bUid);
345
+ const record = buildEdgeRecord(aType, aUid, axbType, bType, bUid, data);
346
+ this.adapter.setDoc(docId, record);
347
+ }
348
+ async updateNode(uid, data) {
349
+ const docId = computeNodeDocId(uid);
350
+ this.adapter.updateDoc(docId, {
351
+ ...data,
352
+ updatedAt: FieldValue2.serverTimestamp()
353
+ });
354
+ }
355
+ async removeNode(uid) {
356
+ const docId = computeNodeDocId(uid);
357
+ this.adapter.deleteDoc(docId);
358
+ }
359
+ async removeEdge(aUid, axbType, bUid) {
360
+ const docId = computeEdgeDocId(aUid, axbType, bUid);
361
+ this.adapter.deleteDoc(docId);
362
+ }
363
+ };
364
+
365
+ // src/batch.ts
366
+ import { FieldValue as FieldValue3 } from "@google-cloud/firestore";
367
+ var GraphBatchImpl = class {
368
+ constructor(adapter, registry) {
369
+ this.adapter = adapter;
370
+ this.registry = registry;
371
+ }
372
+ async putNode(aType, uid, data) {
373
+ if (this.registry) {
374
+ this.registry.validate(aType, NODE_RELATION, aType, data);
375
+ }
376
+ const docId = computeNodeDocId(uid);
377
+ const record = buildNodeRecord(aType, uid, data);
378
+ this.adapter.setDoc(docId, record);
379
+ }
380
+ async putEdge(aType, aUid, axbType, bType, bUid, data) {
381
+ if (this.registry) {
382
+ this.registry.validate(aType, axbType, bType, data);
383
+ }
384
+ const docId = computeEdgeDocId(aUid, axbType, bUid);
385
+ const record = buildEdgeRecord(aType, aUid, axbType, bType, bUid, data);
386
+ this.adapter.setDoc(docId, record);
387
+ }
388
+ async updateNode(uid, data) {
389
+ const docId = computeNodeDocId(uid);
390
+ this.adapter.updateDoc(docId, {
391
+ ...data,
392
+ updatedAt: FieldValue3.serverTimestamp()
393
+ });
394
+ }
395
+ async removeNode(uid) {
396
+ const docId = computeNodeDocId(uid);
397
+ this.adapter.deleteDoc(docId);
398
+ }
399
+ async removeEdge(aUid, axbType, bUid) {
400
+ const docId = computeEdgeDocId(aUid, axbType, bUid);
401
+ this.adapter.deleteDoc(docId);
402
+ }
403
+ async commit() {
404
+ await this.adapter.commit();
405
+ }
406
+ };
407
+
408
+ // src/bulk.ts
409
+ var MAX_BATCH_SIZE = 500;
410
+ var DEFAULT_MAX_RETRIES = 3;
411
+ var BASE_DELAY_MS = 200;
412
+ function sleep(ms) {
413
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
414
+ }
415
+ function chunk(arr, size) {
416
+ const chunks = [];
417
+ for (let i = 0; i < arr.length; i += size) {
418
+ chunks.push(arr.slice(i, i + size));
419
+ }
420
+ return chunks;
421
+ }
422
+ async function bulkDeleteDocIds(db, collectionPath, docIds, options) {
423
+ if (docIds.length === 0) {
424
+ return { deleted: 0, batches: 0, errors: [] };
425
+ }
426
+ const batchSize = Math.min(options?.batchSize ?? MAX_BATCH_SIZE, MAX_BATCH_SIZE);
427
+ const maxRetries = options?.maxRetries ?? DEFAULT_MAX_RETRIES;
428
+ const onProgress = options?.onProgress;
429
+ const chunks = chunk(docIds, batchSize);
430
+ const errors = [];
431
+ let deleted = 0;
432
+ let completedBatches = 0;
433
+ for (let i = 0; i < chunks.length; i++) {
434
+ const ids = chunks[i];
435
+ let committed = false;
436
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
437
+ try {
438
+ const batch = db.batch();
439
+ const collectionRef = db.collection(collectionPath);
440
+ for (const id of ids) {
441
+ batch.delete(collectionRef.doc(id));
442
+ }
443
+ await batch.commit();
444
+ committed = true;
445
+ deleted += ids.length;
446
+ break;
447
+ } catch (err) {
448
+ if (attempt < maxRetries) {
449
+ const delay = BASE_DELAY_MS * Math.pow(2, attempt);
450
+ await sleep(delay);
451
+ } else {
452
+ errors.push({
453
+ batchIndex: i,
454
+ error: err instanceof Error ? err : new Error(String(err)),
455
+ operationCount: ids.length
456
+ });
457
+ }
458
+ }
459
+ }
460
+ if (committed) {
461
+ completedBatches++;
462
+ }
463
+ if (onProgress) {
464
+ onProgress({
465
+ completedBatches,
466
+ totalBatches: chunks.length,
467
+ deletedSoFar: deleted
468
+ });
469
+ }
470
+ }
471
+ return { deleted, batches: completedBatches, errors };
472
+ }
473
+ async function bulkRemoveEdges(db, collectionPath, reader, params, options) {
474
+ const edges = await reader.findEdges(params);
475
+ const docIds = edges.map((e) => computeEdgeDocId(e.aUid, e.axbType, e.bUid));
476
+ return bulkDeleteDocIds(db, collectionPath, docIds, options);
477
+ }
478
+ async function removeNodeCascade(db, collectionPath, reader, uid, options) {
479
+ const [outgoingRaw, incomingRaw] = await Promise.all([
480
+ reader.findEdges({ aUid: uid }),
481
+ reader.findEdges({ bUid: uid })
482
+ ]);
483
+ const outgoing = outgoingRaw.filter((e) => e.axbType !== NODE_RELATION);
484
+ const incoming = incomingRaw.filter((e) => e.axbType !== NODE_RELATION);
485
+ const edgeDocIdSet = /* @__PURE__ */ new Set();
486
+ const allEdges = [];
487
+ for (const edge of [...outgoing, ...incoming]) {
488
+ const docId = computeEdgeDocId(edge.aUid, edge.axbType, edge.bUid);
489
+ if (!edgeDocIdSet.has(docId)) {
490
+ edgeDocIdSet.add(docId);
491
+ allEdges.push(edge);
492
+ }
493
+ }
494
+ const edgeDocIds = allEdges.map((e) => computeEdgeDocId(e.aUid, e.axbType, e.bUid));
495
+ const nodeDocId = computeNodeDocId(uid);
496
+ const allDocIds = [...edgeDocIds, nodeDocId];
497
+ const batchSize = Math.min(options?.batchSize ?? MAX_BATCH_SIZE, MAX_BATCH_SIZE);
498
+ const result = await bulkDeleteDocIds(db, collectionPath, allDocIds, {
499
+ ...options,
500
+ batchSize
501
+ });
502
+ const totalChunks = Math.ceil(allDocIds.length / batchSize);
503
+ const nodeChunkIndex = totalChunks - 1;
504
+ const nodeDeleted = !result.errors.some((e) => e.batchIndex === nodeChunkIndex);
505
+ return {
506
+ ...result,
507
+ edgesDeleted: nodeDeleted ? result.deleted - 1 : result.deleted,
508
+ nodeDeleted
509
+ };
510
+ }
511
+
512
+ // src/dynamic-registry.ts
513
+ import { createHash as createHash2 } from "crypto";
514
+
515
+ // src/json-schema.ts
516
+ import Ajv from "ajv";
517
+ var ajv = new Ajv({ allErrors: true, strict: false });
518
+ function compileSchema(schema, label) {
519
+ const validate = ajv.compile(schema);
520
+ return (data) => {
521
+ if (!validate(data)) {
522
+ const errors = validate.errors ?? [];
523
+ const messages = errors.map((err) => `${err.instancePath || "/"}${err.message ? ": " + err.message : ""}`).join("; ");
524
+ throw new ValidationError(
525
+ `Data validation failed${label ? " for " + label : ""}: ${messages}`,
526
+ errors
527
+ );
528
+ }
529
+ };
530
+ }
531
+ function jsonSchemaToFieldMeta(schema) {
532
+ if (!schema || schema.type !== "object" || !schema.properties) return [];
533
+ const requiredSet = new Set(
534
+ Array.isArray(schema.required) ? schema.required : []
535
+ );
536
+ return Object.entries(schema.properties).map(
537
+ ([name, prop]) => propertyToFieldMeta(name, prop, requiredSet.has(name))
538
+ );
539
+ }
540
+ function propertyToFieldMeta(name, prop, required) {
541
+ if (!prop) return { name, type: "unknown", required };
542
+ if (Array.isArray(prop.enum)) {
543
+ return {
544
+ name,
545
+ type: "enum",
546
+ required,
547
+ enumValues: prop.enum,
548
+ description: prop.description
549
+ };
550
+ }
551
+ if (Array.isArray(prop.oneOf) || Array.isArray(prop.anyOf)) {
552
+ const variants = prop.oneOf ?? prop.anyOf;
553
+ const nonNull = variants.filter((v) => v.type !== "null");
554
+ if (nonNull.length === 1) {
555
+ return propertyToFieldMeta(name, nonNull[0], false);
556
+ }
557
+ return { name, type: "unknown", required, description: prop.description };
558
+ }
559
+ const type = prop.type;
560
+ if (type === "string") {
561
+ return {
562
+ name,
563
+ type: "string",
564
+ required,
565
+ minLength: prop.minLength,
566
+ maxLength: prop.maxLength,
567
+ pattern: prop.pattern,
568
+ description: prop.description
569
+ };
570
+ }
571
+ if (type === "number" || type === "integer") {
572
+ return {
573
+ name,
574
+ type: "number",
575
+ required,
576
+ min: prop.minimum,
577
+ max: prop.maximum,
578
+ isInt: type === "integer" ? true : void 0,
579
+ description: prop.description
580
+ };
581
+ }
582
+ if (type === "boolean") {
583
+ return { name, type: "boolean", required, description: prop.description };
584
+ }
585
+ if (type === "array") {
586
+ const itemMeta = prop.items ? propertyToFieldMeta("item", prop.items, true) : void 0;
587
+ return {
588
+ name,
589
+ type: "array",
590
+ required,
591
+ itemMeta,
592
+ description: prop.description
593
+ };
594
+ }
595
+ if (type === "object") {
596
+ return {
597
+ name,
598
+ type: "object",
599
+ required,
600
+ fields: jsonSchemaToFieldMeta(prop),
601
+ description: prop.description
602
+ };
603
+ }
604
+ return { name, type: "unknown", required, description: prop.description };
605
+ }
606
+
607
+ // src/registry.ts
608
+ function tripleKey(aType, axbType, bType) {
609
+ return `${aType}:${axbType}:${bType}`;
610
+ }
611
+ function createRegistry(input) {
612
+ const map = /* @__PURE__ */ new Map();
613
+ let entries;
614
+ if (Array.isArray(input)) {
615
+ entries = input;
616
+ } else {
617
+ entries = discoveryToEntries(input);
618
+ }
619
+ const entryList = Object.freeze([...entries]);
620
+ for (const entry of entries) {
621
+ const key = tripleKey(entry.aType, entry.axbType, entry.bType);
622
+ const validator = entry.jsonSchema ? compileSchema(entry.jsonSchema, `(${entry.aType}) -[${entry.axbType}]-> (${entry.bType})`) : void 0;
623
+ map.set(key, { entry, validate: validator });
624
+ }
625
+ return {
626
+ lookup(aType, axbType, bType) {
627
+ return map.get(tripleKey(aType, axbType, bType))?.entry;
628
+ },
629
+ validate(aType, axbType, bType, data) {
630
+ const rec = map.get(tripleKey(aType, axbType, bType));
631
+ if (!rec) {
632
+ throw new RegistryViolationError(aType, axbType, bType);
633
+ }
634
+ if (rec.validate) {
635
+ try {
636
+ rec.validate(data);
637
+ } catch (err) {
638
+ if (err instanceof ValidationError) throw err;
639
+ throw new ValidationError(
640
+ `Data validation failed for (${aType}) -[${axbType}]-> (${bType})`,
641
+ err
642
+ );
643
+ }
644
+ }
645
+ },
646
+ entries() {
647
+ return entryList;
648
+ }
649
+ };
650
+ }
651
+ function discoveryToEntries(discovery) {
652
+ const entries = [];
653
+ for (const [name, entity] of discovery.nodes) {
654
+ entries.push({
655
+ aType: name,
656
+ axbType: NODE_RELATION,
657
+ bType: name,
658
+ jsonSchema: entity.schema,
659
+ description: entity.description,
660
+ titleField: entity.titleField,
661
+ subtitleField: entity.subtitleField
662
+ });
663
+ }
664
+ for (const [axbType, entity] of discovery.edges) {
665
+ const topology = entity.topology;
666
+ if (!topology) continue;
667
+ const fromTypes = Array.isArray(topology.from) ? topology.from : [topology.from];
668
+ const toTypes = Array.isArray(topology.to) ? topology.to : [topology.to];
669
+ for (const aType of fromTypes) {
670
+ for (const bType of toTypes) {
671
+ entries.push({
672
+ aType,
673
+ axbType,
674
+ bType,
675
+ jsonSchema: entity.schema,
676
+ description: entity.description,
677
+ inverseLabel: topology.inverseLabel,
678
+ titleField: entity.titleField,
679
+ subtitleField: entity.subtitleField
680
+ });
681
+ }
682
+ }
683
+ }
684
+ return entries;
685
+ }
686
+
687
+ // src/dynamic-registry.ts
688
+ var META_NODE_TYPE = "nodeType";
689
+ var META_EDGE_TYPE = "edgeType";
690
+ var NODE_TYPE_SCHEMA = {
691
+ type: "object",
692
+ required: ["name", "jsonSchema"],
693
+ properties: {
694
+ name: { type: "string", minLength: 1 },
695
+ jsonSchema: { type: "object" },
696
+ description: { type: "string" },
697
+ titleField: { type: "string" },
698
+ subtitleField: { type: "string" },
699
+ viewTemplate: { type: "string" },
700
+ viewCss: { type: "string" }
701
+ },
702
+ additionalProperties: false
703
+ };
704
+ var EDGE_TYPE_SCHEMA = {
705
+ type: "object",
706
+ required: ["name", "from", "to"],
707
+ properties: {
708
+ name: { type: "string", minLength: 1 },
709
+ from: {
710
+ oneOf: [
711
+ { type: "string", minLength: 1 },
712
+ { type: "array", items: { type: "string", minLength: 1 }, minItems: 1 }
713
+ ]
714
+ },
715
+ to: {
716
+ oneOf: [
717
+ { type: "string", minLength: 1 },
718
+ { type: "array", items: { type: "string", minLength: 1 }, minItems: 1 }
719
+ ]
720
+ },
721
+ jsonSchema: { type: "object" },
722
+ inverseLabel: { type: "string" },
723
+ description: { type: "string" },
724
+ titleField: { type: "string" },
725
+ subtitleField: { type: "string" },
726
+ viewTemplate: { type: "string" },
727
+ viewCss: { type: "string" }
728
+ },
729
+ additionalProperties: false
730
+ };
731
+ var BOOTSTRAP_ENTRIES = [
732
+ {
733
+ aType: META_NODE_TYPE,
734
+ axbType: NODE_RELATION,
735
+ bType: META_NODE_TYPE,
736
+ jsonSchema: NODE_TYPE_SCHEMA,
737
+ description: "Meta-type: defines a node type"
738
+ },
739
+ {
740
+ aType: META_EDGE_TYPE,
741
+ axbType: NODE_RELATION,
742
+ bType: META_EDGE_TYPE,
743
+ jsonSchema: EDGE_TYPE_SCHEMA,
744
+ description: "Meta-type: defines an edge type"
745
+ }
746
+ ];
747
+ function createBootstrapRegistry() {
748
+ return createRegistry([...BOOTSTRAP_ENTRIES]);
749
+ }
750
+ function generateDeterministicUid(metaType, name) {
751
+ const hash = createHash2("sha256").update(`${metaType}:${name}`).digest("base64url");
752
+ return hash.slice(0, 21);
753
+ }
754
+ async function createRegistryFromGraph(reader) {
755
+ const [nodeTypes, edgeTypes] = await Promise.all([
756
+ reader.findNodes({ aType: META_NODE_TYPE }),
757
+ reader.findNodes({ aType: META_EDGE_TYPE })
758
+ ]);
759
+ const entries = [...BOOTSTRAP_ENTRIES];
760
+ for (const record of nodeTypes) {
761
+ const data = record.data;
762
+ entries.push({
763
+ aType: data.name,
764
+ axbType: NODE_RELATION,
765
+ bType: data.name,
766
+ jsonSchema: data.jsonSchema,
767
+ description: data.description,
768
+ titleField: data.titleField,
769
+ subtitleField: data.subtitleField
770
+ });
771
+ }
772
+ for (const record of edgeTypes) {
773
+ const data = record.data;
774
+ const fromTypes = Array.isArray(data.from) ? data.from : [data.from];
775
+ const toTypes = Array.isArray(data.to) ? data.to : [data.to];
776
+ for (const aType of fromTypes) {
777
+ for (const bType of toTypes) {
778
+ entries.push({
779
+ aType,
780
+ axbType: data.name,
781
+ bType,
782
+ jsonSchema: data.jsonSchema,
783
+ description: data.description,
784
+ inverseLabel: data.inverseLabel,
785
+ titleField: data.titleField,
786
+ subtitleField: data.subtitleField
787
+ });
788
+ }
789
+ }
790
+ }
791
+ return createRegistry(entries);
792
+ }
793
+
794
+ // src/client.ts
795
+ var _standardModeWarned = false;
796
+ var RESERVED_TYPE_NAMES = /* @__PURE__ */ new Set([META_NODE_TYPE, META_EDGE_TYPE]);
797
+ var GraphClientImpl = class {
798
+ constructor(db, collectionPath, options) {
799
+ this.db = db;
800
+ this.adapter = createFirestoreAdapter(db, collectionPath);
801
+ if (options?.registry && options?.registryMode) {
802
+ throw new DynamicRegistryError(
803
+ 'Cannot provide both "registry" and "registryMode". Use "registry" for static mode or "registryMode" for dynamic mode.'
804
+ );
805
+ }
806
+ if (options?.registryMode) {
807
+ this.dynamicConfig = options.registryMode;
808
+ this.bootstrapRegistry = createBootstrapRegistry();
809
+ const metaCollectionPath = options.registryMode.collection;
810
+ if (metaCollectionPath && metaCollectionPath !== collectionPath) {
811
+ this.metaAdapter = createFirestoreAdapter(db, metaCollectionPath);
812
+ }
813
+ } else {
814
+ this.staticRegistry = options?.registry;
815
+ }
816
+ const requestedMode = options?.queryMode ?? "pipeline";
817
+ const isEmulator = !!process.env.FIRESTORE_EMULATOR_HOST;
818
+ if (isEmulator) {
819
+ this.queryMode = "standard";
820
+ } else {
821
+ this.queryMode = requestedMode;
822
+ }
823
+ if (this.queryMode === "standard" && !isEmulator && requestedMode === "standard" && !_standardModeWarned) {
824
+ _standardModeWarned = true;
825
+ console.warn(
826
+ "[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"
827
+ );
828
+ }
829
+ if (this.queryMode === "pipeline") {
830
+ this.pipelineAdapter = createPipelineQueryAdapter(db, collectionPath);
831
+ if (this.metaAdapter) {
832
+ this.metaPipelineAdapter = createPipelineQueryAdapter(
833
+ db,
834
+ options.registryMode.collection
835
+ );
836
+ }
837
+ }
838
+ }
839
+ adapter;
840
+ pipelineAdapter;
841
+ queryMode;
842
+ // Static mode
843
+ staticRegistry;
844
+ // Dynamic mode
845
+ dynamicConfig;
846
+ bootstrapRegistry;
847
+ dynamicRegistry;
848
+ metaAdapter;
849
+ metaPipelineAdapter;
850
+ // ---------------------------------------------------------------------------
851
+ // Registry routing
852
+ // ---------------------------------------------------------------------------
853
+ /**
854
+ * Get the appropriate registry for validating a write to the given type.
855
+ *
856
+ * - Static mode: returns staticRegistry (or undefined if none set)
857
+ * - Dynamic mode:
858
+ * - Meta-types (nodeType, edgeType): validated against bootstrapRegistry
859
+ * - Domain types: validated against dynamicRegistry (falls back to
860
+ * bootstrapRegistry which rejects unknown types)
861
+ */
862
+ getRegistryForType(aType) {
863
+ if (!this.dynamicConfig) return this.staticRegistry;
864
+ if (aType === META_NODE_TYPE || aType === META_EDGE_TYPE) {
865
+ return this.bootstrapRegistry;
866
+ }
867
+ return this.dynamicRegistry ?? this.bootstrapRegistry;
868
+ }
869
+ /**
870
+ * Get the Firestore adapter for writing the given type.
871
+ * Meta-types route to metaAdapter if a separate collection is configured.
872
+ */
873
+ getAdapterForType(aType) {
874
+ if (this.metaAdapter && (aType === META_NODE_TYPE || aType === META_EDGE_TYPE)) {
875
+ return this.metaAdapter;
876
+ }
877
+ return this.adapter;
878
+ }
879
+ /**
880
+ * Get the combined registry for transaction/batch context.
881
+ * In static mode, returns staticRegistry.
882
+ * In dynamic mode, returns dynamicRegistry (which includes bootstrap entries)
883
+ * or bootstrapRegistry if not yet reloaded.
884
+ */
885
+ getCombinedRegistry() {
886
+ if (!this.dynamicConfig) return this.staticRegistry;
887
+ return this.dynamicRegistry ?? this.bootstrapRegistry;
888
+ }
889
+ // ---------------------------------------------------------------------------
890
+ // Query dispatch
891
+ // ---------------------------------------------------------------------------
892
+ /**
893
+ * Dispatch a query to the appropriate adapter based on queryMode.
894
+ * Pipeline queries use the PipelineQueryAdapter; standard queries
895
+ * use the FirestoreAdapter.
896
+ */
897
+ executeQuery(filters, options) {
898
+ if (this.pipelineAdapter) {
899
+ return this.pipelineAdapter.query(filters, options);
900
+ }
901
+ return this.adapter.query(filters, options);
902
+ }
903
+ // ---------------------------------------------------------------------------
904
+ // GraphReader
905
+ // ---------------------------------------------------------------------------
906
+ async getNode(uid) {
907
+ const docId = computeNodeDocId(uid);
908
+ return this.adapter.getDoc(docId);
909
+ }
910
+ async getEdge(aUid, axbType, bUid) {
911
+ const docId = computeEdgeDocId(aUid, axbType, bUid);
912
+ return this.adapter.getDoc(docId);
913
+ }
914
+ async edgeExists(aUid, axbType, bUid) {
915
+ const record = await this.getEdge(aUid, axbType, bUid);
916
+ return record !== null;
917
+ }
918
+ async findEdges(params) {
919
+ const plan = buildEdgeQueryPlan(params);
920
+ if (plan.strategy === "get") {
921
+ const record = await this.adapter.getDoc(plan.docId);
922
+ return record ? [record] : [];
923
+ }
924
+ return this.executeQuery(plan.filters, plan.options);
925
+ }
926
+ async findNodes(params) {
927
+ const plan = buildNodeQueryPlan(params);
928
+ if (plan.strategy === "get") {
929
+ const record = await this.adapter.getDoc(plan.docId);
930
+ return record ? [record] : [];
931
+ }
932
+ return this.executeQuery(plan.filters, plan.options);
933
+ }
934
+ // ---------------------------------------------------------------------------
935
+ // GraphWriter
936
+ // ---------------------------------------------------------------------------
937
+ async putNode(aType, uid, data) {
938
+ const registry = this.getRegistryForType(aType);
939
+ if (registry) {
940
+ registry.validate(aType, NODE_RELATION, aType, data);
941
+ }
942
+ const adapter = this.getAdapterForType(aType);
943
+ const docId = computeNodeDocId(uid);
944
+ const record = buildNodeRecord(aType, uid, data);
945
+ await adapter.setDoc(docId, record);
946
+ }
947
+ async putEdge(aType, aUid, axbType, bType, bUid, data) {
948
+ const registry = this.getRegistryForType(aType);
949
+ if (registry) {
950
+ registry.validate(aType, axbType, bType, data);
951
+ }
952
+ const adapter = this.getAdapterForType(aType);
953
+ const docId = computeEdgeDocId(aUid, axbType, bUid);
954
+ const record = buildEdgeRecord(aType, aUid, axbType, bType, bUid, data);
955
+ await adapter.setDoc(docId, record);
956
+ }
957
+ async updateNode(uid, data) {
958
+ const docId = computeNodeDocId(uid);
959
+ await this.adapter.updateDoc(docId, {
960
+ ...data,
961
+ updatedAt: FieldValue4.serverTimestamp()
962
+ });
963
+ }
964
+ async removeNode(uid) {
965
+ const docId = computeNodeDocId(uid);
966
+ await this.adapter.deleteDoc(docId);
967
+ }
968
+ async removeEdge(aUid, axbType, bUid) {
969
+ const docId = computeEdgeDocId(aUid, axbType, bUid);
970
+ await this.adapter.deleteDoc(docId);
971
+ }
972
+ // ---------------------------------------------------------------------------
973
+ // Transactions & Batches
974
+ // ---------------------------------------------------------------------------
975
+ async runTransaction(fn) {
976
+ return this.db.runTransaction(async (firestoreTx) => {
977
+ const adapter = createTransactionAdapter(
978
+ this.db,
979
+ this.adapter.collectionPath,
980
+ firestoreTx
981
+ );
982
+ const graphTx = new GraphTransactionImpl(adapter, this.getCombinedRegistry());
983
+ return fn(graphTx);
984
+ });
985
+ }
986
+ batch() {
987
+ const adapter = createBatchAdapter(this.db, this.adapter.collectionPath);
988
+ return new GraphBatchImpl(adapter, this.getCombinedRegistry());
989
+ }
990
+ // ---------------------------------------------------------------------------
991
+ // Bulk operations
992
+ // ---------------------------------------------------------------------------
993
+ async removeNodeCascade(uid, options) {
994
+ return removeNodeCascade(this.db, this.adapter.collectionPath, this, uid, options);
995
+ }
996
+ async bulkRemoveEdges(params, options) {
997
+ return bulkRemoveEdges(this.db, this.adapter.collectionPath, this, params, options);
998
+ }
999
+ // ---------------------------------------------------------------------------
1000
+ // Dynamic registry methods
1001
+ // ---------------------------------------------------------------------------
1002
+ async defineNodeType(name, jsonSchema, description, options) {
1003
+ if (!this.dynamicConfig) {
1004
+ throw new DynamicRegistryError(
1005
+ 'defineNodeType() is only available in dynamic registry mode. Pass registryMode: { mode: "dynamic" } to createGraphClient().'
1006
+ );
1007
+ }
1008
+ if (RESERVED_TYPE_NAMES.has(name)) {
1009
+ throw new DynamicRegistryError(
1010
+ `Cannot define type "${name}": this name is reserved for the meta-registry.`
1011
+ );
1012
+ }
1013
+ const uid = generateDeterministicUid(META_NODE_TYPE, name);
1014
+ const data = { name, jsonSchema };
1015
+ if (description !== void 0) data.description = description;
1016
+ if (options?.titleField !== void 0) data.titleField = options.titleField;
1017
+ if (options?.subtitleField !== void 0) data.subtitleField = options.subtitleField;
1018
+ if (options?.viewTemplate !== void 0) data.viewTemplate = options.viewTemplate;
1019
+ if (options?.viewCss !== void 0) data.viewCss = options.viewCss;
1020
+ await this.putNode(META_NODE_TYPE, uid, data);
1021
+ }
1022
+ async defineEdgeType(name, topology, jsonSchema, description, options) {
1023
+ if (!this.dynamicConfig) {
1024
+ throw new DynamicRegistryError(
1025
+ 'defineEdgeType() is only available in dynamic registry mode. Pass registryMode: { mode: "dynamic" } to createGraphClient().'
1026
+ );
1027
+ }
1028
+ if (RESERVED_TYPE_NAMES.has(name)) {
1029
+ throw new DynamicRegistryError(
1030
+ `Cannot define type "${name}": this name is reserved for the meta-registry.`
1031
+ );
1032
+ }
1033
+ const uid = generateDeterministicUid(META_EDGE_TYPE, name);
1034
+ const data = {
1035
+ name,
1036
+ from: topology.from,
1037
+ to: topology.to
1038
+ };
1039
+ if (jsonSchema !== void 0) data.jsonSchema = jsonSchema;
1040
+ if (topology.inverseLabel !== void 0) data.inverseLabel = topology.inverseLabel;
1041
+ if (description !== void 0) data.description = description;
1042
+ if (options?.titleField !== void 0) data.titleField = options.titleField;
1043
+ if (options?.subtitleField !== void 0) data.subtitleField = options.subtitleField;
1044
+ if (options?.viewTemplate !== void 0) data.viewTemplate = options.viewTemplate;
1045
+ if (options?.viewCss !== void 0) data.viewCss = options.viewCss;
1046
+ await this.putNode(META_EDGE_TYPE, uid, data);
1047
+ }
1048
+ async reloadRegistry() {
1049
+ if (!this.dynamicConfig) {
1050
+ throw new DynamicRegistryError(
1051
+ 'reloadRegistry() is only available in dynamic registry mode. Pass registryMode: { mode: "dynamic" } to createGraphClient().'
1052
+ );
1053
+ }
1054
+ const reader = this.createMetaReader();
1055
+ this.dynamicRegistry = await createRegistryFromGraph(reader);
1056
+ }
1057
+ /**
1058
+ * Create a GraphReader for the meta-collection.
1059
+ * If meta-collection is the same as main collection, returns `this`.
1060
+ * If separate, creates a lightweight reader wrapping the meta adapter.
1061
+ */
1062
+ createMetaReader() {
1063
+ if (!this.metaAdapter) return this;
1064
+ const adapter = this.metaAdapter;
1065
+ const pipelineAdapter = this.metaPipelineAdapter;
1066
+ const executeMetaQuery = (filters, options) => {
1067
+ if (pipelineAdapter) return pipelineAdapter.query(filters, options);
1068
+ return adapter.query(filters, options);
1069
+ };
1070
+ return {
1071
+ async getNode(uid) {
1072
+ return adapter.getDoc(computeNodeDocId(uid));
1073
+ },
1074
+ async getEdge(aUid, axbType, bUid) {
1075
+ return adapter.getDoc(computeEdgeDocId(aUid, axbType, bUid));
1076
+ },
1077
+ async edgeExists(aUid, axbType, bUid) {
1078
+ const record = await adapter.getDoc(computeEdgeDocId(aUid, axbType, bUid));
1079
+ return record !== null;
1080
+ },
1081
+ async findEdges(params) {
1082
+ const plan = buildEdgeQueryPlan(params);
1083
+ if (plan.strategy === "get") {
1084
+ const record = await adapter.getDoc(plan.docId);
1085
+ return record ? [record] : [];
1086
+ }
1087
+ return executeMetaQuery(plan.filters, plan.options);
1088
+ },
1089
+ async findNodes(params) {
1090
+ const plan = buildNodeQueryPlan(params);
1091
+ if (plan.strategy === "get") {
1092
+ const record = await adapter.getDoc(plan.docId);
1093
+ return record ? [record] : [];
1094
+ }
1095
+ return executeMetaQuery(plan.filters, plan.options);
1096
+ }
1097
+ };
1098
+ }
1099
+ };
1100
+ function createGraphClient(db, collectionPath, options) {
1101
+ return new GraphClientImpl(db, collectionPath, options);
1102
+ }
1103
+
1104
+ // src/id.ts
1105
+ import { nanoid } from "nanoid";
1106
+ function generateId() {
1107
+ return nanoid();
1108
+ }
1109
+
1110
+ // src/traverse.ts
1111
+ var DEFAULT_LIMIT = 10;
1112
+ var DEFAULT_MAX_READS = 100;
1113
+ var DEFAULT_CONCURRENCY = 5;
1114
+ var Semaphore = class {
1115
+ constructor(slots) {
1116
+ this.slots = slots;
1117
+ }
1118
+ queue = [];
1119
+ active = 0;
1120
+ async acquire() {
1121
+ if (this.active < this.slots) {
1122
+ this.active++;
1123
+ return;
1124
+ }
1125
+ return new Promise((resolve2) => {
1126
+ this.queue.push(resolve2);
1127
+ });
1128
+ }
1129
+ release() {
1130
+ this.active--;
1131
+ const next = this.queue.shift();
1132
+ if (next) {
1133
+ this.active++;
1134
+ next();
1135
+ }
1136
+ }
1137
+ };
1138
+ var TraversalBuilderImpl = class {
1139
+ constructor(reader, startUid) {
1140
+ this.reader = reader;
1141
+ this.startUid = startUid;
1142
+ }
1143
+ hops = [];
1144
+ follow(axbType, options) {
1145
+ this.hops.push({ axbType, ...options });
1146
+ return this;
1147
+ }
1148
+ async run(options) {
1149
+ if (this.hops.length === 0) {
1150
+ throw new TraversalError("Traversal requires at least one follow() hop");
1151
+ }
1152
+ const maxReads = options?.maxReads ?? DEFAULT_MAX_READS;
1153
+ const concurrency = options?.concurrency ?? DEFAULT_CONCURRENCY;
1154
+ const returnIntermediates = options?.returnIntermediates ?? false;
1155
+ const semaphore = new Semaphore(concurrency);
1156
+ let totalReads = 0;
1157
+ let truncated = false;
1158
+ let sourceUids = [this.startUid];
1159
+ const hopResults = [];
1160
+ for (let depth = 0; depth < this.hops.length; depth++) {
1161
+ const hop = this.hops[depth];
1162
+ if (sourceUids.length === 0) {
1163
+ hopResults.push({
1164
+ axbType: hop.axbType,
1165
+ depth,
1166
+ edges: [],
1167
+ sourceCount: 0,
1168
+ truncated: false
1169
+ });
1170
+ continue;
1171
+ }
1172
+ const hopEdges = [];
1173
+ const sourceCount = sourceUids.length;
1174
+ let hopTruncated = false;
1175
+ const tasks = sourceUids.map((uid) => async () => {
1176
+ if (totalReads >= maxReads) {
1177
+ hopTruncated = true;
1178
+ return;
1179
+ }
1180
+ await semaphore.acquire();
1181
+ try {
1182
+ if (totalReads >= maxReads) {
1183
+ hopTruncated = true;
1184
+ return;
1185
+ }
1186
+ totalReads++;
1187
+ const direction2 = hop.direction ?? "forward";
1188
+ const params = { axbType: hop.axbType };
1189
+ if (direction2 === "forward") {
1190
+ params.aUid = uid;
1191
+ if (hop.bType) params.bType = hop.bType;
1192
+ } else {
1193
+ params.bUid = uid;
1194
+ if (hop.aType) params.aType = hop.aType;
1195
+ }
1196
+ if (direction2 === "forward" && hop.aType) {
1197
+ params.aType = hop.aType;
1198
+ }
1199
+ if (direction2 === "reverse" && hop.bType) {
1200
+ params.bType = hop.bType;
1201
+ }
1202
+ if (hop.orderBy) params.orderBy = hop.orderBy;
1203
+ const limit = hop.limit ?? DEFAULT_LIMIT;
1204
+ if (!hop.filter) {
1205
+ params.limit = limit;
1206
+ }
1207
+ let edges = await this.reader.findEdges(params);
1208
+ if (hop.filter) {
1209
+ edges = edges.filter(hop.filter);
1210
+ edges = edges.slice(0, limit);
1211
+ }
1212
+ hopEdges.push(...edges);
1213
+ } finally {
1214
+ semaphore.release();
1215
+ }
1216
+ });
1217
+ await Promise.all(tasks.map((task) => task()));
1218
+ hopResults.push({
1219
+ axbType: hop.axbType,
1220
+ depth,
1221
+ edges: returnIntermediates ? [...hopEdges] : hopEdges,
1222
+ sourceCount,
1223
+ truncated: hopTruncated
1224
+ });
1225
+ if (hopTruncated) {
1226
+ truncated = true;
1227
+ }
1228
+ const direction = hop.direction ?? "forward";
1229
+ sourceUids = [...new Set(
1230
+ hopEdges.map((e) => direction === "forward" ? e.bUid : e.aUid)
1231
+ )];
1232
+ }
1233
+ const lastHop = hopResults[hopResults.length - 1];
1234
+ return {
1235
+ nodes: lastHop.edges,
1236
+ hops: hopResults,
1237
+ totalReads,
1238
+ truncated
1239
+ };
1240
+ }
1241
+ };
1242
+ function createTraversal(reader, startUid) {
1243
+ return new TraversalBuilderImpl(reader, startUid);
1244
+ }
1245
+
1246
+ // src/views.ts
1247
+ function sanitizeTagPart(s) {
1248
+ return s.toLowerCase().replace(/[^a-z0-9]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
1249
+ }
1250
+ function getCustomElements() {
1251
+ const g = globalThis;
1252
+ if (g.customElements && typeof g.customElements.define === "function") {
1253
+ return g.customElements;
1254
+ }
1255
+ return null;
1256
+ }
1257
+ function resilientView(ViewClass, tagName) {
1258
+ const g = globalThis;
1259
+ if (!g.HTMLElement) return ViewClass;
1260
+ const Base = g.HTMLElement;
1261
+ const Wrapped = class extends ViewClass {
1262
+ connectedCallback() {
1263
+ try {
1264
+ super.connectedCallback?.();
1265
+ } catch (err) {
1266
+ console.warn(`[firegraph] <${tagName}> connectedCallback error:`, err);
1267
+ this._showError(err);
1268
+ }
1269
+ }
1270
+ disconnectedCallback() {
1271
+ try {
1272
+ super.disconnectedCallback?.();
1273
+ } catch (err) {
1274
+ console.warn(`[firegraph] <${tagName}> disconnectedCallback error:`, err);
1275
+ }
1276
+ }
1277
+ set data(v) {
1278
+ try {
1279
+ super.data = v;
1280
+ } catch (err) {
1281
+ console.warn(`[firegraph] <${tagName}> data setter error:`, err);
1282
+ this._showError(err);
1283
+ }
1284
+ }
1285
+ get data() {
1286
+ try {
1287
+ return super.data;
1288
+ } catch {
1289
+ return {};
1290
+ }
1291
+ }
1292
+ _showError(err) {
1293
+ try {
1294
+ this.innerHTML = `<div style="padding:6px;color:#f87171;font-size:11px;font-family:monospace;">View error in &lt;${tagName}&gt;: ${err instanceof Error ? err.message : String(err)}</div>`;
1295
+ } catch {
1296
+ }
1297
+ }
1298
+ };
1299
+ Wrapped.viewName = ViewClass.viewName;
1300
+ Wrapped.description = ViewClass.description;
1301
+ return Wrapped;
1302
+ }
1303
+ function defineViews(input) {
1304
+ const nodes = {};
1305
+ const edges = {};
1306
+ const registry = getCustomElements();
1307
+ for (const [entityType, config] of Object.entries(input.nodes ?? {})) {
1308
+ const viewMetas = [];
1309
+ for (const ViewClass of config.views) {
1310
+ const tagName = `fg-${sanitizeTagPart(entityType)}-${sanitizeTagPart(ViewClass.viewName)}`;
1311
+ viewMetas.push({
1312
+ tagName,
1313
+ viewName: ViewClass.viewName,
1314
+ description: ViewClass.description
1315
+ });
1316
+ if (registry && !registry.get(tagName)) {
1317
+ registry.define(tagName, resilientView(ViewClass, tagName));
1318
+ }
1319
+ }
1320
+ nodes[entityType] = {
1321
+ views: viewMetas,
1322
+ sampleData: config.sampleData
1323
+ };
1324
+ }
1325
+ for (const [axbType, config] of Object.entries(input.edges ?? {})) {
1326
+ const viewMetas = [];
1327
+ for (const ViewClass of config.views) {
1328
+ const tagName = `fg-edge-${sanitizeTagPart(axbType)}-${sanitizeTagPart(ViewClass.viewName)}`;
1329
+ viewMetas.push({
1330
+ tagName,
1331
+ viewName: ViewClass.viewName,
1332
+ description: ViewClass.description
1333
+ });
1334
+ if (registry && !registry.get(tagName)) {
1335
+ registry.define(tagName, resilientView(ViewClass, tagName));
1336
+ }
1337
+ }
1338
+ edges[axbType] = {
1339
+ views: viewMetas,
1340
+ sampleData: config.sampleData
1341
+ };
1342
+ }
1343
+ return { nodes, edges };
1344
+ }
1345
+
1346
+ // src/config.ts
1347
+ function defineConfig(config) {
1348
+ return config;
1349
+ }
1350
+ function resolveView(resolverConfig, availableViewNames, context) {
1351
+ if (!resolverConfig) return "json";
1352
+ const available = new Set(availableViewNames);
1353
+ if (context) {
1354
+ const contextDefault = resolverConfig[context];
1355
+ if (contextDefault && available.has(contextDefault)) {
1356
+ return contextDefault;
1357
+ }
1358
+ }
1359
+ if (resolverConfig.default && available.has(resolverConfig.default)) {
1360
+ return resolverConfig.default;
1361
+ }
1362
+ return "json";
1363
+ }
1364
+
1365
+ // src/discover.ts
1366
+ import { readFileSync, readdirSync, existsSync, statSync } from "fs";
1367
+ import { createRequire } from "module";
1368
+ import { join, resolve } from "path";
1369
+ var DiscoveryError = class extends FiregraphError {
1370
+ constructor(message) {
1371
+ super(message, "DISCOVERY_ERROR");
1372
+ this.name = "DiscoveryError";
1373
+ }
1374
+ };
1375
+ function readJson(filePath) {
1376
+ try {
1377
+ const raw = readFileSync(filePath, "utf-8");
1378
+ return JSON.parse(raw);
1379
+ } catch (err) {
1380
+ const msg = err instanceof SyntaxError ? `Invalid JSON in ${filePath}: ${err.message}` : `Cannot read ${filePath}: ${err.message}`;
1381
+ throw new DiscoveryError(msg);
1382
+ }
1383
+ }
1384
+ function readJsonIfExists(filePath) {
1385
+ if (!existsSync(filePath)) return void 0;
1386
+ return readJson(filePath);
1387
+ }
1388
+ var SCHEMA_SCRIPT_EXTENSIONS = [".ts", ".js", ".mts", ".mjs"];
1389
+ function loadSchema(dir, entityLabel) {
1390
+ for (const ext of SCHEMA_SCRIPT_EXTENSIONS) {
1391
+ const candidate = join(dir, `schema${ext}`);
1392
+ if (existsSync(candidate)) {
1393
+ return loadSchemaModule(candidate, entityLabel);
1394
+ }
1395
+ }
1396
+ const jsonPath = join(dir, "schema.json");
1397
+ if (existsSync(jsonPath)) {
1398
+ return readJson(jsonPath);
1399
+ }
1400
+ throw new DiscoveryError(
1401
+ `Missing schema for ${entityLabel} in ${dir}. Provide a schema.ts (or .js/.mts/.mjs) or schema.json file.`
1402
+ );
1403
+ }
1404
+ var _jiti;
1405
+ function getJiti() {
1406
+ if (!_jiti) {
1407
+ const base = typeof __filename !== "undefined" ? __filename : import.meta.url;
1408
+ const esmRequire = createRequire(base);
1409
+ const { createJiti } = esmRequire("jiti");
1410
+ _jiti = createJiti(base, { interopDefault: true });
1411
+ }
1412
+ return _jiti;
1413
+ }
1414
+ function loadSchemaModule(filePath, entityLabel) {
1415
+ try {
1416
+ const jiti = getJiti();
1417
+ const mod = jiti(filePath);
1418
+ const schema = mod && typeof mod === "object" && "default" in mod ? mod.default : mod;
1419
+ if (!schema || typeof schema !== "object") {
1420
+ throw new DiscoveryError(
1421
+ `Schema file ${filePath} for ${entityLabel} must default-export a JSON Schema object.`
1422
+ );
1423
+ }
1424
+ return schema;
1425
+ } catch (err) {
1426
+ if (err instanceof DiscoveryError) throw err;
1427
+ throw new DiscoveryError(
1428
+ `Failed to load schema module ${filePath} for ${entityLabel}: ${err.message}`
1429
+ );
1430
+ }
1431
+ }
1432
+ var VIEW_EXTENSIONS = [".ts", ".js", ".mts", ".mjs"];
1433
+ function findViewsFile(dir) {
1434
+ for (const ext of VIEW_EXTENSIONS) {
1435
+ const candidate = join(dir, `views${ext}`);
1436
+ if (existsSync(candidate)) return candidate;
1437
+ }
1438
+ return void 0;
1439
+ }
1440
+ function loadNodeEntity(dir, name) {
1441
+ const schema = loadSchema(dir, `node type "${name}"`);
1442
+ const meta = readJsonIfExists(join(dir, "meta.json"));
1443
+ const sampleData = readJsonIfExists(join(dir, "sample.json"));
1444
+ const viewsPath = findViewsFile(dir);
1445
+ return {
1446
+ kind: "node",
1447
+ name,
1448
+ schema,
1449
+ description: meta?.description,
1450
+ titleField: meta?.titleField,
1451
+ subtitleField: meta?.subtitleField,
1452
+ viewDefaults: meta?.viewDefaults,
1453
+ viewsPath,
1454
+ sampleData
1455
+ };
1456
+ }
1457
+ function loadEdgeEntity(dir, name) {
1458
+ const schema = loadSchema(dir, `edge type "${name}"`);
1459
+ const edgePath = join(dir, "edge.json");
1460
+ if (!existsSync(edgePath)) {
1461
+ throw new DiscoveryError(
1462
+ `Missing edge.json for edge type "${name}" in ${dir}. Edge entities must declare topology (from/to node types).`
1463
+ );
1464
+ }
1465
+ const topology = readJson(edgePath);
1466
+ if (!topology.from) {
1467
+ throw new DiscoveryError(
1468
+ `edge.json for "${name}" is missing required "from" field`
1469
+ );
1470
+ }
1471
+ if (!topology.to) {
1472
+ throw new DiscoveryError(
1473
+ `edge.json for "${name}" is missing required "to" field`
1474
+ );
1475
+ }
1476
+ const meta = readJsonIfExists(join(dir, "meta.json"));
1477
+ const sampleData = readJsonIfExists(join(dir, "sample.json"));
1478
+ const viewsPath = findViewsFile(dir);
1479
+ return {
1480
+ kind: "edge",
1481
+ name,
1482
+ schema,
1483
+ topology,
1484
+ description: meta?.description,
1485
+ titleField: meta?.titleField,
1486
+ subtitleField: meta?.subtitleField,
1487
+ viewDefaults: meta?.viewDefaults,
1488
+ viewsPath,
1489
+ sampleData
1490
+ };
1491
+ }
1492
+ function getSubdirectories(dir) {
1493
+ if (!existsSync(dir)) return [];
1494
+ return readdirSync(dir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
1495
+ }
1496
+ function discoverEntities(entitiesDir) {
1497
+ const absDir = resolve(entitiesDir);
1498
+ if (!existsSync(absDir) || !statSync(absDir).isDirectory()) {
1499
+ throw new DiscoveryError(`Entities directory not found: ${entitiesDir}`);
1500
+ }
1501
+ const nodes = /* @__PURE__ */ new Map();
1502
+ const edges = /* @__PURE__ */ new Map();
1503
+ const warnings = [];
1504
+ const nodesDir = join(absDir, "nodes");
1505
+ for (const name of getSubdirectories(nodesDir)) {
1506
+ nodes.set(name, loadNodeEntity(join(nodesDir, name), name));
1507
+ }
1508
+ const edgesDir = join(absDir, "edges");
1509
+ for (const name of getSubdirectories(edgesDir)) {
1510
+ edges.set(name, loadEdgeEntity(join(edgesDir, name), name));
1511
+ }
1512
+ const nodeNames = new Set(nodes.keys());
1513
+ for (const [axbType, entity] of edges) {
1514
+ const topology = entity.topology;
1515
+ const fromTypes = Array.isArray(topology.from) ? topology.from : [topology.from];
1516
+ const toTypes = Array.isArray(topology.to) ? topology.to : [topology.to];
1517
+ for (const ref of [...fromTypes, ...toTypes]) {
1518
+ if (!nodeNames.has(ref)) {
1519
+ warnings.push({
1520
+ code: "DANGLING_TOPOLOGY_REF",
1521
+ message: `Edge "${axbType}" references node type "${ref}" which was not found in the nodes directory`
1522
+ });
1523
+ }
1524
+ }
1525
+ }
1526
+ return {
1527
+ result: { nodes, edges },
1528
+ warnings
1529
+ };
1530
+ }
1531
+ export {
1532
+ BOOTSTRAP_ENTRIES,
1533
+ DiscoveryError,
1534
+ DynamicRegistryError,
1535
+ EDGE_TYPE_SCHEMA,
1536
+ EdgeNotFoundError,
1537
+ FiregraphError,
1538
+ InvalidQueryError,
1539
+ META_EDGE_TYPE,
1540
+ META_NODE_TYPE,
1541
+ NODE_TYPE_SCHEMA,
1542
+ NodeNotFoundError,
1543
+ QueryClient,
1544
+ QueryClientError,
1545
+ RegistryViolationError,
1546
+ TraversalError,
1547
+ ValidationError,
1548
+ buildEdgeQueryPlan,
1549
+ buildEdgeRecord,
1550
+ buildNodeQueryPlan,
1551
+ buildNodeRecord,
1552
+ compileSchema,
1553
+ computeEdgeDocId,
1554
+ computeNodeDocId,
1555
+ createBootstrapRegistry,
1556
+ createGraphClient,
1557
+ createRegistry,
1558
+ createRegistryFromGraph,
1559
+ createTraversal,
1560
+ defineConfig,
1561
+ defineViews,
1562
+ discoverEntities,
1563
+ generateDeterministicUid,
1564
+ generateId,
1565
+ generateTypes,
1566
+ jsonSchemaToFieldMeta,
1567
+ resolveView
1568
+ };
1569
+ //# sourceMappingURL=index.js.map