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.
"recommendations": [
"SAPSE.vscode-cds",
// "CDS Editor !",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"mechatroner.rainbow-csv",

18
.vscode/launch.json vendored
View File

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

View File

@@ -50,7 +50,7 @@ npx jest
## Get Support
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

View File

@@ -11,6 +11,6 @@ ID;parent_ID;name
19;10;Fairy Tale
20;;Non-Fiction
21;20;Biography
22;21;Autobiography
22;20;Autobiography
23;20;Essay
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
USD;$;US Dollar;United States Dollar;840;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
ILS;₪;Shekel;Israeli New Shekel;376;Agorat;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
Name = Name
AuthorName = Author's Name
DateOfBirth = Date of Birth
DateOfDeath = Date of Death
PlaceOfBirth = Place of Birth
PlaceOfDeath = Place of Death
Authors = Authors
Order = Order
Orders = Orders
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) {
"use strict";
return AppComponent.extend("admin.Component", {
metadata: { manifest: "json" }
});
});
sap.ui.define(["sap/fe/core/AppComponent"], ac => ac.extend("admin.Component", {
metadata:{ manifest:'json' }
}))
/* eslint no-undef:0 */

View File

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

View File

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

View File

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

View File

@@ -1,9 +1,9 @@
/*
Common Annotations shared by all apps
Common Annotations shared by all apps
*/
using { sap.capire.bookshop as my } from '@capire/bookshop';
using { sap.common } from '@capire/common';
////////////////////////////////////////////////////////////////////////////
//
@@ -13,7 +13,7 @@ annotate my.Books with @(
Common.SemanticKey: [title],
UI: {
Identification: [{Value:title}],
SelectionFields: [ ID, author_ID, price, currency_code ],
SelectionFields: [ ID, author_ID, price, currency_code ],
LineItem: [
{Value: ID},
{Value: title},
@@ -28,18 +28,25 @@ annotate my.Books with @(
author @ValueList.entity:'Authors';
};
annotate my.Authors with @(
UI: {
Identification: [{Value:name}],
}
);
////////////////////////////////////////////////////////////////////////////
//
// Books Details
//
annotate my.Books with @(
UI: {
HeaderInfo: {
TypeName: '{i18n>Book}',
TypeNamePlural: '{i18n>Books}',
Title: {Value: title},
Description: {Value: author.name}
},
HeaderInfo: {
TypeName: '{i18n>Book}',
TypeNamePlural: '{i18n>Books}',
Title: {Value: title},
Description: {Value: author.name}
},
}
);
@@ -59,199 +66,15 @@ annotate my.Books with {
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 {
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
//
annotate my.Authors with {
ID @title:'{i18n>ID}' @UI.HiddenFilter;
name @title:'{i18n>Name}';
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}';
name @title:'{i18n>AuthorName}';
}

View File

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

View File

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

View File

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

View File

@@ -6,14 +6,14 @@ module.exports = cds.service.impl(function() {
// Reduce stock of ordered books if available stock suffices
this.before ('CREATE', 'Orders', (req) => {
const { Items: items } = req.data
return cds.transaction(req) .run (items.map (item =>
UPDATE (Books) .where ('ID =', item.book_ID)
.and ('stock >=', item.amount)
.set ('stock -=', item.amount)
const { Items: OrderItems } = req.data
return cds.transaction(req) .run (()=> OrderItems.map (order =>
UPDATE (Books) .where ('ID =', order.book_ID)
.and ('stock >=', order.amount)
.set ('stock -=', order.amount)
)) .then (all => all.forEach ((affectedRows,i) => {
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"
},
"scripts": {
"bookshop": "cds watch bookshop",
"fiori": "cds watch fiori",
"mocha": "npx mocha || echo",
"jest": "npx jest --verbose",
"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

View File

@@ -12,12 +12,15 @@
},
"scripts": {
"reviews-service": "cds watch",
"books-reviewed": "cds watch ../reviewed"
"bookshop": "cds watch test/bookshop"
},
"cds": {
"requires": {
"db": {
"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 {{reviews-service}}/Reviews
GET http://localhost:4004/reviews/Reviews?
### Add a new review (with random rating)
POST {{reviews-service}}/Reviews
###
POST http://localhost:4004/reviews/Reviews
Content-Type: application/json;IEEE754Compatible=true
{"subject":"201", "title":"boo"}
@@ -23,19 +20,17 @@ Content-Type: application/json;IEEE754Compatible=true
# (both in-process as well as separate one)
#
@bookshop = http://localhost:4004
### 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
### Alternative OData URL
GET {{bookshop}}/browse/Books/201/reviews?
&$select=rating,date,title
&$top=3
GET http://localhost:4004/browse/Books/201/reviews?
&$select=rating,date,reviewer,title
###
GET {{bookshop}}/browse/Books(201)?
GET http://localhost:4004/browse/Books(201)?
&$select=ID,title,rating
&$expand=reviews
# 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",
"version": "1.0.0",
"dependencies": {
"@capire/bookshop": "../bookshop",
"@capire/reviews": "../reviews",
"@capire/bookshop": "../../../bookshop",
"@capire/reviews": "..",
"@sap/cds": "^3.33.1",
"express": "^4.17.1"
},
@@ -14,6 +14,9 @@
},
"ReviewsService": {
"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
})
test('where ( ... cql | {x:y} )', () => {
xtest('where ( ... cql | {x:y} )', () => {
const args = [`foo`, "'bar'", 3]
const ID = 11
@@ -278,6 +278,7 @@ describe('cds.ql → cqn', () => {
from: { ref: ['Foo'] },
where: cdr
? [
// '(', //> this one is not required
{ ref: ['ID'] },
'=',
{ val: ID },
@@ -286,7 +287,7 @@ describe('cds.ql → cqn', () => {
'in',
{ val: args },
'and',
'(',
'(', //> this one is missing, and that's changing the logic -> that's a BUG
{ ref: ['x'] },
'like',
{ val: '%x%' },
@@ -297,6 +298,7 @@ describe('cds.ql → cqn', () => {
')',
]
: [
'(', //> this one is not required
{ ref: ['ID'] },
'=',
{ val: ID },
@@ -305,7 +307,7 @@ describe('cds.ql → cqn', () => {
'in',
{ val: args },
'and',
'(',
// '(', //> this one is missing, and that's changing the logic -> that's a BUG
{ ref: ['x'] },
'like',
{ val: '%x%' },
@@ -335,31 +337,11 @@ describe('cds.ql → cqn', () => {
{ val: 'bar' },
',',
{ val: 3 },
')'
]
}
')',
],
},
})
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(
@@ -484,38 +466,47 @@ describe('cds.ql → cqn', () => {
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
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', () => {
if (cds.version < '4.1.0') return
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))
*/
test('with', () => {
expect(UPDATE(Foo).with(`foo=`, 11, `bar-=`, 22))
.to.eql(UPDATE(Foo).with({ foo: 11, bar: { '-=': 22 } }))
.to.eql({
UPDATE: {
entity: 'Foo',
data: { foo: 11 },
with: {
foo: { val: 11 },
bar: { xpr: [{ ref: ['bar'] }, '-', { val: 22 }] },
},
},
})
// 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: {
entity: 'Foo',
data: {
car: "foo's bar, car",
},
data: { foo: 11 },
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 cds = require('@sap/cds')
const cwd = process.cwd()
before (()=> process.chdir(__dirname))
after(()=> process.chdir(cwd))
describe('Consuming Services locally', () => {
//
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', () => {
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`, {
headers: { 'Accept-Language': 'de' },
})

View File

@@ -21,7 +21,7 @@ describe('Messaging', ()=>{
let N=0, received=[], M=0
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', ()=>{