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
HenrikD
Participant
Great blog!

Also thanks for sharing the modules.

Regards,

Henrik
Daniel7
Advisor
Advisor
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.
htammen
Active Contributor
Wow! Really great stuff. As a Typescript fan I absolutely like it.
former_member194549
Contributor
0 Kudos
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
calin_crecea
Explorer
0 Kudos

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: [BookHandler, FunctionHandler]
    });
and I get the following error: "TypeError: Reflect.getMetadata is not a function"
Any idea what could be wrong?
(in the sample project https://github.com/HeneryHawk/cap-bookshop-typescript they use 
const hdl = createCombinedHandler({
    handler: [__dirname + "/entities/**/*.js", __dirname + "/functions/**/*.js"],
});  (link here)
calin_crecea
Explorer
0 Kudos
I realized I forgot to import reflect-metadata.
Now I get another error:
"TypeError: Cannot read property '1' of undefined
calin_crecea
Explorer
Made it working finally, the missing piece was

"emitDecoratorMetadata": true

in tsconfig.json

frankmeertens
Explorer
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.
sandeepmalhotra
Participant
0 Kudos
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
Dragolea
Participant

Hi guys,

It appears that cds-routing-handler is not maintained anymore.

But there's a better alternative :

  • CDS-TS-Dispatcher - TypeScript package for handling the events, support for draft
    • Example draft decorators :
      • @OnReadDraft(), @AfterReadDraft(), @OnSaveDraft()
    • Example active entity methods 
      • @OnRead(), @AfterRead(), @BeforeRead()
      • and many more ... 
  • CDS-TS-Repository - Simplified interface for common database actions
    • Example .create, createMany, update, delete, deleteMany, exists, getLocaleTexts, count, updateLocaleTexts ... and many more 

Examples how to use the CDS-TS-Dispatcher & CDS-TS-Repository => GitHub

hakimio
Explorer
Looks awesome. Thank you very much for creating and sharing the libraries 🙂
Labels in this area