@echoes-of-order/eslint-config 1.121.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1093 -0
- package/configs/.gitkeep +1 -0
- package/configs/admin.js +203 -0
- package/configs/api-client.js +46 -0
- package/configs/backend.js +895 -0
- package/configs/domains.js +123 -0
- package/configs/frontend.js +30 -0
- package/configs/image-server.js +26 -0
- package/configs/ionos-proxy.js +372 -0
- package/configs/nestjs.js +156 -0
- package/configs/node.js +92 -0
- package/configs/react.js +111 -0
- package/configs/wiki.js +42 -0
- package/index.js +39 -0
- package/package.json +85 -0
- package/rules/.gitkeep +1 -0
- package/rules/__tests__/analyze-relation-usage.test.js.disabled +300 -0
- package/rules/__tests__/complexity.test.js.disabled +300 -0
- package/rules/__tests__/enforce-dto-factory-in-services.integration.test.js +226 -0
- package/rules/__tests__/enforce-dto-factory-in-services.test.js +177 -0
- package/rules/__tests__/enforce-entity-dto-create-no-id.integration.test.js +18 -0
- package/rules/__tests__/enforce-function-argument-count.test.js.disabled +300 -0
- package/rules/__tests__/enforce-repository-token-handling.test.js +58 -0
- package/rules/__tests__/english-only-code-strings.test.js.disabled +300 -0
- package/rules/__tests__/eslint-rules.integration.test.ts +350 -0
- package/rules/__tests__/integration-test-controller-response-dto.js +261 -0
- package/rules/__tests__/integration-test-dto-factory-in-services.js +260 -0
- package/rules/__tests__/integration-test-no-entity-type-casting.js +161 -0
- package/rules/__tests__/integration-test-typeorm-naming-conventions.js +501 -0
- package/rules/__tests__/test-config.js +33 -0
- package/rules/admin-controller-security.js +180 -0
- package/rules/analyze-relation-usage.js +687 -0
- package/rules/api-response-dto.js +174 -0
- package/rules/auth-guard-required.js +142 -0
- package/rules/backend-specific.js +36 -0
- package/rules/best-practices.js +421 -0
- package/rules/complexity.js +20 -0
- package/rules/controller-architecture.js +340 -0
- package/rules/controller-naming-conventions.js +190 -0
- package/rules/controller-readonly-restriction.js +148 -0
- package/rules/controller-swagger-complete.js +312 -0
- package/rules/controller-swagger-docs.js +119 -0
- package/rules/controller-swagger-english.js +320 -0
- package/rules/coordinate-naming.js +132 -0
- package/rules/custom-mui-button.js +135 -0
- package/rules/dead-code-detection-backend.js +50 -0
- package/rules/dead-code-detection-frontend.js +48 -0
- package/rules/dead-code-detection.js +71 -0
- package/rules/debug-controller-response-dto.js +79 -0
- package/rules/deprecate.js +8 -0
- package/rules/dto-annotation-property-consistency.js +111 -0
- package/rules/dto-entity-mapping-completeness.js +688 -0
- package/rules/dto-entity-swagger-separation.js +265 -0
- package/rules/dto-entity-type-consistency.js +352 -0
- package/rules/dto-entity-type-matching.js +519 -0
- package/rules/dto-naming-convention.js +98 -0
- package/rules/dto-visibility-modifiers.js +159 -0
- package/rules/enforce-api-versioning.js +122 -0
- package/rules/enforce-app-module-registration.js +179 -0
- package/rules/enforce-basecontroller.js +152 -0
- package/rules/enforce-body-request-dto.js +141 -0
- package/rules/enforce-controller-response-dto.js +349 -0
- package/rules/enforce-custom-error-classes.js +242 -0
- package/rules/enforce-database-transaction-safety.js +179 -0
- package/rules/enforce-dto-constructor.js +95 -0
- package/rules/enforce-dto-create-parameter-types.js +170 -0
- package/rules/enforce-dto-create-pattern.js +274 -0
- package/rules/enforce-dto-entity-creation.js +164 -0
- package/rules/enforce-dto-factory-in-services.js +188 -0
- package/rules/enforce-dto-from-entity-method.js +47 -0
- package/rules/enforce-dto-from-entity.js +314 -0
- package/rules/enforce-dto-naming-conventions.js +212 -0
- package/rules/enforce-dto-naming.js +176 -0
- package/rules/enforce-dto-usage-simple.js +114 -0
- package/rules/enforce-dto-usage.js +407 -0
- package/rules/enforce-eager-translation-loading.js +178 -0
- package/rules/enforce-entity-creation-pattern.js +137 -0
- package/rules/enforce-entity-dto-convert-method.js +157 -0
- package/rules/enforce-entity-dto-create-no-id.js +117 -0
- package/rules/enforce-entity-dto-extends-base.js +141 -0
- package/rules/enforce-entity-dto-from-request-dto-structure.js +113 -0
- package/rules/enforce-entity-dto-fromentity-complex.js +69 -0
- package/rules/enforce-entity-dto-fromentity-simple.js +69 -0
- package/rules/enforce-entity-dto-fromrequestdto-structure.js +262 -0
- package/rules/enforce-entity-dto-methods-restriction.js +159 -0
- package/rules/enforce-entity-dto-no-request-dto.js +102 -0
- package/rules/enforce-entity-dto-optional-auto-fields.js +101 -0
- package/rules/enforce-entity-dto-required-methods.js +248 -0
- package/rules/enforce-entity-factory-pattern.js +180 -0
- package/rules/enforce-entity-instantiation-in-toentity.js +125 -0
- package/rules/enforce-enum-for-playable-entities.js +95 -0
- package/rules/enforce-error-handling.js +257 -0
- package/rules/enforce-explicit-dto-types.js +118 -0
- package/rules/enforce-from-request-dto-usage.js +62 -0
- package/rules/enforce-generic-entity-dto.js +71 -0
- package/rules/enforce-inject-decorator.js +133 -0
- package/rules/enforce-lazy-type-loading.js +170 -0
- package/rules/enforce-module-existence.js +157 -0
- package/rules/enforce-nonentity-dto-create.js +107 -0
- package/rules/enforce-playable-entity-naming.js +108 -0
- package/rules/enforce-repository-token-handling.js +92 -0
- package/rules/enforce-request-dto-no-entity-dto.js +201 -0
- package/rules/enforce-request-dto-required-fields.js +217 -0
- package/rules/enforce-result-pattern.js +45 -0
- package/rules/enforce-service-relation-loading.js +116 -0
- package/rules/enforce-test-coverage.js +96 -0
- package/rules/enforce-toentity-conditional-assignment.js +132 -0
- package/rules/enforce-translations-required.js +203 -0
- package/rules/enforce-typeorm-naming-conventions.js +366 -0
- package/rules/enforce-vite-health-metrics.js +240 -0
- package/rules/entity-required-properties.js +321 -0
- package/rules/entity-to-dto-test.js +73 -0
- package/rules/enum-database-validation.js +149 -0
- package/rules/errors.js +190 -0
- package/rules/es6.js +204 -0
- package/rules/eslint-plugin-no-comments.js +44 -0
- package/rules/filename-class-name-match.js +62 -0
- package/rules/forbid-fromentity-outside-entity-folder.js +237 -0
- package/rules/function-params-newline.js +111 -0
- package/rules/imports.js +264 -0
- package/rules/jest.js +13 -0
- package/rules/jsx.js +16 -0
- package/rules/max-classes-per-file.js +49 -0
- package/rules/multiline-formatting.js +146 -0
- package/rules/no-blank-lines-between-decorators-and-properties.js +95 -0
- package/rules/no-comments.js +62 -0
- package/rules/no-dto-constructors.js +126 -0
- package/rules/no-dto-default-values.js +220 -0
- package/rules/no-dto-duplicates.js +127 -0
- package/rules/no-dto-in-entity.js +99 -0
- package/rules/no-dynamic-import-in-types.js +71 -0
- package/rules/no-dynamic-imports-in-controllers.js +95 -0
- package/rules/no-entity-imports-in-controllers.js +101 -0
- package/rules/no-entity-in-swagger-docs.js +139 -0
- package/rules/no-entity-type-casting.js +104 -0
- package/rules/no-fetch.js +77 -0
- package/rules/no-import-meta-env.js +151 -0
- package/rules/no-inline-styles.js +5 -0
- package/rules/no-magic-values.js +85 -0
- package/rules/no-partial-type.js +168 -0
- package/rules/no-relative-imports.js +31 -0
- package/rules/no-tsyringe.js +181 -0
- package/rules/no-type-assertion.js +175 -0
- package/rules/no-undefined-entity-properties.js +121 -0
- package/rules/node.js +44 -0
- package/rules/perfectionist.js +50 -0
- package/rules/performance-minimal.js +155 -0
- package/rules/performance.js +44 -0
- package/rules/pino-logger-format.js +200 -0
- package/rules/prefer-dto-classes.js +112 -0
- package/rules/prefer-dto-create-method.js +225 -0
- package/rules/promises.js +17 -0
- package/rules/react-hooks.js +15 -0
- package/rules/react.js +28 -0
- package/rules/regexp.js +70 -0
- package/rules/require-dto-response.js +81 -0
- package/rules/require-valid-relations.js +388 -0
- package/rules/result-pattern.js +162 -0
- package/rules/security.js +37 -0
- package/rules/service-architecture.js +148 -0
- package/rules/sonarjs.js +26 -0
- package/rules/strict.js +7 -0
- package/rules/style.js +611 -0
- package/rules/stylistic.js +93 -0
- package/rules/typeorm-column-type-validation.js +224 -0
- package/rules/typescript-advanced.js +113 -0
- package/rules/typescript-core.js +111 -0
- package/rules/typescript.js +146 -0
- package/rules/unicorn.js +168 -0
- package/rules/variables.js +51 -0
- package/rules/websocket-architecture.js +115 -0
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint-Regel: Erzwingt Custom Error-Klassen statt generischem Error
|
|
3
|
+
*
|
|
4
|
+
* Diese Regel verbietet `throw new Error()` und erzwingt die Verwendung
|
|
5
|
+
* von spezifischen Error-Klassen für bessere Fehlerbehandlung.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export default {
|
|
9
|
+
meta: {
|
|
10
|
+
type: "problem",
|
|
11
|
+
docs: {
|
|
12
|
+
description: "Enforce custom error classes instead of generic Error",
|
|
13
|
+
category: "Best Practices",
|
|
14
|
+
recommended: true,
|
|
15
|
+
},
|
|
16
|
+
messages: {
|
|
17
|
+
useCustomError: "Use custom error classes instead of generic 'throw new Error()'. Use existing error classes from /src/errors/ like 'NotFoundError', 'ValidationError', 'UnauthorizedError', etc.\n\nExample for '{{suggestedName}}':\nimport {{suggestedName}} from '@/errors/{{suggestedName}}';\nthrow new {{suggestedName}}('your message');",
|
|
18
|
+
suggestCustomError: "Consider using an existing error class like '{{suggestedName}}' from /src/errors/ for this specific error case.",
|
|
19
|
+
noObjectAssignError: "Do not use Object.assign() with Error. This is an anti-pattern!\n\nInstead, use an existing custom error class from /src/errors/ like '{{suggestedName}}':\n\nimport {{suggestedName}} from '@/errors/{{suggestedName}}';\nthrow new {{suggestedName}}('{{errorMessage}}');\n\nIf '{{suggestedName}}' doesn't exist yet, create it:\n\nclass {{suggestedName}} extends Error {\n constructor(message: string) {\n super(message);\n this.name = '{{suggestedName}}';\n }\n}",
|
|
20
|
+
},
|
|
21
|
+
fixable: null,
|
|
22
|
+
schema: [
|
|
23
|
+
{
|
|
24
|
+
type: "object",
|
|
25
|
+
properties: {
|
|
26
|
+
allowedPatterns: {
|
|
27
|
+
type: "array",
|
|
28
|
+
items: { type: "string" },
|
|
29
|
+
description: "Patterns that are allowed to use generic Error",
|
|
30
|
+
},
|
|
31
|
+
exemptFiles: {
|
|
32
|
+
type: "array",
|
|
33
|
+
items: { type: "string" },
|
|
34
|
+
description: "File patterns that are exempt from this rule",
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
additionalProperties: false,
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
create(context) {
|
|
43
|
+
const options = context.options[0] || {};
|
|
44
|
+
const allowedPatterns = options.allowedPatterns || [];
|
|
45
|
+
const exemptFiles = options.exemptFiles || ["**/*.test.ts", "**/*.spec.ts", "**/__tests__/**"];
|
|
46
|
+
|
|
47
|
+
const filename = context.getFilename();
|
|
48
|
+
|
|
49
|
+
// Prüfe ob Datei von der Regel ausgenommen ist
|
|
50
|
+
const isExempt = exemptFiles.some(pattern => {
|
|
51
|
+
if (pattern.includes("**")) {
|
|
52
|
+
const regex = new RegExp(pattern.replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*"));
|
|
53
|
+
return regex.test(filename);
|
|
54
|
+
}
|
|
55
|
+
return filename.includes(pattern);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
if (isExempt) {
|
|
59
|
+
return {};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
ThrowStatement(node) {
|
|
64
|
+
// Prüfe ob es ein 'throw new Error()' ist
|
|
65
|
+
if (
|
|
66
|
+
node.argument &&
|
|
67
|
+
node.argument.type === "NewExpression" &&
|
|
68
|
+
node.argument.callee &&
|
|
69
|
+
node.argument.callee.name === "Error"
|
|
70
|
+
) {
|
|
71
|
+
// Prüfe ob es ein erlaubtes Pattern ist
|
|
72
|
+
const errorMessage = node.argument.arguments[0];
|
|
73
|
+
if (errorMessage && errorMessage.type === "Literal") {
|
|
74
|
+
const message = errorMessage.value;
|
|
75
|
+
const isAllowed = allowedPatterns.some(pattern =>
|
|
76
|
+
message && message.includes(pattern)
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
if (!isAllowed) {
|
|
80
|
+
// Generiere vorgeschlagenen Error-Klassennamen basierend auf Kontext
|
|
81
|
+
const suggestedName = generateErrorClassName(node, context);
|
|
82
|
+
|
|
83
|
+
context.report({
|
|
84
|
+
node,
|
|
85
|
+
messageId: "useCustomError",
|
|
86
|
+
data: {
|
|
87
|
+
suggestedName,
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
} else {
|
|
92
|
+
context.report({
|
|
93
|
+
node,
|
|
94
|
+
messageId: "useCustomError",
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Prüfe ob es ein 'throw Object.assign(new Error(), ...)' ist
|
|
100
|
+
if (
|
|
101
|
+
node.argument &&
|
|
102
|
+
node.argument.type === "CallExpression" &&
|
|
103
|
+
node.argument.callee &&
|
|
104
|
+
node.argument.callee.type === "MemberExpression" &&
|
|
105
|
+
node.argument.callee.object &&
|
|
106
|
+
node.argument.callee.object.name === "Object" &&
|
|
107
|
+
node.argument.callee.property &&
|
|
108
|
+
node.argument.callee.property.name === "assign"
|
|
109
|
+
) {
|
|
110
|
+
// Prüfe ob das erste Argument 'new Error()' ist
|
|
111
|
+
const firstArg = node.argument.arguments[0];
|
|
112
|
+
if (
|
|
113
|
+
firstArg &&
|
|
114
|
+
firstArg.type === "NewExpression" &&
|
|
115
|
+
firstArg.callee &&
|
|
116
|
+
firstArg.callee.name === "Error"
|
|
117
|
+
) {
|
|
118
|
+
const suggestedName = generateErrorClassName(node, context);
|
|
119
|
+
|
|
120
|
+
// Extrahiere Error-Message für bessere Fehlermeldung
|
|
121
|
+
let errorMessage = "your error message";
|
|
122
|
+
if (firstArg.arguments && firstArg.arguments[0]) {
|
|
123
|
+
const messageArg = firstArg.arguments[0];
|
|
124
|
+
if (messageArg.type === "Literal") {
|
|
125
|
+
errorMessage = String(messageArg.value);
|
|
126
|
+
} else if (messageArg.type === "TemplateLiteral" && messageArg.quasis && messageArg.quasis[0]) {
|
|
127
|
+
errorMessage = messageArg.quasis[0].value.raw;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
context.report({
|
|
132
|
+
node,
|
|
133
|
+
messageId: "noObjectAssignError",
|
|
134
|
+
data: {
|
|
135
|
+
suggestedName,
|
|
136
|
+
errorMessage,
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Generiert einen vorgeschlagenen Error-Klassennamen basierend auf dem Kontext
|
|
148
|
+
*/
|
|
149
|
+
function generateErrorClassName(node, context) {
|
|
150
|
+
const filename = context.getFilename();
|
|
151
|
+
const functionName = getFunctionName(node, context);
|
|
152
|
+
|
|
153
|
+
// Extrahiere Entity/Service-Namen aus Dateiname
|
|
154
|
+
const fileNameMatch = filename.match(/\/([^/]+)Service\.ts$/);
|
|
155
|
+
const entityName = fileNameMatch ? fileNameMatch[1] : "Unknown";
|
|
156
|
+
|
|
157
|
+
// Extrahiere Error-Message aus verschiedenen Strukturen
|
|
158
|
+
let errorMessage = null;
|
|
159
|
+
|
|
160
|
+
// Fall 1: throw new Error("message")
|
|
161
|
+
if (node.argument && node.argument.type === "NewExpression" && node.argument.arguments[0]) {
|
|
162
|
+
errorMessage = node.argument.arguments[0];
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Fall 2: throw Object.assign(new Error("message"), {...})
|
|
166
|
+
if (node.argument && node.argument.type === "CallExpression") {
|
|
167
|
+
const firstArg = node.argument.arguments[0];
|
|
168
|
+
if (firstArg && firstArg.type === "NewExpression" && firstArg.arguments[0]) {
|
|
169
|
+
errorMessage = firstArg.arguments[0];
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Generiere basierend auf Error-Message
|
|
174
|
+
if (errorMessage && (errorMessage.type === "Literal" || errorMessage.type === "TemplateLiteral")) {
|
|
175
|
+
let message = "";
|
|
176
|
+
|
|
177
|
+
if (errorMessage.type === "Literal") {
|
|
178
|
+
message = String(errorMessage.value).toLowerCase();
|
|
179
|
+
} else if (errorMessage.type === "TemplateLiteral" && errorMessage.quasis) {
|
|
180
|
+
// Kombiniere alle quasi-Teile für vollständige Message-Analyse
|
|
181
|
+
message = errorMessage.quasis.map(q => q.value.raw).join('').toLowerCase();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (message.includes("not found")) {
|
|
185
|
+
// Versuche spezifischen Entity-Namen aus der ORIGINAL-Message zu extrahieren (nicht lowercase!)
|
|
186
|
+
// Hole die Original-Message
|
|
187
|
+
let originalMessage = "";
|
|
188
|
+
if (errorMessage.type === "Literal") {
|
|
189
|
+
originalMessage = String(errorMessage.value);
|
|
190
|
+
} else if (errorMessage.type === "TemplateLiteral" && errorMessage.quasis) {
|
|
191
|
+
// Kombiniere alle quasi-Teile für vollständige Original-Message
|
|
192
|
+
originalMessage = errorMessage.quasis.map(q => q.value.raw).join('');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Pattern: "ItemStatsType with id" oder "User not found" (mit Original-Groß-/Kleinschreibung)
|
|
196
|
+
const match1 = originalMessage.match(/^([A-Za-z][A-Za-z0-9]*)\s+(?:with|not found)/);
|
|
197
|
+
if (match1 && match1[1]) {
|
|
198
|
+
return `${match1[1]}NotFoundError`;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return `${entityName}NotFoundError`;
|
|
202
|
+
}
|
|
203
|
+
if (message.includes("invalid") || message.includes("validation")) {
|
|
204
|
+
return `Invalid${entityName}Error`;
|
|
205
|
+
}
|
|
206
|
+
if (message.includes("unauthorized") || message.includes("permission")) {
|
|
207
|
+
return `Unauthorized${entityName}Error`;
|
|
208
|
+
}
|
|
209
|
+
if (message.includes("already exists") || message.includes("duplicate")) {
|
|
210
|
+
return `${entityName}AlreadyExistsError`;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Fallback basierend auf Funktionsname
|
|
215
|
+
if (functionName) {
|
|
216
|
+
return `${functionName}Error`;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return `${entityName}Error`;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Findet den Funktionsnamen, in dem der Error geworfen wird
|
|
224
|
+
*/
|
|
225
|
+
function getFunctionName(node, context) {
|
|
226
|
+
let current = node.parent;
|
|
227
|
+
|
|
228
|
+
while (current) {
|
|
229
|
+
if (current.type === "FunctionDeclaration" && current.id) {
|
|
230
|
+
return current.id.name;
|
|
231
|
+
}
|
|
232
|
+
if (current.type === "MethodDefinition" && current.key) {
|
|
233
|
+
return current.key.name;
|
|
234
|
+
}
|
|
235
|
+
if (current.type === "ArrowFunctionExpression" && current.parent && current.parent.type === "VariableDeclarator" && current.parent.id) {
|
|
236
|
+
return current.parent.id.name;
|
|
237
|
+
}
|
|
238
|
+
current = current.parent;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint-Regel: enforce-database-transaction-safety
|
|
3
|
+
* Stellt sicher, dass alle schreibenden Datenbankoperationen in Transaktionen eingewrapped sind
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
7
|
+
const enforceDatabaseTransactionSafetyRule = {
|
|
8
|
+
meta: {
|
|
9
|
+
type: "problem",
|
|
10
|
+
docs: {
|
|
11
|
+
description: "Alle schreibenden Datenbankoperationen müssen in Transaktionen eingewrapped sein",
|
|
12
|
+
category: "Database Safety",
|
|
13
|
+
recommended: true,
|
|
14
|
+
},
|
|
15
|
+
schema: [],
|
|
16
|
+
messages: {
|
|
17
|
+
unsafeDbOperation: "Datenbankoperation '{{operation}}' muss in einer Transaktion ausgeführt werden. Verwende this.dataSource.transaction().",
|
|
18
|
+
missingTransactionImport: "Service führt Datenbankoperationen aus, aber importiert DataSource nicht.",
|
|
19
|
+
bulkOperationWithoutTransaction: "Bulk-Operationen wie '{{operation}}' müssen zwingend in Transaktionen ausgeführt werden.",
|
|
20
|
+
cascadeWithoutTransaction: "Operationen mit Cascade-Effekten müssen in Transaktionen ausgeführt werden für Rollback-Sicherheit.",
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
create(context) {
|
|
24
|
+
const filename = context.getFilename();
|
|
25
|
+
const isServiceFile = filename.includes("/service/") && filename.endsWith(".ts") && !filename.includes("test");
|
|
26
|
+
|
|
27
|
+
if (!isServiceFile) return {};
|
|
28
|
+
|
|
29
|
+
let hasDataSourceImport = false;
|
|
30
|
+
let hasTransaction = false;
|
|
31
|
+
const writingOperations = new Set();
|
|
32
|
+
|
|
33
|
+
// Schreibende Datenbankoperationen, die Transaktionen erfordern
|
|
34
|
+
const WRITING_METHODS = [
|
|
35
|
+
"save", "insert", "update", "delete", "remove", "softDelete", "recover",
|
|
36
|
+
"upsert", "createQueryBuilder", "manager", "merge"
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
// Bulk-Operationen, die besonders kritisch sind
|
|
40
|
+
const BULK_METHODS = [
|
|
41
|
+
"saveBulk", "insertBulk", "updateBulk", "deleteBulk", "removeBulk"
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
function isInTransactionBlock(node) {
|
|
45
|
+
// Prüfe ob sich der Knoten innerhalb eines Transaction-Callbacks befindet
|
|
46
|
+
let parent = node.parent;
|
|
47
|
+
while (parent) {
|
|
48
|
+
if (parent.type === "CallExpression") {
|
|
49
|
+
if (parent.callee?.type === "MemberExpression" &&
|
|
50
|
+
parent.callee.property?.name === "transaction") {
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (parent.type === "ArrowFunctionExpression" || parent.type === "FunctionExpression") {
|
|
55
|
+
// Prüfe ob diese Funktion ein Transaction-Callback ist
|
|
56
|
+
const grandParent = parent.parent;
|
|
57
|
+
if (grandParent?.type === "CallExpression" &&
|
|
58
|
+
grandParent.callee?.type === "MemberExpression" &&
|
|
59
|
+
grandParent.callee.property?.name === "transaction") {
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
parent = parent.parent;
|
|
64
|
+
}
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function hasEntityWithCascade(node) {
|
|
69
|
+
// Vereinfachte Prüfung auf Cascade-Operationen in der Nähe
|
|
70
|
+
const sourceCode = context.getSourceCode();
|
|
71
|
+
const text = sourceCode.getText(node);
|
|
72
|
+
return text.includes("cascade") || text.includes("Cascade");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
ImportDeclaration(node) {
|
|
77
|
+
if (node.source.value === "typeorm" || node.source.value.includes("data-source")) {
|
|
78
|
+
const hasDataSourceSpecifier = node.specifiers.some(spec =>
|
|
79
|
+
(spec.type === "ImportDefaultSpecifier" && spec.local.name.includes("DataSource")) ||
|
|
80
|
+
(spec.type === "ImportSpecifier" && spec.imported.name === "DataSource")
|
|
81
|
+
);
|
|
82
|
+
if (hasDataSourceSpecifier) {
|
|
83
|
+
hasDataSourceImport = true;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
CallExpression(node) {
|
|
89
|
+
// Erkenne Transaction-Blöcke
|
|
90
|
+
if (node.callee?.type === "MemberExpression" &&
|
|
91
|
+
node.callee.property?.name === "transaction") {
|
|
92
|
+
hasTransaction = true;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Prüfe auf Repository-Operationen
|
|
96
|
+
if (node.callee?.type === "MemberExpression") {
|
|
97
|
+
const methodName = node.callee.property?.name;
|
|
98
|
+
|
|
99
|
+
if (WRITING_METHODS.includes(methodName)) {
|
|
100
|
+
writingOperations.add({
|
|
101
|
+
node,
|
|
102
|
+
operation: methodName,
|
|
103
|
+
isBulk: BULK_METHODS.includes(methodName),
|
|
104
|
+
hasCascade: hasEntityWithCascade(node),
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// Prüfe ob Operation in Transaktion ist
|
|
108
|
+
if (!isInTransactionBlock(node)) {
|
|
109
|
+
if (BULK_METHODS.includes(methodName)) {
|
|
110
|
+
context.report({
|
|
111
|
+
node,
|
|
112
|
+
messageId: "bulkOperationWithoutTransaction",
|
|
113
|
+
data: {
|
|
114
|
+
operation: methodName,
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
} else if (hasEntityWithCascade(node)) {
|
|
118
|
+
context.report({
|
|
119
|
+
node,
|
|
120
|
+
messageId: "cascadeWithoutTransaction",
|
|
121
|
+
});
|
|
122
|
+
} else {
|
|
123
|
+
context.report({
|
|
124
|
+
node,
|
|
125
|
+
messageId: "unsafeDbOperation",
|
|
126
|
+
data: {
|
|
127
|
+
operation: methodName,
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Prüfe auf QueryBuilder-Operationen
|
|
136
|
+
if (node.callee?.type === "MemberExpression") {
|
|
137
|
+
const methodName = node.callee.property?.name;
|
|
138
|
+
const dangerousQueryMethods = ["execute", "getMany", "getOne"];
|
|
139
|
+
|
|
140
|
+
if (dangerousQueryMethods.includes(methodName)) {
|
|
141
|
+
// Prüfe ob es sich um eine schreibende Query handelt
|
|
142
|
+
let currentNode = node;
|
|
143
|
+
while (currentNode.parent && currentNode.parent.type === "MemberExpression") {
|
|
144
|
+
currentNode = currentNode.parent;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const sourceCode = context.getSourceCode();
|
|
148
|
+
const chainText = sourceCode.getText(currentNode);
|
|
149
|
+
|
|
150
|
+
if (chainText.includes("insert") || chainText.includes("update") ||
|
|
151
|
+
chainText.includes("delete") || chainText.includes("set")) {
|
|
152
|
+
|
|
153
|
+
if (!isInTransactionBlock(node)) {
|
|
154
|
+
context.report({
|
|
155
|
+
node,
|
|
156
|
+
messageId: "unsafeDbOperation",
|
|
157
|
+
data: {
|
|
158
|
+
operation: "QueryBuilder." + methodName,
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
|
|
167
|
+
"Program:exit"() {
|
|
168
|
+
if (writingOperations.size > 0 && !hasDataSourceImport) {
|
|
169
|
+
context.report({
|
|
170
|
+
loc: { line: 1, column: 0 },
|
|
171
|
+
messageId: "missingTransactionImport",
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
export default enforceDatabaseTransactionSafetyRule;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint-Regel: enforce-dto-constructor
|
|
3
|
+
* Verbietet die Verwendung von Constructor-Aufrufen für DTOs - DTOs sollen mit create() erstellt werden
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Hilfsfunktion um zu prüfen, ob wir uns innerhalb einer DTO-Factory-Methode befinden
|
|
7
|
+
function isInsideCreateMethod(node) {
|
|
8
|
+
let parent = node.parent;
|
|
9
|
+
while (parent) {
|
|
10
|
+
// Prüfe ob wir in einer statischen Methode sind
|
|
11
|
+
if (parent.type === "MethodDefinition" &&
|
|
12
|
+
parent.static === true &&
|
|
13
|
+
parent.key.type === "Identifier" &&
|
|
14
|
+
(parent.key.name === "create" ||
|
|
15
|
+
parent.key.name === "fromEntity" ||
|
|
16
|
+
parent.key.name === "fromRequestDto" ||
|
|
17
|
+
parent.key.name === "fromEntityArray")) {
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Prüfe ob wir in einer normalen Methode sind (für toEntity)
|
|
22
|
+
if (parent.type === "MethodDefinition" &&
|
|
23
|
+
parent.static === false &&
|
|
24
|
+
parent.key.type === "Identifier" &&
|
|
25
|
+
parent.key.name === "toEntity") {
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Prüfe ob wir in einer Arrow-Function oder FunctionExpression sind, die DTO-Factory-Namen enthält
|
|
30
|
+
if ((parent.type === "ArrowFunctionExpression" || parent.type === "FunctionExpression") &&
|
|
31
|
+
parent.parent &&
|
|
32
|
+
parent.parent.type === "VariableDeclarator" &&
|
|
33
|
+
parent.parent.id.type === "Identifier" &&
|
|
34
|
+
(parent.parent.id.name.includes("create") ||
|
|
35
|
+
parent.parent.id.name.includes("fromEntity") ||
|
|
36
|
+
parent.parent.id.name.includes("toEntity"))) {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
parent = parent.parent;
|
|
41
|
+
}
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
46
|
+
const enforceDtoConstructorRule = {
|
|
47
|
+
meta: {
|
|
48
|
+
type: "problem",
|
|
49
|
+
docs: {
|
|
50
|
+
description: "DTOs dürfen nicht mit 'new' erstellt werden, verwende stattdessen create()",
|
|
51
|
+
category: "Best Practices",
|
|
52
|
+
recommended: true,
|
|
53
|
+
},
|
|
54
|
+
fixable: "code",
|
|
55
|
+
schema: [],
|
|
56
|
+
messages: {
|
|
57
|
+
noConstructorCall: "DTOs dürfen nicht mit 'new {{dtoName}}()' erstellt werden. Verwende stattdessen {{dtoName}}.create()",
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
create(context) {
|
|
61
|
+
return {
|
|
62
|
+
// Prüfe auf Constructor-Aufrufe mit DTOs
|
|
63
|
+
NewExpression(node) {
|
|
64
|
+
if (node.callee.type === "Identifier") {
|
|
65
|
+
const className = node.callee.name;
|
|
66
|
+
|
|
67
|
+
// Prüfe ob es sich um eine DTO-Klasse handelt
|
|
68
|
+
if (className && className.endsWith("Dto")) {
|
|
69
|
+
// Prüfe ob wir uns innerhalb einer create-Methode befinden
|
|
70
|
+
if (isInsideCreateMethod(node)) {
|
|
71
|
+
return; // Erlaube 'new' innerhalb von create-Methoden
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
context.report({
|
|
75
|
+
node,
|
|
76
|
+
messageId: "noConstructorCall",
|
|
77
|
+
data: {
|
|
78
|
+
dtoName: className,
|
|
79
|
+
},
|
|
80
|
+
fix(fixer) {
|
|
81
|
+
return fixer.replaceText(node, `${className}.create()`);
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export default {
|
|
92
|
+
rules: {
|
|
93
|
+
"enforce-dto-constructor": enforceDtoConstructorRule,
|
|
94
|
+
},
|
|
95
|
+
};
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Enforce 1:1 type matching for DTO create method parameters.
|
|
3
|
+
* @author echoes-of-order
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { AST_NODE_TYPES } from '@typescript-eslint/utils';
|
|
7
|
+
|
|
8
|
+
export default {
|
|
9
|
+
meta: {
|
|
10
|
+
type: "problem",
|
|
11
|
+
docs: {
|
|
12
|
+
description: "Enforce that DTO create method parameters match the DTO's properties 1:1 regarding optionality.",
|
|
13
|
+
category: "Possible Errors",
|
|
14
|
+
recommended: true,
|
|
15
|
+
},
|
|
16
|
+
messages: {
|
|
17
|
+
parameterOptionalityMismatch: "Parameter '{{paramName}}' in create method has optionality mismatch with DTO property '{{propertyName}}'. DTO property is '{{dtoOptionality}}', but parameter is '{{paramOptionality}}'.",
|
|
18
|
+
propertyNotFound: "Parameter '{{paramName}}' in create method does not correspond to a property in the DTO.",
|
|
19
|
+
},
|
|
20
|
+
schema: [],
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
create(context) {
|
|
24
|
+
/**
|
|
25
|
+
* Extrahiert Properties aus einer Klasse (inkl. Parent-Klasse)
|
|
26
|
+
*/
|
|
27
|
+
function getClassProperties(classNode) {
|
|
28
|
+
const properties = new Map();
|
|
29
|
+
|
|
30
|
+
for (const member of classNode.body.body) {
|
|
31
|
+
if (member.type === AST_NODE_TYPES.PropertyDefinition) {
|
|
32
|
+
const propertyName = member.key.name;
|
|
33
|
+
const isOptional = member.optional || false;
|
|
34
|
+
properties.set(propertyName, { isOptional });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (classNode.superClass && classNode.superClass.type === AST_NODE_TYPES.Identifier) {
|
|
39
|
+
const sourceCode = context.getSourceCode();
|
|
40
|
+
const scope = sourceCode.getScope(classNode);
|
|
41
|
+
const parentClassVar = scope.variables.find((v) => v.name === classNode.superClass.name);
|
|
42
|
+
|
|
43
|
+
if (parentClassVar && parentClassVar.defs.length > 0) {
|
|
44
|
+
const parentDef = parentClassVar.defs[0];
|
|
45
|
+
if (parentDef.node.type === AST_NODE_TYPES.ClassDeclaration) {
|
|
46
|
+
const parentProperties = getClassProperties(parentDef.node);
|
|
47
|
+
for (const [name, info] of parentProperties) {
|
|
48
|
+
if (!properties.has(name)) {
|
|
49
|
+
properties.set(name, info);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return properties;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Extrahiert Properties aus einem Type Literal
|
|
61
|
+
*/
|
|
62
|
+
function getTypeLiteralProperties(typeLiteral) {
|
|
63
|
+
const properties = new Map();
|
|
64
|
+
|
|
65
|
+
for (const member of typeLiteral.members) {
|
|
66
|
+
if (member.type === AST_NODE_TYPES.TSPropertySignature) {
|
|
67
|
+
const propertyName = member.key.name;
|
|
68
|
+
const isOptional = member.optional || false;
|
|
69
|
+
properties.set(propertyName, { isOptional });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return properties;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
MethodDefinition(node) {
|
|
78
|
+
if (
|
|
79
|
+
node.kind === 'method' &&
|
|
80
|
+
node.static &&
|
|
81
|
+
node.key.type === AST_NODE_TYPES.Identifier &&
|
|
82
|
+
node.key.name === 'create' &&
|
|
83
|
+
node.parent.type === AST_NODE_TYPES.ClassBody &&
|
|
84
|
+
node.parent.parent.type === AST_NODE_TYPES.ClassDeclaration
|
|
85
|
+
) {
|
|
86
|
+
const classNode = node.parent.parent;
|
|
87
|
+
|
|
88
|
+
if (classNode.superClass) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const classProperties = getClassProperties(classNode);
|
|
93
|
+
|
|
94
|
+
// Find the 'data' parameter (with or without default value)
|
|
95
|
+
const dataParam = node.value.params.find(
|
|
96
|
+
(param) => {
|
|
97
|
+
if (param.type === AST_NODE_TYPES.Identifier && param.name === 'data') {
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
// Handle default parameters: AssignmentPattern with Identifier
|
|
101
|
+
if (param.type === AST_NODE_TYPES.AssignmentPattern &&
|
|
102
|
+
param.left.type === AST_NODE_TYPES.Identifier &&
|
|
103
|
+
param.left.name === 'data') {
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
if (!dataParam) {
|
|
111
|
+
return; // 'data' parameter not found
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Get type annotation from the parameter (handle both regular and default parameters)
|
|
115
|
+
let typeAnnotation;
|
|
116
|
+
if (dataParam.type === AST_NODE_TYPES.Identifier) {
|
|
117
|
+
typeAnnotation = dataParam.typeAnnotation;
|
|
118
|
+
} else if (dataParam.type === AST_NODE_TYPES.AssignmentPattern) {
|
|
119
|
+
typeAnnotation = dataParam.left.typeAnnotation;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (!typeAnnotation?.typeAnnotation || typeAnnotation.typeAnnotation.type !== AST_NODE_TYPES.TSTypeLiteral) {
|
|
123
|
+
return; // type annotation not found or not an object literal
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const dataTypeLiteral = typeAnnotation.typeAnnotation;
|
|
127
|
+
const dataProperties = getTypeLiteralProperties(dataTypeLiteral);
|
|
128
|
+
|
|
129
|
+
// Check each parameter property against class properties
|
|
130
|
+
for (const [paramName, paramInfo] of dataProperties) {
|
|
131
|
+
const classProperty = classProperties.get(paramName);
|
|
132
|
+
|
|
133
|
+
if (!classProperty) {
|
|
134
|
+
context.report({
|
|
135
|
+
node: dataTypeLiteral,
|
|
136
|
+
messageId: "propertyNotFound",
|
|
137
|
+
data: { paramName },
|
|
138
|
+
});
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (paramInfo.isOptional !== classProperty.isOptional) {
|
|
143
|
+
context.report({
|
|
144
|
+
node: dataTypeLiteral,
|
|
145
|
+
messageId: "parameterOptionalityMismatch",
|
|
146
|
+
data: {
|
|
147
|
+
paramName,
|
|
148
|
+
propertyName: paramName,
|
|
149
|
+
dtoOptionality: classProperty.isOptional ? 'optional' : 'required',
|
|
150
|
+
paramOptionality: paramInfo.isOptional ? 'optional' : 'required',
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Check if all required class properties are present in parameters
|
|
157
|
+
for (const [className, classProperty] of classProperties) {
|
|
158
|
+
if (!classProperty.isOptional && !dataProperties.has(className)) {
|
|
159
|
+
context.report({
|
|
160
|
+
node: dataTypeLiteral,
|
|
161
|
+
messageId: "propertyNotFound",
|
|
162
|
+
data: { paramName: className },
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
},
|
|
170
|
+
};
|