improve entity/crud generation

This commit is contained in:
Roland Schneider 2025-11-20 00:03:36 +01:00
parent 0a26f8a28e
commit d5825644a4
11 changed files with 261 additions and 300 deletions

View File

@ -186,23 +186,33 @@ private async generateTableComponent(tableName: string, columns: TableColumn[],
const singular = this.toSingular(tableName);
const pascal = this.toPascalCase(singular);
return {
singular: singular,
plural: tableName,
singular: this.toKebabCase(singular),
plural: this.toKebabCase(tableName),
pascal: pascal,
camel: this.toCamelCase(singular),
title: pascal,
kebab: singular,
kebab: this.toKebabCase(singular),
};
}
private toSingular(name: string): string {
if (name.endsWith('ies')) return name.slice(0, -3) + 'y';
return name.endsWith('s') ? name.slice(0, -1) : name;
const kebab = this.toKebabCase(name);
if (kebab.endsWith('ies')) return kebab.slice(0, -3) + 'y';
return kebab.endsWith('s') ? kebab.slice(0, -1) : kebab;
}
private toPascalCase(text: string): string {
return text.replace(/(^\w|-\w|_w)/g, (c) => c.replace(/[-_]/, '').toUpperCase());
return this.toKebabCase(text)
.split('-')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join('');
}
private toCamelCase(text: string): string {
const pascal = this.toPascalCase(text);
return pascal.charAt(0).toLowerCase() + pascal.slice(1);
}
private toKebabCase(text: string): string {
return text
.replace(/_/g, '-') // Replace underscores with hyphens
.replace(/([a-z0-9]|(?=[A-Z]))([A-Z])/g, '$1-$2') // Add hyphen before uppercase letters
.toLowerCase();
}
}

View File

@ -1,370 +1,131 @@
// src/services/crud-generator.service.ts
import { Injectable } from '@nestjs/common';
import { ConfigService } from './config.service';
import { ModuleUpdaterService } from './module-updater.service';
import { TemplateService } from './template.service';
import { TableColumn } from 'typeorm';
import * as path from 'path';
import * as fs from 'fs';
import { promises as fsPromises } from 'fs';
import { ModuleUpdaterService } from './module-updater.service';
import { TableColumn } from 'typeorm';
interface NamingConvention {
[key: string]: string;
singular: string;
plural: string;
pascal: string;
camel: string;
searchableFields
searchableFields: string;
}
@Injectable()
export class CrudGeneratorService {
constructor(private readonly configService: ConfigService, private readonly moduleUpdaterService: ModuleUpdaterService,) {
}
constructor(
private readonly configService: ConfigService,
private readonly moduleUpdaterService: ModuleUpdaterService,
private readonly templateService: TemplateService,
) {}
public async generate(tableName: string, columns?: TableColumn[]): Promise<void> {
console.log(`Generating CRUD module for table: ${tableName}...`);
const names = this.getNamingConvention(tableName,columns);
const names = this.getNamingConvention(tableName, columns);
const config = this.configService.get();
const serverRoot = path.resolve(process.cwd(), config.server.path);
// Use the CORRECT kebab-cased name for the directory
const moduleDir = path.join(serverRoot, 'src', names.singular);
try {
// Calculate entity paths relative to their destination files here.
const entityFullPath = path.join(serverRoot, config.server.entitiesPath, `${names.singular}.entity.ts`);
names['entityPathForModule'] = path.relative(moduleDir, entityFullPath).replace(/\\/g, '/').replace('.ts', '');
names['entityPathForDtos'] = path.relative(path.join(moduleDir, 'dto'), entityFullPath).replace(/\\/g, '/').replace('.ts', '');
await this.generateModuleFile(names, moduleDir);
await this.generateControllerFile(names, moduleDir);
await this.generateServiceFile(names, moduleDir);
await this.generateDtoFiles(names, moduleDir);
console.log(`✅ CRUD module for "${tableName}" created successfully in: ${moduleDir}`);
// --- 3. CALL THE UPDATER ---
const moduleFileName = `${names.plural}.module.ts`;
const fullModulePath = path.join(moduleDir, moduleFileName);
await this.moduleUpdaterService.addImportToAppModule(`${names.pascal}sModule`, fullModulePath);
// --- END OF CALL ---
// We can now remove the old warning!
console.log(`✅ CRUD module for "${tableName}" created successfully in: ${moduleDir}`);
console.log('\n✨ AppModule has been updated automatically! ✨');
} catch (error) {
console.error(`❌ An error occurred during CRUD generation:`, error.message);
}
}
private isStringBasedType(dbType: string): boolean {
return dbType.includes('char') || dbType.includes('text') || dbType === 'uuid';
}
private async generateModuleFile(names: NamingConvention, moduleDir: string) {
const template = this.getModuleTemplate(names);
// Use the pre-calculated path
const content = this.templateService.render('nestjs/module.ts.tpl', { ...names, entityPath: names['entityPathForModule'] });
const filePath = path.join(moduleDir, `${names.plural}.module.ts`);
await fsPromises.mkdir(moduleDir, { recursive: true });
fs.writeFileSync(filePath, template);
fs.writeFileSync(filePath, content);
}
private async generateControllerFile(names: NamingConvention, moduleDir: string) {
const template = this.getControllerTemplate(names);
const content = this.templateService.render('nestjs/controller.ts.tpl', names);
const filePath = path.join(moduleDir, `${names.plural}.controller.ts`);
await fsPromises.mkdir(moduleDir, { recursive: true });
fs.writeFileSync(filePath, template);
fs.writeFileSync(filePath, content);
}
private async generateServiceFile(names: NamingConvention, moduleDir: string) {
const template = this.getServiceTemplate(names);
// Use the pre-calculated path
const content = this.templateService.render('nestjs/service.ts.tpl', { ...names, entityPath: names['entityPathForModule'] });
const filePath = path.join(moduleDir, `${names.plural}.service.ts`);
await fsPromises.mkdir(moduleDir, { recursive: true });
fs.writeFileSync(filePath, template);
fs.writeFileSync(filePath, content);
}
// --- END OF MISSING METHODS ---
private async generateDtoFiles(names: NamingConvention, moduleDir: string) {
const dtoDir = path.join(moduleDir, 'dto');
await fsPromises.mkdir(dtoDir, { recursive: true });
fs.writeFileSync(path.join(dtoDir, `create-${names.singular}.dto.ts`), this.getCreateDtoTemplate(names));
fs.writeFileSync(path.join(dtoDir, `update-${names.singular}.dto.ts`), this.getUpdateDtoTemplate(names));
fs.writeFileSync(path.join(dtoDir, `query-${names.singular}.dto.ts`), this.getQueryDtoTemplate(names));
// Use the pre-calculated path
const dtoNames = { ...names, entityPath: names['entityPathForDtos'] };
fs.writeFileSync(path.join(dtoDir, `create-${names.singular}.dto.ts`), this.templateService.render('nestjs/create-dto.ts.tpl', dtoNames));
fs.writeFileSync(path.join(dtoDir, `update-${names.singular}.dto.ts`), this.templateService.render('nestjs/update-dto.ts.tpl', dtoNames));
fs.writeFileSync(path.join(dtoDir, `query-${names.singular}.dto.ts`), this.templateService.render('nestjs/query-dto.ts.tpl', dtoNames));
}
// --- TEMPLATES ---
// (All the template methods from the previous response go here, unchanged)
// --- NAMING HELPERS ---
private getQueryDtoTemplate(names: NamingConvention): string {
return `import { IsOptional, IsString, IsNumber, IsIn, IsBoolean } from 'class-validator';
import { Type } from 'class-transformer';
export class Query${names.pascal}Dto {
@IsOptional()
@Type(() => Number)
@IsNumber()
page?: number;
@IsOptional()
@Type(() => Number)
@IsNumber()
limit?: number;
@IsOptional()
@IsString()
sortBy?: string; // Should be a property of the ${names.pascal} entity
@IsOptional()
@IsIn(['ASC', 'DESC'])
order?: 'ASC' | 'DESC';
// --- Add other filterable properties below ---
// @IsOptional()
// @IsString()
// name?: string;
// @IsOptional()
// @Type(() => Boolean)
// @IsBoolean()
// is_available?: boolean;
}
`;
}
private getControllerTemplate(names: NamingConvention): string {
return `import { Controller, Get, Post, Body, Patch, Param, Delete, Query, ParseIntPipe, DefaultValuePipe } from '@nestjs/common';
import { ${names.pascal}sService } from './${names.plural}.service';
import { Create${names.pascal}Dto } from './dto/create-${names.singular}.dto';
import { Update${names.pascal}Dto } from './dto/update-${names.singular}.dto';
import { Query${names.pascal}Dto } from './dto/query-${names.singular}.dto';
@Controller('${names.plural}')
export class ${names.pascal}sController {
constructor(private readonly ${names.camel}sService: ${names.pascal}sService) {}
@Post()
create(@Body() create${names.pascal}Dto: Create${names.pascal}Dto) {
return this.${names.camel}sService.create(create${names.pascal}Dto);
}
@Get()
findAll(@Query() queryParams: Query${names.pascal}Dto) {
return this.${names.camel}sService.findAll(queryParams);
}
@Get('search')
search(
@Query('q') term: string,
@Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number,
@Query('limit', new DefaultValuePipe(10), ParseIntPipe) limit: number,
) {
return this.${names.camel}sService.search(term, { page, limit });
}
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
return this.${names.camel}sService.findOne(id);
}
@Patch(':id')
update(@Param('id', ParseIntPipe) id: number, @Body() update${names.pascal}Dto: Update${names.pascal}Dto) {
return this.${names.camel}sService.update(id, update${names.pascal}Dto);
}
@Delete(':id')
remove(@Param('id', ParseIntPipe) id: number) {
return this.${names.camel}sService.remove(id);
}
}
`;
}
// In src/services/crud-generator.service.ts
private getServiceTemplate(names: NamingConvention): string {
const entityPath = path.relative(path.join('src', names.singular), path.join(this.configService.get().server.entitiesPath, `${names.singular}.entity`)).replace(/\\/g, '/');
return `import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, FindManyOptions, FindOptionsWhere, ILike } from 'typeorm';
import { Create${names.pascal}Dto } from './dto/create-${names.singular}.dto';
import { Update${names.pascal}Dto } from './dto/update-${names.singular}.dto';
import { Query${names.pascal}Dto } from './dto/query-${names.singular}.dto';
import { ${names.pascal} } from '${entityPath}';
type QueryConfigItem = {
param: keyof Omit<Query${names.pascal}Dto, 'page' | 'limit' | 'sortBy' | 'order'>;
dbField: keyof ${names.pascal};
operator: 'equals' | 'like';
};
@Injectable()
export class ${names.pascal}sService {
constructor(
@InjectRepository(${names.pascal})
private readonly ${names.camel}Repository: Repository<${names.pascal}>,
) {}
private readonly searchableFields: (keyof ${names.pascal})[] = [
${names.searchableFields}
];
create(create${names.pascal}Dto: Create${names.pascal}Dto) {
const newRecord = this.${names.camel}Repository.create(create${names.pascal}Dto);
return this.${names.camel}Repository.save(newRecord);
}
async findAll(queryParams: Query${names.pascal}Dto) {
const { page = 1, limit = 0, sortBy, order, ...filters } = queryParams;
const queryConfig: QueryConfigItem[] = [
// Example: { param: 'name', dbField: 'name', operator: 'like' },
// Example: { param: 'is_available', dbField: 'is_available', operator: 'equals' },
];
// --- START OF THE FIX ---
// 1. Create a loosely typed object to build the where clause.
const whereClause: { [key: string]: any } = {};
// 2. Populate it dynamically. This avoids the TypeScript error.
for (const config of queryConfig) {
if (filters[config.param] !== undefined) {
if (config.operator === 'like') {
whereClause[config.dbField] = ILike(\`%\${filters[config.param]}%\`);
} else {
whereClause[config.dbField] = filters[config.param];
}
}
}
// 3. Assign the complete, built clause to the strongly-typed options.
const findOptions: FindManyOptions<${names.pascal}> = {
where: whereClause as FindOptionsWhere<${names.pascal}>,
};
// --- END OF THE FIX ---
const paginated = limit > 0;
if (paginated) {
findOptions.skip = (page - 1) * limit;
findOptions.take = limit;
}
if (sortBy && order) {
findOptions.order = { [sortBy]: order };
}
const [data, totalItems] = await this.${names.camel}Repository.findAndCount(findOptions);
if (!paginated) {
return { data, total: data.length };
}
return {
data,
meta: {
totalItems,
itemCount: data.length,
itemsPerPage: limit,
totalPages: Math.ceil(totalItems / limit),
currentPage: page,
},
};
}
async search(term: string, options: { page: number; limit: number }) {
if (this.searchableFields.length === 0) {
console.warn('Search is not configured for this entity. Please populate the searchableFields array in the service.');
return { data: [], meta: { totalItems: 0, itemCount: 0, itemsPerPage: options.limit, totalPages: 0, currentPage: options.page } };
}
const whereConditions = this.searchableFields.map(field => ({
[field]: ILike('%'+term+'%'),
}));
const [data, totalItems] = await this.${names.camel}Repository.findAndCount({
where: whereConditions,
skip: (options.page - 1) * options.limit,
take: options.limit,
});
return {
data,
meta: {
totalItems,
itemCount: data.length,
itemsPerPage: options.limit,
totalPages: Math.ceil(totalItems / options.limit),
currentPage: options.page,
},
};
}
async findOne(id: number) {
const record = await this.${names.camel}Repository.findOneBy({ id: id as any });
if (!record) {
throw new NotFoundException(\`${names.pascal} with ID \${id} not found\`);
}
return record;
}
async update(id: number, update${names.pascal}Dto: Update${names.pascal}Dto) {
const record = await this.findOne(id);
Object.assign(record, update${names.pascal}Dto);
return this.${names.camel}Repository.save(record);
}
async remove(id: number) {
const result = await this.${names.camel}Repository.delete(id);
if (result.affected === 0) {
throw new NotFoundException(\`${names.pascal} with ID \${id} not found\`);
}
return { deleted: true, id };
}
}
`;
}
private getModuleTemplate(names: NamingConvention): string {
const entityPath = path.relative(path.join('src', names.singular), path.join(this.configService.get().server.entitiesPath, `${names.singular}.entity`)).replace(/\\/g, '/');
return `import { Module } from '@nestjs/common';\nimport { TypeOrmModule } from '@nestjs/typeorm';\nimport { ${names.pascal}sService } from './${names.plural}.service';\nimport { ${names.pascal}sController } from './${names.plural}.controller';\nimport { ${names.pascal} } from '${entityPath}';\n\n@Module({\n imports: [TypeOrmModule.forFeature([${names.pascal}])],\n controllers: [${names.pascal}sController],\n providers: [${names.pascal}sService],\n})\nexport class ${names.pascal}sModule {}\n`;
}
private getCreateDtoTemplate(names: NamingConvention): string {
const entityPath = path.relative(path.join('src', names.singular, 'dto'), path.join(this.configService.get().server.entitiesPath, `${names.singular}.entity`)).replace(/\\/g, '/');
return `import { OmitType } from '@nestjs/mapped-types';\nimport { ${names.pascal} } from '${entityPath}';\n\n// NOTE: Use class-validator decorators here for production-grade validation.\nexport class Create${names.pascal}Dto extends OmitType(${names.pascal}, ['id']) {}\n`;
}
private getUpdateDtoTemplate(names: NamingConvention): string {
return `import { PartialType } from '@nestjs/mapped-types';\nimport { Create${names.pascal}Dto } from './create-${names.singular}.dto';\n\nexport class Update${names.pascal}Dto extends PartialType(Create${names.pascal}Dto) {}\n`;
private isStringBasedType(dbType: string): boolean {
return dbType.includes('char') || dbType.includes('text') || dbType === 'uuid';
}
private getNamingConvention(tableName: string, columns?: TableColumn[]): NamingConvention {
const singular = this.toSingular(tableName);
const pascal = this.toPascalCase(singular);
const searchableFields = (columns || [])
.filter(c => this.isStringBasedType(c.type))
.map(c => `'${c.name}'`); // Wrap in quotes for the array
.map(c => `'${c.name}'`)
.join(',\n ');
return {
singular: singular,
plural: tableName,
pascal: this.toPascalCase(singular),
singular: this.toKebabCase(singular),
plural: this.toKebabCase(tableName),
pascal: pascal,
camel: this.toCamelCase(singular),
searchableFields
searchableFields: searchableFields
};
}
private toSingular(name: string): string {
if (name.endsWith('ies')) return name.slice(0, -3) + 'y';
return name.endsWith('s') ? name.slice(0, -1) : name;
const kebab = this.toKebabCase(name);
if (kebab.endsWith('ies')) return kebab.slice(0, -3) + 'y';
return kebab.endsWith('s') ? kebab.slice(0, -1) : kebab;
}
private toPascalCase(text: string): string {
return text.replace(/(^\w|-\w|_w)/g, (c) => c.replace(/[-_]/, '').toUpperCase());
return this.toKebabCase(text).split('-').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join('');
}
private toCamelCase(text: string): string {
const pascal = this.toPascalCase(text);
return pascal.charAt(0).toLowerCase() + pascal.slice(1);
}
private toKebabCase(text: string): string {
return text.replace(/_/g, '-').replace(/([a-z0-9]|(?=[A-Z]))([A-Z])/g, '$1-$2').toLowerCase();
}
}

View File

@ -187,6 +187,21 @@ export class ${className} {${props}
}
private toPascalCase(text: string): string {
return text.replace(/(^\w|-\w|_w)/g, (c) => c.replace(/[-_]/, '').toUpperCase());
return this.toKebabCase(text)
.split('-')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join('');
}
private toCamelCase(text: string): string {
const pascal = this.toPascalCase(text);
return pascal.charAt(0).toLowerCase() + pascal.slice(1);
}
private toKebabCase(text: string): string {
return text
.replace(/_/g, '-') // Replace underscores with hyphens
.replace(/([a-z0-9]|(?=[A-Z]))([A-Z])/g, '$1-$2') // Add hyphen before uppercase letters
.toLowerCase();
}
}

View File

@ -14,8 +14,9 @@ export class {{pascal}}DataProvider implements DataProvider<{{pascal}}> {
private {{camel}}Service = inject({{pascal}}Service);
getData(options?: GetDataOptions): Observable<GetDataResponse<{{pascal}}>> {
// The generic table's params are compatible with our NestJS Query DTO
return this.{{camel}}Service.find(options?.params ?? {}).pipe(
const {q,page,limit} = options?.params ?? {};
// The generic table's params are compatible with our NestJS Query DTO
return this.{{camel}}Service.search(q ?? '',page,limit, ).pipe(
map((res) => {
// Adapt the paginated response to the GetDataResponse format
return { data: res };

View File

@ -12,7 +12,7 @@ import {
ActionDefinition,
GenericActionColumn,
} from '../../../../components/generic-action-column/generic-action-column';
import { ProductService } from '../../services/{{singular}}.service';
import { {{pascal}}Service } from '../../services/{{singular}}.service';
import { BehaviorSubject } from 'rxjs';
@Component({

View File

@ -0,0 +1,44 @@
import { Controller, Get, Post, Body, Patch, Param, Delete, Query, ParseIntPipe, DefaultValuePipe } from '@nestjs/common';
import { {{pascal}}sService } from './{{plural}}.service';
import { Create{{pascal}}Dto } from './dto/create-{{singular}}.dto';
import { Update{{pascal}}Dto } from './dto/update-{{singular}}.dto';
import { Query{{pascal}}Dto } from './dto/query-{{singular}}.dto';
@Controller('{{plural}}')
export class {{pascal}}sController {
constructor(private readonly {{camel}}sService: {{pascal}}sService) {}
@Post()
create(@Body() create{{pascal}}Dto: Create{{pascal}}Dto) {
return this.{{camel}}sService.create(create{{pascal}}Dto);
}
@Get()
findAll(@Query() queryParams: Query{{pascal}}Dto) {
return this.{{camel}}sService.findAll(queryParams);
}
@Get('search')
search(
@Query('q') term: string,
@Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number,
@Query('limit', new DefaultValuePipe(10), ParseIntPipe) limit: number,
) {
return this.{{camel}}sService.search(term, { page, limit });
}
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
return this.{{camel}}sService.findOne(id);
}
@Patch(':id')
update(@Param('id', ParseIntPipe) id: number, @Body() update{{pascal}}Dto: Update{{pascal}}Dto) {
return this.{{camel}}sService.update(id, update{{pascal}}Dto);
}
@Delete(':id')
remove(@Param('id', ParseIntPipe) id: number) {
return this.{{camel}}sService.remove(id);
}
}

View File

@ -0,0 +1,4 @@
import { OmitType } from '@nestjs/mapped-types';
import { {{pascal}} } from '{{entityPath}}';
export class Create{{pascal}}Dto extends OmitType({{pascal}}, ['id']) {}

View File

@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { {{pascal}}sService } from './{{plural}}.service';
import { {{pascal}}sController } from './{{plural}}.controller';
import { {{pascal}} } from '{{entityPath}}';
@Module({
imports: [TypeOrmModule.forFeature([{{pascal}}])],
controllers: [{{pascal}}sController],
providers: [{{pascal}}sService],
})
export class {{pascal}}sModule {}

View File

@ -0,0 +1,9 @@
import { IsOptional, IsString, IsNumber, IsIn, IsBoolean } from 'class-validator';
import { Type } from 'class-transformer';
export class Query{{pascal}}Dto {
@IsOptional() @Type(() => Number) @IsNumber() page?: number;
@IsOptional() @Type(() => Number) @IsNumber() limit?: number;
@IsOptional() @IsString() sortBy?: string;
@IsOptional() @IsIn(['ASC', 'DESC']) order?: 'ASC' | 'DESC';
}

View File

@ -0,0 +1,101 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, FindManyOptions, FindOptionsWhere, ILike } from 'typeorm';
import { Create{{pascal}}Dto } from './dto/create-{{singular}}.dto';
import { Update{{pascal}}Dto } from './dto/update-{{singular}}.dto';
import { Query{{pascal}}Dto } from './dto/query-{{singular}}.dto';
import { {{pascal}} } from '{{entityPath}}';
type QueryConfigItem = {
param: keyof Omit<Query{{pascal}}Dto, 'page' | 'limit' | 'sortBy' | 'order'>;
dbField: keyof {{pascal}};
operator: 'equals' | 'like';
};
@Injectable()
export class {{pascal}}sService {
constructor(
@InjectRepository({{pascal}})
private readonly {{camel}}Repository: Repository<{{pascal}}>,
) {}
private readonly searchableFields: (keyof {{pascal}})[] = [
{{searchableFields}}
];
create(create{{pascal}}Dto: Create{{pascal}}Dto) {
const newRecord = this.{{camel}}Repository.create(create{{pascal}}Dto);
return this.{{camel}}Repository.save(newRecord);
}
async findAll(queryParams: Query{{pascal}}Dto) {
const { page = 1, limit = 0, sortBy, order, ...filters } = queryParams;
const queryConfig: QueryConfigItem[] = [];
const whereClause: { [key: string]: any } = {};
for (const config of queryConfig) {
if (filters[config.param] !== undefined) {
if (config.operator === 'like') {
whereClause[config.dbField] = ILike(`%${filters[config.param]}%`);
} else {
whereClause[config.dbField] = filters[config.param];
}
}
}
const findOptions: FindManyOptions<{{pascal}}> = { where: whereClause as FindOptionsWhere<{{pascal}}> };
const paginated = limit > 0;
if (paginated) {
findOptions.skip = (page - 1) * limit;
findOptions.take = limit;
}
if (sortBy && order) {
findOptions.order = { [sortBy]: order };
}
const [data, totalItems] = await this.{{camel}}Repository.findAndCount(findOptions);
if (!paginated) {
return { data, total: data.length };
}
return {
data,
meta: { totalItems, itemCount: data.length, itemsPerPage: limit, totalPages: Math.ceil(totalItems / limit), currentPage: page },
};
}
async search(term: string, options: { page: number; limit: number }) {
if (this.searchableFields.length === 0) {
console.warn('Search is not configured for this entity.');
return { data: [], meta: { totalItems: 0, itemCount: 0, itemsPerPage: options.limit, totalPages: 0, currentPage: options.page } };
}
const whereConditions = this.searchableFields.map(field => ({ [field]: ILike(`%${term}%`) }));
const [data, totalItems] = await this.{{camel}}Repository.findAndCount({
where: whereConditions,
skip: (options.page - 1) * options.limit,
take: options.limit,
});
return {
data,
meta: { totalItems, itemCount: data.length, itemsPerPage: options.limit, totalPages: Math.ceil(totalItems / options.limit), currentPage: options.page },
};
}
async findOne(id: number) {
const record = await this.{{camel}}Repository.findOneBy({ id: id as any });
if (!record) {
throw new NotFoundException(`{{pascal}} with ID ${id} not found`);
}
return record;
}
async update(id: number, update{{pascal}}Dto: Update{{pascal}}Dto) {
const record = await this.findOne(id);
Object.assign(record, update{{pascal}}Dto);
return this.{{camel}}Repository.save(record);
}
async remove(id: number) {
const result = await this.{{camel}}Repository.delete(id);
if (result.affected === 0) {
throw new NotFoundException(`{{pascal}} with ID ${id} not found`);
}
return { deleted: true, id };
}
}

View File

@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/mapped-types';
import { Create{{pascal}}Dto } from './create-{{singular}}.dto';
export class Update{{pascal}}Dto extends PartialType(Create{{pascal}}Dto) {}