@dereekb/firebase 13.12.4 → 13.12.5

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.
@@ -17229,6 +17229,415 @@ function reportCompositeKeyTag(ctx, tag, allowedEncodings) {
17229
17229
  }
17230
17230
  };
17231
17231
 
17232
+ var SPEC_SUFFIX = '.spec.ts';
17233
+ /**
17234
+ * Classifies a spec filename against the conventions for the given parent
17235
+ * folder. Pure function — never touches the filesystem.
17236
+ *
17237
+ * @param config - Inputs.
17238
+ * @param config.filename - Bare filename including the `.spec.ts` suffix
17239
+ * (e.g. `job.scenario.requirement.spec.ts`).
17240
+ * @param config.parentFolderName - Name of the directory containing the file
17241
+ * (e.g. `job`). Used to detect cross-group misplacement.
17242
+ * @returns The classification.
17243
+ */ function classifySpecFile(config) {
17244
+ var filename = config.filename, parentFolderName = config.parentFolderName;
17245
+ var result;
17246
+ if (!filename.endsWith(SPEC_SUFFIX)) {
17247
+ result = {
17248
+ filename: filename,
17249
+ group: '',
17250
+ kind: 'non-spec',
17251
+ subgroups: [],
17252
+ isCanonical: false
17253
+ };
17254
+ } else {
17255
+ var _parts_;
17256
+ var stem = filename.slice(0, -SPEC_SUFFIX.length);
17257
+ var parts = stem.split('.');
17258
+ var group = (_parts_ = parts[0]) !== null && _parts_ !== void 0 ? _parts_ : '';
17259
+ if (group !== parentFolderName) {
17260
+ result = {
17261
+ filename: filename,
17262
+ group: group,
17263
+ kind: 'non-group',
17264
+ subgroups: [],
17265
+ isCanonical: false
17266
+ };
17267
+ } else {
17268
+ var rest = parts.slice(1);
17269
+ result = classifyRemainingSegments({
17270
+ filename: filename,
17271
+ group: group,
17272
+ rest: rest
17273
+ });
17274
+ }
17275
+ }
17276
+ return result;
17277
+ }
17278
+ function classifyRemainingSegments(config) {
17279
+ var filename = config.filename, group = config.group, rest = config.rest;
17280
+ var result;
17281
+ var crudIdx = rest.indexOf('crud');
17282
+ var scenarioIdx = rest.indexOf('scenario');
17283
+ if (crudIdx === 0) {
17284
+ if (rest.length === 1) {
17285
+ result = {
17286
+ filename: filename,
17287
+ group: group,
17288
+ kind: 'crud',
17289
+ subgroups: [],
17290
+ isCanonical: true
17291
+ };
17292
+ } else {
17293
+ result = {
17294
+ filename: filename,
17295
+ group: group,
17296
+ kind: 'crud-subgroup',
17297
+ subgroups: rest.slice(1),
17298
+ isCanonical: true
17299
+ };
17300
+ }
17301
+ } else if (scenarioIdx === 0) {
17302
+ if (rest.length === 1) {
17303
+ result = {
17304
+ filename: filename,
17305
+ group: group,
17306
+ kind: 'scenario',
17307
+ subgroups: [],
17308
+ isCanonical: true
17309
+ };
17310
+ } else {
17311
+ result = {
17312
+ filename: filename,
17313
+ group: group,
17314
+ kind: 'scenario-subgroup',
17315
+ subgroups: rest.slice(1),
17316
+ isCanonical: true
17317
+ };
17318
+ }
17319
+ } else if (crudIdx > 0) {
17320
+ var subgroups = rest.filter(function(_, i) {
17321
+ return i !== crudIdx;
17322
+ });
17323
+ var recommendedRename = buildCanonicalFilename({
17324
+ group: group,
17325
+ bucket: 'crud',
17326
+ subgroups: subgroups
17327
+ });
17328
+ result = {
17329
+ filename: filename,
17330
+ group: group,
17331
+ kind: 'crud-misplaced',
17332
+ subgroups: subgroups,
17333
+ isCanonical: false,
17334
+ recommendedRename: recommendedRename,
17335
+ driftReason: '`crud` segment is not directly after the group name.'
17336
+ };
17337
+ } else if (scenarioIdx > 0) {
17338
+ var subgroups1 = rest.filter(function(_, i) {
17339
+ return i !== scenarioIdx;
17340
+ });
17341
+ var recommendedRename1 = buildCanonicalFilename({
17342
+ group: group,
17343
+ bucket: 'scenario',
17344
+ subgroups: subgroups1
17345
+ });
17346
+ result = {
17347
+ filename: filename,
17348
+ group: group,
17349
+ kind: 'scenario-misplaced',
17350
+ subgroups: subgroups1,
17351
+ isCanonical: false,
17352
+ recommendedRename: recommendedRename1,
17353
+ driftReason: '`scenario` segment is not directly after the group name.'
17354
+ };
17355
+ } else if (rest.length === 0) {
17356
+ var recommendedRename2 = buildCanonicalFilename({
17357
+ group: group,
17358
+ bucket: 'scenario',
17359
+ subgroups: []
17360
+ });
17361
+ result = {
17362
+ filename: filename,
17363
+ group: group,
17364
+ kind: 'no-bucket',
17365
+ subgroups: [],
17366
+ isCanonical: false,
17367
+ recommendedRename: recommendedRename2,
17368
+ driftReason: 'Missing `crud` or `scenario` segment.'
17369
+ };
17370
+ } else {
17371
+ var recommendedRename3 = buildCanonicalFilename({
17372
+ group: group,
17373
+ bucket: 'scenario',
17374
+ subgroups: rest
17375
+ });
17376
+ result = {
17377
+ filename: filename,
17378
+ group: group,
17379
+ kind: 'no-bucket',
17380
+ subgroups: rest,
17381
+ isCanonical: false,
17382
+ recommendedRename: recommendedRename3,
17383
+ driftReason: 'Missing `crud` or `scenario` segment — defaulting suggestion to `scenario`.'
17384
+ };
17385
+ }
17386
+ return result;
17387
+ }
17388
+ /**
17389
+ * Renders a canonical spec filename for the given group + bucket + subgroup
17390
+ * chain. Pure data — used both by drift remediation and by the
17391
+ * `recommendSpecPath()` helper below.
17392
+ *
17393
+ * @param config - Inputs.
17394
+ * @param config.group - The model-group name (e.g. `job`).
17395
+ * @param config.bucket - `crud` or `scenario`.
17396
+ * @param config.subgroups - Optional ordered subgroup segments
17397
+ * (e.g. `['requirement','worker']`).
17398
+ * @returns The canonical filename (e.g. `job.scenario.requirement.worker.spec.ts`).
17399
+ */ function buildCanonicalFilename(config) {
17400
+ var group = config.group, bucket = config.bucket, subgroups = config.subgroups;
17401
+ var tail = subgroups.length === 0 ? '' : ".".concat(subgroups.join('.'));
17402
+ return "".concat(group, ".").concat(bucket).concat(tail, ".spec.ts");
17403
+ }
17404
+
17405
+ /**
17406
+ * Default subpath segment (relative to an app source root) below which the
17407
+ * rule expects model-group function folders. Matches the convention used by
17408
+ * `<apiDir>/src/app/function/<group>/`.
17409
+ */ var DEFAULT_FUNCTION_DIR_SEGMENT = 'src/app/function';
17410
+ function matchFunctionSpecPath(filename, functionDirSegment) {
17411
+ var result;
17412
+ if (filename.endsWith('.spec.ts')) {
17413
+ var normalized = filename.split(node_path.sep).join('/');
17414
+ var marker = "/".concat(functionDirSegment, "/");
17415
+ var markerIdx = normalized.indexOf(marker);
17416
+ if (markerIdx >= 0) {
17417
+ var afterMarker = normalized.slice(markerIdx + marker.length);
17418
+ var parts = afterMarker.split('/');
17419
+ if (parts.length === 2) {
17420
+ var _parts_, _parts_1;
17421
+ result = {
17422
+ filename: (_parts_ = parts[1]) !== null && _parts_ !== void 0 ? _parts_ : '',
17423
+ parentFolderName: (_parts_1 = parts[0]) !== null && _parts_1 !== void 0 ? _parts_1 : ''
17424
+ };
17425
+ } else if (parts.length > 2) {
17426
+ result = {
17427
+ filename: node_path.basename(filename),
17428
+ parentFolderName: node_path.basename(node_path.dirname(filename))
17429
+ };
17430
+ }
17431
+ }
17432
+ }
17433
+ return result;
17434
+ }
17435
+ /**
17436
+ * ESLint rule that enforces the canonical naming convention for Firebase
17437
+ * Functions API spec files: every `.spec.ts` under
17438
+ * `<apiDir>/src/app/function/<group>/` must be `<group>.crud[.<sub>...].spec.ts`
17439
+ * or `<group>.scenario[.<sub>...].spec.ts`. Drift forms surface a rename
17440
+ * suggestion derived from the shared `classifySpecFile` classifier in
17441
+ * `@dereekb/util`, so this rule and the `dbx_model_test_validate_app` MCP
17442
+ * tool never diverge.
17443
+ *
17444
+ * Not auto-fixable: renaming files (and updating any imports/snapshots they
17445
+ * carry) is outside the safe scope of an ESLint autofix.
17446
+ */ var FIREBASE_REQUIRE_CANONICAL_API_SPEC_FILENAME_RULE = {
17447
+ meta: {
17448
+ type: 'suggestion',
17449
+ fixable: undefined,
17450
+ docs: {
17451
+ description: 'Require API spec filenames under `src/app/function/<group>/` to follow the `<group>.crud[.<sub>...].spec.ts` / `<group>.scenario[.<sub>...].spec.ts` convention.',
17452
+ recommended: true
17453
+ },
17454
+ messages: {
17455
+ testFileDriftRename: '`{{filename}}`: {{reason}} Rename to `{{recommendedRename}}`.',
17456
+ testFileMissingBucket: '`{{filename}}`: missing `crud` / `scenario` segment. Rename to `{{recommendedRename}}` (default) or to a `crud` variant if the tests are CRUD-flavored.',
17457
+ testFileNonGroupPlacement: '`{{filename}}`: first segment `{{group}}` does not match the parent folder `{{parentFolderName}}`. Move into `{{group}}/` or rename the prefix to match the current folder.'
17458
+ },
17459
+ schema: [
17460
+ {
17461
+ type: 'object',
17462
+ additionalProperties: false,
17463
+ properties: {
17464
+ functionDirSegment: {
17465
+ type: 'string'
17466
+ }
17467
+ }
17468
+ }
17469
+ ]
17470
+ },
17471
+ create: function create(context) {
17472
+ var _context_options_, _options_functionDirSegment;
17473
+ var options = (_context_options_ = context.options[0]) !== null && _context_options_ !== void 0 ? _context_options_ : {};
17474
+ var functionDirSegment = (_options_functionDirSegment = options.functionDirSegment) !== null && _options_functionDirSegment !== void 0 ? _options_functionDirSegment : DEFAULT_FUNCTION_DIR_SEGMENT;
17475
+ var matched = matchFunctionSpecPath(context.filename, functionDirSegment);
17476
+ function check(programNode) {
17477
+ if (!matched) return;
17478
+ var classification = classifySpecFile({
17479
+ filename: matched.filename,
17480
+ parentFolderName: matched.parentFolderName
17481
+ });
17482
+ if (classification.isCanonical) return;
17483
+ if (classification.kind === 'non-spec') return;
17484
+ if (classification.kind === 'crud-misplaced' || classification.kind === 'scenario-misplaced') {
17485
+ var _classification_driftReason, _classification_recommendedRename;
17486
+ context.report({
17487
+ node: programNode,
17488
+ messageId: 'testFileDriftRename',
17489
+ data: {
17490
+ filename: classification.filename,
17491
+ reason: (_classification_driftReason = classification.driftReason) !== null && _classification_driftReason !== void 0 ? _classification_driftReason : 'segment order does not match the convention.',
17492
+ recommendedRename: (_classification_recommendedRename = classification.recommendedRename) !== null && _classification_recommendedRename !== void 0 ? _classification_recommendedRename : ''
17493
+ }
17494
+ });
17495
+ } else if (classification.kind === 'no-bucket') {
17496
+ var _classification_recommendedRename1;
17497
+ context.report({
17498
+ node: programNode,
17499
+ messageId: 'testFileMissingBucket',
17500
+ data: {
17501
+ filename: classification.filename,
17502
+ recommendedRename: (_classification_recommendedRename1 = classification.recommendedRename) !== null && _classification_recommendedRename1 !== void 0 ? _classification_recommendedRename1 : ''
17503
+ }
17504
+ });
17505
+ } else if (classification.kind === 'non-group') {
17506
+ context.report({
17507
+ node: programNode,
17508
+ messageId: 'testFileNonGroupPlacement',
17509
+ data: {
17510
+ filename: classification.filename,
17511
+ group: classification.group,
17512
+ parentFolderName: matched.parentFolderName
17513
+ }
17514
+ });
17515
+ }
17516
+ }
17517
+ return {
17518
+ Program: function Program(node) {
17519
+ return check(node);
17520
+ }
17521
+ };
17522
+ }
17523
+ };
17524
+
17525
+ function isGroupIndex(filename, functionDirSegment) {
17526
+ var normalized = filename.split(node_path.sep).join('/');
17527
+ var marker = "/".concat(functionDirSegment, "/");
17528
+ var markerIdx = normalized.indexOf(marker);
17529
+ var result = false;
17530
+ if (markerIdx >= 0) {
17531
+ var afterMarker = normalized.slice(markerIdx + marker.length);
17532
+ var parts = afterMarker.split('/');
17533
+ result = parts.length === 2 && parts[1] === 'index.ts';
17534
+ }
17535
+ return result;
17536
+ }
17537
+ function hasCrudSpec(groupDir, group) {
17538
+ var found = false;
17539
+ try {
17540
+ var entries = node_fs.readdirSync(groupDir);
17541
+ var _iteratorNormalCompletion = true, _didIteratorError = false, _iteratorError = undefined;
17542
+ try {
17543
+ for(var _iterator = entries[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true){
17544
+ var entry = _step.value;
17545
+ var classification = classifySpecFile({
17546
+ filename: entry,
17547
+ parentFolderName: group
17548
+ });
17549
+ if (classification.kind === 'crud' || classification.kind === 'crud-subgroup') {
17550
+ found = true;
17551
+ break;
17552
+ }
17553
+ }
17554
+ } catch (err) {
17555
+ _didIteratorError = true;
17556
+ _iteratorError = err;
17557
+ } finally{
17558
+ try {
17559
+ if (!_iteratorNormalCompletion && _iterator.return != null) {
17560
+ _iterator.return();
17561
+ }
17562
+ } finally{
17563
+ if (_didIteratorError) {
17564
+ throw _iteratorError;
17565
+ }
17566
+ }
17567
+ }
17568
+ } catch (unused) {
17569
+ // Directory unreadable — treat as no crud spec; rule will emit the warning
17570
+ // and the user can investigate. We do NOT swallow the error silently
17571
+ // in any other way.
17572
+ }
17573
+ return found;
17574
+ }
17575
+ /**
17576
+ * ESLint rule that fires on every `<apiDir>/src/app/function/<group>/index.ts`
17577
+ * (the canonical anchor file for a Firebase Functions group) and verifies
17578
+ * that the same folder contains a `<group>.crud.spec.ts` (or any
17579
+ * `<group>.crud.<sub>.spec.ts` variant) sibling. Mirrors the coverage check
17580
+ * in `dbx_model_test_validate_app` so editor + CI lint flag missing CRUD
17581
+ * coverage without needing the MCP audit.
17582
+ *
17583
+ * Not auto-fixable: creating a spec file shell with sensible test cases is
17584
+ * outside the safe scope of an ESLint autofix.
17585
+ */ var FIREBASE_REQUIRE_API_CRUD_SPEC_FOR_GROUP_RULE = {
17586
+ meta: {
17587
+ type: 'suggestion',
17588
+ fixable: undefined,
17589
+ docs: {
17590
+ description: 'Require every API model-group function folder to have a `<group>.crud.spec.ts` covering its CRUD function map.',
17591
+ recommended: true
17592
+ },
17593
+ messages: {
17594
+ modelGroupMissingCrudSpec: 'Model group `{{group}}` has no `{{expectedFilename}}`. Add it covering the CRUD function map (create/read/update/delete + permission/error paths).'
17595
+ },
17596
+ schema: [
17597
+ {
17598
+ type: 'object',
17599
+ additionalProperties: false,
17600
+ properties: {
17601
+ functionDirSegment: {
17602
+ type: 'string'
17603
+ }
17604
+ }
17605
+ }
17606
+ ]
17607
+ },
17608
+ create: function create(context) {
17609
+ var _context_options_, _options_functionDirSegment;
17610
+ var options = (_context_options_ = context.options[0]) !== null && _context_options_ !== void 0 ? _context_options_ : {};
17611
+ var functionDirSegment = (_options_functionDirSegment = options.functionDirSegment) !== null && _options_functionDirSegment !== void 0 ? _options_functionDirSegment : DEFAULT_FUNCTION_DIR_SEGMENT;
17612
+ var filename = context.filename;
17613
+ var isAnchor = isGroupIndex(filename, functionDirSegment);
17614
+ function check(programNode) {
17615
+ if (!isAnchor) return;
17616
+ var groupDir = node_path.dirname(filename);
17617
+ var group = node_path.basename(groupDir);
17618
+ if (hasCrudSpec(groupDir, group)) return;
17619
+ var expectedFilename = buildCanonicalFilename({
17620
+ group: group,
17621
+ bucket: 'crud',
17622
+ subgroups: []
17623
+ });
17624
+ context.report({
17625
+ node: programNode,
17626
+ messageId: 'modelGroupMissingCrudSpec',
17627
+ data: {
17628
+ group: group,
17629
+ expectedFilename: expectedFilename
17630
+ }
17631
+ });
17632
+ }
17633
+ return {
17634
+ Program: function Program(node) {
17635
+ return check(node);
17636
+ }
17637
+ };
17638
+ }
17639
+ };
17640
+
17232
17641
  /**
17233
17642
  * ESLint plugin for `@dereekb/firebase` rules.
17234
17643
  *
@@ -17248,7 +17657,9 @@ function reportCompositeKeyTag(ctx, tag, allowedEncodings) {
17248
17657
  'require-firestore-rule-for-service-model': FIREBASE_REQUIRE_FIRESTORE_RULE_FOR_SERVICE_MODEL_RULE,
17249
17658
  'require-dbx-model-service-factory-tag': FIREBASE_REQUIRE_DBX_MODEL_SERVICE_FACTORY_TAG_RULE,
17250
17659
  'require-service-factory-for-dbx-model': FIREBASE_REQUIRE_SERVICE_FACTORY_FOR_DBX_MODEL_RULE,
17251
- 'require-dbx-model-companion-tags': FIREBASE_REQUIRE_DBX_MODEL_COMPANION_TAGS_RULE
17660
+ 'require-dbx-model-companion-tags': FIREBASE_REQUIRE_DBX_MODEL_COMPANION_TAGS_RULE,
17661
+ 'require-canonical-api-spec-filename': FIREBASE_REQUIRE_CANONICAL_API_SPEC_FILENAME_RULE,
17662
+ 'require-api-crud-spec-for-group': FIREBASE_REQUIRE_API_CRUD_SPEC_FOR_GROUP_RULE
17252
17663
  }
17253
17664
  };
17254
17665
  /**
@@ -17268,6 +17679,7 @@ exports.DEFAULT_DISCOVERY_EXCLUDED_DIRS = DEFAULT_DISCOVERY_EXCLUDED_DIRS;
17268
17679
  exports.DEFAULT_FACTORY_SEARCH_ROOTS = DEFAULT_FACTORY_SEARCH_ROOTS;
17269
17680
  exports.DEFAULT_FACTORY_TAG = DEFAULT_FACTORY_TAG;
17270
17681
  exports.DEFAULT_FIRESTORE_RULES_FILENAME = DEFAULT_FIRESTORE_RULES_FILENAME;
17682
+ exports.DEFAULT_FUNCTION_DIR_SEGMENT = DEFAULT_FUNCTION_DIR_SEGMENT;
17271
17683
  exports.DEFAULT_IDENTITY_FACTORY_NAME = DEFAULT_IDENTITY_FACTORY_NAME;
17272
17684
  exports.DEFAULT_INDEX_AFFECTING_CONSTRAINT_NAMES = DEFAULT_INDEX_AFFECTING_CONSTRAINT_NAMES;
17273
17685
  exports.DEFAULT_MODEL_MARKER_TAG = DEFAULT_MODEL_MARKER_TAG;
@@ -17280,7 +17692,9 @@ exports.FIREBASE_ESLINT_PLUGIN = FIREBASE_ESLINT_PLUGIN;
17280
17692
  exports.FIREBASE_MODEL_SERVICE_FACTORY_MODULE = FIREBASE_MODEL_SERVICE_FACTORY_MODULE;
17281
17693
  exports.FIREBASE_MODEL_SERVICE_FACTORY_NAME = FIREBASE_MODEL_SERVICE_FACTORY_NAME;
17282
17694
  exports.FIREBASE_MODULE = FIREBASE_MODULE;
17695
+ exports.FIREBASE_REQUIRE_API_CRUD_SPEC_FOR_GROUP_RULE = FIREBASE_REQUIRE_API_CRUD_SPEC_FOR_GROUP_RULE;
17283
17696
  exports.FIREBASE_REQUIRE_API_DETAILS_FOR_CRUD_FUNCTION_RULE = FIREBASE_REQUIRE_API_DETAILS_FOR_CRUD_FUNCTION_RULE;
17697
+ exports.FIREBASE_REQUIRE_CANONICAL_API_SPEC_FILENAME_RULE = FIREBASE_REQUIRE_CANONICAL_API_SPEC_FILENAME_RULE;
17284
17698
  exports.FIREBASE_REQUIRE_COMPLETE_CRUD_FUNCTION_CONFIG_MAP_RULE = FIREBASE_REQUIRE_COMPLETE_CRUD_FUNCTION_CONFIG_MAP_RULE;
17285
17699
  exports.FIREBASE_REQUIRE_DBX_MODEL_COMPANION_TAGS_RULE = FIREBASE_REQUIRE_DBX_MODEL_COMPANION_TAGS_RULE;
17286
17700
  exports.FIREBASE_REQUIRE_DBX_MODEL_FIREBASE_INDEX_COMPANION_TAGS_RULE = FIREBASE_REQUIRE_DBX_MODEL_FIREBASE_INDEX_COMPANION_TAGS_RULE;