From 4ce0f89e32a9dac68c22dda139a94328e2a3ea32 Mon Sep 17 00:00:00 2001 From: ownagedj Date: Sat, 13 Mar 2021 01:51:05 +0000 Subject: [PATCH] fix: correctly handle default values of deepObject query params (#557) - especially optional deepObject parameters with required fields Co-authored-by: ownagedj <> --- .../parsers/req.parameter.mutator.ts | 30 ++++++++- .../serialized-deep-object.objects.yaml | 62 +++++++++++++++++++ .../serialized.objects.defaults.yaml | 2 + test/serialized-deep-object.objects.spec.ts | 50 +++++++++++++++ 4 files changed, 141 insertions(+), 3 deletions(-) create mode 100644 test/resources/serialized-deep-object.objects.yaml create mode 100644 test/serialized-deep-object.objects.spec.ts diff --git a/src/middlewares/parsers/req.parameter.mutator.ts b/src/middlewares/parsers/req.parameter.mutator.ts index 16dccbce..907c3c4a 100644 --- a/src/middlewares/parsers/req.parameter.mutator.ts +++ b/src/middlewares/parsers/req.parameter.mutator.ts @@ -87,7 +87,7 @@ export class RequestParameterMutator { this.parseJsonAndMutateRequest(req, parameter.in, name); this.handleFormExplode(req, name, schema, parameter); } else if (style === 'deepObject') { - this.handleDeepObject(req, queryString, name); + this.handleDeepObject(req, queryString, name, schema); } else { this.parseJsonAndMutateRequest(req, parameter.in, name); } @@ -103,9 +103,33 @@ export class RequestParameterMutator { }); } - private handleDeepObject(req: Request, qs: string, name: string): void { + private handleDeepObject(req: Request, qs: string, name: string, schema: SchemaObject): void { + const getDefaultSchemaValue = () => { + let defaultValue; + + if (schema.default !== undefined) { + defaultValue = schema.default + } else { + ['allOf', 'oneOf', 'anyOf'].forEach((key) => { + if (schema[key]) { + schema[key].forEach((s) => { + if (s.$ref) { + const compiledSchema = this.ajv.getSchema(s.$ref); + // as any -> https://stackoverflow.com/a/23553128 + defaultValue = defaultValue === undefined ? (compiledSchema.schema as any).default : defaultValue; + } else { + defaultValue = defaultValue === undefined ? s.default : defaultValue; + } + }); + } + }); + } + + return defaultValue; + }; + if (!req.query?.[name]) { - req.query[name] = {}; + req.query[name] = getDefaultSchemaValue(); } this.parseJsonAndMutateRequest(req, 'query', name); // TODO handle url encoded? diff --git a/test/resources/serialized-deep-object.objects.yaml b/test/resources/serialized-deep-object.objects.yaml new file mode 100644 index 00000000..8da7a5e1 --- /dev/null +++ b/test/resources/serialized-deep-object.objects.yaml @@ -0,0 +1,62 @@ +openapi: "3.0.2" +info: + version: 1.0.0 + title: Request Query Serialization + description: Request Query Serialization Test + +servers: + - url: /v1/ + +paths: + /deep_object: + x-vendorExtension1: accounts + get: + x-vendorExtension2: accounts + summary: "retrieve a deep object" + operationId: getDeepObject + parameters: + - in: query + style: deepObject + name: settings + schema: + type: object + required: + - state + 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." + greeting: + type: string + default: "hello" + responses: + "200": + description: the object + +components: + schemas: + Deep: + 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." + greeting: + type: string + default: "hello" diff --git a/test/resources/serialized.objects.defaults.yaml b/test/resources/serialized.objects.defaults.yaml index c9b7ba01..4cd9e9ec 100644 --- a/test/resources/serialized.objects.defaults.yaml +++ b/test/resources/serialized.objects.defaults.yaml @@ -14,6 +14,7 @@ components: default: 25 type: integer type: object + default: {} Sorting: properties: field: @@ -29,6 +30,7 @@ components: - DESC type: string type: object + default: {} info: description: API title: API diff --git a/test/serialized-deep-object.objects.spec.ts b/test/serialized-deep-object.objects.spec.ts new file mode 100644 index 00000000..836fbc19 --- /dev/null +++ b/test/serialized-deep-object.objects.spec.ts @@ -0,0 +1,50 @@ +import * as path from 'path'; +import * as express from 'express'; +import * as request from 'supertest'; +import * as packageJson from '../package.json'; +import { expect } from 'chai'; +import { createApp } from './common/app'; + +describe(packageJson.name, () => { + let app = null; + + before(async () => { + // Set up the express app + const apiSpec = path.join('test', 'resources', 'serialized-deep-object.objects.yaml'); + app = await createApp({ apiSpec }, 3005, (app) => + app.use( + `${app.basePath}`, + express + .Router() + .get(`/deep_object`, (req, res) => res.json(req.query)) + ), + ); + }); + + after(() => { + app.server.close(); + }); + + it('should explode deepObject query params', async () => + request(app) + .get(`${app.basePath}/deep_object?settings[state]=default`) + .expect(200) + .then((r) => { + const expected = { + settings: { + greeting: 'hello', + state: 'default' + } + }; + expect(r.body).to.deep.equals(expected); + })); + + it('should explode deepObject query params (optional query param)', async () => + request(app) + .get(`${app.basePath}/deep_object`) + .expect(200) + .then((r) => { + const expected = {}; + expect(r.body).to.deep.equals(expected); + })); +});