Compare commits

..

1 Commits

Author SHA1 Message Date
Daniel
835f450686 PoC for code sandboxing 2020-05-26 14:12:12 +02:00
40 changed files with 315 additions and 382 deletions

View File

@@ -1,10 +0,0 @@
---
name: Question, feedback or bug?
about: Use our community!
title: ''
labels: ''
assignees: ''
---
Please use our community on https://answers.sap.com/tags/9f13aee1-834c-4105-8e43-ee442775e5ce

View File

@@ -1 +0,0 @@
parallel: true

1
.npmrc Normal file
View File

@@ -0,0 +1 @@
@sap:registry=https://npm.sap.com

View File

@@ -4,7 +4,7 @@
// List of extensions which should be recommended for users of this workspace. // List of extensions which should be recommended for users of this workspace.
"recommendations": [ "recommendations": [
"SAPSE.vscode-cds", // "CDS Editor !",
"dbaeumer.vscode-eslint", "dbaeumer.vscode-eslint",
"esbenp.prettier-vscode", "esbenp.prettier-vscode",
"mechatroner.rainbow-csv", "mechatroner.rainbow-csv",

18
.vscode/launch.json vendored
View File

@@ -6,16 +6,24 @@
"configurations": [ "configurations": [
{ {
"name": "bookshop", "name": "bookshop",
"command": "cds watch bookshop", "cwd": "${workspaceFolder}/bookshop",
"request": "launch", "request": "launch",
"type": "node-terminal", "type": "node",
"runtimeExecutable": "npx",
"runtimeArgs": ["-n"],
"args": ["--", "cds", "run", "--with-mocks", "--in-memory?"],
"console": "integratedTerminal",
"skipFiles": ["<node_internals>/**"] "skipFiles": ["<node_internals>/**"]
}, },
{ {
"name": "Fiori app", "name": "Fiori App",
"command": "cds watch fiori", "cwd": "${workspaceFolder}/fiori",
"request": "launch", "request": "launch",
"type": "node-terminal", "type": "node",
"runtimeExecutable": "npx",
"runtimeArgs": ["-n"],
"args": ["--", "cds", "run", "--with-mocks", "--in-memory?"],
"console": "integratedTerminal",
"skipFiles": ["<node_internals>/**"] "skipFiles": ["<node_internals>/**"]
} }
], ],

View File

@@ -50,7 +50,7 @@ npx jest
## Get Support ## Get Support
Check out the documentation at [https://cap.cloud.sap](https://cap.cloud.sap). <br> Check out the documentation at [https://cap.cloud.sap](https://cap.cloud.sap). <br>
In case you have a question, find a bug, or otherwise need support, please use our [community](https://answers.sap.com/tags/9f13aee1-834c-4105-8e43-ee442775e5ce). In case you find a bug or need support, please [open an issue in here](https://github.com/SAP-samples/cloud-cap-samples/issues/new).
## License ## License

View File

@@ -11,6 +11,6 @@ ID;parent_ID;name
19;10;Fairy Tale 19;10;Fairy Tale
20;;Non-Fiction 20;;Non-Fiction
21;20;Biography 21;20;Biography
22;21;Autobiography 22;20;Autobiography
23;20;Essay 23;20;Essay
24;20;Speech 24;20;Speech
1 ID parent_ID name
11 19 10 Fairy Tale
12 20 Non-Fiction
13 21 20 Biography
14 22 21 20 Autobiography
15 23 20 Essay
16 24 20 Speech

View File

@@ -2,7 +2,7 @@ code;symbol;name;descr;numcode;minor;exponent
EUR;€;Euro;European Euro;978;Cent;2 EUR;€;Euro;European Euro;978;Cent;2
USD;$;US Dollar;United States Dollar;840;Cent;2 USD;$;US Dollar;United States Dollar;840;Cent;2
CAD;$;Canadian Dollar;Canadian Dollar;124;Cent;2 CAD;$;Canadian Dollar;Canadian Dollar;124;Cent;2
AUD;$;Australian Dollar;Australian Dollar;036;Cent;2 AUD;$;Australian Dollar;Canadian Dollar;036;Cent;2
GBP;£;British Pound;Great Britain Pound;826;Penny;2 GBP;£;British Pound;Great Britain Pound;826;Penny;2
ILS;₪;Shekel;Israeli New Shekel;376;Agorat;2 ILS;₪;Shekel;Israeli New Shekel;376;Agorat;2
INR;₹;Rupee;Indian Rupee;356;Paise;2 INR;₹;Rupee;Indian Rupee;356;Paise;2
1 code symbol name descr numcode minor exponent
2 EUR Euro European Euro 978 Cent 2
3 USD $ US Dollar United States Dollar 840 Cent 2
4 CAD $ Canadian Dollar Canadian Dollar 124 Cent 2
5 AUD $ Australian Dollar Australian Dollar Canadian Dollar 036 Cent 2
6 GBP £ British Pound Great Britain Pound 826 Penny 2
7 ILS Shekel Israeli New Shekel 376 Agorat 2
8 INR Rupee Indian Rupee 356 Paise 2

24
ext/.eslintrc Normal file
View File

@@ -0,0 +1,24 @@
{
"extends": "eslint:recommended",
"env": {
"node": true,
"es6": true,
"jest": true
},
"parserOptions": {
"ecmaVersion": 2017
},
"globals": {
"SELECT": true,
"INSERT": true,
"UPDATE": true,
"DELETE": true,
"CREATE": true,
"DROP": true,
"cds": true
},
"rules": {
"no-console": "off",
"require-atomic-updates": "off"
}
}

30
ext/.gitignore vendored Normal file
View File

@@ -0,0 +1,30 @@
# CAP ext
_out
*.db
connection.properties
default-*.json
gen/
node_modules/
package-lock.json
target/
# Web IDE, App Studio
.che/
.gen/
# MTA
*_mta_build_tmp
*.mtar
mta_archives/
# Other
.DS_Store
*.orig
*.log
*.iml
*.flattened-pom.xml
# IDEs
.vscode
.idea

6
ext/db/schema.cds Normal file
View File

@@ -0,0 +1,6 @@
using { sap.capire.bookshop.Books } from '@capire/bookshop';
extend Books with {
ISBN : String;
discount : Decimal @assert.range:[0,1];
}

19
ext/package.json Normal file
View File

@@ -0,0 +1,19 @@
{
"name": "ext",
"version": "1.0.0",
"description": "A simple CAP project.",
"repository": "<Add your repository here>",
"license": "UNLICENSED",
"private": true,
"dependencies": {
"@capire/bookshop": "../bookshop",
"@sap/cds": "^4",
"express": "^4"
},
"devDependencies": {
"sqlite3": "^4"
},
"scripts": {
"start": "npx cds run"
}
}

40
ext/server.js Normal file
View File

@@ -0,0 +1,40 @@
const cds = require ('@sap/cds')
const path = require ('path')
const fs = require ('fs')
const protected = {db:1,messaging:1,auth:1}
const { isfile } = cds.utils
cds.on('served', ()=>{
for (let each of cds.services) {
if (each.name in protected) continue
// search for local srv/<each>.js files and if exist
// activate them in a service extension sandbox
const impl = isfile (path.resolve('srv/'+each.name+'.js'))
if (impl) activate_sandboxed (each,impl)
}
})
function activate_sandboxed (srv,impl) {
console.log (`[cds] - extending ${srv.name} with sandboxed:`, {impl:path.relative(process.cwd(),impl)})
const src = fs.readFileSync (impl)
const sandbox = Object.keys(global).filter(name => name !== 'cds')
const fn = new Function (
'module','cds','global','process', ...sandbox,
src
)
// restricted sandboxed variant of 'module' and 'cds'
const module = {}
const cds = {
service: {
impl: fn=>fn
}
}
fn (module,cds,undefined,undefined, ...sandbox.map((()=>(undefined))))
if (typeof module.exports === 'function') try {
module.exports.call (srv,srv)
} catch (e) {
console.log (e)
}
}
module.exports = cds.server

10
ext/srv/AdminService.js Normal file
View File

@@ -0,0 +1,10 @@
module.exports = cds.service.impl(function(){
this.before(['CREATE','UPDATE'],'Books', req => { //> ....
const book = req.data
if (book.stock < 10 && book.discount > 0.5) {
req.error('Hey, da sind so wenig übrig, die wollen wir nicht zu billig verticken')
}
})
})

View File

@@ -0,0 +1,9 @@
console.log ('Böses Zeug', global, process, cds)
// process.exit()
// const fs = require('fs')
// cds.run('Böses Zeugs')
// SELECT.from ('Foo')
module.exports = cds.service.impl(function(){
this.after('READ','Books', each => each.title += ' (served through sandbox)')
})

View File

@@ -7,19 +7,7 @@ AuthorID = Author ID
Stock = Stock Stock = Stock
Name = Name Name = Name
AuthorName = Author's Name AuthorName = Author's Name
DateOfBirth = Date of Birth
DateOfDeath = Date of Death
PlaceOfBirth = Place of Birth
PlaceOfDeath = Place of Death
Authors = Authors Authors = Authors
Order = Order Order = Order
Orders = Orders Orders = Orders
Price = Price Price = Price
Genre = Genre
Genres = Genres
SubGenres = Sub Genres
NumCode = Numeric Code
MinorUnit = Minor Unit
Exponent = Exponent

View File

@@ -1,8 +1,5 @@
sap.ui.define(["sap/fe/core/AppComponent"], function(AppComponent) { sap.ui.define(["sap/fe/core/AppComponent"], ac => ac.extend("admin.Component", {
"use strict"; metadata:{ manifest:'json' }
return AppComponent.extend("admin.Component", { }))
metadata: { manifest: "json" }
});
});
/* eslint no-undef:0 */ /* eslint no-undef:0 */

View File

@@ -24,7 +24,7 @@
"sap.ui5": { "sap.ui5": {
"dependencies": { "dependencies": {
"libs": { "libs": {
"sap.fe.templates": {} "sap.fe": {}
} }
}, },
"models": { "models": {

View File

@@ -1,7 +1,5 @@
sap.ui.define(["sap/fe/core/AppComponent"], function(AppComponent) { sap.ui.define(["sap/fe/core/AppComponent"], ac => ac.extend("bookshop.Component", {
"use strict"; metadata:{ manifest:'json' }
return AppComponent.extend("bookshop.Component", { }))
metadata: { manifest: "json" }
});
});
/* eslint no-undef:0 */ /* eslint no-undef:0 */

View File

@@ -24,7 +24,7 @@
"sap.ui5": { "sap.ui5": {
"dependencies": { "dependencies": {
"libs": { "libs": {
"sap.fe.templates": {} "sap.fe": {}
} }
}, },
"models": { "models": {

View File

@@ -3,7 +3,7 @@
*/ */
using { sap.capire.bookshop as my } from '@capire/bookshop'; using { sap.capire.bookshop as my } from '@capire/bookshop';
using { sap.common } from '@capire/common';
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
// //
@@ -28,6 +28,13 @@ annotate my.Books with @(
author @ValueList.entity:'Authors'; author @ValueList.entity:'Authors';
}; };
annotate my.Authors with @(
UI: {
Identification: [{Value:name}],
}
);
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
// //
// Books Details // Books Details
@@ -59,199 +66,15 @@ annotate my.Books with {
descr @UI.MultiLineText; descr @UI.MultiLineText;
} }
////////////////////////////////////////////////////////////////////////////
//
// Genres List
//
annotate my.Genres with @(
Common.SemanticKey: [name],
UI: {
SelectionFields: [ name ],
LineItem:[
{Value: name},
{Value: parent.name, Label: 'Main Genre'},
],
}
);
////////////////////////////////////////////////////////////////////////////
//
// Genre Details
//
annotate my.Genres with @(
UI: {
Identification: [{Value:name}],
HeaderInfo: {
TypeName: '{i18n>Genre}',
TypeNamePlural: '{i18n>Genres}',
Title: {Value: name},
Description: {Value: ID}
},
Facets: [
{$Type: 'UI.ReferenceFacet', Label: '{i18n>SubGenres}', Target: 'children/@UI.LineItem'},
],
}
);
////////////////////////////////////////////////////////////////////////////
//
// Genres Elements
//
annotate my.Genres with { annotate my.Genres with {
ID @title: '{i18n>ID}';
name @title: '{i18n>Genre}'; name @title: '{i18n>Genre}';
} }
////////////////////////////////////////////////////////////////////////////
//
// Authors List
//
annotate my.Authors with @(
Common.SemanticKey: [name],
UI: {
Identification: [{Value:name}],
SelectionFields: [ name ],
LineItem:[
{Value: ID},
{Value: name},
{Value: dateOfBirth},
{Value: dateOfDeath},
{Value: placeOfBirth},
{Value: placeOfDeath},
],
}
);
////////////////////////////////////////////////////////////////////////////
//
// Author Details
//
annotate my.Authors with @(
UI: {
HeaderInfo: {
TypeName: '{i18n>Author}',
TypeNamePlural: '{i18n>Authors}',
Title: {Value: name},
Description: {Value: dateOfBirth}
},
Facets: [
{$Type: 'UI.ReferenceFacet', Target: 'books/@UI.LineItem'},
],
}
);
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
// //
// Authors Elements // Authors Elements
// //
annotate my.Authors with { annotate my.Authors with {
ID @title:'{i18n>ID}' @UI.HiddenFilter; ID @title:'{i18n>ID}' @UI.HiddenFilter;
name @title:'{i18n>Name}'; name @title:'{i18n>AuthorName}';
dateOfBirth @title:'{i18n>DateOfBirth}';
dateOfDeath @title:'{i18n>DateOfDeath}';
placeOfBirth @title:'{i18n>PlaceOfBirth}';
placeOfDeath @title:'{i18n>PlaceOfDeath}';
}
////////////////////////////////////////////////////////////////////////////
//
// Languages List
//
annotate common.Languages with @(
Common.SemanticKey: [code],
Identification: [{Value:code}],
UI: {
SelectionFields: [ name, descr ],
LineItem:[
{Value: code},
{Value: name},
],
}
);
////////////////////////////////////////////////////////////////////////////
//
// Language Details
//
annotate common.Languages with @(
UI: {
HeaderInfo: {
TypeName: '{i18n>Language}',
TypeNamePlural: '{i18n>Languages}',
Title: {Value: name},
Description: {Value: descr}
},
Facets: [
{$Type: 'UI.ReferenceFacet', Label: '{i18n>Details}', Target: '@UI.FieldGroup#Details'},
],
FieldGroup#Details: {
Data: [
{Value: code},
{Value: name},
{Value: descr}
]
},
}
);
////////////////////////////////////////////////////////////////////////////
//
// Currencies List
//
annotate common.Currencies with @(
Common.SemanticKey: [code],
Identification: [{Value:code}],
UI: {
SelectionFields: [ name, descr ],
LineItem:[
{Value: descr},
{Value: symbol},
{Value: code},
],
}
);
////////////////////////////////////////////////////////////////////////////
//
// Currency Details
//
annotate common.Currencies with @(
UI: {
HeaderInfo: {
TypeName: '{i18n>Currency}',
TypeNamePlural: '{i18n>Currencies}',
Title: {Value: descr},
Description: {Value: code}
},
Facets: [
{$Type: 'UI.ReferenceFacet', Label: '{i18n>Details}', Target: '@UI.FieldGroup#Details'},
{$Type: 'UI.ReferenceFacet', Label: '{i18n>Extended}', Target: '@UI.FieldGroup#Extended'},
],
FieldGroup#Details: {
Data: [
{Value: name},
{Value: symbol},
{Value: code},
{Value: descr}
]
},
FieldGroup#Extended: {
Data: [
{Value: numcode},
{Value: minor},
{Value: exponent}
]
},
}
);
////////////////////////////////////////////////////////////////////////////
//
// Currencies Elements
//
annotate common.Currencies with {
numcode @title:'{i18n>NumCode}';
minor @title:'{i18n>MinorUnit}';
exponent @title:'{i18n>Exponent}';
} }

View File

@@ -40,8 +40,7 @@
</script> </script>
<script src="https://sapui5.hana.ondemand.com/test-resources/sap/ushell/bootstrap/sandbox.js"></script> <script src="https://sapui5.hana.ondemand.com/test-resources/sap/ushell/bootstrap/sandbox.js"></script>
<!-- <script src="https://sapui5.hana.ondemand.com/resources/sap-ui-core.js" --> <script src="https://sapui5.hana.ondemand.com/resources/sap-ui-core.js"
<script src="https://sapui5.hana.ondemand.com/1.78.6/resources/sap-ui-core.js"
data-sap-ui-libs="sap.m, sap.ushell, sap.collaboration, sap.ui.layout" data-sap-ui-libs="sap.m, sap.ushell, sap.collaboration, sap.ui.layout"
data-sap-ui-compatVersion="edge" data-sap-ui-compatVersion="edge"
data-sap-ui-theme="sap_fiori_3" data-sap-ui-theme="sap_fiori_3"

View File

@@ -1,8 +1,5 @@
sap.ui.define(["sap/fe/core/AppComponent"], function(AppComponent) { sap.ui.define(["sap/fe/core/AppComponent"], ac => ac.extend("orders.Component", {
"use strict"; metadata:{ manifest:'json' }
return AppComponent.extend("orders.Component", { }))
metadata: { manifest: "json" }
});
});
/* eslint no-undef:0 */ /* eslint no-undef:0 */

View File

@@ -24,7 +24,7 @@
"sap.ui5": { "sap.ui5": {
"dependencies": { "dependencies": {
"libs": { "libs": {
"sap.fe.templates": {} "sap.fe": {}
} }
}, },
"models": { "models": {

View File

@@ -6,14 +6,14 @@ module.exports = cds.service.impl(function() {
// Reduce stock of ordered books if available stock suffices // Reduce stock of ordered books if available stock suffices
this.before ('CREATE', 'Orders', (req) => { this.before ('CREATE', 'Orders', (req) => {
const { Items: items } = req.data const { Items: OrderItems } = req.data
return cds.transaction(req) .run (items.map (item => return cds.transaction(req) .run (()=> OrderItems.map (order =>
UPDATE (Books) .where ('ID =', item.book_ID) UPDATE (Books) .where ('ID =', order.book_ID)
.and ('stock >=', item.amount) .and ('stock >=', order.amount)
.set ('stock -=', item.amount) .set ('stock -=', order.amount)
)) .then (all => all.forEach ((affectedRows,i) => { )) .then (all => all.forEach ((affectedRows,i) => {
if (affectedRows === 0) req.error (409, if (affectedRows === 0) req.error (409,
`${items[i].amount} exceeds stock for book #${items[i].book_ID}` `${OrderItems[i].amount} exceeds stock for book #${OrderItems[i].book_ID}`
) )
})) }))
}) })

View File

@@ -19,8 +19,6 @@
"sqlite3": "^4" "sqlite3": "^4"
}, },
"scripts": { "scripts": {
"bookshop": "cds watch bookshop",
"fiori": "cds watch fiori",
"mocha": "npx mocha || echo", "mocha": "npx mocha || echo",
"jest": "npx jest --verbose", "jest": "npx jest --verbose",
"test": "npm run jest -s" "test": "npm run jest -s"

View File

@@ -1 +0,0 @@
cds.requires.messaging.kind = file-based-messaging

View File

@@ -1,43 +0,0 @@
////////////////////////////////////////////////////////////////////////////
//
// This is an example of using a project-local server.js to intercept
// the default bootstrapping process.
//
const cds = require ('@sap/cds')
// Connect CatalogService and ReviewsService when all are served...
cds.once('served', async ({CatalogService}) => {
// reflect entity definitions used below...
const { Books } = cds.entities('sap.capire.bookshop')
const { Reviews } = cds.entities('ReviewsService')
// prepend the following handler so it overrides the default handler
CatalogService.prepend (srv => srv.on ('READ', 'Books/reviews', (req) => {
console.debug ('> delegating request to ReviewsService')
const [id] = req.params, { columns, limit } = req.query.SELECT
return SELECT(columns).from(Reviews).limit(limit).where({subject:String(id)})
}))
// subscribe to events emitted by ReviewsService
const ReviewsService = await cds.connect.to ('ReviewsService')
ReviewsService.on ('reviewed', (msg) => {
console.debug ('> received:', msg.event, msg.data)
const { subject, rating } = msg.data
return UPDATE(Books,subject).with({rating})
})
})
// Other bootstrapping events you could hook in to...
/* eslint-disable no-unused-vars */
cds.on('bootstrap',(app) => {/* ... */})
cds.on('loaded', (model) => {/* ... */})
cds.on('connect', (srv) => {/* ... */})
cds.on('serving', (srv) => {/* ... */})
cds.once('served', (all) => {/* ... */})
cds.once('listening', ({server,url}) => {/* ... */})
// Delegate bootstrapping to built-in server.js
module.exports = cds.server

View File

@@ -1,3 +1 @@
cds.requires.messaging.kind = file-based-messaging
cds.odata.skipValidation = true
PORT = 5005 PORT = 5005

View File

@@ -12,12 +12,15 @@
}, },
"scripts": { "scripts": {
"reviews-service": "cds watch", "reviews-service": "cds watch",
"books-reviewed": "cds watch ../reviewed" "bookshop": "cds watch test/bookshop"
}, },
"cds": { "cds": {
"requires": { "requires": {
"db": { "db": {
"kind": "sql" "kind": "sql"
},
"messaging": {
"kind": "file-based-messaging"
} }
} }
} }

View File

@@ -1,16 +1,13 @@
################################################# #################################################
# #
# To ReviewsService # To ReviewsService mocked in bookshop process
# #
# move the right down:
@reviews-service = http://localhost:4004/reviews
@reviews-service = http://localhost:5005/reviews
### Get all reviews GET http://localhost:4004/reviews/Reviews?
GET {{reviews-service}}/Reviews
### Add a new review (with random rating) ###
POST {{reviews-service}}/Reviews
POST http://localhost:4004/reviews/Reviews
Content-Type: application/json;IEEE754Compatible=true Content-Type: application/json;IEEE754Compatible=true
{"subject":"201", "title":"boo"} {"subject":"201", "title":"boo"}
@@ -23,19 +20,17 @@ Content-Type: application/json;IEEE754Compatible=true
# (both in-process as well as separate one) # (both in-process as well as separate one)
# #
@bookshop = http://localhost:4004
### Request to CatalogService > delegated to ReviewsService ### Request to CatalogService > delegated to ReviewsService
GET {{bookshop}}/browse/Books(201)/reviews? GET http://localhost:4004/browse/Books(201)/reviews?
&$select=rating,date,reviewer,title &$select=rating,date,reviewer,title
### Alternative OData URL ### Alternative OData URL
GET {{bookshop}}/browse/Books/201/reviews? GET http://localhost:4004/browse/Books/201/reviews?
&$select=rating,date,title &$select=rating,date,reviewer,title
&$top=3
### ###
GET {{bookshop}}/browse/Books(201)? GET http://localhost:4004/browse/Books(201)?
&$select=ID,title,rating &$select=ID,title,rating
&$expand=reviews &$expand=reviews
# Note: the $expand only works in case of ReviewsService in same process # Note: the $expand only works in case of ReviewsService in same process

13
reviews/test/5005.http Normal file
View File

@@ -0,0 +1,13 @@
#################################################
#
# To ReviewsService running as separate process
#
GET http://localhost:5005/reviews/Reviews?
###
POST http://localhost:5005/reviews/Reviews
Content-Type: application/json;IEEE754Compatible=true
{"subject":"201", "title":"boo"}

View File

@@ -2,8 +2,8 @@
"name": "@capire/bookshop-with-reviews", "name": "@capire/bookshop-with-reviews",
"version": "1.0.0", "version": "1.0.0",
"dependencies": { "dependencies": {
"@capire/bookshop": "../bookshop", "@capire/bookshop": "../../../bookshop",
"@capire/reviews": "../reviews", "@capire/reviews": "..",
"@sap/cds": "^3.33.1", "@sap/cds": "^3.33.1",
"express": "^4.17.1" "express": "^4.17.1"
}, },
@@ -14,6 +14,9 @@
}, },
"ReviewsService": { "ReviewsService": {
"kind": "odata", "model": "@capire/reviews" "kind": "odata", "model": "@capire/reviews"
},
"messaging": {
"kind": "file-based-messaging"
} }
} }
} }

View File

@@ -0,0 +1,51 @@
////////////////////////////////////////////////////////////////////////////
//
// This is an example of using a project-local server.js to intercept
// the default bootstrapping process.
//
const cds = require ('@sap/cds')
// Mashup services after all are served...
cds.once('served', async()=>{
// react on event messages from reviews service
const ReviewsService = await cds.connect.to ('ReviewsService')
const db = await cds.connect.to ('db')
ReviewsService.on ('reviewed', (msg) => {
console.debug ('> received:', msg.event, msg.data)
const { Books } = db.entities('sap.capire.bookshop')
const { subject, rating } = msg.data
const tx = db.tx (msg) // TODO: db.tx(msg) fully implemented?
return tx.update (Books,subject) .with ({rating})
})
// delegate requests to read reviews to ReviewsService
const CatalogService = await cds.connect.to ('CatalogService')
CatalogService.impl (srv => srv.on ('READ', 'Books/reviews', (req) => {
console.debug ('> delegating to ReviewsService')
const { Reviews } = ReviewsService.entities
const [ subject ] = req.params
const tx = ReviewsService.tx (req)
return tx.read (Reviews) .where({subject}) .columns (req.query.SELECT.columns)
}))
})
// Other bootstrapping events you could hook in to...
/* eslint-disable no-unused-vars */
cds.on('bootstrap',(app) => {/* ... */})
cds.on('loaded', (model) => {/* ... */})
cds.on('connect', (srv) => {/* ... */})
cds.on('serving', (srv) => {/* ... */})
cds.once('listening', ({server,url}) => {/* ... */})
// Delegate bootstrapping to built-in server.js
module.exports = cds.server
// Monkey-patching older releases
if (cds.version < '3.33.4') cds.once('listening', ()=> cds.emit('served'))
// Launch server if started directly from command-line
if (!module.parent) cds.server()

View File

@@ -261,7 +261,7 @@ describe('cds.ql → cqn', () => {
// same for works distinct // same for works distinct
}) })
test('where ( ... cql | {x:y} )', () => { xtest('where ( ... cql | {x:y} )', () => {
const args = [`foo`, "'bar'", 3] const args = [`foo`, "'bar'", 3]
const ID = 11 const ID = 11
@@ -278,6 +278,7 @@ describe('cds.ql → cqn', () => {
from: { ref: ['Foo'] }, from: { ref: ['Foo'] },
where: cdr where: cdr
? [ ? [
// '(', //> this one is not required
{ ref: ['ID'] }, { ref: ['ID'] },
'=', '=',
{ val: ID }, { val: ID },
@@ -286,7 +287,7 @@ describe('cds.ql → cqn', () => {
'in', 'in',
{ val: args }, { val: args },
'and', 'and',
'(', '(', //> this one is missing, and that's changing the logic -> that's a BUG
{ ref: ['x'] }, { ref: ['x'] },
'like', 'like',
{ val: '%x%' }, { val: '%x%' },
@@ -297,6 +298,7 @@ describe('cds.ql → cqn', () => {
')', ')',
] ]
: [ : [
'(', //> this one is not required
{ ref: ['ID'] }, { ref: ['ID'] },
'=', '=',
{ val: ID }, { val: ID },
@@ -305,7 +307,7 @@ describe('cds.ql → cqn', () => {
'in', 'in',
{ val: args }, { val: args },
'and', 'and',
'(', // '(', //> this one is missing, and that's changing the logic -> that's a BUG
{ ref: ['x'] }, { ref: ['x'] },
'like', 'like',
{ val: '%x%' }, { val: '%x%' },
@@ -335,31 +337,11 @@ describe('cds.ql → cqn', () => {
{ val: 'bar' }, { val: 'bar' },
',', ',',
{ val: 3 }, { val: 3 },
')' ')',
] ],
} },
}) })
expect(SELECT.from(Foo).where(`ID=`, ID, `and x in`, args)).to.eql(cqn)
const cqnFluent = {
SELECT: {
from: { ref: ['Foo'] },
where: [
{ ref: ['ID'] },
'=',
{ val: ID },
'and',
{ ref: ['x'] },
'in',
{ list: [
{ val: 'foo' },
{ val: 'bar' },
{ val: 3 }
] }
]
}
}
expect(SELECT.from(Foo).where(`ID=`, ID, `and x in`, args)).to.eql(cqnFluent)
expect(SELECT.from(Foo).where(`ID=${ID} and x in (${args})`)).to.eql(cqn) expect(SELECT.from(Foo).where(`ID=${ID} and x in (${args})`)).to.eql(cqn)
expect( expect(
@@ -484,38 +466,47 @@ describe('cds.ql → cqn', () => {
UPDATE.with allows to pass in plain data payloads, e.g. as obtained from REST clients. UPDATE.with allows to pass in plain data payloads, e.g. as obtained from REST clients.
In addition, UPDATE.with supports specifying expressions, either in CQL fragements In addition, UPDATE.with supports specifying expressions, either in CQL fragements
notation or as simple expression objects. notation or as simple expression objects.
UPDATE.data allows to pass in plain data payloads, e.g. as obtained from REST clients.
The passed in object can be modified subsequently, e.g. by adding or modifying values
before the query is finally executed.
*/ */
test('with + data', () => { test('with', () => {
if (cds.version < '4.1.0') return expect(UPDATE(Foo).with(`foo=`, 11, `bar-=`, 22))
const o = {}
const q = UPDATE(Foo).data(o).with(`bar-=`, 22)
o.foo = 11
expect(q)
.to.eql(UPDATE(Foo).with(`foo=`, 11, `bar-=`, 22))
.to.eql(UPDATE(Foo).with({ foo: 11, bar: { '-=': 22 } })) .to.eql(UPDATE(Foo).with({ foo: 11, bar: { '-=': 22 } }))
.to.eql({ .to.eql({
UPDATE: { UPDATE: {
entity: 'Foo', entity: 'Foo',
data: { foo: 11 },
with: { with: {
foo: { val: 11 },
bar: { xpr: [{ ref: ['bar'] }, '-', { val: 22 }] }, bar: { xpr: [{ ref: ['bar'] }, '-', { val: 22 }] },
}, },
}, },
}) })
// some more // some more
expect(UPDATE(Foo).with(`bar = coalesce(x,y), car = 'foo''s bar, car'`)).to.eql({ // expect(UPDATE(Foo).with(`bar = coalesce(x,y), car = 'foo''s bar, car'`)).to.eql({
// UPDATE: {
// entity: 'Foo',
// with: {
// bar: { func: 'coalesce', args: [{ ref: ['x'] }, { ref: ['y'] }] },
// car: { val: "foo's bar, car" },
// },
// },
// })
})
/*
UPDATE.data allows to pass in plain data payloads, e.g. as obtained from REST clients.
The passed in object can be modified subsequently, e.g. by adding or modifying values
before the query is finally executed.
*/
test('data', () => {
const o = {}
const q = UPDATE(Foo).data(o).with(`bar-=`, 22)
o.foo = 11
expect(q).to.eql({
UPDATE: { UPDATE: {
entity: 'Foo', entity: 'Foo',
data: { data: { foo: 11 },
car: "foo's bar, car",
},
with: { with: {
bar: { func: 'coalesce', args: [{ ref: ['x'] }, { ref: ['y'] }] }, bar: { xpr: [{ ref: ['bar'] }, '-', { val: 22 }] },
}, },
}, },
}) })

View File

@@ -1,10 +1,6 @@
const { expect } = require('./capire') const { expect } = require('./capire')
const cds = require('@sap/cds') const cds = require('@sap/cds')
const cwd = process.cwd()
before (()=> process.chdir(__dirname))
after(()=> process.chdir(cwd))
describe('Consuming Services locally', () => { describe('Consuming Services locally', () => {
// //
before('bootstrap db and services', async () => { before('bootstrap db and services', async () => {

View File

@@ -1,12 +1,3 @@
const cwd = process.cwd()
const is_jest = !!global.test
if (is_jest) { // it's jest
global.before = (msg,fn) => global.beforeAll(fn||msg)
global.after = (msg,fn) => global.afterAll(fn||msg)
}
before (()=> process.chdir(__dirname))
after (()=> process.chdir(cwd))
describe('Custom Handlers', () => { describe('Custom Handlers', () => {
const { GET, POST, expect } = require('./capire').launch('bookshop') const { GET, POST, expect } = require('./capire').launch('bookshop')

View File

@@ -72,7 +72,7 @@ describe('Localized Data', () => {
]) ])
}) })
it('supports @cds.localized:false', async ()=>{ xit('supports @cds.localized:false', async ()=>{
const { data } = await GET(`/browse/BooksSans?&$select=title,localized_title&$expand=currency&$filter=locale eq 'de' or locale eq null`, { const { data } = await GET(`/browse/BooksSans?&$select=title,localized_title&$expand=currency&$filter=locale eq 'de' or locale eq null`, {
headers: { 'Accept-Language': 'de' }, headers: { 'Accept-Language': 'de' },
}) })

View File

@@ -21,7 +21,7 @@ describe('Messaging', ()=>{
let N=0, received=[], M=0 let N=0, received=[], M=0
it ('should add messaging event handlers', ()=>{ it ('should add messaging event handlers', ()=>{
srv.on('reviewed', (msg)=> {received.push(msg)}) srv.on('reviewed', (msg)=> received.push(msg))
}) })
it ('should add more messaging event handlers', ()=>{ it ('should add more messaging event handlers', ()=>{