@typicalday/firegraph 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +253 -6
- package/bin/firegraph.mjs +47 -0
- package/dist/codegen/index.d.cts +1 -1
- package/dist/codegen/index.d.ts +1 -1
- package/dist/editor/server/index.mjs +326 -38
- package/dist/{index-CG3R68Hu.d.cts → index-CQkofEC_.d.cts} +122 -2
- package/dist/{index-CG3R68Hu.d.ts → index-CQkofEC_.d.ts} +122 -2
- package/dist/index.cjs +557 -59
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +157 -4
- package/dist/index.d.ts +157 -4
- package/dist/index.js +548 -59
- package/dist/index.js.map +1 -1
- package/package.json +25 -23
|
@@ -29331,6 +29331,16 @@ import { createHash } from "node:crypto";
|
|
|
29331
29331
|
|
|
29332
29332
|
// src/internal/constants.ts
|
|
29333
29333
|
var NODE_RELATION = "is";
|
|
29334
|
+
var DEFAULT_QUERY_LIMIT = 500;
|
|
29335
|
+
var BUILTIN_FIELDS = /* @__PURE__ */ new Set([
|
|
29336
|
+
"aType",
|
|
29337
|
+
"aUid",
|
|
29338
|
+
"axbType",
|
|
29339
|
+
"bType",
|
|
29340
|
+
"bUid",
|
|
29341
|
+
"createdAt",
|
|
29342
|
+
"updatedAt"
|
|
29343
|
+
]);
|
|
29334
29344
|
var SHARD_SEPARATOR = ":";
|
|
29335
29345
|
|
|
29336
29346
|
// src/docid.ts
|
|
@@ -29409,6 +29419,21 @@ var DynamicRegistryError = class extends FiregraphError {
|
|
|
29409
29419
|
this.name = "DynamicRegistryError";
|
|
29410
29420
|
}
|
|
29411
29421
|
};
|
|
29422
|
+
var QuerySafetyError = class extends FiregraphError {
|
|
29423
|
+
constructor(message) {
|
|
29424
|
+
super(message, "QUERY_SAFETY");
|
|
29425
|
+
this.name = "QuerySafetyError";
|
|
29426
|
+
}
|
|
29427
|
+
};
|
|
29428
|
+
var RegistryScopeError = class extends FiregraphError {
|
|
29429
|
+
constructor(aType, axbType, bType, scopePath, allowedIn) {
|
|
29430
|
+
super(
|
|
29431
|
+
`Type (${aType}) -[${axbType}]-> (${bType}) is not allowed at scope "${scopePath || "root"}". Allowed in: [${allowedIn.join(", ")}]`,
|
|
29432
|
+
"REGISTRY_SCOPE"
|
|
29433
|
+
);
|
|
29434
|
+
this.name = "RegistryScopeError";
|
|
29435
|
+
}
|
|
29436
|
+
};
|
|
29412
29437
|
|
|
29413
29438
|
// src/query.ts
|
|
29414
29439
|
function buildEdgeQueryPlan(params) {
|
|
@@ -29422,27 +29447,32 @@ function buildEdgeQueryPlan(params) {
|
|
|
29422
29447
|
if (axbType) filters.push({ field: "axbType", op: "==", value: axbType });
|
|
29423
29448
|
if (bType) filters.push({ field: "bType", op: "==", value: bType });
|
|
29424
29449
|
if (bUid) filters.push({ field: "bUid", op: "==", value: bUid });
|
|
29425
|
-
const builtinFields = ["aType", "aUid", "axbType", "bType", "bUid", "createdAt", "updatedAt"];
|
|
29426
29450
|
if (params.where) {
|
|
29427
29451
|
for (const clause of params.where) {
|
|
29428
|
-
const field =
|
|
29452
|
+
const field = BUILTIN_FIELDS.has(clause.field) ? clause.field : clause.field.startsWith("data.") ? clause.field : `data.${clause.field}`;
|
|
29429
29453
|
filters.push({ field, op: clause.op, value: clause.value });
|
|
29430
29454
|
}
|
|
29431
29455
|
}
|
|
29432
29456
|
if (filters.length === 0) {
|
|
29433
29457
|
throw new InvalidQueryError("findEdges requires at least one filter parameter");
|
|
29434
29458
|
}
|
|
29435
|
-
const
|
|
29436
|
-
return { strategy: "query", filters, options };
|
|
29459
|
+
const effectiveLimit = limit === void 0 ? DEFAULT_QUERY_LIMIT : limit || void 0;
|
|
29460
|
+
return { strategy: "query", filters, options: { limit: effectiveLimit, orderBy } };
|
|
29437
29461
|
}
|
|
29438
29462
|
function buildNodeQueryPlan(params) {
|
|
29439
|
-
|
|
29440
|
-
|
|
29441
|
-
|
|
29442
|
-
|
|
29443
|
-
|
|
29444
|
-
|
|
29445
|
-
|
|
29463
|
+
const { aType, limit, orderBy } = params;
|
|
29464
|
+
const filters = [
|
|
29465
|
+
{ field: "aType", op: "==", value: aType },
|
|
29466
|
+
{ field: "axbType", op: "==", value: NODE_RELATION }
|
|
29467
|
+
];
|
|
29468
|
+
if (params.where) {
|
|
29469
|
+
for (const clause of params.where) {
|
|
29470
|
+
const field = BUILTIN_FIELDS.has(clause.field) ? clause.field : clause.field.startsWith("data.") ? clause.field : `data.${clause.field}`;
|
|
29471
|
+
filters.push({ field, op: clause.op, value: clause.value });
|
|
29472
|
+
}
|
|
29473
|
+
}
|
|
29474
|
+
const effectiveLimit = limit === void 0 ? DEFAULT_QUERY_LIMIT : limit || void 0;
|
|
29475
|
+
return { strategy: "query", filters, options: { limit: effectiveLimit, orderBy } };
|
|
29446
29476
|
}
|
|
29447
29477
|
|
|
29448
29478
|
// src/internal/firestore-adapter.ts
|
|
@@ -29595,10 +29625,62 @@ function createPipelineQueryAdapter(db2, collectionPath) {
|
|
|
29595
29625
|
|
|
29596
29626
|
// src/transaction.ts
|
|
29597
29627
|
import { FieldValue as FieldValue2 } from "@google-cloud/firestore";
|
|
29628
|
+
|
|
29629
|
+
// src/query-safety.ts
|
|
29630
|
+
var SAFE_INDEX_PATTERNS = [
|
|
29631
|
+
/* @__PURE__ */ new Set(["aUid", "axbType"]),
|
|
29632
|
+
/* @__PURE__ */ new Set(["axbType", "bUid"]),
|
|
29633
|
+
/* @__PURE__ */ new Set(["aType", "axbType"]),
|
|
29634
|
+
/* @__PURE__ */ new Set(["axbType", "bType"])
|
|
29635
|
+
];
|
|
29636
|
+
function analyzeQuerySafety(filters) {
|
|
29637
|
+
const builtinFieldsPresent = /* @__PURE__ */ new Set();
|
|
29638
|
+
let hasDataFilters = false;
|
|
29639
|
+
for (const f of filters) {
|
|
29640
|
+
if (BUILTIN_FIELDS.has(f.field)) {
|
|
29641
|
+
builtinFieldsPresent.add(f.field);
|
|
29642
|
+
} else {
|
|
29643
|
+
hasDataFilters = true;
|
|
29644
|
+
}
|
|
29645
|
+
}
|
|
29646
|
+
for (const pattern of SAFE_INDEX_PATTERNS) {
|
|
29647
|
+
let matched = true;
|
|
29648
|
+
for (const field of pattern) {
|
|
29649
|
+
if (!builtinFieldsPresent.has(field)) {
|
|
29650
|
+
matched = false;
|
|
29651
|
+
break;
|
|
29652
|
+
}
|
|
29653
|
+
}
|
|
29654
|
+
if (matched) {
|
|
29655
|
+
return { safe: true };
|
|
29656
|
+
}
|
|
29657
|
+
}
|
|
29658
|
+
const presentFields = [...builtinFieldsPresent];
|
|
29659
|
+
if (presentFields.length === 0 && hasDataFilters) {
|
|
29660
|
+
return {
|
|
29661
|
+
safe: false,
|
|
29662
|
+
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."
|
|
29663
|
+
};
|
|
29664
|
+
}
|
|
29665
|
+
if (hasDataFilters) {
|
|
29666
|
+
return {
|
|
29667
|
+
safe: false,
|
|
29668
|
+
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.`
|
|
29669
|
+
};
|
|
29670
|
+
}
|
|
29671
|
+
return {
|
|
29672
|
+
safe: false,
|
|
29673
|
+
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.`
|
|
29674
|
+
};
|
|
29675
|
+
}
|
|
29676
|
+
|
|
29677
|
+
// src/transaction.ts
|
|
29598
29678
|
var GraphTransactionImpl = class {
|
|
29599
|
-
constructor(adapter, registry2) {
|
|
29679
|
+
constructor(adapter, registry2, scanProtection = "error", scopePath = "") {
|
|
29600
29680
|
this.adapter = adapter;
|
|
29601
29681
|
this.registry = registry2;
|
|
29682
|
+
this.scanProtection = scanProtection;
|
|
29683
|
+
this.scopePath = scopePath;
|
|
29602
29684
|
}
|
|
29603
29685
|
async getNode(uid) {
|
|
29604
29686
|
const docId = computeNodeDocId(uid);
|
|
@@ -29612,12 +29694,22 @@ var GraphTransactionImpl = class {
|
|
|
29612
29694
|
const record2 = await this.getEdge(aUid, axbType, bUid);
|
|
29613
29695
|
return record2 !== null;
|
|
29614
29696
|
}
|
|
29697
|
+
checkQuerySafety(filters, allowCollectionScan) {
|
|
29698
|
+
if (allowCollectionScan || this.scanProtection === "off") return;
|
|
29699
|
+
const result = analyzeQuerySafety(filters);
|
|
29700
|
+
if (result.safe) return;
|
|
29701
|
+
if (this.scanProtection === "error") {
|
|
29702
|
+
throw new QuerySafetyError(result.reason);
|
|
29703
|
+
}
|
|
29704
|
+
console.warn(`[firegraph] Query safety warning: ${result.reason}`);
|
|
29705
|
+
}
|
|
29615
29706
|
async findEdges(params) {
|
|
29616
29707
|
const plan = buildEdgeQueryPlan(params);
|
|
29617
29708
|
if (plan.strategy === "get") {
|
|
29618
29709
|
const record2 = await this.adapter.getDoc(plan.docId);
|
|
29619
29710
|
return record2 ? [record2] : [];
|
|
29620
29711
|
}
|
|
29712
|
+
this.checkQuerySafety(plan.filters, params.allowCollectionScan);
|
|
29621
29713
|
return this.adapter.query(plan.filters, plan.options);
|
|
29622
29714
|
}
|
|
29623
29715
|
async findNodes(params) {
|
|
@@ -29626,11 +29718,12 @@ var GraphTransactionImpl = class {
|
|
|
29626
29718
|
const record2 = await this.adapter.getDoc(plan.docId);
|
|
29627
29719
|
return record2 ? [record2] : [];
|
|
29628
29720
|
}
|
|
29721
|
+
this.checkQuerySafety(plan.filters, params.allowCollectionScan);
|
|
29629
29722
|
return this.adapter.query(plan.filters, plan.options);
|
|
29630
29723
|
}
|
|
29631
29724
|
async putNode(aType, uid, data) {
|
|
29632
29725
|
if (this.registry) {
|
|
29633
|
-
this.registry.validate(aType, NODE_RELATION, aType, data);
|
|
29726
|
+
this.registry.validate(aType, NODE_RELATION, aType, data, this.scopePath);
|
|
29634
29727
|
}
|
|
29635
29728
|
const docId = computeNodeDocId(uid);
|
|
29636
29729
|
const record2 = buildNodeRecord(aType, uid, data);
|
|
@@ -29638,7 +29731,7 @@ var GraphTransactionImpl = class {
|
|
|
29638
29731
|
}
|
|
29639
29732
|
async putEdge(aType, aUid, axbType, bType, bUid, data) {
|
|
29640
29733
|
if (this.registry) {
|
|
29641
|
-
this.registry.validate(aType, axbType, bType, data);
|
|
29734
|
+
this.registry.validate(aType, axbType, bType, data, this.scopePath);
|
|
29642
29735
|
}
|
|
29643
29736
|
const docId = computeEdgeDocId(aUid, axbType, bUid);
|
|
29644
29737
|
const record2 = buildEdgeRecord(aType, aUid, axbType, bType, bUid, data);
|
|
@@ -29664,13 +29757,14 @@ var GraphTransactionImpl = class {
|
|
|
29664
29757
|
// src/batch.ts
|
|
29665
29758
|
import { FieldValue as FieldValue3 } from "@google-cloud/firestore";
|
|
29666
29759
|
var GraphBatchImpl = class {
|
|
29667
|
-
constructor(adapter, registry2) {
|
|
29760
|
+
constructor(adapter, registry2, scopePath = "") {
|
|
29668
29761
|
this.adapter = adapter;
|
|
29669
29762
|
this.registry = registry2;
|
|
29763
|
+
this.scopePath = scopePath;
|
|
29670
29764
|
}
|
|
29671
29765
|
async putNode(aType, uid, data) {
|
|
29672
29766
|
if (this.registry) {
|
|
29673
|
-
this.registry.validate(aType, NODE_RELATION, aType, data);
|
|
29767
|
+
this.registry.validate(aType, NODE_RELATION, aType, data, this.scopePath);
|
|
29674
29768
|
}
|
|
29675
29769
|
const docId = computeNodeDocId(uid);
|
|
29676
29770
|
const record2 = buildNodeRecord(aType, uid, data);
|
|
@@ -29678,7 +29772,7 @@ var GraphBatchImpl = class {
|
|
|
29678
29772
|
}
|
|
29679
29773
|
async putEdge(aType, aUid, axbType, bType, bUid, data) {
|
|
29680
29774
|
if (this.registry) {
|
|
29681
|
-
this.registry.validate(aType, axbType, bType, data);
|
|
29775
|
+
this.registry.validate(aType, axbType, bType, data, this.scopePath);
|
|
29682
29776
|
}
|
|
29683
29777
|
const docId = computeEdgeDocId(aUid, axbType, bUid);
|
|
29684
29778
|
const record2 = buildEdgeRecord(aType, aUid, axbType, bType, bUid, data);
|
|
@@ -29770,14 +29864,39 @@ async function bulkDeleteDocIds(db2, collectionPath, docIds, options) {
|
|
|
29770
29864
|
return { deleted, batches: completedBatches, errors };
|
|
29771
29865
|
}
|
|
29772
29866
|
async function bulkRemoveEdges(db2, collectionPath, reader, params, options) {
|
|
29773
|
-
const
|
|
29867
|
+
const effectiveParams = params.limit !== void 0 ? { ...params, allowCollectionScan: params.allowCollectionScan ?? true } : { ...params, limit: 0, allowCollectionScan: params.allowCollectionScan ?? true };
|
|
29868
|
+
const edges = await reader.findEdges(effectiveParams);
|
|
29774
29869
|
const docIds = edges.map((e) => computeEdgeDocId(e.aUid, e.axbType, e.bUid));
|
|
29775
29870
|
return bulkDeleteDocIds(db2, collectionPath, docIds, options);
|
|
29776
29871
|
}
|
|
29872
|
+
async function deleteSubcollectionsRecursive(db2, collectionPath, docId, options) {
|
|
29873
|
+
const docRef = db2.collection(collectionPath).doc(docId);
|
|
29874
|
+
const subcollections = await docRef.listCollections();
|
|
29875
|
+
if (subcollections.length === 0) return { deleted: 0, errors: [] };
|
|
29876
|
+
let totalDeleted = 0;
|
|
29877
|
+
const allErrors = [];
|
|
29878
|
+
const subOptions = options ? { batchSize: options.batchSize, maxRetries: options.maxRetries } : void 0;
|
|
29879
|
+
for (const subCollRef of subcollections) {
|
|
29880
|
+
const subCollPath = subCollRef.path;
|
|
29881
|
+
const snapshot = await subCollRef.select().get();
|
|
29882
|
+
const subDocIds = snapshot.docs.map((d) => d.id);
|
|
29883
|
+
for (const subDocId of subDocIds) {
|
|
29884
|
+
const subResult = await deleteSubcollectionsRecursive(db2, subCollPath, subDocId, subOptions);
|
|
29885
|
+
totalDeleted += subResult.deleted;
|
|
29886
|
+
allErrors.push(...subResult.errors);
|
|
29887
|
+
}
|
|
29888
|
+
if (subDocIds.length > 0) {
|
|
29889
|
+
const result = await bulkDeleteDocIds(db2, subCollPath, subDocIds, subOptions);
|
|
29890
|
+
totalDeleted += result.deleted;
|
|
29891
|
+
allErrors.push(...result.errors);
|
|
29892
|
+
}
|
|
29893
|
+
}
|
|
29894
|
+
return { deleted: totalDeleted, errors: allErrors };
|
|
29895
|
+
}
|
|
29777
29896
|
async function removeNodeCascade(db2, collectionPath, reader, uid, options) {
|
|
29778
29897
|
const [outgoingRaw, incomingRaw] = await Promise.all([
|
|
29779
|
-
reader.findEdges({ aUid: uid }),
|
|
29780
|
-
reader.findEdges({ bUid: uid })
|
|
29898
|
+
reader.findEdges({ aUid: uid, allowCollectionScan: true, limit: 0 }),
|
|
29899
|
+
reader.findEdges({ bUid: uid, allowCollectionScan: true, limit: 0 })
|
|
29781
29900
|
]);
|
|
29782
29901
|
const outgoing = outgoingRaw.filter((e) => e.axbType !== NODE_RELATION);
|
|
29783
29902
|
const incoming = incomingRaw.filter((e) => e.axbType !== NODE_RELATION);
|
|
@@ -29790,8 +29909,18 @@ async function removeNodeCascade(db2, collectionPath, reader, uid, options) {
|
|
|
29790
29909
|
allEdges.push(edge);
|
|
29791
29910
|
}
|
|
29792
29911
|
}
|
|
29793
|
-
const
|
|
29912
|
+
const shouldDeleteSubcollections = options?.deleteSubcollections !== false;
|
|
29794
29913
|
const nodeDocId = computeNodeDocId(uid);
|
|
29914
|
+
let subcollectionResult = { deleted: 0, errors: [] };
|
|
29915
|
+
if (shouldDeleteSubcollections) {
|
|
29916
|
+
subcollectionResult = await deleteSubcollectionsRecursive(
|
|
29917
|
+
db2,
|
|
29918
|
+
collectionPath,
|
|
29919
|
+
nodeDocId,
|
|
29920
|
+
options
|
|
29921
|
+
);
|
|
29922
|
+
}
|
|
29923
|
+
const edgeDocIds = allEdges.map((e) => computeEdgeDocId(e.aUid, e.axbType, e.bUid));
|
|
29795
29924
|
const allDocIds = [...edgeDocIds, nodeDocId];
|
|
29796
29925
|
const batchSize = Math.min(options?.batchSize ?? MAX_BATCH_SIZE, MAX_BATCH_SIZE);
|
|
29797
29926
|
const result = await bulkDeleteDocIds(db2, collectionPath, allDocIds, {
|
|
@@ -29801,9 +29930,12 @@ async function removeNodeCascade(db2, collectionPath, reader, uid, options) {
|
|
|
29801
29930
|
const totalChunks = Math.ceil(allDocIds.length / batchSize);
|
|
29802
29931
|
const nodeChunkIndex = totalChunks - 1;
|
|
29803
29932
|
const nodeDeleted = !result.errors.some((e) => e.batchIndex === nodeChunkIndex);
|
|
29933
|
+
const topLevelEdgesDeleted = nodeDeleted ? result.deleted - 1 : result.deleted;
|
|
29804
29934
|
return {
|
|
29805
|
-
|
|
29806
|
-
|
|
29935
|
+
deleted: result.deleted + subcollectionResult.deleted,
|
|
29936
|
+
batches: result.batches,
|
|
29937
|
+
errors: [...result.errors, ...subcollectionResult.errors],
|
|
29938
|
+
edgesDeleted: topLevelEdgesDeleted,
|
|
29807
29939
|
nodeDeleted
|
|
29808
29940
|
};
|
|
29809
29941
|
}
|
|
@@ -29903,6 +30035,39 @@ function propertyToFieldMeta(name, prop, required2) {
|
|
|
29903
30035
|
return { name, type: "unknown", required: required2, description: prop.description };
|
|
29904
30036
|
}
|
|
29905
30037
|
|
|
30038
|
+
// src/scope.ts
|
|
30039
|
+
function matchScope(scopePath, pattern) {
|
|
30040
|
+
if (pattern === "root") return scopePath === "";
|
|
30041
|
+
if (pattern === "**") return true;
|
|
30042
|
+
const pathSegments = scopePath === "" ? [] : scopePath.split("/");
|
|
30043
|
+
const patternSegments = pattern.split("/");
|
|
30044
|
+
return matchSegments(pathSegments, 0, patternSegments, 0);
|
|
30045
|
+
}
|
|
30046
|
+
function matchScopeAny(scopePath, patterns) {
|
|
30047
|
+
if (!patterns || patterns.length === 0) return true;
|
|
30048
|
+
return patterns.some((p) => matchScope(scopePath, p));
|
|
30049
|
+
}
|
|
30050
|
+
function matchSegments(path4, pi, pattern, qi) {
|
|
30051
|
+
if (pi === path4.length && qi === pattern.length) return true;
|
|
30052
|
+
if (qi === pattern.length) return false;
|
|
30053
|
+
const seg = pattern[qi];
|
|
30054
|
+
if (seg === "**") {
|
|
30055
|
+
if (qi === pattern.length - 1) return true;
|
|
30056
|
+
for (let skip = 0; skip <= path4.length - pi; skip++) {
|
|
30057
|
+
if (matchSegments(path4, pi + skip, pattern, qi + 1)) return true;
|
|
30058
|
+
}
|
|
30059
|
+
return false;
|
|
30060
|
+
}
|
|
30061
|
+
if (pi === path4.length) return false;
|
|
30062
|
+
if (seg === "*") {
|
|
30063
|
+
return matchSegments(path4, pi + 1, pattern, qi + 1);
|
|
30064
|
+
}
|
|
30065
|
+
if (path4[pi] === seg) {
|
|
30066
|
+
return matchSegments(path4, pi + 1, pattern, qi + 1);
|
|
30067
|
+
}
|
|
30068
|
+
return false;
|
|
30069
|
+
}
|
|
30070
|
+
|
|
29906
30071
|
// src/registry.ts
|
|
29907
30072
|
function tripleKey(aType, axbType, bType) {
|
|
29908
30073
|
return `${aType}:${axbType}:${bType}`;
|
|
@@ -29917,19 +30082,45 @@ function createRegistry(input) {
|
|
|
29917
30082
|
}
|
|
29918
30083
|
const entryList = Object.freeze([...entries]);
|
|
29919
30084
|
for (const entry of entries) {
|
|
30085
|
+
if (entry.targetGraph && entry.targetGraph.includes("/")) {
|
|
30086
|
+
throw new ValidationError(
|
|
30087
|
+
`Entry (${entry.aType}) -[${entry.axbType}]-> (${entry.bType}) has invalid targetGraph "${entry.targetGraph}" \u2014 must be a single segment (no "/")`
|
|
30088
|
+
);
|
|
30089
|
+
}
|
|
29920
30090
|
const key = tripleKey(entry.aType, entry.axbType, entry.bType);
|
|
29921
30091
|
const validator = entry.jsonSchema ? compileSchema(entry.jsonSchema, `(${entry.aType}) -[${entry.axbType}]-> (${entry.bType})`) : void 0;
|
|
29922
30092
|
map2.set(key, { entry, validate: validator });
|
|
29923
30093
|
}
|
|
30094
|
+
const axbIndex = /* @__PURE__ */ new Map();
|
|
30095
|
+
const axbBuild = /* @__PURE__ */ new Map();
|
|
30096
|
+
for (const entry of entries) {
|
|
30097
|
+
const existing = axbBuild.get(entry.axbType);
|
|
30098
|
+
if (existing) {
|
|
30099
|
+
existing.push(entry);
|
|
30100
|
+
} else {
|
|
30101
|
+
axbBuild.set(entry.axbType, [entry]);
|
|
30102
|
+
}
|
|
30103
|
+
}
|
|
30104
|
+
for (const [key, arr] of axbBuild) {
|
|
30105
|
+
axbIndex.set(key, Object.freeze(arr));
|
|
30106
|
+
}
|
|
29924
30107
|
return {
|
|
29925
30108
|
lookup(aType, axbType, bType) {
|
|
29926
30109
|
return map2.get(tripleKey(aType, axbType, bType))?.entry;
|
|
29927
30110
|
},
|
|
29928
|
-
|
|
30111
|
+
lookupByAxbType(axbType) {
|
|
30112
|
+
return axbIndex.get(axbType) ?? [];
|
|
30113
|
+
},
|
|
30114
|
+
validate(aType, axbType, bType, data, scopePath) {
|
|
29929
30115
|
const rec = map2.get(tripleKey(aType, axbType, bType));
|
|
29930
30116
|
if (!rec) {
|
|
29931
30117
|
throw new RegistryViolationError(aType, axbType, bType);
|
|
29932
30118
|
}
|
|
30119
|
+
if (scopePath !== void 0 && rec.entry.allowedIn && rec.entry.allowedIn.length > 0) {
|
|
30120
|
+
if (!matchScopeAny(scopePath, rec.entry.allowedIn)) {
|
|
30121
|
+
throw new RegistryScopeError(aType, axbType, bType, scopePath, rec.entry.allowedIn);
|
|
30122
|
+
}
|
|
30123
|
+
}
|
|
29933
30124
|
if (rec.validate) {
|
|
29934
30125
|
try {
|
|
29935
30126
|
rec.validate(data);
|
|
@@ -29957,7 +30148,8 @@ function discoveryToEntries(discovery) {
|
|
|
29957
30148
|
jsonSchema: entity.schema,
|
|
29958
30149
|
description: entity.description,
|
|
29959
30150
|
titleField: entity.titleField,
|
|
29960
|
-
subtitleField: entity.subtitleField
|
|
30151
|
+
subtitleField: entity.subtitleField,
|
|
30152
|
+
allowedIn: entity.allowedIn
|
|
29961
30153
|
});
|
|
29962
30154
|
}
|
|
29963
30155
|
for (const [axbType, entity] of discovery.edges) {
|
|
@@ -29965,6 +30157,12 @@ function discoveryToEntries(discovery) {
|
|
|
29965
30157
|
if (!topology) continue;
|
|
29966
30158
|
const fromTypes = Array.isArray(topology.from) ? topology.from : [topology.from];
|
|
29967
30159
|
const toTypes = Array.isArray(topology.to) ? topology.to : [topology.to];
|
|
30160
|
+
const resolvedTargetGraph = entity.targetGraph ?? topology.targetGraph;
|
|
30161
|
+
if (resolvedTargetGraph && resolvedTargetGraph.includes("/")) {
|
|
30162
|
+
throw new ValidationError(
|
|
30163
|
+
`Edge "${axbType}" has invalid targetGraph "${resolvedTargetGraph}" \u2014 must be a single segment (no "/")`
|
|
30164
|
+
);
|
|
30165
|
+
}
|
|
29968
30166
|
for (const aType of fromTypes) {
|
|
29969
30167
|
for (const bType of toTypes) {
|
|
29970
30168
|
entries.push({
|
|
@@ -29975,7 +30173,9 @@ function discoveryToEntries(discovery) {
|
|
|
29975
30173
|
description: entity.description,
|
|
29976
30174
|
inverseLabel: topology.inverseLabel,
|
|
29977
30175
|
titleField: entity.titleField,
|
|
29978
|
-
subtitleField: entity.subtitleField
|
|
30176
|
+
subtitleField: entity.subtitleField,
|
|
30177
|
+
allowedIn: entity.allowedIn,
|
|
30178
|
+
targetGraph: resolvedTargetGraph
|
|
29979
30179
|
});
|
|
29980
30180
|
}
|
|
29981
30181
|
}
|
|
@@ -29996,7 +30196,8 @@ var NODE_TYPE_SCHEMA = {
|
|
|
29996
30196
|
titleField: { type: "string" },
|
|
29997
30197
|
subtitleField: { type: "string" },
|
|
29998
30198
|
viewTemplate: { type: "string" },
|
|
29999
|
-
viewCss: { type: "string" }
|
|
30199
|
+
viewCss: { type: "string" },
|
|
30200
|
+
allowedIn: { type: "array", items: { type: "string", minLength: 1 } }
|
|
30000
30201
|
},
|
|
30001
30202
|
additionalProperties: false
|
|
30002
30203
|
};
|
|
@@ -30023,7 +30224,9 @@ var EDGE_TYPE_SCHEMA = {
|
|
|
30023
30224
|
titleField: { type: "string" },
|
|
30024
30225
|
subtitleField: { type: "string" },
|
|
30025
30226
|
viewTemplate: { type: "string" },
|
|
30026
|
-
viewCss: { type: "string" }
|
|
30227
|
+
viewCss: { type: "string" },
|
|
30228
|
+
allowedIn: { type: "array", items: { type: "string", minLength: 1 } },
|
|
30229
|
+
targetGraph: { type: "string", minLength: 1, pattern: "^[^/]+$" }
|
|
30027
30230
|
},
|
|
30028
30231
|
additionalProperties: false
|
|
30029
30232
|
};
|
|
@@ -30065,7 +30268,8 @@ async function createRegistryFromGraph(reader) {
|
|
|
30065
30268
|
jsonSchema: data.jsonSchema,
|
|
30066
30269
|
description: data.description,
|
|
30067
30270
|
titleField: data.titleField,
|
|
30068
|
-
subtitleField: data.subtitleField
|
|
30271
|
+
subtitleField: data.subtitleField,
|
|
30272
|
+
allowedIn: data.allowedIn
|
|
30069
30273
|
});
|
|
30070
30274
|
}
|
|
30071
30275
|
for (const record2 of edgeTypes) {
|
|
@@ -30082,7 +30286,9 @@ async function createRegistryFromGraph(reader) {
|
|
|
30082
30286
|
description: data.description,
|
|
30083
30287
|
inverseLabel: data.inverseLabel,
|
|
30084
30288
|
titleField: data.titleField,
|
|
30085
|
-
subtitleField: data.subtitleField
|
|
30289
|
+
subtitleField: data.subtitleField,
|
|
30290
|
+
allowedIn: data.allowedIn,
|
|
30291
|
+
targetGraph: data.targetGraph
|
|
30086
30292
|
});
|
|
30087
30293
|
}
|
|
30088
30294
|
}
|
|
@@ -30093,9 +30299,10 @@ async function createRegistryFromGraph(reader) {
|
|
|
30093
30299
|
// src/client.ts
|
|
30094
30300
|
var _standardModeWarned = false;
|
|
30095
30301
|
var RESERVED_TYPE_NAMES = /* @__PURE__ */ new Set([META_NODE_TYPE, META_EDGE_TYPE]);
|
|
30096
|
-
var GraphClientImpl = class {
|
|
30097
|
-
constructor(db2, collectionPath, options) {
|
|
30302
|
+
var GraphClientImpl = class _GraphClientImpl {
|
|
30303
|
+
constructor(db2, collectionPath, options, scopePath = "") {
|
|
30098
30304
|
this.db = db2;
|
|
30305
|
+
this.scopePath = scopePath;
|
|
30099
30306
|
this.adapter = createFirestoreAdapter(db2, collectionPath);
|
|
30100
30307
|
if (options?.registry && options?.registryMode) {
|
|
30101
30308
|
throw new DynamicRegistryError(
|
|
@@ -30125,6 +30332,7 @@ var GraphClientImpl = class {
|
|
|
30125
30332
|
"[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"
|
|
30126
30333
|
);
|
|
30127
30334
|
}
|
|
30335
|
+
this.scanProtection = options?.scanProtection ?? "error";
|
|
30128
30336
|
if (this.queryMode === "pipeline") {
|
|
30129
30337
|
this.pipelineAdapter = createPipelineQueryAdapter(db2, collectionPath);
|
|
30130
30338
|
if (this.metaAdapter) {
|
|
@@ -30138,6 +30346,7 @@ var GraphClientImpl = class {
|
|
|
30138
30346
|
adapter;
|
|
30139
30347
|
pipelineAdapter;
|
|
30140
30348
|
queryMode;
|
|
30349
|
+
scanProtection;
|
|
30141
30350
|
// Static mode
|
|
30142
30351
|
staticRegistry;
|
|
30143
30352
|
// Dynamic mode
|
|
@@ -30146,6 +30355,8 @@ var GraphClientImpl = class {
|
|
|
30146
30355
|
dynamicRegistry;
|
|
30147
30356
|
metaAdapter;
|
|
30148
30357
|
metaPipelineAdapter;
|
|
30358
|
+
// Subgraph scope tracking
|
|
30359
|
+
scopePath;
|
|
30149
30360
|
// ---------------------------------------------------------------------------
|
|
30150
30361
|
// Registry routing
|
|
30151
30362
|
// ---------------------------------------------------------------------------
|
|
@@ -30199,6 +30410,19 @@ var GraphClientImpl = class {
|
|
|
30199
30410
|
}
|
|
30200
30411
|
return this.adapter.query(filters, options);
|
|
30201
30412
|
}
|
|
30413
|
+
/**
|
|
30414
|
+
* Check whether a query's filter set is safe (matches a known index pattern).
|
|
30415
|
+
* Throws QuerySafetyError or logs a warning depending on scanProtection config.
|
|
30416
|
+
*/
|
|
30417
|
+
checkQuerySafety(filters, allowCollectionScan) {
|
|
30418
|
+
if (allowCollectionScan || this.scanProtection === "off") return;
|
|
30419
|
+
const result = analyzeQuerySafety(filters);
|
|
30420
|
+
if (result.safe) return;
|
|
30421
|
+
if (this.scanProtection === "error") {
|
|
30422
|
+
throw new QuerySafetyError(result.reason);
|
|
30423
|
+
}
|
|
30424
|
+
console.warn(`[firegraph] Query safety warning: ${result.reason}`);
|
|
30425
|
+
}
|
|
30202
30426
|
// ---------------------------------------------------------------------------
|
|
30203
30427
|
// GraphReader
|
|
30204
30428
|
// ---------------------------------------------------------------------------
|
|
@@ -30220,6 +30444,7 @@ var GraphClientImpl = class {
|
|
|
30220
30444
|
const record2 = await this.adapter.getDoc(plan.docId);
|
|
30221
30445
|
return record2 ? [record2] : [];
|
|
30222
30446
|
}
|
|
30447
|
+
this.checkQuerySafety(plan.filters, params.allowCollectionScan);
|
|
30223
30448
|
return this.executeQuery(plan.filters, plan.options);
|
|
30224
30449
|
}
|
|
30225
30450
|
async findNodes(params) {
|
|
@@ -30228,6 +30453,7 @@ var GraphClientImpl = class {
|
|
|
30228
30453
|
const record2 = await this.adapter.getDoc(plan.docId);
|
|
30229
30454
|
return record2 ? [record2] : [];
|
|
30230
30455
|
}
|
|
30456
|
+
this.checkQuerySafety(plan.filters, params.allowCollectionScan);
|
|
30231
30457
|
return this.executeQuery(plan.filters, plan.options);
|
|
30232
30458
|
}
|
|
30233
30459
|
// ---------------------------------------------------------------------------
|
|
@@ -30236,7 +30462,7 @@ var GraphClientImpl = class {
|
|
|
30236
30462
|
async putNode(aType, uid, data) {
|
|
30237
30463
|
const registry2 = this.getRegistryForType(aType);
|
|
30238
30464
|
if (registry2) {
|
|
30239
|
-
registry2.validate(aType, NODE_RELATION, aType, data);
|
|
30465
|
+
registry2.validate(aType, NODE_RELATION, aType, data, this.scopePath);
|
|
30240
30466
|
}
|
|
30241
30467
|
const adapter = this.getAdapterForType(aType);
|
|
30242
30468
|
const docId = computeNodeDocId(uid);
|
|
@@ -30246,7 +30472,7 @@ var GraphClientImpl = class {
|
|
|
30246
30472
|
async putEdge(aType, aUid, axbType, bType, bUid, data) {
|
|
30247
30473
|
const registry2 = this.getRegistryForType(aType);
|
|
30248
30474
|
if (registry2) {
|
|
30249
|
-
registry2.validate(aType, axbType, bType, data);
|
|
30475
|
+
registry2.validate(aType, axbType, bType, data, this.scopePath);
|
|
30250
30476
|
}
|
|
30251
30477
|
const adapter = this.getAdapterForType(aType);
|
|
30252
30478
|
const docId = computeEdgeDocId(aUid, axbType, bUid);
|
|
@@ -30278,13 +30504,69 @@ var GraphClientImpl = class {
|
|
|
30278
30504
|
this.adapter.collectionPath,
|
|
30279
30505
|
firestoreTx
|
|
30280
30506
|
);
|
|
30281
|
-
const graphTx = new GraphTransactionImpl(adapter, this.getCombinedRegistry());
|
|
30507
|
+
const graphTx = new GraphTransactionImpl(adapter, this.getCombinedRegistry(), this.scanProtection, this.scopePath);
|
|
30282
30508
|
return fn(graphTx);
|
|
30283
30509
|
});
|
|
30284
30510
|
}
|
|
30285
30511
|
batch() {
|
|
30286
30512
|
const adapter = createBatchAdapter(this.db, this.adapter.collectionPath);
|
|
30287
|
-
return new GraphBatchImpl(adapter, this.getCombinedRegistry());
|
|
30513
|
+
return new GraphBatchImpl(adapter, this.getCombinedRegistry(), this.scopePath);
|
|
30514
|
+
}
|
|
30515
|
+
// ---------------------------------------------------------------------------
|
|
30516
|
+
// Subgraph
|
|
30517
|
+
// ---------------------------------------------------------------------------
|
|
30518
|
+
subgraph(parentNodeUid, name = "graph") {
|
|
30519
|
+
if (!parentNodeUid || parentNodeUid.includes("/")) {
|
|
30520
|
+
throw new FiregraphError(
|
|
30521
|
+
`Invalid parentNodeUid for subgraph: "${parentNodeUid}". Must be a non-empty string without "/".`,
|
|
30522
|
+
"INVALID_SUBGRAPH"
|
|
30523
|
+
);
|
|
30524
|
+
}
|
|
30525
|
+
if (name.includes("/")) {
|
|
30526
|
+
throw new FiregraphError(
|
|
30527
|
+
`Subgraph name must not contain "/": got "${name}". Use chained .subgraph() calls for nested subgraphs.`,
|
|
30528
|
+
"INVALID_SUBGRAPH"
|
|
30529
|
+
);
|
|
30530
|
+
}
|
|
30531
|
+
const subCollectionPath = `${this.adapter.collectionPath}/${parentNodeUid}/${name}`;
|
|
30532
|
+
const newScopePath = this.scopePath ? `${this.scopePath}/${name}` : name;
|
|
30533
|
+
return new _GraphClientImpl(
|
|
30534
|
+
this.db,
|
|
30535
|
+
subCollectionPath,
|
|
30536
|
+
{
|
|
30537
|
+
registry: this.getCombinedRegistry(),
|
|
30538
|
+
queryMode: this.queryMode === "pipeline" ? "pipeline" : "standard",
|
|
30539
|
+
scanProtection: this.scanProtection
|
|
30540
|
+
},
|
|
30541
|
+
newScopePath
|
|
30542
|
+
);
|
|
30543
|
+
}
|
|
30544
|
+
// ---------------------------------------------------------------------------
|
|
30545
|
+
// Collection group query
|
|
30546
|
+
// ---------------------------------------------------------------------------
|
|
30547
|
+
async findEdgesGlobal(params, collectionName) {
|
|
30548
|
+
const name = collectionName ?? this.adapter.collectionPath.split("/").pop();
|
|
30549
|
+
const plan = buildEdgeQueryPlan(params);
|
|
30550
|
+
if (plan.strategy === "get") {
|
|
30551
|
+
throw new FiregraphError(
|
|
30552
|
+
"findEdgesGlobal() requires a query, not a direct document lookup. Omit one of aUid/axbType/bUid to force a query strategy.",
|
|
30553
|
+
"INVALID_QUERY"
|
|
30554
|
+
);
|
|
30555
|
+
}
|
|
30556
|
+
this.checkQuerySafety(plan.filters, params.allowCollectionScan);
|
|
30557
|
+
const collectionGroupRef = this.db.collectionGroup(name);
|
|
30558
|
+
let q = collectionGroupRef;
|
|
30559
|
+
for (const f of plan.filters) {
|
|
30560
|
+
q = q.where(f.field, f.op, f.value);
|
|
30561
|
+
}
|
|
30562
|
+
if (plan.options?.orderBy) {
|
|
30563
|
+
q = q.orderBy(plan.options.orderBy.field, plan.options.orderBy.direction ?? "asc");
|
|
30564
|
+
}
|
|
30565
|
+
if (plan.options?.limit !== void 0) {
|
|
30566
|
+
q = q.limit(plan.options.limit);
|
|
30567
|
+
}
|
|
30568
|
+
const snap = await q.get();
|
|
30569
|
+
return snap.docs.map((doc) => doc.data());
|
|
30288
30570
|
}
|
|
30289
30571
|
// ---------------------------------------------------------------------------
|
|
30290
30572
|
// Bulk operations
|
|
@@ -30316,6 +30598,7 @@ var GraphClientImpl = class {
|
|
|
30316
30598
|
if (options?.subtitleField !== void 0) data.subtitleField = options.subtitleField;
|
|
30317
30599
|
if (options?.viewTemplate !== void 0) data.viewTemplate = options.viewTemplate;
|
|
30318
30600
|
if (options?.viewCss !== void 0) data.viewCss = options.viewCss;
|
|
30601
|
+
if (options?.allowedIn !== void 0) data.allowedIn = options.allowedIn;
|
|
30319
30602
|
await this.putNode(META_NODE_TYPE, uid, data);
|
|
30320
30603
|
}
|
|
30321
30604
|
async defineEdgeType(name, topology, jsonSchema, description, options) {
|
|
@@ -30337,11 +30620,13 @@ var GraphClientImpl = class {
|
|
|
30337
30620
|
};
|
|
30338
30621
|
if (jsonSchema !== void 0) data.jsonSchema = jsonSchema;
|
|
30339
30622
|
if (topology.inverseLabel !== void 0) data.inverseLabel = topology.inverseLabel;
|
|
30623
|
+
if (topology.targetGraph !== void 0) data.targetGraph = topology.targetGraph;
|
|
30340
30624
|
if (description !== void 0) data.description = description;
|
|
30341
30625
|
if (options?.titleField !== void 0) data.titleField = options.titleField;
|
|
30342
30626
|
if (options?.subtitleField !== void 0) data.subtitleField = options.subtitleField;
|
|
30343
30627
|
if (options?.viewTemplate !== void 0) data.viewTemplate = options.viewTemplate;
|
|
30344
30628
|
if (options?.viewCss !== void 0) data.viewCss = options.viewCss;
|
|
30629
|
+
if (options?.allowedIn !== void 0) data.allowedIn = options.allowedIn;
|
|
30345
30630
|
await this.putNode(META_EDGE_TYPE, uid, data);
|
|
30346
30631
|
}
|
|
30347
30632
|
async reloadRegistry() {
|
|
@@ -30524,7 +30809,8 @@ function loadNodeEntity(dir, name) {
|
|
|
30524
30809
|
subtitleField: meta3?.subtitleField,
|
|
30525
30810
|
viewDefaults: meta3?.viewDefaults,
|
|
30526
30811
|
viewsPath,
|
|
30527
|
-
sampleData
|
|
30812
|
+
sampleData,
|
|
30813
|
+
allowedIn: meta3?.allowedIn
|
|
30528
30814
|
};
|
|
30529
30815
|
}
|
|
30530
30816
|
function loadEdgeEntity(dir, name) {
|
|
@@ -30559,7 +30845,9 @@ function loadEdgeEntity(dir, name) {
|
|
|
30559
30845
|
subtitleField: meta3?.subtitleField,
|
|
30560
30846
|
viewDefaults: meta3?.viewDefaults,
|
|
30561
30847
|
viewsPath,
|
|
30562
|
-
sampleData
|
|
30848
|
+
sampleData,
|
|
30849
|
+
allowedIn: meta3?.allowedIn,
|
|
30850
|
+
targetGraph: topology.targetGraph ?? meta3?.targetGraph
|
|
30563
30851
|
};
|
|
30564
30852
|
}
|
|
30565
30853
|
function getSubdirectories(dir) {
|