diff --git a/bookshop/db/schema.cds b/bookshop/db/schema.cds index 5e7f92c9..fd9d6a21 100644 --- a/bookshop/db/schema.cds +++ b/bookshop/db/schema.cds @@ -36,6 +36,7 @@ entity Authors : managed, extensible { extend Authors with { virtual age : Integer; + virtual exampleBook: String; } /** @@ -47,3 +48,9 @@ entity Genres : sap.common.CodeList { children : Composition of many Genres on children.parent = $self; } + +entity Publishers: managed { + key ID: Integer; + name: String(111); + +} \ No newline at end of file diff --git a/bookshop/handlers/AdminService.Authors.READ.js b/bookshop/handlers/AdminService.Authors.READ.js index 315a2c87..ba0acdc5 100644 --- a/bookshop/handlers/AdminService.Authors.READ.js +++ b/bookshop/handlers/AdminService.Authors.READ.js @@ -21,8 +21,21 @@ function getAge(from, to) { return year } -const result_ = Array.isArray(result) ? result : [result] -for (const row of result_) { - row.modifiedBy += " --- read in sandbox" - row.age = getAge(row.dateOfBirth, row.dateOfDeath) +async function run() { + const result_ = Array.isArray(result) ? result : [result] + for (const row of result_) { + row.age = getAge(row.dateOfBirth, row.dateOfDeath) + let res = await SELECT.one`title`.from(`Books`).where({ author_ID: row.ID }) + if (!res) { + res = {} + } + let { title } = res + if (!title) { + title = "no Books yet" + } + row.exampleBook = title + //let pub = await SELECT.one`name`.from(`sap_capire_bookshop_Publishers`) + } } + +run() diff --git a/bookshop/notebook.md b/bookshop/notebook.md index 1ce4bd2e..c7c07988 100644 --- a/bookshop/notebook.md +++ b/bookshop/notebook.md @@ -1,76 +1,112 @@ -```swift -using { Currency, managed, sap } from '@sap/cds/common'; -namespace sap.capire.bookshop; +# Base assumption -/* - Annotations available: - - Entity level - @expression.constraint : [{if: 'expression evaluates to bool'}, on: ['INSERT, UPDATE, DELETE'], error: 'Transaction Rollback and error message', warning: 'Transaction proceeds and warning message'] - @expression.computed : [{expression: 'ability to access request payload and modify it', on: ['INSERT, UPDATE']}] - @event : [{if: 'expression evaluates to bool', on: ['INSERT, UPDATE, DELETE, READ'], when: 'before or after, default before', emit: 'Event Name', to: 'Messaging target, optional'}] - @expresion.code :[{file: 'file name', on:['insert', 'update'], when: 'before or after, default before'}, - {source: 'each => { if (each.stock > 111) {each.title += ` -- 11% discount!`; each.price= each.price*0.9}', on:['insert', 'update'], when: 'before or after'}] - Atribute Level - @assert.constraint : {if: 'stock>=0 OR stock <1000', error: 'i18n/error102'}; - @event : {if: 'expression evaluates to bool', on: ['INSERT, UPDATE, DELETE, READ'], when: 'before or after', emit: 'Event Name', to: 'Messaging target, optional' } +Event handlers will always use **publicly available application API's**(services) - Functions available: - EXISTS(association target) - COUNT,AVG,MIN,MAX,SUM: Composition items, arrays etc - OLD: before image - EACH: loop over composition items +- already done in Sandbox API by overwriting **SELECT**, **UPDATE**, **READ** and **CREATE** - Events covered: - CRUD --> Longhand and Shorthand supported? - Upsert as one event? - Before and after: - Before can change change request payload and stop transaction - After should trigger only asynchronous messages - Specific Events for status changes? I think expression based event emitter suffices +## Inbound data for validations + +- req.target plus expand on related data + - lazy loading on expand +- event facade could have an explicit publishing of specific services or documents (e.g. remote services) +- CQN Protocol adapter for subsequent reads --> req.data plus application service calls +- what is the CDS subset to put in? + - req.data + target-rec (proxy, unloaded) + - ORM type lazy loading (dereferenced) + - application developer could actually provide custom proxies for specific functions + - performance impact of multiple accesses to object graph and multiple DB roundtrips + - can static code checking or developer annotations influence what is loaded into a graph? + - alternative: Stripped-down SELECT limited to req.target and ID + - application service only + - access rights of user respected +- What about to-many relationships? For compositions essential, for associations to be questioned +- Application Service Reads +- outbound data for changes +- call remote services + - register new remote services dynamically + - CAP provides an API on remote services - connect doesn't need to be done by extension developer + - alternative: declarative remote services plumbing with CDS service facade + - model looks like static internal services, remote calls done transparently behind the scenes + +-Emit Events + +- choreography of extension points + - deep inserts vs. fine grained operations + - input validation may be suited for fine grained operations + - today not in scope for performance reasons + - two different use case: Insert new page to book vs. update order-header with items-constraints in place +- reject request, return errors and warnings - suitable for UI, too */ +/* +Annotations available: +Entity level + @expression.constraint : [{if: 'expression evaluates to bool'}, on: ['INSERT, UPDATE, DELETE'], error: 'Transaction Rollback and error message', warning: 'Transaction proceeds and warning message'] + @expression.computed : [{expression: 'ability to access request payload and modify it', on: ['INSERT, UPDATE']}] + @event : [{if: 'expression evaluates to bool', on: ['INSERT, UPDATE, DELETE, READ'], when: 'before or after, default before', emit: 'Event Name', to: 'Messaging target, optional'}] + @expresion.code :[{file: 'file name', on:['insert', 'update'], when: 'before or after, default before'}, + {source: 'each => { if (each.stock > 111) {each.title += `-- 11% discount!`; each.price= each.price*0.9}', on:['insert', 'update'], when: 'before or after'}] +Atribute Level + @assert.constraint : {if: 'stock>=0 OR stock <1000', error: 'i18n/error102'}; + @event : {if: 'expression evaluates to bool', on: ['INSERT, UPDATE, DELETE, READ'], when: 'before or after', emit: 'Event Name', to: 'Messaging target, optional' } + +Functions available: + EXISTS(association target) + COUNT,AVG,MIN,MAX,SUM: Composition items, arrays etc + OLD: before image + EACH: loop over composition items + +Events covered: +CRUD --> Longhand and Shorthand supported? +Upsert as one event? +Before and after: + Before can change change request payload and stop transaction + After should trigger only asynchronous messages +Specific Events for status changes? I think expression based event emitter suffices +*/ //Entity level annotations @expression.constraint : [{if: 'stock>100 AND price>15)', on: ['INSERT', 'UPDATE'], error: 'No Book over price 15 should have more than 100 stock' }, // error, rollback transactions - {if: 'stock>90 AND price>15)', on: ['I', 'U'], warning: 'No Book over price 15 should have more than 100 stock' }] //warning, proceed with transaction but report warning back to UI -@expression.computed : {expression: 'if(stock>100) then price=price*0.9', on: ['INSERT']} //ability to modify the payload of the request, but nothing beyond it +{if: 'stock>90 AND price>15)', on: ['I', 'U'], warning: 'No Book over price 15 should have more than 100 stock' }] //warning, proceed with transaction but report warning back to UI +@expression.computed : {expression: 'if(stock>100) then price=price*0.9', on: ['INSERT']} //ability to modify the payload of the request, but nothing beyond it @expresion.code :[{file: 'sap.capire.bookshop-Books-beforeInsert', on:['insert', 'update'], when: 'before'}, //naming can be arbitrary? - {source: 'each => { if (each.stock > 111) {each.title += ` -- 11% discount!`; each.price= each.price*0.9}', on:['insert', 'update'], when: 'before'}] //alternative +{source: 'each => { if (each.stock > 111) {each.title += `-- 11% discount!`; each.price= each.price*0.9}', on:['insert', 'update'], when: 'before'}] //alternative @event : { if:'price>200', emit: 'Expensive Book', to: 'RulesEngine'} entity Books : managed { - key ID : Integer; - title : localized String(111); @event : {if: 'old.title="Hello"', emit: 'Hello changed' } //old refers to before Image. No "to" clause means message is emitted to any subscriber interested - descr : localized String(1111); - author : Association to Authors @assert.constraint: 'exists(author)'; //function calls need to evaluate to bool - genre : Association to Genres; - stock : Integer @assert.constraint : {if: 'stock>=0 OR stock <1000', error: 'Stock not within permitted parameters'}; //when operand is used, no auto-insert - price : Decimal(9,2) @assert.constraint : '>0'; //insert operand on left side by default - currency : Currency; - image : LargeBinary @Core.MediaType : 'image/png'; - stockWorth: Decimal(9,2) @expression.computed : 'stock*price'; //persisted on write. Overhead in runtime, but performance benefit on read. Payload ignored? - // stockWorth2 = stock*price; -- long term goal from compiler team, not persisted on write, but calculated on read - stockWorth3 : Decimal @expression.computed: 'if (stock*price>1000) then stockWorth3=stock.price else stockworth3=1000'; //which altenative? - stockWorth4 : Decimal @expression.computed: {if: '(stock*price>1000)', then: 'stockWorth3=stock.price', else: 'stockworth3=1000'}; +key ID : Integer; +title : localized String(111); @event : {if: 'old.title="Hello"', emit: 'Hello changed' } //old refers to before Image. No "to" clause means message is emitted to any subscriber interested +descr : localized String(1111); +author : Association to Authors @assert.constraint: 'exists(author)'; //function calls need to evaluate to bool +genre : Association to Genres; +stock : Integer @assert.constraint : {if: 'stock>=0 OR stock <1000', error: 'Stock not within permitted parameters'}; //when operand is used, no auto-insert +price : Decimal(9,2) @assert.constraint : '>0'; //insert operand on left side by default +currency : Currency; +image : LargeBinary @Core.MediaType : 'image/png'; +stockWorth: Decimal(9,2) @expression.computed : 'stock*price'; //persisted on write. Overhead in runtime, but performance benefit on read. Payload ignored? +// stockWorth2 = stock*price; -- long term goal from compiler team, not persisted on write, but calculated on read +stockWorth3 : Decimal @expression.computed: 'if (stock*price>1000) then stockWorth3=stock.price else stockworth3=1000'; //which altenative? +stockWorth4 : Decimal @expression.computed: {if: '(stock*price>1000)', then: 'stockWorth3=stock.price', else: 'stockworth3=1000'}; } //@assert.expression: 'dateOfBirthdateOfBirth'; - placeOfBirth : String; - placeOfDeath : String; - books : Association to many Books on books.author = $self; +key ID : Integer; +name : String(111); +dateOfBirth : Date ; +dateOfDeath : Date @expression.constraint: '>dateOfBirth'; +placeOfBirth : String; +placeOfDeath : String; +books : Association to many Books on books.author = $self; } /** Hierarchically organized Code List for Genres */ entity Genres : sap.common.CodeList { - key ID : Integer; - parent : Association to Genres; - children : Composition of many Genres on children.parent = $self; +key ID : Integer; +parent : Association to Genres; +children : Composition of many Genres on children.parent = $self; } - -``` \ No newline at end of file + +``` + +``` diff --git a/bookshop/sqlite.db b/bookshop/sqlite.db index 1c30dd5e..75ba5ea6 100644 Binary files a/bookshop/sqlite.db and b/bookshop/sqlite.db differ diff --git a/bookshop/srv/admin-service.js b/bookshop/srv/admin-service.js index 7cb498a9..d6c0675f 100644 --- a/bookshop/srv/admin-service.js +++ b/bookshop/srv/admin-service.js @@ -1,5 +1,5 @@ const cds = require("@sap/cds") -const cds_sandbox = require("sap/cds/sandbox") +//const cds_sandbox = require("sap/cds/sandbox") const { VM, VMScript } = require("vm2") const fs = require("fs") const path = require("path") @@ -11,7 +11,7 @@ class AdminService extends cds.ApplicationService { if (!(result === undefined || result == null)) { const code = getCode(req.target.name, "READ") if (code) { - await executeCode(code, req, result) + await executeCode.call(this, code, req, result) } } }) @@ -19,14 +19,14 @@ class AdminService extends cds.ApplicationService { this.before("CREATE", async (req) => { const code = getCode(req.target.name, "CREATE") if (code) { - await executeCode(code, req) + await executeCode.call(this, code, req) } }) this.before("UPDATE", async (req) => { const code = getCode(req.target.name, "CREATE") if (code) { - await executeCode(code, req) + await executeCode.call(this, code, req) } }) @@ -36,13 +36,13 @@ class AdminService extends cds.ApplicationService { if (req.constructor.name === "EventMessage") { const code = getCode(req.event, "ON") if (code) { - await executeCode(code, req) + await executeCode.call(this, code, req) } } else if (req.constructor.name === "ODataRequest") { var output = {} const code = getCode(this.name + "." + req.event, "ON") if (code) { - await executeCode(code, req, {}, output) + await executeCode.call(this, code, req, {}, output) return output } } @@ -63,7 +63,7 @@ var counter = 1; function newLabel() {return "VM2 - req: " + counter++} -function getCode(name, operation) { +function getCodeFromFile(name, operation) { const filename = name + "." + operation + ".js" const file = path.join(__dirname, "..", "handlers", filename) try { @@ -74,51 +74,37 @@ function getCode(name, operation) { } } +function getCodeFromAnnotation(name, operation) { + return "" +} + +function getCode(name, operation) { + let code=getCodeFromAnnotation(name, operation) + if (code==="") {code=getCodeFromFile(name, operation)} + return code +} + function scanCode(code) { //ESLINT } -/* -Base assumption: event handlers will always use publicly available application API's (services) -Inbound data for validations - - this could be a document --> req.target plus expand on related data - - event facade could have an explicit publishing of specific services or documents (e.g. remote services) - - CQN Protocol adapter for subsequent reads --> req.data plus application service calls - - what is the CDS subset to put in? - - req.data + target-rec (proxy, unloaded) - - ORM type lazy loading (dereferenced) - - application developer could actually provide custom proxies for specific functions - - performance impact of multiple accesses to object graph and multiple DB roundtrips - - can static code checking or developer annotations influence what is loaded into a graph? - - alternative: Stripped-down SELECT limited to req.target and ID - - application service only - - access rights of user respected - - What about to-many relationships? For compositions essential, for associations to be questioned - - Application Service Reads - - outbound data for changes - - call remote services - - register new remote services dynamically - - CAP provides an API on remote services - connect doesn't need to be done by extension developer - - alternative: declarative remote services plumbing with CDS service facade - - model looks like static internal services, remote calls done transparently behind the scenes - -Emit Events - - - choreography of extension points - - deep inserts vs. fine grained operations - - input validation may be suited for fine grained operations - - today not in scope for performance reasons - - two different use case: Insert new page to book vs. update order-header with items-constraints in place - - reject request, return errors and warnings - suitable for UI, too - -*/ async function executeCode(code, req, result, output) { + const srv = this const label=newLabel() console.time(label) const vm = new VM({ console: "inherit", timeout: 500, allowAsync: true, - sandbox: { req, result, output, cds, SELECT, INSERT, UPDATE, CREATE, JSON }, + sandbox: { req, + result, + output, + SELECT : (class extends require('@sap/cds/lib/ql/SELECT') {then(r,e) {return srv.run(this).then(r,e)}})._api(), + INSERT : (class extends require('@sap/cds/lib/ql/INSERT') {then(r,e) {return srv.run(this).then(r,e)}})._api(), + UPDATE : (class extends require('@sap/cds/lib/ql/UPDATE') {then(r,e) {return srv.run(this).then(r,e)}})._api(), + CREATE : (class extends require('@sap/cds/lib/ql/CREATE') {then(r,e) {return srv.run(this).then(r,e)}})._api(), + //srv: this, + JSON }, }) try { diff --git a/bookshop/srv/cat-service.cds b/bookshop/srv/cat-service.cds index 2441db25..fd272b9e 100644 --- a/bookshop/srv/cat-service.cds +++ b/bookshop/srv/cat-service.cds @@ -10,6 +10,8 @@ service CatalogService @(path:'/browse') { author.name as author } excluding { createdBy, modifiedBy }; +@readonly entity Publishers as projection on my.Publishers; + @requires: 'authenticated-user' action submitOrder ( book: Books:ID, quantity: Integer ) returns { stock: Integer }; event OrderedBook : { book: Books:ID; quantity: Integer; buyer: String };