feat:改進webpack打包機制、logger可讀性、欄位驗證回傳訊息可讀性
This commit is contained in:
@ -2,7 +2,10 @@
|
||||
"$schema": "https://json.schemastore.org/nest-cli",
|
||||
"collection": "@nestjs/schematics",
|
||||
"sourceRoot": "src",
|
||||
"entryFile": "main.cjs",
|
||||
"compilerOptions": {
|
||||
"webpack": true,
|
||||
"webpackConfigPath": "webpack.config.cjs",
|
||||
"deleteOutDir": true
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,7 +12,7 @@ import { join } from 'path'
|
||||
|
||||
return {
|
||||
pinoHttp: {
|
||||
level: isProd ? 'info' : 'debug',
|
||||
timestamp: () => `,"time":"${new Date().toISOString()}"`,
|
||||
serializers: {
|
||||
req: (req: {
|
||||
id: string
|
||||
@ -31,6 +31,9 @@ import { join } from 'path'
|
||||
ip: req.headers['x-forwarded-for'] ?? req.socket?.remoteAddress,
|
||||
userAgent: req.headers['user-agent'],
|
||||
}),
|
||||
res: (res: { statusCode: number }) => ({
|
||||
statusCode: res.statusCode,
|
||||
}),
|
||||
},
|
||||
transport: isProd
|
||||
? {
|
||||
@ -42,7 +45,7 @@ import { join } from 'path'
|
||||
file: join('logs', 'app'),
|
||||
frequency: 'daily',
|
||||
dateFormat: 'yyyy-MM-dd',
|
||||
extension: '.log',
|
||||
extension: '.json',
|
||||
limit: { count: 14 },
|
||||
mkdir: true,
|
||||
},
|
||||
@ -54,11 +57,19 @@ import { join } from 'path'
|
||||
file: join('logs', 'error'),
|
||||
frequency: 'daily',
|
||||
dateFormat: 'yyyy-MM-dd',
|
||||
extension: '.log',
|
||||
extension: '.json',
|
||||
limit: { count: 30 },
|
||||
mkdir: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
target: 'pino-pretty',
|
||||
options: {
|
||||
colorize: true,
|
||||
translateTime: 'SYS:yyyy-mm-dd HH:MM:ss',
|
||||
ignore: 'pid,hostname',
|
||||
},
|
||||
}
|
||||
],
|
||||
}
|
||||
: {
|
||||
|
||||
9
src/core/pipe/uuid-validation.pipe.ts
Normal file
9
src/core/pipe/uuid-validation.pipe.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { ParseUUIDPipe, BadRequestException } from '@nestjs/common';
|
||||
|
||||
export class UUIDValidationPipe extends ParseUUIDPipe {
|
||||
constructor() {
|
||||
super({
|
||||
exceptionFactory: () => new BadRequestException('傳入參數非 UUID'),
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1,14 +1,104 @@
|
||||
import { BadRequestException } from '@nestjs/common'
|
||||
import type { ValidationError } from 'class-validator'
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
import { type ValidationError } from 'class-validator';
|
||||
|
||||
export function validationExceptionFactory(errors: ValidationError[]): BadRequestException {
|
||||
const forbidden = errors.filter((e) => e.constraints?.whitelistValidation)
|
||||
const ErrorMessageMap: Record<
|
||||
string,
|
||||
(property: string, rawMessage: string) => string
|
||||
> = {
|
||||
isNotEmpty: (prop) => `欄位 [${prop}] 不能為空`,
|
||||
isInt: (prop) => `欄位 [${prop}] 必須是整數`,
|
||||
isUuid: (prop) => `欄位 [${prop}] 必須是有效的UUID`,
|
||||
isObject: (prop) => `欄位 [${prop}] 必須是物件`,
|
||||
isNumber: (prop) => `欄位 [${prop}] 必須是數字`,
|
||||
isString: (prop) => `欄位 [${prop}] 必須是字串`,
|
||||
isEmail: (prop) => `欄位 [${prop}] 的格式不正確`,
|
||||
isEnum: (prop) => `欄位 [${prop}] 的值不在允許範圍內`,
|
||||
isBoolean: (prop) => `欄位 [${prop}] 必須是布林值`,
|
||||
isDate: (prop) => `欄位 [${prop}] 必須是有效的日期`,
|
||||
isArray: (prop) => `欄位 [${prop}] 必須是有陣列`,
|
||||
isUrl: (prop) => `欄位 [${prop}] 必須是有效的網址`,
|
||||
arrayNotEmpty: (prop) => `欄位 [${prop}] 陣列不能為空`,
|
||||
whitelistValidation: (prop) => `欄位 [${prop}] 不在允許的名單中 (多餘欄位)`,
|
||||
|
||||
if (forbidden.length > 0) {
|
||||
const fields = forbidden.map((e) => e.property).join(', ')
|
||||
return new BadRequestException(`不允許的欄位:${fields}`)
|
||||
min: (prop, raw) => {
|
||||
const num = raw.match(/-?\d+/)?.[0] ?? '指定';
|
||||
return `欄位 [${prop}] 最小不能低於 ${num}`;
|
||||
},
|
||||
max: (prop, raw) => {
|
||||
const num = raw.match(/-?\d+/)?.[0] ?? '指定';
|
||||
return `欄位 [${prop}] 最大不能超過 ${num}`;
|
||||
},
|
||||
minLength: (prop, raw) => {
|
||||
const num = raw.match(/\d+/)?.[0] ?? '指定';
|
||||
return `欄位 [${prop}] 長度至少需 ${num} 個字`;
|
||||
},
|
||||
maxLength: (prop, raw) => {
|
||||
const num = raw.match(/\d+/)?.[0] ?? '指定';
|
||||
return `欄位 [${prop}] 長度不能超過 ${num} 個字`;
|
||||
},
|
||||
isLength: (prop, raw) => {
|
||||
const nums = raw.match(/\d+/g);
|
||||
return nums && nums.length >= 2
|
||||
? `欄位 [${prop}] 長度需介於 ${nums[0]} 到 ${nums[1]} 個字之間`
|
||||
: `欄位 [${prop}] 長度不符規範`;
|
||||
},
|
||||
arrayMinSize: (prop, raw) => {
|
||||
const num = raw.match(/\d+/)?.[0] ?? '指定';
|
||||
return `欄位 [${prop}] 至少需包含 ${num} 個項目`;
|
||||
},
|
||||
arrayMaxSize: (prop, raw) => {
|
||||
const num = raw.match(/\d+/)?.[0] ?? '指定';
|
||||
return `欄位 [${prop}] 最多只能包含 ${num} 個項目`;
|
||||
},
|
||||
};
|
||||
|
||||
export function validationExceptionFactory(
|
||||
errors: ValidationError[],
|
||||
): BadRequestException {
|
||||
const formatErrors = (
|
||||
errorList: ValidationError[],
|
||||
parentProp = '',
|
||||
): string[] => {
|
||||
const messages: string[] = [];
|
||||
|
||||
for (const error of errorList) {
|
||||
const fullPropPath = parentProp
|
||||
? `${parentProp}.${error.property}`
|
||||
: error.property;
|
||||
|
||||
if (error.constraints) {
|
||||
Object.keys(error.constraints).forEach((key) => {
|
||||
const translator = ErrorMessageMap[key];
|
||||
const rawMessage = error.constraints![key];
|
||||
|
||||
const isDefaultMessage =
|
||||
rawMessage.includes(error.property) ||
|
||||
rawMessage.includes('should not') ||
|
||||
rawMessage.includes('must be');
|
||||
|
||||
if (translator && isDefaultMessage) {
|
||||
messages.push(translator(fullPropPath, rawMessage));
|
||||
} else {
|
||||
messages.push(rawMessage);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const messages = errors.flatMap((e) => Object.values(e.constraints ?? {}))
|
||||
return new BadRequestException(messages.join('; '))
|
||||
if (error.children && error.children.length > 0) {
|
||||
messages.push(...formatErrors(error.children, fullPropPath));
|
||||
}
|
||||
}
|
||||
|
||||
return messages;
|
||||
};
|
||||
|
||||
const allMessages = formatErrors(errors);
|
||||
const uniqueMessages = Array.from(new Set(allMessages));
|
||||
|
||||
return new BadRequestException({
|
||||
statusCode: 400,
|
||||
message: uniqueMessages.length > 0 ? uniqueMessages[0] : '資料驗證失敗',
|
||||
errors: uniqueMessages,
|
||||
error: 'Bad Request',
|
||||
});
|
||||
}
|
||||
|
||||
31
webpack.config.cjs
Normal file
31
webpack.config.cjs
Normal file
@ -0,0 +1,31 @@
|
||||
const webpack = require('webpack');
|
||||
const nodeExternals = require('webpack-node-externals');
|
||||
|
||||
module.exports = function (options) {
|
||||
return {
|
||||
...options,
|
||||
entry: ['./src/main.ts'],
|
||||
output: {
|
||||
...options.output,
|
||||
filename: 'main.cjs',
|
||||
},
|
||||
target: 'node',
|
||||
externals: [nodeExternals()],
|
||||
optimization: {
|
||||
splitChunks: false,
|
||||
runtimeChunk: false,
|
||||
},
|
||||
resolve: {
|
||||
...options.resolve,
|
||||
extensionAlias: {
|
||||
'.js': ['.ts', '.js'],
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
...options.plugins,
|
||||
new webpack.optimize.LimitChunkCountPlugin({
|
||||
maxChunks: 1,
|
||||
}),
|
||||
],
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user