diff --git a/bookshop/db/schema.cds b/bookshop/db/schema.cds index ec8b119a..915a9289 100644 --- a/bookshop/db/schema.cds +++ b/bookshop/db/schema.cds @@ -1,7 +1,8 @@ -using { Currency, managed, sap } from '@sap/cds/common'; +using { Currency, managed, sap, extensible } from '@sap/cds/common'; namespace sap.capire.bookshop; -entity Books : managed { +@Extensibility.Any.Enabled : true +entity Books : managed, extensible { key ID : Integer; title : localized String(111); descr : localized String(1111); @@ -11,15 +12,26 @@ entity Books : managed { price : Decimal; currency : Currency; image : LargeBinary @Core.MediaType : 'image/png'; + authorName: String; } -entity Authors : managed { +@Extensibility : { + Fields.Enabled : true, + Relations.Enabled : false, + Annotations.Enabled : true, + Logic.Enabled : true, + Logic.constraints: true, + Logic.calculations: true, + Logic.Handler : [create, update, delete, read] +} +entity Authors : managed, extensible { key ID : Integer; name : String(111); dateOfBirth : Date; dateOfDeath : Date; placeOfBirth : String; placeOfDeath : String; + virtual age: Integer; books : Association to many Books on books.author = $self; } diff --git a/bookshop/handlers/AdminService.Authors.CREATE.js b/bookshop/handlers/AdminService.Authors.CREATE.js index 67f096cf..fc03731f 100644 --- a/bookshop/handlers/AdminService.Authors.CREATE.js +++ b/bookshop/handlers/AdminService.Authors.CREATE.js @@ -1,13 +1,13 @@ async function run() { - debugger - while (true) {} - process.exit() + //debugger + //while (true) {} + //process.exit() //1.substring() let res = await cds.read(SELECT.one`title`.from(`Books`).where(`ID=201`)) let { title } = res const data = req.data - data.modifiedBy = "Custom Event handler read changed this!"; - data.placeOfDeath = ' --- Somewhere over ' + title + ' --- create in Sandbox' + data.modifiedBy = "Custom Event handler read changed this!" + data.placeOfDeath = " --- Somewhere over " + title + " --- create in Sandbox" return data } output = run() diff --git a/bookshop/handlers/AdminService.Authors.READ.js b/bookshop/handlers/AdminService.Authors.READ.js new file mode 100644 index 00000000..44f1f6f6 --- /dev/null +++ b/bookshop/handlers/AdminService.Authors.READ.js @@ -0,0 +1,5 @@ +const result_ = Array.isArray(result) ? result : [result] +for (const row of result_) { + row.modifiedBy += " --- read in sandbox" + row.age = 27 +} diff --git a/bookshop/handlers/AdminService.Books.CREATE.js b/bookshop/handlers/AdminService.Books.CREATE.js new file mode 100644 index 00000000..ab9eab58 --- /dev/null +++ b/bookshop/handlers/AdminService.Books.CREATE.js @@ -0,0 +1,9 @@ + +async function run() { + const {stock, price, author_ID} = req.data + if (stock<0) return req.reject('409', 'Stock must not be negative') + if (price<0) return req.reject('409', 'Price must not be negative') + let {name} = await SELECT.one`name`.from(`Authors`).where({ID: author_ID}) + req.data.authorName=name +} +output = run() diff --git a/bookshop/handlers/AdminService.Books.READ.js b/bookshop/handlers/AdminService.Books.READ.js new file mode 100644 index 00000000..1c946e87 --- /dev/null +++ b/bookshop/handlers/AdminService.Books.READ.js @@ -0,0 +1,6 @@ +const result_ = Array.isArray(result) ? result : [result]; +for (const row of result_) { + if (row.stock > 50) { + row.title += " ---Order now for a 10% discount!"; + } +} diff --git a/bookshop/package.json b/bookshop/package.json index f2fb4210..1141a6e4 100644 --- a/bookshop/package.json +++ b/bookshop/package.json @@ -12,7 +12,8 @@ "dependencies": { "@sap/cds": ">=5.9", "express": "^4.17.1", - "passport": ">=0.4.1" + "passport": ">=0.4.1", + "vm2": ">=3.9.9" }, "scripts": { "genres": "cds serve test/genres.cds", @@ -22,8 +23,11 @@ "cds": { "requires": { "db": { - "kind": "sql" + "kind": "sqlite", + "credentials": { + "database": "sqlite.db" + } } } } -} +} \ No newline at end of file diff --git a/bookshop/sqlite.db b/bookshop/sqlite.db new file mode 100644 index 00000000..c5f05ec2 Binary files /dev/null and b/bookshop/sqlite.db differ diff --git a/bookshop/srv/admin-service.cds b/bookshop/srv/admin-service.cds index ea9b0731..d3fd5a9e 100644 --- a/bookshop/srv/admin-service.cds +++ b/bookshop/srv/admin-service.cds @@ -1,5 +1,13 @@ -using { sap.capire.bookshop as my } from '../db/schema'; -service AdminService @(requires:'admin') { - entity Books as projection on my.Books; +using {sap.capire.bookshop as my} from '../db/schema'; + +service AdminService // @(requires : 'admin') +{ + entity Books as projection on my.Books; entity Authors as projection on my.Authors; + action renameAuthor(author : Authors:ID, newName : String); + + event newBook : { + book : Books:ID; + name : Books:title + }; } diff --git a/bookshop/srv/admin-service.js b/bookshop/srv/admin-service.js index d02133d8..7cb498a9 100644 --- a/bookshop/srv/admin-service.js +++ b/bookshop/srv/admin-service.js @@ -1,77 +1,144 @@ -const cds = require("@sap/cds"); -const { VM, VMScript } = require("vm2"); -const fs = require("fs"); -const path = require("path"); +const cds = require("@sap/cds") +const cds_sandbox = require("sap/cds/sandbox") +const { VM, VMScript } = require("vm2") +const fs = require("fs") +const path = require("path") +const { nextTick } = require("process") class AdminService extends cds.ApplicationService { init() { - const { Books, Authors } = cds.entities("sap.capire.bookshop"); - this.after("READ", async (result, req) => { - const code = getCode(req, "READ"); - if (code) { - await executeCode(code, req, result); + if (!(result === undefined || result == null)) { + const code = getCode(req.target.name, "READ") + if (code) { + await executeCode(code, req, result) + } } - }); - - this.after("READ", "ListOfBooks", (each) => { - if (each.stock > 111) each.title += ` -- 11% discount!`; - }); + }) this.before("CREATE", async (req) => { - const code = getCode(req, "CREATE"); + const code = getCode(req.target.name, "CREATE") if (code) { - await executeCode(code, req); - //console.log(req.data) + await executeCode(code, req) } - }); + }) - //this.before("NEW", "Authors", genid); - //this.before("NEW", "Books", genid); - return super.init(); + this.before("UPDATE", async (req) => { + const code = getCode(req.target.name, "CREATE") + if (code) { + await executeCode(code, req) + } + }) + + this.on("*", async (req, next) => { + if (!(req.target === undefined || req.target == null)) return next() + //ToDo: check whether action or event is part of an extension + if (req.constructor.name === "EventMessage") { + const code = getCode(req.event, "ON") + if (code) { + await executeCode(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) + return output + } + } + }) + + //ToDo: Prefix for Service not in event emitter + this.before("CREATE", "Authors", async (req) => { + let Author = req.data + await this.emit("createdAuthor", { Author }) + + }) + + return super.init() } } -function getCode(req, operation) { - const filename = req.target.name + "." + operation + ".js"; - const file = path.join(__dirname, "..", "handlers", filename); +var counter = 1; + +function newLabel() {return "VM2 - req: " + counter++} + +function getCode(name, operation) { + const filename = name + "." + operation + ".js" + const file = path.join(__dirname, "..", "handlers", filename) try { - const code = fs.readFileSync(file, "utf8"); - return code; + const code = fs.readFileSync(file, "utf8") + return code } catch (error) { - return ""; + return "" } } 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 -async function executeCode(code, req, result) { - let output = {}; - console.time("vm2"); + -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 label=newLabel() + console.time(label) const vm = new VM({ console: "inherit", - timeout: 1000, + timeout: 500, allowAsync: true, sandbox: { req, result, output, cds, SELECT, INSERT, UPDATE, CREATE, JSON }, - }); - + }) + try { await vm.run(code) + return output } catch (error) { - req.reject('409','Error in VM') console.log(error) + req.reject("409", "Error in VM") + } + finally { + console.timeEnd(label) } // console.log(req.data) - console.timeEnd("vm2"); } /** Generate primary keys for target entity in request */ async function genid(req) { const { ID } = await cds .tx(req) - .run(SELECT.one.from(req.target).columns("max(ID) as ID")); - req.data.ID = ID - (ID % 100) + 100 + 1; + .run(SELECT.one.from(req.target).columns("max(ID) as ID")) + req.data.ID = ID - (ID % 100) + 100 + 1 } -module.exports = { AdminService }; +module.exports = { AdminService } diff --git a/bookshop/test/requests.http b/bookshop/test/requests.http index 7fddca0f..867a426c 100644 --- a/bookshop/test/requests.http +++ b/bookshop/test/requests.http @@ -1,10 +1,46 @@ @server = http://localhost:4004 @me = Authorization: Basic {{$processEnv USER}}: +@id = 1113 + + +### ------------------------------------------------------------------------ +# Fetch Authors +GET {{server}}/admin/Authors + +### ------------------------------------------------------------------------ +# Fetch one Author +GET {{server}}/admin/Authors({{id}}) + +### ------------------------------------------------------------------------ +# Create Author +POST {{server}}/admin/Authors +Content-Type: application/json;IEEE754Compatible=true + +{ + "ID": {{id}}, + "name": "Nick", + "placeOfBirth": "Somewhere", + "placeOfDeath": "over the Rainbox", + "dateOfBirth" : "1975-05-27" +} + +### ------------------------------------------------------------------------ +# rename author via unbound action +POST {{server}}/admin/renameAuthor +Content-Type: application/json +{{me}} + +{ "author":{{id}}, "newName":"Super Nick" } ### ------------------------------------------------------------------------ # Get service info -GET {{server}}/browse +GET {{server}}/admin +{{me}} + +### ------------------------------------------------------------------------ +# Get $metadata document +GET {{server}}/admin/$metadata {{me}} @@ -23,21 +59,8 @@ GET {{server}}/browse/ListOfBooks? {{me}} -### ------------------------------------------------------------------------ -# Fetch Authors as admin -GET {{server}}/admin/Authors(307) -### ------------------------------------------------------------------------ -# Create Author -POST {{server}}/admin/Authors -Content-Type: application/json;IEEE754Compatible=true -{ - "ID": 317, - "name": "Vitaly", - "placeOfDeath": "", - "placeOfBirth": "" -} ### ------------------------------------------------------------------------ # Fetch Books as admin @@ -50,12 +73,12 @@ Content-Type: application/json;IEEE754Compatible=true Authorization: Basic alice: { - "ID": 13, - "title": "Deh3", + "ID": 16, + "title": "Deh4", "descr": "The Everyman's Library Pocket Poets hardcover series is popular for its compact size and reasonable price which does not compromise content. Poems: Bronte contains poems that demonstrate a sensibility elemental in its force with an imaginative discipline and flexibility of the highest order. Also included are an Editor's Note and an index of first lines.", "author": { "ID": 101 }, "genre": { "ID": 12 }, - "stock": 100, + "stock": -100, "price": "12.05", "currency": { "code": "USD" } }