UNPKG

@ts-rest/nest

Version:

Nest server integration for @ts-rest

539 lines (520 loc) 22.9 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var common = require('@nestjs/common'); var rxjs = require('rxjs'); var core = require('@ts-rest/core'); var core$1 = require('@nestjs/core'); var zod = require('zod'); const TsRestAppRouteMetadataKey = Symbol('ts-rest-app-route'); const TsRestOptionsMetadataKey = Symbol('ts-rest-options'); /** * @deprecated Please use `TsRestHandler` instead - will be removed in v4 */ const initNestServer = (router) => { return { controllerShape: {}, routeShapes: {}, responseShapes: {}, route: router, }; }; /** * Returns the contract containing only non-nested routes required by a NestJS controller * * @deprecated Please use `TsRestHandler` instead - will be removed in v4 */ const nestControllerContract = (router) => { // it's not worth actually filtering the contract at runtime // the typing will already ensure that nested routes cannot be used at compile time return router; }; /****************************************************************************** Copyright (c) Microsoft Corporation. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ***************************************************************************** */ function __decorate(decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; } function __param(paramIndex, decorator) { return function (target, key) { decorator(target, key, paramIndex); } } function __metadata(metadataKey, metadataValue) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(metadataKey, metadataValue); } typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) { var e = new Error(message); return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e; }; const defaultOptions = { jsonQuery: false, validateResponses: false, validateRequestHeaders: true, validateRequestQuery: true, validateRequestBody: true, }; const evaluateTsRestOptions = (globalOptions, context) => { const handlerOptions = Reflect.getMetadata(TsRestOptionsMetadataKey, context.getHandler()); const classOptions = Reflect.getMetadata(TsRestOptionsMetadataKey, context.getClass()); return { ...defaultOptions, ...globalOptions, ...classOptions, ...handlerOptions, }; }; const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN: TS_REST_MODULE_OPTIONS_TOKEN, } = new common.ConfigurableModuleBuilder() .setExtras({ isGlobal: false, }, (definition, extras) => ({ ...definition, global: extras.isGlobal, })) .build(); exports.TsRestModule = class TsRestModule extends ConfigurableModuleClass { }; exports.TsRestModule = __decorate([ common.Module({ exports: [TS_REST_MODULE_OPTIONS_TOKEN], }) ], exports.TsRestModule); exports.TsRestInterceptor = class TsRestInterceptor { constructor(reflector, globalOptions) { this.reflector = reflector; this.globalOptions = globalOptions; } intercept(context, next) { const res = context.switchToHttp().getResponse(); const appRoute = this.reflector.get(TsRestAppRouteMetadataKey, context.getHandler()); if (!appRoute) { // this will respond with a 500 error without revealing this error message in the response body throw new Error('Make sure your route is decorated with @TsRest()'); } const options = evaluateTsRestOptions(this.globalOptions, context); return next.handle().pipe(rxjs.catchError((err) => { if (err instanceof core.TsRestResponseError) { return rxjs.of({ status: err.statusCode, body: err.body, }); } return rxjs.throwError(() => err); }), rxjs.map((value) => { if (core.isAppRouteResponse(value)) { const statusCode = value.status; const response = options.validateResponses ? core.validateResponse({ appRoute, response: value, }) : value; const responseType = appRoute.responses[statusCode]; if (core.isAppRouteOtherResponse(responseType)) { if ('setHeader' in res) { res.setHeader('content-type', responseType.contentType); } else { res.header('content-type', responseType.contentType); } } res.status(response.status); return response.body; } return value; })); } }; exports.TsRestInterceptor = __decorate([ common.Injectable(), __param(1, common.Optional()), __param(1, common.Inject(TS_REST_MODULE_OPTIONS_TOKEN)), __metadata("design:paramtypes", [core$1.Reflector, Object]) ], exports.TsRestInterceptor); /** * As a class decorator, you can configure ts-rest options. As a method decorator, you can assign the route and also configure options * @param appRouteOrOptions For a method decorator, this is the route. For a class decorator, this is the options * @param options For a method decorator, this is the options * * @deprecated Please use `TsRestHandler` instead - will be removed in v4 */ const TsRest = (appRouteOrOptions, options) => { const decorators = []; const isMethodDecorator = 'path' in appRouteOrOptions; const optionsToUse = isMethodDecorator ? options : appRouteOrOptions; if (isMethodDecorator) { decorators.push(...[ common.SetMetadata(TsRestAppRouteMetadataKey, appRouteOrOptions), getMethodDecorator(appRouteOrOptions), common.UseInterceptors(exports.TsRestInterceptor), ]); } if (optionsToUse) { decorators.push(common.SetMetadata(TsRestOptionsMetadataKey, optionsToUse)); } return common.applyDecorators(...decorators); }; const getMethodDecorator = (appRoute) => { switch (appRoute.method) { case 'DELETE': return common.Delete(appRoute.path); case 'GET': return common.Get(appRoute.path); case 'POST': return common.Post(appRoute.path); case 'PATCH': return common.Patch(appRoute.path); case 'PUT': return common.Put(appRoute.path); } }; /** * @deprecated Please use `TsRestHandler` instead - will be removed in v4 */ const Api = (appRoute) => { return TsRest(appRoute); }; let TsRestValidatorPipe = class TsRestValidatorPipe { constructor(globalOptions) { this.globalOptions = globalOptions; } transform(ctx) { const appRoute = Reflect.getMetadata(TsRestAppRouteMetadataKey, ctx.getHandler()); if (!appRoute) { // this will respond with a 500 error without revealing this error message in the response body throw new Error('Make sure your route is decorated with @TsRest()'); } const req = ctx.switchToHttp().getRequest(); const options = evaluateTsRestOptions(this.globalOptions, ctx); const pathParamsResult = core.checkZodSchema(req.params, appRoute.pathParams, { passThroughExtraKeys: true, }); if (!pathParamsResult.success) { throw new common.BadRequestException(core.zodErrorResponse(pathParamsResult.error)); } const headersResult = core.checkZodSchema(req.headers, appRoute.headers, { passThroughExtraKeys: true, }); if (!headersResult.success && options.validateRequestHeaders) { throw new common.BadRequestException(core.zodErrorResponse(headersResult.error)); } const query = options.jsonQuery ? core.parseJsonQueryObject(req.query) : req.query; const queryResult = core.checkZodSchema(query, appRoute.query); if (!queryResult.success && options.validateRequestQuery) { throw new common.BadRequestException(core.zodErrorResponse(queryResult.error)); } const bodyResult = core.checkZodSchema(req.body, appRoute.method === 'GET' ? null : appRoute.body); if (!bodyResult.success && options.validateRequestBody) { throw new common.BadRequestException(core.zodErrorResponse(bodyResult.error)); } return { query: queryResult.success ? queryResult.data : req.query, params: pathParamsResult.data, body: bodyResult.success ? bodyResult.data : req.body, headers: headersResult.success ? headersResult.data : req.headers, }; } }; TsRestValidatorPipe = __decorate([ common.Injectable(), __param(0, common.Optional()), __param(0, common.Inject(TS_REST_MODULE_OPTIONS_TOKEN)), __metadata("design:paramtypes", [Object]) ], TsRestValidatorPipe); /** * Parameter decorator used to parse, validate and return the typed request object * * @deprecated Please use `TsRestHandler` instead - will be removed in v4 */ const TsRestRequest = () => common.createParamDecorator((_, ctx) => { return ctx; })(TsRestValidatorPipe); /** * @deprecated Use `TsRestRequest` instead */ const ApiDecorator = TsRestRequest; class RequestValidationError extends common.BadRequestException { constructor(pathParams, headers, query, body) { super({ paramsResult: pathParams, headersResult: headers, queryResult: query, bodyResult: body, }); this.pathParams = pathParams; this.headers = headers; this.query = query; this.body = body; } } const RequestValidationErrorSchema = zod.z.object({ paramsResult: core.ZodErrorSchema.nullable(), headersResult: core.ZodErrorSchema.nullable(), queryResult: core.ZodErrorSchema.nullable(), bodyResult: core.ZodErrorSchema.nullable(), }); class ResponseValidationError extends common.InternalServerErrorException { constructor(appRoute, error) { super(`[ts-rest] Response validation failed for ${appRoute.method} ${appRoute.path}: ${error.message}`); this.appRoute = appRoute; this.error = error; } } const getHttpVerbDecorator = (route) => { switch (route.method) { case 'GET': return common.Get(route.path); case 'POST': return common.Post(route.path); case 'PUT': return common.Put(route.path); case 'PATCH': return common.Patch(route.path); case 'DELETE': return common.Delete(route.path); } }; const TsRestHandler = (appRouterOrRoute, options) => { return (target, propertyKey, descriptor) => { const isMultiHandler = !core.isAppRoute(appRouterOrRoute); /** * To make multi handler work we've got to do a trick with virtual methods in the class: * * Originally we used the @All decorator on the original method, but this has issues with different controllers conflicting * * Now, we make a new method for each route in the router, and apply the appropriate decorator to it. * * e.g. say there is a contract with two methods, `getPost` and `updatePost` * * we create two new methods in the class, `handler_getPost` and `handler_updatePost` * and decorate @Get on the first and @Put on the second * * Then, we call the original method from the new method */ if (isMultiHandler) { const originalMethod = descriptor.value; // Get parameter metadata using Nest's internal key const ROUTE_ARGS_METADATA = '__routeArguments__'; const originalParamMetadata = Reflect.getMetadata(ROUTE_ARGS_METADATA, target.constructor, propertyKey); Object.entries(appRouterOrRoute).forEach(([routeKey, route]) => { if (core.isAppRoute(route)) { const methodName = `${String(propertyKey)}_${routeKey}`; // Create new method that calls original target[methodName] = async function (...args) { return originalMethod.apply(this, args); }; if (originalParamMetadata) { Reflect.defineMetadata(ROUTE_ARGS_METADATA, originalParamMetadata, target.constructor, methodName); } const paramTypes = Reflect.getMetadata('design:paramtypes', target, propertyKey); if (paramTypes) { Reflect.defineMetadata('design:paramtypes', paramTypes, target, methodName); } const HttpVerbDecorator = getHttpVerbDecorator(route); HttpVerbDecorator(target, methodName, Object.getOwnPropertyDescriptor(target, methodName)); const reflector = new core$1.Reflector(); const metadataKeys = Reflect.getMetadataKeys(descriptor.value); metadataKeys.forEach((key) => { const metadata = reflector.get(key, descriptor.value); if (metadata) { common.SetMetadata(key, metadata)(target, methodName, Object.getOwnPropertyDescriptor(target, methodName)); } }); if (options) { common.SetMetadata(TsRestOptionsMetadataKey, options)(target, methodName, Object.getOwnPropertyDescriptor(target, methodName)); } common.SetMetadata(TsRestAppRouteMetadataKey, { appRoute: route, routeKey, })(target, methodName, Object.getOwnPropertyDescriptor(target, methodName)); common.UseInterceptors(exports.TsRestHandlerInterceptor)(target, methodName, Object.getOwnPropertyDescriptor(target, methodName)); } }); return descriptor; } else { /** * On the single handler we can just apply the HttpVerb decorator to the original method */ if (!core.isAppRoute(appRouterOrRoute)) { throw new Error('Expected app route but received app router'); } const HttpVerbDecorator = getHttpVerbDecorator(appRouterOrRoute); HttpVerbDecorator(target, propertyKey, descriptor); if (options) { common.SetMetadata(TsRestOptionsMetadataKey, options)(target, propertyKey, descriptor); } common.SetMetadata(TsRestAppRouteMetadataKey, { appRoute: appRouterOrRoute, routeKey: null, })(target, propertyKey, descriptor); common.UseInterceptors(exports.TsRestHandlerInterceptor)(target, propertyKey, descriptor); return descriptor; } }; }; /** * * @param contract - The contract or route to implement * @param implementation - Implementation of the route or entire contract as an object * @returns */ const tsRestHandler = (contract, implementation) => implementation; /** * Error you can throw to return a response from a handler */ class TsRestException extends common.HttpException { constructor(route, response, options) { super(response.body, response.status, options); } } exports.TsRestHandlerInterceptor = class TsRestHandlerInterceptor { constructor(reflector, globalOptions) { this.reflector = reflector; this.globalOptions = globalOptions; } /** * We use metadata to store the route, and the key of the route in a router on a given method */ getAppRouteFromContext(ctx) { const appRouteMetadata = this.reflector.get(TsRestAppRouteMetadataKey, ctx.getHandler()); if (!appRouteMetadata) { throw new Error('Could not find app router or app route, ensure you are using the @TsRestHandler decorator on your method'); } if (!core.isAppRoute(appRouteMetadata.appRoute)) { throw new Error('Expected app route but received app router'); } return appRouteMetadata; } intercept(ctx, next) { const res = ctx.switchToHttp().getResponse(); const req = ctx.switchToHttp().getRequest(); const { appRoute, routeKey } = this.getAppRouteFromContext(ctx); const options = evaluateTsRestOptions(this.globalOptions, ctx); const paramsResult = core.checkZodSchema(req.params, appRoute.pathParams, { passThroughExtraKeys: true, }); const headersResult = core.checkZodSchema(req.headers, appRoute.headers, { passThroughExtraKeys: true, }); const query = options.jsonQuery ? core.parseJsonQueryObject(req.query) : req.query; const queryResult = core.checkZodSchema(query, appRoute.query); const bodyResult = core.checkZodSchema(req.body, 'body' in appRoute ? appRoute.body : null); const isHeadersInvalid = !headersResult.success && options.validateRequestHeaders; const isQueryInvalid = !queryResult.success && options.validateRequestQuery; const isBodyInvalid = !bodyResult.success && options.validateRequestBody; if (!paramsResult.success || isHeadersInvalid || isQueryInvalid || isBodyInvalid) { throw new RequestValidationError(!paramsResult.success ? paramsResult.error : null, isHeadersInvalid ? headersResult.error : null, isQueryInvalid ? queryResult.error : null, isBodyInvalid ? bodyResult.error : null); } return next.handle().pipe(rxjs.mergeMap(async (impl) => { let result = null; try { const res = { params: paramsResult.data, query: queryResult.success ? queryResult.data : req.query, body: bodyResult.success ? bodyResult.data : req.body, headers: headersResult.success ? headersResult.data : req.headers, }; /** * If we have a routeKey that means we're in a multi handler, and therefore we * need to call the appropriate method WITHIN the implementation object */ result = routeKey ? await impl[routeKey](res) : await impl(res); } catch (e) { if (e instanceof TsRestException) { result = { status: e.getStatus(), body: e.getResponse(), error: e, }; } else if (e instanceof core.TsRestResponseError) { result = { status: e.statusCode, body: e.body, }; } else { throw e; } } const responseAfterValidation = options.validateResponses ? validateResponse(appRoute, result) : result; const responseType = appRoute.responses[result.status]; if (result.error) { throw new common.HttpException(responseAfterValidation.body, responseAfterValidation.status, { cause: result.error, }); } if (core.isAppRouteOtherResponse(responseType)) { if ('setHeader' in res) { res.setHeader('content-type', responseType.contentType); } else { res.header('content-type', responseType.contentType); } } res.status(responseAfterValidation.status); return responseAfterValidation.body; })); } }; exports.TsRestHandlerInterceptor = __decorate([ common.Injectable(), __param(1, common.Optional()), __param(1, common.Inject(TS_REST_MODULE_OPTIONS_TOKEN)), __metadata("design:paramtypes", [core$1.Reflector, Object]) ], exports.TsRestHandlerInterceptor); const validateResponse = (appRoute, response) => { const { body } = response; const responseType = appRoute.responses[response.status]; const responseSchema = core.isAppRouteOtherResponse(responseType) ? responseType.body : responseType; if (!responseSchema) { throw new common.InternalServerErrorException(`[ts-rest] Couldn't find a response schema for ${response.status} on route ${appRoute.path}`); } const responseValidation = core.checkZodSchema(body, appRoute.responses[response.status]); if (!responseValidation.success) { const { error } = responseValidation; throw new ResponseValidationError(appRoute, error); } return { status: response.status, body: responseValidation.data, }; }; exports.Api = Api; exports.ApiDecorator = ApiDecorator; exports.RequestValidationError = RequestValidationError; exports.RequestValidationErrorSchema = RequestValidationErrorSchema; exports.ResponseValidationError = ResponseValidationError; exports.TsRest = TsRest; exports.TsRestAppRouteMetadataKey = TsRestAppRouteMetadataKey; exports.TsRestException = TsRestException; exports.TsRestHandler = TsRestHandler; exports.TsRestOptionsMetadataKey = TsRestOptionsMetadataKey; exports.TsRestRequest = TsRestRequest; exports.initNestServer = initNestServer; exports.nestControllerContract = nestControllerContract; exports.tsRestHandler = tsRestHandler;