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
Great blog!
Also thanks for sharing the modules.
Regards,
Henrik
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.
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
Hi Daniel,
The dynamic querying is indeed key. I was wondering if there has been any developments around this since your post?
The projections are a powerful way to define the queries and would be great to leverage typescript for this.
Thanks,
Frank.
Wow! Really great stuff. As a Typescript fan I absolutely like it.
Hello, I have made an integration using this blog, but I have tried to use the name of the classes instead of *.js files
const hdl = createCombinedHandler({
handler: [__dirname + "/entities/**/*.js", __dirname + "/functions/**/*.js"],
}); (link here)
I realized I forgot to import reflect-metadata.
Now I get another error:
"TypeError: Cannot read property '1' of undefined
Made it working finally, the missing piece was
in tsconfig.json
Thanks Simon for the nice blog.
Does cds-ts watch will read the file from src folder ( src/server.ts and so on )
If not, please let me know if any specific config I need to run using cds-ts watch
Hi guys,
It appears that cds-routing-handler is not maintained anymore.
But there's a better alternative :
Examples how to use the CDS-TS-Dispatcher & CDS-TS-Repository => GitHub
Looks awesome. Thank you very much for creating and sharing the libraries 🙂