From c40b3a6845d0b863d6ba463a7fdd738cd63335c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Gro=C3=9Fe?= Date: Fri, 26 Aug 2022 23:40:06 +0200 Subject: [PATCH] feat!: add multi support for create --- README.md | 2 +- example/openapi-v3/multi.js | 2 +- lib/openapi.js | 36 +------ lib/v2/generator.js | 105 ++++++++++--------- lib/v3/generator.js | 53 +++++++++- test/index.test.js | 2 +- test/v2/generator.test.js | 84 ++++++++++----- test/v3/expected-memory-spec-multi-only.json | 53 +++++++++- test/v3/expected-memory-spec.json | 2 +- test/v3/generator.test.js | 40 ++++++- types/index.d.ts | 44 +++++--- types/index.test-d.ts | 24 ++++- 12 files changed, 316 insertions(+), 131 deletions(-) diff --git a/README.md b/README.md index c453b93..d3824ed 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,7 @@ __Options:__ - `docsPath` (*optional*, default: `/docs`) - Path where Swagger UI is served - `indexFile` - (*optional*) - Path to a file which is served instead of default Swagger UI index file - `getSwaggerInitializerScript({ docsPath, docsJsonPath, specs })` (*optional*) - Function to create the script that will be served as swagger-initializer.js instead of the default one from Swagger UI - - The function takes one options and should return a string that contains valid JS + - The function takes one options object and should return a string that contains valid JS ### `service.id` diff --git a/example/openapi-v3/multi.js b/example/openapi-v3/multi.js index 1c28256..cdeaa0e 100644 --- a/example/openapi-v3/multi.js +++ b/example/openapi-v3/multi.js @@ -32,7 +32,7 @@ module.exports = (app) => { messageService.docs = { description: 'A service to send and receive messages', - multi: ['patch', 'remove'], + multi: ['patch', 'remove', 'create'], refs: { sortParameter: 'messages_sort_filter' } diff --git a/lib/openapi.js b/lib/openapi.js index 0293d44..d0100af 100644 --- a/lib/openapi.js +++ b/lib/openapi.js @@ -57,7 +57,7 @@ function ignoreService (service, path, includeConfig, ignoreConfig) { function determineMultiOperations (service) { if (service.options && service.options.multi) { - return ['remove', 'update', 'patch'].filter((operation) => _.isFunction(service[operation])); + return ['remove', 'update', 'patch', 'create'].filter((operation) => _.isFunction(service[operation])); } return []; @@ -96,6 +96,8 @@ class OpenApiGenerator { applyDefinitionsToSpecs (service, model) {} // has to implemented in sub class /* istanbul ignore next: abstract method */ getPathParameterSpec (name) {} // has to implemented in sub class + /* istanbul ignore next: abstract method */ + getOperationsRefs (service, model) {} // has to implemented in sub class getOperationSpecDefaults () { return undefined; } getOperationArgs ({ service, apiPath, version }) { @@ -126,35 +128,6 @@ class OpenApiGenerator { }; } - getOperationsRefs (service, model) { - const modelList = `${model}_list`; - const refs = { - findResponse: modelList, - getResponse: model, - createRequest: model, - createResponse: model, - updateRequest: model, - updateResponse: model, - updateMultiRequest: modelList, - updateMultiResponse: modelList, - patchRequest: model, - patchResponse: model, - patchMultiRequest: model, - patchMultiResponse: modelList, - removeResponse: model, - removeMultiResponse: modelList, - filterParameter: model, - sortParameter: '' - }; - if (typeof this.config.defaults.getOperationsRefs === 'function') { - Object.assign(refs, this.config.defaults.getOperationsRefs(model, service)); - } - if (typeof service.docs.refs === 'object') { - Object.assign(refs, service.docs.refs); - } - return refs; - } - addService (service, path) { if (ignoreService(service, path, this.config.include, this.config.ignore)) { return; @@ -208,7 +181,8 @@ class OpenApiGenerator { refs: this.getOperationsRefs(service, model), specs: this.specs, service, - config: this.config + config: this.config, + multiOperations }; const addMethodToSpecs = (pathObj, path, methodIdName, method, httpMethod, customMethod = false) => { diff --git a/lib/v2/generator.js b/lib/v2/generator.js index 8245e97..d752c2a 100644 --- a/lib/v2/generator.js +++ b/lib/v2/generator.js @@ -21,6 +21,16 @@ function idPathParameters (idName, idType, description) { return params; } +function jsonSchemaRef (ref) { + if (typeof ref === 'object' && ref.refs) { + throw new Error('Multiple refs defined as object are only supported with openApiVersion 3'); + } + + return { + $ref: `#/definitions/${ref}` + }; +} + class OpenApiV2Generator extends AbstractApiGenerator { getDefaultSpecs () { return { @@ -36,6 +46,37 @@ class OpenApiV2Generator extends AbstractApiGenerator { }; } + getOperationsRefs (service, model) { + const modelList = `${model}_list`; + const refs = { + findResponse: modelList, + getResponse: model, + createRequest: model, + createResponse: model, + createMultiRequest: model, + createMultiResponse: model, + updateRequest: model, + updateResponse: model, + updateMultiRequest: modelList, + updateMultiResponse: modelList, + patchRequest: model, + patchResponse: model, + patchMultiRequest: model, + patchMultiResponse: modelList, + removeResponse: model, + removeMultiResponse: modelList, + filterParameter: model, + sortParameter: '' + }; + if (typeof this.config.defaults.getOperationsRefs === 'function') { + Object.assign(refs, this.config.defaults.getOperationsRefs(model, service)); + } + if (typeof service.docs.refs === 'object') { + Object.assign(refs, service.docs.refs); + } + return refs; + } + getOperationDefaults () { return { find ({ tags, security, securities, specs, refs }) { @@ -63,9 +104,7 @@ class OpenApiV2Generator extends AbstractApiGenerator { responses: { 200: { description: 'success', - schema: { - $ref: `#/definitions/${refs.findResponse}` - } + schema: jsonSchemaRef(refs.findResponse) }, 401: { description: 'not authenticated' @@ -87,9 +126,7 @@ class OpenApiV2Generator extends AbstractApiGenerator { responses: { 200: { description: 'success', - schema: { - $ref: `#/definitions/${refs.getResponse}` - } + schema: jsonSchemaRef(refs.getResponse) }, 401: { description: 'not authenticated' @@ -114,16 +151,12 @@ class OpenApiV2Generator extends AbstractApiGenerator { in: 'body', name: 'body', required: true, - schema: { - $ref: `#/definitions/${refs.createRequest}` - } + schema: jsonSchemaRef(refs.createRequest) }], responses: { 201: { description: 'created', - schema: { - $ref: `#/definitions/${refs.createResponse}` - } + schema: jsonSchemaRef(refs.createResponse) }, 401: { description: 'not authenticated' @@ -145,16 +178,12 @@ class OpenApiV2Generator extends AbstractApiGenerator { in: 'body', name: 'body', required: true, - schema: { - $ref: `#/definitions/${refs.updateRequest}` - } + schema: jsonSchemaRef(refs.updateRequest) }]), responses: { 200: { description: 'success', - schema: { - $ref: `#/definitions/${refs.updateResponse}` - } + schema: jsonSchemaRef(refs.updateResponse) }, 401: { description: 'not authenticated' @@ -179,16 +208,12 @@ class OpenApiV2Generator extends AbstractApiGenerator { in: 'body', name: 'body', required: true, - schema: { - $ref: `#/definitions/${refs.updateMultiRequest}` - } + schema: jsonSchemaRef(refs.updateMultiRequest) }], responses: { 200: { description: 'success', - schema: { - $ref: `#/definitions/${refs.updateMultiResponse}` - } + schema: jsonSchemaRef(refs.updateMultiResponse) }, 401: { description: 'not authenticated' @@ -210,16 +235,12 @@ class OpenApiV2Generator extends AbstractApiGenerator { in: 'body', name: 'body', required: true, - schema: { - $ref: `#/definitions/${refs.patchRequest}` - } + schema: jsonSchemaRef(refs.patchRequest) }]), responses: { 200: { description: 'success', - schema: { - $ref: `#/definitions/${refs.patchResponse}` - } + schema: jsonSchemaRef(refs.patchResponse) }, 401: { description: 'not authenticated' @@ -245,17 +266,13 @@ class OpenApiV2Generator extends AbstractApiGenerator { in: 'body', name: 'body', required: true, - schema: { - $ref: `#/definitions/${refs.patchMultiRequest}` - } + schema: jsonSchemaRef(refs.patchMultiRequest) } ], responses: { 200: { description: 'success', - schema: { - $ref: `#/definitions/${refs.patchMultiResponse}` - } + schema: jsonSchemaRef(refs.patchMultiResponse) }, 401: { description: 'not authenticated' @@ -277,9 +294,7 @@ class OpenApiV2Generator extends AbstractApiGenerator { responses: { 200: { description: 'success', - schema: { - $ref: `#/definitions/${refs.removeResponse}` - } + schema: jsonSchemaRef(refs.removeResponse) }, 401: { description: 'not authenticated' @@ -304,9 +319,7 @@ class OpenApiV2Generator extends AbstractApiGenerator { responses: { 200: { description: 'success', - schema: { - $ref: `#/definitions/${refs.removeMultiResponse}` - } + schema: jsonSchemaRef(refs.removeMultiResponse) }, 401: { description: 'not authenticated' @@ -349,18 +362,14 @@ class OpenApiV2Generator extends AbstractApiGenerator { in: 'body', name: 'body', required: true, - schema: { - $ref: `#/definitions/${refs[refRequestName]}` - } + schema: jsonSchemaRef(refs[refRequestName]) }); } } const refResponseName = `${method}Response`; if (refs[refResponseName]) { - customDoc.responses['200'].schema = { - $ref: `#/definitions/${refs[refResponseName]}` - }; + customDoc.responses['200'].schema = jsonSchemaRef(refs[refResponseName]); } return customDoc; diff --git a/lib/v3/generator.js b/lib/v3/generator.js index 1c248d5..dff26ff 100644 --- a/lib/v3/generator.js +++ b/lib/v3/generator.js @@ -22,6 +22,19 @@ function filterParameter (refs) { } function jsonSchemaRef (ref) { + if (typeof ref === 'object' && ref.refs) { + const { refs, type, ...rest } = ref; + + return { + 'application/json': { + schema: { + [type]: refs.map(innerRef => ({ $ref: `#/components/schemas/${innerRef}` })), + ...rest + } + } + }; + } + return { 'application/json': { schema: { @@ -60,7 +73,7 @@ class OpenApiV3Generator extends AbstractApiGenerator { components: { schemas: {} }, - openapi: '3.0.2', + openapi: '3.0.3', tags: [], info: {} }; @@ -77,6 +90,37 @@ class OpenApiV3Generator extends AbstractApiGenerator { }; } + getOperationsRefs (service, model) { + const modelList = `${model}_list`; + const refs = { + findResponse: modelList, + getResponse: model, + createRequest: model, + createResponse: model, + createMultiRequest: { refs: [model, modelList], type: 'oneOf' }, + createMultiResponse: { refs: [model, modelList], type: 'oneOf' }, + updateRequest: model, + updateResponse: model, + updateMultiRequest: modelList, + updateMultiResponse: modelList, + patchRequest: model, + patchResponse: model, + patchMultiRequest: model, + patchMultiResponse: modelList, + removeResponse: model, + removeMultiResponse: modelList, + filterParameter: model, + sortParameter: '' + }; + if (typeof this.config.defaults.getOperationsRefs === 'function') { + Object.assign(refs, this.config.defaults.getOperationsRefs(model, service)); + } + if (typeof service.docs.refs === 'object') { + Object.assign(refs, service.docs.refs); + } + return refs; + } + getOperationDefaults () { return { find ({ tags, security, securities, refs }) { @@ -147,18 +191,19 @@ class OpenApiV3Generator extends AbstractApiGenerator { security: utils.security('get', securities, security) }; }, - create ({ tags, security, securities, refs }) { + create ({ tags, security, securities, refs, multiOperations }) { + const multi = multiOperations.includes('create'); return { tags, description: 'Creates a new resource with data.', requestBody: { required: true, - content: jsonSchemaRef(refs.createRequest) + content: multi ? jsonSchemaRef(refs.createMultiRequest) : jsonSchemaRef(refs.createRequest) }, responses: { 201: { description: 'created', - content: jsonSchemaRef(refs.createResponse) + content: multi ? jsonSchemaRef(refs.createMultiResponse) : jsonSchemaRef(refs.createResponse) }, 401: { description: 'not authenticated' diff --git a/test/index.test.js b/test/index.test.js index 8023f42..aa66eb3 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -136,7 +136,7 @@ describe('feathers-swagger', () => { it('serves json specification at docJsonPath', () => { return axios.get('http://localhost:6776/docs.json').then(({ data: docs }) => { - expect(docs.openapi).to.equal('3.0.2'); + expect(docs.openapi).to.equal('3.0.3'); expect(docs.info.title).to.equal('A test'); expect(docs.info.description).to.equal('A description'); expect(docs.paths['/messages']).to.exist; diff --git a/test/v2/generator.test.js b/test/v2/generator.test.js index 8911908..22c6a63 100644 --- a/test/v2/generator.test.js +++ b/test/v2/generator.test.js @@ -90,34 +90,59 @@ describe('swagger v2 generator', function () { specs = {}; }); - it('defaults.schemasGenerator result should be merged into definitions', function () { - const swaggerConfig = { - defaults: { - schemasGenerator (service, model, modelName, schemas) { - expect(service).to.equal(service); - expect(model).to.equal('message'); - expect(modelName).to.equal('message'); - expect(schemas.alreadyThere).to.equal('should_stay'); - - return { - newSchema: 'schema1', - alreadyThere2: 'schema2' - }; + describe('defaults', () => { + it('getOperationsRefs result should be used as default refs', function () { + const swaggerConfig = { + defaults: { + getOperationsRefs (model, service) { + expect(model).to.equal('message'); + expect(service).to.equal(service); + + return { + findResponse: 'own_ref' + }; + } } - } - }; - specs.definitions = { - alreadyThere: 'should_stay', - alreadyThere2: 'will_be_overwritten' - }; - const gen = new OpenApi2Generator(specs, swaggerConfig); + }; + const gen = new OpenApi2Generator(specs, swaggerConfig); - gen.addService(service, 'message'); + gen.addService(service, 'message'); - expect(specs.definitions).to.deep.equal({ - alreadyThere: 'should_stay', - alreadyThere2: 'schema2', - newSchema: 'schema1' + expect(specs.paths['/message'].get.responses[200].schema.$ref) + .to.equal('#/definitions/own_ref'); + expect(specs.paths['/message/{id}'].get.responses[200].schema.$ref) + .to.equal('#/definitions/message'); + }); + + it('schemasGenerator result should be merged into definitions', function () { + const swaggerConfig = { + defaults: { + schemasGenerator (service, model, modelName, schemas) { + expect(service).to.equal(service); + expect(model).to.equal('message'); + expect(modelName).to.equal('message'); + expect(schemas.alreadyThere).to.equal('should_stay'); + + return { + newSchema: 'schema1', + alreadyThere2: 'schema2' + }; + } + } + }; + specs.definitions = { + alreadyThere: 'should_stay', + alreadyThere2: 'will_be_overwritten' + }; + const gen = new OpenApi2Generator(specs, swaggerConfig); + + gen.addService(service, 'message'); + + expect(specs.definitions).to.deep.equal({ + alreadyThere: 'should_stay', + alreadyThere2: 'schema2', + newSchema: 'schema1' + }); }); }); }); @@ -240,6 +265,15 @@ describe('swagger v2 generator', function () { expect(specs.paths['/message/{id}'].get.responses[200].schema.$ref).to.equal('#/definitions/getMessage'); }); + it('refs with multiple schemas object should throw error', () => { + service.docs.refs = { + findResponse: { refs: ['otherRef'], type: 'oneOf' } + }; + + expect(() => gen.addService(service, 'message')) + .to.throw(Error, 'Multiple refs defined as object are only supported with openApiVersion 3'); + }); + describe('array service.id', function () { it('array service.id should be consumed', function () { const service = memory({ id: ['firstId', 'secondId'] }); diff --git a/test/v3/expected-memory-spec-multi-only.json b/test/v3/expected-memory-spec-multi-only.json index daeeaf3..29ebe34 100644 --- a/test/v3/expected-memory-spec-multi-only.json +++ b/test/v3/expected-memory-spec-multi-only.json @@ -5,6 +5,57 @@ }, "paths": { "/message": { + "post": { + "description": "Creates a new resource with data.", + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/message" + }, + { + "$ref": "#/components/schemas/message_list" + } + ] + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/message" + }, + { + "$ref": "#/components/schemas/message_list" + } + ] + } + } + }, + "description": "created" + }, + "401": { + "description": "not authenticated" + }, + "500": { + "description": "general error" + } + }, + "security": [], + "summary": "", + "tags": [ + "message" + ] + }, "put": { "parameters": [], "responses": { @@ -149,7 +200,7 @@ } } }, - "openapi": "3.0.2", + "openapi": "3.0.3", "tags": [ { "name": "message", diff --git a/test/v3/expected-memory-spec.json b/test/v3/expected-memory-spec.json index eed0007..345e8be 100644 --- a/test/v3/expected-memory-spec.json +++ b/test/v3/expected-memory-spec.json @@ -22,7 +22,7 @@ "title": "openapi generator v3 tests", "version": "1.0.0" }, - "openapi": "3.0.2", + "openapi": "3.0.3", "paths": { "/message": { "get": { diff --git a/test/v3/generator.test.js b/test/v3/generator.test.js index 89a6f15..2b70d3c 100644 --- a/test/v3/generator.test.js +++ b/test/v3/generator.test.js @@ -49,7 +49,6 @@ describe('openopi v3 generator', function () { operations: { find: false, get: false, - create: false, update: false, patch: false, remove: false @@ -451,6 +450,20 @@ describe('openopi v3 generator', function () { getResponse: 'message', createRequest: 'message', createResponse: 'message', + createMultiRequest: { + refs: [ + 'message', + 'message_list' + ], + type: 'oneOf' + }, + createMultiResponse: { + refs: [ + 'message', + 'message_list' + ], + type: 'oneOf' + }, updateRequest: 'message', updateResponse: 'message', updateMultiRequest: 'message_list', @@ -925,6 +938,31 @@ describe('openopi v3 generator', function () { .to.equal('#/components/schemas/message_sort'); }); + it('refs object should be allowed for schema refs', function () { + service.docs.refs = { + findResponse: { refs: ['otherRef', 'getMessage'], type: 'anyOf' }, + getResponse: { refs: ['otherRef', 'getMessage'], type: 'oneOf', discriminator: { propertyName: 'type' } } + }; + + gen.addService(service, 'message'); + + expect(specs.paths['/message'].get.responses[200].content['application/json'].schema) + .to.deep.equal({ + anyOf: [ + { $ref: '#/components/schemas/otherRef' }, + { $ref: '#/components/schemas/getMessage' } + ] + }); + expect(specs.paths['/message/{id}'].get.responses[200].content['application/json'].schema) + .to.deep.equal({ + oneOf: [ + { $ref: '#/components/schemas/otherRef' }, + { $ref: '#/components/schemas/getMessage' } + ], + discriminator: { propertyName: 'type' } + }); + }); + it('pathParams should be consumed for path parameter', function () { const pathParam = { description: 'A global path param', diff --git a/types/index.d.ts b/types/index.d.ts index ab3a243..c42615e 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -17,22 +17,34 @@ interface FnGetOperationArgs { } & UnknownObject; } +interface SchemaMultiRef { + refs: string[]; + type: 'oneOf' | 'allOf' | 'anyOf'; + discriminator?: { + propertyName: string; + } +} + +type SchemaRef = string | SchemaMultiRef; + interface OperationRefs { - findResponse?: string; - getResponse?: string; - createRequest?: string; - createResponse?: string; - updateRequest?: string; - updateResponse?: string; - updateMultiRequest?: string; - updateMultiResponse?: string; - patchRequest?: string; - patchResponse?: string; - patchMultiRequest?: string; - patchMultiResponse?: string; - removeResponse?: string; - removeMultiResponse?: string; - [customMethodRef: string]: string | undefined; + findResponse?: SchemaRef; + getResponse?: SchemaRef; + createRequest?: SchemaRef; + createResponse?: SchemaRef; + createMultiRequest?: SchemaRef; + createMultiResponse?: SchemaRef; + updateRequest?: SchemaRef; + updateResponse?: SchemaRef; + updateMultiRequest?: SchemaRef; + updateMultiResponse?: SchemaRef; + patchRequest?: SchemaRef; + patchResponse?: SchemaRef; + patchMultiRequest?: SchemaRef; + patchMultiResponse?: SchemaRef; + removeResponse?: SchemaRef; + removeMultiResponse?: SchemaRef; + [customMethodRef: string]: SchemaRef | undefined; } interface FnGetOperationsRefs { @@ -81,7 +93,7 @@ interface ExternalDocs { type Securities = Array<'find' | 'get' | 'create' | 'update' | 'patch' | 'remove' | 'updateMulti' | 'patchMulti' | 'removeMulti' | 'all' | string>; -type MultiOperations = Array<'update' | 'patch' | 'remove' | 'all'>; +type MultiOperations = Array<'update' | 'patch' | 'remove' | 'create' | 'all'>; declare function feathersSwagger(config: feathersSwagger.SwaggerInitOptions): () => void; diff --git a/types/index.test-d.ts b/types/index.test-d.ts index 79ac26b..ee75d8d 100644 --- a/types/index.test-d.ts +++ b/types/index.test-d.ts @@ -254,6 +254,8 @@ const service: ServiceSwaggerAddon = { refs: { createRequest: 'model', createResponse: 'model', + createMultiRequest: 'model', + createMultiResponse: 'model', findResponse: 'model', getResponse: 'model', patchRequest: 'model', @@ -296,7 +298,7 @@ const serviceEmptyRefs: ServiceSwaggerAddon = { } }; -// array idType +// array idType, operation refs with multiple schemas const serviceIdTypeArray: ServiceSwaggerAddon = { docs: { idType: ['string', 'integer'], @@ -305,6 +307,26 @@ const serviceIdTypeArray: ServiceSwaggerAddon = { patch: ['firstName', 'secondName'], remove: ['firstName', 'secondName'], update: ['firstName', 'secondName'], + }, + refs: { + createRequest: { refs: ['model', 'model2'], type: 'oneOf', discriminator: { propertyName: 'test' } }, + createResponse: { refs: ['model', 'model2'], type: 'allOf' }, + createMultiRequest: { refs: ['model', 'model2'], type: 'oneOf' }, + createMultiResponse: { refs: ['model', 'model2'], type: 'oneOf' }, + findResponse: { refs: ['model', 'model2'], type: 'anyOf' }, + getResponse: { refs: ['model', 'model2'], type: 'oneOf' }, + patchRequest: { refs: ['model', 'model2'], type: 'oneOf' }, + patchResponse: { refs: ['model', 'model2'], type: 'oneOf' }, + patchMultiRequest: { refs: ['model', 'model2'], type: 'oneOf' }, + patchMultiResponse: { refs: ['model', 'model2'], type: 'oneOf' }, + removeResponse: { refs: ['model', 'model2'], type: 'oneOf' }, + removeMultiResponse: { refs: ['model', 'model2'], type: 'oneOf' }, + updateRequest: { refs: ['model', 'model2'], type: 'oneOf' }, + updateResponse: { refs: ['model', 'model2'], type: 'oneOf' }, + updateMultiRequest: { refs: ['model', 'model2'], type: 'oneOf' }, + updateMultiResponse: { refs: ['model', 'model2'], type: 'oneOf' }, + customMethodRequest: { refs: ['model', 'model2'], type: 'oneOf' }, + customMethodResponse: { refs: ['model', 'model2'], type: 'oneOf' }, } } };