UNPKG

@opra/nestjs-http

Version:

Opra NestJS Http Module

247 lines (246 loc) 10.6 kB
import { __decorate, __metadata, __param } from "tslib"; import * as http from 'node:http'; import {} from 'node:http'; import nodePath from 'node:path'; import { isConstructor } from '@jsopen/objects'; import { Controller, Delete, Get, Head, HttpCode, Next, Options, Patch, Post, Put, Req, Res, Search, } from '@nestjs/common'; import { HTTP_CONTROLLER_METADATA, HttpApi, HttpController, HttpOperation, NotFoundError, } from '@opra/common'; import { HttpAdapter, HttpBundle, HttpContext, HttpRequest, HttpResponse, } from '@opra/http'; import { OpraNestUtils, Public } from '@opra/nestjs'; import { asMutable } from 'ts-gems'; /** * OpraHttpNestjsAdapter * * HTTP adapter used to integrate OPRA APIs with the NestJS framework. * This adapter converts OPRA controllers into NestJS controllers and * exports the OPRA documentation ($schema). */ export class OpraHttpNestjsAdapter extends HttpAdapter { /** List of controller classes to be registered with NestJS */ nestControllers = []; /** Platform identifier */ platform = 'nestjs'; /** * Creates a new instance of OpraHttpNestjsAdapter. * * @param options - Adapter configuration options. */ constructor(options) { super(options); this._addRootController(options.schemaIsPublic); if (options.controllers) { for (const c of options.controllers) { this._addToNestControllers(c, this.basePath, []); } } } /** * Closes the adapter. * @returns {Promise<void>} */ async close() { // } async createContext(_req, _res, args) { const ctx = await super.createContext(_req, _res, args); // @ts-ignore ctx.platform = _req.route ? 'express' : 'fastify'; return ctx; } _httpHandler; setHttpHandler(handler) { this._httpHandler = handler; } async handleRawRequest(req, res) { if (!this._httpHandler) throw new Error('HTTP handler is not initialized. Call setHttpHandler() first.'); return new Promise((resolve, reject) => { res.once('finish', resolve); res.once('error', reject); this._httpHandler(req, res); }); } /** * Adds the root controller that serves the OPRA schema. * * @param isPublic - Whether the schema is accessible without authentication. * @protected */ _addRootController(isPublic) { const _this = this; let RootController = class RootController { schema(_req, next) { _this.sendDocumentSchema(_req.opraContext).catch(() => next()); } bundle(_req, _res, next) { Promise.resolve() .then(async () => { const bundle = new HttpBundle({ __adapter: _this, platform: _req.route ? 'express' : 'fastify', request: HttpRequest.create(_req), response: HttpResponse.create(_res), }); await _this.emitAsync('create-bundle', bundle); await _this.handleBundle(bundle); }) .catch(() => next()); } }; __decorate([ Get('/\\$schema'), __param(0, Req()), __param(1, Next()), __metadata("design:type", Function), __metadata("design:paramtypes", [Object, Function]), __metadata("design:returntype", void 0) ], RootController.prototype, "schema", null); __decorate([ Post('/\\$bundle'), HttpCode(200), __param(0, Req()), __param(1, Res()), __param(2, Next()), __metadata("design:type", Function), __metadata("design:paramtypes", [Object, Object, Function]), __metadata("design:returntype", void 0) ], RootController.prototype, "bundle", null); RootController = __decorate([ Controller({ path: this.basePath, }) ], RootController); if (isPublic) { Public()(RootController.prototype, 'schema', Object.getOwnPropertyDescriptor(RootController.prototype, 'schema')); } this.nestControllers.push(RootController); } /** * Adds the specified class and its sub-controllers to the NestJS controller list. * * @param sourceClass - Source OPRA controller class. * @param currentPath - Current URL path. * @param parentTree - List of parent controller classes. * @protected * @throws {@link NotFoundError} Thrown when no suitable endpoint is found for the operation. * @throws {@link TypeError} Thrown when the controller is not a class. */ _addToNestControllers(sourceClass, currentPath, parentTree) { const metadata = Reflect.getMetadata(HTTP_CONTROLLER_METADATA, sourceClass); if (!metadata) return; /* Create a new controller class */ const newClass = { [sourceClass.name]: class extends sourceClass { }, }[sourceClass.name]; /* Copy metadata keys from source class to new one */ OpraNestUtils.copyDecoratorMetadata(newClass, ...parentTree); Controller()(newClass); const newPath = metadata.path ? nodePath.posix.join(currentPath, metadata.path) : currentPath; const adapter = this; // adapter.logger = /* Disable default error handler. Errors will be handled by OpraExceptionFilter */ adapter.on('error', (error) => { throw error; }); this.nestControllers.push(newClass); let metadataKeys; if (metadata.operations) { for (const [k, v] of Object.entries(metadata.operations)) { const operationHandler = sourceClass.prototype[k]; Object.defineProperty(newClass.prototype, k, { writable: true, /* NestJS handler method */ async value(_req, _res) { _res.statusCode = 200; const api = adapter.document.api; const controller = api.findController(sourceClass); const operation = controller?.operations.get(k); const context = asMutable(_req.opraContext); if (!(context && operation && typeof operationHandler === 'function')) { throw new NotFoundError({ message: `No endpoint found for [${_req.method}]${_req.baseUrl}`, details: { path: _req.baseUrl, method: _req.method, }, }); } /* Configure the HttpContext */ context.__docNode = operation.node; context.__oprDef = operation; context.__contDef = operation.owner; context.__controller = this; context.__handler = operationHandler; /* Handle request */ await adapter.handleRequest(context); }, }); /* Copy metadata keys from source function to new one */ metadataKeys = Reflect.getOwnMetadataKeys(operationHandler); const newFn = newClass.prototype[k]; for (const key of metadataKeys) { const m = Reflect.getMetadata(key, operationHandler); Reflect.defineMetadata(key, m, newFn); } Req()(newClass.prototype, k, 0); Res()(newClass.prototype, k, 1); const descriptor = Object.getOwnPropertyDescriptor(newClass.prototype, k); const operationPath = v.mergePath ? newPath + (v.path || '') : nodePath.posix.join(newPath, v.path || ''); switch (v.method || 'GET') { case 'DELETE': /* Call @Delete decorator over new property */ Delete(operationPath)(newClass.prototype, k, descriptor); break; case 'GET': /* Call @Get decorator over new property */ Get(operationPath)(newClass.prototype, k, descriptor); break; case 'HEAD': /* Call @Head decorator over new property */ Head(operationPath)(newClass.prototype, k, descriptor); break; case 'OPTIONS': /* Call @Options decorator over new property */ Options(operationPath)(newClass.prototype, k, descriptor); break; case 'PATCH': /* Call @Patch decorator over new property */ Patch(operationPath)(newClass.prototype, k, descriptor); break; case 'POST': /* Call @Post decorator over new property */ Post(operationPath)(newClass.prototype, k, descriptor); break; case 'PUT': /* Call @Put decorator over new property */ Put(operationPath)(newClass.prototype, k, descriptor); break; case 'SEARCH': /* Call @Search decorator over new property */ Search(operationPath)(newClass.prototype, k, descriptor); break; default: break; } } } if (metadata.controllers) { for (const child of metadata.controllers) { if (!isConstructor(child)) throw new TypeError('Controllers should be injectable a class'); this._addToNestControllers(child, newPath, [ ...parentTree, sourceClass, ]); } } } }