diff --git a/examples/access-control-migration/package.json b/examples/access-control-migration/package.json index 69c41ebdd298..dbd7607f5f8a 100644 --- a/examples/access-control-migration/package.json +++ b/examples/access-control-migration/package.json @@ -24,6 +24,7 @@ "test:dev": "lb-mocha --allow-console-logs dist/__tests__/**/*.js", "verify": "npm pack && tar xf loopback-access-control-migration*.tgz && tree package && npm run clean", "migrate": "node ./dist/migrate", + "openapi-spec": "node ./dist/openapi-spec", "prestart": "npm run build", "start": "node ." }, diff --git a/examples/access-control-migration/src/migrate.ts b/examples/access-control-migration/src/migrate.ts new file mode 100644 index 000000000000..a709b9d2ebaa --- /dev/null +++ b/examples/access-control-migration/src/migrate.ts @@ -0,0 +1,25 @@ +// Copyright IBM Corp. 2020. All Rights Reserved. +// Node module: @loopback/example-access-control-migration +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {AccessControlApplication} from './application'; + +export async function migrate(args: string[]) { + const existingSchema = args.includes('--rebuild') ? 'drop' : 'alter'; + console.log('Migrating schemas (%s existing schema)', existingSchema); + + const app = new AccessControlApplication(); + await app.boot(); + await app.migrateSchema({existingSchema}); + + // Connectors usually keep a pool of opened connections, + // this keeps the process running even after all work is done. + // We need to exit explicitly. + process.exit(0); +} + +migrate(process.argv).catch(err => { + console.error('Cannot migrate database schema', err); + process.exit(1); +}); diff --git a/examples/access-control-migration/src/openapi-spec.ts b/examples/access-control-migration/src/openapi-spec.ts new file mode 100644 index 000000000000..725350d88cf8 --- /dev/null +++ b/examples/access-control-migration/src/openapi-spec.ts @@ -0,0 +1,28 @@ +// Copyright IBM Corp. 2020. All Rights Reserved. +// Node module: @loopback/example-access-control-migration +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {ApplicationConfig} from '@loopback/core'; +import {AccessControlApplication} from './application'; + +/** + * Export the OpenAPI spec from the application + */ +async function exportOpenApiSpec(): Promise { + const config: ApplicationConfig = { + rest: { + port: +(process.env.PORT ?? 3000), + host: process.env.HOST ?? 'localhost', + }, + }; + const outFile = process.argv[2] ?? ''; + const app = new AccessControlApplication(config); + await app.boot(); + await app.exportOpenApiSpec(outFile); +} + +exportOpenApiSpec().catch(err => { + console.error('Fail to export OpenAPI spec from the application.', err); + process.exit(1); +}); diff --git a/examples/greeting-app/package.json b/examples/greeting-app/package.json index 2171ec05334f..26772817c24c 100644 --- a/examples/greeting-app/package.json +++ b/examples/greeting-app/package.json @@ -22,6 +22,7 @@ "test": "lb-mocha \"dist/__tests__/**/*.js\"", "posttest": "npm run lint", "test:dev": "lb-mocha --allow-console-logs dist/__tests__/**/*.js && npm run posttest", + "openapi-spec": "node ./dist/openapi-spec", "prestart": "npm run build", "start": "node .", "verify": "npm pack && tar xf *example-greeting-app*.tgz && tree package && npm run clean" diff --git a/examples/greeting-app/src/openapi-spec.ts b/examples/greeting-app/src/openapi-spec.ts new file mode 100644 index 000000000000..1c8cb6ccb6b6 --- /dev/null +++ b/examples/greeting-app/src/openapi-spec.ts @@ -0,0 +1,28 @@ +// Copyright IBM Corp. 2020. All Rights Reserved. +// Node module: @loopback/example-greeting-app +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {ApplicationConfig} from '@loopback/core'; +import {GreetingApplication} from './application'; + +/** + * Export the OpenAPI spec from the application + */ +async function exportOpenApiSpec(): Promise { + const config: ApplicationConfig = { + rest: { + port: +(process.env.PORT ?? 3000), + host: process.env.HOST ?? 'localhost', + }, + }; + const outFile = process.argv[2] ?? ''; + const app = new GreetingApplication(config); + await app.boot(); + await app.exportOpenApiSpec(outFile); +} + +exportOpenApiSpec().catch(err => { + console.error('Fail to export OpenAPI spec from the application.', err); + process.exit(1); +}); diff --git a/examples/lb3-application/package.json b/examples/lb3-application/package.json index e20e6df99820..6ee0c12068b7 100644 --- a/examples/lb3-application/package.json +++ b/examples/lb3-application/package.json @@ -24,6 +24,7 @@ "test:dev": "lb-mocha --allow-console-logs dist/__tests__/**/*.js && npm run posttest", "verify": "npm pack && tar xf loopback-lb3-application*.tgz && tree package && npm run clean", "migrate": "node ./dist/migrate", + "openapi-spec": "node ./dist/openapi-spec", "prestart": "npm run build", "start": "node ." }, diff --git a/examples/lb3-application/src/openapi-spec.ts b/examples/lb3-application/src/openapi-spec.ts new file mode 100644 index 000000000000..3bd58c7a7d0d --- /dev/null +++ b/examples/lb3-application/src/openapi-spec.ts @@ -0,0 +1,28 @@ +// Copyright IBM Corp. 2020. All Rights Reserved. +// Node module: @loopback/example-lb3-application +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {ApplicationConfig} from '@loopback/core'; +import {CoffeeShopApplication} from './application'; + +/** + * Export the OpenAPI spec from the application + */ +async function exportOpenApiSpec(): Promise { + const config: ApplicationConfig = { + rest: { + port: +(process.env.PORT ?? 3000), + host: process.env.HOST ?? 'localhost', + }, + }; + const outFile = process.argv[2] ?? ''; + const app = new CoffeeShopApplication(config); + await app.boot(); + await app.exportOpenApiSpec(outFile); +} + +exportOpenApiSpec().catch(err => { + console.error('Fail to export OpenAPI spec from the application.', err); + process.exit(1); +}); diff --git a/examples/multi-tenancy/package.json b/examples/multi-tenancy/package.json index d2a39babdc5d..f4d0cf1ca1c1 100644 --- a/examples/multi-tenancy/package.json +++ b/examples/multi-tenancy/package.json @@ -28,6 +28,7 @@ "docker:build": "docker build -t @loopback/example-multi-tenancy .", "docker:run": "docker run -p 3000:3000 -d @loopback/example-multi-tenancy", "migrate": "node ./dist/migrate", + "openapi-spec": "node ./dist/openapi-spec", "prestart": "npm run build", "start": "node -r source-map-support/register .", "clean": "lb-clean dist *.tsbuildinfo .eslintcache" diff --git a/examples/multi-tenancy/src/openapi-spec.ts b/examples/multi-tenancy/src/openapi-spec.ts new file mode 100644 index 000000000000..65df7ab893c0 --- /dev/null +++ b/examples/multi-tenancy/src/openapi-spec.ts @@ -0,0 +1,28 @@ +// Copyright IBM Corp. 2020. All Rights Reserved. +// Node module: @loopback/example-multi-tenancy +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {ApplicationConfig} from '@loopback/core'; +import {ExampleMultiTenancyApplication} from './application'; + +/** + * Export the OpenAPI spec from the application + */ +async function exportOpenApiSpec(): Promise { + const config: ApplicationConfig = { + rest: { + port: +(process.env.PORT ?? 3000), + host: process.env.HOST ?? 'localhost', + }, + }; + const outFile = process.argv[2] ?? ''; + const app = new ExampleMultiTenancyApplication(config); + await app.boot(); + await app.exportOpenApiSpec(outFile); +} + +exportOpenApiSpec().catch(err => { + console.error('Fail to export OpenAPI spec from the application.', err); + process.exit(1); +}); diff --git a/examples/rest-crud/package.json b/examples/rest-crud/package.json index 5c40130a7a9d..7bdee19cdb32 100644 --- a/examples/rest-crud/package.json +++ b/examples/rest-crud/package.json @@ -24,6 +24,7 @@ "test:dev": "lb-mocha --allow-console-logs dist/__tests__/**/*.js && npm run posttest", "verify": "npm pack && tar xf loopback-rest-crud*.tgz && tree package && npm run clean", "migrate": "node ./dist/migrate", + "openapi-spec": "node ./dist/openapi-spec", "prestart": "npm run build", "start": "node ." }, diff --git a/examples/rest-crud/src/openapi-spec.ts b/examples/rest-crud/src/openapi-spec.ts new file mode 100644 index 000000000000..641e349c589f --- /dev/null +++ b/examples/rest-crud/src/openapi-spec.ts @@ -0,0 +1,28 @@ +// Copyright IBM Corp. 2020. All Rights Reserved. +// Node module: @loopback/example-rest-crud +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {ApplicationConfig} from '@loopback/core'; +import {TodoApplication} from './application'; + +/** + * Export the OpenAPI spec from the application + */ +async function exportOpenApiSpec(): Promise { + const config: ApplicationConfig = { + rest: { + port: +(process.env.PORT ?? 3000), + host: process.env.HOST ?? 'localhost', + }, + }; + const outFile = process.argv[2] ?? ''; + const app = new TodoApplication(config); + await app.boot(); + await app.exportOpenApiSpec(outFile); +} + +exportOpenApiSpec().catch(err => { + console.error('Fail to export OpenAPI spec from the application.', err); + process.exit(1); +}); diff --git a/examples/todo-jwt/package.json b/examples/todo-jwt/package.json index 64f59fdf3f03..09b40c9b752d 100644 --- a/examples/todo-jwt/package.json +++ b/examples/todo-jwt/package.json @@ -24,6 +24,7 @@ "test:dev": "lb-mocha --allow-console-logs dist/__tests__/**/*.js && npm run posttest", "verify": "npm pack && tar xf loopback-todo*.tgz && tree package && npm run clean", "migrate": "node ./dist/migrate", + "openapi-spec": "node ./dist/openapi-spec", "prestart": "npm run build", "start": "node ." }, diff --git a/examples/todo-jwt/src/openapi-spec.ts b/examples/todo-jwt/src/openapi-spec.ts new file mode 100644 index 000000000000..9c9da05ee9a1 --- /dev/null +++ b/examples/todo-jwt/src/openapi-spec.ts @@ -0,0 +1,28 @@ +// Copyright IBM Corp. 2020. All Rights Reserved. +// Node module: @loopback/example-todo-jwt +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {ApplicationConfig} from '@loopback/core'; +import {TodoListApplication} from './application'; + +/** + * Export the OpenAPI spec from the application + */ +async function exportOpenApiSpec(): Promise { + const config: ApplicationConfig = { + rest: { + port: +(process.env.PORT ?? 3000), + host: process.env.HOST ?? 'localhost', + }, + }; + const outFile = process.argv[2] ?? ''; + const app = new TodoListApplication(config); + await app.boot(); + await app.exportOpenApiSpec(outFile); +} + +exportOpenApiSpec().catch(err => { + console.error('Fail to export OpenAPI spec from the application.', err); + process.exit(1); +}); diff --git a/examples/todo-list/package.json b/examples/todo-list/package.json index baa1dfd9812f..01d690aafe11 100644 --- a/examples/todo-list/package.json +++ b/examples/todo-list/package.json @@ -24,6 +24,7 @@ "test:dev": "lb-mocha --allow-console-logs dist/__tests__/**/*.js && npm run posttest", "verify": "npm pack && tar xf loopback-todo-list*.tgz && tree package && npm run clean", "migrate": "node ./dist/migrate", + "openapi-spec": "node ./dist/openapi-spec", "prestart": "npm run build", "start": "node ." }, diff --git a/examples/todo-list/src/openapi-spec.ts b/examples/todo-list/src/openapi-spec.ts new file mode 100644 index 000000000000..a17bf33d1920 --- /dev/null +++ b/examples/todo-list/src/openapi-spec.ts @@ -0,0 +1,28 @@ +// Copyright IBM Corp. 2020. All Rights Reserved. +// Node module: @loopback/example-todo-list +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {ApplicationConfig} from '@loopback/core'; +import {TodoListApplication} from './application'; + +/** + * Export the OpenAPI spec from the application + */ +async function exportOpenApiSpec(): Promise { + const config: ApplicationConfig = { + rest: { + port: +(process.env.PORT ?? 3000), + host: process.env.HOST ?? 'localhost', + }, + }; + const outFile = process.argv[2] ?? ''; + const app = new TodoListApplication(config); + await app.boot(); + await app.exportOpenApiSpec(outFile); +} + +exportOpenApiSpec().catch(err => { + console.error('Fail to export OpenAPI spec from the application.', err); + process.exit(1); +}); diff --git a/examples/todo/package.json b/examples/todo/package.json index 249e3c0e5ab4..0956e5d6cc47 100644 --- a/examples/todo/package.json +++ b/examples/todo/package.json @@ -24,6 +24,7 @@ "test:dev": "lb-mocha --allow-console-logs dist/__tests__/**/*.js && npm run posttest", "verify": "npm pack && tar xf loopback-todo*.tgz && tree package && npm run clean", "migrate": "node ./dist/migrate", + "openapi-spec": "node ./dist/openapi-spec", "prestart": "npm run build", "start": "node ." }, diff --git a/examples/todo/src/openapi-spec.ts b/examples/todo/src/openapi-spec.ts new file mode 100644 index 000000000000..161966ef0cb6 --- /dev/null +++ b/examples/todo/src/openapi-spec.ts @@ -0,0 +1,28 @@ +// Copyright IBM Corp. 2018,2020. All Rights Reserved. +// Node module: @loopback/example-todo +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {ApplicationConfig} from '@loopback/core'; +import {TodoListApplication} from './application'; + +/** + * Export the OpenAPI spec from the application + */ +async function exportOpenApiSpec(): Promise { + const config: ApplicationConfig = { + rest: { + port: +(process.env.PORT ?? 3000), + host: process.env.HOST ?? 'localhost', + }, + }; + const outFile = process.argv[2] ?? ''; + const app = new TodoListApplication(config); + await app.boot(); + await app.exportOpenApiSpec(outFile); +} + +exportOpenApiSpec().catch(err => { + console.error('Fail to export OpenAPI spec from the application.', err); + process.exit(1); +}); diff --git a/examples/validation-app/package.json b/examples/validation-app/package.json index f525cb664ad5..02f5cb59dddc 100644 --- a/examples/validation-app/package.json +++ b/examples/validation-app/package.json @@ -26,6 +26,7 @@ "docker:build": "docker build -t validation-app .", "docker:run": "docker run -p 3000:3000 -d validation-app", "migrate": "node ./dist/migrate", + "openapi-spec": "node ./dist/openapi-spec", "prestart": "npm run build", "start": "node -r source-map-support/register ." }, diff --git a/examples/validation-app/src/openapi-spec.ts b/examples/validation-app/src/openapi-spec.ts new file mode 100644 index 000000000000..e5c351543766 --- /dev/null +++ b/examples/validation-app/src/openapi-spec.ts @@ -0,0 +1,28 @@ +// Copyright IBM Corp. 2020. All Rights Reserved. +// Node module: @loopback/example-validation-app +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {ApplicationConfig} from '@loopback/core'; +import {ValidationApplication} from './application'; + +/** + * Export the OpenAPI spec from the application + */ +async function exportOpenApiSpec(): Promise { + const config: ApplicationConfig = { + rest: { + port: +(process.env.PORT ?? 3000), + host: process.env.HOST ?? 'localhost', + }, + }; + const outFile = process.argv[2] ?? ''; + const app = new ValidationApplication(config); + await app.boot(); + await app.exportOpenApiSpec(outFile); +} + +exportOpenApiSpec().catch(err => { + console.error('Fail to export OpenAPI spec from the application.', err); + process.exit(1); +}); diff --git a/packages/cli/generators/app/templates/src/openapi-spec.ts.ejs b/packages/cli/generators/app/templates/src/openapi-spec.ts.ejs new file mode 100644 index 000000000000..e98842137100 --- /dev/null +++ b/packages/cli/generators/app/templates/src/openapi-spec.ts.ejs @@ -0,0 +1,23 @@ +import {ApplicationConfig} from '@loopback/core'; +import {<%= project.applicationName %>} from './application'; + +/** + * Export the OpenAPI spec from the application + */ +async function exportOpenApiSpec(): Promise { + const config: ApplicationConfig = { + rest: { + port: +(process.env.PORT ?? 3000), + host: process.env.HOST ?? 'localhost', + }, + }; + const outFile = process.argv[2] ?? ''; + const app = new <%= project.applicationName %>(config); + await app.boot(); + await app.exportOpenApiSpec(outFile); +} + +exportOpenApiSpec().catch(err => { + console.error('Fail to export OpenAPI spec from the application.', err); + process.exit(1); +}); diff --git a/packages/cli/generators/project/templates/package.json.ejs b/packages/cli/generators/project/templates/package.json.ejs index 5ec1431ac4bb..795774189e9c 100644 --- a/packages/cli/generators/project/templates/package.json.ejs +++ b/packages/cli/generators/project/templates/package.json.ejs @@ -51,6 +51,7 @@ "docker:run": "docker run -p 3000:3000 -d <%= project.name -%>", <%_ } -%> "migrate": "node ./dist/migrate", + "openapi-spec": "node ./dist/openapi-spec", "prestart": "npm run build", "start": "node -r source-map-support/register .", <% } -%> diff --git a/packages/cli/generators/project/templates/package.plain.json.ejs b/packages/cli/generators/project/templates/package.plain.json.ejs index 16bc893846da..5aa08b3ff644 100644 --- a/packages/cli/generators/project/templates/package.plain.json.ejs +++ b/packages/cli/generators/project/templates/package.plain.json.ejs @@ -52,6 +52,7 @@ "docker:run": "docker run -p 3000:3000 -d <%= project.name -%>", <%_ } -%> "migrate": "node ./dist/migrate", + "openapi-spec": "node ./dist/openapi-spec", "start": "npm run build && node -r source-map-support/register .", <% } -%> "prepare": "npm run build" diff --git a/packages/cli/package.json b/packages/cli/package.json index ccdae2de6a9f..ab400147e3ed 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -167,7 +167,9 @@ "@loopback/example-passport-login": "^1.4.1", "@loopback/authentication-jwt": "^0.3.1", "@loopback/context-explorer": "^0.1.5", - "@loopback/express": "^1.2.1" + "@loopback/express": "^1.2.1", + "@types/js-yaml": "^3.12.4", + "js-yaml": "^3.13.1" } }, "copyright.owner": "IBM Corp.", diff --git a/packages/cli/snapshots/integration/generators/app.integration.snapshots.js b/packages/cli/snapshots/integration/generators/app.integration.snapshots.js index 52eaaa17544f..8ac6114f8988 100644 --- a/packages/cli/snapshots/integration/generators/app.integration.snapshots.js +++ b/packages/cli/snapshots/integration/generators/app.integration.snapshots.js @@ -324,6 +324,34 @@ npm-debug.log `; +exports[`app-generator specific files generates openapi spec script 1`] = ` +import {ApplicationConfig} from '@loopback/core'; +import {MyAppApplication} from './application'; + +/** + * Export the OpenAPI spec from the application + */ +async function exportOpenApiSpec(): Promise { + const config: ApplicationConfig = { + rest: { + port: +(process.env.PORT ?? 3000), + host: process.env.HOST ?? 'localhost', + }, + }; + const outFile = process.argv[2] ?? ''; + const app = new MyAppApplication(config); + await app.boot(); + await app.exportOpenApiSpec(outFile); +} + +exportOpenApiSpec().catch(err => { + console.error('Fail to export OpenAPI spec from the application.', err); + process.exit(1); +}); + +`; + + exports[`app-generator with --apiconnect adds imports for ApiConnectComponent 1`] = ` import {BootMixin} from '@loopback/boot'; import {ApplicationConfig} from '@loopback/core'; diff --git a/packages/cli/test/integration/generators/app.integration.js b/packages/cli/test/integration/generators/app.integration.js index 787dcaccdeb5..66fadf6db456 100644 --- a/packages/cli/test/integration/generators/app.integration.js +++ b/packages/cli/test/integration/generators/app.integration.js @@ -59,6 +59,14 @@ describe('app-generator specific files', () => { assertFilesToMatchSnapshot({}, 'src/migrate.ts'); }); + it('generates openapi spec script', () => { + assertFilesToMatchSnapshot({}, 'src/openapi-spec.ts'); + assert.fileContent( + 'package.json', + /"openapi-spec": "node \.\/dist\/openapi-spec"/, + ); + }); + it('generates docker files', () => { assertFilesToMatchSnapshot({}, 'Dockerfile', '.dockerignore'); diff --git a/packages/rest/src/__tests__/integration/export-openapi-spec.integration.ts b/packages/rest/src/__tests__/integration/export-openapi-spec.integration.ts new file mode 100644 index 000000000000..ad9e3fd93a0a --- /dev/null +++ b/packages/rest/src/__tests__/integration/export-openapi-spec.integration.ts @@ -0,0 +1,118 @@ +// Copyright IBM Corp. 2019,2020. All Rights Reserved. +// Node module: @loopback/rest +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {ApplicationConfig} from '@loopback/core'; +import {expect, givenHttpServerConfig, TestSandbox} from '@loopback/testlab'; +import fs from 'fs'; +import {safeLoad} from 'js-yaml'; +import path from 'path'; +import {format} from 'util'; +import {get, RestApplication} from '../..'; + +const sandbox = new TestSandbox(path.resolve(__dirname, '../../../.sandbox')); + +describe('exportOpenApiSpec', () => { + let app: MyApp; + let lastLog = ''; + + const log = (formatter: unknown, ...args: unknown[]) => { + lastLog = format(formatter, ...args); + }; + + const expectedSpec = { + openapi: '3.0.0', + info: { + title: 'LoopBack Application', + version: '1.0.0', + }, + paths: { + '/hello': { + get: { + 'x-controller-name': 'MyController', + 'x-operation-name': 'hello', + tags: ['MyController'], + responses: { + '200': { + description: 'Return value of MyController.hello', + }, + }, + operationId: 'MyController.hello', + }, + }, + }, + servers: [ + { + url: '/', + }, + ], + }; + + beforeEach(async () => { + lastLog = ''; + await givenApp(); + }); + + it('saves the spec to a json file for RestApplication', async () => { + const file = path.join(sandbox.path, 'openapi.json'); + await app.exportOpenApiSpec(file, log); + expectJsonSpec(file); + }); + + it('saves the spec to a json file for RestServer', async () => { + const file = path.join(sandbox.path, 'openapi.json'); + await app.restServer.exportOpenApiSpec(file, log); + expectJsonSpec(file); + }); + + it('writes the spec as a json document to console', async () => { + await app.exportOpenApiSpec('-', log); + expect(JSON.parse(lastLog)).to.eql(expectedSpec); + }); + + it('saves the spec to a yaml file', async () => { + const file = path.join(sandbox.path, 'openapi.yaml'); + await app.restServer.exportOpenApiSpec(file, log); + expect(lastLog.match(/The OpenAPI spec is saved to .+ openapi\.yaml$/)); + const content = fs.readFileSync(file, 'utf-8'); + expect(safeLoad(content)).to.eql(expectedSpec); + }); + + it('saves the spec to a yml file', async () => { + const file = path.join(sandbox.path, 'openapi.yml'); + await app.restServer.exportOpenApiSpec(file, log); + expect(lastLog.match(/The OpenAPI spec is saved to .+ openapi\.yml$/)); + const content = fs.readFileSync(file, 'utf-8'); + expect(safeLoad(content)).to.eql(expectedSpec); + }); + + after(() => sandbox.reset()); + + function expectJsonSpec(file: string) { + expect(lastLog.match(/The OpenAPI spec is saved to .+ openapi\.json$/)); + const content = fs.readFileSync(file, 'utf-8'); + expect(JSON.parse(content)).to.eql(expectedSpec); + } + + class MyController { + @get('/hello') + hello() { + return 'Hello'; + } + } + + class MyApp extends RestApplication { + constructor(config: ApplicationConfig) { + super(config); + this.controller(MyController); + } + async boot(): Promise {} + } + + async function givenApp() { + app = new MyApp({rest: givenHttpServerConfig()}); + await app.boot(); + return app; + } +}); diff --git a/packages/rest/src/rest.application.ts b/packages/rest/src/rest.application.ts index 7666bd5ae448..8618d0baa1c5 100644 --- a/packages/rest/src/rest.application.ts +++ b/packages/rest/src/rest.application.ts @@ -387,4 +387,18 @@ export class RestApplication extends Application implements HttpServerLike { ): void { this.restServer.mountExpressRouter(basePath, router, spec); } + + /** + * Export the OpenAPI spec to the given json or yaml file + * @param outFile - File name for the spec. The extension of the file + * determines the format of the file. + * - `yaml` or `yml`: YAML + * - `json` or other: JSON + * If the outFile is not provided or its value is `''` or `'-'`, the spec is + * written to the console using the `log` function. + * @param log - Log function, default to `console.log` + */ + async exportOpenApiSpec(outFile = '', log = console.log): Promise { + return this.restServer.exportOpenApiSpec(outFile, log); + } } diff --git a/packages/rest/src/rest.server.ts b/packages/rest/src/rest.server.ts index 45e7258e4b91..5ddf6ab439c4 100644 --- a/packages/rest/src/rest.server.ts +++ b/packages/rest/src/rest.server.ts @@ -32,6 +32,7 @@ import cors from 'cors'; import debugFactory from 'debug'; import express, {ErrorRequestHandler} from 'express'; import {PathParams} from 'express-serve-static-core'; +import fs from 'fs'; import {IncomingMessage, ServerResponse} from 'http'; import {ServerOptions} from 'https'; import {safeDump} from 'js-yaml'; @@ -987,6 +988,34 @@ export class RestServer extends BaseMiddlewareRegistry ): void { this._externalRoutes.mountRouter(basePath, router, spec); } + + /** + * Export the OpenAPI spec to the given json or yaml file + * @param outFile - File name for the spec. The extension of the file + * determines the format of the file. + * - `yaml` or `yml`: YAML + * - `json` or other: JSON + * If the outFile is not provided or its value is `''` or `'-'`, the spec is + * written to the console using the `log` function. + * @param log - Log function, default to `console.log` + */ + async exportOpenApiSpec(outFile = '', log = console.log): Promise { + const spec = await this.getApiSpec(); + if (outFile === '-' || outFile === '') { + const json = JSON.stringify(spec, null, 2); + log('%s', json); + return; + } + const fileName = outFile.toLowerCase(); + if (fileName.endsWith('.yaml') || fileName.endsWith('.yml')) { + const yaml = safeDump(spec); + fs.writeFileSync(outFile, yaml, 'utf-8'); + } else { + const json = JSON.stringify(spec, null, 2); + fs.writeFileSync(outFile, json, 'utf-8'); + } + log('The OpenAPI spec has been saved to %s.', outFile); + } } /**