Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
aschmidt93 opened this issue Sep 18, 2024 · 0 comments
Open

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

aschmidt93 opened this issue Sep 18, 2024 · 0 comments

Comments

@aschmidt93
Copy link

aschmidt93 commented Sep 18, 2024

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>;
    };
  }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant