4.2.2 • Published 1 year ago

@dxfrontier/cds-ts-dispatcher v4.2.2

Weekly downloads
-
License
MIT
Repository
github
Last release
1 year ago

SAP ts-node Node.js Express.js json npm

NPM Downloads NPM Downloads NPM Version

Tests GitHub Actions Workflow Status GitHub last commit (branch) GitHub issues GitHub contributors GitHub Repo stars

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

Install @sap/cds-dk, typescript, ts-node globally:

npm install -g @sap/cds-dk typescript ts-node

Installation

Option 1 : Install CDS-TS-Dispatcher - New project

Use the following steps if you want to create a new SAP CAP project.

  1. Create new folder :
mkdir project
cd project
  1. Initialize the CDS folder structure :
cds init
  1. Add TypeScript and CDS-Typer to your npm package.json:
cds add typescript
  1. Add CDS-TS-Dispatcher to your npm package.json :
npm install @dxfrontier/cds-ts-dispatcher
  1. 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"]
}
  1. Install packages
npm install
  1. Run the CDS-TS server
cds-ts w

!IMPORTANT CDS-TS-Dispatcher uses @sap/cds, @sap/cds-dk version 8

Option 2 : Install CDS-TS-Dispatcher - Existing TypeScript project

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": {
    "esModuleInterop": true,
    "skipLibCheck": true,
    "allowJs": true,
    "strictPropertyInitialization": false,
    "forceConsistentCasingInFileNames": true,
    "allowSyntheticDefaultImports": true,
    "strictNullChecks": true,
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",

    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,

    "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

Generate CDS Typed entities

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.

Execute the command :

npx @cap-js/cds-typer "*" --outputDirectory ./srv/util/types/entities
  • Target folder :./srv/util/types/entities - Change to your desired destination folder.

Important

!IMPORTANT Import always the generated entities from the service folders and not from the index.ts

alt text

!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';

Migration: from @sap/cds v7 to v8

Use the following steps if you want to migrate from @sap/cds@7 to @sap/cds@8:

  1. Verify you've installed the cds@v8 globally by running the following command:
cds -v -i
packagesversion
@cap-js/asyncapi1.0.1
@cap-js/cds-typer0.24.0
@cap-js/cds-types0.6.4
@cap-js/openapi1.0.4
@cap-js/sqlite1.7.3
@sap/cds8.1.0
@sap/cds-compiler5.1.2
@sap/cds-dk (global)8.0.2
@sap/cds-fiori1.2.7
@sap/cds-foss5.0.1
@sap/cds-lsp8.0.0
@sap/cds-mtxs1.18.2
@sap/eslint-plugin-cds3.0.4
Node.jsv22.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
  1. 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
  1. After running command above the package.json will look similar to :
{
  "dependencies": {
    "@dxfrontier/cds-ts-dispatcher": "^4.0.0",
    "@dxfrontier/cds-ts-repository": "^5.1.6",
    "@sap/cds": "^8.7.2"
  },
  "devDependencies": {
    "@cap-js/sqlite": "^1.8.0",
    "@cap-js/cds-types": "^0.9.0",
    "typescript": "^5.5.4",
    "@types/node": "^22.1.0",
    "@cap-js/cds-typer": "0.33.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

Usage

Architecture

We recommend adhering to the Controller-Service-Repository design pattern using the following folder structure:

  1. EntityHandler (Controller) - Responsible for managing the REST interface to the business logic implemented in ServiceLogic
  2. ServiceLogic (Service) - Contains business logic implementations
  3. Repository (Repository) - This component is dedicated to handling entity manipulation operations by leveraging the power of CDS-QL.

Controller-Service-Repository suggested folder structure

alt text <= expanded folders => alt text

!TIP You can have a look over the CDS-TS-Dispatcher-Samples where we use the Controller-Service-Repository pattern and Dispatcher.

CDSDispatcher

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

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

Decorators

Class

@EntityHandler

The @EntityHandler decorator is utilized at the class-level to annotate a class with:

  1. A specific entity that will serve as the base entity for all handler decorators within the class.
  2. '*' as all entities that will serve as the base entity for all handler decorators within the class.

Overloads

MethodParametersDescription
1. EntityHandler(entity: CDSTyper)Must be a CDS-Typer generated classIt ensures that all handlers within the class operate with the specified entity context.
2. EntityHandler(entity: '*')A wildcard '*' indicating all entitiesIt 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

@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

@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.

[Optional] - CDS-TS-Repository - BaseRepository

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

@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, OnSubscribe } 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) {
    // ...
  }

  // Unbound event
  @OnSubscribe({ 
    eventName: 'event_name'
    type: 'SAME_NODE_PROCESS' 
  })
  private async onSubscribe(@Req() req: Request<{foo: string, bar: number}>): Promise<void> {
    // 
  }
}

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

@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
  }
}

!TIP

Inside of the decorator use method you can use @CatchAndSetErrorCode and @CatchAndSetErrorMessage decorators.

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

  1. Think of it (middleware) like as a reusable class, enhancing the functionality of all events within the class.
  2. Middlewares when applied with @Use are executed before the normal events.
  3. 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.

Field

@Inject

@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)

@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)

@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.

Parameter

@Req

@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

@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 of RequestResponse 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');
  }
}

!TIP Decorator @Res can be used in all After, Before and On events.

@Results / @Result

@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 Request Body.

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

@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

@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 type Error.

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

@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 retrieved JWT 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

@IsPresent\(key: Key, property: PickQueryPropsByKey\)

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 are INSERT, SELECT, UPDATE, UPSERT, DELETE.
  • property (string): Specifies the property based on the key.

Return

  • boolean: This decorator returns true if property 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

@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 returns true if at least one of the specified roles is assigned to the current request user, otherwise false.

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

@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 returns true if field / 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

@GetQuery\(key: Key, property: PickQueryPropsByKey\)

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 are INSERT, SELECT, UPDATE, UPSERT, DELETE.
  • property (string): Specifies the property based on the key.

Return: Varies based on the specified property :

    • @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', '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', 'data') data: GetQueryType['data']
    • @GetQuery('UPDATE', 'entity') entity: GetQueryType['entity']
    • @GetQuery('UPDATE', 'where') where: GetQueryType['where']
    • @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', '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

@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 the Request 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

@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 the Request is single instance
  • false when the Request is entity 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

@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

@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

@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 from cds.env. This path follows the format property 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.

Type Parameters

  • T: The CDS 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 the CDS ENV TS interfaces, generated based on the structure of your current cds.env project specific configuration (retrieved from cds env get cli command).
...
@dispatcher
...
  • package.json will be updated with a new import:
 "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'; 
@Msg

@Msg()

The @Msg decorator is a parameter decorator used to inject response Emitter directly into a method parameter.

Parameters

Return :

  • The decorator returns an object of type :
{
  event: string,
  data: T,
  headers: any,
  inbound: boolean
}

Example

import { OnSubscribe, Messaging } from '@dxfrontier/cds-ts-dispatcher';  
import type { SubscriberType } from '@dxfrontier/cds-ts-dispatcher';  

@OnSubscribe({
  eventName: 'AnEventName',
  type: 'MESSAGE_BROKER',
})
public async onSubscribe(@Msg() msg: SubscriberType<{ foo: number; bar: string }>): Promise<void> {
  // ...
}

!IMPORTANT To have the msg typed you can use type SubscriberType<T> where T can be a CDS event or a plain object.

!TIP Decorator @Msg should be used exclusively on decorator @OnSubscribe.

Method-active entity

Before

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 type Request

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

@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

@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

@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

@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.

@BeforeAll

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:

DRAFT

For draft entities, the @BeforeAll decorator will be triggered when at least one of the following events occurs:

@BeforeAll()

Example 1

In this example, the @BeforeAll() decorator of the beforeAllEvents method is executed before any CRUD operation (CREATE, READ, UPDATE, DELETE, BOUND ACTIONS, BOUND FUNCTIONS)) on the MyEntity entity.

Since the class is annotated with @EntityHandler(MyEntity), the decorator is scoped to this specific entity, meaning it will only be triggered for operations related to MyEntity.

import { BeforeAll, EntityHandler } from "@dxfrontier/cds-ts-dispatcher";
import type { Request } 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'

  @BeforeAll()
  private async beforeAllEvents(@Req() req: Request<MyEntity>) {
    // ...
  }
}

Equivalent to 'JS'

this.before('*', MyEntity, async (req) => {
  // ...
});

Example 2

In this example, the @BeforeAll() decorator is used in a more generic way. Unlike Example 1, where it applies only to MyEntity, this setup ensures that beforeAllEvents is triggered for all entities.

This means the method will execute before any CRUD operation on any entity handled within the class.

import { BeforeAll, EntityHandler, CDS_DISPATCHER } from "@dxfrontier/cds-ts-dispatcher";
import type { Request } from '@dxfrontier/cds-ts-dispatcher';

import { MyEntity } from 'YOUR_CDS_TYPER_ENTITIES_LOCATION';

@EntityHandler(CDS_DISPATCHER.ALL_ENTITIES)
export class BookHandler {
  // ...
  constructor() {}
  // All events like @AfterRead, @BeforeRead, ... will be triggered based on 'MyEntity'

  @BeforeAll()
  private async beforeAllEvents(@Req() req: Request<MyEntity>) {
    // ...
  }
}

Equivalent to 'JS'

this.before('*', '*', 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.

After

Use @AfterCreate(), @AfterRead(), @AfterUpdate(), @AfterDelete() register handlers to run after the .on handlers, frequently used to enrich outbound data.

The handlers receive two arguments:

ParametersDecoratorDescription
results, req@AfterReadAn array of type MyEntity[] and the Request.
result, req@AfterUpdate @AfterCreateAn object of type MyEntity and the Request.
deleted, req@AfterDeleteA 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

@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

@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 Decorat

3.2.2

2 years ago

3.2.1

2 years ago

3.2.0

2 years ago

3.2.6

2 years ago

3.2.5

2 years ago

3.2.4

2 years ago

3.2.3

2 years ago

4.2.2

1 year ago

4.0.1

1 year ago

4.0.0

1 year ago

4.2.1

1 year ago

4.2.0

1 year ago

3.2.7

1 year ago

3.1.2

2 years ago

3.1.1

2 years ago

4.1.3

1 year ago

4.1.0

1 year ago

4.1.2

1 year ago

4.1.1

1 year ago

3.0.0

2 years ago

2.1.2

2 years ago

2.1.1

2 years ago

2.1.4

2 years ago

2.1.3

2 years ago

2.1.0

2 years ago

2.0.19

2 years ago

2.0.26

2 years ago

2.0.24

2 years ago

2.0.22

2 years ago

2.0.20

2 years ago

2.0.21

2 years ago

2.0.15

2 years ago

2.0.16

2 years ago

2.0.14

2 years ago

2.0.17

2 years ago

2.0.18

2 years ago

2.0.13

2 years ago

2.0.12

2 years ago

2.0.11

2 years ago

2.0.10

2 years ago

2.0.5

2 years ago

2.0.6

2 years ago

2.0.9

2 years ago

2.0.8

2 years ago

2.0.3

2 years ago

2.0.2

2 years ago

2.0.4

2 years ago

2.0.1

2 years ago

2.0.0

2 years ago

1.1.2

2 years ago

1.1.1

2 years ago

1.1.0

2 years ago

1.0.6

2 years ago

1.0.5

2 years ago

1.0.4

2 years ago

1.0.3

2 years ago

1.0.2

2 years ago

1.0.1

2 years ago

1.0.0

2 years ago

0.1.25

2 years ago

0.1.23

2 years ago

0.1.24

2 years ago

0.1.22

2 years ago

0.1.21

2 years ago

0.1.20

2 years ago

0.1.19

2 years ago

0.1.18

2 years ago

0.1.17

2 years ago

0.1.16

2 years ago

0.1.15

2 years ago

0.1.14

3 years ago

0.1.13

3 years ago

0.1.12

3 years ago

0.1.11

3 years ago