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)

 

Sara Sampaio

Sara Sampaio

Author Since: March 10, 2022

0 0 votes
Article Rating
Subscribe
Notify of
0 Comments
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x