返回 AiToEarn
starter.ts
根目录 / project / aitoearn-backend / libs / common / src / starter.ts
1 import type { DynamicModule, Provider, Type } from '@nestjs/common'
2 import type { NestApplication } from '@nestjs/core'
3 import type { NestExpressApplication } from '@nestjs/platform-express'
4 import type { SchemaObject } from '@nestjs/swagger/dist/interfaces/open-api-spec.interface'
5 import type { Request, Response } from 'express'
6 import type { IncomingMessage, ServerResponse } from 'node:http'
7 import type { StreamEntry } from 'pino'
8 import type { BaseConfig } from './config'
9 import { HttpStatus, Logger, Module, VersioningType } from '@nestjs/common'
10 import { APP_FILTER, APP_INTERCEPTOR, APP_PIPE, NestFactory } from '@nestjs/core'
11 import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'
12 import { apiReference } from '@scalar/nestjs-api-reference'
13 import { customAlphabet } from 'nanoid'
14 import { LoggerModule, Logger as PinoLogger } from 'nestjs-pino'
15 import pino from 'pino'
16 import client from 'prom-client'
17 import { z } from 'zod'
18 import { GlobalExceptionFilter } from './filters'
19 import { HttpMetricsInterceptor, PropagationInterceptor, RequestContextInterceptor, ResponseInterceptor } from './interceptors'
20 import { CloudWatchLogger, ConsoleLogger, FeishuLogger, serializeError } from './loggers'
21 import { ZodValidationPipe } from './pipes'
22 import { patchNestJsSwagger, zodToJsonSchemaOptions } from './utils'
23
24 import './utils/load-file-from-env.util'
25
26 z.config(z.locales.zhCN())
27
28 patchNestJsSwagger()
29
30 const logger = new Logger('Bootstrap')
31
32 function setupMetrics(app: NestApplication & NestExpressApplication) {
33 client.collectDefaultMetrics()
34 app.use('/metrics', async (_req: Request, res: Response) => {
35 res.set('Content-Type', client.register.contentType)
36 const metrics = await client.register.metrics()
37 res.end(metrics)
38 })
39 }
40
41 function preserveRawBody(req: Request & { rawBody?: Buffer }, _res: Response, buf: Buffer) {
42 if (buf.length > 0) {
43 req.rawBody = Buffer.from(buf)
44 }
45 }
46
47 @Module({})
48 class RootModule {
49 static setup(args: Omit<DynamicModule, 'module'>): DynamicModule {
50 return {
51 module: RootModule,
52 ...args,
53 }
54 }
55 }
56
57 export interface StartApplicationOptions {
58 setupOpenapi?: (builder: DocumentBuilder) => DocumentBuilder
59 setupApp?: (app: NestApplication) => void
60 }
61 export async function startApplication(Module: Type<unknown>, config: BaseConfig, options: StartApplicationOptions = {}) {
62 if (config.enableConfigLogging) {
63 logger.log(JSON.stringify(config, null, 2))
64 }
65 const loggers: StreamEntry[] = []
66
67 if (config.logger?.console?.enable) {
68 loggers.push({
69 level: config.logger.console.level,
70 stream: new ConsoleLogger(config.logger.console),
71 })
72 }
73
74 if (config.logger?.cloudWatch?.enable) {
75 loggers.push({
76 level: config.logger.cloudWatch.level,
77 stream: new CloudWatchLogger(config.logger.cloudWatch),
78 })
79 }
80
81 if (config.logger?.feishu?.enable) {
82 loggers.push({
83 level: config.logger.feishu.level,
84 stream: new FeishuLogger(config.logger.feishu),
85 })
86 }
87
88 const reqIdGenerator = customAlphabet('0123456789abcdefghijklmnopqrstuvwxyz', 21)
89
90 const imports: DynamicModule[] = [
91 LoggerModule.forRoot({
92 pinoHttp: [
93 {
94 level: 'trace',
95 serializers: {
96 err: serializeError,
97 error: serializeError,
98 },
99 genReqId: (req: IncomingMessage, res: ServerResponse) => {
100 const incomingRequestId = req.headers['x-request-id']
101 const requestId = typeof incomingRequestId === 'string'
102 ? incomingRequestId
103 : Array.isArray(incomingRequestId)
104 ? incomingRequestId[0]
105 : reqIdGenerator()
106 req.headers['x-request-id'] = requestId
107 if (!res.headersSent) {
108 res.setHeader('x-request-id', requestId)
109 }
110 return requestId
111 },
112 },
113 pino.multistream(loggers),
114 ],
115 }),
116 ]
117 const providers: Provider[] = [
118 {
119 provide: APP_INTERCEPTOR,
120 useClass: HttpMetricsInterceptor,
121 },
122 {
123 provide: APP_INTERCEPTOR,
124 useClass: RequestContextInterceptor,
125 },
126 /**
127 * 传播上下文
128 */
129 {
130 provide: APP_INTERCEPTOR,
131 useClass: PropagationInterceptor,
132 },
133 {
134 provide: APP_PIPE,
135 useClass: ZodValidationPipe,
136 },
137 {
138 provide: APP_INTERCEPTOR,
139 useClass: ResponseInterceptor,
140 },
141 {
142 provide: APP_FILTER,
143 useValue: new GlobalExceptionFilter({
144 returnBadRequestDetails: config.enableBadRequestDetails,
145 }),
146 },
147 ]
148
149 const app = await NestFactory.create<
150 NestApplication & NestExpressApplication
151 >(RootModule.setup({
152 imports: [...imports, Module],
153 providers,
154 }), {
155 cors: true,
156 })
157
158 app.useLogger(app.get(PinoLogger))
159
160 app.enableVersioning({ type: VersioningType.URI })
161
162 if (config.globalPrefix)
163 app.setGlobalPrefix(config.globalPrefix, { exclude: ['/'] })
164
165 if (options.setupApp) {
166 options.setupApp(app)
167 }
168
169 if (config.openapi?.enable) {
170 const builder = new DocumentBuilder()
171 .setTitle(config.openapi.title)
172 .setDescription(config.openapi.description)
173 .setOpenAPIVersion('3.0.0')
174 .addBearerAuth()
175
176 if (options.setupOpenapi) {
177 options.setupOpenapi(builder)
178 }
179
180 const openApiDocument = SwaggerModule.createDocument(
181 app,
182 builder.build(),
183 )
184
185 if (openApiDocument.components?.schemas) {
186 const zodSchemas = z.toJSONSchema(z.globalRegistry, { ...zodToJsonSchemaOptions, io: 'input' }).schemas
187 Object.keys(zodSchemas).forEach((key) => {
188 const schema = zodSchemas[key]
189 delete schema.$id
190 })
191 openApiDocument.components.schemas = {
192 ...openApiDocument.components.schemas,
193 ...zodSchemas as Record<string, SchemaObject>,
194 }
195 }
196 app.use(
197 `${config.openapi.path}/openapi.json`,
198 (_req: Request, res: Response) => { res.json(openApiDocument) },
199 )
200
201 app.use(
202 config.openapi.path,
203 apiReference({
204 persistAuth: true,
205 content: openApiDocument,
206 }),
207 )
208 }
209
210 app.enableShutdownHooks()
211
212 app.getHttpAdapter().get('/health', (_req, res) => res.status(HttpStatus.OK).send('OK'))
213 setupMetrics(app)
214
215 let closing: Promise<void> | undefined
216 app.getHttpAdapter().all('/_shutdown', async (_req: Request, res: Response) => {
217 res.status(HttpStatus.OK).send('Shutting down...')
218 if (closing)
219 return
220 closing = app.close()
221 await closing
222 process.exit(0)
223 })
224
225 app.useBodyParser('text', {
226 limit: '50mb',
227 type: ['text/*', 'application/xml', 'application/atom+xml'],
228 verify: preserveRawBody,
229 })
230 app.useBodyParser('json', { limit: '50mb', verify: preserveRawBody })
231 app.useBodyParser('urlencoded', { limit: '50mb', extended: true, verify: preserveRawBody })
232 app.set('query parser', 'extended')
233
234 app.disable('x-powered-by')
235
236 app.enable('trust proxy')
237
238 process.on('uncaughtException', (reason) => {
239 logger.error(reason)
240 })
241 process.on('unhandledRejection', (reason) => {
242 logger.error(reason, 'Unhandled Rejection')
243 })
244 process.on('exit', (code) => {
245 logger.log(`app exiting with code ${code}`)
246 })
247
248 await app.startAllMicroservices()
249 await app.listen(config.port, () => {
250 logger.log(`app started at port ${config.port}`)
251 if (config.openapi?.enable)
252 logger.log(`swagger docs: http://localhost:${config.port}${config.openapi.path}`)
253 })
254 }
255
255 lines TYPESCRIPT