@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.
Files changed (38) hide show
  1. package/eslint/package.json +3 -3
  2. package/firebase-api-manifest/main.js +177 -21
  3. package/firebase-api-manifest/package.json +3 -3
  4. package/firestore-indexes/src/generate-firestore-indexes-cli.d.ts +12 -4
  5. package/firestore-indexes/src/model-firebase-index-analyze.d.ts +12 -2
  6. package/firestore-query-manifest/main.js +26 -14
  7. package/firestore-query-manifest/package.json +3 -3
  8. package/generate-firestore-indexes/main.js +118 -20
  9. package/generate-firestore-indexes/package.json +2 -2
  10. package/generate-mcp-manifest/main.js +5 -2
  11. package/generate-mcp-manifest/package.json +3 -3
  12. package/generate-route-manifest/main.js +1 -1
  13. package/generate-route-manifest/package.json +2 -2
  14. package/index.esm.js +2651 -1387
  15. package/lint-cache/package.json +2 -2
  16. package/manifest-extract/index.esm.js +148 -2
  17. package/manifest-extract/package.json +2 -2
  18. package/manifest-extract/src/lib/extract-models.d.ts +4 -2
  19. package/manifest-extract/src/lib/types.d.ts +65 -0
  20. package/model-test/index.esm.js +4 -54
  21. package/model-test/package.json +2 -2
  22. package/package.json +6 -6
  23. package/route/package.json +7 -7
  24. package/src/lib/doctor/build-drift.check.d.ts +21 -0
  25. package/src/lib/doctor/doctor.command.factory.d.ts +21 -2
  26. package/src/lib/doctor/firestore-session.check.d.ts +7 -1
  27. package/src/lib/doctor/index.d.ts +1 -0
  28. package/src/lib/firestore/firestore.sdk-identity.d.ts +193 -0
  29. package/src/lib/firestore/index.d.ts +1 -0
  30. package/src/lib/manifest/types.d.ts +73 -2
  31. package/src/lib/mcp-scan/manifest/css-utilities-schema.d.ts +2 -2
  32. package/src/lib/mcp-scan/manifest/tokens-schema.d.ts +2 -2
  33. package/src/lib/runner/build-stamp.d.ts +138 -0
  34. package/src/lib/runner/index.d.ts +1 -0
  35. package/src/lib/runner/run.d.ts +13 -0
  36. package/test/package.json +9 -9
  37. package/validate/index.js +17 -7
  38. package/validate/package.json +3 -3
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@dereekb/dbx-cli/eslint",
3
- "version": "13.41.0",
3
+ "version": "13.43.0",
4
4
  "type": "module",
5
5
  "peerDependencies": {
6
- "@dereekb/dbx-cli": "13.41.0",
7
- "@dereekb/util": "13.41.0",
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 [...importsByPackage.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([pkg, names]) => {
26
- const sortedNames = [...names].sort(compareStrings).join(", ");
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
- return { identities, interfaces, converters, enums, modelGroups, serviceFactories };
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.identities.length === 0 && extraction.modelGroups.length === 0 && extraction.converters.length === 0 && extraction.serviceFactories.length === 0) continue;
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(".id.ts") && !name.endsWith(".d.ts");
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 [...referenced].sort((a, b) => a.localeCompare(b))) {
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, enumNames });
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, enumNames } = input;
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
- for (const name of enumNames) {
1429
- const re = new RegExp(String.raw`\b${name}\b`);
1430
- if (re.test(tsType)) {
1431
- result = name;
1432
- break;
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.41.0",
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.41.0",
11
- "@dereekb/util": "13.41.0",
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 a downstream `-firebase` component for `@dbxModelFirebaseIndex`-
5
- * tagged factories (the same pipeline `scan-model-firebase-indexes` uses),
6
- * runs the analyzer, and emits a canonical `firestore.indexes.json`
7
- * payload via {@link generateFirestoreIndexesJson}.
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). Encodes Firestore's
9
- * field-order rule for composites:
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.41.0",
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.41.0",
20
- "@dereekb/util": "13.41.0"
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 [...importsByPackage.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([pkg, names]) => {
41
- const sortedNames = [...names].sort(compareStrings).join(", ");
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 = [...accumulated.entries()].map(([collection, value]) => ({
85
+ const collections = Array.from(accumulated.entries()).map(([collection, value]) => ({
86
86
  collection,
87
- paths: [...value.paths].sort(),
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 [...parents];
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: [...dirs] });
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.41.0",
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.41.0",
11
- "@dereekb/util": "13.41.0"
10
+ "@dereekb/dbx-cli": "13.43.0",
11
+ "@dereekb/util": "13.43.0"
12
12
  }
13
13
  }