Search

    Reuse, Compose, and Integrate

    Learn here how to compose enhanced, verticalized solutions by reusing content from other projects, and adapt it to your needs by adding extensions or projections.

    Content

    Introduction & Overview

    CAP promotes reuse and composition by importing content from reuse packages. Reuse content, shared and imported that way, can comprise models, code, initial data, and i18n bundles.

    Usage Scenarios

    By applying CAP’s techniques for reuse, composition and integration, one can address several different usage scenarios, as depicted in the following illustration.

    Scenarios

    1. Verticalized/Composite Solutions — Pick one or more reuse packages/services. Enhance them, mash them up into a composite solution, and offer this as a new packaged solution to clients.

    2. Prebuilt Extension Packages — instead of offering a new packaged solution, you could also just provide your enhancements as a prebuilt extension package, for example, for verticalization, which you in turn offer to others as a reuse package.

    3. Prebuilt Integration Packages — prebuilt extension packages could also involve prefabricated integrations to services in backend systems, like S/4HANA and SuccessFactors.

    4. Prebuilt Business Data Packages — a variant of 3, in which you would provide a reuse package, that just provides initial data for certain entities, like a list of Languages, Countries, Regions, Currencies, and more.

    5. Customizing SaaS Solutions — customers, that are subscribers of SaaS solutions, can apply the same techniques to adapt SaaS solutions to their needs. They can use prebuilt extension or business data packages, or create their own custom-defined ones.

    Examples from cap/samples

    In the upcoming sections, we frequently refer to examples from cap/samples:

    composites

    • @capire/common is a prebuilt extension and business data package for Countries, Currencies, and Languages.
    • @capire/bookshop provides a basic bookshop app and reuse services .
    • @capire/reviews provides an independent reuse service.
    • @capire/orders provides another independent reuse service.
    • @capire/fiori combines all of the above into a composite application.

    Prepare for Subsequent Exercises

    If you like to exercise the code snippets in following sections, do that:

    1)   Get cap/samples and have them served through a local mock npm registry:

    git clone https://github.com/sap-samples/cloud-cap-samples samples
    cd samples
    npm install
    npm run registry
    

    Keep that running while running npm commands referring to @capire/... packages.

    2)   Start a sample project somewhere in a second terminal:

    cds init sample
    cd sample
    npm i
    # ... run the upcoming commands in here
    

    Importing Reuse Packages

    CAP and CDS promote reuse of prebuilt content based on npm techniques. The following figure depicts the basic procedure.

    We use npm as a package manager simply because we didn’t want to reinvent wheels here. We might also support other package managers in future, such as Maven for Java projects. Until then, we also use npm for Java projects.

    Using npm add/install from NPM Registries

    Use npm add/install to import reuse packages to your project, like so:

    npm add @capire/bookshop @capire/common
    

    npm add is an alias for npm install → see npm docs

    This installs the content of these packages into your project’s node_modules folder and adds corresponding dependencies to your package.json:

    {
      "name": "sample", "version": "1.0.0",
      "dependencies": {
        "@capire/bookshop": "^1.0.0",
        "@capire/common": "^1.0.0",
        ...
      }
    }
    

    These dependencies allow you to use npm outdated, npm update, and npm install later to get the latest versions of imported packages.

    Importing from Other Sources

    Instead of importing from NPM registries, you can also import from local sources. This can be other CAP projects, you’ve access to or tarballs of reuse packages, for example, downloaded from some marketplace.

    npm add ~/Downloads/@capire-bookshop-1.0.0.tgz
    npm add ../bookshop
    

    You can use npm pack to create tarballs from your projects in case you want to share them with others.

    Embedding vs. Integrating Reuse Services

    By default, when importing reuse packages, all imported content becomes an integral part of your project, it literally becomes embedded into your project. This applies to all the things an imported package can contain, such as:

    • Domain models
    • Service definitions
    • Service implementations
    • i18n bundles
    • Initial data

    Learn more about initial data handling.

    Yet, you decide which parts to actually use and activate in your project by means of model references as shown in the following sections.

    Instead of embedding reuse content, you can also integrate with remote services, deployed as separate micro services as outlined in section Service Integration.

    Reusing Imported Models

    Although all important content is embedded into your project, you decide which parts to actually use and activate by means of model references. For example, if an imported package would come with three service definitions, it’s still you, who decides which of those to serve as part of your app, if any. The rule is:

    Active by Reachability Everything you are referring to from your own models is served. Everything outside of your models is ignored.

    Via using from Directives

    Use the definitions from imported models through using directives as usual. For example like so to simply add all:

    using from '@capire/bookshop';
    using from '@capire/common';
    

    The cds compiler finds the imported content in node_modules when processing imports with absolute targets as shown previously.

    Using index.cds Entry Points

    The above using from statements assume that the imported packages provide index.cds in their roots as public entry points, which they do. For example see @capire/bookshop/index.cds:

    // exposing everything...
    using from './db/schema';
    using from './srv/cat-service';
    using from './srv/admin-service';
    

    This index.cds imports and therefore activates everything. Running cds watch in your project would show you this log output, indicating that all initial data and services from your imported packages are now embedded and served from your app:

    [cds] - connect to db > sqlite { database: ':memory:' }
     > filling sap.common.Currencies from common/data/sap.common-Currencies.csv
     > filling sap.common.Currencies_texts from common/data/sap.common-Currencies_texts.csv
     > filling sap.capire.bookshop.Authors from bookshop/db/data/sap.capire.bookshop-Authors.csv
     > filling sap.capire.bookshop.Books from bookshop/db/data/sap.capire.bookshop-Books.csv
     > filling sap.capire.bookshop.Books_texts from bookshop/db/data/sap.capire.bookshop-Books_texts.csv
     > filling sap.capire.bookshop.Genres from bookshop/db/data/sap.capire.bookshop-Genres.csv
    /> successfully deployed to sqlite in-memory db
    
    [cds] - serving AdminService { at: '/admin', impl: 'bookshop/srv/admin-service.js' }
    [cds] - serving CatalogService { at: '/browse', impl: 'bookshop/srv/cat-service.js' }
    

    Using Different Entry Points

    In case you don’t want everything but only a part, you can change your using from directives like that:

    using { CatalogService } from '@capire/bookshop/srv/cat-service';
    

    With that, the output of cds watch would reduce to:

    [cds] - connect to db > sqlite { database: ':memory:' }
     > filling sap.capire.bookshop.Authors from bookshop/db/data/sap.capire.bookshop-Authors.csv
     > filling sap.capire.bookshop.Books from bookshop/db/data/sap.capire.bookshop-Books.csv
     > filling sap.capire.bookshop.Books_texts from bookshop/db/data/sap.capire.bookshop-Books_texts.csv
     > filling sap.capire.bookshop.Genres from bookshop/db/data/sap.capire.bookshop-Genres.csv
    /> successfully deployed to sqlite in-memory db
    
    [cds] - serving CatalogService { at: '/browse', impl: 'bookshop/srv/cat-service.js' }
    

    Only the CatalogService is served now.

    Check the readme files coming with reuse packages about which entry points are safe to be used.

    Using and Extending Imported Definitions

    You can freely use all definitions from the imported models in the same way as you use definitions from your own models. This includes using declared types, adding associations to imported entities, building views on top of imported entities, and so on.

    You can even extend imported definitions, for example, add elements to imported entities, or add / override annotations, without limitations.

    Here’s an example from the @capire/fiori/srv/mashup.cds:

    using { sap.capire.bookshop.Books } from '@capire/bookshop';
    using { ReviewsService.Reviews } from '@capire/reviews';
    
    // Extend Books with access to Reviews and average ratings
    extend Books with {
      reviews : Composition of many Reviews on reviews.subject = $self.ID;
      rating  : Decimal;
    }
    

    Reusing Imported Code

    Also service implementations, in particular custom-coding is imported and served in embedding projects. Follow the instructions, if you need to add additional custom handlers.

    In Node.js

    One way to add own implementations is to replace the service implementation as follows:

    1. Add/override the @impl annotation
      using { CatalogService } from '@capire/bookshop';
      annotate CatalogService with @impl:'srv/my-cat-service-impl';
      
    2. Place your implementation in srv/my-cat-service-impl.js:
      module.exports = cds.service.impl (function(){
       this.on (...) // add your event handlers
      })
      
    3. In case the imported package already had a custom implementation, you can include that as follows:
      const base_impl = require ('@capire/bookshop/srv/cat-service')
      module.exports = cds.service.impl (async function(){
        this.on (...) // add your event handlers
        await base_impl.call (this,this)
      })
      

      Ensure to invoke the base implementation exactly like that, with await. And ensure to check the imported package’s readme, whether access to that implementation modules is safe.

    In Java

    PENDING…

    Reusing Imported UIs

    In case imported packages provide UIs, you can also serve them as part of your app — for example, using standard express.js middleware means in Node.js.

    Example 1: a Vue.js UI

    The @capire/fiori app has this in its server.js to serve the Vue.js app imported with @capire/bookshop:

    const express = require('express')
    const cds = require('@sap/cds')
    
    cds.once('bootstrap',(app)=>{
      const {dirname} = require('path')
      // serving the vue.js app imported from @capire/bookshop
      const vue_app = dirname (require.resolve('@capire/bookshop/app/vue/index.html'))
      app.use ('/vue', express.static(vue_app))
    })
    
    ...
    module.exports = cds.server
    

    More about Vue.js in our Getting Started in a Nutshell This ensures all static content for the app is served from the imported package.

    Example 2: a Fiori UI

    The same technique is applied to serve static content for a Fiori app, also the server.js of @capire/fiori:

    ...
      // serving the orders app imported from @capire/orders
      const orders_app = dirname (require.resolve('@capire/orders/app/orders/webapp/manifest.json'))
      app.use ('/orders/webapp', express.static(orders_app))
    ...
    

    Learn more about serving Fiori UIs.

    In both cases, all dynamic requests to the service endpoint anyways reach the embedded service, which is automatically served at the same endpoint it was served in bookshop.

    Service Integration

    Instead of embedding and serving imported services as part of your application, you can decide to integrate with them, having them deployed and run as separate Micro Services.

    Subsections of this section:

    Import Remote Service’s APIs

    This is described in the Import Reuse Packages section → using npm add.

    Here’s the effect of this step in @capire/fiori/package.json:

      "dependencies": {
        "@capire/bookshop": "^1.0.0",
        "@capire/reviews": "^1.0.0",
        "@capire/orders": "^1.0.0",
        "@capire/common": "^1.0.0",
        ...
      },
    

    Configuring Required Services

    To configure required remote services in Node.js, simply add respective entries to the cds.requires config option. You can see an example again in @capire/fiori/package.json, which integrates @capire/reviews and @capire/orders as remote service:

    "cds": {
      "requires": {
        "ReviewsService": {
          "kind": "odata", "model": "@capire/reviews"
        },
        "OrdersService": {
          "kind": "odata", "model": "@capire/orders"
        },
      }
    }
    

    Essentially, this tells the service loader to not serve that service as part of your application, but expects a service binding at runtime in order to connect to the external service provider.

    Restricted Reuse Options

    As models of integrated services serve as imported APIs only, you’re restricted with respect to how you can use the models of services to integrate with. For example, adding fields is only possible, or cross-service navigation and expands.

    Yet, there are options to make some of such things work programmatically. This is explained in the next section based on the integration of @capire/reviews in @capire/fiori.

    Delegating Calls to Remote Services

    Starting out from that use case: The bookshop app exposed through @capire/fiori shall allow end users to see the top 10 reviews in the details page on books.

    To avoid CORS issues, the request from the UI goes to the main CatalogService serving the end users UI and is delegated from that to the remote ReviewsService, as depicted in this sequence diagram:

    delegate-requests

    And this is how we do that in @cap/fiori/srv/mashup.js:

    const CatalogService = await cds.connect.to ('CatalogService')
    const ReviewsService = await cds.connect.to ('ReviewsService')
    CatalogService.prepend (srv => srv.on ('READ', 'Books/reviews', (req) => {
      console.debug ('> delegating request to ReviewsService')
      const [id] = req.params, { columns, limit } = req.query.SELECT
      return ReviewsService.tx(req).read ('Reviews',columns).limit(limit).where({subject:String(id)})
    }))
    

    Investigating that step by step:

    1. We connect to both, the CatalogService (local) and the ReviewsService (remote) to mash them up as follows…
    2. We register an .on handler with the CatalogService, which delegates the incoming request to the ReviewsService
    3. We wrap that into a call to .prepend as the .on handler needs to supersede the default generic handlers provided by the CAP runtime → see ref docs for srv.prepend.

    Running with Mocked Remote Services

    If you start @capire/fiori locally with cds watch all required services are automatically mocked, as you can see in the log output on server start:

    [cds] - serving AdminService { at: '/admin', impl: 'bookshop/srv/admin-service.js' }
    [cds] - serving CatalogService { at: '/browse', impl: 'bookshop/srv/cat-service.js' }
    [cds] - mocking OrdersService { at: '/orders', impl: 'orders/srv/orders-service.js' }
    [cds] - mocking ReviewsService { at: '/reviews', impl: 'reviews/srv/reviews-service.js' }
    

    → OrdersService and ReviewsService are mocked, that is served in the same process, in the same way as the local services.

    This allows developing and testing functionality with minimum complexity and overhead in fast, close-loop dev cycles.

    As all services are co-located in the same process, sharing the same database, you can send requests like that, which join/expand across Books and Reviews:

    GET http://localhost:4004/browse/Books/201?
    &$expand=reviews
    &$select=ID,title,rating
    

    Testing Remote Integration Locally

    As a next step, following CAP’s Grow-as-you-go philosophy, we can run the services as separate processes to test the remote integration, but still locally in a low-complexity setup. We use the automatic binding by cds watch as follows:

    1. Start the three servers separately, each in a separate shell (from within the root folder in your cloned cap/samples project):
      cds watch orders --port 4006
      
      cds watch reviews --port 4005
      
      cds watch fiori --port 4004
      
    2. Send a few requests to the reviews service (port 4005) to add Reviews:
      POST http://localhost:4005/Reviews
      Content-Type: application/json;IEEE754Compatible=true
      Authorization: Basic itsme:secret
      {"subject":"201", "title":"boo", "rating":3 }
      
    3. Send a request to bookshop (port 4004) to fetch reviews via CatalogService:
      GET http://localhost:4004/browse/Books/201/reviews?
      &$select=rating,date,title
      &$top=3
      

    Find a script for that in @capire/fiori/test/requests.http.

    Binding Required Services

    Service bindings provide the details about how to reach a required service at runtime, that is, providing requisite credentials, most prominently the target service’s url.

    Basic Mechanism Using cds.env and Process env Variables

    At the end of the day, the CAP Node.js runtime expects to find the service bindings in the respective entries in cds.env.requires:

    1. Configured required services constitute end points for service bindings:

      "cds": {
        "requires": {
       "ReviewsService": {...},
        }
      }
      
    2. These are made available to the runtime via cds.env.requires.

      const { ReviewsService } = cds.env.requires
      
    3. Service Bindings essentially fill in credentials to these entries.

      const { ReviewsService } = cds.env.requires
      ReviewsService.credentials = {
        url: "http://localhost:4005/reviews"
      }
      

    While you could do the latter in test suites, you would never ever provide credentials in a hard-coded way like that in productive code, of course. Instead, you’d use one of the options presented in the following sections.

    Automatic Bindings by cds watch

    When running separate services locally as described in the former section, this was done automatically by cds watch, as indicated by this line in the bootstrapping log output:

    [cds] - using bindings from: { registry: '~/.cds-services.json' }
    

    You can cmd/ctrl-click or double click on that to see this file’s content, and find something like that:

    {
      "cds": {
        "provides": {
          "OrdersService": {
            "kind": "odata",
            "credentials": {
              "url": "http://localhost:4006/orders"
            }
          },
          "ReviewsService": {
            "kind": "odata",
            "credentials": {
              "url": "http://localhost:4005/reviews"
            }
          },
          "AdminService": {
            "kind": "odata",
            "credentials": {
              "url": "http://localhost:4004/admin"
            }
          },
          "CatalogService": {
            "kind": "odata",
            "credentials": {
              "url": "http://localhost:4004/browse"
            }
          }
        }
      }
    }
    

    Whenever you start a CAP server with cds watch, this is what happens automatically:

    1. for all provided services, corresponding entries are written to ~/cds-services.json with respective credentials, namely the url.

    2. for all required services, corresponding entries are fetched from ~/cds-services.json . If found, the credentials are filled into the respective entry in cds.env.requires.<service> as introduced previously.

    In effect, all the services you start locally in separate processes automatically receive their required bindings and hence are able to talk to each other out-of-the-box.

    Through Process Environment Variables

    You could pass credentials as process environment variables, for example in ad-hoc tests from the command line:

    export cds_requires_ReviewsService_credentials_url=http://localhost:4005/reviews
    cds watch fiori
    

    … or add them to a local .env file for repeated local tests:

    cds.requires.ReviewsService.credentials = { "url": "http://localhost:4005/reviews" }
    

    Note: never check in or deploy such .env files!

    Through VCAP_SERVICES

    When deploying to Cloud Foundry, service bindings are provided in VCAP_SERVICES process environment variables as documented here.

    In Target Cloud Platforms

    Find information about how to do so in different environment under these links:

    Providing Reuse Packages

    In general, every CAP-based product can serve as a reuse package consumed by others. There’s actually not much to be cared for. Just create models and implementations as usual. The following sections are about additional things to consider, as a provider of a reuse package.

    Provide Public Entry Points

    Following the Node.js approach, there’s no public/private mechanism in CDS. Instead, it’s good and proven practice to add an index.cds in the root folder of reuse packages, similar to the use of index.js files in Node.

    For example:

    namespace my.reuse.package;
    using from './db/schema';
    using from './srv/cat-service';
    using from './srv/admin-service';
    

    This allows your users to refer to your models in using directives using just the package name, like so:

    using { my.thing } from 'my-reuse-package';
    

    In addition, you might want to provide other entry points to ease partial usage options. For example, you could provide a schema.cds file in your root, to allow using the domain model without services:

    using { my.domain.entity } from 'my-reuse-package/schema';
    using { my.service } from 'my-reuse-package/services';
    

    Provide Custom Handlers

    In general custom handlers can be placed in files matching the naming of the .cds files they belong to. In a reuse package, you have to use the @impl annotation to make it explicit which custom handler to use. In addition you need to use the fully qualified module path inside the @impl annotation.

    Imagine that our bookshop is an @sap-scoped reuse module and the CatalogService has a custom handler. This is how the service definition would look like:

    service CatalogService @(impl: '@sap/bookshop/srv/cat-service.js') {...}
    

    Add a Readme

    You should inform potential consumers about the best and recommended ways to reuse content provided by your package. At least you provide information about:

    • What is provided – schemas, services, data, and so on
    • What are the recommended, stable entry points

    Publish/Share with Consumers

    The preferred way to share reuse packages is by publishing to NPM registries, like npmjs.org, or pkg.github.com. This allows consumers to apply semver-based version management.

    However, at the end of the day, any other way to share package tarballs, which you create with npm pack would work as well.

    Customizing SaaS Usage

    Customers, that are subscribers of SaaS solutions, can use the same reuse and extend techniques to customize their usage of SaaS applications, for example, by:

    • Adding/overriding annotations
    • Adding custom fields and entities
    • Adding custom data
    • Adding custom i18n bundles
    • Importing prebuilt extension packages

    The main difference is how and from where the import happens:

    1. The reuse package, in this case is the subscribed SaaS application.
    2. The import happens via cds extend.
    3. The imported package is always named _base.
    4. The extensions are applied via cds activate.

    Learn more on that in the SaaS Extensibility guide.

    Show/Hide Beta Features