Skip to content

Commit 1f657ab

Browse files
Merge pull request #80 from uniocjs/dev-groupguanfang
2 parents 0a024fe + d7c2422 commit 1f657ab

File tree

7 files changed

+192
-71
lines changed

7 files changed

+192
-71
lines changed

.changeset/chubby-impalas-invite.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@unioc/adapter-nestjs": patch
3+
---
4+
5+
feat: implement guard support (#24)

packages/adapter/adapter-nestjs/src/arguments-host-builder.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ import type { HttpArgumentsHost, RpcArgumentsHost, WsArgumentsHost } from '@nest
33

44
export class ArgumentsHostBuilder implements ArgumentsHost {
55
constructor(
6-
private readonly _args: readonly unknown[],
7-
private readonly _type: ContextType,
6+
protected readonly _args: readonly unknown[],
7+
protected readonly _type: ContextType,
88
) {}
99

1010
private _httpArgumentsHost: HttpArgumentsHost = {} as any
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import type { ContextType, ExecutionContext, Type } from '@nestjs/common'
2+
import type { IClass } from '@unioc/shared'
3+
import { ArgumentsHostBuilder } from './arguments-host-builder'
4+
5+
export class ExecutionContextBuilder extends ArgumentsHostBuilder implements ExecutionContext {
6+
constructor(
7+
protected readonly _args: readonly unknown[],
8+
protected readonly _type: ContextType,
9+
protected readonly _target: IClass,
10+
protected readonly _handler: (...args: unknown[]) => unknown,
11+
) {
12+
super(_args, _type)
13+
}
14+
15+
getClass<T = any>(): Type<T> {
16+
return this._target
17+
}
18+
19+
getHandler(): (...args: unknown[]) => unknown {
20+
return this._handler
21+
}
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import type { IResult } from '@unioc/shared'
2+
import type { IRestfulConnect } from '@unioc/web'
3+
4+
export class EndingHandler {
5+
async sendConnectEndingResponse(ctx: IRestfulConnect.WebContext, result: IResult): Promise<void> {
6+
if (ctx.response.writableEnded || ctx.response.writableFinished)
7+
return
8+
9+
if ('send' in ctx.response && typeof ctx.response.send === 'function' && 'status' in ctx.response && typeof ctx.response.status === 'function') {
10+
if (result.type === 'result') {
11+
ctx.response.send(result.value)
12+
}
13+
else {
14+
ctx.response.status(500).send({
15+
statusCode: 500,
16+
message: 'Internal Server Error',
17+
error: await this._toReadableError(result.error),
18+
})
19+
}
20+
return
21+
}
22+
23+
if (result.type === 'result') {
24+
ctx.response.end(await this._toSendableString(result.value))
25+
}
26+
else {
27+
ctx.response.statusCode = 500
28+
ctx.response.end(
29+
await this._toSendableString({
30+
statusCode: 500,
31+
message: 'Internal Server Error',
32+
error: await this._toReadableError(result.error),
33+
}),
34+
)
35+
}
36+
}
37+
38+
private async _stringifyAsync(data: unknown): Promise<string> {
39+
return JSON.stringify(data)
40+
}
41+
42+
private async _toSendableString(data: unknown): Promise<string> {
43+
if (typeof data === 'string')
44+
return data
45+
if (typeof data === 'object' && data !== null) {
46+
return await this._stringifyAsync(data)
47+
.catch(async error => this._stringifyAsync({
48+
statusCode: 500,
49+
message: 'Internal Server Error',
50+
error: await this._toReadableError(error),
51+
}))
52+
.catch(async error => this._stringifyAsync({
53+
statusCode: 500,
54+
message: 'Internal Server Error',
55+
error: await this._toReadableError(error),
56+
}))
57+
}
58+
return String(data)
59+
}
60+
61+
private async _toReadableError(error: unknown): Promise<Record<string, unknown>> {
62+
if (error && error instanceof Error) {
63+
const result: Record<string, unknown> = {}
64+
if (error.name !== undefined)
65+
result.name = error.name
66+
for (const key of Reflect.ownKeys(error))
67+
result[key as string] = error[key as keyof Error]
68+
return result
69+
}
70+
return error as Record<string, unknown>
71+
}
72+
}

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

+47-68
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import type { IMethodExecuteOptions, IRestfulConnect, IRestMethodOperator } from '@unioc/web'
22
import type { INestJSMethodParamMetadata, NestJSMethodWrapper } from './method-wrapper'
3+
import { UnauthorizedException } from '@nestjs/common'
4+
import { ExecutionContextBuilder } from '../execution-context-builder'
5+
import { EndingHandler } from './ending-handler'
36
import { NestJSMethodOperator } from './method-operator'
47

58
export class NestJSRestfulHandler implements IRestfulConnect.Handler {
@@ -55,7 +58,7 @@ export class NestJSRestfulHandler implements IRestfulConnect.Handler {
5558
}
5659
}
5760

58-
private async _buildParams(methodWrapper: NestJSMethodWrapper, ctx: IRestfulConnect.WebContext): Promise<unknown[]> {
61+
protected async buildParams(methodWrapper: NestJSMethodWrapper, ctx: IRestfulConnect.WebContext): Promise<unknown[]> {
5962
const params: unknown[] = []
6063
const metadata = methodWrapper.getMethodParamMetadata()
6164
const paramTypes = methodWrapper.getParamTypes()
@@ -109,83 +112,59 @@ export class NestJSRestfulHandler implements IRestfulConnect.Handler {
109112
&& typeof options.adapterType === 'string'
110113
}
111114

115+
private readonly _endingHandler = new EndingHandler()
116+
117+
protected async executeGuards(methodWrapper: NestJSMethodWrapper, ctx: IRestfulConnect.WebContext, params: unknown[]): Promise<void> {
118+
const methodGuards = methodWrapper.getMethodGuards()
119+
const controllerGuards = methodWrapper.getControllerOperator().getControllerGuards()
120+
121+
const controllerTarget = methodWrapper
122+
.getControllerOperator()
123+
.getControllerWrapper()
124+
.getClassWrapper()
125+
.getTarget()
126+
127+
const resolvedGuards = await methodWrapper.getControllerOperator()
128+
.getControllerWrapper()
129+
.getRestfulScanner()
130+
.mergeAndResolveToInstance(
131+
methodGuards,
132+
controllerGuards,
133+
)
134+
135+
const canActivate = await methodWrapper.getControllerOperator()
136+
.getControllerWrapper()
137+
.getRestfulScanner()
138+
.executeGuards(
139+
resolvedGuards,
140+
new ExecutionContextBuilder(
141+
params,
142+
'http',
143+
controllerTarget,
144+
controllerTarget.prototype[methodWrapper.getPropertyKey()],
145+
),
146+
)
147+
148+
if (canActivate === false)
149+
throw new UnauthorizedException()
150+
}
151+
112152
async handleConnectRequest(methodOperator: IRestMethodOperator, ctx: IRestfulConnect.WebContext): Promise<void> {
113153
if (!(methodOperator instanceof NestJSMethodOperator))
114154
throw new Error('Method operator is not a NestJSMethodOperator')
115155

116156
const methodWrapper = methodOperator.getMethodWrapper()
117-
const params = await this._buildParams(methodWrapper, ctx)
118157

158+
// 1. Build params with pipes
159+
const params = await this.buildParams(methodWrapper, ctx)
160+
// 2. Execute guards
161+
await this.executeGuards(methodWrapper, ctx, params)
162+
// 3. Execute the controller method
119163
const result = await methodWrapper.execute(params, {
120164
webContext: ctx,
121165
adapterType: 'connect',
122166
handlerType: 'nestjs',
123167
})
124-
125-
if (ctx.response.writableEnded || ctx.response.writableFinished)
126-
return
127-
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-
}
139-
return
140-
}
141-
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({
149-
statusCode: 500,
150-
message: 'Internal Server Error',
151-
error: await this._toReadableError(result.error),
152-
}),
153-
)
154-
}
155-
}
156-
157-
private async _toReadableError(error: unknown): Promise<Record<string, unknown>> {
158-
if (error && error instanceof Error) {
159-
const result: Record<string, unknown> = {}
160-
if (error.name !== undefined)
161-
result.name = error.name
162-
for (const key of Reflect.ownKeys(error))
163-
result[key as string] = error[key as keyof Error]
164-
return result
165-
}
166-
return error as Record<string, unknown>
167-
}
168-
169-
private async _stringifyAsync(data: unknown): Promise<string> {
170-
return JSON.stringify(data)
171-
}
172-
173-
private async _toSendableString(data: unknown): Promise<string> {
174-
if (typeof data === 'string')
175-
return data
176-
if (typeof data === 'object' && data !== null) {
177-
return await this._stringifyAsync(data)
178-
.catch(async error => this._stringifyAsync({
179-
statusCode: 500,
180-
message: 'Internal Server Error',
181-
error: await this._toReadableError(error),
182-
}))
183-
.catch(async error => this._stringifyAsync({
184-
statusCode: 500,
185-
message: 'Internal Server Error',
186-
error: await this._toReadableError(error),
187-
}))
188-
}
189-
return String(data)
168+
await this._endingHandler.sendConnectEndingResponse(ctx, result)
190169
}
191170
}

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

+11-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { ArgumentMetadata, ArgumentsHost, ExceptionFilter, PipeTransform } from '@nestjs/common'
1+
import type { ArgumentMetadata, ArgumentsHost, CanActivate, ExceptionFilter, ExecutionContext, PipeTransform } from '@nestjs/common'
22
import type { IArgument } from '@unioc/core'
33
import type { IClass } from '@unioc/shared'
44
import type { IHttpParam, IRestfulConnect, IRestfulScanner } from '@unioc/web'
@@ -120,6 +120,16 @@ export class NestJSRestfulScanner extends RestfulScanner implements IRestfulScan
120120
return 'no-match'
121121
}
122122

123+
async executeGuards(resolvedGuards: CanActivate[], context: ExecutionContext): Promise<boolean> {
124+
for (const guard of resolvedGuards) {
125+
const canActivate = await guard.canActivate(context)
126+
if (canActivate === true)
127+
continue
128+
return false
129+
}
130+
return true
131+
}
132+
123133
resolveConnectHandler(): IRestfulConnect.Handler {
124134
return new NestJSRestfulHandler()
125135
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { CanActivate, Controller, ExecutionContext, Get, Injectable, UseGuards } from '@nestjs/common'
2+
import { ExpressApp } from '@unioc/web-express'
3+
import request from 'supertest'
4+
import { NestJS } from '../src'
5+
import 'reflect-metadata'
6+
7+
it('should use guard', async () => {
8+
@Injectable()
9+
class TestGuard implements CanActivate {
10+
canActivate(_context: ExecutionContext): boolean {
11+
return false
12+
}
13+
}
14+
15+
@Controller()
16+
class TestController {
17+
@Get('/test-guard')
18+
@UseGuards(TestGuard)
19+
test() {
20+
return 'ok'
21+
}
22+
}
23+
24+
const app = new ExpressApp().use(NestJS, {
25+
controllers: [TestController],
26+
providers: [TestGuard],
27+
})
28+
await app.initialize()
29+
30+
await request(app.getExpressApp())
31+
.get('/test-guard')
32+
.expect(401)
33+
})

0 commit comments

Comments
 (0)