cds-typer icon indicating copy to clipboard operation
cds-typer copied to clipboard

[SUGGESTION] Use types instead of classes/functions for generation of cds entities/types/aspects

Open stockbal opened this issue 7 months ago • 8 comments

Description

The currently used approach with classes and functions seems overcomplicated and is also kind of hard to read. In addition it feels weird to map class definitions to objects (cds.entities). The direct usage of cds.entities on root level of the generated modules also require dynamic imports to avoid runtime issues (in jest this is still an experimental feature).

Suggested Solution

  • Use type instead of class
  • The types for entities (singular/plural) and for types/aspects contain only the properties like declared in cds
  • aspect/inheritence chains are realized with type intersections
    entity Books : cuid, managed {}
    
    type Book {} & __.cuid & __.managed
    
  • each namespace file (index.js/index.ts) will get a new function export called entities which will return a typed version of cds.entities(<namespace>). This return type Entitites of that function, will contain extended versions of the plain types that will also contain the properties like actions or drafts. The elements of that property would what the current class definitions provide.

Example for index.ts

import * as _ from './..';
import * as _my_bookshop from './../my/bookshop';
import * as __ from './../_';
export default { name: 'CatalogService' }

// NOTE: inline enums will be placed under the namespace of the declaring entity
export namespace Book {
  // enum
  export const type = {
    Ebook: "Ebook",
    Hardcover: "Hardcover",
    Paperback: "Paperback",
  } as const;
  export type type = "Ebook" | "Hardcover" | "Paperback"
}

export type Book = {
  title?: string | null
  genre?: _my_bookshop.Genre | null
  type?: Book.type | null
  stock?: number | null
  author?: __.Association.to<_my_bookshop.Author> | null
  author_ID?: string | null
  publishers?: __.Composition.of.many<Books2Publishers>
} & _.cuid & _.managed & _my_bookshop.Generic
export type Books = Array<Book> & {$count?: number}

export type Books2Publisher = {
  book?: __.Association.to<_my_bookshop.Book> | null
  book_ID?: string | null
  publisher?: __.Association.to<_my_bookshop.Publisher> | null
  publisher_ID?: string | null
} & _.cuid
export type Books2Publishers = Array<Books2Publisher> & {$count?: number}

// type for cds.entities of namespace CatalogService
type Entities = {
  Book: __.Constructable<Book> & __.singular & __.withName & {
    actions: {
      order: { (quantity: number | null): any, __parameters: {quantity: number | null}, __returns: any, kind: 'action'}
    }
    drafts: __.Constructable<__.DraftEntity<Book>>
  }
  Books: __.ArrayConstructable<Book> & __.withName & {
    drafts: __.ArrayConstructable<__.DraftEntity<Book>>
  }
  Books2Publisher: __.Constructable<Books2Publisher> & __.singular & __.withName & {
    drafts: __.Constructable<__.DraftEntity<Books2Publisher>>
  }
  Books2Publishers: __.ArrayConstructable<Books2Publisher> & __.withName & {
    drafts: __.ArrayConstructable<__.DraftEntity<Books2Publisher>>
  }
}
/**
 * @returns {Entities} entities of namespace "CatalogService"
 */
export declare function entities(): Entities

Example of index.js

const cds = require('@sap/cds')
module.exports = { name: 'CatalogService' }
let _entities
module.exports.entities = function() {
  if (_entities) return _entities
  const csn = cds.entities('CatalogService')
  _entities = {
    Book: { is_singular: true, __proto__: csn.Books },
    Books: csn.Books,
    Books2Publisher: { is_singular: true, __proto__: csn.Books2Publishers },
    Books2Publishers: csn.Books2Publishers,
  }
  return _entities
}
// enums
module.exports.Book = {}
module.exports.Book.type ??= { Ebook: "Ebook", Hardcover: "Hardcover", Paperback: "Paperback" }

Sample Service implementation

import { ApplicationService } from "@sap/cds";
import * as cats from "#cds-models/CatalogService";
import { entities } from "#cds-models/my/bookshop";

export default class CatService extends ApplicationService {
  override async init() {
    const { Books } = cats.entities();

    this.before("UPDATE", Books.drafts, async (req) => {
      const { Authors } = entities();
      const author = await SELECT.one.from(Authors).columns("name");

      if (!author) req.reject(400, "Author not found");
    });
    return super.init();
  }
}

I know this change would break a lot of projects that are currently using cds-typer (including some I am personally involved in 😅) but I think it poses some benefits and should at least be discussed.

Benefits

  • leaner and more readable types
  • index.js and index.ts move closer together
  • get rid of dynamic imports

This approach is currently implemented and functional on the following branch https://github.com/stockbal/cds-typer/tree/refactor/replace-classes-with-types

P.S.: This branch also contains a surefire way to only provide the drafts property in entities when it's truly there during runtime.

Tell me what you think about this new approach 😎

Alternatives

No response

Additional Context

No response

stockbal avatar Jul 22 '24 12:07 stockbal