nestgen-cli 0.0.7 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,10 @@
1
+ export function parseArchitectureProfile(value) {
2
+ const profile = value ?? 'simple';
3
+ if (profile !== 'simple' && profile !== 'advanced')
4
+ throw new Error('Profil invalide. Valeurs acceptées : simple, advanced.');
5
+ return profile;
6
+ }
7
+ export const profileCosts = {
8
+ simple: 'Modules Nest directs : moins de fichiers, adapté aux CRUD et services simples.',
9
+ advanced: 'Ports/adaptateurs : frontières explicites, coût de maintenance plus élevé, adapté aux domaines complexes.',
10
+ };
@@ -0,0 +1,142 @@
1
+ import crypto from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import ts from 'typescript';
5
+ // The legacy preflight is JavaScript while the migration is incremental.
6
+ // @ts-expect-error declaration added when preflight moves into this engine.
7
+ import { inspectProject } from '../../nestjs-generator/features/preflight.mjs';
8
+ const stateFile = '.nestgen-transaction.json';
9
+ function describe(rawName) {
10
+ const name = rawName.trim().toLowerCase().replace(/_/g, '-');
11
+ if (!/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(name))
12
+ throw new Error('Nom de module invalide.');
13
+ const pascal = name
14
+ .split('-')
15
+ .map((part) => part[0].toUpperCase() + part.slice(1))
16
+ .join('');
17
+ return {
18
+ name,
19
+ pascal,
20
+ camel: pascal[0].toLowerCase() + pascal.slice(1),
21
+ route: `${name}s`,
22
+ table: `${name.replaceAll('-', '_')}s`,
23
+ };
24
+ }
25
+ function updateAppModule(source, moduleClass, moduleImport, moduleReference = moduleClass) {
26
+ const file = ts.createSourceFile('app.module.ts', source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
27
+ const appModule = file.statements.find((statement) => ts.isClassDeclaration(statement) && statement.name?.text === 'AppModule');
28
+ const decorator = appModule &&
29
+ ts
30
+ .getDecorators(appModule)
31
+ ?.find((candidate) => ts.isCallExpression(candidate.expression) &&
32
+ ts.isIdentifier(candidate.expression.expression) &&
33
+ candidate.expression.expression.text === 'Module');
34
+ if (!decorator ||
35
+ !ts.isCallExpression(decorator.expression) ||
36
+ !ts.isObjectLiteralExpression(decorator.expression.arguments[0]))
37
+ throw new Error('AppModule doit utiliser @Module({ ... }).');
38
+ const imports = decorator.expression.arguments[0].properties.find((property) => ts.isPropertyAssignment(property) && ts.isIdentifier(property.name) && property.name.text === 'imports');
39
+ if (!imports || !ts.isArrayLiteralExpression(imports.initializer))
40
+ throw new Error('AppModule doit définir imports: [] pour enregistrer un module.');
41
+ if (imports.initializer.elements.some((element) => element.getText(file) === moduleReference))
42
+ return source;
43
+ const importLine = `import { ${moduleClass} } from '${moduleImport}';\n`;
44
+ const prefix = source.includes(`from '${moduleImport}'`) ? '' : importLine;
45
+ const index = imports.initializer.getEnd() - 1 + prefix.length;
46
+ const lastElement = imports.initializer.elements.at(-1);
47
+ const tail = lastElement ? source.slice(lastElement.end, imports.initializer.getEnd() - 1) : '';
48
+ const separator = lastElement && !tail.includes(',') ? ', ' : '';
49
+ const updated = `${prefix}${source}`;
50
+ return `${updated.slice(0, index)}${separator}${moduleReference}${updated.slice(index)}`;
51
+ }
52
+ /** Registers a generated feature in the project's root Nest module. */
53
+ export function registerModuleInAppModule(source, moduleClass, moduleImport, moduleReference = moduleClass) {
54
+ return updateAppModule(source, moduleClass, moduleImport, moduleReference);
55
+ }
56
+ function updatePrismaSchema(source, resource) {
57
+ const modelPattern = new RegExp(`\\bmodel\\s+${resource.pascal}\\b`);
58
+ if (modelPattern.test(source))
59
+ throw new Error(`Le modèle Prisma ${resource.pascal} existe déjà.`);
60
+ return `${source.trimEnd()}\n\nmodel ${resource.pascal} {\n id String @id @default(uuid())\n name String\n email String @unique\n\n @@map("${resource.table}")\n}\n`;
61
+ }
62
+ function ensurePrismaRuntime(projectRoot) {
63
+ const prismaDirectory = path.join(projectRoot, 'src', 'prisma');
64
+ const servicePath = path.join(prismaDirectory, 'prisma.service.ts');
65
+ const modulePath = path.join(prismaDirectory, 'prisma.module.ts');
66
+ if (fs.existsSync(servicePath) || fs.existsSync(modulePath))
67
+ return;
68
+ fs.mkdirSync(prismaDirectory, { recursive: true });
69
+ fs.writeFileSync(servicePath, "import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common';\nimport { PrismaClient } from '@prisma/client';\n\n@Injectable()\nexport class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {\n async onModuleInit(): Promise<void> { await this.$connect(); }\n async onModuleDestroy(): Promise<void> { await this.$disconnect(); }\n}\n");
70
+ fs.writeFileSync(modulePath, "import { Global, Module } from '@nestjs/common';\nimport { PrismaService } from './prisma.service';\n\n@Global()\n@Module({ providers: [PrismaService], exports: [PrismaService] })\nexport class PrismaModule {}\n");
71
+ }
72
+ function files(resource, orm) {
73
+ const token = `${resource.pascal}RepositoryToken`;
74
+ const result = new Map();
75
+ result.set(`core/domain/entities/${resource.name}.entity.ts`, `export class ${resource.pascal} {\n constructor(public readonly id: string | undefined, public name: string, public email: string) {}\n}\n`);
76
+ result.set(`core/domain/ports/${resource.name}.repository.ts`, `import { ${resource.pascal} } from '../entities/${resource.name}.entity';\n\nexport const ${token} = Symbol('${resource.pascal}RepositoryPort');\nexport interface ${resource.pascal}RepositoryPort { save(value: ${resource.pascal}): Promise<${resource.pascal}>; findById(id: string): Promise<${resource.pascal} | null>; }\n`);
77
+ result.set(`core/application/commands/create-${resource.name}.command.ts`, `export class Create${resource.pascal}Command { constructor(public readonly name: string, public readonly email: string) {} }\n`);
78
+ result.set(`core/application/commands/create-${resource.name}.handler.ts`, `import { Inject } from '@nestjs/common';\nimport { CommandHandler, ICommandHandler } from '@nestjs/cqrs';\nimport { Create${resource.pascal}Command } from './create-${resource.name}.command';\nimport { ${resource.pascal} } from '../../domain/entities/${resource.name}.entity';\nimport { ${resource.pascal}RepositoryPort, ${token} } from '../../domain/ports/${resource.name}.repository';\n@CommandHandler(Create${resource.pascal}Command)\nexport class Create${resource.pascal}Handler implements ICommandHandler<Create${resource.pascal}Command> { constructor(@Inject(${token}) private readonly repository: ${resource.pascal}RepositoryPort) {} async execute(command: Create${resource.pascal}Command): Promise<string> { return (await this.repository.save(new ${resource.pascal}(undefined, command.name, command.email))).id!; } }\n`);
79
+ result.set(`interfaces/dtos/create-${resource.name}.dto.ts`, `import { IsEmail, IsString } from 'class-validator';\nexport class Create${resource.pascal}Dto { @IsString() name!: string; @IsEmail() email!: string; }\n`);
80
+ result.set(`interfaces/controllers/${resource.name}.controller.ts`, `import { Body, Controller, Post } from '@nestjs/common';\nimport { CommandBus } from '@nestjs/cqrs';\nimport { Create${resource.pascal}Command } from '../../core/application/commands/create-${resource.name}.command';\nimport { Create${resource.pascal}Dto } from '../dtos/create-${resource.name}.dto';\n@Controller('${resource.route}') export class ${resource.pascal}Controller { constructor(private readonly commandBus: CommandBus) {} @Post() async create(@Body() dto: Create${resource.pascal}Dto) { return { id: await this.commandBus.execute(new Create${resource.pascal}Command(dto.name, dto.email)) }; } }\n`);
81
+ if (orm === 'typeorm') {
82
+ result.set(`infrastructure/persistences/repositories/${resource.name}.orm.ts`, `import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';\n@Entity('${resource.table}') export class ${resource.pascal}Entity { @PrimaryGeneratedColumn('uuid') id!: string; @Column() name!: string; @Column({ unique: true }) email!: string; }\n`);
83
+ result.set(`infrastructure/persistences/repositories/${resource.name}.typeorm.repository.ts`, `import { Injectable } from '@nestjs/common';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { Repository } from 'typeorm';\nimport { ${resource.pascal} } from '../../../core/domain/entities/${resource.name}.entity';\nimport { ${resource.pascal}RepositoryPort } from '../../../core/domain/ports/${resource.name}.repository';\nimport { ${resource.pascal}Entity } from './${resource.name}.orm';\n@Injectable() export class ${resource.pascal}TypeOrmRepository implements ${resource.pascal}RepositoryPort { constructor(@InjectRepository(${resource.pascal}Entity) private readonly repository: Repository<${resource.pascal}Entity>) {} async save(value: ${resource.pascal}) { const saved = await this.repository.save(this.repository.create({ name: value.name, email: value.email })); return new ${resource.pascal}(saved.id, saved.name, saved.email); } async findById(id: string) { const found = await this.repository.findOneBy({ id }); return found ? new ${resource.pascal}(found.id, found.name, found.email) : null; } }\n`);
84
+ result.set(`${resource.name}.module.ts`, `import { Module } from '@nestjs/common';\nimport { CqrsModule } from '@nestjs/cqrs';\nimport { TypeOrmModule } from '@nestjs/typeorm';\nimport { Create${resource.pascal}Handler } from './core/application/commands/create-${resource.name}.handler';\nimport { ${token} } from './core/domain/ports/${resource.name}.repository';\nimport { ${resource.pascal}Entity } from './infrastructure/persistences/repositories/${resource.name}.orm';\nimport { ${resource.pascal}TypeOrmRepository } from './infrastructure/persistences/repositories/${resource.name}.typeorm.repository';\nimport { ${resource.pascal}Controller } from './interfaces/controllers/${resource.name}.controller';\n@Module({ imports: [CqrsModule, TypeOrmModule.forFeature([${resource.pascal}Entity])], controllers: [${resource.pascal}Controller], providers: [Create${resource.pascal}Handler, { provide: ${token}, useClass: ${resource.pascal}TypeOrmRepository }] }) export class ${resource.pascal}Module {}\n`);
85
+ }
86
+ else {
87
+ result.set(`infrastructure/persistences/repositories/${resource.name}.prisma.repository.ts`, `import { Injectable } from '@nestjs/common';\nimport { PrismaService } from '../../../../../prisma/prisma.service';\nimport { ${resource.pascal} } from '../../../core/domain/entities/${resource.name}.entity';\nimport { ${resource.pascal}RepositoryPort } from '../../../core/domain/ports/${resource.name}.repository';\n@Injectable() export class ${resource.pascal}PrismaRepository implements ${resource.pascal}RepositoryPort { constructor(private readonly prisma: PrismaService) {} async save(value: ${resource.pascal}) { const saved = await this.prisma.${resource.camel}.create({ data: { name: value.name, email: value.email } }); return new ${resource.pascal}(saved.id, saved.name, saved.email); } async findById(id: string) { const found = await this.prisma.${resource.camel}.findUnique({ where: { id } }); return found ? new ${resource.pascal}(found.id, found.name, found.email) : null; } }\n`);
88
+ result.set(`${resource.name}.module.ts`, `import { Module } from '@nestjs/common';\nimport { CqrsModule } from '@nestjs/cqrs';\nimport { PrismaModule } from '../../prisma/prisma.module';\nimport { Create${resource.pascal}Handler } from './core/application/commands/create-${resource.name}.handler';\nimport { ${token} } from './core/domain/ports/${resource.name}.repository';\nimport { ${resource.pascal}PrismaRepository } from './infrastructure/persistences/repositories/${resource.name}.prisma.repository';\nimport { ${resource.pascal}Controller } from './interfaces/controllers/${resource.name}.controller';\n@Module({ imports: [CqrsModule, PrismaModule], controllers: [${resource.pascal}Controller], providers: [Create${resource.pascal}Handler, { provide: ${token}, useClass: ${resource.pascal}PrismaRepository }] }) export class ${resource.pascal}Module {}\n`);
89
+ }
90
+ return result;
91
+ }
92
+ export function generateModule(projectRoot, rawName, orm) {
93
+ if (orm !== 'typeorm' && orm !== 'prisma')
94
+ throw new Error(`ORM non supporté : ${orm}.`);
95
+ inspectProject(projectRoot, orm);
96
+ const resource = describe(rawName);
97
+ const pending = path.join(projectRoot, stateFile);
98
+ const moduleDirectory = path.join(projectRoot, 'src/app', resource.name);
99
+ const stage = path.join(projectRoot, `.nestgen-stage-${crypto.randomUUID()}`);
100
+ const appModule = path.join(projectRoot, 'src/app.module.ts');
101
+ const prismaSchema = path.join(projectRoot, 'prisma/schema.prisma');
102
+ if (fs.existsSync(pending))
103
+ throw new Error(`Transaction interrompue détectée : ${stateFile}.`);
104
+ if (fs.existsSync(moduleDirectory))
105
+ throw new Error(`Le module ${resource.name} existe déjà.`);
106
+ const originalAppModule = fs.readFileSync(appModule, 'utf8');
107
+ const originalSchema = orm === 'prisma' ? fs.readFileSync(prismaSchema, 'utf8') : undefined;
108
+ const state = { status: 'prepared', stage, moduleDirectory, appModule };
109
+ try {
110
+ for (const [relative, content] of files(resource, orm)) {
111
+ const target = path.join(stage, relative);
112
+ fs.mkdirSync(path.dirname(target), { recursive: true });
113
+ fs.writeFileSync(target, content);
114
+ }
115
+ const updated = updateAppModule(originalAppModule, `${resource.pascal}Module`, `./app/${resource.name}/${resource.name}.module`);
116
+ fs.writeFileSync(pending, JSON.stringify(state, null, 2));
117
+ fs.mkdirSync(path.dirname(moduleDirectory), { recursive: true });
118
+ fs.renameSync(stage, moduleDirectory);
119
+ state.status = 'module-applied';
120
+ fs.writeFileSync(pending, JSON.stringify(state, null, 2));
121
+ const temporary = `${appModule}.nestgen-${process.pid}`;
122
+ fs.writeFileSync(temporary, updated);
123
+ fs.renameSync(temporary, appModule);
124
+ if (originalSchema) {
125
+ fs.writeFileSync(prismaSchema, updatePrismaSchema(originalSchema, resource));
126
+ ensurePrismaRuntime(projectRoot);
127
+ }
128
+ fs.rmSync(pending);
129
+ }
130
+ catch (error) {
131
+ if (state.status === 'module-applied')
132
+ fs.rmSync(moduleDirectory, { recursive: true, force: true });
133
+ if (state.status === 'module-applied')
134
+ fs.writeFileSync(appModule, originalAppModule);
135
+ if (originalSchema)
136
+ fs.writeFileSync(prismaSchema, originalSchema);
137
+ throw error;
138
+ }
139
+ finally {
140
+ fs.rmSync(stage, { recursive: true, force: true });
141
+ }
142
+ }
@@ -0,0 +1,39 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { parseArchitectureProfile } from './architecture-profile.js';
4
+ export const configFileName = 'nestgen.config.json';
5
+ export const defaultConfig = {
6
+ version: 1,
7
+ orm: 'typeorm',
8
+ profile: 'simple',
9
+ sourceRoot: 'src',
10
+ packageManager: 'npm',
11
+ templateVersion: 1,
12
+ };
13
+ export function loadConfigIfPresent(projectRoot) {
14
+ return fs.existsSync(path.join(projectRoot, configFileName)) ? loadConfig(projectRoot) : undefined;
15
+ }
16
+ export function loadConfig(projectRoot) {
17
+ const filePath = path.join(projectRoot, configFileName);
18
+ const config = JSON.parse(fs.readFileSync(filePath, 'utf8'));
19
+ if (config.version !== 1 || config.templateVersion !== 1)
20
+ throw new Error('Version de configuration ou de templates non supportée.');
21
+ if (config.orm !== 'typeorm' && config.orm !== 'prisma')
22
+ throw new Error('ORM de configuration invalide.');
23
+ if (config.sourceRoot !== 'src')
24
+ throw new Error('Seul sourceRoot "src" est supporté.');
25
+ const packageManager = config.packageManager;
26
+ if (!packageManager || !['npm', 'pnpm', 'yarn'].includes(packageManager))
27
+ throw new Error('Package manager invalide.');
28
+ return {
29
+ version: 1,
30
+ orm: config.orm,
31
+ profile: parseArchitectureProfile(config.profile),
32
+ sourceRoot: config.sourceRoot,
33
+ packageManager,
34
+ templateVersion: 1,
35
+ };
36
+ }
37
+ export function writeConfig(projectRoot, config) {
38
+ fs.writeFileSync(path.join(projectRoot, configFileName), `${JSON.stringify(config, null, 2)}\n`, { flag: 'wx' });
39
+ }
@@ -0,0 +1,117 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { parseArchitectureProfile } from './architecture-profile.js';
4
+ import { registerModuleInAppModule } from './module-generator.js';
5
+ import { prismaType, typescriptType } from './resource-spec.js';
6
+ function pascal(value) {
7
+ return value
8
+ .split('-')
9
+ .map((part) => `${part[0].toUpperCase()}${part.slice(1)}`)
10
+ .join('');
11
+ }
12
+ function entityColumn(field) {
13
+ const type = field.type === 'number'
14
+ ? "'double precision'"
15
+ : field.type === 'boolean'
16
+ ? "'boolean'"
17
+ : field.type === 'date'
18
+ ? "'timestamptz'"
19
+ : field.type === 'uuid'
20
+ ? "'uuid'"
21
+ : "'varchar'";
22
+ const options = [`type: ${type}`, `nullable: ${field.nullable}`];
23
+ if (field.unique)
24
+ options.push('unique: true');
25
+ return ` @Column({ ${options.join(', ')} })\n ${field.name}${field.nullable ? '?' : '!'}: ${typescriptType(field)}${field.nullable ? ' | null' : ''};`;
26
+ }
27
+ function validationDecorators(field, optional) {
28
+ const decorators = optional ? ['@IsOptional()'] : [];
29
+ decorators.push(field.type === 'number'
30
+ ? '@IsNumber()'
31
+ : field.type === 'boolean'
32
+ ? '@IsBoolean()'
33
+ : field.type === 'date'
34
+ ? '@IsDateString()'
35
+ : field.type === 'uuid'
36
+ ? '@IsUUID()'
37
+ : '@IsString()');
38
+ return decorators.map((decorator) => ` ${decorator}`).join('\n');
39
+ }
40
+ function dtoFields(fields, optional) {
41
+ return fields
42
+ .map((field) => {
43
+ const isOptional = optional || field.nullable;
44
+ const type = field.type === 'date' ? 'string' : typescriptType(field);
45
+ return `${validationDecorators(field, isOptional)}\n ${field.name}${isOptional ? '?' : '!'}: ${type}${field.nullable ? ' | null' : ''};`;
46
+ })
47
+ .join('\n\n');
48
+ }
49
+ function propertyMap(fields) {
50
+ return fields
51
+ .map((field) => {
52
+ const value = `input.${field.name}`;
53
+ return `${field.name}: ${field.type === 'date' ? `${value} ? new Date(${value}) : ${value}` : value}`;
54
+ })
55
+ .join(', ');
56
+ }
57
+ function featureFiles(name, className, fields, route, table, profile) {
58
+ const advanced = profile === 'advanced';
59
+ const entityProperties = fields.map(entityColumn).join('\n\n');
60
+ const fieldAssignments = propertyMap(fields);
61
+ const domainProperties = fields
62
+ .map((field) => ` ${field.name}!: ${typescriptType(field)}${field.nullable ? ' | null' : ''};`)
63
+ .join('\n');
64
+ const files = new Map();
65
+ files.set('domain/' + name + '.ts', `export class ${className} {\n id!: string;\n${domainProperties}\n}\n`);
66
+ files.set('dto/create-' + name + '.dto.ts', `import { IsBoolean, IsDateString, IsNumber, IsOptional, IsString, IsUUID } from 'class-validator';\n\nexport class Create${className}Dto {\n${dtoFields(fields, false)}\n}\n`);
67
+ files.set('dto/update-' + name + '.dto.ts', `import { IsBoolean, IsDateString, IsNumber, IsOptional, IsString, IsUUID } from 'class-validator';\n\nexport class Update${className}Dto {\n${dtoFields(fields, true)}\n}\n`);
68
+ files.set('dto/list-' + name + '.query.ts', `import { Type } from 'class-transformer';\nimport { IsInt, IsOptional, Max, Min } from 'class-validator';\n\nexport const MAX_PAGE_SIZE = 100;\n\nexport class List${className}Query {\n @IsOptional()\n @Type(() => Number)\n @IsInt()\n @Min(1)\n page = 1;\n\n @IsOptional()\n @Type(() => Number)\n @IsInt()\n @Min(1)\n @Max(MAX_PAGE_SIZE)\n limit = 20;\n}\n`);
69
+ files.set('persistence/' + name + '.entity.ts', `import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';\n\n@Entity({ name: '${table}' })\nexport class ${className}Entity {\n @PrimaryGeneratedColumn('uuid')\n id!: string;\n\n${entityProperties}\n}\n`);
70
+ files.set('persistence/' + name + '.repository.ts', `import { ConflictException, Injectable, NotFoundException } from '@nestjs/common';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { QueryFailedError, Repository } from 'typeorm';\nimport { Create${className}Dto } from '../dto/create-${name}.dto.js';\nimport { Update${className}Dto } from '../dto/update-${name}.dto.js';\nimport { ${className} } from '../domain/${name}.js';\n${advanced ? `import { ${className}RepositoryPort } from '../domain/${name}.repository.port.js';\n` : ''}import { ${className}Entity } from './${name}.entity.js';\n\n@Injectable()\nexport class ${className}Repository${advanced ? ` implements ${className}RepositoryPort` : ''} {\n constructor(@InjectRepository(${className}Entity) private readonly repository: Repository<${className}Entity>) {}\n\n async create(input: Create${className}Dto): Promise<${className}> {\n try {\n return this.toDomain(await this.repository.save(this.repository.create({ ${fieldAssignments} })));\n } catch (error) {\n this.rethrowPersistenceError(error);\n }\n }\n\n async findOne(id: string): Promise<${className}> {\n const value = await this.repository.findOneBy({ id });\n if (!value) throw new NotFoundException('${className} introuvable');\n return this.toDomain(value);\n }\n\n async findMany(skip: number, take: number): Promise<${className}[]> {\n return (await this.repository.find({ skip, take, order: { id: 'ASC' } })).map((value) => this.toDomain(value));\n }\n\n async update(id: string, input: Update${className}Dto): Promise<${className}> {\n const existing = await this.repository.preload({ id, ${fieldAssignments} });\n if (!existing) throw new NotFoundException('${className} introuvable');\n try {\n return this.toDomain(await this.repository.save(existing));\n } catch (error) {\n this.rethrowPersistenceError(error);\n }\n }\n\n async remove(id: string): Promise<void> {\n const result = await this.repository.delete(id);\n if (!result.affected) throw new NotFoundException('${className} introuvable');\n }\n\n private toDomain(value: ${className}Entity): ${className} {\n return Object.assign(new ${className}(), value);\n }\n\n private rethrowPersistenceError(error: unknown): never {\n if (error instanceof QueryFailedError && (error.driverError as { code?: string }).code === '23505')\n throw new ConflictException('Une ressource avec cette valeur unique existe déjà.');\n throw error;\n }\n}\n`);
71
+ if (advanced) {
72
+ files.set('domain/' + name + '.repository.port.ts', `import { Create${className}Dto } from '../dto/create-${name}.dto.js';\nimport { Update${className}Dto } from '../dto/update-${name}.dto.js';\nimport { ${className} } from './${name}.js';\n\nexport const ${className}RepositoryToken = Symbol('${className}RepositoryPort');\nexport interface ${className}RepositoryPort { create(input: Create${className}Dto): Promise<${className}>; findOne(id: string): Promise<${className}>; findMany(skip: number, take: number): Promise<${className}[]>; update(id: string, input: Update${className}Dto): Promise<${className}>; remove(id: string): Promise<void>; }\n`);
73
+ files.set('application/' + name + '.service.ts', `import { Inject, Injectable } from '@nestjs/common';\nimport { Create${className}Dto } from '../dto/create-${name}.dto.js';\nimport { Update${className}Dto } from '../dto/update-${name}.dto.js';\nimport { ${className}RepositoryPort, ${className}RepositoryToken } from '../domain/${name}.repository.port.js';\n\n@Injectable() export class ${className}Service { constructor(@Inject(${className}RepositoryToken) private readonly repository: ${className}RepositoryPort) {} create(input: Create${className}Dto) { return this.repository.create(input); } findOne(id: string) { return this.repository.findOne(id); } findMany(skip: number, take: number) { return this.repository.findMany(skip, take); } update(id: string, input: Update${className}Dto) { return this.repository.update(id, input); } remove(id: string) { return this.repository.remove(id); } }\n`);
74
+ }
75
+ files.set(name + '.controller.ts', `import { Body, Controller, Delete, Get, HttpCode, Param, Patch, Post, Query } from '@nestjs/common';\nimport { Create${className}Dto } from './dto/create-${name}.dto.js';\nimport { List${className}Query } from './dto/list-${name}.query.js';\nimport { Update${className}Dto } from './dto/update-${name}.dto.js';\nimport { ${className}${advanced ? 'Service' : 'Repository'} } from './${advanced ? 'application/' + name + '.service' : 'persistence/' + name + '.repository'}.js';\n\n@Controller('${route}')\nexport class ${className}Controller {\n constructor(private readonly repository: ${className}${advanced ? 'Service' : 'Repository'}) {}\n\n @Post()\n create(@Body() dto: Create${className}Dto) { return this.repository.create(dto); }\n\n @Get()\n async list(@Query() query: List${className}Query) {\n const skip = (query.page - 1) * query.limit;\n return { page: query.page, limit: query.limit, data: await this.repository.findMany(skip, query.limit) };\n }\n\n @Get(':id')\n get(@Param('id') id: string) { return this.repository.findOne(id); }\n\n @Patch(':id')\n update(@Param('id') id: string, @Body() dto: Update${className}Dto) { return this.repository.update(id, dto); }\n\n @Delete(':id')\n @HttpCode(204)\n async remove(@Param('id') id: string): Promise<void> { await this.repository.remove(id); }\n}\n`);
76
+ files.set(name + '.module.ts', `import { Module } from '@nestjs/common';\nimport { TypeOrmModule } from '@nestjs/typeorm';\nimport { ${className}Controller } from './${name}.controller.js';\n${advanced ? `import { ${className}Service } from './application/${name}.service.js';\nimport { ${className}RepositoryToken } from './domain/${name}.repository.port.js';\n` : ''}import { ${className}Entity } from './persistence/${name}.entity.js';\nimport { ${className}Repository } from './persistence/${name}.repository.js';\n\n@Module({\n imports: [TypeOrmModule.forFeature([${className}Entity])],\n controllers: [${className}Controller],\n providers: [${className}Repository${advanced ? `, { provide: ${className}RepositoryToken, useExisting: ${className}Repository }, ${className}Service` : ''}],\n})\nexport class ${className}Module {}\n`);
77
+ files.set('persistence/' + name + '.prisma', `model ${className} {\n id String @id @default(uuid())\n${fields.map((field) => ` ${field.name} ${prismaType(field)}`).join('\n')}\n\n @@map("${table}")\n}\n`);
78
+ return files;
79
+ }
80
+ export function generateResource(projectRoot, options) {
81
+ const profile = parseArchitectureProfile(options.profile);
82
+ const orm = options.orm ?? 'typeorm';
83
+ if (orm !== 'typeorm')
84
+ throw new Error('La commande resource prend actuellement en charge TypeORM uniquement.');
85
+ const name = options.name.toLowerCase();
86
+ const className = pascal(name);
87
+ const directory = path.join(projectRoot, 'src', 'app', name);
88
+ const appModulePath = path.join(projectRoot, 'src', 'app.module.ts');
89
+ if (!/^[a-z][a-z0-9-]*$/.test(name))
90
+ throw new Error('Nom de ressource invalide.');
91
+ if (fs.existsSync(directory))
92
+ throw new Error(`La ressource ${name} existe déjà.`);
93
+ if (!fs.existsSync(appModulePath))
94
+ throw new Error('src/app.module.ts introuvable.');
95
+ const files = featureFiles(name, className, options.fields, options.route, options.table, profile);
96
+ const appModule = registerModuleInAppModule(fs.readFileSync(appModulePath, 'utf8'), `${className}Module`, `./app/${name}/${name}.module.js`, `${className}Module`);
97
+ const stage = path.join(projectRoot, `.nestgen-resource-${name}-${process.pid}`);
98
+ try {
99
+ for (const [relative, content] of files) {
100
+ const target = path.join(stage, relative);
101
+ fs.mkdirSync(path.dirname(target), { recursive: true });
102
+ fs.writeFileSync(target, content);
103
+ }
104
+ fs.writeFileSync(path.join(stage, 'resource.json'), `${JSON.stringify({ name, route: options.route, table: options.table, orm, profile, fields: options.fields }, null, 2)}\n`);
105
+ fs.mkdirSync(path.dirname(directory), { recursive: true });
106
+ fs.renameSync(stage, directory);
107
+ fs.writeFileSync(appModulePath, appModule);
108
+ }
109
+ catch (error) {
110
+ if (fs.existsSync(directory))
111
+ fs.rmSync(directory, { recursive: true, force: true });
112
+ throw error;
113
+ }
114
+ finally {
115
+ fs.rmSync(stage, { recursive: true, force: true });
116
+ }
117
+ }
@@ -0,0 +1,44 @@
1
+ const fieldPattern = /^([a-z][a-z0-9]*):(string|number|boolean|date|uuid)(\?)?(!)?$/i;
2
+ export function parseResourceFields(values) {
3
+ const names = new Set();
4
+ const fields = values.map((value) => {
5
+ const match = fieldPattern.exec(value.trim());
6
+ if (!match)
7
+ throw new Error(`Champ invalide : ${value}. Format attendu : nom:type, avec ? (nullable) ou ! (unique).`);
8
+ const [, rawName, rawType, optional, unique] = match;
9
+ const name = `${rawName[0].toLowerCase()}${rawName.slice(1)}`;
10
+ if (name === 'id')
11
+ throw new Error('Le champ id est géré par NestGen et ne doit pas être déclaré.');
12
+ if (names.has(name))
13
+ throw new Error(`Le champ ${name} est déclaré plusieurs fois.`);
14
+ names.add(name);
15
+ return {
16
+ name,
17
+ type: rawType.toLowerCase(),
18
+ nullable: Boolean(optional),
19
+ unique: Boolean(unique),
20
+ };
21
+ });
22
+ if (!fields.length)
23
+ throw new Error('Une ressource requiert au moins un champ métier.');
24
+ return fields;
25
+ }
26
+ export function typescriptType(field) {
27
+ return field.type === 'number'
28
+ ? 'number'
29
+ : field.type === 'boolean'
30
+ ? 'boolean'
31
+ : field.type === 'date'
32
+ ? 'Date'
33
+ : 'string';
34
+ }
35
+ export function prismaType(field) {
36
+ const type = field.type === 'number'
37
+ ? 'Float'
38
+ : field.type === 'boolean'
39
+ ? 'Boolean'
40
+ : field.type === 'date'
41
+ ? 'DateTime'
42
+ : 'String';
43
+ return `${type}${field.nullable ? '?' : ''}${field.unique ? ' @unique' : ''}`;
44
+ }