@dereekb/dbx-cli 13.37.0 → 13.39.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 +8 -8
- package/firebase-api-manifest/main.js +191 -166
- package/firebase-api-manifest/package.json +3 -3
- package/firestore-query-manifest/main.js +2063 -0
- package/firestore-query-manifest/package.json +13 -0
- package/firestore-rules/src/firestore-rules-scan.d.ts +89 -0
- package/firestore-rules/src/index.d.ts +1 -0
- package/generate-firestore-indexes/main.js +2 -2
- package/generate-firestore-indexes/package.json +2 -2
- package/generate-mcp-manifest/main.js +1 -0
- package/generate-mcp-manifest/package.json +3 -3
- package/generate-route-manifest/package.json +2 -2
- package/index.esm.js +12615 -6863
- package/lint-cache/package.json +2 -2
- package/manifest-extract/index.esm.js +4 -1
- package/manifest-extract/package.json +7 -7
- package/manifest-extract/src/lib/types.d.ts +6 -0
- package/model-test/LICENSE +21 -0
- package/model-test/index.d.ts +1 -0
- package/model-test/index.esm.js +5984 -0
- package/model-test/package.json +21 -0
- package/model-test/src/index.d.ts +41 -0
- package/model-test/src/lib/fixture/archetype.d.ts +33 -0
- package/model-test/src/lib/fixture/extract.d.ts +37 -0
- package/model-test/src/lib/fixture/format.json.d.ts +24 -0
- package/model-test/src/lib/fixture/format.markdown.d.ts +23 -0
- package/model-test/src/lib/fixture/forward.d.ts +48 -0
- package/model-test/src/lib/fixture/framework-fixtures.d.ts +70 -0
- package/model-test/src/lib/fixture/inspect.d.ts +22 -0
- package/model-test/src/lib/fixture/scaffold.d.ts +74 -0
- package/model-test/src/lib/fixture/types.d.ts +175 -0
- package/model-test/src/lib/test/discover.d.ts +81 -0
- package/model-test/src/lib/test/extract.d.ts +49 -0
- package/model-test/src/lib/test/format.hotspots.d.ts +23 -0
- package/model-test/src/lib/test/format.json.d.ts +25 -0
- package/model-test/src/lib/test/format.list-app.d.ts +30 -0
- package/model-test/src/lib/test/format.markdown.d.ts +25 -0
- package/model-test/src/lib/test/hotspots.d.ts +98 -0
- package/model-test/src/lib/test/inspect.d.ts +32 -0
- package/model-test/src/lib/test/search.d.ts +34 -0
- package/model-test/src/lib/test/types.d.ts +179 -0
- package/package.json +23 -22
- package/route/package.json +12 -12
- package/src/lib/api/firestore-session.client.d.ts +54 -0
- package/src/lib/api/get-args.helper.d.ts +4 -1
- package/src/lib/api/get-many.command.d.ts +8 -3
- package/src/lib/api/get.command.d.ts +13 -5
- package/src/lib/api/index.d.ts +1 -0
- package/src/lib/config/env.d.ts +106 -0
- package/src/lib/config/firestore-session.cache.d.ts +86 -0
- package/src/lib/config/index.d.ts +1 -0
- package/src/lib/config/paths.d.ts +3 -1
- package/src/lib/context/cli.context.d.ts +87 -3
- package/src/lib/doctor/firestore-session.check.d.ts +113 -0
- package/src/lib/doctor/index.d.ts +1 -0
- package/src/lib/firestore/firestore-get.command.d.ts +28 -0
- package/src/lib/firestore/firestore-queries.command.d.ts +26 -0
- package/src/lib/firestore/firestore-query.command.d.ts +25 -0
- package/src/lib/firestore/firestore.accessor.d.ts +85 -0
- package/src/lib/firestore/firestore.collection.d.ts +44 -0
- package/src/lib/firestore/firestore.error.d.ts +14 -0
- package/src/lib/firestore/firestore.models.d.ts +223 -0
- package/src/lib/firestore/firestore.query-params.d.ts +34 -0
- package/src/lib/firestore/firestore.query.d.ts +79 -0
- package/src/lib/firestore/firestore.read.d.ts +180 -0
- package/src/lib/firestore/firestore.session.d.ts +97 -0
- package/src/lib/firestore/index.d.ts +13 -0
- package/src/lib/firestore/query-info-utils.d.ts +48 -0
- package/src/lib/firestore/query-registry.d.ts +32 -0
- package/src/lib/index.d.ts +3 -0
- package/src/lib/manifest/types.d.ts +117 -0
- package/src/lib/mcp-scan/manifest/dbx-docs-ui-examples-schema.d.ts +2 -2
- package/src/lib/mcp-scan/manifest/model-snapshot-fields-schema.d.ts +2 -2
- package/src/lib/mcp-scan/manifest/pipes-schema.d.ts +2 -2
- package/src/lib/mcp-scan/manifest/ui-components-schema.d.ts +2 -2
- package/src/lib/mcp-scan/manifest/utils-schema.d.ts +2 -2
- package/src/lib/mcp-scan/scan/dbx-docs-ui-examples-extract.d.ts +1 -1
- package/src/lib/mcp-scan/scan/extract-models/types.d.ts +5 -0
- package/src/lib/mcp-scan/scan/ui-components-extract.d.ts +1 -1
- package/src/lib/middleware/auth.middleware.d.ts +6 -0
- package/src/lib/runner/run.d.ts +28 -1
- package/src/lib/scan-helpers/emit-generated-ts.d.ts +76 -0
- package/src/lib/scan-helpers/exported-from-package.d.ts +29 -0
- package/src/lib/util/index.d.ts +1 -0
- package/src/lib/util/output.d.ts +10 -0
- package/src/lib/util/stdin.d.ts +39 -2
- package/src/lib/util/table.d.ts +40 -0
- package/test/index.esm.js +4 -2
- package/test/package.json +14 -14
- package/test/src/lib/cli-test.d.ts +12 -2
- package/validate/LICENSE +21 -0
- package/validate/index.js +16641 -0
- package/validate/package.json +11 -0
- package/eslint/index.cjs.default.js +0 -1
- package/eslint/index.cjs.js +0 -1112
- package/eslint/index.cjs.mjs +0 -2
- package/index.cjs.js +0 -59813
- package/manifest-extract/index.cjs.default.js +0 -1
- package/manifest-extract/index.cjs.js +0 -1592
- package/manifest-extract/index.cjs.mjs +0 -2
- package/route/index.cjs.default.js +0 -1
- package/route/index.cjs.js +0 -18
- package/route/index.cjs.mjs +0 -2
- package/test/index.cjs.default.js +0 -1
- package/test/index.cjs.js +0 -381
- package/test/index.cjs.mjs +0 -2
|
@@ -0,0 +1,2063 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire as __createRequire } from 'node:module';
|
|
3
|
+
const require = __createRequire(import.meta.url);
|
|
4
|
+
|
|
5
|
+
// packages/dbx-cli/firestore-query-manifest/src/main.ts
|
|
6
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
|
|
7
|
+
import { isAbsolute as isAbsolute2, relative as relative2, resolve as resolve3 } from "node:path";
|
|
8
|
+
|
|
9
|
+
// packages/dbx-cli/firestore-query-manifest/package.json
|
|
10
|
+
var package_default = {
|
|
11
|
+
name: "@dereekb/dbx-cli-firestore-query-manifest",
|
|
12
|
+
version: "13.39.0",
|
|
13
|
+
private: true,
|
|
14
|
+
type: "module",
|
|
15
|
+
devDependencies: {
|
|
16
|
+
eslint: "10.4.0"
|
|
17
|
+
},
|
|
18
|
+
peerDependencies: {
|
|
19
|
+
"@dereekb/dbx-cli": "13.39.0",
|
|
20
|
+
"@dereekb/util": "13.39.0"
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// packages/util/src/lib/string/sort.ts
|
|
25
|
+
var compareStrings = (a, b) => a.localeCompare(b);
|
|
26
|
+
|
|
27
|
+
// packages/dbx-cli/src/lib/scan-helpers/emit-generated-ts.ts
|
|
28
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
29
|
+
import { dirname } from "node:path";
|
|
30
|
+
import { format, resolveConfig } from "prettier";
|
|
31
|
+
// @__NO_SIDE_EFFECTS__
|
|
32
|
+
function renderGroupedImportLines(imports) {
|
|
33
|
+
const importsByPackage = /* @__PURE__ */ new Map();
|
|
34
|
+
for (const { packageName, identifier } of imports) {
|
|
35
|
+
if (!packageName || !identifier) continue;
|
|
36
|
+
const set = importsByPackage.get(packageName) ?? /* @__PURE__ */ new Set();
|
|
37
|
+
set.add(identifier);
|
|
38
|
+
importsByPackage.set(packageName, set);
|
|
39
|
+
}
|
|
40
|
+
return [...importsByPackage.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([pkg, names]) => {
|
|
41
|
+
const sortedNames = [...names].sort(compareStrings).join(", ");
|
|
42
|
+
return `import { ${sortedNames} } from '${pkg}';`;
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
async function formatGeneratedTs(source, outputFile) {
|
|
46
|
+
const config = await resolveConfig(outputFile);
|
|
47
|
+
return format(source, { ...config, filepath: outputFile });
|
|
48
|
+
}
|
|
49
|
+
function writeGeneratedTsFile(input) {
|
|
50
|
+
const { outputFile, contents } = input;
|
|
51
|
+
const outputDir = dirname(outputFile);
|
|
52
|
+
let result;
|
|
53
|
+
if (!existsSync(outputDir)) {
|
|
54
|
+
mkdirSync(outputDir, { recursive: true });
|
|
55
|
+
}
|
|
56
|
+
if (existsSync(outputFile) && readFileSync(outputFile, "utf8") === contents) {
|
|
57
|
+
result = "unchanged";
|
|
58
|
+
} else {
|
|
59
|
+
writeFileSync(outputFile, contents);
|
|
60
|
+
result = "wrote";
|
|
61
|
+
}
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// packages/dbx-cli/src/lib/scan-helpers/exported-from-package.ts
|
|
66
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, statSync } from "node:fs";
|
|
67
|
+
import { dirname as dirname2, isAbsolute, join, resolve } from "node:path";
|
|
68
|
+
function isExportedFromPackage(input) {
|
|
69
|
+
const { packageRoot, identifier } = input;
|
|
70
|
+
const indexPath = locateBarrelEntry(packageRoot);
|
|
71
|
+
return indexPath ? findIdentifierInBarrelChain(indexPath, identifier, /* @__PURE__ */ new Set()) : false;
|
|
72
|
+
}
|
|
73
|
+
function locateBarrelEntry(packageRoot) {
|
|
74
|
+
const candidates = [join(packageRoot, "src", "index.ts"), join(packageRoot, "src", "index.d.ts"), join(packageRoot, "index.d.ts"), join(packageRoot, "index.ts")];
|
|
75
|
+
return candidates.find((candidate) => existsSync2(candidate));
|
|
76
|
+
}
|
|
77
|
+
var EXPORT_DECL_PATTERNS = [/export\s+(?:declare\s+)?const\s+IDENT\b/, /export\s+(?:declare\s+)?function\s+IDENT\b/, /export\s*\{[^}]*\bIDENT\b[^}]*\}/];
|
|
78
|
+
function findIdentifierInBarrelChain(filePath, identifier, visited) {
|
|
79
|
+
let result = false;
|
|
80
|
+
if (!visited.has(filePath)) {
|
|
81
|
+
visited.add(filePath);
|
|
82
|
+
const text = readFileTextSafe(filePath);
|
|
83
|
+
if (text !== void 0) {
|
|
84
|
+
result = barrelTextDeclaresIdentifier(text, identifier) || reExportChainHasIdentifier({ fromFile: filePath, text, identifier, visited });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return result;
|
|
88
|
+
}
|
|
89
|
+
function readFileTextSafe(filePath) {
|
|
90
|
+
let text;
|
|
91
|
+
try {
|
|
92
|
+
text = readFileSync2(filePath, "utf8");
|
|
93
|
+
} catch {
|
|
94
|
+
text = void 0;
|
|
95
|
+
}
|
|
96
|
+
return text;
|
|
97
|
+
}
|
|
98
|
+
function barrelTextDeclaresIdentifier(text, identifier) {
|
|
99
|
+
let result = false;
|
|
100
|
+
for (const pattern of EXPORT_DECL_PATTERNS) {
|
|
101
|
+
const re = new RegExp(pattern.source.replace("IDENT", escapeRegExp(identifier)));
|
|
102
|
+
if (re.test(text)) {
|
|
103
|
+
result = true;
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
109
|
+
function reExportChainHasIdentifier(input) {
|
|
110
|
+
const { fromFile, text, identifier, visited } = input;
|
|
111
|
+
let result = false;
|
|
112
|
+
const dir = dirname2(fromFile);
|
|
113
|
+
for (const reExportTarget of collectReExportTargets(text)) {
|
|
114
|
+
const resolved = resolveReExport(dir, reExportTarget);
|
|
115
|
+
if (resolved && findIdentifierInBarrelChain(resolved, identifier, visited)) {
|
|
116
|
+
result = true;
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return result;
|
|
121
|
+
}
|
|
122
|
+
function collectReExportTargets(text) {
|
|
123
|
+
const out = [];
|
|
124
|
+
const re = /export\s*(?:\*|\{[^}]*\})\s*from\s*['"]([^'"]+)['"]/g;
|
|
125
|
+
let match;
|
|
126
|
+
while ((match = re.exec(text)) !== null) {
|
|
127
|
+
out.push(match[1]);
|
|
128
|
+
}
|
|
129
|
+
return out;
|
|
130
|
+
}
|
|
131
|
+
function resolveReExport(fromDir, target) {
|
|
132
|
+
let result;
|
|
133
|
+
if (target.startsWith(".")) {
|
|
134
|
+
const candidate = isAbsolute(target) ? target : resolve(fromDir, target);
|
|
135
|
+
for (const ext of [".ts", ".mts", ".d.ts", "/index.ts", "/index.mts", "/index.d.ts"]) {
|
|
136
|
+
const probe = hasTsModuleExtension(candidate) ? candidate : candidate + ext;
|
|
137
|
+
const resolved = resolveExistingTsPath(probe);
|
|
138
|
+
if (resolved) {
|
|
139
|
+
result = resolved;
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return result;
|
|
145
|
+
}
|
|
146
|
+
function hasTsModuleExtension(value) {
|
|
147
|
+
return value.endsWith(".ts") || value.endsWith(".mts");
|
|
148
|
+
}
|
|
149
|
+
function resolveExistingTsPath(probe) {
|
|
150
|
+
let result;
|
|
151
|
+
if (existsSync2(probe)) {
|
|
152
|
+
const stat = statSync(probe);
|
|
153
|
+
if (stat.isFile()) {
|
|
154
|
+
result = probe;
|
|
155
|
+
} else if (stat.isDirectory()) {
|
|
156
|
+
const sourceIndex = join(probe, "index.ts");
|
|
157
|
+
if (existsSync2(sourceIndex)) {
|
|
158
|
+
result = sourceIndex;
|
|
159
|
+
} else {
|
|
160
|
+
const declarationIndex = join(probe, "index.d.ts");
|
|
161
|
+
if (existsSync2(declarationIndex)) {
|
|
162
|
+
result = declarationIndex;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return result;
|
|
168
|
+
}
|
|
169
|
+
function escapeRegExp(value) {
|
|
170
|
+
return value.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// packages/dbx-cli/firestore-query-manifest/src/bind-factories.ts
|
|
174
|
+
function bindQueryFactories(input) {
|
|
175
|
+
const { componentRoot, entries, isExported = isExportedFromPackage } = input;
|
|
176
|
+
const bound = [];
|
|
177
|
+
const warnings = [];
|
|
178
|
+
for (const entry of entries) {
|
|
179
|
+
const found = isExported({ packageRoot: componentRoot, identifier: entry.name });
|
|
180
|
+
if (!found) {
|
|
181
|
+
warnings.push(`[no-factory] ${entry.module} \xB7 ${entry.slug} \u2192 ${entry.name} not exported`);
|
|
182
|
+
}
|
|
183
|
+
bound.push({ entry, bound: found });
|
|
184
|
+
}
|
|
185
|
+
return { bound, warnings };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// packages/dbx-cli/firestore-query-manifest/src/emit.ts
|
|
189
|
+
async function renderQueryManifest(input) {
|
|
190
|
+
const { outputFile, entries, projectName, namespace } = input;
|
|
191
|
+
const factoryImports = entries.filter((x) => x.bound).map((x) => ({ packageName: x.entry.module, identifier: x.entry.name }));
|
|
192
|
+
const importLines = renderGroupedImportLines(factoryImports);
|
|
193
|
+
const sorted = [...entries].sort((a, b) => compareStrings(a.entry.slug, b.entry.slug));
|
|
194
|
+
const source = `/* eslint-disable @nx/enforce-module-boundaries */
|
|
195
|
+
// AUTO-GENERATED \u2014 DO NOT EDIT.
|
|
196
|
+
// Run \`npx nx run ${projectName}:generate-firestore-query-manifest\` to refresh.
|
|
197
|
+
|
|
198
|
+
${importLines.join("\n")}
|
|
199
|
+
import { type CliFirestoreQueryManifest } from '@dereekb/dbx-cli';
|
|
200
|
+
|
|
201
|
+
export const ${namespace}: CliFirestoreQueryManifest = [
|
|
202
|
+
${sorted.map((x) => renderEntry(x)).join(",\n")}
|
|
203
|
+
];
|
|
204
|
+
`;
|
|
205
|
+
return formatGeneratedTs(source, outputFile);
|
|
206
|
+
}
|
|
207
|
+
function renderEntry({ entry, bound }) {
|
|
208
|
+
const fields = [
|
|
209
|
+
`slug: ${JSON.stringify(entry.slug)}`,
|
|
210
|
+
`name: ${JSON.stringify(entry.name)}`,
|
|
211
|
+
`module: ${JSON.stringify(entry.module)}`,
|
|
212
|
+
`subpath: ${JSON.stringify(entry.subpath)}`,
|
|
213
|
+
`model: ${JSON.stringify(entry.model)}`,
|
|
214
|
+
`collection: ${JSON.stringify(entry.collection)}`,
|
|
215
|
+
`isNested: ${entry.isNested ? "true" : "false"}`,
|
|
216
|
+
`scope: ${JSON.stringify(entry.scope)}`,
|
|
217
|
+
`signature: ${JSON.stringify(entry.signature)}`,
|
|
218
|
+
`params: ${renderParams(entry.params)}`,
|
|
219
|
+
entry.description ? `description: ${JSON.stringify(entry.description)}` : void 0,
|
|
220
|
+
entry.category ? `category: ${JSON.stringify(entry.category)}` : void 0,
|
|
221
|
+
entry.tags && entry.tags.length > 0 ? `tags: ${JSON.stringify(entry.tags)}` : void 0,
|
|
222
|
+
entry.example ? `example: ${JSON.stringify(entry.example)}` : void 0,
|
|
223
|
+
entry.relatedSlugs && entry.relatedSlugs.length > 0 ? `relatedSlugs: ${JSON.stringify(entry.relatedSlugs)}` : void 0,
|
|
224
|
+
entry.manual ? "manual: true" : void 0,
|
|
225
|
+
entry.skip ? "skip: true" : void 0,
|
|
226
|
+
entry.excluded ? "excluded: true" : void 0,
|
|
227
|
+
entry.dispatcher ? "dispatcher: true" : void 0,
|
|
228
|
+
bound ? `factory: ${entry.name}` : void 0
|
|
229
|
+
];
|
|
230
|
+
return ` { ${fields.filter(Boolean).join(", ")} }`;
|
|
231
|
+
}
|
|
232
|
+
function renderParams(params) {
|
|
233
|
+
const items = params.map((param) => {
|
|
234
|
+
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"}`];
|
|
235
|
+
return `{ ${parts.filter(Boolean).join(", ")} }`;
|
|
236
|
+
});
|
|
237
|
+
return `[${items.join(", ")}]`;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// packages/dbx-cli/firestore-indexes/src/model-firebase-index-build-manifest.ts
|
|
241
|
+
import { relative, resolve as resolve2 } from "node:path";
|
|
242
|
+
import { type as type3 } from "arktype";
|
|
243
|
+
|
|
244
|
+
// packages/dbx-cli/firestore-indexes/src/model-firebase-index-schema.ts
|
|
245
|
+
import { type } from "arktype";
|
|
246
|
+
var DEFAULT_FIRESTORE_INDEX_DENSITY = "SPARSE_ALL";
|
|
247
|
+
var FIRESTORE_WHERE_OPERATORS = ["==", "!=", "<", "<=", ">", ">=", "in", "not-in", "array-contains", "array-contains-any"];
|
|
248
|
+
var ConstraintSequenceEntry = type({
|
|
249
|
+
kind: '"where" | "orderBy"',
|
|
250
|
+
fieldPath: "string",
|
|
251
|
+
"operator?": '"==" | "!=" | "<" | "<=" | ">" | ">=" | "in" | "not-in" | "array-contains" | "array-contains-any"',
|
|
252
|
+
"direction?": '"asc" | "desc"',
|
|
253
|
+
"fromHelper?": "string"
|
|
254
|
+
});
|
|
255
|
+
var ConstraintSequence = type({
|
|
256
|
+
"pathLabel?": "string",
|
|
257
|
+
entries: ConstraintSequenceEntry.array()
|
|
258
|
+
});
|
|
259
|
+
var DerivedIndexField = type({
|
|
260
|
+
fieldPath: "string",
|
|
261
|
+
"order?": '"ASCENDING" | "DESCENDING"',
|
|
262
|
+
"arrayConfig?": '"CONTAINS"'
|
|
263
|
+
});
|
|
264
|
+
var DerivedComposite = type({
|
|
265
|
+
collectionGroup: "string",
|
|
266
|
+
queryScope: '"COLLECTION" | "COLLECTION_GROUP"',
|
|
267
|
+
fields: DerivedIndexField.array(),
|
|
268
|
+
density: '"SPARSE_ALL" | "SPARSE_ANY" | "DENSE"'
|
|
269
|
+
});
|
|
270
|
+
var DerivedFieldOverrideVariant = type({
|
|
271
|
+
queryScope: '"COLLECTION" | "COLLECTION_GROUP"',
|
|
272
|
+
"order?": '"ASCENDING" | "DESCENDING"',
|
|
273
|
+
"arrayConfig?": '"CONTAINS"'
|
|
274
|
+
});
|
|
275
|
+
var DerivedFieldOverride = type({
|
|
276
|
+
collectionGroup: "string",
|
|
277
|
+
fieldPath: "string",
|
|
278
|
+
variants: DerivedFieldOverrideVariant.array()
|
|
279
|
+
});
|
|
280
|
+
var ModelFirebaseIndexParamEntry = type({
|
|
281
|
+
name: "string",
|
|
282
|
+
type: "string",
|
|
283
|
+
description: "string",
|
|
284
|
+
optional: "boolean"
|
|
285
|
+
});
|
|
286
|
+
var ModelFirebaseIndexEntry = type({
|
|
287
|
+
slug: "string",
|
|
288
|
+
name: "string",
|
|
289
|
+
module: "string",
|
|
290
|
+
subpath: "string",
|
|
291
|
+
signature: "string",
|
|
292
|
+
description: "string",
|
|
293
|
+
model: "string",
|
|
294
|
+
collection: "string",
|
|
295
|
+
isNested: "boolean",
|
|
296
|
+
scope: '"COLLECTION" | "COLLECTION_GROUP"',
|
|
297
|
+
manual: "boolean",
|
|
298
|
+
skip: "boolean",
|
|
299
|
+
"specOnly?": "boolean",
|
|
300
|
+
"excluded?": "boolean",
|
|
301
|
+
category: "string",
|
|
302
|
+
params: ModelFirebaseIndexParamEntry.array(),
|
|
303
|
+
returns: "string",
|
|
304
|
+
tags: "string[]",
|
|
305
|
+
constraintSequences: ConstraintSequence.array(),
|
|
306
|
+
derivedComposites: DerivedComposite.array(),
|
|
307
|
+
derivedFieldOverrides: DerivedFieldOverride.array(),
|
|
308
|
+
"example?": "string",
|
|
309
|
+
"relatedSlugs?": "string[]",
|
|
310
|
+
"skillRefs?": "string[]",
|
|
311
|
+
"deprecated?": "boolean | string",
|
|
312
|
+
"since?": "string"
|
|
313
|
+
});
|
|
314
|
+
var ModelFirebaseIndexManifest = type({
|
|
315
|
+
version: "1",
|
|
316
|
+
source: "string",
|
|
317
|
+
module: "string",
|
|
318
|
+
generatedAt: "string",
|
|
319
|
+
generator: "string",
|
|
320
|
+
entries: ModelFirebaseIndexEntry.array()
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
// packages/dbx-cli/firestore-indexes/src/model-firebase-index-analyze.ts
|
|
324
|
+
var EQUALITY_OPERATORS = /* @__PURE__ */ new Set(["==", "in"]);
|
|
325
|
+
var RANGE_OPERATORS = /* @__PURE__ */ new Set(["<", "<=", ">", ">=", "!=", "not-in"]);
|
|
326
|
+
var ARRAY_OPERATORS = /* @__PURE__ */ new Set(["array-contains", "array-contains-any"]);
|
|
327
|
+
function analyzeModelFirebaseIndexEntries(entries) {
|
|
328
|
+
const out = [];
|
|
329
|
+
for (const entry of entries) {
|
|
330
|
+
out.push(analyzeEntry(entry));
|
|
331
|
+
}
|
|
332
|
+
return out;
|
|
333
|
+
}
|
|
334
|
+
function analyzeEntry(entry) {
|
|
335
|
+
if (entry.skip || entry.manual || entry.excluded || entry.constraintSequences.length === 0) {
|
|
336
|
+
return {
|
|
337
|
+
extractedEntry: entry,
|
|
338
|
+
derivedComposites: [],
|
|
339
|
+
derivedFieldOverrides: [],
|
|
340
|
+
warnings: []
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
const composites = [];
|
|
344
|
+
const fieldOverrides = /* @__PURE__ */ new Map();
|
|
345
|
+
const warnings = [];
|
|
346
|
+
for (const sequence of entry.constraintSequences) {
|
|
347
|
+
const result = analyzeSequence({ sequence, collection: entry.collection, scope: entry.scope, factoryName: entry.name, allowArrayContainsAny: entry.allowArrayContainsAny });
|
|
348
|
+
for (const warning of result.warnings) {
|
|
349
|
+
warnings.push(warning);
|
|
350
|
+
}
|
|
351
|
+
if (result.kind === "composite") {
|
|
352
|
+
composites.push(result.composite);
|
|
353
|
+
} else if (result.kind === "fieldOverride") {
|
|
354
|
+
const key = result.fieldOverride.fieldPath;
|
|
355
|
+
const list = fieldOverrides.get(key) ?? [];
|
|
356
|
+
if (!list.some((v) => fieldOverrideVariantEquals(v, result.fieldOverride.variant))) {
|
|
357
|
+
list.push(result.fieldOverride.variant);
|
|
358
|
+
}
|
|
359
|
+
fieldOverrides.set(key, list);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
const derivedFieldOverrides = [];
|
|
363
|
+
for (const [fieldPath, variants] of fieldOverrides) {
|
|
364
|
+
derivedFieldOverrides.push({ collectionGroup: entry.collection, fieldPath, variants });
|
|
365
|
+
}
|
|
366
|
+
return {
|
|
367
|
+
extractedEntry: entry,
|
|
368
|
+
derivedComposites: composites,
|
|
369
|
+
derivedFieldOverrides,
|
|
370
|
+
warnings
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
function bucketConstraints(entries) {
|
|
374
|
+
const equalities = [];
|
|
375
|
+
const ranges = [];
|
|
376
|
+
const arrays = [];
|
|
377
|
+
const orderBys = [];
|
|
378
|
+
const seenEqualityFields = /* @__PURE__ */ new Set();
|
|
379
|
+
const seenRangeFields = /* @__PURE__ */ new Set();
|
|
380
|
+
const seenArrayFields = /* @__PURE__ */ new Set();
|
|
381
|
+
const distinctFields = /* @__PURE__ */ new Set();
|
|
382
|
+
for (const entry of entries) {
|
|
383
|
+
distinctFields.add(entry.fieldPath);
|
|
384
|
+
if (entry.kind === "orderBy") {
|
|
385
|
+
orderBys.push(entry);
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
const op = entry.operator ?? "==";
|
|
389
|
+
if (EQUALITY_OPERATORS.has(op)) {
|
|
390
|
+
if (!seenEqualityFields.has(entry.fieldPath)) {
|
|
391
|
+
seenEqualityFields.add(entry.fieldPath);
|
|
392
|
+
equalities.push(entry);
|
|
393
|
+
}
|
|
394
|
+
} else if (RANGE_OPERATORS.has(op)) {
|
|
395
|
+
if (!seenRangeFields.has(entry.fieldPath)) {
|
|
396
|
+
seenRangeFields.add(entry.fieldPath);
|
|
397
|
+
ranges.push(entry);
|
|
398
|
+
}
|
|
399
|
+
} else if (ARRAY_OPERATORS.has(op) && !seenArrayFields.has(entry.fieldPath)) {
|
|
400
|
+
seenArrayFields.add(entry.fieldPath);
|
|
401
|
+
arrays.push(entry);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
return {
|
|
405
|
+
equalities,
|
|
406
|
+
ranges,
|
|
407
|
+
arrays,
|
|
408
|
+
orderBys,
|
|
409
|
+
distinctFieldCount: distinctFields.size
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
function analyzeSequence(input) {
|
|
413
|
+
const { sequence, collection, scope, factoryName, allowArrayContainsAny } = input;
|
|
414
|
+
const buckets = bucketConstraints(sequence.entries);
|
|
415
|
+
const warnings = [];
|
|
416
|
+
if (buckets.ranges.length > 1) {
|
|
417
|
+
warnings.push({ kind: "multiple-range-fields", factoryName, fields: buckets.ranges.map((r) => r.fieldPath) });
|
|
418
|
+
}
|
|
419
|
+
if (!allowArrayContainsAny) {
|
|
420
|
+
for (const arrayEntry of buckets.arrays) {
|
|
421
|
+
if (arrayEntry.operator === "array-contains-any") {
|
|
422
|
+
warnings.push({ kind: "unsupported-array-contains-any", factoryName, field: arrayEntry.fieldPath });
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
if (buckets.distinctFieldCount === 0) {
|
|
427
|
+
return { kind: "auto", warnings };
|
|
428
|
+
}
|
|
429
|
+
if (isSingleFieldQuery(buckets)) {
|
|
430
|
+
return analyzeSingleField({ buckets, collection, scope, warnings });
|
|
431
|
+
}
|
|
432
|
+
return analyzeMultiField({ buckets, sequenceEntries: sequence.entries, collection, scope, factoryName, warnings });
|
|
433
|
+
}
|
|
434
|
+
function isSingleFieldQuery(buckets) {
|
|
435
|
+
return buckets.distinctFieldCount === 1;
|
|
436
|
+
}
|
|
437
|
+
function analyzeSingleField(input) {
|
|
438
|
+
const { buckets, collection, scope, warnings } = input;
|
|
439
|
+
if (scope === "COLLECTION") {
|
|
440
|
+
return { kind: "auto", warnings };
|
|
441
|
+
}
|
|
442
|
+
const fieldPath = pickFirstFieldFromBuckets(buckets);
|
|
443
|
+
if (fieldPath === void 0) {
|
|
444
|
+
return { kind: "auto", warnings };
|
|
445
|
+
}
|
|
446
|
+
let variant;
|
|
447
|
+
if (buckets.arrays.length === 1) {
|
|
448
|
+
variant = { queryScope: "COLLECTION_GROUP", arrayConfig: "CONTAINS" };
|
|
449
|
+
} else if (buckets.orderBys.length === 1) {
|
|
450
|
+
const direction = buckets.orderBys[0].direction ?? "asc";
|
|
451
|
+
variant = { queryScope: "COLLECTION_GROUP", order: orderForDirection(direction) };
|
|
452
|
+
} else {
|
|
453
|
+
variant = { queryScope: "COLLECTION_GROUP", order: "ASCENDING" };
|
|
454
|
+
}
|
|
455
|
+
void collection;
|
|
456
|
+
return { kind: "fieldOverride", fieldOverride: { fieldPath, variant }, warnings };
|
|
457
|
+
}
|
|
458
|
+
function pickFirstFieldFromBuckets(buckets) {
|
|
459
|
+
let result;
|
|
460
|
+
if (buckets.equalities.length > 0) {
|
|
461
|
+
result = buckets.equalities[0].fieldPath;
|
|
462
|
+
} else if (buckets.ranges.length > 0) {
|
|
463
|
+
result = buckets.ranges[0].fieldPath;
|
|
464
|
+
} else if (buckets.arrays.length > 0) {
|
|
465
|
+
result = buckets.arrays[0].fieldPath;
|
|
466
|
+
} else if (buckets.orderBys.length > 0) {
|
|
467
|
+
result = buckets.orderBys[0].fieldPath;
|
|
468
|
+
}
|
|
469
|
+
return result;
|
|
470
|
+
}
|
|
471
|
+
function analyzeMultiField(input) {
|
|
472
|
+
const { buckets, collection, scope, factoryName, sequenceEntries, warnings } = input;
|
|
473
|
+
const fields = [];
|
|
474
|
+
const placedFields = /* @__PURE__ */ new Set();
|
|
475
|
+
const orderByByField = collectOrderByDirections({ orderBys: buckets.orderBys, factoryName, warnings });
|
|
476
|
+
for (const entry of sequenceEntries) {
|
|
477
|
+
if (entry.kind !== "where") {
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
480
|
+
if (placedFields.has(entry.fieldPath)) {
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
const op = entry.operator ?? "==";
|
|
484
|
+
if (EQUALITY_OPERATORS.has(op)) {
|
|
485
|
+
fields.push({ fieldPath: entry.fieldPath, order: "ASCENDING" });
|
|
486
|
+
placedFields.add(entry.fieldPath);
|
|
487
|
+
} else if (RANGE_OPERATORS.has(op)) {
|
|
488
|
+
const direction = orderByByField.get(entry.fieldPath) ?? "asc";
|
|
489
|
+
fields.push({ fieldPath: entry.fieldPath, order: orderForDirection(direction) });
|
|
490
|
+
placedFields.add(entry.fieldPath);
|
|
491
|
+
} else if (ARRAY_OPERATORS.has(op)) {
|
|
492
|
+
fields.push({ fieldPath: entry.fieldPath, arrayConfig: "CONTAINS" });
|
|
493
|
+
placedFields.add(entry.fieldPath);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
for (const orderBy of buckets.orderBys) {
|
|
497
|
+
if (placedFields.has(orderBy.fieldPath)) {
|
|
498
|
+
continue;
|
|
499
|
+
}
|
|
500
|
+
const direction = orderBy.direction ?? "asc";
|
|
501
|
+
fields.push({ fieldPath: orderBy.fieldPath, order: orderForDirection(direction) });
|
|
502
|
+
placedFields.add(orderBy.fieldPath);
|
|
503
|
+
}
|
|
504
|
+
if (fields.length < 2) {
|
|
505
|
+
return { kind: "auto", warnings };
|
|
506
|
+
}
|
|
507
|
+
const composite = {
|
|
508
|
+
collectionGroup: collection,
|
|
509
|
+
queryScope: scope,
|
|
510
|
+
fields,
|
|
511
|
+
density: DEFAULT_FIRESTORE_INDEX_DENSITY
|
|
512
|
+
};
|
|
513
|
+
return { kind: "composite", composite, warnings };
|
|
514
|
+
}
|
|
515
|
+
function collectOrderByDirections(input) {
|
|
516
|
+
const { orderBys, factoryName, warnings } = input;
|
|
517
|
+
const out = /* @__PURE__ */ new Map();
|
|
518
|
+
const conflicts = /* @__PURE__ */ new Map();
|
|
519
|
+
for (const orderBy of orderBys) {
|
|
520
|
+
const direction = orderBy.direction ?? "asc";
|
|
521
|
+
const existing = out.get(orderBy.fieldPath);
|
|
522
|
+
if (existing === void 0) {
|
|
523
|
+
out.set(orderBy.fieldPath, direction);
|
|
524
|
+
} else if (existing !== direction) {
|
|
525
|
+
const set = conflicts.get(orderBy.fieldPath) ?? /* @__PURE__ */ new Set([existing]);
|
|
526
|
+
set.add(direction);
|
|
527
|
+
conflicts.set(orderBy.fieldPath, set);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
for (const [field, dirs] of conflicts) {
|
|
531
|
+
warnings.push({ kind: "orderby-conflict", factoryName, field, directions: [...dirs] });
|
|
532
|
+
}
|
|
533
|
+
return out;
|
|
534
|
+
}
|
|
535
|
+
function orderForDirection(direction) {
|
|
536
|
+
return direction === "desc" ? "DESCENDING" : "ASCENDING";
|
|
537
|
+
}
|
|
538
|
+
function fieldOverrideVariantEquals(a, b) {
|
|
539
|
+
return a.queryScope === b.queryScope && a.order === b.order && a.arrayConfig === b.arrayConfig;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// packages/dbx-cli/firestore-indexes/src/model-firebase-index-extract.ts
|
|
543
|
+
import { Node as Node2, SyntaxKind as SyntaxKind2 } from "ts-morph";
|
|
544
|
+
|
|
545
|
+
// packages/dbx-cli/firestore-indexes/src/firestore-query-helpers.ts
|
|
546
|
+
var FIRESTORE_QUERY_HELPERS = [
|
|
547
|
+
// Inequality helpers — single base where, no orderBy.
|
|
548
|
+
{ name: "whereDateIsBefore", fieldArgIndex: 0, parts: [{ kind: "where", operator: "<" }] },
|
|
549
|
+
{ name: "whereDateIsAfter", fieldArgIndex: 0, parts: [{ kind: "where", operator: ">" }] },
|
|
550
|
+
{ name: "whereDateIsOnOrBefore", fieldArgIndex: 0, parts: [{ kind: "where", operator: "<=" }] },
|
|
551
|
+
{ name: "whereDateIsOnOrAfter", fieldArgIndex: 0, parts: [{ kind: "where", operator: ">=" }] },
|
|
552
|
+
// *WithSort variants — base where + orderBy on the same field. Helper
|
|
553
|
+
// emits orderBy first, then where. Matches `constraint.template.ts`.
|
|
554
|
+
{
|
|
555
|
+
name: "whereDateIsBeforeWithSort",
|
|
556
|
+
fieldArgIndex: 0,
|
|
557
|
+
directionArgIndex: 2,
|
|
558
|
+
defaultDirection: "asc",
|
|
559
|
+
parts: [
|
|
560
|
+
{ kind: "orderBy", useCallSiteDirection: true, direction: "asc" },
|
|
561
|
+
{ kind: "where", operator: "<" }
|
|
562
|
+
]
|
|
563
|
+
},
|
|
564
|
+
{
|
|
565
|
+
name: "whereDateIsAfterWithSort",
|
|
566
|
+
fieldArgIndex: 0,
|
|
567
|
+
directionArgIndex: 2,
|
|
568
|
+
defaultDirection: "asc",
|
|
569
|
+
parts: [
|
|
570
|
+
{ kind: "orderBy", useCallSiteDirection: true, direction: "asc" },
|
|
571
|
+
{ kind: "where", operator: ">" }
|
|
572
|
+
]
|
|
573
|
+
},
|
|
574
|
+
{
|
|
575
|
+
name: "whereDateIsOnOrBeforeWithSort",
|
|
576
|
+
fieldArgIndex: 0,
|
|
577
|
+
directionArgIndex: 2,
|
|
578
|
+
defaultDirection: "asc",
|
|
579
|
+
parts: [
|
|
580
|
+
{ kind: "orderBy", useCallSiteDirection: true, direction: "asc" },
|
|
581
|
+
{ kind: "where", operator: "<=" }
|
|
582
|
+
]
|
|
583
|
+
},
|
|
584
|
+
{
|
|
585
|
+
name: "whereDateIsOnOrAfterWithSort",
|
|
586
|
+
fieldArgIndex: 0,
|
|
587
|
+
directionArgIndex: 2,
|
|
588
|
+
defaultDirection: "asc",
|
|
589
|
+
parts: [
|
|
590
|
+
{ kind: "orderBy", useCallSiteDirection: true, direction: "asc" },
|
|
591
|
+
{ kind: "where", operator: ">=" }
|
|
592
|
+
]
|
|
593
|
+
},
|
|
594
|
+
// Range helpers — two-bound where + orderBy.
|
|
595
|
+
{
|
|
596
|
+
name: "whereDateIsBetween",
|
|
597
|
+
fieldArgIndex: 0,
|
|
598
|
+
directionArgIndex: 2,
|
|
599
|
+
defaultDirection: "asc",
|
|
600
|
+
parts: [
|
|
601
|
+
{ kind: "where", operator: ">=" },
|
|
602
|
+
{ kind: "where", operator: "<=" },
|
|
603
|
+
{ kind: "orderBy", useCallSiteDirection: true, direction: "asc" }
|
|
604
|
+
]
|
|
605
|
+
},
|
|
606
|
+
{
|
|
607
|
+
name: "whereDateIsInRange",
|
|
608
|
+
fieldArgIndex: 0,
|
|
609
|
+
directionArgIndex: 2,
|
|
610
|
+
defaultDirection: "asc",
|
|
611
|
+
parts: [
|
|
612
|
+
{ kind: "where", operator: ">=" },
|
|
613
|
+
{ kind: "where", operator: "<=" },
|
|
614
|
+
{ kind: "orderBy", useCallSiteDirection: true, direction: "asc" }
|
|
615
|
+
]
|
|
616
|
+
},
|
|
617
|
+
// Dynamic single-field helper. The body branches on which DateRange bounds
|
|
618
|
+
// are present, but every branch operates on the same fieldPath argument and
|
|
619
|
+
// produces some subset of orderBy(field) + where(field,'>=') + where(field,'<=').
|
|
620
|
+
// For Firestore composite-index purposes the worst-case set collapses to a
|
|
621
|
+
// single ordered range index on that field — so the descriptor emits the
|
|
622
|
+
// worst case unconditionally. The body's internal conditionals are NOT
|
|
623
|
+
// scanned (helpers are opaque to the extractor; only tagged callers are).
|
|
624
|
+
{
|
|
625
|
+
name: "filterWithDateRange",
|
|
626
|
+
fieldArgIndex: 0,
|
|
627
|
+
directionArgIndex: 2,
|
|
628
|
+
defaultDirection: "asc",
|
|
629
|
+
parts: [
|
|
630
|
+
{ kind: "where", operator: ">=" },
|
|
631
|
+
{ kind: "where", operator: "<=" },
|
|
632
|
+
{ kind: "orderBy", useCallSiteDirection: true, direction: "asc" }
|
|
633
|
+
]
|
|
634
|
+
},
|
|
635
|
+
// Prefix-string and child-doc helpers — range where + orderBy.
|
|
636
|
+
{
|
|
637
|
+
name: "whereStringValueHasPrefix",
|
|
638
|
+
fieldArgIndex: 0,
|
|
639
|
+
directionArgIndex: 2,
|
|
640
|
+
defaultDirection: "asc",
|
|
641
|
+
parts: [
|
|
642
|
+
{ kind: "where", operator: ">=" },
|
|
643
|
+
{ kind: "where", operator: "<" },
|
|
644
|
+
{ kind: "orderBy", useCallSiteDirection: true, direction: "asc" }
|
|
645
|
+
]
|
|
646
|
+
},
|
|
647
|
+
{
|
|
648
|
+
name: "allChildDocumentsUnderRelativePath",
|
|
649
|
+
fieldArgIndex: 0,
|
|
650
|
+
directionArgIndex: 2,
|
|
651
|
+
defaultDirection: "asc",
|
|
652
|
+
parts: [
|
|
653
|
+
{ kind: "where", operator: ">=" },
|
|
654
|
+
{ kind: "where", operator: "<" },
|
|
655
|
+
{ kind: "orderBy", useCallSiteDirection: true, direction: "asc" }
|
|
656
|
+
]
|
|
657
|
+
}
|
|
658
|
+
];
|
|
659
|
+
function getFirestoreQueryHelperDescriptor(name) {
|
|
660
|
+
return FIRESTORE_QUERY_HELPERS.find((h) => h.name === name);
|
|
661
|
+
}
|
|
662
|
+
function expandFirestoreQueryHelper(input) {
|
|
663
|
+
const { descriptor, fieldPath, direction } = input;
|
|
664
|
+
const resolvedDirection = direction ?? descriptor.defaultDirection;
|
|
665
|
+
const out = [];
|
|
666
|
+
for (const part of descriptor.parts) {
|
|
667
|
+
if (part.kind === "where") {
|
|
668
|
+
const operator = part.operator ?? "==";
|
|
669
|
+
out.push({ kind: "where", fieldPath, operator, fromHelper: descriptor.name });
|
|
670
|
+
} else {
|
|
671
|
+
const direction2 = part.useCallSiteDirection && resolvedDirection !== void 0 ? resolvedDirection : part.direction ?? "asc";
|
|
672
|
+
out.push({ kind: "orderBy", fieldPath, direction: direction2, fromHelper: descriptor.name });
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
return out;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// packages/dbx-cli/src/lib/scan-helpers/scan-extract-utils.ts
|
|
679
|
+
import { Node, SyntaxKind } from "ts-morph";
|
|
680
|
+
function unwrapFenced(text) {
|
|
681
|
+
const trimmed = text.trim();
|
|
682
|
+
const match = /^```[a-zA-Z]*\n([\s\S]*?)\n```\s*$/.exec(trimmed);
|
|
683
|
+
return match ? match[1] : trimmed;
|
|
684
|
+
}
|
|
685
|
+
function splitListTagText(text) {
|
|
686
|
+
const out = [];
|
|
687
|
+
for (const piece of text.split(/[\s,]+/)) {
|
|
688
|
+
const trimmed = piece.trim();
|
|
689
|
+
if (trimmed.length > 0) {
|
|
690
|
+
out.push(trimmed);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
return out;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// packages/dbx-cli/firestore-indexes/src/model-firebase-index-extract.ts
|
|
697
|
+
var INDEX_MARKER = "dbxModelFirebaseIndex";
|
|
698
|
+
var INDEX_DISPATCHER_TAG = "dbxModelFirebaseIndexDispatcher";
|
|
699
|
+
var INDEX_MODEL_TAG = "dbxModelFirebaseIndexModel";
|
|
700
|
+
var INDEX_SCOPE_TAG = "dbxModelFirebaseIndexScope";
|
|
701
|
+
var INDEX_MANUAL_TAG = "dbxModelFirebaseIndexManual";
|
|
702
|
+
var INDEX_SKIP_TAG = "dbxModelFirebaseIndexSkip";
|
|
703
|
+
var INDEX_SPEC_FILES_ONLY_TAG = "dbxModelFirebaseIndexSpecFilesOnly";
|
|
704
|
+
var INDEX_EXCLUDE_TAG = "dbxModelFirebaseIndexExclude";
|
|
705
|
+
var INDEX_ALLOW_ARRAY_CONTAINS_ANY_TAG = "dbxModelFirebaseIndexAllowArrayContainsAny";
|
|
706
|
+
var INDEX_CATEGORY_TAG = "dbxModelFirebaseIndexCategory";
|
|
707
|
+
var INDEX_TAGS_TAG = "dbxModelFirebaseIndexTags";
|
|
708
|
+
var INDEX_RELATED_TAG = "dbxModelFirebaseIndexRelated";
|
|
709
|
+
var INDEX_SKILL_REFS_TAG = "dbxModelFirebaseIndexSkillRefs";
|
|
710
|
+
var INDEX_SLUG_TAG = "dbxModelFirebaseIndexSlug";
|
|
711
|
+
var INDEX_PATH_TAG = "dbxModelFirebaseIndexPath";
|
|
712
|
+
var COMPLEX_BODY_SYNTAX_KINDS = /* @__PURE__ */ new Map([
|
|
713
|
+
[SyntaxKind2.IfStatement, "if"],
|
|
714
|
+
[SyntaxKind2.SwitchStatement, "switch"],
|
|
715
|
+
[SyntaxKind2.ConditionalExpression, "ternary"],
|
|
716
|
+
[SyntaxKind2.ForStatement, "loop"],
|
|
717
|
+
[SyntaxKind2.ForOfStatement, "loop"],
|
|
718
|
+
[SyntaxKind2.ForInStatement, "loop"],
|
|
719
|
+
[SyntaxKind2.WhileStatement, "loop"],
|
|
720
|
+
[SyntaxKind2.DoStatement, "loop"]
|
|
721
|
+
]);
|
|
722
|
+
var VALID_SCOPE_TAG_VALUES = /* @__PURE__ */ new Set(["COLLECTION", "COLLECTION_GROUP"]);
|
|
723
|
+
var TRUE_TAG_VALUES = /* @__PURE__ */ new Set(["", "true", "yes"]);
|
|
724
|
+
var FALSE_TAG_VALUES = /* @__PURE__ */ new Set(["false", "no"]);
|
|
725
|
+
var WHERE_OPERATOR_SET = new Set(FIRESTORE_WHERE_OPERATORS);
|
|
726
|
+
function extractModelFirebaseIndexEntries(input) {
|
|
727
|
+
const { project, identityResolver } = input;
|
|
728
|
+
const entries = [];
|
|
729
|
+
const warnings = [];
|
|
730
|
+
const slugProvenance = /* @__PURE__ */ new Map();
|
|
731
|
+
for (const sourceFile of project.getSourceFiles()) {
|
|
732
|
+
const filePath = sourceFile.getFilePath();
|
|
733
|
+
const candidates = collectTaggedFunctions(sourceFile);
|
|
734
|
+
for (const candidate of candidates) {
|
|
735
|
+
const built = buildEntry({ candidate, filePath, identityResolver });
|
|
736
|
+
if (built.kind === "ok") {
|
|
737
|
+
const previous = slugProvenance.get(built.entry.slug);
|
|
738
|
+
if (previous === void 0) {
|
|
739
|
+
slugProvenance.set(built.entry.slug, { name: built.entry.name, filePath, line: built.entry.line });
|
|
740
|
+
entries.push(built.entry);
|
|
741
|
+
} else {
|
|
742
|
+
warnings.push({ kind: "duplicate-slug", severity: "warning", name: built.entry.name, slug: built.entry.slug, previousName: previous.name, filePath, line: built.entry.line });
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
for (const warning of built.warnings) {
|
|
746
|
+
warnings.push(warning);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
return { entries, warnings };
|
|
751
|
+
}
|
|
752
|
+
function collectTaggedFunctions(sourceFile) {
|
|
753
|
+
const out = [];
|
|
754
|
+
for (const decl of sourceFile.getFunctions()) {
|
|
755
|
+
if (!decl.isExported()) {
|
|
756
|
+
continue;
|
|
757
|
+
}
|
|
758
|
+
const jsDocs = findTaggedDocs(decl.getJsDocs());
|
|
759
|
+
if (jsDocs.length > 0) {
|
|
760
|
+
out.push({ decl, jsDocs });
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
return out;
|
|
764
|
+
}
|
|
765
|
+
function findTaggedDocs(jsDocs) {
|
|
766
|
+
let hasMarker = false;
|
|
767
|
+
for (const doc of jsDocs) {
|
|
768
|
+
for (const tag of doc.getTags()) {
|
|
769
|
+
if (tag.getTagName() === INDEX_MARKER) {
|
|
770
|
+
hasMarker = true;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
return hasMarker ? jsDocs : [];
|
|
775
|
+
}
|
|
776
|
+
function readJsDocTags(jsDocs) {
|
|
777
|
+
const state = {
|
|
778
|
+
summaries: [],
|
|
779
|
+
slug: void 0,
|
|
780
|
+
model: void 0,
|
|
781
|
+
scope: void 0,
|
|
782
|
+
manual: false,
|
|
783
|
+
skip: false,
|
|
784
|
+
specOnly: false,
|
|
785
|
+
excluded: false,
|
|
786
|
+
dispatcher: false,
|
|
787
|
+
allowArrayContainsAny: false,
|
|
788
|
+
category: void 0,
|
|
789
|
+
explicitTags: [],
|
|
790
|
+
relatedSlugs: [],
|
|
791
|
+
skillRefs: [],
|
|
792
|
+
examples: [],
|
|
793
|
+
paramDescriptions: /* @__PURE__ */ new Map(),
|
|
794
|
+
returnsText: void 0,
|
|
795
|
+
deprecated: void 0,
|
|
796
|
+
since: void 0,
|
|
797
|
+
paths: []
|
|
798
|
+
};
|
|
799
|
+
for (const jsDoc of jsDocs) {
|
|
800
|
+
const description = jsDoc.getDescription().trim();
|
|
801
|
+
if (description.length > 0) {
|
|
802
|
+
state.summaries.push(description);
|
|
803
|
+
}
|
|
804
|
+
for (const tag of jsDoc.getTags()) {
|
|
805
|
+
const tagName = tag.getTagName();
|
|
806
|
+
const text = tag.getCommentText()?.trim() ?? "";
|
|
807
|
+
if (tagName === "param") {
|
|
808
|
+
const paramName = tag.getName?.() ?? extractParamName(tag.getText());
|
|
809
|
+
if (paramName !== void 0 && paramName.length > 0) {
|
|
810
|
+
state.paramDescriptions.set(paramName, text);
|
|
811
|
+
}
|
|
812
|
+
} else {
|
|
813
|
+
applyTag(state, tagName, text);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
return {
|
|
818
|
+
summary: state.summaries.join("\n\n"),
|
|
819
|
+
slug: state.slug,
|
|
820
|
+
model: state.model,
|
|
821
|
+
scope: state.scope,
|
|
822
|
+
manual: state.manual,
|
|
823
|
+
skip: state.skip,
|
|
824
|
+
specOnly: state.specOnly,
|
|
825
|
+
excluded: state.excluded,
|
|
826
|
+
dispatcher: state.dispatcher,
|
|
827
|
+
allowArrayContainsAny: state.allowArrayContainsAny,
|
|
828
|
+
category: state.category,
|
|
829
|
+
explicitTags: state.explicitTags,
|
|
830
|
+
relatedSlugs: state.relatedSlugs,
|
|
831
|
+
skillRefs: state.skillRefs,
|
|
832
|
+
examples: state.examples,
|
|
833
|
+
paramDescriptions: state.paramDescriptions,
|
|
834
|
+
returnsText: state.returnsText,
|
|
835
|
+
deprecated: state.deprecated,
|
|
836
|
+
since: state.since,
|
|
837
|
+
paths: state.paths.map((p) => [...p])
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
function applyTag(state, name, text) {
|
|
841
|
+
switch (name) {
|
|
842
|
+
case INDEX_MARKER:
|
|
843
|
+
break;
|
|
844
|
+
case INDEX_SLUG_TAG:
|
|
845
|
+
state.slug = text;
|
|
846
|
+
break;
|
|
847
|
+
case INDEX_MODEL_TAG:
|
|
848
|
+
state.model = text;
|
|
849
|
+
break;
|
|
850
|
+
case INDEX_SCOPE_TAG:
|
|
851
|
+
state.scope = text;
|
|
852
|
+
break;
|
|
853
|
+
case INDEX_MANUAL_TAG:
|
|
854
|
+
state.manual = parseBooleanTag(text) ?? true;
|
|
855
|
+
break;
|
|
856
|
+
case INDEX_SKIP_TAG:
|
|
857
|
+
state.skip = parseBooleanTag(text) ?? true;
|
|
858
|
+
break;
|
|
859
|
+
case INDEX_SPEC_FILES_ONLY_TAG:
|
|
860
|
+
state.specOnly = parseBooleanTag(text) ?? true;
|
|
861
|
+
break;
|
|
862
|
+
case INDEX_EXCLUDE_TAG:
|
|
863
|
+
state.excluded = parseBooleanTag(text) ?? true;
|
|
864
|
+
break;
|
|
865
|
+
case INDEX_DISPATCHER_TAG:
|
|
866
|
+
state.dispatcher = parseBooleanTag(text) ?? true;
|
|
867
|
+
break;
|
|
868
|
+
case INDEX_ALLOW_ARRAY_CONTAINS_ANY_TAG:
|
|
869
|
+
state.allowArrayContainsAny = parseBooleanTag(text) ?? true;
|
|
870
|
+
break;
|
|
871
|
+
case INDEX_CATEGORY_TAG:
|
|
872
|
+
state.category = text;
|
|
873
|
+
break;
|
|
874
|
+
case INDEX_TAGS_TAG:
|
|
875
|
+
for (const tag of splitListTagText(text)) {
|
|
876
|
+
state.explicitTags.push(tag.toLowerCase());
|
|
877
|
+
}
|
|
878
|
+
break;
|
|
879
|
+
case INDEX_RELATED_TAG:
|
|
880
|
+
for (const slug of splitListTagText(text)) {
|
|
881
|
+
state.relatedSlugs.push(slug);
|
|
882
|
+
}
|
|
883
|
+
break;
|
|
884
|
+
case INDEX_SKILL_REFS_TAG:
|
|
885
|
+
for (const ref of splitListTagText(text)) {
|
|
886
|
+
state.skillRefs.push(ref);
|
|
887
|
+
}
|
|
888
|
+
break;
|
|
889
|
+
case INDEX_PATH_TAG: {
|
|
890
|
+
const fields = [...splitListTagText(text)];
|
|
891
|
+
if (fields.length > 0) {
|
|
892
|
+
state.paths.push(fields);
|
|
893
|
+
}
|
|
894
|
+
break;
|
|
895
|
+
}
|
|
896
|
+
case "example":
|
|
897
|
+
state.examples.push(unwrapFenced(text));
|
|
898
|
+
break;
|
|
899
|
+
case "returns":
|
|
900
|
+
case "return":
|
|
901
|
+
state.returnsText = text;
|
|
902
|
+
break;
|
|
903
|
+
case "deprecated":
|
|
904
|
+
state.deprecated = text.length > 0 ? text : true;
|
|
905
|
+
break;
|
|
906
|
+
case "since":
|
|
907
|
+
if (text.length > 0) {
|
|
908
|
+
state.since = text;
|
|
909
|
+
}
|
|
910
|
+
break;
|
|
911
|
+
default:
|
|
912
|
+
break;
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
function parseBooleanTag(text) {
|
|
916
|
+
const lowered = text.trim().toLowerCase();
|
|
917
|
+
let result;
|
|
918
|
+
if (TRUE_TAG_VALUES.has(lowered)) {
|
|
919
|
+
result = true;
|
|
920
|
+
} else if (FALSE_TAG_VALUES.has(lowered)) {
|
|
921
|
+
result = false;
|
|
922
|
+
}
|
|
923
|
+
return result;
|
|
924
|
+
}
|
|
925
|
+
function extractParamName(rawTag) {
|
|
926
|
+
const match = /@param\s+(?:\{[^}]*\}\s+)?(\S+)/.exec(rawTag);
|
|
927
|
+
return match?.[1];
|
|
928
|
+
}
|
|
929
|
+
function buildEntry(input) {
|
|
930
|
+
const { candidate, filePath, identityResolver } = input;
|
|
931
|
+
const warnings = [];
|
|
932
|
+
const name = candidate.decl.getName();
|
|
933
|
+
const line = candidate.decl.getStartLineNumber();
|
|
934
|
+
if (name === void 0 || name.length === 0) {
|
|
935
|
+
warnings.push({ kind: "missing-name", severity: "warning", filePath, line });
|
|
936
|
+
return { kind: "skipped", warnings };
|
|
937
|
+
}
|
|
938
|
+
const tags = readJsDocTags(candidate.jsDocs);
|
|
939
|
+
const resolved = resolveIdentity({ tags, identityResolver, name, filePath, line, warnings });
|
|
940
|
+
if (resolved === void 0) {
|
|
941
|
+
return { kind: "skipped", warnings };
|
|
942
|
+
}
|
|
943
|
+
const scope = resolveScope({ tags, resolved, name, filePath, line, warnings });
|
|
944
|
+
if (scope === void 0) {
|
|
945
|
+
return { kind: "skipped", warnings };
|
|
946
|
+
}
|
|
947
|
+
const modelTag = tags.model;
|
|
948
|
+
const entry = composeEntry({ candidate, tags, modelTag, resolved, scope, name, line, filePath, warnings });
|
|
949
|
+
return { kind: "ok", entry, warnings };
|
|
950
|
+
}
|
|
951
|
+
function resolveIdentity(input) {
|
|
952
|
+
const { tags, identityResolver, name, filePath, line, warnings } = input;
|
|
953
|
+
if (tags.model === void 0 || tags.model.length === 0) {
|
|
954
|
+
warnings.push({ kind: "missing-model-tag", severity: "warning", name, filePath, line });
|
|
955
|
+
return void 0;
|
|
956
|
+
}
|
|
957
|
+
const resolved = identityResolver.lookupByTypeName(tags.model);
|
|
958
|
+
if (resolved === void 0) {
|
|
959
|
+
warnings.push({ kind: "unresolved-model", severity: "warning", name, model: tags.model, filePath, line });
|
|
960
|
+
return void 0;
|
|
961
|
+
}
|
|
962
|
+
return resolved;
|
|
963
|
+
}
|
|
964
|
+
function composeEntry(input) {
|
|
965
|
+
const { candidate, tags, modelTag, resolved, scope, name, line, filePath, warnings } = input;
|
|
966
|
+
const slug = tags.slug && tags.slug.length > 0 ? tags.slug : toKebabCase(name);
|
|
967
|
+
const params = collectParams(candidate.decl, tags.paramDescriptions);
|
|
968
|
+
const returnType = candidate.decl.getReturnTypeNode()?.getText() ?? candidate.decl.getReturnType().getText();
|
|
969
|
+
const paramSignature = params.map((p) => `${p.name}${p.optional ? "?" : ""}: ${p.type}`).join(", ");
|
|
970
|
+
const returnSignature = returnType.length > 0 ? returnType : "unknown";
|
|
971
|
+
const signature = `${name}(${paramSignature}): ${returnSignature}`;
|
|
972
|
+
const returns = tags.returnsText && tags.returnsText.length > 0 ? tags.returnsText : returnType;
|
|
973
|
+
const example = tags.examples.length > 0 ? tags.examples[0] : "";
|
|
974
|
+
const category = tags.category && tags.category.length > 0 ? tags.category : "misc";
|
|
975
|
+
const tagSet = buildTagSet({ name, slug, summary: tags.summary, explicit: tags.explicitTags, category, model: modelTag });
|
|
976
|
+
const { constraintSequences, dispatcherDelegates } = resolveConstraintSequences({ candidate, tags, name, line, filePath, warnings });
|
|
977
|
+
if (tags.excluded) {
|
|
978
|
+
warnings.push({ kind: "excluded-factory", severity: "warning", name, filePath, line });
|
|
979
|
+
}
|
|
980
|
+
return {
|
|
981
|
+
slug,
|
|
982
|
+
name,
|
|
983
|
+
model: modelTag,
|
|
984
|
+
collection: resolved.collection,
|
|
985
|
+
isNested: resolved.isNested,
|
|
986
|
+
scope,
|
|
987
|
+
manual: tags.manual,
|
|
988
|
+
skip: tags.skip,
|
|
989
|
+
specOnly: tags.specOnly,
|
|
990
|
+
excluded: tags.excluded,
|
|
991
|
+
dispatcher: tags.dispatcher,
|
|
992
|
+
dispatcherDelegates,
|
|
993
|
+
allowArrayContainsAny: tags.allowArrayContainsAny,
|
|
994
|
+
category,
|
|
995
|
+
signature,
|
|
996
|
+
description: tags.summary,
|
|
997
|
+
params,
|
|
998
|
+
returns,
|
|
999
|
+
tags: tagSet,
|
|
1000
|
+
...tags.relatedSlugs.length > 0 ? { relatedSlugs: tags.relatedSlugs } : {},
|
|
1001
|
+
...tags.skillRefs.length > 0 ? { skillRefs: tags.skillRefs } : {},
|
|
1002
|
+
example,
|
|
1003
|
+
constraintSequences,
|
|
1004
|
+
...tags.deprecated === void 0 ? {} : { deprecated: tags.deprecated },
|
|
1005
|
+
...tags.since === void 0 ? {} : { since: tags.since },
|
|
1006
|
+
filePath,
|
|
1007
|
+
line
|
|
1008
|
+
};
|
|
1009
|
+
}
|
|
1010
|
+
function resolveConstraintSequences(input) {
|
|
1011
|
+
const { candidate, tags, name, line, filePath, warnings } = input;
|
|
1012
|
+
const bodyResult = extractConstraintsFromBody({ decl: candidate.decl, factoryName: name, filePath, dispatcher: tags.dispatcher });
|
|
1013
|
+
for (const warning of bodyResult.warnings) {
|
|
1014
|
+
warnings.push(warning);
|
|
1015
|
+
}
|
|
1016
|
+
if (tags.skip || tags.specOnly || tags.dispatcher || bodyResult.skipped) {
|
|
1017
|
+
return { constraintSequences: [], dispatcherDelegates: bodyResult.dispatcherDelegates };
|
|
1018
|
+
}
|
|
1019
|
+
const constraintSequences = buildConstraintSequences({
|
|
1020
|
+
bodyEntries: bodyResult.entries,
|
|
1021
|
+
conditionalFields: bodyResult.conditionalFields,
|
|
1022
|
+
paths: tags.paths,
|
|
1023
|
+
factoryName: name,
|
|
1024
|
+
filePath,
|
|
1025
|
+
line,
|
|
1026
|
+
warnings
|
|
1027
|
+
});
|
|
1028
|
+
return { constraintSequences, dispatcherDelegates: [] };
|
|
1029
|
+
}
|
|
1030
|
+
function resolveScope(input) {
|
|
1031
|
+
const { tags, resolved, name, filePath, line, warnings } = input;
|
|
1032
|
+
let result;
|
|
1033
|
+
if (tags.scope === void 0 || tags.scope.length === 0) {
|
|
1034
|
+
result = resolved.isNested ? "COLLECTION_GROUP" : "COLLECTION";
|
|
1035
|
+
} else if (VALID_SCOPE_TAG_VALUES.has(tags.scope)) {
|
|
1036
|
+
result = tags.scope;
|
|
1037
|
+
} else {
|
|
1038
|
+
warnings.push({ kind: "unsupported-scope", severity: "warning", name, scope: tags.scope, filePath, line });
|
|
1039
|
+
result = void 0;
|
|
1040
|
+
}
|
|
1041
|
+
return result;
|
|
1042
|
+
}
|
|
1043
|
+
function collectParams(decl, descriptions) {
|
|
1044
|
+
const params = decl.getParameters();
|
|
1045
|
+
const out = [];
|
|
1046
|
+
for (const param of params) {
|
|
1047
|
+
const name = param.getName();
|
|
1048
|
+
const typeNode = param.getTypeNode()?.getText();
|
|
1049
|
+
const type4 = typeNode ?? param.getType().getText() ?? "unknown";
|
|
1050
|
+
const description = descriptions.get(name) ?? "";
|
|
1051
|
+
const optional = param.isOptional() || param.hasInitializer();
|
|
1052
|
+
out.push({ name, type: type4, description, optional });
|
|
1053
|
+
}
|
|
1054
|
+
return out;
|
|
1055
|
+
}
|
|
1056
|
+
function buildConstraintSequences(input) {
|
|
1057
|
+
const { bodyEntries, conditionalFields, paths, factoryName, filePath, line, warnings } = input;
|
|
1058
|
+
if (paths.length === 0) {
|
|
1059
|
+
if (conditionalFields.length > 0) {
|
|
1060
|
+
warnings.push({ kind: "missing-paths", severity: "warning", name: factoryName, conditionalFields, filePath, line });
|
|
1061
|
+
}
|
|
1062
|
+
return [{ pathLabel: "all", entries: [...bodyEntries] }];
|
|
1063
|
+
}
|
|
1064
|
+
const entriesByField = /* @__PURE__ */ new Map();
|
|
1065
|
+
for (const entry of bodyEntries) {
|
|
1066
|
+
const list = entriesByField.get(entry.fieldPath) ?? [];
|
|
1067
|
+
list.push(entry);
|
|
1068
|
+
entriesByField.set(entry.fieldPath, list);
|
|
1069
|
+
}
|
|
1070
|
+
const sequences = [];
|
|
1071
|
+
for (const path of paths) {
|
|
1072
|
+
const selected = [];
|
|
1073
|
+
let matchedAnyField = false;
|
|
1074
|
+
for (const field of path) {
|
|
1075
|
+
const entriesForField = entriesByField.get(field);
|
|
1076
|
+
if (entriesForField === void 0) {
|
|
1077
|
+
warnings.push({ kind: "unknown-path-field", severity: "warning", name: factoryName, field, filePath, line });
|
|
1078
|
+
continue;
|
|
1079
|
+
}
|
|
1080
|
+
matchedAnyField = true;
|
|
1081
|
+
for (const entry of entriesForField) {
|
|
1082
|
+
selected.push(entry);
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
if (!matchedAnyField) {
|
|
1086
|
+
continue;
|
|
1087
|
+
}
|
|
1088
|
+
sequences.push({ pathLabel: path.join(","), entries: selected });
|
|
1089
|
+
}
|
|
1090
|
+
return sequences;
|
|
1091
|
+
}
|
|
1092
|
+
function extractConstraintsFromBody(input) {
|
|
1093
|
+
const { decl, factoryName, filePath, dispatcher } = input;
|
|
1094
|
+
const entries = [];
|
|
1095
|
+
const conditionalFieldSet = /* @__PURE__ */ new Set();
|
|
1096
|
+
const warnings = [];
|
|
1097
|
+
const body = decl.getBody();
|
|
1098
|
+
if (dispatcher) {
|
|
1099
|
+
const violation = body === void 0 ? void 0 : findFirstConstraintCall(body);
|
|
1100
|
+
if (violation !== void 0) {
|
|
1101
|
+
warnings.push({ kind: "non-delegating-dispatcher", severity: "error", name: factoryName, callee: violation.callee, filePath, line: violation.line });
|
|
1102
|
+
}
|
|
1103
|
+
const dispatcherDelegates = body === void 0 ? [] : collectDispatcherDelegates(body);
|
|
1104
|
+
return { entries, conditionalFields: [], warnings, skipped: true, dispatcherDelegates };
|
|
1105
|
+
}
|
|
1106
|
+
if (body !== void 0) {
|
|
1107
|
+
const branch = findFirstBranchNode(body);
|
|
1108
|
+
if (branch !== void 0) {
|
|
1109
|
+
warnings.push({ kind: "complex-query-body", severity: "error", name: factoryName, branchKind: branch.branchKind, filePath, line: branch.line });
|
|
1110
|
+
return { entries, conditionalFields: [], warnings, skipped: true, dispatcherDelegates: [] };
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
const initialVisited = /* @__PURE__ */ new Set([buildDeclKey(decl)]);
|
|
1114
|
+
walkBodyInto({
|
|
1115
|
+
decl,
|
|
1116
|
+
factoryName,
|
|
1117
|
+
filePath,
|
|
1118
|
+
visited: initialVisited,
|
|
1119
|
+
warnings,
|
|
1120
|
+
entries,
|
|
1121
|
+
conditionalFieldSet,
|
|
1122
|
+
forcedConditional: void 0
|
|
1123
|
+
});
|
|
1124
|
+
const conditionalFields = [];
|
|
1125
|
+
const seenConditional = /* @__PURE__ */ new Set();
|
|
1126
|
+
for (const entry of entries) {
|
|
1127
|
+
if (conditionalFieldSet.has(entry.fieldPath) && !seenConditional.has(entry.fieldPath)) {
|
|
1128
|
+
seenConditional.add(entry.fieldPath);
|
|
1129
|
+
conditionalFields.push(entry.fieldPath);
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
return { entries, conditionalFields, warnings, skipped: false, dispatcherDelegates: [] };
|
|
1133
|
+
}
|
|
1134
|
+
function collectDispatcherDelegates(bodyNode) {
|
|
1135
|
+
const calls = bodyNode.getDescendantsOfKind(SyntaxKind2.CallExpression);
|
|
1136
|
+
calls.sort((a, b) => a.getStart() - b.getStart());
|
|
1137
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1138
|
+
const out = [];
|
|
1139
|
+
for (const call of calls) {
|
|
1140
|
+
const expression = call.getExpression();
|
|
1141
|
+
if (!Node2.isIdentifier(expression)) {
|
|
1142
|
+
continue;
|
|
1143
|
+
}
|
|
1144
|
+
const name = expression.getText();
|
|
1145
|
+
if (name.length === 0 || seen.has(name)) {
|
|
1146
|
+
continue;
|
|
1147
|
+
}
|
|
1148
|
+
if (name === "where" || name === "orderBy" || getFirestoreQueryHelperDescriptor(name) !== void 0) {
|
|
1149
|
+
continue;
|
|
1150
|
+
}
|
|
1151
|
+
seen.add(name);
|
|
1152
|
+
out.push(name);
|
|
1153
|
+
}
|
|
1154
|
+
return out;
|
|
1155
|
+
}
|
|
1156
|
+
function walkBodyInto(input) {
|
|
1157
|
+
const { decl, forcedConditional } = input;
|
|
1158
|
+
const body = decl.getBody();
|
|
1159
|
+
if (body === void 0) {
|
|
1160
|
+
return;
|
|
1161
|
+
}
|
|
1162
|
+
const calls = body.getDescendantsOfKind(SyntaxKind2.CallExpression);
|
|
1163
|
+
calls.sort((a, b) => a.getStart() - b.getStart());
|
|
1164
|
+
for (const call of calls) {
|
|
1165
|
+
const callee = resolveCallExpressionCallee(call);
|
|
1166
|
+
if (callee === void 0) {
|
|
1167
|
+
continue;
|
|
1168
|
+
}
|
|
1169
|
+
const callConditional = forcedConditional ?? isWithinConditionalBranch(call, body);
|
|
1170
|
+
processCallExpression({ ...input, body, call, callee, callConditional });
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
function resolveCallExpressionCallee(call) {
|
|
1174
|
+
const expression = call.getExpression();
|
|
1175
|
+
const isIdentifierCallee = Node2.isIdentifier(expression);
|
|
1176
|
+
const isPropertyCallee = Node2.isPropertyAccessExpression(expression);
|
|
1177
|
+
if (!isIdentifierCallee && !isPropertyCallee) {
|
|
1178
|
+
return void 0;
|
|
1179
|
+
}
|
|
1180
|
+
const name = isIdentifierCallee ? expression.getText() : expression.getName();
|
|
1181
|
+
if (name === void 0) {
|
|
1182
|
+
return void 0;
|
|
1183
|
+
}
|
|
1184
|
+
return { expression, name, isIdentifierCallee };
|
|
1185
|
+
}
|
|
1186
|
+
function processCallExpression(input) {
|
|
1187
|
+
const { callee } = input;
|
|
1188
|
+
if (callee.name === "where") {
|
|
1189
|
+
handleWhereCall(input);
|
|
1190
|
+
return;
|
|
1191
|
+
}
|
|
1192
|
+
if (callee.name === "orderBy") {
|
|
1193
|
+
handleOrderByCall(input);
|
|
1194
|
+
return;
|
|
1195
|
+
}
|
|
1196
|
+
const descriptor = getFirestoreQueryHelperDescriptor(callee.name);
|
|
1197
|
+
if (descriptor !== void 0) {
|
|
1198
|
+
handleHelperCall({ ...input, descriptor });
|
|
1199
|
+
return;
|
|
1200
|
+
}
|
|
1201
|
+
if (callee.isIdentifierCallee) {
|
|
1202
|
+
tryTransitiveResolution({
|
|
1203
|
+
call: input.call,
|
|
1204
|
+
identifier: callee.expression,
|
|
1205
|
+
calleeName: callee.name,
|
|
1206
|
+
callConditional: input.callConditional,
|
|
1207
|
+
factoryName: input.factoryName,
|
|
1208
|
+
filePath: input.filePath,
|
|
1209
|
+
visited: input.visited,
|
|
1210
|
+
warnings: input.warnings,
|
|
1211
|
+
entries: input.entries,
|
|
1212
|
+
conditionalFieldSet: input.conditionalFieldSet
|
|
1213
|
+
});
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
function pushUnresolvedFieldWarning(input, calleeLabel) {
|
|
1217
|
+
input.warnings.push({ kind: "unresolved-field", severity: "warning", name: input.factoryName, callee: calleeLabel, filePath: input.filePath, line: input.call.getStartLineNumber() });
|
|
1218
|
+
}
|
|
1219
|
+
function recordConstraintEntry(input, entry) {
|
|
1220
|
+
input.entries.push(entry);
|
|
1221
|
+
if (input.callConditional) {
|
|
1222
|
+
input.conditionalFieldSet.add(entry.fieldPath);
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
function handleWhereCall(input) {
|
|
1226
|
+
const parsed = parseWhereCall(input.call);
|
|
1227
|
+
if (parsed === void 0) {
|
|
1228
|
+
pushUnresolvedFieldWarning(input, "where");
|
|
1229
|
+
return;
|
|
1230
|
+
}
|
|
1231
|
+
recordConstraintEntry(input, parsed);
|
|
1232
|
+
}
|
|
1233
|
+
function handleOrderByCall(input) {
|
|
1234
|
+
const parsed = parseOrderByCall(input.call);
|
|
1235
|
+
if (parsed === void 0) {
|
|
1236
|
+
pushUnresolvedFieldWarning(input, "orderBy");
|
|
1237
|
+
return;
|
|
1238
|
+
}
|
|
1239
|
+
recordConstraintEntry(input, parsed);
|
|
1240
|
+
}
|
|
1241
|
+
function handleHelperCall(input) {
|
|
1242
|
+
const { call, callee, descriptor } = input;
|
|
1243
|
+
const args = call.getArguments();
|
|
1244
|
+
const fieldArg = args[descriptor.fieldArgIndex];
|
|
1245
|
+
const fieldPath = fieldArg === void 0 ? void 0 : readStringLiteral(fieldArg);
|
|
1246
|
+
if (fieldPath === void 0) {
|
|
1247
|
+
pushUnresolvedFieldWarning(input, callee.name);
|
|
1248
|
+
return;
|
|
1249
|
+
}
|
|
1250
|
+
const direction = descriptor.directionArgIndex === void 0 ? void 0 : readDirectionLiteral(args[descriptor.directionArgIndex]);
|
|
1251
|
+
for (const expanded of expandFirestoreQueryHelper({ descriptor, fieldPath, direction })) {
|
|
1252
|
+
input.entries.push(expanded);
|
|
1253
|
+
}
|
|
1254
|
+
if (input.callConditional) {
|
|
1255
|
+
input.conditionalFieldSet.add(fieldPath);
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
function tryTransitiveResolution(input) {
|
|
1259
|
+
const { call, identifier, calleeName, callConditional, factoryName, filePath, visited, warnings, entries, conditionalFieldSet } = input;
|
|
1260
|
+
const resolved = resolveCalleeDeclaration(identifier);
|
|
1261
|
+
if (resolved === void 0) {
|
|
1262
|
+
return;
|
|
1263
|
+
}
|
|
1264
|
+
const returnType = readReturnTypeText(resolved.decl);
|
|
1265
|
+
if (!isConstraintRelatedReturnType(returnType)) {
|
|
1266
|
+
return;
|
|
1267
|
+
}
|
|
1268
|
+
if (!resolved.hasBody) {
|
|
1269
|
+
warnings.push({ kind: "unresolvable-transitive-callee", severity: "warning", name: factoryName, callee: calleeName, filePath, line: call.getStartLineNumber() });
|
|
1270
|
+
return;
|
|
1271
|
+
}
|
|
1272
|
+
const calleeKey = buildDeclKey(resolved.decl);
|
|
1273
|
+
if (visited.has(calleeKey)) {
|
|
1274
|
+
warnings.push({ kind: "transitive-cycle", severity: "warning", name: factoryName, callee: calleeName, filePath, line: call.getStartLineNumber() });
|
|
1275
|
+
return;
|
|
1276
|
+
}
|
|
1277
|
+
if (!isIndexTagged(resolved.decl)) {
|
|
1278
|
+
const calleeFilePath = resolved.decl.getSourceFile().getFilePath();
|
|
1279
|
+
const calleeLine = resolved.decl.getStartLineNumber();
|
|
1280
|
+
warnings.push({ kind: "unannotated-query-helper", severity: "warning", name: factoryName, callee: calleeName, calleeFilePath, calleeLine, filePath, line: call.getStartLineNumber() });
|
|
1281
|
+
}
|
|
1282
|
+
const nextVisited = /* @__PURE__ */ new Set([...visited, calleeKey]);
|
|
1283
|
+
walkBodyInto({
|
|
1284
|
+
decl: resolved.decl,
|
|
1285
|
+
factoryName,
|
|
1286
|
+
filePath,
|
|
1287
|
+
visited: nextVisited,
|
|
1288
|
+
warnings,
|
|
1289
|
+
entries,
|
|
1290
|
+
conditionalFieldSet,
|
|
1291
|
+
forcedConditional: callConditional
|
|
1292
|
+
});
|
|
1293
|
+
}
|
|
1294
|
+
function resolveCalleeDeclaration(identifier) {
|
|
1295
|
+
const symbol = identifier.getSymbol();
|
|
1296
|
+
if (symbol === void 0) {
|
|
1297
|
+
return void 0;
|
|
1298
|
+
}
|
|
1299
|
+
const aliased = symbol.getAliasedSymbol();
|
|
1300
|
+
const declarations = (aliased ?? symbol).getDeclarations();
|
|
1301
|
+
let result;
|
|
1302
|
+
for (const d of declarations) {
|
|
1303
|
+
if (Node2.isFunctionDeclaration(d) && d.isExported()) {
|
|
1304
|
+
result = { decl: d, hasBody: d.getBody() !== void 0 };
|
|
1305
|
+
break;
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
return result;
|
|
1309
|
+
}
|
|
1310
|
+
function isConstraintRelatedReturnType(returnType) {
|
|
1311
|
+
if (returnType.length === 0) {
|
|
1312
|
+
return false;
|
|
1313
|
+
}
|
|
1314
|
+
return returnType.includes("FirestoreQueryConstraint");
|
|
1315
|
+
}
|
|
1316
|
+
function readReturnTypeText(decl) {
|
|
1317
|
+
const node = decl.getReturnTypeNode();
|
|
1318
|
+
return node === void 0 ? decl.getReturnType().getText() : node.getText();
|
|
1319
|
+
}
|
|
1320
|
+
function isIndexTagged(decl) {
|
|
1321
|
+
let result = false;
|
|
1322
|
+
for (const doc of decl.getJsDocs()) {
|
|
1323
|
+
for (const tag of doc.getTags()) {
|
|
1324
|
+
if (tag.getTagName() === INDEX_MARKER) {
|
|
1325
|
+
result = true;
|
|
1326
|
+
break;
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
if (result) {
|
|
1330
|
+
break;
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
return result;
|
|
1334
|
+
}
|
|
1335
|
+
function buildDeclKey(decl) {
|
|
1336
|
+
const name = decl.getName() ?? "<anonymous>";
|
|
1337
|
+
return `${decl.getSourceFile().getFilePath()}::${name}`;
|
|
1338
|
+
}
|
|
1339
|
+
function findFirstBranchNode(bodyNode) {
|
|
1340
|
+
let result;
|
|
1341
|
+
for (const [syntaxKind, branchKind] of COMPLEX_BODY_SYNTAX_KINDS) {
|
|
1342
|
+
const nodes = bodyNode.getDescendantsOfKind(syntaxKind);
|
|
1343
|
+
if (nodes.length > 0) {
|
|
1344
|
+
const first = nodes.sort((a, b) => a.getStart() - b.getStart())[0];
|
|
1345
|
+
const line = first.getStartLineNumber();
|
|
1346
|
+
if (result === void 0 || line < result.line) {
|
|
1347
|
+
result = { branchKind, line };
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
return result;
|
|
1352
|
+
}
|
|
1353
|
+
function getCallExpressionName(expression) {
|
|
1354
|
+
if (Node2.isIdentifier(expression)) {
|
|
1355
|
+
return expression.getText();
|
|
1356
|
+
}
|
|
1357
|
+
if (Node2.isPropertyAccessExpression(expression)) {
|
|
1358
|
+
return expression.getName();
|
|
1359
|
+
}
|
|
1360
|
+
return void 0;
|
|
1361
|
+
}
|
|
1362
|
+
function findFirstConstraintCall(bodyNode) {
|
|
1363
|
+
const calls = bodyNode.getDescendantsOfKind(SyntaxKind2.CallExpression);
|
|
1364
|
+
calls.sort((a, b) => a.getStart() - b.getStart());
|
|
1365
|
+
let result;
|
|
1366
|
+
for (const call of calls) {
|
|
1367
|
+
const expression = call.getExpression();
|
|
1368
|
+
const calleeName = getCallExpressionName(expression);
|
|
1369
|
+
if (calleeName === void 0) {
|
|
1370
|
+
continue;
|
|
1371
|
+
}
|
|
1372
|
+
if (calleeName === "where" || calleeName === "orderBy" || getFirestoreQueryHelperDescriptor(calleeName) !== void 0) {
|
|
1373
|
+
result = { callee: calleeName, line: call.getStartLineNumber() };
|
|
1374
|
+
break;
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
return result;
|
|
1378
|
+
}
|
|
1379
|
+
function isWithinConditionalBranch(call, bodyNode) {
|
|
1380
|
+
let node = call.getParent();
|
|
1381
|
+
let result = false;
|
|
1382
|
+
while (node !== void 0 && node !== bodyNode) {
|
|
1383
|
+
const kind = node.getKind();
|
|
1384
|
+
if (kind === SyntaxKind2.IfStatement || kind === SyntaxKind2.ConditionalExpression || kind === SyntaxKind2.SwitchStatement || kind === SyntaxKind2.CaseClause) {
|
|
1385
|
+
result = true;
|
|
1386
|
+
break;
|
|
1387
|
+
}
|
|
1388
|
+
node = node.getParent();
|
|
1389
|
+
}
|
|
1390
|
+
return result;
|
|
1391
|
+
}
|
|
1392
|
+
function parseWhereCall(call) {
|
|
1393
|
+
const args = call.getArguments();
|
|
1394
|
+
if (args.length < 2) {
|
|
1395
|
+
return void 0;
|
|
1396
|
+
}
|
|
1397
|
+
const fieldPath = readStringLiteral(args[0]);
|
|
1398
|
+
if (fieldPath === void 0) {
|
|
1399
|
+
return void 0;
|
|
1400
|
+
}
|
|
1401
|
+
const opLiteral = readStringLiteral(args[1]);
|
|
1402
|
+
let operator = "==";
|
|
1403
|
+
if (opLiteral !== void 0 && WHERE_OPERATOR_SET.has(opLiteral)) {
|
|
1404
|
+
operator = opLiteral;
|
|
1405
|
+
}
|
|
1406
|
+
return { kind: "where", fieldPath, operator };
|
|
1407
|
+
}
|
|
1408
|
+
function parseOrderByCall(call) {
|
|
1409
|
+
const args = call.getArguments();
|
|
1410
|
+
if (args.length === 0) {
|
|
1411
|
+
return void 0;
|
|
1412
|
+
}
|
|
1413
|
+
const fieldPath = readStringLiteral(args[0]);
|
|
1414
|
+
if (fieldPath === void 0) {
|
|
1415
|
+
return void 0;
|
|
1416
|
+
}
|
|
1417
|
+
const direction = readDirectionLiteral(args[1]) ?? "asc";
|
|
1418
|
+
return { kind: "orderBy", fieldPath, direction };
|
|
1419
|
+
}
|
|
1420
|
+
function readStringLiteral(node) {
|
|
1421
|
+
let result;
|
|
1422
|
+
if (node !== void 0 && (Node2.isStringLiteral(node) || Node2.isNoSubstitutionTemplateLiteral(node))) {
|
|
1423
|
+
result = node.getLiteralText();
|
|
1424
|
+
}
|
|
1425
|
+
return result;
|
|
1426
|
+
}
|
|
1427
|
+
function readDirectionLiteral(node) {
|
|
1428
|
+
const text = readStringLiteral(node);
|
|
1429
|
+
let result;
|
|
1430
|
+
if (text === "asc" || text === "desc") {
|
|
1431
|
+
result = text;
|
|
1432
|
+
}
|
|
1433
|
+
return result;
|
|
1434
|
+
}
|
|
1435
|
+
function toKebabCase(name) {
|
|
1436
|
+
if (name.length === 0) {
|
|
1437
|
+
return "";
|
|
1438
|
+
}
|
|
1439
|
+
const withSeparators = name.replaceAll(/([A-Z]+)([A-Z][a-z])/g, "$1-$2").replaceAll(/([a-z0-9])([A-Z])/g, "$1-$2").replaceAll(/_+/g, "-").replaceAll(/\s+/g, "-");
|
|
1440
|
+
return withSeparators.toLowerCase();
|
|
1441
|
+
}
|
|
1442
|
+
var STOPWORDS = /* @__PURE__ */ new Set(["the", "a", "an", "and", "or", "of", "for", "to", "in", "on", "at", "is", "are", "be", "this", "that", "with", "when", "if", "as", "by", "from", "into", "returns", "return", "query"]);
|
|
1443
|
+
function buildTagSet(input) {
|
|
1444
|
+
const { name, slug, summary, explicit, category, model } = input;
|
|
1445
|
+
const out = [];
|
|
1446
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1447
|
+
function add(token) {
|
|
1448
|
+
const lower = token.toLowerCase();
|
|
1449
|
+
if (lower.length === 0 || seen.has(lower)) {
|
|
1450
|
+
return;
|
|
1451
|
+
}
|
|
1452
|
+
seen.add(lower);
|
|
1453
|
+
out.push(lower);
|
|
1454
|
+
}
|
|
1455
|
+
for (const tag of explicit) {
|
|
1456
|
+
add(tag);
|
|
1457
|
+
}
|
|
1458
|
+
add(category);
|
|
1459
|
+
add(model);
|
|
1460
|
+
for (const piece of slug.split("-")) {
|
|
1461
|
+
add(piece);
|
|
1462
|
+
}
|
|
1463
|
+
add(name);
|
|
1464
|
+
for (const piece of toKebabCase(model).split("-")) {
|
|
1465
|
+
add(piece);
|
|
1466
|
+
}
|
|
1467
|
+
if (explicit.length === 0) {
|
|
1468
|
+
const summaryTokens = summary.toLowerCase().replaceAll(/[^a-z0-9\s]+/g, " ").split(/\s+/).filter((t) => t.length > 2 && !STOPWORDS.has(t));
|
|
1469
|
+
let added = 0;
|
|
1470
|
+
for (const token of summaryTokens) {
|
|
1471
|
+
if (added >= 8) {
|
|
1472
|
+
break;
|
|
1473
|
+
}
|
|
1474
|
+
const before = out.length;
|
|
1475
|
+
add(token);
|
|
1476
|
+
if (out.length > before) {
|
|
1477
|
+
added += 1;
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
return out;
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
// packages/dbx-cli/firestore-indexes/src/firestore-model-identity-resolver.ts
|
|
1485
|
+
import { Node as Node3, SyntaxKind as SyntaxKind3 } from "ts-morph";
|
|
1486
|
+
function buildIdentityResolverFromProject(project) {
|
|
1487
|
+
const records = [];
|
|
1488
|
+
for (const sourceFile of project.getSourceFiles()) {
|
|
1489
|
+
for (const statement of sourceFile.getVariableStatements()) {
|
|
1490
|
+
for (const declaration of statement.getDeclarations()) {
|
|
1491
|
+
const initializer = declaration.getInitializer();
|
|
1492
|
+
if (initializer === void 0 || !Node3.isCallExpression(initializer)) {
|
|
1493
|
+
continue;
|
|
1494
|
+
}
|
|
1495
|
+
const parsed = parseFirestoreModelIdentityCall(initializer);
|
|
1496
|
+
if (parsed === void 0) {
|
|
1497
|
+
continue;
|
|
1498
|
+
}
|
|
1499
|
+
records.push({
|
|
1500
|
+
modelType: parsed.modelType,
|
|
1501
|
+
collection: parsed.collection,
|
|
1502
|
+
isNested: parsed.isNested,
|
|
1503
|
+
identityConstName: declaration.getName()
|
|
1504
|
+
});
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
return buildResolverFromRecords(records);
|
|
1509
|
+
}
|
|
1510
|
+
function parseFirestoreModelIdentityCall(call) {
|
|
1511
|
+
const expression = call.getExpression();
|
|
1512
|
+
let calleeName;
|
|
1513
|
+
if (Node3.isIdentifier(expression)) {
|
|
1514
|
+
calleeName = expression.getText();
|
|
1515
|
+
} else if (Node3.isPropertyAccessExpression(expression)) {
|
|
1516
|
+
calleeName = expression.getName();
|
|
1517
|
+
}
|
|
1518
|
+
if (calleeName !== "firestoreModelIdentity") {
|
|
1519
|
+
return void 0;
|
|
1520
|
+
}
|
|
1521
|
+
const args = call.getArguments();
|
|
1522
|
+
if (args.length === 0) {
|
|
1523
|
+
return void 0;
|
|
1524
|
+
}
|
|
1525
|
+
const firstArg = args[0];
|
|
1526
|
+
let isNested = false;
|
|
1527
|
+
let stringArgsStart = 0;
|
|
1528
|
+
if (firstArg.getKind() === SyntaxKind3.Identifier || firstArg.getKind() === SyntaxKind3.PropertyAccessExpression) {
|
|
1529
|
+
isNested = true;
|
|
1530
|
+
stringArgsStart = 1;
|
|
1531
|
+
} else if (firstArg.getKind() !== SyntaxKind3.StringLiteral && firstArg.getKind() !== SyntaxKind3.NoSubstitutionTemplateLiteral) {
|
|
1532
|
+
return void 0;
|
|
1533
|
+
}
|
|
1534
|
+
const stringArgs = [];
|
|
1535
|
+
let invalidArg = false;
|
|
1536
|
+
for (let i = stringArgsStart; i < args.length; i += 1) {
|
|
1537
|
+
const arg = args[i];
|
|
1538
|
+
if (Node3.isStringLiteral(arg) || Node3.isNoSubstitutionTemplateLiteral(arg)) {
|
|
1539
|
+
stringArgs.push(arg.getLiteralText());
|
|
1540
|
+
} else {
|
|
1541
|
+
invalidArg = true;
|
|
1542
|
+
break;
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
let result;
|
|
1546
|
+
if (!invalidArg && stringArgs.length > 0) {
|
|
1547
|
+
const modelType = stringArgs[0];
|
|
1548
|
+
const collection = stringArgs.length >= 2 ? stringArgs[1] : modelType;
|
|
1549
|
+
result = { modelType, collection, isNested };
|
|
1550
|
+
}
|
|
1551
|
+
return result;
|
|
1552
|
+
}
|
|
1553
|
+
function buildResolverFromRecords(records) {
|
|
1554
|
+
const byModelType = /* @__PURE__ */ new Map();
|
|
1555
|
+
const byIdentityConst = /* @__PURE__ */ new Map();
|
|
1556
|
+
for (const record of records) {
|
|
1557
|
+
if (!byModelType.has(record.modelType)) {
|
|
1558
|
+
byModelType.set(record.modelType, record);
|
|
1559
|
+
}
|
|
1560
|
+
if (!byIdentityConst.has(record.identityConstName)) {
|
|
1561
|
+
byIdentityConst.set(record.identityConstName, record);
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
function lookupByTypeName(typeName) {
|
|
1565
|
+
const camel = toCamelCase(typeName);
|
|
1566
|
+
let result = byModelType.get(camel);
|
|
1567
|
+
result ??= byIdentityConst.get(`${camel}Identity`);
|
|
1568
|
+
if (result === void 0) {
|
|
1569
|
+
const lower = camel.toLowerCase();
|
|
1570
|
+
result = records.find((r) => r.modelType.toLowerCase() === lower);
|
|
1571
|
+
}
|
|
1572
|
+
return result;
|
|
1573
|
+
}
|
|
1574
|
+
return {
|
|
1575
|
+
lookupByTypeName,
|
|
1576
|
+
lookupByModelType: (modelType) => byModelType.get(modelType),
|
|
1577
|
+
lookupByIdentityConst: (identityConstName) => byIdentityConst.get(identityConstName),
|
|
1578
|
+
all: () => records
|
|
1579
|
+
};
|
|
1580
|
+
}
|
|
1581
|
+
function toCamelCase(typeName) {
|
|
1582
|
+
if (typeName.length === 0) {
|
|
1583
|
+
return typeName;
|
|
1584
|
+
}
|
|
1585
|
+
const firstChar = typeName.charAt(0);
|
|
1586
|
+
let result;
|
|
1587
|
+
if (firstChar >= "A" && firstChar <= "Z") {
|
|
1588
|
+
result = firstChar.toLowerCase() + typeName.slice(1);
|
|
1589
|
+
} else {
|
|
1590
|
+
result = typeName;
|
|
1591
|
+
}
|
|
1592
|
+
return result;
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
// packages/dbx-cli/firestore-indexes/src/model-firebase-index-scan-config-schema.ts
|
|
1596
|
+
import { type as type2 } from "arktype";
|
|
1597
|
+
var ModelFirebaseIndexScanSection = type2({
|
|
1598
|
+
"source?": "string",
|
|
1599
|
+
"module?": "string",
|
|
1600
|
+
include: "string[] >= 1",
|
|
1601
|
+
"exclude?": "string[]",
|
|
1602
|
+
"out?": "string"
|
|
1603
|
+
});
|
|
1604
|
+
var ModelFirebaseIndexScanConfig = type2({
|
|
1605
|
+
version: "1",
|
|
1606
|
+
modelFirebaseIndex: ModelFirebaseIndexScanSection
|
|
1607
|
+
});
|
|
1608
|
+
var DEFAULT_MODEL_FIREBASE_INDEX_SCAN_OUT_PATH = "model-firebase-index.mcp.generated.json";
|
|
1609
|
+
var MODEL_FIREBASE_INDEX_SCAN_CONFIG_FILENAME = "dbx-mcp.scan.json";
|
|
1610
|
+
|
|
1611
|
+
// packages/dbx-cli/src/lib/scan-helpers/scan-io.ts
|
|
1612
|
+
import { glob as fsGlob, readFile as nodeReadFile } from "node:fs/promises";
|
|
1613
|
+
import { resolve as resolvePath } from "node:path";
|
|
1614
|
+
import { Project } from "ts-morph";
|
|
1615
|
+
var defaultReadFile = (path) => nodeReadFile(path, "utf-8");
|
|
1616
|
+
var defaultGlobber = async (input) => {
|
|
1617
|
+
const { projectRoot, include, exclude } = input;
|
|
1618
|
+
const excludeMatchers = exclude.map(globToRegex);
|
|
1619
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1620
|
+
const matches = [];
|
|
1621
|
+
for (const pattern of include) {
|
|
1622
|
+
for await (const match of fsGlob(pattern, { cwd: projectRoot })) {
|
|
1623
|
+
if (excludeMatchers.some((rx) => rx.test(match))) {
|
|
1624
|
+
continue;
|
|
1625
|
+
}
|
|
1626
|
+
if (!seen.has(match)) {
|
|
1627
|
+
seen.add(match);
|
|
1628
|
+
matches.push(match);
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
return matches;
|
|
1633
|
+
};
|
|
1634
|
+
async function loadPackageName(packagePath, readFile) {
|
|
1635
|
+
let raw = null;
|
|
1636
|
+
try {
|
|
1637
|
+
raw = await readFile(packagePath);
|
|
1638
|
+
} catch {
|
|
1639
|
+
raw = null;
|
|
1640
|
+
}
|
|
1641
|
+
let result;
|
|
1642
|
+
if (raw === null) {
|
|
1643
|
+
result = { kind: "fail", outcome: { kind: "no-package", packagePath } };
|
|
1644
|
+
} else {
|
|
1645
|
+
let parsed;
|
|
1646
|
+
let parseError = null;
|
|
1647
|
+
try {
|
|
1648
|
+
parsed = JSON.parse(raw);
|
|
1649
|
+
} catch (err) {
|
|
1650
|
+
parseError = err instanceof Error ? err.message : String(err);
|
|
1651
|
+
}
|
|
1652
|
+
if (parseError === null) {
|
|
1653
|
+
const name = parsed?.name;
|
|
1654
|
+
if (typeof name !== "string" || name.length === 0) {
|
|
1655
|
+
result = { kind: "fail", outcome: { kind: "invalid-package", packagePath, error: "package.json is missing a non-empty `name` field" } };
|
|
1656
|
+
} else {
|
|
1657
|
+
result = { kind: "ok", packageName: name };
|
|
1658
|
+
}
|
|
1659
|
+
} else {
|
|
1660
|
+
result = { kind: "fail", outcome: { kind: "invalid-package", packagePath, error: parseError } };
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
return result;
|
|
1664
|
+
}
|
|
1665
|
+
async function loadScanSection(input) {
|
|
1666
|
+
const { configPath, readFile, parseSection } = input;
|
|
1667
|
+
const readResult = await readScanConfigRaw(configPath, readFile);
|
|
1668
|
+
let result;
|
|
1669
|
+
if (readResult.kind === "error") {
|
|
1670
|
+
result = { kind: "fail", outcome: { kind: "invalid-scan-config", configPath, error: `failed to read config: ${readResult.error}` } };
|
|
1671
|
+
} else if (readResult.kind === "enoent") {
|
|
1672
|
+
result = { kind: "fail", outcome: { kind: "no-config", configPath } };
|
|
1673
|
+
} else {
|
|
1674
|
+
result = parseScanSectionFromRaw(readResult.raw, configPath, parseSection);
|
|
1675
|
+
}
|
|
1676
|
+
return result;
|
|
1677
|
+
}
|
|
1678
|
+
async function readScanConfigRaw(configPath, readFile) {
|
|
1679
|
+
let result;
|
|
1680
|
+
try {
|
|
1681
|
+
const raw = await readFile(configPath);
|
|
1682
|
+
result = { kind: "ok", raw };
|
|
1683
|
+
} catch (err) {
|
|
1684
|
+
const code = err?.code;
|
|
1685
|
+
if (code === "ENOENT") {
|
|
1686
|
+
result = { kind: "enoent" };
|
|
1687
|
+
} else {
|
|
1688
|
+
result = { kind: "error", error: err instanceof Error ? err.message : String(err) };
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
return result;
|
|
1692
|
+
}
|
|
1693
|
+
function parseScanSectionFromRaw(raw, configPath, parseSection) {
|
|
1694
|
+
const jsonResult = parseJsonString(raw);
|
|
1695
|
+
let result;
|
|
1696
|
+
if (jsonResult.ok) {
|
|
1697
|
+
const sectionResult = parseSection(jsonResult.value);
|
|
1698
|
+
if (sectionResult.ok) {
|
|
1699
|
+
result = { kind: "ok", section: sectionResult.section };
|
|
1700
|
+
} else {
|
|
1701
|
+
result = { kind: "fail", outcome: { kind: "invalid-scan-config", configPath, error: sectionResult.error } };
|
|
1702
|
+
}
|
|
1703
|
+
} else {
|
|
1704
|
+
result = { kind: "fail", outcome: { kind: "invalid-scan-config", configPath, error: jsonResult.error } };
|
|
1705
|
+
}
|
|
1706
|
+
return result;
|
|
1707
|
+
}
|
|
1708
|
+
function parseJsonString(raw) {
|
|
1709
|
+
let result;
|
|
1710
|
+
try {
|
|
1711
|
+
result = { ok: true, value: JSON.parse(raw) };
|
|
1712
|
+
} catch (err) {
|
|
1713
|
+
result = { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
1714
|
+
}
|
|
1715
|
+
return result;
|
|
1716
|
+
}
|
|
1717
|
+
async function buildScanProject(input) {
|
|
1718
|
+
const { projectRoot, filePaths, readFile } = input;
|
|
1719
|
+
const project = new Project({ useInMemoryFileSystem: true, skipAddingFilesFromTsConfig: true });
|
|
1720
|
+
for (const relPath of filePaths) {
|
|
1721
|
+
const absolute = resolvePath(projectRoot, relPath);
|
|
1722
|
+
const text = await readFile(absolute);
|
|
1723
|
+
project.createSourceFile(absolute, text, { overwrite: true });
|
|
1724
|
+
}
|
|
1725
|
+
return project;
|
|
1726
|
+
}
|
|
1727
|
+
function globToRegex(pattern) {
|
|
1728
|
+
let body = "";
|
|
1729
|
+
let index = 0;
|
|
1730
|
+
while (index < pattern.length) {
|
|
1731
|
+
const char = pattern[index];
|
|
1732
|
+
if (char === "*" && pattern[index + 1] === "*") {
|
|
1733
|
+
body += ".*";
|
|
1734
|
+
index += 2;
|
|
1735
|
+
if (pattern[index] === "/") {
|
|
1736
|
+
index += 1;
|
|
1737
|
+
}
|
|
1738
|
+
} else if (char === "*") {
|
|
1739
|
+
body += "[^/]*";
|
|
1740
|
+
index += 1;
|
|
1741
|
+
} else if (char === "?") {
|
|
1742
|
+
body += "[^/]";
|
|
1743
|
+
index += 1;
|
|
1744
|
+
} else if (".+^${}()|[]\\".includes(char)) {
|
|
1745
|
+
body += `\\${char}`;
|
|
1746
|
+
index += 1;
|
|
1747
|
+
} else {
|
|
1748
|
+
body += char;
|
|
1749
|
+
index += 1;
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
return new RegExp(`^${body}$`);
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
// packages/dbx-cli/firestore-indexes/src/model-firebase-index-build-manifest.ts
|
|
1756
|
+
var DEFAULT_READ_FILE = defaultReadFile;
|
|
1757
|
+
var DEFAULT_GLOBBER = defaultGlobber;
|
|
1758
|
+
async function buildModelFirebaseIndexManifest(input) {
|
|
1759
|
+
const { projectRoot, generator, readFile = DEFAULT_READ_FILE, globber = DEFAULT_GLOBBER, now = () => /* @__PURE__ */ new Date() } = input;
|
|
1760
|
+
const configPath = resolve2(projectRoot, MODEL_FIREBASE_INDEX_SCAN_CONFIG_FILENAME);
|
|
1761
|
+
const packagePath = resolve2(projectRoot, "package.json");
|
|
1762
|
+
const configOutcome = await loadScanSection({
|
|
1763
|
+
configPath,
|
|
1764
|
+
readFile,
|
|
1765
|
+
parseSection: (parsed) => {
|
|
1766
|
+
const validated2 = ModelFirebaseIndexScanConfig(parsed);
|
|
1767
|
+
if (validated2 instanceof type3.errors) {
|
|
1768
|
+
return { ok: false, error: validated2.summary };
|
|
1769
|
+
}
|
|
1770
|
+
return { ok: true, section: validated2.modelFirebaseIndex };
|
|
1771
|
+
}
|
|
1772
|
+
});
|
|
1773
|
+
if (configOutcome.kind !== "ok") {
|
|
1774
|
+
return configOutcome.outcome;
|
|
1775
|
+
}
|
|
1776
|
+
const scanSection = configOutcome.section;
|
|
1777
|
+
const packageOutcome = await loadPackageName(packagePath, readFile);
|
|
1778
|
+
if (packageOutcome.kind !== "ok") {
|
|
1779
|
+
return packageOutcome.outcome;
|
|
1780
|
+
}
|
|
1781
|
+
const packageName = packageOutcome.packageName;
|
|
1782
|
+
const filePaths = await globber({
|
|
1783
|
+
projectRoot,
|
|
1784
|
+
include: scanSection.include,
|
|
1785
|
+
exclude: scanSection.exclude ?? []
|
|
1786
|
+
});
|
|
1787
|
+
const project = await buildScanProject({ projectRoot, filePaths, readFile });
|
|
1788
|
+
const identityResolver = buildIdentityResolverFromProject(project);
|
|
1789
|
+
const extractResult = extractModelFirebaseIndexEntries({ project, identityResolver, projectRoot });
|
|
1790
|
+
const analyzed = analyzeModelFirebaseIndexEntries(extractResult.entries);
|
|
1791
|
+
const buildWarnings = [];
|
|
1792
|
+
for (const warning of extractResult.warnings) {
|
|
1793
|
+
buildWarnings.push({ stage: "extract", warning });
|
|
1794
|
+
}
|
|
1795
|
+
for (const entry of analyzed) {
|
|
1796
|
+
for (const warning of entry.warnings) {
|
|
1797
|
+
buildWarnings.push({ stage: "analyze", warning });
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
const moduleName = scanSection.module ?? packageName;
|
|
1801
|
+
const sourceLabel = scanSection.source ?? packageName;
|
|
1802
|
+
const entries = analyzed.map((analyzedEntry) => assembleEntry({ analyzedEntry, moduleName, projectRoot }));
|
|
1803
|
+
const manifest = {
|
|
1804
|
+
version: 1,
|
|
1805
|
+
source: sourceLabel,
|
|
1806
|
+
module: moduleName,
|
|
1807
|
+
generatedAt: now().toISOString(),
|
|
1808
|
+
generator,
|
|
1809
|
+
entries
|
|
1810
|
+
};
|
|
1811
|
+
const validated = ModelFirebaseIndexManifest(manifest);
|
|
1812
|
+
let outcome;
|
|
1813
|
+
if (validated instanceof type3.errors) {
|
|
1814
|
+
outcome = { kind: "invalid-manifest", error: validated.summary };
|
|
1815
|
+
} else {
|
|
1816
|
+
const outPath = resolve2(projectRoot, scanSection.out ?? DEFAULT_MODEL_FIREBASE_INDEX_SCAN_OUT_PATH);
|
|
1817
|
+
const entryFilePathsBySlug = /* @__PURE__ */ new Map();
|
|
1818
|
+
const dispatcherSummaries = [];
|
|
1819
|
+
for (const analyzedEntry of analyzed) {
|
|
1820
|
+
const extracted = analyzedEntry.extractedEntry;
|
|
1821
|
+
entryFilePathsBySlug.set(extracted.slug, extracted.filePath);
|
|
1822
|
+
if (extracted.dispatcher) {
|
|
1823
|
+
dispatcherSummaries.push({ slug: extracted.slug, name: extracted.name, delegates: [...extracted.dispatcherDelegates] });
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
outcome = {
|
|
1827
|
+
kind: "success",
|
|
1828
|
+
manifest: validated,
|
|
1829
|
+
outPath,
|
|
1830
|
+
scannedFileCount: filePaths.length,
|
|
1831
|
+
extractWarnings: buildWarnings,
|
|
1832
|
+
entryFilePathsBySlug,
|
|
1833
|
+
dispatcherSummaries
|
|
1834
|
+
};
|
|
1835
|
+
}
|
|
1836
|
+
return outcome;
|
|
1837
|
+
}
|
|
1838
|
+
var SRC_PREFIXES = ["/src/lib/", "/src/"];
|
|
1839
|
+
function deriveSubpath(filePath, projectRoot) {
|
|
1840
|
+
const normalised = filePath.replaceAll("\\", "/");
|
|
1841
|
+
const projectNormalised = projectRoot.replaceAll("\\", "/");
|
|
1842
|
+
let relativePath;
|
|
1843
|
+
if (normalised.startsWith(projectNormalised)) {
|
|
1844
|
+
relativePath = normalised.slice(projectNormalised.length).replace(/^\/+/, "");
|
|
1845
|
+
} else {
|
|
1846
|
+
relativePath = relative(projectRoot, filePath).replaceAll("\\", "/");
|
|
1847
|
+
}
|
|
1848
|
+
for (const prefix of SRC_PREFIXES) {
|
|
1849
|
+
const stripped = `/${relativePath}`;
|
|
1850
|
+
const idx = stripped.indexOf(prefix);
|
|
1851
|
+
if (idx >= 0) {
|
|
1852
|
+
const remainder = stripped.slice(idx + prefix.length);
|
|
1853
|
+
return remainder.replace(/\.ts$/, "");
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
return relativePath.replace(/\.ts$/, "");
|
|
1857
|
+
}
|
|
1858
|
+
function assembleEntry(input) {
|
|
1859
|
+
const { analyzedEntry, moduleName, projectRoot } = input;
|
|
1860
|
+
const entry = analyzedEntry.extractedEntry;
|
|
1861
|
+
const subpath = deriveSubpath(entry.filePath, projectRoot);
|
|
1862
|
+
const out = {
|
|
1863
|
+
slug: entry.slug,
|
|
1864
|
+
name: entry.name,
|
|
1865
|
+
module: moduleName,
|
|
1866
|
+
subpath,
|
|
1867
|
+
signature: entry.signature,
|
|
1868
|
+
description: entry.description,
|
|
1869
|
+
model: entry.model,
|
|
1870
|
+
collection: entry.collection,
|
|
1871
|
+
isNested: entry.isNested,
|
|
1872
|
+
scope: entry.scope,
|
|
1873
|
+
manual: entry.manual,
|
|
1874
|
+
skip: entry.skip,
|
|
1875
|
+
...entry.specOnly ? { specOnly: true } : {},
|
|
1876
|
+
...entry.excluded ? { excluded: true } : {},
|
|
1877
|
+
category: entry.category,
|
|
1878
|
+
params: entry.params.map((p) => ({ ...p })),
|
|
1879
|
+
returns: entry.returns,
|
|
1880
|
+
tags: [...entry.tags],
|
|
1881
|
+
constraintSequences: entry.constraintSequences.map((s) => ({
|
|
1882
|
+
...s.pathLabel === void 0 ? {} : { pathLabel: s.pathLabel },
|
|
1883
|
+
entries: s.entries.map((e) => ({ ...e }))
|
|
1884
|
+
})),
|
|
1885
|
+
derivedComposites: analyzedEntry.derivedComposites.map((c) => ({ ...c, fields: c.fields.map((f) => ({ ...f })) })),
|
|
1886
|
+
derivedFieldOverrides: analyzedEntry.derivedFieldOverrides.map((f) => ({ ...f, variants: f.variants.map((v) => ({ ...v })) })),
|
|
1887
|
+
...entry.example.length > 0 ? { example: entry.example } : {},
|
|
1888
|
+
...entry.relatedSlugs && entry.relatedSlugs.length > 0 ? { relatedSlugs: [...entry.relatedSlugs] } : {},
|
|
1889
|
+
...entry.skillRefs && entry.skillRefs.length > 0 ? { skillRefs: [...entry.skillRefs] } : {},
|
|
1890
|
+
...entry.deprecated === void 0 ? {} : { deprecated: entry.deprecated },
|
|
1891
|
+
...entry.since === void 0 ? {} : { since: entry.since }
|
|
1892
|
+
};
|
|
1893
|
+
return out;
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
// packages/dbx-cli/firestore-query-manifest/src/find-query-entries.ts
|
|
1897
|
+
async function findQueryEntries(input) {
|
|
1898
|
+
const { componentRoot, generator, buildManifest = buildModelFirebaseIndexManifest } = input;
|
|
1899
|
+
const outcome = await buildManifest({ projectRoot: componentRoot, generator });
|
|
1900
|
+
let result;
|
|
1901
|
+
if (outcome.kind === "success") {
|
|
1902
|
+
const dispatcherNames = new Set(outcome.dispatcherSummaries.map((x) => x.name));
|
|
1903
|
+
const all = outcome.manifest.entries;
|
|
1904
|
+
const kept = all.filter((x) => x.specOnly !== true);
|
|
1905
|
+
result = {
|
|
1906
|
+
kind: "success",
|
|
1907
|
+
droppedSpecOnly: all.length - kept.length,
|
|
1908
|
+
entries: kept.map((entry) => ({
|
|
1909
|
+
slug: entry.slug,
|
|
1910
|
+
name: entry.name,
|
|
1911
|
+
module: entry.module,
|
|
1912
|
+
subpath: entry.subpath,
|
|
1913
|
+
model: entry.model,
|
|
1914
|
+
collection: entry.collection,
|
|
1915
|
+
isNested: entry.isNested,
|
|
1916
|
+
scope: entry.scope,
|
|
1917
|
+
signature: entry.signature,
|
|
1918
|
+
params: entry.params.map((p) => ({ name: p.name, type: p.type, optional: p.optional, ...p.description ? { description: p.description } : {} })),
|
|
1919
|
+
...entry.description ? { description: entry.description } : {},
|
|
1920
|
+
...entry.category ? { category: entry.category } : {},
|
|
1921
|
+
...entry.tags.length > 0 ? { tags: entry.tags } : {},
|
|
1922
|
+
...entry.example ? { example: entry.example } : {},
|
|
1923
|
+
...entry.relatedSlugs && entry.relatedSlugs.length > 0 ? { relatedSlugs: entry.relatedSlugs } : {},
|
|
1924
|
+
...entry.manual ? { manual: true } : {},
|
|
1925
|
+
...entry.skip ? { skip: true } : {},
|
|
1926
|
+
...entry.excluded ? { excluded: true } : {},
|
|
1927
|
+
...dispatcherNames.has(entry.name) ? { dispatcher: true } : {}
|
|
1928
|
+
}))
|
|
1929
|
+
};
|
|
1930
|
+
} else {
|
|
1931
|
+
result = { kind: "failure", message: describeFailure(componentRoot, outcome) };
|
|
1932
|
+
}
|
|
1933
|
+
return result;
|
|
1934
|
+
}
|
|
1935
|
+
function describeFailure(componentRoot, outcome) {
|
|
1936
|
+
let result;
|
|
1937
|
+
switch (outcome.kind) {
|
|
1938
|
+
case "no-config":
|
|
1939
|
+
result = `${componentRoot}: no dbx-mcp.scan.json at ${outcome.configPath}. Add one with a \`modelFirebaseIndex.include\` glob.`;
|
|
1940
|
+
break;
|
|
1941
|
+
case "invalid-scan-config":
|
|
1942
|
+
result = `${componentRoot}: dbx-mcp.scan.json at ${outcome.configPath} is invalid \u2014 ${outcome.error}`;
|
|
1943
|
+
break;
|
|
1944
|
+
case "no-package":
|
|
1945
|
+
result = `${componentRoot}: no package.json at ${outcome.packagePath}; the entry \`module\` cannot be derived.`;
|
|
1946
|
+
break;
|
|
1947
|
+
case "invalid-package":
|
|
1948
|
+
result = `${componentRoot}: package.json at ${outcome.packagePath} is invalid \u2014 ${outcome.error}`;
|
|
1949
|
+
break;
|
|
1950
|
+
default:
|
|
1951
|
+
result = `${componentRoot}: the extracted manifest failed validation \u2014 ${outcome.error}`;
|
|
1952
|
+
break;
|
|
1953
|
+
}
|
|
1954
|
+
return result;
|
|
1955
|
+
}
|
|
1956
|
+
|
|
1957
|
+
// packages/dbx-cli/firestore-query-manifest/src/main.ts
|
|
1958
|
+
var WORKSPACE_ROOT = process.cwd();
|
|
1959
|
+
var GENERATOR = `@dereekb/dbx-cli-firestore-query-manifest@${package_default.version}`;
|
|
1960
|
+
async function main() {
|
|
1961
|
+
const flags = parseFlags(process.argv.slice(2));
|
|
1962
|
+
if (flags.components.length === 0 || !flags.output) {
|
|
1963
|
+
printUsageAndExit();
|
|
1964
|
+
return;
|
|
1965
|
+
}
|
|
1966
|
+
const outputFile = resolveWorkspacePath(flags.output);
|
|
1967
|
+
const projectName = flags.project ?? "<cli>";
|
|
1968
|
+
const namespace = deriveNamespace(flags.project);
|
|
1969
|
+
const collected = [];
|
|
1970
|
+
const warnings = [];
|
|
1971
|
+
let droppedSpecOnly = 0;
|
|
1972
|
+
for (const component of flags.components) {
|
|
1973
|
+
const componentRoot = resolveWorkspacePath(component);
|
|
1974
|
+
const found = await findQueryEntries({ componentRoot, generator: GENERATOR });
|
|
1975
|
+
if (found.kind === "failure") {
|
|
1976
|
+
console.error(found.message);
|
|
1977
|
+
process.exit(1);
|
|
1978
|
+
return;
|
|
1979
|
+
}
|
|
1980
|
+
droppedSpecOnly += found.droppedSpecOnly;
|
|
1981
|
+
const bound = bindQueryFactories({ componentRoot, entries: found.entries });
|
|
1982
|
+
collected.push(...bound.bound);
|
|
1983
|
+
warnings.push(...bound.warnings);
|
|
1984
|
+
}
|
|
1985
|
+
for (const warning of warnings) {
|
|
1986
|
+
console.warn(warning);
|
|
1987
|
+
}
|
|
1988
|
+
const formatted = await renderQueryManifest({ outputFile, entries: collected, projectName, namespace });
|
|
1989
|
+
const relOutput = relative2(WORKSPACE_ROOT, outputFile);
|
|
1990
|
+
if (flags.check) {
|
|
1991
|
+
const current = existsSync3(outputFile) ? readFileSync3(outputFile, "utf8") : void 0;
|
|
1992
|
+
if (current !== formatted) {
|
|
1993
|
+
console.error(`[check] ${relOutput} is out of date \u2014 run the generator and commit the result.`);
|
|
1994
|
+
process.exit(1);
|
|
1995
|
+
return;
|
|
1996
|
+
}
|
|
1997
|
+
console.log(`[check] ${relOutput} is up to date.`);
|
|
1998
|
+
} else {
|
|
1999
|
+
const outcome = writeGeneratedTsFile({ outputFile, contents: formatted });
|
|
2000
|
+
console.log(`[${outcome}] ${relOutput}`);
|
|
2001
|
+
}
|
|
2002
|
+
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`);
|
|
2004
|
+
if (flags.strict && boundCount < collected.length) {
|
|
2005
|
+
console.error(`[strict] ${collected.length - boundCount} factor(y|ies) failed to bind \u2014 failing build.`);
|
|
2006
|
+
process.exit(1);
|
|
2007
|
+
}
|
|
2008
|
+
}
|
|
2009
|
+
function resolveWorkspacePath(value) {
|
|
2010
|
+
return isAbsolute2(value) ? value : resolve3(WORKSPACE_ROOT, value);
|
|
2011
|
+
}
|
|
2012
|
+
function deriveNamespace(projectName) {
|
|
2013
|
+
const base = (projectName ?? "cli").replaceAll(/[^a-zA-Z0-9]+/g, "_");
|
|
2014
|
+
return `${base.toUpperCase()}_FIRESTORE_QUERY_MANIFEST`;
|
|
2015
|
+
}
|
|
2016
|
+
function parseFlags(argv) {
|
|
2017
|
+
const components = [];
|
|
2018
|
+
let output;
|
|
2019
|
+
let project;
|
|
2020
|
+
let strict = false;
|
|
2021
|
+
let check = false;
|
|
2022
|
+
for (const arg of argv) {
|
|
2023
|
+
if (arg === "--strict") {
|
|
2024
|
+
strict = true;
|
|
2025
|
+
} else if (arg === "--check") {
|
|
2026
|
+
check = true;
|
|
2027
|
+
} else if (arg.startsWith("--component=")) {
|
|
2028
|
+
const value = arg.slice("--component=".length).trim();
|
|
2029
|
+
if (value) components.push(value);
|
|
2030
|
+
} else if (arg.startsWith("--output=")) {
|
|
2031
|
+
output = arg.slice("--output=".length);
|
|
2032
|
+
} else if (arg.startsWith("--project=")) {
|
|
2033
|
+
project = arg.slice("--project=".length);
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
return { components, output, project, strict, check };
|
|
2037
|
+
}
|
|
2038
|
+
function printUsageAndExit() {
|
|
2039
|
+
console.error(String.raw`generate-firestore-query-manifest
|
|
2040
|
+
|
|
2041
|
+
Usage:
|
|
2042
|
+
node dist/packages/dbx-cli/firestore-query-manifest/main.js \
|
|
2043
|
+
--project=<name> \
|
|
2044
|
+
--component=<component-dir> [--component=<component-dir> ...] \
|
|
2045
|
+
--output=<path-to-query.manifest.generated.ts> \
|
|
2046
|
+
[--strict] [--check]
|
|
2047
|
+
|
|
2048
|
+
Required flags:
|
|
2049
|
+
--component=<dir> A "-firebase" component root to scan. Repeatable.
|
|
2050
|
+
--output=<path> Path to the manifest TS file to write (workspace-relative ok).
|
|
2051
|
+
|
|
2052
|
+
Optional:
|
|
2053
|
+
--project=<name> Project name for the regenerate banner; also derives the constant name.
|
|
2054
|
+
--strict Fail when any tagged factory is not exported from its component barrel.
|
|
2055
|
+
--check Do not write; fail when the committed file is out of date.`);
|
|
2056
|
+
process.exit(1);
|
|
2057
|
+
}
|
|
2058
|
+
try {
|
|
2059
|
+
await main();
|
|
2060
|
+
} catch (e) {
|
|
2061
|
+
console.error(e);
|
|
2062
|
+
process.exit(1);
|
|
2063
|
+
}
|