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 · 1 comment
Open

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

aschmidt93 opened this issue Sep 18, 2024 · 1 comment

Comments

@aschmidt93
Copy link
Contributor

aschmidt93 commented Sep 18, 2024

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

// [email protected]
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>;
    };
  }
@daogrady
Copy link
Contributor

Hi Andreas,

thank you for your suggestion! I think in principle, this could be a nice addition to our types. The problem I am seeing here is how services are currently emitted by cds-typer. As you are probably aware, each service will be reflected by their own directory with two files, index.js and index.ts, in it.
So you'd get a directory:

.
└── @cds-models/
    └── OrderServiceTypes/
        ├── index.js
        └── index.ts

Therefore, the service is represented by the directory structure. For your suggestion to work, we'd actually need to have an artefact, be it some class or type, representing the service, too.
In fact, that has been introduced recently, but it already causes problems when you have a service Foo that also contains an entity Foo.
So we'd end up with:

.
└── @cds-models/
    └── OrderServiceTypes/
        ├── index.js
        └── index.ts/
            ├── class Foo  <- this is the entity
            └── class Foo  <- this is the service, so you now have Foo.Foo

So right now, we are looking into getting rid of this artificial service class inside the file that is already representing the service.
Long story short: while I like the idea in general, I'm afraid I don't see a proper implementation with the structure that is currently emitted by cds-typer.

Best,
Daniel

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

2 participants