diff --git a/src/middlewares/parsers/schema.preprocessor.ts b/src/middlewares/parsers/schema.preprocessor.ts index 53836579..283e8d4b 100644 --- a/src/middlewares/parsers/schema.preprocessor.ts +++ b/src/middlewares/parsers/schema.preprocessor.ts @@ -23,6 +23,7 @@ interface TraversalState { interface TopLevelPathNodes { requestBodies: Root[]; + requestParameters: Root[]; responses: Root[]; } interface TopLevelSchemaNodes extends TopLevelPathNodes { @@ -43,14 +44,31 @@ class Node { } type SchemaObjectNode = Node; +function isParameterObject(node: ParameterObject | ReferenceObject): node is ParameterObject { + return !((node as ReferenceObject).$ref); +} +function isReferenceObject(node: ArraySchemaObject | NonArraySchemaObject | ReferenceObject): node is ReferenceObject { + return !!((node as ReferenceObject).$ref); +} +function isArraySchemaObject(node: ArraySchemaObject | NonArraySchemaObject | ReferenceObject): node is ArraySchemaObject { + return !!((node as ArraySchemaObject).items); +} +function isNonArraySchemaObject(node: ArraySchemaObject | NonArraySchemaObject | ReferenceObject): node is NonArraySchemaObject { + return !isArraySchemaObject(node) && !isReferenceObject(node); +} + class Root extends Node { constructor(schema: T, path: string[]) { super(null, schema, path); } } -type SchemaObject = OpenAPIV3.SchemaObject; +type ArraySchemaObject = OpenAPIV3.ArraySchemaObject; +type NonArraySchemaObject = OpenAPIV3.NonArraySchemaObject; +type OperationObject = OpenAPIV3.OperationObject; +type ParameterObject = OpenAPIV3.ParameterObject; type ReferenceObject = OpenAPIV3.ReferenceObject; +type SchemaObject = OpenAPIV3.SchemaObject; type Schema = ReferenceObject | SchemaObject; if (!Array.prototype['flatMap']) { @@ -99,6 +117,7 @@ export class SchemaPreprocessor { schemas: componentSchemas, requestBodies: r.requestBodies, responses: r.responses, + requestParameters: r.requestParameters, }; // Traverse the schemas @@ -127,6 +146,7 @@ export class SchemaPreprocessor { private gatherSchemaNodesFromPaths(): TopLevelPathNodes { const requestBodySchemas = []; + const requestParameterSchemas = []; const responseSchemas = []; for (const [p, pi] of Object.entries(this.apiDoc.paths)) { @@ -140,14 +160,18 @@ export class SchemaPreprocessor { const node = new Root(operation, path); const requestBodies = this.extractRequestBodySchemaNodes(node); const responseBodies = this.extractResponseSchemaNodes(node); + const requestParameters = this.extractRequestParameterSchemaNodes(node); requestBodySchemas.push(...requestBodies); responseSchemas.push(...responseBodies); + requestParameterSchemas.push(...requestParameters); } } } + return { requestBodies: requestBodySchemas, + requestParameters: requestParameterSchemas, responses: responseSchemas, }; } @@ -230,6 +254,10 @@ export class SchemaPreprocessor { for (const node of nodes.responses) { recurse(null, node, initOpts()); } + + for (const node of nodes.requestParameters) { + recurse(null, node, initOpts()); + } } private schemaVisitor( @@ -505,6 +533,28 @@ export class SchemaPreprocessor { return schemas; } + private extractRequestParameterSchemaNodes( + operationNode: Root, + ): Root[] { + + return (operationNode.schema.parameters ?? []).flatMap((node) => { + const parameterObject = isParameterObject(node) ? node : undefined; + if (!parameterObject?.schema) return []; + + const schema = isNonArraySchemaObject(parameterObject.schema) ? + parameterObject.schema : + undefined; + if (!schema) return []; + + return new Root(schema, [ + ...operationNode.path, + 'parameters', + parameterObject.name, + parameterObject.in + ]); + }); + } + private resolveSchema(schema): T { if (!schema) return null; const ref = schema?.['$ref']; @@ -541,7 +591,7 @@ export class SchemaPreprocessor { ) => // if name or ref exists and are equal (opParam['name'] && opParam['name'] === pathParam['name']) || - (opParam['$ref'] && opParam['$ref'] === pathParam['$ref']); + (opParam['$ref'] && opParam['$ref'] === pathParam['$ref']); // Add Path level query param to list ONLY if there is not already an operation-level query param by the same name. for (const param of parameters) { diff --git a/test/resources/serdes.yaml b/test/resources/serdes.yaml index d8fff47b..6a4f29ed 100644 --- a/test/resources/serdes.yaml +++ b/test/resources/serdes.yaml @@ -13,6 +13,17 @@ paths: required: true schema: $ref: "#/components/schemas/ObjectId" + - name: date-time-from-inline + in: query + required: false + schema: + type: string + format: date-time + - name: date-time-from-schema + in: query + required: false + schema: + $ref: "#/components/schemas/DateTime" - name: baddateresponse in: query schema: @@ -29,21 +40,51 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/User" + allOf: + - $ref: "#/components/schemas/User" + - type: object + properties: + summary: + type: object + additionalProperties: + type: object + properties: + value: + type: string + typeof: + type: string /users: post: requestBody: content : application/json: schema: - $ref: '#/components/schemas/User' + allOf: + - $ref: '#/components/schemas/User' + - type: object + properties: + creationDateTimeInline: + type: string + format: date-time responses: 200: description: "" content: application/json: schema: - $ref: "#/components/schemas/User" + allOf: + - $ref: "#/components/schemas/User" + - type: object + properties: + summary: + type: object + additionalProperties: + type: object + properties: + value: + type: string + typeof: + type: string components: schemas: ObjectId: diff --git a/test/serdes.spec.ts b/test/serdes.spec.ts index 499b7878..25774403 100644 --- a/test/serdes.spec.ts +++ b/test/serdes.spec.ts @@ -26,6 +26,15 @@ class BadDate extends Date { } } +function toSummary(title, value) { + return { + [title]: { + value: value?.toISOString?.() || value?.toString(), + typeof: typeof value + } + } +} + describe('serdes', () => { let app = null; @@ -63,6 +72,10 @@ describe('serdes', () => { creationDateTime: date, creationDate: date, shortOrLong: 'a', + summary: { + ...toSummary('req.query.date-time-from-inline', req.query['date-time-from-inline']), + ...toSummary('req.query.date-time-from-schema', req.query['date-time-from-schema']), + }, }); }); app.post([`${app.basePath}/users`], (req, res) => { @@ -75,7 +88,13 @@ describe('serdes', () => { if (typeof req.body.creationDateTime !== 'object' || !(req.body.creationDateTime instanceof Date)) { throw new Error("Should be deserialized to Date object"); } - res.json(req.body); + if (typeof req.body.creationDateTimeInline !== 'object' || !(req.body.creationDateTimeInline instanceof Date)) { + throw new Error("Should be deserialized to Date object"); + } + res.json({ + ...req.body, + summary: Object.entries(req.body).reduce((acc, [k, v]) => Object.assign(acc, toSummary(k, v)), {}) + }); }); app.use((err, req, res, next) => { res.status(err.status ?? 500).json({ @@ -103,12 +122,16 @@ describe('serdes', () => { it('should control GOOD id format and get a response in expected format', async () => request(app) - .get(`${app.basePath}/users/5fdefd13a6640bb5fb5fa925`) + .get(`${app.basePath}/users/5fdefd13a6640bb5fb5fa925?date-time-from-inline=2019-11-20T01%3A11%3A54.930Z&date-time-from-schema=2020-11-20T01%3A11%3A54.930Z`) .expect(200) .then((r) => { expect(r.body.id).to.equal('5fdefd13a6640bb5fb5fa925'); expect(r.body.creationDate).to.equal('2020-12-20'); expect(r.body.creationDateTime).to.equal("2020-12-20T07:28:19.213Z"); + expect(r.body.summary['req.query.date-time-from-schema'].value).to.equal("2020-11-20T01:11:54.930Z"); + expect(r.body.summary['req.query.date-time-from-schema'].typeof).to.equal("object"); + expect(r.body.summary['req.query.date-time-from-inline'].value).to.equal("2019-11-20T01:11:54.930Z"); + expect(r.body.summary['req.query.date-time-from-inline'].typeof).to.equal("object"); })); it('should POST also works with deserialize on request then serialize en response', async () => @@ -117,6 +140,7 @@ describe('serdes', () => { .send({ id: '5fdefd13a6640bb5fb5fa925', creationDateTime: '2020-12-20T07:28:19.213Z', + creationDateTimeInline: '2019-11-21T07:24:19.213Z', creationDate: '2020-12-20', shortOrLong: 'ab', }) @@ -126,6 +150,12 @@ describe('serdes', () => { expect(r.body.id).to.equal('5fdefd13a6640bb5fb5fa925'); expect(r.body.creationDate).to.equal('2020-12-20'); expect(r.body.creationDateTime).to.equal("2020-12-20T07:28:19.213Z"); + expect(r.body.summary['creationDate'].value).to.equal('2020-12-20T00:00:00.000Z'); + expect(r.body.summary['creationDate'].typeof).to.equal('object'); + expect(r.body.summary['creationDateTime'].value).to.equal('2020-12-20T07:28:19.213Z'); + expect(r.body.summary['creationDateTime'].typeof).to.equal('object'); + expect(r.body.summary['creationDateTimeInline'].value).to.equal('2019-11-21T07:24:19.213Z'); + expect(r.body.summary['creationDateTimeInline'].typeof).to.equal('object'); })); it('should POST throw error on invalid schema ObjectId', async () =>