Initial commit
This commit is contained in:
28
src/app.controller.spec.ts
Normal file
28
src/app.controller.spec.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { AppController } from './app.controller';
|
||||
import { AppService } from './app.service';
|
||||
|
||||
describe('AppController', () => {
|
||||
let appController: AppController;
|
||||
|
||||
beforeEach(async () => {
|
||||
const app: TestingModule = await Test.createTestingModule({
|
||||
controllers: [AppController],
|
||||
providers: [AppService],
|
||||
}).compile();
|
||||
|
||||
appController = app.get<AppController>(AppController);
|
||||
});
|
||||
|
||||
describe('healthcheck', () => {
|
||||
it('should return "OK"', () => {
|
||||
expect(appController.healthcheck()).toBe('OK');
|
||||
});
|
||||
});
|
||||
|
||||
describe('version', () => {
|
||||
it('should return "OK"', () => {
|
||||
expect(appController.healthcheck()).toBe(process.env.APP_VERSION);
|
||||
});
|
||||
});
|
||||
});
|
||||
17
src/app.controller.ts
Normal file
17
src/app.controller.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
import { AppService } from './app.service';
|
||||
|
||||
@Controller()
|
||||
export class AppController {
|
||||
constructor(private readonly appService: AppService) {}
|
||||
|
||||
@Get('/healthcheck')
|
||||
healthcheck(): any {
|
||||
return this.appService.healthcheck();
|
||||
}
|
||||
|
||||
@Get('/version')
|
||||
version(): any {
|
||||
return this.appService.version();
|
||||
}
|
||||
}
|
||||
57
src/app.module.ts
Normal file
57
src/app.module.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import {
|
||||
Logger,
|
||||
MiddlewareConsumer,
|
||||
Module,
|
||||
NestModule,
|
||||
RequestMethod,
|
||||
} from '@nestjs/common';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { APP_FILTER } from '@nestjs/core';
|
||||
import { AppController } from './app.controller';
|
||||
import { AppService } from './app.service';
|
||||
import { UsersModule } from './users/users.module';
|
||||
import { AllExceptionsFilter } from './logger/any-exception.filter';
|
||||
import loggerMiddleware from './logger/logger.middleware';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { StaffsModule } from './staffs/staffs.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
// For Configuration
|
||||
ConfigModule.forRoot(),
|
||||
// For ORM-MySQL
|
||||
TypeOrmModule.forRoot({
|
||||
type: 'mysql',
|
||||
host: 'localhost',
|
||||
port: 3306,
|
||||
username: 'root',
|
||||
password: 'example',
|
||||
database: 'example_nodejs_nest_crud',
|
||||
// entities: ['dist/modules/**/*.mysql.entity{.ts,.js}'],
|
||||
autoLoadEntities: true,
|
||||
// IMPORTANT: disable sync
|
||||
// synchronize: false,
|
||||
synchronize: true,
|
||||
}),
|
||||
UsersModule,
|
||||
StaffsModule,
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [
|
||||
Logger,
|
||||
AppService,
|
||||
{
|
||||
provide: APP_FILTER,
|
||||
// useClass: HttpExceptionFilter,
|
||||
useClass: AllExceptionsFilter,
|
||||
},
|
||||
],
|
||||
})
|
||||
export class AppModule implements NestModule {
|
||||
// Apply logger middleware for all routes
|
||||
configure(consumer: MiddlewareConsumer) {
|
||||
consumer
|
||||
.apply(loggerMiddleware)
|
||||
.forRoutes({ path: '*', method: RequestMethod.ALL });
|
||||
}
|
||||
}
|
||||
19
src/app.service.ts
Normal file
19
src/app.service.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Logger, Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
@Injectable()
|
||||
export class AppService {
|
||||
constructor(
|
||||
private readonly logger: Logger,
|
||||
private configService: ConfigService,
|
||||
) {}
|
||||
healthcheck() {
|
||||
return 'OK';
|
||||
}
|
||||
version() {
|
||||
this.logger.debug(
|
||||
this.configService.get<string>('APP_VERSION') || 'Not APP_VERSION',
|
||||
);
|
||||
return process.env.APP_VERSION || '0.0.0';
|
||||
}
|
||||
}
|
||||
77
src/logger/any-exception.filter.ts
Normal file
77
src/logger/any-exception.filter.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import {
|
||||
ExceptionFilter,
|
||||
Catch,
|
||||
ArgumentsHost,
|
||||
HttpException,
|
||||
HttpStatus,
|
||||
Logger,
|
||||
} from '@nestjs/common';
|
||||
import { error } from 'console';
|
||||
|
||||
// Catch any exception
|
||||
@Catch()
|
||||
export class AllExceptionsFilter implements ExceptionFilter {
|
||||
constructor(private readonly logger: Logger) {}
|
||||
catch(exception: unknown, host: ArgumentsHost) {
|
||||
const ctx = host.switchToHttp();
|
||||
const response = ctx.getResponse();
|
||||
const request = ctx.getRequest();
|
||||
|
||||
const status =
|
||||
exception instanceof HttpException
|
||||
? exception.getStatus()
|
||||
: HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
|
||||
const { body, url, method, originalUrl, rawHeaders } = request;
|
||||
let message = exception as string;
|
||||
let stack = undefined;
|
||||
|
||||
if (exception instanceof Error) {
|
||||
message = exception.message;
|
||||
// trim the stack lines
|
||||
if (exception.stack) {
|
||||
stack = exception.stack.split('\n').slice(1, 3).join('\n');
|
||||
}
|
||||
}
|
||||
|
||||
// For HTTP Exception, don't log the stack
|
||||
// if (exception instanceof HttpException) {
|
||||
// stack = null;
|
||||
// }
|
||||
|
||||
let logMsg: any;
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
logMsg = {
|
||||
message,
|
||||
request: {
|
||||
method,
|
||||
url,
|
||||
body,
|
||||
// originalUrl,
|
||||
rawHeaders,
|
||||
},
|
||||
};
|
||||
this.logger.error(logMsg, stack, AllExceptionsFilter.name);
|
||||
} else {
|
||||
const requstBody =
|
||||
Object.keys(body).length > 0 ? ` Body=${JSON.stringify(body)}` : '';
|
||||
|
||||
const rawHeadersStr =
|
||||
Object.keys(rawHeaders).length > 0
|
||||
? ` RawHeaders=${JSON.stringify(rawHeaders)}`
|
||||
: '';
|
||||
|
||||
logMsg = `${message}\n${method.toLocaleUpperCase()} ${url}${requstBody}${rawHeadersStr}`;
|
||||
this.logger.error(logMsg, stack, AllExceptionsFilter.name);
|
||||
}
|
||||
|
||||
response.header('Content-Type', 'application/json; charset=utf-8');
|
||||
response.status(status).json({
|
||||
statusCode: status,
|
||||
timestamp: new Date().toISOString(),
|
||||
path: request.url,
|
||||
error: message,
|
||||
});
|
||||
}
|
||||
}
|
||||
88
src/logger/index.ts
Normal file
88
src/logger/index.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* index.ts: logging utilities
|
||||
*
|
||||
* See:
|
||||
* - https://github.com/gremo/nest-winston
|
||||
* - https://github.com/winstonjs/winston
|
||||
*
|
||||
* (C) 2023
|
||||
* Price.com.hk
|
||||
*/
|
||||
|
||||
import * as winston from 'winston';
|
||||
import { utilities as nestWinstonModuleUtilities } from 'nest-winston';
|
||||
|
||||
const { combine, timestamp, printf, colorize } = winston.format;
|
||||
|
||||
// Wrapper of createLogger, with the common options used for Price's usvc
|
||||
export function createLogger(options?: winston.LoggerOptions): winston.Logger {
|
||||
const logLevel = process.env.LOG_LEVEL
|
||||
? process.env.LOG_LEVEL
|
||||
: process.env.NODE_ENV === 'production'
|
||||
? 'info'
|
||||
: 'debug';
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
return winston.createLogger({
|
||||
...options,
|
||||
transports: [new winston.transports.Console()],
|
||||
format: combine(
|
||||
nestWinstonModuleUtilities.format.nestLike(),
|
||||
winston.format.json(),
|
||||
winston.format((info) => {
|
||||
// Remove reduntant 'value' while object is logged instead of msg string
|
||||
if (info.value) {
|
||||
delete info.value;
|
||||
}
|
||||
// Remove timestamp (for changing timestamp to 'time')
|
||||
if (info.timestamp) {
|
||||
delete info.timestamp;
|
||||
return info;
|
||||
}
|
||||
return info;
|
||||
})(),
|
||||
timestamp({
|
||||
// format: 'YYYY-MM-DDTHH:mm:ss.SSSZZ',
|
||||
format: 'YYYY-MM-DDTHH:mm:ssZZ',
|
||||
alias: 'time',
|
||||
}),
|
||||
),
|
||||
level: logLevel,
|
||||
});
|
||||
} else {
|
||||
return winston.createLogger({
|
||||
...options,
|
||||
transports: [new winston.transports.Console()],
|
||||
format: combine(
|
||||
nestWinstonModuleUtilities.format.nestLike(),
|
||||
// timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
|
||||
timestamp({ format: 'HH:mm:ss' }),
|
||||
colorize(),
|
||||
// Format for debug-console, not in JSON meta param is ensured by splat()
|
||||
printf(
|
||||
({
|
||||
context,
|
||||
namespace,
|
||||
module,
|
||||
timestamp,
|
||||
level,
|
||||
message,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
value, // No print
|
||||
...meta
|
||||
}) => {
|
||||
const infoContext = context ? `[${context}] ` : '';
|
||||
const infoString =
|
||||
module || namespace ? `[${module || namespace}] ` : '';
|
||||
return `${timestamp} ${level.padEnd(
|
||||
15,
|
||||
)} ${infoString}${infoContext}${message}${
|
||||
Object.keys(meta).length > 0 ? ' - ' + JSON.stringify(meta) : ''
|
||||
}`;
|
||||
},
|
||||
),
|
||||
),
|
||||
level: logLevel,
|
||||
});
|
||||
}
|
||||
}
|
||||
71
src/logger/logger.middleware.ts
Normal file
71
src/logger/logger.middleware.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import {
|
||||
Inject,
|
||||
Injectable,
|
||||
NestMiddleware,
|
||||
Logger,
|
||||
LoggerService,
|
||||
} from '@nestjs/common';
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
|
||||
@Injectable()
|
||||
export default class LoggerMiddleware implements NestMiddleware {
|
||||
// TODO: Access log if env not production
|
||||
constructor(@Inject(Logger) private readonly logger: LoggerService) {}
|
||||
|
||||
use(req: Request, res: Response, next: NextFunction) {
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
const logObj = {
|
||||
message: 'web-request',
|
||||
request: this.requestInJson(req),
|
||||
};
|
||||
if (process.env.LOG_LEVEL_INFO_FOR_WEB_REQUEST === 'true') {
|
||||
this.logger.log(logObj, LoggerMiddleware.name);
|
||||
} else {
|
||||
this.logger.debug(logObj, LoggerMiddleware.name);
|
||||
}
|
||||
} else {
|
||||
const logMsg = this.requestInLine(req);
|
||||
if (process.env.LOG_LEVEL_INFO_FOR_WEB_REQUEST === 'true') {
|
||||
this.logger.log(logMsg, LoggerMiddleware.name);
|
||||
} else {
|
||||
this.logger.debug(logMsg, LoggerMiddleware.name);
|
||||
}
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
requestInLine(req: Request): string {
|
||||
const { headers, url, method, body, socket } = req;
|
||||
const xRealIp = headers['X-Real-IP'];
|
||||
const xForwardedFor = headers['X-Forwarded-For'];
|
||||
const { ip: cIp } = req;
|
||||
const { remoteAddress } = socket || {};
|
||||
const ip = xRealIp || xForwardedFor || cIp || remoteAddress;
|
||||
|
||||
const requstBody =
|
||||
Object.keys(body).length > 0 ? ` Body=${JSON.stringify(body)}` : '';
|
||||
const requstIp = ip ? ` SrcIP=${ip}` : '';
|
||||
|
||||
return `${method.toLocaleUpperCase()} ${url}${requstBody}${requstIp}`;
|
||||
}
|
||||
|
||||
requestInJson: (req: Request) => {
|
||||
[prop: string]: any;
|
||||
} = (req) => {
|
||||
const { headers, url, method, body, socket } = req;
|
||||
const xRealIp = headers['X-Real-IP'];
|
||||
const xForwardedFor = headers['X-Forwarded-For'];
|
||||
const { ip: cIp } = req;
|
||||
const { remoteAddress } = socket || {};
|
||||
const ip = xRealIp || xForwardedFor || cIp || remoteAddress;
|
||||
|
||||
return {
|
||||
url,
|
||||
host: headers.host,
|
||||
ip,
|
||||
method,
|
||||
body,
|
||||
};
|
||||
};
|
||||
}
|
||||
44
src/main.ts
Normal file
44
src/main.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
|
||||
import { AppModule } from './app.module';
|
||||
import { WinstonModule } from 'nest-winston';
|
||||
|
||||
import { createLogger } from './logger';
|
||||
import { ValidationPipe } from '@nestjs/common';
|
||||
|
||||
const DEFAULT_APP_PORT = 3000;
|
||||
|
||||
async function bootstrap() {
|
||||
const instance = createLogger({});
|
||||
|
||||
const app = await NestFactory.create(AppModule, {
|
||||
logger: WinstonModule.createLogger({ instance }),
|
||||
});
|
||||
|
||||
// Use the shutdown hooks
|
||||
// app.enableShutdownHooks();
|
||||
|
||||
// Use Validation Pipe
|
||||
app.useGlobalPipes(new ValidationPipe());
|
||||
|
||||
// Setup the Swagger if ENABLE_SWAGGER is True OR NODE_ENV is NOT 'production'
|
||||
if (
|
||||
process.env.NODE_ENV !== 'production' ||
|
||||
process.env.ENABLE_SWAGGER === 'true'
|
||||
) {
|
||||
const swaggerDoc = SwaggerModule.createDocument(
|
||||
app,
|
||||
new DocumentBuilder()
|
||||
.setTitle('Basic Nestjs API Service')
|
||||
.setDescription('Boilerplate project of the Basic Nestjs API Service')
|
||||
.setVersion('1.0')
|
||||
.addServer(process.env.SWAGGER_SERVER_PATH)
|
||||
.build(),
|
||||
);
|
||||
SwaggerModule.setup('api', app, swaggerDoc);
|
||||
}
|
||||
|
||||
// Start the web app
|
||||
await app.listen(process.env.APP_PORT || DEFAULT_APP_PORT);
|
||||
}
|
||||
bootstrap();
|
||||
10
src/staffs/README.md
Normal file
10
src/staffs/README.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# Staffs Module
|
||||
|
||||
- This module demonstrates using:
|
||||
- CRUD
|
||||
- TypeORM: MySQL
|
||||
- TypeORM Repository
|
||||
- No QueryBuilder
|
||||
- TypeOrmModule.forFeature & TypeOrmModule.forRoot{ autoLoadEntities: true }
|
||||
- The Entities
|
||||
- DB and Web-API entities are shared
|
||||
36
src/staffs/dto/staff-create.dto.ts
Normal file
36
src/staffs/dto/staff-create.dto.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNotEmpty, IsString, IsNumber, IsOptional } from 'class-validator';
|
||||
export class StaffCreateDto {
|
||||
// NOTE: Since the id is auto-inc, so no id for the creation
|
||||
// id: number;
|
||||
|
||||
@ApiProperty({
|
||||
type: String,
|
||||
name: 'name',
|
||||
description: 'Staff name',
|
||||
required: true,
|
||||
})
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
name: string;
|
||||
|
||||
@ApiProperty({
|
||||
type: Number,
|
||||
name: 'staffCode',
|
||||
description: 'Staff code/ID',
|
||||
required: false,
|
||||
})
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
staffCode: number;
|
||||
|
||||
@ApiProperty({
|
||||
type: Number,
|
||||
name: 'departmentId',
|
||||
description: 'Department ID',
|
||||
required: false,
|
||||
})
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
departmentId: number;
|
||||
}
|
||||
4
src/staffs/dto/staff-update.dto.ts
Normal file
4
src/staffs/dto/staff-update.dto.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { PartialType } from '@nestjs/swagger';
|
||||
import { StaffCreateDto } from './staff-create.dto';
|
||||
|
||||
export class StaffUpdateDto extends PartialType(StaffCreateDto) {}
|
||||
57
src/staffs/entities/staff.entity.ts
Normal file
57
src/staffs/entities/staff.entity.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
|
||||
|
||||
// @Entity('staffs')
|
||||
@Entity()
|
||||
export class Staff {
|
||||
@ApiProperty({
|
||||
type: 'number',
|
||||
name: 'id',
|
||||
description: 'id of staff, auto-incremented',
|
||||
required: true,
|
||||
})
|
||||
@PrimaryGeneratedColumn('increment')
|
||||
id: number;
|
||||
|
||||
@ApiProperty({
|
||||
type: String,
|
||||
name: 'name',
|
||||
description: 'Staff name',
|
||||
required: true,
|
||||
})
|
||||
@Column({
|
||||
type: 'varchar',
|
||||
})
|
||||
name: string;
|
||||
|
||||
@ApiProperty({
|
||||
type: Number,
|
||||
name: 'staffCode',
|
||||
description: 'Staff code/ID',
|
||||
required: false,
|
||||
})
|
||||
@Column({
|
||||
name: 'staff_code',
|
||||
type: 'varchar',
|
||||
length: 10,
|
||||
nullable: true,
|
||||
})
|
||||
staffCode: number;
|
||||
|
||||
@ApiProperty({
|
||||
type: Number,
|
||||
name: 'departmentId',
|
||||
description: 'Department ID',
|
||||
required: false,
|
||||
})
|
||||
@Column({
|
||||
// Match existing column naming convention
|
||||
name: 'department_id',
|
||||
type: 'tinyint',
|
||||
unsigned: true,
|
||||
nullable: true,
|
||||
})
|
||||
departmentId: number;
|
||||
|
||||
// TODO: JOIN TABLE CASE
|
||||
}
|
||||
76
src/staffs/staffs.controller.ts
Normal file
76
src/staffs/staffs.controller.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Body,
|
||||
Patch,
|
||||
Param,
|
||||
Delete,
|
||||
NotFoundException,
|
||||
HttpCode,
|
||||
BadRequestException,
|
||||
} from '@nestjs/common';
|
||||
import { ApiResponse } from '@nestjs/swagger';
|
||||
import { StaffsService } from './staffs.service';
|
||||
import { Staff } from './entities/staff.entity';
|
||||
import { StaffCreateDto } from './dto/staff-create.dto';
|
||||
import { StaffUpdateDto } from './dto/staff-update.dto';
|
||||
import { EntityNotFoundError } from 'typeorm';
|
||||
|
||||
@Controller('staffs')
|
||||
export class StaffsController {
|
||||
constructor(private readonly staffsService: StaffsService) {}
|
||||
|
||||
private handleEntityNotFoundError(id: number, e: Error) {
|
||||
if (e instanceof EntityNotFoundError) {
|
||||
throw new NotFoundException(`Not found: id=${id}`);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@Get()
|
||||
@ApiResponse({ type: [Staff] })
|
||||
findAll(): Promise<Staff[]> {
|
||||
return this.staffsService.findAll();
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@ApiResponse({ type: Staff })
|
||||
async findOne(@Param('id') id: number): Promise<Staff | null> {
|
||||
// NOTE: the + operator returns the numeric representation of the object.
|
||||
return this.staffsService.findOne(+id).catch((err) => {
|
||||
this.handleEntityNotFoundError(id, err);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
@Post()
|
||||
@ApiResponse({ type: [Staff] })
|
||||
create(@Body() staffCreateDto: StaffCreateDto) {
|
||||
return this.staffsService.create(staffCreateDto);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
// Status 204 because no content will be returned
|
||||
@HttpCode(204)
|
||||
async update(
|
||||
@Param('id') id: number,
|
||||
@Body() staffUpdateDto: StaffUpdateDto,
|
||||
): Promise<void> {
|
||||
if (Object.keys(staffUpdateDto).length === 0) {
|
||||
throw new BadRequestException('Request body is empty');
|
||||
}
|
||||
await this.staffsService.update(+id, staffUpdateDto).catch((err) => {
|
||||
this.handleEntityNotFoundError(id, err);
|
||||
});
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@HttpCode(204)
|
||||
async remove(@Param('id') id: number): Promise<void> {
|
||||
await this.staffsService.remove(+id).catch((err) => {
|
||||
this.handleEntityNotFoundError(id, err);
|
||||
});
|
||||
}
|
||||
}
|
||||
15
src/staffs/staffs.module.ts
Normal file
15
src/staffs/staffs.module.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Logger, Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { StaffsService } from './staffs.service';
|
||||
import { StaffsController } from './staffs.controller';
|
||||
import { Staff } from './entities/staff.entity';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Staff])],
|
||||
controllers: [StaffsController],
|
||||
providers: [Logger, StaffsService],
|
||||
// If you want to use the repository outside of the module
|
||||
// which imports TypeOrmModule.forFeature, you'll need to re-export the providers generated by it.
|
||||
// exports: [TypeOrmModule],
|
||||
})
|
||||
export class StaffsModule {}
|
||||
50
src/staffs/staffs.service.ts
Normal file
50
src/staffs/staffs.service.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { Injectable, Logger, NotFoundException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { EntityNotFoundError, Repository } from 'typeorm';
|
||||
import { Staff } from './entities/staff.entity';
|
||||
import { StaffCreateDto } from './dto/staff-create.dto';
|
||||
import { StaffUpdateDto } from './dto/staff-update.dto';
|
||||
|
||||
@Injectable()
|
||||
export class StaffsService {
|
||||
constructor(
|
||||
private readonly logger: Logger,
|
||||
@InjectRepository(Staff)
|
||||
private staffsRepository: Repository<Staff>,
|
||||
) {}
|
||||
|
||||
create(staffCreateDto: StaffCreateDto): Promise<Staff> {
|
||||
// Use Repository.create() will copy the values to destination object.
|
||||
const staff = this.staffsRepository.create(staffCreateDto);
|
||||
return this.staffsRepository.save(staff);
|
||||
}
|
||||
|
||||
findAll(): Promise<Staff[]> {
|
||||
return this.staffsRepository.find();
|
||||
}
|
||||
|
||||
findOne(id: number): Promise<Staff> {
|
||||
return this.staffsRepository.findOneOrFail({ where: { id } });
|
||||
}
|
||||
|
||||
async update(id: number, staffUpdateDto: StaffUpdateDto): Promise<void> {
|
||||
await this.isExist(id);
|
||||
await this.staffsRepository.update({ id }, staffUpdateDto);
|
||||
}
|
||||
|
||||
async remove(id: number): Promise<void> {
|
||||
await this.isExist(id);
|
||||
await this.staffsRepository.delete(id);
|
||||
}
|
||||
|
||||
// Helper function: Check if the entity exist.
|
||||
// If entity does not exsit, return the Promise.reject()
|
||||
private async isExist(id: number): Promise<void> {
|
||||
const cnt = await this.staffsRepository.countBy({ id });
|
||||
if (cnt > 0) {
|
||||
return;
|
||||
} else {
|
||||
return Promise.reject(new EntityNotFoundError(Staff, { where: { id } }));
|
||||
}
|
||||
}
|
||||
}
|
||||
39
src/users/dto/create-user.dto.ts
Normal file
39
src/users/dto/create-user.dto.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import {
|
||||
IsEmail,
|
||||
IsNotEmpty,
|
||||
IsDefined,
|
||||
IsString,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
} from 'class-validator';
|
||||
export class CreateUserDto {
|
||||
// NOTE: Since the id is autoinc, so no id for user creation
|
||||
// @ApiProperty({
|
||||
// type: 'number',
|
||||
// name: 'id',
|
||||
// description: 'id of user',
|
||||
// required: false,
|
||||
// })
|
||||
// id: number;
|
||||
|
||||
@ApiProperty({
|
||||
type: String,
|
||||
name: 'name',
|
||||
description: 'name of user',
|
||||
required: true,
|
||||
})
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
name: string;
|
||||
|
||||
@ApiProperty({
|
||||
type: Number,
|
||||
name: 'age',
|
||||
description: 'age of user',
|
||||
required: false,
|
||||
})
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
age: number;
|
||||
}
|
||||
4
src/users/dto/update-user.dto.ts
Normal file
4
src/users/dto/update-user.dto.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { PartialType } from '@nestjs/swagger';
|
||||
import { CreateUserDto } from './create-user.dto';
|
||||
|
||||
export class UpdateUserDto extends PartialType(CreateUserDto) {}
|
||||
54
src/users/entities/user.entity.ts
Normal file
54
src/users/entities/user.entity.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { CreateUserDto } from '../dto/create-user.dto';
|
||||
import { UpdateUserDto } from '../dto/update-user.dto';
|
||||
// See: https://cimpleo.com/blog/nestjs-modules-and-swagger-practices/
|
||||
|
||||
// export interface IUser {
|
||||
// id: number;
|
||||
// name: string;
|
||||
// }
|
||||
|
||||
// export class User implements IUser {
|
||||
export class User {
|
||||
@ApiProperty({
|
||||
type: 'number',
|
||||
name: 'id',
|
||||
description: 'id of user',
|
||||
required: false,
|
||||
})
|
||||
id: number;
|
||||
|
||||
@ApiProperty({
|
||||
type: 'string',
|
||||
name: 'name',
|
||||
description: 'name of user',
|
||||
required: false,
|
||||
})
|
||||
name: string;
|
||||
|
||||
protected constructor() {
|
||||
this.id = -1;
|
||||
this.name = '';
|
||||
}
|
||||
|
||||
// Factory methods
|
||||
public static create(id = 0, name = ''): User {
|
||||
const user = new User();
|
||||
user.id = id;
|
||||
user.name = name;
|
||||
return user;
|
||||
}
|
||||
|
||||
public static createFromCreateUserDto(
|
||||
userDto: CreateUserDto | UpdateUserDto,
|
||||
id?: number,
|
||||
): User {
|
||||
const user = new User();
|
||||
if (id) {
|
||||
user.id = id;
|
||||
}
|
||||
user.name = userDto.name;
|
||||
|
||||
return user;
|
||||
}
|
||||
}
|
||||
49
src/users/users.controller.ts
Normal file
49
src/users/users.controller.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Body,
|
||||
Patch,
|
||||
Param,
|
||||
Delete,
|
||||
} from '@nestjs/common';
|
||||
import { ApiResponse } from '@nestjs/swagger';
|
||||
import { UsersService } from './users.service';
|
||||
import { CreateUserDto } from './dto/create-user.dto';
|
||||
import { UpdateUserDto } from './dto/update-user.dto';
|
||||
import { User } from './entities/user.entity';
|
||||
|
||||
@Controller('users')
|
||||
export class UsersController {
|
||||
constructor(private readonly usersService: UsersService) {}
|
||||
|
||||
@Post()
|
||||
create(@Body() createUserDto: CreateUserDto) {
|
||||
return this.usersService.create(createUserDto);
|
||||
}
|
||||
|
||||
@Get()
|
||||
@ApiResponse({ type: [User], status: 200 })
|
||||
async findAll(): Promise<User[]> {
|
||||
return this.usersService.findAll();
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@ApiResponse({ type: User, status: 200 })
|
||||
async findOne(@Param('id') id: number): Promise<User> {
|
||||
return this.usersService.findOne(+id);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
update(
|
||||
@Param('id') id: number,
|
||||
@Body() updateUserDto: UpdateUserDto,
|
||||
): Promise<User> {
|
||||
return this.usersService.update(+id, updateUserDto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
remove(@Param('id') id: number): Promise<User> {
|
||||
return this.usersService.remove(+id);
|
||||
}
|
||||
}
|
||||
10
src/users/users.module.ts
Normal file
10
src/users/users.module.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Logger, Module } from '@nestjs/common';
|
||||
import { UsersService } from './users.service';
|
||||
import { UsersController } from './users.controller';
|
||||
import { UserRepository } from './users.repository';
|
||||
|
||||
@Module({
|
||||
controllers: [UsersController],
|
||||
providers: [Logger, UsersService, UserRepository],
|
||||
})
|
||||
export class UsersModule {}
|
||||
72
src/users/users.repository.ts
Normal file
72
src/users/users.repository.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { Injectable, Logger, NotFoundException } from '@nestjs/common';
|
||||
import { User } from './entities/user.entity';
|
||||
|
||||
@Injectable()
|
||||
export class UserRepository {
|
||||
constructor(private readonly logger: Logger) {}
|
||||
|
||||
private readonly users = new Map<number, User>();
|
||||
|
||||
public async findAll(): Promise<User[]> {
|
||||
return [...this.users.values()];
|
||||
}
|
||||
|
||||
public async get(id: number): Promise<User> {
|
||||
// const user = this.users.find((user) => user.id === id);
|
||||
if (!this.users.has(id)) {
|
||||
throw new NotFoundException('User not found');
|
||||
}
|
||||
return this.users.get(id);
|
||||
}
|
||||
|
||||
public async add(user: User): Promise<void> {
|
||||
const largestId = Math.max(...Array.from(this.users.keys()));
|
||||
|
||||
// Can get size from map directly but we simulate reading from external service
|
||||
// So calling the method instead
|
||||
// const cnt = await this.count();
|
||||
// await this.get(cnt - 1)
|
||||
// .then((lastuser) => {
|
||||
// id = lastuser.id + 1;
|
||||
// })
|
||||
// .catch(() => {
|
||||
// id = 0;
|
||||
// });
|
||||
|
||||
user.id = largestId < 0 ? 0 : largestId + 1;
|
||||
this.users.set(user.id, user);
|
||||
this.logger.log({ message: 'User added', user });
|
||||
}
|
||||
|
||||
public async update(id: number, user: User): Promise<User> {
|
||||
// Can get size from map directly but we simulate reading from external service
|
||||
// So calling the method instead
|
||||
// It will throw exception if not found.
|
||||
const oldUser = await this.get(id);
|
||||
|
||||
this.users.set(id, user);
|
||||
this.logger.log({ message: 'User updated', from: oldUser, to: user });
|
||||
return oldUser;
|
||||
}
|
||||
|
||||
public async delete(id: number): Promise<User> {
|
||||
let deletedUser: User;
|
||||
await this.get(id)
|
||||
.then((user) => {
|
||||
this.users.delete(id);
|
||||
this.logger.log({ message: 'User deleted', user });
|
||||
deletedUser = user;
|
||||
})
|
||||
.catch(() => {
|
||||
throw new NotFoundException(
|
||||
`User not found, No user deleted, id=${id}`,
|
||||
);
|
||||
});
|
||||
|
||||
return deletedUser;
|
||||
}
|
||||
|
||||
public async count(): Promise<number> {
|
||||
return this.users.size;
|
||||
}
|
||||
}
|
||||
36
src/users/users.service.ts
Normal file
36
src/users/users.service.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { CreateUserDto } from './dto/create-user.dto';
|
||||
import { UpdateUserDto } from './dto/update-user.dto';
|
||||
import { User } from './entities/user.entity';
|
||||
import { UserRepository } from './users.repository';
|
||||
|
||||
@Injectable()
|
||||
export class UsersService {
|
||||
constructor(
|
||||
private readonly logger: Logger,
|
||||
private readonly userRepository: UserRepository,
|
||||
) {}
|
||||
|
||||
async create(createUserDto: CreateUserDto): Promise<User> {
|
||||
const user = User.createFromCreateUserDto(createUserDto, -1);
|
||||
await this.userRepository.add(user);
|
||||
return user;
|
||||
}
|
||||
|
||||
async findAll(): Promise<User[]> {
|
||||
return this.userRepository.findAll();
|
||||
}
|
||||
|
||||
async findOne(id: number): Promise<User> {
|
||||
return this.userRepository.get(id);
|
||||
}
|
||||
|
||||
async update(id: number, updateUserDto: UpdateUserDto): Promise<User> {
|
||||
const user = User.createFromCreateUserDto(updateUserDto, id);
|
||||
return this.userRepository.update(id, user);
|
||||
}
|
||||
|
||||
async remove(id: number): Promise<User> {
|
||||
return this.userRepository.delete(id);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user