nestgen-cli 0.0.7 → 0.2.1
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/engine/architecture-profile.js +10 -0
- package/dist/engine/file-transaction.js +166 -0
- package/dist/engine/generated-code.js +26 -0
- package/dist/engine/generation-manifest.js +118 -0
- package/dist/engine/migration-plan.js +78 -0
- package/dist/engine/module-generator.js +157 -0
- package/dist/engine/project-config.js +42 -0
- package/dist/engine/project-path.js +25 -0
- package/dist/engine/project-preflight.js +96 -0
- package/dist/engine/resource-generator.js +1153 -0
- package/dist/engine/resource-spec.js +287 -0
- package/examples/store-api/README.md +76 -0
- package/examples/store-api/definitions/customers.resource.json +14 -0
- package/examples/store-api/definitions/order-items.resource.json +26 -0
- package/examples/store-api/definitions/orders.resource.json +22 -0
- package/examples/store-api/definitions/products.resource.json +19 -0
- package/examples/store-api/nestgen.example.json +7 -0
- package/nestgen.js +700 -86
- package/nestjs-generator/features/add_module.sh +68 -24
- package/nestjs-generator/features/configure_bootstrap.mjs +42 -0
- package/nestjs-generator/features/docker.sh +169 -36
- package/nestjs-generator/features/git.sh +4 -2
- package/nestjs-generator/features/inject_module_to_app.sh +22 -69
- package/nestjs-generator/features/operational_foundation.mjs +210 -0
- package/nestjs-generator/features/operations.sh +16 -0
- package/nestjs-generator/features/preflight.mjs +13 -0
- package/nestjs-generator/features/prisma.sh +48 -8
- package/nestjs-generator/features/resource_name.mjs +57 -0
- package/nestjs-generator/features/swagger.sh +7 -20
- package/nestjs-generator/features/typeorm.sh +68 -7
- package/nestjs-generator/features/update_app_module.mjs +146 -0
- package/nestjs-generator/features/utils.sh +52 -14
- package/nestjs-generator/generate_project.sh +46 -70
- package/package.json +63 -31
- package/readme.md +298 -120
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
const [sourceDirectory, orm] = process.argv.slice(2);
|
|
5
|
+
|
|
6
|
+
if (!sourceDirectory || !['typeorm', 'prisma'].includes(orm)) {
|
|
7
|
+
console.error('❌ Usage : operational_foundation.mjs <source-directory> <typeorm|prisma>');
|
|
8
|
+
process.exitCode = 1;
|
|
9
|
+
} else {
|
|
10
|
+
const operationsDirectory = path.join(sourceDirectory, 'operations');
|
|
11
|
+
const appModule = path.join(sourceDirectory, 'app.module.ts');
|
|
12
|
+
|
|
13
|
+
if (!fs.existsSync(appModule)) {
|
|
14
|
+
console.error(`❌ AppModule introuvable : ${appModule}`);
|
|
15
|
+
process.exitCode = 1;
|
|
16
|
+
} else if (fs.existsSync(operationsDirectory)) {
|
|
17
|
+
console.error(`❌ ${operationsDirectory} existe déjà. Le socle d’exploitation n’a pas été remplacé.`);
|
|
18
|
+
process.exitCode = 1;
|
|
19
|
+
} else {
|
|
20
|
+
const databaseHealth =
|
|
21
|
+
orm === 'typeorm'
|
|
22
|
+
? `import { Injectable } from '@nestjs/common';
|
|
23
|
+
import { InjectDataSource } from '@nestjs/typeorm';
|
|
24
|
+
import type { DataSource } from 'typeorm';
|
|
25
|
+
|
|
26
|
+
@Injectable()
|
|
27
|
+
export class DatabaseHealthService {
|
|
28
|
+
constructor(@InjectDataSource() private readonly dataSource: DataSource) {}
|
|
29
|
+
|
|
30
|
+
async check(): Promise<{ status: 'up' | 'down' }> {
|
|
31
|
+
try {
|
|
32
|
+
if (!this.dataSource.isInitialized) return { status: 'down' };
|
|
33
|
+
await this.dataSource.query('SELECT 1');
|
|
34
|
+
return { status: 'up' };
|
|
35
|
+
} catch {
|
|
36
|
+
return { status: 'down' };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
`
|
|
41
|
+
: `import { Injectable } from '@nestjs/common';
|
|
42
|
+
import { PrismaService } from '../prisma/prisma.service';
|
|
43
|
+
|
|
44
|
+
@Injectable()
|
|
45
|
+
export class DatabaseHealthService {
|
|
46
|
+
constructor(private readonly prisma: PrismaService) {}
|
|
47
|
+
|
|
48
|
+
async check(): Promise<{ status: 'up' | 'down' }> {
|
|
49
|
+
try {
|
|
50
|
+
await this.prisma.$queryRawUnsafe('SELECT 1');
|
|
51
|
+
return { status: 'up' };
|
|
52
|
+
} catch {
|
|
53
|
+
return { status: 'down' };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
`;
|
|
58
|
+
|
|
59
|
+
const files = new Map([
|
|
60
|
+
[
|
|
61
|
+
'request-context.ts',
|
|
62
|
+
`import { AsyncLocalStorage } from 'node:async_hooks';
|
|
63
|
+
|
|
64
|
+
export const requestContext = new AsyncLocalStorage<{ requestId: string }>();
|
|
65
|
+
`,
|
|
66
|
+
],
|
|
67
|
+
[
|
|
68
|
+
'request-id.middleware.ts',
|
|
69
|
+
`import { Injectable, type NestMiddleware } from '@nestjs/common';
|
|
70
|
+
import { randomUUID } from 'node:crypto';
|
|
71
|
+
import type { NextFunction, Request, Response } from 'express';
|
|
72
|
+
import { requestContext } from './request-context';
|
|
73
|
+
|
|
74
|
+
const REQUEST_ID = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$/;
|
|
75
|
+
|
|
76
|
+
@Injectable()
|
|
77
|
+
export class RequestIdMiddleware implements NestMiddleware {
|
|
78
|
+
use(request: Request, response: Response, next: NextFunction): void {
|
|
79
|
+
const candidate = request.header('x-request-id');
|
|
80
|
+
const requestId = candidate && REQUEST_ID.test(candidate) ? candidate : randomUUID();
|
|
81
|
+
response.setHeader('x-request-id', requestId);
|
|
82
|
+
requestContext.run({ requestId }, next);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
`,
|
|
86
|
+
],
|
|
87
|
+
[
|
|
88
|
+
'structured-logger.service.ts',
|
|
89
|
+
`import { Injectable, type LoggerService } from '@nestjs/common';
|
|
90
|
+
import { requestContext } from './request-context';
|
|
91
|
+
|
|
92
|
+
const SENSITIVE_KEY = /password|secret|token|authorization|cookie|api[-_]?key/i;
|
|
93
|
+
|
|
94
|
+
export function redact(value: unknown): unknown {
|
|
95
|
+
if (Array.isArray(value)) return value.map(redact);
|
|
96
|
+
if (!value || typeof value !== 'object') return value;
|
|
97
|
+
if (value instanceof Error) return { name: value.name, message: value.message };
|
|
98
|
+
return Object.fromEntries(
|
|
99
|
+
Object.entries(value).map(([key, nested]) => [key, SENSITIVE_KEY.test(key) ? '[REDACTED]' : redact(nested)]),
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
@Injectable()
|
|
104
|
+
export class StructuredLogger implements LoggerService {
|
|
105
|
+
log(message: unknown, ...optional: unknown[]): void {
|
|
106
|
+
this.write('log', message, optional);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
error(message: unknown, ...optional: unknown[]): void {
|
|
110
|
+
this.write('error', message, optional);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
warn(message: unknown, ...optional: unknown[]): void {
|
|
114
|
+
this.write('warn', message, optional);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
debug(message: unknown, ...optional: unknown[]): void {
|
|
118
|
+
this.write('debug', message, optional);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
verbose(message: unknown, ...optional: unknown[]): void {
|
|
122
|
+
this.write('verbose', message, optional);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private write(level: string, message: unknown, optional: unknown[]): void {
|
|
126
|
+
const context = requestContext.getStore();
|
|
127
|
+
const record = {
|
|
128
|
+
timestamp: new Date().toISOString(),
|
|
129
|
+
level,
|
|
130
|
+
message: typeof message === 'string' ? message : redact(message),
|
|
131
|
+
...(context ? { requestId: context.requestId } : {}),
|
|
132
|
+
...(optional.length > 0 ? { details: redact(optional) } : {}),
|
|
133
|
+
};
|
|
134
|
+
process[level === 'error' ? 'stderr' : 'stdout'].write(JSON.stringify(record) + '\\n');
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
`,
|
|
138
|
+
],
|
|
139
|
+
['database-health.service.ts', databaseHealth],
|
|
140
|
+
[
|
|
141
|
+
'health.controller.ts',
|
|
142
|
+
`import { Controller, Get, ServiceUnavailableException } from '@nestjs/common';
|
|
143
|
+
import { DatabaseHealthService } from './database-health.service';
|
|
144
|
+
|
|
145
|
+
@Controller('health')
|
|
146
|
+
export class HealthController {
|
|
147
|
+
constructor(private readonly databaseHealth: DatabaseHealthService) {}
|
|
148
|
+
|
|
149
|
+
@Get('live')
|
|
150
|
+
live(): { status: 'ok'; checks: { application: 'up' } } {
|
|
151
|
+
return { status: 'ok', checks: { application: 'up' } };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
@Get('ready')
|
|
155
|
+
async ready(): Promise<{ status: 'ok'; checks: { database: 'up' } }> {
|
|
156
|
+
const database = await this.databaseHealth.check();
|
|
157
|
+
if (database.status === 'down') {
|
|
158
|
+
throw new ServiceUnavailableException({ status: 'error', checks: { database: 'down' } });
|
|
159
|
+
}
|
|
160
|
+
return { status: 'ok', checks: { database: 'up' } };
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
`,
|
|
164
|
+
],
|
|
165
|
+
[
|
|
166
|
+
'operations.module.ts',
|
|
167
|
+
`import { MiddlewareConsumer, Module, type NestModule } from '@nestjs/common';
|
|
168
|
+
${orm === 'prisma' ? "import { PrismaModule } from '../prisma/prisma.module';\n" : ''}import { DatabaseHealthService } from './database-health.service';
|
|
169
|
+
import { HealthController } from './health.controller';
|
|
170
|
+
import { RequestIdMiddleware } from './request-id.middleware';
|
|
171
|
+
import { StructuredLogger } from './structured-logger.service';
|
|
172
|
+
|
|
173
|
+
@Module({
|
|
174
|
+
imports: [${orm === 'prisma' ? 'PrismaModule' : ''}],
|
|
175
|
+
controllers: [HealthController],
|
|
176
|
+
providers: [DatabaseHealthService, RequestIdMiddleware, StructuredLogger],
|
|
177
|
+
exports: [StructuredLogger],
|
|
178
|
+
})
|
|
179
|
+
export class OperationsModule implements NestModule {
|
|
180
|
+
configure(consumer: MiddlewareConsumer): void {
|
|
181
|
+
consumer.apply(RequestIdMiddleware).forRoutes('*');
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
`,
|
|
185
|
+
],
|
|
186
|
+
[
|
|
187
|
+
'README.md',
|
|
188
|
+
`# Socle d’exploitation
|
|
189
|
+
|
|
190
|
+
Cette option est locale au projet et ne requiert aucun service externe. Elle expose :
|
|
191
|
+
|
|
192
|
+
- \`GET /health/live\` : le processus Nest peut répondre ; aucun appel de dépendance n’est fait.
|
|
193
|
+
- \`GET /health/ready\` : le processus et la base PostgreSQL sont prêts. Une base indisponible retourne \`503\` avec \`{ status: 'error', checks: { database: 'down' } }\`.
|
|
194
|
+
|
|
195
|
+
Chaque réponse reçoit \`x-request-id\`. Une valeur cliente sûre est conservée ; sinon NestGen en crée une. Le logger Nest produit un JSON par ligne avec cet identifiant, et masque récursivement les clés contenant \`password\`, \`secret\`, \`token\`, \`authorization\`, \`cookie\` ou \`apiKey\`.
|
|
196
|
+
|
|
197
|
+
\`main.ts\` active \`enableShutdownHooks()\`. Ainsi, \`SIGTERM\` et \`SIGINT\` ferment le contexte Nest ; TypeORM ferme sa connexion et Prisma exécute son \`onModuleDestroy\` existant. Les migrations restent explicites et ne sont jamais appliquées par ces endpoints.
|
|
198
|
+
`,
|
|
199
|
+
],
|
|
200
|
+
]);
|
|
201
|
+
|
|
202
|
+
fs.mkdirSync(operationsDirectory, { recursive: true });
|
|
203
|
+
try {
|
|
204
|
+
for (const [name, content] of files) fs.writeFileSync(path.join(operationsDirectory, name), content);
|
|
205
|
+
} catch (error) {
|
|
206
|
+
fs.rmSync(operationsDirectory, { recursive: true, force: true });
|
|
207
|
+
throw error;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
ORM=${1:?ORM requis}
|
|
6
|
+
FEATURES_PATH="$(dirname "$0")"
|
|
7
|
+
|
|
8
|
+
if [[ "$ORM" != "typeorm" && "$ORM" != "prisma" ]]; then
|
|
9
|
+
echo "❌ ORM non supporté : $ORM" >&2
|
|
10
|
+
exit 1
|
|
11
|
+
fi
|
|
12
|
+
|
|
13
|
+
node "$FEATURES_PATH/operational_foundation.mjs" src "$ORM"
|
|
14
|
+
node "$FEATURES_PATH/update_app_module.mjs" src/app.module.ts OperationsModule './operations/operations.module' "$ORM"
|
|
15
|
+
|
|
16
|
+
echo "✅ Socle d’exploitation configuré : /health/live et /health/ready."
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { inspectProject } from '../../dist/engine/project-preflight.js';
|
|
2
|
+
|
|
3
|
+
export { inspectProject };
|
|
4
|
+
|
|
5
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
6
|
+
try {
|
|
7
|
+
const [projectRoot, orm] = process.argv.slice(2);
|
|
8
|
+
process.stdout.write(`${JSON.stringify(inspectProject(projectRoot, orm))}\n`);
|
|
9
|
+
} catch (cause) {
|
|
10
|
+
console.error(`❌ ${cause.message}`);
|
|
11
|
+
process.exitCode = 1;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -1,21 +1,32 @@
|
|
|
1
|
-
#!/bin/bash
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
set -euo pipefail
|
|
2
4
|
|
|
3
5
|
# ────── Charger les helpers ──────
|
|
4
|
-
FEATURES_PATH="$(dirname "$0")
|
|
6
|
+
FEATURES_PATH="$(dirname "$0")"
|
|
5
7
|
source "$FEATURES_PATH/utils.sh"
|
|
6
8
|
source "$FEATURES_PATH/logger.sh"
|
|
7
9
|
|
|
8
10
|
|
|
9
|
-
PM=$1
|
|
10
|
-
APP_NAME=$2
|
|
11
|
+
PM=${1:?Package manager requis}
|
|
12
|
+
APP_NAME=${2:?Nom_application_requis}
|
|
13
|
+
|
|
14
|
+
if [ -e prisma/schema.prisma ]; then
|
|
15
|
+
echo "❌ prisma/schema.prisma existe déjà. Aucune configuration Prisma n'a été remplacée."
|
|
16
|
+
exit 1
|
|
17
|
+
fi
|
|
11
18
|
|
|
12
|
-
$PM
|
|
13
|
-
$PM
|
|
14
|
-
|
|
19
|
+
pm_add_dev "$PM" prisma@6
|
|
20
|
+
pm_add "$PM" @prisma/client@6
|
|
21
|
+
pm_exec "$PM" prisma init
|
|
15
22
|
|
|
23
|
+
if [ ! -e .env ]; then
|
|
16
24
|
cat > .env <<EOF
|
|
17
25
|
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/$APP_NAME"
|
|
18
26
|
EOF
|
|
27
|
+
else
|
|
28
|
+
echo "ℹ️ .env existe déjà et a été conservé."
|
|
29
|
+
fi
|
|
19
30
|
|
|
20
31
|
cat > prisma/schema.prisma <<EOF
|
|
21
32
|
generator client {
|
|
@@ -28,6 +39,35 @@ datasource db {
|
|
|
28
39
|
}
|
|
29
40
|
EOF
|
|
30
41
|
|
|
31
|
-
|
|
42
|
+
mkdir -p src/prisma
|
|
43
|
+
cat > src/prisma/prisma.service.ts <<'EOF'
|
|
44
|
+
import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
|
|
45
|
+
import { PrismaClient } from '@prisma/client';
|
|
46
|
+
|
|
47
|
+
@Injectable()
|
|
48
|
+
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
|
|
49
|
+
async onModuleInit(): Promise<void> {
|
|
50
|
+
await this.$connect();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async onModuleDestroy(): Promise<void> {
|
|
54
|
+
await this.$disconnect();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
EOF
|
|
58
|
+
|
|
59
|
+
cat > src/prisma/prisma.module.ts <<'EOF'
|
|
60
|
+
import { Global, Module } from '@nestjs/common';
|
|
61
|
+
import { PrismaService } from './prisma.service';
|
|
62
|
+
|
|
63
|
+
@Global()
|
|
64
|
+
@Module({
|
|
65
|
+
providers: [PrismaService],
|
|
66
|
+
exports: [PrismaService],
|
|
67
|
+
})
|
|
68
|
+
export class PrismaModule {}
|
|
69
|
+
EOF
|
|
70
|
+
|
|
71
|
+
pm_exec "$PM" prisma generate
|
|
32
72
|
|
|
33
73
|
echo "✅ Prisma initialisé avec succès !"
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
const RESOURCE_NAME_PATTERN = /^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$/i;
|
|
2
|
+
const ROUTE_PATTERN = /^[a-z][a-z0-9-]*$/;
|
|
3
|
+
const TABLE_PATTERN = /^[a-z][a-z0-9_]*$/;
|
|
4
|
+
|
|
5
|
+
function fail(message) {
|
|
6
|
+
throw new Error(`Nom de ressource invalide : ${message}`);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function pluralize(word) {
|
|
10
|
+
if (/(?:s|x|z|ch|sh)$/i.test(word)) return `${word}es`;
|
|
11
|
+
if (/[^aeiou]y$/i.test(word)) return `${word.slice(0, -1)}ies`;
|
|
12
|
+
return `${word}s`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function describeResource(rawName, options = {}) {
|
|
16
|
+
const name = String(rawName ?? '').trim();
|
|
17
|
+
if (!RESOURCE_NAME_PATTERN.test(name)) {
|
|
18
|
+
fail('il doit commencer par une lettre et ne contenir que des lettres, chiffres, tirets ou underscores.');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const segments = name.toLowerCase().split(/[-_]/);
|
|
22
|
+
const singular = segments.join('-');
|
|
23
|
+
const plural = options.plural
|
|
24
|
+
? String(options.plural).trim().toLowerCase()
|
|
25
|
+
: `${segments.slice(0, -1).join('-')}${segments.length > 1 ? '-' : ''}${pluralize(segments.at(-1))}`;
|
|
26
|
+
const route = options.route ? String(options.route).trim().toLowerCase() : plural;
|
|
27
|
+
const table = options.table ? String(options.table).trim().toLowerCase() : plural.replaceAll('-', '_');
|
|
28
|
+
|
|
29
|
+
if (!ROUTE_PATTERN.test(plural)) fail('le pluriel doit être une ressource kebab-case.');
|
|
30
|
+
if (!ROUTE_PATTERN.test(route)) fail('la route doit être une ressource kebab-case.');
|
|
31
|
+
if (!TABLE_PATTERN.test(table)) fail('la table doit être une ressource snake_case.');
|
|
32
|
+
|
|
33
|
+
const pascal = segments.map((segment) => `${segment[0].toUpperCase()}${segment.slice(1)}`).join('');
|
|
34
|
+
return {
|
|
35
|
+
name: singular,
|
|
36
|
+
pascal,
|
|
37
|
+
camel: `${pascal[0].toLowerCase()}${pascal.slice(1)}`,
|
|
38
|
+
plural,
|
|
39
|
+
route,
|
|
40
|
+
table,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
45
|
+
try {
|
|
46
|
+
const [rawName] = process.argv.slice(2);
|
|
47
|
+
const resource = describeResource(rawName, {
|
|
48
|
+
plural: process.env.RESOURCE_PLURAL,
|
|
49
|
+
route: process.env.RESOURCE_ROUTE,
|
|
50
|
+
table: process.env.RESOURCE_TABLE,
|
|
51
|
+
});
|
|
52
|
+
for (const [key, value] of Object.entries(resource)) console.log(`${key.toUpperCase()}=${value}`);
|
|
53
|
+
} catch (error) {
|
|
54
|
+
console.error(`❌ ${error.message}`);
|
|
55
|
+
process.exitCode = 1;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -1,27 +1,14 @@
|
|
|
1
|
-
#!/bin/bash
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
set -euo pipefail
|
|
2
4
|
|
|
3
5
|
# ────── Charger les helpers ──────
|
|
4
|
-
FEATURES_PATH="$(dirname "$0")
|
|
6
|
+
FEATURES_PATH="$(dirname "$0")"
|
|
5
7
|
source "$FEATURES_PATH/utils.sh"
|
|
6
8
|
source "$FEATURES_PATH/logger.sh"
|
|
7
9
|
|
|
8
10
|
|
|
9
|
-
PM=$1
|
|
10
|
-
|
|
11
|
-
$PM install @nestjs/swagger swagger-ui-express
|
|
12
|
-
|
|
13
|
-
# Ajout dans main.ts (à faire manuellement ou via automatisation)
|
|
14
|
-
echo ""
|
|
15
|
-
echo "📘 Pour activer Swagger, ajoute ceci dans main.ts :"
|
|
16
|
-
echo ""
|
|
17
|
-
cat <<'DOC'
|
|
18
|
-
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
|
|
11
|
+
PM=${1:?Package manager requis}
|
|
19
12
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
.setDescription('The API description')
|
|
23
|
-
.setVersion('1.0')
|
|
24
|
-
.build();
|
|
25
|
-
const document = SwaggerModule.createDocument(app, config);
|
|
26
|
-
SwaggerModule.setup('api', app, document);
|
|
27
|
-
DOC
|
|
13
|
+
pm_add "$PM" @nestjs/swagger@12 swagger-ui-express@5
|
|
14
|
+
log_success "Swagger sera activé sur /api dans src/main.ts."
|
|
@@ -1,19 +1,80 @@
|
|
|
1
|
-
#!/bin/bash
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
PM=${1:?Package manager requis}
|
|
6
|
+
APP_NAME=${2:?Nom_application_requis}
|
|
5
7
|
|
|
6
8
|
# ────── Charger les helpers ──────
|
|
7
9
|
FEATURES_PATH="$(dirname "$0")"
|
|
8
10
|
source "$FEATURES_PATH/utils.sh"
|
|
9
11
|
source "$FEATURES_PATH/logger.sh"
|
|
10
12
|
|
|
11
|
-
INSTALL_CMD=$(get_install_cmd "$PM")
|
|
12
|
-
|
|
13
13
|
log_info "📦 Installation de TypeORM et PostgreSQL..."
|
|
14
14
|
|
|
15
|
-
$PM
|
|
15
|
+
pm_add "$PM" @nestjs/typeorm@12 typeorm@0.3 pg@8 dotenv@16
|
|
16
|
+
pm_add_dev "$PM" ts-node@10
|
|
17
|
+
|
|
18
|
+
mkdir -p src/database/migrations
|
|
19
|
+
|
|
20
|
+
cat > src/database/typeorm.config.ts <<'EOF'
|
|
21
|
+
import type { TypeOrmModuleOptions } from '@nestjs/typeorm';
|
|
22
|
+
|
|
23
|
+
function required(name: string): string {
|
|
24
|
+
const value = process.env[name];
|
|
25
|
+
if (!value) throw new Error(`Variable d'environnement manquante : ${name}`);
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function typeOrmOptions(): TypeOrmModuleOptions {
|
|
30
|
+
return {
|
|
31
|
+
type: 'postgres',
|
|
32
|
+
host: required('DATABASE_HOST'),
|
|
33
|
+
port: Number(process.env.DATABASE_PORT ?? 5432),
|
|
34
|
+
username: required('DATABASE_USER'),
|
|
35
|
+
password: required('DATABASE_PASSWORD'),
|
|
36
|
+
database: required('DATABASE_NAME'),
|
|
37
|
+
autoLoadEntities: true,
|
|
38
|
+
synchronize: false,
|
|
39
|
+
migrationsRun: false,
|
|
40
|
+
migrations: ['dist/database/migrations/*.js'],
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
EOF
|
|
44
|
+
|
|
45
|
+
cat > src/database/data-source.ts <<'EOF'
|
|
46
|
+
import 'dotenv/config';
|
|
47
|
+
import { DataSource, type DataSourceOptions } from 'typeorm';
|
|
48
|
+
import { typeOrmOptions } from './typeorm.config';
|
|
49
|
+
|
|
50
|
+
const options = typeOrmOptions();
|
|
51
|
+
|
|
52
|
+
export default new DataSource({
|
|
53
|
+
...(options as DataSourceOptions),
|
|
54
|
+
entities: ['src/**/*.entity{.ts,.js}'],
|
|
55
|
+
migrations: ['src/database/migrations/*{.ts,.js}'],
|
|
56
|
+
});
|
|
57
|
+
EOF
|
|
58
|
+
|
|
59
|
+
cat > src/database/migrations/0000000000000-InitialSchema.ts <<'EOF'
|
|
60
|
+
import type { MigrationInterface, QueryRunner } from 'typeorm';
|
|
61
|
+
|
|
62
|
+
export class InitialSchema0000000000000 implements MigrationInterface {
|
|
63
|
+
async up(_queryRunner: QueryRunner): Promise<void> {}
|
|
64
|
+
async down(_queryRunner: QueryRunner): Promise<void> {}
|
|
65
|
+
}
|
|
66
|
+
EOF
|
|
67
|
+
|
|
68
|
+
if [ ! -e .env.example ]; then
|
|
69
|
+
cat > .env.example <<'EOF'
|
|
70
|
+
DATABASE_HOST=localhost
|
|
71
|
+
DATABASE_PORT=5432
|
|
72
|
+
DATABASE_USER=postgres
|
|
73
|
+
DATABASE_PASSWORD=change-me
|
|
74
|
+
DATABASE_NAME=appdb
|
|
75
|
+
EOF
|
|
76
|
+
fi
|
|
16
77
|
|
|
17
78
|
log_success "✅ TypeORM installé avec succès"
|
|
18
79
|
|
|
19
|
-
log_info "💡 TypeORM
|
|
80
|
+
log_info "💡 TypeORM utilise src/database/typeorm.config.ts et src/database/data-source.ts pour des migrations explicites."
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
|
|
3
|
+
const [appModulePath, moduleClass, modulePath, orm] = process.argv.slice(2);
|
|
4
|
+
|
|
5
|
+
function fail(message) {
|
|
6
|
+
throw new Error(`Impossible de mettre à jour AppModule : ${message}`);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function findMatching(source, start, open, close) {
|
|
10
|
+
let depth = 0;
|
|
11
|
+
let quote = null;
|
|
12
|
+
let escaped = false;
|
|
13
|
+
let lineComment = false;
|
|
14
|
+
let blockComment = false;
|
|
15
|
+
|
|
16
|
+
for (let index = start; index < source.length; index += 1) {
|
|
17
|
+
const character = source[index];
|
|
18
|
+
const next = source[index + 1];
|
|
19
|
+
|
|
20
|
+
if (lineComment) {
|
|
21
|
+
if (character === '\n') lineComment = false;
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
if (blockComment) {
|
|
25
|
+
if (character === '*' && next === '/') {
|
|
26
|
+
blockComment = false;
|
|
27
|
+
index += 1;
|
|
28
|
+
}
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (quote) {
|
|
32
|
+
if (!escaped && character === quote) quote = null;
|
|
33
|
+
escaped = !escaped && character === '\\';
|
|
34
|
+
if (character !== '\\') escaped = false;
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (character === '/' && next === '/') {
|
|
38
|
+
lineComment = true;
|
|
39
|
+
index += 1;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (character === '/' && next === '*') {
|
|
43
|
+
blockComment = true;
|
|
44
|
+
index += 1;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (character === '"' || character === "'" || character === '`') {
|
|
48
|
+
quote = character;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (character === open) depth += 1;
|
|
52
|
+
if (character === close) {
|
|
53
|
+
depth -= 1;
|
|
54
|
+
if (depth === 0) return index;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
fail(`délimiteur ${open}${close} non fermé.`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function findDecoratorObject(source) {
|
|
62
|
+
const decoratorMatch = /@Module\s*\(/.exec(source);
|
|
63
|
+
if (!decoratorMatch) fail('décorateur @Module introuvable.');
|
|
64
|
+
|
|
65
|
+
const objectStart = source.indexOf('{', decoratorMatch.index + decoratorMatch[0].length);
|
|
66
|
+
if (objectStart === -1) fail('objet du décorateur @Module introuvable.');
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
start: objectStart,
|
|
70
|
+
end: findMatching(source, objectStart, '{', '}'),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function findImportsArray(source, object) {
|
|
75
|
+
const propertyPattern = /\bimports\s*:\s*\[/g;
|
|
76
|
+
let match;
|
|
77
|
+
|
|
78
|
+
while ((match = propertyPattern.exec(source)) !== null) {
|
|
79
|
+
if (match.index > object.end) break;
|
|
80
|
+
const before = source.slice(object.start + 1, match.index);
|
|
81
|
+
let depth = 0;
|
|
82
|
+
for (const character of before) {
|
|
83
|
+
if (character === '{') depth += 1;
|
|
84
|
+
if (character === '}') depth -= 1;
|
|
85
|
+
}
|
|
86
|
+
if (depth !== 0) continue;
|
|
87
|
+
|
|
88
|
+
const arrayStart = match.index + match[0].lastIndexOf('[');
|
|
89
|
+
return { start: arrayStart, end: findMatching(source, arrayStart, '[', ']') };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
fail('tableau imports dans @Module introuvable ou non supporté.');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function containsToken(items, token) {
|
|
96
|
+
const escaped = token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
97
|
+
return new RegExp(`\\b${escaped}\\b`).test(items);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function appendImports(source, array, additions) {
|
|
101
|
+
if (additions.length === 0) return source;
|
|
102
|
+
|
|
103
|
+
const current = source.slice(array.start + 1, array.end);
|
|
104
|
+
const separator = current.trim() === '' ? '' : current.trimEnd().endsWith(',') ? '\n' : ',\n';
|
|
105
|
+
const indentation = current.includes('\n') ? (current.match(/\n([ \t]*)[^\n]*$/)?.[1] ?? ' ') : '';
|
|
106
|
+
const inserted = additions.map((item) => `${indentation}${item}`).join(',\n');
|
|
107
|
+
const replacement =
|
|
108
|
+
current.trim() === ''
|
|
109
|
+
? current.includes('\n')
|
|
110
|
+
? `\n${inserted}\n `
|
|
111
|
+
: inserted
|
|
112
|
+
: `${current}${separator}${inserted}`;
|
|
113
|
+
|
|
114
|
+
return `${source.slice(0, array.start + 1)}${replacement}${source.slice(array.end)}`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function addImport(source, line) {
|
|
118
|
+
return source.includes(line) ? source : `${line}\n${source}`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
let source = fs.readFileSync(appModulePath, 'utf8');
|
|
123
|
+
const object = findDecoratorObject(source);
|
|
124
|
+
const array = findImportsArray(source, object);
|
|
125
|
+
const currentImports = source.slice(array.start + 1, array.end);
|
|
126
|
+
const additions = [];
|
|
127
|
+
|
|
128
|
+
if (!containsToken(currentImports, 'CqrsModule')) additions.push('CqrsModule');
|
|
129
|
+
if (!containsToken(currentImports, moduleClass)) additions.push(moduleClass);
|
|
130
|
+
if (orm === 'typeorm' && !/\bTypeOrmModule\.forRoot(?:Async)?\s*\(/.test(currentImports)) {
|
|
131
|
+
additions.unshift('TypeOrmModule.forRootAsync({ useFactory: typeOrmOptions })');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
source = appendImports(source, array, additions);
|
|
135
|
+
source = addImport(source, `import { ${moduleClass} } from '${modulePath}';`);
|
|
136
|
+
source = addImport(source, "import { CqrsModule } from '@nestjs/cqrs';");
|
|
137
|
+
if (orm === 'typeorm') {
|
|
138
|
+
source = addImport(source, "import { typeOrmOptions } from './database/typeorm.config';");
|
|
139
|
+
source = addImport(source, "import { TypeOrmModule } from '@nestjs/typeorm';");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
fs.writeFileSync(appModulePath, source);
|
|
143
|
+
} catch (error) {
|
|
144
|
+
console.error(`❌ ${error.message}`);
|
|
145
|
+
process.exitCode = 1;
|
|
146
|
+
}
|