@dereekb/dbx-cli 13.39.0 → 13.41.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.
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@dereekb/dbx-cli/eslint",
3
- "version": "13.39.0",
3
+ "version": "13.41.0",
4
4
  "type": "module",
5
5
  "peerDependencies": {
6
- "@dereekb/dbx-cli": "13.39.0",
7
- "@dereekb/util": "13.39.0",
6
+ "@dereekb/dbx-cli": "13.41.0",
7
+ "@dereekb/util": "13.41.0",
8
8
  "@typescript-eslint/utils": "8.59.3",
9
9
  "typescript": "5.9.3"
10
10
  },
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "@dereekb/dbx-cli-firebase-api-manifest",
3
- "version": "13.39.0",
3
+ "version": "13.41.0",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "devDependencies": {
7
7
  "ts-morph": "^21.0.0"
8
8
  },
9
9
  "peerDependencies": {
10
- "@dereekb/dbx-cli": "13.39.0",
11
- "@dereekb/util": "13.39.0",
10
+ "@dereekb/dbx-cli": "13.41.0",
11
+ "@dereekb/util": "13.41.0",
12
12
  "prettier": "3.8.3"
13
13
  }
14
14
  }
@@ -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.39.0",
12
+ version: "13.41.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.39.0",
20
- "@dereekb/util": "13.39.0"
19
+ "@dereekb/dbx-cli": "13.41.0",
20
+ "@dereekb/util": "13.41.0"
21
21
  }
22
22
  };
23
23
 
@@ -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 = [...accumulated.entries()].map(([collection, value]) => ({
86
+ collection,
87
+ paths: [...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 [...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"}`];
@@ -1985,7 +2226,16 @@ async function main() {
1985
2226
  for (const warning of warnings) {
1986
2227
  console.warn(warning);
1987
2228
  }
1988
- const formatted = await renderQueryManifest({ outputFile, entries: collected, projectName, namespace });
2229
+ const queryModes = applyQueryModes({ collected, rules: flags.rules });
2230
+ if (queryModes.kind === "failure") {
2231
+ console.error(queryModes.message);
2232
+ process.exit(1);
2233
+ return;
2234
+ }
2235
+ for (const warning of queryModes.warnings) {
2236
+ console.warn(warning);
2237
+ }
2238
+ const formatted = await renderQueryManifest({ outputFile, entries: queryModes.entries, projectName, namespace });
1989
2239
  const relOutput = relative2(WORKSPACE_ROOT, outputFile);
1990
2240
  if (flags.check) {
1991
2241
  const current = existsSync3(outputFile) ? readFileSync3(outputFile, "utf8") : void 0;
@@ -2000,12 +2250,38 @@ async function main() {
2000
2250
  console.log(`[${outcome}] ${relOutput}`);
2001
2251
  }
2002
2252
  const boundCount = collected.filter((x) => x.bound).length;
2003
- console.log(`Summary: ${flags.components.length} component(s) \xB7 ${collected.length} entries \xB7 ${boundCount} invocable \xB7 ${collected.length - boundCount} unbound \xB7 ${droppedSpecOnly} spec-only dropped`);
2253
+ 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
2254
  if (flags.strict && boundCount < collected.length) {
2005
2255
  console.error(`[strict] ${collected.length - boundCount} factor(y|ies) failed to bind \u2014 failing build.`);
2006
2256
  process.exit(1);
2007
2257
  }
2008
2258
  }
2259
+ function applyQueryModes(input) {
2260
+ const { collected, rules } = input;
2261
+ let result;
2262
+ if (rules) {
2263
+ const rulesFile = resolveWorkspacePath(rules);
2264
+ if (existsSync3(rulesFile)) {
2265
+ const annotated = annotateQueryEntryMode({ entries: collected.map((x) => x.entry), rulesSource: readFileSync3(rulesFile, "utf8") });
2266
+ const entries = annotated.entries.map((entry, index) => ({ ...collected[index], entry }));
2267
+ const warnings = annotated.unavailableSlugs.map((slug) => `[query-unavailable] ${slug} \u2014 no client can run this query; see \`firestore-queries ${slug}\`.`);
2268
+ if (annotated.parentChildSlugs.length > 0) {
2269
+ warnings.push(`[query-parent-child] ${annotated.parentChildSlugs.length} quer${annotated.parentChildSlugs.length === 1 ? "y" : "ies"} run only with --parent: ${annotated.parentChildSlugs.join(", ")}`);
2270
+ }
2271
+ result = {
2272
+ kind: "success",
2273
+ entries,
2274
+ warnings,
2275
+ summary: `${annotated.model} model \xB7 ${annotated.parentChildSlugs.length} parent-child \xB7 ${annotated.unavailableSlugs.length} unavailable`
2276
+ };
2277
+ } else {
2278
+ result = { kind: "failure", message: `[rules] ${relative2(WORKSPACE_ROOT, rulesFile)} does not exist \u2014 fix --rules or drop it.` };
2279
+ }
2280
+ } else {
2281
+ result = { kind: "success", entries: collected, warnings: [], summary: "not scanned (pass --rules=<firestore.rules> to resolve query modes)" };
2282
+ }
2283
+ return result;
2284
+ }
2009
2285
  function resolveWorkspacePath(value) {
2010
2286
  return isAbsolute2(value) ? value : resolve3(WORKSPACE_ROOT, value);
2011
2287
  }
@@ -2017,6 +2293,7 @@ function parseFlags(argv) {
2017
2293
  const components = [];
2018
2294
  let output;
2019
2295
  let project;
2296
+ let rules;
2020
2297
  let strict = false;
2021
2298
  let check = false;
2022
2299
  for (const arg of argv) {
@@ -2031,9 +2308,12 @@ function parseFlags(argv) {
2031
2308
  output = arg.slice("--output=".length);
2032
2309
  } else if (arg.startsWith("--project=")) {
2033
2310
  project = arg.slice("--project=".length);
2311
+ } else if (arg.startsWith("--rules=")) {
2312
+ const value = arg.slice("--rules=".length).trim();
2313
+ if (value) rules = value;
2034
2314
  }
2035
2315
  }
2036
- return { components, output, project, strict, check };
2316
+ return { components, output, project, rules, strict, check };
2037
2317
  }
2038
2318
  function printUsageAndExit() {
2039
2319
  console.error(String.raw`generate-firestore-query-manifest
@@ -2043,7 +2323,7 @@ Usage:
2043
2323
  --project=<name> \
2044
2324
  --component=<component-dir> [--component=<component-dir> ...] \
2045
2325
  --output=<path-to-query.manifest.generated.ts> \
2046
- [--strict] [--check]
2326
+ [--rules=<path-to-firestore.rules>] [--strict] [--check]
2047
2327
 
2048
2328
  Required flags:
2049
2329
  --component=<dir> A "-firebase" component root to scan. Repeatable.
@@ -2051,6 +2331,10 @@ Required flags:
2051
2331
 
2052
2332
  Optional:
2053
2333
  --project=<name> Project name for the regenerate banner; also derives the constant name.
2334
+ --rules=<path> The app's firestore.rules. Stamps each entry with how it must be invoked
2335
+ (model / parent-child / unavailable), so a catalogued-but-unrunnable query is
2336
+ flagged at generation time instead of surfacing as AUTH_FORBIDDEN at call
2337
+ time. Omitted => unknown.
2054
2338
  --strict Fail when any tagged factory is not exported from its component barrel.
2055
2339
  --check Do not write; fail when the committed file is out of date.`);
2056
2340
  process.exit(1);
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@dereekb/dbx-cli-firestore-query-manifest",
3
- "version": "13.39.0",
3
+ "version": "13.41.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.39.0",
11
- "@dereekb/util": "13.39.0"
10
+ "@dereekb/dbx-cli": "13.41.0",
11
+ "@dereekb/util": "13.41.0"
12
12
  }
13
13
  }
@@ -5,14 +5,14 @@ const require = __createRequire(import.meta.url);
5
5
  // packages/dbx-cli/generate-firestore-indexes/package.json
6
6
  var package_default = {
7
7
  name: "@dereekb/dbx-cli-generate-firestore-indexes",
8
- version: "13.39.0",
8
+ version: "13.41.0",
9
9
  private: true,
10
10
  type: "module",
11
11
  devDependencies: {
12
12
  eslint: "10.4.0"
13
13
  },
14
14
  peerDependencies: {
15
- "@dereekb/dbx-cli": "13.39.0"
15
+ "@dereekb/dbx-cli": "13.41.0"
16
16
  }
17
17
  };
18
18
 
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@dereekb/dbx-cli-generate-firestore-indexes",
3
- "version": "13.39.0",
3
+ "version": "13.41.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.39.0"
10
+ "@dereekb/dbx-cli": "13.41.0"
11
11
  }
12
12
  }
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@dereekb/dbx-cli-generate-mcp-manifest",
3
- "version": "13.39.0",
3
+ "version": "13.41.0",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "peerDependencies": {
7
- "@dereekb/dbx-cli": "13.39.0",
8
- "@dereekb/model": "13.39.0",
7
+ "@dereekb/dbx-cli": "13.41.0",
8
+ "@dereekb/model": "13.41.0",
9
9
  "arktype": "^2.2.0",
10
10
  "jiti": "2.6.1"
11
11
  }
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@dereekb/dbx-cli-generate-route-manifest",
3
- "version": "13.39.0",
3
+ "version": "13.41.0",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "peerDependencies": {
7
- "@dereekb/dbx-cli": "13.39.0",
7
+ "@dereekb/dbx-cli": "13.41.0",
8
8
  "ts-morph": "^21.0.0"
9
9
  }
10
10
  }