Skip to Content
Technical Articles

Taking CAP to the next level – with TypeScript

Many of you may have heard about the new SAP Cloud Application Programming Model, CAP for short. It is the go-to programming model for SAP cloud-based applications. And it’s great.

CAP relies not only on Java but also on JavaScript or Node.js. Node.js is also very common in cloud development. But as the projects get bigger and bigger, the weak typing of JavaScript can be challenging. Here TypeScript comes into play to get a stronger typing. TypeScript is great and I love it.

So why not just combine these two great things together?

From this idea my colleague Michael Baudler created two modules, which allow you to combine TypeScript and CAP without any problems.

Let me introduce cds2tpes and cds-routing-handlers.
-> GitHub Demo

cds2types

cds2types is a tool that creates typescript interfaces and enums from the CDS definitions. This means that the entities defined by CDS can also be used fully typed in your TypeScript code.

I just show an example using the Bookshop demo.

cds2types can be easily installed via npm or yarn:

$ npm install --save-dev cds2types

Let’s look at a CDS example:

// schema.cds
using { Currency, managed, sap } from '@sap/cds/common';
namespace sap.capire.bookshop;
 
entity Books : managed {
  key ID : Integer;
  title  : localized String(111);
  descr  : localized String(1111);
  author : Association to Authors;
  genre  : Association to Genres;
  stock  : Integer;
  price  : Decimal(9,2);
  currency : Currency;
}
 
entity Authors : managed {
  key ID : Integer;
  name   : String(111);
  dateOfBirth  : Date;
  dateOfDeath  : Date;
  placeOfBirth : String;
  placeOfDeath : String;
  books  : Association to many Books on books.author = $self;
}
 
/** Hierarchically organized Code List for Genres */
entity Genres : sap.common.CodeList {
  key ID   : Integer;
  parent   : Association to Genres;
  children : Composition of many Genres on children.parent = $self;
}

// service.cds
using { sap.capire.bookshop as my } from './schema';
service CatalogService @(path:'/browse') {
 
  @readonly entity Books as SELECT from my.Books {*,
    author.name as author
  } excluding { createdBy, modifiedBy };
 
  @requires_: 'authenticated-user'
  action submitOrder (book : Books.ID, amount: Integer);
}

Now when we run the CLI:

$ cds2types --cds ./service.cds --output ./service.ts --prefix I

We get the following output:

export namespace sap.capire.bookshop {
    export interface IAuthors extends IManaged {
        ID: number;
        name: string;
        dateOfBirth: Date;
        dateOfDeath: Date;
        placeOfBirth: string;
        placeOfDeath: string;
        books?: IBooks[];
    }
    export interface IBooks extends IManaged {
        ID: number;
        title: string;
        descr: string;
        author?: IAuthors;
        author_ID?: number;
        genre?: IGenres;
        genre_ID?: number;
        stock: number;
        price: number;
        currency: unknown;
        currency_code?: string;
    }
    export interface IGenres extends sap.common.ICodeList {
        ID: number;
        parent?: IGenres;
        parent_ID?: number;
        children: unknown;
    }
    export enum Entity {
        Authors = "sap.capire.bookshop.Authors",
        Books = "sap.capire.bookshop.Books",
        Genres = "sap.capire.bookshop.Genres",
    }
    export enum SanitizedEntity {
        Authors = "Authors",
        Books = "Books",
        Genres = "Genres",
    }
}
export namespace CatalogService {
    export enum ActionSubmitOrder {
        name = "submitOrder",
        paramBook = "book",
        paramAmount = "amount",
    }
    export interface IActionSubmitOrderParams {
        book: unknown;
        amount: number;
    }
    export interface IBooks {
        createdAt?: Date;
        modifiedAt?: Date;
        ID: number;
        title: string;
        descr: string;
        author: string;
        genre?: IGenres;
        genre_ID?: number;
        stock: number;
        price: number;
        currency: unknown;
        currency_code?: string;
    }
    export interface ICurrencies {
        name: string;
        descr: string;
        code: string;
        symbol: string;
    }
    export interface IGenres {
        name: string;
        descr: string;
        ID: number;
        parent?: IGenres;
        parent_ID?: number;
        children: unknown;
    }
    export enum Entity {
        Books = "CatalogService.Books",
        Currencies = "CatalogService.Currencies",
        Genres = "CatalogService.Genres",
    }
    export enum SanitizedEntity {
        Books = "Books",
        Currencies = "Currencies",
        Genres = "Genres",
    }
}

We get interfaces with all attributes of the entities and enums for all entities defined in the data model and the service definition.

cds-routing-handlers

Maybe you already know the routing-controllers for express.js. cds-routing-handlers is the same, only for CDS. With the cds-routing-handlers, classes can be defined as handlers for certain entities using a decorator. The methods of the class can then be defined as handlers for certain hooks of these entities.

cds-routing-handlers can also be easily installed via npm or yarn:

$ npm install cds-routing-handlers

Before, the handlers had to be implemented in files with the same name as the service definition cds file.

// service.js
const express = require("express");
 
function registerHandlers(srv) {
    srv.on("READ", "Entity", async () => {
        // Handle the read here...
    });
}
 
const server = express();
cds.serve("./gen/").at("odata").in(server).with(registerHandlers);

But with the cds-routing-handlers, the implementation of the handlers can be spread over any number of classes.

// ./handlers/entity.handler.ts
import { Handler, OnRead, AfterRead, Entities, Req } from "cds-routing-handlers";
import { CatalogService } from "../entities": // if you are using cds2types ;-)
 
@Handler(CatalogService.SanitizedEntity.Entity)
export class EntityHandler {
    @OnRead()
    public async read(@Req() req: any): Promise<CatalogService.IEntity[]> {
        // Handle the read here...
    }

    @AfterRead()
    public async anyOtherMethod(@Entities() data: CatalogService.IEntity[], @Req() req: any): Promise<void> {
        // Handle after read here...
    }
}

And when starting the express server, these handlers only need to be referenced.

// ./server.ts
import "reflect-metadata";
import cds from "@sap/cds";
import express from "express";
import { createCombinedHandler } from "cds-routing-handlers";
import { EntityHandler } from "./handlers/entity.handler.ts";
 
const server = express();
 
const handler = createCombinedHandler({
    handler: [EntityHandler],
});
 
cds.serve("./gen/").at("odata").in(server).with(handler);

what next?

You all, use CAP together with TypeScript and our modules and build great applications.

A sample implementation using both modules can be found in a sample project on GitHub.

Happy Coding

4 Comments
You must be Logged on to comment or reply to a post.
  • Pretty cool. I also did some rudimentary steps in this direction last year. Also wired it up with our typings from `@sap/cds/api` as well as with reflected models at runtime so that you can use those generated classes as stand-ins for reflected entities and get proper code completion when writing queries using our dynamic `cds.ql`, e.g. in projections and results of statements like that.

    const author = await SELECT.from (Authors, 111, a => {
      // code completion for elements...
      a.ID, a.name, a.books (b => { 
        // code completion for elements in nested expands...    
        b.ID, b.title
      })
    })
    // code completion on query results
    console.log (author.name)

    Should also work with that classes out of the box if they’s fulfil the CSN entities contract.

    Note: dynamic querying is key; using static classes for data access would kill extensibility.

    • Hi, Daniel,

      thank you for your comment.
      I’m pleased to hear that positive feedback is also coming from the CAP team.

      Thanks for your input.

      Kind regards
      Simon