Compare commits
38 Commits
recap
...
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 |
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
|
||||||
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.
|
// List of extensions which should be recommended for users of this workspace.
|
||||||
"recommendations": [
|
"recommendations": [
|
||||||
// "CDS Editor !",
|
"SAPSE.vscode-cds",
|
||||||
"dbaeumer.vscode-eslint",
|
"dbaeumer.vscode-eslint",
|
||||||
"esbenp.prettier-vscode",
|
"esbenp.prettier-vscode",
|
||||||
"mechatroner.rainbow-csv",
|
"mechatroner.rainbow-csv",
|
||||||
|
|||||||
18
.vscode/launch.json
vendored
18
.vscode/launch.json
vendored
@@ -6,24 +6,16 @@
|
|||||||
"configurations": [
|
"configurations": [
|
||||||
{
|
{
|
||||||
"name": "bookshop",
|
"name": "bookshop",
|
||||||
"cwd": "${workspaceFolder}/bookshop",
|
"command": "cds watch bookshop",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"type": "node",
|
"type": "node-terminal",
|
||||||
"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",
|
||||||
"cwd": "${workspaceFolder}/fiori",
|
"command": "cds watch fiori",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"type": "node",
|
"type": "node-terminal",
|
||||||
"runtimeExecutable": "npx",
|
|
||||||
"runtimeArgs": ["-n"],
|
|
||||||
"args": ["--", "cds", "run", "--with-mocks", "--in-memory?"],
|
|
||||||
"console": "integratedTerminal",
|
|
||||||
"skipFiles": ["<node_internals>/**"]
|
"skipFiles": ["<node_internals>/**"]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -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 find a bug or need support, please [open an issue in here](https://github.com/SAP-samples/cloud-cap-samples/issues/new).
|
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
|
## License
|
||||||
|
|||||||
@@ -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;20;Autobiography
|
22;21;Autobiography
|
||||||
23;20;Essay
|
23;20;Essay
|
||||||
24;20;Speech
|
24;20;Speech
|
||||||
|
|||||||
|
@@ -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;Canadian Dollar;036;Cent;2
|
AUD;$;Australian Dollar;Australian 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
|
||||||
|
|||||||
|
@@ -7,7 +7,19 @@ 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
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
sap.ui.define(["sap/fe/core/AppComponent"], ac => ac.extend("admin.Component", {
|
sap.ui.define(["sap/fe/core/AppComponent"], function(AppComponent) {
|
||||||
metadata:{ manifest:'json' }
|
"use strict";
|
||||||
}))
|
return AppComponent.extend("admin.Component", {
|
||||||
|
metadata: { manifest: "json" }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
/* eslint no-undef:0 */
|
/* eslint no-undef:0 */
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
"sap.ui5": {
|
"sap.ui5": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"libs": {
|
"libs": {
|
||||||
"sap.fe": {}
|
"sap.fe.templates": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"models": {
|
"models": {
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
sap.ui.define(["sap/fe/core/AppComponent"], ac => ac.extend("bookshop.Component", {
|
sap.ui.define(["sap/fe/core/AppComponent"], function(AppComponent) {
|
||||||
metadata:{ manifest:'json' }
|
"use strict";
|
||||||
}))
|
return AppComponent.extend("bookshop.Component", {
|
||||||
|
metadata: { manifest: "json" }
|
||||||
|
});
|
||||||
|
});
|
||||||
/* eslint no-undef:0 */
|
/* eslint no-undef:0 */
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
"sap.ui5": {
|
"sap.ui5": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"libs": {
|
"libs": {
|
||||||
"sap.fe": {}
|
"sap.fe.templates": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"models": {
|
"models": {
|
||||||
|
|||||||
@@ -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,13 +28,6 @@ annotate my.Books with @(
|
|||||||
author @ValueList.entity:'Authors';
|
author @ValueList.entity:'Authors';
|
||||||
};
|
};
|
||||||
|
|
||||||
annotate my.Authors with @(
|
|
||||||
UI: {
|
|
||||||
Identification: [{Value:name}],
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
//
|
//
|
||||||
// Books Details
|
// Books Details
|
||||||
@@ -66,15 +59,199 @@ 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>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>
|
||||||
|
|
||||||
<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"
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
sap.ui.define(["sap/fe/core/AppComponent"], ac => ac.extend("orders.Component", {
|
sap.ui.define(["sap/fe/core/AppComponent"], function(AppComponent) {
|
||||||
metadata:{ manifest:'json' }
|
"use strict";
|
||||||
}))
|
return AppComponent.extend("orders.Component", {
|
||||||
|
metadata: { manifest: "json" }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
/* eslint no-undef:0 */
|
/* eslint no-undef:0 */
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
"sap.ui5": {
|
"sap.ui5": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"libs": {
|
"libs": {
|
||||||
"sap.fe": {}
|
"sap.fe.templates": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"models": {
|
"models": {
|
||||||
|
|||||||
@@ -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: OrderItems } = req.data
|
const { Items: items } = req.data
|
||||||
return cds.transaction(req) .run (()=> OrderItems.map (order =>
|
return cds.transaction(req) .run (items.map (item =>
|
||||||
UPDATE (Books) .where ('ID =', order.book_ID)
|
UPDATE (Books) .where ('ID =', item.book_ID)
|
||||||
.and ('stock >=', order.amount)
|
.and ('stock >=', item.amount)
|
||||||
.set ('stock -=', order.amount)
|
.set ('stock -=', item.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,
|
||||||
`${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"
|
"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"
|
||||||
|
|||||||
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",
|
"name": "@capire/bookshop-with-reviews",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@capire/bookshop": "../../../bookshop",
|
"@capire/bookshop": "../bookshop",
|
||||||
"@capire/reviews": "..",
|
"@capire/reviews": "../reviews",
|
||||||
"@sap/cds": "^3.33.1",
|
"@sap/cds": "^3.33.1",
|
||||||
"express": "^4.17.1"
|
"express": "^4.17.1"
|
||||||
},
|
},
|
||||||
@@ -14,9 +14,6 @@
|
|||||||
},
|
},
|
||||||
"ReviewsService": {
|
"ReviewsService": {
|
||||||
"kind": "odata", "model": "@capire/reviews"
|
"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
|
||||||
|
|
||||||
###
|
### 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"}
|
||||||
@@ -20,17 +23,19 @@ 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 http://localhost:4004/browse/Books(201)/reviews?
|
GET {{bookshop}}/browse/Books(201)/reviews?
|
||||||
&$select=rating,date,reviewer,title
|
&$select=rating,date,reviewer,title
|
||||||
|
|
||||||
### Alternative OData URL
|
### Alternative OData URL
|
||||||
GET http://localhost:4004/browse/Books/201/reviews?
|
GET {{bookshop}}/browse/Books/201/reviews?
|
||||||
&$select=rating,date,reviewer,title
|
&$select=rating,date,title
|
||||||
|
&$top=3
|
||||||
|
|
||||||
###
|
###
|
||||||
GET http://localhost:4004/browse/Books(201)?
|
GET {{bookshop}}/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
|
||||||
@@ -1 +1,3 @@
|
|||||||
|
cds.requires.messaging.kind = file-based-messaging
|
||||||
|
cds.odata.skipValidation = true
|
||||||
PORT = 5005
|
PORT = 5005
|
||||||
@@ -12,15 +12,12 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"reviews-service": "cds watch",
|
"reviews-service": "cds watch",
|
||||||
"bookshop": "cds watch test/bookshop"
|
"books-reviewed": "cds watch ../reviewed"
|
||||||
},
|
},
|
||||||
"cds": {
|
"cds": {
|
||||||
"requires": {
|
"requires": {
|
||||||
"db": {
|
"db": {
|
||||||
"kind": "sql"
|
"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('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()
|
|
||||||
@@ -261,7 +261,7 @@ describe('cds.ql → cqn', () => {
|
|||||||
// same for works distinct
|
// same for works distinct
|
||||||
})
|
})
|
||||||
|
|
||||||
xtest('where ( ... cql | {x:y} )', () => {
|
test('where ( ... cql | {x:y} )', () => {
|
||||||
const args = [`foo`, "'bar'", 3]
|
const args = [`foo`, "'bar'", 3]
|
||||||
const ID = 11
|
const ID = 11
|
||||||
|
|
||||||
@@ -278,7 +278,6 @@ 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 },
|
||||||
@@ -287,7 +286,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%' },
|
||||||
@@ -298,7 +297,6 @@ describe('cds.ql → cqn', () => {
|
|||||||
')',
|
')',
|
||||||
]
|
]
|
||||||
: [
|
: [
|
||||||
'(', //> this one is not required
|
|
||||||
{ ref: ['ID'] },
|
{ ref: ['ID'] },
|
||||||
'=',
|
'=',
|
||||||
{ val: ID },
|
{ val: ID },
|
||||||
@@ -307,7 +305,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%' },
|
||||||
@@ -337,11 +335,31 @@ 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(
|
||||||
@@ -466,47 +484,38 @@ 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', () => {
|
test('with + data', () => {
|
||||||
expect(UPDATE(Foo).with(`foo=`, 11, `bar-=`, 22))
|
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(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: { foo: 11 },
|
data: {
|
||||||
|
car: "foo's bar, car",
|
||||||
|
},
|
||||||
with: {
|
with: {
|
||||||
bar: { xpr: [{ ref: ['bar'] }, '-', { val: 22 }] },
|
bar: { func: 'coalesce', args: [{ ref: ['x'] }, { ref: ['y'] }] },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
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 () => {
|
||||||
|
|||||||
@@ -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', () => {
|
describe('Custom Handlers', () => {
|
||||||
const { GET, POST, expect } = require('./capire').launch('bookshop')
|
const { GET, POST, expect } = require('./capire').launch('bookshop')
|
||||||
|
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ describe('Localized Data', () => {
|
|||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
xit('supports @cds.localized:false', async ()=>{
|
it('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' },
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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', ()=>{
|
||||||
|
|||||||
Reference in New Issue
Block a user