@dereekb/dbx-cli 13.40.0 → 13.42.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/eslint/package.json +3 -3
- package/firebase-api-manifest/main.js +156 -18
- package/firebase-api-manifest/package.json +3 -3
- package/firestore-indexes/src/generate-firestore-indexes-cli.d.ts +12 -4
- package/firestore-indexes/src/model-firebase-index-analyze.d.ts +12 -2
- package/firestore-query-manifest/main.js +306 -12
- package/firestore-query-manifest/package.json +3 -3
- package/generate-firestore-indexes/main.js +118 -20
- package/generate-firestore-indexes/package.json +2 -2
- package/generate-mcp-manifest/main.js +5 -2
- package/generate-mcp-manifest/package.json +3 -3
- package/generate-route-manifest/main.js +1 -1
- package/generate-route-manifest/package.json +2 -2
- package/index.esm.js +1389 -933
- package/lint-cache/package.json +2 -2
- package/manifest-extract/index.esm.js +148 -2
- package/manifest-extract/package.json +2 -2
- package/manifest-extract/src/lib/extract-models.d.ts +4 -2
- package/manifest-extract/src/lib/types.d.ts +65 -0
- package/model-test/index.esm.js +4 -54
- package/model-test/package.json +2 -2
- package/package.json +6 -6
- package/route/package.json +7 -7
- package/src/lib/doctor/firestore-session.check.d.ts +23 -2
- package/src/lib/firestore/firestore.collection.d.ts +29 -0
- package/src/lib/firestore/firestore.query.d.ts +1 -1
- package/src/lib/firestore/index.d.ts +1 -0
- package/src/lib/firestore/query-info-utils.d.ts +6 -1
- package/src/lib/firestore/query-mode.d.ts +120 -0
- package/src/lib/manifest/types.d.ts +161 -2
- package/src/lib/mcp-scan/manifest/css-utilities-schema.d.ts +2 -2
- package/src/lib/mcp-scan/manifest/tokens-schema.d.ts +2 -2
- package/test/package.json +9 -9
- package/validate/index.js +12 -7
- package/validate/package.json +3 -3
|
@@ -9,15 +9,15 @@ import { isAbsolute as isAbsolute2, relative as relative2, resolve as resolve3 }
|
|
|
9
9
|
// packages/dbx-cli/firestore-query-manifest/package.json
|
|
10
10
|
var package_default = {
|
|
11
11
|
name: "@dereekb/dbx-cli-firestore-query-manifest",
|
|
12
|
-
version: "13.
|
|
12
|
+
version: "13.42.0",
|
|
13
13
|
private: true,
|
|
14
14
|
type: "module",
|
|
15
15
|
devDependencies: {
|
|
16
16
|
eslint: "10.4.0"
|
|
17
17
|
},
|
|
18
18
|
peerDependencies: {
|
|
19
|
-
"@dereekb/dbx-cli": "13.
|
|
20
|
-
"@dereekb/util": "13.
|
|
19
|
+
"@dereekb/dbx-cli": "13.42.0",
|
|
20
|
+
"@dereekb/util": "13.42.0"
|
|
21
21
|
}
|
|
22
22
|
};
|
|
23
23
|
|
|
@@ -37,8 +37,8 @@ function renderGroupedImportLines(imports) {
|
|
|
37
37
|
set.add(identifier);
|
|
38
38
|
importsByPackage.set(packageName, set);
|
|
39
39
|
}
|
|
40
|
-
return
|
|
41
|
-
const sortedNames =
|
|
40
|
+
return Array.from(importsByPackage.entries()).sort(([a], [b]) => a.localeCompare(b)).map(([pkg, names]) => {
|
|
41
|
+
const sortedNames = Array.from(names).sort(compareStrings).join(", ");
|
|
42
42
|
return `import { ${sortedNames} } from '${pkg}';`;
|
|
43
43
|
});
|
|
44
44
|
}
|
|
@@ -62,6 +62,241 @@ function writeGeneratedTsFile(input) {
|
|
|
62
62
|
return result;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
// packages/dbx-cli/firestore-rules/src/firestore-rules-scan.ts
|
|
66
|
+
var RULES_ROOT_PREFIX = /^\/?databases\/\{[^}]*\}\/documents/;
|
|
67
|
+
function scanFirestoreRules(source) {
|
|
68
|
+
const text = stripComments(source);
|
|
69
|
+
const accumulated = /* @__PURE__ */ new Map();
|
|
70
|
+
walkMatchBlocks(text, (block) => {
|
|
71
|
+
const collection = collectionForPathSegments(block.segments);
|
|
72
|
+
if (collection == null) return;
|
|
73
|
+
const current = accumulated.get(collection) ?? (() => {
|
|
74
|
+
const created = { paths: /* @__PURE__ */ new Set(), get: "unmatched", list: "unmatched", collectionGroup: false };
|
|
75
|
+
accumulated.set(collection, created);
|
|
76
|
+
return created;
|
|
77
|
+
})();
|
|
78
|
+
current.paths.add("/" + block.segments.join("/"));
|
|
79
|
+
current.collectionGroup = current.collectionGroup || block.segments.some(isRecursiveWildcard);
|
|
80
|
+
for (const allow of block.allows) {
|
|
81
|
+
if (allow.ops.has("get") || allow.ops.has("read")) current.get = mergeAccess(current.get, allow.access);
|
|
82
|
+
if (allow.ops.has("list") || allow.ops.has("read")) current.list = mergeAccess(current.list, allow.access);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
const collections = Array.from(accumulated.entries()).map(([collection, value]) => ({
|
|
86
|
+
collection,
|
|
87
|
+
paths: Array.from(value.paths).sort(),
|
|
88
|
+
get: value.get,
|
|
89
|
+
list: value.list,
|
|
90
|
+
collectionGroup: value.collectionGroup,
|
|
91
|
+
serverOnly: value.get !== "allowed" && value.list !== "allowed"
|
|
92
|
+
})).sort((a, b) => a.collection.localeCompare(b.collection));
|
|
93
|
+
return { collections };
|
|
94
|
+
}
|
|
95
|
+
// @__NO_SIDE_EFFECTS__
|
|
96
|
+
function firestoreRulesAccessForCollection(scan, collection) {
|
|
97
|
+
return scan.collections.find((x) => x.collection === collection) ?? { collection, paths: [], get: "unmatched", list: "unmatched", collectionGroup: false, serverOnly: true };
|
|
98
|
+
}
|
|
99
|
+
function mergeAccess(current, incoming) {
|
|
100
|
+
let result;
|
|
101
|
+
if (current === "allowed" || incoming === "allowed") {
|
|
102
|
+
result = "allowed";
|
|
103
|
+
} else if (current === "denied" || incoming === "denied") {
|
|
104
|
+
result = "denied";
|
|
105
|
+
} else {
|
|
106
|
+
result = "unmatched";
|
|
107
|
+
}
|
|
108
|
+
return result;
|
|
109
|
+
}
|
|
110
|
+
function collectionForPathSegments(segments) {
|
|
111
|
+
const candidate = segments.length >= 2 ? segments[segments.length - 2] : void 0;
|
|
112
|
+
return candidate != null && !isWildcard(candidate) ? candidate : void 0;
|
|
113
|
+
}
|
|
114
|
+
function isWildcard(segment) {
|
|
115
|
+
return segment.startsWith("{") && segment.endsWith("}");
|
|
116
|
+
}
|
|
117
|
+
function isRecursiveWildcard(segment) {
|
|
118
|
+
return isWildcard(segment) && segment.includes("=**");
|
|
119
|
+
}
|
|
120
|
+
function stripComments(source) {
|
|
121
|
+
let result = "";
|
|
122
|
+
let index = 0;
|
|
123
|
+
let quote;
|
|
124
|
+
while (index < source.length) {
|
|
125
|
+
const char = source[index];
|
|
126
|
+
const next = source[index + 1];
|
|
127
|
+
if (quote != null) {
|
|
128
|
+
result += char;
|
|
129
|
+
if (char === "\\" && index + 1 < source.length) {
|
|
130
|
+
result += next;
|
|
131
|
+
index += 2;
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (char === quote) quote = void 0;
|
|
135
|
+
index += 1;
|
|
136
|
+
} else if (char === "'" || char === '"') {
|
|
137
|
+
quote = char;
|
|
138
|
+
result += char;
|
|
139
|
+
index += 1;
|
|
140
|
+
} else if (char === "/" && next === "/") {
|
|
141
|
+
while (index < source.length && source[index] !== "\n") index += 1;
|
|
142
|
+
} else if (char === "/" && next === "*") {
|
|
143
|
+
index += 2;
|
|
144
|
+
while (index < source.length && !(source[index] === "*" && source[index + 1] === "/")) index += 1;
|
|
145
|
+
index += 2;
|
|
146
|
+
} else {
|
|
147
|
+
result += char;
|
|
148
|
+
index += 1;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return result;
|
|
152
|
+
}
|
|
153
|
+
function walkMatchBlocks(text, visit) {
|
|
154
|
+
const pathStack = [];
|
|
155
|
+
const braceFrames = [];
|
|
156
|
+
let index = 0;
|
|
157
|
+
while (index < text.length) {
|
|
158
|
+
const matchStart = findKeyword(text, index, "match");
|
|
159
|
+
const braceIndex = text.indexOf("{", index);
|
|
160
|
+
const closeIndex = text.indexOf("}", index);
|
|
161
|
+
const allowStart = findKeyword(text, index, "allow");
|
|
162
|
+
const next = Math.min(...[matchStart, braceIndex, closeIndex, allowStart].filter((x) => x >= 0).concat([text.length]));
|
|
163
|
+
if (next >= text.length) break;
|
|
164
|
+
if (next === matchStart) {
|
|
165
|
+
const parsed = parseMatchHeader(text, matchStart);
|
|
166
|
+
pathStack.push(parsed.segments);
|
|
167
|
+
braceFrames.push({ isMatch: true, allows: [] });
|
|
168
|
+
index = parsed.bodyStart;
|
|
169
|
+
} else if (next === allowStart) {
|
|
170
|
+
const parsed = parseAllow(text, allowStart);
|
|
171
|
+
const frame = braceFrames[braceFrames.length - 1];
|
|
172
|
+
if (frame?.isMatch && parsed) frame.allows.push(parsed.allow);
|
|
173
|
+
index = parsed ? parsed.end : allowStart + "allow".length;
|
|
174
|
+
} else if (next === braceIndex) {
|
|
175
|
+
braceFrames.push({ isMatch: false, allows: [] });
|
|
176
|
+
index = braceIndex + 1;
|
|
177
|
+
} else {
|
|
178
|
+
const frame = braceFrames.pop();
|
|
179
|
+
if (frame?.isMatch) {
|
|
180
|
+
const segments = stripRootPrefix(pathStack.flat());
|
|
181
|
+
visit({ segments, allows: frame.allows });
|
|
182
|
+
pathStack.pop();
|
|
183
|
+
}
|
|
184
|
+
index = closeIndex + 1;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
function stripRootPrefix(segments) {
|
|
189
|
+
const joined = segments.join("/");
|
|
190
|
+
const stripped = joined.replace(RULES_ROOT_PREFIX, "");
|
|
191
|
+
return stripped.split("/").filter((x) => x.length > 0);
|
|
192
|
+
}
|
|
193
|
+
function parseMatchHeader(text, start) {
|
|
194
|
+
const braceIndex = text.indexOf("{", start + "match".length);
|
|
195
|
+
let cursor = start + "match".length;
|
|
196
|
+
let path = "";
|
|
197
|
+
while (cursor < text.length) {
|
|
198
|
+
const char = text[cursor];
|
|
199
|
+
if (char === "{") {
|
|
200
|
+
const close = text.indexOf("}", cursor);
|
|
201
|
+
const inner = text.slice(cursor, close + 1);
|
|
202
|
+
if (close > cursor && /^\{[A-Za-z_][\w]*(=\*\*)?\}$/.test(inner)) {
|
|
203
|
+
path += inner;
|
|
204
|
+
cursor = close + 1;
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
if (char === "\n" && path.trim().length > 0) break;
|
|
210
|
+
path += char;
|
|
211
|
+
cursor += 1;
|
|
212
|
+
}
|
|
213
|
+
const bodyBrace = text.includes("{", cursor - 1) ? text.indexOf("{", cursor) : braceIndex;
|
|
214
|
+
const segments = path.trim().split("/").map((x) => x.trim()).filter((x) => x.length > 0);
|
|
215
|
+
return { segments, bodyStart: (bodyBrace >= 0 ? bodyBrace : cursor) + 1 };
|
|
216
|
+
}
|
|
217
|
+
function parseAllow(text, start) {
|
|
218
|
+
const colonIndex = text.indexOf(":", start);
|
|
219
|
+
const semiIndex = text.indexOf(";", start);
|
|
220
|
+
let result;
|
|
221
|
+
if (colonIndex >= 0 && semiIndex > colonIndex) {
|
|
222
|
+
const ops = new Set(
|
|
223
|
+
text.slice(start + "allow".length, colonIndex).split(",").map((x) => x.trim()).filter((x) => x.length > 0)
|
|
224
|
+
);
|
|
225
|
+
const condition = text.slice(colonIndex + 1, semiIndex).replace(/^\s*if\s*/, "").trim();
|
|
226
|
+
result = { allow: { ops, access: condition === "false" ? "denied" : "allowed" }, end: semiIndex + 1 };
|
|
227
|
+
}
|
|
228
|
+
return result;
|
|
229
|
+
}
|
|
230
|
+
function findKeyword(text, from, keyword) {
|
|
231
|
+
let index = text.indexOf(keyword, from);
|
|
232
|
+
while (index >= 0) {
|
|
233
|
+
const before = index === 0 ? " " : text[index - 1];
|
|
234
|
+
const after = text[index + keyword.length] ?? " ";
|
|
235
|
+
if (!/[\w$]/.test(before) && !/[\w$]/.test(after)) break;
|
|
236
|
+
index = text.indexOf(keyword, index + keyword.length);
|
|
237
|
+
}
|
|
238
|
+
return index;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// packages/dbx-cli/src/lib/firestore/query-mode.ts
|
|
242
|
+
// @__NO_SIDE_EFFECTS__
|
|
243
|
+
function cliFirestoreQueryModeForRules(input) {
|
|
244
|
+
const { scope, isNested, collectionGroup, list, parentPaths } = input;
|
|
245
|
+
const base = { list, collectionGroup, ...parentPaths && parentPaths.length > 0 ? { parentPaths } : {} };
|
|
246
|
+
let result;
|
|
247
|
+
if (list !== "allowed") {
|
|
248
|
+
const reason = list === "denied" ? "list-denied" : "list-unmatched";
|
|
249
|
+
result = { mode: "unavailable", rules: { ...base, reason } };
|
|
250
|
+
} else if (scope === "COLLECTION_GROUP" && !collectionGroup) {
|
|
251
|
+
result = { mode: isNested ? "parent-child" : "unavailable", rules: { ...base, reason: "no-collection-group-rule" } };
|
|
252
|
+
} else if (scope === "COLLECTION" && isNested) {
|
|
253
|
+
result = { mode: "parent-child", rules: { ...base, reason: "nested-collection-scope" } };
|
|
254
|
+
} else {
|
|
255
|
+
result = { mode: "model", rules: base };
|
|
256
|
+
}
|
|
257
|
+
return result;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// packages/dbx-cli/firestore-query-manifest/src/annotate-query-mode.ts
|
|
261
|
+
function annotateQueryEntryMode(input) {
|
|
262
|
+
const { entries, rulesSource } = input;
|
|
263
|
+
const scan = scanFirestoreRules(rulesSource);
|
|
264
|
+
let model = 0;
|
|
265
|
+
const unavailableSlugs = [];
|
|
266
|
+
const parentChildSlugs = [];
|
|
267
|
+
const annotated = entries.map((entry) => {
|
|
268
|
+
const rules = firestoreRulesAccessForCollection(scan, entry.collection);
|
|
269
|
+
const { mode, rules: entryRules } = cliFirestoreQueryModeForRules({
|
|
270
|
+
scope: entry.scope,
|
|
271
|
+
isNested: entry.isNested,
|
|
272
|
+
collectionGroup: rules.collectionGroup,
|
|
273
|
+
list: rules.list,
|
|
274
|
+
parentPaths: parentPathsForCollection(scan, entry.collection)
|
|
275
|
+
});
|
|
276
|
+
if (mode === "model") {
|
|
277
|
+
model += 1;
|
|
278
|
+
} else if (mode === "parent-child") {
|
|
279
|
+
parentChildSlugs.push(entry.slug);
|
|
280
|
+
} else {
|
|
281
|
+
unavailableSlugs.push(entry.slug);
|
|
282
|
+
}
|
|
283
|
+
return { ...entry, queryMode: mode, rules: entryRules };
|
|
284
|
+
});
|
|
285
|
+
return { entries: annotated, model, unavailableSlugs, parentChildSlugs };
|
|
286
|
+
}
|
|
287
|
+
function parentPathsForCollection(scan, collection) {
|
|
288
|
+
const paths = firestoreRulesAccessForCollection(scan, collection).paths;
|
|
289
|
+
const parents = /* @__PURE__ */ new Set();
|
|
290
|
+
for (const path of paths) {
|
|
291
|
+
const segments = path.split("/").filter((x) => x.length > 0);
|
|
292
|
+
const parentSegments = segments.slice(0, -2);
|
|
293
|
+
if (parentSegments.length > 0 && !parentSegments.some((x) => x.includes("=**"))) {
|
|
294
|
+
parents.add(parentSegments.join("/"));
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return Array.from(parents);
|
|
298
|
+
}
|
|
299
|
+
|
|
65
300
|
// packages/dbx-cli/src/lib/scan-helpers/exported-from-package.ts
|
|
66
301
|
import { existsSync as existsSync2, readFileSync as readFileSync2, statSync } from "node:fs";
|
|
67
302
|
import { dirname as dirname2, isAbsolute, join, resolve } from "node:path";
|
|
@@ -225,10 +460,16 @@ function renderEntry({ entry, bound }) {
|
|
|
225
460
|
entry.skip ? "skip: true" : void 0,
|
|
226
461
|
entry.excluded ? "excluded: true" : void 0,
|
|
227
462
|
entry.dispatcher ? "dispatcher: true" : void 0,
|
|
463
|
+
entry.queryMode ? `queryMode: ${JSON.stringify(entry.queryMode)}` : void 0,
|
|
464
|
+
entry.rules ? `rules: ${renderRules(entry.rules)}` : void 0,
|
|
228
465
|
bound ? `factory: ${entry.name}` : void 0
|
|
229
466
|
];
|
|
230
467
|
return ` { ${fields.filter(Boolean).join(", ")} }`;
|
|
231
468
|
}
|
|
469
|
+
function renderRules(rules) {
|
|
470
|
+
const parts = [`list: ${JSON.stringify(rules.list)}`, `collectionGroup: ${rules.collectionGroup ? "true" : "false"}`, rules.reason ? `reason: ${JSON.stringify(rules.reason)}` : void 0, rules.parentPaths && rules.parentPaths.length > 0 ? `parentPaths: ${JSON.stringify(rules.parentPaths)}` : void 0];
|
|
471
|
+
return `{ ${parts.filter(Boolean).join(", ")} }`;
|
|
472
|
+
}
|
|
232
473
|
function renderParams(params) {
|
|
233
474
|
const items = params.map((param) => {
|
|
234
475
|
const parts = [`name: ${JSON.stringify(param.name)}`, `type: ${JSON.stringify(param.type)}`, param.description ? `description: ${JSON.stringify(param.description)}` : void 0, `optional: ${param.optional ? "true" : "false"}`];
|
|
@@ -429,11 +670,18 @@ function analyzeSequence(input) {
|
|
|
429
670
|
if (isSingleFieldQuery(buckets)) {
|
|
430
671
|
return analyzeSingleField({ buckets, collection, scope, warnings });
|
|
431
672
|
}
|
|
673
|
+
if (scope === "COLLECTION" && isEqualityOnlySequence(buckets)) {
|
|
674
|
+
warnings.push({ kind: "equality-only-composite-skipped", factoryName, fields: buckets.equalities.map((e) => e.fieldPath) });
|
|
675
|
+
return { kind: "auto", warnings };
|
|
676
|
+
}
|
|
432
677
|
return analyzeMultiField({ buckets, sequenceEntries: sequence.entries, collection, scope, factoryName, warnings });
|
|
433
678
|
}
|
|
434
679
|
function isSingleFieldQuery(buckets) {
|
|
435
680
|
return buckets.distinctFieldCount === 1;
|
|
436
681
|
}
|
|
682
|
+
function isEqualityOnlySequence(buckets) {
|
|
683
|
+
return buckets.ranges.length === 0 && buckets.arrays.length === 0 && buckets.orderBys.length === 0 && buckets.equalities.length > 0 && buckets.equalities.every((e) => (e.operator ?? "==") === "==");
|
|
684
|
+
}
|
|
437
685
|
function analyzeSingleField(input) {
|
|
438
686
|
const { buckets, collection, scope, warnings } = input;
|
|
439
687
|
if (scope === "COLLECTION") {
|
|
@@ -528,7 +776,7 @@ function collectOrderByDirections(input) {
|
|
|
528
776
|
}
|
|
529
777
|
}
|
|
530
778
|
for (const [field, dirs] of conflicts) {
|
|
531
|
-
warnings.push({ kind: "orderby-conflict", factoryName, field, directions:
|
|
779
|
+
warnings.push({ kind: "orderby-conflict", factoryName, field, directions: Array.from(dirs) });
|
|
532
780
|
}
|
|
533
781
|
return out;
|
|
534
782
|
}
|
|
@@ -1009,7 +1257,7 @@ function composeEntry(input) {
|
|
|
1009
1257
|
}
|
|
1010
1258
|
function resolveConstraintSequences(input) {
|
|
1011
1259
|
const { candidate, tags, name, line, filePath, warnings } = input;
|
|
1012
|
-
const bodyResult = extractConstraintsFromBody({ decl: candidate.decl, factoryName: name, filePath, dispatcher: tags.dispatcher });
|
|
1260
|
+
const bodyResult = extractConstraintsFromBody({ decl: candidate.decl, factoryName: name, filePath, dispatcher: tags.dispatcher, suppressed: tags.skip || tags.specOnly });
|
|
1013
1261
|
for (const warning of bodyResult.warnings) {
|
|
1014
1262
|
warnings.push(warning);
|
|
1015
1263
|
}
|
|
@@ -1090,11 +1338,14 @@ function buildConstraintSequences(input) {
|
|
|
1090
1338
|
return sequences;
|
|
1091
1339
|
}
|
|
1092
1340
|
function extractConstraintsFromBody(input) {
|
|
1093
|
-
const { decl, factoryName, filePath, dispatcher } = input;
|
|
1341
|
+
const { decl, factoryName, filePath, dispatcher, suppressed } = input;
|
|
1094
1342
|
const entries = [];
|
|
1095
1343
|
const conditionalFieldSet = /* @__PURE__ */ new Set();
|
|
1096
1344
|
const warnings = [];
|
|
1097
1345
|
const body = decl.getBody();
|
|
1346
|
+
if (suppressed) {
|
|
1347
|
+
return { entries, conditionalFields: [], warnings, skipped: true, dispatcherDelegates: [] };
|
|
1348
|
+
}
|
|
1098
1349
|
if (dispatcher) {
|
|
1099
1350
|
const violation = body === void 0 ? void 0 : findFirstConstraintCall(body);
|
|
1100
1351
|
if (violation !== void 0) {
|
|
@@ -1985,7 +2236,16 @@ async function main() {
|
|
|
1985
2236
|
for (const warning of warnings) {
|
|
1986
2237
|
console.warn(warning);
|
|
1987
2238
|
}
|
|
1988
|
-
const
|
|
2239
|
+
const queryModes = applyQueryModes({ collected, rules: flags.rules });
|
|
2240
|
+
if (queryModes.kind === "failure") {
|
|
2241
|
+
console.error(queryModes.message);
|
|
2242
|
+
process.exit(1);
|
|
2243
|
+
return;
|
|
2244
|
+
}
|
|
2245
|
+
for (const warning of queryModes.warnings) {
|
|
2246
|
+
console.warn(warning);
|
|
2247
|
+
}
|
|
2248
|
+
const formatted = await renderQueryManifest({ outputFile, entries: queryModes.entries, projectName, namespace });
|
|
1989
2249
|
const relOutput = relative2(WORKSPACE_ROOT, outputFile);
|
|
1990
2250
|
if (flags.check) {
|
|
1991
2251
|
const current = existsSync3(outputFile) ? readFileSync3(outputFile, "utf8") : void 0;
|
|
@@ -2000,12 +2260,38 @@ async function main() {
|
|
|
2000
2260
|
console.log(`[${outcome}] ${relOutput}`);
|
|
2001
2261
|
}
|
|
2002
2262
|
const boundCount = collected.filter((x) => x.bound).length;
|
|
2003
|
-
console.log(`Summary: ${flags.components.length} component(s) \xB7 ${collected.length} entries \xB7 ${boundCount}
|
|
2263
|
+
console.log(`Summary: ${flags.components.length} component(s) \xB7 ${collected.length} entries \xB7 ${boundCount} bound \xB7 ${collected.length - boundCount} unbound \xB7 ${droppedSpecOnly} spec-only dropped \xB7 rules: ${queryModes.summary}`);
|
|
2004
2264
|
if (flags.strict && boundCount < collected.length) {
|
|
2005
2265
|
console.error(`[strict] ${collected.length - boundCount} factor(y|ies) failed to bind \u2014 failing build.`);
|
|
2006
2266
|
process.exit(1);
|
|
2007
2267
|
}
|
|
2008
2268
|
}
|
|
2269
|
+
function applyQueryModes(input) {
|
|
2270
|
+
const { collected, rules } = input;
|
|
2271
|
+
let result;
|
|
2272
|
+
if (rules) {
|
|
2273
|
+
const rulesFile = resolveWorkspacePath(rules);
|
|
2274
|
+
if (existsSync3(rulesFile)) {
|
|
2275
|
+
const annotated = annotateQueryEntryMode({ entries: collected.map((x) => x.entry), rulesSource: readFileSync3(rulesFile, "utf8") });
|
|
2276
|
+
const entries = annotated.entries.map((entry, index) => ({ ...collected[index], entry }));
|
|
2277
|
+
const warnings = annotated.unavailableSlugs.map((slug) => `[query-unavailable] ${slug} \u2014 no client can run this query; see \`firestore-queries ${slug}\`.`);
|
|
2278
|
+
if (annotated.parentChildSlugs.length > 0) {
|
|
2279
|
+
warnings.push(`[query-parent-child] ${annotated.parentChildSlugs.length} quer${annotated.parentChildSlugs.length === 1 ? "y" : "ies"} run only with --parent: ${annotated.parentChildSlugs.join(", ")}`);
|
|
2280
|
+
}
|
|
2281
|
+
result = {
|
|
2282
|
+
kind: "success",
|
|
2283
|
+
entries,
|
|
2284
|
+
warnings,
|
|
2285
|
+
summary: `${annotated.model} model \xB7 ${annotated.parentChildSlugs.length} parent-child \xB7 ${annotated.unavailableSlugs.length} unavailable`
|
|
2286
|
+
};
|
|
2287
|
+
} else {
|
|
2288
|
+
result = { kind: "failure", message: `[rules] ${relative2(WORKSPACE_ROOT, rulesFile)} does not exist \u2014 fix --rules or drop it.` };
|
|
2289
|
+
}
|
|
2290
|
+
} else {
|
|
2291
|
+
result = { kind: "success", entries: collected, warnings: [], summary: "not scanned (pass --rules=<firestore.rules> to resolve query modes)" };
|
|
2292
|
+
}
|
|
2293
|
+
return result;
|
|
2294
|
+
}
|
|
2009
2295
|
function resolveWorkspacePath(value) {
|
|
2010
2296
|
return isAbsolute2(value) ? value : resolve3(WORKSPACE_ROOT, value);
|
|
2011
2297
|
}
|
|
@@ -2017,6 +2303,7 @@ function parseFlags(argv) {
|
|
|
2017
2303
|
const components = [];
|
|
2018
2304
|
let output;
|
|
2019
2305
|
let project;
|
|
2306
|
+
let rules;
|
|
2020
2307
|
let strict = false;
|
|
2021
2308
|
let check = false;
|
|
2022
2309
|
for (const arg of argv) {
|
|
@@ -2031,9 +2318,12 @@ function parseFlags(argv) {
|
|
|
2031
2318
|
output = arg.slice("--output=".length);
|
|
2032
2319
|
} else if (arg.startsWith("--project=")) {
|
|
2033
2320
|
project = arg.slice("--project=".length);
|
|
2321
|
+
} else if (arg.startsWith("--rules=")) {
|
|
2322
|
+
const value = arg.slice("--rules=".length).trim();
|
|
2323
|
+
if (value) rules = value;
|
|
2034
2324
|
}
|
|
2035
2325
|
}
|
|
2036
|
-
return { components, output, project, strict, check };
|
|
2326
|
+
return { components, output, project, rules, strict, check };
|
|
2037
2327
|
}
|
|
2038
2328
|
function printUsageAndExit() {
|
|
2039
2329
|
console.error(String.raw`generate-firestore-query-manifest
|
|
@@ -2043,7 +2333,7 @@ Usage:
|
|
|
2043
2333
|
--project=<name> \
|
|
2044
2334
|
--component=<component-dir> [--component=<component-dir> ...] \
|
|
2045
2335
|
--output=<path-to-query.manifest.generated.ts> \
|
|
2046
|
-
[--strict] [--check]
|
|
2336
|
+
[--rules=<path-to-firestore.rules>] [--strict] [--check]
|
|
2047
2337
|
|
|
2048
2338
|
Required flags:
|
|
2049
2339
|
--component=<dir> A "-firebase" component root to scan. Repeatable.
|
|
@@ -2051,6 +2341,10 @@ Required flags:
|
|
|
2051
2341
|
|
|
2052
2342
|
Optional:
|
|
2053
2343
|
--project=<name> Project name for the regenerate banner; also derives the constant name.
|
|
2344
|
+
--rules=<path> The app's firestore.rules. Stamps each entry with how it must be invoked
|
|
2345
|
+
(model / parent-child / unavailable), so a catalogued-but-unrunnable query is
|
|
2346
|
+
flagged at generation time instead of surfacing as AUTH_FORBIDDEN at call
|
|
2347
|
+
time. Omitted => unknown.
|
|
2054
2348
|
--strict Fail when any tagged factory is not exported from its component barrel.
|
|
2055
2349
|
--check Do not write; fail when the committed file is out of date.`);
|
|
2056
2350
|
process.exit(1);
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dereekb/dbx-cli-firestore-query-manifest",
|
|
3
|
-
"version": "13.
|
|
3
|
+
"version": "13.42.0",
|
|
4
4
|
"private": true,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"devDependencies": {
|
|
7
7
|
"eslint": "10.4.0"
|
|
8
8
|
},
|
|
9
9
|
"peerDependencies": {
|
|
10
|
-
"@dereekb/dbx-cli": "13.
|
|
11
|
-
"@dereekb/util": "13.
|
|
10
|
+
"@dereekb/dbx-cli": "13.42.0",
|
|
11
|
+
"@dereekb/util": "13.42.0"
|
|
12
12
|
}
|
|
13
13
|
}
|