Compare commits
49 Commits
preview
...
enable-fix
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ae20e682be | ||
|
|
53527aac50 | ||
|
|
04f9f2a06f | ||
|
|
fff62e68f1 | ||
|
|
bd65af43eb | ||
|
|
6f9133cd4f | ||
|
|
441c82b4c9 | ||
|
|
fa7cff4123 | ||
|
|
1b69064752 | ||
|
|
ada05cf279 | ||
|
|
4b78a8b637 | ||
|
|
ade170367b | ||
|
|
5c5afd2790 | ||
|
|
74ee6f34e4 | ||
|
|
9a9b7aeb86 | ||
|
|
cfc01bbc4f | ||
|
|
aaac6cc678 | ||
|
|
99fdf0c038 | ||
|
|
6ccecfecae | ||
|
|
e5f0a7ef73 | ||
|
|
de54f70570 | ||
|
|
e1c6118cb4 | ||
|
|
a3e4865d97 | ||
|
|
641df50422 | ||
|
|
0f026ed56c | ||
|
|
57a3c5f178 | ||
|
|
522ec8e071 | ||
|
|
7b1c3d8b3a | ||
|
|
5ba69b5021 | ||
|
|
fd796b54ef | ||
|
|
660344b623 | ||
|
|
d20c29a758 | ||
|
|
604cc0514c | ||
|
|
ff351455dd | ||
|
|
8f74bd32a9 | ||
|
|
c181afe8c6 | ||
|
|
9ade3e6b6a | ||
|
|
6f9737ae38 | ||
|
|
0a552b4346 | ||
|
|
6367081e9d | ||
|
|
3e73683d99 | ||
|
|
2b345ca447 | ||
|
|
20593f2fa2 | ||
|
|
ca45aa1cf7 | ||
|
|
e408836c2a | ||
|
|
3000a9e2df | ||
|
|
b83236de2a | ||
|
|
46b3b8aaec | ||
|
|
59f5c82684 |
3
.env
3
.env
@@ -1,2 +1 @@
|
||||
cds.features.snapi = y
|
||||
cds.cdsc.beta.aspectCompositions = true
|
||||
cds.features.snapi = y
|
||||
10
.github/ISSUE_TEMPLATE/question--feedback-or-bug-.md
vendored
Normal file
10
.github/ISSUE_TEMPLATE/question--feedback-or-bug-.md
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
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
|
||||
28
.github/workflows/node.js.yml
vendored
Normal file
28
.github/workflows/node.js.yml
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
|
||||
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
|
||||
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [10.x, 12.16.2]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- run: npm install
|
||||
- run: npm test
|
||||
1
.mocharc.yml
Normal file
1
.mocharc.yml
Normal file
@@ -0,0 +1 @@
|
||||
parallel: true
|
||||
2
.vscode/extensions.json
vendored
2
.vscode/extensions.json
vendored
@@ -4,7 +4,7 @@
|
||||
|
||||
// List of extensions which should be recommended for users of this workspace.
|
||||
"recommendations": [
|
||||
// "CDS Editor !",
|
||||
"SAPSE.vscode-cds",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"esbenp.prettier-vscode",
|
||||
"mechatroner.rainbow-csv",
|
||||
|
||||
18
.vscode/launch.json
vendored
18
.vscode/launch.json
vendored
@@ -6,24 +6,16 @@
|
||||
"configurations": [
|
||||
{
|
||||
"name": "bookshop",
|
||||
"cwd": "${workspaceFolder}/bookshop",
|
||||
"command": "cds watch bookshop",
|
||||
"request": "launch",
|
||||
"type": "node",
|
||||
"runtimeExecutable": "npx",
|
||||
"runtimeArgs": ["-n"],
|
||||
"args": ["--", "cds", "run", "--with-mocks", "--in-memory?"],
|
||||
"console": "integratedTerminal",
|
||||
"type": "node-terminal",
|
||||
"skipFiles": ["<node_internals>/**"]
|
||||
},
|
||||
{
|
||||
"name": "Fiori App",
|
||||
"cwd": "${workspaceFolder}/fiori",
|
||||
"name": "Fiori app",
|
||||
"command": "cds watch fiori",
|
||||
"request": "launch",
|
||||
"type": "node",
|
||||
"runtimeExecutable": "npx",
|
||||
"runtimeArgs": ["-n"],
|
||||
"args": ["--", "cds", "run", "--with-mocks", "--in-memory?"],
|
||||
"console": "integratedTerminal",
|
||||
"type": "node-terminal",
|
||||
"skipFiles": ["<node_internals>/**"]
|
||||
}
|
||||
],
|
||||
|
||||
11
README.md
11
README.md
@@ -2,6 +2,7 @@
|
||||
|
||||
Find here a collection of samples for the [SAP Cloud Application Programming Model](https://cap.cloud.sap) organized in a simplistic [monorepo setup](samples.md#all-in-one-monorepo). → See [**Overview** of contained samples](samples.md)
|
||||
|
||||

|
||||
|
||||
### Preliminaries
|
||||
|
||||
@@ -39,19 +40,19 @@ After that open this link in your browser: [http://localhost:4004](http://localh
|
||||
|
||||
### Testing
|
||||
|
||||
Run the provided tests with [_jest_](http://jestjs.io) or [_mocha_](http://mochajs.org), e.g.:
|
||||
Run the provided tests with [_jest_](http://jestjs.io) or [_mocha_](http://mochajs.org), for example:
|
||||
```sh
|
||||
npx jest
|
||||
```
|
||||
> While mocha is a bit smaller and faster, jest runs tests in parallel and isolation which allows to run all tests.
|
||||
> While mocha is a bit smaller and faster, jest runs tests in parallel and isolation, which allows to run all tests.
|
||||
|
||||
|
||||
## Get Support
|
||||
|
||||
Check out the cap docs at [https://cap.cloud.sap](https://cap.cloud.sap). <br>
|
||||
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).
|
||||
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).
|
||||
|
||||
|
||||
## License
|
||||
|
||||
Copyright (c) 2019 SAP SE or an SAP affiliate company. All rights reserved. This file is licensed under the Apache Software License, version 2.0 except as noted otherwise in the [LICENSE](/LICENSE) file.
|
||||
Copyright (c) 2020 SAP SE or an SAP affiliate company. All rights reserved. This file is licensed under the Apache Software License, version 2.0 except as noted otherwise in the [LICENSE](/LICENSE) file.
|
||||
|
||||
@@ -11,6 +11,6 @@ ID;parent_ID;name
|
||||
19;10;Fairy Tale
|
||||
20;;Non-Fiction
|
||||
21;20;Biography
|
||||
22;20;Autobiography
|
||||
22;21;Autobiography
|
||||
23;20;Essay
|
||||
24;20;Speech
|
||||
|
||||
|
@@ -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;Canadian Dollar;036;Cent;2
|
||||
AUD;$;Australian Dollar;Australian 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
|
||||
|
||||
|
@@ -7,7 +7,19 @@ 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
|
||||
@@ -1,5 +1,8 @@
|
||||
sap.ui.define(["sap/fe/core/AppComponent"], ac => ac.extend("admin.Component", {
|
||||
metadata:{ manifest:'json' }
|
||||
}))
|
||||
sap.ui.define(["sap/fe/core/AppComponent"], function(AppComponent) {
|
||||
"use strict";
|
||||
return AppComponent.extend("admin.Component", {
|
||||
metadata: { manifest: "json" }
|
||||
});
|
||||
});
|
||||
|
||||
/* eslint no-undef:0 */
|
||||
@@ -24,7 +24,7 @@
|
||||
"sap.ui5": {
|
||||
"dependencies": {
|
||||
"libs": {
|
||||
"sap.fe": {}
|
||||
"sap.fe.templates": {}
|
||||
}
|
||||
},
|
||||
"models": {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
sap.ui.define(["sap/fe/core/AppComponent"], ac => ac.extend("bookshop.Component", {
|
||||
metadata:{ manifest:'json' }
|
||||
}))
|
||||
|
||||
sap.ui.define(["sap/fe/core/AppComponent"], function(AppComponent) {
|
||||
"use strict";
|
||||
return AppComponent.extend("bookshop.Component", {
|
||||
metadata: { manifest: "json" }
|
||||
});
|
||||
});
|
||||
/* eslint no-undef:0 */
|
||||
@@ -24,7 +24,7 @@
|
||||
"sap.ui5": {
|
||||
"dependencies": {
|
||||
"libs": {
|
||||
"sap.fe": {}
|
||||
"sap.fe.templates": {}
|
||||
}
|
||||
},
|
||||
"models": {
|
||||
|
||||
@@ -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,25 +28,18 @@ 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}
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@@ -66,15 +59,199 @@ 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 {
|
||||
name @title: '{i18n>Genre}';
|
||||
ID @title: '{i18n>ID}';
|
||||
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>AuthorName}';
|
||||
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}';
|
||||
}
|
||||
|
||||
@@ -40,7 +40,8 @@
|
||||
</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-compatVersion="edge"
|
||||
data-sap-ui-theme="sap_fiori_3"
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
sap.ui.define(["sap/fe/core/AppComponent"], ac => ac.extend("orders.Component", {
|
||||
metadata:{ manifest:'json' }
|
||||
}))
|
||||
sap.ui.define(["sap/fe/core/AppComponent"], function(AppComponent) {
|
||||
"use strict";
|
||||
return AppComponent.extend("orders.Component", {
|
||||
metadata: { manifest: "json" }
|
||||
});
|
||||
});
|
||||
|
||||
/* eslint no-undef:0 */
|
||||
@@ -24,7 +24,7 @@
|
||||
"sap.ui5": {
|
||||
"dependencies": {
|
||||
"libs": {
|
||||
"sap.fe": {}
|
||||
"sap.fe.templates": {}
|
||||
}
|
||||
},
|
||||
"models": {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
ID;amount;parent_ID;book_ID
|
||||
58040e66-1dcd-4ffb-ab10-fdce32028b79;1;7e2f2640-6866-4dcf-8f4d-3027aa831cad;201
|
||||
64e718c9-ff99-47f1-8ca3-950c850777d4;1;7e2f2640-6866-4dcf-8f4d-3027aa831cad;271
|
||||
e9641166-e050-4261-bfee-d1e797e6cb7f;2;64e718c9-ff99-47f1-8ca3-950c850777d4;252
|
||||
ID;amount;parent_ID;book_ID;netAmount
|
||||
58040e66-1dcd-4ffb-ab10-fdce32028b79;1;7e2f2640-6866-4dcf-8f4d-3027aa831cad;201;11.11
|
||||
64e718c9-ff99-47f1-8ca3-950c850777d4;1;7e2f2640-6866-4dcf-8f4d-3027aa831cad;271;15
|
||||
e9641166-e050-4261-bfee-d1e797e6cb7f;2;64e718c9-ff99-47f1-8ca3-950c850777d4;252;28
|
||||
|
@@ -4,10 +4,13 @@ namespace sap.capire.bookshop;
|
||||
|
||||
entity Orders : cuid, managed {
|
||||
OrderNo : String @title:'Order Number'; //> readable key
|
||||
Items : Composition of many {
|
||||
key pos : Integer;
|
||||
book : Association to Books;
|
||||
amount : Integer;
|
||||
};
|
||||
Items : Composition of many OrderItems on Items.parent = $self;
|
||||
currency : Currency;
|
||||
}
|
||||
|
||||
entity OrderItems : cuid {
|
||||
parent : Association to Orders;
|
||||
book : Association to Books;
|
||||
amount : Integer;
|
||||
netAmount : Decimal(9,2);
|
||||
}
|
||||
|
||||
@@ -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: 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)
|
||||
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)
|
||||
)) .then (all => all.forEach ((affectedRows,i) => {
|
||||
if (affectedRows === 0) req.error (409,
|
||||
`${OrderItems[i].amount} exceeds stock for book #${OrderItems[i].book_ID}`
|
||||
`${items[i].amount} exceeds stock for book #${items[i].book_ID}`
|
||||
)
|
||||
}))
|
||||
})
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
"sqlite3": "^4"
|
||||
},
|
||||
"scripts": {
|
||||
"bookshop": "cds watch bookshop",
|
||||
"fiori": "cds watch fiori",
|
||||
"mocha": "npx mocha || echo",
|
||||
"jest": "npx jest --verbose",
|
||||
"test": "npm run jest -s"
|
||||
|
||||
1
reviewed/.env
Normal file
1
reviewed/.env
Normal file
@@ -0,0 +1 @@
|
||||
cds.requires.messaging.kind = file-based-messaging
|
||||
@@ -2,8 +2,8 @@
|
||||
"name": "@capire/bookshop-with-reviews",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@capire/bookshop": "../../../bookshop",
|
||||
"@capire/reviews": "..",
|
||||
"@capire/bookshop": "../bookshop",
|
||||
"@capire/reviews": "../reviews",
|
||||
"@sap/cds": "^3.33.1",
|
||||
"express": "^4.17.1"
|
||||
},
|
||||
@@ -14,10 +14,7 @@
|
||||
},
|
||||
"ReviewsService": {
|
||||
"kind": "odata", "model": "@capire/reviews"
|
||||
},
|
||||
"messaging": {
|
||||
"kind": "file-based-messaging"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
43
reviewed/server.js
Normal file
43
reviewed/server.js
Normal file
@@ -0,0 +1,43 @@
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// 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
|
||||
@@ -1,13 +1,16 @@
|
||||
#################################################
|
||||
#
|
||||
# To ReviewsService mocked in bookshop process
|
||||
# To ReviewsService
|
||||
#
|
||||
# move the right down:
|
||||
@reviews-service = http://localhost:4004/reviews
|
||||
@reviews-service = http://localhost:5005/reviews
|
||||
|
||||
GET http://localhost:4004/reviews/Reviews?
|
||||
### Get all reviews
|
||||
GET {{reviews-service}}/Reviews
|
||||
|
||||
###
|
||||
|
||||
POST http://localhost:4004/reviews/Reviews
|
||||
### Add a new review (with random rating)
|
||||
POST {{reviews-service}}/Reviews
|
||||
Content-Type: application/json;IEEE754Compatible=true
|
||||
|
||||
{"subject":"201", "title":"boo"}
|
||||
@@ -20,17 +23,19 @@ 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 http://localhost:4004/browse/Books(201)/reviews?
|
||||
GET {{bookshop}}/browse/Books(201)/reviews?
|
||||
&$select=rating,date,reviewer,title
|
||||
|
||||
### Alternative OData URL
|
||||
GET http://localhost:4004/browse/Books/201/reviews?
|
||||
&$select=rating,date,reviewer,title
|
||||
GET {{bookshop}}/browse/Books/201/reviews?
|
||||
&$select=rating,date,title
|
||||
&$top=3
|
||||
|
||||
###
|
||||
GET http://localhost:4004/browse/Books(201)?
|
||||
GET {{bookshop}}/browse/Books(201)?
|
||||
&$select=ID,title,rating
|
||||
&$expand=reviews
|
||||
# Note: the $expand only works in case of ReviewsService in same process
|
||||
@@ -1 +1,3 @@
|
||||
cds.requires.messaging.kind = file-based-messaging
|
||||
cds.odata.skipValidation = true
|
||||
PORT = 5005
|
||||
@@ -12,15 +12,12 @@
|
||||
},
|
||||
"scripts": {
|
||||
"reviews-service": "cds watch",
|
||||
"bookshop": "cds watch test/bookshop"
|
||||
"books-reviewed": "cds watch ../reviewed"
|
||||
},
|
||||
"cds": {
|
||||
"requires": {
|
||||
"db": {
|
||||
"kind": "sql"
|
||||
},
|
||||
"messaging": {
|
||||
"kind": "file-based-messaging"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
#################################################
|
||||
#
|
||||
# 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"}
|
||||
@@ -1,51 +0,0 @@
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// 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('boostrap',(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()
|
||||
@@ -261,7 +261,7 @@ describe('cds.ql → cqn', () => {
|
||||
// same for works distinct
|
||||
})
|
||||
|
||||
xtest('where ( ... cql | {x:y} )', () => {
|
||||
test('where ( ... cql | {x:y} )', () => {
|
||||
const args = [`foo`, "'bar'", 3]
|
||||
const ID = 11
|
||||
|
||||
@@ -278,7 +278,6 @@ describe('cds.ql → cqn', () => {
|
||||
from: { ref: ['Foo'] },
|
||||
where: cdr
|
||||
? [
|
||||
// '(', //> this one is not required
|
||||
{ ref: ['ID'] },
|
||||
'=',
|
||||
{ val: ID },
|
||||
@@ -287,7 +286,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%' },
|
||||
@@ -298,7 +297,6 @@ describe('cds.ql → cqn', () => {
|
||||
')',
|
||||
]
|
||||
: [
|
||||
'(', //> this one is not required
|
||||
{ ref: ['ID'] },
|
||||
'=',
|
||||
{ val: ID },
|
||||
@@ -307,7 +305,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%' },
|
||||
@@ -337,11 +335,31 @@ describe('cds.ql → cqn', () => {
|
||||
{ val: 'bar' },
|
||||
',',
|
||||
{ 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(
|
||||
@@ -359,6 +377,11 @@ describe('cds.ql → cqn', () => {
|
||||
CQL`SELECT from Foo where x in (SELECT y from Bar)`
|
||||
)
|
||||
|
||||
// using query api
|
||||
expect(SELECT.from('Books').where(
|
||||
`author.name in`, SELECT('name').from('Authors'))).to.eql(CQL`SELECT from Books where author.name in (SELECT name from Authors)`
|
||||
)
|
||||
|
||||
// in classical semi joins
|
||||
expect(
|
||||
SELECT('x').from(Foo) .where ( `exists`,
|
||||
@@ -461,47 +484,38 @@ 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.
|
||||
*/
|
||||
test('with', () => {
|
||||
expect(UPDATE(Foo).with(`foo=`, 11, `bar-=`, 22))
|
||||
|
||||
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))
|
||||
.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({
|
||||
// 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({
|
||||
expect(UPDATE(Foo).with(`bar = coalesce(x,y), car = 'foo''s bar, car'`)).to.eql({
|
||||
UPDATE: {
|
||||
entity: 'Foo',
|
||||
data: { foo: 11 },
|
||||
data: {
|
||||
car: "foo's bar, car",
|
||||
},
|
||||
with: {
|
||||
bar: { xpr: [{ ref: ['bar'] }, '-', { val: 22 }] },
|
||||
bar: { func: 'coalesce', args: [{ ref: ['x'] }, { ref: ['y'] }] },
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
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 () => {
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
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')
|
||||
|
||||
|
||||
@@ -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', ()=>{
|
||||
|
||||
Reference in New Issue
Block a user