@dereekb/dbx-cli 13.41.0 → 13.43.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/eslint/package.json +3 -3
- package/firebase-api-manifest/main.js +177 -21
- package/firebase-api-manifest/package.json +3 -3
- package/firestore-indexes/src/generate-firestore-indexes-cli.d.ts +12 -4
- package/firestore-indexes/src/model-firebase-index-analyze.d.ts +12 -2
- package/firestore-query-manifest/main.js +26 -14
- package/firestore-query-manifest/package.json +3 -3
- package/generate-firestore-indexes/main.js +118 -20
- package/generate-firestore-indexes/package.json +2 -2
- package/generate-mcp-manifest/main.js +5 -2
- package/generate-mcp-manifest/package.json +3 -3
- package/generate-route-manifest/main.js +1 -1
- package/generate-route-manifest/package.json +2 -2
- package/index.esm.js +2651 -1387
- package/lint-cache/package.json +2 -2
- package/manifest-extract/index.esm.js +148 -2
- package/manifest-extract/package.json +2 -2
- package/manifest-extract/src/lib/extract-models.d.ts +4 -2
- package/manifest-extract/src/lib/types.d.ts +65 -0
- package/model-test/index.esm.js +4 -54
- package/model-test/package.json +2 -2
- package/package.json +6 -6
- package/route/package.json +7 -7
- package/src/lib/doctor/build-drift.check.d.ts +21 -0
- package/src/lib/doctor/doctor.command.factory.d.ts +21 -2
- package/src/lib/doctor/firestore-session.check.d.ts +7 -1
- package/src/lib/doctor/index.d.ts +1 -0
- package/src/lib/firestore/firestore.sdk-identity.d.ts +193 -0
- package/src/lib/firestore/index.d.ts +1 -0
- package/src/lib/manifest/types.d.ts +73 -2
- package/src/lib/mcp-scan/manifest/css-utilities-schema.d.ts +2 -2
- package/src/lib/mcp-scan/manifest/tokens-schema.d.ts +2 -2
- package/src/lib/runner/build-stamp.d.ts +138 -0
- package/src/lib/runner/index.d.ts +1 -0
- package/src/lib/runner/run.d.ts +13 -0
- package/test/package.json +9 -9
- package/validate/index.js +17 -7
- package/validate/package.json +3 -3
package/eslint/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dereekb/dbx-cli/eslint",
|
|
3
|
-
"version": "13.
|
|
3
|
+
"version": "13.43.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"peerDependencies": {
|
|
6
|
-
"@dereekb/dbx-cli": "13.
|
|
7
|
-
"@dereekb/util": "13.
|
|
6
|
+
"@dereekb/dbx-cli": "13.43.0",
|
|
7
|
+
"@dereekb/util": "13.43.0",
|
|
8
8
|
"@typescript-eslint/utils": "8.59.3",
|
|
9
9
|
"typescript": "5.9.3"
|
|
10
10
|
},
|
|
@@ -22,8 +22,8 @@ function renderGroupedImportLines(imports) {
|
|
|
22
22
|
set.add(identifier);
|
|
23
23
|
importsByPackage.set(packageName, set);
|
|
24
24
|
}
|
|
25
|
-
return
|
|
26
|
-
const sortedNames =
|
|
25
|
+
return Array.from(importsByPackage.entries()).sort(([a], [b]) => a.localeCompare(b)).map(([pkg, names]) => {
|
|
26
|
+
const sortedNames = Array.from(names).sort(compareStrings).join(", ");
|
|
27
27
|
return `import { ${sortedNames} } from '${pkg}';`;
|
|
28
28
|
});
|
|
29
29
|
}
|
|
@@ -695,13 +695,18 @@ function escapeRegExp(value) {
|
|
|
695
695
|
}
|
|
696
696
|
|
|
697
697
|
// packages/dbx-cli/manifest-extract/src/lib/extract-models.ts
|
|
698
|
-
import { Node as Node4, Project as Project3 } from "ts-morph";
|
|
698
|
+
import { Node as Node4, SyntaxKind, Project as Project3 } from "ts-morph";
|
|
699
699
|
var READ_LEVEL_VALUES = /* @__PURE__ */ new Set(["system", "owner", "admin-only", "permissions"]);
|
|
700
700
|
var SERVICE_FACTORY_TAG = "dbxModelServiceFactory";
|
|
701
701
|
var MCP_TOOL_NAME_SEGMENT_TAG = "dbxModelMcpToolNameSegment";
|
|
702
702
|
var MODEL_TYPE_VALUE_PATTERN = /^[a-z][A-Za-z0-9_$]*$/;
|
|
703
703
|
var TOOL_NAME_SEGMENT_PATTERN = /^[A-Za-z][A-Za-z0-9_$]*$/;
|
|
704
704
|
var IDENTITY_FN = "firestoreModelIdentity";
|
|
705
|
+
var SINGLE_ITEM_COLLECTION_FN = "singleItemFirestoreCollection";
|
|
706
|
+
var ROOT_SINGLE_ITEM_COLLECTION_FN = "rootSingleItemFirestoreCollection";
|
|
707
|
+
var SINGLE_ITEM_COLLECTION_FN_NAMES = [SINGLE_ITEM_COLLECTION_FN, ROOT_SINGLE_ITEM_COLLECTION_FN];
|
|
708
|
+
var MODEL_IDENTITY_KEY = "modelIdentity";
|
|
709
|
+
var SINGLE_ITEM_IDENTIFIER_KEY = "singleItemIdentifier";
|
|
705
710
|
var CONVERTER_FN_NAMES = ["snapshotConverterFunctions", "firestoreSubObject", "firestoreObjectArray"];
|
|
706
711
|
var SUB_OBJECT_FN = "firestoreSubObject";
|
|
707
712
|
var OBJECT_ARRAY_FN = "firestoreObjectArray";
|
|
@@ -718,7 +723,9 @@ function extractModelsFromSource(input) {
|
|
|
718
723
|
const enums = readEnums(sourceFile);
|
|
719
724
|
const modelGroups = readModelGroups(sourceFile);
|
|
720
725
|
const serviceFactories = readServiceFactories(sourceFile);
|
|
721
|
-
|
|
726
|
+
const singleItemCollections = readSingleItemCollections(sourceFile);
|
|
727
|
+
const stringConstants = readStringConstants(sourceFile);
|
|
728
|
+
return { identities, interfaces, converters, enums, modelGroups, serviceFactories, singleItemCollections, stringConstants };
|
|
722
729
|
}
|
|
723
730
|
function readIdentities(sourceFile) {
|
|
724
731
|
const out = [];
|
|
@@ -838,6 +845,65 @@ function readServiceFactoryModelType(jsDocs) {
|
|
|
838
845
|
}
|
|
839
846
|
return result;
|
|
840
847
|
}
|
|
848
|
+
function readSingleItemCollections(sourceFile) {
|
|
849
|
+
const out = [];
|
|
850
|
+
const text = sourceFile.getFullText();
|
|
851
|
+
if (SINGLE_ITEM_COLLECTION_FN_NAMES.some((name) => text.includes(name))) {
|
|
852
|
+
for (const call of sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
853
|
+
const fnName = readCalleeName(call);
|
|
854
|
+
if (fnName !== SINGLE_ITEM_COLLECTION_FN && fnName !== ROOT_SINGLE_ITEM_COLLECTION_FN) continue;
|
|
855
|
+
const entry = buildSingleItemCollection(call, fnName === ROOT_SINGLE_ITEM_COLLECTION_FN);
|
|
856
|
+
if (entry) out.push(entry);
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
return out;
|
|
860
|
+
}
|
|
861
|
+
function readCalleeName(call) {
|
|
862
|
+
const expression = call.getExpression();
|
|
863
|
+
return Node4.isPropertyAccessExpression(expression) ? expression.getName() : expression.getText();
|
|
864
|
+
}
|
|
865
|
+
function buildSingleItemCollection(call, root) {
|
|
866
|
+
let result;
|
|
867
|
+
const args = call.getArguments();
|
|
868
|
+
if (args.length > 0) {
|
|
869
|
+
const config = args[0];
|
|
870
|
+
if (Node4.isObjectLiteralExpression(config)) {
|
|
871
|
+
const identityNode = readPropertyValue(config, MODEL_IDENTITY_KEY);
|
|
872
|
+
if (identityNode && Node4.isIdentifier(identityNode)) {
|
|
873
|
+
const identifierNode = readPropertyValue(config, SINGLE_ITEM_IDENTIFIER_KEY);
|
|
874
|
+
result = {
|
|
875
|
+
identityConst: identityNode.getText(),
|
|
876
|
+
root,
|
|
877
|
+
documentId: readStringLiteralText(identifierNode),
|
|
878
|
+
documentIdConstRef: identifierNode && Node4.isIdentifier(identifierNode) ? identifierNode.getText() : void 0,
|
|
879
|
+
explicitIdentifier: config.getProperty(SINGLE_ITEM_IDENTIFIER_KEY) !== void 0,
|
|
880
|
+
line: call.getStartLineNumber()
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
return result;
|
|
886
|
+
}
|
|
887
|
+
function readStringConstants(sourceFile) {
|
|
888
|
+
const out = [];
|
|
889
|
+
for (const statement of sourceFile.getVariableStatements()) {
|
|
890
|
+
if (!statement.isExported()) continue;
|
|
891
|
+
for (const decl of statement.getDeclarations()) {
|
|
892
|
+
const value = readStringLiteralText(decl.getInitializer());
|
|
893
|
+
if (value !== void 0) {
|
|
894
|
+
out.push({ name: decl.getName(), value });
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
return out;
|
|
899
|
+
}
|
|
900
|
+
function readStringLiteralText(node) {
|
|
901
|
+
let current = node;
|
|
902
|
+
while (current !== void 0 && Node4.isAsExpression(current)) {
|
|
903
|
+
current = current.getExpression();
|
|
904
|
+
}
|
|
905
|
+
return current !== void 0 && (Node4.isStringLiteral(current) || Node4.isNoSubstitutionTemplateLiteral(current)) ? current.getLiteralText() : void 0;
|
|
906
|
+
}
|
|
841
907
|
function readConverters(sourceFile) {
|
|
842
908
|
const out = [];
|
|
843
909
|
for (const statement of sourceFile.getVariableStatements()) {
|
|
@@ -1134,14 +1200,17 @@ function findModelFiles(packageRoot) {
|
|
|
1134
1200
|
const text = readFileSync6(filePath, "utf8");
|
|
1135
1201
|
if (!textHasModelMarker(text)) continue;
|
|
1136
1202
|
const extraction = extractModelsFromSource({ name: filePath, text });
|
|
1137
|
-
if (extraction
|
|
1203
|
+
if (!hasExtractedArtifact(extraction)) continue;
|
|
1138
1204
|
out.push({ filePath, extraction });
|
|
1139
1205
|
}
|
|
1140
1206
|
}
|
|
1141
1207
|
return out;
|
|
1142
1208
|
}
|
|
1143
1209
|
function textHasModelMarker(text) {
|
|
1144
|
-
return text.includes("firestoreModelIdentity(") || text.includes("@dbxModelGroup") || text.includes("snapshotConverterFunctions") || text.includes("firestoreSubObject") || text.includes("firestoreObjectArray") || text.includes("@dbxModelServiceFactory");
|
|
1210
|
+
return text.includes("firestoreModelIdentity(") || text.includes("@dbxModelGroup") || text.includes("snapshotConverterFunctions") || text.includes("firestoreSubObject") || text.includes("firestoreObjectArray") || text.includes("@dbxModelServiceFactory") || text.includes("singleItemFirestoreCollection") || text.includes("rootSingleItemFirestoreCollection") || text.includes("export enum ");
|
|
1211
|
+
}
|
|
1212
|
+
function hasExtractedArtifact(extraction) {
|
|
1213
|
+
return extraction.identities.length > 0 || extraction.modelGroups.length > 0 || extraction.converters.length > 0 || extraction.serviceFactories.length > 0 || extraction.singleItemCollections.length > 0 || extraction.enums.length > 0;
|
|
1145
1214
|
}
|
|
1146
1215
|
function* walkSourceFiles(dir) {
|
|
1147
1216
|
for (const entry of readdirSync2(dir).sort()) {
|
|
@@ -1156,7 +1225,7 @@ function* walkSourceFiles(dir) {
|
|
|
1156
1225
|
}
|
|
1157
1226
|
}
|
|
1158
1227
|
function isCandidateSourceFile(name) {
|
|
1159
|
-
return name.endsWith(".ts") && !name.endsWith(".api.ts") && !name.endsWith(".spec.ts") && !name.endsWith(".test.ts") && !name.endsWith(".
|
|
1228
|
+
return name.endsWith(".ts") && !name.endsWith(".api.ts") && !name.endsWith(".spec.ts") && !name.endsWith(".test.ts") && !name.endsWith(".d.ts");
|
|
1160
1229
|
}
|
|
1161
1230
|
function safeIsDirectory2(p) {
|
|
1162
1231
|
let result;
|
|
@@ -1172,6 +1241,8 @@ function safeIsDirectory2(p) {
|
|
|
1172
1241
|
var MAX_NESTED_DEPTH = 8;
|
|
1173
1242
|
var LONG_NAME_RE = /^[a-z][a-zA-Z0-9]*$/;
|
|
1174
1243
|
var ENUM_GENERIC_RE = /firestoreEnum<(\w+)>|optionalFirestoreEnum<(\w+)>/;
|
|
1244
|
+
var TS_TYPE_TOKEN_RE = /[A-Za-z_$][\w$]*/g;
|
|
1245
|
+
var DEFAULT_SINGLE_ITEM_DOCUMENT_ID = "0";
|
|
1175
1246
|
function assembleModels(input) {
|
|
1176
1247
|
const registries = buildGlobalRegistries(input.extractions);
|
|
1177
1248
|
const accumulator = { seen: /* @__PURE__ */ new Set(), entries: [] };
|
|
@@ -1185,7 +1256,7 @@ function collectModelEnums(input) {
|
|
|
1185
1256
|
const registry = buildEnumRegistry(input.extractions);
|
|
1186
1257
|
const referenced = collectReferencedEnumNames(input.models);
|
|
1187
1258
|
const out = {};
|
|
1188
|
-
for (const name of
|
|
1259
|
+
for (const name of Array.from(referenced).sort((a, b) => a.localeCompare(b))) {
|
|
1189
1260
|
const extracted = registry.get(name);
|
|
1190
1261
|
if (extracted) {
|
|
1191
1262
|
out[name] = buildModelEnumEntry(extracted);
|
|
@@ -1227,17 +1298,31 @@ function buildGlobalRegistries(extractions) {
|
|
|
1227
1298
|
const interfaceRegistry = /* @__PURE__ */ new Map();
|
|
1228
1299
|
const groupByModelName = /* @__PURE__ */ new Map();
|
|
1229
1300
|
const serviceFactoryByModelType = /* @__PURE__ */ new Map();
|
|
1301
|
+
const enumNames = /* @__PURE__ */ new Set();
|
|
1302
|
+
const singleItemByIdentityConst = /* @__PURE__ */ new Map();
|
|
1303
|
+
const identityByConst = /* @__PURE__ */ new Map();
|
|
1304
|
+
const stringConstants = /* @__PURE__ */ new Map();
|
|
1230
1305
|
for (const { extraction, sourceFile } of extractions) {
|
|
1231
1306
|
registerConverters(extraction.converters, converterRegistry);
|
|
1232
1307
|
registerInterfaces(extraction.interfaces, interfaceRegistry);
|
|
1233
1308
|
registerModelGroups(extraction.modelGroups, groupByModelName);
|
|
1309
|
+
for (const e of extraction.enums) enumNames.add(e.name);
|
|
1310
|
+
for (const identity of extraction.identities) {
|
|
1311
|
+
if (!identityByConst.has(identity.identityConst)) identityByConst.set(identity.identityConst, identity);
|
|
1312
|
+
}
|
|
1313
|
+
for (const single of extraction.singleItemCollections) {
|
|
1314
|
+
if (!singleItemByIdentityConst.has(single.identityConst)) singleItemByIdentityConst.set(single.identityConst, single);
|
|
1315
|
+
}
|
|
1316
|
+
for (const constant of extraction.stringConstants) {
|
|
1317
|
+
if (!stringConstants.has(constant.name)) stringConstants.set(constant.name, constant.value);
|
|
1318
|
+
}
|
|
1234
1319
|
for (const factory of extraction.serviceFactories) {
|
|
1235
1320
|
if (!serviceFactoryByModelType.has(factory.modelType)) {
|
|
1236
1321
|
serviceFactoryByModelType.set(factory.modelType, { exportName: factory.exportName, sourceFile });
|
|
1237
1322
|
}
|
|
1238
1323
|
}
|
|
1239
1324
|
}
|
|
1240
|
-
return { converterRegistry, interfaceRegistry, groupByModelName, serviceFactoryByModelType };
|
|
1325
|
+
return { converterRegistry, interfaceRegistry, groupByModelName, serviceFactoryByModelType, enumNames, singleItemByIdentityConst, identityByConst, stringConstants };
|
|
1241
1326
|
}
|
|
1242
1327
|
function registerConverters(converters, registry) {
|
|
1243
1328
|
for (const converter of converters) {
|
|
@@ -1259,10 +1344,9 @@ function registerModelGroups(modelGroups, registry) {
|
|
|
1259
1344
|
}
|
|
1260
1345
|
}
|
|
1261
1346
|
function appendEntriesFromSource(source, registries, accumulator) {
|
|
1262
|
-
const enumNames = new Set(source.extraction.enums.map((e) => e.name));
|
|
1263
1347
|
for (const identity of source.extraction.identities) {
|
|
1264
1348
|
if (accumulator.seen.has(identity.identityConst)) continue;
|
|
1265
|
-
const entry = buildEntryForIdentity({ identity, source, registries
|
|
1349
|
+
const entry = buildEntryForIdentity({ identity, source, registries });
|
|
1266
1350
|
if (entry) {
|
|
1267
1351
|
accumulator.seen.add(identity.identityConst);
|
|
1268
1352
|
accumulator.entries.push(entry);
|
|
@@ -1270,7 +1354,7 @@ function appendEntriesFromSource(source, registries, accumulator) {
|
|
|
1270
1354
|
}
|
|
1271
1355
|
}
|
|
1272
1356
|
function buildEntryForIdentity(input) {
|
|
1273
|
-
const { identity, source, registries
|
|
1357
|
+
const { identity, source, registries } = input;
|
|
1274
1358
|
let result;
|
|
1275
1359
|
if (identity.collectionPrefix !== void 0) {
|
|
1276
1360
|
const modelName = capitalize(identity.modelType);
|
|
@@ -1283,7 +1367,7 @@ function buildEntryForIdentity(input) {
|
|
|
1283
1367
|
iface,
|
|
1284
1368
|
interfaceRegistry: registries.interfaceRegistry,
|
|
1285
1369
|
converterRegistry: registries.converterRegistry,
|
|
1286
|
-
enumNames,
|
|
1370
|
+
enumNames: registries.enumNames,
|
|
1287
1371
|
depth: 0,
|
|
1288
1372
|
visitedConverters: /* @__PURE__ */ new Set()
|
|
1289
1373
|
});
|
|
@@ -1297,6 +1381,12 @@ function buildManifestEntry(input) {
|
|
|
1297
1381
|
const { identity, modelName, collectionPrefix, iface, fields, source, registries } = input;
|
|
1298
1382
|
const modelGroup = registries.groupByModelName.get(modelName);
|
|
1299
1383
|
const serviceFactory = registries.serviceFactoryByModelType.get(identity.modelType);
|
|
1384
|
+
const single = registries.singleItemByIdentityConst.get(identity.identityConst);
|
|
1385
|
+
const singleItemIdentifier = single ? resolveSingleItemDocumentId(single, registries) : void 0;
|
|
1386
|
+
if (single && singleItemIdentifier === void 0) {
|
|
1387
|
+
console.warn(`[single-item-id-unresolved] ${source.sourceFile}:${single.line} \xB7 ${identity.modelType} \u2192 singleItemIdentifier ${single.documentIdConstRef ?? "(expression)"} could not be resolved to a string; the model's fixed document id is omitted from the manifest`);
|
|
1388
|
+
}
|
|
1389
|
+
const exampleKey = buildExampleKey({ identity, collectionPrefix, singleItemIdentifier, registries });
|
|
1300
1390
|
return {
|
|
1301
1391
|
modelType: identity.modelType,
|
|
1302
1392
|
modelName,
|
|
@@ -1304,6 +1394,9 @@ function buildManifestEntry(input) {
|
|
|
1304
1394
|
identityConst: identity.identityConst,
|
|
1305
1395
|
collectionPrefix,
|
|
1306
1396
|
...identity.parentIdentityConst ? { parentIdentityConst: identity.parentIdentityConst } : {},
|
|
1397
|
+
...single ? { singleton: true } : {},
|
|
1398
|
+
...singleItemIdentifier === void 0 ? {} : { singleItemIdentifier },
|
|
1399
|
+
...exampleKey === void 0 ? {} : { exampleKey },
|
|
1307
1400
|
...iface.description ? { description: iface.description } : {},
|
|
1308
1401
|
sourcePackage: source.sourcePackage,
|
|
1309
1402
|
sourceFile: source.sourceFile,
|
|
@@ -1314,6 +1407,46 @@ function buildManifestEntry(input) {
|
|
|
1314
1407
|
...serviceFactory ? { serviceFactory } : {}
|
|
1315
1408
|
};
|
|
1316
1409
|
}
|
|
1410
|
+
function resolveSingleItemDocumentId(single, registries) {
|
|
1411
|
+
let result;
|
|
1412
|
+
if (!single.explicitIdentifier) {
|
|
1413
|
+
result = DEFAULT_SINGLE_ITEM_DOCUMENT_ID;
|
|
1414
|
+
} else if (single.documentId !== void 0) {
|
|
1415
|
+
result = single.documentId;
|
|
1416
|
+
} else if (single.documentIdConstRef !== void 0) {
|
|
1417
|
+
result = registries.stringConstants.get(single.documentIdConstRef);
|
|
1418
|
+
}
|
|
1419
|
+
return result;
|
|
1420
|
+
}
|
|
1421
|
+
function buildExampleKey(input) {
|
|
1422
|
+
const { identity, collectionPrefix, singleItemIdentifier, registries } = input;
|
|
1423
|
+
const segments = [`${collectionPrefix}/${singleItemIdentifier ?? modelIdPlaceholder(identity.modelType)}`];
|
|
1424
|
+
const visited = /* @__PURE__ */ new Set([identity.identityConst]);
|
|
1425
|
+
let parentConst = identity.parentIdentityConst;
|
|
1426
|
+
let resolved = true;
|
|
1427
|
+
while (resolved && parentConst !== void 0) {
|
|
1428
|
+
const parent = visited.has(parentConst) ? void 0 : registries.identityByConst.get(parentConst);
|
|
1429
|
+
if (parent === void 0) {
|
|
1430
|
+
resolved = false;
|
|
1431
|
+
} else {
|
|
1432
|
+
visited.add(parent.identityConst);
|
|
1433
|
+
segments.unshift(`${collectionNameFor(parent)}/${ancestorDocumentIdFor(parent, registries)}`);
|
|
1434
|
+
parentConst = parent.parentIdentityConst;
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
return resolved ? segments.join("/") : void 0;
|
|
1438
|
+
}
|
|
1439
|
+
function collectionNameFor(identity) {
|
|
1440
|
+
return identity.collectionPrefix ?? identity.modelType.toLowerCase();
|
|
1441
|
+
}
|
|
1442
|
+
function ancestorDocumentIdFor(identity, registries) {
|
|
1443
|
+
const single = registries.singleItemByIdentityConst.get(identity.identityConst);
|
|
1444
|
+
const documentId = single ? resolveSingleItemDocumentId(single, registries) : void 0;
|
|
1445
|
+
return documentId ?? modelIdPlaceholder(identity.modelType);
|
|
1446
|
+
}
|
|
1447
|
+
function modelIdPlaceholder(modelType) {
|
|
1448
|
+
return `<${modelType}Id>`;
|
|
1449
|
+
}
|
|
1317
1450
|
function findConverterForInterface(extraction, interfaceName) {
|
|
1318
1451
|
return extraction.converters.find((c) => c.interfaceName === interfaceName);
|
|
1319
1452
|
}
|
|
@@ -1425,11 +1558,13 @@ function resolveLongName(fieldName, propLongName) {
|
|
|
1425
1558
|
function resolveEnumRef(converter, tsType, enumNames) {
|
|
1426
1559
|
let result;
|
|
1427
1560
|
if (tsType) {
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1561
|
+
TS_TYPE_TOKEN_RE.lastIndex = 0;
|
|
1562
|
+
let match = TS_TYPE_TOKEN_RE.exec(tsType);
|
|
1563
|
+
while (result === void 0 && match !== null) {
|
|
1564
|
+
if (enumNames.has(match[0])) {
|
|
1565
|
+
result = match[0];
|
|
1566
|
+
} else {
|
|
1567
|
+
match = TS_TYPE_TOKEN_RE.exec(tsType);
|
|
1433
1568
|
}
|
|
1434
1569
|
}
|
|
1435
1570
|
}
|
|
@@ -1453,7 +1588,7 @@ function deriveValidatorName(paramsTypeName) {
|
|
|
1453
1588
|
|
|
1454
1589
|
// packages/dbx-cli/firebase-api-manifest/src/generate-api-manifest/emit.ts
|
|
1455
1590
|
async function renderManifest(input) {
|
|
1456
|
-
const { outputFile, entries, projectName, namespace, modelEntries, modelNamespace, enumEntries, enumNamespace, emitConverters = false } = input;
|
|
1591
|
+
const { outputFile, entries, projectName, namespace, modelEntries, modelNamespace, enumEntries, enumNamespace, emitConverters = false, generatorVersion } = input;
|
|
1457
1592
|
const validatorImports = [];
|
|
1458
1593
|
for (const entry of entries) {
|
|
1459
1594
|
if (!entry.packageName || !entry.validatorName) continue;
|
|
@@ -1463,7 +1598,7 @@ async function renderManifest(input) {
|
|
|
1463
1598
|
const entryLines = entries.map((e) => renderEntry(e));
|
|
1464
1599
|
const emitModels = Boolean(modelEntries && modelEntries.length > 0 && modelNamespace);
|
|
1465
1600
|
const emitEnums = Boolean(enumEntries && Object.keys(enumEntries).length > 0 && enumNamespace);
|
|
1466
|
-
const dbxCliTypeNames = ["type CliApiManifest", ...emitModels ? ["type CliModelManifest"] : [], ...emitEnums ? ["type CliEnumManifest"] : []];
|
|
1601
|
+
const dbxCliTypeNames = ["type CliApiManifest", "type CliGeneratedManifestStamp", ...emitModels ? ["type CliModelManifest"] : [], ...emitEnums ? ["type CliEnumManifest"] : []];
|
|
1467
1602
|
const dbxCliTypeImports = `import { ${dbxCliTypeNames.join(", ")} } from '@dereekb/dbx-cli';`;
|
|
1468
1603
|
const modelSection = emitModels ? `
|
|
1469
1604
|
|
|
@@ -1482,6 +1617,8 @@ export const ${enumNamespace}: CliEnumManifest = ${renderEnumManifest(enumEntrie
|
|
|
1482
1617
|
${importLines.join("\n")}
|
|
1483
1618
|
${dbxCliTypeImports}
|
|
1484
1619
|
|
|
1620
|
+
export const ${namespace}_STAMP: CliGeneratedManifestStamp = { generatorVersion: ${JSON.stringify(generatorVersion)} };
|
|
1621
|
+
|
|
1485
1622
|
export const ${namespace}: CliApiManifest = [
|
|
1486
1623
|
${entryLines.join(",\n")}
|
|
1487
1624
|
];${modelSection}${enumSection}
|
|
@@ -1525,6 +1662,9 @@ function renderModelEntry(entry, emitConverters) {
|
|
|
1525
1662
|
`identityConst: ${JSON.stringify(entry.identityConst)}`,
|
|
1526
1663
|
`collectionPrefix: ${JSON.stringify(entry.collectionPrefix)}`,
|
|
1527
1664
|
entry.parentIdentityConst ? `parentIdentityConst: ${JSON.stringify(entry.parentIdentityConst)}` : void 0,
|
|
1665
|
+
entry.singleton ? "singleton: true" : void 0,
|
|
1666
|
+
entry.singleItemIdentifier === void 0 ? void 0 : `singleItemIdentifier: ${JSON.stringify(entry.singleItemIdentifier)}`,
|
|
1667
|
+
entry.exampleKey === void 0 ? void 0 : `exampleKey: ${JSON.stringify(entry.exampleKey)}`,
|
|
1528
1668
|
entry.description ? `description: ${JSON.stringify(entry.description)}` : void 0,
|
|
1529
1669
|
`sourcePackage: ${JSON.stringify(entry.sourcePackage)}`,
|
|
1530
1670
|
`sourceFile: ${JSON.stringify(entry.sourceFile)}`,
|
|
@@ -1580,6 +1720,22 @@ function renderModelField(field, emitConverters) {
|
|
|
1580
1720
|
return `{ ${parts.filter(Boolean).join(", ")} }`;
|
|
1581
1721
|
}
|
|
1582
1722
|
|
|
1723
|
+
// packages/dbx-cli/firebase-api-manifest/package.json
|
|
1724
|
+
var package_default = {
|
|
1725
|
+
name: "@dereekb/dbx-cli-firebase-api-manifest",
|
|
1726
|
+
version: "13.43.0",
|
|
1727
|
+
private: true,
|
|
1728
|
+
type: "module",
|
|
1729
|
+
devDependencies: {
|
|
1730
|
+
"ts-morph": "^21.0.0"
|
|
1731
|
+
},
|
|
1732
|
+
peerDependencies: {
|
|
1733
|
+
"@dereekb/dbx-cli": "13.43.0",
|
|
1734
|
+
"@dereekb/util": "13.43.0",
|
|
1735
|
+
prettier: "3.8.3"
|
|
1736
|
+
}
|
|
1737
|
+
};
|
|
1738
|
+
|
|
1583
1739
|
// packages/dbx-cli/firebase-api-manifest/src/generate-api-manifest/main.ts
|
|
1584
1740
|
var WORKSPACE_ROOT = process.cwd();
|
|
1585
1741
|
async function main() {
|
|
@@ -1670,7 +1826,7 @@ async function main() {
|
|
|
1670
1826
|
const modelEntries = flags.emitModels ? assembleModels({ extractions: modelSources }) : [];
|
|
1671
1827
|
const filteredModelEntries = flags.only ? modelEntries.filter((m) => flags.only?.has(m.modelType)) : modelEntries;
|
|
1672
1828
|
const enumEntries = flags.emitModels ? collectModelEnums({ extractions: modelSources, models: filteredModelEntries }) : {};
|
|
1673
|
-
const formatted = await renderManifest({ outputFile, entries: collected, projectName, namespace, modelEntries: filteredModelEntries, modelNamespace: deriveModelNamespace(flags.project), enumEntries, enumNamespace: deriveEnumNamespace(flags.project), emitConverters: flags.emitModelConverters });
|
|
1829
|
+
const formatted = await renderManifest({ outputFile, entries: collected, projectName, namespace, modelEntries: filteredModelEntries, modelNamespace: deriveModelNamespace(flags.project), enumEntries, enumNamespace: deriveEnumNamespace(flags.project), emitConverters: flags.emitModelConverters, generatorVersion: package_default.version });
|
|
1674
1830
|
const outcome = writeGeneratedTsFile({ outputFile, contents: formatted });
|
|
1675
1831
|
console.log(`[${outcome}] ${relative2(WORKSPACE_ROOT, outputFile)}`);
|
|
1676
1832
|
const groupCount = packageCache.size === 0 ? 0 : new Set(collected.map((c) => c.entry.groupName)).size;
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dereekb/dbx-cli-firebase-api-manifest",
|
|
3
|
-
"version": "13.
|
|
3
|
+
"version": "13.43.0",
|
|
4
4
|
"private": true,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"devDependencies": {
|
|
7
7
|
"ts-morph": "^21.0.0"
|
|
8
8
|
},
|
|
9
9
|
"peerDependencies": {
|
|
10
|
-
"@dereekb/dbx-cli": "13.
|
|
11
|
-
"@dereekb/util": "13.
|
|
10
|
+
"@dereekb/dbx-cli": "13.43.0",
|
|
11
|
+
"@dereekb/util": "13.43.0",
|
|
12
12
|
"prettier": "3.8.3"
|
|
13
13
|
}
|
|
14
14
|
}
|
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `generate-firestore-indexes` subcommand entry point.
|
|
3
3
|
*
|
|
4
|
-
* Walks
|
|
5
|
-
* tagged factories (the same pipeline
|
|
6
|
-
* runs the analyzer, and emits a
|
|
7
|
-
* payload via
|
|
4
|
+
* Walks one or more `-firebase` components/packages for
|
|
5
|
+
* `@dbxModelFirebaseIndex`-tagged factories (the same pipeline
|
|
6
|
+
* `scan-model-firebase-indexes` uses), runs the analyzer, and emits a
|
|
7
|
+
* canonical `firestore.indexes.json` payload via
|
|
8
|
+
* {@link generateFirestoreIndexesJson}.
|
|
9
|
+
*
|
|
10
|
+
* `--component` is repeatable, and that is the supported way to derive a
|
|
11
|
+
* single deployed indexes file from several packages. Firebase deploys one
|
|
12
|
+
* indexes file per database, and nothing merges per-package files — a
|
|
13
|
+
* package's composites otherwise survive in a downstream file only by the
|
|
14
|
+
* "collections no factory touches" preservation rule, which silently drops
|
|
15
|
+
* them the moment a local factory touches the same collection.
|
|
8
16
|
*
|
|
9
17
|
* Two modes:
|
|
10
18
|
*
|
|
@@ -5,8 +5,14 @@
|
|
|
5
5
|
* extractor and decides, per `(collection, scope, constraintSequence)`,
|
|
6
6
|
* whether the query requires a composite index, a `fieldOverrides[]`
|
|
7
7
|
* variant, or neither (because Firestore's automatic single-field
|
|
8
|
-
* `COLLECTION`-scope index already covers the query).
|
|
9
|
-
*
|
|
8
|
+
* `COLLECTION`-scope index already covers the query).
|
|
9
|
+
*
|
|
10
|
+
* "Neither" also covers a multi-field `COLLECTION`-scope sequence made up
|
|
11
|
+
* purely of `==` filters: Firestore merges the automatic single-field
|
|
12
|
+
* indexes to serve those, so emitting a composite would only add write
|
|
13
|
+
* latency and storage. See {@link isEqualityOnlySequence}.
|
|
14
|
+
*
|
|
15
|
+
* Encodes Firestore's field-order rule for composites:
|
|
10
16
|
*
|
|
11
17
|
* 1. Equality (`==`, `in`) fields first, in source order.
|
|
12
18
|
* 2. A single range/inequality field next (direction taken from any
|
|
@@ -51,6 +57,10 @@ export type AnalyzerWarning = {
|
|
|
51
57
|
readonly kind: 'unsupported-array-contains-any';
|
|
52
58
|
readonly factoryName: string;
|
|
53
59
|
readonly field: string;
|
|
60
|
+
} | {
|
|
61
|
+
readonly kind: 'equality-only-composite-skipped';
|
|
62
|
+
readonly factoryName: string;
|
|
63
|
+
readonly fields: readonly string[];
|
|
54
64
|
};
|
|
55
65
|
/**
|
|
56
66
|
* Runs the analyzer over every extracted entry.
|
|
@@ -9,15 +9,15 @@ import { isAbsolute as isAbsolute2, relative as relative2, resolve as resolve3 }
|
|
|
9
9
|
// packages/dbx-cli/firestore-query-manifest/package.json
|
|
10
10
|
var package_default = {
|
|
11
11
|
name: "@dereekb/dbx-cli-firestore-query-manifest",
|
|
12
|
-
version: "13.
|
|
12
|
+
version: "13.43.0",
|
|
13
13
|
private: true,
|
|
14
14
|
type: "module",
|
|
15
15
|
devDependencies: {
|
|
16
16
|
eslint: "10.4.0"
|
|
17
17
|
},
|
|
18
18
|
peerDependencies: {
|
|
19
|
-
"@dereekb/dbx-cli": "13.
|
|
20
|
-
"@dereekb/util": "13.
|
|
19
|
+
"@dereekb/dbx-cli": "13.43.0",
|
|
20
|
+
"@dereekb/util": "13.43.0"
|
|
21
21
|
}
|
|
22
22
|
};
|
|
23
23
|
|
|
@@ -37,8 +37,8 @@ function renderGroupedImportLines(imports) {
|
|
|
37
37
|
set.add(identifier);
|
|
38
38
|
importsByPackage.set(packageName, set);
|
|
39
39
|
}
|
|
40
|
-
return
|
|
41
|
-
const sortedNames =
|
|
40
|
+
return Array.from(importsByPackage.entries()).sort(([a], [b]) => a.localeCompare(b)).map(([pkg, names]) => {
|
|
41
|
+
const sortedNames = Array.from(names).sort(compareStrings).join(", ");
|
|
42
42
|
return `import { ${sortedNames} } from '${pkg}';`;
|
|
43
43
|
});
|
|
44
44
|
}
|
|
@@ -82,9 +82,9 @@ function scanFirestoreRules(source) {
|
|
|
82
82
|
if (allow.ops.has("list") || allow.ops.has("read")) current.list = mergeAccess(current.list, allow.access);
|
|
83
83
|
}
|
|
84
84
|
});
|
|
85
|
-
const collections =
|
|
85
|
+
const collections = Array.from(accumulated.entries()).map(([collection, value]) => ({
|
|
86
86
|
collection,
|
|
87
|
-
paths:
|
|
87
|
+
paths: Array.from(value.paths).sort(),
|
|
88
88
|
get: value.get,
|
|
89
89
|
list: value.list,
|
|
90
90
|
collectionGroup: value.collectionGroup,
|
|
@@ -294,7 +294,7 @@ function parentPathsForCollection(scan, collection) {
|
|
|
294
294
|
parents.add(parentSegments.join("/"));
|
|
295
295
|
}
|
|
296
296
|
}
|
|
297
|
-
return
|
|
297
|
+
return Array.from(parents);
|
|
298
298
|
}
|
|
299
299
|
|
|
300
300
|
// packages/dbx-cli/src/lib/scan-helpers/exported-from-package.ts
|
|
@@ -422,7 +422,7 @@ function bindQueryFactories(input) {
|
|
|
422
422
|
|
|
423
423
|
// packages/dbx-cli/firestore-query-manifest/src/emit.ts
|
|
424
424
|
async function renderQueryManifest(input) {
|
|
425
|
-
const { outputFile, entries, projectName, namespace } = input;
|
|
425
|
+
const { outputFile, entries, projectName, namespace, generatorVersion } = input;
|
|
426
426
|
const factoryImports = entries.filter((x) => x.bound).map((x) => ({ packageName: x.entry.module, identifier: x.entry.name }));
|
|
427
427
|
const importLines = renderGroupedImportLines(factoryImports);
|
|
428
428
|
const sorted = [...entries].sort((a, b) => compareStrings(a.entry.slug, b.entry.slug));
|
|
@@ -431,7 +431,9 @@ async function renderQueryManifest(input) {
|
|
|
431
431
|
// Run \`npx nx run ${projectName}:generate-firestore-query-manifest\` to refresh.
|
|
432
432
|
|
|
433
433
|
${importLines.join("\n")}
|
|
434
|
-
import { type CliFirestoreQueryManifest } from '@dereekb/dbx-cli';
|
|
434
|
+
import { type CliFirestoreQueryManifest, type CliGeneratedManifestStamp } from '@dereekb/dbx-cli';
|
|
435
|
+
|
|
436
|
+
export const ${namespace}_STAMP: CliGeneratedManifestStamp = { generatorVersion: ${JSON.stringify(generatorVersion)} };
|
|
435
437
|
|
|
436
438
|
export const ${namespace}: CliFirestoreQueryManifest = [
|
|
437
439
|
${sorted.map((x) => renderEntry(x)).join(",\n")}
|
|
@@ -670,11 +672,18 @@ function analyzeSequence(input) {
|
|
|
670
672
|
if (isSingleFieldQuery(buckets)) {
|
|
671
673
|
return analyzeSingleField({ buckets, collection, scope, warnings });
|
|
672
674
|
}
|
|
675
|
+
if (scope === "COLLECTION" && isEqualityOnlySequence(buckets)) {
|
|
676
|
+
warnings.push({ kind: "equality-only-composite-skipped", factoryName, fields: buckets.equalities.map((e) => e.fieldPath) });
|
|
677
|
+
return { kind: "auto", warnings };
|
|
678
|
+
}
|
|
673
679
|
return analyzeMultiField({ buckets, sequenceEntries: sequence.entries, collection, scope, factoryName, warnings });
|
|
674
680
|
}
|
|
675
681
|
function isSingleFieldQuery(buckets) {
|
|
676
682
|
return buckets.distinctFieldCount === 1;
|
|
677
683
|
}
|
|
684
|
+
function isEqualityOnlySequence(buckets) {
|
|
685
|
+
return buckets.ranges.length === 0 && buckets.arrays.length === 0 && buckets.orderBys.length === 0 && buckets.equalities.length > 0 && buckets.equalities.every((e) => (e.operator ?? "==") === "==");
|
|
686
|
+
}
|
|
678
687
|
function analyzeSingleField(input) {
|
|
679
688
|
const { buckets, collection, scope, warnings } = input;
|
|
680
689
|
if (scope === "COLLECTION") {
|
|
@@ -769,7 +778,7 @@ function collectOrderByDirections(input) {
|
|
|
769
778
|
}
|
|
770
779
|
}
|
|
771
780
|
for (const [field, dirs] of conflicts) {
|
|
772
|
-
warnings.push({ kind: "orderby-conflict", factoryName, field, directions:
|
|
781
|
+
warnings.push({ kind: "orderby-conflict", factoryName, field, directions: Array.from(dirs) });
|
|
773
782
|
}
|
|
774
783
|
return out;
|
|
775
784
|
}
|
|
@@ -1250,7 +1259,7 @@ function composeEntry(input) {
|
|
|
1250
1259
|
}
|
|
1251
1260
|
function resolveConstraintSequences(input) {
|
|
1252
1261
|
const { candidate, tags, name, line, filePath, warnings } = input;
|
|
1253
|
-
const bodyResult = extractConstraintsFromBody({ decl: candidate.decl, factoryName: name, filePath, dispatcher: tags.dispatcher });
|
|
1262
|
+
const bodyResult = extractConstraintsFromBody({ decl: candidate.decl, factoryName: name, filePath, dispatcher: tags.dispatcher, suppressed: tags.skip || tags.specOnly });
|
|
1254
1263
|
for (const warning of bodyResult.warnings) {
|
|
1255
1264
|
warnings.push(warning);
|
|
1256
1265
|
}
|
|
@@ -1331,11 +1340,14 @@ function buildConstraintSequences(input) {
|
|
|
1331
1340
|
return sequences;
|
|
1332
1341
|
}
|
|
1333
1342
|
function extractConstraintsFromBody(input) {
|
|
1334
|
-
const { decl, factoryName, filePath, dispatcher } = input;
|
|
1343
|
+
const { decl, factoryName, filePath, dispatcher, suppressed } = input;
|
|
1335
1344
|
const entries = [];
|
|
1336
1345
|
const conditionalFieldSet = /* @__PURE__ */ new Set();
|
|
1337
1346
|
const warnings = [];
|
|
1338
1347
|
const body = decl.getBody();
|
|
1348
|
+
if (suppressed) {
|
|
1349
|
+
return { entries, conditionalFields: [], warnings, skipped: true, dispatcherDelegates: [] };
|
|
1350
|
+
}
|
|
1339
1351
|
if (dispatcher) {
|
|
1340
1352
|
const violation = body === void 0 ? void 0 : findFirstConstraintCall(body);
|
|
1341
1353
|
if (violation !== void 0) {
|
|
@@ -2235,7 +2247,7 @@ async function main() {
|
|
|
2235
2247
|
for (const warning of queryModes.warnings) {
|
|
2236
2248
|
console.warn(warning);
|
|
2237
2249
|
}
|
|
2238
|
-
const formatted = await renderQueryManifest({ outputFile, entries: queryModes.entries, projectName, namespace });
|
|
2250
|
+
const formatted = await renderQueryManifest({ outputFile, entries: queryModes.entries, projectName, namespace, generatorVersion: package_default.version });
|
|
2239
2251
|
const relOutput = relative2(WORKSPACE_ROOT, outputFile);
|
|
2240
2252
|
if (flags.check) {
|
|
2241
2253
|
const current = existsSync3(outputFile) ? readFileSync3(outputFile, "utf8") : void 0;
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dereekb/dbx-cli-firestore-query-manifest",
|
|
3
|
-
"version": "13.
|
|
3
|
+
"version": "13.43.0",
|
|
4
4
|
"private": true,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"devDependencies": {
|
|
7
7
|
"eslint": "10.4.0"
|
|
8
8
|
},
|
|
9
9
|
"peerDependencies": {
|
|
10
|
-
"@dereekb/dbx-cli": "13.
|
|
11
|
-
"@dereekb/util": "13.
|
|
10
|
+
"@dereekb/dbx-cli": "13.43.0",
|
|
11
|
+
"@dereekb/util": "13.43.0"
|
|
12
12
|
}
|
|
13
13
|
}
|