The goal of CDS-TS-Dispatcher is to significantly reduce the boilerplate code required to implement Typescript handlers provided by the SAP CAP framework.
- Table of Contents
- Prerequisites
- Installation
- Usage
Deployment
to BTP using MTABest practices
&tips
Samples
- Contributing
- License
- Authors
Install @sap/cds-dk, typescript
, ts-node
globally:
npm install -g @sap/cds-dk typescript ts-node
Use the following steps if you want to create a new SAP CAP project.
- Create new folder :
mkdir project
cd project
- Initialize the CDS folder structure :
cds init
- Add
TypeScript
and CDS-Typer to your npm package.json:
cds add typescript
- Add
CDS-TS-Dispatcher
to your npm package.json :
npm install @dxfrontier/cds-ts-dispatcher
- It is recommended to use the following tsconfig.json properties:
{
"compilerOptions": {
"esModuleInterop": true,
"skipLibCheck": true,
"allowJs": true,
"resolveJsonModule": true,
"isolatedModules": true,
"strictNullChecks": true,
"strictPropertyInitialization": false,
"forceConsistentCasingInFileNames": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"target": "ES2021",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "./gen/srv",
"rootDir": ".",
"paths": {
"#cds-models/*": ["./@cds-models/*/index.ts"]
}
},
"include": ["./srv", "./@dispatcher"]
}
- Install packages
npm install
- Run the
CDS-TS
server
cds-ts w
Important
CDS-TS-Dispatcher uses @sap/cds
, @sap/cds-dk
version 8
Use the following steps if you want to create a new SAP CAP project.
- Create new folder :
mkdir new-sap-cap-project
cd new-sap-cap-project
- Initialize the CDS folder structure :
cds init
- Add CDS-Typer to your npm package.json:
cds add typer
npm install
- Add the the following NPM packages :
npm install @dxfrontier/cds-ts-dispatcher@2
npm install --save-dev @types/node
- Add a tsconfig.json :
tsc --init
- It is recommended to use the following tsconfig.json properties:
{
"compilerOptions": {
"esModuleInterop": true,
"skipLibCheck": true,
"allowJs": true,
"resolveJsonModule": true,
"isolatedModules": true,
"strictNullChecks": true,
"strictPropertyInitialization": false,
"forceConsistentCasingInFileNames": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"target": "ES2021",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "./gen/srv",
"rootDir": ".",
"paths": {
"#cds-models/*": ["./@cds-models/*/index.ts"]
}
},
"include": ["./srv", "./@dispatcher"]
}
- Run the
CDS-TS
server
cds-ts watch
Use the following steps if you want to migrate from @sap/cds@7
to @sap/cds@8
:
- Verify you've installed the
cds@v8
globally by running the following command:
cds -v -i
packages | version |
---|---|
@cap-js/asyncapi | 1.0.1 |
@cap-js/cds-typer | 0.24.0 |
@cap-js/cds-types | 0.6.4 |
@cap-js/openapi | 1.0.4 |
@cap-js/sqlite | 1.7.3 |
@sap/cds |
8.1.0 |
@sap/cds-compiler | 5.1.2 |
@sap/cds-dk (global) |
8.0.2 |
@sap/cds-fiori | 1.2.7 |
@sap/cds-foss | 5.0.1 |
@sap/cds-lsp | 8.0.0 |
@sap/cds-mtxs | 1.18.2 |
@sap/eslint-plugin-cds | 3.0.4 |
Node.js | v22.4.1 |
Tip
If you see a smaller version than @sap/cds-dk (global)
8.0.2
run the following command :
npm install -g @sap/cds-dk@latest
- Run the following command inside of your project:
cds add typescript
Tip
Command above will add the following packages:
@types/node
@cap-js/cds-types
@cap-js/cds-typer
typescript
- After running command above the
package.json
will look similar to :
{
"dependencies": {
"@dxfrontier/cds-ts-dispatcher": "^3.0.0",
"@dxfrontier/cds-ts-repository": "^1.1.3",
"@sap/cds": "^8.1.0",
"express": "^4.19.2"
},
"devDependencies": {
"@cap-js/sqlite": "^1.7.3",
"@cap-js/cds-types": "^0.6.4",
"typescript": "^5.5.4",
"@types/node": "^22.1.0",
"@cap-js/cds-typer": ">=0.24.0"
},
"scripts": {
"start": "cds-serve",
"watch": "cds-ts w",
},
}
Important
You might delete the node_modules
folder and package-lock.json
in case npm run watch
fails working.
Re-run the following command :
npm install
Use the following steps if you want to add only the @dxfrontier/cds-ts-dispatcher to an existing project :
npm install @dxfrontier/cds-ts-dispatcher
It is recommended to use the following tsconfig.json properties:
{
"compilerOptions": {
/* Base Options: */
"esModuleInterop": true,
"skipLibCheck": true,
"allowJs": true,
"strictPropertyInitialization": false,
"forceConsistentCasingInFileNames": true,
"allowSyntheticDefaultImports": true,
"strictNullChecks": true,
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
/* Allow decorators */
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
/* Strictness */
"strict": true,
"lib": ["es2022"],
"outDir": "./gen/srv"
},
"include": ["./srv"]
}
Warning
If below message appears
-----------------------------------------------------------------------
WARNING: Package '@sap/cds' was loaded from different installations: [
'***/node_modules/@sap/cds/lib/index.js',
'***/node_modules/@dxfrontier/cds-ts-dispatcher/node_modules/@sap/cds/lib/index.js'
] Rather ensure a single install only to avoid hard-to-resolve errors.
-----------------------------------------------------------------------
Run the following command :
npm install -g @sap/cds-dk@latest
Execute the following commands :
cds add typer
npm install
Tip
If above option is being used, this means whenever we change a .CDS
file the changes will reflect in the generated @cds-models
folder.
Important
Import always the generated entities
from the service
folders and not from the index.ts
Tip
By default cds-typer will create in your package.json
a quick path alias like :
"imports": {
"#cds-models/*": "./@cds-models/*/index.js"
}
Use import helper to import entities from #cds-models
like example :
import { Book } from '#cds-models/CatalogService';
We recommend adhering to the Controller-Service-Repository design pattern using the following folder structure:
- EntityHandler
(Controller)
- Responsible for managing the REST interface to the business logic implemented in ServiceLogic - ServiceLogic
(Service)
- Contains business logic implementations - Repository
(Repository)
- This component is dedicated to handling entity manipulation operations by leveraging the power of CDS-QL.
Controller-Service-Repository
suggested folder structure
Tip
You can have a look over the CDS-TS-Dispatcher-Samples where we use the Controller-Service-Repository pattern and Dispatcher.
CDSDispatcher(entities
: Constructable[]
)
The CDSDispatcher
constructor allows you to create an instance for dispatching and managing entities.
Parameters
entities (Array)
: An array of Entity handler(s) (Constructable) that represent the entities in the CDS.
Method
initialize
: Theinitialize
method of theCDSDispatcher
class is used to initialize Entity handler(s) and all of their dependencies : Services, Repositories, UnboundActions
Example
import { CDSDispatcher } from '@dxfrontier/cds-ts-dispatcher';
export = new CDSDispatcher([
// Entities
BookHandler,
ReviewHandler,
BookStatsHandler,
// Draft
BookEventsHandler,
// Unbound actions
UnboundActionsHandler,
]).initialize();
// or use
// module.exports = new CDSDispatcher([ ...
Visual image
The @EntityHandler
decorator is utilized at the class-level
to annotate a class with:
- A specific
entity
that will serve as the base entity for all handler decorators within the class. '*'
asall entities
that will serve as the base entity for all handler decorators within the class.
Overloads
Method | Parameters | Description |
---|---|---|
1. EntityHandler(entity : CDSTyper) |
Must be a CDS-Typer generated class |
It ensures that all handlers within the class operate with the specified entity context . |
2. EntityHandler(entity : '*' ) |
A wildcard '*' indicating all entities |
It ensures that all handlers within the class operate with a generic context indicating that registered events will be triggered for all all entities (active entities and draft entities ) Excluded will be @OnAction(), @OnFunction(), @OnEvent(), @OnError() as these actions belongs to the Service itself. |
Parameters
entity (CDSTyperEntity | '*')
: A specialized class generated using the CDS-Typer or generic wild card'*'
applicable to all entities.
Example 1
using CDS-Typer
import { EntityHandler } from '@dxfrontier/cds-ts-dispatcher';
import { MyEntity } from 'YOUR_CDS_TYPER_ENTITIES_LOCATION';
@EntityHandler(MyEntity)
export class BookHandler {
// ...
constructor() {}
// All events like @AfterRead, @BeforeRead, ... will be triggered based on 'MyEntity'
}
Tip
After creation of BookHandler
class, you can import it
into the CDSDispatcher.
import { CDSDispatcher } from '@dxfrontier/cds-ts-dispatcher';
export = new CDSDispatcher([
// Entities
BookHandler,
// Unbound actions
// ...
]).initialize();
Example 2
using *
wildcard indicating that events will be triggered for all entities
import { EntityHandler, CDS_DISPATCHER } from '@dxfrontier/cds-ts-dispatcher';
@EntityHandler(CDS_DISPATCHER.ALL_ENTITIES) // or use the '*'
export class AllEntities {
// ...
constructor() {}
// All events like @AfterRead, @BeforeRead, ... will be triggered on all entities using wildcard '*'
}
Tip
After creation of AllEntities
class, you can import it
into the CDSDispatcher.
import { CDSDispatcher } from '@dxfrontier/cds-ts-dispatcher';
export = new CDSDispatcher([
// Entities
AllEntities,
// Unbound actions
// ...
]).initialize();
Note
MyEntity was generated using CDS-Typer and imported in the class.
@ServiceLogic()
The @ServiceLogic
decorator is utilized at the class-level
to annotate a class
as a specialized class containing only business logic.
Example
import { ServiceLogic } from '@dxfrontier/cds-ts-dispatcher';
@ServiceLogic()
export class CustomerService {
// ...
constructor() {}
// ...
}
Tip
When applying @ServiceLogic()
decorator, the class becomes eligible to be used with Inject decorator for Dependency injection
.
@Repository()
The @Repository
decorator is utilized as a class-level
annotation that designates a particular class
as a specialized Repository
, this class should contain only CDS-QL code.
import { Repository } from '@dxfrontier/cds-ts-dispatcher';
@Repository()
export class CustomerRepository {
// ...
constructor() {}
// ...
}
Tip
When applying @Repository()
decorator, the class becomes eligible to be used with Inject decorator for Dependency injection
.
The CDS-TS-Repository - BaseRepository was designed to reduce the boilerplate code required to implement data access layer for persistance entities.
It simplifies the implementation by offering a set of ready-to-use actions for interacting with the database. These actions include:
.create()
: Create new records in the database..getAll()
: Retrieve all records from the database..find()
: Query the database to find specific data..delete()
: Remove records from the database..exists()
: Check the existence of data in the database.- and many more ...
Example
import { Repository } from '@dxfrontier/cds-ts-dispatcher';
import { BaseRepository } from '@dxfrontier/cds-ts-repository';
import { MyEntity } from 'YOUR_CDS_TYPER_ENTITIES_LOCATION';
@Repository()
export class CustomerRepository extends BaseRepository<MyEntity> {
constructor() {
super(MyEntity);
}
public async aMethod() {
const created = await this.create(...)
const createdMany = await this.createMany(...)
const updated = await this.update(...)
// ...
}
}
To get started, refer to the official documentation CDS-TS-Repository - BaseRepository. Explore the capabilities it offers and enhance your data access layer with ease.
Note
MyEntity was generated using CDS-Typer and imported in the class.
@UnboundActions()
The @UnboundActions
decorator is utilized at the class-level
to annotate a class
as a specialized class which will be used only for Unbound actions.
The following decorators can be used inside of @UnboundActions()
:
Example
import { UnboundActions, OnAction, OnFunction, OnEvent, Req, Next, Error } from '@dxfrontier/cds-ts-dispatcher';
import { MyAction, MyFunction, MyEvent } from 'YOUR_CDS_TYPER_ENTITIES_LOCATION';
import type { ActionRequest, ActionReturn, Request, NextEvent } from '@dxfrontier/cds-ts-dispatcher';
@UnboundActions()
export class UnboundActionsHandler {
// ... @Inject dependencies, if needed.
constructor() {}
// Unbound action
@OnAction(MyAction)
private async onActionMethod(
@Req() req: ActionRequest<typeof MyAction>,
@Next() next: NextEvent,
): ActionReturn<typeof MyAction> {
// ...
}
// Unbound Function
@OnFunction(MyFunction)
private async onFunctionMethod(
@Req() req: ActionRequest<typeof MyFunction>,
@Next() next: NextEvent,
): ActionReturn<typeof MyFunction> {
// ...
}
// Unbound event
@OnEvent(MyEvent)
private async onEventMethod(@Req() req: Request<MyEvent>) {
// ...
}
// Unbound error
@OnError()
private onErrorMethod(@Error() err: Error, @Req() req: Request) {
// ...
}
}
Imported it
in the CDSDispatcher
import { CDSDispatcher } from '@dxfrontier/cds-ts-dispatcher';
export = new CDSDispatcher([ UnboundActionsHandler, ...])
// or
// use module.exports = new CDSDispatcher( ... )
Note
The reason behind introducing a distinct decorator for Unbound actions
stems from the fact that these actions are not associated with any specific Entity
but instead these actions belongs to the Service itself.
@Use(...Middleware[]
)
The @Use
decorator simplifies the integration of middlewares into your classes.
When @Use
decorator applied at the class-level
this decorator inject middlewares into the class and gain access to the req: Request
and next: NextMiddleware
middleware across all events (@AfterRead, @OnRead ...)
within that class.
Middleware decorators can perform the following tasks:
- Execute any code.
- Make changes to the request object.
- End the request-response cycle.
- Call the next middleware function in the stack.
- If the current middleware function does not end the request-response cycle, it must call
next()
to pass control to the next middleware function. Otherwise, the request will be left hanging.
Parameters
...Middleware[])
: Middleware classes to be injected.
Example:
middleware implementation
import type { MiddlewareImpl, NextMiddleware, Request } from '@dxfrontier/cds-ts-dispatcher';
export class MiddlewareClass implements MiddlewareImpl {
public async use(req: Request, next: NextMiddleware) {
console.log('Middleware use method called.');
await next(); // call next middleware
}
}
Example
usage
import { EntityHandler, Use, Inject, CDS_DISPATCHER } from '@dxfrontier/cds-ts-dispatcher';
import { MyEntity } from 'YOUR_CDS_TYPER_ENTITIES_LOCATION';
import { Middleware1, Middleware2, MiddlewareN } from 'YOUR_MIDDLEWARE_LOCATION';
import type { Service } from '@dxfrontier/cds-ts-dispatcher';
@EntityHandler(MyEntity)
@Use(Middleware1, Middleware2, MiddlewareN)
export class CustomerHandler {
// ...
@Inject(CDS_DISPATCHER.SRV) private srv: Service;
// ...
constructor() {}
// ...
}
Tip
- Think of it (middleware) like as a reusable class, enhancing the functionality of all events within the class.
- Middlewares when applied with
@Use
are executed before the normal events. - If you need to apply middleware to a
method
you should use the method specific @Use decorator .
Warning
If req.reject()
is used inside of middleware this will stop the stack of middlewares, this means that next middleware will not be executed.
Note
MyEntity was generated using CDS-Typer and imported in the class.
@Inject(serviceIdentifier: ServiceIdentifierOrFunc<unknown>
)
The @Inject
decorator is utilized as a field-level
decorator and allows you to inject dependencies into your classes.
Parameters
serviceIdentifier(ServiceIdentifierOrFunc<unknown>)
: A Class representing the service to inject.
Example
import { EntityHandler, Inject, CDS_DISPATCHER } from "@dxfrontier/cds-ts-dispatcher";
import type { Service } from '@dxfrontier/cds-ts-dispatcher';
import { MyEntity } from 'YOUR_CDS_TYPER_ENTITIES_LOCATION';
@EntityHandler(MyEntity)
export class CustomerHandler {
...
@Inject(CustomerService) private customerService: CustomerService
@Inject(CustomerRepository) private customerService: CustomerRepository
@Inject(AnyOtherInjectableClass) private repository: AnyOtherInjectableClass
@Inject(CDS_DISPATCHER.SRV) private srv: Service
// ...
constructor() {}
// ...
}
Note
MyEntity was generated using CDS-Typer and imported in the class.
@Inject(CDS_DISPATCHER.SRV
) private srv: Service
This specialized @Inject
can be used as a constant
in and contains the CDS.ApplicationService
for further enhancements.
It can be injected in the following :
Example
import { EntityHandler, Inject, CDS_DISPATCHER } from '@dxfrontier/cds-ts-dispatcher';
import { MyEntity } from 'YOUR_CDS_TYPER_ENTITIES_LOCATION';
import type { Service } from '@dxfrontier/cds-ts-dispatcher';
@EntityHandler(MyEntity)
// OR @ServiceLogic()
// OR @Repository()
// OR @UnboundActions()
export class CustomerHandler {
// @Inject dependencies
@Inject(CDS_DISPATCHER.SRV) private readonly srv: Service;
constructor() {}
// ...
}
Tip
The CDS.ApplicationService can be accessed trough this.srv
.
Note
MyEntity was generated using CDS-Typer and imported in the the class.
@Inject(CDS_DISPATCHER.OUTBOXED_SRV
) private srv: Service
This specialized @Inject
can be used as a constant
and contains the CDS.outboxed
service.
It can be injected in the following :
Example
import { EntityHandler, Inject, CDS_DISPATCHER } from '@dxfrontier/cds-ts-dispatcher';
import { MyEntity } from 'YOUR_CDS_TYPER_ENTITIES_LOCATION';
import type { Service } from '@dxfrontier/cds-ts-dispatcher';
@EntityHandler(MyEntity)
// OR @ServiceLogic()
// OR @Repository()
// OR @UnboundActions()
export class CustomerHandler {
// @Inject dependencies
@Inject(CDS_DISPATCHER.OUTBOXED_SRV) private readonly outboxedSrv: Service;
constructor() {}
// ...
}
Tip
More info about outboxed
ca be found at SAP CAP Node.js Outboxed
Tip
The CDS.ApplicationService can be accessed trough this.outboxedSrv
Note
MyEntity was generated using CDS-Typer and imported in the the class.
@Req()
The @Req
decorator is utilized at the parameter level
to annotate a parameter with the Request
object, providing access to request-related information of the current event.
Return
Request
: An instance of@sap/cds
-Request
Example
import { EntityHandler, Req, Results } from '@dxfrontier/cds-ts-dispatcher';
import { MyEntity } from 'YOUR_CDS_TYPER_ENTITIES_LOCATION';
import type { Request } from '@dxfrontier/cds-ts-dispatcher';
@EntityHandler(MyEntity)
export class BookHandler {
// ...
constructor() {}
// ... all events like @AfterRead, @BeforeRead ...
@AfterRead()
private async aMethod(@Req() req: Request, @Results() results: MyEntity[]) {
// ... req...
}
}
@Res()
The @Res
decorator is utilized at the parameter level
to annotate a parameter with the Request.http.res - (Response)
object, providing access to response-related information of the current event and it can be used to enhance the Response
.
Return
RequestResponse
: An instance ofRequestResponse
providing you response-related information.
Example
import { EntityHandler, Req, Results } from '@dxfrontier/cds-ts-dispatcher';
import { MyEntity } from 'YOUR_CDS_TYPER_ENTITIES_LOCATION';
import type { Request, RequestResponse } from '@dxfrontier/cds-ts-dispatcher';
@EntityHandler(MyEntity)
export class BookHandler {
// ...
constructor() {}
// ... all events like @AfterRead, @BeforeRead ...
@AfterRead()
private async aMethod(@Req() req: Request, @Res() response: RequestResponse, @Results() results: MyEntity[]) {
// Example: we assume we want to add a new header language on the response
// We use => res.setHeader('Accept-Language', 'DE_de');
}
}
@Results() / @Result
The @Results
decorator is utilized at the parameter level
to annotate a parameter with the request Results
.
Return
Array / object
: Contains the OData RequestBody
.
Example
import { EntityHandler, Req, Results } from '@dxfrontier/cds-ts-dispatcher';
import { MyEntity } from 'YOUR_CDS_TYPER_ENTITIES_LOCATION';
import type { Request } from '@dxfrontier/cds-ts-dispatcher';
@EntityHandler(MyEntity)
export class BookHandler {
// ...
constructor() {}
// ... all events like @AfterRead, @BeforeRead ...
@AfterRead()
private async aMethod(@Req() req: Request, @Results() results: MyEntity[]) {
// ...
}
}
Tip
When using @AfterCreate(), @AfterUpdate() and @AfterDelete() it's recommended to use the @Result
decorator for single object result and @Results
for arrays of objects.
@AfterCreate()
@AfterUpdate()
private async aMethod(
@Result() result: Book, // <== @Result() decorator used to annotate it's a an object and not an array
@Req() req: Request,
) {
// ...
}
@AfterRead()
private async aMethod(
@Results() result: Book[], // <== @Results() decorator used to annotate as array of objects
@Req() req: Request,
) {
// ...
}
@AfterDelete()
private async aMethod(
@Result() deleted: boolean, // <== @Result() decorator used to annotate as a boolean
@Req() req: Request,
) {
// ...
}
Tip
Decorators @Results()
and @Result()
can be applied to all After events.
@Next()
The @Next
decorator is utilized at the parameter level
to annotate a parameter with the Next
function, which is used to proceed to the next event in the chain of execution.
Return
NextEvent
: The next event in chain to be called.
Example
import { EntityHandler, Req, Results } from '@dxfrontier/cds-ts-dispatcher';
import { MyEntity } from 'YOUR_CDS_TYPER_ENTITIES_LOCATION';
import type { Request, NextEvent } from '@dxfrontier/cds-ts-dispatcher';
@EntityHandler(MyEntity)
export class BookHandler {
// ...
constructor() {}
// ... all events like @AfterRead, @BeforeRead, @OnCreate ...
@OnCreate()
public async onCreate(@Req() req: Request<MyEntity>, @Next() next: NextEvent) {
return next();
}
}
Tip
Decorator @Next
can be applied to all On, On - draft event decorators.
@Error()
The @Error
decorator is utilized at the parameter level
to annotate a parameter with the Error
and contains information regarding the failed Request
.
Return
Error
: An instance of typeError
.
Example
import { UnboundActions, Req, Error } from '@dxfrontier/cds-ts-dispatcher';
import { MyEntity } from 'YOUR_CDS_TYPER_ENTITIES_LOCATION';
import type { Request } from '@dxfrontier/cds-ts-dispatcher';
@UnboundActions()
export class UnboundActionsHandler {
// ...
constructor() {}
@OnError()
public onError(@Error() err: Error, @Req() req: Request): void {
// ...
}
}
Tip
Decorator @Error
can be applied to @OnError() decorator which resides inside of the @UnboundActions().
@Jwt()
The @Jwt
decorator is utilized at the parameter level
. It will retrieve the to retrieve JWT
from the Request
that is based on the node req.http.req - IncomingMessage
.
Fails if no authorization header is given or has the wrong format.
Return
string
|undefined
: The retrievedJWT token
or undefined if no token was found.
Example
import { EntityHandler, Req, Results, Jwt } from '@dxfrontier/cds-ts-dispatcher';
import { MyEntity } from 'YOUR_CDS_TYPER_ENTITIES_LOCATION';
import type { Request } from '@dxfrontier/cds-ts-dispatcher';
@EntityHandler(MyEntity)
export class BookHandler {
// ...
constructor() {}
// ... all events like @AfterRead, @BeforeRead ...
@AfterRead()
private async aMethod(@Req() req: Request, @Results() results: MyEntity[], @Jwt() jwt: string | undefined) {
// ... req...
}
}
Important
Expected format is Bearer <TOKEN>
.
@IsPresent<Key extends CRUDQueryKeys>(key: Key, property: PickQueryPropsByKey<Key>)
The @IsPresent
decorator is utilized at the parameter level
. It allows you to verify the existence of a specified Query property
values.
Parameters
key (string)
: Specifies the type of query operation. Accepted values areINSERT
,SELECT
,UPDATE
,UPSERT
,DELETE
.property (string)
: Specifies the property based on thekey
.
Return
boolean
: This decorator returnstrue
ifproperty
value
is filled,false
otherwise
Example
import { EntityHandler, Req, Results, IsPresent } from '@dxfrontier/cds-ts-dispatcher';
import { MyEntity } from 'YOUR_CDS_TYPER_ENTITIES_LOCATION';
import type { Request } from '@dxfrontier/cds-ts-dispatcher';
@EntityHandler(MyEntity)
class BookHandler {
// ...
constructor() {}
@AfterRead()
private async aMethod(
@Req() req: Request,
@Results() results: MyEntity[],
@IsPresent('SELECT', 'columns') columnsPresent: boolean,
) {
if (columnsPresent) {
// ...
}
// ...
}
}
Tip
Decorator @IsPresent() works well with @GetQuery().
@IsRole(...roles: string[])
The @IsRole
decorator is utilized at the parameter level
. It allows you to verify
if the User
has assigned a given role.
It applies an logical OR
on the specified roles, meaning it checks if at least one
of the specified roles is assigned
Parameters
role (...string[])
: An array of role names to check if are assigned.
Return
boolean
: This decorator returnstrue
if at least one of the specified roles is assigned to the current request user, otherwisefalse
.
Example
import { EntityHandler, Req, Results, IsPresent, IsRole } from '@dxfrontier/cds-ts-dispatcher';
import { MyEntity } from 'YOUR_CDS_TYPER_ENTITIES_LOCATION';
import type { Request } from '@dxfrontier/cds-ts-dispatcher';
@EntityHandler(MyEntity)
class BookHandler {
// ...
constructor() {}
@AfterRead()
private async aMethod(
@Req() req: Request,
@Results() results: MyEntity[],
@IsRole('role', 'anotherRole') roleAssigned: boolean,
) {
if (roleAssigned) {
// ...
}
// ...
}
}
Tip
The role names correspond to the values of @requires
and the @restrict.grants.to
annotations in your CDS
models.
@IsColumnSupplied<T>(field : keyof T)
The @IsColumnSupplied<T>(field : keyof T)
decorator is utilized at the parameter level
. It allows your to verify the existence of a column in the SELECT
, INSERT
or UPSERT
Query.
Parameters
field (string)
: A string representing the name of the column to be verified.
Type Parameters
T
: The entity type (e.g.,MyEntity
) representing the table or collection on which the decorator operates. This allows TypeScript to enforce type safety for the field parameter.
Return
:
boolean
: This decorator returnstrue
iffield / column
was found,false
otherwise
Example
import { EntityHandler, Req, Results, IsPresent } from '@dxfrontier/cds-ts-dispatcher';
import { MyEntity } from 'YOUR_CDS_TYPER_ENTITIES_LOCATION';
import type { Request } from '@dxfrontier/cds-ts-dispatcher';
@EntityHandler(MyEntity)
class BookHandler {
// ...
constructor() {}
@AfterRead()
private async aMethod(
@Req() req: Request,
@Results() results: MyEntity[],
@IsColumnSupplied<MyEntity>('price') priceSupplied: boolean,
) {
if (priceSupplied) {
// ...
}
// ...
}
}
@GetQuery<Key extends CRUDQueryKeys>(key: Key, property: PickQueryPropsByKey<Key>)
The @GetQuery
decorator is utilized at the parameter level
. It allows you to retrieve Query property
values.
Parameters
key (string)
: Specifies the type of query operation. Accepted values areINSERT
,SELECT
,UPDATE
,UPSERT
,DELETE
.property (string)
: Specifies the property based on thekey
.
Return
: Varies based on the specified property :
-
SELECT
- @GetQuery(
'SELECT'
,'columns'
) columns:GetQueryType['columns']['forSelect']
- @GetQuery(
'SELECT'
,'distinct'
) distinct:GetQueryType['distinct']
- @GetQuery(
'SELECT'
,'excluding'
) excluding:GetQueryType['excluding']
- @GetQuery(
'SELECT'
,'from'
) from:GetQueryType['from']['forSelect']
- @GetQuery(
'SELECT'
,'groupBy'
) groupBy:GetQueryType['groupBy']
- @GetQuery(
'SELECT'
,'having'
) having:GetQueryType['having']
- @GetQuery(
'SELECT'
,'limit'
) limit:GetQueryType['limit']
- @GetQuery(
'SELECT'
,'limit.rows'
) limitRows:GetQueryType['limit']['rows']
- @GetQuery(
'SELECT'
,'limit.offset'
) limitOffset:GetQueryType['limit']['offset']
- @GetQuery(
'SELECT'
,'mixin'
) mixin:GetQueryType['mixin']
- @GetQuery(
'SELECT'
,'one'
) one:GetQueryType['one']
- @GetQuery(
'SELECT'
,'orderBy'
) orderBy:GetQueryType['orderBy']
- @GetQuery(
'SELECT'
,'where'
) where:GetQueryType['where']
- @GetQuery(
-
INSERT
- @GetQuery(
'INSERT'
,'as'
) as:GetQueryType['as']
- @GetQuery(
'INSERT'
,'columns'
) columns:GetQueryType['columns']['forInsert']
- @GetQuery(
'INSERT'
,'entries'
) entries:GetQueryType['entries']
- @GetQuery(
'INSERT'
,'into'
) into:GetQueryType['into']
- @GetQuery(
'INSERT'
,'rows'
) rows:GetQueryType['rows']
- @GetQuery(
'INSERT'
,'values'
) values:GetQueryType['values']
- @GetQuery(
-
UPDATE
- @GetQuery(
'UPDATE'
,'data'
) data:GetQueryType['data']
- @GetQuery(
'UPDATE'
,'entity'
) entity:GetQueryType['entity']
- @GetQuery(
'UPDATE'
,'where'
) where:GetQueryType['where']
- @GetQuery(
-
UPSERT
- @GetQuery(
'UPSERT'
,'columns'
) columns:GetQueryType['columns'][forUpsert]
- @GetQuery(
'UPSERT'
,'entries'
) entries:GetQueryType['entries']
- @GetQuery(
'UPSERT'
,'into'
) into:GetQueryType['into']
- @GetQuery(
'UPSERT'
,'rows'
) rows:GetQueryType['rows']
- @GetQuery(
'UPSERT'
,'values'
) values:GetQueryType['values']
- @GetQuery(
-
DELETE
-
@GetQuery(
'DELETE'
,'from'
) from:GetQueryType['from'][forDelete]
-
@GetQuery(
'DELETE'
,'where'
) columns:GetQueryType['where']
-
Example
import { EntityHandler, Req, Results, IsPresent, GetQuery } from '@dxfrontier/cds-ts-dispatcher';
import { MyEntity } from 'YOUR_CDS_TYPER_ENTITIES_LOCATION';
import type { Request, GetQueryType } from '@dxfrontier/cds-ts-dispatcher';
@EntityHandler(MyEntity)
class BookHandler {
// ...
constructor() {}
@AfterRead()
private async aMethod(
@Req() req: Request,
@Results() results: MyEntity[],
// Check existence of columns
@IsPresent('SELECT', 'columns') columnsPresent: boolean,
// Get columns
@GetQuery('SELECT', 'columns') columns: GetQueryType['columns']['forSelect'],
@GetQuery('SELECT', 'orderBy') orderBy: GetQueryType['orderBy'],
@GetQuery('SELECT', 'groupBy') groupBy: GetQueryType['groupBy'],
) {
if (columnsPresent) {
// do something with columns values
// columns.forEach(...)
}
// ...
}
}
Tip
Decorator @GetQuery() can be used to get the Query property and @IsPresent() can check if the Query property is empty or not.
@GetRequest(property : keyof Request)
The @GetRequest
decorator is utilized at the parameter level
. It allows you to retrieve the specified property
value from the Request
object.
Parameters
property (string)
: Specifies the property to retrieve from theRequest
object.
Return
: Varies based on the specified property :
- @GetRequest(
'entity'
) entity:Request['entity']
, - @GetRequest(
'event'
) event:Request['event']
, - @GetRequest(
'features'
) features:Request['features']
, - @GetRequest(
'headers'
) headers:Request['headers']
, - @GetRequest(
'http'
) http:Request['http']
, - @GetRequest(
'id'
) id:Request['id']
, - @GetRequest(
'locale'
) locale:Request['locale']
, - @GetRequest(
'method'
) method:Request['method']
, - @GetRequest(
'params'
) params:Request['params']
, - @GetRequest(
'query'
) query:Request['query']
, - @GetRequest(
'subject'
) subject:Request['subject']
, - @GetRequest(
'target'
) target:Request['target']
, - @GetRequest(
'tenant'
) tenant:Request['tenant']
, - @GetRequest(
'timestamp'
) timestamp:Request['timestamp']
, - @GetRequest(
'user'
) user:Request['user']
,
Example
import { EntityHandler, Results, GetRequest } from '@dxfrontier/cds-ts-dispatcher';
import { MyEntity } from 'YOUR_CDS_TYPER_ENTITIES_LOCATION';
import type { Request } from '@dxfrontier/cds-ts-dispatcher';
@EntityHandler(MyEntity)
class BookHandler {
// ...
constructor() {}
@AfterRead()
private async aMethod(
// @Req() req: Request, we assume we don't need the hole Request object and we need only 'locale' and 'method'
@Results() results: MyEntity[],
@GetRequest('locale') locale: Request['locale'],
@GetRequest('method') method: Request['method'],
) {
// do something with 'locale' and 'method' ...
}
}
Tip
Type Request
can be import from :
import type { Request } from '@dxfrontier/cds-ts-dispatcher';
@SingleInstanceSwitch
The @SingleInstanceSwitch()
decorator is applied at the parameter level
.
It allows you to manage different behaviors based on whether the request is for a single entity instance
or an entity set
, the parameter assigned to the decorator will behave like a switch
.
Return
true
when theRequest
issingle instance
false
when theRequest
isentity set
Example 1
Single request : http://localhost:4004/odata/v4/main/`MyEntity(ID=2f12d711-b09e-4b57-b035-2cbd0a023a09)`
import { AfterRead, SingleInstanceCapable } from "@dxfrontier/cds-ts-dispatcher";
import type { Request } from '@dxfrontier/cds-ts-dispatcher';
import { MyEntity } from 'YOUR_CDS_TYPER_ENTITIES_LOCATION';
@AfterRead()
private async singeInstanceMethodAndEntitySet(@Results() results : MyEntity[], @Req() req: Request<MyEntity>, @SingleInstanceSwitch() isSingleInstance: boolean) {
if(isSingleInstance) {
// This will be executed only when single instance is called : http://localhost:4004/odata/v4/main/MyEntity(ID=2f12d711-b09e-4b57-b035-2cbd0a023a09)
return this.customerService.handleSingleInstance(req)
}
// nothing to entity set
}
Example 2
Entity request : http://localhost:4004/odata/v4/main/`MyEntity`
import { AfterRead, SingleInstanceCapable } from "@dxfrontier/cds-ts-dispatcher";
import type { Request } from '@dxfrontier/cds-ts-dispatcher';
import { MyEntity } from 'YOUR_CDS_TYPER_ENTITIES_LOCATION';
@AfterRead()
private async singeInstanceMethodAndEntitySet(@Results() results : MyEntity[], @Req() req: Request<MyEntity>, @SingleInstanceSwitch() isSingleInstance: boolean) {
if(isSingleInstance) {
// This will be executed only when single instance is called : http://localhost:4004/odata/v4/main/MyEntity(ID=2f12d711-b09e-4b57-b035-2cbd0a023a09)
// ...
}
// ... this will be executed when entity set is called : http://localhost:4004/odata/v4/main/MyEntity
results[0] = {
name : 'new value'
}
}
Tip
Decorator @SingleInstanceSwitch
can be used together with the following decorator events:
Note
MyEntity was generated using CDS-Typer and imported in the the class.
@ValidationResults
The @ValidationResults
decorator allows to capture and inject validation results directly into a method parameter, allowing access to individual validation flags
within the decorated method.
When used alongside the @Validate decorator, it enables you to perform conditional logic based on specific validation outcomes.
Example
@BeforeCreate()
@Validate<MyEntity>({ action: 'isLowercase', exposeValidatorResult: true }, 'comment')
@Validate<MyEntity>({ action: 'endsWith', target: 'N', exposeValidatorResult: true }, 'description')
public async beforeCreate(
@Req() req: Request<MyEntity>,
@ValidationResults() validator: ValidatorFlags<'isLowercase' | 'endsWith'>
) {
// Conditional handling based on validation flags
if (validator.isLowercase) {
// Execute logic when field `comment` is lowercase
}
else {
// Execute logic when field `comment` is not lowercase
}
if (validator.endsWith) {
// Execute logic when field `description` is endsWith with letter 'N'
}
else {
// Execute logic when field `description` doesn't endsWith with letter 'N'
}
}
Important
For @ValidationResults
to work, each @Validate decorator must set the exposeValidatorResult
option to true
. This ensures that the validation results are available as flags in the method.
@Locale
Parameter decorator used to inject locale information into a method parameter.
Example
@BeforeCreate()
public async beforeCreate(
@Req() req: Request<MyEntity>,
@Locale() locale: string
) {
if (locale === 'en-US') {
// handle logic specific to the 'en-US' locale
}
}
@Env<T>(env: PropertyStringPath<T>)
The @Env
decorator is a parameter decorator used to inject values from the cds.env configuration object directly into a method parameter.
Parameters
env (string)
: A string path representing a property fromcds.env
. This path follows the formatproperty string path
, which allows access to deeply nested configuration properties.- E.g. :
'requires.db.credentials.url'
corresponds to cds.env.requires.db.credentials.url object.
- E.g. :
Type Parameters
T
: TheCDS environmental variables
type (e.g.,cds env get
) representing the collection on which the decorator operates. This allows TypeScript to enforce type safety.
Return
:
- The decorator returns the value of the specified
cds.env
property value.
Example
import { CDS_ENV } from '#dispatcher';
@BeforeCreate()
public async beforeCreate(
@Req() req: Request<MyEntity>,
@Env<CDS_ENV>('requires.db.credentials.url') dbUrl: CDS_ENV['requires']['db']['credentials']['url'],
// or @Env<CDS_ENV>('requires.db.credentials.url') dbUrl: string
// or @Env<CDS_ENV>('requires.db.credentials.url') dbUrl: any
// or any other type if you do not want to use the CDS_ENV generated types
) {
if (dbUrl) {
// handle custom logic ...
}
}
Note
When you install cds-ts-dispatcher (e.g. npm install @dxfrontier/cds-ts-dispatcher)
or run a general npm install
, the following will be generated or updated :
- New
@dispatcher
folder is generated at the project root. This folder contains theCDS ENV TS interfaces
, generated based on the structure of your currentcds.env
project specific configuration (retrieved fromcds env get
cli command).
...
@dispatcher
...
package.json
will be updated with a newimport
:
"imports": {
"#dispatcher": "./@dispatcher/index.js"
}
tsconfig.json
will be updated:
"include": [
"...",
"./@dispatcher"
]
.gitignore
will be updated::
...
@dispatcher
Note
The @dispatcher
folder is regenerated each time you run npm install.
Tip
You can import the generated CDS env
from the generated @dispatcher
folder by using :
import { CDS_ENV } from '#dispatcher';
Use @BeforeCreate(), @BeforeRead(), @BeforeUpdate(), @BeforeDelete() to register handlers to run before .on
handlers, frequently used for validating user input.
The handlers receive one argument:
req
of typeRequest
See also the official SAP JS CDS-Before event
Tip
If @odata.draft.enabled: true
to manage event handlers for draft version you can use
@BeforeCreateDraft()
@BeforeReadDraft()
@BeforeUpdateDraft()
@BeforeDeleteDraft()
@BeforeCreate()
Example
import { BeforeCreate } from "@dxfrontier/cds-ts-dispatcher";
import type { Request } from '@dxfrontier/cds-ts-dispatcher';
import { MyEntity } from 'YOUR_CDS_TYPER_ENTITIES_LOCATION';
@BeforeCreate()
private async beforeCreateMethod(@Req() req: Request<MyEntity>) {
// ...
}
Equivalent to 'JS'
this.before('CREATE', MyEntity, async (req) => {
// ...
});
Important
It is important to note that the decorator @BeforeCreate()
will be triggered based on the EntityHandler argument
=> MyEntity
.
Note
MyEntity was generated using CDS-Typer and imported in the the class.
@BeforeRead()
Example
import { BeforeRead } from "@dxfrontier/cds-ts-dispatcher";
import type { Request } from '@dxfrontier/cds-ts-dispatcher';
import { MyEntity } from 'YOUR_CDS_TYPER_ENTITIES_LOCATION';
@BeforeRead()
private async beforeReadMethod(@Req() req: Request<MyEntity>) {
// ...
}
Equivalent to 'JS'
this.before('READ', MyEntity, async (req) => {
// ...
});
Important
Decorator @BeforeRead()
will be triggered based on the EntityHandler argument
=> MyEntity
.
Note
MyEntity was generated using CDS-Typer and imported in the the class.
@BeforeUpdate()
Example
import { BeforeUpdate } from "@dxfrontier/cds-ts-dispatcher";
import type { Request } from '@dxfrontier/cds-ts-dispatcher';
import { MyEntity } from 'YOUR_CDS_TYPER_ENTITIES_LOCATION';
@BeforeUpdate()
private async beforeUpdateMethod(@Req() req: Request<MyEntity>) {
// ...
}
Equivalent to 'JS'
this.before('UPDATE', MyEntity, async (req) => {
// ...
});
Important
Decorator @BeforeUpdate()
will be triggered based on the EntityHandler argument
=> MyEntity
Note
MyEntity was generated using CDS-Typer and imported in the the class.
@BeforeDelete()
Example
import { BeforeDelete } from "@dxfrontier/cds-ts-dispatcher";
import type { Request } from '@dxfrontier/cds-ts-dispatcher';
import { MyEntity } from 'YOUR_CDS_TYPER_ENTITIES_LOCATION';
@BeforeDelete()
private async beforeDeleteMethod(@Req() req: Request<MyEntity>) {
// ...
}
Equivalent to 'JS'
this.before('DELETE', MyEntity, async (req) => {
// ...
});
Important
Decorator @BeforeDelete()
will be triggered based on the EntityHandler argument
=> MyEntity
.
Note
MyEntity was generated using CDS-Typer and imported in the the class.
The @BeforeAll
decorator is triggered whenever any CRUD (Create, Read, Update, Delete) event occurs, whether the entity is active
or in draft
mode.
ACTIVE ENTITY
For active entities, the @BeforeAll decorator will be triggered when at least one of the following events occurs:
CREATE
@BeforeCreate(), @AfterCreate(), @OnCreate()READ
@BeforeRead(), @AfterRead(), @OnRead()UPDATE
@BeforeUpdate(), @AfterUpdate(), @OnUpdate()DELETE
@BeforeDelete(), @AfterDelete(), @OnDelete()BOUND ACTIONS
@OnBoundAction()BOUND FUNCTIONS
@OnBoundFunction()
DRAFT
For draft entities, the @BeforeAll decorator will be triggered when at least one of the following events occurs:
CREATE
@BeforeNewDraft(), @AfterNewDraft(), @OnNewDraft()CANCEL
@BeforeCancelDraft(), @AfterCancelDraft(), @OnCancelDraft()EDIT
@BeforeEditDraft(), @AfterEditDraft(), @OnEditDraft()SAVE
@BeforeSaveDraft(), @AfterSaveDraft(), @OnSaveDraft()- âž• All active entity Before, After, On events which have a
Draft
variant.
@BeforeAll()
Example
import { BeforeAll } from "@dxfrontier/cds-ts-dispatcher";
import type { Request } from '@dxfrontier/cds-ts-dispatcher';
import { MyEntity } from 'YOUR_CDS_TYPER_ENTITIES_LOCATION';
@BeforeAll()
private async beforeAllEvents(@Req() req: Request<MyEntity>) {
// ...
}
Equivalent to 'JS'
this.before('*', MyEntity, async (req) => {
// ...
});
Important
Decorator @BeforeAll()
will be triggered based on the EntityHandler argument
=> MyEntity
.
Tip
If the entity has drafts enabled @odata.draft.enabled: true
, the @BeforeAll
decorator will still be triggered for draft events.
Note
MyEntity was generated using CDS-Typer and imported in the the class.
Use @AfterCreate(), @AfterRead(), @AfterUpdate(), @AfterDelete() register handlers to run after the .on
handlers, frequently used to enrich outbound data.
The handlers receive two arguments:
Parameters | Decorator | Description |
---|---|---|
results, req |
@AfterRead |
An array of type MyEntity[] and the Request . |
result, req |
@AfterUpdate @AfterCreate |
An object of type MyEntity and the Request . |
deleted, req |
@AfterDelete |
A boolean indicating whether the instance was deleted and the Request . |
Tip
If @odata.draft.enabled: true
to manage event handlers for draft version you can use :
@AfterCreateDraft()
@AfterReadDraft()
@AfterReadDraftSingleInstance()
@AfterUpdateDraft()
@AfterDeleteDraft()
@AfterCreate()
Example
import { AfterCreate } from "@dxfrontier/cds-ts-dispatcher";
import type { Request } from '@dxfrontier/cds-ts-dispatcher';
import { MyEntity } from 'YOUR_CDS_TYPER_ENTITIES_LOCATION';
@AfterCreate()
private async afterCreateMethod(@Result() result: MyEntity, @Req() req: Request<MyEntity>) {
// ...
}
Equivalent to 'JS'
this.after('CREATE', MyEntity, async (result, req) => {
// ...
});
Important
Decorator @AfterCreate()
will be triggered based on the EntityHandler argument
=> MyEntity
.
Note
MyEntity was generated using CDS-Typer and imported in the the class.
@AfterRead()
Example
import { AfterRead, Results, Req } from "@dxfrontier/cds-ts-dispatcher";
import type { Request } from '@dxfrontier/cds-ts-dispatcher';
import { MyEntity } from 'YOUR_CDS_TYPER_ENTITIES_LOCATION';
@AfterRead()
private async afterReadMethod(@Results() results: MyEntity[], @Req() req: Request<MyEntity>) {
// ...
}
Equivalent to 'JS'
this.after('READ', MyEntity, async (results, req) => {
// ...
});
Important
Decorator @AfterRead()
will be triggered based on the EntityHandler argument
MyEntity
.
Note
MyEntity was generated using CDS-Typer and imported in the the class.
@AfterReadEachInstance()
The @AfterReadEachInstance
decorator is used to execute custom logic after performing a read operation on each individual instance
. This behavior is analogous to the JavaScript Array.prototype.forEach
method.
Example
import { AfterReadEachInstance, Result, Req } from "@dxfrontier/cds-ts-dispatcher";
import type { Request } from '@dxfrontier/cds-ts-dispatcher';
import { MyEntity } from 'YOUR_CDS_TYPER_ENTITIES_LOCATION';
@AfterReadEachInstance()
private async afterEach(@Result() result: MyEntity, @Req() req: Request<MyEntity>) {
// ...
}
Equivalent to 'JS'
this.after('each', MyEntity, async (result, req) => {
// ...
});
Important
Decorator @AfterReadEachInstance()
will be triggered based on the EntityHandler argument
MyEntity
.
Note
MyEntity was generated using CDS-Typer and imported in the the class.
@AfterUpdate()
Example
Single request : http://localhost:4004/odata/v4/main/`MyEntity(ID=2f12d711-b09e-4b57-b035-2cbd0a023a09)`
import { AfterUpdate } from "@dxfrontier/cds-ts-dispatcher";
import type { Request } from '@dxfrontier/cds-ts-dispatcher';
import { MyEntity } from 'YOUR_CDS_TYPER_ENTITIES_LOCATION';
@AfterUpdate()
private async afterUpdateMethod(@Result() result: MyEntity, @Req() req: Request<MyEntity>) {
// ...
}
Equivalent to 'JS'
this.after('UPDATE', MyEntity, async (result, req) => {
// ...
});
Important
Decorator @AfterUpdate()
will be triggered based on the EntityHandler argument
=> MyEntity
.
Note
MyEntity was generated using CDS-Typer and imported in the the class.
@AfterDelete()
Example
import { AfterDelete} from "@dxfrontier/cds-ts-dispatcher";
import type { Request } from '@dxfrontier/cds-ts-dispatcher';
import { MyEntity } from 'YOUR_CDS_TYPER_ENTITIES_LOCATION';
@AfterDelete()
private async afterDeleteMethod(@Result() deleted: boolean, @Req() req: Request) {
// ...
}
Equivalent to 'JS'
this.after('DELETE', MyEntity, async (deleted, req) => {
// ...
});
Important
Decorator @AfterDelete()
will be triggered based on the EntityHandler argument
=> MyEntity
.
Note
MyEntity was generated using CDS-Typer and imported in the the class.
The @AfterAll
decorator is triggered whenever any CRUD (Create, Read, Update, Delete) event occurs, whether the entity is active
or in draft
mode.
ACTIVE ENTITY
For active entities, the @BeforeAll decorator will be triggered when at least one of the following events occurs:
CREATE
@BeforeCreate(), @AfterCreate(), @OnCreate()READ
@BeforeRead(), @AfterRead(), @OnRead()UPDATE
@BeforeUpdate(), @AfterUpdate(), @OnUpdate()DELETE
@BeforeDelete(), @AfterDelete(), @OnDelete()BOUND ACTIONS
@OnBoundAction()BOUND FUNCTIONS
@OnBoundFunction()
DRAFT
For draft entities, the @BeforeAll decorator will be triggered when at least one of the following events occurs:
CREATE
@BeforeNewDraft(), @AfterNewDraft(), @OnNewDraft()CANCEL
@BeforeCancelDraft(), @AfterCancelDraft(), @OnCancelDraft()EDIT
@BeforeEditDraft(), @AfterEditDraft(), @OnEditDraft()SAVE
@BeforeSaveDraft(), @AfterSaveDraft(), @OnSaveDraft()- âž• All active entity Before, After, On events which have a
Draft
variant.
@AfterAll()
Example
import { AfterAll} from "@dxfrontier/cds-ts-dispatcher";
import type { Request } from '@dxfrontier/cds-ts-dispatcher';
import { MyEntity } from 'YOUR_CDS_TYPER_ENTITIES_LOCATION';
@AfterAll()
private async afterAll(@Result() result: MyEntity | MyEntity[] | boolean, @Req() req: Request) {
if(Array.isArray(result)) {
// when after `READ` event was triggered
}
else if(typeof result === 'boolean' ) {
// when after `DELETE` event was triggered
}
else {
// when after `CREATE`, `UPDATE` was triggered
}
// ...
}
Equivalent to 'JS'
this.after('*', MyEntity, async (result, req) => {
// ...
});
Important
Decorator @AfterAll()
will be triggered based on the EntityHandler argument
=> MyEntity
.
Tip
If the entity has drafts enabled @odata.draft.enabled: true
, the @AfterAll
decorator will still be triggered for draft events.
Note
MyEntity was generated using CDS-Typer and imported in the the class.
Use @OnCreate(), @OnRead(), @OnUpdate(), @OnDelete(), OnAction(), @OnFunction(), @OnBoundAction(), @OnBoundFunction() handlers to fulfill requests, e.g. by reading/writing data from/to databases handlers.
The handlers receive two arguments:
req
of typeRequest
next
of typeNextEvent
Tip
If @odata.draft.enabled: true
to manage event handlers for draft version you can use :
@OnCreateDraft()
@OnReadDraft()
@OnUpdateDraft()
@OnDeleteDraft()
@OnBoundActionDraft()
@OnBoundFunctionDraft()
@OnCreate()
Example
import { OnCreate, Next } from "@dxfrontier/cds-ts-dispatcher";
import type { Request, NextEvent } from '@dxfrontier/cds-ts-dispatcher';
import { MyEntity } from 'YOUR_CDS_TYPER_ENTITIES_LOCATION';
@OnCreate()
private async onCreateMethod(@Req() req: Request<MyEntity>, @Next() next: NextEvent) {
// ...
return next();
}
Equivalent to 'JS'
this.on('CREATE', MyEntity, async (req, next) => {
// ...
});
Important
Decorator @OnCreate()
will be triggered based on the EntityHandler argument
=> MyEntity
.
Note
MyEntity was generated using CDS-Typer and imported in the the class.
@OnRead()
Example
import { OnRead, Next } from "@dxfrontier/cds-ts-dispatcher";
import type { Request, NextEvent } from '@dxfrontier/cds-ts-dispatcher';
import { MyEntity } from 'YOUR_CDS_TYPER_ENTITIES_LOCATION';
@OnRead()
private async onReadMethod(@Req() req: Request<MyEntity>, @Next() next: NextEvent) {
// ...
return next();
}
Equivalent to 'JS'
this.on('READ', MyEntity, async (req, next) => {
// ...
});
Important
Decorator @OnRead()
will be triggered based on the EntityHandler argument
=> MyEntity
.
Note
MyEntity was generated using CDS-Typer and imported in the the class.
@OnUpdate()
Example
import { OnUpdate, Next } from "@dxfrontier/cds-ts-dispatcher";
import type { Request, NextEvent } from '@dxfrontier/cds-ts-dispatcher';
import { MyEntity } from 'YOUR_CDS_TYPER_ENTITIES_LOCATION';
@OnUpdate()
private async onUpdateMethod(@Req() req: Request<MyEntity>, @Next() next: NextEvent) {
// ...
return next();
}
Equivalent to 'JS'
this.on('UPDATE', MyEntity, async (req, next) => {
// ...
});
Important
Decorator @OnUpdate()
will be triggered based on the EntityHandler argument
=> MyEntity
.
Note
MyEntity was generated using CDS-Typer and imported in the the class.
@OnDelete()
Example
import { OnDelete, Next } from "@dxfrontier/cds-ts-dispatcher";
import type { Request, NextEvent } from '@dxfrontier/cds-ts-dispatcher';
import { MyEntity } from 'YOUR_CDS_TYPER_ENTITIES_LOCATION';
@OnDelete()
private async onDeleteMethod(@Req() req: Request<MyEntity>, @Next() next: NextEvent) {
// ...
return next();
}
Equivalent to 'JS'
this.on('DELETE', MyEntity, async (req, next) => {
// ...
});
Important
Decorator @OnDelete()
will be triggered based on the EntityHandler argument
=> MyEntity
.
Note
MyEntity was generated using CDS-Typer and imported in the the class.
@OnAction(name
: CdsAction)
Parameters
name (CdsAction)
: Representing theCDS action
defined in theCDS file
Example
import { OnAction, Req, Next } from "@dxfrontier/cds-ts-dispatcher";
import type { ActionRequest, ActionReturn, NextEvent } from '@dxfrontier/cds-ts-dispatcher';
import { AnAction } from 'YOUR_CDS_TYPER_ENTITIES_LOCATION';
@OnAction(AnAction)
private async onActionMethod(@Req() req: ActionRequest<typeof AnAction>, @Next() next: NextEvent): ActionReturn<typeof AnAction> {
// ...
}
Equivalent to 'JS'
this.on(AnAction, async (req, next) => {
// ...
});
Note
AnAction was generated using CDS-Typer and imported in the the class.
Important
Decorator @OnAction
should be used inside @UnboundActions() class.
@OnFunction(name
: CdsFunction)
Parameters
name (CdsFunction)
: Representing theCDS action
defined in theCDS file
.
Example
import { OnFunction, Req, Next } from "@dxfrontier/cds-ts-dispatcher";
import type { ActionRequest, ActionReturn, NextEvent } from '@dxfrontier/cds-ts-dispatcher';
import { AFunction } from 'YOUR_CDS_TYPER_ENTITIES_LOCATION';
@OnFunction(AFunction)
private async onFunctionMethod(@Req() req: ActionRequest<typeof AFunction>, @Next() next: NextEvent): ActionReturn<typeof AFunction> {
// ...
}
Equivalent to 'JS'
this.on(AFunction, async (req) => {
// ...
});
Note
AFunction was generated using CDS-Typer and imported in the the class.
Important
Decorator @OnFunction
should be used inside @UnboundAction() class.
@OnEvent(name
: CdsEvent)
The @OnEvent
decorator facilitates the listening of messages from a message broker.
This decorator is particularly useful in conjunction with the Emit method to handle triggered events.
Parameters
name (CdsEvent)
: Representing theCDS event
defined in theCDS file
.
Example
import { OnEvent, Req } from "@dxfrontier/cds-ts-dispatcher";
import type { Request } from '@dxfrontier/cds-ts-dispatcher';
import { AEvent } from 'YOUR_CDS_TYPER_ENTITIES_LOCATION';
@OnEvent(AEvent)
private async onEventMethod(@Req() req: Request<AEvent>) {
// ...
}
Equivalent to 'JS'
this.on('AEvent', async (req) => {
// ...
});
[!NOTE] > AEvent was generated using CDS-Typer and imported in the the class.
Important
Decorator @OnEvent
should be used inside @UnboundActions class.
Tip
More info can be found at https://cap.cloud.sap/docs/guides/messaging/
@OnError()
Use @OnError
decorator to register custom error handler.
Error handlers are invoked whenever an error occurs during event processing of all potential events and requests, and are used to augment or modify error messages, before they go out to clients.
Example
import { OnError, Error, Req } from "@dxfrontier/cds-ts-dispatcher";
import type { Request } from '@dxfrontier/cds-ts-dispatcher';
@OnError()
private onError(@Error() err: Error, @Req() req: Request) { // sync func
err.message = 'New message'
// ...
}
Equivalent to 'JS'
this.on('error', (err, req) => {
err.message = 'New message';
// ...
});
Important
Decorator @OnError
should be used inside @UnboundActions class.
Caution
OnError callback are expected to be a sync
function, i.e., not async
, not returning Promises
.
Tip
More info can be found at SAP CAP Error
@OnBoundAction(name
: CdsAction)
Parameters
name (CdsAction)
: Representing theCDS action
defined in theCDS file
.
Example
import { OnBoundAction, Req, Next } from "@dxfrontier/cds-ts-dispatcher";
import type { ActionRequest, ActionReturn, NextEvent } from '@dxfrontier/cds-ts-dispatcher';
import { MyEntity } from 'YOUR_CDS_TYPER_ENTITIES_LOCATION';
@OnBoundAction(MyEntity.actions.AnAction)
private async onActionMethod(@Req() req: ActionRequest<typeof MyEntity.actions.AnAction>, @Next() next: NextEvent): ActionReturn<typeof MyEntity.actions.AnAction> {
// ...
}
Equivalent to 'JS'
this.on(MyEntity.actions.AnAction, MyEntity, async (req) => {
// ...
});
Important
Decorator @OnBoundAction()
will be triggered based on the EntityHandler argument
=> MyEntity
.
Note
MyEntity was generated using CDS-Typer and imported in the the class.
@OnBoundFunction(name
: CdsFunction)
Parameters
name (CdsFunction)
: Representing theCDS action
defined in theCDS file
.
Example
import { OnBoundFunction, Req, Next } from "@dxfrontier/cds-ts-dispatcher";
import type { ActionRequest, ActionReturn, NextEvent } from '@dxfrontier/cds-ts-dispatcher';
import { MyEntity } from 'YOUR_CDS_TYPER_ENTITIES_LOCATION';
@OnBoundFunction(MyEntity.actions.AFunction)
private async onFunctionMethod(@Req() req: ActionRequest<typeof MyEntity.actions.AFunction>, @Next() next: NextEvent): ActionReturn<typeof MyEntity.actions.AFunction> {
// ...
}
Equivalent to 'JS'
this.on(MyEntity.actions.AFunction, MyEntity, async (req) => {
// ...
});
Important
Decorator @OnBoundFunction()
will be triggered based on the EntityHandler argument
=> MyEntity
.
Note
MyEntity was generated using CDS-Typer and imported in the the class.
@OnAll()
The @OnAll
decorator is triggered whenever any CRUD (Create, Read, Update, Delete) event occurs, whether the entity is active
or in draft
mode.
ACTIVE ENTITY
For active entities, the @BeforeAll decorator will be triggered when at least one of the following events occurs:
CREATE
@BeforeCreate(), @AfterCreate(), @OnCreate()READ
@BeforeRead(), @AfterRead(), @OnRead()UPDATE
@BeforeUpdate(), @AfterUpdate(), @OnUpdate()DELETE
@BeforeDelete(), @AfterDelete(), @OnDelete()BOUND ACTIONS
@OnBoundAction()BOUND FUNCTIONS
@OnBoundFunction()
DRAFT
For draft entities, the @BeforeAll decorator will be triggered when at least one of the following events occurs:
CREATE
@BeforeNewDraft(), @AfterNewDraft(), @OnNewDraft()CANCEL
@BeforeCancelDraft(), @AfterCancelDraft(), @OnCancelDraft()EDIT
@BeforeEditDraft(), @AfterEditDraft(), @OnEditDraft()SAVE
@BeforeSaveDraft(), @AfterSaveDraft(), @OnSaveDraft()- âž• All active entity Before, After, On events which have a
Draft
variant.
Note
Exception will be the following decorators @OnEvent(), @OnError() and UNBOUND ACTIONS
@OnAction(), UNBOUND FUNCTIONS
@OnFunction() as these are bound to the service itself and not to an entity.
Example
import { OnAll, Next } from "@dxfrontier/cds-ts-dispatcher";
import type { Request, NextEvent } from '@dxfrontier/cds-ts-dispatcher';
import { MyEntity } from 'YOUR_CDS_TYPER_ENTITIES_LOCATION';
@OnAll()
private async onAll(@Req() req: Request, @Next() next: NextEvent) {
// ...
return next();
}
Equivalent to 'JS'
this.on('*', MyEntity, async (req, next) => {
// ...
});
Important
Decorator @OnAll()
will be triggered based on the EntityHandler argument
=> MyEntity
.
Tip
If the entity has drafts enabled @odata.draft.enabled: true
, the @OnAll
decorator will still be triggered for draft events.
Note
MyEntity was generated using CDS-Typer and imported in the the class.
Use @BeforeNewDraft(), @BeforeCancelDraft(), @BeforeEditDraft(), @BeforeSaveDraft(), @BeforeCreateDraft(), @BeforeReadDraft(), @BeforeUpdateDraft(), @BeforeDeleteDraft()
to register handlers to run before.on
handlers, frequently used for validating user input.
The handlers receive one argument:
req
of typeRequest
@BeforeNewDraft()
Use this decorator when you want to validate inputs before a new draft is created.
Example
import { BeforeNewDraft, Request } from "@dxfrontier/cds-ts-dispatcher";
import type { Request } from '@dxfrontier/cds-ts-dispatcher';
import { MyEntity } from 'YOUR_CDS_TYPER_ENTITIES_LOCATION';
@BeforeNewDraft()
private async beforeCreateDraftMethod(@Req() req: Request<MyEntity>) {
// ...
}
Equivalent to 'JS'
this.before('NEW', MyEntity.drafts, async (req) => {
// ...
});
Important
Decorator @BeforeNewDraft()
will be triggered based on the EntityHandler argument
=> MyEntity
.
Note
MyEntity was generated using CDS-Typer and imported in the the class.
@BeforeCancelDraft()
Use this decorator when you want to validate inputs before a draft is discarded.
Example
import { BeforeCancelDraft } from "@dxfrontier/cds-ts-dispatcher";
import type { Request } from '@dxfrontier/cds-ts-dispatcher';
import { MyEntity } from 'YOUR_CDS_TYPER_ENTITIES_LOCATION';
@BeforeCancelDraft()
private async beforeCancelDraftMethod(@Req() req: Request<MyEntity>) {
// ...
}
Equivalent to 'JS'
this.before('CANCEL', MyEntity.drafts, async (req) => {
// ...
});
Important
Decorator @BeforeCancelDraft()
will be triggered based on the EntityHandler argument
=> MyEntity
.
Note
MyEntity was generated using CDS-Typer and imported in the the class.
@BeforeEditDraft()
Use this decorator when you want to validate inputs when a new draft is created from an active instance.
Example
import { BeforeEditDraft } from "@dxfrontier/cds-ts-dispatcher";
import type { Request } from '@dxfrontier/cds-ts-dispatcher';
import { MyEntity } from 'YOUR_CDS_TYPER_ENTITIES_LOCATION';
@BeforeEditDraft()
private async beforeEditDraftMethod(@Req() req: Request<MyEntity>) {
// ...
}
Equivalent to 'JS'
this.before('EDIT', MyEntity, async (req) => {
// ...
});
Important
Decorator @BeforeEditDraft()
will be triggered based on the EntityHandler argument
=> MyEntity
.
Note
MyEntity was generated using CDS-Typer and imported in the the class.
@BeforeSaveDraft()
Use this decorator when you want to validate inputs when active entity is changed.
Example
import { BeforeSaveDraft } from "@dxfrontier/cds-ts-dispatcher";
import type { Request } from '@dxfrontier/cds-ts-dispatcher';
import { MyEntity } from 'YOUR_CDS_TYPER_ENTITIES_LOCATION';
@BeforeSaveDraft()
private async beforeSaveDraftMethod(@Req() req: Request<MyEntity>) {
// ...
}
Equivalent to 'JS'
this.before('SAVE', MyEntity, async (req) => {
// ...
});
Important
Decorator @BeforeSaveDraft()
will be triggered based on the EntityHandler argument
=> MyEntity
.
Note
MyEntity was generated using CDS-Typer and imported in the the class.
Use @AfterNewDraft(), @AfterCancelDraft(), @AfterEditDraft(), @AfterSaveDraft(), @AfterCreateDraft(), @AfterReadDraft(), @AfterUpdateDraft(), @AfterDeleteDraft()
register handlers to run after the .on
handlers, frequently used to enrich outbound data.
The handlers receive two arguments:
The results from the preceding .on
handler, with the following types:
Parameters | Decorator | Description |
---|---|---|
results, req |
@AfterRead |
An array of type MyEntity[] and the Request . |
result, req |
@AfterUpdate @AfterCreate |
An object of type MyEntity and the Request . |
deleted, req |
@AfterDelete |
A boolean indicating whether the instance was deleted and the Request . |
@AfterNewDraft()
Use this decorator when you want to enhance outbound data when a new draft is created.
Example
import { AfterNewDraft } from "@dxfrontier/cds-ts-dispatcher";
import type { Request } from '@dxfrontier/cds-ts-dispatcher';
import { MyEntity } from 'YOUR_CDS_TYPER_ENTITIES_LOCATION';
@AfterNewDraft()
private async afterNewDraftMethod(@Result() result: MyEntity, @Req() req: Request<MyEntity>) {
// ...
}
Equivalent to 'JS'
this.after('NEW', MyEntity.drafts, async (results, req) => {
// ...
});
Important
Decorator @AfterNewDraft()
will be triggered based on the EntityHandler argument
=> MyEntity
.
Note
MyEntity was generated using CDS-Typer and imported in the the class.
@AfterCancelDraft()
Use this decorator when you want to enhance outbound data when a draft is discarded.
Example
import { AfterCancelDraft } from "@dxfrontier/cds-ts-dispatcher";
import type { Request } from '@dxfrontier/cds-ts-dispatcher';
import { MyEntity } from 'YOUR_CDS_TYPER_ENTITIES_LOCATION';
@AfterCancelDraft()
private async afterCancelDraftMethod(@Result() result: MyEntity, @Req() req: Request<MyEntity>) {
// ...
}
Equivalent to 'JS'
this.after('CANCEL', MyEntity.drafts, async (results, req) => {
// ...
});
Important
Decorator @AfterCancelDraft()
will be triggered based on the EntityHandler argument
=> MyEntity
.
Note
MyEntity was generated using CDS-Typer and imported in the the class.
@AfterEditDraft()
Use this decorator when you want to enhance outbound data when a new draft is created from an active instance.
Example
import { AfterEditDraft } from "@dxfrontier/cds-ts-dispatcher";
import type { Request } from '@dxfrontier/cds-ts-dispatcher';
import { MyEntity } from 'YOUR_CDS_TYPER_ENTITIES_LOCATION';
@AfterEditDraft()
private async afterEditDraftMethod(@Result() result: MyEntity, @Req() req: Request<MyEntity>) {
// ...
}
Equivalent to 'JS'
this.after('EDIT', MyEntity, async (results, req) => {
// ...
});
Important
Decorator @AfterEditDraft()
will be triggered based on the EntityHandler argument
=> MyEntity
.
Note
MyEntity was generated using CDS-Typer and imported in the the class.
@AfterSaveDraft()
Use this decorator when you want to enhance outbound data when the active entity is changed.
Example
import { AfterSaveDraft } from "@dxfrontier/cds-ts-dispatcher";
import type { Request } from '@dxfrontier/cds-ts-dispatcher';
import { MyEntity } from 'YOUR_CDS_TYPER_ENTITIES_LOCATION';
@AfterSaveDraft()
private async afterSaveDraftMethod(@Result() result: MyEntity, @Req() req: Request<MyEntity>) {
// ...
}
Equivalent to 'JS'
this.after('SAVE', MyEntity, async (results, req) => {
// ...
});
Important
Decorator @AfterSaveDraft()
will be triggered based on the EntityHandler argument
=> MyEntity
.
Note
MyEntity was generated using CDS-Typer and imported in the the class.
Use @OnNewDraft(), @OnCancelDraft(), @OnSaveDraft(), @OnEditDraft(), @OnReadDraft(), @OnUpdateDraft(), @OnCreateDraft(), @OnDeleteDraft(), @OnBoundActionDraft(), @OnBoundFunctionDraft() handlers to support for both, active and draft entities.
The handlers receive two arguments:
req
of typeRequest
next
of typeNextEvent
See Official SAP Fiori-draft
@OnNewDraft()
This decorator will be triggered when a new draft is created
.
Example
import { OnNewDraft, Req, Next } from "@dxfrontier/cds-ts-dispatcher";
import type { Request, NextEvent } from '@dxfrontier/cds-ts-dispatcher';
import { MyEntity } from 'YOUR_CDS_TYPER_ENTITIES_LOCATION';
@OnNewDraft()
private async onNewDraft(@Req() req: Request<MyEntity>, @Next() next: NextEvent) {
// ...
}
Equivalent to 'JS'
this.on('NEW', MyEntity.drafts, async (req, next) => {
// ...
});
Important
Decorator @OnNewDraft()
will be triggered based on the EntityHandler argument
=> MyEntity
.
Note
MyEntity was generated using CDS-Typer and imported in the the class.
@OnCancelDraft()
This decorator will be triggered when a draft is cancelled
.
Example
import { OnCancelDraft, Req, Next } from "@dxfrontier/cds-ts-dispatcher";
import type { Request, NextEvent } from '@dxfrontier/cds-ts-dispatcher';
import { MyEntity } from 'YOUR_CDS_TYPER_ENTITIES_LOCATION';
@OnCancelDraft()
private async onCancelDraft(@Req() req: Request<MyEntity>, @Next() next: NextEvent) {
// ...
}
Equivalent to 'JS'
this.on('CANCEL', MyEntity.drafts, async (req, next) => {
// ...
});
Important
Decorator @OnCancelDraft()
will be triggered based on the EntityHandler argument
=> MyEntity
.
Note
MyEntity was generated using CDS-Typer and imported in the the class.
@OnEditDraft()
This decorator will be triggered when a new draft is created from an active instance
Example
import { OnEditDraft, Req, Next } from "@dxfrontier/cds-ts-dispatcher";
import type { Request, NextEvent } from '@dxfrontier/cds-ts-dispatcher';
import { MyEntity } from 'YOUR_CDS_TYPER_ENTITIES_LOCATION';
@OnEditDraft()
private async onEditDraft(@Req() req: Request<MyEntity>, @Next() next: NextEvent) {
// ...
}
Equivalent to 'JS'
this.on('EDIT', MyEntity, async (req, next) => {
// ...
});
Important
Decorator @OnEditDraft()
will be triggered based on the EntityHandler argument
=> MyEntity
.
Note
MyEntity was generated using CDS-Typer and imported in the the class.
@OnSaveDraft()
This decorator will be triggered when the active entity is changed
Example
import { OnSaveDraft, Req, Next } from "@dxfrontier/cds-ts-dispatcher";
import type { Request, NextEvent } from '@dxfrontier/cds-ts-dispatcher';
import { MyEntity } from 'YOUR_CDS_TYPER_ENTITIES_LOCATION';
@OnSaveDraft()
private async onSaveDraft(@Req() req: Request<MyEntity>, @Next() next: NextEvent) {
// ...
}
Equivalent to 'JS'
this.on('SAVE', MyEntity, async (req, next) => {
// ...
});
Important
Decorator @OnSaveDraft()
will be triggered based on the EntityHandler argument
=> MyEntity
.
Note
MyEntity was generated using CDS-Typer and imported in the the class.
All active entity On, Before, After events have also a Draft
variant.
Note
Except the @OnAction(), @OnFunction(), @OnEvent(), @OnError() as these actions are bound to the service and not to an entity.
@AfterReadSingleInstance()
The @AfterReadSingleInstance
decorator is utilized as a method-level and it can be used when you want to execute custom logic for single instance request.
If you want to execute logic for both cases (single instance and entity set) then you should use the @AfterRead() and applying the @SingleInstanceSwitch() decorator to switch between entity set and single instance.
Example
Single request : http://localhost:4004/odata/v4/main/MyEntity(ID=2f12d711-b09e-4b57-b035-2cbd0a023a09)
import { AfterReadSingleInstance, Result, Req } from "@dxfrontier/cds-ts-dispatcher";
import type { Request } from '@dxfrontier/cds-ts-dispatcher';
import { MyEntity } from 'YOUR_CDS_TYPER_ENTITIES_LOCATION';
@AfterReadSingleInstance()
private async afterReadSingleInstance(@Result() result: MyEntity, @Req() req: Request<MyEntity>) {
// This will be executed only when single instance is called : http://localhost:4004/odata/v4/main/MyEntity(ID=2f12d711-b09e-4b57-b035-2cbd0a023a09)
// ...
}
Caution
If @AfterReadSingleInstance() is used all together with @AfterRead(), the event GET (Read), PATCH (Update)
will trigger both decorators, this applies only when both decorators are used in the same @EntityHandler().
The vice-versa doesn't apply, this means that if you trigger the entity set request the @AfterReadSingleInstance() will not be triggered.
Example GET
: http://localhost:4004/odata/v4/main/MyEntity(ID=2f12d711-b09e-4b57-b035-2cbd0a023a09)
// use this
@AfterReadSingleInstance()
private async afterReadSingleInstance(
@Req() req: Request,
@Result() result: MyEntity
): Promise<void> {
// The `GET` will trigger the single instance request
}
// or this as alternative to above `@AfterReadSingleInstance`
@AfterRead()
private async afterRead(
@Req() req: Request,
@Results() results: MyEntity[],
@SingleInstanceSwitch() singleInstance: boolean
): Promise<void> {
// The `GET` will trigger for both cases (single instance & entity instance), but you can use the `singleInstance` flag to verify if it's single or entity set.
if(singleInstance) {
}
}
Important
Decorator @AfterReadSingleInstance() will be triggered based on the EntityHandler argument
MyEntity
.
Tip
If @odata.draft.enabled: true
and you need to read the draft then you should use @AfterReadDraftSingleInstance()
decorator.
Note
MyEntity was generated using CDS-Typer and imported in the the class.
@Prepend({ eventDecorator : string })
The @Prepend
decorator is utilized as a method-level
decorator to register an event handler to run before existing ones like @BeforeCreate() @AfterCreate() @OnAction() ..., etc.
Parameters
eventDecorator (string)
: The eventDecorator can be one of the following :BEFORE
:'BeforeCreate'
,'BeforeRead'
,'BeforeUpdate'
,'BeforeDelete'
,'BeforeAll'
.AFTER
:'AfterCreate'
,'AfterRead'
,'AfterReadEachInstance'
,'AfterReadSingleInstance'
,'AfterUpdate'
,'AfterDelete'
,'AfterAll'
.ON:
'OnCreate'
,'OnRead'
,'OnUpdate'
,'OnDelete'
,'OnAll'
,'OnAction'
,'OnFunction'
,'OnBoundAction'
,'OnBoundFunction'
,'OnEvent'
,'OnError'
.
actionName: (CDSFunction)
: Action name, applicable only forOnAction
,OnBoundAction
,OnFunction
,OnBoundFunction
.eventName: (CDSEvent)
: Event name, applicable only forOnEvent
.
Example 1
import { Prepend, AfterRead, Req, Results } from '@dxfrontier/cds-ts-dispatcher';
import type { Request } from '@dxfrontier/cds-ts-dispatcher';
import { MyEntity } from 'YOUR_CDS_TYPER_ENTITIES_LOCATION';
@Prepend({ eventDecorator: 'AfterRead' })
private async prepend(@Req() req: Request): Promise<void> {
req.locale = 'DE_de';
}
@AfterRead()
private async afterRead(MyEntity
@Req() req: Request,
@Results() results: MyEntity[],
) {
// req.locale will have the value 'DE_de' ...
// ...
}
Example 2
import { Prepend, OnEvent, Req } from '@dxfrontier/cds-ts-dispatcher';
import type { Request } from '@dxfrontier/cds-ts-dispatcher';
import { MyEvent } from 'YOUR_CDS_TYPER_ENTITIES_LOCATION';
@Prepend({ eventDecorator: 'OnEvent', eventName: MyEvent })
private async prepend(@Req() req: Request<MyEvent>) {
req.locale = 'DE_de';
}
@OnEvent(MyEvent)
public async handleMyEvent(@Req() req: Request<MyEvent>) {
// req.locale will have the value 'DE_de' ...
// ...
}
Tip
The @Prepend
decorator can be used for example :
- When you want to prepare various things
before
reaching the actual event. - Making transformation on
Request
,Result
, ...before
reaching the actual event. - ...
Tip
If @odata.draft.enabled: true
and you need to read the draft then you should use @PrependDraft()
decorator.
@Validate<T>({ action, options? }, ...fields: Array<keyof T>)
The @Validate
decorator is utilized as a method-level
decorator, used to validate fields
of your entity before reaching your event callback.
Tip
Think of it as a pre-validation
helper.
The @Validate
decorator is useful when you need to validate
the Request
.data
(Request Body) of the @sap/cds - Request
object in the following contexts:
ON
BEFORE
Parameters
-
action: Choose from a list of predefined Validators .
-
options?: [Optional] Additional options for customizing the validation process.
The
options
parameter accepts the following settings to customize validation behavior:-
exposeValidatorResult
(boolean):
Determines if the validator results are captured and passed to the method as parameters.- If
true
, the validator results (e.g.,ValidatorFlags<'isBoolean' | 'equals'>
) will be available as a parameter in the method. - To use this, apply the
@ValidationResults
decorator to the method parameter where you want the validator flags injected.
Default:
false
Example:
@Validate<MyEntity>({ action: 'endsWith', target: 'N', exposeValidatorResult: true }, 'description') public async beforeCreate( @Req() req: Request<MyEntity>, @ValidationResults() validator: ValidatorFlags<'isBoolean' | 'equals'>, ) { if (validator.isBoolean) { // logic based on validation result } }
- If
-
customMessage
(string):
Custom message to replace the default validation error message.- If provided, this message will be shown instead of the default error.
-
mandatoryFieldValidation
(boolean):
Controls whether the validator requires the presence of the fields inRequest.data
.- If
true
, the specified fields must be present inRequest.data
, or validation will fail. - If
false
(or omitted), validation will pass even if the field is missing.
Default:
false
- If
-
-
...fields: Specify the fields of your entity that require validation.
Returns
The decorator will raise a Request.reject
message if the validation requirements are not met.
Validators
Below is a list of available validators:
Action | Description | Options |
---|---|---|
contains | Check if the string contains the seed. | ignoreCase: boolean | undefined; default : false minOccurrences: number | undefined; default 1 |
equals | Check if the string matches the comparison. | |
matches | Check if the string matches the pattern. | |
startsWith | Checks if the string starts with the given target string. |
|
endsWith | Checks if the string ends with the given target string. |
|
isMailtoURI | Check if the string is a Mailto URI format. | allow_display_name: If set to true , the validator will also match Display Name <email-address> . Default: false require_display_name: If set to true , the validator will reject strings without the format Display Name <email-address> . Default: false allow_utf8_local_part: If set to false , the validator will not allow any non-English UTF8 character in the email address' local part. Default: true require_tld: If set to false , email addresses without a TLD in their domain will also be matched. Default: true ignore_max_length: If set to true , the validator will not check for the standard max length of an email. Default: false allow_ip_domain: If set to true , the validator will allow IP addresses in the host part. Default: false domain_specific_validation: If set to true , some additional validation will be enabled, e.g., disallowing certain syntactically valid email addresses that are rejected by GMail. Default: false host_blacklist: An array of strings. If the part of the email after the @ symbol matches any string in the array, validation fails. host_whitelist: An array of strings. If the part of the email after the @ symbol does not match any string in the array, validation fails. blacklisted_chars: A string. If any character in the string appears in the name part of the email, validation fails. |
isNumeric | Check if the string contains only numbers. | no_symbols: If set to locale: An object specifying the locale information for alpha validation. |
isTime | Check if the string is a valid time e.g. 23:01:59 . |
hourFormat: Specify the hour format to validate. Use mode: Specify the time format to validate. Use |
isLatLong | check if the string is a valid latitude-longitude coordinate in the format lat,long or lat, long . |
|
isMD5 | check if the string is a MD5 hash. | |
isMimeType | check if the string matches to a valid [MIME type] format. | |
isPort | check if the string is a valid port number. | |
isSlug | check if the string is of type slug. | |
isISBN | check if the string is an [ISBN] | version: "10", "13", 10, 13; |
isEmail | check if the string is an email. | Same as isMailtoURI |
isAlpha | Check if the string contains only letters (a-zA-Z). | |
isAlphanumeric | Check if the string contains only letters and numbers. | |
isCreditCard | Check if the string is a credit card. | provider: Specify the credit card provider to validate. Use one of the following: |
isCurrency | Check if the string is a valid currency amount. | symbol: The currency symbol to be expected. Default: '$' require_symbol: If set to true , the validator will expect the currency symbol to be present. Default: false allow_space_after_symbol: If set to true , the validator will allow a space after the currency symbol. Default: false symbol_after_digits: If set to true , the currency symbol will be expected after the digits. Default: false allow_negatives: If set to true , negative currency values will be allowed. Default: true parens_for_negatives: If set to true , negative currency values will be enclosed in parentheses. Default: false negative_sign_before_digits: If set to true , the negative sign will be placed before the digits. Default: false negative_sign_after_digits: If set to true , the negative sign will be placed after the digits. Default: false allow_negative_sign_placeholder: If set to true , the validator will allow a placeholder - for negative values. Default: false thousands_separator: The thousands separator to be expected. Default: ',' decimal_separator: The decimal separator to be expected. Default: '.' allow_decimal: If set to true , decimal values will be allowed. Default: true require_decimal: If set to true , a decimal value will be required. Default: false digits_after_decimal: An array of numbers representing the exact number of digits allowed after the decimal point. Default: [2] allow_space_after_digits: If set to true , the validator will allow a space after the digits. Default: false |
isDataURI | Check if the string is a data URI format. | |
isDate | Check if the string is a valid date. | format: A string representing the expected date format. Default: undefined strictMode: If set to true , the validator will reject inputs different from the specified format. Default: false delimiters: An array of allowed date delimiters. Default: ['/', '-'] |
isEmpty | Check if the string has a length of zero. | ignore_whitespace: If set to true , whitespace characters will be ignored. Default: false |
isIBAN | Check if a string is an IBAN. | whitelist: An array of IBAN countries to whitelist. Default: undefined blacklist: An array of IBAN countries to blacklist. Default: undefined |
isIMEI | Check if the string is a valid IMEI. | allow_hyphens: If set to true , allows IMEI numbers with hyphens. Default: false |
isIP | Check if the string is an IP (version 4 or 6). | "4", "6", 4, 6; |
isIdentityCard | Check if the string is a valid identity card code. | locale: An array of supported locales for identity cards. Acceptable values: "ar-LY", "ar-TN", "ES", "FI", "he-IL", "IN", "IR", "IT", "LK", "NO", "PL", "TH", "zh-CN", "zh-HK", "zh-TW" or 'any' |
isIn | Check if the string is in an array of allowed values. | |
isJSON | Check if the string is valid JSON (note: uses JSON.parse ). |
|
isJWT | Check if the string is a valid JWT token. | |
isLength | Check if the string's length falls in a range. | min: The minimum length allowed for the string. Default: 0 max: The maximum length allowed for the string. Default: undefined |
isLowercase | Check if the string is lowercase. | |
isMobilePhone | Check if the string is a mobile phone number. | strictMode: If set to true , the mobile phone number must be supplied with the country code and must start with + . Default: false |
isPassportNumber | Check if the string is a valid passport number relative to a specific country code. | |
isPostalCode | Check if the string is a postal code. | "AD", "AT", "AU", "BE", "BG", "BR", "CA", "CH", "CN", "CZ", "DE", "DK", "DZ", "EE", "ES", "FI", "FR", "GB", "GR", "HR", "HU", "ID", "IE", "IL", "IN", "IR", "IS", "IT", "JP", "KE", "KR", "LI", "LT", "LU", "LV", "MX", "MT", "NL", "NO", "NZ", "PL", "PR", "PT", "RO", "RU", "SA", "SE", "SI", "SK", "TN", "TW", "UA", "US", "ZA", "ZM" |
isURL | Check if the string is a URL. | protocols: An array of allowed protocols. Default: ['http', 'https', 'ftp'] require_tld: If set to true , URLs must have a top-level domain. Default: true require_protocol: If set to true , URLs must have a protocol. Default: false require_host: If set to true , URLs must have a host. Default: true require_port: If set to true , isURL will check if a port is present in the URL. Default: false require_valid_protocol: If set to true , URLs must have a valid protocol. Default: true allow_underscores: If set to true , underscores are allowed in URLs. Default: false host_whitelist: An array of allowed hosts. host_blacklist: An array of disallowed hosts. allow_trailing_dot: If set to true , trailing dots are allowed in URLs. Default: false allow_protocol_relative_urls: If set to true , protocol-relative URLs are allowed. Default: false disallow_auth: If set to true , authentication credentials in URLs are disallowed. Default: false allow_fragments: If set to true , URL fragments are allowed. Default: true allow_query_components: If set to true , URL query components are allowed. Default: true validate_length: If set to true , URLs will be validated for length. Default: true |
isUUID | Check if the string is a UUID (version 1, 2, 3, 4, or 5). | |
isUppercase | Check if the string is uppercase. | |
isVAT | Checks that the string is a valid VAT number. | |
isWhitelisted | Checks if characters appear in the whitelist. | |
isInt | Check if the string is an integer. | min: to check the integer min boundary max: to check the integer max boundary allow_leading_zeroes: if false , will disallow integer values with leading zeroes. Default: true lt: enforce integers being greater than the value provided gt: enforce integers being less than the value provided |
isHexadecimal | Check if the string is a hexadecimal number. | |
isFloat | Check if the string is a float. | min: less or equal max: greater or equal gt: greater than lt: less than locale: FloatLocale |
isHash | Check if the string is a hash of export type algorithm. | "md4", "md5", "sha1", "sha256", "sha384", "sha512", "ripemd128", "ripemd160", "tiger128", "tiger160", "tiger192", "crc32", "crc32b" |
isEAN | Check if the string is an EAN (European Article Number). | |
isDecimal | Check if the string represents a decimal number, such as 0.1 , .3 , 1.1 , 1.00003 , 4.0 etc. |
force_decimal: If set to true , the validator will only return true if the string contains a decimal number. Default: false decimal_digits: Specifies the number of decimal digits allowed. It can be given as a range like '1,3' , a specific value like '3' , or min like '1,' . Default: '1,' locale: The locale to use for number formatting. Default: 'en-US' |
isBoolean | Check if a string is a boolean. | loose: If set to true , the validator will match a valid boolean string of any case, including ['true', 'True', 'TRUE'], and also 'yes' and 'no'. If set to false , the validator will strictly match ['true', 'false', '0', '1']. Default: false |
isBIC | Check if a string is a BIC (Bank Identification Code) or SWIFT code. | |
isBefore | Check if the string is a date that's before the specified date. | |
isAfter | Check if the string is a date that's after the specified date. |
Example 1
import {
EntityHandler,
Inject,
CDS_DISPATCHER,
Validate,
BeforeCreate,
BeforeUpdate,
OnCreate,
OnUpdate,
Req,
Next,
} from '@dxfrontier/cds-ts-dispatcher';
import type { Request, Service, NextEvent } from '@dxfrontier/cds-ts-dispatcher';
import { MyEntity } from 'YOUR_CDS_TYPER_ENTITIES_LOCATION';
@EntityHandler(MyEntity)
export class CustomerHandler {
// ...
@Inject(CDS_DISPATCHER.SRV) private srv: Service;
// ...
constructor() {}
@BeforeCreate()
@Validate<MyEntity>({ action: 'isLowercase' }, 'comment')
@Validate<MyEntity>({ action: 'endsWith', target: 'N' }, 'description')
private async beforeCreate(@Req() req: Request<MyEntity>) {
// ...
}
@BeforeUpdate()
@Validate<MyEntity>({ action: 'startsWith', target: 'COMMENT:' }, 'comment')
@Validate<MyEntity>({ action: 'isAlphanumeric' }, 'description')
private async beforeUpdate(@Req() req: Request<MyEntity>) {
// ...
}
@OnCreate()
@Validate<MyEntity>({ action: 'isAlphanumeric' }, 'book_ID')
private async onCreate(@Req() req: Request<MyEntity>, @Next() next: NextEvent) {
// ...
return next();
}
@OnUpdate()
@Validate<MyEntity>({ action: 'isLength', options: { min: 5 } }, 'comment')
private async onUpdate(@Req() req: Request<MyEntity>, @Next() next: NextEvent) {
// ...
return next();
}
// ...
}
Example 2
: @Validate
is used inside of @UnboundActions
import { UnboundActions, OnAction, OnFunction, OnEvent, Validate, Next, Req } from '@dxfrontier/cds-ts-dispatcher';
import type { ExposeFields, Request, ActionRequest, ActionReturn, NextEvent } from '@dxfrontier/cds-ts-dispatcher';
import { SomeAction, SomeFunction, OrderedBook } from 'YOUR_CDS_TYPER_ENTITIES_LOCATION';
@UnboundActions()
class UnboundActionsHandler {
@OnAction(SomeAction)
@Validate<ExposeFields<typeof SomeAction>>({ action: 'isIn', values: [1, 2] }, 'book', 'quantity')
private async onActionMethod(
@Req() req: ActionRequest<typeof SomeAction>,
@Next() _: NextEvent,
): ActionReturn<typeof SomeAction> {
// ...
}
@OnFunction(SomeFunction)
@Validate<ExposeFields<typeof SomeFunction>>({ action: 'isIn', values: [1, 2] }, 'book', 'quantity')
private async onFunctionMethod(
@Req() req: ActionRequest<typeof SomeFunction>,
@Next() next: NextEvent,
): ActionReturn<typeof SomeFunction> {
// ...
}
@OnEvent(OrderedBook)
@Validate<OrderedBook>({ action: 'isIn', values: [1, 2] }, 'book', 'quantity')
private async onEvent(@Req() req: Request<OrderedBook>) {
// ...
}
}
Tip
If you want to catch the validators use the following decorator @ValidationResults
This approach is useful if you need to conditionally process data based on validation outcomes within the same function.
Example Use Case: Suppose you want to validate multiple fields but only perform certain actions when specific validations pass. By using @ValidationResults
, you gain access
to a detailed validator object
containing the results
for each validation, allowing for flexible and targeted logic.
@Validate<MyEntity>({ action: 'isLowercase', exposeValidatorResult: true }, 'comment') @Validate<MyEntity>({ action: 'endsWith', target: 'N', exposeValidatorResult: true }, 'description') public async beforeCreate( @Req() req: Request<MyEntity>, @ValidationResults() validator: ValidatorFlags<'isLowercase' | 'endsWith'>) { // Conditional handling based on validation flags if (validator.isLowercase) { // Execute logic when the "isLowercase" validation succeeds } if (validator.endsWith) { // Separate handling for "endsWith" validation result } }
Important
To get the fields for
you must use the ExposeFields
type inside of the @Validate
decorator.
@FieldsFormatter<T>({ action, options? }, ...fields: Array<keyof T>)
The @FieldsFormatter
is used as a method level
decorator to modify
/enhance
fields.
The @FieldsFormatter
decorator can be used on the following decorators :
-
When you want to
modify
/enhance
theresults
of your callback.AFTER
-
When you want to
modify
/enhance
theRequest
.data
(Request Body) of the@sap/cds - Request
object.
Parameters
action
: Choose from a list of predefinedFormatters
.options?
: [Optional] Additional options for customizing the formatter process....fields
: Specify the fields of your entity that require formatting.
Formatters
Here are the available formatter methods:
Action | Description |
---|---|
blacklist | Remove characters that appear in the blacklist. |
ltrim | Trim characters from the left-side of the input. |
rtrim | Trim characters from the right-side of the input. |
trim | Trim characters from both sides of the input. |
escape | Replace < , > , & , ' , " and / with HTML entities. |
unescape | Replaces HTML encoded entities with < , > , & , ' , " and / . |
toLower | Converts string, as a whole, to lower case. |
toUpper | Converts string, as a whole, to upper case. |
upperFirst | Converts the first character of the string to upper case. |
lowerFirst | Converts the first character of the string to lower case. |
replace | Replaces matches for pattern in string with replacement. Note: This method is based on String#replace. |
truncate | Truncates string if it’s longer than the given maximum string length. The last characters of the truncated, string are replaced with the omission string which defaults to "…". |
snakeCase | Snake case (or snakecase) is the process of writing compound words so that the words are separated with an underscore symbol () instead of a space. The first letter is usually changed to lowercase. Some examples of Snake case would be "foo_bar" or "hello_world". |
kebabCase | Kebab case, also known as "spinal case" or "hyphen case," involves writing compound words in lowercase letters and separating them with hyphens ("-"). For example, the phrase "user settings panel" would be represented as "user-settings-panel" in the kebab case. |
camelCase | The format indicates the first word starting with either case, then the following words having an initial uppercase letter. CustomerName, LastName ... |
customFormatter | Apply a custom formatter when standard ones do not satisfy your needs. |
Example 1
import {
EntityHandler,
Inject,
CDS_DISPATCHER,
BeforeCreate,
BeforeUpdate,
AfterRead,
OnCreate,
OnUpdate,
FieldsFormatter,
Req,
Next,
} from '@dxfrontier/cds-ts-dispatcher';
import type { Service, Request, NextEvent } from '@dxfrontier/cds-ts-dispatcher';
import { MyEntity } from 'YOUR_CDS_TYPER_ENTITIES_LOCATION';
@EntityHandler(MyEntity)
export class CustomerHandler {
// ...
@Inject(CDS_DISPATCHER.SRV) private srv: Service;
// ...
constructor() {}
@BeforeCreate()
@FieldsFormatter<MyEntity>({ action: 'blacklist', charsToRemove: 'le' }, 'name')
private async beforeCreate(@Req() req: Request<MyEntity>) {
// ...
}
@BeforeUpdate()
@FieldsFormatter<MyEntity>({ action: 'truncate', options: { length: 7 } }, 'comment')
private async beforeUpdate(@Req() req: Request<MyEntity>) {
// ...
}
@AfterRead()
@FieldsFormatter<MyEntity>({ action: 'toUpper' }, 'lastName')
@FieldsFormatter<MyEntity>(
{
action: 'customFormatter',
callback(req, results) {
if (results) {
// make first item 'toLowerCase' and leave the rest 'toUpper'
results[0].lastName = results[0].lastName?.toLowerCase();
}
},
},
'lastName',
)
private async afterRead(@Results() results: MyEntity[], @Req() req: Request<MyEntity>) {
// ...
}
@OnCreate()
@FieldsFormatter<MyEntity>({ action: 'ltrim' }, 'language')
private async onCreate(@Req() req: Request<MyEntity>, @Next() next: NextEvent) {
// ...
return next();
}
@OnUpdate()
@FieldsFormatter<MyEntity>({ action: 'trim' }, 'format')
private async onUpdate(@Req() req: Request<MyEntity>, @Next() next: NextEvent) {
// ...
return next();
}
// ...
}
Tip
See best practice for customFormatter
Example 2
: using @FieldsFormatter
decorator inside the @UnboundActions
import {
UnboundActions,
OnAction,
OnFunction,
OnEvent,
FieldsFormatter,
Req,
Next,
} from '@dxfrontier/cds-ts-dispatcher';
import type { ActionRequest, ActionReturn, ExposeFields, NextEvent } from '@dxfrontier/cds-ts-dispatcher';
import { MyEntity } from 'YOUR_CDS_TYPER_ENTITIES_LOCATION';
@UnboundActions()
class UnboundActionsHandler {
@OnAction(AnAction)
@FieldsFormatter<ExposeFields<typeof AnAction>>({ action: 'toLower' }, 'descr', 'bookName')
private async onActionMethod(
@Req() req: ActionRequest<typeof AnAction>,
@Next() _: NextEvent,
): ActionReturn<typeof AnAction> {
// ...
return next();
}
@OnFunction(AFunction)
@FieldsFormatter<ExposeFields<typeof AFunction>>({ action: 'toUpper' }, 'lastName')
private async onFunctionMethod(
@Req() req: ActionRequest<typeof AFunction>,
@Next() next: NextEvent,
): ActionReturn<typeof AFunction> {
// ...
return next();
}
@OnEvent(AnEvent)
@FieldsFormatter<AnEvent>({ action: 'upperFirst' }, 'name')
private async onEvent(@Req() req: Request<AnEvent>) {
// ...
}
}
Important
To get the fields for
you must use the ExposeFields type
inside of the @FieldsFormatter
decorator.
@ExecutionAllowedForRole(...roles: string[])
The @ExecutionAllowedForRole
is used as a method level
decorator and was designed to enforce role-based access control
, ensuring that only users with specific roles
are authorized to execute the event.
It applies an logical OR
on the specified roles, meaning it checks if at least one
of the specified roles is assigned to the current request, then the execution will be allowed.
Parameters
...roles: string[]
: Specifies the roles that are permitted to execute the event logic.
Example
@AfterRead()
@ExecutionAllowedForRole('Manager', 'User', 'CEO')
private async afterRead(
@Req() req: Request,
@Results() results: BookSale[],
) {
// Method implementation
// Code will be executed only in case of User ( Manager, User and CEO )
}
@Use(...Middleware[]
)
The @Use
decorator is utilized as a method-level
decorator and allows you to inject middlewares into your method.
Middleware decorators can perform the following tasks:
- Execute any code.
- Make changes to the request object.
- End the request-response cycle.
- Call the next middleware function in the stack.
- If the current middleware function does not end the request-response cycle, it must call
next()
to pass control to the next middleware function. Otherwise, the request will be left hanging.
Parameters
...Middleware[])
: Middleware classes to be injected.
Example:
middleware implementation
import type { MiddlewareImpl, NextMiddleware, Request } from '@dxfrontier/cds-ts-dispatcher';
export class MiddlewareClass implements MiddlewareImpl {
public async use(req: Request, next: NextMiddleware) {
console.log('Middleware use method called.');
await next();
}
}
Example
usage
import { EntityHandler, Use, Inject, CDS_DISPATCHER } from '@dxfrontier/cds-ts-dispatcher';
import type { Service, Request } from '@dxfrontier/cds-ts-dispatcher';
import { MiddlewareClass } from 'YOUR_MIDDLEWARE_LOCATION';
import { MyEntity } from 'YOUR_CDS_TYPER_ENTITIES_LOCATION';
@EntityHandler(MyEntity)
export class CustomerHandler {
// ...
@Inject(CDS_DISPATCHER.SRV) private srv: Service;
// ...
constructor() {}
@AfterRead()
@Use(MiddlewareClass)
private async aMethod(@Results() results: MyEntity[], @Req() req: Request) {
// ...
}
// ...
}
Tip
- Middlewares when applied with
@Use
are executed before the normal events. - If you need to apply middleware to
class
you can have a look over class specific @Use decorator .
Warning
If req.reject()
is being used inside of middleware this will stop the stack of middlewares, this means that next middleware will not be executed.
Note
MyEntity was generated using CDS-Typer and imported in the class.
- Add
mta.yaml
to your project using the following command :
cds add mta
- Install
npm-run-all
package:
npm install --save-dev npm-run-all
- Modify your
package.json
by adding the followingscripts
:
"build:cds": "echo 'STEP 1 : Build CDS' && cds build --production",
"build:ts": "echo 'STEP 2 : Transpile TS => JS' && tsc",
"build:srv:clean:ts": "echo 'Step 3: Clean TS files from srv folder' && find gen/srv/srv -type f -name '*.ts' -delete",
"build:production": "run-s build:cds build:ts build:srv:clean:ts"
- Modify
mta.yaml
as follows :
- builder: custom
commands:
- npm ci
- npm run build:production
- npx @cap-js/cds-typer "*" --outputDirectory gen/srv/@cds-models
Steps
explained :
npm ci
- Will do a clean installnpm run build:production
- will run the package.json script command for CDS build and transpilation of TS to JS and clean theTS files
.npx @cap-js/cds-typer "*" --outputDirectory gen/srv/@cds-models
- will make sure the @cds-models are generated.
- Install MTA Build tool globally:
npm i -g mbt
- Run command to produce the
.mtar file
mbt build
- Deploy your
mtar
to BTP
Can I stack multiple decorators on the same callback ?
Yes, you can stack multiple decorators.
Example 1
@AfterRead()
@Use(MiddlewareMethodAfterRead1, MiddlewareMethodAfterRead2)
@FieldsFormatter<MyEntity>({ action: 'blacklist', charsToRemove: 'Mysterious' }, 'title')
private async aMethod(@Results() results: MyEntity[], @Req() req: Request) {
// ...
}
Example 2
@BeforeRead()
@BeforeCreate()
@BeforeUpdate()
@BeforeDelete()
private async aMethod(@Req() req: Request<MyEntity>) {
// ..
}
Is the sequence of decorators important ?
Yes, it is important as typescript executes the decorators :
- for
Class
- frombottom
totop
- for
method
- fromtop
tobottom
@SecondClassDecorator() // second executed
@FirstClassDecorator() // first executed
class MyClass {
@FirstDecorator() // first executed
@SecondDecorator() // second executed
myMethod() {
console.log('Method called');
}
}
Best practices for @FieldsFormatter - customFormatter
To have all the custom formatters in one place you can create a new formatters.ts
where you will place all custom formatters and use export
to make them visible.
// formatter.ts
export const customFormatter: Formatters<BookFormat> = {
action: 'customFormatter',
callback(req, results) {
if (results && results.length > 0) {
// make first item 'toLowerCase' and leave the rest 'toUpperCase'
results[0].format = results[0].format?.toLowerCase();
}
},
};
Import the customFormatter
in your handler
@AfterRead()
@FieldsFormatter<MyEntity>(customFormatter, 'format') // import it here
private async afterRead(@Results() results: MyEntity[], @Req() req: Request<MyEntity>) {
// ...
}
[!TIP] To get all the Formatters typing you can import the
Formatters\<T>
type whereT
is the entity of yourCDS
provided by CDS-Typer
Find here a collection of samples for the CDS-TS-Dispatcher-Samples
Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.
Please make sure to update tests as appropriate.
Copyright (c) 2024 DXFrontier
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.
- @dragolea
- @sblessing
- @ABS GmbH team