From 6c27228d6235bf3e39736f7261ca55a59dbab5b8 Mon Sep 17 00:00:00 2001 From: nkaputnik Date: Thu, 28 Jul 2022 14:55:37 +0200 Subject: [PATCH] Sandbox API with Application Service --- bookshop/db/schema.cds | 7 + .../handlers/AdminService.Authors.READ.js | 21 ++- bookshop/notebook.md | 146 +++++++++++------- bookshop/sqlite.db | Bin 77824 -> 81920 bytes bookshop/srv/admin-service.js | 68 ++++---- bookshop/srv/cat-service.cds | 2 + 6 files changed, 144 insertions(+), 100 deletions(-) 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 1c30dd5ecd36826f53d11423d25dc1e930430d27..75ba5ea6ece0a2570368b2c4bcdda7a79c7a4361 100644 GIT binary patch delta 3070 zcmeHJeQ*=k5x>2Y?oKD2?rixaf60<;OmG2P5;7(>U_`=9RAV9#8A6Q5bwLm}{s3&; zp$)AinS@X#L0)QsX&vYv4aszZ>z9NMNnE!{N&>~iq479S2RmtF1}F)T!cd^}Bx4|S z_`~@-H*@dazWwdT+kJccT81?(E!yoFNm`1cQq*_+nH`-rqd-gLSImfWw9;stVUn&% zm!%8RIcZdPgWJ#VV;6CYG{0f$=}t(}KOK!mn1~>gSG8M#36)FuANr^eb{RRPw3Lr* zz<$P*?ceaj+M32EgXaK-gi1`_C;Pw+Nu06?NQ%9JsGO(7$ay%+p zF>SYWi^bycUD+Bj)U3l7cg_a^`*&ow4RWa8wUvHMz*D-Vygu|(jhT(sj61E=Pc zk0&-{)Wr4zOaX1|FhEcUKfrJnpARCwwvII2?Jc6$sN6;E-JNq66*4xXS+Zd5ugw|$ zCBAa+lI8ZY1X4ML z4)SDmkgV!pl)L3(#_685)cIhs%T?@|l|R$tdZN0%p?+gs!-ht;)9s$_ESi1~fc(g$ zr6(@+qQb$%r3TD)lnhCfF!P zN?92n4vdMO5!&8l@vVdJ(5q8%&0C}NN(+YG{0F_pj52+iu2j(f_9;j~XU}K6Dbb|D zb>os)^v4gFP#_t-zdz1|o=CzOa3V_--lud>Nb%j=L3W?^6-^sG1cl;r`eeNT(}_b) z20rmWjpR-udA%91BgQ&h1$-A)NV=X*Gl_DSX@-EOoS=27dYnrPIoOZZ7qbS^2s;-HP;)zdhovXXDj zX|4(W7%yC&fIDtM6b z)O!LtFAA9T<+5UMaK*<4hPQWlBC$C+7 z8H9*DE0OTc8LCMWI$xI)2W9H(E8?b$+S(O0>IO)OpG{W(5hc|a`;0E}c>~dZL>%@W zy)RVxf?u3to3ogr91d!Eh`Fb?dx{HP`OX>c`!@f=Vy7qHUFeB9G9f4CN`+=-*_L1o zGQeCgc7JdeyWIKi2Mh13VjK`xfw32?V3}}>jty9$apF=Nv!Hh3Qi=_pnz&S#4gCKD DPV(1f delta 4180 zcmeHKYitzP6`nIQJ2N}8JG)+cc8zW9U4va5<7J;-@A^RyX^jO48*q3U$7xJUAy9-% zf>KxBUE4TGA3-_J@kdqx2UI~YV{)ygG=N0twrCfCLx&?KgvmLvDKu(I10qLvN z3Giqove#=AMx!Yu(Fbyz&8hqs{LalNF~%x{o{_){$JTD%w%kE0UH z4Ad-2sLEUdCIhRtcA5J~>y~*v%f*+|s9mf^32QF;q48pu_1F=BJ8wiEhc5L0Yb=TwDzP2Oa z3k0fsk*d1@>Q{12^z=u4U*Tc;qfR>yeulYTn^D0|d#+=}EUmK5{nnC8 z!faRl?xlMPc_q!%nm3`wE8b0M`>cI?^XV+r^)Y8iQ#{(nxKIwzj}2;3HEprFBKm#eHx zEssgx5Wge%xZUhk=6O1gs-Z6LS>QsxQ4`6e@GfdkeaggF*irMyALK-LA+Ic0AoQ7y z3bTE*P+fg)6)=gm0?k_rZNMb$GqfdD@Ej>7<@vnQ+A8z~4J@21COMzej=SLrV0zcP z)V+V(K!X!~a&egcqJmC*c)_|ck5?KSg~YPk^^5!PH*{Mrdgh`tyG#dTcRtwAJJ<8no7YbQ4L8;H z)KYM8<(x57Z&w+wtXU%rG~xp^l#y^Zp*O}w)O@3p>Ft`WesjEwW|A#M>W0)QsL-pv z0JkXX)6&}_FRbTYH!Wg@$P1EmjRR83;>ALL7@w*jwhVu2!|&u0?e9w!;^I88tHW$P z;LBDB3Um21@}@J5v?yuhNM#23dmH%iEe7(nBQ&f4%LBa9)+Y4MGJJe_+%qA=4W%Gz z^I50_sL(c>;1IwZ&B>Fy78l`7pc~4yUkUI_Ku?e7W5VRhe5TrbBT6sPzlyRO_UlU; z2RU=dsXyy)Gj07jyFc?jbLUAvM^-i8{g3h{lbt8`neIrU76#5y25eRhgm3XUu5 z)(L&}d-KVE?-`YFSw0kLN6VoTAgVd%!0RAdPBO15%a#fKxf=NkUk5y+5@aJU;q66G zXKs%06Wv&My>_$`He_SuDyU@=|6Qggfy@MymKGt=ZCH9>gu^Fu;Lk*r^c?L+4iG>H z7z#_yoCuYXv2-yQ$%S)nt9dKMQz=Sz^G&ANteYu%;RjvhwH~BMZxuV>SsHwJ zR3YU`bioMB)Bf&)Qkv;4^=fBlk|*+!?6LcWyCiYZ36&)qYP6pe!Cb(DbHRj5ir`UP zTm_Z*Juk6;n$@s+q8c&@K8z3M5P`Vmj1%rHIDfGorIQubTFZBeB^iQ{6a`7z;D{w&Xs zrOet%)<4)n&lH(jSNgqcMNj>x%^cb#-|c0N<5n=(ADVyQw*M0c C_JY3v 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 };