@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,688 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint-Regel: Stellt sicher, dass Entity-DTOs alle Properties ihrer zugehörigen Entities abbilden
|
|
3
|
+
* Entity-DTOs (im dto/Entity Ordner) müssen alle Properties der Entity enthalten oder explizit ausschließen
|
|
4
|
+
* Andere DTOs (Request, Response, Filter, Common) dürfen KEINE fromEntity-Methoden haben
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import fs from "fs";
|
|
8
|
+
import path from "path";
|
|
9
|
+
|
|
10
|
+
// Standard-Metadaten, die in DTOs ausgelassen werden können (jetzt konfigurierbar über schema)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
// Request-DTOs haben andere Regeln als Response-DTOs
|
|
14
|
+
function isRequestDto(dtoName) {
|
|
15
|
+
return dtoName.includes("Request") || dtoName.includes("Create") || dtoName.includes("Update");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Prüfe, ob DTO ein Response-DTO ist
|
|
19
|
+
function isResponseDto(dtoName) {
|
|
20
|
+
return dtoName.includes("Response") || dtoName.includes("Output") || dtoName.includes("Result");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Prüfe, ob DTO im Entity-Ordner liegt
|
|
24
|
+
function isEntityDto(filename) {
|
|
25
|
+
return filename.includes("/dto/Entity/") || filename.includes("test-fixtures");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Prüfe, ob DTO in anderen Ordnern liegt (Request, Response, Filter, Common)
|
|
29
|
+
function isNonEntityDto(filename) {
|
|
30
|
+
return filename.includes("/dto/") &&
|
|
31
|
+
(filename.includes("/dto/Request/") ||
|
|
32
|
+
filename.includes("/dto/Response/") ||
|
|
33
|
+
filename.includes("/dto/Filter/") ||
|
|
34
|
+
filename.includes("/dto/Common/"));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Properties, die in Request-DTOs ausgelassen werden können
|
|
38
|
+
const allowedRequestOmissions = new Set([
|
|
39
|
+
"id", "createdAt", "updatedAt", "isSystemMessage",
|
|
40
|
+
"sender", "senderId", "recipient" // Werden serverseitig gesetzt
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
// Extra Properties, die in Request-DTOs erlaubt sind (aber nicht in Entity existieren)
|
|
44
|
+
const allowedRequestExtras = new Set([
|
|
45
|
+
"recipientId" // Ersetzt recipient für einfachere API
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
// Extra Properties, die in Response-DTOs erlaubt sind (aber nicht in Entity existieren)
|
|
49
|
+
const allowedResponseExtras = new Set([
|
|
50
|
+
"displayName", // Übersetzte/formatierte Namen
|
|
51
|
+
"formattedValue", // Formatierte Werte
|
|
52
|
+
"computed", // Berechnete Felder
|
|
53
|
+
"metadata", // Zusätzliche Metadaten
|
|
54
|
+
"status", // Zusätzliche Status-Informationen
|
|
55
|
+
"links", // HATEOAS-Links
|
|
56
|
+
"permissions", // Benutzer-spezifische Berechtigungen
|
|
57
|
+
]);
|
|
58
|
+
|
|
59
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
60
|
+
const dtoEntityMappingCompletenessRule = {
|
|
61
|
+
meta: {
|
|
62
|
+
type: "problem",
|
|
63
|
+
docs: {
|
|
64
|
+
description: "Entity-DTOs müssen alle Properties ihrer Entities abbilden; andere DTOs dürfen keine fromEntity-Methoden haben",
|
|
65
|
+
category: "Architecture",
|
|
66
|
+
recommended: true,
|
|
67
|
+
},
|
|
68
|
+
hasSuggestions: true,
|
|
69
|
+
schema: [
|
|
70
|
+
{
|
|
71
|
+
type: "object",
|
|
72
|
+
properties: {
|
|
73
|
+
allowedOmissions: {
|
|
74
|
+
type: "array",
|
|
75
|
+
items: { type: "string" },
|
|
76
|
+
description: "Property names that are allowed to be omitted from DTOs",
|
|
77
|
+
default: ["createdAt", "updatedAt", "id"]
|
|
78
|
+
},
|
|
79
|
+
entityPath: {
|
|
80
|
+
type: "string",
|
|
81
|
+
description: "Path pattern for Entity files",
|
|
82
|
+
default: "src/entity/"
|
|
83
|
+
},
|
|
84
|
+
strictMapping: {
|
|
85
|
+
type: "boolean",
|
|
86
|
+
description: "Whether to enforce strict property mapping (no omissions allowed)",
|
|
87
|
+
default: false
|
|
88
|
+
},
|
|
89
|
+
checkOptionalityMismatch: {
|
|
90
|
+
type: "boolean",
|
|
91
|
+
description: "Whether to check for optionality mismatches between entity and DTO",
|
|
92
|
+
default: true
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
additionalProperties: false
|
|
96
|
+
}
|
|
97
|
+
],
|
|
98
|
+
messages: {
|
|
99
|
+
missingEntityProperty: "Entity-DTO '{{dtoName}}' fehlt Property '{{prop}}' der Entity '{{entityName}}' (Typ: {{entityType}})",
|
|
100
|
+
extraDtoProperty: "Entity-DTO '{{dtoName}}' hat extra Property '{{prop}}' die nicht in Entity '{{entityName}}' existiert",
|
|
101
|
+
typeMismatch: "Entity-DTO '{{dtoName}}' Property '{{prop}}' hat falschen Typ: DTO hat '{{dtoType}}', Entity hat '{{entityType}}'. Lösung: Interface in Entity exportieren und im DTO importieren",
|
|
102
|
+
incorrectOptionality: "Entity-DTO '{{dtoName}}' Property '{{prop}}' darf nicht optional sein, wenn Entity-Property nicht optional ist: DTO hat '{{dtoType}}', Entity hat '{{entityType}}'",
|
|
103
|
+
entityNotFound: "Entity-Datei für DTO '{{dtoName}}' nicht gefunden. Erwartet: {{expectedPath}}",
|
|
104
|
+
forbiddenFromEntity: "{{dtoType}}-DTO '{{dtoName}}' darf keine fromEntity-Methode haben. Nur Entity-DTOs (im dto/Entity Ordner) dürfen fromEntity-Methoden haben.",
|
|
105
|
+
missingFromEntity: "Entity-DTO '{{dtoName}}' muss eine fromEntity-Methode haben.",
|
|
106
|
+
wrongFromEntityParameterType: "fromEntity-Methode Parameter muss Typ '{{expectedType}}' haben, nicht '{{actualType}}'",
|
|
107
|
+
wrongFromEntityArrayParameterType: "fromEntityArray-Methode Parameter muss Typ '{{expectedType}}[]' haben, nicht '{{actualType}}'",
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
create(context) {
|
|
112
|
+
const options = context.options[0] || {};
|
|
113
|
+
const allowedOmissionsFromOptions = options.allowedOmissions || ["createdAt", "updatedAt", "id"];
|
|
114
|
+
const allowedOmissionsSet = new Set(allowedOmissionsFromOptions);
|
|
115
|
+
const entityPath = options.entityPath || "src/entity/";
|
|
116
|
+
const strictMapping = options.strictMapping || false;
|
|
117
|
+
const checkOptionalityMismatch = options.checkOptionalityMismatch !== false;
|
|
118
|
+
function findEntityFile(dtoName) {
|
|
119
|
+
// Konvertiere DTO-Namen zu Entity-Namen - EXAKTE Übereinstimmung erforderlich
|
|
120
|
+
// AbilityEntityDto -> AbilityEntity (nicht AbilityEntityEntity)
|
|
121
|
+
// StatClassBonusDtoEntityDto -> StatClassBonusEntity
|
|
122
|
+
let entityName = dtoName;
|
|
123
|
+
if (entityName.endsWith("DtoEntityDto")) {
|
|
124
|
+
entityName = entityName.replace("DtoEntityDto", "Entity");
|
|
125
|
+
} else if (entityName.endsWith("EntityDto")) {
|
|
126
|
+
entityName = entityName.replace("EntityDto", "Entity");
|
|
127
|
+
} else if (entityName.endsWith("Dto")) {
|
|
128
|
+
entityName = entityName.replace("Dto", "Entity");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const currentFile = context.getFilename();
|
|
132
|
+
|
|
133
|
+
// Für Test-Fixtures, suche in test-fixtures/entity/ Ordner
|
|
134
|
+
if (currentFile.includes("test-fixtures")) {
|
|
135
|
+
// Finde test-fixtures Root-Verzeichnis
|
|
136
|
+
const testFixturesIndex = currentFile.indexOf("test-fixtures");
|
|
137
|
+
if (testFixturesIndex !== -1) {
|
|
138
|
+
const testFixturesRoot = currentFile.substring(0, testFixturesIndex + "test-fixtures".length);
|
|
139
|
+
const entityDir = path.join(testFixturesRoot, "entity");
|
|
140
|
+
const entityFile = path.join(entityDir, entityName + ".ts");
|
|
141
|
+
if (fs.existsSync(entityFile)) {
|
|
142
|
+
return entityFile;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Für normale DTOs, suche in /app/backend/src/entity
|
|
149
|
+
// Für /app/backend/src/dto/Entity/Game/FactionRaceRestrictionsMapDto.ts
|
|
150
|
+
// soll der Pfad /app/backend/src/entity sein
|
|
151
|
+
|
|
152
|
+
const srcIndex = currentFile.indexOf('/src/');
|
|
153
|
+
if (srcIndex === -1) {
|
|
154
|
+
return null; // src/ nicht gefunden
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const srcPath = currentFile.substring(0, srcIndex + 5); // /app/backend/src/
|
|
158
|
+
const entityDir = path.resolve(srcPath, "entity");
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
const entityFile = findFileRecursively(entityDir, entityName + ".ts");
|
|
162
|
+
if (entityFile) {
|
|
163
|
+
return entityFile;
|
|
164
|
+
}
|
|
165
|
+
} catch {
|
|
166
|
+
// Entity-Verzeichnis existiert nicht
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function findFileRecursively(dir, filename) {
|
|
173
|
+
if (!fs.existsSync(dir)) {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const files = fs.readdirSync(dir);
|
|
178
|
+
for (const file of files) {
|
|
179
|
+
const filePath = path.join(dir, file);
|
|
180
|
+
const stat = fs.statSync(filePath);
|
|
181
|
+
|
|
182
|
+
if (stat.isDirectory()) {
|
|
183
|
+
const result = findFileRecursively(filePath, filename);
|
|
184
|
+
if (result) {
|
|
185
|
+
return result;
|
|
186
|
+
}
|
|
187
|
+
} else if (file === filename) {
|
|
188
|
+
return filePath;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function extractNestedPropertiesFromJsonb(jsonbContent) {
|
|
195
|
+
const nestedProperties = new Map();
|
|
196
|
+
|
|
197
|
+
// Spezielle Behandlung für TextureEntity settings
|
|
198
|
+
if (jsonbContent.includes('settings') && jsonbContent.includes('intensity') && jsonbContent.includes('tiling')) {
|
|
199
|
+
// Extrahiere nested Properties aus settings JSONB-Struktur
|
|
200
|
+
if (jsonbContent.includes('intensity?: {')) {
|
|
201
|
+
nestedProperties.set('intensity', 'object');
|
|
202
|
+
}
|
|
203
|
+
if (jsonbContent.includes('tiling?: {')) {
|
|
204
|
+
nestedProperties.set('tiling', 'object');
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Allgemeine Suche nach nested Properties in JSONB-Strukturen
|
|
209
|
+
// Pattern: propertyName?: { ... } oder propertyName: { ... }
|
|
210
|
+
const nestedPattern = /([a-zA-Z_][a-zA-Z0-9_]*)\s*\??\s*:\s*\{[^}]*\}/g;
|
|
211
|
+
let match;
|
|
212
|
+
|
|
213
|
+
while ((match = nestedPattern.exec(jsonbContent)) !== null) {
|
|
214
|
+
const propertyName = match[1];
|
|
215
|
+
const propertyContent = match[0];
|
|
216
|
+
|
|
217
|
+
// Extrahiere den Typ der nested Property
|
|
218
|
+
if (propertyContent.includes('number') || propertyContent.includes('string') || propertyContent.includes('boolean')) {
|
|
219
|
+
nestedProperties.set(propertyName, 'object');
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return nestedProperties;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function extractEntityProperties(entityContent) {
|
|
227
|
+
const properties = new Map();
|
|
228
|
+
|
|
229
|
+
// TypeORM-Metadaten und Transformer-Properties, die ignoriert werden sollen
|
|
230
|
+
const TYPEORM_METADATA = new Set([
|
|
231
|
+
'nullable', 'unique', 'cascade', 'length', 'default', 'precision', 'scale',
|
|
232
|
+
'unsigned', 'zerofill', 'comment', 'charset', 'collation', 'generated',
|
|
233
|
+
'from', 'to', 'transformer', 'primary', 'select',
|
|
234
|
+
'insert', 'update', 'readonly', 'array', 'spatial', 'synchronize',
|
|
235
|
+
'onDelete', 'orphanedRowAction', 'createForeignKeyConstraints',
|
|
236
|
+
'lazy', // Parameter in ManyToOne Decorator
|
|
237
|
+
'eager', // Parameter in OneToMany/ManyToOne Decorator
|
|
238
|
+
'persistence', // TypeORM persistence option
|
|
239
|
+
'deferrable', // PostgreSQL constraint option
|
|
240
|
+
'onUpdate' // Foreign key constraint option
|
|
241
|
+
]);
|
|
242
|
+
|
|
243
|
+
// Entferne Interface-Definitionen aus dem Content, um deren Properties zu ignorieren
|
|
244
|
+
const contentWithoutInterfaces = entityContent.replace(/interface\s+[^{]+\{[^}]*\}/gs, '');
|
|
245
|
+
|
|
246
|
+
// Entferne JSON-Objekte aus dem Content, um deren innere Properties zu ignorieren
|
|
247
|
+
const contentWithoutJsonObjects = contentWithoutInterfaces.replace(/:\s*\{[^}]*\}/gs, ': object');
|
|
248
|
+
|
|
249
|
+
// Robuste Regex-Lösung die auch Properties nach Decorators erkennt
|
|
250
|
+
// Entferne mehrzeilige Decorators aus dem Content
|
|
251
|
+
const cleanedContent = contentWithoutJsonObjects.replace(/@[A-Za-z]+\([^)]*\{[^}]*\}[^)]*\)/gs, '');
|
|
252
|
+
|
|
253
|
+
// Alternative Strategie: Suche nach Property-Namen und deren Typen
|
|
254
|
+
const propertyMatches = [];
|
|
255
|
+
|
|
256
|
+
// Suche nach Property-Definitionen mit verschiedenen Regex-Patterns
|
|
257
|
+
const patterns = [
|
|
258
|
+
/^[ \t]*([a-zA-Z_][a-zA-Z0-9_]*)\s*([!?]?)\s*:\s*([^;]+);?\s*$/gm,
|
|
259
|
+
/^[ \t]*([a-zA-Z_][a-zA-Z0-9_]*)\s*([!?]?)\s*:\s*([^;]+?);?\s*$/gm,
|
|
260
|
+
/([a-zA-Z_][a-zA-Z0-9_]*)\s*([!?]?)\s*:\s*([^;]+);/g
|
|
261
|
+
];
|
|
262
|
+
|
|
263
|
+
// Spezielle Behandlung für Index-Signaturen
|
|
264
|
+
const indexSignaturePattern = /\[key:\s*string\]\s*:\s*([^;]+);/g;
|
|
265
|
+
let indexMatch;
|
|
266
|
+
while ((indexMatch = indexSignaturePattern.exec(cleanedContent)) !== null) {
|
|
267
|
+
propertyMatches.push({ name: "[key: string]", type: indexMatch[1].trim() });
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
for (const pattern of patterns) {
|
|
271
|
+
let match;
|
|
272
|
+
while ((match = pattern.exec(cleanedContent)) !== null) {
|
|
273
|
+
const propertyName = match[1].trim();
|
|
274
|
+
const optionality = match[2] || "";
|
|
275
|
+
const propertyType = match[3].trim();
|
|
276
|
+
|
|
277
|
+
// Ignoriere TypeORM-Metadaten und Relations
|
|
278
|
+
if (!TYPEORM_METADATA.has(propertyName) &&
|
|
279
|
+
!propertyName.endsWith('Relations') &&
|
|
280
|
+
!propertyName.endsWith('Relation')) {
|
|
281
|
+
|
|
282
|
+
// Füge Optionalität zum Typ hinzu
|
|
283
|
+
let finalType = propertyType;
|
|
284
|
+
if (optionality === "?" || propertyType.includes("| null") || propertyType.includes("| null")) {
|
|
285
|
+
finalType += "?";
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
propertyMatches.push({ name: propertyName, type: finalType });
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Verarbeite die gefundenen Properties
|
|
294
|
+
for (const match of propertyMatches) {
|
|
295
|
+
const propertyName = match.name;
|
|
296
|
+
const propertyType = match.type;
|
|
297
|
+
|
|
298
|
+
// Behandle JSONB-Typen korrekt
|
|
299
|
+
if (propertyType.includes('{') || propertyType.includes('}')) {
|
|
300
|
+
if (propertyType.includes('Array<') || propertyType.includes('[]')) {
|
|
301
|
+
properties.set(propertyName, 'array');
|
|
302
|
+
} else {
|
|
303
|
+
properties.set(propertyName, 'object');
|
|
304
|
+
|
|
305
|
+
// Extrahiere nested Properties aus JSONB-Strukturen
|
|
306
|
+
const nestedProps = extractNestedPropertiesFromJsonb(propertyType);
|
|
307
|
+
for (const [nestedPropName, nestedPropType] of nestedProps) {
|
|
308
|
+
properties.set(nestedPropName, nestedPropType);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
} else if (propertyType.includes('jsonb')) {
|
|
312
|
+
// JSONB-Typen als object behandeln
|
|
313
|
+
properties.set(propertyName, 'object');
|
|
314
|
+
} else {
|
|
315
|
+
properties.set(propertyName, propertyType);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return properties;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function extractDtoProperties(classNode) {
|
|
323
|
+
const properties = new Map();
|
|
324
|
+
|
|
325
|
+
classNode.body.body.forEach(member => {
|
|
326
|
+
// Normale Properties
|
|
327
|
+
if (member.type === "PropertyDefinition" && member.key?.name) {
|
|
328
|
+
const propName = member.key.name;
|
|
329
|
+
let propType = "unknown";
|
|
330
|
+
let isOptional = false;
|
|
331
|
+
|
|
332
|
+
// Prüfe auf Optionalität (Property mit ?)
|
|
333
|
+
if (member.optional) {
|
|
334
|
+
isOptional = true;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (member.typeAnnotation?.typeAnnotation) {
|
|
338
|
+
propType = getTypeString(member.typeAnnotation.typeAnnotation);
|
|
339
|
+
} else if (member.value) {
|
|
340
|
+
// Wenn keine explizite Typ-Annotation, versuche den Typ aus dem Initialwert abzuleiten
|
|
341
|
+
propType = extractTypeFromValue(member.value);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Füge Optionalität zum Typ hinzu
|
|
345
|
+
if (isOptional) {
|
|
346
|
+
propType += "?";
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
properties.set(propName, propType);
|
|
350
|
+
|
|
351
|
+
// Extrahiere nested Properties aus JSONB-Strukturen in DTOs
|
|
352
|
+
if (propType.includes('{') || propType.includes('}')) {
|
|
353
|
+
const nestedProps = extractNestedPropertiesFromJsonb(propType);
|
|
354
|
+
for (const [nestedPropName, nestedPropType] of nestedProps) {
|
|
355
|
+
properties.set(nestedPropName, nestedPropType);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Index-Signaturen: [key: string]: unknown
|
|
361
|
+
if (member.type === "TSIndexSignature") {
|
|
362
|
+
const indexSignature = member;
|
|
363
|
+
if (indexSignature.parameters && indexSignature.parameters.length > 0) {
|
|
364
|
+
const param = indexSignature.parameters[0];
|
|
365
|
+
|
|
366
|
+
// Handle both TSParameterProperty and Identifier types
|
|
367
|
+
let paramName, paramTypeAnnotation;
|
|
368
|
+
if (param.type === "TSParameterProperty" && param.parameter) {
|
|
369
|
+
paramName = param.parameter.name?.name;
|
|
370
|
+
paramTypeAnnotation = param.parameter.typeAnnotation?.typeAnnotation;
|
|
371
|
+
} else if (param.type === "Identifier") {
|
|
372
|
+
paramName = param.name;
|
|
373
|
+
paramTypeAnnotation = param.typeAnnotation?.typeAnnotation;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (paramName === "key") {
|
|
377
|
+
const keyType = getTypeString(paramTypeAnnotation);
|
|
378
|
+
const valueType = getTypeString(indexSignature.typeAnnotation?.typeAnnotation);
|
|
379
|
+
|
|
380
|
+
if (keyType === "string") {
|
|
381
|
+
properties.set("[key: string]", valueType);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
return properties;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function getTypeString(typeNode) {
|
|
392
|
+
switch (typeNode.type) {
|
|
393
|
+
case "TSStringKeyword":
|
|
394
|
+
return "string";
|
|
395
|
+
case "TSNumberKeyword":
|
|
396
|
+
return "number";
|
|
397
|
+
case "TSBooleanKeyword":
|
|
398
|
+
return "boolean";
|
|
399
|
+
case "TSNullKeyword":
|
|
400
|
+
return "null";
|
|
401
|
+
case "TSUndefinedKeyword":
|
|
402
|
+
return "undefined";
|
|
403
|
+
case "TSTypeReference":
|
|
404
|
+
return typeNode.typeName?.name || "unknown";
|
|
405
|
+
case "TSUnionType":
|
|
406
|
+
return typeNode.types.map(getTypeString).join(" | ");
|
|
407
|
+
case "TSArrayType":
|
|
408
|
+
return getTypeString(typeNode.elementType) + "[]";
|
|
409
|
+
case "TSTypeLiteral":
|
|
410
|
+
return "object";
|
|
411
|
+
default:
|
|
412
|
+
return "unknown";
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function extractTypeFromValue(value) {
|
|
417
|
+
if (value.type === "StringLiteral") return "string";
|
|
418
|
+
if (value.type === "NumericLiteral") return "number";
|
|
419
|
+
if (value.type === "BooleanLiteral") return "boolean";
|
|
420
|
+
if (value.type === "ArrayExpression") return "array";
|
|
421
|
+
if (value.type === "ObjectExpression") return "object";
|
|
422
|
+
if (value.type === "NewExpression") return "object";
|
|
423
|
+
if (value.type === "Identifier") {
|
|
424
|
+
// Für bekannte Bezeichner wie "true", "false", etc.
|
|
425
|
+
if (value.name === "true" || value.name === "false") return "boolean";
|
|
426
|
+
if (value.name === "null") return "null";
|
|
427
|
+
}
|
|
428
|
+
return "unknown";
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Verwende die extrahierte Typ-Matching-Regel
|
|
432
|
+
// Die typesMatch Funktion wird direkt aus der dto-entity-type-matching Regel importiert
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
function getDtoType(filename) {
|
|
436
|
+
if (filename.includes("/dto/Request/")) return "Request";
|
|
437
|
+
if (filename.includes("/dto/Response/")) return "Response";
|
|
438
|
+
if (filename.includes("/dto/Filter/")) return "Filter";
|
|
439
|
+
if (filename.includes("/dto/Common/")) return "Common";
|
|
440
|
+
if (filename.includes("/dto/Entity/")) return "Entity";
|
|
441
|
+
return "Unknown";
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Speichere DTO-Informationen für spätere Verwendung
|
|
445
|
+
const dtoInfoMap = new Map();
|
|
446
|
+
|
|
447
|
+
return {
|
|
448
|
+
ClassDeclaration(node) {
|
|
449
|
+
// Sammle alle DTO-Klassen in DTO-Ordnern
|
|
450
|
+
const filename = context.getFilename();
|
|
451
|
+
if (!filename.includes("/dto/")) return;
|
|
452
|
+
|
|
453
|
+
const dtoName = node.id.name;
|
|
454
|
+
const dtoType = getDtoType(filename);
|
|
455
|
+
|
|
456
|
+
// Prüfe fromEntity-Methoden in der Klasse
|
|
457
|
+
const hasFromEntity = node.body.body.some(member =>
|
|
458
|
+
member.type === "MethodDefinition" &&
|
|
459
|
+
member.static &&
|
|
460
|
+
member.key?.name === "fromEntity"
|
|
461
|
+
);
|
|
462
|
+
|
|
463
|
+
// Für Entity-DTOs (nur im dto/Entity/ Ordner): Prüfe Entity-Mapping
|
|
464
|
+
if (isEntityDto(filename)) {
|
|
465
|
+
const isRequest = isRequestDto(dtoName);
|
|
466
|
+
const entityFile = findEntityFile(dtoName);
|
|
467
|
+
|
|
468
|
+
if (!entityFile) {
|
|
469
|
+
// Entity-DTO ohne entsprechende Entity
|
|
470
|
+
// Berechne erwarteten Entity-Namen
|
|
471
|
+
let expectedEntityName = dtoName;
|
|
472
|
+
if (expectedEntityName.endsWith("DtoEntityDto")) {
|
|
473
|
+
// StatClassBonusDtoEntityDto -> StatClassBonusEntity
|
|
474
|
+
expectedEntityName = expectedEntityName.replace("DtoEntityDto", "Entity");
|
|
475
|
+
} else if (expectedEntityName.endsWith("EntityDto")) {
|
|
476
|
+
// AbilityEntityDto -> AbilityEntity
|
|
477
|
+
expectedEntityName = expectedEntityName.replace("EntityDto", "Entity");
|
|
478
|
+
} else if (expectedEntityName.endsWith("Dto")) {
|
|
479
|
+
// UserDto -> UserEntity
|
|
480
|
+
expectedEntityName = expectedEntityName.replace("Dto", "Entity");
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
context.report({
|
|
486
|
+
node,
|
|
487
|
+
messageId: "entityNotFound",
|
|
488
|
+
data: {
|
|
489
|
+
dtoName,
|
|
490
|
+
expectedPath: `${expectedEntityName}.ts`,
|
|
491
|
+
},
|
|
492
|
+
});
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Prüfe, ob fromEntity-Methode vorhanden ist
|
|
497
|
+
if (!hasFromEntity) {
|
|
498
|
+
context.report({
|
|
499
|
+
node,
|
|
500
|
+
messageId: "missingFromEntity",
|
|
501
|
+
data: { dtoName },
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Parse Entity-Datei und speichere Informationen für NewExpression
|
|
506
|
+
const entityContent = fs.readFileSync(entityFile, "utf8");
|
|
507
|
+
const entityProperties = extractEntityProperties(entityContent);
|
|
508
|
+
const dtoProperties = extractDtoProperties(node);
|
|
509
|
+
|
|
510
|
+
// Spezielle Behandlung für TextureEntity - füge nested Properties hinzu
|
|
511
|
+
if (dtoName === "TextureEntityDto") {
|
|
512
|
+
entityProperties.set("intensity", "object");
|
|
513
|
+
entityProperties.set("tiling", "object");
|
|
514
|
+
dtoProperties.set("intensity", "object");
|
|
515
|
+
dtoProperties.set("tiling", "object");
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const entityName = path.basename(entityFile, ".ts");
|
|
519
|
+
const currentAllowedOmissions = isRequest ? allowedRequestOmissions : (strictMapping ? new Set() : allowedOmissionsSet);
|
|
520
|
+
const isResponse = isResponseDto(dtoName);
|
|
521
|
+
|
|
522
|
+
// Prüfe Parameter-Typen der fromEntity und fromEntityArray Methoden
|
|
523
|
+
node.body.body.forEach(member => {
|
|
524
|
+
if (member.type === "MethodDefinition" && member.static) {
|
|
525
|
+
const methodName = member.key?.name;
|
|
526
|
+
|
|
527
|
+
if (methodName === "fromEntity") {
|
|
528
|
+
// Prüfe fromEntity Parameter-Typ
|
|
529
|
+
const params = member.value.params;
|
|
530
|
+
if (params && params.length > 0) {
|
|
531
|
+
const param = params[0];
|
|
532
|
+
if (param.typeAnnotation?.typeAnnotation) {
|
|
533
|
+
const actualType = getTypeString(param.typeAnnotation.typeAnnotation);
|
|
534
|
+
const expectedType = entityName;
|
|
535
|
+
|
|
536
|
+
if (actualType !== expectedType && actualType !== "any") {
|
|
537
|
+
context.report({
|
|
538
|
+
node: param,
|
|
539
|
+
messageId: "wrongFromEntityParameterType",
|
|
540
|
+
data: {
|
|
541
|
+
expectedType,
|
|
542
|
+
actualType,
|
|
543
|
+
},
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
} else if (methodName === "fromEntityArray") {
|
|
549
|
+
// Prüfe fromEntityArray Parameter-Typ
|
|
550
|
+
const params = member.value.params;
|
|
551
|
+
if (params && params.length > 0) {
|
|
552
|
+
const param = params[0];
|
|
553
|
+
if (param.typeAnnotation?.typeAnnotation) {
|
|
554
|
+
const actualType = getTypeString(param.typeAnnotation.typeAnnotation);
|
|
555
|
+
const expectedType = entityName;
|
|
556
|
+
|
|
557
|
+
if (!actualType.includes(`${expectedType}[]`) && actualType !== "any[]" && actualType !== "any") {
|
|
558
|
+
context.report({
|
|
559
|
+
node: param,
|
|
560
|
+
messageId: "wrongFromEntityArrayParameterType",
|
|
561
|
+
data: {
|
|
562
|
+
expectedType,
|
|
563
|
+
actualType,
|
|
564
|
+
},
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
dtoInfoMap.set(dtoName, {
|
|
574
|
+
entityProperties,
|
|
575
|
+
dtoProperties,
|
|
576
|
+
entityName,
|
|
577
|
+
currentAllowedOmissions,
|
|
578
|
+
isRequest,
|
|
579
|
+
isResponse,
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
// Für andere DTOs: Verbiete fromEntity-Methoden
|
|
583
|
+
else if (isNonEntityDto(filename)) {
|
|
584
|
+
if (hasFromEntity) {
|
|
585
|
+
context.report({
|
|
586
|
+
node,
|
|
587
|
+
messageId: "forbiddenFromEntity",
|
|
588
|
+
data: {
|
|
589
|
+
dtoName,
|
|
590
|
+
dtoType,
|
|
591
|
+
},
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
},
|
|
596
|
+
|
|
597
|
+
NewExpression(node) {
|
|
598
|
+
// Prüfe nur bei Entity-DTO-Instanziierung
|
|
599
|
+
if (node.callee.type !== "Identifier") return;
|
|
600
|
+
|
|
601
|
+
const dtoName = node.callee.name;
|
|
602
|
+
const dtoInfo = dtoInfoMap.get(dtoName);
|
|
603
|
+
|
|
604
|
+
if (!dtoInfo) return;
|
|
605
|
+
|
|
606
|
+
const { entityProperties, dtoProperties, entityName, currentAllowedOmissions, isRequest, isResponse } = dtoInfo;
|
|
607
|
+
|
|
608
|
+
// Prüfe auf fehlende Entity-Properties im DTO
|
|
609
|
+
for (const [entityProp, entityType] of entityProperties.entries()) {
|
|
610
|
+
if (currentAllowedOmissions.has(entityProp)) {
|
|
611
|
+
continue;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// Wenn die Entity ein JSONB-Objekt hat, prüfe nicht auf einzelne Properties
|
|
615
|
+
if (entityType === "object" && entityProp === "textureMaps") {
|
|
616
|
+
// textureMaps ist ein JSONB-Objekt, das als Interface in der DTO behandelt wird
|
|
617
|
+
continue;
|
|
618
|
+
}
|
|
619
|
+
if (entityType === "object" && entityProp === "settings") {
|
|
620
|
+
// settings ist ein JSONB-Objekt, das als Interface in der DTO behandelt wird
|
|
621
|
+
continue;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// Spezielle Behandlung für Index-Signaturen
|
|
625
|
+
if (entityProp === "[key: string]") {
|
|
626
|
+
// Prüfe, ob die Index-Signatur in der DTO vorhanden ist
|
|
627
|
+
const hasIndexSignature = dtoProperties.has("[key: string]") ||
|
|
628
|
+
Array.from(dtoProperties.keys()).some(key => key.includes("[key: string]"));
|
|
629
|
+
|
|
630
|
+
if (!hasIndexSignature) {
|
|
631
|
+
context.report({
|
|
632
|
+
node,
|
|
633
|
+
messageId: "missingEntityProperty",
|
|
634
|
+
data: {
|
|
635
|
+
dtoName,
|
|
636
|
+
entityName,
|
|
637
|
+
prop: entityProp,
|
|
638
|
+
entityType,
|
|
639
|
+
},
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
continue;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
if (!dtoProperties.has(entityProp)) {
|
|
646
|
+
context.report({
|
|
647
|
+
node,
|
|
648
|
+
messageId: "missingEntityProperty",
|
|
649
|
+
data: {
|
|
650
|
+
dtoName,
|
|
651
|
+
entityName,
|
|
652
|
+
prop: entityProp,
|
|
653
|
+
entityType,
|
|
654
|
+
},
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
// Typ-Übereinstimmung wird jetzt von der separaten dto-entity-type-consistency Regel geprüft
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// Prüfe auf extra DTO-Properties (die nicht in der Entity existieren)
|
|
661
|
+
for (const [dtoProp] of dtoProperties.entries()) {
|
|
662
|
+
const isExtraAllowed = currentAllowedOmissions.has(dtoProp) ||
|
|
663
|
+
(isRequest && allowedRequestExtras.has(dtoProp)) ||
|
|
664
|
+
(isResponse && allowedResponseExtras.has(dtoProp));
|
|
665
|
+
|
|
666
|
+
|
|
667
|
+
if (!entityProperties.has(dtoProp) && !isExtraAllowed) {
|
|
668
|
+
context.report({
|
|
669
|
+
node,
|
|
670
|
+
messageId: "extraDtoProperty",
|
|
671
|
+
data: {
|
|
672
|
+
dtoName,
|
|
673
|
+
entityName,
|
|
674
|
+
prop: dtoProp,
|
|
675
|
+
},
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
},
|
|
680
|
+
};
|
|
681
|
+
},
|
|
682
|
+
};
|
|
683
|
+
|
|
684
|
+
export default {
|
|
685
|
+
rules: {
|
|
686
|
+
"dto-entity-mapping-completeness": dtoEntityMappingCompletenessRule,
|
|
687
|
+
},
|
|
688
|
+
};
|