diff --git a/package-lock.json b/package-lock.json index 7103e96e..4d6c2d00 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "express-openapi-validator", - "version": "4.10.4", + "version": "4.11.0-beta.2", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -3371,11 +3371,6 @@ "integrity": "sha1-dx7Hg540c9nEzeKLGTlMNWL09tM=", "dev": true }, - "lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" - }, "lodash.uniq": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", diff --git a/package.json b/package.json index 3fb965a5..102b3ab0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "express-openapi-validator", - "version": "4.10.4", + "version": "4.11.0-beta.2", "description": "Automatically validate API requests and responses with OpenAPI 3 and Express.", "main": "dist/index.js", "scripts": { @@ -34,11 +34,9 @@ "dependencies": { "ajv": "^6.12.6", "content-type": "^1.0.4", - "js-yaml": "^3.14.0", "json-schema-ref-parser": "^9.0.6", "lodash.clonedeep": "^4.5.0", "lodash.get": "^4.4.2", - "lodash.merge": "^4.6.2", "lodash.uniq": "^4.5.0", "lodash.zipobject": "^4.1.3", "media-typer": "^1.1.0", diff --git a/src/framework/index.ts b/src/framework/index.ts index f262c23e..5c807201 100644 --- a/src/framework/index.ts +++ b/src/framework/index.ts @@ -1,5 +1,4 @@ import * as fs from 'fs'; -import * as jsYaml from 'js-yaml'; import * as path from 'path'; import * as $RefParser from 'json-schema-ref-parser'; import { OpenAPISchemaValidator } from './openapi.schema.validator'; @@ -23,13 +22,12 @@ export class OpenAPIFramework { visitor: OpenAPIFrameworkVisitor, ): Promise { const args = this.args; - const apiDoc = await this.copy( - await this.loadSpec(args.apiDoc, args.$refParser), - ); + const apiDoc = await this.loadSpec(args.apiDoc, args.$refParser); + const basePathObs = this.getBasePathsFromServers(apiDoc.servers); const basePaths = Array.from( basePathObs.reduce((acc, bp) => { - bp.all().forEach(path => acc.add(path)); + bp.all().forEach((path) => acc.add(path)); return acc; }, new Set()), ); @@ -55,7 +53,7 @@ export class OpenAPIFramework { } } const getApiDoc = () => { - return this.copy(apiDoc); + return apiDoc; }; this.sortApiDocTags(apiDoc); @@ -87,13 +85,9 @@ export class OpenAPIFramework { // Get document, or throw exception on error try { process.chdir(specDir); - const docWithRefs = jsYaml.safeLoad( - fs.readFileSync(absolutePath, 'utf8'), - { json: true }, - ); return $refParser.mode === 'dereference' - ? $RefParser.dereference(docWithRefs) - : $RefParser.bundle(docWithRefs); + ? $RefParser.dereference(absolutePath) + : $RefParser.bundle(absolutePath); } finally { process.chdir(origCwd); } @@ -108,10 +102,6 @@ export class OpenAPIFramework { : $RefParser.bundle(filePath); } - private copy(obj: T): T { - return JSON.parse(JSON.stringify(obj)); - } - private sortApiDocTags(apiDoc: OpenAPIV3.Document): void { if (apiDoc && Array.isArray(apiDoc.tags)) { apiDoc.tags.sort((a, b): number => { @@ -131,6 +121,6 @@ export class OpenAPIFramework { const basePath = new BasePath(server); basePathsMap[basePath.expressPath] = basePath; } - return Object.keys(basePathsMap).map(key => basePathsMap[key]); + return Object.keys(basePathsMap).map((key) => basePathsMap[key]); } } diff --git a/src/framework/openapi.context.ts b/src/framework/openapi.context.ts index ef5ff4bb..62106cce 100644 --- a/src/framework/openapi.context.ts +++ b/src/framework/openapi.context.ts @@ -1,7 +1,5 @@ import { OpenAPIV3 } from './types'; import { Spec, RouteMetadata } from './openapi.spec.loader'; -import { Console } from 'console'; - export interface RoutePair { expressRoute: string; diff --git a/src/framework/openapi.schema.validator.ts b/src/framework/openapi.schema.validator.ts index 8d85d6ca..c0f5351f 100644 --- a/src/framework/openapi.schema.validator.ts +++ b/src/framework/openapi.schema.validator.ts @@ -1,5 +1,4 @@ import * as Ajv from 'ajv'; -import * as merge from 'lodash.merge'; import * as draftSchema from 'ajv/lib/refs/json-schema-draft-04.json'; // https://github.com/OAI/OpenAPI-Specification/blob/master/schemas/v3.0/schema.json import * as openapi3Schema from './openapi.v3.schema.json'; @@ -7,13 +6,7 @@ import { OpenAPIV3 } from './types.js'; export class OpenAPISchemaValidator { private validator: Ajv.ValidateFunction; - constructor({ - version, - extensions, - }: { - version: string; - extensions?: object; - }) { + constructor({ version }: { version: string; extensions?: object }) { const v = new Ajv({ schemaId: 'auto', allErrors: true }); v.addMetaSchema(draftSchema); @@ -21,9 +14,8 @@ export class OpenAPISchemaValidator { if (!ver) throw Error('version missing from OpenAPI specification'); if (ver != 3) throw Error('OpenAPI v3 specification version is required'); - const schema = merge({}, openapi3Schema, extensions ?? {}); - v.addSchema(schema); - this.validator = v.compile(schema); + v.addSchema(openapi3Schema); + this.validator = v.compile(openapi3Schema); } public validate( diff --git a/src/index.ts b/src/index.ts index fa391aa1..9d0ce703 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,3 @@ -import * as _uniq from 'lodash.uniq'; import * as res from './resolvers'; import { OpenApiValidator, OpenApiValidatorOpts } from './openapi.validator'; import { OpenApiSpecLoader } from './framework/openapi.spec.loader'; diff --git a/src/middlewares/openapi.metadata.ts b/src/middlewares/openapi.metadata.ts index 87d4fa1e..67817cd2 100644 --- a/src/middlewares/openapi.metadata.ts +++ b/src/middlewares/openapi.metadata.ts @@ -1,6 +1,5 @@ import * as _zipObject from 'lodash.zipobject'; import { pathToRegexp } from 'path-to-regexp'; -import * as deepCopy from 'lodash.clonedeep'; import { Response, NextFunction } from 'express'; import { OpenApiContext } from '../framework/openapi.context'; import { diff --git a/src/middlewares/parsers/schema.preprocessor.ts b/src/middlewares/parsers/schema.preprocessor.ts index d2951e8b..196ccd9e 100644 --- a/src/middlewares/parsers/schema.preprocessor.ts +++ b/src/middlewares/parsers/schema.preprocessor.ts @@ -102,7 +102,7 @@ export class SchemaPreprocessor { const componentSchemas = this.gatherComponentSchemaNodes(); const r = this.gatherSchemaNodesFromPaths(); - // Now that we've processed paths, clonse the spec + // Now that we've processed paths, clone a response spec if we are validating responses this.apiDocRes = !!this.responseOpts ? cloneDeep(this.apiDoc) : null; const schemaNodes = { @@ -168,20 +168,30 @@ export class SchemaPreprocessor { * @param visit a function to invoke per node */ private traverseSchemas(nodes: TopLevelSchemaNodes, visit) { + const seen = new Set(); const recurse = (parent, node, opts: TraversalStates) => { - const schema = this.resolveSchema(node.schema); - if (!schema) { - // if we can't dereference a path within the schema, skip preprocessing - // TODO handle refs like below during preprocessing - // #/paths/~1subscription/get/requestBody/content/application~1json/schema/properties/subscription + const schema = node.schema; + + if (!schema || seen.has(schema)) return; + + seen.add(schema); + + if (schema.$ref) { + const resolvedSchema = this.resolveSchema(schema); + const path = schema.$ref.split('/').slice(1); + + (opts).req.originalSchema = schema; + (opts).res.originalSchema = schema; + + visit(parent, node, opts); + recurse(node, new Node(schema, resolvedSchema, path), opts); return; } + // Save the original schema so we can check if it was a $ref - (opts).req.originalSchema = node.schema; - (opts).res.originalSchema = node.schema; + (opts).req.originalSchema = schema; + (opts).res.originalSchema = schema; - // TODO mark visited, and skip visited - // TODO Visit api docs visit(parent, node, opts); if (schema.allOf) { @@ -199,8 +209,8 @@ export class SchemaPreprocessor { const child = new Node(node, s, [...node.path, 'anyOf', i + '']); recurse(node, child, opts); }); - } else if (node.schema.properties) { - Object.entries(node.schema.properties).forEach(([id, cschema]) => { + } else if (schema.properties) { + Object.entries(schema.properties).forEach(([id, cschema]) => { const path = [...node.path, 'properties', id]; const child = new Node(node, cschema, path); recurse(node, child, opts); @@ -249,7 +259,7 @@ export class SchemaPreprocessor { const options = opts[kind]; options.path = node.path; - if (nschema) { + if (nschema) { // This null check should no longer be necessary this.handleSerDes(pschema, nschema, options); this.handleReadonly(pschema, nschema, options); this.processDiscriminator(pschema, nschema, options); diff --git a/src/openapi.validator.ts b/src/openapi.validator.ts index 7e559a73..2f7080af 100644 --- a/src/openapi.validator.ts +++ b/src/openapi.validator.ts @@ -2,7 +2,6 @@ import ono from 'ono'; import ajv = require('ajv'); import * as express from 'express'; import * as _uniq from 'lodash.uniq'; -import * as cloneDeep from 'lodash.clonedeep'; import * as middlewares from './middlewares'; import { Application, Response, NextFunction, Router } from 'express'; import { OpenApiContext } from './framework/openapi.context'; diff --git a/test/511.spec.ts b/test/511.spec.ts new file mode 100644 index 00000000..4d18b07f --- /dev/null +++ b/test/511.spec.ts @@ -0,0 +1,155 @@ +import * as request from 'supertest'; +import { createApp } from './common/app'; + +describe('511 schema.preprocessor inheritance', () => { + let app = null; + + before(async () => { + // set up express app + app = await createApp( + { + apiSpec: apiSpec(), + validateResponses: true, + }, + 3001, + (app) => { + app.post('/example', (req, res) => { + res.status(201).json({ + object_type: 'PolyObject1', + shared_prop1: 'sp1', + shared_prop2: 'sp2', + polyObject1SpecificProp1: 'poly1', + }); + }); + }, + false, + ); + return app; + }); + + after(() => { + app.server.close(); + }); + + it('should return 201', async () => + request(app) + .post(`/example`) + .send({ + object_type: 'PolyObject1', + shared_prop1: 'sp1', + shared_prop2: 'sp2', + polyObject1SpecificProp1: 'poly1', + }) + .expect(201)); +}); + +function apiSpec(): any { + return { + openapi: '3.0.0', + info: { + title: 'Example API', + version: '0.1.0', + }, + servers: [ + { + url: 'https://localhost/', + }, + ], + paths: { + '/example': { + post: { + description: 'Request', + requestBody: { + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/PolyObject', + }, + }, + }, + }, + responses: { + '201': { + description: 'Response', + content: { + 'application/json': { + schema: { + type: 'object', + }, + }, + }, + }, + }, + }, + }, + }, + components: { + schemas: { + PolyObject: { + type: 'object', + discriminator: { + propertyName: 'object_type', + mapping: { + PolyObject1: '#/components/schemas/PolyObject1', + PolyObject2: '#/components/schemas/PolyObject2', + }, + }, + oneOf: [ + { + $ref: '#/components/schemas/PolyObject1', + }, + { + $ref: '#/components/schemas/PolyObject2', + }, + ], + }, + PolyObjectBase: { + type: 'object', + required: ['object_type'], + properties: { + object_type: { + type: 'string', + enum: ['PolyObject1', 'PolyObject2'], + }, + shared_prop1: { + type: 'string', + }, + shared_prop2: { + type: 'string', + }, + }, + }, + PolyObject1: { + allOf: [ + { + $ref: '#/components/schemas/PolyObjectBase', + }, + { + type: 'object', + properties: { + polyObject1SpecificProp1: { + type: 'string', + }, + }, + }, + ], + }, + PolyObject2: { + allOf: [ + { + $ref: '#/components/schemas/PolyObjectBase', + }, + { + type: 'object', + properties: { + polyObject2SpecificProp1: { + type: 'string', + }, + }, + }, + ], + }, + }, + }, + }; +}