Plugins for cds add
cds add
commands add project configuration to your CAP project.
Built-in
Many plugins are already part of @sap/cds-dk
, and all are implemented using the public APIs documented here. They provide you with a large set of standard features that support CAP's grow-as-you-go approach.
See the list of built-in add
plugins
Create a Plugin from Scratch
CAP provides APIs to create your own cds add
plugins. In addition, we provide you with utility functions for common tasks, to easily replicate the behavior of built-in commands.
Example: cds add postgres
In the following, we show you how to implement a cds add
plugin for PostgreSQL support.
Our cds add postgres
will:
- Register with
cds-dk
- Add a Dockerfile to start a PostgreSQL instance for development
- Integrate with
cds add mta
for Cloud Foundry deployment - Integrate with
cds add helm
for Kyma deployment - Integrate with
cds help
Starting with 1, register the plugin:
cds.add?.register?.('postgres', require('lib/add')) // ...or inline:
cds.add?.register?.('postgres', class extends cds.add.Plugin {})
In our example, we'll create a file lib/add.js:
const cds = require('@sap/cds')
module.exports = class extends cds.add.Plugin {
}
For step 2 we need to implement the run
method. Here we add all configuration that doesn't need integration with other plugins. In our example, we use this method to add a Docker configuration to the project to start the PostgreSQL instance locally:
const cds = require('@sap/cds-dk') //> load from cds-dk
const { write, path } = cds.utils, { join } = path
module.exports = class extends cds.add.Plugin {
async run() {
const pg = join(__dirname, 'add/pg.yaml')
await copy(pg).to('pg.yaml') //> 'to' is relative to cds.root
}
}
services:
db:
image: postgres:alpine
environment: { POSTGRES_PASSWORD: postgres }
ports: [ '5432:5432' ]
restart: always
Step 3 requires us to integrate with another cds add
command. Namely, we want cds add mta
to include PostgreSQL configuration when generating the mta.yaml deployment descriptor for Cloud Foundry. Vice versa, cds add postgres
should augment the mta.yaml if already present.
In this case, we can use the combine
method, which is executed when any cds add
command is run. This mechanism allows us to plug in accordingly.
We create an mta.yaml.hbs file to use as a template. The .hbs file also allows dynamic replacements using the Mustache syntax.
Using the merge
helper provided by the cds.add
API we can define semantics to merge this template into the project's mta.yaml
:
const cds = require('@sap/cds-dk') //> load from cds-dk
const { write, path } = cds.utils, { join } = path
const { readProject, merge, registries } = cds.add
const { srv4 } = registries.mta
module.exports = class extends cds.add.Plugin {
async run() {
const pg = join(__dirname, 'pg.yaml')
await copy(pg).to('pg.yaml')
}
async combine() {
const project = readProject()
const { hasMta, srvPath } = project
if (hasMta) {
const srv = srv4(srvPath) // Node.js or Java server module
const postgres = { in: 'resources',
where: { 'parameters.service': 'postgresql-db' }
}
const postgresDeployer = { in: 'modules',
where: { type: 'nodejs', path: 'gen/pg' }
}
await merge(__dirname, 'add/mta.yml.hbs').into('mta.yaml', {
project, // for Mustache replacements
additions: [srv, postgres, postgresDeployer],
relationships: [{
insert: [postgres, 'name'],
into: [srv, 'requires', 'name']
}, {
insert: [postgres, 'name'],
into: [postgresDeployer, 'requires', 'name']
}]
})
}
// if (hasHelm) {
// ...
// if (hasMultitenancy) {
// ...
}
}
modules:
- name: {{appName}}-srv
type: {{language}}
path: {{& srvPath}}
requires:
- name: {{appName}}-postgres
- name: {{appName}}-postgres-deployer
type: nodejs
path: gen/pg
parameters:
buildpack: nodejs_buildpack
no-route: true
no-start: true
tasks:
- name: deploy-to-postgresql
command: npm start
requires:
- name: {{appName}}-postgres
resources:
- name: {{appName}}-postgres
type: org.cloudfoundry.managed-service
parameters:
service: postgresql-db
service-plan: development
Step 4 integrates with cds add helm
:
const cds = require('@sap/cds-dk') //> load from cds-dk
const { copy, path } = cds.utils, { join } = path
const { readProject, merge, registries } = cds.add
const { srv4 } = registries.mta
module.exports = class extends cds.add.Plugin {
async run() {
const pg = join(__dirname, 'pg.yaml')
await copy(pg).to('pg.yaml')
}
async combine() {
const project = readProject()
const { hasMta, hasHelm, srvPath } = project
const { hasMta, srvPath } = project
if (hasMta) {
...
}
if (hasHelm) {
await merge(__dirname, 'add/values.yaml.hbs')
.into('chart/values.yaml', { with: project })
}
}
}
srv:
bindings:
db:
serviceInstanceName: postgres
postgres-deployer:
image:
repository: <your-container-registry>/{{appName}}-postgres-deployer
tag: latest
bindings:
postgres:
serviceInstanceName: postgres
postgres:
serviceOfferingName: postgres
servicePlanName: default
Common integrations
Typically integrations are for deployment descriptors (cds add mta
and cds add helm
), security descriptors (cds add xsuaa
), or changes that might impact your plugin configuration (cds add multitenancy
).
For step 5 we'll add some command-specific options to let users override the output path for the pg.yaml
file when running cds add postgres --out <dir>
:
const cds = require('@sap/cds-dk') //> load from cds-dk
const { copy, path } = cds.utils, { join } = path
module.exports = class extends cds.add.Plugin {
options() {
return {
'out': {
type: 'string',
short: 'o',
help: 'The output directory for the pg.yaml file.',
}
}
}
async run() {
const pg = join(__dirname, 'pg.yaml')
await copy(pg).to('pg.yaml') //> 'to' is relative to cds.root
await copy(pg).to(cds.cli.options.out, 'pg.yaml') //> 'to' is relative to cds.root
}
async combine() {
/* ... */
}
}
Call cds add
for an NPM package beta
Similar to npx -p
, you can use the --package/-p
option to directly install a package from an npm registry before running the command. This lets you invoke cds add
for CDS plugins easily with a single command:
cds add my-facet -p @cap-js-community/example
Install directly from your GitHub branch
For example, if your plugin's code is in https://github.com/cap-js-community/example
on branch cds-add
and registers the command cds add my-facet
, then doing an integration test of your plugin with @sap/cds-dk
in a single command:
cds add my-facet -p @cap-js-community/example@git+https://github.com/cap-js-community/example#cds-add
Plugin API
Find here a complete overview of public cds add
APIs.
register(name, impl)
Register a plugin for cds add
by providing a name and plugin implementation:
/* ... */
cds.add?.register?.('postgres',
class extends cds.add.Plugin {
async run() { /* ... */ }
async combine() { /* ... */ }
}
)
...or use the standard Node.js require
mechanism to load it from elsewhere:
cds.add?.register?.('postgres', require('./lib/add') )
run()
This method is invoked when cds add
is run for your plugin. In here, do any modifications that are not depending on other plugins and must be run once only.
async run() {
const { copy, path } = cds.utils, { mvn, readProject } = cds.add
await copy (path.join(__dirname, 'files/pg.yaml')).to('pg.yaml')
const { isJava } = readProject()
if (isJava) await mvn.add('postgres')
}
In contrast to
combine
,run
is not invoked when othercds add
commands are run.
combine()
This method is invoked, when cds add
is run for other plugins. In here, do any modifications with dependencies on other plugins.
These adjustments typically include enhancing the mta.yaml for Cloud Foundry or values.yaml for Kyma, or adding roles to an xs-security.json.
async combine() {
const { hasMta, hasXsuaa, hasHelm } = readProject()
if (hasMta) { /* adjust mta.yaml */ }
if (hasHelm) { /* adjust values.yaml */ }
if (hasXsuaa) { /* adjust xs-security.json */ }
}
options()
The options
method allows to specify custom options for your plugin:
options() {
return {
'out': {
type: 'string',
short: 'o',
help: 'The output directory. By default the application root.',
}
}
}
We follow the Node.js util.parseArgs
structure, with an additional help
field to provide manual text for cds add help
.
Run cds add help
to validate...
You should now see output similar to this:
$ cds help add SYNOPSIS ··· OPTIONS ··· FEATURE OPTIONS ··· cds add postgres --out | -o The output directory. By default the application root.
See if your command can do without custom options
cds add
commands should come with carefully chosen defaults and avoid offloading the decision-making to the end-user.
requires()
The requires
function allows to specify other plugins that need to be run as a prerequisite:
requires() {
return ['xsuaa'] //> runs 'cds add xsuaa' before plugin is run
}
Use this feature sparingly
Having to specify hard-wired dependencies could point to a lack of coherence in the plugin.
Utilities API
readProject()
This method lets you retrieve a project descriptor for the productive environment.
const { isJava, hasMta, hasPostgres } = cds.add.readProject()
Any plugin provided by cds add
can be availability-checked. The readable properties are prefixed by has
or is
, in addition to being converted to camel-case. A few examples:
facet | properties |
---|---|
java | hasJava or isJava |
hana | hasHana or isHana |
html5-repo | hasHtml5Repo or isHtml5Repo |
... | ... |
merge(from).into(file, o?)
CAP provides a uniform convenience API to simplify merging operations on the most typical configuration formats — JSON and YAML files.
For YAML in particular, comments are preserved
cds.add.merge
can perform AST-level merging operations. This means, even comments in both your provided template and the user YAML are preserved.
A large number of merging operations can be done without specifying additional semantics, but simply specifying from
and file
:
const config = { cds: { requires: { db: 'postgres' } } }
cds.add.merge(config).into('package.json')
Semantic-less mode merges and de-duplicates flat arrays
Consider this source.json
and target.json
:
// source.json
{
"my-plugin": {
"x": "value",
"z": ["a", "b"]
}
}
// target.json
{
"my-plugin": {
"y": "value",
"z": ["b", "c"]
}
}
A cds.add.merge('source.json').into('target.json')
produces this result:
// target.json
{
"my-plugin": {
"x": "value",
"y": "value",
"z": ["b", "c"]
"z": ["a", "b", "c"]
}
}
We can also specify options for more complex merging semantics or Mustache replacements:
const { merge, readProject, registries } = cds.add
// Generic variants for maps and flat arrays
await merge(__dirname, 'lib/add/package-plugin.json').into('package.json')
await merge({ some: 'variable' }).into('package.json')
// With Mustache replacements
const project = readProject()
await merge(__dirname, 'lib/add/package.json.hbs').into('package.json', {
with: project
})
// With Mustache replacements and semantics for nested arrays
const srv = registries.mta.srv4(srvPath)
const postgres = {
in: 'resources',
where: { 'parameters.service': 'postgresql-db' }
}
const postgresDeployer = {
in: 'modules',
where: { type: 'nodejs', path: 'gen/pg' }
}
await merge(__dirname, 'lib/add/mta.yml.hbs').into('mta.yaml', {
with: project,
additions: [srv, postgres, postgresDeployer],
relationships: [{
insert: [postgres, 'name'],
into: [srv, 'requires', 'name']
}, {
insert: [postgres, 'name'],
into: [postgresDeployer, 'requires', 'name']
}]
})
.registries
cds.add
provides a default registry of common elements in configuration files, simplifying the merging semantics specification:
const { srv4, approuter } = cds.add.registries.mta
...and use it like this:
const project = readProject()
const { hasMta, srvPath } = project
if (hasMta) {
const srv = registries.mta.srv4(srvPath)
const postgres = {
in: 'resources',
where: { 'parameters.service': 'postgresql-db' }
}
await merge(__dirname, 'lib/add/mta.yml.hbs').into('mta.yaml', {
project,
additions: [srv, postgres, postgresDeployer],
relationships: [
...
]
})
}
mvn.add()
For better Java support, plugins can easily invoke mvn com.sap.cds:cds-maven-plugin:add
goals using mvn.add
:
async run() {
const { isJava } = readProject()
const { mvn } = cds.add
if (isJava) await mvn.add('postgres')
}
Checklist for Production
Key to the success of your cds add
plugin is seamless integration with other technologies used in the target projects. As CAP supports many technologies out of the box, consider the following when reasoning about the scope of your minimum viable product:
- Single- and Multitenancy
- Node.js and Java runtimes
- Cloud Foundry (via MTA)
- Kyma (via Helm)
- App Router
- Authentication
Best Practices
Adhere to established best practices in CAP-provided plugins to ensure your plugin meets user expectations.
Consider cds add
vs cds build
In contrast to cds build
, cds add
is concerned with source files outside of your gen folder. Common examples are deployment descriptors such as mta.yaml for Cloud Foundry or values.yaml for Kyma deployment. Unlike generated files, those are usually checked in to your version control system.
Don't do too much work in cds add
If your cds add
plugin creates or modifies a large number of files, this can be incidental for high component coupling. Check if configuration for your service can be simplified and provide sensible defaults. Consider generating the files in a cds build
plugin instead.
Embrace out-of-the-box
From a consumer point of view, your plugin is integrated by adding it to the package.json dependencies
and provides sensible default configuration without further modification.
Embrace grow-as-you-go and separate concerns
A strength of cds add
is the gradual increase in project complexity. All-in-the-box templates pose the danger of bringing maintainability and cost overhead by adding stuff you might not need. Decrease dependencies between plugins wherever possible.