@pjts/eslint-style-rules 0.0.0-stage → 0.0.3

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 (113) hide show
  1. package/README.md +138 -2
  2. package/dist/index.d.ts +38 -0
  3. package/dist/index.js +232 -0
  4. package/dist/index.js.map +1 -0
  5. package/dist/lib/ast.d.ts +27 -0
  6. package/dist/lib/ast.js +60 -0
  7. package/dist/lib/ast.js.map +1 -0
  8. package/dist/lib/db.d.ts +13 -0
  9. package/dist/lib/db.js +50 -0
  10. package/dist/lib/db.js.map +1 -0
  11. package/dist/lib/messages.d.ts +13 -0
  12. package/dist/lib/messages.js +76 -0
  13. package/dist/lib/messages.js.map +1 -0
  14. package/dist/lib/rule.d.ts +9 -0
  15. package/dist/lib/rule.js +10 -0
  16. package/dist/lib/rule.js.map +1 -0
  17. package/dist/rules/app-class-suffix.d.ts +3 -0
  18. package/dist/rules/app-class-suffix.js +28 -0
  19. package/dist/rules/app-class-suffix.js.map +1 -0
  20. package/dist/rules/app-service-endpoints.d.ts +12 -0
  21. package/dist/rules/app-service-endpoints.js +79 -0
  22. package/dist/rules/app-service-endpoints.js.map +1 -0
  23. package/dist/rules/auth-access.d.ts +14 -0
  24. package/dist/rules/auth-access.js +122 -0
  25. package/dist/rules/auth-access.js.map +1 -0
  26. package/dist/rules/db-client-dependency.d.ts +11 -0
  27. package/dist/rules/db-client-dependency.js +74 -0
  28. package/dist/rules/db-client-dependency.js.map +1 -0
  29. package/dist/rules/db-writes-in-repositories.d.ts +12 -0
  30. package/dist/rules/db-writes-in-repositories.js +105 -0
  31. package/dist/rules/db-writes-in-repositories.js.map +1 -0
  32. package/dist/rules/decorated-class-contract.d.ts +6 -0
  33. package/dist/rules/decorated-class-contract.js +37 -0
  34. package/dist/rules/decorated-class-contract.js.map +1 -0
  35. package/dist/rules/entity-constructor-data.d.ts +6 -0
  36. package/dist/rules/entity-constructor-data.js +35 -0
  37. package/dist/rules/entity-constructor-data.js.map +1 -0
  38. package/dist/rules/event-sourced-aggregate.d.ts +15 -0
  39. package/dist/rules/event-sourced-aggregate.js +134 -0
  40. package/dist/rules/event-sourced-aggregate.js.map +1 -0
  41. package/dist/rules/find-one-naming.d.ts +7 -0
  42. package/dist/rules/find-one-naming.js +38 -0
  43. package/dist/rules/find-one-naming.js.map +1 -0
  44. package/dist/rules/import-only-in.d.ts +7 -0
  45. package/dist/rules/import-only-in.js +52 -0
  46. package/dist/rules/import-only-in.js.map +1 -0
  47. package/dist/rules/knex-first.d.ts +3 -0
  48. package/dist/rules/knex-first.js +28 -0
  49. package/dist/rules/knex-first.js.map +1 -0
  50. package/dist/rules/message-file.d.ts +18 -0
  51. package/dist/rules/message-file.js +118 -0
  52. package/dist/rules/message-file.js.map +1 -0
  53. package/dist/rules/message-handler.d.ts +19 -0
  54. package/dist/rules/message-handler.js +114 -0
  55. package/dist/rules/message-handler.js.map +1 -0
  56. package/dist/rules/message-name-reference.d.ts +7 -0
  57. package/dist/rules/message-name-reference.js +52 -0
  58. package/dist/rules/message-name-reference.js.map +1 -0
  59. package/dist/rules/message-names.d.ts +7 -0
  60. package/dist/rules/message-names.js +80 -0
  61. package/dist/rules/message-names.js.map +1 -0
  62. package/dist/rules/module-layout.d.ts +9 -0
  63. package/dist/rules/module-layout.js +71 -0
  64. package/dist/rules/module-layout.js.map +1 -0
  65. package/dist/rules/module-service-getters.d.ts +19 -0
  66. package/dist/rules/module-service-getters.js +132 -0
  67. package/dist/rules/module-service-getters.js.map +1 -0
  68. package/dist/rules/no-async-in-domain.d.ts +2 -0
  69. package/dist/rules/no-async-in-domain.js +116 -0
  70. package/dist/rules/no-async-in-domain.js.map +1 -0
  71. package/dist/rules/no-duplicate-sdk-types.d.ts +6 -0
  72. package/dist/rules/no-duplicate-sdk-types.js +40 -0
  73. package/dist/rules/no-duplicate-sdk-types.js.map +1 -0
  74. package/dist/rules/no-entity-assembly-in-app.d.ts +7 -0
  75. package/dist/rules/no-entity-assembly-in-app.js +37 -0
  76. package/dist/rules/no-entity-assembly-in-app.js.map +1 -0
  77. package/dist/rules/no-entity-mutation-outside-domain.d.ts +11 -0
  78. package/dist/rules/no-entity-mutation-outside-domain.js +68 -0
  79. package/dist/rules/no-entity-mutation-outside-domain.js.map +1 -0
  80. package/dist/rules/no-message-construction-outside-domain.d.ts +15 -0
  81. package/dist/rules/no-message-construction-outside-domain.js +45 -0
  82. package/dist/rules/no-message-construction-outside-domain.js.map +1 -0
  83. package/dist/rules/no-reconstitute.d.ts +3 -0
  84. package/dist/rules/no-reconstitute.js +22 -0
  85. package/dist/rules/no-reconstitute.js.map +1 -0
  86. package/dist/rules/port-file.d.ts +9 -0
  87. package/dist/rules/port-file.js +94 -0
  88. package/dist/rules/port-file.js.map +1 -0
  89. package/dist/rules/port-has-adapters.d.ts +9 -0
  90. package/dist/rules/port-has-adapters.js +62 -0
  91. package/dist/rules/port-has-adapters.js.map +1 -0
  92. package/dist/rules/repository-naming.d.ts +19 -0
  93. package/dist/rules/repository-naming.js +122 -0
  94. package/dist/rules/repository-naming.js.map +1 -0
  95. package/dist/rules/repository-serialization.d.ts +6 -0
  96. package/dist/rules/repository-serialization.js +50 -0
  97. package/dist/rules/repository-serialization.js.map +1 -0
  98. package/dist/rules/return-entities.d.ts +6 -0
  99. package/dist/rules/return-entities.js +38 -0
  100. package/dist/rules/return-entities.js.map +1 -0
  101. package/dist/rules/save-contract.d.ts +7 -0
  102. package/dist/rules/save-contract.js +41 -0
  103. package/dist/rules/save-contract.js.map +1 -0
  104. package/dist/rules/test-location.d.ts +14 -0
  105. package/dist/rules/test-location.js +94 -0
  106. package/dist/rules/test-location.js.map +1 -0
  107. package/dist/rules/throw-app-error.d.ts +12 -0
  108. package/dist/rules/throw-app-error.js +68 -0
  109. package/dist/rules/throw-app-error.js.map +1 -0
  110. package/dist/rules/trackable-repository.d.ts +10 -0
  111. package/dist/rules/trackable-repository.js +97 -0
  112. package/dist/rules/trackable-repository.js.map +1 -0
  113. package/package.json +46 -4
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Repository implementations live in `infrastructure/repositories/<storage>/` and are prefixed after
3
+ * it. A repository with several infrastructural dependencies lives in a combined folder whose
4
+ * segments are joined with `+` in config order, and concatenates the prefixes:
5
+ *
6
+ * repositories/pg/PgTaskRepository.ts
7
+ * repositories/memory/InMemoryTaskRepository.ts
8
+ * repositories/pg+nats/PgNatsTaskRepository.ts (Postgres + NATS outbox)
9
+ *
10
+ * The folder must also match what the file imports: a storage whose `imports` signature is found
11
+ * must be part of the folder, and every folder segment with a signature must be found.
12
+ *
13
+ * Options:
14
+ * storages: { pg: { prefix: 'Pg', imports: ['@pjts/infra#PostgresDbClient', 'knex'] }, memory: { prefix: 'InMemory' } }
15
+ * `imports` entries are `module`, `module#ImportedName` or `scope/*`.
16
+ * requireImplementationFolder (default true), fileNameMatchesClass (default true)
17
+ */
18
+ declare const _default: import("eslint").Rule.RuleModule;
19
+ export default _default;
@@ -0,0 +1,122 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const tslib_1 = require("tslib");
4
+ const node_path_1 = tslib_1.__importDefault(require("node:path"));
5
+ const ast_1 = require("../lib/ast");
6
+ const rule_1 = require("../lib/rule");
7
+ /**
8
+ * Repository implementations live in `infrastructure/repositories/<storage>/` and are prefixed after
9
+ * it. A repository with several infrastructural dependencies lives in a combined folder whose
10
+ * segments are joined with `+` in config order, and concatenates the prefixes:
11
+ *
12
+ * repositories/pg/PgTaskRepository.ts
13
+ * repositories/memory/InMemoryTaskRepository.ts
14
+ * repositories/pg+nats/PgNatsTaskRepository.ts (Postgres + NATS outbox)
15
+ *
16
+ * The folder must also match what the file imports: a storage whose `imports` signature is found
17
+ * must be part of the folder, and every folder segment with a signature must be found.
18
+ *
19
+ * Options:
20
+ * storages: { pg: { prefix: 'Pg', imports: ['@pjts/infra#PostgresDbClient', 'knex'] }, memory: { prefix: 'InMemory' } }
21
+ * `imports` entries are `module`, `module#ImportedName` or `scope/*`.
22
+ * requireImplementationFolder (default true), fileNameMatchesClass (default true)
23
+ */
24
+ exports.default = (0, rule_1.defineRule)({
25
+ meta: {
26
+ type: 'suggestion',
27
+ docs: { description: 'Tie repository class names to their storage folder, imports and file name' },
28
+ messages: {
29
+ folder: 'Repository "{{name}}" must live in infrastructure/repositories/<{{storages}}>/ (combine with "+" for several).',
30
+ unknownStorage: 'Unknown storage "{{segment}}" in "{{folder}}/" — known: {{storages}}.',
31
+ order: 'Name the folder "{{expected}}/" — storages are combined in the order {{storages}}.',
32
+ prefix: 'Repository "{{name}}" lives in "{{folder}}/", so it must be named {{prefix}}…Repository.',
33
+ undeclared: '"{{name}}" depends on {{storage}} ({{via}}), so it belongs in "repositories/{{expected}}/" as {{prefix}}…Repository.',
34
+ unused: '"{{name}}" lives in "{{folder}}/" but does not depend on {{storage}} — move it to "repositories/{{expected}}/".',
35
+ fileName: 'File "{{file}}.ts" must be named after its repository class "{{name}}" (one repository per file).',
36
+ },
37
+ schema: [{
38
+ type: 'object',
39
+ properties: {
40
+ storages: {
41
+ type: 'object',
42
+ additionalProperties: {
43
+ type: 'object',
44
+ properties: { prefix: { type: 'string' }, imports: { type: 'array', items: { type: 'string' } } },
45
+ required: ['prefix'],
46
+ additionalProperties: false,
47
+ },
48
+ },
49
+ requireImplementationFolder: { type: 'boolean' },
50
+ fileNameMatchesClass: { type: 'boolean' },
51
+ },
52
+ additionalProperties: false,
53
+ }],
54
+ },
55
+ create(context) {
56
+ const { storages = { pg: { prefix: 'Pg', imports: ['@pjts/infra#PostgresDbClient', 'knex', 'pg'] }, memory: { prefix: 'InMemory' } }, requireImplementationFolder = true, fileNameMatchesClass = true, } = context.options[0] ?? {};
57
+ const keys = Object.keys(storages);
58
+ const allPrefixes = keys.map((key) => storages[key].prefix);
59
+ const file = (0, ast_1.filePath)(context);
60
+ const folder = file.match(/\/infrastructure\/repositories\/([^/]+)\/[^/]+$/)?.[1] ?? null;
61
+ const baseName = node_path_1.default.basename(file).replace(/\.tsx?$/, '');
62
+ /** storage key → the import that revealed it, for every storage whose signature this file imports. */
63
+ const detected = new Map();
64
+ const matches = (spec, source, importedNames) => {
65
+ const [module, name] = spec.split('#');
66
+ const moduleMatches = module.endsWith('/*') ? source.startsWith(module.slice(0, -1)) : source === module;
67
+ return moduleMatches && (!name || importedNames.includes(name));
68
+ };
69
+ const folderFor = (segments) => keys.filter((key) => segments.includes(key)).join('+');
70
+ const prefixFor = (segments) => keys.filter((key) => segments.includes(key)).map((key) => storages[key].prefix).join('');
71
+ return {
72
+ ImportDeclaration(node) {
73
+ const source = node.source.value;
74
+ const importedNames = node.specifiers.map((s) => s.imported?.name ?? s.local.name);
75
+ for (const key of keys) {
76
+ const spec = (storages[key].imports ?? []).find((candidate) => matches(candidate, source, importedNames));
77
+ if (spec && !detected.has(key))
78
+ detected.set(key, spec.replace('#', ' → '));
79
+ }
80
+ },
81
+ 'ClassDeclaration[id.name=/Repository$/]'(node) {
82
+ const name = node.id.name;
83
+ const report = (messageId, data) => context.report({ node: node.id, messageId, data: { name, ...data } });
84
+ if (fileNameMatchesClass && baseName !== name)
85
+ report('fileName', { file: baseName });
86
+ if (!folder) {
87
+ if (requireImplementationFolder)
88
+ report('folder', { storages: keys.join('|') });
89
+ return;
90
+ }
91
+ const segments = folder.split('+');
92
+ const unknown = segments.filter((segment) => !(segment in storages));
93
+ if (unknown.length > 0) {
94
+ for (const segment of unknown)
95
+ report('unknownStorage', { segment, folder, storages: keys.join(', ') });
96
+ return;
97
+ }
98
+ if (folderFor(segments) !== folder)
99
+ report('order', { expected: folderFor(segments), storages: keys.join(' + ') });
100
+ // The prefix must be exactly the folder's: `PgNatsX` in pg/ is as wrong as `PgX` in pg+nats/.
101
+ const prefix = prefixFor(segments);
102
+ const rest = name.slice(prefix.length);
103
+ if (!name.startsWith(prefix) || !/^[A-Z]/.test(rest) || allPrefixes.some((p) => rest.startsWith(p) && /^[A-Z]/.test(rest.slice(p.length)))) {
104
+ report('prefix', { folder, prefix });
105
+ }
106
+ const required = [...new Set([...segments, ...detected.keys()])];
107
+ for (const [key, via] of detected) {
108
+ if (!segments.includes(key)) {
109
+ report('undeclared', { storage: key, via, expected: folderFor(required), prefix: prefixFor(required) });
110
+ }
111
+ }
112
+ for (const key of segments) {
113
+ if ((storages[key].imports ?? []).length > 0 && !detected.has(key)) {
114
+ const actual = segments.filter((segment) => segment !== key);
115
+ report('unused', { folder, storage: key, expected: actual.length ? folderFor(actual) : '<storage it uses>' });
116
+ }
117
+ }
118
+ },
119
+ };
120
+ },
121
+ });
122
+ //# sourceMappingURL=repository-naming.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"repository-naming.js","sourceRoot":"","sources":["../../src/rules/repository-naming.ts"],"names":[],"mappings":";;;AAAA,kEAA6B;AAC7B,oCAAsC;AACtC,sCAAyC;AAEzC;;;;;;;;;;;;;;;;GAgBG;AACH,kBAAe,IAAA,iBAAU,EAAC;IACxB,IAAI,EAAE;QACJ,IAAI,EAAE,YAAY;QAClB,IAAI,EAAE,EAAE,WAAW,EAAE,2EAA2E,EAAE;QAClG,QAAQ,EAAE;YACR,MAAM,EAAE,gHAAgH;YACxH,cAAc,EAAE,uEAAuE;YACvF,KAAK,EAAE,oFAAoF;YAC3F,MAAM,EAAE,0FAA0F;YAClG,UAAU,EAAE,sHAAsH;YAClI,MAAM,EAAE,iHAAiH;YACzH,QAAQ,EAAE,mGAAmG;SAC9G;QACD,MAAM,EAAE,CAAC;gBACP,IAAI,EAAE,QAAQ;gBACd,UAAU,EAAE;oBACV,QAAQ,EAAE;wBACR,IAAI,EAAE,QAAQ;wBACd,oBAAoB,EAAE;4BACpB,IAAI,EAAE,QAAQ;4BACd,UAAU,EAAE,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,EAAE;4BACjG,QAAQ,EAAE,CAAC,QAAQ,CAAC;4BACpB,oBAAoB,EAAE,KAAK;yBAC5B;qBACF;oBACD,2BAA2B,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE;oBAChD,oBAAoB,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE;iBAC1C;gBACD,oBAAoB,EAAE,KAAK;aAC5B,CAAC;KACH;IACD,MAAM,CAAC,OAAO;QACZ,MAAM,EACJ,QAAQ,GAAG,EAAE,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,8BAA8B,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,EAAE,EAC5H,2BAA2B,GAAG,IAAI,EAClC,oBAAoB,GAAG,IAAI,GAC5B,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QAC7B,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACnC,MAAM,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC;QAC5D,MAAM,IAAI,GAAG,IAAA,cAAQ,EAAC,OAAO,CAAC,CAAC;QAC/B,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,iDAAiD,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC;QAC1F,MAAM,QAAQ,GAAG,mBAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC;QAE5D,sGAAsG;QACtG,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAE,CAAC;QAC3B,MAAM,OAAO,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,aAAa,EAAE,EAAE;YAC9C,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;YACvC,MAAM,aAAa,GAAG,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,KAAK,MAAM,CAAC;YACzG,OAAO,aAAa,IAAI,CAAC,CAAC,IAAI,IAAI,aAAa,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC;QAClE,CAAC,CAAC;QAEF,MAAM,SAAS,GAAG,CAAC,QAAQ,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACvF,MAAM,SAAS,GAAG,CAAC,QAAQ,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAEzH,OAAO;YACL,iBAAiB,CAAC,IAAI;gBACpB,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC;gBACjC,MAAM,aAAa,GAAG,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,EAAE,IAAI,IAAI,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;gBACnF,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;oBACvB,MAAM,IAAI,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,aAAa,CAAC,CAAC,CAAC;oBAC1G,IAAI,IAAI,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC;wBAAE,QAAQ,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC,CAAC;gBAC9E,CAAC;YACH,CAAC;YAED,yCAAyC,CAAC,IAAI;gBAC5C,MAAM,IAAI,GAAG,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC;gBAC1B,MAAM,MAAM,GAAG,CAAC,SAAS,EAAE,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,GAAG,IAAI,EAAE,EAAE,CAAC,CAAC;gBAE1G,IAAI,oBAAoB,IAAI,QAAQ,KAAK,IAAI;oBAAE,MAAM,CAAC,UAAU,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC;gBAEtF,IAAI,CAAC,MAAM,EAAE,CAAC;oBACZ,IAAI,2BAA2B;wBAAE,MAAM,CAAC,QAAQ,EAAE,EAAE,QAAQ,EAAE,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;oBAChF,OAAO;gBACT,CAAC;gBAED,MAAM,QAAQ,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;gBACnC,MAAM,OAAO,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,IAAI,QAAQ,CAAC,CAAC,CAAC;gBACrE,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;oBACvB,KAAK,MAAM,OAAO,IAAI,OAAO;wBAAE,MAAM,CAAC,gBAAgB,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;oBACxG,OAAO;gBACT,CAAC;gBACD,IAAI,SAAS,CAAC,QAAQ,CAAC,KAAK,MAAM;oBAAE,MAAM,CAAC,OAAO,EAAE,EAAE,QAAQ,EAAE,SAAS,CAAC,QAAQ,CAAC,EAAE,QAAQ,EAAE,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;gBAEnH,8FAA8F;gBAC9F,MAAM,MAAM,GAAG,SAAS,CAAC,QAAQ,CAAC,CAAC;gBACnC,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;gBACvC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;oBAC3I,MAAM,CAAC,QAAQ,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;gBACvC,CAAC;gBAED,MAAM,QAAQ,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,QAAQ,EAAE,GAAG,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;gBACjE,KAAK,MAAM,CAAC,GAAG,EAAE,GAAG,CAAC,IAAI,QAAQ,EAAE,CAAC;oBAClC,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;wBAC5B,MAAM,CAAC,YAAY,EAAE,EAAE,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,QAAQ,EAAE,SAAS,CAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,SAAS,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;oBAC1G,CAAC;gBACH,CAAC;gBACD,KAAK,MAAM,GAAG,IAAI,QAAQ,EAAE,CAAC;oBAC3B,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;wBACnE,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,KAAK,GAAG,CAAC,CAAC;wBAC7D,MAAM,CAAC,QAAQ,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,mBAAmB,EAAE,CAAC,CAAC;oBAChH,CAAC;gBACH,CAAC;YACH,CAAC;SACF,CAAC;IACJ,CAAC;CACF,CAAC,CAAC"}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * persistence repositories map with `serialize` (entity → record) and `deserialize`
3
+ * (record → entity), and `deserialize` passes the record straight into `new Entity({...})`.
4
+ */
5
+ declare const _default: import("eslint").Rule.RuleModule;
6
+ export default _default;
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const ast_1 = require("../lib/ast");
4
+ const rule_1 = require("../lib/rule");
5
+ const LEGACY_MAPPERS = /^(toDomain|toEntity|toRecord|toRow|fromRow|fromRecord|mapRow|mapRecord|rowTo\w+|recordTo\w+)$/;
6
+ /**
7
+ * persistence repositories map with `serialize` (entity → record) and `deserialize`
8
+ * (record → entity), and `deserialize` passes the record straight into `new Entity({...})`.
9
+ */
10
+ exports.default = (0, rule_1.defineRule)({
11
+ meta: {
12
+ type: 'suggestion',
13
+ docs: { description: 'Require serialize/deserialize mapping methods on persistence repositories' },
14
+ messages: {
15
+ missing: '"{{name}}" must define `{{method}}`.',
16
+ legacy: 'Rename `{{method}}` to `serialize` / `deserialize`.',
17
+ constructor: '`deserialize` must return `new <Entity>({...})`.',
18
+ },
19
+ schema: [{
20
+ type: 'object',
21
+ properties: { classPattern: { type: 'string' } },
22
+ additionalProperties: false,
23
+ }],
24
+ },
25
+ create(context) {
26
+ const classPattern = new RegExp(context.options[0]?.classPattern ?? '^Pg\\w*Repository$');
27
+ return {
28
+ 'ClassDeclaration[id]'(node) {
29
+ const name = node.id.name;
30
+ if (!classPattern.test(name))
31
+ return;
32
+ for (const method of ['serialize', 'deserialize']) {
33
+ if (!(0, ast_1.findMethod)(node, method))
34
+ context.report({ node: node.id, messageId: 'missing', data: { name, method } });
35
+ }
36
+ for (const method of (0, ast_1.methods)(node)) {
37
+ if (LEGACY_MAPPERS.test((0, ast_1.memberName)(method) ?? '')) {
38
+ context.report({ node: method.key, messageId: 'legacy', data: { method: (0, ast_1.memberName)(method) } });
39
+ }
40
+ }
41
+ const deserialize = (0, ast_1.findMethod)(node, 'deserialize');
42
+ const returned = deserialize?.value.body?.body.filter((statement) => statement.type === 'ReturnStatement') ?? [];
43
+ if (returned.some((statement) => statement.argument?.type !== 'NewExpression')) {
44
+ context.report({ node: deserialize.key, messageId: 'constructor' });
45
+ }
46
+ },
47
+ };
48
+ },
49
+ });
50
+ //# sourceMappingURL=repository-serialization.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"repository-serialization.js","sourceRoot":"","sources":["../../src/rules/repository-serialization.ts"],"names":[],"mappings":";;AAAA,oCAA6D;AAC7D,sCAAyC;AAEzC,MAAM,cAAc,GAAG,+FAA+F,CAAC;AAEvH;;;GAGG;AACH,kBAAe,IAAA,iBAAU,EAAC;IACxB,IAAI,EAAE;QACJ,IAAI,EAAE,YAAY;QAClB,IAAI,EAAE,EAAE,WAAW,EAAE,2EAA2E,EAAE;QAClG,QAAQ,EAAE;YACR,OAAO,EAAE,sCAAsC;YAC/C,MAAM,EAAE,qDAAqD;YAC7D,WAAW,EAAE,kDAAkD;SAChE;QACD,MAAM,EAAE,CAAC;gBACP,IAAI,EAAE,QAAQ;gBACd,UAAU,EAAE,EAAE,YAAY,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE;gBAChD,oBAAoB,EAAE,KAAK;aAC5B,CAAC;KACH;IACD,MAAM,CAAC,OAAO;QACZ,MAAM,YAAY,GAAG,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,YAAY,IAAI,oBAAoB,CAAC,CAAC;QAC1F,OAAO;YACL,sBAAsB,CAAC,IAAI;gBACzB,MAAM,IAAI,GAAG,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC;gBAC1B,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC;oBAAE,OAAO;gBAErC,KAAK,MAAM,MAAM,IAAI,CAAC,WAAW,EAAE,aAAa,CAAC,EAAE,CAAC;oBAClD,IAAI,CAAC,IAAA,gBAAU,EAAC,IAAI,EAAE,MAAM,CAAC;wBAAE,OAAO,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,SAAS,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,CAAC,CAAC;gBACjH,CAAC;gBACD,KAAK,MAAM,MAAM,IAAI,IAAA,aAAO,EAAC,IAAI,CAAC,EAAE,CAAC;oBACnC,IAAI,cAAc,CAAC,IAAI,CAAC,IAAA,gBAAU,EAAC,MAAM,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC;wBAClD,OAAO,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,GAAG,EAAE,SAAS,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE,MAAM,EAAE,IAAA,gBAAU,EAAC,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC;oBAClG,CAAC;gBACH,CAAC;gBACD,MAAM,WAAW,GAAG,IAAA,gBAAU,EAAC,IAAI,EAAE,aAAa,CAAC,CAAC;gBACpD,MAAM,QAAQ,GAAG,WAAW,EAAE,KAAK,CAAC,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS,CAAC,IAAI,KAAK,iBAAiB,CAAC,IAAI,EAAE,CAAC;gBACjH,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS,CAAC,QAAQ,EAAE,IAAI,KAAK,eAAe,CAAC,EAAE,CAAC;oBAC/E,OAAO,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,WAAW,CAAC,GAAG,EAAE,SAAS,EAAE,aAAa,EAAE,CAAC,CAAC;gBACtE,CAAC;YACH,CAAC;SACF,CAAC;IACJ,CAAC;CACF,CAAC,CAAC"}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * service methods return domain entities (`Promise<Task>`); the framework serializes them
3
+ * through `toJSON`, so the app layer never calls it nor declares response-DTO return types.
4
+ */
5
+ declare const _default: import("eslint").Rule.RuleModule;
6
+ export default _default;
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const ast_1 = require("../lib/ast");
4
+ const rule_1 = require("../lib/rule");
5
+ /**
6
+ * service methods return domain entities (`Promise<Task>`); the framework serializes them
7
+ * through `toJSON`, so the app layer never calls it nor declares response-DTO return types.
8
+ */
9
+ exports.default = (0, rule_1.defineRule)({
10
+ meta: {
11
+ type: 'suggestion',
12
+ docs: { description: 'Forbid hand-serialization and response-DTO return types in the app layer' },
13
+ messages: {
14
+ toJSON: 'Return the entity — the framework serializes it via toJSON.',
15
+ responseType: 'Return the domain entity, not "{{type}}".',
16
+ },
17
+ schema: [{
18
+ type: 'object',
19
+ properties: { responseTypePattern: { type: 'string' } },
20
+ additionalProperties: false,
21
+ }],
22
+ },
23
+ create(context) {
24
+ const responseType = new RegExp(context.options[0]?.responseTypePattern ?? '(ResponseDto|Response|JSON|Json)$');
25
+ return {
26
+ 'CallExpression[callee.property.name="toJSON"][arguments.length=0]': (node) => context.report({ node, messageId: 'toJSON' }),
27
+ 'MethodDefinition[kind="method"][accessibility!="private"] > FunctionExpression'(fn) {
28
+ const inner = (0, ast_1.promiseTypeArgument)(fn.returnType);
29
+ const type = inner?.type === 'TSArrayType' ? inner.elementType : inner;
30
+ const name = type?.type === 'TSTypeReference' ? type.typeName.name : null;
31
+ if (name && responseType.test(name)) {
32
+ context.report({ node: fn.returnType, messageId: 'responseType', data: { type: name } });
33
+ }
34
+ },
35
+ };
36
+ },
37
+ });
38
+ //# sourceMappingURL=return-entities.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"return-entities.js","sourceRoot":"","sources":["../../src/rules/return-entities.ts"],"names":[],"mappings":";;AAAA,oCAAiD;AACjD,sCAAyC;AAEzC;;;GAGG;AACH,kBAAe,IAAA,iBAAU,EAAC;IACxB,IAAI,EAAE;QACJ,IAAI,EAAE,YAAY;QAClB,IAAI,EAAE,EAAE,WAAW,EAAE,0EAA0E,EAAE;QACjG,QAAQ,EAAE;YACR,MAAM,EAAE,6DAA6D;YACrE,YAAY,EAAE,2CAA2C;SAC1D;QACD,MAAM,EAAE,CAAC;gBACP,IAAI,EAAE,QAAQ;gBACd,UAAU,EAAE,EAAE,mBAAmB,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE;gBACvD,oBAAoB,EAAE,KAAK;aAC5B,CAAC;KACH;IACD,MAAM,CAAC,OAAO;QACZ,MAAM,YAAY,GAAG,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,mBAAmB,IAAI,mCAAmC,CAAC,CAAC;QAChH,OAAO;YACL,mEAAmE,EAAE,CAAC,IAAI,EAAE,EAAE,CAC5E,OAAO,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,CAAC;YAC/C,gFAAgF,CAAC,EAAE;gBACjF,MAAM,KAAK,GAAG,IAAA,yBAAmB,EAAC,EAAE,CAAC,UAAU,CAAC,CAAC;gBACjD,MAAM,IAAI,GAAG,KAAK,EAAE,IAAI,KAAK,aAAa,CAAC,CAAC,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,CAAC,KAAK,CAAC;gBACvE,MAAM,IAAI,GAAG,IAAI,EAAE,IAAI,KAAK,iBAAiB,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC;gBAC1E,IAAI,IAAI,IAAI,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;oBACpC,OAAO,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,EAAE,CAAC,UAAU,EAAE,SAAS,EAAE,cAAc,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,CAAC,CAAC;gBAC3F,CAAC;YACH,CAAC;SACF,CAAC;IACJ,CAAC;CACF,CAAC,CAAC"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * respond from the domain entity, never from what `repo.save()` returned.
3
+ * `save` signatures are `Promise<void>`; a genuine `RETURNING` case is documented with
4
+ * `// eslint-disable-next-line theralign/save-contract -- RETURNING <column>`.
5
+ */
6
+ declare const _default: import("eslint").Rule.RuleModule;
7
+ export default _default;
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const ast_1 = require("../lib/ast");
4
+ const rule_1 = require("../lib/rule");
5
+ /**
6
+ * respond from the domain entity, never from what `repo.save()` returned.
7
+ * `save` signatures are `Promise<void>`; a genuine `RETURNING` case is documented with
8
+ * `// eslint-disable-next-line theralign/save-contract -- RETURNING <column>`.
9
+ */
10
+ exports.default = (0, rule_1.defineRule)({
11
+ meta: {
12
+ type: 'suggestion',
13
+ docs: { description: 'Enforce the repository save() contract' },
14
+ messages: {
15
+ usedResult: 'Respond from the entity itself, not from save()\'s return value.',
16
+ signature: '`save` must return Promise<void> unless the SQL uses RETURNING.',
17
+ },
18
+ schema: [],
19
+ },
20
+ create(context) {
21
+ const isSaveCall = (node) => (0, ast_1.calledMethodName)((0, ast_1.unwrapAwait)(node)) === 'save';
22
+ const checkSignature = (key, returnType) => {
23
+ const inner = (0, ast_1.promiseTypeArgument)(returnType);
24
+ if (inner !== undefined && inner?.type !== 'TSVoidKeyword')
25
+ context.report({ node: key, messageId: 'signature' });
26
+ };
27
+ return {
28
+ VariableDeclarator(node) {
29
+ if (isSaveCall(node.init))
30
+ context.report({ node, messageId: 'usedResult' });
31
+ },
32
+ ReturnStatement(node) {
33
+ if (isSaveCall(node.argument))
34
+ context.report({ node, messageId: 'usedResult' });
35
+ },
36
+ 'TSMethodSignature[key.name="save"]': (node) => checkSignature(node.key, node.returnType),
37
+ 'MethodDefinition[key.name="save"]': (node) => checkSignature(node.key, node.value.returnType),
38
+ };
39
+ },
40
+ });
41
+ //# sourceMappingURL=save-contract.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"save-contract.js","sourceRoot":"","sources":["../../src/rules/save-contract.ts"],"names":[],"mappings":";;AAAA,oCAAgF;AAChF,sCAAyC;AAEzC;;;;GAIG;AACH,kBAAe,IAAA,iBAAU,EAAC;IACxB,IAAI,EAAE;QACJ,IAAI,EAAE,YAAY;QAClB,IAAI,EAAE,EAAE,WAAW,EAAE,wCAAwC,EAAE;QAC/D,QAAQ,EAAE;YACR,UAAU,EAAE,kEAAkE;YAC9E,SAAS,EAAE,iEAAiE;SAC7E;QACD,MAAM,EAAE,EAAE;KACX;IACD,MAAM,CAAC,OAAO;QACZ,MAAM,UAAU,GAAG,CAAC,IAAI,EAAE,EAAE,CAAC,IAAA,sBAAgB,EAAC,IAAA,iBAAW,EAAC,IAAI,CAAC,CAAC,KAAK,MAAM,CAAC;QAC5E,MAAM,cAAc,GAAG,CAAC,GAAG,EAAE,UAAU,EAAE,EAAE;YACzC,MAAM,KAAK,GAAG,IAAA,yBAAmB,EAAC,UAAU,CAAC,CAAC;YAC9C,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,EAAE,IAAI,KAAK,eAAe;gBAAE,OAAO,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,GAAG,EAAE,SAAS,EAAE,WAAW,EAAE,CAAC,CAAC;QACpH,CAAC,CAAC;QACF,OAAO;YACL,kBAAkB,CAAC,IAAI;gBACrB,IAAI,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC;oBAAE,OAAO,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,YAAY,EAAE,CAAC,CAAC;YAC/E,CAAC;YACD,eAAe,CAAC,IAAI;gBAClB,IAAI,UAAU,CAAC,IAAI,CAAC,QAAQ,CAAC;oBAAE,OAAO,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,YAAY,EAAE,CAAC,CAAC;YACnF,CAAC;YACD,oCAAoC,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,cAAc,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,UAAU,CAAC;YACzF,mCAAmC,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,cAAc,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC;SAC/F,CAAC;IACJ,CAAC;CACF,CAAC,CAAC"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Tests sit next to what they test and are named after it:
3
+ *
4
+ * app/DashboardView.ts
5
+ * app/DashboardView.test.ts ✓
6
+ * app/DashboardView.cross-tenant.test.ts ✓ (optional single suffix)
7
+ * app/__tests__/DashboardView.test.ts ✗ → app/DashboardView.test.ts
8
+ * app/cross-tenant.test.ts ✗ → not named after a file in app/
9
+ *
10
+ * Checked as: the first segment of the test's file name is a source file in the same directory.
11
+ * When it isn't, the rule searches the source root (the nearest `src/`) to suggest where it belongs.
12
+ */
13
+ declare const _default: import("eslint").Rule.RuleModule;
14
+ export default _default;
@@ -0,0 +1,94 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const tslib_1 = require("tslib");
4
+ const node_fs_1 = tslib_1.__importDefault(require("node:fs"));
5
+ const node_path_1 = tslib_1.__importDefault(require("node:path"));
6
+ const ast_1 = require("../lib/ast");
7
+ const rule_1 = require("../lib/rule");
8
+ const TEST_FILE = /^([^.]+)(?:\.([^.]+))?\.test\.tsx?$/;
9
+ /** source root → Map<basename without extension, relative paths> of every non-test source file (per lint run). */
10
+ const sourceIndex = new Map();
11
+ const indexOf = (root) => {
12
+ if (!sourceIndex.has(root)) {
13
+ const index = new Map();
14
+ const walk = (dir) => {
15
+ for (const entry of node_fs_1.default.readdirSync(dir, { withFileTypes: true })) {
16
+ const full = node_path_1.default.join(dir, entry.name);
17
+ if (entry.isDirectory()) {
18
+ if (entry.name !== 'node_modules')
19
+ walk(full);
20
+ }
21
+ else if (/\.tsx?$/.test(entry.name) && !/\.(test|d)\.tsx?$/.test(entry.name)) {
22
+ const name = entry.name.replace(/\.tsx?$/, '');
23
+ index.set(name, [...(index.get(name) ?? []), node_path_1.default.relative(root, full).split(node_path_1.default.sep).join('/')]);
24
+ }
25
+ }
26
+ };
27
+ walk(root);
28
+ sourceIndex.set(root, index);
29
+ }
30
+ return sourceIndex.get(root);
31
+ };
32
+ /**
33
+ * Tests sit next to what they test and are named after it:
34
+ *
35
+ * app/DashboardView.ts
36
+ * app/DashboardView.test.ts ✓
37
+ * app/DashboardView.cross-tenant.test.ts ✓ (optional single suffix)
38
+ * app/__tests__/DashboardView.test.ts ✗ → app/DashboardView.test.ts
39
+ * app/cross-tenant.test.ts ✗ → not named after a file in app/
40
+ *
41
+ * Checked as: the first segment of the test's file name is a source file in the same directory.
42
+ * When it isn't, the rule searches the source root (the nearest `src/`) to suggest where it belongs.
43
+ */
44
+ exports.default = (0, rule_1.defineRule)({
45
+ meta: {
46
+ type: 'suggestion',
47
+ docs: { description: 'Require tests to sit next to, and be named after, the file they test' },
48
+ messages: {
49
+ folder: 'Tests sit next to the code they test — move to {{target}}.',
50
+ name: 'Test files are named "<TestedFile>.<optional-suffix>.test.ts" — "{{file}}" has more than one suffix.',
51
+ misplaced: '"{{file}}" tests {{tested}} — move it next to it: {{target}}.',
52
+ ambiguous: '"{{file}}" must sit next to {{tested}}.ts — candidates: {{candidates}}.',
53
+ untested: '"{{file}}" must be named after the file it tests ("<TestedFile>.<suffix>.test.ts") and sit next to it — no "{{tested}}.ts" exists.',
54
+ },
55
+ schema: [],
56
+ },
57
+ create(context) {
58
+ const file = (0, ast_1.filePath)(context);
59
+ const base = node_path_1.default.posix.basename(file);
60
+ if (!/\.test\.tsx?$/.test(base))
61
+ return {};
62
+ return {
63
+ Program(node) {
64
+ const report = (messageId, data) => context.report({ node, loc: { line: 1, column: 0 }, messageId, data: { file: base, ...data } });
65
+ const match = base.match(TEST_FILE);
66
+ if (!match)
67
+ return report('name', {});
68
+ const tested = match[1];
69
+ const srcAt = file.lastIndexOf('/src/');
70
+ const root = srcAt === -1 ? node_path_1.default.posix.dirname(file) : file.slice(0, srcAt + '/src'.length);
71
+ const dir = node_path_1.default.posix.dirname(file);
72
+ const inTestsFolder = dir.split('/').includes('__tests__');
73
+ const codeDir = inTestsFolder ? dir.split('/').filter((segment) => segment !== '__tests__').join('/') : dir;
74
+ const rel = (absolute) => node_path_1.default.posix.relative(root, absolute);
75
+ const exists = (directory) => ['.ts', '.tsx'].some((ext) => node_fs_1.default.existsSync(`${directory}/${tested}${ext}`));
76
+ if (!inTestsFolder && exists(dir))
77
+ return;
78
+ if (inTestsFolder && exists(codeDir))
79
+ return report('folder', { target: `src/${rel(`${codeDir}/${base}`)}` });
80
+ // Prefer a match inside the test's own module (`modules/<name>/`) over same-named files elsewhere.
81
+ const all = indexOf(root).get(tested) ?? [];
82
+ const module = rel(dir).match(/^modules\/[^/]+\//)?.[0];
83
+ const local = module ? all.filter((relative) => relative.startsWith(module)) : [];
84
+ const candidates = (local.length ? local : all).map((relative) => `src/${node_path_1.default.posix.dirname(relative)}/${base}`);
85
+ if (candidates.length === 1)
86
+ return report('misplaced', { tested: `${tested}.ts`, target: candidates[0] });
87
+ if (candidates.length > 1)
88
+ return report('ambiguous', { tested, candidates: candidates.join(', ') });
89
+ return report('untested', { tested });
90
+ },
91
+ };
92
+ },
93
+ });
94
+ //# sourceMappingURL=test-location.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test-location.js","sourceRoot":"","sources":["../../src/rules/test-location.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Every error the API throws is an `AppError` from `@pjts/core`:
3
+ *
4
+ * throw AppError.notFound(`Task ${id} not found`);
5
+ * throw new AppError(409, 'VERSION_NOT_DRAFT');
6
+ *
7
+ * Anything else — `new Error(...)`, custom `Error` subclasses, values returned by helpers — is
8
+ * flagged. Rethrowing the caught error unchanged (`catch (e) { …; throw e; }`) is allowed by default
9
+ * (`allowRethrow`): its throw site was checked where it was created, or it comes from a library.
10
+ */
11
+ declare const _default: import("eslint").Rule.RuleModule;
12
+ export default _default;
@@ -0,0 +1,68 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const rule_1 = require("../lib/rule");
4
+ /**
5
+ * Every error the API throws is an `AppError` from `@pjts/core`:
6
+ *
7
+ * throw AppError.notFound(`Task ${id} not found`);
8
+ * throw new AppError(409, 'VERSION_NOT_DRAFT');
9
+ *
10
+ * Anything else — `new Error(...)`, custom `Error` subclasses, values returned by helpers — is
11
+ * flagged. Rethrowing the caught error unchanged (`catch (e) { …; throw e; }`) is allowed by default
12
+ * (`allowRethrow`): its throw site was checked where it was created, or it comes from a library.
13
+ */
14
+ exports.default = (0, rule_1.defineRule)({
15
+ meta: {
16
+ type: 'problem',
17
+ docs: { description: 'Require thrown errors to be AppError' },
18
+ messages: {
19
+ appError: 'Throw an AppError (`throw AppError.badRequest(...)` / `throw new AppError(status, ...)`), not {{what}}.',
20
+ },
21
+ schema: [{
22
+ type: 'object',
23
+ properties: { module: { type: 'string' }, allowRethrow: { type: 'boolean' } },
24
+ additionalProperties: false,
25
+ }],
26
+ },
27
+ create(context) {
28
+ const { module = '@pjts/core', allowRethrow = true } = context.options[0] ?? {};
29
+ /** Local names bound to the real AppError. */
30
+ const appErrorNames = new Set();
31
+ const isAppError = (node) => node?.type === 'Identifier' && appErrorNames.has(node.name);
32
+ const isCaughtParam = (identifier) => {
33
+ for (const ancestor of context.sourceCode.getAncestors(identifier).reverse()) {
34
+ if (ancestor.type === 'CatchClause')
35
+ return ancestor.param?.type === 'Identifier' && ancestor.param.name === identifier.name;
36
+ if (/Function/.test(ancestor.type))
37
+ return false;
38
+ }
39
+ return false;
40
+ };
41
+ const describe = (argument) => {
42
+ if (argument.type === 'NewExpression')
43
+ return `new ${context.sourceCode.getText(argument.callee)}(...)`;
44
+ return `\`${context.sourceCode.getText(argument).slice(0, 40)}\``;
45
+ };
46
+ return {
47
+ ImportDeclaration(node) {
48
+ if (node.source.value !== module)
49
+ return;
50
+ for (const specifier of node.specifiers) {
51
+ if ((specifier.imported?.name ?? specifier.local.name) === 'AppError')
52
+ appErrorNames.add(specifier.local.name);
53
+ }
54
+ },
55
+ ThrowStatement(node) {
56
+ const argument = node.argument;
57
+ if (argument.type === 'NewExpression' && isAppError(argument.callee))
58
+ return;
59
+ if (argument.type === 'CallExpression' && argument.callee.type === 'MemberExpression' && isAppError(argument.callee.object))
60
+ return;
61
+ if (allowRethrow && argument.type === 'Identifier' && isCaughtParam(argument))
62
+ return;
63
+ context.report({ node: argument, messageId: 'appError', data: { what: describe(argument) } });
64
+ },
65
+ };
66
+ },
67
+ });
68
+ //# sourceMappingURL=throw-app-error.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"throw-app-error.js","sourceRoot":"","sources":["../../src/rules/throw-app-error.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Repositories built on the pjx persistence bases (`RepositoryBase`, `OutboxRepository`):
3
+ *
4
+ * - every `deserialize…` method carries `@reconstitute` — it marks the entity as restored and copies
5
+ * its version, so `save()` updates (with the optimistic-lock check) instead of inserting a duplicate;
6
+ * - an event-sourced aggregate (`extends EventSourceable`) is persisted by an `…OutboxRepository`, so
7
+ * its emitted events reach the outbox in the same transaction as its state.
8
+ */
9
+ declare const _default: import("eslint").Rule.RuleModule;
10
+ export default _default;