@dereekb/dbx-cli 13.40.0 → 13.42.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 (35) hide show
  1. package/eslint/package.json +3 -3
  2. package/firebase-api-manifest/main.js +156 -18
  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 +306 -12
  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 +1389 -933
  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/firestore-session.check.d.ts +23 -2
  25. package/src/lib/firestore/firestore.collection.d.ts +29 -0
  26. package/src/lib/firestore/firestore.query.d.ts +1 -1
  27. package/src/lib/firestore/index.d.ts +1 -0
  28. package/src/lib/firestore/query-info-utils.d.ts +6 -1
  29. package/src/lib/firestore/query-mode.d.ts +120 -0
  30. package/src/lib/manifest/types.d.ts +161 -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/test/package.json +9 -9
  34. package/validate/index.js +12 -7
  35. package/validate/package.json +3 -3
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@dereekb/dbx-cli/eslint",
3
- "version": "13.40.0",
3
+ "version": "13.42.0",
4
4
  "type": "module",
5
5
  "peerDependencies": {
6
- "@dereekb/dbx-cli": "13.40.0",
7
- "@dereekb/util": "13.40.0",
6
+ "@dereekb/dbx-cli": "13.42.0",
7
+ "@dereekb/util": "13.42.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
  }
@@ -1525,6 +1660,9 @@ function renderModelEntry(entry, emitConverters) {
1525
1660
  `identityConst: ${JSON.stringify(entry.identityConst)}`,
1526
1661
  `collectionPrefix: ${JSON.stringify(entry.collectionPrefix)}`,
1527
1662
  entry.parentIdentityConst ? `parentIdentityConst: ${JSON.stringify(entry.parentIdentityConst)}` : void 0,
1663
+ entry.singleton ? "singleton: true" : void 0,
1664
+ entry.singleItemIdentifier === void 0 ? void 0 : `singleItemIdentifier: ${JSON.stringify(entry.singleItemIdentifier)}`,
1665
+ entry.exampleKey === void 0 ? void 0 : `exampleKey: ${JSON.stringify(entry.exampleKey)}`,
1528
1666
  entry.description ? `description: ${JSON.stringify(entry.description)}` : void 0,
1529
1667
  `sourcePackage: ${JSON.stringify(entry.sourcePackage)}`,
1530
1668
  `sourceFile: ${JSON.stringify(entry.sourceFile)}`,
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "@dereekb/dbx-cli-firebase-api-manifest",
3
- "version": "13.40.0",
3
+ "version": "13.42.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.40.0",
11
- "@dereekb/util": "13.40.0",
10
+ "@dereekb/dbx-cli": "13.42.0",
11
+ "@dereekb/util": "13.42.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.