cds-typer
cds-typer copied to clipboard
[SUGGESTION] Use types instead of classes/functions for generation of cds entities/types/aspects
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 ofclass
- 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 ofcds.entities(<namespace>)
. This return typeEntitites
of that function, will contain extended versions of the plain types that will also contain the properties likeactions
ordrafts
. 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