diff --git a/package-lock.json b/package-lock.json index 0ef5bcf24df6..cdec6142eb72 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25693,6 +25693,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/openapi-filter": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/openapi-filter/-/openapi-filter-3.2.3.tgz", + "integrity": "sha512-P1CtNLgN3trqm/Y0TYMS33gjGaDvip81z5wtxwUhGDym9jV46F9jDX0VG9mqjg+zJIF2JBm9yDB4oljzwSndDA==", + "dependencies": { + "reftools": "^1.1.1", + "yaml": "2.2", + "yargs": "^17.7.1" + }, + "bin": { + "openapi-filter": "openapi-filter.js" + } + }, + "node_modules/openapi-filter/node_modules/yaml": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.2.2.tgz", + "integrity": "sha512-CBKFWExMn46Foo4cldiChEzn7S7SRV+wqiluAb6xmueD/fGyRHIhX8m14vVGgeFWjN540nKCNVj6P21eQjgTuA==", + "engines": { + "node": ">= 14" + } + }, "node_modules/openapi-types": { "version": "12.1.3", "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", @@ -36227,6 +36248,7 @@ "minimist": "^1.2.8", "mkdirp": "^3.0.1", "natural-compare": "^1.4.0", + "openapi-filter": "^3.2.3", "pacote": "^17.0.6", "pluralize": "^8.0.0", "regenerate": "^1.4.2", diff --git a/packages/cli/.yo-rc.json b/packages/cli/.yo-rc.json index b679fbe72ece..fd12913f9b29 100644 --- a/packages/cli/.yo-rc.json +++ b/packages/cli/.yo-rc.json @@ -975,6 +975,27 @@ "name": "promote-anonymous-schemas", "hide": false }, + "readonly": { + "description": "Generate only GET endpoints.", + "required": false, + "type": "Boolean", + "name": "readonly", + "hide": false + }, + "exclude": { + "description": "Exclude endpoints with provided regex.", + "required": false, + "type": "String", + "name": "exclude", + "hide": false + }, + "include": { + "description": "Only include endpoints with provided regex.", + "required": false, + "type": "String", + "name": "include", + "hide": false + }, "config": { "type": "String", "alias": "c", diff --git a/packages/cli/generators/openapi/index.js b/packages/cli/generators/openapi/index.js index 9370f6349e66..d7f9cb82dca2 100644 --- a/packages/cli/generators/openapi/index.js +++ b/packages/cli/generators/openapi/index.js @@ -87,6 +87,24 @@ module.exports = class OpenApiGenerator extends BaseGenerator { type: Boolean, }); + this.option('readonly', { + description: g.f('Generate only GET endpoints.'), + required: false, + type: Boolean, + }); + + this.option('exclude', { + description: g.f('Exclude endpoints with provided regex.'), + required: false, + type: String, + }); + + this.option('include', { + description: g.f('Only include endpoints with provided regex.'), + required: false, + type: String, + }); + return super._setupGenerator(); } @@ -211,6 +229,57 @@ module.exports = class OpenApiGenerator extends BaseGenerator { } } + async askForReadonly() { + if (this.shouldExit()) return; + const prompts = [ + { + name: 'readonly', + message: g.f('Generate only GET endpoints.'), + when: false, + // when: !this.options.readonly, + default: false, + }, + ]; + const answers = await this.prompt(prompts); + if (answers.readonly) { + this.options.readonly = answers.readonly; + } + } + + async askForExclude() { + if (this.shouldExit()) return; + const prompts = [ + { + name: 'exclude', + message: g.f('Exclude endpoints with provided regex.'), + // when: !this.options.exclude, + when: false, + default: false, + }, + ]; + const answers = await this.prompt(prompts); + if (answers.exclude) { + this.options.exclude = answers.exclude; + } + } + + async askForInclude() { + if (this.shouldExit()) return; + const prompts = [ + { + name: 'include', + message: g.f('Only include endpoints with provided regex.'), + when: false, + // when: !this.options.include, + default: false, + }, + ]; + const answers = await this.prompt(prompts); + if (answers.include) { + this.options.include = answers.include; + } + } + async askForSpecUrlOrPath() { if (this.shouldExit()) return; if (this.dataSourceInfo && this.dataSourceInfo.specPath) { @@ -236,11 +305,19 @@ module.exports = class OpenApiGenerator extends BaseGenerator { async loadAndBuildApiSpec() { if (this.shouldExit()) return; + if (this.options.exclude && this.options.include) { + this.exit( + new Error('We cannot have include and exclude at the same time.'), + ); + } try { const result = await loadAndBuildSpec(this.url, { log: this.log, validate: this.options.validate, promoteAnonymousSchemas: this.options['promote-anonymous-schemas'], + readonly: this.options.readonly, + exclude: this.options.exclude, + include: this.options.include, }); debugJson('OpenAPI spec', result.apiSpec); Object.assign(this, result); diff --git a/packages/cli/generators/openapi/spec-loader.js b/packages/cli/generators/openapi/spec-loader.js index e67b052b17ed..2dfeebaf6e09 100644 --- a/packages/cli/generators/openapi/spec-loader.js +++ b/packages/cli/generators/openapi/spec-loader.js @@ -11,6 +11,7 @@ const {debugJson, cloneSpecObject} = require('./utils'); const {generateControllerSpecs} = require('./spec-helper'); const {generateModelSpecs, registerNamedSchemas} = require('./schema-helper'); const {ResolverError} = require('@apidevtools/json-schema-ref-parser'); +const openapiFilter = require('openapi-filter'); /** * Load swagger specs from the given url or file path; handle yml or json @@ -58,9 +59,12 @@ async function loadSpec(specUrlStr, {log, validate} = {}) { async function loadAndBuildSpec( url, - {log, validate, promoteAnonymousSchemas} = {}, + {log, validate, promoteAnonymousSchemas, readonly, exclude, include} = {}, ) { - const apiSpec = await loadSpec(url, {log, validate}); + let apiSpec = await loadSpec(url, {log, validate}); + + apiSpec = filterSpec(apiSpec, readonly, exclude, include); + // First populate the type registry for named schemas const typeRegistry = { objectTypeMapping: new Map(), @@ -77,6 +81,113 @@ async function loadAndBuildSpec( }; } +function getIndiciesOf(searchStr, str, caseSensitive) { + const searchStrLen = searchStr.length; + if (searchStrLen === 0) { + return []; + } + let startIndex = 0, + index; + const indices = []; + if (!caseSensitive) { + str = str.toLowerCase(); + searchStr = searchStr.toLowerCase(); + } + while ((index = str.indexOf(searchStr, startIndex)) > -1) { + indices.push(index); + startIndex = index + searchStrLen; + } + return indices; +} + +function insertAtIndex(str, substring, index) { + return str.slice(0, index) + substring + str.slice(index); +} + +function applyFilters(stringifiedSpecs, options) { + let specs = JSON.parse(stringifiedSpecs); + const openapiComponent = specs.components; + specs = openapiFilter.filter(specs, options); + specs.components = openapiComponent; + return specs; +} + +function findIndexes(stringSpecs, regex) { + let result; + const indices = []; + while ((result = regex.exec(stringSpecs))) { + indices.push(result.index); + } + return indices; +} + +function excludeOrIncludeSpec(specs, filter, options) { + let stringifiedSpecs = JSON.stringify(specs); + const regex = new RegExp(filter, 'g'); + + const indexes = findIndexes(stringifiedSpecs, regex); + let indiciesCount = 0; + while (indiciesCount < indexes.length) { + const ind = indexes[indiciesCount]; + for (let i = ind; i < stringifiedSpecs.length; i++) { + const toMatch = + stringifiedSpecs[i] + stringifiedSpecs[i + 1] + stringifiedSpecs[i + 2]; + if (toMatch === '":{') { + stringifiedSpecs = insertAtIndex( + stringifiedSpecs, + '"x-filter": true,', + i + 3, + ); + indiciesCount++; + break; + } + } + } + return applyFilters(stringifiedSpecs, options); +} + +function readonlySpec(specs, options) { + let stringifiedSpecs = JSON.stringify(specs); + const excludeOps = ['"post":', '"patch":', '"put":', '"delete":']; + excludeOps.forEach(operator => { + let indices = getIndiciesOf(operator, stringifiedSpecs); + let indiciesCount = 0; + while (indiciesCount < indices.length) { + indices = getIndiciesOf(operator, stringifiedSpecs); + const index = indices[indiciesCount]; + stringifiedSpecs = insertAtIndex( + stringifiedSpecs, + '"x-filter": true,', + index + operator.length + 1, + ); + indiciesCount++; + } + }); + return applyFilters(stringifiedSpecs, options); +} + +function filterSpec(specs, readonly, exclude, include) { + const options = { + valid: true, + info: true, + strip: true, + flags: ['x-filter'], + servers: true, + }; + if (readonly) { + specs = readonlySpec(specs, options); + } + if (exclude) { + // exclude only specified - include everything else + specs = excludeOrIncludeSpec(specs, exclude, options); + } + if (include) { + // include only specified - exclude everything else + specs = excludeOrIncludeSpec(specs, include, {...options, inverse: true}); + } + return specs; +} + module.exports = { loadSpec, loadAndBuildSpec, diff --git a/packages/cli/package.json b/packages/cli/package.json index 236607e1220d..7260f64d9d14 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -59,6 +59,7 @@ "mkdirp": "^3.0.1", "natural-compare": "^1.4.0", "pacote": "^17.0.6", + "openapi-filter": "^3.2.3", "pluralize": "^8.0.0", "regenerate": "^1.4.2", "semver": "^7.5.4", diff --git a/packages/cli/snapshots/integration/cli/cli.integration.snapshots.js b/packages/cli/snapshots/integration/cli/cli.integration.snapshots.js index 92103958edf2..74559bdbc0ff 100644 --- a/packages/cli/snapshots/integration/cli/cli.integration.snapshots.js +++ b/packages/cli/snapshots/integration/cli/cli.integration.snapshots.js @@ -1033,6 +1033,27 @@ exports[`cli saves command metadata to .yo-rc.json 1`] = ` "name": "promote-anonymous-schemas", "hide": false }, + "readonly": { + "description": "Generate only GET endpoints.", + "required": false, + "type": "Boolean", + "name": "readonly", + "hide": false + }, + "exclude": { + "description": "Exclude endpoints with provided regex.", + "required": false, + "type": "String", + "name": "exclude", + "hide": false + }, + "include": { + "description": "Only include endpoints with provided regex.", + "required": false, + "type": "String", + "name": "include", + "hide": false + }, "config": { "type": "String", "alias": "c", diff --git a/packages/cli/snapshots/integration/generators/openapi-client.integration.snapshots.js b/packages/cli/snapshots/integration/generators/openapi-client.integration.snapshots.js index b24b3ce8f1c6..c537ea080f3c 100644 --- a/packages/cli/snapshots/integration/generators/openapi-client.integration.snapshots.js +++ b/packages/cli/snapshots/integration/generators/openapi-client.integration.snapshots.js @@ -1819,6 +1819,1503 @@ export type ErrorWithRelations = Error & ErrorRelations; +`; + + +exports[`openapi-generator with --client generate all apis except passed in exclude [exclude apis matching the pattern "v3.1/alpha/*"] 1`] = ` +export * from './open-api.controller'; + +`; + + +exports[`openapi-generator with --client generate all apis except passed in exclude [exclude apis matching the pattern "v3.1/alpha/*"] 2`] = ` +import {api, operation, param, requestBody} from '@loopback/rest'; +import {RestCountries} from '../models/rest-countries.model'; + +/** + * The controller class is generated from OpenAPI spec with operations tagged + * by . + * + */ +@api({ + components: { + schemas: { + RestCountries: { + type: 'object', + }, + }, + }, + paths: {}, +}) +export class OpenApiController { + constructor() {} + /** + * + * + * @param fields + */ + @operation('get', '/v3.1/all', { + operationId: 'getAllCountries', + parameters: [ + { + name: 'fields', + in: 'query', + required: true, + schema: { + type: 'string', + }, + }, + ], + responses: { + default: { + description: 'getAllCountries default response', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/RestCountries', + }, + }, + }, + }, + }, +}) + async getAllCountries(@param({ + name: 'fields', + in: 'query', + required: true, + schema: { + type: 'string', + }, +}) fields: string): Promise { + throw new Error('Not implemented'); + } + /** + * Creates a country record + * + * @param _requestBody country to added + * @returns country response + */ + @operation('post', '/v3.1/all', { + description: 'Creates a country record', + operationId: 'createCountry', + requestBody: { + description: 'country to added', + required: true, + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/RestCountries', + }, + }, + }, + }, + responses: { + '200': { + description: 'country response', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/RestCountries', + }, + }, + }, + }, + default: { + description: 'unexpected error', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/Error', + }, + }, + }, + }, + }, +}) + async createCountry(@requestBody({ + description: 'country to added', + required: true, + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/RestCountries', + }, + }, + }, +}) _requestBody: RestCountries): Promise { + throw new Error('Not implemented'); + } + /** + * + * + * @param currency + * @param fields + */ + @operation('get', '/v3.1/currency/{currency}', { + operationId: 'getByCurrency', + parameters: [ + { + name: 'currency', + in: 'path', + required: true, + schema: { + type: 'string', + }, + }, + { + name: 'fields', + in: 'query', + required: true, + schema: { + type: 'string', + }, + }, + ], + responses: { + default: { + description: 'getByCurrency default response', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/RestCountries', + }, + }, + }, + }, + }, +}) + async getByCurrency(@param({ + name: 'currency', + in: 'path', + required: true, + schema: { + type: 'string', + }, +}) currency: string, @param({ + name: 'fields', + in: 'query', + required: true, + schema: { + type: 'string', + }, +}) fields: string): Promise { + throw new Error('Not implemented'); + } + /** + * + * + * @param name + * @param fullText + * @param fields + */ + @operation('get', '/v3.1/name/{name}', { + operationId: 'getByName', + parameters: [ + { + name: 'name', + in: 'path', + required: true, + schema: { + type: 'string', + }, + }, + { + name: 'fullText', + in: 'query', + required: true, + schema: { + type: 'boolean', + }, + }, + { + name: 'fields', + in: 'query', + required: true, + schema: { + type: 'string', + }, + }, + ], + responses: { + default: { + description: 'getByName default response', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/RestCountries', + }, + }, + }, + }, + }, +}) + async getByName(@param({ + name: 'name', + in: 'path', + required: true, + schema: { + type: 'string', + }, +}) name: string, @param({ + name: 'fullText', + in: 'query', + required: true, + schema: { + type: 'boolean', + }, +}) fullText: boolean, @param({ + name: 'fields', + in: 'query', + required: true, + schema: { + type: 'string', + }, +}) fields: string): Promise { + throw new Error('Not implemented'); + } + /** + * + * + * @param capital + * @param fields + */ + @operation('get', '/v3.1/capital/{capital}', { + operationId: 'getByCapital', + parameters: [ + { + name: 'capital', + in: 'path', + required: true, + schema: { + type: 'string', + }, + }, + { + name: 'fields', + in: 'query', + required: true, + schema: { + type: 'string', + }, + }, + ], + responses: { + default: { + description: 'getByCapital default response', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/RestCountries', + }, + }, + }, + }, + }, +}) + async getByCapital(@param({ + name: 'capital', + in: 'path', + required: true, + schema: { + type: 'string', + }, +}) capital: string, @param({ + name: 'fields', + in: 'query', + required: true, + schema: { + type: 'string', + }, +}) fields: string): Promise { + throw new Error('Not implemented'); + } + /** + * + * + * @param region + * @param fields + */ + @operation('get', '/v3.1/region/{region}', { + operationId: 'getByContinent', + parameters: [ + { + name: 'region', + in: 'path', + required: true, + schema: { + type: 'string', + }, + }, + { + name: 'fields', + in: 'query', + required: true, + schema: { + type: 'string', + }, + }, + ], + responses: { + default: { + description: 'getByContinent default response', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/RestCountries', + }, + }, + }, + }, + }, +}) + async getByContinent(@param({ + name: 'region', + in: 'path', + required: true, + schema: { + type: 'string', + }, +}) region: string, @param({ + name: 'fields', + in: 'query', + required: true, + schema: { + type: 'string', + }, +}) fields: string): Promise { + throw new Error('Not implemented'); + } + /** + * + * + * @param subregion + * @param fields + */ + @operation('get', '/v3.1/subregion/{subregion}', { + operationId: 'getBySubRegion', + parameters: [ + { + name: 'subregion', + in: 'path', + required: true, + schema: { + type: 'string', + }, + }, + { + name: 'fields', + in: 'query', + required: true, + schema: { + type: 'string', + }, + }, + ], + responses: { + default: { + description: 'getBySubRegion default response', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/RestCountries', + }, + }, + }, + }, + }, +}) + async getBySubRegion(@param({ + name: 'subregion', + in: 'path', + required: true, + schema: { + type: 'string', + }, +}) subregion: string, @param({ + name: 'fields', + in: 'query', + required: true, + schema: { + type: 'string', + }, +}) fields: string): Promise { + throw new Error('Not implemented'); + } + /** + * + * + * @param lang + * @param fields + */ + @operation('get', '/v3.1/lang/{lang}', { + operationId: 'getByLanguage', + parameters: [ + { + name: 'lang', + in: 'path', + required: true, + schema: { + type: 'string', + }, + }, + { + name: 'fields', + in: 'query', + required: true, + schema: { + type: 'string', + }, + }, + ], + responses: { + default: { + description: 'getByLanguage default response', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/RestCountries', + }, + }, + }, + }, + }, +}) + async getByLanguage(@param({ + name: 'lang', + in: 'path', + required: true, + schema: { + type: 'string', + }, +}) lang: string, @param({ + name: 'fields', + in: 'query', + required: true, + schema: { + type: 'string', + }, +}) fields: string): Promise { + throw new Error('Not implemented'); + } + /** + * + * + * @param demonym + * @param fields + */ + @operation('get', '/v3.1/demonym/{demonym}', { + operationId: 'getByDemonym', + parameters: [ + { + name: 'demonym', + in: 'path', + required: true, + schema: { + type: 'string', + }, + }, + { + name: 'fields', + in: 'query', + required: true, + schema: { + type: 'string', + }, + }, + ], + responses: { + default: { + description: 'getByDemonym default response', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/RestCountries', + }, + }, + }, + }, + }, +}) + async getByDemonym(@param({ + name: 'demonym', + in: 'path', + required: true, + schema: { + type: 'string', + }, +}) demonym: string, @param({ + name: 'fields', + in: 'query', + required: true, + schema: { + type: 'string', + }, +}) fields: string): Promise { + throw new Error('Not implemented'); + } + /** + * + * + * @param translation + * @param fields + */ + @operation('get', '/v3.1/translation/{translation}', { + operationId: 'getByTranslation', + parameters: [ + { + name: 'translation', + in: 'path', + required: true, + schema: { + type: 'string', + }, + }, + { + name: 'fields', + in: 'query', + required: true, + schema: { + type: 'string', + }, + }, + ], + responses: { + default: { + description: 'getByTranslation default response', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/RestCountries', + }, + }, + }, + }, + }, +}) + async getByTranslation(@param({ + name: 'translation', + in: 'path', + required: true, + schema: { + type: 'string', + }, +}) translation: string, @param({ + name: 'fields', + in: 'query', + required: true, + schema: { + type: 'string', + }, +}) fields: string): Promise { + throw new Error('Not implemented'); + } + /** + * + * + * @param status + * @param fields + */ + @operation('get', '/v3.1/independent', { + operationId: 'getIndependentCountries', + parameters: [ + { + name: 'status', + in: 'query', + required: true, + schema: { + type: 'boolean', + }, + }, + { + name: 'fields', + in: 'query', + required: true, + schema: { + type: 'string', + }, + }, + ], + responses: { + default: { + description: 'getIndependentCountries default response', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/RestCountries', + }, + }, + }, + }, + }, +}) + async getIndependentCountries(@param({ + name: 'status', + in: 'query', + required: true, + schema: { + type: 'boolean', + }, +}) status: boolean, @param({ + name: 'fields', + in: 'query', + required: true, + schema: { + type: 'string', + }, +}) fields: string): Promise { + throw new Error('Not implemented'); + } +} + + +`; + + +exports[`openapi-generator with --client generate apis only passed in include [include apis matching the pattern "v3.1/alpha/*"] 1`] = ` +export * from './open-api.controller'; + +`; + + +exports[`openapi-generator with --client generate apis only passed in include [include apis matching the pattern "v3.1/alpha/*"] 2`] = ` +import {api, operation, param, requestBody} from '@loopback/rest'; + +/** + * The controller class is generated from OpenAPI spec with operations tagged + * by . + * + */ +@api({ + components: { + schemas: { + RestCountries: { + type: 'object', + }, + }, + }, + paths: {}, +}) +export class OpenApiController { + constructor() {} + /** + * + * + * @param alphacode + * @param fields + */ + @operation('get', '/v3.1/alpha/{alphacode}', { + operationId: 'getByAlpha', + parameters: [ + { + name: 'alphacode', + in: 'path', + required: true, + schema: { + type: 'string', + }, + }, + { + name: 'fields', + in: 'query', + required: true, + schema: { + type: 'string', + }, + }, + ], + responses: { + default: { + description: 'getByAlpha default response', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/RestCountries', + }, + }, + }, + }, + }, +}) + async getByAlpha(@param({ + name: 'alphacode', + in: 'path', + required: true, + schema: { + type: 'string', + }, +}) alphacode: string, @param({ + name: 'fields', + in: 'query', + required: true, + schema: { + type: 'string', + }, +}) fields: string): Promise { + throw new Error('Not implemented'); + } + /** + * + * + * @param codes + * @param fields + */ + @operation('get', '/v3.1/alpha/', { + operationId: 'getByAlphaList', + parameters: [ + { + name: 'codes', + in: 'query', + required: true, + schema: { + type: 'string', + }, + }, + { + name: 'fields', + in: 'query', + required: true, + schema: { + type: 'string', + }, + }, + ], + responses: { + default: { + description: 'getByAlphaList default response', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/RestCountries', + }, + }, + }, + }, + }, +}) + async getByAlphaList(@param({ + name: 'codes', + in: 'query', + required: true, + schema: { + type: 'string', + }, +}) codes: string, @param({ + name: 'fields', + in: 'query', + required: true, + schema: { + type: 'string', + }, +}) fields: string): Promise { + throw new Error('Not implemented'); + } +} + + +`; + + +exports[`openapi-generator with --client generate readonly apis when passed readonly: true 1`] = ` +export * from './open-api.controller'; + +`; + + +exports[`openapi-generator with --client generate readonly apis when passed readonly: true 2`] = ` +import {api, operation, param, requestBody} from '@loopback/rest'; + +/** + * The controller class is generated from OpenAPI spec with operations tagged + * by . + * + */ +@api({ + components: { + schemas: { + RestCountries: { + type: 'object', + }, + }, + }, + paths: {}, +}) +export class OpenApiController { + constructor() {} + /** + * + * + * @param fields + */ + @operation('get', '/v3.1/all', { + operationId: 'getAllCountries', + parameters: [ + { + name: 'fields', + in: 'query', + required: true, + schema: { + type: 'string', + }, + }, + ], + responses: { + default: { + description: 'getAllCountries default response', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/RestCountries', + }, + }, + }, + }, + }, +}) + async getAllCountries(@param({ + name: 'fields', + in: 'query', + required: true, + schema: { + type: 'string', + }, +}) fields: string): Promise { + throw new Error('Not implemented'); + } + /** + * + * + * @param alphacode + * @param fields + */ + @operation('get', '/v3.1/alpha/{alphacode}', { + operationId: 'getByAlpha', + parameters: [ + { + name: 'alphacode', + in: 'path', + required: true, + schema: { + type: 'string', + }, + }, + { + name: 'fields', + in: 'query', + required: true, + schema: { + type: 'string', + }, + }, + ], + responses: { + default: { + description: 'getByAlpha default response', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/RestCountries', + }, + }, + }, + }, + }, +}) + async getByAlpha(@param({ + name: 'alphacode', + in: 'path', + required: true, + schema: { + type: 'string', + }, +}) alphacode: string, @param({ + name: 'fields', + in: 'query', + required: true, + schema: { + type: 'string', + }, +}) fields: string): Promise { + throw new Error('Not implemented'); + } + /** + * + * + * @param codes + * @param fields + */ + @operation('get', '/v3.1/alpha/', { + operationId: 'getByAlphaList', + parameters: [ + { + name: 'codes', + in: 'query', + required: true, + schema: { + type: 'string', + }, + }, + { + name: 'fields', + in: 'query', + required: true, + schema: { + type: 'string', + }, + }, + ], + responses: { + default: { + description: 'getByAlphaList default response', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/RestCountries', + }, + }, + }, + }, + }, +}) + async getByAlphaList(@param({ + name: 'codes', + in: 'query', + required: true, + schema: { + type: 'string', + }, +}) codes: string, @param({ + name: 'fields', + in: 'query', + required: true, + schema: { + type: 'string', + }, +}) fields: string): Promise { + throw new Error('Not implemented'); + } + /** + * + * + * @param currency + * @param fields + */ + @operation('get', '/v3.1/currency/{currency}', { + operationId: 'getByCurrency', + parameters: [ + { + name: 'currency', + in: 'path', + required: true, + schema: { + type: 'string', + }, + }, + { + name: 'fields', + in: 'query', + required: true, + schema: { + type: 'string', + }, + }, + ], + responses: { + default: { + description: 'getByCurrency default response', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/RestCountries', + }, + }, + }, + }, + }, +}) + async getByCurrency(@param({ + name: 'currency', + in: 'path', + required: true, + schema: { + type: 'string', + }, +}) currency: string, @param({ + name: 'fields', + in: 'query', + required: true, + schema: { + type: 'string', + }, +}) fields: string): Promise { + throw new Error('Not implemented'); + } + /** + * + * + * @param name + * @param fullText + * @param fields + */ + @operation('get', '/v3.1/name/{name}', { + operationId: 'getByName', + parameters: [ + { + name: 'name', + in: 'path', + required: true, + schema: { + type: 'string', + }, + }, + { + name: 'fullText', + in: 'query', + required: true, + schema: { + type: 'boolean', + }, + }, + { + name: 'fields', + in: 'query', + required: true, + schema: { + type: 'string', + }, + }, + ], + responses: { + default: { + description: 'getByName default response', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/RestCountries', + }, + }, + }, + }, + }, +}) + async getByName(@param({ + name: 'name', + in: 'path', + required: true, + schema: { + type: 'string', + }, +}) name: string, @param({ + name: 'fullText', + in: 'query', + required: true, + schema: { + type: 'boolean', + }, +}) fullText: boolean, @param({ + name: 'fields', + in: 'query', + required: true, + schema: { + type: 'string', + }, +}) fields: string): Promise { + throw new Error('Not implemented'); + } + /** + * + * + * @param capital + * @param fields + */ + @operation('get', '/v3.1/capital/{capital}', { + operationId: 'getByCapital', + parameters: [ + { + name: 'capital', + in: 'path', + required: true, + schema: { + type: 'string', + }, + }, + { + name: 'fields', + in: 'query', + required: true, + schema: { + type: 'string', + }, + }, + ], + responses: { + default: { + description: 'getByCapital default response', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/RestCountries', + }, + }, + }, + }, + }, +}) + async getByCapital(@param({ + name: 'capital', + in: 'path', + required: true, + schema: { + type: 'string', + }, +}) capital: string, @param({ + name: 'fields', + in: 'query', + required: true, + schema: { + type: 'string', + }, +}) fields: string): Promise { + throw new Error('Not implemented'); + } + /** + * + * + * @param region + * @param fields + */ + @operation('get', '/v3.1/region/{region}', { + operationId: 'getByContinent', + parameters: [ + { + name: 'region', + in: 'path', + required: true, + schema: { + type: 'string', + }, + }, + { + name: 'fields', + in: 'query', + required: true, + schema: { + type: 'string', + }, + }, + ], + responses: { + default: { + description: 'getByContinent default response', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/RestCountries', + }, + }, + }, + }, + }, +}) + async getByContinent(@param({ + name: 'region', + in: 'path', + required: true, + schema: { + type: 'string', + }, +}) region: string, @param({ + name: 'fields', + in: 'query', + required: true, + schema: { + type: 'string', + }, +}) fields: string): Promise { + throw new Error('Not implemented'); + } + /** + * + * + * @param subregion + * @param fields + */ + @operation('get', '/v3.1/subregion/{subregion}', { + operationId: 'getBySubRegion', + parameters: [ + { + name: 'subregion', + in: 'path', + required: true, + schema: { + type: 'string', + }, + }, + { + name: 'fields', + in: 'query', + required: true, + schema: { + type: 'string', + }, + }, + ], + responses: { + default: { + description: 'getBySubRegion default response', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/RestCountries', + }, + }, + }, + }, + }, +}) + async getBySubRegion(@param({ + name: 'subregion', + in: 'path', + required: true, + schema: { + type: 'string', + }, +}) subregion: string, @param({ + name: 'fields', + in: 'query', + required: true, + schema: { + type: 'string', + }, +}) fields: string): Promise { + throw new Error('Not implemented'); + } + /** + * + * + * @param lang + * @param fields + */ + @operation('get', '/v3.1/lang/{lang}', { + operationId: 'getByLanguage', + parameters: [ + { + name: 'lang', + in: 'path', + required: true, + schema: { + type: 'string', + }, + }, + { + name: 'fields', + in: 'query', + required: true, + schema: { + type: 'string', + }, + }, + ], + responses: { + default: { + description: 'getByLanguage default response', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/RestCountries', + }, + }, + }, + }, + }, +}) + async getByLanguage(@param({ + name: 'lang', + in: 'path', + required: true, + schema: { + type: 'string', + }, +}) lang: string, @param({ + name: 'fields', + in: 'query', + required: true, + schema: { + type: 'string', + }, +}) fields: string): Promise { + throw new Error('Not implemented'); + } + /** + * + * + * @param demonym + * @param fields + */ + @operation('get', '/v3.1/demonym/{demonym}', { + operationId: 'getByDemonym', + parameters: [ + { + name: 'demonym', + in: 'path', + required: true, + schema: { + type: 'string', + }, + }, + { + name: 'fields', + in: 'query', + required: true, + schema: { + type: 'string', + }, + }, + ], + responses: { + default: { + description: 'getByDemonym default response', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/RestCountries', + }, + }, + }, + }, + }, +}) + async getByDemonym(@param({ + name: 'demonym', + in: 'path', + required: true, + schema: { + type: 'string', + }, +}) demonym: string, @param({ + name: 'fields', + in: 'query', + required: true, + schema: { + type: 'string', + }, +}) fields: string): Promise { + throw new Error('Not implemented'); + } + /** + * + * + * @param translation + * @param fields + */ + @operation('get', '/v3.1/translation/{translation}', { + operationId: 'getByTranslation', + parameters: [ + { + name: 'translation', + in: 'path', + required: true, + schema: { + type: 'string', + }, + }, + { + name: 'fields', + in: 'query', + required: true, + schema: { + type: 'string', + }, + }, + ], + responses: { + default: { + description: 'getByTranslation default response', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/RestCountries', + }, + }, + }, + }, + }, +}) + async getByTranslation(@param({ + name: 'translation', + in: 'path', + required: true, + schema: { + type: 'string', + }, +}) translation: string, @param({ + name: 'fields', + in: 'query', + required: true, + schema: { + type: 'string', + }, +}) fields: string): Promise { + throw new Error('Not implemented'); + } + /** + * + * + * @param status + * @param fields + */ + @operation('get', '/v3.1/independent', { + operationId: 'getIndependentCountries', + parameters: [ + { + name: 'status', + in: 'query', + required: true, + schema: { + type: 'boolean', + }, + }, + { + name: 'fields', + in: 'query', + required: true, + schema: { + type: 'string', + }, + }, + ], + responses: { + default: { + description: 'getIndependentCountries default response', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/RestCountries', + }, + }, + }, + }, + }, +}) + async getIndependentCountries(@param({ + name: 'status', + in: 'query', + required: true, + schema: { + type: 'boolean', + }, +}) status: boolean, @param({ + name: 'fields', + in: 'query', + required: true, + schema: { + type: 'string', + }, +}) fields: string): Promise { + throw new Error('Not implemented'); + } +} + + `; diff --git a/packages/cli/test/fixtures/openapi/3.0/restcountries.yaml b/packages/cli/test/fixtures/openapi/3.0/restcountries.yaml new file mode 100644 index 000000000000..04cc79163d8a --- /dev/null +++ b/packages/cli/test/fixtures/openapi/3.0/restcountries.yaml @@ -0,0 +1,293 @@ +openapi: 3.0.1 +info: + title: rest-countries + description: Get information about countries via a RESTful API + contact: + name: Alejandro Matos + url: https://restcountries.com + license: + name: Mozilla Public License MPL 2.0 + url: https://www.mozilla.org/en-US/MPL/2.0/ + version: "3.1" +paths: + /v3.1/all: + get: + operationId: getAllCountries + parameters: + - name: fields + in: query + required: true + schema: + type: string + responses: + default: + description: getAllCountries default response + content: + application/json: + schema: + $ref: '#/components/schemas/RestCountries' + + post: + description: Creates a country record + operationId: createCountry + requestBody: + description: country to added + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/RestCountries' + responses: + '200': + description: country response + content: + application/json: + schema: + $ref: '#/components/schemas/RestCountries' + default: + description: unexpected error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + /v3.1/alpha/{alphacode}: + get: + operationId: getByAlpha + parameters: + - name: alphacode + in: path + required: true + schema: + type: string + - name: fields + in: query + required: true + schema: + type: string + responses: + default: + description: getByAlpha default response + content: + application/json: + schema: + $ref: '#/components/schemas/RestCountries' + /v3.1/alpha/: + get: + operationId: getByAlphaList + parameters: + - name: codes + in: query + required: true + schema: + type: string + - name: fields + in: query + required: true + schema: + type: string + responses: + default: + description: getByAlphaList default response + content: + application/json: + schema: + $ref: '#/components/schemas/RestCountries' + /v3.1/currency/{currency}: + get: + operationId: getByCurrency + parameters: + - name: currency + in: path + required: true + schema: + type: string + - name: fields + in: query + required: true + schema: + type: string + responses: + default: + description: getByCurrency default response + content: + application/json: + schema: + $ref: '#/components/schemas/RestCountries' + /v3.1/name/{name}: + get: + operationId: getByName + parameters: + - name: name + in: path + required: true + schema: + type: string + - name: fullText + in: query + required: true + schema: + type: boolean + - name: fields + in: query + required: true + schema: + type: string + responses: + default: + description: getByName default response + content: + application/json: + schema: + $ref: '#/components/schemas/RestCountries' + /v3.1/capital/{capital}: + get: + operationId: getByCapital + parameters: + - name: capital + in: path + required: true + schema: + type: string + - name: fields + in: query + required: true + schema: + type: string + responses: + default: + description: getByCapital default response + content: + application/json: + schema: + $ref: '#/components/schemas/RestCountries' + /v3.1/region/{region}: + get: + operationId: getByContinent + parameters: + - name: region + in: path + required: true + schema: + type: string + - name: fields + in: query + required: true + schema: + type: string + responses: + default: + description: getByContinent default response + content: + application/json: + schema: + $ref: '#/components/schemas/RestCountries' + /v3.1/subregion/{subregion}: + get: + operationId: getBySubRegion + parameters: + - name: subregion + in: path + required: true + schema: + type: string + - name: fields + in: query + required: true + schema: + type: string + responses: + default: + description: getBySubRegion default response + content: + application/json: + schema: + $ref: '#/components/schemas/RestCountries' + /v3.1/lang/{lang}: + get: + operationId: getByLanguage + parameters: + - name: lang + in: path + required: true + schema: + type: string + - name: fields + in: query + required: true + schema: + type: string + responses: + default: + description: getByLanguage default response + content: + application/json: + schema: + $ref: '#/components/schemas/RestCountries' + /v3.1/demonym/{demonym}: + get: + operationId: getByDemonym + parameters: + - name: demonym + in: path + required: true + schema: + type: string + - name: fields + in: query + required: true + schema: + type: string + responses: + default: + description: getByDemonym default response + content: + application/json: + schema: + $ref: '#/components/schemas/RestCountries' + /v3.1/translation/{translation}: + get: + operationId: getByTranslation + parameters: + - name: translation + in: path + required: true + schema: + type: string + - name: fields + in: query + required: true + schema: + type: string + responses: + default: + description: getByTranslation default response + content: + application/json: + schema: + $ref: '#/components/schemas/RestCountries' + /v3.1/independent: + get: + operationId: getIndependentCountries + parameters: + - name: status + in: query + required: true + schema: + type: boolean + - name: fields + in: query + required: true + schema: + type: string + responses: + default: + description: getIndependentCountries default response + content: + application/json: + schema: + $ref: '#/components/schemas/RestCountries' +components: + schemas: + RestCountries: + type: object \ No newline at end of file diff --git a/packages/cli/test/integration/generators/openapi-client.integration.js b/packages/cli/test/integration/generators/openapi-client.integration.js index 7198930155d5..cd5129ed9320 100644 --- a/packages/cli/test/integration/generators/openapi-client.integration.js +++ b/packages/cli/test/integration/generators/openapi-client.integration.js @@ -22,6 +22,11 @@ const props = { dataSourceName: 'petStore', }; +const restountriesProps = { + url: path.resolve(__dirname, '../../fixtures/openapi/3.0/restcountries.yaml'), + dataSourceName: 'restcountries', +}; + describe('openapi-generator with --client', /** @this {Mocha.Suite} */ function () { // These tests take longer to execute, they used to time out on Travis CI this.timeout(10000); @@ -42,6 +47,42 @@ describe('openapi-generator with --client', /** @this {Mocha.Suite} */ function assertModels(); }); + it('generate readonly apis when passed readonly: true', async () => { + await testUtils + .executeGenerator(generator) + .inDir(sandbox.path, () => testUtils.givenLBProject(sandbox.path)) + .withPrompts(restountriesProps) + .withOptions({readonly: true}); + assertControllers(); + assertDataSources(); + assertServices(); + assertModels(); + }); + + it('generate apis only passed in include [include apis matching the pattern "v3.1/alpha/*"]', async () => { + await testUtils + .executeGenerator(generator) + .inDir(sandbox.path, () => testUtils.givenLBProject(sandbox.path)) + .withPrompts(restountriesProps) + .withOptions({include: 'v3.1/alpha/*'}); + assertControllers(); + assertDataSources(); + assertServices(); + assertModels(); + }); + + it('generate all apis except passed in exclude [exclude apis matching the pattern "v3.1/alpha/*"]', async () => { + await testUtils + .executeGenerator(generator) + .inDir(sandbox.path, () => testUtils.givenLBProject(sandbox.path)) + .withPrompts(restountriesProps) + .withOptions({exclude: 'v3.1/alpha/*'}); + assertControllers(); + assertDataSources(); + assertServices(); + assertModels(); + }); + it('allows baseModel option', async () => { await testUtils .executeGenerator(generator)