Sandbox API with Application Service

This commit is contained in:
nkaputnik
2022-07-28 14:55:37 +02:00
parent 44880c7745
commit 6c27228d62
6 changed files with 144 additions and 100 deletions

View File

@@ -36,6 +36,7 @@ entity Authors : managed, extensible {
extend Authors with { extend Authors with {
virtual age : Integer; virtual age : Integer;
virtual exampleBook: String;
} }
/** /**
@@ -47,3 +48,9 @@ entity Genres : sap.common.CodeList {
children : Composition of many Genres children : Composition of many Genres
on children.parent = $self; on children.parent = $self;
} }
entity Publishers: managed {
key ID: Integer;
name: String(111);
}

View File

@@ -21,8 +21,21 @@ function getAge(from, to) {
return year return year
} }
const result_ = Array.isArray(result) ? result : [result] async function run() {
for (const row of result_) { const result_ = Array.isArray(result) ? result : [result]
row.modifiedBy += " --- read in sandbox" for (const row of result_) {
row.age = getAge(row.dateOfBirth, row.dateOfDeath) 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()

View File

@@ -1,76 +1,112 @@
```swift # Base assumption
using { Currency, managed, sap } from '@sap/cds/common';
namespace sap.capire.bookshop; Event handlers will always use **publicly available application API's**(services)
- already done in Sandbox API by overwriting **SELECT**, **UPDATE**, **READ** and **CREATE**
## 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: Annotations available:
Entity level 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.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']}] @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'}] @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'}, @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'}] {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 Atribute Level
@assert.constraint : {if: 'stock>=0 OR stock <1000', error: 'i18n/error102'}; @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 : {if: 'expression evaluates to bool', on: ['INSERT, UPDATE, DELETE, READ'], when: 'before or after', emit: 'Event Name', to: 'Messaging target, optional' }
Functions available: Functions available:
EXISTS(association target) EXISTS(association target)
COUNT,AVG,MIN,MAX,SUM: Composition items, arrays etc COUNT,AVG,MIN,MAX,SUM: Composition items, arrays etc
OLD: before image OLD: before image
EACH: loop over composition items EACH: loop over composition items
Events covered: Events covered:
CRUD --> Longhand and Shorthand supported? CRUD --> Longhand and Shorthand supported?
Upsert as one event? Upsert as one event?
Before and after: Before and after:
Before can change change request payload and stop transaction Before can change change request payload and stop transaction
After should trigger only asynchronous messages After should trigger only asynchronous messages
Specific Events for status changes? I think expression based event emitter suffices Specific Events for status changes? I think expression based event emitter suffices
*/ */
//Entity level annotations //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 @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 {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 @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? @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'} @event : { if:'price>200', emit: 'Expensive Book', to: 'RulesEngine'}
entity Books : managed { entity Books : managed {
key ID : Integer; 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 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); descr : localized String(1111);
author : Association to Authors @assert.constraint: 'exists(author)'; //function calls need to evaluate to bool author : Association to Authors @assert.constraint: 'exists(author)'; //function calls need to evaluate to bool
genre : Association to Genres; 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 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 price : Decimal(9,2) @assert.constraint : '>0'; //insert operand on left side by default
currency : Currency; currency : Currency;
image : LargeBinary @Core.MediaType : 'image/png'; 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? 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 // 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? 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'}; stockWorth4 : Decimal @expression.computed: {if: '(stock*price>1000)', then: 'stockWorth3=stock.price', else: 'stockworth3=1000'};
} }
//@assert.expression: 'dateOfBirth<dateOfDeath' //@assert.expression: 'dateOfBirth<dateOfDeath'
entity Authors : managed { entity Authors : managed {
key ID : Integer; key ID : Integer;
name : String(111); name : String(111);
dateOfBirth : Date ; dateOfBirth : Date ;
dateOfDeath : Date @expression.constraint: '>dateOfBirth'; dateOfDeath : Date @expression.constraint: '>dateOfBirth';
placeOfBirth : String; placeOfBirth : String;
placeOfDeath : String; placeOfDeath : String;
books : Association to many Books on books.author = $self; books : Association to many Books on books.author = $self;
} }
/** Hierarchically organized Code List for Genres */ /** Hierarchically organized Code List for Genres */
entity Genres : sap.common.CodeList { entity Genres : sap.common.CodeList {
key ID : Integer; key ID : Integer;
parent : Association to Genres; parent : Association to Genres;
children : Composition of many Genres on children.parent = $self; children : Composition of many Genres on children.parent = $self;
} }
``` ```
```

Binary file not shown.

View File

@@ -1,5 +1,5 @@
const cds = require("@sap/cds") 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 { VM, VMScript } = require("vm2")
const fs = require("fs") const fs = require("fs")
const path = require("path") const path = require("path")
@@ -11,7 +11,7 @@ class AdminService extends cds.ApplicationService {
if (!(result === undefined || result == null)) { if (!(result === undefined || result == null)) {
const code = getCode(req.target.name, "READ") const code = getCode(req.target.name, "READ")
if (code) { 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) => { this.before("CREATE", async (req) => {
const code = getCode(req.target.name, "CREATE") const code = getCode(req.target.name, "CREATE")
if (code) { if (code) {
await executeCode(code, req) await executeCode.call(this, code, req)
} }
}) })
this.before("UPDATE", async (req) => { this.before("UPDATE", async (req) => {
const code = getCode(req.target.name, "CREATE") const code = getCode(req.target.name, "CREATE")
if (code) { 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") { if (req.constructor.name === "EventMessage") {
const code = getCode(req.event, "ON") const code = getCode(req.event, "ON")
if (code) { if (code) {
await executeCode(code, req) await executeCode.call(this, code, req)
} }
} else if (req.constructor.name === "ODataRequest") { } else if (req.constructor.name === "ODataRequest") {
var output = {} var output = {}
const code = getCode(this.name + "." + req.event, "ON") const code = getCode(this.name + "." + req.event, "ON")
if (code) { if (code) {
await executeCode(code, req, {}, output) await executeCode.call(this, code, req, {}, output)
return output return output
} }
} }
@@ -63,7 +63,7 @@ var counter = 1;
function newLabel() {return "VM2 - req: " + counter++} function newLabel() {return "VM2 - req: " + counter++}
function getCode(name, operation) { function getCodeFromFile(name, operation) {
const filename = name + "." + operation + ".js" const filename = name + "." + operation + ".js"
const file = path.join(__dirname, "..", "handlers", filename) const file = path.join(__dirname, "..", "handlers", filename)
try { 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) { function scanCode(code) {
//ESLINT //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) { async function executeCode(code, req, result, output) {
const srv = this
const label=newLabel() const label=newLabel()
console.time(label) console.time(label)
const vm = new VM({ const vm = new VM({
console: "inherit", console: "inherit",
timeout: 500, timeout: 500,
allowAsync: true, 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 { try {

View File

@@ -10,6 +10,8 @@ service CatalogService @(path:'/browse') {
author.name as author author.name as author
} excluding { createdBy, modifiedBy }; } excluding { createdBy, modifiedBy };
@readonly entity Publishers as projection on my.Publishers;
@requires: 'authenticated-user' @requires: 'authenticated-user'
action submitOrder ( book: Books:ID, quantity: Integer ) returns { stock: Integer }; action submitOrder ( book: Books:ID, quantity: Integer ) returns { stock: Integer };
event OrderedBook : { book: Books:ID; quantity: Integer; buyer: String }; event OrderedBook : { book: Books:ID; quantity: Integer; buyer: String };