nestgen-cli 0.1.0 → 0.2.2

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.
Files changed (33) hide show
  1. package/dist/cli/doctor-output.js +45 -0
  2. package/dist/cli/output.js +65 -0
  3. package/dist/engine/file-transaction.js +166 -0
  4. package/dist/engine/generated-code.js +26 -0
  5. package/dist/engine/generation-manifest.js +118 -0
  6. package/dist/engine/migration-plan.js +78 -0
  7. package/dist/engine/module-generator.js +80 -65
  8. package/dist/engine/project-config.js +5 -2
  9. package/dist/engine/project-path.js +25 -0
  10. package/dist/engine/project-preflight.js +96 -0
  11. package/dist/engine/resource-generator.js +1104 -68
  12. package/dist/engine/resource-spec.js +254 -11
  13. package/examples/store-api/README.md +76 -0
  14. package/examples/store-api/definitions/customers.resource.json +14 -0
  15. package/examples/store-api/definitions/order-items.resource.json +26 -0
  16. package/examples/store-api/definitions/orders.resource.json +22 -0
  17. package/examples/store-api/definitions/products.resource.json +19 -0
  18. package/examples/store-api/nestgen.example.json +7 -0
  19. package/nestgen.js +488 -84
  20. package/nestjs-generator/features/configure_bootstrap.mjs +5 -2
  21. package/nestjs-generator/features/docker.sh +6 -4
  22. package/nestjs-generator/features/git.sh +1 -1
  23. package/nestjs-generator/features/inject_module_to_app.sh +8 -6
  24. package/nestjs-generator/features/logger.sh +8 -4
  25. package/nestjs-generator/features/operational_foundation.mjs +210 -0
  26. package/nestjs-generator/features/operations.sh +18 -0
  27. package/nestjs-generator/features/preflight.mjs +2 -44
  28. package/nestjs-generator/features/prisma.sh +3 -3
  29. package/nestjs-generator/features/typeorm.sh +3 -3
  30. package/nestjs-generator/features/utils.sh +5 -3
  31. package/nestjs-generator/generate_project.sh +15 -9
  32. package/package.json +11 -4
  33. package/readme.md +234 -13
@@ -0,0 +1,45 @@
1
+ import chalk from 'chalk';
2
+ /** Human-readable presentation only; structured diagnostics remain unchanged. */
3
+ export function formatDoctorOutput(diagnostics, options = {}) {
4
+ const groups = [
5
+ { severity: 'ERROR', title: 'À corriger', marker: '✖', color: chalk.red },
6
+ { severity: 'WARNING', title: 'À vérifier', marker: '⚠', color: chalk.yellow },
7
+ { severity: 'INFO', title: 'Vérifications', marker: '✓', color: chalk.green },
8
+ ];
9
+ const lines = options.quiet
10
+ ? []
11
+ : [
12
+ '',
13
+ ` ${chalk.cyan.bold('◆ NestGen')} ${chalk.bold('Doctor')}`,
14
+ chalk.dim(' Diagnostic de votre environnement'),
15
+ '',
16
+ ];
17
+ for (const group of groups) {
18
+ if (options.quiet && group.severity === 'INFO')
19
+ continue;
20
+ const entries = diagnostics.filter((diagnostic) => diagnostic.severity === group.severity);
21
+ if (!entries.length)
22
+ continue;
23
+ if (!options.quiet)
24
+ lines.push(` ${group.color.bold(group.title)} ${chalk.dim(`(${entries.length})`)}`, '');
25
+ for (const diagnostic of entries) {
26
+ lines.push(` ${group.color(group.marker)} ${diagnostic.cause}`);
27
+ if (diagnostic.severity !== 'INFO' || (diagnostic.id === 'NESTGEN_CONFIG' && !diagnostic.detail))
28
+ lines.push(` → ${diagnostic.action}`);
29
+ if (options.verbose) {
30
+ lines.push(chalk.dim(` Code : ${diagnostic.id}`));
31
+ if (diagnostic.detail)
32
+ lines.push(chalk.dim(` Détail : ${diagnostic.detail}`));
33
+ }
34
+ }
35
+ if (!options.quiet)
36
+ lines.push('');
37
+ }
38
+ if (!options.quiet) {
39
+ const count = (severity) => diagnostics.filter((diagnostic) => diagnostic.severity === severity).length;
40
+ const errors = count('ERROR');
41
+ const warnings = count('WARNING');
42
+ lines.push(chalk.dim(' ────────────────────────────────────────'), ` ${errors ? chalk.red.bold('✖ Des corrections sont nécessaires') : warnings ? chalk.yellow.bold('⚠ Quelques points à vérifier') : chalk.green.bold('✓ Votre environnement est prêt')}`, ` ${chalk.red(`${errors} erreur${errors > 1 ? 's' : ''}`)} · ${chalk.yellow(`${warnings} avertissement${warnings > 1 ? 's' : ''}`)} · ${chalk.dim(`${count('INFO')} vérifications`)}`, '');
43
+ }
44
+ return lines.join('\n');
45
+ }
@@ -0,0 +1,65 @@
1
+ import chalk from 'chalk';
2
+ export function heading(title, subtitle) {
3
+ return `\n ${chalk.cyan.bold('◆ NestGen')} ${chalk.bold(title)}\n${subtitle ? `${chalk.dim(` ${subtitle}`)}\n` : ''}`;
4
+ }
5
+ export function success(message) {
6
+ return `\n ${chalk.green('✓')} ${chalk.bold(message)}\n`;
7
+ }
8
+ export function formatError(message) {
9
+ return `${heading('Erreur')} ${chalk.red('✖')} ${message.split('\n').join('\n ')}\n`;
10
+ }
11
+ export function formatConfig(config, subtitle) {
12
+ const rows = [
13
+ ['ORM', config.orm],
14
+ ['Architecture', config.profile],
15
+ ['Sources', config.sourceRoot],
16
+ ['Package manager', config.packageManager],
17
+ ['Version configuration', config.version],
18
+ ['Version templates', config.templateVersion],
19
+ ];
20
+ return `${heading('Configuration', subtitle)}${rows.map(([label, value]) => ` ${chalk.dim(label.padEnd(24))}${value}`).join('\n')}\n`;
21
+ }
22
+ export function formatMigration(migration) {
23
+ return [
24
+ ` ${chalk.bold('Migration à préparer')}`,
25
+ ` ${chalk.cyan(migration.command)}`,
26
+ chalk.dim(' Cette commande prépare la migration ; aucune migration appliquée.'),
27
+ ...migration.risks.map((risk) => ` ${risk.severity === 'danger' ? chalk.red('✖') : chalk.yellow('⚠')} ${risk.message}`),
28
+ '',
29
+ ].join('\n');
30
+ }
31
+ export function formatPlan(plan, verbose = false) {
32
+ const lines = [heading('Aperçu', `Génération ${plan.operation} · aucun fichier écrit`)];
33
+ for (const change of plan.changes) {
34
+ const label = change.status === 'create'
35
+ ? chalk.green('+ Créer')
36
+ : change.status === 'replace'
37
+ ? chalk.cyan('~ Modifier')
38
+ : chalk.red('✖ Conflit');
39
+ lines.push(` ${label} ${change.path}`);
40
+ if (change.reason)
41
+ lines.push(` ${chalk.yellow(change.reason)}`);
42
+ if (verbose && change.diff)
43
+ lines.push(chalk.dim(` ${change.diff.split('\n').join('\n ')}`));
44
+ }
45
+ for (const conflict of plan.conflicts.filter((conflict) => !conflict.path))
46
+ lines.push(` ${chalk.red('✖')} ${conflict.reason}`);
47
+ lines.push('', chalk.dim(' ────────────────────────────────────────'), ` ${plan.changes.length} fichiers · ${plan.conflicts.length} conflits`, '');
48
+ if (plan.migration)
49
+ lines.push(formatMigration(plan.migration));
50
+ return lines.join('\n');
51
+ }
52
+ /** Align help columns from the complete existing command/option descriptions. */
53
+ export function formatHelp(usage) {
54
+ return `${heading('Aide')}${usage
55
+ .split('\n')
56
+ .map((line) => {
57
+ if (!line)
58
+ return '';
59
+ if (!line.startsWith(' '))
60
+ return ` ${chalk.bold(line)}`;
61
+ const [label, description] = line.trim().split(/\s{2,}/);
62
+ return description ? ` ${chalk.cyan(label.padEnd(34))} ${description}` : ` ${line.trim()}`;
63
+ })
64
+ .join('\n')}\n`;
65
+ }
@@ -0,0 +1,166 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { projectPath } from './project-path.js';
4
+ const transactionDirectory = '.nestgen-transaction';
5
+ function contentDiff(file, before, after) {
6
+ const removed = before
7
+ .split(/(?<=\n)/)
8
+ .map((line) => `-${line}`)
9
+ .join('');
10
+ const added = after
11
+ .split(/(?<=\n)/)
12
+ .map((line) => `+${line}`)
13
+ .join('');
14
+ return `--- ${file}\n+++ ${file}\n@@\n${removed}${added}`;
15
+ }
16
+ /** Inspect the exact file writes of a generation without touching the filesystem. */
17
+ export function previewFileChanges(projectRoot, changes) {
18
+ const root = fs.realpathSync(projectRoot);
19
+ const seen = new Set();
20
+ return changes.map((change) => {
21
+ let target;
22
+ try {
23
+ target = projectPath(root, change.path);
24
+ }
25
+ catch (cause) {
26
+ return {
27
+ ...change,
28
+ status: 'conflict',
29
+ reason: cause instanceof Error ? cause.message : String(cause),
30
+ };
31
+ }
32
+ if (seen.has(target))
33
+ return { ...change, status: 'conflict', reason: `Destination dupliquée : ${change.path}` };
34
+ seen.add(target);
35
+ if (path.relative(root, target).split(path.sep)[0].startsWith(transactionDirectory))
36
+ return { ...change, status: 'conflict', reason: `Destination réservée : ${change.path}` };
37
+ const stat = fs.lstatSync(target, { throwIfNoEntry: false });
38
+ if (change.operation === 'create' && stat)
39
+ return { ...change, status: 'conflict', reason: `Le fichier existe déjà : ${change.path}` };
40
+ if (change.operation === 'replace' && !stat?.isFile())
41
+ return { ...change, status: 'conflict', reason: `Fichier à remplacer introuvable : ${change.path}` };
42
+ const before = stat ? fs.readFileSync(target, 'utf8') : '';
43
+ return {
44
+ ...change,
45
+ status: change.operation,
46
+ diff: contentDiff(change.path, before, change.content),
47
+ };
48
+ });
49
+ }
50
+ /** Apply a fully validated plan; roll back all applied files when a filesystem operation fails. */
51
+ export function applyFileChanges(projectRoot, changes) {
52
+ const root = fs.realpathSync(projectRoot);
53
+ const stage = path.join(root, transactionDirectory);
54
+ const targets = new Set();
55
+ const plan = changes.map((change, index) => {
56
+ const target = projectPath(root, change.path);
57
+ const relative = path.relative(root, target);
58
+ if (relative.split(path.sep)[0].startsWith('.nestgen-transaction'))
59
+ throw new Error(`Destination réservée : ${change.path}`);
60
+ if (targets.has(target))
61
+ throw new Error(`Destination dupliquée : ${change.path}`);
62
+ targets.add(target);
63
+ const stat = fs.lstatSync(target, { throwIfNoEntry: false });
64
+ if (change.operation === 'create' && stat)
65
+ throw new Error(`Le fichier existe déjà : ${change.path}`);
66
+ if (change.operation === 'replace' && !stat?.isFile())
67
+ throw new Error(`Fichier à remplacer introuvable : ${change.path}`);
68
+ return {
69
+ ...change,
70
+ target,
71
+ original: stat ? fs.readFileSync(target) : undefined,
72
+ mode: stat ? stat.mode & 0o777 : 0o666 & ~process.umask(),
73
+ prepared: path.join(stage, `new-${index}`),
74
+ backup: path.join(stage, `old-${index}`),
75
+ };
76
+ });
77
+ for (const item of plan) {
78
+ let parent = path.dirname(item.target);
79
+ while (parent !== root) {
80
+ if (targets.has(parent))
81
+ throw new Error(`Conflit fichier/répertoire : ${parent}`);
82
+ parent = path.dirname(parent);
83
+ }
84
+ }
85
+ if (fs.existsSync(path.join(root, '.nestgen-transaction.json')))
86
+ throw new Error('Transaction interrompue détectée : .nestgen-transaction.json.');
87
+ // mkdir without recursive is an exclusive lock; never remove a lock owned by another invocation.
88
+ fs.mkdirSync(stage);
89
+ const directories = [];
90
+ const applied = [];
91
+ let retainBackups = false;
92
+ let committed = false;
93
+ try {
94
+ fs.writeFileSync(path.join(stage, 'manifest.json'), JSON.stringify(plan.map(({ path: file, operation, mode }, index) => ({ file, operation, mode, index }))));
95
+ for (const item of plan) {
96
+ fs.writeFileSync(item.prepared, item.content, { flag: 'wx', mode: item.mode });
97
+ fs.chmodSync(item.prepared, item.mode);
98
+ if (item.original !== undefined) {
99
+ fs.writeFileSync(item.backup, item.original, { flag: 'wx', mode: item.mode });
100
+ fs.chmodSync(item.backup, item.mode);
101
+ }
102
+ }
103
+ for (const item of plan) {
104
+ const missing = [];
105
+ let parent = path.dirname(item.target);
106
+ while (!fs.existsSync(parent)) {
107
+ missing.push(parent);
108
+ parent = path.dirname(parent);
109
+ }
110
+ for (const directory of missing.reverse()) {
111
+ fs.mkdirSync(directory);
112
+ directories.push(directory);
113
+ }
114
+ if (item.original !== undefined) {
115
+ if (!fs.readFileSync(item.target).equals(item.original))
116
+ throw new Error(`Le fichier a changé pendant la génération : ${item.path}`);
117
+ }
118
+ else if (fs.existsSync(item.target)) {
119
+ throw new Error(`Le fichier est apparu pendant la génération : ${item.path}`);
120
+ }
121
+ fs.renameSync(item.prepared, item.target);
122
+ applied.push(item);
123
+ }
124
+ committed = true;
125
+ }
126
+ catch (error) {
127
+ const failures = [];
128
+ for (const item of applied.reverse()) {
129
+ try {
130
+ if (item.original !== undefined)
131
+ fs.renameSync(item.backup, item.target);
132
+ else
133
+ fs.unlinkSync(item.target);
134
+ }
135
+ catch (rollbackError) {
136
+ failures.push(rollbackError);
137
+ }
138
+ }
139
+ for (const directory of directories.reverse()) {
140
+ try {
141
+ fs.rmdirSync(directory);
142
+ }
143
+ catch (rollbackError) {
144
+ failures.push(rollbackError);
145
+ }
146
+ }
147
+ if (failures.length) {
148
+ retainBackups = true;
149
+ throw new AggregateError([error, ...failures], `Restauration incomplète ; sauvegardes conservées : ${stage}`);
150
+ }
151
+ throw error;
152
+ }
153
+ finally {
154
+ if (!retainBackups) {
155
+ try {
156
+ fs.rmSync(stage, { recursive: true, force: true });
157
+ }
158
+ catch (cleanupError) {
159
+ if (!committed)
160
+ throw cleanupError;
161
+ // The complete plan is already committed. Never report a failed generation after that point.
162
+ console.warn(`Génération terminée ; nettoyage manuel nécessaire : ${stage}`, cleanupError);
163
+ }
164
+ }
165
+ }
166
+ }
@@ -0,0 +1,26 @@
1
+ import path from 'node:path';
2
+ import { format, resolveConfig } from 'prettier';
3
+ /** Format newly generated TypeScript in memory before the transaction can write anything. */
4
+ export async function formatGeneratedCode(root, changes) {
5
+ const result = [];
6
+ for (const change of changes) {
7
+ // Existing shared files contain user formatting; their targeted edits must stay targeted.
8
+ if (change.operation !== 'create' || !change.path.endsWith('.ts')) {
9
+ result.push(change);
10
+ continue;
11
+ }
12
+ const filepath = path.join(root, change.path);
13
+ const options = await resolveConfig(filepath, { editorconfig: true, useCache: false });
14
+ result.push({
15
+ ...change,
16
+ content: await format(change.content, {
17
+ singleQuote: true,
18
+ trailingComma: 'all',
19
+ ...options,
20
+ filepath,
21
+ parser: 'typescript',
22
+ }),
23
+ });
24
+ }
25
+ return result;
26
+ }
@@ -0,0 +1,118 @@
1
+ import { createHash } from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { projectPath } from './project-path.js';
6
+ export const generationManifestPath = '.nestgen/generation-manifest.json';
7
+ const manifestVersion = 1;
8
+ function generatorVersion() {
9
+ const packagePath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../package.json');
10
+ const manifest = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
11
+ if (typeof manifest.version !== 'string')
12
+ throw new Error('Version du générateur introuvable.');
13
+ return manifest.version;
14
+ }
15
+ function generationKey(definition) {
16
+ return `${definition.kind}:${definition.sourceRoot}/app/${definition.name}`;
17
+ }
18
+ export function resourceGenerationKey(sourceRoot, name) {
19
+ return `resource:${sourceRoot}/app/${name}`;
20
+ }
21
+ function sha256(content) {
22
+ return createHash('sha256').update(content).digest('hex');
23
+ }
24
+ function normalizeManifest(manifest) {
25
+ return {
26
+ version: 1,
27
+ generations: Object.fromEntries(Object.entries(manifest.generations)
28
+ .sort(([left], [right]) => left.localeCompare(right))
29
+ .map(([key, entry]) => [
30
+ key,
31
+ {
32
+ definition: entry.definition,
33
+ files: Object.fromEntries(Object.entries(entry.files).sort(([left], [right]) => left.localeCompare(right))),
34
+ generator: entry.generator,
35
+ },
36
+ ])),
37
+ };
38
+ }
39
+ export function loadGenerationManifest(projectRoot) {
40
+ const file = projectPath(projectRoot, generationManifestPath);
41
+ if (!fs.existsSync(file))
42
+ return undefined;
43
+ let value;
44
+ try {
45
+ value = JSON.parse(fs.readFileSync(file, 'utf8'));
46
+ }
47
+ catch {
48
+ throw new Error('Le manifeste de génération est invalide.');
49
+ }
50
+ if (!value || typeof value !== 'object' || value.version !== manifestVersion)
51
+ throw new Error('Version de manifeste de génération non supportée.');
52
+ const generations = value.generations;
53
+ if (!generations || typeof generations !== 'object' || Array.isArray(generations))
54
+ throw new Error('Le manifeste de génération est invalide.');
55
+ return normalizeManifest(value);
56
+ }
57
+ function resourceDefinition(name, orm, sourceRoot, route, table, profile, fields, indexes, relations, list) {
58
+ return {
59
+ kind: 'resource',
60
+ name,
61
+ orm,
62
+ sourceRoot,
63
+ route,
64
+ table,
65
+ profile,
66
+ // Defaults can contain domain secrets. They remain in the resource definition,
67
+ // but generation metadata must stay safe to commit.
68
+ fields: fields.map(({ defaultValue: _defaultValue, ...field }) => field),
69
+ indexes,
70
+ relations,
71
+ list,
72
+ };
73
+ }
74
+ export function moduleGenerationDefinition(name, orm, sourceRoot) {
75
+ return { kind: 'module', name, orm, sourceRoot };
76
+ }
77
+ export function resourceGenerationDefinition(name, orm, sourceRoot, route, table, profile, fields, indexes, relations, list) {
78
+ return resourceDefinition(name, orm, sourceRoot, route, table, profile, fields, indexes, relations, list);
79
+ }
80
+ /** Add the versioned generation manifest to the same transaction as generated files. */
81
+ export function withGenerationManifest(projectRoot, changes, definition) {
82
+ const existing = loadGenerationManifest(projectRoot) ?? { version: 1, generations: {} };
83
+ const files = Object.fromEntries(changes
84
+ .map((change) => [change.path, sha256(change.content)])
85
+ .sort(([left], [right]) => left.localeCompare(right)));
86
+ const manifest = normalizeManifest({
87
+ ...existing,
88
+ generations: {
89
+ ...existing.generations,
90
+ [generationKey(definition)]: {
91
+ definition,
92
+ files,
93
+ generator: { name: 'nestgen-cli', version: generatorVersion() },
94
+ },
95
+ },
96
+ });
97
+ return [
98
+ ...changes,
99
+ {
100
+ path: generationManifestPath,
101
+ content: `${JSON.stringify(manifest, null, 2)}\n`,
102
+ operation: existing.generations[generationKey(definition)] ||
103
+ fs.existsSync(projectPath(projectRoot, generationManifestPath))
104
+ ? 'replace'
105
+ : 'create',
106
+ },
107
+ ];
108
+ }
109
+ /** Report generated files changed after their recorded successful generation. */
110
+ export function modifiedGeneratedFiles(projectRoot, entry) {
111
+ return Object.entries(entry.files)
112
+ .filter(([file, fingerprint]) => {
113
+ const target = projectPath(projectRoot, file);
114
+ return !fs.existsSync(target) || sha256(fs.readFileSync(target, 'utf8')) !== fingerprint;
115
+ })
116
+ .map(([file]) => file)
117
+ .sort();
118
+ }
@@ -0,0 +1,78 @@
1
+ import { loadGenerationManifest, resourceGenerationKey } from './generation-manifest.js';
2
+ function migrationName(value) {
3
+ const normalized = value.trim().toLowerCase();
4
+ if (!/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(normalized))
5
+ throw new Error('Nom de migration invalide. Utilisez des minuscules, chiffres et tirets.');
6
+ return normalized;
7
+ }
8
+ function indexKey(index) {
9
+ return `${index.unique ? 'unique:' : 'index:'}${index.fields.join('+')}`;
10
+ }
11
+ /** Plan an official ORM migration without generating or applying it. */
12
+ export function planResourceMigration(projectRoot, sourceRoot, resourceName, orm, requestedName, fields, indexes, relations) {
13
+ const manifest = loadGenerationManifest(projectRoot);
14
+ const entry = manifest?.generations[resourceGenerationKey(sourceRoot, resourceName)];
15
+ if (!entry || entry.definition.kind !== 'resource')
16
+ throw new Error(`Aucun manifeste de génération pour la ressource ${resourceName}.`);
17
+ const previous = entry.definition;
18
+ if (previous.kind !== 'resource' || entry.definition.orm !== orm)
19
+ throw new Error(`Le manifeste de la ressource ${resourceName} ne correspond pas à l'ORM ${orm}.`);
20
+ const risks = [];
21
+ const previousFields = new Map(previous.fields.map((field) => [field.name, field]));
22
+ const nextFields = new Map(fields.map((field) => [field.name, field]));
23
+ for (const [name, field] of previousFields) {
24
+ const next = nextFields.get(name);
25
+ if (!next) {
26
+ risks.push({
27
+ id: 'COLUMN_REMOVED',
28
+ severity: 'danger',
29
+ message: `Suppression potentielle de la colonne ${name}.`,
30
+ });
31
+ continue;
32
+ }
33
+ if (next.type !== field.type) {
34
+ risks.push({
35
+ id: 'FIELD_TYPE_CHANGED',
36
+ severity: 'danger',
37
+ message: `Changement de type potentiel pour ${name} (${field.type} vers ${next.type}).`,
38
+ });
39
+ }
40
+ if (field.nullable && !next.nullable) {
41
+ risks.push({
42
+ id: 'NULLABILITY_TIGHTENED',
43
+ severity: 'warning',
44
+ message: `${name} devient obligatoire ; les lignes existantes doivent être complétées.`,
45
+ });
46
+ }
47
+ }
48
+ const previousIndexes = new Set(previous.indexes.filter((index) => index.unique).map(indexKey));
49
+ for (const index of indexes.filter((item) => item.unique)) {
50
+ if (!previousIndexes.has(indexKey(index)))
51
+ risks.push({
52
+ id: 'UNIQUE_CONSTRAINT_ADDED',
53
+ severity: 'warning',
54
+ message: `Contrainte unique ajoutée sur ${index.fields.join(', ')} ; vérifiez les doublons existants.`,
55
+ });
56
+ }
57
+ const previousRelations = new Set(previous.relations.map((relation) => `${relation.type}:${relation.target}:${relation.field ?? ''}`));
58
+ const nextRelations = new Set(relations.map((relation) => `${relation.type}:${relation.target}:${relation.field ?? ''}`));
59
+ for (const relation of previousRelations) {
60
+ if (!nextRelations.has(relation))
61
+ risks.push({
62
+ id: 'RELATION_REMOVED',
63
+ severity: 'danger',
64
+ message: `Suppression potentielle de relation ${relation}.`,
65
+ });
66
+ }
67
+ const name = migrationName(requestedName);
68
+ return {
69
+ version: 1,
70
+ orm,
71
+ migrationName: name,
72
+ command: orm === 'typeorm'
73
+ ? `npx typeorm-ts-node-commonjs migration:generate src/database/migrations/${name} --dataSource src/database/data-source.ts`
74
+ : `npx prisma migrate dev --create-only --name ${name}`,
75
+ risks,
76
+ appliesMigration: false,
77
+ };
78
+ }