Skip to content

Commit 37b33b6

Browse files
Merge pull request #78 from uniocjs/dev-groupguanfang
feat: add testing file for filter, update nest.js handler's param
2 parents 6f16505 + fc9c373 commit 37b33b6

File tree

6 files changed

+191
-24
lines changed

6 files changed

+191
-24
lines changed

.changeset/tall-dogs-shine.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@unioc/adapter-nestjs": patch
3+
"@unioc/core": patch
4+
---
5+
6+
feat: add testing file for filter, update nest.js handler's param

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
"bumpp": "^10.1.0",
4141
"create-unioc": "workspace:*",
4242
"eslint": "^9.24.0",
43+
"express": "^5.1.0",
4344
"external-editor": "^3.1.0",
4445
"fast-glob": "^3.3.3",
4546
"js-yaml": "^4.1.0",

packages/adapter/adapter-nestjs/src/restful/restful-handler.ts

+56-16
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,39 @@ export class NestJSRestfulHandler implements IRestfulConnect.Handler {
1919
if (typeof ctx.param === 'object' && ctx.param !== null)
2020
return currentMetadata.data ? (ctx.param as Record<string, unknown>)[currentMetadata.data as string] : ctx.param
2121
return ctx.param
22+
23+
case 'ip':
24+
return ctx.ip
25+
26+
case 'context':
27+
throw new Error('[adapter-nestjs] The param type "context" is not supported.')
28+
29+
case 'request':
30+
return ctx.request
31+
32+
case 'response':
33+
return ctx.response
34+
35+
case 'next':
36+
return ctx.next
37+
38+
case 'file':
39+
return ctx.file
40+
41+
case 'files':
42+
return ctx.files
43+
44+
case 'headers':
45+
return ctx.headers
46+
47+
case 'host':
48+
return ctx.host
49+
50+
case 'rawBody':
51+
return ctx.rawBody
52+
53+
case 'session':
54+
return ctx.session
2255
}
2356
}
2457

@@ -42,8 +75,10 @@ export class NestJSRestfulHandler implements IRestfulConnect.Handler {
4275

4376
if (!currentMetadata)
4477
continue
45-
if (currentMetadata.paramType !== 'body' && currentMetadata.paramType !== 'query' && currentMetadata.paramType !== 'param' && currentMetadata.paramType !== 'custom')
78+
if (currentMetadata.paramType !== 'body' && currentMetadata.paramType !== 'query' && currentMetadata.paramType !== 'param' && currentMetadata.paramType !== 'custom') {
79+
params[i] = this._getParamValue(currentMetadata, ctx)
4680
continue
81+
}
4782

4883
const transformedParams = await methodWrapper.getControllerOperator()
4984
.getControllerWrapper()
@@ -90,28 +125,33 @@ export class NestJSRestfulHandler implements IRestfulConnect.Handler {
90125
if (ctx.response.writableEnded || ctx.response.writableFinished)
91126
return
92127

93-
if ('send' in ctx.response && typeof ctx.response.send === 'function') {
94-
ctx.response.send(
95-
result.type === 'result'
96-
? result.value
97-
: {
98-
statusCode: 500,
99-
message: 'Internal Server Error',
100-
error: await this._toReadableError(result.error),
101-
},
102-
)
128+
if ('send' in ctx.response && typeof ctx.response.send === 'function' && 'status' in ctx.response && typeof ctx.response.status === 'function') {
129+
if (result.type === 'result') {
130+
ctx.response.send(result.value)
131+
}
132+
else {
133+
ctx.response.status(500).send({
134+
statusCode: 500,
135+
message: 'Internal Server Error',
136+
error: await this._toReadableError(result.error),
137+
})
138+
}
103139
return
104140
}
105141

106-
ctx.response.end(
107-
result.type === 'result'
108-
? await this._toSendableString(result.value)
109-
: await this._toSendableString({
142+
if (result.type === 'result') {
143+
ctx.response.end(await this._toSendableString(result.value))
144+
}
145+
else {
146+
ctx.response.statusCode = 500
147+
ctx.response.end(
148+
await this._toSendableString({
110149
statusCode: 500,
111150
message: 'Internal Server Error',
112151
error: await this._toReadableError(result.error),
113152
}),
114-
)
153+
)
154+
}
115155
}
116156

117157
private async _toReadableError(error: unknown): Promise<Record<string, unknown>> {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import { IncomingMessage, ServerResponse } from 'node:http'
2+
import { ArgumentsHost, Body, Catch, ContextType, Controller, ExceptionFilter, Get, Next, Param, Post, Query, Req, Res, UseFilters } from '@nestjs/common'
3+
import { ExpressApp } from '@unioc/web-express'
4+
import express, { NextFunction, Request, Response } from 'express'
5+
import request from 'supertest'
6+
import { NestJS } from '../src'
7+
import 'reflect-metadata'
8+
9+
it('should use internal error message', async () => {
10+
@Controller()
11+
class TestController {
12+
@Get()
13+
willThrowError() {
14+
throw new Error('test')
15+
}
16+
}
17+
18+
const app = new ExpressApp().use(NestJS, {
19+
controllers: [TestController],
20+
})
21+
await app.initialize()
22+
await request(app.getExpressApp())
23+
.get('/')
24+
.expect('Content-Type', /application\/json/)
25+
.expect(500)
26+
.expect((res) => {
27+
expect(res.body.statusCode).toBe(500)
28+
expect(res.body.message).toBe('Internal Server Error')
29+
expect(res.body.error.message).toBe('test')
30+
expect(res.body.error.stack).toMatch(/Error: test/)
31+
expect(res.body.error.name).toBe('Error')
32+
})
33+
})
34+
35+
it('should use filter & testing params', async () => {
36+
@Catch()
37+
class TestFilter implements ExceptionFilter {
38+
catch(exception: any, host: ArgumentsHost) {
39+
const index1 = host.getArgByIndex(0)
40+
expect(index1).toBe('1')
41+
42+
const index2 = host.getArgByIndex(1)
43+
expect(index2).toBeInstanceOf(IncomingMessage)
44+
45+
const index3 = host.getArgByIndex(2)
46+
expect(index3).toBeInstanceOf(ServerResponse)
47+
48+
const index4 = host.getArgByIndex(3)
49+
expect(index4).toBeInstanceOf(Function)
50+
51+
const contextType = host.getType()
52+
expect(contextType).toBe('http' satisfies ContextType)
53+
54+
const ctx = host.switchToHttp()
55+
const request = ctx.getRequest<import('express').Request>()
56+
expect(request).toBeInstanceOf(IncomingMessage)
57+
const response = ctx.getResponse<import('express').Response>()
58+
59+
console.error('Handled a error in [filter.test.ts: `should use filter`]:')
60+
console.error(exception)
61+
62+
response.status(500).json({
63+
statusCode: 500,
64+
message: 'Throw an error and catch in test filter!',
65+
error: exception.message,
66+
})
67+
}
68+
}
69+
70+
@Controller()
71+
@UseFilters(TestFilter)
72+
class TestController {
73+
@Post('/test/:id')
74+
willThrowError(
75+
@Query('a') a: string,
76+
@Req() request: Request,
77+
@Res() response: Response,
78+
@Next() next: NextFunction,
79+
@Param() param: Record<string, unknown>,
80+
@Param('id') id: string,
81+
@Body() body: any,
82+
) {
83+
expect(a).toBe('1')
84+
expect(request).toBeInstanceOf(IncomingMessage)
85+
expect(response).toBeInstanceOf(ServerResponse)
86+
expect(next).toBeInstanceOf(Function)
87+
expect(Object.keys(param).length).toBe(1)
88+
expect(param.id).toBe('1')
89+
expect(id).toBe('1')
90+
expect(Object.keys(body).length).toBe(1)
91+
throw new Error('test')
92+
}
93+
}
94+
95+
const app = new ExpressApp().use(NestJS, {
96+
controllers: [TestController],
97+
providers: [TestFilter],
98+
})
99+
app.getExpressApp().use(express.json())
100+
await app.initialize()
101+
102+
await request(app.getExpressApp())
103+
.post('/test/1')
104+
.query({ a: '1' })
105+
.send({
106+
foo: 'bar',
107+
})
108+
.expect('Content-Type', /application\/json/)
109+
.expect(500)
110+
.expect((res) => {
111+
expect(res.body.statusCode).toBe(500)
112+
expect(res.body.message).toBe('Throw an error and catch in test filter!')
113+
expect(res.body.error).toBe('test')
114+
})
115+
})

packages/core/core/src/contexts/classes/class-executor.ts

+10-8
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { IClass, IResult } from '@unioc/shared'
1+
import type { IClass, IError, IResult, ISuccess } from '@unioc/shared'
22
import type { ClassWrapper } from '../../container/class-wrapper'
33
import type { IClassExecutor, IClassWrapper } from '../../types'
44

@@ -20,20 +20,22 @@ export class ClassExecutor<TClass extends IClass = IClass> implements IClassExec
2020
const invokeContext = await pluginExecutor.executeInvoke(classWrapper, options.methodName, options.arguments)
2121
const args = invokeContext.getMethodArguments()
2222

23-
try {
24-
const result = await instance[options.methodName](...args)
23+
async function executor(): Promise<unknown> {
24+
return await instance[options.methodName](...args)
25+
}
26+
27+
return executor().then(async (result) => {
2528
const handleContext = await pluginExecutor.executeHandle(classWrapper, options.methodName, args, result, 'result', options.extraOptions)
2629
return {
2730
type: 'result',
2831
value: handleContext.getExecuteResult() as T,
29-
}
30-
}
31-
catch (error) {
32+
} satisfies ISuccess<T>
33+
}).catch(async (error) => {
3234
const handleContext = await pluginExecutor.executeHandle(classWrapper, options.methodName, args, error, 'error', options.extraOptions)
3335
return {
3436
type: 'error',
3537
error: handleContext.getExecuteResult() as TE,
36-
}
37-
}
38+
} satisfies IError<TE>
39+
})
3840
}
3941
}

pnpm-lock.yaml

+3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)