nestcraftx 0.2.4 → 0.2.6
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/.gitattributes +6 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +33 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +19 -0
- package/.github/ISSUE_TEMPLATE/pull_request_template.md +24 -0
- package/CHANGELOG.fr.md +97 -97
- package/CHANGELOG.md +98 -98
- package/CLI_USAGE.fr.md +331 -331
- package/CLI_USAGE.md +364 -364
- package/DEMO.fr.md +292 -292
- package/DEMO.md +294 -294
- package/LICENSE +21 -21
- package/MIGRATION_GUIDE.fr.md +127 -127
- package/MIGRATION_GUIDE.md +124 -124
- package/QUICK_START.fr.md +152 -152
- package/QUICK_START.md +169 -169
- package/README.fr.md +653 -659
- package/SECURITY.md +10 -0
- package/bin/nestcraft.js +84 -64
- package/commands/demo.js +333 -330
- package/commands/generate.js +93 -0
- package/commands/generateConf.js +91 -0
- package/commands/help.js +78 -78
- package/commands/info.js +48 -48
- package/commands/new.js +338 -335
- package/commands/start.js +19 -19
- package/commands/test.js +7 -7
- package/package.json +41 -41
- package/readme.md +638 -643
- package/utils/cliParser.js +133 -76
- package/utils/colors.js +62 -62
- package/utils/configs/configureDocker.js +120 -120
- package/utils/configs/setupCleanArchitecture.js +563 -557
- package/utils/configs/setupLightArchitecture.js +701 -660
- package/utils/envGenerator.js +122 -122
- package/utils/file-utils/packageJsonUtils.js +49 -55
- package/utils/file-utils/saveProjectConfig.js +36 -0
- package/utils/fullModeInput.js +607 -607
- package/utils/generators/application/dtoUpdater.js +54 -0
- package/utils/generators/cleanModuleGenerator.js +475 -0
- package/utils/generators/database/setupDatabase.js +31 -0
- package/utils/generators/domain/entityUpdater.js +78 -0
- package/utils/generators/infrastructure/mapperUpdater.js +65 -0
- package/utils/generators/lightModuleGenerator.js +131 -0
- package/utils/generators/relation/relation.engine.js +64 -0
- package/utils/interactive/askEntityInputs.js +165 -0
- package/utils/lightModeInput.js +460 -460
- package/utils/loggers/logError.js +7 -7
- package/utils/loggers/logInfo.js +7 -7
- package/utils/loggers/logSuccess.js +7 -7
- package/utils/loggers/logWarning.js +7 -7
- package/utils/setups/orms/typeOrmSetup.js +630 -630
- package/utils/setups/projectSetup.js +46 -46
- package/utils/setups/setupAuth.js +973 -926
- package/utils/setups/setupDatabase.js +75 -75
- package/utils/setups/setupLogger.js +69 -59
- package/utils/setups/setupMongoose.js +377 -432
- package/utils/setups/setupPrisma.js +802 -630
- package/utils/setups/setupSwagger.js +97 -88
- package/utils/shell.js +32 -32
- package/utils/spinner.js +57 -57
- package/utils/systemCheck.js +124 -124
- package/utils/userInput.js +421 -421
- package/utils/utils.js +2197 -1762
|
@@ -1,630 +1,630 @@
|
|
|
1
|
-
// setupTypeORM.js
|
|
2
|
-
// const { execSync } = require("child_process");
|
|
3
|
-
// const path = require("path");
|
|
4
|
-
// const { runCommand } = require("../shell");
|
|
5
|
-
|
|
6
|
-
const {
|
|
7
|
-
updateFile,
|
|
8
|
-
capitalize,
|
|
9
|
-
createFile,
|
|
10
|
-
createDirectory,
|
|
11
|
-
decapitalize,
|
|
12
|
-
} = require("../../userInput");
|
|
13
|
-
const { logInfo } = require("../../loggers/logInfo");
|
|
14
|
-
const { logSuccess } = require("../../loggers/logSuccess");
|
|
15
|
-
const path = require("path");
|
|
16
|
-
const { runCommand } = require("../../shell");
|
|
17
|
-
const { updatePackageJson } = require("../../file-utils/packageJsonUtils");
|
|
18
|
-
const { info } = require("../../colors");
|
|
19
|
-
|
|
20
|
-
async function setupTypeORM(inputs) {
|
|
21
|
-
logInfo("📦 Installing TypeORM and PostgreSQL dependencies...");
|
|
22
|
-
|
|
23
|
-
const mode = inputs.mode;
|
|
24
|
-
await runCommand(
|
|
25
|
-
"npm install @nestjs/typeorm typeorm pg reflect-metadata",
|
|
26
|
-
"TypeORM and PostgreSQL dependencies installed successfully"
|
|
27
|
-
); // Updating app.module.ts with TypeORM
|
|
28
|
-
|
|
29
|
-
const appModulePath = "src/app.module.ts";
|
|
30
|
-
const typeOrmImport = `import { TypeOrmModule } from '@nestjs/typeorm';`;
|
|
31
|
-
const typeOrmConfig = `
|
|
32
|
-
TypeOrmModule.forRoot({
|
|
33
|
-
type: 'postgres',
|
|
34
|
-
host: process.env.POSTGRES_HOST,
|
|
35
|
-
port: process.env.POSTGRES_PORT
|
|
36
|
-
? parseInt(process.env.POSTGRES_PORT, 10)
|
|
37
|
-
: 5432,
|
|
38
|
-
username: process.env.POSTGRES_USER,
|
|
39
|
-
password: process.env.POSTGRES_PASSWORD,
|
|
40
|
-
database: process.env.POSTGRES_DB,
|
|
41
|
-
autoLoadEntities: true, // Reinstated for automatic loading of entities registered in .forFeature
|
|
42
|
-
synchronize: true, // Only for dev use!
|
|
43
|
-
// dropSchema: true, //// ⚠️ wipes the entire schema on every restart! Only for dev use!
|
|
44
|
-
}),`;
|
|
45
|
-
|
|
46
|
-
logInfo("⚙️ Updating app.module.ts with TypeORM..."); // 1. Updating TypeOrmModule.forRoot()
|
|
47
|
-
await updateFile({
|
|
48
|
-
path: appModulePath,
|
|
49
|
-
pattern: `ConfigModule.forRoot({
|
|
50
|
-
isGlobal: true, // Make ConfigModule globally accessible
|
|
51
|
-
envFilePath: '.env', // Load environment variables
|
|
52
|
-
}),`,
|
|
53
|
-
replacement: ` ConfigModule.forRoot({
|
|
54
|
-
isGlobal: true, // Make ConfigModule globally accessible
|
|
55
|
-
envFilePath: '.env', // Load environment variables
|
|
56
|
-
}),
|
|
57
|
-
${typeOrmConfig}`,
|
|
58
|
-
}); // 2. Adding TypeOrmModule import
|
|
59
|
-
|
|
60
|
-
await updateFile({
|
|
61
|
-
path: appModulePath,
|
|
62
|
-
pattern: "import { Module } from '@nestjs/common';",
|
|
63
|
-
replacement: `import { Module } from '@nestjs/common';
|
|
64
|
-
${typeOrmImport}`,
|
|
65
|
-
}); // Entity Generation
|
|
66
|
-
|
|
67
|
-
logInfo("📁 Generating entities for TypeORM...");
|
|
68
|
-
|
|
69
|
-
await createDirectory("src/entities");
|
|
70
|
-
|
|
71
|
-
for (const entity of inputs.entitiesData.entities) {
|
|
72
|
-
const entityName = capitalize(entity.name);
|
|
73
|
-
const entityNameLower = decapitalize(entity.name);
|
|
74
|
-
let fieldsContent = "";
|
|
75
|
-
let relationsContent = "";
|
|
76
|
-
let imports = [
|
|
77
|
-
"Entity",
|
|
78
|
-
"Column",
|
|
79
|
-
"PrimaryGeneratedColumn",
|
|
80
|
-
"CreateDateColumn",
|
|
81
|
-
"UpdateDateColumn",
|
|
82
|
-
];
|
|
83
|
-
let extraImports = ""; // --- Basic Data Field Generation Logic --- // Filtering to avoid duplication of relationship fields
|
|
84
|
-
|
|
85
|
-
const relationFields = inputs.entitiesData.relations
|
|
86
|
-
.reduce((acc, rel) => {
|
|
87
|
-
// Foreign key and relation name (Many side)
|
|
88
|
-
if (
|
|
89
|
-
rel.from.toLowerCase() === entityNameLower &&
|
|
90
|
-
["n-1", "1-1"].includes(rel.type)
|
|
91
|
-
) {
|
|
92
|
-
acc.push(`${rel.to}Id`, rel.to);
|
|
93
|
-
}
|
|
94
|
-
if (
|
|
95
|
-
rel.to.toLowerCase() === entityNameLower &&
|
|
96
|
-
["1-n", "1-1"].includes(rel.type)
|
|
97
|
-
) {
|
|
98
|
-
acc.push(`${rel.from}Id`, rel.from);
|
|
99
|
-
} // List name (One side)
|
|
100
|
-
if (rel.from.toLowerCase() === entityNameLower && rel.type === "1-n") {
|
|
101
|
-
acc.push(`${rel.to}s`);
|
|
102
|
-
}
|
|
103
|
-
if (rel.to.toLowerCase() === entityNameLower && rel.type === "n-1") {
|
|
104
|
-
acc.push(`${rel.from}s`);
|
|
105
|
-
} // For 1-1
|
|
106
|
-
if (rel.type === "1-1") {
|
|
107
|
-
// We manage both sides, ensure only one has the foreign key
|
|
108
|
-
// We just exclude model names that will become relations
|
|
109
|
-
acc.push(rel.from, rel.to);
|
|
110
|
-
}
|
|
111
|
-
return acc;
|
|
112
|
-
}, [])
|
|
113
|
-
.map((f) => f.toLowerCase());
|
|
114
|
-
|
|
115
|
-
const isUserEntity = entity.name.toLowerCase() === "user";
|
|
116
|
-
const hasRoleField = entity.fields.some((f) => f.name === "role");
|
|
117
|
-
|
|
118
|
-
// Utilisation d'un Set pour éviter les doublons d'imports
|
|
119
|
-
const enumImports = new Set();
|
|
120
|
-
|
|
121
|
-
if (isUserEntity && hasRoleField) {
|
|
122
|
-
const rolePath =
|
|
123
|
-
mode === "full"
|
|
124
|
-
? "src/user/domain/enums/role.enum"
|
|
125
|
-
: "src/common/enums/role.enum";
|
|
126
|
-
enumImports.add(`import { Role } from '${rolePath}';`);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// Liste des colonnes déjà présentes dans le template de classe
|
|
130
|
-
const RESERVED_FIELDS = ["id", "createdat", "updatedat"];
|
|
131
|
-
|
|
132
|
-
for (const field of entity.fields) {
|
|
133
|
-
const fieldNameLower = field.name.toLowerCase();
|
|
134
|
-
|
|
135
|
-
// 1. Skip si le champ est géré par une relation
|
|
136
|
-
if (relationFields.includes(fieldNameLower)) continue;
|
|
137
|
-
|
|
138
|
-
// 2. Skip si le champ est déjà présent par défaut (id, createdAt, updatedAt)
|
|
139
|
-
if (RESERVED_FIELDS.includes(fieldNameLower)) {
|
|
140
|
-
console.log(
|
|
141
|
-
`${info("[INFO]")} Skipping default field: ${
|
|
142
|
-
field.name
|
|
143
|
-
} for entity ${entityName}`
|
|
144
|
-
);
|
|
145
|
-
continue;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const mapping = mapTypeToTypeORM(field.type);
|
|
149
|
-
const columnOptions = [`type: '${mapping.type}'`];
|
|
150
|
-
|
|
151
|
-
// 1. Gestion des Options spécifiques (Enum, JSON, Array)
|
|
152
|
-
if (mapping.type === "enum") {
|
|
153
|
-
columnOptions.push(`enum: ${mapping.tsType}`);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// ✅ Default role = USER pour User.role
|
|
157
|
-
if (
|
|
158
|
-
isUserEntity &&
|
|
159
|
-
field.name.toLowerCase() === "role" &&
|
|
160
|
-
mapping.tsType === "Role"
|
|
161
|
-
) {
|
|
162
|
-
columnOptions.push("default: Role.USER");
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
if (field.optional) {
|
|
166
|
-
columnOptions.push("nullable: true");
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
if (field.name.toLowerCase() === "email") {
|
|
170
|
-
columnOptions.push("unique: true");
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// 2. Gestion des Imports d'Enums Custom
|
|
174
|
-
const isEnum = mapping.type === "enum";
|
|
175
|
-
const isNotRole = mapping.tsType.toLowerCase() !== "role";
|
|
176
|
-
|
|
177
|
-
if (isEnum && isNotRole) {
|
|
178
|
-
// On stocke l'import dans le Set
|
|
179
|
-
enumImports.add(
|
|
180
|
-
`import { ${
|
|
181
|
-
mapping.tsType
|
|
182
|
-
} } from '../shared/enums/${mapping.tsType.toLowerCase()}.enum';`
|
|
183
|
-
);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// 3. Construction du champ
|
|
187
|
-
fieldsContent += `
|
|
188
|
-
@Column({ ${columnOptions.join(", ")} })
|
|
189
|
-
${field.name}${field.optional ? "?" : ""}: ${mapping.tsType};
|
|
190
|
-
`;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// On ajoute tous les imports uniques au début du fichier
|
|
194
|
-
extraImports += Array.from(enumImports).join("\n") + "\n";
|
|
195
|
-
|
|
196
|
-
// --- TypeORM Relation Generation Logic ---
|
|
197
|
-
for (const relation of inputs.entitiesData.relations) {
|
|
198
|
-
const relFrom = relation.from;
|
|
199
|
-
const relTo = relation.to;
|
|
200
|
-
const relType = relation.type;
|
|
201
|
-
|
|
202
|
-
if (relType === "1-n") {
|
|
203
|
-
// If the current entity is on the 'One' side (relFrom), it holds the @OneToMany
|
|
204
|
-
if (relFrom.toLowerCase() === entityNameLower) {
|
|
205
|
-
const targetEntity = capitalize(relTo);
|
|
206
|
-
relationsContent += `
|
|
207
|
-
@OneToMany(() => ${targetEntity}, (${decapitalize(
|
|
208
|
-
relTo
|
|
209
|
-
)}) => ${decapitalize(relTo)}.${relFrom.toLowerCase()})
|
|
210
|
-
${relTo.toLowerCase()}s: ${targetEntity}[];
|
|
211
|
-
`;
|
|
212
|
-
imports.push("OneToMany");
|
|
213
|
-
extraImports += `\nimport { ${targetEntity} } from './${targetEntity}.entity';`;
|
|
214
|
-
} // If the current entity is on the 'Many' side (relTo), it holds the @ManyToOne
|
|
215
|
-
else if (relTo.toLowerCase() === entityNameLower) {
|
|
216
|
-
const targetEntity = capitalize(relFrom);
|
|
217
|
-
const fkName = `${relFrom.toLowerCase()}Id`;
|
|
218
|
-
relationsContent += `
|
|
219
|
-
@Column({ type: 'uuid' }) // Foreign Key
|
|
220
|
-
${fkName}: string;
|
|
221
|
-
|
|
222
|
-
@ManyToOne(() => ${targetEntity}, (${decapitalize(
|
|
223
|
-
relFrom
|
|
224
|
-
)}) => ${decapitalize(relFrom)}.${relTo.toLowerCase()}s)
|
|
225
|
-
${relFrom.toLowerCase()}: ${targetEntity};
|
|
226
|
-
`;
|
|
227
|
-
imports.push("ManyToOne");
|
|
228
|
-
extraImports += `\nimport { ${targetEntity} } from './${targetEntity}.entity';`;
|
|
229
|
-
}
|
|
230
|
-
} else if (relType === "n-1") {
|
|
231
|
-
// n-1 is the inverse: relFrom is the 'Many', relTo is the 'One'
|
|
232
|
-
|
|
233
|
-
// If the current entity is on the 'Many' side (relFrom), it holds the @ManyToOne
|
|
234
|
-
if (relFrom.toLowerCase() === entityNameLower) {
|
|
235
|
-
const targetEntity = capitalize(relTo);
|
|
236
|
-
const fkName = `${relTo.toLowerCase()}Id`;
|
|
237
|
-
relationsContent += `
|
|
238
|
-
@Column({ type: 'uuid' }) // Foreign Key
|
|
239
|
-
${fkName}: string;
|
|
240
|
-
|
|
241
|
-
@ManyToOne(() => ${targetEntity}, (${decapitalize(
|
|
242
|
-
relTo
|
|
243
|
-
)}) => ${decapitalize(relTo)}.${relFrom.toLowerCase()}s)
|
|
244
|
-
${relTo.toLowerCase()}: ${targetEntity};
|
|
245
|
-
`;
|
|
246
|
-
imports.push("ManyToOne");
|
|
247
|
-
extraImports += `\nimport { ${targetEntity} } from './${targetEntity}.entity';`;
|
|
248
|
-
} // If the current entity is on the 'One' side (relTo), it holds the @OneToMany
|
|
249
|
-
else if (relTo.toLowerCase() === entityNameLower) {
|
|
250
|
-
const targetEntity = capitalize(relFrom);
|
|
251
|
-
relationsContent += `
|
|
252
|
-
@OneToMany(() => ${targetEntity}, (${decapitalize(
|
|
253
|
-
relFrom
|
|
254
|
-
)}) => ${decapitalize(relFrom)}.${relTo.toLowerCase()})
|
|
255
|
-
${relFrom.toLowerCase()}s: ${targetEntity}[];
|
|
256
|
-
`;
|
|
257
|
-
imports.push("OneToMany");
|
|
258
|
-
extraImports += `\nimport { ${targetEntity} } from './${targetEntity}.entity';`;
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
if (relType === "1-1") {
|
|
262
|
-
// If the current entity is on the 'from' side, it holds the @OneToOne and the foreign key
|
|
263
|
-
if (relFrom.toLowerCase() === entityNameLower) {
|
|
264
|
-
const targetEntity = capitalize(relTo);
|
|
265
|
-
const fkName = `${relTo.toLowerCase()}Id`;
|
|
266
|
-
|
|
267
|
-
relationsContent += `
|
|
268
|
-
@Column({ type: 'uuid', unique: true }) // Unique foreign key for 1-1
|
|
269
|
-
${fkName}: string;
|
|
270
|
-
|
|
271
|
-
@OneToOne(() => ${targetEntity}, (${decapitalize(relTo)}) => ${decapitalize(
|
|
272
|
-
relTo
|
|
273
|
-
)}.${relFrom.toLowerCase()})
|
|
274
|
-
@JoinColumn({ name: '${fkName}' }) // Requires JoinColumn for the foreign key
|
|
275
|
-
${relTo.toLowerCase()}: ${targetEntity};
|
|
276
|
-
`;
|
|
277
|
-
imports.push("OneToOne", "JoinColumn");
|
|
278
|
-
extraImports += `\nimport { ${targetEntity} } from './${targetEntity}.entity';`;
|
|
279
|
-
} // If the current entity is on the 'to' side, it holds the inverse @OneToOne relationship (mapping)
|
|
280
|
-
else if (relTo.toLowerCase() === entityNameLower) {
|
|
281
|
-
const targetEntity = capitalize(relFrom);
|
|
282
|
-
relationsContent += `
|
|
283
|
-
@OneToOne(() => ${targetEntity}, (${decapitalize(
|
|
284
|
-
relFrom
|
|
285
|
-
)}) => ${decapitalize(relFrom)}.${relTo.toLowerCase()})
|
|
286
|
-
${relFrom.toLowerCase()}: ${targetEntity};
|
|
287
|
-
`;
|
|
288
|
-
imports.push("OneToOne");
|
|
289
|
-
extraImports += `\nimport { ${targetEntity} } from './${targetEntity}.entity';`;
|
|
290
|
-
}
|
|
291
|
-
} else if (relType === "n-n") {
|
|
292
|
-
const targetEntity = capitalize(
|
|
293
|
-
relFrom.toLowerCase() === entityNameLower ? relTo : relFrom
|
|
294
|
-
);
|
|
295
|
-
const currentSide =
|
|
296
|
-
relFrom.toLowerCase() === entityNameLower ? relTo : relFrom;
|
|
297
|
-
const otherSide =
|
|
298
|
-
relFrom.toLowerCase() === entityNameLower ? relFrom : relTo; // TypeORM requires JoinTable on one side (we choose the 'from' side for simplicity)
|
|
299
|
-
|
|
300
|
-
if (relFrom.toLowerCase() === entityNameLower) {
|
|
301
|
-
relationsContent += `
|
|
302
|
-
@ManyToMany(() => ${targetEntity}, (${decapitalize(
|
|
303
|
-
currentSide
|
|
304
|
-
)}) => ${decapitalize(currentSide)}.${otherSide.toLowerCase()}s)
|
|
305
|
-
@JoinTable() // Adding JoinTable to create the junction table
|
|
306
|
-
${currentSide.toLowerCase()}s: ${targetEntity}[];
|
|
307
|
-
`;
|
|
308
|
-
imports.push("ManyToMany", "JoinTable");
|
|
309
|
-
} else {
|
|
310
|
-
relationsContent += `
|
|
311
|
-
@ManyToMany(() => ${targetEntity}, (${decapitalize(
|
|
312
|
-
currentSide
|
|
313
|
-
)}) => ${decapitalize(currentSide)}.${otherSide.toLowerCase()}s)
|
|
314
|
-
${currentSide.toLowerCase()}s: ${targetEntity}[];
|
|
315
|
-
`;
|
|
316
|
-
imports.push("ManyToMany");
|
|
317
|
-
}
|
|
318
|
-
extraImports += `\nimport { ${targetEntity} } from './${targetEntity}.entity';`;
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
// --- Final File Generation ---
|
|
323
|
-
const uniqueImports = Array.from(new Set(imports)).join(", ");
|
|
324
|
-
|
|
325
|
-
const content = `${extraImports}
|
|
326
|
-
import { ${uniqueImports} } from 'typeorm';
|
|
327
|
-
|
|
328
|
-
@Entity('${entityNameLower}')
|
|
329
|
-
export class ${entityName} {
|
|
330
|
-
@PrimaryGeneratedColumn('uuid')
|
|
331
|
-
id: string;
|
|
332
|
-
|
|
333
|
-
@CreateDateColumn()
|
|
334
|
-
createdAt: Date;
|
|
335
|
-
|
|
336
|
-
@UpdateDateColumn()
|
|
337
|
-
updatedAt: Date;
|
|
338
|
-
|
|
339
|
-
${fieldsContent}
|
|
340
|
-
${relationsContent}
|
|
341
|
-
}`;
|
|
342
|
-
|
|
343
|
-
await createFile({
|
|
344
|
-
path: `src/entities/${entityName}.entity.ts`,
|
|
345
|
-
contente: content,
|
|
346
|
-
});
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
if (inputs.isDemo) {
|
|
350
|
-
await setupTypeOrmSeeding(inputs);
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
logSuccess("✅ TypeORM configuration complete. Ready to code!");
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
/**
|
|
357
|
-
* Maps entity types to TypeORM column types (PostgreSQL).
|
|
358
|
-
* @param type The input type (string, number, boolean, Date, enum, array, etc.)
|
|
359
|
-
* @returns { object: type, tsType: string } The mapping information
|
|
360
|
-
*/
|
|
361
|
-
function mapTypeToTypeORM(type) {
|
|
362
|
-
// Special case for arrays (e.g., 'string[]')
|
|
363
|
-
if (type.endsWith("[]")) {
|
|
364
|
-
const innerType = type.slice(0, -2);
|
|
365
|
-
const innerMapping = mapTypeToTypeORM(innerType); // Recursive
|
|
366
|
-
return {
|
|
367
|
-
type: innerMapping.type,
|
|
368
|
-
tsType: `${innerMapping.tsType}[]`,
|
|
369
|
-
options: "array: true", // TypeORM option for arrays
|
|
370
|
-
};
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
const typeLower = type.toLowerCase();
|
|
374
|
-
|
|
375
|
-
switch (typeLower) {
|
|
376
|
-
case "string":
|
|
377
|
-
return { type: "varchar", tsType: "string" };
|
|
378
|
-
case "text":
|
|
379
|
-
return { type: "text", tsType: "string" };
|
|
380
|
-
case "number":
|
|
381
|
-
case "float":
|
|
382
|
-
return { type: "float", tsType: "number" };
|
|
383
|
-
case "int":
|
|
384
|
-
return { type: "integer", tsType: "number" };
|
|
385
|
-
case "decimal":
|
|
386
|
-
return { type: "decimal", tsType: "number" };
|
|
387
|
-
case "uuid":
|
|
388
|
-
return { type: "uuid", tsType: "string" };
|
|
389
|
-
case "date":
|
|
390
|
-
return { type: "timestamp", tsType: "Date" };
|
|
391
|
-
case "boolean":
|
|
392
|
-
return { type: "boolean", tsType: "boolean" };
|
|
393
|
-
case "json":
|
|
394
|
-
case "object":
|
|
395
|
-
return { type: "jsonb", tsType: "any" }; // JSONB type is flexible
|
|
396
|
-
|
|
397
|
-
default: // Case of an Enum type or a named object (e.g., 'Address')
|
|
398
|
-
// By default, we assume it's an enum to be stored in the database (varchar)
|
|
399
|
-
if (type.charAt(0) === type.charAt(0).toUpperCase()) {
|
|
400
|
-
// For Enums, TypeORM needs the 'enum' and 'enumName' option
|
|
401
|
-
return {
|
|
402
|
-
type: "enum",
|
|
403
|
-
tsType: type, // The name of the Enum/object in TypeScript
|
|
404
|
-
options: `enum: ${type}, enumName: '${type.toLowerCase()}_enum'`, // Example of enum options
|
|
405
|
-
};
|
|
406
|
-
}
|
|
407
|
-
return { type: "varchar", tsType: "string" };
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
async function setupTypeOrmSeeding(inputs) {
|
|
412
|
-
logInfo("⚙️ Configuring seeding for TypeORM...");
|
|
413
|
-
|
|
414
|
-
// --- Dependencies ---
|
|
415
|
-
const typeOrmDevDeps = [
|
|
416
|
-
"ts-node",
|
|
417
|
-
"@types/node",
|
|
418
|
-
"@types/bcrypt",
|
|
419
|
-
"typeorm-extension",
|
|
420
|
-
];
|
|
421
|
-
|
|
422
|
-
await runCommand(
|
|
423
|
-
`${inputs.packageManager} add -D ${typeOrmDevDeps.join(" ")}`,
|
|
424
|
-
"❌ Failed to install TypeORM seeding dependencies"
|
|
425
|
-
);
|
|
426
|
-
|
|
427
|
-
await runCommand(
|
|
428
|
-
`${inputs.packageManager} install bcrypt`,
|
|
429
|
-
"❌ Failed to install bcrypt"
|
|
430
|
-
);
|
|
431
|
-
|
|
432
|
-
// --- Scripts in package.json ---
|
|
433
|
-
const typeOrmScripts = {
|
|
434
|
-
"typeorm:migrate:run":
|
|
435
|
-
"typeorm-ts-node-commonjs migration:run -d ./src/database/typeorm.config.ts",
|
|
436
|
-
"typeorm:seed":
|
|
437
|
-
"typeorm-extension seed:run -d src/database/typeorm.config.ts",
|
|
438
|
-
seed: "npm run typeorm:seed",
|
|
439
|
-
};
|
|
440
|
-
|
|
441
|
-
await updatePackageJson(inputs, typeOrmScripts);
|
|
442
|
-
|
|
443
|
-
// --- Creating structure and Seeder ---
|
|
444
|
-
await createDirectory("src/database/seeders");
|
|
445
|
-
|
|
446
|
-
const userSeederContent = generateTypeOrmSeederContent();
|
|
447
|
-
await createFile({
|
|
448
|
-
path: `src/database/seeders/DemoSeeder.ts`,
|
|
449
|
-
contente: userSeederContent,
|
|
450
|
-
});
|
|
451
|
-
|
|
452
|
-
await createFile({
|
|
453
|
-
path: `src/database/typeorm.config.ts`,
|
|
454
|
-
contente: `import * as dotenv from 'dotenv';
|
|
455
|
-
dotenv.config();
|
|
456
|
-
import { DataSource } from 'typeorm';
|
|
457
|
-
import { DataSourceOptions } from 'typeorm';
|
|
458
|
-
import { SeederOptions } from 'typeorm-extension';
|
|
459
|
-
import { DemoSeeder } from './seeders/DemoSeeder';
|
|
460
|
-
import { User } from 'src/entities/User.entity';
|
|
461
|
-
import { Post } from 'src/entities/Post.entity';
|
|
462
|
-
import { Comment } from 'src/entities/Comment.entity';
|
|
463
|
-
import { Session } from 'src/entities/Session.entity';
|
|
464
|
-
|
|
465
|
-
const config: DataSourceOptions & SeederOptions = {
|
|
466
|
-
type: 'postgres',
|
|
467
|
-
host: process.env.POSTGRES_HOST,
|
|
468
|
-
port: Number(process.env.POSTGRES_PORT ?? 5432),
|
|
469
|
-
username: process.env.POSTGRES_USER,
|
|
470
|
-
password: process.env.POSTGRES_PASSWORD,
|
|
471
|
-
database: process.env.POSTGRES_DB,
|
|
472
|
-
|
|
473
|
-
entities: [User, Post, Comment, Session],
|
|
474
|
-
|
|
475
|
-
synchronize: process.env.NODE_ENV !== 'production',
|
|
476
|
-
logging: process.env.NODE_ENV === 'development',
|
|
477
|
-
|
|
478
|
-
seeds: [DemoSeeder],
|
|
479
|
-
};
|
|
480
|
-
|
|
481
|
-
export const AppDataSource = new DataSource(config);
|
|
482
|
-
`,
|
|
483
|
-
});
|
|
484
|
-
|
|
485
|
-
logSuccess("✅ TypeORM seeding configured.");
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
function generateTypeOrmSeederContent() {
|
|
489
|
-
return `
|
|
490
|
-
import { DataSource } from 'typeorm';
|
|
491
|
-
import { User } from 'src/entities/User.entity';
|
|
492
|
-
import { Post } from 'src/entities/Post.entity';
|
|
493
|
-
import { Comment } from 'src/entities/Comment.entity';
|
|
494
|
-
import * as bcrypt from 'bcrypt';
|
|
495
|
-
|
|
496
|
-
export class DemoSeeder {
|
|
497
|
-
constructor() {}
|
|
498
|
-
|
|
499
|
-
async run(dataSource: DataSource) {
|
|
500
|
-
console.log('🌱 Starting TypeORM seeding...');
|
|
501
|
-
|
|
502
|
-
const userRepository = dataSource.getRepository(User);
|
|
503
|
-
const postRepository = dataSource.getRepository(Post);
|
|
504
|
-
const commentRepository = dataSource.getRepository(Comment);
|
|
505
|
-
|
|
506
|
-
// --- 1. ADMIN ---
|
|
507
|
-
const salt = await bcrypt.genSalt(10);
|
|
508
|
-
const hashedPassword = await bcrypt.hash('password123', salt);
|
|
509
|
-
|
|
510
|
-
const admin = userRepository.create({
|
|
511
|
-
email: 'admin@nestcraft.com',
|
|
512
|
-
password: hashedPassword,
|
|
513
|
-
username: 'NestCraftAdmin',
|
|
514
|
-
isActive: true,
|
|
515
|
-
});
|
|
516
|
-
|
|
517
|
-
const exists = await userRepository.findOneBy({
|
|
518
|
-
email: admin.email,
|
|
519
|
-
});
|
|
520
|
-
|
|
521
|
-
if (!exists) {
|
|
522
|
-
await userRepository.save(admin);
|
|
523
|
-
console.log('👑 Admin user created');
|
|
524
|
-
} else {
|
|
525
|
-
admin.id = exists.id;
|
|
526
|
-
console.log('👑 Admin user realy exists');
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
// --- 2. DEMO USERS ---
|
|
530
|
-
const demoUsersData = [
|
|
531
|
-
{ email: 'emma.jones@demo.com', password: hashedPassword, username: 'EmmaJones', isActive: true },
|
|
532
|
-
{ email: 'lucas.martin@demo.com', password: hashedPassword, username: 'LucasMartin', isActive: true },
|
|
533
|
-
{ email: 'sophia.bernard@demo.com', password: hashedPassword, username: 'SophiaBernard', isActive: true },
|
|
534
|
-
{ email: 'alexandre.dubois@demo.com', password: hashedPassword, username: 'AlexandreDubois', isActive: true },
|
|
535
|
-
{ email: 'chloe.moreau@demo.com', password: hashedPassword, username: 'ChloeMoreau', isActive: true },
|
|
536
|
-
];
|
|
537
|
-
|
|
538
|
-
const users: User[] = [];
|
|
539
|
-
|
|
540
|
-
for (const userData of demoUsersData) {
|
|
541
|
-
let user = await userRepository.findOneBy({ email: userData.email });
|
|
542
|
-
|
|
543
|
-
if (!user) {
|
|
544
|
-
user = userRepository.create(userData);
|
|
545
|
-
user = await userRepository.save(user);
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
users.push(user);
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
console.log(\`👥 \${users.length} demo users created.\`);
|
|
552
|
-
|
|
553
|
-
const allUsers = [admin, ...users];
|
|
554
|
-
|
|
555
|
-
// --- 3. DEMO POSTS ---
|
|
556
|
-
const postsData = [
|
|
557
|
-
{
|
|
558
|
-
title: 'The Basics of NestJS for Modern Developers',
|
|
559
|
-
content: 'Discover how to build a robust and maintainable API with NestJS...',
|
|
560
|
-
published: true,
|
|
561
|
-
userId: allUsers[1].id,
|
|
562
|
-
},
|
|
563
|
-
{
|
|
564
|
-
title: 'How to Secure Your API with JWT',
|
|
565
|
-
content: 'JWT authentication is a standard for securing APIs...',
|
|
566
|
-
published: true,
|
|
567
|
-
userId: allUsers[2].id,
|
|
568
|
-
},
|
|
569
|
-
{
|
|
570
|
-
title: 'Optimizing Node.js API Performance',
|
|
571
|
-
content: 'Discover best practices for improving performance...',
|
|
572
|
-
published: true,
|
|
573
|
-
userId: allUsers[3].id,
|
|
574
|
-
},
|
|
575
|
-
{
|
|
576
|
-
title: 'Introduction to Prisma ORM',
|
|
577
|
-
content: 'Prisma is a modern ORM that simplifies interactions with the database...',
|
|
578
|
-
published: true,
|
|
579
|
-
userId: allUsers[4].id,
|
|
580
|
-
},
|
|
581
|
-
{
|
|
582
|
-
title: 'Understanding Clean Architecture',
|
|
583
|
-
content: 'Clean Architecture helps separate business logic from the rest of the code...',
|
|
584
|
-
published: false,
|
|
585
|
-
userId: allUsers[0].id,
|
|
586
|
-
},
|
|
587
|
-
];
|
|
588
|
-
|
|
589
|
-
const posts = await postRepository.save(postsData);
|
|
590
|
-
console.log(\`📝 \${posts.length} articles created.\`);
|
|
591
|
-
|
|
592
|
-
// --- 4. DEMO COMMENTS ---
|
|
593
|
-
const commentsData = [
|
|
594
|
-
{
|
|
595
|
-
content: 'Excellent article! I was able to apply these tips directly to my NestJS project.',
|
|
596
|
-
post: posts[0],
|
|
597
|
-
userId: allUsers[2].id,
|
|
598
|
-
},
|
|
599
|
-
{
|
|
600
|
-
content: 'Very clear and well explained, thanks for sharing about Prisma 👏',
|
|
601
|
-
post: posts[3],
|
|
602
|
-
userId: allUsers[0].id,
|
|
603
|
-
},
|
|
604
|
-
{
|
|
605
|
-
content: "I didn't know about JWT before this article, it's a real revelation.",
|
|
606
|
-
post: posts[1],
|
|
607
|
-
userId: allUsers[4].id,
|
|
608
|
-
},
|
|
609
|
-
{
|
|
610
|
-
content: 'Clean Architecture always seemed blurry to me, this article finally enlightened me.',
|
|
611
|
-
post: posts[4],
|
|
612
|
-
userId: allUsers[1].id,
|
|
613
|
-
},
|
|
614
|
-
{
|
|
615
|
-
content: 'Thanks for the content! I would like to see a complete tutorial with NestJS + Prisma.',
|
|
616
|
-
post: posts[2],
|
|
617
|
-
userId: allUsers[3].id,
|
|
618
|
-
},
|
|
619
|
-
];
|
|
620
|
-
|
|
621
|
-
const comments = await commentRepository.save(commentsData);
|
|
622
|
-
console.log(\`💬 \${comments.length} comments created.\`);
|
|
623
|
-
|
|
624
|
-
console.log('✅ TypeORM seeding finished successfully! 🚀');
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
`;
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
module.exports = { setupTypeORM };
|
|
1
|
+
// setupTypeORM.js
|
|
2
|
+
// const { execSync } = require("child_process");
|
|
3
|
+
// const path = require("path");
|
|
4
|
+
// const { runCommand } = require("../shell");
|
|
5
|
+
|
|
6
|
+
const {
|
|
7
|
+
updateFile,
|
|
8
|
+
capitalize,
|
|
9
|
+
createFile,
|
|
10
|
+
createDirectory,
|
|
11
|
+
decapitalize,
|
|
12
|
+
} = require("../../userInput");
|
|
13
|
+
const { logInfo } = require("../../loggers/logInfo");
|
|
14
|
+
const { logSuccess } = require("../../loggers/logSuccess");
|
|
15
|
+
const path = require("path");
|
|
16
|
+
const { runCommand } = require("../../shell");
|
|
17
|
+
const { updatePackageJson } = require("../../file-utils/packageJsonUtils");
|
|
18
|
+
const { info } = require("../../colors");
|
|
19
|
+
|
|
20
|
+
async function setupTypeORM(inputs) {
|
|
21
|
+
logInfo("📦 Installing TypeORM and PostgreSQL dependencies...");
|
|
22
|
+
|
|
23
|
+
const mode = inputs.mode;
|
|
24
|
+
await runCommand(
|
|
25
|
+
"npm install @nestjs/typeorm typeorm pg reflect-metadata",
|
|
26
|
+
"TypeORM and PostgreSQL dependencies installed successfully"
|
|
27
|
+
); // Updating app.module.ts with TypeORM
|
|
28
|
+
|
|
29
|
+
const appModulePath = "src/app.module.ts";
|
|
30
|
+
const typeOrmImport = `import { TypeOrmModule } from '@nestjs/typeorm';`;
|
|
31
|
+
const typeOrmConfig = `
|
|
32
|
+
TypeOrmModule.forRoot({
|
|
33
|
+
type: 'postgres',
|
|
34
|
+
host: process.env.POSTGRES_HOST,
|
|
35
|
+
port: process.env.POSTGRES_PORT
|
|
36
|
+
? parseInt(process.env.POSTGRES_PORT, 10)
|
|
37
|
+
: 5432,
|
|
38
|
+
username: process.env.POSTGRES_USER,
|
|
39
|
+
password: process.env.POSTGRES_PASSWORD,
|
|
40
|
+
database: process.env.POSTGRES_DB,
|
|
41
|
+
autoLoadEntities: true, // Reinstated for automatic loading of entities registered in .forFeature
|
|
42
|
+
synchronize: true, // Only for dev use!
|
|
43
|
+
// dropSchema: true, //// ⚠️ wipes the entire schema on every restart! Only for dev use!
|
|
44
|
+
}),`;
|
|
45
|
+
|
|
46
|
+
logInfo("⚙️ Updating app.module.ts with TypeORM..."); // 1. Updating TypeOrmModule.forRoot()
|
|
47
|
+
await updateFile({
|
|
48
|
+
path: appModulePath,
|
|
49
|
+
pattern: `ConfigModule.forRoot({
|
|
50
|
+
isGlobal: true, // Make ConfigModule globally accessible
|
|
51
|
+
envFilePath: '.env', // Load environment variables
|
|
52
|
+
}),`,
|
|
53
|
+
replacement: ` ConfigModule.forRoot({
|
|
54
|
+
isGlobal: true, // Make ConfigModule globally accessible
|
|
55
|
+
envFilePath: '.env', // Load environment variables
|
|
56
|
+
}),
|
|
57
|
+
${typeOrmConfig}`,
|
|
58
|
+
}); // 2. Adding TypeOrmModule import
|
|
59
|
+
|
|
60
|
+
await updateFile({
|
|
61
|
+
path: appModulePath,
|
|
62
|
+
pattern: "import { Module } from '@nestjs/common';",
|
|
63
|
+
replacement: `import { Module } from '@nestjs/common';
|
|
64
|
+
${typeOrmImport}`,
|
|
65
|
+
}); // Entity Generation
|
|
66
|
+
|
|
67
|
+
logInfo("📁 Generating entities for TypeORM...");
|
|
68
|
+
|
|
69
|
+
await createDirectory("src/entities");
|
|
70
|
+
|
|
71
|
+
for (const entity of inputs.entitiesData.entities) {
|
|
72
|
+
const entityName = capitalize(entity.name);
|
|
73
|
+
const entityNameLower = decapitalize(entity.name);
|
|
74
|
+
let fieldsContent = "";
|
|
75
|
+
let relationsContent = "";
|
|
76
|
+
let imports = [
|
|
77
|
+
"Entity",
|
|
78
|
+
"Column",
|
|
79
|
+
"PrimaryGeneratedColumn",
|
|
80
|
+
"CreateDateColumn",
|
|
81
|
+
"UpdateDateColumn",
|
|
82
|
+
];
|
|
83
|
+
let extraImports = ""; // --- Basic Data Field Generation Logic --- // Filtering to avoid duplication of relationship fields
|
|
84
|
+
|
|
85
|
+
const relationFields = inputs.entitiesData.relations
|
|
86
|
+
.reduce((acc, rel) => {
|
|
87
|
+
// Foreign key and relation name (Many side)
|
|
88
|
+
if (
|
|
89
|
+
rel.from.toLowerCase() === entityNameLower &&
|
|
90
|
+
["n-1", "1-1"].includes(rel.type)
|
|
91
|
+
) {
|
|
92
|
+
acc.push(`${rel.to}Id`, rel.to);
|
|
93
|
+
}
|
|
94
|
+
if (
|
|
95
|
+
rel.to.toLowerCase() === entityNameLower &&
|
|
96
|
+
["1-n", "1-1"].includes(rel.type)
|
|
97
|
+
) {
|
|
98
|
+
acc.push(`${rel.from}Id`, rel.from);
|
|
99
|
+
} // List name (One side)
|
|
100
|
+
if (rel.from.toLowerCase() === entityNameLower && rel.type === "1-n") {
|
|
101
|
+
acc.push(`${rel.to}s`);
|
|
102
|
+
}
|
|
103
|
+
if (rel.to.toLowerCase() === entityNameLower && rel.type === "n-1") {
|
|
104
|
+
acc.push(`${rel.from}s`);
|
|
105
|
+
} // For 1-1
|
|
106
|
+
if (rel.type === "1-1") {
|
|
107
|
+
// We manage both sides, ensure only one has the foreign key
|
|
108
|
+
// We just exclude model names that will become relations
|
|
109
|
+
acc.push(rel.from, rel.to);
|
|
110
|
+
}
|
|
111
|
+
return acc;
|
|
112
|
+
}, [])
|
|
113
|
+
.map((f) => f.toLowerCase());
|
|
114
|
+
|
|
115
|
+
const isUserEntity = entity.name.toLowerCase() === "user";
|
|
116
|
+
const hasRoleField = entity.fields.some((f) => f.name === "role");
|
|
117
|
+
|
|
118
|
+
// Utilisation d'un Set pour éviter les doublons d'imports
|
|
119
|
+
const enumImports = new Set();
|
|
120
|
+
|
|
121
|
+
if (isUserEntity && hasRoleField) {
|
|
122
|
+
const rolePath =
|
|
123
|
+
mode === "full"
|
|
124
|
+
? "src/user/domain/enums/role.enum"
|
|
125
|
+
: "src/common/enums/role.enum";
|
|
126
|
+
enumImports.add(`import { Role } from '${rolePath}';`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Liste des colonnes déjà présentes dans le template de classe
|
|
130
|
+
const RESERVED_FIELDS = ["id", "createdat", "updatedat"];
|
|
131
|
+
|
|
132
|
+
for (const field of entity.fields) {
|
|
133
|
+
const fieldNameLower = field.name.toLowerCase();
|
|
134
|
+
|
|
135
|
+
// 1. Skip si le champ est géré par une relation
|
|
136
|
+
if (relationFields.includes(fieldNameLower)) continue;
|
|
137
|
+
|
|
138
|
+
// 2. Skip si le champ est déjà présent par défaut (id, createdAt, updatedAt)
|
|
139
|
+
if (RESERVED_FIELDS.includes(fieldNameLower)) {
|
|
140
|
+
console.log(
|
|
141
|
+
`${info("[INFO]")} Skipping default field: ${
|
|
142
|
+
field.name
|
|
143
|
+
} for entity ${entityName}`
|
|
144
|
+
);
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const mapping = mapTypeToTypeORM(field.type);
|
|
149
|
+
const columnOptions = [`type: '${mapping.type}'`];
|
|
150
|
+
|
|
151
|
+
// 1. Gestion des Options spécifiques (Enum, JSON, Array)
|
|
152
|
+
if (mapping.type === "enum") {
|
|
153
|
+
columnOptions.push(`enum: ${mapping.tsType}`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ✅ Default role = USER pour User.role
|
|
157
|
+
if (
|
|
158
|
+
isUserEntity &&
|
|
159
|
+
field.name.toLowerCase() === "role" &&
|
|
160
|
+
mapping.tsType === "Role"
|
|
161
|
+
) {
|
|
162
|
+
columnOptions.push("default: Role.USER");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (field.optional) {
|
|
166
|
+
columnOptions.push("nullable: true");
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (field.name.toLowerCase() === "email") {
|
|
170
|
+
columnOptions.push("unique: true");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// 2. Gestion des Imports d'Enums Custom
|
|
174
|
+
const isEnum = mapping.type === "enum";
|
|
175
|
+
const isNotRole = mapping.tsType.toLowerCase() !== "role";
|
|
176
|
+
|
|
177
|
+
if (isEnum && isNotRole) {
|
|
178
|
+
// On stocke l'import dans le Set
|
|
179
|
+
enumImports.add(
|
|
180
|
+
`import { ${
|
|
181
|
+
mapping.tsType
|
|
182
|
+
} } from '../shared/enums/${mapping.tsType.toLowerCase()}.enum';`
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// 3. Construction du champ
|
|
187
|
+
fieldsContent += `
|
|
188
|
+
@Column({ ${columnOptions.join(", ")} })
|
|
189
|
+
${field.name}${field.optional ? "?" : ""}: ${mapping.tsType};
|
|
190
|
+
`;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// On ajoute tous les imports uniques au début du fichier
|
|
194
|
+
extraImports += Array.from(enumImports).join("\n") + "\n";
|
|
195
|
+
|
|
196
|
+
// --- TypeORM Relation Generation Logic ---
|
|
197
|
+
for (const relation of inputs.entitiesData.relations) {
|
|
198
|
+
const relFrom = relation.from;
|
|
199
|
+
const relTo = relation.to;
|
|
200
|
+
const relType = relation.type;
|
|
201
|
+
|
|
202
|
+
if (relType === "1-n") {
|
|
203
|
+
// If the current entity is on the 'One' side (relFrom), it holds the @OneToMany
|
|
204
|
+
if (relFrom.toLowerCase() === entityNameLower) {
|
|
205
|
+
const targetEntity = capitalize(relTo);
|
|
206
|
+
relationsContent += `
|
|
207
|
+
@OneToMany(() => ${targetEntity}, (${decapitalize(
|
|
208
|
+
relTo
|
|
209
|
+
)}) => ${decapitalize(relTo)}.${relFrom.toLowerCase()})
|
|
210
|
+
${relTo.toLowerCase()}s: ${targetEntity}[];
|
|
211
|
+
`;
|
|
212
|
+
imports.push("OneToMany");
|
|
213
|
+
extraImports += `\nimport { ${targetEntity} } from './${targetEntity}.entity';`;
|
|
214
|
+
} // If the current entity is on the 'Many' side (relTo), it holds the @ManyToOne
|
|
215
|
+
else if (relTo.toLowerCase() === entityNameLower) {
|
|
216
|
+
const targetEntity = capitalize(relFrom);
|
|
217
|
+
const fkName = `${relFrom.toLowerCase()}Id`;
|
|
218
|
+
relationsContent += `
|
|
219
|
+
@Column({ type: 'uuid' }) // Foreign Key
|
|
220
|
+
${fkName}: string;
|
|
221
|
+
|
|
222
|
+
@ManyToOne(() => ${targetEntity}, (${decapitalize(
|
|
223
|
+
relFrom
|
|
224
|
+
)}) => ${decapitalize(relFrom)}.${relTo.toLowerCase()}s)
|
|
225
|
+
${relFrom.toLowerCase()}: ${targetEntity};
|
|
226
|
+
`;
|
|
227
|
+
imports.push("ManyToOne");
|
|
228
|
+
extraImports += `\nimport { ${targetEntity} } from './${targetEntity}.entity';`;
|
|
229
|
+
}
|
|
230
|
+
} else if (relType === "n-1") {
|
|
231
|
+
// n-1 is the inverse: relFrom is the 'Many', relTo is the 'One'
|
|
232
|
+
|
|
233
|
+
// If the current entity is on the 'Many' side (relFrom), it holds the @ManyToOne
|
|
234
|
+
if (relFrom.toLowerCase() === entityNameLower) {
|
|
235
|
+
const targetEntity = capitalize(relTo);
|
|
236
|
+
const fkName = `${relTo.toLowerCase()}Id`;
|
|
237
|
+
relationsContent += `
|
|
238
|
+
@Column({ type: 'uuid' }) // Foreign Key
|
|
239
|
+
${fkName}: string;
|
|
240
|
+
|
|
241
|
+
@ManyToOne(() => ${targetEntity}, (${decapitalize(
|
|
242
|
+
relTo
|
|
243
|
+
)}) => ${decapitalize(relTo)}.${relFrom.toLowerCase()}s)
|
|
244
|
+
${relTo.toLowerCase()}: ${targetEntity};
|
|
245
|
+
`;
|
|
246
|
+
imports.push("ManyToOne");
|
|
247
|
+
extraImports += `\nimport { ${targetEntity} } from './${targetEntity}.entity';`;
|
|
248
|
+
} // If the current entity is on the 'One' side (relTo), it holds the @OneToMany
|
|
249
|
+
else if (relTo.toLowerCase() === entityNameLower) {
|
|
250
|
+
const targetEntity = capitalize(relFrom);
|
|
251
|
+
relationsContent += `
|
|
252
|
+
@OneToMany(() => ${targetEntity}, (${decapitalize(
|
|
253
|
+
relFrom
|
|
254
|
+
)}) => ${decapitalize(relFrom)}.${relTo.toLowerCase()})
|
|
255
|
+
${relFrom.toLowerCase()}s: ${targetEntity}[];
|
|
256
|
+
`;
|
|
257
|
+
imports.push("OneToMany");
|
|
258
|
+
extraImports += `\nimport { ${targetEntity} } from './${targetEntity}.entity';`;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
if (relType === "1-1") {
|
|
262
|
+
// If the current entity is on the 'from' side, it holds the @OneToOne and the foreign key
|
|
263
|
+
if (relFrom.toLowerCase() === entityNameLower) {
|
|
264
|
+
const targetEntity = capitalize(relTo);
|
|
265
|
+
const fkName = `${relTo.toLowerCase()}Id`;
|
|
266
|
+
|
|
267
|
+
relationsContent += `
|
|
268
|
+
@Column({ type: 'uuid', unique: true }) // Unique foreign key for 1-1
|
|
269
|
+
${fkName}: string;
|
|
270
|
+
|
|
271
|
+
@OneToOne(() => ${targetEntity}, (${decapitalize(relTo)}) => ${decapitalize(
|
|
272
|
+
relTo
|
|
273
|
+
)}.${relFrom.toLowerCase()})
|
|
274
|
+
@JoinColumn({ name: '${fkName}' }) // Requires JoinColumn for the foreign key
|
|
275
|
+
${relTo.toLowerCase()}: ${targetEntity};
|
|
276
|
+
`;
|
|
277
|
+
imports.push("OneToOne", "JoinColumn");
|
|
278
|
+
extraImports += `\nimport { ${targetEntity} } from './${targetEntity}.entity';`;
|
|
279
|
+
} // If the current entity is on the 'to' side, it holds the inverse @OneToOne relationship (mapping)
|
|
280
|
+
else if (relTo.toLowerCase() === entityNameLower) {
|
|
281
|
+
const targetEntity = capitalize(relFrom);
|
|
282
|
+
relationsContent += `
|
|
283
|
+
@OneToOne(() => ${targetEntity}, (${decapitalize(
|
|
284
|
+
relFrom
|
|
285
|
+
)}) => ${decapitalize(relFrom)}.${relTo.toLowerCase()})
|
|
286
|
+
${relFrom.toLowerCase()}: ${targetEntity};
|
|
287
|
+
`;
|
|
288
|
+
imports.push("OneToOne");
|
|
289
|
+
extraImports += `\nimport { ${targetEntity} } from './${targetEntity}.entity';`;
|
|
290
|
+
}
|
|
291
|
+
} else if (relType === "n-n") {
|
|
292
|
+
const targetEntity = capitalize(
|
|
293
|
+
relFrom.toLowerCase() === entityNameLower ? relTo : relFrom
|
|
294
|
+
);
|
|
295
|
+
const currentSide =
|
|
296
|
+
relFrom.toLowerCase() === entityNameLower ? relTo : relFrom;
|
|
297
|
+
const otherSide =
|
|
298
|
+
relFrom.toLowerCase() === entityNameLower ? relFrom : relTo; // TypeORM requires JoinTable on one side (we choose the 'from' side for simplicity)
|
|
299
|
+
|
|
300
|
+
if (relFrom.toLowerCase() === entityNameLower) {
|
|
301
|
+
relationsContent += `
|
|
302
|
+
@ManyToMany(() => ${targetEntity}, (${decapitalize(
|
|
303
|
+
currentSide
|
|
304
|
+
)}) => ${decapitalize(currentSide)}.${otherSide.toLowerCase()}s)
|
|
305
|
+
@JoinTable() // Adding JoinTable to create the junction table
|
|
306
|
+
${currentSide.toLowerCase()}s: ${targetEntity}[];
|
|
307
|
+
`;
|
|
308
|
+
imports.push("ManyToMany", "JoinTable");
|
|
309
|
+
} else {
|
|
310
|
+
relationsContent += `
|
|
311
|
+
@ManyToMany(() => ${targetEntity}, (${decapitalize(
|
|
312
|
+
currentSide
|
|
313
|
+
)}) => ${decapitalize(currentSide)}.${otherSide.toLowerCase()}s)
|
|
314
|
+
${currentSide.toLowerCase()}s: ${targetEntity}[];
|
|
315
|
+
`;
|
|
316
|
+
imports.push("ManyToMany");
|
|
317
|
+
}
|
|
318
|
+
extraImports += `\nimport { ${targetEntity} } from './${targetEntity}.entity';`;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// --- Final File Generation ---
|
|
323
|
+
const uniqueImports = Array.from(new Set(imports)).join(", ");
|
|
324
|
+
|
|
325
|
+
const content = `${extraImports}
|
|
326
|
+
import { ${uniqueImports} } from 'typeorm';
|
|
327
|
+
|
|
328
|
+
@Entity('${entityNameLower}')
|
|
329
|
+
export class ${entityName} {
|
|
330
|
+
@PrimaryGeneratedColumn('uuid')
|
|
331
|
+
id: string;
|
|
332
|
+
|
|
333
|
+
@CreateDateColumn()
|
|
334
|
+
createdAt: Date;
|
|
335
|
+
|
|
336
|
+
@UpdateDateColumn()
|
|
337
|
+
updatedAt: Date;
|
|
338
|
+
|
|
339
|
+
${fieldsContent}
|
|
340
|
+
${relationsContent}
|
|
341
|
+
}`;
|
|
342
|
+
|
|
343
|
+
await createFile({
|
|
344
|
+
path: `src/entities/${entityName}.entity.ts`,
|
|
345
|
+
contente: content,
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (inputs.isDemo) {
|
|
350
|
+
await setupTypeOrmSeeding(inputs);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
logSuccess("✅ TypeORM configuration complete. Ready to code!");
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Maps entity types to TypeORM column types (PostgreSQL).
|
|
358
|
+
* @param type The input type (string, number, boolean, Date, enum, array, etc.)
|
|
359
|
+
* @returns { object: type, tsType: string } The mapping information
|
|
360
|
+
*/
|
|
361
|
+
function mapTypeToTypeORM(type) {
|
|
362
|
+
// Special case for arrays (e.g., 'string[]')
|
|
363
|
+
if (type.endsWith("[]")) {
|
|
364
|
+
const innerType = type.slice(0, -2);
|
|
365
|
+
const innerMapping = mapTypeToTypeORM(innerType); // Recursive
|
|
366
|
+
return {
|
|
367
|
+
type: innerMapping.type,
|
|
368
|
+
tsType: `${innerMapping.tsType}[]`,
|
|
369
|
+
options: "array: true", // TypeORM option for arrays
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const typeLower = type.toLowerCase();
|
|
374
|
+
|
|
375
|
+
switch (typeLower) {
|
|
376
|
+
case "string":
|
|
377
|
+
return { type: "varchar", tsType: "string" };
|
|
378
|
+
case "text":
|
|
379
|
+
return { type: "text", tsType: "string" };
|
|
380
|
+
case "number":
|
|
381
|
+
case "float":
|
|
382
|
+
return { type: "float", tsType: "number" };
|
|
383
|
+
case "int":
|
|
384
|
+
return { type: "integer", tsType: "number" };
|
|
385
|
+
case "decimal":
|
|
386
|
+
return { type: "decimal", tsType: "number" };
|
|
387
|
+
case "uuid":
|
|
388
|
+
return { type: "uuid", tsType: "string" };
|
|
389
|
+
case "date":
|
|
390
|
+
return { type: "timestamp", tsType: "Date" };
|
|
391
|
+
case "boolean":
|
|
392
|
+
return { type: "boolean", tsType: "boolean" };
|
|
393
|
+
case "json":
|
|
394
|
+
case "object":
|
|
395
|
+
return { type: "jsonb", tsType: "any" }; // JSONB type is flexible
|
|
396
|
+
|
|
397
|
+
default: // Case of an Enum type or a named object (e.g., 'Address')
|
|
398
|
+
// By default, we assume it's an enum to be stored in the database (varchar)
|
|
399
|
+
if (type.charAt(0) === type.charAt(0).toUpperCase()) {
|
|
400
|
+
// For Enums, TypeORM needs the 'enum' and 'enumName' option
|
|
401
|
+
return {
|
|
402
|
+
type: "enum",
|
|
403
|
+
tsType: type, // The name of the Enum/object in TypeScript
|
|
404
|
+
options: `enum: ${type}, enumName: '${type.toLowerCase()}_enum'`, // Example of enum options
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
return { type: "varchar", tsType: "string" };
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
async function setupTypeOrmSeeding(inputs) {
|
|
412
|
+
logInfo("⚙️ Configuring seeding for TypeORM...");
|
|
413
|
+
|
|
414
|
+
// --- Dependencies ---
|
|
415
|
+
const typeOrmDevDeps = [
|
|
416
|
+
"ts-node",
|
|
417
|
+
"@types/node",
|
|
418
|
+
"@types/bcrypt",
|
|
419
|
+
"typeorm-extension",
|
|
420
|
+
];
|
|
421
|
+
|
|
422
|
+
await runCommand(
|
|
423
|
+
`${inputs.packageManager} add -D ${typeOrmDevDeps.join(" ")}`,
|
|
424
|
+
"❌ Failed to install TypeORM seeding dependencies"
|
|
425
|
+
);
|
|
426
|
+
|
|
427
|
+
await runCommand(
|
|
428
|
+
`${inputs.packageManager} install bcrypt`,
|
|
429
|
+
"❌ Failed to install bcrypt"
|
|
430
|
+
);
|
|
431
|
+
|
|
432
|
+
// --- Scripts in package.json ---
|
|
433
|
+
const typeOrmScripts = {
|
|
434
|
+
"typeorm:migrate:run":
|
|
435
|
+
"typeorm-ts-node-commonjs migration:run -d ./src/database/typeorm.config.ts",
|
|
436
|
+
"typeorm:seed":
|
|
437
|
+
"typeorm-extension seed:run -d src/database/typeorm.config.ts",
|
|
438
|
+
seed: "npm run typeorm:seed",
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
await updatePackageJson(inputs, typeOrmScripts);
|
|
442
|
+
|
|
443
|
+
// --- Creating structure and Seeder ---
|
|
444
|
+
await createDirectory("src/database/seeders");
|
|
445
|
+
|
|
446
|
+
const userSeederContent = generateTypeOrmSeederContent();
|
|
447
|
+
await createFile({
|
|
448
|
+
path: `src/database/seeders/DemoSeeder.ts`,
|
|
449
|
+
contente: userSeederContent,
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
await createFile({
|
|
453
|
+
path: `src/database/typeorm.config.ts`,
|
|
454
|
+
contente: `import * as dotenv from 'dotenv';
|
|
455
|
+
dotenv.config();
|
|
456
|
+
import { DataSource } from 'typeorm';
|
|
457
|
+
import { DataSourceOptions } from 'typeorm';
|
|
458
|
+
import { SeederOptions } from 'typeorm-extension';
|
|
459
|
+
import { DemoSeeder } from './seeders/DemoSeeder';
|
|
460
|
+
import { User } from 'src/entities/User.entity';
|
|
461
|
+
import { Post } from 'src/entities/Post.entity';
|
|
462
|
+
import { Comment } from 'src/entities/Comment.entity';
|
|
463
|
+
import { Session } from 'src/entities/Session.entity';
|
|
464
|
+
|
|
465
|
+
const config: DataSourceOptions & SeederOptions = {
|
|
466
|
+
type: 'postgres',
|
|
467
|
+
host: process.env.POSTGRES_HOST,
|
|
468
|
+
port: Number(process.env.POSTGRES_PORT ?? 5432),
|
|
469
|
+
username: process.env.POSTGRES_USER,
|
|
470
|
+
password: process.env.POSTGRES_PASSWORD,
|
|
471
|
+
database: process.env.POSTGRES_DB,
|
|
472
|
+
|
|
473
|
+
entities: [User, Post, Comment, Session],
|
|
474
|
+
|
|
475
|
+
synchronize: process.env.NODE_ENV !== 'production',
|
|
476
|
+
logging: process.env.NODE_ENV === 'development',
|
|
477
|
+
|
|
478
|
+
seeds: [DemoSeeder],
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
export const AppDataSource = new DataSource(config);
|
|
482
|
+
`,
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
logSuccess("✅ TypeORM seeding configured.");
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function generateTypeOrmSeederContent() {
|
|
489
|
+
return `
|
|
490
|
+
import { DataSource } from 'typeorm';
|
|
491
|
+
import { User } from 'src/entities/User.entity';
|
|
492
|
+
import { Post } from 'src/entities/Post.entity';
|
|
493
|
+
import { Comment } from 'src/entities/Comment.entity';
|
|
494
|
+
import * as bcrypt from 'bcrypt';
|
|
495
|
+
|
|
496
|
+
export class DemoSeeder {
|
|
497
|
+
constructor() {}
|
|
498
|
+
|
|
499
|
+
async run(dataSource: DataSource) {
|
|
500
|
+
console.log('🌱 Starting TypeORM seeding...');
|
|
501
|
+
|
|
502
|
+
const userRepository = dataSource.getRepository(User);
|
|
503
|
+
const postRepository = dataSource.getRepository(Post);
|
|
504
|
+
const commentRepository = dataSource.getRepository(Comment);
|
|
505
|
+
|
|
506
|
+
// --- 1. ADMIN ---
|
|
507
|
+
const salt = await bcrypt.genSalt(10);
|
|
508
|
+
const hashedPassword = await bcrypt.hash('password123', salt);
|
|
509
|
+
|
|
510
|
+
const admin = userRepository.create({
|
|
511
|
+
email: 'admin@nestcraft.com',
|
|
512
|
+
password: hashedPassword,
|
|
513
|
+
username: 'NestCraftAdmin',
|
|
514
|
+
isActive: true,
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
const exists = await userRepository.findOneBy({
|
|
518
|
+
email: admin.email,
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
if (!exists) {
|
|
522
|
+
await userRepository.save(admin);
|
|
523
|
+
console.log('👑 Admin user created');
|
|
524
|
+
} else {
|
|
525
|
+
admin.id = exists.id;
|
|
526
|
+
console.log('👑 Admin user realy exists');
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// --- 2. DEMO USERS ---
|
|
530
|
+
const demoUsersData = [
|
|
531
|
+
{ email: 'emma.jones@demo.com', password: hashedPassword, username: 'EmmaJones', isActive: true },
|
|
532
|
+
{ email: 'lucas.martin@demo.com', password: hashedPassword, username: 'LucasMartin', isActive: true },
|
|
533
|
+
{ email: 'sophia.bernard@demo.com', password: hashedPassword, username: 'SophiaBernard', isActive: true },
|
|
534
|
+
{ email: 'alexandre.dubois@demo.com', password: hashedPassword, username: 'AlexandreDubois', isActive: true },
|
|
535
|
+
{ email: 'chloe.moreau@demo.com', password: hashedPassword, username: 'ChloeMoreau', isActive: true },
|
|
536
|
+
];
|
|
537
|
+
|
|
538
|
+
const users: User[] = [];
|
|
539
|
+
|
|
540
|
+
for (const userData of demoUsersData) {
|
|
541
|
+
let user = await userRepository.findOneBy({ email: userData.email });
|
|
542
|
+
|
|
543
|
+
if (!user) {
|
|
544
|
+
user = userRepository.create(userData);
|
|
545
|
+
user = await userRepository.save(user);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
users.push(user);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
console.log(\`👥 \${users.length} demo users created.\`);
|
|
552
|
+
|
|
553
|
+
const allUsers = [admin, ...users];
|
|
554
|
+
|
|
555
|
+
// --- 3. DEMO POSTS ---
|
|
556
|
+
const postsData = [
|
|
557
|
+
{
|
|
558
|
+
title: 'The Basics of NestJS for Modern Developers',
|
|
559
|
+
content: 'Discover how to build a robust and maintainable API with NestJS...',
|
|
560
|
+
published: true,
|
|
561
|
+
userId: allUsers[1].id,
|
|
562
|
+
},
|
|
563
|
+
{
|
|
564
|
+
title: 'How to Secure Your API with JWT',
|
|
565
|
+
content: 'JWT authentication is a standard for securing APIs...',
|
|
566
|
+
published: true,
|
|
567
|
+
userId: allUsers[2].id,
|
|
568
|
+
},
|
|
569
|
+
{
|
|
570
|
+
title: 'Optimizing Node.js API Performance',
|
|
571
|
+
content: 'Discover best practices for improving performance...',
|
|
572
|
+
published: true,
|
|
573
|
+
userId: allUsers[3].id,
|
|
574
|
+
},
|
|
575
|
+
{
|
|
576
|
+
title: 'Introduction to Prisma ORM',
|
|
577
|
+
content: 'Prisma is a modern ORM that simplifies interactions with the database...',
|
|
578
|
+
published: true,
|
|
579
|
+
userId: allUsers[4].id,
|
|
580
|
+
},
|
|
581
|
+
{
|
|
582
|
+
title: 'Understanding Clean Architecture',
|
|
583
|
+
content: 'Clean Architecture helps separate business logic from the rest of the code...',
|
|
584
|
+
published: false,
|
|
585
|
+
userId: allUsers[0].id,
|
|
586
|
+
},
|
|
587
|
+
];
|
|
588
|
+
|
|
589
|
+
const posts = await postRepository.save(postsData);
|
|
590
|
+
console.log(\`📝 \${posts.length} articles created.\`);
|
|
591
|
+
|
|
592
|
+
// --- 4. DEMO COMMENTS ---
|
|
593
|
+
const commentsData = [
|
|
594
|
+
{
|
|
595
|
+
content: 'Excellent article! I was able to apply these tips directly to my NestJS project.',
|
|
596
|
+
post: posts[0],
|
|
597
|
+
userId: allUsers[2].id,
|
|
598
|
+
},
|
|
599
|
+
{
|
|
600
|
+
content: 'Very clear and well explained, thanks for sharing about Prisma 👏',
|
|
601
|
+
post: posts[3],
|
|
602
|
+
userId: allUsers[0].id,
|
|
603
|
+
},
|
|
604
|
+
{
|
|
605
|
+
content: "I didn't know about JWT before this article, it's a real revelation.",
|
|
606
|
+
post: posts[1],
|
|
607
|
+
userId: allUsers[4].id,
|
|
608
|
+
},
|
|
609
|
+
{
|
|
610
|
+
content: 'Clean Architecture always seemed blurry to me, this article finally enlightened me.',
|
|
611
|
+
post: posts[4],
|
|
612
|
+
userId: allUsers[1].id,
|
|
613
|
+
},
|
|
614
|
+
{
|
|
615
|
+
content: 'Thanks for the content! I would like to see a complete tutorial with NestJS + Prisma.',
|
|
616
|
+
post: posts[2],
|
|
617
|
+
userId: allUsers[3].id,
|
|
618
|
+
},
|
|
619
|
+
];
|
|
620
|
+
|
|
621
|
+
const comments = await commentRepository.save(commentsData);
|
|
622
|
+
console.log(\`💬 \${comments.length} comments created.\`);
|
|
623
|
+
|
|
624
|
+
console.log('✅ TypeORM seeding finished successfully! 🚀');
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
`;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
module.exports = { setupTypeORM };
|