Skip to content

Commit

Permalink
add type definitions
Browse files Browse the repository at this point in the history
  • Loading branch information
Carmine DiMascio committed Oct 15, 2019
1 parent 9c5ac4a commit cf0c890
Show file tree
Hide file tree
Showing 12 changed files with 102 additions and 41 deletions.
9 changes: 9 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"@types/express": "^4.17.0",
"@types/mocha": "^5.2.7",
"@types/morgan": "^1.7.36",
"@types/multer": "^1.3.10",
"@types/node": "^11.13.18",
"@types/supertest": "^2.0.8",
"body-parser": "^1.19.0",
Expand Down
3 changes: 2 additions & 1 deletion src/framework/openapi.context.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { OpenApiSpecLoader } from './openapi.spec.loader';
import { OpenAPIFrameworkArgs } from './index';
import { OpenAPIV3 } from './types';

export class OpenApiContext {
// TODO cleanup structure (group related functionality)
expressRouteMap = {};
openApiRouteMap = {};
routes = [];
apiDoc;
apiDoc: OpenAPIV3.Document;
private basePaths: Set<string>;
constructor(opts: OpenAPIFrameworkArgs) {
const openApiRouteDiscovery = new OpenApiSpecLoader(opts);
Expand Down
5 changes: 4 additions & 1 deletion src/framework/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Request } from 'express';
import { Request, Response, NextFunction } from 'express';
import { Logger } from 'ts-log';
import BasePath from './base.path';
export {
Expand Down Expand Up @@ -381,6 +381,9 @@ export interface OpenApiRequest extends Request {
openapi;
}

export type OpenApiRequestHandler = (req: OpenApiRequest, res: Response, next: NextFunction) => any;


export interface IJsonSchema {
id?: string;
$schema?: string;
Expand Down
42 changes: 30 additions & 12 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
import ono from 'ono';
import * as _ from 'lodash';
import { Application, Request } from 'express';
import {
Application,
Request,
Response,
NextFunction,
RequestHandler,
} from 'express';
import { OpenApiContext } from './framework/openapi.context';
import { OpenAPIV3, OpenApiRequest } from './framework/types';
import {
OpenAPIV3,
OpenApiRequest,
OpenApiRequestHandler,
} from './framework/types';
import * as middlewares from './middlewares';

export type SecurityHandlers = {
Expand Down Expand Up @@ -52,13 +62,22 @@ export class OpenApiValidator {

// install param on routes with paths
for (const p of _.uniq(pathParams)) {
app.param(p, (req: OpenApiRequest, res, next, value, name) => {
if (req.openapi.pathParams) {
// override path params
req.params[name] = req.openapi.pathParams[name] || req.params[name];
}
next();
});
app.param(
p,
(
req: OpenApiRequest,
res: Response,
next: NextFunction,
value: any,
name: string,
) => {
if (req.openapi.pathParams) {
// override path params
req.params[name] = req.openapi.pathParams[name] || req.params[name];
}
next();
},
);
}

const { coerceTypes, unknownFormats } = this.options;
Expand All @@ -73,9 +92,8 @@ export class OpenApiValidator {
},
);

const requestValidatorMw = (req, res, next) => {
return requestValidator.validate(req, res, next);
};
const requestValidatorMw: OpenApiRequestHandler = (req, res, next) =>
requestValidator.validate(req, res, next);

const responseValidator = new middlewares.ResponseValidator(
this.context.apiDoc,
Expand Down
19 changes: 16 additions & 3 deletions src/middlewares/ajv/index.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,29 @@
import * as Ajv from 'ajv';
import * as draftSchema from 'ajv/lib/refs/json-schema-draft-04.json';
import { formats } from './formats';
import { OpenAPIV3 } from '../../framework/types';

const TYPE_JSON = 'application/json';

export function createRequestAjv(openApiSpec, options: any = {}) {
export function createRequestAjv(
openApiSpec: OpenAPIV3.Document,
options: any = {},
) {
return createAjv(openApiSpec, options);
}
export function createResponseAjv(openApiSpec, options: any = {}) {

export function createResponseAjv(
openApiSpec: OpenAPIV3.Document,
options: any = {},
) {
return createAjv(openApiSpec, options, false);
}
function createAjv(openApiSpec, options: any = {}, request: boolean = true) {

function createAjv(
openApiSpec: OpenAPIV3.Document,
options: any = {},
request: boolean = true,
) {
const ajv = new Ajv({
...options,
schemaId: 'auto',
Expand Down
9 changes: 6 additions & 3 deletions src/middlewares/openapi.metadata.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import * as pathToRegexp from 'path-to-regexp';
import * as _ from 'lodash';
import { OpenApiContext } from '../framework/openapi.context';
import { OpenApiRequest, OpenApiRequestHandler } from '../framework/types';

export function applyOpenApiMetadata(openApiContext: OpenApiContext) {
return (req, res, next) => {
export function applyOpenApiMetadata(
openApiContext: OpenApiContext,
): OpenApiRequestHandler {
return (req, res, next): any => {
const matched = lookupRoute(req);

if (matched) {
Expand All @@ -20,7 +23,7 @@ export function applyOpenApiMetadata(openApiContext: OpenApiContext) {
-next();
};

function lookupRoute(req) {
function lookupRoute(req: OpenApiRequest) {
const path = req.path;
const method = req.method;
const routeEntries = Object.entries(openApiContext.expressRouteMap);
Expand Down
33 changes: 19 additions & 14 deletions src/middlewares/openapi.multipart.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import { OpenApiContext } from '../framework/openapi.context';
import { validationError } from './util';
import * as multer from 'multer';
import { Request } from 'express';
import { OpenApiRequest, OpenApiRequestHandler } from '../framework/types';
const multer = require('multer');

export function multipart(openApiContext: OpenApiContext, multerOpts: {} = {}) {
export function multipart(
OpenApiContext: OpenApiContext,
multerOpts: {} = {},
): OpenApiRequestHandler {
const mult = multer(multerOpts);
return (req, res, next) => {
if (isMultipart(req)) {
Expand All @@ -16,20 +21,22 @@ export function multipart(openApiContext: OpenApiContext, multerOpts: {} = {}) {
// TODO:
// If a form parameter 'file' is defined to take file value, but the user provides a string value instead
// req.files will be empty and req.body.file will be populated with a string
// This will incorrectly PASS validation.
// This will incorrectly PASS validation.
// Instead, we should return a 400 with an invalid type e.g. file expects a file, but found string.
//
// In order to support this, we likely need to inspect the schema directly to find the type.
//
// In order to support this, we likely need to inspect the schema directly to find the type.
// For example, if param with type: 'string', format: 'binary' is defined, we expect to see it in
// req.files. If it's not present we should throw a 400
//
//
// This is a bit complex because the schema may be defined inline (easy) or via a $ref (complex) in which
// case we must follow the $ref to check the type.
if (req.files) {
// add files to body
req.files.forEach(f => {
req.body[f.fieldname] = '';
});
(<Express.Multer.File[]>req.files).forEach(
(f: Express.Multer.File) => {
req.body[f.fieldname] = '';
},
);
}
next();
}
Expand All @@ -40,12 +47,12 @@ export function multipart(openApiContext: OpenApiContext, multerOpts: {} = {}) {
};
}

function isValidContentType(req) {
function isValidContentType(req: Request) {
const contentType = req.headers['content-type'];
return !contentType || contentType.includes('multipart/form-data');
}

function isMultipart(req) {
function isMultipart(req: OpenApiRequest) {
return (
req.openapi &&
req.openapi.schema &&
Expand All @@ -55,10 +62,9 @@ function isMultipart(req) {
);
}

function error(req, err) {
function error(req: OpenApiRequest, err: Error) {
if (err instanceof multer.MulterError) {
// TODO is special handling for MulterErrors needed
console.error(err);
return validationError(500, req.path, err.message);
} else {
// HACK
Expand All @@ -69,7 +75,6 @@ function error(req, err) {
if (missingField) {
return validationError(400, req.path, 'multipart file(s) required');
} else {
console.error(err);
return validationError(500, req.path, err.message);
}
}
Expand Down
6 changes: 4 additions & 2 deletions src/middlewares/openapi.request.validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import {
ajvErrorsToValidatorError,
} from './util';
import ono from 'ono';
import { NextFunction, Response } from 'express';
import { OpenAPIV3, OpenApiRequest } from '../framework/types';

const TYPE_JSON = 'application/json';

Expand All @@ -13,13 +15,13 @@ export class RequestValidator {
private _apiDocs;
private ajv;

constructor(apiDocs, options = {}) {
constructor(apiDocs: OpenAPIV3.Document, options = {}) {
this._middlewareCache = {};
this._apiDocs = apiDocs;
this.ajv = createRequestAjv(apiDocs, options);
}

validate(req, res, next) {
validate(req: OpenApiRequest, res: Response, next: NextFunction): void {
if (!req.openapi) {
// this path was not found in open api and
// this path is not defined under an openapi base path
Expand Down
4 changes: 3 additions & 1 deletion src/middlewares/util.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import ono from 'ono';
import { Request } from 'express';
import { OpenApiRequest } from '../framework/types';

export function extractContentType(req) {
export function extractContentType(req: Request) {
let contentType = req.headers['content-type'] || 'not_provided';
let end = contentType.indexOf(';');
end = end === -1 ? contentType.length : end;
Expand Down
2 changes: 1 addition & 1 deletion test/additional.props.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as request from 'supertest';
import * as path from 'path';
import * as express from 'express';
import { expect } from 'chai';
import * as request from 'supertest';
import { createApp } from './common/app';

const packageJson = require('../package.json');
Expand Down
10 changes: 7 additions & 3 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
{
"compilerOptions": {
"declaration": true,
"target": "es2015",
"target": "es2017",
"lib": ["es6", "dom"],
"module": "commonjs",
"outDir": "dist",
"sourceMap": true,
"resolveJsonModule": true
},

"exclude": ["node_modules"],
"include": ["typings.d.ts", "src/**/*.ts", "src/middlewares/modded.express.mung.ts"]
"include": [
"typings.d.ts",
"src/**/*.ts",
"src/middlewares/modded.express.mung.ts"
]
}

0 comments on commit cf0c890

Please sign in to comment.