Skip to content

[SUGGESTION] Generic Service class usable with cds-typer #242

Description

@aschmidt93

The Service class is currently not generic but has some generic methods, e.g. emit:

// cds-types@0.6.5
export class Service extends QueryAPI {
    emit: {
        <T = any>(details: { event: types.event, data?: object, headers?: object }): Promise<T>,
        <T = any>(event: types.event, data?: object, headers?: object): Promise<T>,
    }
}

This is cumbersome to use, as users have to manually provide the types and there are no compile time checks.

With the output of cds-typer many properties of the Service class can be fully typed. Therefore, I suggest making the Service class generic where the generic parameter is the output of cds-typer for a service. Alternatively, define a new TypedService type if no changes shall be made to the Service class. This concept is already used for adding handlers to actions and functions of a service.

export class Service<T extends ServiceDefinition = any> extends QueryAPI { ...}

The type ServiceDefinition shall describe the output of cds-typer for a service, much like CdsFunction describes the output of cds-typer for a service operation

export type CdsFunction = {
    (...args: any[]): any,
    __parameters: object,
    __returns: any,
}

Example usage:

// order-service.cds
service OrderService {
    event orderCanceled {
        orderID : String;
    }

    action cancelOrder(orderID : String);
}
import type * as OrderServiceTypes from "@cds-models/OrderService"

const orderService : Service<typeof OrderServiceTypes> = cds.services["OrderService"];

// Error: param `orderID` missing
await orderService.emit("orderCanceled", {});

// auto-completion and compile time checks
await orderService.cancelOrder({orderID : "1"});
await orderService.send("cancelOrder", {orderID : "2"});

In my projects I usually define the TypedService class which looks something like this for the emit method. This is far from perfect and can and should be improved.

  // the `kind` property is currently missing in `CdsFunction`
  type ActionFunctionDef = CdsFunction & {
    kind: "action" | "function";
  };

  /**
   * Definition of a service as generated by cds-typer
   */
  export type ServiceDefinition = {
    [key: string]:
      | ActionFunctionDef
      | EventDef
      | EntityDefinition
      | EntitySetDefinition<any>
      | unknown;
  };

  type ActionFunctionDef = {
    __returns: any;
    __parameters: any;
    (...args: any): any;
    kind: "action" | "function";
  };

  export type ServiceActionsFunctions<T extends ServiceDefinition> = {
    [key in keyof T as T[key] extends ActionFunctionDef
      ? key
      : never]: T[key] extends ActionFunctionDef ? T[key] : never;
  };

  type EventDef = new () => Record<string, any>;

  /**
   * Definition of a single entity (row in a table) as generated by cds-typer
   */
  export type EntityDefinition = { new (...args: any[]): any; readonly actions: Record<any, any> };

  /**
   * Definition of an entity set as generated by cds-typer
   */
  export type EntitySetDefinition<T extends InstanceType<EntityDefinition>> = {
    new (...args: any[]): Array<T>;
  };

  /**
   * Resolves to `true` if `T` is an EventDef
   */
  type IsEventDef<T> = T extends EntityDefinition
    ? false
    : T extends EntitySetDefinition<infer _>
      ? false
      : T extends EventDef
        ? true
        : false;

  /**
   * Available events in a service
   */
  export type ServiceEvents<T extends ServiceDefinition> = {
    [key in keyof T as IsEventDef<T[key]> extends true
      ? key
      : never]: T[key] extends new () => infer U ? U : never;
  };

  /**
   * Available entity sets of a service
   */
  export type ServiceEntitySets<T extends ServiceDefinition> = {
    [K in keyof T as T[K] extends EntitySetDefinition<infer U>
      ? K
      : never]: T[K] extends EntitySetDefinition<infer U> ? EntitySetDefinition<U> : never;
  };

// module augmentation
class ApplicationService<T extends ServiceDefinition> extends Service {
    emit: {
      <E extends keyof ServiceEvents<T>>(details: {
        event: E;
        data?: ServiceEvents<T>[E];
        headers?: object;
      }): Promise<T>;
      <E extends keyof ServiceEvents<T>>(
        event: E,
        data?: ServiceEvents<T>[E],
        headers?: object
      ): Promise<T>;
    };
  }

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions