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
} }
async function run() {
const result_ = Array.isArray(result) ? result : [result] const result_ = Array.isArray(result) ? result : [result]
for (const row of result_) { for (const row of result_) {
row.modifiedBy += " --- read in sandbox"
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,6 +1,43 @@
```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:
@@ -28,11 +65,8 @@ namespace sap.capire.bookshop;
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
@@ -74,3 +108,5 @@ entity Genres : sap.common.CodeList {
} }
``` ```
```

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 };