Compare commits
20 Commits
data-load
...
test-exten
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d5c45d0326 | ||
|
|
390bd82502 | ||
|
|
7f6b87171a | ||
|
|
21e74bbbfb | ||
|
|
102b15c3cd | ||
|
|
73db2e96bc | ||
|
|
7bb58ee2d5 | ||
|
|
27e82d16e0 | ||
|
|
ff9bbe6d8d | ||
|
|
5f1b7b8cbf | ||
|
|
0253300557 | ||
|
|
5429617e0b | ||
|
|
139d957495 | ||
|
|
404427237b | ||
|
|
211c7e43ae | ||
|
|
08ec0c7d0d | ||
|
|
0b8e7cece9 | ||
|
|
c33613fa57 | ||
|
|
c841765791 | ||
|
|
aeee07cbcd |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -17,3 +17,4 @@ reviews/msg-box
|
|||||||
reviews/db/test.db
|
reviews/db/test.db
|
||||||
|
|
||||||
*.openapi3.json
|
*.openapi3.json
|
||||||
|
sqlite.db
|
||||||
|
|||||||
@@ -1,17 +1,22 @@
|
|||||||
const { exec } = require ('child_process')
|
const { exec } = require ('child_process')
|
||||||
|
const isWin = process.platform === 'win32'
|
||||||
const express = require ('express')
|
const express = require ('express')
|
||||||
const fs = require ('fs')
|
const fs = require ('fs')
|
||||||
const app = express()
|
const app = express()
|
||||||
|
|
||||||
const { PORT=4444 } = process.env
|
const { PORT=4444 } = process.env
|
||||||
const [,,port=PORT] = process.argv
|
const [,,port=PORT,scope='@capire'] = process.argv
|
||||||
const cwd = __dirname
|
const cwd = __dirname
|
||||||
|
|
||||||
|
// clean up on start (exit handler might not complete on Windows)
|
||||||
|
exec(isWin ? 'del *.tgz' : 'rm *.tgz', {cwd})
|
||||||
|
|
||||||
|
|
||||||
app.use('/-/:tarball', (req,res,next) => {
|
app.use('/-/:tarball', (req,res,next) => {
|
||||||
console.debug ('GET', req.params)
|
console.debug ('GET', req.params)
|
||||||
try {
|
try {
|
||||||
const { tarball } = req.params
|
const { tarball } = req.params
|
||||||
const [, pkg ] = /^capire-(\w+)/.exec(tarball)
|
const [, pkg ] = /^\w+-(\w+)/.exec(tarball)
|
||||||
fs.lstat(tarball,(err => {
|
fs.lstat(tarball,(err => {
|
||||||
if (err) exec(`npm pack ../${pkg}`,{cwd},next)
|
if (err) exec(`npm pack ../${pkg}`,{cwd},next)
|
||||||
else next()
|
else next()
|
||||||
@@ -25,12 +30,14 @@ app.use('/-/:tarball', (req,res,next) => {
|
|||||||
app.use('/-', express.static(__dirname))
|
app.use('/-', express.static(__dirname))
|
||||||
|
|
||||||
app.get('/*', (req,res)=>{
|
app.get('/*', (req,res)=>{
|
||||||
|
const urlRegex = /^\/(@\w+)\/(\w+)/
|
||||||
const url = decodeURIComponent(req.url)
|
const url = decodeURIComponent(req.url)
|
||||||
console.debug ('GET',url)
|
console.debug ('GET',url)
|
||||||
try {
|
try {
|
||||||
const [, capire, pkg ] = /^\/(@capire)\/(\w+)/.exec(url)
|
if (!urlRegex.test(url)) return res.sendStatus(404)
|
||||||
const package = require (`${capire}/${pkg}/package.json`)
|
const [, scpe, pkg ] = urlRegex.exec(url)
|
||||||
const tarball = `capire-${pkg}-${package.version}.tgz`
|
const package = require (`${scpe}/${pkg}/package.json`)
|
||||||
|
const tarball = `${scpe.slice(1)}-${pkg}-${package.version}.tgz`
|
||||||
// https://github.com/npm/registry/blob/master/docs/responses/package-metadata.md
|
// https://github.com/npm/registry/blob/master/docs/responses/package-metadata.md
|
||||||
res.json({
|
res.json({
|
||||||
"name": package.name,
|
"name": package.name,
|
||||||
@@ -42,29 +49,30 @@ app.get('/*', (req,res)=>{
|
|||||||
"name": package.name,
|
"name": package.name,
|
||||||
"version": package.version,
|
"version": package.version,
|
||||||
"dist": {
|
"dist": {
|
||||||
"tarball": `http://localhost:${port}/-/${tarball}`
|
"tarball": `/-/${tarball}`
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
if (e.code === 'MODULE_NOT_FOUND') return res.sendStatus(404)
|
||||||
res.sendStatus(404)
|
console.error(e); throw e
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
app.listen(port, ()=>{
|
const server = app.listen(port, ()=>{
|
||||||
console.log (`npm set @capire:registry=http://localhost:${port}`)
|
const url = `http://localhost:${server.address().port}`
|
||||||
console.log (`@capire registry listening on http://localhost:${port}`)
|
console.log (`npm set ${scope}:registry=${url}`)
|
||||||
exec(`npm set @capire:registry=http://localhost:${port}`)
|
exec(`npm set ${scope}:registry=${url}`)
|
||||||
|
console.log (`${scope} registry listening on ${url}`)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
const _exit = ()=>{
|
const _exit = ()=>{
|
||||||
console.log ('\nnpm conf rm @capire:registry')
|
server.close()
|
||||||
exec('npm conf rm @capire:registry')
|
exec(`npm conf rm "${scope}:registry"`, ()=> { process.exit() })
|
||||||
exec('rm *.tgz')
|
|
||||||
process.exit()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
process.on ('SIGTERM',_exit)
|
process.on ('SIGTERM',_exit)
|
||||||
process.on ('SIGHUP',_exit)
|
process.on ('SIGHUP',_exit)
|
||||||
process.on ('SIGINT',_exit)
|
process.on ('SIGINT',_exit)
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
<td>{{ book.author }}</td>
|
<td>{{ book.author }}</td>
|
||||||
<td>{{ book.genre.name }}</td>
|
<td>{{ book.genre.name }}</td>
|
||||||
<td class="rating-stars">
|
<td class="rating-stars">
|
||||||
{{ ('★'.repeat(Math.round(book.rating))+'☆☆☆☆☆').slice(0,5) }}
|
{{ ('★'.repeat(Math.round(book.rating))+'☆☆☆☆☆').slice(0,5) }} ({{ book.numberOfReviews }})
|
||||||
</td>
|
</td>
|
||||||
<td>{{ book.currency && book.currency.symbol }} {{ book.price }}</td>
|
<td>{{ book.currency && book.currency.symbol }} {{ book.price }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
using { Currency, managed, sap } from '@sap/cds/common';
|
using { Currency, managed, sap, extensible } from '@sap/cds/common';
|
||||||
namespace sap.capire.bookshop;
|
namespace sap.capire.bookshop;
|
||||||
|
|
||||||
entity Books : managed {
|
entity Books : managed, extensible {
|
||||||
key ID : Integer;
|
key ID : Integer;
|
||||||
title : localized String(111);
|
title : localized String(111);
|
||||||
descr : localized String(1111);
|
descr : localized String(1111);
|
||||||
@@ -13,7 +13,7 @@ entity Books : managed {
|
|||||||
image : LargeBinary @Core.MediaType : 'image/png';
|
image : LargeBinary @Core.MediaType : 'image/png';
|
||||||
}
|
}
|
||||||
|
|
||||||
entity Authors : managed {
|
entity Authors : managed, extensible {
|
||||||
key ID : Integer;
|
key ID : Integer;
|
||||||
name : String(111);
|
name : String(111);
|
||||||
dateOfBirth : Date;
|
dateOfBirth : Date;
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
exports.CatalogService = require('./srv/cat-service')
|
const { CatalogService } = require('./srv/cat-service')
|
||||||
|
module.exports = { CatalogService }
|
||||||
|
|||||||
@@ -1,55 +1,81 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||||
|
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Bookshop</title>
|
||||||
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
<script>
|
||||||
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8" />
|
window["sap-ushell-config"] = {
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
defaultRenderer: "fiori2",
|
||||||
<title>Bookshop</title>
|
applications: {
|
||||||
|
"browse-books": {
|
||||||
|
title: "Browse Books",
|
||||||
|
description: "w/ SAP Fiori Elements",
|
||||||
|
additionalInformation: "SAPUI5.Component=bookshop",
|
||||||
|
applicationType: "URL",
|
||||||
|
url: "/browse/webapp",
|
||||||
|
navigationMode: "embedded",
|
||||||
|
},
|
||||||
|
"manage-books": {
|
||||||
|
title: "Manage Books",
|
||||||
|
description: "w/ SAP Fiori Elements",
|
||||||
|
additionalInformation: "SAPUI5.Component=admin",
|
||||||
|
applicationType: "URL",
|
||||||
|
url: "/admin/webapp",
|
||||||
|
navigationMode: "embedded",
|
||||||
|
},
|
||||||
|
"manage-orders": {
|
||||||
|
title: "Manage Orders",
|
||||||
|
description: "w/ SAP Fiori Elements",
|
||||||
|
additionalInformation: "SAPUI5.Component=orders",
|
||||||
|
applicationType: "URL",
|
||||||
|
url: "/orders/webapp",
|
||||||
|
navigationMode: "embedded",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
bootstrapPlugins: {
|
||||||
|
RuntimeAuthoringPlugin: {
|
||||||
|
component: "sap.ushell.plugins.rta",
|
||||||
|
config: {
|
||||||
|
validateAppVersion: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
PersonalizePlugin: {
|
||||||
|
component: "sap.ushell.plugins.rta-personalize",
|
||||||
|
config: {
|
||||||
|
validateAppVersion: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
<script>
|
<script
|
||||||
window["sap-ushell-config"] = {
|
id="sap-ushell-bootstrap"
|
||||||
defaultRenderer: "fiori2",
|
src="https://sapui5nightly.int.sap.eu2.hana.ondemand.com/test-resources/sap/ushell/bootstrap/sandbox.js"
|
||||||
applications: {
|
></script>
|
||||||
"browse-books": {
|
<script
|
||||||
title: "Browse Books",
|
id="sap-ui-bootstrap"
|
||||||
description: "w/ SAP Fiori Elements",
|
src="https://sapui5nightly.int.sap.eu2.hana.ondemand.com/resources/sap-ui-core.js"
|
||||||
additionalInformation: "SAPUI5.Component=bookshop",
|
data-sap-ui-libs="sap.m, sap.ushell, sap.collaboration, sap.ui.layout"
|
||||||
applicationType : "URL",
|
data-sap-ui-compatVersion="edge"
|
||||||
url: "/browse/webapp",
|
data-sap-ui-theme="sap_fiori_3"
|
||||||
navigationMode: "embedded"
|
data-sap-ui-frameOptions="allow"
|
||||||
},
|
data-sap-ui-bindingSyntax="complex"
|
||||||
"manage-books": {
|
></script>
|
||||||
title: "Manage Books",
|
<script>
|
||||||
description: "w/ SAP Fiori Elements",
|
sap.ui
|
||||||
additionalInformation: "SAPUI5.Component=admin",
|
.getCore()
|
||||||
applicationType : "URL",
|
.attachInit(() =>
|
||||||
url: "/admin/webapp",
|
sap.ushell.Container.createRenderer().placeAt("content")
|
||||||
navigationMode: "embedded"
|
);
|
||||||
},
|
sap.ui
|
||||||
"manage-orders": {
|
.getCore()
|
||||||
title: "Manage Orders",
|
.getConfiguration()
|
||||||
description: "w/ SAP Fiori Elements",
|
.setFlexibilityServices([{ connector: "SessionStorageConnector" }]);
|
||||||
additionalInformation: "SAPUI5.Component=orders",
|
</script>
|
||||||
applicationType : "URL",
|
</head>
|
||||||
url: "/orders/webapp",
|
<body class="sapUiBody" id="content"></body>
|
||||||
navigationMode: "embedded"
|
</html>
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script id="sap-ushell-bootstrap" src="https://sapui5.hana.ondemand.com/test-resources/sap/ushell/bootstrap/sandbox.js"></script>
|
|
||||||
<script id="sap-ui-bootstrap" 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"
|
|
||||||
data-sap-ui-frameOptions="allow"
|
|
||||||
></script>
|
|
||||||
<script>
|
|
||||||
sap.ui.getCore().attachInit(()=> sap.ushell.Container.createRenderer().placeAt("content"))
|
|
||||||
</script>
|
|
||||||
|
|
||||||
</head>
|
|
||||||
<body class="sapUiBody" id="content"></body>
|
|
||||||
</html>
|
|
||||||
|
|||||||
@@ -22,6 +22,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sap.ui5": {
|
"sap.ui5": {
|
||||||
|
"flexEnabled": true,
|
||||||
|
"config": {
|
||||||
|
"experimentalCAPScenario": true
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"libs": {
|
"libs": {
|
||||||
"sap.fe.templates": {}
|
"sap.fe.templates": {}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sap.ui5": {
|
"sap.ui5": {
|
||||||
|
"flexEnabled": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"libs": {
|
"libs": {
|
||||||
"sap.fe.templates": {}
|
"sap.fe.templates": {}
|
||||||
|
|||||||
@@ -30,15 +30,20 @@
|
|||||||
"kind": "odata",
|
"kind": "odata",
|
||||||
"model": "@capire/orders"
|
"model": "@capire/orders"
|
||||||
},
|
},
|
||||||
|
"extensibility": {
|
||||||
|
"kind": "uiflex"
|
||||||
|
},
|
||||||
"db": {
|
"db": {
|
||||||
"kind": "sql",
|
"kind": "sqlite",
|
||||||
"[development]": {
|
"credentials": {
|
||||||
"model": "db/sqlite"
|
"database": "sqlite.db"
|
||||||
},
|
|
||||||
"[production]": {
|
|
||||||
"model": "db/hana"
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"messaging": {
|
||||||
|
"[development]": { "kind": "file-based-messaging" },
|
||||||
|
"[hybrid]": { "kind": "enterprise-messaging-shared" },
|
||||||
|
"kind": "enterprise-messaging"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -12,6 +12,7 @@ using { sap.capire.bookshop.Books } from '@capire/bookshop';
|
|||||||
using { ReviewsService.Reviews } from '@capire/reviews';
|
using { ReviewsService.Reviews } from '@capire/reviews';
|
||||||
extend Books with {
|
extend Books with {
|
||||||
reviews : Composition of many Reviews on reviews.subject = $self.ID;
|
reviews : Composition of many Reviews on reviews.subject = $self.ID;
|
||||||
|
numberOfReviews : Integer;
|
||||||
rating : Decimal;
|
rating : Decimal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,8 +41,8 @@ module.exports = async()=>{ // called by server.js
|
|||||||
//
|
//
|
||||||
ReviewsService.on ('reviewed', (msg) => {
|
ReviewsService.on ('reviewed', (msg) => {
|
||||||
console.debug ('> received:', msg.event, msg.data)
|
console.debug ('> received:', msg.event, msg.data)
|
||||||
const { subject, rating } = msg.data
|
const { subject, count, rating } = msg.data
|
||||||
return UPDATE(Books,subject).with({rating})
|
return UPDATE(Books,subject).with({ numberOfReviews:count, rating })
|
||||||
// ^ Note: the framework will execute this and take care for db.tx
|
// ^ Note: the framework will execute this and take care for db.tx
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
14
fiori/test.http
Normal file
14
fiori/test.http
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# Add integer extension field to Books
|
||||||
|
POST http://localhost:4004/extensibility/addExtension
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"extensions": [
|
||||||
|
"{\"extend\":\"AdminService.Books\",\"elements\":{\"neuesFeld2\":{\"type\":\"cds.String\", \"default\":\"hallo\"}}}"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
###
|
||||||
|
# { "extend": "AdminService.Books", "elements":{
|
||||||
|
# "abc":{"type":"cds.Integer"}
|
||||||
|
# }},
|
||||||
@@ -19,19 +19,38 @@
|
|||||||
url: "/orders/webapp",
|
url: "/orders/webapp",
|
||||||
navigationMode: "embedded"
|
navigationMode: "embedded"
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
bootstrapPlugins: {
|
||||||
|
RuntimeAuthoringPlugin: {
|
||||||
|
component: "sap.ushell.plugins.rta",
|
||||||
|
config: {
|
||||||
|
validateAppVersion: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
PersonalizePlugin: {
|
||||||
|
component: "sap.ushell.plugins.rta-personalize",
|
||||||
|
config: {
|
||||||
|
validateAppVersion: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script id="sap-ushell-bootstrap" src="https://sapui5.hana.ondemand.com/test-resources/sap/ushell/bootstrap/sandbox.js"></script>
|
<script id="sap-ushell-bootstrap" src="https://sapui5nightly.int.sap.eu2.hana.ondemand.com/test-resources/sap/ushell/bootstrap/sandbox.js"></script>
|
||||||
<script id="sap-ui-bootstrap" src="https://sapui5.hana.ondemand.com/resources/sap-ui-core.js"
|
<script id="sap-ui-bootstrap" src="https://sapui5nightly.int.sap.eu2.hana.ondemand.com/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"
|
||||||
data-sap-ui-frameOptions="allow"
|
data-sap-ui-frameOptions="allow"
|
||||||
></script>
|
data-sap-ui-bindingSyntax="complex"
|
||||||
|
></script>
|
||||||
<script>
|
<script>
|
||||||
sap.ui.getCore().attachInit(()=> sap.ushell.Container.createRenderer().placeAt("content"))
|
sap.ui.getCore().attachInit(()=> sap.ushell.Container.createRenderer().placeAt("content"));
|
||||||
|
sap.ui
|
||||||
|
.getCore()
|
||||||
|
.getConfiguration()
|
||||||
|
.setFlexibilityServices([{ connector: "SessionStorageConnector" }]);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -22,6 +22,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sap.ui5": {
|
"sap.ui5": {
|
||||||
|
"flexEnabled": true,
|
||||||
|
"config": {
|
||||||
|
"experimentalCAPScenario": true
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"libs": {
|
"libs": {
|
||||||
"sap.fe.templates": {}
|
"sap.fe.templates": {}
|
||||||
|
|||||||
3
package-lock.json
generated
3
package-lock.json
generated
@@ -1,3 +1,6 @@
|
|||||||
|
// 20211103133211
|
||||||
|
// https://raw.githubusercontent.com/SAP-samples/cloud-cap-samples/main/package-lock.json
|
||||||
|
|
||||||
{
|
{
|
||||||
"name": "@capire/samples",
|
"name": "@capire/samples",
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
|
|||||||
@@ -12,7 +12,8 @@
|
|||||||
"@capire/media": "./media",
|
"@capire/media": "./media",
|
||||||
"@capire/orders": "./orders",
|
"@capire/orders": "./orders",
|
||||||
"@capire/reviews": "./reviews",
|
"@capire/reviews": "./reviews",
|
||||||
"@sap/cds": "^5.5.3"
|
"@sap/cds": "git+https://github.tools.sap/cap/cds.git",
|
||||||
|
"@sap/cds-dk": "*"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"chai": "^4.3.4",
|
"chai": "^4.3.4",
|
||||||
|
|||||||
@@ -8,10 +8,11 @@ service ReviewsService {
|
|||||||
action unlike (review: type of Reviews:ID);
|
action unlike (review: type of Reviews:ID);
|
||||||
|
|
||||||
// Async API
|
// Async API
|
||||||
event reviewed : {
|
event reviewed : {
|
||||||
subject: type of Reviews:subject;
|
subject : type of Reviews:subject;
|
||||||
rating: Decimal(2,1)
|
count : Integer;
|
||||||
}
|
rating : Decimal;
|
||||||
|
}
|
||||||
|
|
||||||
// Input validation
|
// Input validation
|
||||||
annotate Reviews with {
|
annotate Reviews with {
|
||||||
|
|||||||
@@ -12,11 +12,11 @@ module.exports = cds.service.impl (function(){
|
|||||||
// Emit an event to inform subscribers about new avg ratings for reviewed subjects
|
// Emit an event to inform subscribers about new avg ratings for reviewed subjects
|
||||||
this.after (['CREATE','UPDATE','DELETE'], 'Reviews', async function(_,req) {
|
this.after (['CREATE','UPDATE','DELETE'], 'Reviews', async function(_,req) {
|
||||||
const {subject} = req.data
|
const {subject} = req.data
|
||||||
const {rating} = await cds.tx(req) .run (
|
const { count, rating } = await cds.tx(req) .run (
|
||||||
SELECT.one (['round(avg(rating),2) as rating']) .from (Reviews) .where ({subject})
|
SELECT.one `round(avg(rating),2) as rating, count(*) as count` .from (Reviews) .where ({subject})
|
||||||
)
|
)
|
||||||
global.it || console.log ('< emitting:', 'reviewed', { subject, rating })
|
global.it || console.log ('< emitting:', 'reviewed', { subject, count, rating })
|
||||||
await this.emit ('reviewed', { subject, rating })
|
await this.emit ('reviewed', { subject, count, rating })
|
||||||
})
|
})
|
||||||
|
|
||||||
// Increment counter for reviews considered helpful
|
// Increment counter for reviews considered helpful
|
||||||
|
|||||||
3
reviews/test/app/bookshop.html
Normal file
3
reviews/test/app/bookshop.html
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<head>
|
||||||
|
<meta http-equiv="refresh" content="0;url=app/bookshop/index.html">
|
||||||
|
</head>
|
||||||
3
reviews/test/app/reviews.html
Normal file
3
reviews/test/app/reviews.html
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<head>
|
||||||
|
<meta http-equiv="refresh" content="0;url=app/reviews/index.html">
|
||||||
|
</head>
|
||||||
17
reviews/test/package.json
Normal file
17
reviews/test/package.json
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"name": "@capire/bookshop-with-reviews",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@capire/bookshop": "*",
|
||||||
|
"@capire/reviews": "*",
|
||||||
|
"@sap/cds": "^5",
|
||||||
|
"express": "^4.17.1"
|
||||||
|
},
|
||||||
|
"cds": {
|
||||||
|
"requires": {
|
||||||
|
"db": {
|
||||||
|
"kind": "sql"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
13
reviews/test/srv/bookshop.cds
Normal file
13
reviews/test/srv/bookshop.cds
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
// Use enhanced implementation for CatalogService
|
||||||
|
using { CatalogService } from '@capire/bookshop';
|
||||||
|
annotate CatalogService with @impl:'srv/bookshop.js';
|
||||||
|
|
||||||
|
|
||||||
|
// Extend Books with access to Reviews and average ratings
|
||||||
|
using { sap.capire.bookshop.Books } from '@capire/bookshop';
|
||||||
|
using { ReviewsService.Reviews } from '@capire/reviews';
|
||||||
|
extend Books with {
|
||||||
|
reviews : Composition of many Reviews on reviews.subject = $self.ID;
|
||||||
|
rating : Decimal;
|
||||||
|
numberOfReviews : Integer;
|
||||||
|
}
|
||||||
27
reviews/test/srv/bookshop.js
Normal file
27
reviews/test/srv/bookshop.js
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
const { CatalogService } = require('@capire/bookshop')
|
||||||
|
const cds = require ('@sap/cds')
|
||||||
|
|
||||||
|
module.exports = class extends CatalogService {async init(){
|
||||||
|
|
||||||
|
const { Books } = cds.entities('sap.capire.bookshop')
|
||||||
|
|
||||||
|
// Connect to ReviewsService to receive `reviewed` events from it
|
||||||
|
const ReviewsService = await cds.connect.to ('ReviewsService')
|
||||||
|
ReviewsService.on ('reviewed', (msg) => {
|
||||||
|
console.debug ('> received:', msg.event, msg.data)
|
||||||
|
const { subject, count, rating } = msg.data
|
||||||
|
return UPDATE(Books,subject).with({ numberOfReviews:count, rating })
|
||||||
|
})
|
||||||
|
|
||||||
|
return super.init()
|
||||||
|
}}
|
||||||
|
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Helper for serving static content from npm-installed packages
|
||||||
|
const {dirname,resolve} = require('path')
|
||||||
|
const {static} = require('express')
|
||||||
|
cds.once('listening',()=>{
|
||||||
|
cds.app.use ('/app/bookshop', static (dirname (require.resolve('@capire/bookshop'))+'/app/vue'))
|
||||||
|
cds.app.use ('/app/reviews', static (resolve (__dirname, '../../app/vue')))
|
||||||
|
})
|
||||||
@@ -86,14 +86,24 @@ describe('cds.ql → cqn', () => {
|
|||||||
.to.eql(SELECT.from(Foo,{ID:11}))
|
.to.eql(SELECT.from(Foo,{ID:11}))
|
||||||
.to.eql(SELECT.from(Foo).byKey(11))
|
.to.eql(SELECT.from(Foo).byKey(11))
|
||||||
.to.eql(SELECT.from(Foo).byKey({ID:11}))
|
.to.eql(SELECT.from(Foo).byKey({ID:11}))
|
||||||
expect.one(cqn)
|
if (cds.version >= '5.6.0') {
|
||||||
.to.eql({
|
expect.one(cqn)
|
||||||
SELECT: {
|
.to.eql({
|
||||||
one: true,
|
SELECT: {
|
||||||
from: { ref: ['Foo'] },
|
one: true,
|
||||||
where: [{ ref: ['ID'] }, '=', { val: 11 }],
|
from: { ref: [{ id: 'Foo', where: [{ ref: ['ID'] }, '=', { val: 11 }] }] },
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
expect.one(cqn)
|
||||||
|
.to.eql({
|
||||||
|
SELECT: {
|
||||||
|
one: true,
|
||||||
|
from: { ref: ['Foo'] },
|
||||||
|
where: [{ ref: ['ID'] }, '=', { val: 11 }],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -131,15 +141,27 @@ describe('cds.ql → cqn', () => {
|
|||||||
// Test combination with key as second argument to .from
|
// Test combination with key as second argument to .from
|
||||||
expect(cqn = SELECT.from(Foo, 11, ['a']))
|
expect(cqn = SELECT.from(Foo, 11, ['a']))
|
||||||
.to.eql(SELECT.from(Foo, 11, foo => foo.a))
|
.to.eql(SELECT.from(Foo, 11, foo => foo.a))
|
||||||
expect.one(cqn)
|
|
||||||
.to.eql({
|
if (cds.version >= '5.6.0') {
|
||||||
SELECT: {
|
expect.one(cqn)
|
||||||
one: true,
|
.to.eql({
|
||||||
from: { ref: ['Foo'] },
|
SELECT: {
|
||||||
columns: [{ ref: ['a'] }],
|
one: true,
|
||||||
where: [{ ref: ['ID'] }, '=', { val: 11 }],
|
from: { ref: [{ id: 'Foo', where: [{ ref: ['ID'] }, '=', { val: 11 }]}] },
|
||||||
},
|
columns: [{ ref: ['a'] }]
|
||||||
})
|
},
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
expect.one(cqn)
|
||||||
|
.to.eql({
|
||||||
|
SELECT: {
|
||||||
|
one: true,
|
||||||
|
from: { ref: ['Foo'] },
|
||||||
|
columns: [{ ref: ['a'] }],
|
||||||
|
where: [{ ref: ['ID'] }, '=', { val: 11 }],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -551,21 +573,31 @@ describe('cds.ql → cqn', () => {
|
|||||||
|
|
||||||
describe(`UPDATE...`, () => {
|
describe(`UPDATE...`, () => {
|
||||||
test('entity (..., <key>)', () => {
|
test('entity (..., <key>)', () => {
|
||||||
expect(UPDATE(Books, 4711))
|
const cqnWhere = {
|
||||||
.to.eql(UPDATE(Books, { ID: 4711 }))
|
|
||||||
.to.eql(UPDATE(Books).byKey(4711))
|
|
||||||
.to.eql(UPDATE(Books).byKey({ ID: 4711 }))
|
|
||||||
.to.eql(UPDATE(Books).where({ ID: 4711 }))
|
|
||||||
.to.eql(UPDATE(Books).where(`ID=`, 4711))
|
|
||||||
.to.eql(UPDATE.entity(Books, 4711))
|
|
||||||
.to.eql(UPDATE.entity(Books, { ID: 4711 }))
|
|
||||||
// etc...
|
|
||||||
.to.eql({
|
|
||||||
UPDATE: {
|
UPDATE: {
|
||||||
entity: 'capire.bookshop.Books',
|
entity: 'capire.bookshop.Books',
|
||||||
where: [{ ref: ['ID'] }, '=', { val: 4711 }],
|
where: [{ ref: ['ID'] }, '=', { val: 4711 }],
|
||||||
},
|
},
|
||||||
})
|
}
|
||||||
|
expect(UPDATE(Books).where({ ID: 4711 }))
|
||||||
|
.to.eql(UPDATE(Books).where(`ID=`, 4711))
|
||||||
|
.to.eql(cqnWhere)
|
||||||
|
|
||||||
|
const cqnKey = (cds.version >= '5.6.0') ?
|
||||||
|
{
|
||||||
|
UPDATE: {
|
||||||
|
entity: { ref: [{ id: 'capire.bookshop.Books', where: [{ ref: ['ID'] }, '=', { val: 4711 }] }] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: cqnWhere
|
||||||
|
expect(UPDATE(Books, 4711))
|
||||||
|
.to.eql(UPDATE(Books, { ID: 4711 }))
|
||||||
|
.to.eql(UPDATE(Books).byKey(4711))
|
||||||
|
.to.eql(UPDATE(Books).byKey({ ID: 4711 }))
|
||||||
|
.to.eql(UPDATE.entity(Books, 4711))
|
||||||
|
.to.eql(UPDATE.entity(Books, { ID: 4711 }))
|
||||||
|
// etc...
|
||||||
|
.to.eql(cqnKey)
|
||||||
})
|
})
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -616,20 +648,29 @@ describe('cds.ql → cqn', () => {
|
|||||||
|
|
||||||
describe(`DELETE...`, () => {
|
describe(`DELETE...`, () => {
|
||||||
test('from (..., <key>)', () => {
|
test('from (..., <key>)', () => {
|
||||||
|
const cqnWhere = {
|
||||||
|
DELETE: {
|
||||||
|
from: 'capire.bookshop.Books',
|
||||||
|
where: [{ ref: ['ID'] }, '=', { val: 4711 }],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
expect(DELETE.from(Books).where({ ID: 4711 }))
|
||||||
|
.to.eql(DELETE.from(Books).where(`ID=`, 4711))
|
||||||
|
.to.eql(cqnWhere)
|
||||||
|
const cqnKey = (cds.version >= '5.6.0') ?
|
||||||
|
{
|
||||||
|
DELETE: {
|
||||||
|
from: { ref: [{ id: 'capire.bookshop.Books', where: [{ ref: ['ID'] }, '=', { val: 4711 }]}] }
|
||||||
|
},
|
||||||
|
} : cqnWhere
|
||||||
|
|
||||||
expect(DELETE(Books, 4711))
|
expect(DELETE(Books, 4711))
|
||||||
.to.eql(DELETE(Books, { ID: 4711 }))
|
.to.eql(DELETE(Books, { ID: 4711 }))
|
||||||
.to.eql(DELETE.from(Books, 4711))
|
.to.eql(DELETE.from(Books, 4711))
|
||||||
.to.eql(DELETE.from(Books, { ID: 4711 }))
|
.to.eql(DELETE.from(Books, { ID: 4711 }))
|
||||||
.to.eql(DELETE.from(Books).byKey(4711))
|
.to.eql(DELETE.from(Books).byKey(4711))
|
||||||
.to.eql(DELETE.from(Books).byKey({ ID: 4711 }))
|
.to.eql(DELETE.from(Books).byKey({ ID: 4711 }))
|
||||||
.to.eql(DELETE.from(Books).where({ ID: 4711 }))
|
.to.eql(cqnKey)
|
||||||
.to.eql(DELETE.from(Books).where(`ID=`, 4711))
|
|
||||||
.to.eql({
|
|
||||||
DELETE: {
|
|
||||||
from: 'capire.bookshop.Books',
|
|
||||||
where: [{ ref: ['ID'] }, '=', { val: 4711 }],
|
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test('/w plain SQL', () => {
|
test('/w plain SQL', () => {
|
||||||
|
|||||||
@@ -60,11 +60,11 @@ describe('Messaging', ()=>{
|
|||||||
expect(M).equals(N)
|
expect(M).equals(N)
|
||||||
expect(received.length).equals(N)
|
expect(received.length).equals(N)
|
||||||
expect(received.map(m=>m.data)).to.deep.equal([
|
expect(received.map(m=>m.data)).to.deep.equal([
|
||||||
{ subject: '201', rating: 1 },
|
{ count: 1, subject: '201', rating: 1 },
|
||||||
{ subject: '201', rating: 1.5 },
|
{ count: 2, subject: '201', rating: 1.5 },
|
||||||
{ subject: '201', rating: 2 },
|
{ count: 3, subject: '201', rating: 2 },
|
||||||
{ subject: '201', rating: 2.5 },
|
{ count: 4, subject: '201', rating: 2.5 },
|
||||||
{ subject: '201', rating: 3 },
|
{ count: 5, subject: '201', rating: 3 },
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
54
test/registry.test.js
Normal file
54
test/registry.test.js
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
|
||||||
|
const { fork } = require('child_process')
|
||||||
|
const { resolve } = require('path')
|
||||||
|
const Axios = require('axios')
|
||||||
|
const verbose = process.env.CDS_TEST_VERBOSE
|
||||||
|
// ||true
|
||||||
|
|
||||||
|
describe('Local NPM registry', () => {
|
||||||
|
let registry
|
||||||
|
let axios
|
||||||
|
const cwd = resolve(__dirname, '..')
|
||||||
|
|
||||||
|
beforeAll(async ()=> {
|
||||||
|
const env = Object.assign(process.env, {PORT:'0'})
|
||||||
|
const res = await exec (resolve(cwd, '.registry/server.js'), {cwd, stdio: 'pipe', env})
|
||||||
|
registry = res.cp
|
||||||
|
axios = Axios.default.create ({ baseURL: res.url, validateStatus: (status)=>status<500 })
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(() => { registry.kill() })
|
||||||
|
|
||||||
|
for (const mod of ['bookshop','fiori','orders','reviews']) {
|
||||||
|
it(`should serve ${mod}`, async () => {
|
||||||
|
const resp = await axios.get(`/@capire/${mod}`)
|
||||||
|
expect(resp.data).toMatchObject({name: `@capire/${mod}`, versions:{}})
|
||||||
|
const versions = Object.values(resp.data.versions)
|
||||||
|
await axios.get(versions[0].dist.tarball)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
it(`should return 404 for unknown packages`, async () => {
|
||||||
|
let resp = await axios.get(`/@capire/foo`)
|
||||||
|
expect(resp.status).toEqual(404)
|
||||||
|
resp = await axios.get(`/foo`)
|
||||||
|
expect(resp.status).toEqual(404)
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
function exec (script, opts) {
|
||||||
|
return new Promise((resolve, reject)=> {
|
||||||
|
const cp = fork (script, [], opts)
|
||||||
|
.on('error', err => reject(new Error(err)))
|
||||||
|
cp.stdout.on('data', chunk => {
|
||||||
|
if (verbose) console.log(chunk.toString())
|
||||||
|
if (chunk.toString().match(/listening.*(http:.*:\d+)/i)) {
|
||||||
|
resolve({cp, url:RegExp.$1})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
cp.stderr.on('data', chunk => {
|
||||||
|
if (verbose) console.error(chunk.toString())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user