🚀 Big News: Socket Acquires Coana to Bring Reachability Analysis to Every Appsec Team.Learn more
Socket
DemoInstallSign in
Socket

openapi-backend

Package Overview
Dependencies
Maintainers
1
Versions
127
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

openapi-backend - npm Package Compare versions

Comparing version

to
0.2.0

86

index.d.ts

@@ -0,1 +1,3 @@

import _ from 'lodash';
import Ajv from 'ajv';
import { OpenAPIV3 } from 'openapi-types';

@@ -25,8 +27,23 @@ declare type Handler = (...args: any[]) => Promise<any>;

};
export declare function parseRequest(req: RequestObject, path?: string): {
params: _.Dictionary<string>;
cookies: {};
requestBody: any;
method: string;
path: string;
headers: {
[key: string]: string | string[];
};
query?: string | {
[key: string]: string | string[];
};
body?: any;
};
export declare function validateDefinition(definition: OpenAPIV3.Document): OpenAPIV3.Document;
interface ConstructorOpts {
document: OpenAPIV3.Document;
definition: OpenAPIV3.Document | string;
strict?: boolean;
validate?: boolean;
handlers?: {
[operationId: string]: Handler;
[handler: string]: Handler;
};

@@ -37,11 +54,70 @@ }

strict: boolean;
validate: boolean;
handlers: {
[operationId: string]: Handler;
};
initalized: boolean;
private document;
private internalHandlers;
constructor(opts: ConstructorOpts);
validateRequest(req: RequestObject): Promise<boolean>;
init(): Promise<this>;
handleRequest(req: RequestObject, ...handlerArgs: any[]): Promise<any>;
getOperation(operationId: string): OpenAPIV3.OperationObject;
matchOperation(req: RequestObject): OpenAPIV3.OperationObject;
validateRequest(req: RequestObject): {
ajv: Ajv.Ajv;
valid: boolean | Ajv.Thenable<any>;
};
getOperations(): {
tags?: string[];
summary?: string;
description?: string;
externalDocs?: OpenAPIV3.ExternalDocumentationObject;
operationId?: string;
parameters?: (OpenAPIV3.ReferenceObject | OpenAPIV3.ParameterObject)[];
requestBody?: OpenAPIV3.ReferenceObject | OpenAPIV3.RequestBodyObject;
responses?: OpenAPIV3.ResponsesObject;
callbacks?: {
[callback: string]: OpenAPIV3.ReferenceObject | OpenAPIV3.CallbackObject;
};
deprecated?: boolean;
security?: OpenAPIV3.SecurityRequirementObject[];
servers?: OpenAPIV3.ServerObject[];
path: string;
method: string;
}[];
getOperation(operationId: string): {
tags?: string[];
summary?: string;
description?: string;
externalDocs?: OpenAPIV3.ExternalDocumentationObject;
operationId?: string;
parameters?: (OpenAPIV3.ReferenceObject | OpenAPIV3.ParameterObject)[];
requestBody?: OpenAPIV3.ReferenceObject | OpenAPIV3.RequestBodyObject;
responses?: OpenAPIV3.ResponsesObject;
callbacks?: {
[callback: string]: OpenAPIV3.ReferenceObject | OpenAPIV3.CallbackObject;
};
deprecated?: boolean;
security?: OpenAPIV3.SecurityRequirementObject[];
servers?: OpenAPIV3.ServerObject[];
path: string;
method: string;
};
matchOperation(req: RequestObject): {
tags?: string[];
summary?: string;
description?: string;
externalDocs?: OpenAPIV3.ExternalDocumentationObject;
operationId?: string;
parameters?: (OpenAPIV3.ReferenceObject | OpenAPIV3.ParameterObject)[];
requestBody?: OpenAPIV3.ReferenceObject | OpenAPIV3.RequestBodyObject;
responses?: OpenAPIV3.ResponsesObject;
callbacks?: {
[callback: string]: OpenAPIV3.ReferenceObject | OpenAPIV3.CallbackObject;
};
deprecated?: boolean;
security?: OpenAPIV3.SecurityRequirementObject[];
servers?: OpenAPIV3.ServerObject[];
path: string;
method: string;
};
register(handlers: {

@@ -48,0 +124,0 @@ [operationId: string]: Handler;

258

index.js

@@ -53,5 +53,12 @@ "use strict";

var lodash_1 = __importDefault(require("lodash"));
var ajv_1 = __importDefault(require("ajv"));
var swagger_parser_1 = __importDefault(require("swagger-parser"));
var openapi_schema_validation_1 = require("openapi-schema-validation");
// normalises request
// - http method is lowercase
// - path leading slash 👍
// - path trailing slash 👎
// - path query string 👎
function normalizeRequest(req) {
return __assign({}, req, { path: req.path
return __assign({}, req, { path: (req.path || '')
.trim()

@@ -63,2 +70,33 @@ .split('?')[0] // remove query string

exports.normalizeRequest = normalizeRequest;
// parses request
// - parse json body
// - parse path params based on uri template
// - parse query string
// - parse cookies from headers
function parseRequest(req, path) {
var requestBody;
try {
requestBody = JSON.parse(req.body);
}
catch (_a) {
// suppress json parsing errors
}
// @TODO: parse query string from req.path + req.query
// @TODO: parse cookie from headers
var cookies = {};
// normalize
req = normalizeRequest(req);
// parse path
var paramPlaceholder = '{[^\\/]*}';
var pathPattern = "^" + path.replace(new RegExp(paramPlaceholder, 'g'), '([^\\/]+)').replace(/\//g, '\\/') + "$";
var paramValueArray = new RegExp(pathPattern).exec(req.path).splice(1);
var paramNameArray = (path.match(new RegExp(paramPlaceholder, 'g')) || []).map(function (param) {
return param.replace(/[{}]/g, '');
});
var params = lodash_1.default.zipObject(paramNameArray, paramValueArray);
return __assign({}, req, { params: params,
cookies: cookies,
requestBody: requestBody });
}
exports.parseRequest = parseRequest;
function validateDefinition(definition) {

@@ -75,32 +113,45 @@ var _a = openapi_schema_validation_1.validate(definition, 3), valid = _a.valid, errors = _a.errors;

function OpenAPIBackend(opts) {
this.internalHandlers = ['404', 'notFound', '501', 'notImplemented'];
this.definition = opts.document;
this.internalHandlers = ['404', 'notFound', '501', 'notImplemented', '400', 'validationFail'];
this.document = opts.definition;
this.strict = Boolean(opts.strict);
this.handlers = {};
// validate definition
try {
validateDefinition(this.definition);
}
catch (err) {
if (this.strict) {
// in strict-mode, fail hard and re-throw the error
throw err;
}
else {
// just emit a warning about the validation errors
console.warn(err);
}
}
// register handlers
if (opts.handlers) {
this.register(opts.handlers);
}
this.validate = lodash_1.default.isNil(opts.validate) ? true : opts.validate;
this.handlers = opts.handlers || {};
}
OpenAPIBackend.prototype.validateRequest = function (req) {
OpenAPIBackend.prototype.init = function () {
return __awaiter(this, void 0, void 0, function () {
var operation;
return __generator(this, function (_a) {
operation = this.matchOperation(req);
// @TODO: perform Ajv validation for input
return [2 /*return*/, true];
var _a, err_1;
return __generator(this, function (_b) {
switch (_b.label) {
case 0:
if (!!this.definition) return [3 /*break*/, 4];
_b.label = 1;
case 1:
_b.trys.push([1, 3, , 4]);
// load, dereference and valdidate definition
_a = this;
return [4 /*yield*/, swagger_parser_1.default.dereference(this.document)];
case 2:
// load, dereference and valdidate definition
_a.definition = _b.sent();
validateDefinition(this.definition);
return [3 /*break*/, 4];
case 3:
err_1 = _b.sent();
if (this.strict) {
// in strict-mode, fail hard and re-throw the error
throw err_1;
}
else {
// just emit a warning about the validation errors
console.warn(err_1);
}
return [3 /*break*/, 4];
case 4:
// register handlers
if (this.handlers) {
this.register(this.handlers);
}
this.initalized = true;
return [2 /*return*/, this];
}
});

@@ -115,39 +166,116 @@ });

return __awaiter(this, void 0, void 0, function () {
var operation, notFoundHandler, operationId, notImplementedHandler, routeHandler;
return __generator(this, function (_a) {
operation = this.matchOperation(req);
if (!operation || !operation.operationId) {
notFoundHandler = this.handlers['404'] || this.handlers['notFound'];
if (!notFoundHandler) {
throw Error("operation not found for request and no 404|notFound handler was registered");
}
return [2 /*return*/, notFoundHandler.apply(void 0, handlerArgs)];
var operation, notFoundHandler, operationId, _a, ajv, valid, validationFailHandler, prettyErrors, routeHandler, notImplementedHandler;
return __generator(this, function (_b) {
switch (_b.label) {
case 0:
if (!!this.initalized) return [3 /*break*/, 2];
// api has not yet been initalised
return [4 /*yield*/, this.init()];
case 1:
// api has not yet been initalised
_b.sent();
_b.label = 2;
case 2:
operation = this.matchOperation(req);
if (!operation || !operation.operationId) {
notFoundHandler = this.handlers['404'] || this.handlers['notFound'];
if (!notFoundHandler) {
throw Error("404-notFound: no route matches request");
}
return [2 /*return*/, notFoundHandler.apply(void 0, handlerArgs)];
}
operationId = operation.operationId;
if (this.validate) {
_a = this.validateRequest(req), ajv = _a.ajv, valid = _a.valid;
if (!valid) {
validationFailHandler = this.handlers['400'] || this.handlers['validationFail'];
if (!validationFailHandler) {
prettyErrors = JSON.stringify(ajv.errors, null, 2);
throw Error("400-validationFail: " + operationId + ", errors: " + prettyErrors);
}
return [2 /*return*/, validationFailHandler.apply(void 0, [ajv.errors].concat(handlerArgs))];
}
}
routeHandler = this.handlers[operationId];
if (!routeHandler) {
notImplementedHandler = this.handlers['501'] || this.handlers['notImplemented'];
if (!notImplementedHandler) {
throw Error("501-notImplemented: " + operationId + " no handler registered");
}
return [2 /*return*/, notImplementedHandler.apply(void 0, handlerArgs)];
}
// handle route
return [2 /*return*/, routeHandler.apply(void 0, handlerArgs)];
}
operationId = operation.operationId;
if (!operationId || !this.handlers[operationId]) {
notImplementedHandler = this.handlers['501'] || this.handlers['notImplemented'];
if (!notImplementedHandler) {
throw Error("no handler registered for " + operationId + " and no 501|notImplemented handler was registered");
}
return [2 /*return*/, notImplementedHandler.apply(void 0, handlerArgs)];
}
routeHandler = this.handlers[operationId];
return [2 /*return*/, routeHandler.apply(void 0, handlerArgs)];
});
});
};
OpenAPIBackend.prototype.getOperation = function (operationId) {
return (lodash_1.default.chain(this.definition.paths)
// flatten operations into an array
.values()
.flatMap(lodash_1.default.values)
// match operationId
.find({ operationId: operationId })
.value());
OpenAPIBackend.prototype.validateRequest = function (req) {
var ajv = new ajv_1.default({
coerceTypes: true,
});
var operation = this.matchOperation(req);
var _a = parseRequest(req, operation.path), params = _a.params, query = _a.query, headers = _a.headers, cookies = _a.cookies, requestBody = _a.requestBody;
var parameters = lodash_1.default.omitBy({
path: params,
query: query,
header: headers,
cookie: cookies,
requestBody: requestBody,
}, lodash_1.default.isNil);
// build input validation schema for operation
// @TODO: pre-build this for each operation for performance
var schema = {
title: 'Request',
type: 'object',
additionalProperties: false,
properties: {
path: {
type: 'object',
additionalProperties: false,
properties: {},
required: [],
},
query: {
type: 'object',
properties: {},
additionalProperties: false,
required: [],
},
header: {
type: 'object',
additionalProperties: true,
properties: {},
required: [],
},
cookie: {
type: 'object',
additionalProperties: true,
properties: {},
required: [],
},
},
};
// params are dereferenced here, no reference objects.
var operationParameters = operation.parameters || [];
operationParameters.map(function (param) {
var target = schema.properties[param.in];
if (param.required) {
target.required.push(param.name);
}
target.properties[param.name] = param.schema;
});
if (operation.requestBody) {
// @TODO: infer most specific media type from headers
var mediaType = 'application/json';
var jsonbody = operation.requestBody.content[mediaType];
if (jsonbody && jsonbody.schema) {
schema.properties.requestBody = jsonbody.schema;
}
}
return { ajv: ajv, valid: ajv.validate(schema, parameters) };
};
OpenAPIBackend.prototype.matchOperation = function (req) {
// normalize request for matching
req = normalizeRequest(req);
var operations = lodash_1.default.chain(this.definition.paths)
// flatten operations into an array with path + method
// flatten operations into an array with path + method
OpenAPIBackend.prototype.getOperations = function () {
return lodash_1.default.chain(this.definition.paths)
.entries()

@@ -163,2 +291,10 @@ .flatMap(function (_a) {

.value();
};
OpenAPIBackend.prototype.getOperation = function (operationId) {
return lodash_1.default.find(this.getOperations(), { operationId: operationId });
};
OpenAPIBackend.prototype.matchOperation = function (req) {
// normalize request for matching
req = normalizeRequest(req);
var operations = this.getOperations();
// first check for an exact match

@@ -179,3 +315,3 @@ var exactMatch = lodash_1.default.find(operations, function (_a) {

// convert openapi path template to a regex pattern. {id} becomes ([^/]+)
var pathPattern = "^" + path.replace(/\{.*\}/g, '([^/]+)').replace(/\//g, '\\/') + "\\/?$";
var pathPattern = "^" + path.replace(/\{.*\}/g, '([^/]+)').replace(/\//g, '\\/') + "$";
return Boolean(req.path.match(new RegExp(pathPattern, 'g')));

@@ -182,0 +318,0 @@ });

{
"name": "openapi-backend",
"description": "Tools for building API backends with the OpenAPI standard",
"version": "0.1.0",
"version": "0.2.0",
"author": "Viljami Kuosmanen <viljami@avoinsorsa.fi>",

@@ -26,2 +26,8 @@ "license": "MIT",

],
"dependencies": {
"@types/lodash": "^4.14.117",
"@types/swagger-parser": "^4.0.2",
"lodash": "^4.17.11",
"swagger-parser": "^6.0.1"
},
"devDependencies": {

@@ -46,7 +52,3 @@ "@types/jest": "^23.3.7",

"test": "NODE_ENV=test jest"
},
"dependencies": {
"@types/lodash": "^4.14.117",
"lodash": "^4.17.11"
}
}

@@ -1,7 +0,17 @@

# OpenAPI Backend Tools
# OpenAPI Backend
[![Build Status](https://travis-ci.org/anttiviljami/openapi-backend.svg?branch=master)](https://travis-ci.org/anttiviljami/openapi-backend)
[![npm version](https://badge.fury.io/js/openapi-backend.svg)](https://badge.fury.io/js/openapi-backend)
[![License](http://img.shields.io/:license-mit-blue.svg)](http://anttiviljami.mit-license.org)
Tools for building API backends with the OpenAPI standard
Tools for building API backends with the [OpenAPI standard](https://github.com/OAI/OpenAPI-Specification)
## Features
- Build APIs by describing them in [OpenAPI document specification](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md)
and importing them via YAML, JSON or as a JavaScript object
- Register handlers for API operations in your favourite Node.js backend like [Express](#express), [Hapi](#hapi),
[AWS Lambda](#aws-serverless-lambda) or [Azure Functions](#azure-serverless-function)
- Use [JSON Schema](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#data-types) to validate
API requests. OpenAPI Backend uses the [AJV](https://ajv.js.org/) library under the hood for performant validation
## Quick Start

@@ -19,4 +29,4 @@

const api = new OpenAPIBackend({
document: {
openapi: '3.0.0',
definition: {
openapi: '3.0.2',
info: {

@@ -34,8 +44,2 @@ title: 'My API',

},
post: {
operationId: 'createPet',
responses: {
201: { description: 'ok' },
},
},
},

@@ -45,2 +49,12 @@ '/pets/{id}': {

operationId: 'getPetById',
parameters: [
{
name: 'id',
in: 'path',
required: true,
schema: {
type: 'integer',
},
},
],
responses: {

@@ -50,8 +64,2 @@ 200: { description: 'ok' },

},
get: {
operationId: 'deletePetById',
responses: {
200: { description: 'ok' },
},
},
},

@@ -62,9 +70,11 @@ },

// your platform specific request handlers here
getPets: async () => { status: 200, body: 'ok' },
createPet: async () => { status: 201, body: 'ok' }) },
getPetById: async () => { status: 200, body: 'ok' }) },
deletePetById: async () => { status: 200, body: 'ok' }) },
notFound: async () => { status: 404, body: 'not found' }) },
getPets: async (req) => ({ status: 200, body: 'ok' }),
getPetById: async (req) => ({ status: 200, body: 'ok' }),
notFound: async (req) => ({ status: 404, body: 'not found' }),
validationFail: async (err, req) => ({ status: 400, body: JSON.stringify({ err }) }),
},
});
// initalize the backend
api.init();
```

@@ -71,0 +81,0 @@