CDS Routing-Handlers
Package to route and implement CDS handlers via a class based system in Typescript.
Table of Content
- Installation ๐ป
- Usage โจ๏ธ
- Decorator Reference ๐
- Example ๐งท
- Bugs and Features ๐๐ก
- License ๐
1. Installation
$ npm install cds-routing-handlers
OR
$ yarn add cds-routing-handlers
Before you can use it you also need to install reflect-metadata
.
$ npm install reflect-metadata
OR
$ yarn add reflect-metadata
Once installed, make sure you import it before using CDS Routing-Handlers.
My recommendation would be to place the import at the top of your main entry file.
import "reflect-metadata";
Finally, one last step is required to use it. We need to tell the Typescript compiler to use decorators.
Place the following settings in your tsconfig.json
:
{
"emitDecoratorMetadata": true,
"experimentalDecorators": true
}
2. Usage
Before:
const express = require("express");
function registerHandlers(srv) {
srv.on("READ", "Entity", async () => {
});
}
const server = express();
cds.serve("./gen/")
.at("odata")
.in(server)
.with(registerHandlers);
With CDS Routing-Handlers:
import { Handler, OnRead } from "cds-routing-handlers";
@Handler("Entity")
export class EntityHandler {
@OnRead()
public async read(@Srv() srv: any, @Req() req: any): Promise<void> {
}
}
import express from "express";
import { createCombinedHandler } from "cds-routing-handlers";
const server = express();
const handler = createCombinedHandler({
handler: [__dirname + "/handlers/**/*.js"],
});
cds.serve("./gen/")
.at("odata")
.in(server)
.with(handler);
import { EntityHandler } from "./handlers/entity.handler.ts";
const handler = createCombinedHandler({
handler: [EntityHandler],
});
cds.serve("./gen/")
.at("odata")
.in(server)
.with(handler);
Depencency Injection Support
CDS Routing-Handlers can be used in conjunction with typedi.
import { useContainer } from "cds-routing-handlers";
import { Container } from "typedi";
useContainer(Container);
That's it now you can inject service into your handler classes.
import { Handler, Func, Param } from "cds-routing-handlers";
import { Service, Inject } from "typedi";
@Service()
export class GreeterService {
public greet(name: string): string {
return "Hello, " + name;
}
}
@Handler()
export class GreeterHandler {
@Inject()
private greeterService: GreeterService;
@Func("hello")
public async hello(@Param("name") name: string): Promise<string> {
return this.greeterService.greet(name);
}
}
Middleware
In addition to the before handler you can implement custom middlewares for either the complete service (global) or a specific entity.
Global Middleware
import { ICdsMiddleware, Middleware, Req, Jwt } from "cds-routing-handlers";
@Middleware({ global: true, priority: 1 })
export class GobalMiddleware implements ICdsMiddleware {
public async use(@Req() req: any, @Jwt() jwt: string): Promise<void> {
console.log("I am global middleware prio 1");
}
}
import express from "express";
import { createCombinedHandler } from "cds-routing-handlers";
import { GlobalMiddleware } from "./global.middleware";
const server = express();
const handler = createCombinedHandler({
handler: [__dirname + "/handlers/**/*.js"],
middlewares: [GlobalMiddleware],
});
cds.serve("./gen/")
.at("odata")
.in(server)
.with(handler);
Handler Middleware
import { ICdsMiddleware, Middleware, Req, Jwt } from "cds-routing-handlers";
@Middleware()
export class HandlerMiddleware implements ICdsMiddleware {
public async use(@Req() req: any, @Jwt() jwt: string): Promise<void> {
console.log("I am handler middleware!");
}
}
import { Handler, Use, OnRead } from "cds-routing-handlers";
import { HandlerMiddleware } from "../handler.middleware";
@Handler("Entity")
@Use(HandlerMiddleware)
export class EntityHandler {
@OnRead()
public async read(@Srv() srv: any, @Req() req: any): Promise<void> {
}
}
import express from "express";
import { createCombinedHandler } from "cds-routing-handlers";
import { HandlerMiddleware } from "./handler.middleware";
const server = express();
const handler = createCombinedHandler({
handler: [__dirname + "/handlers/**/*.js"],
middlewares: [HandlerMiddleware],
});
cds.serve("./gen/")
.at("odata")
.in(server)
.with(handler);
User Checker
To avoid duplicate code for user retrieval in all handlers over the JWT token you can implement a user checker which lets you inject your custom user object with the @User()
decorator.
import { IUserChecker, UserChecker, Jwt } from "cds-routing-handlers";
export interface IUser {
username: string;
}
@UserChecker()
export class CustomUserChecker implements IUserChecker {
public async check(@Jwt() jwt: string): Promise<IUser> {
return {
username: "mrbandler",
};
}
}
import { Handler, OnRead } from "cds-routing-handlers";
import { IUser } from "../custom.userchecker";
@Handler("Entity")
export class EntityHandler {
@OnRead()
public async read(@User() user: IUser): Promise<void> {
}
}
import express from "express";
import { createCombinedHandler } from "cds-routing-handlers";
import { CustomUserChecker } from "./custom.userchecker";
const server = express();
const handler = createCombinedHandler({
handler: [__dirname + "/handlers/**/*.js"],
userChecker: CustomUserChecker,
});
cds.serve("./gen/")
.at("odata")
.in(server)
.with(handler);
Complete Handler API
import { Handler } from "cds-routing-handlers";
import { BeforeCreate, OnCreate, AfterCreate } from "cds-routing-handlers";
import { BeforeRead, OnRead, AfterRead } from "cds-routing-handlers";
import { BeforeUpdate, OnUpdate, AfterUpdate } from "cds-routing-handlers";
import { BeforeDelete, OnDelete, AfterDelete } from "cds-routing-handlers";
import { Func, Action } from "cds-routing-handlers";
import { Req, Srv, Param, ParamObj, Jwt } from "cds-routing-handlers";
interface IFoobar {
Id: string;
Foo: string;
Bar: number;
}
@Handler("Foobar")
export class FooBarHandler {
@BeforeCreate()
public async beforeCreate(): Promise<IFoobar> {
}
@OnCreate()
public async onCreate(): Promise<IFoobar> {
}
@AfterCreate()
public async afterCreate(): Promise<IFoobar> {
}
@BeforeRead()
public async beforeRead(): Promise<IFoobar> {
}
@OnRead()
public async onRead(): Promise<IFoobar> {
}
@AfterRead()
public async afterRead(): Promise<IFoobar> {
}
@BeforeUpdate()
public async beforeUpdate(): Promise<IFoobar> {
}
@OnUpdate()
public async onUpdate(): Promise<IFoobar> {
}
@AfterUpdate()
public async afterUpdate(): Promise<IFoobar> {
}
@BeforeDelete()
public async beforeDelete(): Promise<void> {
}
@OnDelete()
public async onDelete(): Promise<void> {
}
@AfterDelete()
public async afterDelete(): Promise<void> {
}
}
@Handler("Foobar")
export class FooBarRejectHandler {
@OnRead()
@OnReject(500, "Foobar not found")
public async onRead(): Promise<IFoobar> {
}
@OnRead()
@OnReject(500, "Foobar not found", true)
public async onRead(): Promise<IFoobar> {
}
}
@Handler()
export class FooBarRejectHandler {
@Func("foo")
public async foo(@Param("bar") bar: string): Promise<string> {
return "Foo, " + bar;
}
@Action("bar")
public async bar(@Param("foo") foo: string, @Param("noop") noop: string): Promise<void> {
console.log("Foo Param", foo);
console.log("Noop Param", noop);
}
}
interface IDoItParams {
id: string;
do: string;
times: number;
}
@Handler()
export class ParamExampleHandler {
@Func("hello")
public async foo(@Param("title") title: string, @Param("name") name: string): Promise<string> {
return `Hello, ${title} ${name}`;
}
@Action("doIt")
public async doIt(@ParamObj() params: IDoItParams): Promise<string> {
console.log(params);
}
@OnRead()
public async read(@Srv() srv: any, @Req() req: any): Promise<string> {
}
@OnRead()
public async read(@Jwt() jwt: string): Promise<string> {
}
@AfterRead()
public async afterRead(@Entities() entities: IDoItParams[]): Promise<IDoItParams[]> {
return entities.map(e => {
e.id = "After handler was here";
return e;
});
}
}
3. Decorator Reference
Middleware Decorators
Signature | Example | Description |
---|
@Middleware(options?: { global?: boolean, priority?: number }) | @Middleware({ global: true, priority: 1 }) class CustomMiddleware implements ICdsMiddleware or
@Middleware() class CustomMiddleware implements ICdsMiddleware | Class that is marked with this decorator is registered as a middleware and needs to implement the ICdsMiddleware interface. The middleware can then be used to intercept all incoming request via a global definition or intercept all incoming requests on a specific entity handler. |
User Checker Decorators
Signature | Example | Description |
---|
@UserChecker() | @UserChecker() class CustomUserChecker implements IUserChecker | Class that is marked with this decorator is registered as a user checker and needs to implement the IUserChecker interface. The user checker can then be used to provide a custom user object via the @User() decorator. |
Handler Decorators
Signature | Example | Description |
---|
@Handler(entity?: string) | @Handler("Books") class BooksHandler | Class that is marked with this decorator is registered as a handler and its annotated methods are registered as actions. The entity parameter is used to differentiate and register the correct actions for the corresponding entity. |
@Use(...middlewares: Function[]) | @Use(MiddlewareClass) class BooksHandler | A class that is marked with this decorator also needs to be marked with the @Handler() decorator to be used. If a handler is implemented the use method from the middleware is called every time a new requests comes in, regardless of the action (CREATE , READ , UPDATE , DELETE ) to be executed. |
Handler Action Decorators
In this table we assume that all action handlers a within a @Handler
decorated class with the entity Books
.
Signature | Example | Description | @sap/cds analogue |
---|
@BeforeCreate() | @BeforeCreate() async beforeCreate() | Methods marked with this decorator will register a request made with POST HTTP Method to the specified entity, before it will be handled by the actual handler. | srv.before("CREATE", "Books", async (req) => ...) |
@OnCreate() | @OnCreate() async onCreate() | Methods marked with this decorator will register a request made with POST HTTP Method to the specified entity. | srv.on("CREATE", "Books", async (req) => ...) |
@AfterCreate() | @AfterCreate() async afterCreate() | Methods marked with this decorator will register a request made with POST HTTP Method to the specified entity, after it was handled by the actual handler. | srv.after("CREATE", "Books", async (books, req) => ...) |
@BeforeRead() | @BeforeRead() async beforeRead() | Methods marked with this decorator will register a request made with GET HTTP Method to the specified entity, before it will be handled by the actual handler. | srv.before("READ", "Books", async (req) => ...) |
@OnRead() | @OnRead() async onRead() | Methods marked with this decorator will register a request made with GET HTTP Method to the specified entity. | srv.on("READ", "Books", async (req) => ...) |
@AfterRead() | @AfterRead() async afterRead() | Methods marked with this decorator will register a request made with GET HTTP Method to the specified entity, after it was handled by the actual handler. | srv.after("READ", "Books", async (books, req) => ...) |
@BeforeUpdate() | @BeforeUpdate() async beforeUpdate() | Methods marked with this decorator will register a request made with PUT HTTP Method to the specified entity, before it will be handled by the actual handler. | srv.before("UPDATE", "Books", async (req) => ...) |
@OnUpdate() | @OnUpdate() async onUpdate() | Methods marked with this decorator will register a request made with PUT HTTP Method to the specified entity. | srv.on("UPDATE", "Books", async (req) => ...) |
@AfterUpdate() | @AfterUpdate() async afterUpdate() | Methods marked with this decorator will register a request made with DELETE HTTP Method to the specified entity, after it was handled by the actual handler. | srv.after("UPDATE", "Books", async (books, req) => ...) |
@BeforeDelete() | @BeforeDelete() async beforeDelete() | Methods marked with this decorator will register a request made with DELETE HTTP Method to the specified entity, before it will be handled by the actual handler. | srv.before("DELETE", "Books", async (req) => ...) |
@OnDelete() | @OnDelete() async onDelete() | Methods marked with this decorator will register a request made with DELETE HTTP Method to the specified entity. | srv.on("DELETE", "Books", async (req) => ...) |
@AfterDelete() | @AfterDelete() async afterDelete() | Methods marked with this decorator will register a request made with DELETE HTTP Method to the specified entity, after it was handled by the actual handler. | srv.after("DELETE", "Books", async (books, req) => ...) |
@Func(name: string) | @Func("doIt") async doIt() | Methods marked with this decorator will register a request made with either GET or POST HTTP. The payload can either be given by path parameters or body. | srv.on("doIt", "Books", async (req) => ...) OR srv.on("doIt", async (req) => ...) |
@Action(name: string) | @Action("doItNow") async doItNow() | Methods marked with this decorator will register a request made with either GET or POST HTTP. The payload can either be given by path parameters or body. | srv.on("doItNow", "Books", async (req) => ...) OR srv.on("doItNow", async (req) => ...) |
@OnReject(code: number, message: string, appendErrorMessage: boolean = false) | @OnReject(500, "Nope, that didn't work") async handler() | Methods marked with this decorator will return a error object when a handler fails. | srv.on("READ", "Books", async (req) => {req.reject(500, "Nope, that didn't work")}) |
Method Parameter Decorators
Signature | Example | Description | @sap/cds analogue |
---|
@Req() | onRead(@Req() req: any) | Injects the request object. | srv.on("READ", "Books", async (req) => ...) |
@Srv() | onRead(@Srv() srv: any) | Injects the service object. | srv.on("READ", "Books", async (req) => { // Acess srv here }) |
@Param(name: string) | doIt(@Param("times") times: number) | Injects a Function/Action Import parameter. | srv.on("READ", "Books", async (req) => { let times = req.data["times"] as number; }) |
@ParamObj() | doIt(@ParamObj() doItParams: IDoItParams) | Injects a Function/Action Import parameter object. | srv.on("READ", "Books", async (req) => { let doItParams = req.data as IDoItParams; }) |
@Entities() | afterRead(@Entities() entities: IBook[]) | Injects entities from a previous handler on @After* handlers. | srv.after("READ", "Books", async (books, req) => ...) |
@Jwt() | onRead(@Jwt() jwt: string) | Injects the JWT token, when found on incoming request. | N/A |
@Data() | onRead(@Data() book: IBook) | Injects the data object from a incoming request. It's actually the same as @ParamObj() with a different identifier to differentiate between Function/Action imports and other handlers. | srv.on("READ", "Books", async(req) => { const book = req.data as I Book}) |
@Next() | onRead(@Next() next: Function) | Injects the next handler function for flow control with multiple handlers. | srv.on("READ", "Books", async(req, next) => ... ) |
@Locale() | onRead(@Locale() locale: string) | Injects the locale of the current requesting user. | srv.on("READ", "Books", async(req) => const locale = req.user.locale) |
@User() | onRead(@User() user: IUser) | Injects the custom user object that is created via the UserChecker implementation. | N/A |
4. Example
For a complete example checkout the ./test
directory which contains the project I am testing with.
Additionally you can use my cds-routing-handler Postman collection which contains definitions for every endpoint from the project.
5. Bugs and Features
Please open a issue when you encounter any bugs ๐ or if you have an idea for a additional feature ๐ก.
6. License
MIT License
Copyright (c) 2019 mrbandler
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.