Skip to content

Commit

Permalink
feat: allow srv handler registration for array of typed entities (#267)
Browse files Browse the repository at this point in the history
Co-authored-by: Daniel O'Grady <103028279+daogrady@users.noreply.github.com>
  • Loading branch information
stockbal and daogrady authored Oct 8, 2024
1 parent 21cb890 commit c7f22b2
Show file tree
Hide file tree
Showing 4 changed files with 72 additions and 13 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/).
- `cds.requires` types for MTX services
- `cds.utils.colors` types
- The CQL methods `.where` and `.having` now suggest property names for certain overloads.
- `Service.before/on/after(event, target...)` now accept also an array of typer-generated classes in the `target` parameter

### Changed
- Most `cds.requires` entries are now optionals.
Expand Down
18 changes: 9 additions & 9 deletions apis/services.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,8 +225,8 @@ export class Service extends QueryAPI {
// Provider API
prepend (fn: ServiceImpl): this

on<T extends ArrayConstructable>(eve: types.event, entity: T, handler: CRUDEventHandler.On<Unwrap<T>>): this
on<T extends Constructable>(eve: types.event, entity: T, handler: CRUDEventHandler.On<InstanceType<T>>): this
on<T extends ArrayConstructable>(eve: types.event, entity: T | T[], handler: CRUDEventHandler.On<Unwrap<T>>): this
on<T extends Constructable>(eve: types.event, entity: T | T[], handler: CRUDEventHandler.On<InstanceType<T>>): this
on<F extends CdsFunction>(boundAction: F, service: string, handler: ActionEventHandler<F['__parameters'], void | Error | F['__returns']>): this
on<F extends CdsFunction>(unboundAction: F, handler: ActionEventHandler<F['__parameters'], void | Error | F['__returns']>): this
on (eve: types.event, entity: types.target, handler: OnEventHandler): this
Expand All @@ -239,8 +239,8 @@ export class Service extends QueryAPI {
// onFailed (eve: types.Events, handler: types.EventHandler): this
before<F extends CdsFunction>(boundAction: F, service: string, handler: CRUDEventHandler.Before<F['__parameters'], void | Error | F['__returns']>): this
before<F extends CdsFunction>(unboundAction: F, handler: CRUDEventHandler.Before<F['__parameters'], void | Error | F['__returns']>): this
before<T extends ArrayConstructable>(eve: types.event, entity: T, handler: CRUDEventHandler.Before<Unwrap<T>>): this
before<T extends Constructable>(eve: types.event, entity: T, handler: CRUDEventHandler.Before<InstanceType<T>>): this
before<T extends ArrayConstructable>(eve: types.event, entity: T | T[], handler: CRUDEventHandler.Before<Unwrap<T>>): this
before<T extends Constructable>(eve: types.event, entity: T | T[], handler: CRUDEventHandler.Before<InstanceType<T>>): this
before (eve: types.event, entity: types.target, handler: EventHandler): this
before (eve: types.event, handler: EventHandler): this

Expand All @@ -249,11 +249,11 @@ export class Service extends QueryAPI {
// (3) check if T is scalar -> use T directly
// this streamlines that in _most_ cases, handlers will receive a single object.
// _Except_ for after.read handlers (1), which will change its inflection based on T.
after<T extends ArrayConstructable>(event: 'READ', entity: T, handler: CRUDEventHandler.After<InstanceType<T>>): this
after<T extends ArrayConstructable>(event: 'each', entity: T, handler: CRUDEventHandler.After<Unwrap<T>>): this
after<T extends Constructable>(event: 'READ' | 'each', entity: T, handler: CRUDEventHandler.After<InstanceType<T>>): this
after<T extends ArrayConstructable>(eve: types.event, entity: T, handler: CRUDEventHandler.After<Unwrap<T>>): this
after<T extends Constructable>(eve: types.event, entity: T, handler: CRUDEventHandler.After<InstanceType<T>>): this
after<T extends ArrayConstructable>(event: 'READ', entity: T | T[], handler: CRUDEventHandler.After<InstanceType<T>>): this
after<T extends ArrayConstructable>(event: 'each', entity: T | T[], handler: CRUDEventHandler.After<Unwrap<T>>): this
after<T extends Constructable>(event: 'READ' | 'each', entity: T | T[], handler: CRUDEventHandler.After<InstanceType<T>>): this
after<T extends ArrayConstructable>(eve: types.event, entity: T | T[], handler: CRUDEventHandler.After<Unwrap<T>>): this
after<T extends Constructable>(eve: types.event, entity: T | T[], handler: CRUDEventHandler.After<InstanceType<T>>): this
after<F extends CdsFunction>(boundAction: F, service: string, handler: CRUDEventHandler.After<F['__parameters'], void | Error | F['__returns']>): this
after<F extends CdsFunction>(unboundAction: F, handler: CRUDEventHandler.After<F['__parameters'], void | Error | F['__returns']>): this
after (eve: types.event, entity: types.target, handler: ResultsHandler): this
Expand Down
59 changes: 55 additions & 4 deletions test/typescript/apis/project/cds-services.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import cds, { Service, TypedRequest } from '@sap/cds'
import { Foo, Foos, action } from './dummy'
import { Bars, Bar, Foo, Foos, action } from './dummy'
const model = cds.reflect({})
const { Book: Books } = model.entities
import express from 'express'
Expand Down Expand Up @@ -195,6 +195,27 @@ srv.on('error', (err, req) => {
function isOne(p: TypedRequest<Foo> | Foo | undefined ) { if(!p) return; p instanceof Foo ? p.x.toFixed : p.data.x.toFixed}
function isMany(p: TypedRequest<Foos> | Foos | undefined) { if(!p) return; p instanceof Foos ? p[0].x.toFixed : p.data[0].x.toFixed}

function isOneOfMany(p: TypedRequest<Foo | Bar> | Foo | Bar | undefined ) {
if(!p) return;
if ("data" in p) {
if (p.data instanceof Foo) p.data.x.toFixed;
else p.data.name.split;
} else {
if (p instanceof Foo) p.x.toFixed;
else p.name.split;
}
}
function isManyOfMany(p: TypedRequest<Foos | Bars> | Foos | Bars | undefined) {
if(!p) return;
if ("data" in p) {
if (p.data instanceof Foos) p.data[0].x.toFixed;
else p.data[0].name.split;
} else {
if (p instanceof Foos) p[0].x.toFixed;
else p[0].name.split;
}
}

// Typed bound/ unbound actions
// The handler must return a number to be in line with action's signature (or void)
srv.on(action, req => req.data.foo.x)
Expand All @@ -207,17 +228,33 @@ srv.before('CREATE', Foos, req => isOne(req))
srv.after('CREATE', Foo, (data) => { isOne(data); return data })
srv.after('CREATE', Foos, (data) => isOne(data))

srv.on('CREATE', [Foo, Bar], (req, next) => { isOneOfMany(req); return next() })
srv.on('CREATE', [Foos, Bars], (req, next) => { isOneOfMany(req); return next() })
srv.before('CREATE', [Foo, Bar], req => { isOneOfMany(req); return req.data })
srv.before('CREATE', [Foos, Bars], req => { isOneOfMany(req); return req.data })
srv.after('CREATE', [Foo, Bar], (data) => { isOneOfMany(data); return data })
srv.after('CREATE', [Foos, Bars], (data) => { isOneOfMany(data); return data })

// Handlers with classes. Singular and plural are to be reflected in what the handler receives
srv.on('READ', Foo, (req, next) => { isOne(req); return next() })
srv.on('READ', Foos, (req, next) => { isOne(req); return next() })
srv.before('READ', Foo, req => { isOne(req); return req.data })
srv.before('READ', Foos, req => isOne(req))
srv.after('READ', Foo, (data) => { isOne(data); return data })
srv.after("each", Foos, (data) => { isOne(data); return data })
srv.after('READ', Foos, (data) => isMany(data))

srv.after('EACH', Foo, (data) => { isOne(data); return data })
srv.after('EACH', Foos, (data) => isOne(data))
srv.after("each", Foos, (data) => { isOne(data); return data })
srv.after("each", Foo, (data) => { isOne(data); return data })

srv.on('READ', [Foo, Bar], (req, next) => { isOneOfMany(req); return next() })
srv.on('READ', [Foos, Bars], (req, next) => { isOneOfMany(req); return next() })
srv.before('READ', [Foo, Bar], req => { isOneOfMany(req); return req.data })
srv.before('READ', [Foos, Bars], req => isOneOfMany(req))
srv.after('READ', [Foo, Bar], (data) => { isOneOfMany(data); return data })
srv.after('READ', [Foos, Bars], (data) => isManyOfMany(data))

srv.after("each", [Foos, Bars], (data) => { isOneOfMany(data); return data })
srv.after("each", [Foo, Bar], (data) => { isOneOfMany(data); return data })

srv.on('UPDATE', Foo, (req, next) => { isOne(req); return next() })
srv.on('UPDATE', Foos, (req, next) => { isOne(req); return next() })
Expand All @@ -226,13 +263,27 @@ srv.before('UPDATE', Foos, req => isOne(req))
srv.after('UPDATE', Foo, (data) => { isOne(data); return data })
srv.after('UPDATE', Foos, (data) => isOne(data))

srv.on('UPDATE', [Foo, Bar], (req, next) => { isOneOfMany(req); return next() })
srv.on('UPDATE', [Foos, Bars], (req, next) => { isOneOfMany(req); return next() })
srv.before('UPDATE', [Foo, Bar], req => { isOneOfMany(req); return req.data })
srv.before('UPDATE', [Foos, Bars], req => isOneOfMany(req))
srv.after('UPDATE', [Foo, Bar], (data) => { isOneOfMany(data); return data })
srv.after('UPDATE', [Foos, Bars], (data) => isOneOfMany(data))

srv.on('DELETE', Foo, (req, next) => { isOne(req); return next() })
srv.on('DELETE', Foos, (req, next) => { isOne(req); return next() })
srv.before('DELETE', Foo, req => { isOne(req); return req.data })
srv.before('DELETE', Foos, req => isOne(req))
srv.after('DELETE', Foo, (data) => { isOne(data); return data })
srv.after('DELETE', Foos, (data) => isOne(data))

srv.on('DELETE', [Foo, Bar], (req, next) => { isOneOfMany(req); return next() })
srv.on('DELETE', [Foos, Bars], (req, next) => { isOneOfMany(req); return next() })
srv.before('DELETE', [Foo, Bar], req => { isOneOfMany(req); return req.data })
srv.before('DELETE', [Foos, Bars], req => isOneOfMany(req))
srv.after('DELETE', [Foo, Bar], (data) => { isOneOfMany(data); return data })
srv.after('DELETE', [Foos, Bars], (data) => isOneOfMany(data))

// unbound
srv.before(action, (req) => {
req.data.foo
Expand Down
7 changes: 7 additions & 0 deletions test/typescript/apis/project/dummy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ export class Foo {

export class Foos extends Array<Foo> { static readonly drafts: typeof Foo }

export class Bar {
static readonly drafts: typeof Bar
name: string = "bar"
}

export class Bars extends Array<Bar> { static readonly drafts: typeof Foo }


// for bound/ unbound actions
export const action = ((foo: Foo) => 42) as Action
Expand Down

0 comments on commit c7f22b2

Please sign in to comment.