Skip to Content
Technical Articles
Author's profile photo Daniel Schlachter

Reusable components for CAP with cds-plugin

Here is a new concept that is going to revolutionise CAP app development =)

Let me first give you an example and then explain why it matters:

Introduction:

I have a business requirement to add a new feature to my CAP application. Some of the strings that my CAP application delivers should have a random emoji added at the end.

So if we look at our typical bookshop example, the title of the book would be appended by a random emoji whenever we fetch the data:

emojification

Now usually what you would do is write a custom handler directly in your app, which adds the emojis.

If another project later on wants to have the same functionality, you copy that custom handler around. If something needs to be changed in the implementation you now have to maintain the same code in multiple places.

But what if we would have this feature in a truly reusable fashion, cleanly separated away from the rest of your code?

This is where cds-plugin comes in!

End-to-End example:

Create the base project

Lets create a new project with executing this in the terminal:

cds init && cds add samples
  • With cds initΒ  we create a new project
  • With cds add samples we add a sample database model, service definition and some sample data. This is our bookshop example

Create the plugin

Now let’s create a plugin that extends the functionality of this base application.

Create a folder emojiPlugin and run npm init -y in that folder:

mkdir emojiPlugin && cd emojiPlugin && npm init -y && cd ..

This is going to create a package.json file in the new folder.

Create a new file emojiPlugin/cds-plugin.js

As a first version of our plugin, we will just show a simple log message. Add this as content in the cds-plugin.js file:

console.log('my awesome plugin')

This is where the magic happens: If we set a dependency in the package.json of the main project to our emojiPlugin, cds will go and look for cds-plugin.js and execute the content.

So let’s set this dependency.

Wire the plugin to the base app

In the package.json in the main folder (not the plugin folder!) we add a dependency to our emojiPlugin and we also define emojiplugin as an npm workspace

{
  ...,
  "workspaces": [
    "emojiplugin"
  ],
  "dependencies": {
    ...
    "emojiplugin": "*"
  },
  ...
}

Now all that is needed is to run npm i to install the dependencies. Afterwards run the cds server locally with cds w

npm i && cds w

You should see something like this in the console:

cds serve all --with-mocks --in-memory? 
live reload enabled for browsers 

        ___________________________
 
my awesome plugin
[cds] - loaded plugin: { impl: 'emojiplugin/cds-plugin' }
[cds] - loaded model from 2 file(s):

  db/data-model.cds
  srv/cat-service.cds

We can see the my awesome pluginoutput from our cds-plugin.js file followed by [cds] - loaded plugin: { impl: 'emojiplugin/cds-plugin' }which confirms that the plugin was loaded. Congratulations! You have successfully created your first cds plugin!

But where are the emojis?

So far we have only printed a console statement with our plugin. But we wanted emojis!

In theΒ srv/cat-service.cds add the following at the end of the file:

annotate CatalogService.Books with {
  title @randomEmoji;
};

We made up our own annotation here, called Β @randomEmoji. What we want to achieve is that any field that has this annotation is affected by our plugin.

Now we are going to add the implementation to our plugin, which will look for this annotation and append the emoji. In the emojiPlugin/cds-plugin.js file replace the console.log statement with the following:

const cds = require('@sap/cds')
//most important --> define emojis!
const emojis = ['', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '']
//our business logic...
function getRandomEmoji() {
  return emojis[Math.floor(Math.random() * emojis.length)]
}
// we register ourselves to the cds once served event
// a one-time event, emitted when all services have been bootstrapped and added to the express app
cds.once('served', () => {
  // go through all services
  for (let srv of cds.services) {
    // go through all entities
    for (let entity of srv.entities) {
      // go through all elements in the entity and collect those with @randomEmoji annotation
      const emojiElements = []
      for (const key in entity.elements) {
        const element = entity.elements[key]
        // check if there is an annotation called randomEmoji on the element
        if (element['@randomEmoji']) emojiElements.push(element.name)
      }
      if (emojiElements.length) {
        // register a new handler on the service, that is called on every read operation
        srv.after('READ', entity, data => {
          if (!data) return
          // if we read a single entry, we don't get an array of data, so let's make sure we deal with an array
          let myData = Array.isArray(data) ? data : [data]
          // go through all query read results (in this case the books)
          for (let entry of myData) {
            for (const element of emojiElements) {
              if (entry[element]) {
                entry[element] += getRandomEmoji()
              }
            }
          }
        })
      }
    }
  }
})
As the code block above keeps replacing my emojis, please copy this into the sample above:
const emojis = [“πŸ˜€”, “πŸ˜ƒ”, “πŸ˜„”, “😁”, “πŸ˜†”, “πŸ˜…”, “πŸ˜‚”, “🀣”, “πŸ₯²”, “πŸ₯Ή”, “😊”, “πŸ˜‡”, “πŸ™‚”, “πŸ™ƒ”, “πŸ˜‰”, “😌”, “😍”]

What we’ve learned

  • Plugins can encapsulate functionality and separate it away from the application
  • They are easily reusable in other apps
  • We can hook ourselves into cds standard events like once.served and register event handlers

Why I think this is a game changer:

  • Having a clearly defined approach for developing and consuming plugins is great
  • Open Source Contributions – Developers or teams can contribute their own plugins
  • We are also using this approach ourselves already! See for example the graphql adapter
  • Also, we are looking into making more BTP services easier to consume from CAP side. Those service integrations could be developed as a plugin by the teams, which develop this service

The small print

We are consuming the npm package locally from our file system. But this could of course be a published npm package or linked via npm link. When deploying the application, we need to make sure that this dependency is also resolved correctly.

In a multi tenancy scenario with extensibility the plugin would be loaded before the extensibility is applied, so the example above would need to be written differently (otherwise the annotation is added AFTER the plugin was loaded and is not considered)

 

 

Assigned Tags

      8 Comments
      You must be Logged on to comment or reply to a post.
      Author's profile photo Jason Scott
      Jason Scott

      Awesome post and a great feature. I’m wondering if you can expand a little on the differences to the other reuse methods documented in the CAP Reuse & Compose cookbook. Is this plugin mechanism a β€œbetter” way now or when would you most likely use each technique?

      Author's profile photo Daniel Schlachter
      Daniel Schlachter
      Blog Post Author

      Thank you for the feedback! I would see all the options described in the cookbook you mentioned as a toolbox, from which you choose the right tool(s) for you. cds-plugin is an additional tool in that box. Similar to the server.js in the main project you can have a cds-plugin.js in the plugin folder.

      Author's profile photo Daniel Hutzel
      Daniel Hutzel

      Greatly explained Daniel πŸ‘

      Author's profile photo Sreehari V Pillai
      Sreehari V Pillai

      thanks for this. I wrote a blog on similar topic . Also , the build of MTA failed for me. I had to copy the plugin directory to the gen folder with build script. Have you faced the same error ? if yes - How did you fix it.

      Sree

      Author's profile photo Daniel Schlachter
      Daniel Schlachter
      Blog Post Author

      Hi Sree,

      there is a workaround for this at the moment, if the name in package.json is @capire/... it will be resolved correctly during deployment.

      Unfortunately, the regular approach of resolving npm workspace dependencies does not work directly.

      Thanks

      Daniel

      Author's profile photo Sreehari V Pillai
      Sreehari V Pillai

      sure . I added a script for this .Thanks.

      Author's profile photo Merve GΓΌl
      Merve GΓΌl

      Hi Daniel,

      Thank you for the post and explaining the feature to us!

      Would that be possible to share this example in a Github repo?

      Thanks in advance,

      Merve

      Author's profile photo Sreehari V Pillai
      Sreehari V Pillai

      Not in repo , but worth a read here

      S