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