Technology Blogs by Members
Explore a vibrant mix of technical expertise, industry insights, and tech buzz in member blogs covering SAP products, technology, and events. Get in the mix!
cancel
Showing results for 
Search instead for 
Did you mean: 
former_member194549
Contributor
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 mrbandler 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
11 Comments
Labels in this area