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)
Annotations available:
Entity level - already done in Sandbox API by overwriting **SELECT**, **UPDATE**, **READ** and **CREATE**
@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: ## Inbound data for validations
EXISTS(association target)
COUNT,AVG,MIN,MAX,SUM: Composition items, arrays etc
OLD: before image
EACH: loop over composition items
Events covered: - req.target plus expand on related data
CRUD --> Longhand and Shorthand supported? - lazy loading on expand
Upsert as one event? - event facade could have an explicit publishing of specific services or documents (e.g. remote services)
Before and after: - CQN Protocol adapter for subsequent reads --> req.data plus application service calls
Before can change change request payload and stop transaction - what is the CDS subset to put in?
After should trigger only asynchronous messages - req.data + target-rec (proxy, unloaded)
Specific Events for status changes? I think expression based event emitter suffices - 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 //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 };