Skip to content

Commit

Permalink
Auto registration as CAP plugin (#87)
Browse files Browse the repository at this point in the history
- The middleware registers itself when loaded as a CAP plugin
- Declarative configuration through `package.json` / `cds-rc.json`
- Programmatic registration is still possible, but disables auto mode then
- Make dependency to `@sap/cds-dk` optional.
   If running with the `cds` executable during development,
   the dependency is fulfilled anyways. Only when running as part of
   a deployed appi with `cds-serve`, then such a dependency is needed.
  • Loading branch information
chgeo authored Jan 18, 2024
1 parent b729840 commit 07cc93e
Show file tree
Hide file tree
Showing 13 changed files with 732 additions and 169 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,16 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/).

## Version 0.8.0 - tbd

### Added

- Auto-registration mode: the middleware registers itself when loaded as a CAP plugin
- Declarative configuration through `package.json` / `cds-rc.json`
- Programmatic registration is still possible, but disables auto mode then

### Changed

- Make dependency to `@sap/cds-dk` optional. If running with the `cds` executable during development, the dependency is fulfilled anyways. Only when running as part of a deployed app with `cds-serve`, then such a dependency is needed.

## Version 0.7.0 - 2023-12-04

### Changed
Expand Down
72 changes: 48 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,48 +1,76 @@
# cds-swagger-ui-express

An express middleware to serve OpenAPI definitions for [CAP](https://cap.cloud.sap) services in Swagger UI.
An CAP plugin to serve OpenAPI definitions for [CAP](https://cap.cloud.sap) services in Swagger UI.
Builds on top of [swagger-ui-express](https://www.npmjs.com/package/swagger-ui-express).

![Preview](https://raw.githubusercontent.com/chgeo/cds-swagger-ui-express/main/assets/cds-swagger-ui.png)

## Installation
## Setup

In your project, execute
In your project, just add a dependency like so:
```sh
npm install --save-dev cds-swagger-ui-express
npm add --save-dev cds-swagger-ui-express
```

## Usage
Once loaded by CAP, the package registers itself as a middleware with a default configuration.

Have this in your [`server.js`](https://cap.cloud.sap/docs/node.js/cds-server#custom-server-js):
## Run

```js
const cds = require ('@sap/cds')
module.exports = cds.server
After starting the app with `cds watch` or so, Swagger UI is served at `/$api-docs/<service-path>`, like http://localhost:4004/$api-docs/browse/

## Configuration

if (process.env.NODE_ENV !== 'production') {
const cds_swagger = require ('cds-swagger-ui-express')
cds.on ('bootstrap', app => app.use (cds_swagger()) )
You can set the most prominent options in `package.json` or `cds-rc.json`, as in other CAP apps:
```jsonc
"cds": {
"swagger": {
"basePath": "/$api-docs", // the root path to mount the middleware on
"apiPath": "", // the root path for the services (useful if behind a reverse proxy)
"diagram": true, // whether to render the YUML diagram
"odataVersion": "4.0" // the OData Version to compile the OpenAPI specs. Defaults to 4.01
}
}
```

Swagger UI is then served on `/$api-docs/<service-path>`, like http://localhost:4004/$api-docs/browse/
To disable the plugin, set this:
```jsonc
"cds": {
"swagger": false
}
```

Note that you can also set environment variables for each option, like `CDS_SWAGGER=false`. See the [`cds.env` docs](https://cap.cloud.sap/docs/node.js/cds-env#process-env) for more.

## Configuration

## Programmatic Usage (advanced)

If you need to register the plugin programmatically, e.g. in certain conditions only, you can do so in your [`server.js`](https://cap.cloud.sap/docs/node.js/cds-server#custom-server-js):

```js
const cds = require ('@sap/cds')
const cds_swagger = require ('cds-swagger-ui-express')
cds.on ('bootstrap', app =>
app.use (cds_swagger ())
)
```

In this case, the default 'auto registration' as plugin is disabled automatically to avoid conflicts.

### Programmatic Configuration

If the middleware is registered programmatically, you need to pass in the options through the API as well. No configuration from `package.json` is used here.

Call `cds_swagger ({...})` with the following object as first parameter:
```jsonc
{
"basePath": "/$api-docs", // the root path to mount the middleware on
"apiPath": "", // the root path for the services (useful if behind a reverse proxy)
"diagram": true, // whether to render the YUML diagram
"odataVersion": "4.0" // the OData Version to compile the OpenAPI specs. Defaults to 4.01
"basePath": ...,
// see section above for more
}
```

## Swagger Configuration
#### Swagger UI Options

Call `cds_swagger ({...}, {...})` with an additional object as second parameter. This object is passed to `swagger-ui-express` as [custom options](https://www.npmjs.com/package/swagger-ui-express#user-content-custom-swagger-options).
Call `cds_swagger ({...}, {...})` with an additional object as <em>second</em> parameter. This object is passed to `swagger-ui-express` as [custom options](https://www.npmjs.com/package/swagger-ui-express#user-content-custom-swagger-options).

Example:

Expand All @@ -61,7 +89,3 @@ For questions to specific properties, contact the maintainers of [swagger-ui-exp
### Notes

If you call [`cds.serve`](https://cap.cloud.sap/docs/node.js/cds-serve#cds-serve) on your own in your `server.js`, make sure to install this middleware _before_, as it relies on CDS' [`serving` events](https://cap.cloud.sap/docs/node.js/cds-server#cdson--serving-service).

### Known Issues

None at this time.
19 changes: 19 additions & 0 deletions cds-plugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
const cds = require ('@sap/cds')
const DEBUG = cds.debug('swagger')
const { registered } = require ('./lib/etc')

cds.on ('bootstrap', app => {
const { swagger } = cds.env
if (!swagger) {
return DEBUG?.('Plugin disabled by configuration')
}

if (cds[registered]) {
return DEBUG?.('Plugin disabled: already registered programmatically')
}

DEBUG?.('Plugin with options', swagger)
const cds_swagger = require('./lib/middleware')
app.use(cds_swagger(swagger))

})
67 changes: 7 additions & 60 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,63 +1,10 @@
const cds = require('@sap/cds-dk')
const swaggerUi = require('swagger-ui-express')
const express = require('express')
const { join } = require('path')
const LOG = cds.log('swagger')
const cds = require ('@sap/cds')
const DEBUG = cds.debug('swagger')

DEBUG?.('Programmatic registration')
const { registered } = require ('./lib/etc')
cds[registered] = true

/**
* Creates express middleware to serve CDS services in the Swagger UI
*
* @param {CdsSwaggerOptions} options - options for cds-swagger-ui itself
* @param {swaggerUi.SwaggerUiOptions} swaggerUiOptions - options passed to swagger-ui-express
* @returns {express.RequestHandler} - an express middleware
*/
module.exports = (options={}, swaggerUiOptions={}) => {
options = Object.assign({ basePath: '/$api-docs', apiPath: '' }, options)
const router = express.Router()
module.exports = require('./lib/middleware')

cds.on('serving', service => {
const apiPath = options.basePath + service.path
const mount = apiPath.replace('$', '[\\$]')
LOG._debug && LOG.debug('serving Swagger UI for ', { service: service.name, at: apiPath })

const uiOptions = Object.assign({ customSiteTitle: `${service.name} - Swagger UI` }, swaggerUiOptions)
router.use(mount, (req, _, next) => {
req.swaggerDoc = toOpenApiDoc(service, options)
next()
}, swaggerUi.serveFiles(), swaggerUi.setup(null, uiOptions))

addLinkToIndexHtml(service, apiPath)
})
return router
}

const cache = {}
function toOpenApiDoc (service, options = {}) {
if (!cache[service.name]) {
cache[service.name] = cds.compile.to.openapi(service.model, {
service: service.name,
'odata-version': options.odataVersion,
'openapi:url': join('/', options.apiPath, service.path),
'openapi:diagram': ('diagram' in options ? options.diagram : true),
to: 'openapi' // workaround needed for cds-dk 7.4
})
}
return cache[service.name]
}

function addLinkToIndexHtml (service, apiPath) {
const provider = (entity) => {
if (entity) return // avoid link on entity level, looks too messy
return { href: apiPath, name: 'Open API Preview', title: 'Show in Swagger UI' }
}
service.$linkProviders ? service.$linkProviders.push(provider) : service.$linkProviders = [provider]
}

/**
* @typedef {Object} CdsSwaggerOptions
* @property {string} basePath - the root path to mount the middleware on
* @property {string} apiPath - the root path for the services (useful if behind a reverse proxy)
* @property {boolean} diagram - whether to render the YUML diagram
* @property {string} odataVersion - the OData version used to compile the OpenAPI specs. Defaults to 4.01
*/
// important: this file must not be loaded when running as plugin ('auto' registration mode)
1 change: 1 addition & 0 deletions lib/etc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports.registered = Symbol.for('swaggerui_registered')
82 changes: 82 additions & 0 deletions lib/middleware.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
const cds = requireCdsOpenAPI()
const LOG = cds.log('swagger')

const swaggerUi = require('swagger-ui-express')
const express = require('express')
const { join } = require('path')

/**
* Creates express middleware to serve CDS services in the Swagger UI
*
* @param {CdsSwaggerOptions} options - options for cds-swagger-ui itself
* @param {swaggerUi.SwaggerUiOptions} swaggerUiOptions - options passed to swagger-ui-express
* @returns {express.RequestHandler} - an express middleware
*/
module.exports = (options={}, swaggerUiOptions={}) => {
options = Object.assign({ basePath: '/$api-docs', apiPath: '' }, options)

const router = express.Router()

cds.on('serving', service => {
const apiPath = options.basePath + service.path
const mount = apiPath.replace('$', '[\\$]')
LOG._debug && LOG.debug('serving Swagger UI for', { service: service.name, at: apiPath })

const uiOptions = Object.assign({ customSiteTitle: `${service.name} - Swagger UI` }, swaggerUiOptions)
router.use(mount, (req, _, next) => {
req.swaggerDoc = toOpenApiDoc(service, options)
next()
}, swaggerUi.serveFiles(), swaggerUi.setup(null, uiOptions))

addLinkToIndexHtml(service, apiPath)
})
return router
}

const cache = {}
function toOpenApiDoc (service, options = {}) {
if (!cache[service.name]) {
cache[service.name] = cds.compile.to.openapi(service.model, {
service: service.name,
'odata-version': options.odataVersion,
'openapi:url': join('/', options.apiPath, service.path),
'openapi:diagram': ('diagram' in options ? options.diagram : true),
to: 'openapi' // workaround needed for cds-dk 7.4
})
}
return cache[service.name]
}

function addLinkToIndexHtml (service, apiPath) {
const provider = (entity) => {
if (entity) return // avoid link on entity level, looks too messy
return { href: apiPath, name: 'Open API Preview', title: 'Show in Swagger UI' }
}
service.$linkProviders ? service.$linkProviders.push(provider) : service.$linkProviders = [provider]
}

/**
* Loads the compile.to.openapi function from @sap/cds-dk or throws an error if not installed.
* Will get simpler in the future when we have a dedicated openapi plugin.
*
* @returns { import('@sap/cds-dk') }
*/
function requireCdsOpenAPI () {
try {
return require('@sap/cds-dk')
} catch (err) {
const cds = require('@sap/cds')
if (!cds.compile.to.openapi) {
throw new Error(`'@sap/cds-dk' is not installed. Add it as a (dev) dependency to use this plugin`, { cause: err })
}
return cds
}
}

/**
* @typedef {Object} CdsSwaggerOptions
* @property {string} basePath - the root path to mount the middleware on
* @property {string} apiPath - the root path for the services (useful if behind a reverse proxy)
* @property {boolean} diagram - whether to render the YUML diagram
* @property {string} odataVersion - the OData version used to compile the OpenAPI specs. Defaults to 4.01
*/
Loading

0 comments on commit 07cc93e

Please sign in to comment.