From dc2c827167ed051dc68375142c53d5225074f0f1 Mon Sep 17 00:00:00 2001 From: Carmine DiMascio Date: Sat, 21 Dec 2019 15:54:32 -0500 Subject: [PATCH] fix non xOf serialisable object --- src/middlewares/openapi.request.validator.ts | 97 ++++++++++++++------ test/resources/serialized.objects.yaml | 26 ++++++ test/serialized.objects.spec.ts | 35 ++++++- 3 files changed, 126 insertions(+), 32 deletions(-) diff --git a/src/middlewares/openapi.request.validator.ts b/src/middlewares/openapi.request.validator.ts index eb913e00..9de3e662 100644 --- a/src/middlewares/openapi.request.validator.ts +++ b/src/middlewares/openapi.request.validator.ts @@ -128,18 +128,24 @@ export class RequestValidator { const validator = this.ajv.compile(schema); return (req: OpenApiRequest, res: Response, next: NextFunction): void => { - // forcing convert to object if scheme describes param as object + explode // for easy validation, keep the schema but update whereabouts of its sub components parameters.parseObjectExplode.forEach(item => { if (req[item.reqField]) { // check if there is at least one of the nested properties before create the parent - const atLeastOne = item.properties.some(p => req[item.reqField].hasOwnProperty(p)); + const atLeastOne = item.properties.some(p => + req[item.reqField].hasOwnProperty(p), + ); if (atLeastOne) { req[item.reqField][item.name] = {}; item.properties.forEach(property => { + // const removeME = schema; if (req[item.reqField][property]) { - req[item.reqField][item.name][property] = req[item.reqField][property]; + const type = + schema.properties[item.reqField].properties[item.name] + .properties[property].type; + [req[item.reqField][item.name]][property] = + req[item.reqField][property]; delete req[item.reqField][property]; } }); @@ -319,13 +325,13 @@ export class RequestValidator { ): string[] { return usedSecuritySchema && securitySchema ? usedSecuritySchema - .filter(obj => Object.entries(obj).length !== 0) - .map(sec => { - const securityKey = Object.keys(sec)[0]; - return securitySchema[securityKey]; - }) - .filter(sec => sec?.in === 'query') - .map(sec => sec.name) + .filter(obj => Object.entries(obj).length !== 0) + .map(sec => { + const securityKey = Object.keys(sec)[0]; + return securitySchema[securityKey]; + }) + .filter(sec => sec?.in === 'query') + .map(sec => sec.name) : []; } @@ -387,14 +393,11 @@ export class RequestValidator { } else if ($in === 'query') { // handle complex json types in schema const schemaHasObject = schema => - schema && ( - schema.type === 'object' || - [].concat( - schema.allOf, - schema.oneOf, - schema.anyOf, - ).some(schemaHasObject) - ); + schema && + (schema.type === 'object' || + [] + .concat(schema.allOf, schema.oneOf, schema.anyOf) + .some(schemaHasObject)); if (schemaHasObject(parameterSchema)) { parseJson.push({ name, reqField }); @@ -422,24 +425,54 @@ export class RequestValidator { // handle object serialization in query if (parameter?.style === 'form' && parameter?.explode === true) { // fetch the keys used for this kind of explode - - const properties = ['allOf', 'oneOf', 'anyOf'].reduce( - (acc, key) => { - if (!parameter.schema.hasOwnProperty(key)) { + const hasXOf = + parameterSchema.allOf || + parameterSchema.oneOf || + parameterSchema.anyOf; + + const xOfProperties = schema => { + return ['allOf', 'oneOf', 'anyOf'].reduce((acc, key) => { + if (!schema.hasOwnProperty(key)) { return acc; } else { - const found_properties = parameter.schema[key].reduce((acc2, obj) => { - return (obj.type === 'object') + const found_properties = schema[key].reduce((acc2, obj) => { + return obj.type === 'object' ? acc2.concat(...Object.keys(obj.properties)) : acc2; }, []); - return (found_properties.length > 0) + return found_properties.length > 0 ? acc.concat(...found_properties) : acc; } - }, - [], - ); + }, []); + }; + + const properties = hasXOf + ? xOfProperties(parameterSchema) + : parameterSchema.type === 'object' + ? Object.keys(parameterSchema.properties) + : []; + // const properties = hasXOf + // ? ['allOf', 'oneOf', 'anyOf'].reduce((acc, key) => { + // if (!parameter.schema.hasOwnProperty(key)) { + // return acc; + // } else { + // const found_properties = parameter.schema[key].reduce( + // (acc2, obj) => { + // return obj.type === 'object' + // ? acc2.concat(...Object.keys(obj.properties)) + // : acc2; + // }, + // [], + // ); + // return found_properties.length > 0 + // ? acc.concat(...found_properties) + // : acc; + // } + // }, []) + // : parameterSchema.type === 'object' + // ? Object.keys(parameterSchema.properties) + // : []; parseObjectExplode.push({ reqField, name, properties }); } @@ -459,6 +492,12 @@ export class RequestValidator { } }); - return { schema, parseJson, parseArray, parseArrayExplode, parseObjectExplode }; + return { + schema, + parseJson, + parseArray, + parseArrayExplode, + parseObjectExplode, + }; } } diff --git a/test/resources/serialized.objects.yaml b/test/resources/serialized.objects.yaml index 83a0b6c4..2ddc8547 100644 --- a/test/resources/serialized.objects.yaml +++ b/test/resources/serialized.objects.yaml @@ -9,6 +9,32 @@ servers: - url: /v1/ paths: + /tags: + get: + summary: "Retrieve all tags" + operationId: getTags + parameters: + - in: query + style: form + name: settings + explode: true + schema: + type: object + properties: + tag_ids: + type: array + items: + type: integer + minimum: 0 + minItems: 1 + state: + type: string + enum: ["default", "validated", "pending"] + default: "default" + description: "Filter the tags by their validity. The default value ('default') stands for no filtering." + responses: + '200': + description: "An array of tag" /serialisable: get: summary: "Retrieve something" diff --git a/test/serialized.objects.spec.ts b/test/serialized.objects.spec.ts index ee27a090..07143df5 100644 --- a/test/serialized.objects.spec.ts +++ b/test/serialized.objects.spec.ts @@ -16,7 +16,8 @@ describe(packageJson.name, () => { `${app.basePath}`, express .Router() - .get(`/serialisable`, (req, res) => res.json(req.query)), + .get(`/serialisable`, (req, res) => res.json(req.query)) + .get(`/tags`, (req, res) => res.json(req.query)), ), ); }); @@ -31,14 +32,16 @@ describe(packageJson.name, () => { .query({ onlyValidated: true, timestamp: '2019-06-24T12:34:56.789Z', + onlySelected: [1, 2, 3], fooBar: '{"foo":"bar"}', }) .expect(200) .then(response => { + console.log(response.body); expect(response.body).to.deep.equal({ settings: { onlyValidated: true, - onlySelected: [], + onlySelected: [1, 2, 3], }, timestamp: '2019-06-24T12:34:56.789Z', fooBar: { @@ -70,6 +73,32 @@ describe(packageJson.name, () => { }) .expect(400) .then(response => { - expect(response.body.message).to.equal('request.query.settings should be object'); + expect(response.body.message).to.equal( + 'request.query.settings should be object', + ); + })); + + it('should explode query param object e.g. tag_ids, state as query params', async () => + request(app) + .get(`${app.basePath}/tags`) + .query({ + tag_ids: 1, + }) + .expect(400) + .then(r => { + expect(r.body) + .to.have.property('message') + .that.equals('request.query.settings.tag_ids should be array'); + })); + + it.only('should explode query param object e.g. tag_ids, state as query params', async () => + request(app) + .get(`${app.basePath}/tags`) + .query({ + tag_ids: [1], + }) + .expect(200) + .then(r => { + console.log(r.body); })); });