@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,203 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
meta: {
|
|
3
|
+
type: "problem",
|
|
4
|
+
docs: {
|
|
5
|
+
description: "Enforce that translations field is always required XxxTranslation*[] array (e.g., ItemTranslationEntityDto[]) - never optional, nullable, or non-Translation type",
|
|
6
|
+
category: "Best Practices",
|
|
7
|
+
recommended: true,
|
|
8
|
+
},
|
|
9
|
+
messages: {
|
|
10
|
+
translationsOptional: "Field 'translations' must not be optional. Translations are mandatory - use empty array (e.g., ItemTranslationEntityDto[]) instead of undefined.",
|
|
11
|
+
translationsNullable: "Field 'translations' must not be nullable. Translations are mandatory - use empty array (e.g., ItemTranslationEntityDto[]) instead of null.",
|
|
12
|
+
translationsMissingRequired: "Field 'translations' must be required. Remove @IsOptional decorator - translations are mandatory.",
|
|
13
|
+
translationsWrongType: "Field 'translations' must be of type XxxTranslation*[] (e.g., ItemTranslationEntityDto[], CharacterTranslationDto[]), not '{{actualType}}'. Type name must contain 'Translation'.",
|
|
14
|
+
},
|
|
15
|
+
schema: [],
|
|
16
|
+
},
|
|
17
|
+
create (context) {
|
|
18
|
+
// Helper function to check if a type is *Translation* array (e.g., ItemTranslationEntityDto[], CharacterTranslationDto[])
|
|
19
|
+
function isTranslationType(typeAnnotation) {
|
|
20
|
+
if (!typeAnnotation) return false;
|
|
21
|
+
|
|
22
|
+
const type = typeAnnotation.typeAnnotation || typeAnnotation;
|
|
23
|
+
|
|
24
|
+
// Check for XxxTranslation*[] array (e.g., ItemTranslationEntityDto[])
|
|
25
|
+
if (type.type === "TSArrayType" &&
|
|
26
|
+
type.elementType &&
|
|
27
|
+
type.elementType.type === "TSTypeReference" &&
|
|
28
|
+
type.elementType.typeName &&
|
|
29
|
+
type.elementType.typeName.type === "Identifier" &&
|
|
30
|
+
type.elementType.typeName.name.includes("Translation")) {
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Check for Array<XxxTranslation*> (e.g., Array<ItemTranslationEntityDto>)
|
|
35
|
+
if (type.type === "TSTypeReference" &&
|
|
36
|
+
type.typeName &&
|
|
37
|
+
type.typeName.type === "Identifier" &&
|
|
38
|
+
type.typeName.name === "Array" &&
|
|
39
|
+
type.typeParameters &&
|
|
40
|
+
type.typeParameters.params &&
|
|
41
|
+
type.typeParameters.params.length === 1 &&
|
|
42
|
+
type.typeParameters.params[0].type === "TSTypeReference" &&
|
|
43
|
+
type.typeParameters.params[0].typeName &&
|
|
44
|
+
type.typeParameters.params[0].typeName.type === "Identifier" &&
|
|
45
|
+
type.typeParameters.params[0].typeName.name.includes("Translation")) {
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Helper function to get type name for error messages
|
|
53
|
+
function getTypeName(typeAnnotation) {
|
|
54
|
+
if (!typeAnnotation) return "unknown";
|
|
55
|
+
|
|
56
|
+
const type = typeAnnotation.typeAnnotation || typeAnnotation;
|
|
57
|
+
|
|
58
|
+
if (type.type === "TSTypeReference" && type.typeName) {
|
|
59
|
+
return type.typeName.name || "unknown";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (type.type === "TSArrayType") {
|
|
63
|
+
return "Array";
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return type.type || "unknown";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
PropertyDefinition (node) {
|
|
71
|
+
if (node.key.type !== "Identifier" || node.key.name !== "translations") {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const isOptional = node.optional === true;
|
|
76
|
+
if (isOptional) {
|
|
77
|
+
context.report({
|
|
78
|
+
node,
|
|
79
|
+
messageId: "translationsOptional",
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (node.typeAnnotation && node.typeAnnotation.typeAnnotation) {
|
|
86
|
+
const typeAnnotation = node.typeAnnotation.typeAnnotation;
|
|
87
|
+
|
|
88
|
+
if (typeAnnotation.type === "TSUnionType") {
|
|
89
|
+
const hasNull = typeAnnotation.types.some((t) => t.type === "TSNullKeyword");
|
|
90
|
+
const hasUndefined = typeAnnotation.types.some((t) => t.type === "TSUndefinedKeyword");
|
|
91
|
+
|
|
92
|
+
if (hasNull) {
|
|
93
|
+
context.report({
|
|
94
|
+
node,
|
|
95
|
+
messageId: "translationsNullable",
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (hasUndefined) {
|
|
100
|
+
context.report({
|
|
101
|
+
node,
|
|
102
|
+
messageId: "translationsOptional",
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Check if type is Translation or Translation[]
|
|
108
|
+
if (!isTranslationType(node.typeAnnotation)) {
|
|
109
|
+
context.report({
|
|
110
|
+
node,
|
|
111
|
+
messageId: "translationsWrongType",
|
|
112
|
+
data: {
|
|
113
|
+
actualType: getTypeName(node.typeAnnotation)
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const decorators = node.decorators || [];
|
|
120
|
+
const hasIsOptional = decorators.some((decorator) => {
|
|
121
|
+
if (decorator.expression.type === "CallExpression") {
|
|
122
|
+
return decorator.expression.callee.name === "IsOptional";
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return false;
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
if (hasIsOptional) {
|
|
129
|
+
context.report({
|
|
130
|
+
node,
|
|
131
|
+
messageId: "translationsMissingRequired",
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
ClassProperty (node) {
|
|
136
|
+
if (node.key.type !== "Identifier" || node.key.name !== "translations") {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const isOptional = node.optional === true;
|
|
141
|
+
if (isOptional) {
|
|
142
|
+
context.report({
|
|
143
|
+
node,
|
|
144
|
+
messageId: "translationsOptional",
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (node.typeAnnotation && node.typeAnnotation.typeAnnotation) {
|
|
151
|
+
const typeAnnotation = node.typeAnnotation.typeAnnotation;
|
|
152
|
+
|
|
153
|
+
if (typeAnnotation.type === "TSUnionType") {
|
|
154
|
+
const hasNull = typeAnnotation.types.some((t) => t.type === "TSNullKeyword");
|
|
155
|
+
const hasUndefined = typeAnnotation.types.some((t) => t.type === "TSUndefinedKeyword");
|
|
156
|
+
|
|
157
|
+
if (hasNull) {
|
|
158
|
+
context.report({
|
|
159
|
+
node,
|
|
160
|
+
messageId: "translationsNullable",
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (hasUndefined) {
|
|
165
|
+
context.report({
|
|
166
|
+
node,
|
|
167
|
+
messageId: "translationsOptional",
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Check if type is Translation or Translation[]
|
|
173
|
+
if (!isTranslationType(node.typeAnnotation)) {
|
|
174
|
+
context.report({
|
|
175
|
+
node,
|
|
176
|
+
messageId: "translationsWrongType",
|
|
177
|
+
data: {
|
|
178
|
+
actualType: getTypeName(node.typeAnnotation)
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const decorators = node.decorators || [];
|
|
185
|
+
const hasIsOptional = decorators.some((decorator) => {
|
|
186
|
+
if (decorator.expression.type === "CallExpression") {
|
|
187
|
+
return decorator.expression.callee.name === "IsOptional";
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return false;
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
if (hasIsOptional) {
|
|
194
|
+
context.report({
|
|
195
|
+
node,
|
|
196
|
+
messageId: "translationsMissingRequired",
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview TypeORM Naming Conventions Enforcement
|
|
3
|
+
* Stellt sicher, dass:
|
|
4
|
+
* 1. Tabellennamen im snake_case Format sind (@Entity)
|
|
5
|
+
* 2. Spaltennamen explizit im snake_case Format angegeben werden (@Column, @PrimaryGeneratedColumn, etc.)
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Konvertiert PascalCase/camelCase zu snake_case
|
|
10
|
+
*/
|
|
11
|
+
function toSnakeCase(str) {
|
|
12
|
+
return str
|
|
13
|
+
.replace(/([A-Z])/g, "_$1")
|
|
14
|
+
.toLowerCase()
|
|
15
|
+
.replace(/^_/, "");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Prüft ob ein String im snake_case Format ist
|
|
20
|
+
*/
|
|
21
|
+
function isSnakeCase(str) {
|
|
22
|
+
return /^[a-z][a-z0-9]*(_[a-z0-9]+)*$/.test(str);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Extrahiert den Tabellennamen aus @Entity Decorator
|
|
27
|
+
*/
|
|
28
|
+
function getEntityTableName(decorator) {
|
|
29
|
+
if (!decorator.expression.arguments || decorator.expression.arguments.length === 0) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const firstArg = decorator.expression.arguments[0];
|
|
34
|
+
|
|
35
|
+
// @Entity("TableName")
|
|
36
|
+
if (firstArg.type === "Literal") {
|
|
37
|
+
return firstArg.value;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// @Entity({ name: "TableName" })
|
|
41
|
+
if (firstArg.type === "ObjectExpression") {
|
|
42
|
+
const nameProp = firstArg.properties.find(
|
|
43
|
+
prop => prop.key && prop.key.name === "name"
|
|
44
|
+
);
|
|
45
|
+
if (nameProp && nameProp.value.type === "Literal") {
|
|
46
|
+
return nameProp.value.value;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Extrahiert den Spaltennamen aus Column-Decorator
|
|
55
|
+
*/
|
|
56
|
+
function getColumnName(decorator) {
|
|
57
|
+
if (!decorator.expression.arguments || decorator.expression.arguments.length === 0) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const firstArg = decorator.expression.arguments[0];
|
|
62
|
+
|
|
63
|
+
// @Column({ name: "column_name" })
|
|
64
|
+
if (firstArg.type === "ObjectExpression") {
|
|
65
|
+
const nameProp = firstArg.properties.find(
|
|
66
|
+
prop => prop.key && prop.key.name === "name"
|
|
67
|
+
);
|
|
68
|
+
if (nameProp && nameProp.value.type === "Literal") {
|
|
69
|
+
return nameProp.value.value;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// @PrimaryGeneratedColumn("uuid", { name: "column_name" })
|
|
74
|
+
if (firstArg.type === "Literal" && decorator.expression.arguments.length > 1) {
|
|
75
|
+
const secondArg = decorator.expression.arguments[1];
|
|
76
|
+
if (secondArg.type === "ObjectExpression") {
|
|
77
|
+
const nameProp = secondArg.properties.find(
|
|
78
|
+
prop => prop.key && prop.key.name === "name"
|
|
79
|
+
);
|
|
80
|
+
if (nameProp && nameProp.value.type === "Literal") {
|
|
81
|
+
return nameProp.value.value;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Prüft ob ein Decorator ein Column-Decorator ist
|
|
91
|
+
*/
|
|
92
|
+
function isColumnDecorator(decorator) {
|
|
93
|
+
const columnDecorators = [
|
|
94
|
+
"Column",
|
|
95
|
+
"PrimaryColumn",
|
|
96
|
+
"PrimaryGeneratedColumn",
|
|
97
|
+
"CreateDateColumn",
|
|
98
|
+
"UpdateDateColumn",
|
|
99
|
+
"DeleteDateColumn",
|
|
100
|
+
"VersionColumn",
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
return columnDecorators.includes(decorator.expression.callee?.name);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Prüft ob ein Decorator ein Relation-Decorator ist
|
|
108
|
+
*/
|
|
109
|
+
function isRelationDecorator(decorator) {
|
|
110
|
+
const relationDecorators = [
|
|
111
|
+
"ManyToOne",
|
|
112
|
+
"OneToOne",
|
|
113
|
+
"OneToMany",
|
|
114
|
+
"ManyToMany",
|
|
115
|
+
"JoinColumn",
|
|
116
|
+
"JoinTable",
|
|
117
|
+
];
|
|
118
|
+
|
|
119
|
+
return relationDecorators.includes(decorator.expression.callee?.name);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
123
|
+
const enforceTypeormNamingConventionsRule = {
|
|
124
|
+
meta: {
|
|
125
|
+
type: "problem",
|
|
126
|
+
docs: {
|
|
127
|
+
description: "Enforce snake_case naming conventions for TypeORM table and column names",
|
|
128
|
+
category: "TypeORM",
|
|
129
|
+
recommended: true,
|
|
130
|
+
},
|
|
131
|
+
fixable: "code",
|
|
132
|
+
schema: [],
|
|
133
|
+
messages: {
|
|
134
|
+
tableNotSnakeCase: "Table name '{{tableName}}' must be in snake_case format. Use '{{suggestedName}}' instead.",
|
|
135
|
+
columnNameRequired: "Column '{{propertyName}}' must have an explicit 'name' parameter in snake_case format. Add: name: '{{suggestedName}}'",
|
|
136
|
+
columnNotSnakeCase: "Column name '{{columnName}}' for property '{{propertyName}}' must be in snake_case format. Use '{{suggestedName}}' instead.",
|
|
137
|
+
relationColumnRequired: "Relation column '{{propertyName}}' should have an explicit 'name' parameter in snake_case format if it maps to a database column.",
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
create(context) {
|
|
142
|
+
const filename = context.getFilename();
|
|
143
|
+
const isEntityFile = filename.includes("/entity/") && filename.endsWith(".ts") && !filename.includes("test");
|
|
144
|
+
|
|
145
|
+
if (!isEntityFile) return {};
|
|
146
|
+
|
|
147
|
+
const sourceCode = context.getSourceCode();
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
/**
|
|
151
|
+
* Prüfe @Entity Decorator für Tabellennamen
|
|
152
|
+
*/
|
|
153
|
+
ClassDeclaration(node) {
|
|
154
|
+
if (!node.decorators) return;
|
|
155
|
+
|
|
156
|
+
const entityDecorator = node.decorators.find(
|
|
157
|
+
decorator => decorator.expression.callee?.name === "Entity"
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
if (!entityDecorator) return;
|
|
161
|
+
|
|
162
|
+
const tableName = getEntityTableName(entityDecorator);
|
|
163
|
+
|
|
164
|
+
if (tableName && !isSnakeCase(tableName)) {
|
|
165
|
+
const suggestedName = toSnakeCase(tableName);
|
|
166
|
+
|
|
167
|
+
context.report({
|
|
168
|
+
node: entityDecorator,
|
|
169
|
+
messageId: "tableNotSnakeCase",
|
|
170
|
+
data: {
|
|
171
|
+
tableName,
|
|
172
|
+
suggestedName,
|
|
173
|
+
},
|
|
174
|
+
fix(fixer) {
|
|
175
|
+
// Ersetze den Tabellennamen im Decorator
|
|
176
|
+
const decoratorText = sourceCode.getText(entityDecorator.expression);
|
|
177
|
+
|
|
178
|
+
// Fall 1: @Entity("TableName")
|
|
179
|
+
if (decoratorText.includes(`"${tableName}"`)) {
|
|
180
|
+
const newText = decoratorText.replace(`"${tableName}"`, `"${suggestedName}"`);
|
|
181
|
+
return fixer.replaceText(entityDecorator.expression, newText);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Fall 2: @Entity('TableName')
|
|
185
|
+
if (decoratorText.includes(`'${tableName}'`)) {
|
|
186
|
+
const newText = decoratorText.replace(`'${tableName}'`, `'${suggestedName}'`);
|
|
187
|
+
return fixer.replaceText(entityDecorator.expression, newText);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Fall 3: @Entity({ name: "TableName" })
|
|
191
|
+
const newText = decoratorText.replace(
|
|
192
|
+
new RegExp(`name:\\s*["']${tableName}["']`),
|
|
193
|
+
`name: "${suggestedName}"`
|
|
194
|
+
);
|
|
195
|
+
return fixer.replaceText(entityDecorator.expression, newText);
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Prüfe Column-Decorators für Spaltennamen
|
|
203
|
+
*/
|
|
204
|
+
PropertyDefinition(node) {
|
|
205
|
+
if (!node.decorators || node.decorators.length === 0) return;
|
|
206
|
+
|
|
207
|
+
const propertyName = node.key.name;
|
|
208
|
+
|
|
209
|
+
// Prüfe alle Column-Decorators
|
|
210
|
+
node.decorators.forEach(decorator => {
|
|
211
|
+
if (isColumnDecorator(decorator)) {
|
|
212
|
+
const columnName = getColumnName(decorator);
|
|
213
|
+
const suggestedName = toSnakeCase(propertyName);
|
|
214
|
+
|
|
215
|
+
// Fall 1: Kein name-Parameter angegeben
|
|
216
|
+
if (!columnName) {
|
|
217
|
+
context.report({
|
|
218
|
+
node: decorator,
|
|
219
|
+
messageId: "columnNameRequired",
|
|
220
|
+
data: {
|
|
221
|
+
propertyName,
|
|
222
|
+
suggestedName,
|
|
223
|
+
},
|
|
224
|
+
fix(fixer) {
|
|
225
|
+
const decoratorName = decorator.expression.callee.name;
|
|
226
|
+
const decoratorText = sourceCode.getText(decorator.expression);
|
|
227
|
+
|
|
228
|
+
// @Column() -> @Column({ name: "column_name" })
|
|
229
|
+
if (!decorator.expression.arguments || decorator.expression.arguments.length === 0) {
|
|
230
|
+
return fixer.replaceText(
|
|
231
|
+
decorator.expression,
|
|
232
|
+
`${decoratorName}({ name: "${suggestedName}" })`
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const firstArg = decorator.expression.arguments[0];
|
|
237
|
+
|
|
238
|
+
// @PrimaryGeneratedColumn("uuid") -> @PrimaryGeneratedColumn("uuid", { name: "column_name" })
|
|
239
|
+
if (firstArg.type === "Literal") {
|
|
240
|
+
const typeValue = sourceCode.getText(firstArg);
|
|
241
|
+
return fixer.replaceText(
|
|
242
|
+
decorator.expression,
|
|
243
|
+
`${decoratorName}(${typeValue}, { name: "${suggestedName}" })`
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// @Column({ type: "varchar" }) -> @Column({ name: "column_name", type: "varchar" })
|
|
248
|
+
if (firstArg.type === "ObjectExpression") {
|
|
249
|
+
const isMultiline = decoratorText.includes('\n');
|
|
250
|
+
|
|
251
|
+
if (isMultiline) {
|
|
252
|
+
// Mehrzeilig: Extrahiere Einrückung und füge name schön formatiert ein
|
|
253
|
+
const lines = decoratorText.split('\n');
|
|
254
|
+
const firstPropertyLine = lines.find(line => line.trim() && !line.trim().startsWith('{'));
|
|
255
|
+
const indent = firstPropertyLine ? firstPropertyLine.match(/^\s*/)[0] : ' ';
|
|
256
|
+
|
|
257
|
+
const newText = decoratorText.replace(
|
|
258
|
+
/\{\s*\n?\s*/,
|
|
259
|
+
`{\n${indent}name: "${suggestedName}",\n${indent}`
|
|
260
|
+
);
|
|
261
|
+
return fixer.replaceText(decorator.expression, newText);
|
|
262
|
+
} else {
|
|
263
|
+
// Einzeilig: Füge name am Anfang ein
|
|
264
|
+
const newText = decoratorText.replace(
|
|
265
|
+
/\{\s*/,
|
|
266
|
+
`{ name: "${suggestedName}", `
|
|
267
|
+
);
|
|
268
|
+
return fixer.replaceText(decorator.expression, newText);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return null;
|
|
273
|
+
},
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
// Fall 2: name-Parameter ist nicht im snake_case Format
|
|
277
|
+
else if (!isSnakeCase(columnName)) {
|
|
278
|
+
context.report({
|
|
279
|
+
node: decorator,
|
|
280
|
+
messageId: "columnNotSnakeCase",
|
|
281
|
+
data: {
|
|
282
|
+
columnName,
|
|
283
|
+
propertyName,
|
|
284
|
+
suggestedName,
|
|
285
|
+
},
|
|
286
|
+
fix(fixer) {
|
|
287
|
+
const decoratorText = sourceCode.getText(decorator.expression);
|
|
288
|
+
const newText = decoratorText.replace(
|
|
289
|
+
new RegExp(`name:\\s*["']${columnName}["']`),
|
|
290
|
+
`name: "${suggestedName}"`
|
|
291
|
+
);
|
|
292
|
+
return fixer.replaceText(decorator.expression, newText);
|
|
293
|
+
},
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Prüfe auch JoinColumn in Relationen
|
|
299
|
+
if (decorator.expression.callee?.name === "JoinColumn") {
|
|
300
|
+
const columnName = getColumnName(decorator);
|
|
301
|
+
const suggestedName = toSnakeCase(propertyName) + "_id";
|
|
302
|
+
|
|
303
|
+
if (!columnName) {
|
|
304
|
+
context.report({
|
|
305
|
+
node: decorator,
|
|
306
|
+
messageId: "relationColumnRequired",
|
|
307
|
+
data: {
|
|
308
|
+
propertyName,
|
|
309
|
+
suggestedName,
|
|
310
|
+
},
|
|
311
|
+
fix(fixer) {
|
|
312
|
+
const decoratorText = sourceCode.getText(decorator.expression);
|
|
313
|
+
|
|
314
|
+
// @JoinColumn() -> @JoinColumn({ name: "property_name_id" })
|
|
315
|
+
if (!decorator.expression.arguments || decorator.expression.arguments.length === 0) {
|
|
316
|
+
return fixer.replaceText(
|
|
317
|
+
decorator.expression,
|
|
318
|
+
`JoinColumn({ name: "${suggestedName}" })`
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// @JoinColumn({ referencedColumnName: "id" }) -> @JoinColumn({ name: "property_name_id", referencedColumnName: "id" })
|
|
323
|
+
const firstArg = decorator.expression.arguments[0];
|
|
324
|
+
if (firstArg.type === "ObjectExpression") {
|
|
325
|
+
const newText = decoratorText.replace(
|
|
326
|
+
/\{\s*/,
|
|
327
|
+
`{ name: "${suggestedName}", `
|
|
328
|
+
);
|
|
329
|
+
return fixer.replaceText(decorator.expression, newText);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return null;
|
|
333
|
+
},
|
|
334
|
+
});
|
|
335
|
+
} else if (!isSnakeCase(columnName)) {
|
|
336
|
+
context.report({
|
|
337
|
+
node: decorator,
|
|
338
|
+
messageId: "columnNotSnakeCase",
|
|
339
|
+
data: {
|
|
340
|
+
columnName,
|
|
341
|
+
propertyName,
|
|
342
|
+
suggestedName,
|
|
343
|
+
},
|
|
344
|
+
fix(fixer) {
|
|
345
|
+
const decoratorText = sourceCode.getText(decorator.expression);
|
|
346
|
+
const newText = decoratorText.replace(
|
|
347
|
+
new RegExp(`name:\\s*["']${columnName}["']`),
|
|
348
|
+
`name: "${suggestedName}"`
|
|
349
|
+
);
|
|
350
|
+
return fixer.replaceText(decorator.expression, newText);
|
|
351
|
+
},
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
},
|
|
357
|
+
};
|
|
358
|
+
},
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
export default {
|
|
362
|
+
rules: {
|
|
363
|
+
"enforce-typeorm-naming-conventions": enforceTypeormNamingConventionsRule,
|
|
364
|
+
},
|
|
365
|
+
};
|
|
366
|
+
|