typeorm-dto-generator 1.0.1 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +156 -0
- package/dist/index.d.ts +133 -52
- package/dist/index.js +833 -768
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +803 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +27 -9
- package/dist/.tsbuildinfo +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/types.d.ts +0 -43
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -1
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,803 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { Project, Node } from "ts-morph";
|
|
5
|
+
var TORMDTOGenerator = class {
|
|
6
|
+
config;
|
|
7
|
+
project;
|
|
8
|
+
sourceInfoByClassName = /* @__PURE__ */ new Map();
|
|
9
|
+
constructor(config) {
|
|
10
|
+
this.config = this.createConfig(config);
|
|
11
|
+
this.project = new Project({
|
|
12
|
+
skipAddingFilesFromTsConfig: true
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
async run() {
|
|
16
|
+
this.loadEntitySourceInfo();
|
|
17
|
+
const metas = await this.createEntityMetas();
|
|
18
|
+
fs.rmSync(this.config.dtoOutputDir, {
|
|
19
|
+
recursive: true,
|
|
20
|
+
force: true
|
|
21
|
+
});
|
|
22
|
+
fs.mkdirSync(this.config.dtoOutputDir, {
|
|
23
|
+
recursive: true
|
|
24
|
+
});
|
|
25
|
+
this.ensureCommonTypesFile(metas);
|
|
26
|
+
this.validateCommonTypeCoverage(metas);
|
|
27
|
+
for (const meta of metas) {
|
|
28
|
+
this.writeFile(
|
|
29
|
+
path.join(this.config.dtoOutputDir, meta.dtoFileName),
|
|
30
|
+
this.createDTOContent(meta, metas)
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
this.writeFile(
|
|
34
|
+
path.join(this.config.dtoOutputDir, `index.ts`),
|
|
35
|
+
`${this.createDTOIndexContent(metas)}
|
|
36
|
+
`
|
|
37
|
+
);
|
|
38
|
+
this.writeFile(
|
|
39
|
+
this.config.mapperOutputFile,
|
|
40
|
+
this.createGeneratedMapperContent(metas)
|
|
41
|
+
);
|
|
42
|
+
this.log(`Generated DTO files: ${metas.length}`);
|
|
43
|
+
this.log(`Generated DTO directory: ${this.config.dtoOutputDir}`);
|
|
44
|
+
this.log(`Generated mapper file: ${this.config.mapperOutputFile}`);
|
|
45
|
+
return {
|
|
46
|
+
metas,
|
|
47
|
+
dtoOutputDir: this.config.dtoOutputDir,
|
|
48
|
+
mapperOutputFile: this.config.mapperOutputFile
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
log(message) {
|
|
52
|
+
if (this.config.debug)
|
|
53
|
+
console.log(message);
|
|
54
|
+
}
|
|
55
|
+
createConfig(config) {
|
|
56
|
+
if (!config.datasource)
|
|
57
|
+
throw new Error("datasource is required");
|
|
58
|
+
const root = process.cwd();
|
|
59
|
+
const dtoOutputDir = config.dtoOutputDir || path.join(root, "dto");
|
|
60
|
+
const mapperOutputFile = config.mapperOutputFile || path.join(root, "dto.mapper.ts");
|
|
61
|
+
const sharedDTOImportPath = config.sharedDTOImportPath || path.relative(path.parse(mapperOutputFile).dir, dtoOutputDir).replaceAll("\\", "/");
|
|
62
|
+
return {
|
|
63
|
+
debug: config.debug || false,
|
|
64
|
+
dtoOutputDir,
|
|
65
|
+
mapperOutputFile,
|
|
66
|
+
sharedDTOImportPath: sharedDTOImportPath.startsWith(".") ? sharedDTOImportPath : `./${sharedDTOImportPath}`,
|
|
67
|
+
includeExtensionOnImports: config.includeExtensionOnImports ?? true,
|
|
68
|
+
excludePropertyNames: config.excludePropertyNames ?? [
|
|
69
|
+
"password",
|
|
70
|
+
"passwordHash",
|
|
71
|
+
"secret",
|
|
72
|
+
"refreshToken",
|
|
73
|
+
"accessToken",
|
|
74
|
+
"keyHash",
|
|
75
|
+
"token",
|
|
76
|
+
"otp",
|
|
77
|
+
"salt"
|
|
78
|
+
],
|
|
79
|
+
customScalarTypeMap: config.customScalarTypeMap ?? {
|
|
80
|
+
BigInt: "string",
|
|
81
|
+
bigint: "string",
|
|
82
|
+
Buffer: "string",
|
|
83
|
+
Decimal: "string"
|
|
84
|
+
},
|
|
85
|
+
...config
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
writeFile(filePath, content) {
|
|
89
|
+
fs.mkdirSync(path.dirname(filePath), {
|
|
90
|
+
recursive: true
|
|
91
|
+
});
|
|
92
|
+
fs.writeFileSync(filePath, content);
|
|
93
|
+
}
|
|
94
|
+
toKebabCase(value) {
|
|
95
|
+
return value.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/[\s_]+/g, "-").toLowerCase();
|
|
96
|
+
}
|
|
97
|
+
unique(items) {
|
|
98
|
+
return [...new Set(items)];
|
|
99
|
+
}
|
|
100
|
+
createRelativeJSImportPath(fromFile, targetFile) {
|
|
101
|
+
const relativePath = path.relative(path.dirname(fromFile), targetFile).replace(/\\/g, "/").replace(/\.(ts|tsx|mts|cts)$/, this.config.includeExtensionOnImports ? ".ts" : "");
|
|
102
|
+
return relativePath.startsWith(".") ? relativePath : `./${relativePath}`;
|
|
103
|
+
}
|
|
104
|
+
getCommonTypesFilePath() {
|
|
105
|
+
return path.join(this.config.dtoOutputDir, "common.dto.ts");
|
|
106
|
+
}
|
|
107
|
+
getCommonTypesImportSpecifier(dtoFilePath) {
|
|
108
|
+
return this.createRelativeJSImportPath(dtoFilePath, this.getCommonTypesFilePath());
|
|
109
|
+
}
|
|
110
|
+
getDatasourceEntities() {
|
|
111
|
+
const entities = this.config.datasource.options.entities ?? [];
|
|
112
|
+
if (Array.isArray(entities)) {
|
|
113
|
+
return entities;
|
|
114
|
+
}
|
|
115
|
+
return Object.values(entities);
|
|
116
|
+
}
|
|
117
|
+
normalizeEntityPattern(pattern) {
|
|
118
|
+
return pattern.replace(/\\/g, "/");
|
|
119
|
+
}
|
|
120
|
+
isSourceFilePath(filePath) {
|
|
121
|
+
return /\.(ts|tsx|mts|cts|js|jsx|mjs|cjs)$/.test(filePath);
|
|
122
|
+
}
|
|
123
|
+
isEntitySourceFilePath(filePath) {
|
|
124
|
+
return /\.entity\.(ts|tsx|mts|cts|js|jsx|mjs|cjs)$/.test(filePath);
|
|
125
|
+
}
|
|
126
|
+
normalizeAbsolutePath(filePath) {
|
|
127
|
+
return path.resolve(filePath).replace(/\\/g, "/").toLowerCase();
|
|
128
|
+
}
|
|
129
|
+
isIgnoredSourcePath(filePath) {
|
|
130
|
+
const normalized = this.normalizeAbsolutePath(filePath);
|
|
131
|
+
const dtoOutputDir = this.normalizeAbsolutePath(this.config.dtoOutputDir);
|
|
132
|
+
const mapperOutputFile = this.normalizeAbsolutePath(this.config.mapperOutputFile);
|
|
133
|
+
const ignoredSegments = [
|
|
134
|
+
"/node_modules/",
|
|
135
|
+
"/.git/",
|
|
136
|
+
"/.next/",
|
|
137
|
+
"/dist/",
|
|
138
|
+
"/build/",
|
|
139
|
+
"/coverage/"
|
|
140
|
+
];
|
|
141
|
+
if (ignoredSegments.some((segment) => normalized.includes(segment))) {
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
if (normalized === dtoOutputDir || normalized.startsWith(`${dtoOutputDir}/`)) {
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
return normalized === mapperOutputFile;
|
|
148
|
+
}
|
|
149
|
+
discoverProjectSourcePaths() {
|
|
150
|
+
const result = [];
|
|
151
|
+
const walk = (directory) => {
|
|
152
|
+
if (!fs.existsSync(directory)) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
156
|
+
const fullPath = path.join(directory, entry.name);
|
|
157
|
+
const normalized = fullPath.replace(/\\/g, "/");
|
|
158
|
+
if (this.isIgnoredSourcePath(normalized)) {
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
if (entry.isDirectory()) {
|
|
162
|
+
walk(fullPath);
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (entry.isFile() && this.isSourceFilePath(fullPath)) {
|
|
166
|
+
result.push(fullPath);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
walk(process.cwd());
|
|
171
|
+
return result;
|
|
172
|
+
}
|
|
173
|
+
addEntityOptionSourceFiles() {
|
|
174
|
+
const sourceFiles = [];
|
|
175
|
+
for (const entity of this.getDatasourceEntities()) {
|
|
176
|
+
if (typeof entity !== "string") {
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
sourceFiles.push(...this.project.addSourceFilesAtPaths(this.normalizeEntityPattern(entity)));
|
|
180
|
+
}
|
|
181
|
+
return sourceFiles;
|
|
182
|
+
}
|
|
183
|
+
addDiscoveredSourceFiles() {
|
|
184
|
+
const paths = this.discoverProjectSourcePaths();
|
|
185
|
+
return paths.map((filePath) => {
|
|
186
|
+
return this.project.addSourceFileAtPathIfExists(filePath);
|
|
187
|
+
}).filter((sourceFile) => {
|
|
188
|
+
return Boolean(sourceFile);
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
loadEntitySourceInfo() {
|
|
192
|
+
const fromEntityOptions = this.addEntityOptionSourceFiles();
|
|
193
|
+
this.addDiscoveredSourceFiles();
|
|
194
|
+
const candidateFiles = fromEntityOptions.length ? fromEntityOptions : this.project.getSourceFiles();
|
|
195
|
+
for (const sourceFile of candidateFiles) {
|
|
196
|
+
for (const classDeclaration of sourceFile.getClasses()) {
|
|
197
|
+
if (!this.isEntityClass(classDeclaration)) {
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
this.sourceInfoByClassName.set(classDeclaration.getNameOrThrow(), {
|
|
201
|
+
filePath: sourceFile.getFilePath(),
|
|
202
|
+
enumTypeByProperty: this.collectEnumTypesByProperty(classDeclaration),
|
|
203
|
+
typeByProperty: this.collectTypesByProperty(classDeclaration)
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
isEntityClass(classDeclaration) {
|
|
209
|
+
return classDeclaration.getDecorators().some((decorator) => {
|
|
210
|
+
return decorator.getName() === "Entity";
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
getDecoratorObjectLiteral(property) {
|
|
214
|
+
for (const decorator of property.getDecorators()) {
|
|
215
|
+
const objectLiteral = decorator.getArguments().find((argument) => {
|
|
216
|
+
return Node.isObjectLiteralExpression(argument);
|
|
217
|
+
});
|
|
218
|
+
if (objectLiteral && Node.isObjectLiteralExpression(objectLiteral)) {
|
|
219
|
+
return objectLiteral;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
getColumnEnumInitializer(property) {
|
|
225
|
+
var _a;
|
|
226
|
+
const objectLiteral = this.getDecoratorObjectLiteral(property);
|
|
227
|
+
const enumProperty = objectLiteral == null ? void 0 : objectLiteral.getProperty("enum");
|
|
228
|
+
if (!enumProperty || !Node.isPropertyAssignment(enumProperty)) {
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
return ((_a = enumProperty.getInitializer()) == null ? void 0 : _a.getText()) ?? null;
|
|
232
|
+
}
|
|
233
|
+
getEnumTypeNameFromProperty(property) {
|
|
234
|
+
var _a;
|
|
235
|
+
const typeText = (_a = property.getTypeNode()) == null ? void 0 : _a.getText();
|
|
236
|
+
if (typeText && /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(typeText)) {
|
|
237
|
+
return typeText;
|
|
238
|
+
}
|
|
239
|
+
const enumInitializer = this.getColumnEnumInitializer(property);
|
|
240
|
+
if (enumInitializer && /^[A-Za-z_$][A-Za-z0-9_$.]*$/.test(enumInitializer)) {
|
|
241
|
+
return enumInitializer.split(".").pop() ?? enumInitializer;
|
|
242
|
+
}
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
collectEnumTypesByProperty(classDeclaration) {
|
|
246
|
+
const enumTypeByProperty = /* @__PURE__ */ new Map();
|
|
247
|
+
for (const property of this.collectProperties(classDeclaration)) {
|
|
248
|
+
const enumTypeName = this.getEnumTypeNameFromProperty(property);
|
|
249
|
+
if (enumTypeName) {
|
|
250
|
+
enumTypeByProperty.set(property.getName(), enumTypeName);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return enumTypeByProperty;
|
|
254
|
+
}
|
|
255
|
+
cleanTypeText(value) {
|
|
256
|
+
return value.replace(/import\(".*?"\)\./g, "").replace(/\s+/g, " ").trim();
|
|
257
|
+
}
|
|
258
|
+
collectTypesByProperty(classDeclaration) {
|
|
259
|
+
var _a;
|
|
260
|
+
const typeByProperty = /* @__PURE__ */ new Map();
|
|
261
|
+
for (const property of this.collectProperties(classDeclaration)) {
|
|
262
|
+
const typeText = (_a = property.getTypeNode()) == null ? void 0 : _a.getText();
|
|
263
|
+
if (typeText) {
|
|
264
|
+
typeByProperty.set(property.getName(), this.cleanTypeText(typeText));
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return typeByProperty;
|
|
268
|
+
}
|
|
269
|
+
collectProperties(classDeclaration, visitedClassNames = /* @__PURE__ */ new Set()) {
|
|
270
|
+
const className = classDeclaration.getName();
|
|
271
|
+
if (className && visitedClassNames.has(className)) {
|
|
272
|
+
return [];
|
|
273
|
+
}
|
|
274
|
+
if (className) {
|
|
275
|
+
visitedClassNames.add(className);
|
|
276
|
+
}
|
|
277
|
+
const baseClass = classDeclaration.getBaseClass();
|
|
278
|
+
const baseProperties = baseClass ? this.collectProperties(baseClass, visitedClassNames) : [];
|
|
279
|
+
return [...baseProperties, ...classDeclaration.getProperties()];
|
|
280
|
+
}
|
|
281
|
+
getClassName(target, fallback) {
|
|
282
|
+
return typeof target === "function" ? target.name : fallback;
|
|
283
|
+
}
|
|
284
|
+
async ensureDatasourceMetadata() {
|
|
285
|
+
if (this.config.datasource.entityMetadatas.length) {
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
const datasource = this.config.datasource;
|
|
289
|
+
if (typeof datasource.buildMetadatas !== "function") {
|
|
290
|
+
throw new Error("DTOGeneratorService config error: datasource cannot build entity metadata.");
|
|
291
|
+
}
|
|
292
|
+
await datasource.buildMetadatas();
|
|
293
|
+
}
|
|
294
|
+
isExcludedColumn(column) {
|
|
295
|
+
return this.config.excludePropertyNames.includes(column.propertyName) || column.isSelect === false || Boolean(column.relationMetadata);
|
|
296
|
+
}
|
|
297
|
+
isDateColumn(column) {
|
|
298
|
+
return column.type === Date || [
|
|
299
|
+
"date",
|
|
300
|
+
"datetime",
|
|
301
|
+
"datetime2",
|
|
302
|
+
"timestamp",
|
|
303
|
+
"timestamp with time zone",
|
|
304
|
+
"timestamp without time zone",
|
|
305
|
+
"time",
|
|
306
|
+
"time with time zone",
|
|
307
|
+
"time without time zone"
|
|
308
|
+
].includes(String(column.type));
|
|
309
|
+
}
|
|
310
|
+
isJsonColumn(column) {
|
|
311
|
+
return ["json", "jsonb", "simple-json"].includes(String(column.type));
|
|
312
|
+
}
|
|
313
|
+
resolveEnumTypeName(entityName, column) {
|
|
314
|
+
var _a;
|
|
315
|
+
const enumTypeName = (_a = this.sourceInfoByClassName.get(entityName)) == null ? void 0 : _a.enumTypeByProperty.get(column.propertyName);
|
|
316
|
+
if (enumTypeName) {
|
|
317
|
+
return enumTypeName;
|
|
318
|
+
}
|
|
319
|
+
if (column.enumName) {
|
|
320
|
+
return column.enumName;
|
|
321
|
+
}
|
|
322
|
+
return `${entityName}${column.propertyName.charAt(0).toUpperCase()}${column.propertyName.slice(1)}`;
|
|
323
|
+
}
|
|
324
|
+
resolveSourcePropertyType(entityName, propertyName) {
|
|
325
|
+
var _a;
|
|
326
|
+
const sourceType = (_a = this.sourceInfoByClassName.get(entityName)) == null ? void 0 : _a.typeByProperty.get(propertyName);
|
|
327
|
+
if (!sourceType) {
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
330
|
+
return sourceType.split("|").map((item) => item.trim()).map((item) => item === "Date" ? "string" : item).join(" | ");
|
|
331
|
+
}
|
|
332
|
+
resolveColumnType(entityName, column) {
|
|
333
|
+
var _a;
|
|
334
|
+
if ((_a = column.enum) == null ? void 0 : _a.length) {
|
|
335
|
+
return this.resolveEnumTypeName(entityName, column);
|
|
336
|
+
}
|
|
337
|
+
const sourceType = this.resolveSourcePropertyType(entityName, column.propertyName);
|
|
338
|
+
if (sourceType) {
|
|
339
|
+
return sourceType;
|
|
340
|
+
}
|
|
341
|
+
const typeText = typeof column.type === "function" ? column.type.name : String(column.type);
|
|
342
|
+
if (this.config.customScalarTypeMap[typeText]) {
|
|
343
|
+
return this.config.customScalarTypeMap[typeText];
|
|
344
|
+
}
|
|
345
|
+
if (this.isDateColumn(column)) {
|
|
346
|
+
return "string";
|
|
347
|
+
}
|
|
348
|
+
if (column.type === String || [
|
|
349
|
+
"char",
|
|
350
|
+
"varchar",
|
|
351
|
+
"nvarchar",
|
|
352
|
+
"text",
|
|
353
|
+
"tinytext",
|
|
354
|
+
"mediumtext",
|
|
355
|
+
"longtext",
|
|
356
|
+
"uuid",
|
|
357
|
+
"simple-enum"
|
|
358
|
+
].includes(typeText)) {
|
|
359
|
+
return "string";
|
|
360
|
+
}
|
|
361
|
+
if (column.type === Number || [
|
|
362
|
+
"int",
|
|
363
|
+
"integer",
|
|
364
|
+
"tinyint",
|
|
365
|
+
"smallint",
|
|
366
|
+
"mediumint",
|
|
367
|
+
"bigint",
|
|
368
|
+
"float",
|
|
369
|
+
"double",
|
|
370
|
+
"double precision",
|
|
371
|
+
"real",
|
|
372
|
+
"decimal",
|
|
373
|
+
"numeric"
|
|
374
|
+
].includes(typeText)) {
|
|
375
|
+
return "number";
|
|
376
|
+
}
|
|
377
|
+
if (column.type === Boolean || ["bool", "boolean"].includes(typeText)) {
|
|
378
|
+
return "boolean";
|
|
379
|
+
}
|
|
380
|
+
if (this.isJsonColumn(column)) {
|
|
381
|
+
return "any";
|
|
382
|
+
}
|
|
383
|
+
if (typeText === "object") {
|
|
384
|
+
return "Record<string, unknown>";
|
|
385
|
+
}
|
|
386
|
+
return typeText;
|
|
387
|
+
}
|
|
388
|
+
appendNullType(typeText) {
|
|
389
|
+
if (typeText.split("|").map((item) => item.trim()).includes("null")) {
|
|
390
|
+
return typeText;
|
|
391
|
+
}
|
|
392
|
+
return `${typeText} | null`;
|
|
393
|
+
}
|
|
394
|
+
columnToMeta(entityName, column) {
|
|
395
|
+
const resolvedType = this.resolveColumnType(entityName, column);
|
|
396
|
+
return {
|
|
397
|
+
name: column.propertyName,
|
|
398
|
+
optional: false,
|
|
399
|
+
type: column.isNullable ? this.appendNullType(resolvedType) : resolvedType,
|
|
400
|
+
isDate: this.isDateColumn(column),
|
|
401
|
+
mapAsNumber: resolvedType === "number",
|
|
402
|
+
enumValues: column.enum ? [...column.enum] : void 0
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
relationToMeta(relation) {
|
|
406
|
+
const targetClassName = this.getClassName(relation.inverseEntityMetadata.target, relation.inverseEntityMetadata.name);
|
|
407
|
+
return {
|
|
408
|
+
name: relation.propertyName,
|
|
409
|
+
targetClassName,
|
|
410
|
+
targetDTOName: `${targetClassName}DTO`,
|
|
411
|
+
isArray: relation.isOneToMany || relation.isManyToMany,
|
|
412
|
+
nullable: relation.isNullable
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
async createEntityMetas() {
|
|
416
|
+
await this.ensureDatasourceMetadata();
|
|
417
|
+
const metas = this.config.datasource.entityMetadatas.map((metadata) => {
|
|
418
|
+
var _a;
|
|
419
|
+
const className = this.getClassName(metadata.target, metadata.name);
|
|
420
|
+
const moduleName = this.toKebabCase(className);
|
|
421
|
+
const columns = metadata.columns.filter((column) => !this.isExcludedColumn(column)).map((column) => this.columnToMeta(className, column));
|
|
422
|
+
const relations = metadata.relations.map((relation) => this.relationToMeta(relation));
|
|
423
|
+
return {
|
|
424
|
+
className,
|
|
425
|
+
dtoName: `${className}DTO`,
|
|
426
|
+
moduleName,
|
|
427
|
+
dtoFileName: `${moduleName}.dto.ts`,
|
|
428
|
+
mapperName: `parse${className}DTO`,
|
|
429
|
+
filePath: (_a = this.sourceInfoByClassName.get(className)) == null ? void 0 : _a.filePath,
|
|
430
|
+
columns,
|
|
431
|
+
relations
|
|
432
|
+
};
|
|
433
|
+
});
|
|
434
|
+
return metas.sort((a, b) => {
|
|
435
|
+
return a.className.localeCompare(b.className);
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
findMetaByClassName(metas, className) {
|
|
439
|
+
return metas.find((meta) => {
|
|
440
|
+
return meta.className === className;
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
getValidRelations(meta, metas) {
|
|
444
|
+
return meta.relations.filter((relation) => {
|
|
445
|
+
return Boolean(this.findMetaByClassName(metas, relation.targetClassName));
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
createDTOContent(meta, metas) {
|
|
449
|
+
var _a;
|
|
450
|
+
const dtoFilePath = path.join(this.config.dtoOutputDir, meta.dtoFileName);
|
|
451
|
+
const validRelations = this.getValidRelations(meta, metas);
|
|
452
|
+
const relationImports = this.unique(
|
|
453
|
+
validRelations.filter((relation) => relation.targetClassName !== meta.className).map((relation) => relation.targetClassName)
|
|
454
|
+
).map((targetClassName) => {
|
|
455
|
+
const targetMeta = this.findMetaByClassName(metas, targetClassName);
|
|
456
|
+
if (!targetMeta) {
|
|
457
|
+
return null;
|
|
458
|
+
}
|
|
459
|
+
const targetPath = path.join(this.config.dtoOutputDir, targetMeta.dtoFileName);
|
|
460
|
+
return [targetMeta.dtoName, this.createRelativeJSImportPath(dtoFilePath, targetPath)];
|
|
461
|
+
}).filter((item) => {
|
|
462
|
+
return item !== null;
|
|
463
|
+
});
|
|
464
|
+
const relationTypeNames = new Set(validRelations.map((relation) => relation.targetDTOName));
|
|
465
|
+
const externalTypeImports = this.collectExternalTypeImports(meta, dtoFilePath, relationTypeNames);
|
|
466
|
+
const importMap = /* @__PURE__ */ new Map();
|
|
467
|
+
for (const [typeName, moduleSpecifier] of [...relationImports, ...externalTypeImports]) {
|
|
468
|
+
if (!importMap.has(moduleSpecifier)) {
|
|
469
|
+
importMap.set(moduleSpecifier, /* @__PURE__ */ new Set());
|
|
470
|
+
}
|
|
471
|
+
(_a = importMap.get(moduleSpecifier)) == null ? void 0 : _a.add(typeName);
|
|
472
|
+
}
|
|
473
|
+
const imports = [...importMap.entries()].sort((a, b) => a[0].localeCompare(b[0])).map(([moduleSpecifier, typeNames]) => {
|
|
474
|
+
let newModuleSpecifier = moduleSpecifier;
|
|
475
|
+
if (!this.config.includeExtensionOnImports) {
|
|
476
|
+
newModuleSpecifier = newModuleSpecifier.replace(".ts", "");
|
|
477
|
+
}
|
|
478
|
+
return `import type { ${[...typeNames].sort((a, b) => a.localeCompare(b)).join(", ")} } from '${newModuleSpecifier}'`;
|
|
479
|
+
});
|
|
480
|
+
const columnLines = meta.columns.map((column) => {
|
|
481
|
+
const optional = column.optional ? "?" : "";
|
|
482
|
+
return ` ${column.name}${optional}: ${column.type}`;
|
|
483
|
+
});
|
|
484
|
+
const relationLines = validRelations.map((relation) => {
|
|
485
|
+
const type = relation.isArray ? `${relation.targetDTOName}[]` : `${relation.targetDTOName}${relation.nullable ? " | null" : ""}`;
|
|
486
|
+
return ` ${relation.name}?: ${type}`;
|
|
487
|
+
});
|
|
488
|
+
const importBlock = imports.length ? `${imports.join("\n")}
|
|
489
|
+
|
|
490
|
+
` : "";
|
|
491
|
+
const body = [...columnLines, ...relationLines].join("\n");
|
|
492
|
+
return `${importBlock}export type ${meta.dtoName} = {
|
|
493
|
+
${body}
|
|
494
|
+
}
|
|
495
|
+
`;
|
|
496
|
+
}
|
|
497
|
+
collectExternalTypeImports(meta, dtoFilePath, relationTypeNames) {
|
|
498
|
+
const symbols = this.collectExternalTypeSymbols(meta, relationTypeNames);
|
|
499
|
+
const importSpecifier = this.getCommonTypesImportSpecifier(dtoFilePath);
|
|
500
|
+
return [...symbols].map((symbol) => {
|
|
501
|
+
return [symbol, importSpecifier];
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
collectExternalTypeSymbols(meta, relationTypeNames) {
|
|
505
|
+
const symbols = /* @__PURE__ */ new Set();
|
|
506
|
+
const ignoredNames = /* @__PURE__ */ new Set([
|
|
507
|
+
"string",
|
|
508
|
+
"number",
|
|
509
|
+
"boolean",
|
|
510
|
+
"null",
|
|
511
|
+
"undefined",
|
|
512
|
+
"unknown",
|
|
513
|
+
"any",
|
|
514
|
+
"object",
|
|
515
|
+
"bigint",
|
|
516
|
+
"never",
|
|
517
|
+
"void",
|
|
518
|
+
"true",
|
|
519
|
+
"false",
|
|
520
|
+
"Record",
|
|
521
|
+
"Array",
|
|
522
|
+
"Promise",
|
|
523
|
+
"ReadonlyArray"
|
|
524
|
+
]);
|
|
525
|
+
for (const column of meta.columns) {
|
|
526
|
+
const identifiers = column.type.match(/\b[A-Za-z_][A-Za-z0-9_]*\b/g) ?? [];
|
|
527
|
+
for (const identifier of identifiers) {
|
|
528
|
+
if (ignoredNames.has(identifier)) {
|
|
529
|
+
continue;
|
|
530
|
+
}
|
|
531
|
+
if (relationTypeNames.has(identifier) || identifier === meta.dtoName) {
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
534
|
+
symbols.add(identifier);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
return symbols;
|
|
538
|
+
}
|
|
539
|
+
collectAllExternalTypeSymbols(metas) {
|
|
540
|
+
const symbols = /* @__PURE__ */ new Set();
|
|
541
|
+
for (const meta of metas) {
|
|
542
|
+
const relationTypeNames = new Set(meta.relations.map((relation) => relation.targetDTOName));
|
|
543
|
+
const metaSymbols = this.collectExternalTypeSymbols(meta, relationTypeNames);
|
|
544
|
+
for (const symbol of metaSymbols) {
|
|
545
|
+
symbols.add(symbol);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
return symbols;
|
|
549
|
+
}
|
|
550
|
+
findTypeDeclarationByName(typeName) {
|
|
551
|
+
for (const sourceFile of this.project.getSourceFiles()) {
|
|
552
|
+
if (this.isIgnoredSourcePath(sourceFile.getFilePath())) {
|
|
553
|
+
continue;
|
|
554
|
+
}
|
|
555
|
+
const enumDeclaration = sourceFile.getEnum(typeName);
|
|
556
|
+
if (enumDeclaration) {
|
|
557
|
+
return enumDeclaration;
|
|
558
|
+
}
|
|
559
|
+
const interfaceDeclaration = sourceFile.getInterface(typeName);
|
|
560
|
+
if (interfaceDeclaration) {
|
|
561
|
+
return interfaceDeclaration;
|
|
562
|
+
}
|
|
563
|
+
const typeAliasDeclaration = sourceFile.getTypeAlias(typeName);
|
|
564
|
+
if (typeAliasDeclaration) {
|
|
565
|
+
return typeAliasDeclaration;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
return null;
|
|
569
|
+
}
|
|
570
|
+
findEnumDeclarationByName(typeName) {
|
|
571
|
+
for (const sourceFile of this.project.getSourceFiles()) {
|
|
572
|
+
if (this.isIgnoredSourcePath(sourceFile.getFilePath())) {
|
|
573
|
+
continue;
|
|
574
|
+
}
|
|
575
|
+
const enumDeclaration = sourceFile.getEnum(typeName);
|
|
576
|
+
if (enumDeclaration) {
|
|
577
|
+
return enumDeclaration;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
return null;
|
|
581
|
+
}
|
|
582
|
+
normalizeDeclarationToExportText(typeName, preferEnum = false) {
|
|
583
|
+
const declaration = preferEnum ? this.findEnumDeclarationByName(typeName) ?? this.findTypeDeclarationByName(typeName) : this.findTypeDeclarationByName(typeName);
|
|
584
|
+
if (!declaration) {
|
|
585
|
+
return null;
|
|
586
|
+
}
|
|
587
|
+
const declarationText = declaration.getText().trim();
|
|
588
|
+
if (declarationText.startsWith("export ")) {
|
|
589
|
+
return declarationText;
|
|
590
|
+
}
|
|
591
|
+
return `export ${declarationText}`;
|
|
592
|
+
}
|
|
593
|
+
formatEnumKey(value) {
|
|
594
|
+
const normalized = String(value).replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[^A-Za-z0-9]+/g, "_").replace(/^_+|_+$/g, "").toUpperCase();
|
|
595
|
+
return normalized || `VALUE_${String(value)}`;
|
|
596
|
+
}
|
|
597
|
+
formatEnumValue(value) {
|
|
598
|
+
return typeof value === "number" ? String(value) : `'${value.replace(/'/g, "\\'")}'`;
|
|
599
|
+
}
|
|
600
|
+
createCommonEnumDeclaration(typeName, values) {
|
|
601
|
+
const uniqueValues = this.unique(values);
|
|
602
|
+
const lines = uniqueValues.map((value) => {
|
|
603
|
+
return ` ${this.formatEnumKey(value)} = ${this.formatEnumValue(value)}`;
|
|
604
|
+
});
|
|
605
|
+
return `export enum ${typeName} {
|
|
606
|
+
${lines.join(",\n")}
|
|
607
|
+
}`;
|
|
608
|
+
}
|
|
609
|
+
ensureCommonTypesFile(metas) {
|
|
610
|
+
var _a;
|
|
611
|
+
const symbols = [...this.collectAllExternalTypeSymbols(metas)].sort((a, b) => a.localeCompare(b));
|
|
612
|
+
const enumBySymbol = /* @__PURE__ */ new Map();
|
|
613
|
+
for (const meta of metas) {
|
|
614
|
+
for (const column of meta.columns) {
|
|
615
|
+
if ((_a = column.enumValues) == null ? void 0 : _a.length) {
|
|
616
|
+
enumBySymbol.set(column.type, column.enumValues);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
const lines = symbols.map((symbol) => {
|
|
621
|
+
const enumValues = enumBySymbol.get(symbol);
|
|
622
|
+
const declarationText = this.normalizeDeclarationToExportText(symbol, Boolean(enumValues));
|
|
623
|
+
if (declarationText) {
|
|
624
|
+
return declarationText.replace(/^export declare /, "export ");
|
|
625
|
+
}
|
|
626
|
+
if (!enumValues) {
|
|
627
|
+
throw new Error(`DTOGeneratorService config error: could not generate common type '${symbol}'.`);
|
|
628
|
+
}
|
|
629
|
+
return this.createCommonEnumDeclaration(symbol, enumValues);
|
|
630
|
+
});
|
|
631
|
+
this.writeFile(this.getCommonTypesFilePath(), `${lines.join("\n\n")}
|
|
632
|
+
`);
|
|
633
|
+
}
|
|
634
|
+
resolveCommonTypesSourceContent() {
|
|
635
|
+
const commonTypesFilePath = this.getCommonTypesFilePath();
|
|
636
|
+
if (!fs.existsSync(commonTypesFilePath)) {
|
|
637
|
+
return null;
|
|
638
|
+
}
|
|
639
|
+
return fs.readFileSync(commonTypesFilePath, "utf-8");
|
|
640
|
+
}
|
|
641
|
+
validateCommonTypeCoverage(metas) {
|
|
642
|
+
const commonTypesContent = this.resolveCommonTypesSourceContent();
|
|
643
|
+
if (!commonTypesContent) {
|
|
644
|
+
throw new Error(`DTOGeneratorService config error: common.dto.ts was not generated in '${this.config.dtoOutputDir}'.`);
|
|
645
|
+
}
|
|
646
|
+
const missing = [];
|
|
647
|
+
for (const meta of metas) {
|
|
648
|
+
const relationTypeNames = new Set(meta.relations.map((relation) => relation.targetDTOName));
|
|
649
|
+
const symbols = this.collectExternalTypeSymbols(meta, relationTypeNames);
|
|
650
|
+
for (const symbol of symbols) {
|
|
651
|
+
const pattern = new RegExp(`export\\s+(?:enum|type|interface)\\s+${symbol}\\b`);
|
|
652
|
+
if (!pattern.test(commonTypesContent)) {
|
|
653
|
+
const columns = meta.columns.filter((column) => {
|
|
654
|
+
const identifiers = column.type.match(/\b[A-Za-z_][A-Za-z0-9_]*\b/g) ?? [];
|
|
655
|
+
return identifiers.includes(symbol);
|
|
656
|
+
}).map((column) => column.name);
|
|
657
|
+
const uniqueColumns = this.unique(columns);
|
|
658
|
+
missing.push(`${symbol} (used in ${meta.dtoName}.${uniqueColumns.join(", ")})`);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
if (missing.length) {
|
|
663
|
+
const details = this.unique(missing).sort((a, b) => a.localeCompare(b)).join("\n- ");
|
|
664
|
+
throw new Error(
|
|
665
|
+
`DTOGeneratorService validation error: generated/common.dto.ts is missing exports for:
|
|
666
|
+
- ${details}`
|
|
667
|
+
);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
createDTOIndexContent(metas) {
|
|
671
|
+
const dtoTypeExports = metas.map((meta) => {
|
|
672
|
+
return `export type { ${meta.dtoName} } from './${meta.moduleName}.dto${this.config.includeExtensionOnImports ? ".ts" : ""}'`;
|
|
673
|
+
}).join("\n");
|
|
674
|
+
return `export * from './common.dto${this.config.includeExtensionOnImports ? ".ts" : ""}'
|
|
675
|
+
${dtoTypeExports}`;
|
|
676
|
+
}
|
|
677
|
+
getMetasWithFilePath(metas) {
|
|
678
|
+
return metas.filter((meta) => {
|
|
679
|
+
return Boolean(meta.filePath);
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
createEntityImports(metas) {
|
|
683
|
+
var _a;
|
|
684
|
+
const metasWithFilePath = this.getMetasWithFilePath(metas);
|
|
685
|
+
if (metasWithFilePath.length !== metas.length) {
|
|
686
|
+
return "";
|
|
687
|
+
}
|
|
688
|
+
const importMap = /* @__PURE__ */ new Map();
|
|
689
|
+
for (const meta of metasWithFilePath) {
|
|
690
|
+
if (!meta.filePath) {
|
|
691
|
+
continue;
|
|
692
|
+
}
|
|
693
|
+
const moduleSpecifier = this.createRelativeJSImportPath(this.config.mapperOutputFile, meta.filePath);
|
|
694
|
+
if (!importMap.has(moduleSpecifier)) {
|
|
695
|
+
importMap.set(moduleSpecifier, /* @__PURE__ */ new Set());
|
|
696
|
+
}
|
|
697
|
+
(_a = importMap.get(moduleSpecifier)) == null ? void 0 : _a.add(meta.className);
|
|
698
|
+
}
|
|
699
|
+
return [...importMap.entries()].sort((a, b) => a[0].localeCompare(b[0])).map(([moduleSpecifier, classNames]) => {
|
|
700
|
+
let newModuleSpecifier = moduleSpecifier;
|
|
701
|
+
if (!this.config.includeExtensionOnImports) {
|
|
702
|
+
newModuleSpecifier = newModuleSpecifier.replace(".ts", "");
|
|
703
|
+
}
|
|
704
|
+
return `import { ${[...classNames].sort((a, b) => a.localeCompare(b)).join(", ")} } from '${newModuleSpecifier}'`;
|
|
705
|
+
}).join("\n");
|
|
706
|
+
}
|
|
707
|
+
getMapperEntityTypeName(meta) {
|
|
708
|
+
return meta.filePath ? meta.className : `${meta.className}EntityLike`;
|
|
709
|
+
}
|
|
710
|
+
createStructuralEntityTypes(metas) {
|
|
711
|
+
if (this.getMetasWithFilePath(metas).length === metas.length) {
|
|
712
|
+
return "";
|
|
713
|
+
}
|
|
714
|
+
return metas.map((meta) => {
|
|
715
|
+
return `type ${meta.className}EntityLike = Record<string, any>`;
|
|
716
|
+
}).join("\n");
|
|
717
|
+
}
|
|
718
|
+
createGeneratedMapperContent(metas) {
|
|
719
|
+
const entityImports = this.createEntityImports(metas);
|
|
720
|
+
const dtoImports = `import type { ${metas.map((meta) => meta.dtoName).join(", ")} } from '${this.config.sharedDTOImportPath}'`;
|
|
721
|
+
const structuralTypes = this.createStructuralEntityTypes(metas);
|
|
722
|
+
const mapperFunctions = metas.map((meta) => {
|
|
723
|
+
return this.createMapperFunctionContent(meta, metas);
|
|
724
|
+
});
|
|
725
|
+
const blocks = [
|
|
726
|
+
entityImports,
|
|
727
|
+
dtoImports,
|
|
728
|
+
structuralTypes,
|
|
729
|
+
mapperFunctions.join("\n\n")
|
|
730
|
+
].filter(Boolean);
|
|
731
|
+
let contents = `${blocks.join("\n")}
|
|
732
|
+
|
|
733
|
+
`;
|
|
734
|
+
const dtoDateContent = `export function toDTODate(value: Date | string): string
|
|
735
|
+
export function toDTODate(value: Date | string | null | undefined): string | null
|
|
736
|
+
export function toDTODate(value: Date | string | null | undefined) {
|
|
737
|
+
if (!value) {
|
|
738
|
+
return null
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
if (value instanceof Date) {
|
|
742
|
+
return value.toISOString()
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
return value
|
|
746
|
+
}
|
|
747
|
+
`;
|
|
748
|
+
contents += dtoDateContent;
|
|
749
|
+
return contents;
|
|
750
|
+
}
|
|
751
|
+
createMapperFunctionContent(meta, metas) {
|
|
752
|
+
const validRelations = this.getValidRelations(meta, metas);
|
|
753
|
+
const entityTypeName = this.getMapperEntityTypeName(meta);
|
|
754
|
+
const columnLines = meta.columns.map((column) => {
|
|
755
|
+
let value = `entity.${column.name}`;
|
|
756
|
+
if (column.isDate) {
|
|
757
|
+
value = `toDTODate(entity.${column.name})`;
|
|
758
|
+
} else if (column.mapAsNumber) {
|
|
759
|
+
value = column.type.includes("null") ? `entity.${column.name} === null || entity.${column.name} === undefined ? null : Number(entity.${column.name})` : `Number(entity.${column.name})`;
|
|
760
|
+
}
|
|
761
|
+
return ` ${column.name}: ${value}`;
|
|
762
|
+
});
|
|
763
|
+
const relationLines = validRelations.map((relation) => {
|
|
764
|
+
return this.createRelationMapperLine(relation, metas);
|
|
765
|
+
});
|
|
766
|
+
const relationBlock = relationLines.length ? `
|
|
767
|
+
${relationLines.join("\n\n")}
|
|
768
|
+
` : "";
|
|
769
|
+
return `export function ${meta.mapperName}(entity: ${entityTypeName}): ${meta.dtoName} {
|
|
770
|
+
const dto: ${meta.dtoName} = {
|
|
771
|
+
${columnLines.join(",\n")}
|
|
772
|
+
}
|
|
773
|
+
${relationBlock}
|
|
774
|
+
return dto
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
export function ${meta.mapperName}Array(entities: ${entityTypeName}[]): ${meta.dtoName}[] {
|
|
778
|
+
return entities.map(${meta.mapperName})
|
|
779
|
+
|
|
780
|
+
}`;
|
|
781
|
+
}
|
|
782
|
+
createRelationMapperLine(relation, metas) {
|
|
783
|
+
const targetMeta = this.findMetaByClassName(metas, relation.targetClassName);
|
|
784
|
+
const parserName = targetMeta == null ? void 0 : targetMeta.mapperName;
|
|
785
|
+
if (relation.isArray) {
|
|
786
|
+
return ` if (Array.isArray(entity.${relation.name})) {
|
|
787
|
+
dto.${relation.name} = entity.${relation.name}.map(${parserName})
|
|
788
|
+
}`;
|
|
789
|
+
}
|
|
790
|
+
if (relation.nullable) {
|
|
791
|
+
return ` if (entity.${relation.name} !== undefined) {
|
|
792
|
+
dto.${relation.name} = entity.${relation.name} === null ? null : ${parserName}(entity.${relation.name})
|
|
793
|
+
}`;
|
|
794
|
+
}
|
|
795
|
+
return ` if (entity.${relation.name} !== undefined && entity.${relation.name} !== null) {
|
|
796
|
+
dto.${relation.name} = ${parserName}(entity.${relation.name})
|
|
797
|
+
}`;
|
|
798
|
+
}
|
|
799
|
+
};
|
|
800
|
+
export {
|
|
801
|
+
TORMDTOGenerator
|
|
802
|
+
};
|
|
803
|
+
//# sourceMappingURL=index.mjs.map
|