Skip to content

Commit

Permalink
Merge pull request #44 from austinwoon/chore/response-serializer
Browse files Browse the repository at this point in the history
feat: zod serializer interceptor
  • Loading branch information
Evgeny Zakharov authored May 2, 2023
2 parents 6af7e68 + 2ad0bc6 commit 0683ca2
Show file tree
Hide file tree
Showing 2 changed files with 128 additions and 0 deletions.
76 changes: 76 additions & 0 deletions src/serializer.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { createMock } from '@golevelup/ts-jest'
import { CallHandler, ExecutionContext } from '@nestjs/common'
import { Reflector } from '@nestjs/core'
import { lastValueFrom, of } from 'rxjs'
import { z } from 'zod'
import { createZodDto } from './dto'
import { ZodValidationException } from './exception'
import { ZodSerializerInterceptor } from './serializer'

describe('ZodSerializerInterceptor', () => {
const UserSchema = z.object({
username: z.string(),
})

class UserDto extends createZodDto(UserSchema) {}

const testUser = {
username: 'test',
password: 'test',
}

const context = createMock<ExecutionContext>()

test('interceptor should strip out password', async () => {
const handler = createMock<CallHandler>({
handle: () => of(testUser),
})

const reflector = createMock<Reflector>({
getAllAndOverride: () => UserDto,
})

const interceptor = new ZodSerializerInterceptor(reflector)

const userObservable = interceptor.intercept(context, handler)
const user: typeof testUser = await lastValueFrom(userObservable)

expect(user.password).toBe(undefined)
expect(user.username).toBe('test')
})

test('wrong response shape should throw ZodValidationException', async () => {
const handler = createMock<CallHandler>({
handle: () => of({ user: 'test' }),
})

const reflector = createMock<Reflector>({
getAllAndOverride: () => UserDto,
})

const interceptor = new ZodSerializerInterceptor(reflector)

const userObservable = interceptor.intercept(context, handler)
expect(lastValueFrom(userObservable)).rejects.toBeInstanceOf(
ZodValidationException
)
})

test('interceptor should not strip out password if no UserDto is defined', async () => {
const handler = createMock<CallHandler>({
handle: () => of(testUser),
})

const reflector = createMock<Reflector>({
getAllAndOverride: jest.fn(),
})

const interceptor = new ZodSerializerInterceptor(reflector)

const userObservable = interceptor.intercept(context, handler)
const user: typeof testUser = await lastValueFrom(userObservable)

expect(user.password).toBe('test')
expect(user.username).toBe('test')
})
})
52 changes: 52 additions & 0 deletions src/serializer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import {
CallHandler,
ExecutionContext,
Inject,
Injectable,
NestInterceptor,
SetMetadata,
StreamableFile,
} from '@nestjs/common'
import { map, Observable } from 'rxjs'
import { ZodSchema } from 'zod'
import { ZodDto } from './dto'
import { validate } from './validate'

// NOTE (external)
// We need to deduplicate them here due to the circular dependency
// between core and common packages
const REFLECTOR = 'Reflector'

export const ZodSerializerDtoOptions = 'ZOD_SERIALIZER_DTO_OPTIONS' as const

export const ZodSerializerDto = (dto: ZodDto | ZodSchema) =>
SetMetadata(ZodSerializerDtoOptions, dto)

@Injectable()
export class ZodSerializerInterceptor implements NestInterceptor {
constructor(@Inject(REFLECTOR) protected readonly reflector: any) {}

intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const responseSchema = this.getContextResponseSchema(context)

return next.handle().pipe(
map((res: object | object[]) => {
if (!responseSchema) return res
if (typeof res !== 'object' || res instanceof StreamableFile) return res

return Array.isArray(res)
? res.map((item) => validate(item, responseSchema))
: validate(res, responseSchema)
})
)
}

protected getContextResponseSchema(
context: ExecutionContext
): ZodDto | ZodSchema | undefined {
return this.reflector.getAllAndOverride(ZodSerializerDtoOptions, [
context.getHandler(),
context.getClass(),
])
}
}

0 comments on commit 0683ca2

Please sign in to comment.