Commit f890cb0a authored by mingyard's avatar mingyard

feat:搭建项目

parent 96b2a3aa
{
"version": "0.2",
"language": "en",
"ignorePaths": [
".cspell.json",
"README.md"
],
"words": [
"dotenv",
"healthz",
"Millis",
"nestjs",
"oidc",
"pids",
"typeorm"
]
}
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
project: 'tsconfig.json',
tsconfigRootDir: __dirname,
sourceType: 'module',
},
plugins: ['@typescript-eslint/eslint-plugin'],
extends: [
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended',
],
root: true,
env: {
node: true,
jest: true,
},
ignorePatterns: ['.eslintrc.js'],
rules: {
'@typescript-eslint/interface-name-prefix': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
},
};
# 设置文件的合并策略
# .gitlab-ci.yml 文件禁止自动合并
.gitlab-ci.yml -merge
\ No newline at end of file
# compiled output
/dist
/node_modules
/build
# Logs
logs
*.log
npm-debug.log*
pnpm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# OS
.DS_Store
# Tests
/coverage
/.nyc_output
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# temp directory
.temp
.tmp
# Runtime data
pids
*.pid
*.seed
*.pid.lock
{
"singleQuote": true,
"trailingComma": "all"
}
\ No newline at end of file
# translation-server ## Description
翻译机 翻译机项目
\ No newline at end of file
## Project setup
```bash
$ yarn install
```
## Compile and run the project
```bash
# development
$ yarn run start
# watch mode
$ yarn run start:dev
# production mode
$ yarn run start:prod
```
## Run tests
```bash
# unit tests
$ yarn run test
# e2e tests
$ yarn run test:e2e
# test coverage
$ yarn run test:cov
```
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true
}
}
{
"name": "translation-server",
"version": "0.0.1",
"description": "",
"author": "",
"private": true,
"license": "UNLICENSED",
"engines": {
"node": ">= 20.0.0"
},
"scripts": {
"build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@nestjs/cache-manager": "^2.3.0",
"@nestjs/common": "^10.0.0",
"@nestjs/core": "^10.0.0",
"@nestjs/platform-express": "^10.4.15",
"@nestjs/swagger": "^8.1.0",
"@nestjs/typeorm": "^10.0.2",
"axios": "^1.7.9",
"cache-manager": "^6.3.2",
"cache-manager-redis-yet": "^5.1.5",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"dotenv": "^16.4.7",
"lodash": "^4.17.21",
"mysql2": "^3.12.0",
"on-headers": "^1.0.2",
"redis": "^4.7.0",
"reflect-metadata": "^0.2.0",
"rxjs": "^7.8.1",
"typeorm": "^0.3.20"
},
"devDependencies": {
"@nestjs/cli": "^10.0.0",
"@nestjs/schematics": "^10.0.0",
"@nestjs/testing": "^10.0.0",
"@types/express": "^5.0.0",
"@types/jest": "^29.5.2",
"@types/node": "^20.3.1",
"@types/supertest": "^6.0.0",
"@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^8.0.0",
"eslint": "^8.0.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.0",
"jest": "^29.5.0",
"prettier": "^3.0.0",
"source-map-support": "^0.5.21",
"supertest": "^7.0.0",
"ts-jest": "^29.1.0",
"ts-loader": "^9.4.3",
"ts-node": "^10.9.1",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.1.3"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
}
import {
MiddlewareConsumer,
Module,
NestModule,
RequestMethod,
} from '@nestjs/common';
import { config } from './config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { SystemController } from './controller/system.controller';
import { UserModule } from './controller/user/user.module';
@Module({
imports: [
TypeOrmModule.forRoot({
...config.database,
name: 'default',
entities: [__dirname + '/**/*.entity{.ts,.js}'],
keepConnectionAlive: true,
synchronize: false,
maxQueryExecutionTime: 200,
logging: [config.database.logging],
}),
UserModule,
],
controllers: [SystemController],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply().forRoutes({ path: '*', method: RequestMethod.ALL });
}
}
import { CacheModule } from '@nestjs/cache-manager';
import { RedisClientOptions } from 'redis';
import { redisStore } from 'cache-manager-redis-yet';
import { Module } from '@nestjs/common';
import { config } from '../../config';
import { CacheService } from './cache.service';
@Module({
imports: [
CacheModule.register<RedisClientOptions>({
store: redisStore,
url: config.cache.redis.url,
isGlobal: true,
}),
],
providers: [CacheService],
exports: [CacheService],
})
export class AppCacheModule {}
import { Inject, Injectable } from '@nestjs/common';
import { Response } from 'express';
import { v4 } from 'uuid';
import { CACHE_MANAGER } from '@nestjs/cache-manager';
import { RedisCache } from 'cache-manager-redis-yet';
import { IRequest } from '@/interface';
import { config } from '../../config';
// const EXPIRE_TIME_IN_SEC = 86400;
const EXPIRE_TIME_IN_SEC = 3600;
export function readKeyFromCookie(
type: string,
req: IRequest,
throwError = true,
) {
const key = req.cookies[type];
if (!key && throwError) {
throw new Error('Without ' + type);
}
return key;
}
export function saveKeyIntoCookie(
type: string,
key: string,
res: Response,
options: {
timeout?: number;
} = {},
) {
const { timeout = EXPIRE_TIME_IN_SEC } = options;
res.cookie(type, key, {
maxAge: timeout * 1000,
secure: true,
sameSite: 'none',
httpOnly: true,
signed: true,
});
}
export async function clearKeyFromCookie(type: string, res: Response) {
res.clearCookie(type, {
// domain: getSubDomainHost('').split(':')[0],
});
}
/**
* 负责在顶级域名下存储一次性的临时 session,用来在流程中的各端点之间传递状态信息
* 将redis的key保存在cookie中,将js对象转为string生成redis键值对
*/
@Injectable()
export class CacheService {
constructor(@Inject(CACHE_MANAGER) private cacheManager: RedisCache) {}
async setNx<T = Record<string, any>>(
type: string,
key: string,
value: T,
options: {
timeout?: number;
} = {},
) {
const { timeout = EXPIRE_TIME_IN_SEC } = options;
await this.cacheManager.store.client.set(
config.env + ':' + type + ':' + key,
JSON.stringify(value),
// 超时秒数
{ EX: timeout, NX: true },
);
}
/**
* 创建一个 Interaction,存储到 redis 中。
*
* @param type 类型
* @param value 值
* @param options 额外参数
* - timeout:过期时间,默认为 86400 秒
* - key:唯一标识,不传会自动生成
* @returns Interaction 的唯一标识
*/
async create<T = Record<string, any>>(
type: string,
key: string,
value: T,
options: {
timeout?: number;
} = {},
) {
const { timeout = EXPIRE_TIME_IN_SEC } = options;
await this.cacheManager.store.client.set(
config.env + ':' + type + ':' + key,
JSON.stringify(value),
// 超时秒数
{ EX: timeout },
);
return key;
}
/**
* 创建一个 Interaction,存储到 cookie 和 redis 中。
*
* @param type 类型
* @param value 值
* @param res 响应对象
* @param options 额外参数
* - timeout:过期时间,默认为 86400 秒
* - key:唯一标识,不传会自动生成`
* @returns Interaction 的唯一标识
*/
async createIntoCookie<T = Record<string, any>>(
type: string,
value: T,
res: Response,
options: {
timeout?: number;
key?: string;
} = {},
) {
const key = options.key ?? v4();
await this.create(type, key, value, options);
saveKeyIntoCookie(type, key, res, options);
return key;
}
/**
* 更新一个 Interaction 的值。
*
* @param type 类型
* @param value 值
* @param key 唯一标识
*/
async update<T = Record<string, any>>(
type: string,
key: string,
value: T,
options: {
timeout?: number;
} = {},
) {
await this.cacheManager.set(
config.env + ':' + type + ':' + key,
JSON.stringify(value),
options.timeout ?? EXPIRE_TIME_IN_SEC,
);
}
/**
* 读取缓存值。
*
* @param type 类型
* @param key 唯一标识
* @returns Interaction 的值
*/
async read<T = Record<string, any>>(type: string, key: string) {
const value = await this.cacheManager.get<T>(
config.env + ':' + type + ':' + key,
);
return value;
}
/**
* 清除一个 Interaction。
*
* @param type 类型
* @param key 唯一标识
*/
async clear(type: string, key: string) {
await this.cacheManager.set(config.env + ':' + type + ':' + key, 10);
}
}
export enum ErrorCodeEnum {
/**
* HTTP CODE 400
* 错误请求
*/
BAD_REQUEST = 400,
/**
* HTTP CODE 401
* 未授权
*/
UNAUTHORIZED = 401,
/**
* HTTP CODE 403
* 没有权限
*/
FORBIDDEN = 403,
/**
* HTTP CODE 404
* 未找到
*/
NOT_FOUND = 404,
/**
* HTTP CODE 500
* 服务器错误
*/
SERVER_ERROR = 500,
/**
* 自定义 CODE 1001
* 用户不存在
*/
USER_NOT_FOUND = 1001,
/**
* 自定义 CODE 1002
* 邮箱未验证
*/
EMAIL_NOT_VERIFIED = 1002,
/**
* 自定义 CODE 1003
* 无效的参数
*/
INVALID_PARAMETER = 1003,
/**
* 自定义 CODE 1004
* 操作失败
*/
OPERATION_FAILED = 1004,
}
import { ErrorCodeEnum } from '@/common/enum/ErrorCodeEnum';
import { v4 } from 'uuid';
export interface InternalJsonError {
uniqueId: string;
className: string;
code: number;
data: Record<string, any>;
message: string;
stack: string;
}
/**
* 所有已知异常的基类,throw 语句抛出的异常必须继承自这个类型
*
* 如果需要在业务逻辑中单独处理某种异常,请选择一个最合适的子类并继承它来创建自己的异常类型,然后使用 instanceof 在 catch 语句中捕获它
*/
export abstract class BaseError extends Error {
private readonly uniqueId = v4();
private data: Record<string, any> = {};
private privateData: Record<string, any> = {};
/**
* 可选的 API 码,参见 ErrorCodeEnum
*/
protected getErrorCode(): ErrorCodeEnum {
return undefined;
}
/**
* 所有异常类的构造器必须为 protected 或 private,通过静态工厂方法选择性开放给外部调用
*/
protected constructor(
message: string,
data?: Record<string, any>,
privateData?: Record<string, any>,
) {
super(message);
this.data = data;
this.privateData = privateData;
}
/**
* 获取自定义数据字段,可重写
*/
getData(): Record<string, any> {
return this.data;
}
/**
* 获取私有自定义数据字段,限定内部可见,可重写
*/
getPrivateData(): Record<string, any> {
return this.privateData;
}
/**
* 转换为对外显示用字符串
*/
toString() {
return JSON.stringify(this.toJson());
}
protected setData(data: Record<string, any>) {
this.data = data;
}
protected setPrivateData(data: Record<string, any>) {
this.privateData = data;
}
/**
* 转换为对外显示用 JSON
*
* 不要将此方法用于业务逻辑!
*/
toJson() {
return {
uniqueId: this.uniqueId,
code: this.getErrorCode(),
data: this.getData(),
message: this.message,
};
}
/**
* 转换为内部调试用字符串
*
* 不要将此方法用于业务逻辑!
*/
toStringInternal() {
return JSON.stringify(this.toJsonInternal());
}
/**
* 转换为内部调试用 JSON
*
* 不要将此方法用于业务逻辑!
*/
toJsonInternal(): InternalJsonError {
return {
uniqueId: this.uniqueId,
className: this.constructor.name,
code: this.getErrorCode(),
data: {
...this.getData(),
...this.getPrivateData(),
},
message: this.message,
stack: this.stack,
};
}
}
import { ErrorCodeEnum } from '@/common/enum/ErrorCodeEnum';
import { BaseError } from './BaseError';
/**
* 资源未找到
*/
export class ServerError extends BaseError {
protected getErrorCode() {
return ErrorCodeEnum.SERVER_ERROR;
}
static default(message?: string) {
return new ServerError(message ?? 'SERVER ERROR');
}
}
import { ErrorCodeEnum } from '@/common/enum/ErrorCodeEnum';
import { BaseError } from '../BaseError';
/**
* 错误请求
*/
export class BadRequestError extends BaseError {
protected getErrorCode() {
return ErrorCodeEnum.BAD_REQUEST;
}
static default(message?: string) {
return new BadRequestError(message ?? 'BAD REQUEST');
}
}
import { BaseError } from '@/common/exception/BaseError';
import { ErrorCodeEnum } from '@/common/enum/ErrorCodeEnum';
/**
* 没有权限
*/
export class ForbiddenError extends BaseError {
protected getErrorCode() {
return ErrorCodeEnum.FORBIDDEN;
}
static default(message?: string) {
return new ForbiddenError(message ?? 'Not Permission');
}
}
import { ErrorCodeEnum } from '@/common/enum/ErrorCodeEnum';
import { BaseError } from '../BaseError';
/**
* 尚未登录
*/
export class UnauthorizedError extends BaseError {
protected getErrorCode() {
return ErrorCodeEnum.UNAUTHORIZED;
}
static default(message?: string) {
return new UnauthorizedError(message ?? 'Unauthorized');
}
}
import { ErrorCodeEnum } from '@/common/enum/ErrorCodeEnum';
import { UnauthorizedError } from './UnauthorizedError';
export class UserStatusError extends UnauthorizedError {
protected getErrorCode() {
return ErrorCodeEnum.EMAIL_NOT_VERIFIED;
}
static default(message?: string) {
return new UserStatusError(message ?? 'User is not active');
}
}
import {
ArgumentsHost,
BadRequestException,
Catch,
ExceptionFilter,
ForbiddenException,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { Response } from 'express';
import { IRequest } from '@/interface';
import { isArray, isString } from 'lodash';
@Injectable()
@Catch()
export class ApiExceptionsFilter implements ExceptionFilter {
catch(exception: any, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const req = ctx.getRequest<IRequest>();
if (exception instanceof ForbiddenException) {
return response.status(403).json({
code: 403,
message: 'Not Permission',
});
}
if (exception instanceof UnauthorizedException) {
const statusCode = (exception as any)?.response?.statusCode || 400;
return response.status(401).json({
code: statusCode,
message: (exception as any)?.response?.message || 'Bad Request',
});
}
if (exception instanceof BadRequestException) {
const validationErrors = (exception as any)?.response?.message;
const statusCode = (exception as any)?.response?.statusCode || 400;
if (isArray(validationErrors) && validationErrors?.length) {
return response.status(statusCode).json({
code: statusCode,
message: validationErrors[0],
});
} else {
return response.status(statusCode).json({
code: statusCode,
message: (exception as any)?.response?.message || 'Bad Request',
});
}
}
// 未知错误,触发错误报警
console.error('请求 HOST:' + req?.hostname);
console.error('请求路径:' + JSON.stringify(req?.path));
console.error('请求参数 QUERY:' + JSON.stringify(req?.query));
console.error('请求参数 BODY:' + JSON.stringify(req?.body));
console.error(
'\n***\n',
'未知错误',
JSON.stringify(exception?.message ?? exception),
);
const status = exception?.status || 500;
console.error('requestId:' + req.requestId);
return response.status(status || 500).json({
code: status,
message: isString(exception.message)
? exception.message
: JSON.parse(exception.message),
requestId: req.requestId,
});
}
}
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@Injectable()
export class ApiResponseInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
map((data) => ({
code: 200,
message: 'success',
data: data === null ? '' : data,
})),
);
}
}
import { axiosGetRequest, axiosPostRequest } from './request';
import { config } from '../../../config';
import * as qs from 'qs';
import { BadRequestException, UnauthorizedException } from '@nestjs/common';
const host = config.server;
export async function commonGetJsonRequest(
userToken: string,
url: string,
sp: Record<string, any>,
options?: {
headers?: Record<string, any>;
},
) {
const endpoint = new URL(host + url);
const { data } = await axiosGetRequest(
endpoint.toString() + '?' + qs.stringify(sp),
{
headers: {
Accept: 'application/json',
Authorization: userToken,
...options?.headers,
},
},
);
if (typeof data !== 'object') {
throw new BadRequestException({
statusCode: 400,
message: `API 返回值不是 JSON 格式,实际格式为 ${typeof data}`,
});
}
if (data?.statusCode !== 200 && data?.code !== 200) {
if (data?.statusCode === 401) {
throw new UnauthorizedException({
statusCode: 401,
message: '未授权请求',
});
}
throw new BadRequestException({
statusCode: data?.statusCode || 400,
message: data?.message,
});
}
return data;
}
export async function commonPostJsonRequest(
userToken: string,
url: string,
params: Record<string, any>,
options?: {
headers?: Record<string, any>;
},
) {
const { data } = await axiosPostRequest(host + url, params, {
headers: {
Accept: 'application/json',
Authorization: userToken,
...options?.headers,
},
});
if (typeof data !== 'object') {
throw new BadRequestException({
statusCode: 400,
message: `API 返回值不是 JSON 格式,实际格式为 ${typeof data}`,
});
}
if (data?.statusCode !== 200 && data?.code !== 200) {
if (data?.statusCode === 401) {
throw new UnauthorizedException({
statusCode: 401,
message: '未授权请求',
});
}
throw new BadRequestException({
statusCode: data?.statusCode || 400,
message: data?.message,
});
}
return data;
}
import { BadRequestError } from '@/common/exception/badRequest/BadRequestError';
import { ServerError } from '@/common/exception/ServerError';
import Axios, { AxiosRequestConfig, AxiosResponse, isAxiosError } from 'axios';
import { isObject } from 'lodash';
export async function axiosGetRequest(
url: string,
config?: AxiosRequestConfig,
errorHandler?: (res: AxiosResponse) => Error,
) {
try {
return await Axios.get(url, { ...config, timeout: 100000 });
} catch (e) {
if (isAxiosError(e)) {
console.error(e.response);
if (e.response) {
if (errorHandler) {
throw errorHandler(e.response);
}
if (isObject(e.response?.data)) {
const { data: errorData } = e.response;
throw BadRequestError.default(
(errorData as any)?.errorMessage ||
(errorData as any)?.error ||
(errorData as any)?.error_description,
);
}
throw BadRequestError.default(e.response?.data);
} else if (e.request) {
throw BadRequestError.default(
JSON.stringify(e.response?.data || e.response),
);
}
}
throw ServerError.default(e.message);
}
}
export async function axiosPostRequest(
url: string,
data?: any,
config?: AxiosRequestConfig,
errorHandler?: (res: AxiosResponse) => Error,
) {
try {
// await 不能删
return await Axios.post(url, data, { ...config, timeout: 10000 });
} catch (e) {
if (isAxiosError(e)) {
if (e.response) {
console.error(e.response, 'request error');
if (errorHandler) {
throw errorHandler(e.response);
}
if (isObject(e.response?.data)) {
const { data: errorData } = e.response;
throw BadRequestError.default(
(errorData as any)?.errorMessage ||
(errorData as any)?.error ||
(errorData as any)?.error_description,
);
}
throw BadRequestError.default(e.response?.data);
} else if (e.request) {
throw BadRequestError.default(
JSON.stringify(e.response?.data || e.response),
);
}
}
throw ServerError.default(e.message);
}
}
export async function axiosPatchRequest(
url: string,
data?: any,
config?: AxiosRequestConfig,
errorHandler?: (res: AxiosResponse) => Error,
) {
try {
// await 不能删
return await Axios.patch(url, data, { ...config, timeout: 10000 });
} catch (e) {
if (isAxiosError(e)) {
if (e.response) {
console.error(e.response?.data);
if (errorHandler) {
throw errorHandler(e.response);
}
if (isObject(e.response?.data)) {
const { data: errorData } = e.response;
throw BadRequestError.default(
(errorData as any)?.errorMessage ||
(errorData as any)?.error ||
(errorData as any)?.error_description,
);
}
throw BadRequestError.default(e.response?.data);
} else if (e.request) {
throw BadRequestError.default(
JSON.stringify(e.response?.data || e.response),
);
}
}
throw ServerError.default(e.message);
}
}
export async function axiosJsonGetRequest(
url: string,
sp: Record<string, any>,
headers?: Record<string, any>,
): Promise<Record<string, any>> {
const endpoint = new URL(url);
for (const [k, v] of Object.entries(sp)) {
endpoint.searchParams.set(k, v);
}
const { data } = await axiosGetRequest(endpoint.toString(), {
headers: {
...headers,
Accept: 'application/json',
},
});
if (typeof data !== 'object') {
throw BadRequestError.default(
`API 返回值不是 JSON 格式,实际格式为 ${typeof data}`,
);
}
return data;
}
export async function axiosJsonPostRequest(
url: string,
params: Record<string, any>,
headers?: Record<string, any>,
): Promise<Record<string, any>> {
const { data } = await axiosPostRequest(url, params, {
headers: {
...headers,
Accept: 'application/json',
},
});
if (typeof data !== 'object') {
throw BadRequestError.default(
`API 返回值不是 JSON 格式,实际格式为 ${typeof data}`,
);
}
return data;
}
export async function axiosJsonPatchRequest(
url: string,
params: Record<string, any>,
headers?: Record<string, any>,
): Promise<Record<string, any>> {
const { data } = await axiosPatchRequest(url, params, {
headers: {
...headers,
Accept: 'application/json',
},
});
if (typeof data !== 'object') {
throw BadRequestError.default(
`API 返回值不是 JSON 格式,实际格式为 ${typeof data}`,
);
}
return data;
}
import * as crypto from 'crypto';
export const MD5 = (str: string) => {
return crypto.createHash('md5').update(str).digest('hex');
};
import { env } from './env';
/**
* 应用配置
*/
interface AppConfig {
/**
* 环境变量
*/
env: string;
/** 应用启动端口 */
port: number;
// api 域名
server: string;
// 一级域名
domain: string;
// json limit size
jsonLimit: string;
cors: {
origin: string[];
};
session: {
secret: string;
};
rsaSecret: {
/** 应用公钥 */
publicKey: string;
/** 应用私钥 */
privateKey: string;
};
}
const server = env.APP_SERVER ?? 'http://localhost:3000';
const cors = process.env?.APP_CORS?.split(',');
export const app: AppConfig = {
domain: env.APP_DOMAIN ?? 'https://localhost:3000',
env: env.APP_ENV ?? 'prod',
port: Number(env.APP_PORT ?? 3000),
server,
jsonLimit: env.APP__JSON_LIMIT ?? '50mb',
cors: {
origin: cors ?? [],
},
session: {
secret: env.APP_SESSION_SECRET ?? 'api-service',
},
rsaSecret: {
publicKey: env.APP_RSA_PUBLIC_KEY ?? '',
privateKey: env.APP_RSA_PRIVATE_KEY ?? '',
},
};
import { env } from './env';
import { RedisClientOptions } from 'redis';
import { ServerError } from '@/common/exception/ServerError';
export type SUPPORTED_CACHE_STORAGE_TYPE = 'lru' | 'redis';
export interface RedisConfig {
url: string;
/**
* 是否开启集群模式
*/
clusterEnabled: boolean;
/** Redis 配置项 */
options: RedisClientOptions;
}
/**
* 缓存配置
*/
export interface CacheConfig {
/** 缓存驱动类型 */
type: SUPPORTED_CACHE_STORAGE_TYPE;
/** Redis 配置项 */
redis: RedisConfig;
}
export const cache: CacheConfig = {
type: 'redis',
redis: {
url: env.APP_CACHE_URL ?? 'localhost/6379',
clusterEnabled: Boolean(env.APP_CACHE_CLUSTER_ENABLED ?? false),
options: JSON.parse(env.APP_CACHE_OPTIONS ?? '{}'),
},
};
if (!cache.redis.clusterEnabled && !cache.redis.url) {
throw ServerError.default('配置文件缺少 redis url 配置');
}
import { env } from './env';
/** 连接数配置 */
export interface ConnectionPoolConfig {
max: number; // 最大连接数
min: number; // 最小连接数
idleTimeoutMillis: number; // 空闲回收时间
}
export interface DatabaseConfig {
type: 'mysql';
database?: string;
url?: string;
synchronize: boolean;
pool: ConnectionPoolConfig;
logging: 'error' | 'warn' | 'log' | 'migration';
maxQueryExecutionTime?: number;
supportBigNumbers: boolean;
bigNumberStrings: boolean;
}
export const database: DatabaseConfig = {
type: (env.APP_DATABASE_TYPE as any) ?? 'sqlite',
database: env.APP_DATABASE_DATABASE ?? './database.sqlite',
url: env.APP_DATABASE_URL,
synchronize: Boolean(env.APP_DATABASE_SYNCHRONIZE ?? false),
pool: {
max: Number(env.APP_DATABASE_POOL_MAX ?? 32),
min: Number(env.APP_DATABASE_POOL_MIN ?? 16),
idleTimeoutMillis: Number(
env.APP_DATABASE_POOL_IDLE_TIMEOUT_MILLIS ?? 20000,
),
},
maxQueryExecutionTime: Number(
env.APP_DATABASE_MAX_QUERY_EXECUTION_TIME ?? 200,
), // 单位为毫秒
logging: (env.APP_DATABASE_LOGGING as any) ?? 'error',
bigNumberStrings: Boolean(env.APP_DATABASE_BIG_NUMBER_STRINGS ?? false),
supportBigNumbers: Boolean(env.APP_DATABASE_SUPPORT_BIG_NUMBERS ?? true),
};
import 'dotenv/config'; // 加载 .env 文件
export const env = process.env;
import { app } from './app';
import { cache } from './cache';
import { database } from './database';
export const config = {
...app,
cache,
database,
};
import { Controller, Get, HttpCode } from '@nestjs/common';
@Controller()
export class SystemController {
@Get('/healthz')
@HttpCode(200)
healthz() {
return { code: 200, message: 'ok', data: 'ok' };
}
}
import { PartialType } from '@nestjs/mapped-types';
import { CreateUserDto } from './create-user.dto';
export class UpdateUserDto extends PartialType(CreateUserDto) {}
import { Test, TestingModule } from '@nestjs/testing';
import { UserController } from './user.controller';
import { UserService } from './user.service';
describe('UserController', () => {
let controller: UserController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [UserController],
providers: [UserService],
}).compile();
controller = module.get<UserController>(UserController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});
import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/common';
import { UserService } from './user.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
@Controller('user')
export class UserController {
constructor(private readonly userService: UserService) {}
@Post()
create(@Body() createUserDto: CreateUserDto) {
return this.userService.create(createUserDto);
}
@Get()
findAll() {
return this.userService.findAll();
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.userService.findOne(+id);
}
@Patch(':id')
update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
return this.userService.update(+id, updateUserDto);
}
@Delete(':id')
remove(@Param('id') id: string) {
return this.userService.remove(+id);
}
}
import { Module } from '@nestjs/common';
import { UserService } from './user.service';
import { UserController } from './user.controller';
@Module({
controllers: [UserController],
providers: [UserService],
})
export class UserModule {}
import { Test, TestingModule } from '@nestjs/testing';
import { UserService } from './user.service';
describe('UserService', () => {
let service: UserService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [UserService],
}).compile();
service = module.get<UserService>(UserService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});
import { Injectable } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
@Injectable()
export class UserService {
create(createUserDto: CreateUserDto) {
return 'This action adds a new user';
}
findAll() {
return `This action returns all user`;
}
findOne(id: number) {
return `This action returns a #${id} user`;
}
update(id: number, updateUserDto: UpdateUserDto) {
return `This action updates a #${id} user`;
}
remove(id: number) {
return `This action removes a #${id} user`;
}
}
import * as express from 'express';
// JSON 类型递归定义
type JSONValue =
| string
| number
| boolean
| { [x: string]: JSONValue }
| Array<JSONValue>;
export enum Lang {
zhCn = 'zh-CN',
enUs = 'en-US',
zhTw = 'zh-TW',
jaJP = 'ja-JP',
}
export enum Status {
true = 1,
false = 0,
}
export interface IRequest extends express.Request {
/** 二级域名 */
subDomain: string;
/** 请求 ID */
requestId: string;
/** 是否为 graphql 请求 */
isGraphqlRequest: boolean;
/** 请求主体唯一标志符 */
operatorArn: string;
/** 请求主体类型 */
operatorType: 'user' | 'role';
/** 请求主体 ID,有可能是用户 ID 或者角色 ID,需要通过 operatorType 判断 */
operatorId: string;
/** 传递给访问日志中间件的原始异常对象 */
rawErrorObject: any;
userAgent: string;
loggedIn: boolean;
clientIp: string;
device: string;
os: string;
octetString: string;
token: string;
lang: Lang;
/** 请求完整链接,包含 protocol, query */
fullUrl: string;
/** 当前请求的二级域名,如 https://xxxx.baidu.com */
hostWithProtocol: string;
}
export interface DecodedToken {
aud: string;
jti: string;
name?: string;
phone?: string;
email?: string;
sub: string;
given_name?: string;
family_name?: string;
middle_name?: string;
nickname?: string;
preferred_username?: string;
profile?: string;
picture?: string;
website?: string;
email_verified?: boolean;
gender?: string;
username?: string;
locale?: string;
phone_number?: string;
phone_number_verified?: boolean;
address?: JSONValue;
}
export type KeyValuePair = Record<string, any>;
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { IRequest } from './interface';
import { Response } from 'express';
import * as onHeaders from 'on-headers';
import { setupOpenApi } from './openapi';
import { NestExpressApplication } from '@nestjs/platform-express';
import { config } from './config';
async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(AppModule, {
rawBody: true,
});
app.use((req: IRequest, res: Response, next) => {
onHeaders(res, () => {
res.setHeader('Cache-Control', 'no-cache');
});
next();
});
setupOpenApi(app);
await app.listen(config.port);
console.log('Server Listen ' + config.port);
}
bootstrap();
import { INestApplication } from '@nestjs/common';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
export const setupOpenApi = (app: INestApplication) => {
const builder = new DocumentBuilder()
.setTitle('Translation API')
.setDescription(
'HTTP status code of 200 indicates a successful response, while any non-200 status code signifies an error response. Specifically, when the HTTP status code is 401, it means that the user is unauthenticated and needs to be redirected to the login page for authentication.',
)
.setVersion('1.0')
.addCookieAuth('translation_session');
/**
* openapi 展示
*/
const document = SwaggerModule.createDocument(app, builder.build(), {
include: [],
});
Object.assign(document.info, {
'x-meta': {
title: 'Translation Server API',
description:
'Translation starts with campus mutual aid and provides a platform for everyone to promote each other and grow together.',
image: '',
},
});
SwaggerModule.setup('openapi', app, document);
console.log(`OpenAPI Swagger Start ! /openapi`);
};
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from './../src/app.module';
describe('AppController (e2e)', () => {
let app: INestApplication;
beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
it('/ (GET)', () => {
return request(app.getHttpServer())
.get('/')
.expect(200)
.expect('Hello World!');
});
});
{
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": ".",
"testEnvironment": "node",
"testRegex": ".e2e-spec.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
}
}
{
"extends": "./tsconfig.json",
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
}
{
"compilerOptions": {
"module": "commonjs",
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"target": "ES2021",
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./",
"paths": {
"@/*": ["./src/*"]
},
"incremental": true,
"skipLibCheck": true,
"strictNullChecks": false,
"noImplicitAny": false,
"resolveJsonModule": true,
"strictBindCallApply": false,
"forceConsistentCasingInFileNames": false,
"noFallthroughCasesInSwitch": false
},
"include": ["src/**/*", "migrations/migrations", "migrations"]
}
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment