generated from fahricansecer/boilerplate-be
Initial commit
This commit is contained in:
128
src/common/base/base.controller.ts
Normal file
128
src/common/base/base.controller.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import {
|
||||
Get,
|
||||
Post,
|
||||
Put,
|
||||
Delete,
|
||||
Param,
|
||||
Query,
|
||||
Body,
|
||||
HttpCode,
|
||||
ParseUUIDPipe,
|
||||
} from '@nestjs/common';
|
||||
import {
|
||||
ApiOperation,
|
||||
ApiOkResponse,
|
||||
ApiNotFoundResponse,
|
||||
ApiBadRequestResponse,
|
||||
} from '@nestjs/swagger';
|
||||
import { BaseService } from './base.service';
|
||||
import { PaginationDto } from '../dto/pagination.dto';
|
||||
import {
|
||||
ApiResponse,
|
||||
createSuccessResponse,
|
||||
createPaginatedResponse,
|
||||
} from '../types/api-response.type';
|
||||
|
||||
/**
|
||||
* Generic base controller with common CRUD endpoints
|
||||
* Extend this class for entity-specific controllers
|
||||
*
|
||||
* Note: Use decorators like @Controller() on the child class
|
||||
*/
|
||||
export abstract class BaseController<T, CreateDto, UpdateDto> {
|
||||
constructor(
|
||||
protected readonly service: BaseService<T, CreateDto, UpdateDto>,
|
||||
protected readonly entityName: string,
|
||||
) {}
|
||||
|
||||
@Get()
|
||||
@HttpCode(200)
|
||||
@ApiOperation({ summary: 'Get all records with pagination' })
|
||||
@ApiOkResponse({ description: 'Records retrieved successfully' })
|
||||
async findAll(
|
||||
@Query() pagination: PaginationDto,
|
||||
): Promise<ApiResponse<{ items: T[]; meta: any }>> {
|
||||
const result = await this.service.findAll(pagination);
|
||||
return createPaginatedResponse(
|
||||
result.items,
|
||||
result.meta.total,
|
||||
result.meta.page,
|
||||
result.meta.limit,
|
||||
`${this.entityName} list retrieved successfully`,
|
||||
);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@HttpCode(200)
|
||||
@ApiOperation({ summary: 'Get a record by ID' })
|
||||
@ApiOkResponse({ description: 'Record retrieved successfully' })
|
||||
@ApiNotFoundResponse({ description: 'Record not found' })
|
||||
async findOne(
|
||||
@Param('id', ParseUUIDPipe) id: string,
|
||||
): Promise<ApiResponse<T>> {
|
||||
const result = await this.service.findOne(id);
|
||||
return createSuccessResponse(
|
||||
result,
|
||||
`${this.entityName} retrieved successfully`,
|
||||
);
|
||||
}
|
||||
|
||||
@Post()
|
||||
@HttpCode(200)
|
||||
@ApiOperation({ summary: 'Create a new record' })
|
||||
@ApiOkResponse({ description: 'Record created successfully' })
|
||||
@ApiBadRequestResponse({ description: 'Validation failed' })
|
||||
async create(@Body() createDto: CreateDto): Promise<ApiResponse<T>> {
|
||||
const result = await this.service.create(createDto);
|
||||
return createSuccessResponse(
|
||||
result,
|
||||
`${this.entityName} created successfully`,
|
||||
201,
|
||||
);
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
@HttpCode(200)
|
||||
@ApiOperation({ summary: 'Update an existing record' })
|
||||
@ApiOkResponse({ description: 'Record updated successfully' })
|
||||
@ApiNotFoundResponse({ description: 'Record not found' })
|
||||
async update(
|
||||
@Param('id', ParseUUIDPipe) id: string,
|
||||
@Body() updateDto: UpdateDto,
|
||||
): Promise<ApiResponse<T>> {
|
||||
const result = await this.service.update(id, updateDto);
|
||||
return createSuccessResponse(
|
||||
result,
|
||||
`${this.entityName} updated successfully`,
|
||||
);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@HttpCode(200)
|
||||
@ApiOperation({ summary: 'Delete a record (soft delete)' })
|
||||
@ApiOkResponse({ description: 'Record deleted successfully' })
|
||||
@ApiNotFoundResponse({ description: 'Record not found' })
|
||||
async delete(
|
||||
@Param('id', ParseUUIDPipe) id: string,
|
||||
): Promise<ApiResponse<T>> {
|
||||
const result = await this.service.delete(id);
|
||||
return createSuccessResponse(
|
||||
result,
|
||||
`${this.entityName} deleted successfully`,
|
||||
);
|
||||
}
|
||||
|
||||
@Post(':id/restore')
|
||||
@HttpCode(200)
|
||||
@ApiOperation({ summary: 'Restore a soft-deleted record' })
|
||||
@ApiOkResponse({ description: 'Record restored successfully' })
|
||||
async restore(
|
||||
@Param('id', ParseUUIDPipe) id: string,
|
||||
): Promise<ApiResponse<T>> {
|
||||
const result = await this.service.restore(id);
|
||||
return createSuccessResponse(
|
||||
result,
|
||||
`${this.entityName} restored successfully`,
|
||||
);
|
||||
}
|
||||
}
|
||||
165
src/common/base/base.service.ts
Normal file
165
src/common/base/base.service.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
import { NotFoundException, Logger } from '@nestjs/common';
|
||||
import { PrismaService } from '../../database/prisma.service';
|
||||
import { PaginationDto } from '../dto/pagination.dto';
|
||||
import { PaginationMeta } from '../types/api-response.type';
|
||||
|
||||
/**
|
||||
* Generic base service with common CRUD operations
|
||||
* Extend this class for entity-specific services
|
||||
*/
|
||||
export abstract class BaseService<T, CreateDto, UpdateDto> {
|
||||
protected readonly logger: Logger;
|
||||
|
||||
constructor(
|
||||
protected readonly prisma: PrismaService,
|
||||
protected readonly modelName: string,
|
||||
) {
|
||||
this.logger = new Logger(`${modelName}Service`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Prisma model delegate
|
||||
*/
|
||||
protected get model() {
|
||||
return (this.prisma as any)[this.modelName.toLowerCase()];
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all records with pagination
|
||||
*/
|
||||
async findAll(
|
||||
pagination: PaginationDto,
|
||||
where?: any,
|
||||
): Promise<{ items: T[]; meta: PaginationMeta }> {
|
||||
const { skip, take, orderBy } = pagination;
|
||||
|
||||
const [items, total] = await Promise.all([
|
||||
this.model.findMany({
|
||||
where,
|
||||
skip,
|
||||
take,
|
||||
orderBy,
|
||||
}),
|
||||
this.model.count({ where }),
|
||||
]);
|
||||
|
||||
const totalPages = Math.ceil(total / take);
|
||||
|
||||
return {
|
||||
items,
|
||||
meta: {
|
||||
total,
|
||||
page: pagination.page || 1,
|
||||
limit: pagination.limit || 10,
|
||||
totalPages,
|
||||
hasNextPage: (pagination.page || 1) < totalPages,
|
||||
hasPreviousPage: (pagination.page || 1) > 1,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a single record by ID
|
||||
*/
|
||||
async findOne(id: string, include?: any): Promise<T> {
|
||||
const record = await this.model.findUnique({
|
||||
where: { id },
|
||||
include,
|
||||
});
|
||||
|
||||
if (!record) {
|
||||
throw new NotFoundException(`${this.modelName} not found`);
|
||||
}
|
||||
|
||||
return record;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a single record by custom criteria
|
||||
*/
|
||||
findOneBy(where: any, include?: any): Promise<T | null> {
|
||||
return this.model.findFirst({
|
||||
where,
|
||||
include,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new record
|
||||
*/
|
||||
create(data: CreateDto, include?: any): Promise<T> {
|
||||
return this.model.create({
|
||||
data,
|
||||
include,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing record
|
||||
*/
|
||||
async update(id: string, data: UpdateDto, include?: any): Promise<T> {
|
||||
// Check if record exists
|
||||
await this.findOne(id);
|
||||
|
||||
return this.model.update({
|
||||
where: { id },
|
||||
data,
|
||||
include,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Soft delete a record (sets deletedAt)
|
||||
*/
|
||||
async delete(id: string): Promise<T> {
|
||||
// Check if record exists
|
||||
await this.findOne(id);
|
||||
|
||||
return this.model.delete({
|
||||
where: { id },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Hard delete a record (permanently removes)
|
||||
*/
|
||||
async hardDelete(id: string): Promise<T> {
|
||||
// Check if record exists
|
||||
await this.findOne(id);
|
||||
|
||||
return this.prisma.hardDelete(this.modelName, { id });
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore a soft-deleted record
|
||||
*/
|
||||
async restore(id: string): Promise<T> {
|
||||
return this.prisma.restore(this.modelName, { id });
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a record exists
|
||||
*/
|
||||
async exists(id: string): Promise<boolean> {
|
||||
const count = await this.model.count({
|
||||
where: { id },
|
||||
});
|
||||
return count > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count records matching criteria
|
||||
*/
|
||||
count(where?: any): Promise<number> {
|
||||
return this.model.count({ where });
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a transaction
|
||||
*/
|
||||
transaction<R>(fn: (prisma: PrismaService) => Promise<R>): Promise<R> {
|
||||
return this.prisma.$transaction(async (tx) => {
|
||||
return fn(tx as unknown as PrismaService);
|
||||
});
|
||||
}
|
||||
}
|
||||
2
src/common/base/index.ts
Normal file
2
src/common/base/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './base.service';
|
||||
export * from './base.controller';
|
||||
Reference in New Issue
Block a user