arrest
Version:
OpenAPI v3 compliant REST framework for Node.js, with support for MongoDB and JSON-Schema
248 lines • 9.76 kB
JavaScript
import { Scopes } from '@vivocha/scopes';
import { getLogger } from 'debuggo';
import { Eredita } from 'eredita';
import express, { Router } from 'express';
import http from 'http';
import https from 'https';
import _ from 'lodash';
import needle from 'needle';
import { parse, rebase, ValidationError } from 'openapi-police';
import semver from 'semver';
import { deprecate } from 'util';
import { DEFAULT_DOCUMENT } from './defaults.js';
import { RESTError } from './error.js';
import { SchemaResource } from './schema.js';
import { rebaseOASDefinitions, refsRebaser, removeSchemaDeclaration, removeUnusedSchemas } from './util.js';
let reqId = 0;
export class API {
document;
logger;
resources;
originalSchemas = {};
dynamicSchemas = {};
internalRouter;
parseOptions;
constructor(info) {
this.document = Eredita.deepExtend(_.cloneDeep(DEFAULT_DOCUMENT), { info: info || {} });
if (!semver.valid(this.document.info.version)) {
throw new Error('Invalid version');
}
this.logger = getLogger(this.getDebugLabel(), undefined, false);
this.resources = [];
this.parseOptions = {
scope: 'http://vivocha.com/api/v3',
retriever: this.defaultSchemaRetriever.bind(this),
};
}
getDebugLabel() {
return 'arrest';
}
getDebugContext() {
return `#${++reqId}`;
}
async defaultSchemaRetriever(url) {
const response = await needle('get', url);
if (response.statusCode !== 200) {
throw new RESTError(response.statusCode);
}
return response.body;
}
addResource(resource) {
this.resources.push(resource);
resource.attach(this);
return this;
}
registerSchema(name, schema) {
this.originalSchemas[name] = _.cloneDeep(schema);
if (!this.document.components) {
this.document.components = {};
}
if (!this.document.components.schemas) {
this.document.components.schemas = {};
}
this.document.components.schemas[name] = removeSchemaDeclaration(rebase(name, _.cloneDeep(schema), refsRebaser));
}
registerDynamicSchema(name, schema) {
this.dynamicSchemas[name] = schema;
}
registerOperation(path, method, operation) {
if (!this.document.paths) {
this.document.paths = {};
}
let _path = path;
if (_path.length > 1 && _path[_path.length - 1] === '/') {
_path = _path.substr(0, _path.length - 1);
}
if (!this.document.paths[_path]) {
this.document.paths[_path] = {};
}
this.document.paths[_path][method] = operation;
return () => this.document.paths[_path][method];
}
registerTag(tag) {
if (!this.document.tags) {
this.document.tags = [];
}
this.document.tags.push(tag);
}
registerOauth2Scope(name, description) {
const oauth2Defs = this.getOauth2Schemes();
oauth2Defs.forEach((i) => {
for (let f in i.flows) {
const flow = i.flows[f];
if (!flow.scopes) {
flow.scopes = {};
}
flow.scopes[name] = description;
}
});
}
getOauth2Schemes() {
const out = [];
if (this.document.components && this.document.components.securitySchemes) {
const schemes = this.document.components.securitySchemes;
for (let k in schemes) {
const s = schemes[k];
if (s.type === 'oauth2') {
out.push(s);
}
}
}
return out;
}
async listen(httpPort, httpsPort, httpsOptions) {
if (!httpPort && !httpsPort) {
throw new Error('no listen ports specified');
}
let router = await this.router();
let app = express();
app.use(router);
app.use(API.handle404Error);
let out = [];
if (httpsPort) {
if (!httpsOptions) {
throw new Error('no https options');
}
else {
out.push(https.createServer(httpsOptions, app).listen(httpsPort));
}
}
if (httpPort) {
out.push(http.createServer(app).listen(httpPort));
}
return out.length == 1 ? out[0] : out;
}
router(options) {
if (!this.internalRouter) {
this.internalRouter = (async () => {
this.logger.info('creating router');
const router = Router(options);
router.use((_req, res, next) => {
let req = _req;
if (!req.logger) {
req.logger = getLogger(this.getDebugLabel(), this.getDebugContext(), false);
}
next();
});
if (this.securityValidator !== API.prototype.securityValidator) {
router.use(deprecate(this.securityValidator, 'API.securityValidator is deprecated. Use API.initSecurity instead.').bind(this));
}
else {
router.use(this.initSecurity.bind(this));
}
for (let name in this.dynamicSchemas) {
this.registerSchema(name, await this.dynamicSchemas[name].spec());
}
if (this.document.components && this.document.components.schemas && Object.keys(this.document.components.schemas).length > 0) {
this.addResource(new SchemaResource());
}
// Move definitions into #/components/schemas/ as schemas
this.document = rebaseOASDefinitions(this.document);
// Remove unused schemas and parameters from openapi spec
this.document = removeUnusedSchemas(this.document);
const originalDocument = _.cloneDeep(this.document);
router.get('/openapi.json', (req, res, next) => {
if (!req.headers['host']) {
next(API.newError(400, 'Bad Request', 'Missing Host header in the request'));
}
else {
res.set('Access-Control-Allow-Origin', '*');
res.set('Access-Control-Allow-Methods', 'GET');
const out = _.cloneDeep(originalDocument);
const baseUrl = `${req.headers['x-forwarded-proto'] || req.protocol}://${req.headers['host']}${req.baseUrl}`;
out.servers = [
{
url: baseUrl,
},
];
// Normalize OAuth2 urls
if (out.components && out.components.securitySchemes) {
for (let i in out.components.securitySchemes) {
const s = out.components.securitySchemes[i];
if (s.type === 'oauth2' && s.flows) {
for (let j in s.flows) {
const f = s.flows[j];
['authorizationUrl', 'tokenUrl', 'refreshUrl'].forEach((k) => {
if (f[k]) {
f[k] = new URL(f[k], baseUrl).toString();
}
});
}
}
}
}
res.json(out);
}
});
this.document = await parse(this.document, this.parseOptions);
for (let resource of this.resources) {
await resource.router(router, options);
}
router.use(this.handleError);
return router;
})();
}
return this.internalRouter;
}
async attach(base, options) {
let router = await this.router(options);
base.use('/v' + semver.major(this.document.info.version), router);
return base;
}
initSecurity(req, _res, next) {
req.logger.warn('using default security validator');
if (!req.scopes) {
req.logger.warn('scopes not set, setting default to *');
req.scopes = new Scopes('*');
}
next();
}
securityValidator(req, res, next) {
this.initSecurity(req, res, next);
}
handleError(err, req, res, next) {
if (err.name === 'RESTError') {
req.logger.error('REST ERROR', err);
RESTError.send(res, err.code, err.message, err.info);
}
else if (err.name === 'ValidationError') {
req.logger.error('DATA ERROR', err);
RESTError.send(res, 400, err.name, ValidationError.getInfo(err));
}
else {
req.logger.error('GENERIC ERROR', err, err.stack);
RESTError.send(res, 500, 'internal');
}
}
static newError(code, message, info, err) {
return new RESTError(code, message, info, err);
}
static fireError(code, message, info, err) {
throw API.newError(code, message, info, err);
}
static handle404Error(req, res, next) {
req.logger.warn('404 Resource Not Found');
RESTError.send(res, 404, 'Not Found', 'the requested resource cannot be found, check the endpoint URL');
}
}
//# sourceMappingURL=api.js.map