First push
This commit is contained in:
@@ -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;
|
namespace sap.capire.bookshop;
|
||||||
|
|
||||||
entity Books : managed {
|
@Extensibility.Any.Enabled : true
|
||||||
|
entity Books : managed, extensible {
|
||||||
key ID : Integer;
|
key ID : Integer;
|
||||||
title : localized String(111);
|
title : localized String(111);
|
||||||
descr : localized String(1111);
|
descr : localized String(1111);
|
||||||
@@ -11,15 +12,26 @@ entity Books : managed {
|
|||||||
price : Decimal;
|
price : Decimal;
|
||||||
currency : Currency;
|
currency : Currency;
|
||||||
image : LargeBinary @Core.MediaType : 'image/png';
|
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;
|
key ID : Integer;
|
||||||
name : String(111);
|
name : String(111);
|
||||||
dateOfBirth : Date;
|
dateOfBirth : Date;
|
||||||
dateOfDeath : Date;
|
dateOfDeath : Date;
|
||||||
placeOfBirth : String;
|
placeOfBirth : String;
|
||||||
placeOfDeath : String;
|
placeOfDeath : String;
|
||||||
|
virtual age: Integer;
|
||||||
books : Association to many Books on books.author = $self;
|
books : Association to many Books on books.author = $self;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
async function run() {
|
async function run() {
|
||||||
debugger
|
//debugger
|
||||||
while (true) {}
|
//while (true) {}
|
||||||
process.exit()
|
//process.exit()
|
||||||
//1.substring()
|
//1.substring()
|
||||||
let res = await cds.read(SELECT.one`title`.from(`Books`).where(`ID=201`))
|
let res = await cds.read(SELECT.one`title`.from(`Books`).where(`ID=201`))
|
||||||
let { title } = res
|
let { title } = res
|
||||||
const data = req.data
|
const data = req.data
|
||||||
data.modifiedBy = "Custom Event handler read changed this!";
|
data.modifiedBy = "Custom Event handler read changed this!"
|
||||||
data.placeOfDeath = ' --- Somewhere over ' + title + ' --- create in Sandbox'
|
data.placeOfDeath = " --- Somewhere over " + title + " --- create in Sandbox"
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
output = run()
|
output = run()
|
||||||
|
|||||||
5
bookshop/handlers/AdminService.Authors.READ.js
Normal file
5
bookshop/handlers/AdminService.Authors.READ.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
const result_ = Array.isArray(result) ? result : [result]
|
||||||
|
for (const row of result_) {
|
||||||
|
row.modifiedBy += " --- read in sandbox"
|
||||||
|
row.age = 27
|
||||||
|
}
|
||||||
9
bookshop/handlers/AdminService.Books.CREATE.js
Normal file
9
bookshop/handlers/AdminService.Books.CREATE.js
Normal file
@@ -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()
|
||||||
6
bookshop/handlers/AdminService.Books.READ.js
Normal file
6
bookshop/handlers/AdminService.Books.READ.js
Normal file
@@ -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!";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,7 +12,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sap/cds": ">=5.9",
|
"@sap/cds": ">=5.9",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"passport": ">=0.4.1"
|
"passport": ">=0.4.1",
|
||||||
|
"vm2": ">=3.9.9"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"genres": "cds serve test/genres.cds",
|
"genres": "cds serve test/genres.cds",
|
||||||
@@ -22,8 +23,11 @@
|
|||||||
"cds": {
|
"cds": {
|
||||||
"requires": {
|
"requires": {
|
||||||
"db": {
|
"db": {
|
||||||
"kind": "sql"
|
"kind": "sqlite",
|
||||||
|
"credentials": {
|
||||||
|
"database": "sqlite.db"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
BIN
bookshop/sqlite.db
Normal file
BIN
bookshop/sqlite.db
Normal file
Binary file not shown.
@@ -1,5 +1,13 @@
|
|||||||
using { sap.capire.bookshop as my } from '../db/schema';
|
using {sap.capire.bookshop as my} from '../db/schema';
|
||||||
service AdminService @(requires:'admin') {
|
|
||||||
entity Books as projection on my.Books;
|
service AdminService // @(requires : 'admin')
|
||||||
|
{
|
||||||
|
entity Books as projection on my.Books;
|
||||||
entity Authors as projection on my.Authors;
|
entity Authors as projection on my.Authors;
|
||||||
|
action renameAuthor(author : Authors:ID, newName : String);
|
||||||
|
|
||||||
|
event newBook : {
|
||||||
|
book : Books:ID;
|
||||||
|
name : Books:title
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,77 +1,144 @@
|
|||||||
const cds = require("@sap/cds");
|
const cds = require("@sap/cds")
|
||||||
const { VM, VMScript } = require("vm2");
|
const cds_sandbox = require("sap/cds/sandbox")
|
||||||
const fs = require("fs");
|
const { VM, VMScript } = require("vm2")
|
||||||
const path = require("path");
|
const fs = require("fs")
|
||||||
|
const path = require("path")
|
||||||
|
const { nextTick } = require("process")
|
||||||
|
|
||||||
class AdminService extends cds.ApplicationService {
|
class AdminService extends cds.ApplicationService {
|
||||||
init() {
|
init() {
|
||||||
const { Books, Authors } = cds.entities("sap.capire.bookshop");
|
|
||||||
|
|
||||||
this.after("READ", async (result, req) => {
|
this.after("READ", async (result, req) => {
|
||||||
const code = getCode(req, "READ");
|
if (!(result === undefined || result == null)) {
|
||||||
if (code) {
|
const code = getCode(req.target.name, "READ")
|
||||||
await executeCode(code, req, result);
|
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) => {
|
this.before("CREATE", async (req) => {
|
||||||
const code = getCode(req, "CREATE");
|
const code = getCode(req.target.name, "CREATE")
|
||||||
if (code) {
|
if (code) {
|
||||||
await executeCode(code, req);
|
await executeCode(code, req)
|
||||||
//console.log(req.data)
|
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
//this.before("NEW", "Authors", genid);
|
this.before("UPDATE", async (req) => {
|
||||||
//this.before("NEW", "Books", genid);
|
const code = getCode(req.target.name, "CREATE")
|
||||||
return super.init();
|
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) {
|
var counter = 1;
|
||||||
const filename = req.target.name + "." + operation + ".js";
|
|
||||||
const file = path.join(__dirname, "..", "handlers", filename);
|
function newLabel() {return "VM2 - req: " + counter++}
|
||||||
|
|
||||||
|
function getCode(name, operation) {
|
||||||
|
const filename = name + "." + operation + ".js"
|
||||||
|
const file = path.join(__dirname, "..", "handlers", filename)
|
||||||
try {
|
try {
|
||||||
const code = fs.readFileSync(file, "utf8");
|
const code = fs.readFileSync(file, "utf8")
|
||||||
return code;
|
return code
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return "";
|
return ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
async function executeCode(code, req, result) {
|
-Emit Events
|
||||||
let output = {};
|
|
||||||
console.time("vm2");
|
- 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({
|
const vm = new VM({
|
||||||
console: "inherit",
|
console: "inherit",
|
||||||
timeout: 1000,
|
timeout: 500,
|
||||||
allowAsync: true,
|
allowAsync: true,
|
||||||
sandbox: { req, result, output, cds, SELECT, INSERT, UPDATE, CREATE, JSON },
|
sandbox: { req, result, output, cds, SELECT, INSERT, UPDATE, CREATE, JSON },
|
||||||
});
|
})
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await vm.run(code)
|
await vm.run(code)
|
||||||
|
return output
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
req.reject('409','Error in VM')
|
|
||||||
console.log(error)
|
console.log(error)
|
||||||
|
req.reject("409", "Error in VM")
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
console.timeEnd(label)
|
||||||
}
|
}
|
||||||
// console.log(req.data)
|
// console.log(req.data)
|
||||||
console.timeEnd("vm2");
|
|
||||||
}
|
}
|
||||||
/** Generate primary keys for target entity in request */
|
/** Generate primary keys for target entity in request */
|
||||||
async function genid(req) {
|
async function genid(req) {
|
||||||
const { ID } = await cds
|
const { ID } = await cds
|
||||||
.tx(req)
|
.tx(req)
|
||||||
.run(SELECT.one.from(req.target).columns("max(ID) as ID"));
|
.run(SELECT.one.from(req.target).columns("max(ID) as ID"))
|
||||||
req.data.ID = ID - (ID % 100) + 100 + 1;
|
req.data.ID = ID - (ID % 100) + 100 + 1
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { AdminService };
|
module.exports = { AdminService }
|
||||||
|
|||||||
@@ -1,10 +1,46 @@
|
|||||||
@server = http://localhost:4004
|
@server = http://localhost:4004
|
||||||
@me = Authorization: Basic {{$processEnv USER}}:
|
@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 service info
|
||||||
GET {{server}}/browse
|
GET {{server}}/admin
|
||||||
|
{{me}}
|
||||||
|
|
||||||
|
### ------------------------------------------------------------------------
|
||||||
|
# Get $metadata document
|
||||||
|
GET {{server}}/admin/$metadata
|
||||||
{{me}}
|
{{me}}
|
||||||
|
|
||||||
|
|
||||||
@@ -23,21 +59,8 @@ GET {{server}}/browse/ListOfBooks?
|
|||||||
{{me}}
|
{{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
|
# Fetch Books as admin
|
||||||
@@ -50,12 +73,12 @@ Content-Type: application/json;IEEE754Compatible=true
|
|||||||
Authorization: Basic alice:
|
Authorization: Basic alice:
|
||||||
|
|
||||||
{
|
{
|
||||||
"ID": 13,
|
"ID": 16,
|
||||||
"title": "Deh3",
|
"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.",
|
"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 },
|
"author": { "ID": 101 },
|
||||||
"genre": { "ID": 12 },
|
"genre": { "ID": 12 },
|
||||||
"stock": 100,
|
"stock": -100,
|
||||||
"price": "12.05",
|
"price": "12.05",
|
||||||
"currency": { "code": "USD" }
|
"currency": { "code": "USD" }
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user