@opra/nestjs-http
Version:
Opra NestJS Http Module
247 lines (246 loc) • 10.6 kB
JavaScript
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,
]);
}
}
}
}