Compare commits

..

78 Commits

Author SHA1 Message Date
Daniel
fc3afc51e6 Remove workaround for authorization glitch 2020-11-21 01:35:24 +01:00
Daniel
e15a6192b6 Aligned orders w/ managed compositions 2020-11-21 01:32:39 +01:00
Daniel
d1eb14f638 Minor Cleanup 2020-11-20 17:22:20 +01:00
Daniel
394a8b5d12 Revised layout of .html pages 2020-11-20 16:11:33 +01:00
Daniel
dae8e96fe1 Adding Vue.js apps for reviews service 2020-11-20 16:11:33 +01:00
Daniel
8f01bf911e Using fake Products entity in @capire/orders 2020-11-20 12:53:25 +01:00
Daniel Hutzel
932f56812c Update server.js 2020-11-17 20:32:47 +01:00
Daniel
d80084bfb7 Added @capire:registry 2020-11-17 17:51:18 +01:00
Daniel
f2c37ec162 composites 2020-11-17 07:35:43 +01:00
Daniel Hutzel
d0d08b1ee1 Enable authorizations w/ dummy-auth (#158)
* Enable authorizations w/ dummy-auth

* fixed: some tests run in privileged mode

* Fixed tests to skip auth

* npm test --silent

* Added dependency to passport
2020-11-12 23:56:04 +01:00
Daniel
ffaec7aa07 Using sqlite 5 2020-11-12 17:06:50 +01:00
Christian Georgi
1587521bd3 Use newer syntax 2020-11-05 09:40:18 +01:00
Andre Meyering
c04c423c0d cat-services.cds: Use "type of" to use "Books:id"
The syntax `Books.id` (dot) is deprecated and is warned about in the latest
CDS releases.  It should be replaced by either `Books:id` (colon, supported
since cds-compiler v1.41.0) or `type of Books:id` (tested with cds-compiler
v1.20.0 from November 2019).
2020-11-05 09:40:18 +01:00
Daniel
7b5c60c3c7 cleanup mocha settings 2020-10-29 17:35:57 +01:00
Christian Georgi
8317503b20 Update issue template 2020-10-29 15:39:38 +01:00
Christian Georgi
f6c838e6ea Update question--feedback-or-bug-.md 2020-10-29 15:35:06 +01:00
Christian Georgi
5d64ca3555 Use test API from cds lib instead of custom one 2020-10-29 11:12:18 +01:00
Daniel
392106d44a Quick fix for affected rows 2020-10-28 16:55:05 +01:00
Daniel
df55110b9b Updated to use srv-to-srv + declared events 2020-10-28 15:17:28 +01:00
Daniel
d9a06d16f1 clean up legacy stuff 2020-10-26 16:21:30 +01:00
Daniel
c424517770 fixing reviews sample 2020-10-22 18:11:46 +02:00
Christian Georgi
363e9aa9a8 Update requests.http 2020-10-12 15:17:03 +02:00
Christian Georgi
7131c13500 Add bookshop image streaming impl 2020-10-12 15:17:03 +02:00
Daniel
934a00eff1 updated gaps 2020-10-07 23:12:48 +02:00
Christian Georgi
a238390b73 Add Node.js 14 to CI 2020-09-30 11:00:21 +02:00
Christian Georgi
54e170ef88 Fix contact name 2020-09-30 10:56:50 +02:00
Christian Georgi
df3cfbb956 Add license badge 2020-09-30 09:40:49 +02:00
Christian Georgi
14756e47f7 Add license files compliant to reuse tool
See https://github.com/fsfe/reuse-tool
2020-09-30 09:24:44 +02:00
Gregor Wolf
90edc4289f Add REST Client test for media 2020-09-30 08:55:29 +02:00
Gregor Wolf
7ae618d803 Add media to scripts and dependencies 2020-09-30 08:55:29 +02:00
Iwona Hahn
e19447b700 update cat-service.js
based on feedback in the community, see also [PR](https://github.wdf.sap.corp/cap/cap.github.wdf.sap.corp/pull/1864)
2020-09-29 18:55:45 +02:00
Dr. David Kunz
ab18c12a69 Merge pull request #135 from SAP-samples/messaging-cds-4
new messaging
2020-09-15 19:03:06 +02:00
D065023
a456eae8ba Merge branch 'messaging-cds-4' of https://github.com/SAP-samples/cloud-cap-samples into messaging-cds-4 2020-09-15 16:36:23 +02:00
D065023
d8308fe7a3 review/reviewed -> reviewed 2020-09-15 16:36:04 +02:00
Dr. David Kunz
a75f8bdc45 Update reviews/srv/reviews-service.js
Co-authored-by: Daniel Hutzel <daniel.hutzel@sap.com>
2020-09-15 16:35:24 +02:00
D065023
a71aaf75a1 new messaging 2020-09-15 15:30:14 +02:00
Daniel Hutzel
6fdd91b8c8 Merge pull request #130 from SAP-samples/test-for-4.2
Updated tests for 4.2
2020-08-27 13:29:24 +02:00
Daniel
62c3969185 Updated tests for 4.2 2020-08-27 13:26:46 +02:00
Christian Georgi
2d608cd882 Add script IDs for UI5 and Fiori bootstrap
Fixes the CORS issues with the hana.ondemand servers
2020-08-25 11:02:02 +02:00
Daniel Hutzel
ae8fe083ed Merge pull request #127 from SAP-samples/cds.test
Prepare moving test kit over to @sap/cds
2020-08-19 12:38:38 +02:00
Daniel
6b3d9db4e9 Remove obsolete dep to @capire/tests 2020-08-19 12:28:53 +02:00
Daniel
332fae3761 Also monkey-patching cds.load 2020-08-19 10:06:36 +02:00
Daniel
039f62209c Prepare moving test kit over to @sap/cds 2020-08-19 09:38:07 +02:00
Daniel Hutzel
a43ade103c Merge pull request #126 from SAP-samples/fix-call-next
Fixing tests to prefer proper calls to next()
2020-08-12 14:00:20 +02:00
Daniel
a6f1d48670 Fixing tests to prefer proper calls to next() 2020-08-12 13:56:49 +02:00
Daniel Hutzel
bd65af43eb Merge pull request #125 from SAP-samples/simplified-reviewed
Simplified reviewed/server.js
2020-08-10 23:24:51 +02:00
Daniel
6f9133cd4f Simplified reviewed/server.js 2020-08-06 19:32:07 +02:00
Daniel Hutzel
441c82b4c9 Merge pull request #123 from SAP-samples/Pinning-Fiori-to-last-known-good
Pinning Fiori to last-known-good
2020-08-01 14:47:30 +02:00
Daniel
fa7cff4123 Pinning Fiori to last-known-good 2020-08-01 14:44:13 +02:00
Christian Georgi
1b69064752 Make cuid.ID Core.Computed for now
+ TODO to check w/ Fiori
2020-07-31 17:18:14 +02:00
Christian Georgi
ada05cf279 Cosmetics 2020-07-31 16:28:08 +02:00
Christian Georgi
4b78a8b637 Do no longer use run blocks - removed in cds 4 2020-07-31 16:28:08 +02:00
Daniel Hutzel
5c5afd2790 Merge pull request #118 from SAP-samples/fixed-tests
Fxed cwd in tests
2020-07-23 02:02:01 +02:00
Daniel
74ee6f34e4 Fixed jest mocha 2020-07-23 02:00:34 +02:00
Daniel
9a9b7aeb86 Fxed cwd in tests 2020-07-23 01:56:56 +02:00
Daniel Hutzel
cfc01bbc4f Merge pull request #117 from SAP-samples/fixed-tests
Secured cwd in test
2020-07-23 01:26:27 +02:00
Daniel
aaac6cc678 Secured cwd in test 2020-07-23 01:24:39 +02:00
Daniel Hutzel
99fdf0c038 Merge pull request #116 from SAP-samples/using-dot-env
Doing bindings in .env
2020-07-17 16:05:34 +02:00
Daniel
6ccecfecae Doing bindings in .env 2020-07-17 16:04:03 +02:00
Daniel Hutzel
e5f0a7ef73 Merge pull request #115 from SAP-samples/prepare-4
Prepare tests for upcomming rel 4
2020-07-17 01:52:02 +02:00
Daniel
de54f70570 Prepare tests for upcomming rel 4 2020-07-17 01:45:43 +02:00
Daniel Hutzel
e1c6118cb4 Merge pull request #114 from SAP-samples/fixed-messaging-test
Fixed messaging test
2020-07-17 01:05:14 +02:00
Daniel
a3e4865d97 Fixed messaging test 2020-07-17 01:03:30 +02:00
Daniel Hutzel
641df50422 Merge pull request #111 from SAP-samples/Recommending-VSCode-extension-for-CDS
Recommending vscode extension for CDS
2020-07-09 10:40:30 +02:00
Daniel
0f026ed56c It's finally out... 2020-07-09 10:37:35 +02:00
Daniel Hutzel
57a3c5f178 Merge pull request #110 from SAP-samples/simplified-debugging
Simplified debugging
2020-07-07 17:31:34 +02:00
Daniel
522ec8e071 Simplified debugging 2020-07-07 17:29:30 +02:00
Christian Georgi
7b1c3d8b3a More UI annotations
Make sure all entities can be properly displayed in Fiori preview.
Especially UI.LineItems seems to be mandatory in newer Fiori versions.
2020-07-02 19:35:04 +02:00
Christian Georgi
5ba69b5021 Update issue templates 2020-06-30 09:05:27 +02:00
Christian Georgi
fd796b54ef Redirect to community 2020-06-25 09:25:11 +02:00
ianquigley-sap
660344b623 Delete .npmrc
The @sap scoped registry should now point to registry.npmjs.org rather than npm.sap.com.  Please see https://jam4.sapjam.com/feed/item/eOtNGT1RSxVzknBtpSy2My for full details on the migration.  npm.sap.com will not be updated going forward for the CDS library
2020-06-24 12:07:40 +02:00
Daniel Hutzel
d20c29a758 Merge pull request #101 from SAP-samples/mocha-parallel
Running all mocha tests in parallel
2020-06-19 11:40:20 +02:00
Daniel
604cc0514c Running all mocha tests in parallel 2020-06-19 09:51:37 +02:00
I051442
ff351455dd Update for sap.fe usage starting with UI5 1.78 2020-06-18 17:19:02 +02:00
Daniel Hutzel
8f74bd32a9 Merge pull request #91 from SAP-samples/fix-for-mocha
Stick to standard .skip
2020-06-02 11:02:01 +02:00
Daniel
c181afe8c6 Stick to standard .skip 2020-06-02 10:57:57 +02:00
Daniel Hutzel
9ade3e6b6a Merge pull request #90 from SAP-samples/reviewed
Separated reviewed and reviews
2020-05-29 16:58:37 +02:00
Daniel
6f9737ae38 Separated reviewed and reviews 2020-05-29 16:56:03 +02:00
96 changed files with 1352 additions and 1021 deletions

1
.env
View File

@@ -1 +0,0 @@
cds.features.snapi = y

View File

@@ -0,0 +1,10 @@
---
name: This channel is CLOSED.
about: Use our community at https://answers.sap.com/tags/9f13aee1-834c-4105-8e43-ee442775e5ce
title: ''
labels: ''
assignees: ''
---
Please use our community on https://answers.sap.com/tags/9f13aee1-834c-4105-8e43-ee442775e5ce

View File

@@ -16,7 +16,7 @@ jobs:
strategy: strategy:
matrix: matrix:
node-version: [10.x, 12.16.2] node-version: [10.x, 12.x, 14.x]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2

1
.npmrc
View File

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

71
.registry/server.js Normal file
View File

@@ -0,0 +1,71 @@
const { exec } = require ('child_process')
const express = require ('express')
const fs = require ('fs')
const app = express()
const { PORT=4444 } = process.env
const [,,port=PORT] = process.argv
app.use('/-/:tarball', (req,res,next) => {
const url = decodeURIComponent(req.url)
console.debug ('GET', req.params)
try {
const { tarball } = req.params
const [, pkg ] = /^capire-(\w+)/.exec(tarball)
fs.lstat(tarball,(err => {
if (err) exec(`npm pack ../${pkg}`,next)
else next()
}))
} catch (e) {
console.error(e)
res.sendStatus(500)
}
})
app.use('/-', express.static(__dirname))
app.get('/*', (req,res)=>{
const url = decodeURIComponent(req.url)
console.debug ('GET',url)
try {
const [, capire, pkg ] = /^\/(@capire)\/(\w+)/.exec(url)
const package = require (`${capire}/${pkg}/package.json`)
const tarball = `capire-${pkg}-${package.version}.tgz`
// https://github.com/npm/registry/blob/master/docs/responses/package-metadata.md
res.json({
"name": package.name,
"dist-tags": {
"latest": package.version
},
"versions": {
[package.version]: {
"name": package.name,
"version": package.version,
"dist": {
"tarball": `http://localhost:${port}/-/${tarball}`
},
}
},
})
} catch (e) {
console.error(e)
res.sendStatus(404)
}
})
app.listen(port, ()=>{
console.log (`npm set @capire:registry=http://localhost:${port}`)
console.log (`@capire registry listening on http://localhost:${port}`)
exec(`npm set @capire:registry=http://localhost:${port}`)
})
const _exit = ()=>{
console.log ('\nnpm conf rm @capire:registry')
exec('npm conf rm @capire:registry')
exec('rm *.tgz')
process.exit()
}
process.on ('SIGTERM',_exit)
process.on ('SIGHUP',_exit)
process.on ('SIGINT',_exit)
process.on ('SIGUSR2',_exit)

29
.reuse/dep5 Normal file
View File

@@ -0,0 +1,29 @@
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: cloud-cap-samples
Upstream-Contact: <Christian Georgi (christian.georgi@sap.com)>
Source: https://github.com/SAP-samples/cloud-cap-samples
Disclaimer: The code in this project may include calls to APIs (“API Calls”) of
SAP or third-party products or services developed outside of this project
(“External Products”).
“APIs” means application programming interfaces, as well as their respective
specifications and implementing code that allows software to communicate with
other software.
API Calls to External Products are not licensed under the open source license
that governs this project. The use of such API Calls and related External
Products are subject to applicable additional agreements with the relevant
provider of the External Products. In no event shall the open source license
that governs this project grant any rights in or to any External Products,or
alter, expand or supersede any terms of the applicable additional agreements.
If you have a valid license agreement with SAP for the use of a particular SAP
External Product, then you may make use of any API Calls included in this
projects code for that SAP External Product, subject to the terms of such
license agreement. If you do not have a valid license agreement for the use of
a particular SAP External Product, then you may only make use of any API Calls
in this project for that SAP External Product for your internal, non-productive
and non-commercial test and evaluation of such API Calls. Nothing herein grants
you any rights to use or access any SAP External Product, or provide any third
parties the right to use of access any SAP External Product, through API Calls.
Files: *
Copyright: 2019-2020 SAP SE or an SAP affiliate company and cap-cloud-samples
License: Apache-2.0

View File

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

27
.vscode/launch.json vendored
View File

@@ -4,26 +4,27 @@
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0", "version": "0.2.0",
"configurations": [ "configurations": [
{
"name": "Attach by Process ID",
"processId": "${command:PickProcess}",
"request": "attach",
"skipFiles": [
"<node_internals>/**"
],
"type": "pwa-node"
},
{ {
"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>/**"]
} }
], ],

335
LICENSE
View File

@@ -1,217 +1,208 @@
Apache License Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION Version 2.0, January 2004
http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION,
AND DISTRIBUTION
1. Definitions. 1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all "License" shall mean the terms and conditions for use, reproduction, and distribution
other entities that control, are controlled by, or are under common as defined by Sections 1 through 9 of this document.
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical "Licensor" shall mean the copyright owner or entity authorized by the copyright
transformation or translation of a Source form, including but owner that is granting the License.
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including "Legal Entity" shall mean the union of the acting entity and all other entities
the original version of the Work and any modifications or additions that control, are controlled by, or are under common control with that entity.
to that Work or Derivative Works thereof, that is intentionally For the purposes of this definition, "control" means (i) the power, direct
submitted to Licensor for inclusion in the Work by the copyright owner or indirect, to cause the direction or management of such entity, whether
or by an individual or Legal Entity authorized to submit on behalf of by contract or otherwise, or (ii) ownership of fifty percent (50%) or more
the copyright owner. For the purposes of this definition, "submitted" of the outstanding shares, or (iii) beneficial ownership of such entity.
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of "You" (or "Your") shall mean an individual or Legal Entity exercising permissions
this License, each Contributor hereby grants to You a perpetual, granted by this License.
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices "Source" form shall mean the preferred form for making modifications, including
stating that You changed the files; and but not limited to software source code, documentation source, and configuration
files.
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must "Object" form shall mean any form resulting from mechanical transformation
include a readable copy of the attribution notices contained or translation of a Source form, including but not limited to compiled object
within such NOTICE file, excluding those notices that do not code, generated documentation, and conversions to other media types.
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or, "Work" shall mean the work of authorship, whether in Source or Object form,
within a display generated by the Derivative Works, if and made available under the License, as indicated by a copyright notice that
wherever such third-party notices normally appear. The contents is included in or attached to the work (an example is provided in the Appendix
of the NOTICE file are for informational purposes only and below).
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed "Derivative Works" shall mean any work, whether in Source or Object form,
that is based on (or derived from) the Work and for which the editorial revisions,
annotations, elaborations, or other modifications represent, as a whole, an
original work of authorship. For the purposes of this License, Derivative
Works shall not include works that remain separable from, or merely link (or
bind by name) to the interfaces of, the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including the original version
of the Work and any modifications or additions to that Work or Derivative
Works thereof, that is intentionally submitted to Licensor for inclusion in
the Work by the copyright owner or by an individual or Legal Entity authorized
to submit on behalf of the copyright owner. For the purposes of this definition,
"submitted" means any form of electronic, verbal, or written communication
sent to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems, and
issue tracking systems that are managed by, or on behalf of, the Licensor
for the purpose of discussing and improving the Work, but excluding communication
that is conspicuously marked or otherwise designated in writing by the copyright
owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf
of whom a Contribution has been received by Licensor and subsequently incorporated
within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of this
License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive,
no-charge, royalty-free, irrevocable copyright license to reproduce, prepare
Derivative Works of, publicly display, publicly perform, sublicense, and distribute
the Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of this License,
each Contributor hereby grants to You a perpetual, worldwide, non-exclusive,
no-charge, royalty-free, irrevocable (except as stated in this section) patent
license to make, have made, use, offer to sell, sell, import, and otherwise
transfer the Work, where such license applies only to those patent claims
licensable by such Contributor that are necessarily infringed by their Contribution(s)
alone or by combination of their Contribution(s) with the Work to which such
Contribution(s) was submitted. If You institute patent litigation against
any entity (including a cross-claim or counterclaim in a lawsuit) alleging
that the Work or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses granted to You
under this License for that Work shall terminate as of the date such litigation
is filed.
4. Redistribution. You may reproduce and distribute copies of the Work or
Derivative Works thereof in any medium, with or without modifications, and
in Source or Object form, provided that You meet the following conditions:
(a) You must give any other recipients of the Work or Derivative Works a copy
of this License; and
(b) You must cause any modified files to carry prominent notices stating that
You changed the files; and
(c) You must retain, in the Source form of any Derivative Works that You distribute,
all copyright, patent, trademark, and attribution notices from the Source
form of the Work, excluding those notices that do not pertain to any part
of the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its distribution,
then any Derivative Works that You distribute must include a readable copy
of the attribution notices contained within such NOTICE file, excluding those
notices that do not pertain to any part of the Derivative Works, in at least
one of the following places: within a NOTICE text file distributed as part
of the Derivative Works; within the Source form or documentation, if provided
along with the Derivative Works; or, within a display generated by the Derivative
Works, if and wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and do not modify the
License. You may add Your own attribution notices within Derivative Works
that You distribute, alongside or as an addendum to the NOTICE text from the
Work, provided that such additional attribution notices cannot be construed
as modifying the License. as modifying the License.
You may add Your own copyright statement to Your modifications and You may add Your own copyright statement to Your modifications and may provide
may provide additional or different license terms and conditions additional or different license terms and conditions for use, reproduction,
for use, reproduction, or distribution of Your modifications, or or distribution of Your modifications, or for any such Derivative Works as
for any such Derivative Works as a whole, provided Your use, a whole, provided Your use, reproduction, and distribution of the Work otherwise
reproduction, and distribution of the Work otherwise complies with complies with the conditions stated in this License.
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise, 5. Submission of Contributions. Unless You explicitly state otherwise, any
any Contribution intentionally submitted for inclusion in the Work Contribution intentionally submitted for inclusion in the Work by You to the
by You to the Licensor shall be under the terms and conditions of Licensor shall be under the terms and conditions of this License, without
this License, without any additional terms or conditions. any additional terms or conditions. Notwithstanding the above, nothing herein
Notwithstanding the above, nothing herein shall supersede or modify shall supersede or modify the terms of any separate license agreement you
the terms of any separate license agreement you may have executed may have executed with Licensor regarding such Contributions.
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade 6. Trademarks. This License does not grant permission to use the trade names,
names, trademarks, service marks, or product names of the Licensor, trademarks, service marks, or product names of the Licensor, except as required
except as required for reasonable and customary use in describing the for reasonable and customary use in describing the origin of the Work and
origin of the Work and reproducing the content of the NOTICE file. reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or 7. Disclaimer of Warranty. Unless required by applicable law or agreed to
agreed to in writing, Licensor provides the Work (and each in writing, Licensor provides the Work (and each Contributor provides its
Contributor provides its Contributions) on an "AS IS" BASIS, Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or KIND, either express or implied, including, without limitation, any warranties
implied, including, without limitation, any warranties or conditions or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness
PARTICULAR PURPOSE. You are solely responsible for determining the of using or redistributing the Work and assume any risks associated with Your
appropriateness of using or redistributing the Work and assume any exercise of permissions under this License.
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory, 8. Limitation of Liability. In no event and under no legal theory, whether
whether in tort (including negligence), contract, or otherwise, in tort (including negligence), contract, or otherwise, unless required by
unless required by applicable law (such as deliberate and grossly applicable law (such as deliberate and grossly negligent acts) or agreed to
negligent acts) or agreed to in writing, shall any Contributor be in writing, shall any Contributor be liable to You for damages, including
liable to You for damages, including any direct, indirect, special, any direct, indirect, special, incidental, or consequential damages of any
incidental, or consequential damages of any character arising as a character arising as a result of this License or out of the use or inability
result of this License or out of the use or inability to use the to use the Work (including but not limited to damages for loss of goodwill,
Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial
work stoppage, computer failure or malfunction, or any and all damages or losses), even if such Contributor has been advised of the possibility
other commercial damages or losses), even if such Contributor of such damages.
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing 9. Accepting Warranty or Additional Liability. While redistributing the Work
the Work or Derivative Works thereof, You may choose to offer, or Derivative Works thereof, You may choose to offer, and charge a fee for,
and charge a fee for, acceptance of support, warranty, indemnity, acceptance of support, warranty, indemnity, or other liability obligations
or other liability obligations and/or rights consistent with this and/or rights consistent with this License. However, in accepting such obligations,
License. However, in accepting such obligations, You may act only You may act only on Your own behalf and on Your sole responsibility, not on
on Your own behalf and on Your sole responsibility, not on behalf behalf of any other Contributor, and only if You agree to indemnify, defend,
of any other Contributor, and only if You agree to indemnify, and hold each Contributor harmless for any liability incurred by, or claims
defend, and hold each Contributor harmless for any liability asserted against, such Contributor by reason of your accepting any such warranty
incurred by, or claims asserted against, such Contributor by reason or additional liability. END OF TERMS AND CONDITIONS
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work. APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following To apply the Apache License to your work, attach the following boilerplate
boilerplate notice, with the fields enclosed by brackets "[]" notice, with the fields enclosed by brackets "[]" replaced with your own identifying
replaced with your own identifying information. (Don't include information. (Don't include the brackets!) The text should be enclosed in
the brackets!) The text should be enclosed in the appropriate the appropriate comment syntax for the file format. We also recommend that
comment syntax for the file format. We also recommend that a a file or class name and description of purpose be included on the same "printed
file or class name and description of purpose be included on the page" as the copyright notice for easier identification within third-party
same "printed page" as the copyright notice for easier archives.
identification within third-party archives.
Copyright [yyyy] [name of copyright owner] Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
You may obtain a copy of the License at You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0 http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
--------------------------------------------------------------------------------------------------------------------------
APIs
This project may include APIs to SAP or third-party products or services.
The use of these APIs, products and services may be subject to additional
agreements. In no event shall the application of the Apache Software
License, v.2 to this project grant any rights in or to these APIs,
products or services that would alter, expand, be inconsistent with,
or supersede any terms of these additional agreements. “API” means
application programming interfaces, as well as their respective
specifications and implementing code that allows other software
products to communicate with or call on SAP or third party products
or services (for example, SAP Enterprise Services, BAPIs, Idocs, RFCs
and ABAP calls or other user exits) and may be made available through
SAP or third party products, SDKs, documentation or other media.

View File

@@ -3,6 +3,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). &rarr; See [**Overview** of contained samples](samples.md) 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). &rarr; See [**Overview** of contained samples](samples.md)
![](https://github.com/SAP-samples/cloud-cap-samples/workflows/CI/badge.svg) ![](https://github.com/SAP-samples/cloud-cap-samples/workflows/CI/badge.svg)
[![REUSE status](https://api.reuse.software/badge/github.com/SAP-samples/cloud-cap-samples)](https://api.reuse.software/info/github.com/SAP-samples/cloud-cap-samples)
### Preliminaries ### Preliminaries
@@ -47,12 +48,28 @@ 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.
### Serve `npm`
We've simple npm registry mock included which allows you to do an `npm install @capire/<package>` anywhere locally. Use it as follows:
1. Start the @capire registry:
```sh
npm run registry
```
> While running this will have `@capire:registry=http://localhost:4444` set with npmrc.
2. Install one of the @capire packages wherever you like, e.g.:
```sh
npm add @capire/common @capire/bookshop
```
## 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
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. 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](LICENSES/Apache-2.0.txt) file.

View File

@@ -1 +0,0 @@
cds.features.snapi = y

View File

@@ -9,33 +9,36 @@ const books = new Vue ({
data: { data: {
list: [], list: [],
book: { descr:'( click on a row to see details... )' }, book: undefined,
order: { amount:1, succeeded:'', failed:'' } order: { amount:1, succeeded:'', failed:'' }
}, },
methods: { methods: {
search: ({target:{value:v}}) => books.fetch (v && '$search='+v), search: ({target:{value:v}}) => books.fetch(v && '&$search='+v),
async fetch (_filter='') { async fetch (etc='') {
const columns = 'ID,title,author,price,stock', details = 'genre,currency' const {data} = await GET(`/ListOfBooks?$expand=genre,currency${etc}`)
const {data} = await GET(`/Books?$select=${columns}&$expand=${details}&${_filter}`)
books.list = data.value books.list = data.value
}, },
async inspect () { async inspect (eve) {
const book = books.book = books.list [event.currentTarget.rowIndex-1] const book = books.book = books.list [eve.currentTarget.rowIndex-1]
book.descr || await GET(`/Books/${book.ID}/descr/$value`) .then (({data}) => book.descr = data) const res = await GET(`/Books/${book.ID}?$select=descr,stock,image`)
Object.assign (book, res.data)
books.order = { amount:1 } books.order = { amount:1 }
setTimeout (()=> $('form > input').focus(), 111) setTimeout (()=> $('form > input').focus(), 111)
}, },
submitOrder () { event.preventDefault() async submitOrder () {
const {book,order} = books, amount = parseInt (order.amount) || 1 const {book,order} = books, amount = parseInt (order.amount) || 1 // REVISIT: Okra should be less strict
POST(`/submitOrder`, { amount, book: book.ID }) try {
.then (()=> books.order = { amount, succeeded: `Successfully orderd ${amount} item(s).` }) const res = await POST(`/submitOrder`, { amount, book: book.ID })
.catch (e=> books.order = { amount, failed: e.response.data.error.message }) book.stock = res.data.stock
GET(`/Books/${book.ID}/stock/$value`).then (res => book.stock = res.data) books.order = { amount, succeeded: `Successfully orderd ${amount} item(s).` }
} catch (e) {
books.order = { amount, failed: e.response.data.error.message }
}
} }
} }

View File

@@ -7,50 +7,56 @@
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue"></script> <script src="https://cdn.jsdelivr.net/npm/vue"></script>
<style> <style>
#books tr:hover { background: #f2f2f2; cursor: pointer; } .hovering tr:hover td { color:cyan; background: #123; cursor: pointer; }
form { float:right; display:flex; flex-direction: row-reverse } .rating-stars { color:teal }
form #amount { width: 5em } .succeeded { color:teal }
.is-success { color: #0d920d } .failed { color:red }
.has-error { color: #df1010 }
</style> </style>
</head> </head>
<body class="small-container", style="margin-top: 70px;"> <body class="small-container", style="margin-top: 70px;">
<div id='app'> <div id='app'>
<h1> Capire Books </h1> <h1> {{ document.title }} </h1>
<input type="text" placeholder="Search..." @input="search"> <input type="text" placeholder="Search..." @input="search">
<table id='books'> <table id='books' class="hovering">
<thead> <thead>
<th> Book </th> <th> Book </th>
<th> Author </th> <th> Author </th>
<th> Genre </th> <th> Genre </th>
<th> Rating </th>
<th> Price </th> <th> Price </th>
</thead> </thead>
<tr v-for="book in list" v-bind:id="book.ID" v-on:click="inspect"> <tr v-for="book in list" v-bind:id="book.ID" v-on:click="inspect">
<td>{{ book.title }}</td> <td>{{ book.title }}</td>
<td>{{ book.author }}</td> <td>{{ book.author }}</td>
<td>{{ book.genre.name }}</td> <td>{{ book.genre.name }}</td>
<td class="rating-stars">
{{ ('★'.repeat(Math.round(book.rating))+'☆☆☆☆☆').slice(0,5) }}
</td>
<td>{{ book.currency.symbol }} {{ book.price }}</td> <td>{{ book.currency.symbol }} {{ book.price }}</td>
</tr> </tr>
</table> </table>
<div v-if="book.title"> <div v-if="book">
<img v-bind:src="book.image" alt=""/>
<label style="text-align:right"> <label style="text-align:right">
<span class="is-success"> {{ order.succeeded }} </span> <span class="succeeded"> {{ order.succeeded }} </span>
<span class="has-error"> {{ order.failed }} </span> <span class="failed"> {{ order.failed }} </span>
&nbsp;&nbsp; {{ book.stock }} in stock &nbsp;&nbsp; {{ book.stock }} in stock
</label> </label>
<form @submit="submitOrder"> <form @submit.prevent="submitOrder" style="float:right; display:flex; flex-direction:row-reverse">
<input type="number" id="amount" v-model="order.amount" v-bind:class="{ 'has-error': order.failed }"> <input type="number" v-model="order.amount" v-bind:class="{ failed: order.failed }" style="width:5em">
<input type="submit" value="Order:" class="muted-button"> <input type="submit" value="Order:" class="muted-button">
</form> </form>
</div>
<h4> {{ book.title }} </h4> <h4> {{ book.title }} </h4>
<p> {{ book.descr }} </p> <p> {{ book.descr }} </p>
</div>
<div v-else>
( click on a row to see details... )
</div>
</div> </div>
</body> </body>

View File

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

View File

@@ -8,8 +8,9 @@ entity Books : managed {
author : Association to Authors; author : Association to Authors;
genre : Association to Genres; genre : Association to Genres;
stock : Integer; stock : Integer;
price : Decimal(9,2); price : Decimal;
currency : Currency; currency : Currency;
image : LargeBinary @Core.MediaType : 'image/png';
} }
entity Authors : managed { entity Authors : managed {

View File

@@ -3,9 +3,10 @@
"version": "1.0.0", "version": "1.0.0",
"description": "A simple self-contained bookshop service.", "description": "A simple self-contained bookshop service.",
"dependencies": { "dependencies": {
"@capire/common": "../common", "@capire/common": "*",
"@sap/cds": ">=3.33.1", "@sap/cds": "^4",
"express": "^4.17.1" "express": "^4.17.1",
"passport": "0.4.1"
}, },
"scripts": { "scripts": {
"genres": "cds serve test/genres.cds", "genres": "cds serve test/genres.cds",

0
bookshop/sqlite.db Normal file
View File

View File

@@ -1,5 +1,5 @@
using { sap.capire.bookshop as my } from '../db/schema'; using { sap.capire.bookshop as my } from '../db/schema';
service AdminService @(requires_:'admin') { service AdminService @(requires:'admin') {
entity Books as projection on my.Books; entity Books as projection on my.Books;
entity Authors as projection on my.Authors; entity Authors as projection on my.Authors;
} }

View File

@@ -0,0 +1,12 @@
const cds = require('@sap/cds')
module.exports = cds.service.impl (function(){
this.before ('NEW','Authors', genid)
this.before ('NEW','Books', genid)
})
/** Generate primary keys for target entity in request */
async function genid (req) {
const {ID} = await cds.tx(req).run (SELECT.one.from(req.target).columns('max(ID) as ID'))
req.data.ID = ID - ID % 100 + 100 + 1
}

View File

@@ -5,6 +5,10 @@ service CatalogService @(path:'/browse') {
author.name as author author.name as author
} excluding { createdBy, modifiedBy }; } excluding { createdBy, modifiedBy };
@requires_: 'authenticated-user' @readonly entity ListOfBooks as SELECT from Books
action submitOrder (book : Books.ID, amount: Integer); excluding { descr, stock };
@requires: 'authenticated-user'
action submitOrder ( book: Books:ID, amount: Integer ) returns { stock: Integer };
event OrderedBook : { book: Books:ID; amount: Integer; buyer: String };
} }

View File

@@ -1,20 +1,28 @@
const cds = require('@sap/cds') const cds = require('@sap/cds')
module.exports = async function (){ const { Books } = cds.entities ('sap.capire.bookshop')
const db = await cds.connect.to('db') // connect to database service class CatalogService extends cds.ApplicationService { async init(){
const { Books } = db.entities // get reflected definitions
// Reduce stock of ordered books if available stock suffices // Reduce stock of ordered books if available stock suffices
this.on ('submitOrder', async req => { this.on ('submitOrder', async req => {
const {book,amount} = req.data const {book,amount} = req.data, tx = cds.tx(req)
const n = await UPDATE (Books, book) let {stock} = await tx.read('stock').from(Books,book)
.with ({ stock: {'-=': amount }}) if (stock >= amount) {
.where ({ stock: {'>=': amount }}) await tx.update (Books,book).with ({ stock: stock -= amount })
n > 0 || req.error (409,`${amount} exceeds stock for book #${book}`) this.emit ('OrderedBook', { book, amount, buyer:req.user.id })
return { stock }
}
else return req.error (409,`${amount} exceeds stock for book #${book}`)
}) })
// Add some discount for overstocked books // Add some discount for overstocked books
this.after ('READ','Books', each => { this.after ('READ','Books', each => {
if (each.stock > 111) each.title += ` -- 11% discount!` if (each.stock > 111) {
}) each.title += ` -- 11% discount!`
} }
})
return super.init()
}}
module.exports = { CatalogService }

View File

@@ -32,6 +32,37 @@ GET {{server}}/admin/Authors?
# &sap-language=de # &sap-language=de
Authorization: Basic alice: Authorization: Basic alice:
### ------------------------------------------------------------------------
# Create book
POST {{server}}/admin/Books
Content-Type: application/json;IEEE754Compatible=true
Authorization: Basic alice:
{
"ID": 2,
"title": "Poems : Pocket Poets",
"descr": "The Everyman's Library Pocket Poets hardcover series is popular for its compact size and reasonable price which does not compromise content. Poems: Bronte contains poems that demonstrate a sensibility elemental in its force with an imaginative discipline and flexibility of the highest order. Also included are an Editor's Note and an index of first lines.",
"author": { "ID": 101 },
"genre": { "ID": 12 },
"stock": 5,
"price": "12.05",
"currency": { "code": "USD" }
}
### ------------------------------------------------------------------------
# Put image to books
PUT {{server}}/admin/Books(2)/image
Content-Type: image/png
Authorization: Basic alice:
data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAANwAAADcCAYAAAAbWs+BAAAGwElEQVR4Ae3cwZFbNxBFUY5rkrDTmKAUk5QT03Aa44U22KC7NHptw+DRikVAXf8fzC3u8Hj4R4AAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgZzAW26USQT+e4HPx+Mz+RRvj0e0kT+SD2cWAQK1gOBqH6sEogKCi3IaRqAWEFztY5VAVEBwUU7DCNQCgqt9rBKICgguymkYgVpAcLWPVQJRAcFFOQ0jUAsIrvaxSiAqILgop2EEagHB1T5WCUQFBBflNIxALSC42scqgaiA4KKchhGoBQRX+1glEBUQXJTTMAK1gOBqH6sEogKCi3IaRqAWeK+Xb1z9iN558fHxcSPS9p2ezx/ROz4e4TtIHt+3j/61hW9f+2+7/+UXbifjewIDAoIbQDWSwE5AcDsZ3xMYEBDcAKqRBHYCgtvJ+J7AgIDgBlCNJLATENxOxvcEBgQEN4BqJIGdgOB2Mr4nMCAguAFUIwnsBAS3k/E9gQEBwQ2gGklgJyC4nYzvCQwICG4A1UgCOwHB7WR8T2BAQHADqEYS2AkIbifjewIDAoIbQDWSwE5AcDsZ3xMYEEjfTzHwiK91B8npd6Q8n8/oGQ/ckRJ9vvQwv3BpUfMIFAKCK3AsEUgLCC4tah6BQkBwBY4lAmkBwaVFzSNQCAiuwLFEIC0guLSoeQQKAcEVOJYIpAUElxY1j0AhILgCxxKBtIDg0qLmESgEBFfgWCKQFhBcWtQ8AoWA4AocSwTSAoJLi5pHoBAQXIFjiUBaQHBpUfMIFAKCK3AsEUgLCC4tah6BQmDgTpPsHSTFs39p6fQ7Q770UsV/Ov19X+2OFL9wxR+rJQJpAcGlRc0jUAgIrsCxRCAtILi0qHkECgHBFTiWCKQFBJcWNY9AISC4AscSgbSA4NKi5hEoBARX4FgikBYQXFrUPAKFgOAKHEsE0gKCS4uaR6AQEFyBY4lAWkBwaVHzCBQCgitwLBFICwguLWoegUJAcAWOJQJpAcGlRc0jUAgIrsCxRCAt8J4eePq89B0ar3ZnyOnve/rfn1+400/I810lILirjtPLnC4guNNPyPNdJSC4q47Ty5wuILjTT8jzXSUguKuO08ucLiC400/I810lILirjtPLnC4guNNPyPNdJSC4q47Ty5wuILjTT8jzXSUguKuO08ucLiC400/I810lILirjtPLnC4guNNPyPNdJSC4q47Ty5wuILjTT8jzXSUguKuO08ucLiC400/I810l8JZ/m78+szP/zI47fJo7Q37vgJ7PHwN/07/3TOv/9gu3avhMYFhAcMPAxhNYBQS3avhMYFhAcMPAxhNYBQS3avhMYFhAcMPAxhNYBQS3avhMYFhAcMPAxhNYBQS3avhMYFhAcMPAxhNYBQS3avhMYFhAcMPAxhNYBQS3avhMYFhAcMPAxhNYBQS3avhMYFhAcMPAxhNYBQS3avhMYFhAcMPAxhNYBQS3avhMYFhg4P6H9J0maYHXuiMlrXf+vOfA33Turf3C5SxNItAKCK4lsoFATkBwOUuTCLQCgmuJbCCQExBcztIkAq2A4FoiGwjkBASXszSJQCsguJbIBgI5AcHlLE0i0AoIriWygUBOQHA5S5MItAKCa4lsIJATEFzO0iQCrYDgWiIbCOQEBJezNIlAKyC4lsgGAjkBweUsTSLQCgiuJbKBQE5AcDlLkwi0Akff//Dz6U+/I6U1/sUNr3bnytl3kPzi4bXb/cK1RDYQyAkILmdpEoFWQHAtkQ0EcgKCy1maRKAVEFxLZAOBnIDgcpYmEWgFBNcS2UAgJyC4nKVJBFoBwbVENhDICQguZ2kSgVZAcC2RDQRyAoLLWZpEoBUQXEtkA4GcgOByliYRaAUE1xLZQCAnILicpUkEWgHBtUQ2EMgJCC5naRKBVkBwLZENBHIC/4M7TXIv+3PS22d24qvdQfL3C/7N5P5i/MLlLE0i0AoIriWygUBOQHA5S5MItAKCa4lsIJATEFzO0iQCrYDgWiIbCOQEBJezNIlAKyC4lsgGAjkBweUsTSLQCgiuJbKBQE5AcDlLkwi0AoJriWwgkBMQXM7SJAKtgOBaIhsI5AQEl7M0iUArILiWyAYCOQHB5SxNItAKCK4lsoFATkBwOUuTCBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIDAvyrwDySEJ2VQgUSoAAAAAElFTkSuQmCC
### ------------------------------------------------------------------------
# Reading image from from the server directly
GET {{server}}/browse/Books(2)/image
### ------------------------------------------------------------------------ ### ------------------------------------------------------------------------
# Submit Order as authenticated user # Submit Order as authenticated user

View File

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

View File

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

30
ext/.gitignore vendored
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1 +1,2 @@
cds.features.snapi = y # cds.requires.messaging.kind = file-based-messaging
PORT = 4004

View File

@@ -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

View File

@@ -28,7 +28,7 @@
navigationMode: "embedded" navigationMode: "embedded"
}, },
"manage-orders": { "manage-orders": {
title: "Order Books", title: "Manage Orders",
description: "... testing FE v42", description: "... testing FE v42",
additionalInformation: "SAPUI5.Component=orders", additionalInformation: "SAPUI5.Component=orders",
applicationType : "URL", applicationType : "URL",
@@ -39,8 +39,9 @@
}; };
</script> </script>
<script src="https://sapui5.hana.ondemand.com/test-resources/sap/ushell/bootstrap/sandbox.js"></script> <script id="sap-ushell-bootstrap" 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 id="sap-ui-bootstrap" src="https://sapui5.hana.ondemand.com/resources/sap-ui-core.js" -->
<script id="sap-ui-bootstrap" src="https://sapui5.hana.ondemand.com/1.78.6/resources/sap-ui-core.js"
data-sap-ui-libs="sap.m, sap.ushell, sap.collaboration, sap.ui.layout" data-sap-ui-libs="sap.m, sap.ushell, sap.collaboration, sap.ui.layout"
data-sap-ui-compatVersion="edge" data-sap-ui-compatVersion="edge"
data-sap-ui-theme="sap_fiori_3" data-sap-ui-theme="sap_fiori_3"

View File

@@ -1,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 */

View File

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

3
fiori/app/bookshop.html Normal file
View File

@@ -0,0 +1,3 @@
<head>
<meta http-equiv="refresh" content="0;url=vue/bookshop/index.html">
</head>

View File

@@ -7,6 +7,8 @@ using CatalogService from '@capire/bookshop';
annotate CatalogService.Books with @( annotate CatalogService.Books with @(
UI: { UI: {
HeaderInfo: { HeaderInfo: {
TypeName: 'Book',
TypeNamePlural: 'Books',
Description: {Value: author} Description: {Value: author}
}, },
HeaderFacets: [ HeaderFacets: [

View File

@@ -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 */

View File

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

View File

@@ -3,7 +3,7 @@
*/ */
using { sap.capire.bookshop as my } from '@capire/bookshop'; using { sap.capire.bookshop as my } from '@capire/bookshop';
using { sap.common } from '@capire/common';
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
// //
@@ -28,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}';
} }

View File

@@ -4,7 +4,9 @@
using from './admin/fiori-service'; using from './admin/fiori-service';
using from './browse/fiori-service'; using from './browse/fiori-service';
using from './orders/fiori-service';
using from './common'; using from './common';
using from '@capire/common'; using from '@capire/common';
// only works in case of embedded orders service
using from '@capire/orders/app/orders/fiori-service';

View File

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

3
fiori/app/reviews.html Normal file
View File

@@ -0,0 +1,3 @@
<head>
<meta http-equiv="refresh" content="0;url=vue/reviews/index.html">
</head>

View File

@@ -1,3 +0,0 @@
// Proxy for importing schema from bookshop sample
using from '@capire/bookshop';
namespace sap.capire.bookshop;

View File

@@ -2,11 +2,13 @@
"name": "@capire/fiori", "name": "@capire/fiori",
"version": "1.0.0", "version": "1.0.0",
"dependencies": { "dependencies": {
"@capire/bookshop": "../bookshop", "@capire/bookshop": "*",
"@capire/orders": "../orders", "@capire/reviews": "*",
"@capire/common": "../common", "@capire/orders": "*",
"@sap/cds": ">=3.33.1", "@capire/common": "*",
"express": "^4.17.1" "@sap/cds": "^4",
"express": "^4.17.1",
"passport": "0.4.1"
}, },
"scripts": { "scripts": {
"start": "cds run --in-memory?", "start": "cds run --in-memory?",
@@ -14,6 +16,12 @@
}, },
"cds": { "cds": {
"requires": { "requires": {
"ReviewsService": {
"kind": "odata", "model": "@capire/reviews"
},
"OrdersService": {
"kind": "odata", "model": "@capire/orders"
},
"db": { "db": {
"kind": "sql" "kind": "sql"
} }

19
fiori/server.js Normal file
View File

@@ -0,0 +1,19 @@
const express = require ('express')
const cds = require ('@sap/cds')
cds.once('bootstrap',(app)=>{
const {dirname} = require ('path')
// serving the orders app imported from @capire/orders
const orders_app = dirname (require.resolve('@capire/orders/app/orders/webapp/manifest.json'))
app.use ('/orders/webapp', express.static(orders_app))
// serving the vue.js app imported from @capire/bookshop
const bookshop_app = dirname (require.resolve('@capire/bookshop/app/vue/index.html'))
app.use ('/vue/bookshop', express.static(bookshop_app))
// serving the vue.js app imported from @capire/reviews
const reviews_app = dirname (require.resolve('@capire/reviews/app/vue/index.html'))
app.use ('/vue/reviews', express.static(reviews_app))
})
cds.once('served', require('./srv/mashup'))
module.exports = cds.server

View File

@@ -1,3 +0,0 @@
// Proxy for importing services from bookshop sample
using from '@capire/bookshop';
annotate AdminService with @impl:'srv/admin-service.js';

View File

@@ -1,8 +0,0 @@
const cds = require('@sap/cds')
module.exports = cds.service.impl (async function() {
const {Books} = cds.entities
const {ID} = await SELECT.one.from(Books).columns('max(ID) as ID')
let newID = ID - ID % 100 + 100
this.before ('NEW','Books', req => req.data.ID = ++newID)
})

25
fiori/srv/mashup.cds Normal file
View File

@@ -0,0 +1,25 @@
////////////////////////////////////////////////////////////////////////////
//
// Mashing up imported models...
//
using { sap.capire.bookshop.Books } from '@capire/bookshop';
//
// Extend Books with access to Reviews and average ratings
//
using { ReviewsService.Reviews } from '@capire/reviews';
extend Books with {
reviews : Composition of many Reviews on reviews.subject = $self.ID;
rating : Decimal;
}
//
// Extend Orders with Books as Products
//
using { sap.capire.orders.Orders_Items } from '@capire/orders';
extend Orders_Items with {
book : Association to Books on product.ID = book.ID
}

59
fiori/srv/mashup.js Normal file
View File

@@ -0,0 +1,59 @@
////////////////////////////////////////////////////////////////////////////
//
// Mashing up provided and required services...
//
module.exports = async()=>{ // called by server.js
const cds = require('@sap/cds')
const CatalogService = await cds.connect.to ('CatalogService')
const ReviewsService = await cds.connect.to ('ReviewsService')
const OrdersService = await cds.connect.to ('OrdersService')
const db = await cds.connect.to ('db')
// reflect entity definitions used below...
const { Books } = db.entities ('sap.capire.bookshop')
//
// Delegate requests to read reviews to the ReviewsService
// Note: prepend is neccessary to intercept generic 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 ReviewsService.tx(req).read ('Reviews',columns).limit(limit).where({subject:String(id)})
}))
//
// Create an order with the OrdersService when CatalogService signals a new order
//
CatalogService.on ('OrderedBook', async (msg) => {
const { book, amount, buyer } = msg.data
const { title, price } = await db.tx(msg).read (Books, book, b => { b.title, b.price })
return OrdersService.tx(msg).create ('Orders').entries({
OrderNo: 'Order at '+ (new Date).toLocaleString(),
Items: [{ product:{ID:`${book}`}, title, price, amount }],
buyer, createdBy: buyer
})
})
//
// Update Books' average ratings when ReviewsService signals updatd reviews
//
ReviewsService.on ('reviewed', (msg) => {
console.debug ('> received:', msg.event, msg.data)
const { subject, rating } = msg.data
return UPDATE(Books,subject).with({rating})
// ^ Note: the framework will execute this and take care for db.tx
})
//
// Reduce stock of ordered books for orders are created from Orders admin UI
//
OrdersService.on ('OrderChanged', async (msg) => {
console.debug ('> received:', msg.event, msg.data)
const { product, deltaAmount } = msg.data
return UPDATE (Books) .where ('ID =', product)
.and ('stock >=', deltaAmount)
.set ('stock -=', deltaAmount)
})
}

48
fiori/test/requests.http Normal file
View File

@@ -0,0 +1,48 @@
@bookshop = http://localhost:4004
@reviews-service = {{bookshop}}/reviews
# Uncomment this when running a separate reviews service
@reviews-service = http://localhost:4005/reviews
#################################################
#
# Reviews Service
#
GET {{reviews-service}}/Reviews
###
POST {{reviews-service}}/Reviews
Authorization: Basic {{$processEnv USER}}:
Content-Type: application/json
{"subject":"201", "title":"boo", "rating":3 }
#################################################
#
# Bookshop Services
#
GET {{bookshop}}/browse/Books/201/reviews?
&$select=rating,date,title
&$top=3
###
GET {{bookshop}}/browse/Books(201)?
&$select=ID,title,rating
&$expand=reviews
#################################################
#
# Orders Service
#
GET {{bookshop}}/orders/Orders

BIN
media/test/Test.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

35
media/test/media.http Normal file
View File

@@ -0,0 +1,35 @@
### Requires REST Client for VS Code
### https://marketplace.visualstudio.com/items?itemName=humao.rest-client
###
@protocol = http
@host = localhost
@port = 4004
### Read Pictures
GET {{protocol}}://{{host}}:{{port}}/media-server/Media
Authorization: Basic admin:
### Create Picture with mediatype
POST {{protocol}}://{{host}}:{{port}}/media-server/Media
Authorization: Basic admin:
Accept: application/json
Content-Type: application/json
{
"id": 1,
"mediaType": "image/png"
}
### Upload Binary PNG
PUT {{protocol}}://{{host}}:{{port}}/media-server/Media(1)/content
Authorization: Basic admin:
Content-Type: image/png
< ./Test.png
### Read Binary
GET {{protocol}}://{{host}}:{{port}}/media-server/Media(1)/content
Authorization: Basic admin:
### Delete Image
DELETE {{protocol}}://{{host}}:{{port}}/media-server/Media(1)
Authorization: Basic admin:

2
orders/.env Normal file
View File

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

39
orders/app/fiori-app.html Normal file
View File

@@ -0,0 +1,39 @@
<!DOCTYPE html>
<html>
<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>
<script>
window["sap-ushell-config"] = {
defaultRenderer: "fiori2",
applications: {
"manage-orders": {
title: "Manage Orders",
description: "... testing FE v42",
additionalInformation: "SAPUI5.Component=orders",
applicationType : "URL",
url: "/orders/webapp",
navigationMode: "embedded"
}
}
};
</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>

5
orders/app/index.cds Normal file
View File

@@ -0,0 +1,5 @@
/*
This model controls what gets served to Fiori frontends...
*/
using from './orders/fiori-service';

View File

@@ -1,44 +1,27 @@
using OrdersService from '@capire/orders/srv/orders-service';
annotate OrdersService.Books with {
price @Common.FieldControl: #ReadOnly;
}
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
// //
// Common // Note: this is designed for the OrdersService being co-located with
// bookshop. It does not work if OrdersService is run as a separate
// process, and is not intended to do so.
// //
annotate OrdersService.OrderItems with { ////////////////////////////////////////////////////////////////////////////
book @(
Common: {
Text: book.title,
FieldControl: #Mandatory using { OrdersService } from '../../srv/orders-service';
},
ValueList.entity:'Books',
);
amount @(
Common.FieldControl: #Mandatory
);
}
@odata.draft.enabled @odata.draft.enabled
annotate OrdersService.Orders with @( annotate OrdersService.Orders with @(
UI: { UI: {
////////////////////////////////////////////////////////////////////////////
//
// Lists of Orders
//
SelectionFields: [ createdAt, createdBy ], SelectionFields: [ createdAt, createdBy ],
LineItem: [ LineItem: [
{Value: createdBy, Label:'Customer'}, {Value: OrderNo, Label:'OrderNo'},
{Value: buyer, Label:'Customer'},
{Value: createdAt, Label:'Date'} {Value: createdAt, Label:'Date'}
], ],
////////////////////////////////////////////////////////////////////////////
//
// Order Details
//
HeaderInfo: { HeaderInfo: {
TypeName: 'Order', TypeNamePlural: 'Orders', TypeName: 'Order', TypeNamePlural: 'Orders',
Title: { Title: {
@@ -62,7 +45,7 @@ annotate OrdersService.Orders with @(
], ],
FieldGroup#Details: { FieldGroup#Details: {
Data: [ Data: [
{Value: currency_code, Label:'Currency'} {Value: currency.code, Label:'Currency'}
] ]
}, },
FieldGroup#Created: { FieldGroup#Created: {
@@ -85,36 +68,25 @@ annotate OrdersService.Orders with @(
//The enity types name is OrdersService.my_bookshop_OrderItems annotate OrdersService.Orders_Items with @(
//The annotations below are not generated in edmx WHY?
annotate OrdersService.OrderItems with @(
UI: { UI: {
HeaderInfo: {
TypeName: 'Order Item', TypeNamePlural: ' ',
Title: {
Value: book.title
},
Description: {Value: book.descr}
},
// There is no filterbar for items so the selctionfileds is not needed
SelectionFields: [ book_ID ],
////////////////////////////////////////////////////////////////////////////
//
// Lists of OrderItems
//
LineItem: [ LineItem: [
{Value: book_ID, Label:'Book'}, {Value: product_ID, Label:'Product ID'},
//The following entry is only used to have the assoication followed in the read event {Value: title, Label:'Product Title'},
{Value: book.price, Label:'Book Price'}, {Value: price, Label:'Unit Price'},
{Value: amount, Label:'Quantity'}, {Value: amount, Label:'Quantity'},
], ],
Identification: [ //Is the main field group Identification: [ //Is the main field group
//{Value: ID, Label:'ID'}, //A guid shouldn't be on the UI
{Value: book_ID, Label:'Book'},
{Value: amount, Label:'Amount'}, {Value: amount, Label:'Amount'},
{Value: title, Label:'Product'},
{Value: price, Label:'Unit Price'},
], ],
Facets: [ Facets: [
{$Type: 'UI.ReferenceFacet', Label: '{i18n>OrderItems}', Target: '@UI.Identification'}, {$Type: 'UI.ReferenceFacet', Label: '{i18n>OrderItems}', Target: '@UI.Identification'},
], ],
}, },
) {
amount @(
Common.FieldControl: #Mandatory
); );
};

View File

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

View File

@@ -24,7 +24,7 @@
"sap.ui5": { "sap.ui5": {
"dependencies": { "dependencies": {
"libs": { "libs": {
"sap.fe": {} "sap.fe.templates": {}
} }
}, },
"models": { "models": {
@@ -121,7 +121,7 @@
"name": "sap.fe.templates.ObjectPage", "name": "sap.fe.templates.ObjectPage",
"options": { "options": {
"settings" : { "settings" : {
"entitySet": "OrderItems" "entitySet": "Orders_Items"
} }
} }
}, },

View File

@@ -1,4 +0,0 @@
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
1 ID amount parent_ID book_ID netAmount
2 58040e66-1dcd-4ffb-ab10-fdce32028b79 1 7e2f2640-6866-4dcf-8f4d-3027aa831cad 201 11.11
3 64e718c9-ff99-47f1-8ca3-950c850777d4 1 7e2f2640-6866-4dcf-8f4d-3027aa831cad 271 15
4 e9641166-e050-4261-bfee-d1e797e6cb7f 2 64e718c9-ff99-47f1-8ca3-950c850777d4 252 28

View File

@@ -1,3 +0,0 @@
ID;modifiedAt;createdAt;createdBy;modifiedBy;OrderNo;currency_code
7e2f2640-6866-4dcf-8f4d-3027aa831cad;;2019-01-31;john.doe@test.com;;1;EUR
64e718c9-ff99-47f1-8ca3-950c850777d4;;2019-01-30;jane.doe@test.com;;2;EUR
1 ID modifiedAt createdAt createdBy modifiedBy OrderNo currency_code
2 7e2f2640-6866-4dcf-8f4d-3027aa831cad 2019-01-31 john.doe@test.com 1 EUR
3 64e718c9-ff99-47f1-8ca3-950c850777d4 2019-01-30 jane.doe@test.com 2 EUR

View File

@@ -0,0 +1,3 @@
ID;createdAt;createdBy;buyer;OrderNo;currency_code
7e2f2640-6866-4dcf-8f4d-3027aa831cad;2019-01-31;john.doe@test.com;john.doe@test.com;1;EUR
64e718c9-ff99-47f1-8ca3-950c850777d4;2019-01-30;jane.doe@test.com;jane.doe@test.com;2;EUR
1 ID createdAt createdBy buyer OrderNo currency_code
2 7e2f2640-6866-4dcf-8f4d-3027aa831cad 2019-01-31 john.doe@test.com john.doe@test.com 1 EUR
3 64e718c9-ff99-47f1-8ca3-950c850777d4 2019-01-30 jane.doe@test.com jane.doe@test.com 2 EUR

View File

@@ -0,0 +1,4 @@
ID;up__ID;amount;product_ID;title;price
58040e66-1dcd-4ffb-ab10-fdce32028b79;7e2f2640-6866-4dcf-8f4d-3027aa831cad;1;201;Wuthering Heights;11.11
64e718c9-ff99-47f1-8ca3-950c850777d4;7e2f2640-6866-4dcf-8f4d-3027aa831cad;1;271;Catweazle;15
e9641166-e050-4261-bfee-d1e797e6cb7f;64e718c9-ff99-47f1-8ca3-950c850777d4;2;252;Eleonora;28
1 ID up__ID amount product_ID title price
2 58040e66-1dcd-4ffb-ab10-fdce32028b79 7e2f2640-6866-4dcf-8f4d-3027aa831cad 1 201 Wuthering Heights 11.11
3 64e718c9-ff99-47f1-8ca3-950c850777d4 7e2f2640-6866-4dcf-8f4d-3027aa831cad 1 271 Catweazle 15
4 e9641166-e050-4261-bfee-d1e797e6cb7f 64e718c9-ff99-47f1-8ca3-950c850777d4 2 252 Eleonora 28

View File

@@ -1,16 +1,26 @@
using { sap.capire.bookshop.Books } from '@capire/bookshop'; using { Currency, User, managed, cuid } from '@sap/cds/common';
using { Currency, managed, cuid } from '@sap/cds/common'; namespace sap.capire.orders;
namespace sap.capire.bookshop;
entity Orders : cuid, managed { entity Orders : cuid, managed {
OrderNo : String @title:'Order Number'; //> readable key OrderNo : String @title:'Order Number'; //> readable key
Items : Composition of many OrderItems on Items.parent = $self; Items : Composition of many Orders_Items on Items.up_ = $self;
buyer : User;
currency : Currency; currency : Currency;
} }
entity OrderItems : cuid { entity Orders_Items {
parent : Association to Orders; key ID : UUID;
book : Association to Books; up_ : Association to Orders;
product : Association to Products @assert.integrity:false; // REVISIT: this is a temporary workaround for a glitch in cds-runtime
amount : Integer; amount : Integer;
netAmount : Decimal(9,2); title : String;
price : Double;
} }
/** This is a stand-in for arbitrary ordered Products */
entity Products @(cds.persistence.skip:'always') {
key ID : String;
}
// Activate extension package
using from '@capire/common';

6
orders/index.cds Normal file
View File

@@ -0,0 +1,6 @@
/*
This model controls what gets exposed
*/
namespace sap.capire.orders;
using from './srv/orders-service';
using from './db/schema';

View File

@@ -1,4 +1,8 @@
{ {
"name": "@capire/orders", "name": "@capire/orders",
"version": "1.0.0" "version": "1.0.0",
"dependencies": {
"@capire/common": "*",
"@sap/cds": "^4.3.0"
}
} }

View File

@@ -1,6 +1,5 @@
using { sap.capire.bookshop as my } from '../db/schema'; using { sap.capire.orders as my } from '../db/schema';
service OrdersService { service OrdersService {
entity Orders as projection on my.Orders; entity Orders as projection on my.Orders;
entity Books as projection on my.Books;
} }

View File

@@ -1,21 +1,37 @@
const cds = require ('@sap/cds') const cds = require ('@sap/cds')
class OrdersService extends cds.ApplicationService {
module.exports = cds.service.impl(function() { /** register custom handlers */
init(){
const { Orders_Items:OrderItems } = this.entities
const { Books } = cds.entities this.before ('UPDATE', 'Orders', async function(req) {
const { ID, Items } = req.data
// Reduce stock of ordered books if available stock suffices if (Items) for (let { product_ID, amount } of Items) {
this.before ('CREATE', 'Orders', (req) => { const { amount:before } = await cds.tx(req).run (
const { Items: OrderItems } = req.data SELECT.one.from (OrderItems, oi => oi.amount) .where ({up__ID:ID, product_ID})
return cds.transaction(req) .run (()=> OrderItems.map (order =>
UPDATE (Books) .where ('ID =', order.book_ID)
.and ('stock >=', order.amount)
.set ('stock -=', order.amount)
)) .then (all => all.forEach ((affectedRows,i) => {
if (affectedRows === 0) req.error (409,
`${OrderItems[i].amount} exceeds stock for book #${OrderItems[i].book_ID}`
) )
})) if (amount != before) this.orderChanged (product_ID, amount-before)
}
}) })
this.before ('DELETE', 'Orders', async function(req) {
const { ID } = req.data
const Items = await cds.tx(req).run (
SELECT.from (OrderItems, oi => { oi.product_ID, oi.amount }) .where ({up__ID:ID})
)
if (Items) for (let it of Items) this.orderChanged (it.product_ID, -it.amount)
}) })
return super.init()
}
/** order changed -> broadcast event */
orderChanged (product, deltaAmount) {
// Emit events to inform subscribers about changes in orders
console.log ('> emitting:', 'OrderChanged', { product, deltaAmount })
return this.emit ('OrderChanged', { product, deltaAmount })
}
}
module.exports = OrdersService

View File

@@ -5,23 +5,31 @@
"repository": "https://github.com/sap-samples/cloud-cap-samples.git", "repository": "https://github.com/sap-samples/cloud-cap-samples.git",
"author": "daniel.hutzel@sap.com", "author": "daniel.hutzel@sap.com",
"dependencies": { "dependencies": {
"@capire/bookshop": "file:bookshop", "@capire/bookshop": "./bookshop",
"@capire/common": "file:common", "@capire/common": "./common",
"@capire/fiori": "file:fiori", "@capire/fiori": "./fiori",
"@capire/orders": "file:orders", "@capire/hello": "./hello",
"@capire/reviews": "file:reviews", "@capire/media": "./media",
"@capire/tests": "file:test" "@capire/orders": "./orders",
"@capire/reviews": "./reviews"
}, },
"devDependencies": { "devDependencies": {
"chai": "^4.2.0", "chai": "^4.2.0",
"chai-as-promised": "^7.1.1", "chai-as-promised": "^7.1.1",
"chai-subset": "^1.6.0", "chai-subset": "^1.6.0",
"sqlite3": "^4" "sqlite3": "^5"
}, },
"scripts": { "scripts": {
"registry": "cd .registry && node server.js",
"bookshop": "cds watch bookshop",
"fiori": "cds watch fiori",
"media": "cds watch media",
"mocha": "npx mocha || echo", "mocha": "npx mocha || echo",
"jest": "npx jest --verbose", "jest": "npx jest",
"test": "npm run jest -s" "test": "npm run jest --silent"
},
"mocha": {
"parallel": true
}, },
"jest": { "jest": {
"testEnvironment": "node" "testEnvironment": "node"

View File

@@ -1 +1,2 @@
PORT = 5005 cds.requires.messaging.kind = file-based-messaging
PORT = 4005

72
reviews/app/vue/app.js Normal file
View File

@@ -0,0 +1,72 @@
/* global Vue axios */ //> from vue.html
const $ = sel => document.querySelector(sel)
const GET = (url) => axios.get('/reviews'+url)
const PUT = (cmd,data) => axios.patch('/reviews'+cmd,data)
const POST = (cmd,data) => axios.post('/reviews'+cmd,data)
const reviews = new Vue ({
el:'#app',
data: {
list: [],
review: undefined,
message: {},
Ratings: Object.entries({
5 : '★★★★★',
4 : '★★★★',
3 : '★★★',
2 : '★★',
1 : '★',
}).reverse()
},
methods: {
search: ({target:{value:v}}) => reviews.fetch(v && '&$search='+v),
async fetch (etc='') {
const {data} = await GET(`/Reviews?${etc}`)
reviews.list = data.value
},
async inspect (eve) {
const review = reviews.review = reviews.list [eve.currentTarget.rowIndex-1]
const res = await GET(`/Reviews/${review.ID}/text/$value`)
review.text = res.data
reviews.message = {}
},
async newReview () {
reviews.review = {}
reviews.message = {}
setTimeout (()=> $('form > input').focus(), 111)
},
async submitReview () {
const review = reviews.review; review.rating = parseInt (review.rating) // REVISIT: Okra should be less strict
try {
if (!review.ID) {
const res = await POST(`/Reviews`,review)
reviews.ID = res.data.ID
} else {
console.trace()
await PUT(`/Reviews/${review.ID}`,review)
}
reviews.message = { succeeded: 'Your review was submitted successfully. Thanks.' }
} catch (e) {
reviews.message = { failed: e.response.data.error.message }
}
}
},
filters: {
stars: (r) => ('★'.repeat(Math.round(r))+'☆☆☆☆☆').slice(0,5),
datetime: (d) => d && new Date(d).toLocaleString(),
},
})
// initially fill list of my reviews
reviews.fetch()

View File

@@ -0,0 +1,62 @@
<!DOCTYPE html>
<html>
<head>
<title> Capire Reviews </title>
<link rel="stylesheet" href="https://unpkg.com/primitive-ui/dist/css/main.css">
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
<style>
.hovering tr:hover td { color:cyan; background: #123; cursor: pointer; }
.rating-stars { color:teal }
.succeeded { color:teal }
.failed { color:red }
textarea { line-height: 1.4em;}
</style>
</head>
<body class="small-container", style="margin-top: 70px;">
<div id='app'>
<h1> {{ document.title }} </h1>
<input type="text" placeholder="Search..." @input="search">
<table id='my-reviews' class="hovering">
<thead>
<th> Subject </th>
<th> Rating </th>
<th> Title </th>
<th> Date </th>
</thead>
<tr v-for="review in list" v-bind:id="review.ID" v-on:click="inspect">
<td>{{ review.subject }}</td>
<td class="rating-stars">{{ review.rating | stars }}</td>
<td>{{ review.title }}</td>
<td>{{ review.date | datetime }}</td>
</tr>
</table>
<button v-on:click="newReview()" class="round-button muted-button">Add Review...</button>
<form v-if="review" @submit.prevent="submitReview">
<input id="subject" type="text" v-model="review.subject" style="font-weight:bold; display:inline; width:20%">
<input type="text" v-model="review.title" style="font-weight:bold; display:inline; width:60%">
<select v-model="review.rating" style="font-weight:bold; display:inline; width:17%; float: right;">
<option v-for="option in Ratings" v-bind:value="option[0]"> {{ option[1] }} </option>
</select>
<textarea v-model="review.text" rows="9"></textarea>
<input type="submit" value="Submit" class="round-button muted-button">
<span class="succeeded"> {{ message.succeeded }} </span>
<span class="failed"> {{ message.failed }} </span>
</form>
<div v-else style="margin-top: 2em;">
( click on a row to see details... )
</div>
</div>
</body>
<script src="app.js"></script>
</html>

View File

@@ -0,0 +1,5 @@
subject;rating;title;text
201;5;Intriguing;Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.
201;4;Fascinating;Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Id diam maecenas ultricies mi eget mauris pharetra et. Risus at ultrices mi tempus imperdiet nulla malesuada pellentesque. Pulvinar mattis nunc sed blandit libero. Facilisis magna etiam tempor orci eu. Nec sagittis aliquam malesuada bibendum arcu. Eu consequat ac felis donec. Ultricies tristique nulla aliquet enim tortor at auctor urna nunc. Tortor posuere ac ut consequat semper viverra nam libero. Amet nisl suscipit adipiscing bibendum est ultricies integer quis auctor. Scelerisque purus semper eget duis at tellus. Elementum tempus egestas sed sed risus pretium. Arcu dictum varius duis at. Amet luctus venenatis lectus magna fringilla urna. Eget velit aliquet sagittis id consectetur purus ut faucibus. Vitae auctor eu augue ut lectus. Fermentum iaculis eu non diam phasellus vestibulum.
207;2;What is this?;Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Libero justo laoreet sit amet cursus sit amet dictum. Nunc faucibus a pellentesque sit. Dis parturient montes nascetur ridiculus mus mauris vitae ultricies. Enim nunc faucibus a pellentesque. Commodo quis imperdiet massa tincidunt nunc pulvinar sapien. Cras ornare arcu dui vivamus. Facilisi etiam dignissim diam quis enim lobortis. Et molestie ac feugiat sed. Urna neque viverra justo nec ultrices dui. Ullamcorper a lacus vestibulum sed arcu non. Volutpat ac tincidunt vitae semper quis. Dignissim sodales ut eu sem. Feugiat in fermentum posuere urna nec. At augue eget arcu dictum varius.
251;3;It's dark...;Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Suscipit tellus mauris a diam. Velit aliquet sagittis id consectetur purus ut. Viverra adipiscing at in tellus integer. Vitae elementum curabitur vitae nunc. Mattis ullamcorper velit sed ullamcorper morbi. Diam quis enim lobortis scelerisque. Auctor neque vitae tempus quam pellentesque nec nam aliquam. Semper auctor neque vitae tempus. Quis eleifend quam adipiscing vitae proin. Neque convallis a cras semper auctor neque vitae. Imperdiet massa tincidunt nunc pulvinar sapien et ligula. Sit amet consectetur adipiscing elit ut aliquam purus. Pretium quam vulputate dignissim suspendisse.
1 subject rating title text
2 201 5 Intriguing Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.
3 201 4 Fascinating Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Id diam maecenas ultricies mi eget mauris pharetra et. Risus at ultrices mi tempus imperdiet nulla malesuada pellentesque. Pulvinar mattis nunc sed blandit libero. Facilisis magna etiam tempor orci eu. Nec sagittis aliquam malesuada bibendum arcu. Eu consequat ac felis donec. Ultricies tristique nulla aliquet enim tortor at auctor urna nunc. Tortor posuere ac ut consequat semper viverra nam libero. Amet nisl suscipit adipiscing bibendum est ultricies integer quis auctor. Scelerisque purus semper eget duis at tellus. Elementum tempus egestas sed sed risus pretium. Arcu dictum varius duis at. Amet luctus venenatis lectus magna fringilla urna. Eget velit aliquet sagittis id consectetur purus ut faucibus. Vitae auctor eu augue ut lectus. Fermentum iaculis eu non diam phasellus vestibulum.
4 207 2 What is this? Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Libero justo laoreet sit amet cursus sit amet dictum. Nunc faucibus a pellentesque sit. Dis parturient montes nascetur ridiculus mus mauris vitae ultricies. Enim nunc faucibus a pellentesque. Commodo quis imperdiet massa tincidunt nunc pulvinar sapien. Cras ornare arcu dui vivamus. Facilisi etiam dignissim diam quis enim lobortis. Et molestie ac feugiat sed. Urna neque viverra justo nec ultrices dui. Ullamcorper a lacus vestibulum sed arcu non. Volutpat ac tincidunt vitae semper quis. Dignissim sodales ut eu sem. Feugiat in fermentum posuere urna nec. At augue eget arcu dictum varius.
5 251 3 It's dark... Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Suscipit tellus mauris a diam. Velit aliquet sagittis id consectetur purus ut. Viverra adipiscing at in tellus integer. Vitae elementum curabitur vitae nunc. Mattis ullamcorper velit sed ullamcorper morbi. Diam quis enim lobortis scelerisque. Auctor neque vitae tempus quam pellentesque nec nam aliquam. Semper auctor neque vitae tempus. Quis eleifend quam adipiscing vitae proin. Neque convallis a cras semper auctor neque vitae. Imperdiet massa tincidunt nunc pulvinar sapien et ligula. Sit amet consectetur adipiscing elit ut aliquam purus. Pretium quam vulputate dignissim suspendisse.

View File

@@ -17,7 +17,7 @@ entity Reviews {
liked : Integer default 0; // counter for likes as helpful review (count of all _likes belonging to this review) liked : Integer default 0; // counter for likes as helpful review (count of all _likes belonging to this review)
} }
type Rating : Decimal(3,2) enum { type Rating : Integer enum {
Best = 5; Best = 5;
Good = 4; Good = 4;
Avg = 3; Avg = 3;

View File

@@ -1 +1,2 @@
using from './srv/reviews-service'; using from './srv/reviews-service';
namespace sap.capire.reviews;

View File

@@ -7,20 +7,17 @@
"index.cds" "index.cds"
], ],
"dependencies": { "dependencies": {
"@sap/cds": ">=3.33.1", "@sap/cds": "^4",
"express": "^4.17.1" "express": "^4.17.1"
}, },
"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"
} }
} }
} }

View File

@@ -11,27 +11,27 @@ service ReviewsService {
event reviewed : { event reviewed : {
subject: type of Reviews:subject; subject: type of Reviews:subject;
rating: Decimal(2,1) rating: Decimal(2,1)
}; }
// Input validation // Input validation
annotate Reviews with { annotate Reviews with {
subject @mandatory; subject @mandatory;
title @mandatory; title @mandatory;
rating @mandatory @assert.enum; rating @assert.enum;
} }
} }
// Access control restrictions // Access control restrictions
annotate ReviewsService.Reviews with @restrict_:[ annotate ReviewsService.Reviews with @restrict:[
{ grant:'READ', to:'any' }, // everybody can read reviews { grant:'READ', to:'any' }, // everybody can read reviews
{ grant:'CREATE', to:'authenticated-user' }, // users must login to add reviews { grant:'CREATE', to:'authenticated-user' }, // users must login to add reviews
{ grant:'UPDATE', to:'authenticated-user', where:'reviewer=$user' }, { grant:'UPDATE', to:'authenticated-user', where:'reviewer=$user' },
{ grant:'DELETE', to:'admin' }, { grant:'DELETE', to:'admin' },
]; ];
annotate ReviewsService with @restrict_:[ annotate ReviewsService with @restrict:[
{ grant:'like', to:'identified-user' }, { grant:'like', to:'identified-user' },
{ grant:'unlike', to:'identified-user', where:'user=$user' }, { grant:'unlike', to:'identified-user', where:'user=$user' },
]; ];

View File

@@ -1,5 +1,5 @@
const cds = require ('@sap/cds') const cds = require ('@sap/cds')
module.exports = cds.service.impl (function(){ module.exports = cds.service.impl (async function(){
// Get the CSN definition for Reviews from the db schema for sub-sequent queries // Get the CSN definition for Reviews from the db schema for sub-sequent queries
// ( Note: we explicitly specify the namespace to support embedded reuse ) // ( Note: we explicitly specify the namespace to support embedded reuse )
@@ -10,23 +10,20 @@ 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
// ( Note: req.on.succeeded ensures we only do that if there's no error ) this.after (['CREATE','UPDATE','DELETE'], 'Reviews', async function(_,req) {
this.after (['CREATE','UPDATE','DELETE'], 'Reviews', async(_,req) => {
const {subject} = req.data const {subject} = req.data
const {rating} = await cds.transaction(req) .run ( const {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']) .from (Reviews) .where ({subject})
) )
req.on ('succeeded', ()=>{
global.it || console.log ('< emitting:', 'reviewed', { subject, rating }) global.it || console.log ('< emitting:', 'reviewed', { subject, rating })
this.emit ('reviewed', { subject, rating }) this.emit ('reviewed', { subject, rating })
}) })
})
// Increment counter for reviews considered helpful // Increment counter for reviews considered helpful
this.on ('like', (req) => { this.on ('like', (req) => {
if (!req.user) return req.reject(400, 'You must be identified to like a review') if (!req.user) return req.reject(400, 'You must be identified to like a review')
const {review} = req.data, {user} = req const {review} = req.data, {user} = req
const tx = cds.transaction(req) const tx = cds.tx(req)
return tx.run ([ return tx.run ([
INSERT.into (Likes) .entries ({review_ID: review, user: user.id}), INSERT.into (Likes) .entries ({review_ID: review, user: user.id}),
UPDATE (Reviews) .set({liked: {'+=': 1}}) .where({ID:review}) UPDATE (Reviews) .set({liked: {'+=': 1}}) .where({ID:review})
@@ -37,7 +34,7 @@ module.exports = cds.service.impl (function(){
this.on ('unlike', async (req) => { this.on ('unlike', async (req) => {
if (!req.user) return req.reject(400, 'You must be identified to remove a former like of yours') if (!req.user) return req.reject(400, 'You must be identified to remove a former like of yours')
const {review} = req.data, {user} = req const {review} = req.data, {user} = req
const tx = cds.transaction(req) const tx = cds.tx(req)
const affectedRows = await tx.run (DELETE.from (Likes) .where ({review_ID: review,user: user.id})) const affectedRows = await tx.run (DELETE.from (Likes) .where ({review_ID: review,user: user.id}))
if (affectedRows === 1) return tx.run (UPDATE (Reviews) .set ({liked: {'-=': 1}}) .where ({ID:review})) if (affectedRows === 1) return tx.run (UPDATE (Reviews) .set ({liked: {'-=': 1}}) .where ({ID:review}))
}) })

View File

@@ -1,36 +0,0 @@
#################################################
#
# To ReviewsService mocked in bookshop process
#
GET http://localhost:4004/reviews/Reviews?
###
POST http://localhost:4004/reviews/Reviews
Content-Type: application/json;IEEE754Compatible=true
{"subject":"201", "title":"boo"}
#################################################
#
# Bookshop Requests involving reviews
# (both in-process as well as separate one)
#
### Request to CatalogService > delegated to ReviewsService
GET http://localhost:4004/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 http://localhost:4004/browse/Books(201)?
&$select=ID,title,rating
&$expand=reviews
# Note: the $expand only works in case of ReviewsService in same process

View File

@@ -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"}

View File

@@ -1,23 +0,0 @@
{
"name": "@capire/bookshop-with-reviews",
"version": "1.0.0",
"dependencies": {
"@capire/bookshop": "../../../bookshop",
"@capire/reviews": "..",
"@sap/cds": "^3.33.1",
"express": "^4.17.1"
},
"cds": {
"requires": {
"db": {
"kind": "sql"
},
"ReviewsService": {
"kind": "odata", "model": "@capire/reviews"
},
"messaging": {
"kind": "file-based-messaging"
}
}
}
}

View File

@@ -1,16 +0,0 @@
//
// Extending Books with Reviews
//
using { sap.capire.bookshop.Books } from '@capire/bookshop';
using { ReviewsService.Reviews } from '@capire/reviews';
extend Books with {
/** Access to detailed collection of Reviews */
reviews : Composition of many Reviews on reviews.subject = $self.ID;
/** Average rating */
rating : Reviews.rating;
}
// Temporary workaround for cap/issues#4112:
annotate Reviews with @cds.autoexpose;

View File

@@ -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()

View File

@@ -4,12 +4,12 @@ The list below gives an overview of the samples provided in subdirectories.
Each sub directory essentially is a individual npm package arranged in an [all-in-one monorepo](all-in-one-monorepo) umbrella setup. Each sub directory essentially is a individual npm package arranged in an [all-in-one monorepo](all-in-one-monorepo) umbrella setup.
## [hello](hello) ## [@capire/hello-world](hello)
- A simplistic [Hello World](https://cap.cloud.sap/docs/get-started/hello-world) service using [CDS](https://cap.cloud.sap/docs/cds/) and [cds.services](https://cap.cloud.sap/docs/node.js/api#services-api). - A simplistic [Hello World](https://cap.cloud.sap/docs/get-started/hello-world) service using [CDS](https://cap.cloud.sap/docs/cds/) and [cds.services](https://cap.cloud.sap/docs/node.js/api#services-api).
## [bookshop](bookshop) ## [@capire/bookshop](bookshop)
- [Getting Started](https://cap.cloud.sap/docs/get-started/in-a-nutshell) with CAP, briefly introducing: - [Getting Started](https://cap.cloud.sap/docs/get-started/in-a-nutshell) with CAP, briefly introducing:
- [Project Setup](https://cap.cloud.sap/docs/get-started/) and [Layouts](https://cap.cloud.sap/docs/get-started/projects) - [Project Setup](https://cap.cloud.sap/docs/get-started/) and [Layouts](https://cap.cloud.sap/docs/get-started/projects)
@@ -20,7 +20,7 @@ Each sub directory essentially is a individual npm package arranged in an [all-i
- [Using Databases](https://cap.cloud.sap/docs/guides/databases) - [Using Databases](https://cap.cloud.sap/docs/guides/databases)
## [common](common) ## [@capire/common](common)
- Showcases how to extend [@sap/cds/common](https://cap.cloud.sap/docs/cds/common) thereby covering... - Showcases how to extend [@sap/cds/common](https://cap.cloud.sap/docs/cds/common) thereby covering...
- Building [extension packages](https://cap.cloud.sap/docs/guides/domain-models#aspects-extensibility) - Building [extension packages](https://cap.cloud.sap/docs/guides/domain-models#aspects-extensibility)
@@ -30,14 +30,14 @@ Each sub directory essentially is a individual npm package arranged in an [all-i
- Used in the [fiori app sample](#fiori) - Used in the [fiori app sample](#fiori)
## [orders](orders) ## [@capire/orders](orders)
- Adds orders to the [bookshop](#bookshop), thereby demonstrating... - A standalone orders mgmt service, demonstrating...
- Using [Compositions](https://cap.cloud.sap/docs/cds/cdl#compositions) in [Domain Models](https://cap.cloud.sap/docs/guides/domain-models), along with - Using [Compositions](https://cap.cloud.sap/docs/cds/cdl#compositions) in [Domain Models](https://cap.cloud.sap/docs/guides/domain-models), along with
- [Serving deeply nested documents](https://cap.cloud.sap/docs/guides/generic-providers#serving-structured-data) - [Serving deeply nested documents](https://cap.cloud.sap/docs/guides/generic-providers#serving-structured-data)
## [reviews](reviews) ## [@capire/reviews](reviews)
- Shows how to implement a modular service to manage product reviews, including... - Shows how to implement a modular service to manage product reviews, including...
- Consuming other services synchronously and asynchronously - Consuming other services synchronously and asynchronously
@@ -50,14 +50,19 @@ Each sub directory essentially is a individual npm package arranged in an [all-i
- As well as managed data, input validations and authorization - As well as managed data, input validations and authorization
## [fiori](fiori) ## [@capire/fiori](fiori)
- [Adds a Fiori elements application](https://cap.cloud.sap/docs/guides/fiori/), introducing to... - A [composite app, reusing and combining](https://cap.cloud.sap/docs/guides/verticalize) these packages:
- [@capire/bookshop](bookshop)
- [@capire/reviews](reviews)
- [@capire/orders](orders)
- [@capire/common](common)
- [Adds a Fiori elements application](https://cap.cloud.sap/docs/guides/fiori/) to bookshop, thereby introducing to...
- [OData Annotations](https://cap.cloud.sap/docs/guides/fiori#adding-odata-annotations) in `.cds` files - [OData Annotations](https://cap.cloud.sap/docs/guides/fiori#adding-odata-annotations) in `.cds` files
- Support for [Fiori Draft](https://cap.cloud.sap/docs/guides/fiori#draft) - Support for [Fiori Draft](https://cap.cloud.sap/docs/guides/fiori#draft)
- Support for [Value Helps](https://cap.cloud.sap/docs/guides/fiori#value-help) - Support for [Value Helps](https://cap.cloud.sap/docs/guides/fiori#value-help)
- Serving Fiori apps locally - Serving Fiori apps locally
- Combining most of the other samples through [package reuse](https://cap.cloud.sap/docs/get-started/projects#sharing-and-reusing-content) - [The Vue.js app](bookshop/app/vue) imported from bookshop is served as well
<br> <br>

View File

@@ -1,151 +0,0 @@
////////////////////////////////////////////////////////////////////////////
//
// This is included with cds in newer versions
//
////////////////////////////////////////////////////////////////////////////
const { resolve, dirname } = require('path')
const cwd = process.cwd()
// harmonizing jest and mocha
const is_mocha = !global.test
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)
} else { // it's mocha
global.beforeAll = global.before
global.afterAll = global.after
global.test = global.it
}
/**
* Test kit for jest or mocha testing, which can be used statically
* via the getters for chai, expect and assert or through a server
* started with .launch(...).
*/
class CDSTestKit {
/**
* Creates a new instance serving projects from subfolders under the root
* specified by a sequence of path components which are concatenated with
* path.resolve().
*/
for (...paths) {
const tk = new CDSTestKit
tk.root = resolve (...paths)
return tk
}
/** Lazily loads and returns an instance of chai */
get chai() {
const chai = require('chai')
chai.use (require('chai-as-promised'))
chai.use (require('chai-subset'))
Object.defineProperty (CDSTestKit.prototype, 'chai', {value:chai})
return chai
}
get expect(){ return this.chai.expect }
get assert(){ return this.chai.assert }
/**
* Launches a cds server with arbitrary port and returns a subclass which
* also acts as an axios lookalike, providing methods to send requests.
*/
launch (project, ...args) {
// Setting up test server
const console = global.console, logs=[]
const axios = require('axios').default
const lazy = this, test = {
GET: (path,...etc) => axios.get (test.url+path,...etc) .catch(_error),
PUT: (path,...etc) => axios.put (test.url+path,...etc) .catch(_error),
POST: (path,...etc) => axios.post (test.url+path,...etc) .catch(_error),
PATCH: (path,...etc) => axios.patch (test.url+path,...etc) .catch(_error),
DELETE: (path,...etc) => axios.delete (test.url+path,...etc) .catch(_error),
get: (path,...etc) => axios.get (test.url+path,...etc) .catch(_error),
put: (path,...etc) => axios.put (test.url+path,...etc) .catch(_error),
post: (path,...etc) => axios.post (test.url+path,...etc) .catch(_error),
patch: (path,...etc) => axios.patch (test.url+path,...etc) .catch(_error),
delete: (path,...etc) => axios.delete (test.url+path,...etc) .catch(_error),
/** Lazily loads and returns an instance of chai */
get chai(){ return lazy.chai },
get expect(){ return lazy.expect },
get assert(){ return lazy.assert },
}
// launch cds server...
before (`launching cds server for ${project}...`, done => {
const cds = require('@sap/cds'), { isdir } = cds.utils
let cmd = 'run'
if (project.startsWith('cds ')) [ cmd, project ] = [ project.slice(4), args.shift() ]
if (!args.length) args = ['--in-memory?']
// Supporting .launch (<package name>)
if (cmd === 'run') {
if (isdir(project)) ; //> all fine
else if (isdir(resolve(this.root,project))) project = resolve(this.root,project)
else try { project = dirname (require.resolve(project+'/package.json')) }
catch(e) { throw cds.error (`Cannot resolve project folder for '${project}'`) }
}
if (!process.env.CDS_TEST_VERBOSE) global.console = { __proto__: global.console, logs,
time: ()=>{}, timeEnd: (...args)=> logs.push(args),
debug: (...args)=> logs.push(args),
log: (...args)=> logs.push(args),
warn: (...args)=> logs.push(args),
error: (...args)=> logs.push(args),
dump(){ for (let each of logs) console.log (...each) },
}
// return done (new Error(11))
process.env.PORT = '0'
const p = cds.exec (cmd, project, ...args) // TODO w/ @sap/cds@3.33.3: , '--port', '0')
if (p && 'catch' in p) p.catch (e => {
if (is_mocha) console.error(e)
done(e)
})
cds.once('listening', ({ server, url }) => {
Object.assign (test,{server,url})
done()
})
})
// shutdown cds server...
after (done => {
if (global.console !== console) global.console = console
if (cwd !== process.cwd()) process.chdir(cwd)
test.server ? test.server.close (done) : done()
process.emit('shutdown')
})
function _error (e) {
if (!e.response) throw e
if (!e.response.data) throw e
if (!e.response.data.error) throw e
const { code, message } = e.response.data.error
throw new Error (code && code !== 'null' ? `${code} - ${message}` : message)
}
return test
}
}
// eslint-disable-next-line no-global-assign
require = (mod) => {
try { return module.require(mod) }
catch(e) { if (e.code === 'MODULE_NOT_FOUND') throw new Error (`
Failed to load required package '${mod}'. Please add it thru:
npm add -D ${mod === 'chai' ? 'chai chai-as-promised chai-subset' : mod}
`) }
}
module.exports = new CDSTestKit
exports.root = __dirname

View File

@@ -1 +0,0 @@
module.exports = require('./_cds_tests').for(__dirname,'..')

View File

@@ -1,15 +1,9 @@
const { expect } = require('./capire') const cds = require('@sap/cds/lib')
const cds = require('@sap/cds') const { expect } = cds.test
const CQL = ([cql]) => cds.parse.cql(cql) const CQL = ([cql]) => cds.parse.cql(cql)
const Foo = { name: 'Foo' } const Foo = { name: 'Foo' }
const Books = { name: 'capire.bookshop.Books' } const Books = { name: 'capire.bookshop.Books' }
const is_cds_333 = cds.version >= '3.33.3'
if (!is_cds_333) {
// Monky-patching v3.33.3 features in older releases
const up = UPDATE('x').constructor.prototype
up.with = up.set
}
const { parse:cdr } = cds.ql const { parse:cdr } = cds.ql
// while jest has 'test' as alias to 'it', mocha doesn't // while jest has 'test' as alias to 'it', mocha doesn't
@@ -19,8 +13,9 @@ describe('cds.ql → cqn', () => {
// //
let cqn let cqn
describe(`BUGS + GAPS...`, () => { describe.skip(`BUGS + GAPS...`, () => {
!(cdr ? it : it.skip)('should consistently handle *', () => {
it('should consistently handle *', () => {
expect({ expect({
SELECT: { from: { ref: ['Foo'] }, columns: ['*'] }, SELECT: { from: { ref: ['Foo'] }, columns: ['*'] },
}) })
@@ -30,30 +25,18 @@ describe('cds.ql → cqn', () => {
.to.eql(SELECT.from(Foo,['*'])) .to.eql(SELECT.from(Foo,['*']))
}) })
!(cdr ? it : it.skip)('should correctly handle { ... and:{...} }', () => {
expect(SELECT.from(Foo).where({ x: 1, and: { y: 2, or: { z: 3 } } })).to.eql({ it('should consistently handle lists', () => {
SELECT: { const ID = 11, args = [`foo`, "'bar'", 3]
from: { ref: ['Foo'] }, const cqn = CQL`SELECT from Foo where ID=11 and x in (foo,'bar',3)`
where: [ expect(SELECT.from(Foo).where(`ID=${ID} and x in (${args})`)).to.eql(cqn)
{ ref: ['x'] }, expect(SELECT.from(Foo).where(`ID=`, ID, `and x in`, args)).to.eql(cqn)
'=', expect(SELECT.from(Foo).where({ ID, x:args })).to.eql(cqn)
{ val: 1 },
'and',
'(',
{ ref: ['y'] },
'=',
{ val: 2 },
'or',
{ ref: ['z'] },
'=',
{ val: 3 },
')',
],
},
})
}) })
}) })
describe(`SELECT...`, () => { describe(`SELECT...`, () => {
test('from ( Foo )', () => { test('from ( Foo )', () => {
expect({ expect({
@@ -184,7 +167,6 @@ describe('cds.ql → cqn', () => {
}) })
}) })
is_cds_333 &&
test('from ( ..., => _.expand ( x=>{...}))', () => { test('from ( ..., => _.expand ( x=>{...}))', () => {
// SELECT from Foo { *, x, bar.*, car{*}, boo { *, moo.zoo } } // SELECT from Foo { *, x, bar.*, car{*}, boo { *, moo.zoo } }
expect( expect(
@@ -212,7 +194,6 @@ describe('cds.ql → cqn', () => {
}) })
}) })
is_cds_333 &&
test('from ( ..., => _.inline ( _=>{...}))', () => { test('from ( ..., => _.inline ( _=>{...}))', () => {
// SELECT from Foo { *, x, bar.*, car{*}, boo { *, moo.zoo } } // SELECT from Foo { *, x, bar.*, car{*}, boo { *, moo.zoo } }
expect( expect(
@@ -261,7 +242,30 @@ describe('cds.ql → cqn', () => {
// same for works distinct // same for works distinct
}) })
xtest('where ( ... cql | {x:y} )', () => { it('should correctly handle { ... and:{...} }', () => {
expect(SELECT.from(Foo).where({ x: 1, and: { y: 2, or: { z: 3 } } })).to.eql({
SELECT: {
from: { ref: ['Foo'] },
where: [
{ ref: ['x'] },
'=',
{ val: 1 },
'and',
'(',
{ ref: ['y'] },
'=',
{ val: 2 },
'or',
{ ref: ['z'] },
'=',
{ val: 3 },
')',
],
},
})
})
test('where ( ... cql | {x:y} )', () => {
const args = [`foo`, "'bar'", 3] const args = [`foo`, "'bar'", 3]
const ID = 11 const ID = 11
@@ -298,7 +302,7 @@ describe('cds.ql → cqn', () => {
')', ')',
] ]
: [ : [
'(', //> this one is not required // '(', //> this one is not required
{ ref: ['ID'] }, { ref: ['ID'] },
'=', '=',
{ val: ID }, { val: ID },
@@ -307,7 +311,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 '(', //> this one is missing, and that's changing the logic -> that's a BUG
{ ref: ['x'] }, { ref: ['x'] },
'like', 'like',
{ val: '%x%' }, { val: '%x%' },
@@ -341,8 +345,6 @@ describe('cds.ql → cqn', () => {
], ],
}, },
}) })
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(
SELECT.from(Foo).where(`x=`, 1, `or y.z is null and (a>`, 2, `or b=`, 3, `)`) SELECT.from(Foo).where(`x=`, 1, `or y.z is null and (a>`, 2, `or b=`, 3, `)`)
@@ -466,47 +468,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'] }] },
}, },
}, },
}) })

View File

@@ -1,30 +1,26 @@
const { expect } = require('./capire') const cds = require('@sap/cds/lib')
const cds = require('@sap/cds') const { expect } = cds.test (
'serve', 'AdminService', '--from', '@capire/bookshop,@capire/common', '--in-memory'
).in(__dirname)
describe('Consuming Services locally', () => { describe('Consuming Services locally', () => {
// //
before('bootstrap db and services', async () => { it('bootrapped the database successfully', ()=>{
const model = await cds.load(['@capire/bookshop', '@capire/common']) const { AdminService } = cds.services
await cds.deploy(model).to('sqlite::memory:')
const { AdminService } = await cds.serve('AdminService').from(model)
const { Authors } = AdminService.entities const { Authors } = AdminService.entities
expect(AdminService).not.to.be.undefined expect(AdminService).not.to.be.undefined
expect(Authors).not.to.be.undefined expect(Authors).not.to.be.undefined
}) })
it('bootrapped the database successfully', ()=>{})
it('supports targets as strings or reflected defs', async () => { it('supports targets as strings or reflected defs', async () => {
const AdminService = await cds.connect.to('AdminService') const AdminService = await cds.connect.to('AdminService')
const { Authors } = AdminService.entities const { Authors } = AdminService.entities
expect(await AdminService.read(Authors)) const _ = expect (await AdminService.read(Authors))
.to.eql(await AdminService.read('Authors')) .to.eql(await AdminService.read('Authors'))
.to.eql(await AdminService.run(SELECT.from(Authors))) .to.eql(await AdminService.run(SELECT.from(Authors)))
// temporary workaround // temporary workaround
if (AdminService.read.fix_50) if (cds.version >= '4.2.0')
expect(await AdminService.run(SELECT.from(Authors))).to.eql( _.to.eql(await AdminService.run(SELECT.from('Authors')))
await AdminService.run(SELECT.from('Authors'))
)
}) })
it('allows reading from local services using cds.ql', async () => { it('allows reading from local services using cds.ql', async () => {
@@ -39,9 +35,8 @@ describe('Consuming Services locally', () => {
}) })
}).where(`name like`, 'E%') }).where(`name like`, 'E%')
// temporary workaround // temporary workaround
if (!AdminService.read.fix_50) { if (cds.version < '4.2.0')
query.SELECT.from.ref[0] = 'AdminService.Authors' query.SELECT.from.ref[0] = 'AdminService.Authors'
}
const authors = await AdminService.run(query) const authors = await AdminService.run(query)
expect(authors).to.containSubset([ expect(authors).to.containSubset([
{ {
@@ -79,17 +74,14 @@ describe('Consuming Services locally', () => {
}) })
} }
const query1 = SELECT.from(Authors, projection).where(`name like`, 'E%') const query1 = SELECT.from(Authors, projection).where(`name like`, 'E%')
const query2 = cds.read(Authors, projection).where(`name like`, 'E%')
expect(await cds.run(query1)) expect(await cds.run(query1))
.to.eql(await db.run(query1)) .to.eql(await db.run(query1))
.to.eql(await srv.run(query1)) .to.eql(await srv.run(query1))
.to.eql(await srv.read(Authors, projection).where(`name like`, 'E%')) .to.eql(await srv.read(Authors, projection).where(`name like`, 'E%'))
const query2 = cds.read(Authors, projection).where(`name like`, 'E%')
// temporary workaround
if (srv.read.fix_50)
expect(await cds.run(query1))
.to.eql(await cds.run(query2)) .to.eql(await cds.run(query2))
.to.eql(await db.run(query2)) .to.eql(await db.run(query2))
.to.eql(await srv.run(query2)) .to.eql(await srv.run(query2))
.to.eql(await db.read(Authors, projection).where(`name like`, 'E%')) // FIXME!! .to.eql(await db.read(Authors, projection).where(`name like`, 'E%'))
}) })
}) })

View File

@@ -1,5 +1,7 @@
const cds = require('@sap/cds/lib'); cds.User = cds.User.Privileged // skip auth
const { GET, POST, expect } = cds.test('bookshop').in(__dirname,'..')
describe('Custom Handlers', () => { describe('Custom Handlers', () => {
const { GET, POST, expect } = require('./capire').launch('bookshop')
it('should reject out-of-stock orders', async () => { it('should reject out-of-stock orders', async () => {
await expect( await expect(

View File

@@ -1,6 +1,7 @@
const cds = require('@sap/cds/lib')
const { GET, expect } = cds.test('serve','hello/world.cds').in(__dirname,'..')
describe('Hello world!', () => { describe('Hello world!', () => {
const { GET, expect } = require('./capire').launch('cds serve', __dirname+'/../hello/world.cds', '')
it('should say hello with class impl', async () => { it('should say hello with class impl', async () => {
const {data} = await GET `/say/hello(to='world')` const {data} = await GET `/say/hello(to='world')`

View File

@@ -1,6 +1,6 @@
const cwd = process.cwd(); process.chdir (__dirname) //> only for internal CI/CD@SAP const cwd = process.cwd(); process.chdir (__dirname) //> only for internal CI/CD@SAP
const {expect} = require('./capire') const cds = require('@sap/cds/lib')
const cds = require ('@sap/cds') const {expect} = cds.test
// monkey patching older releases: // monkey patching older releases:
if (!cds.compile.cdl) cds.compile.cdl = cds.parse if (!cds.compile.cdl) cds.compile.cdl = cds.parse
@@ -76,7 +76,7 @@ describe('Hierarchical Data', ()=>{
it ('supports cascaded deletes', async()=>{ it ('supports cascaded deletes', async()=>{
const affectedRows = await DELETE.from (Cats) .where ({ID:[102,106]}) const affectedRows = await DELETE.from (Cats) .where ({ID:[102,106]})
expect (affectedRows) .to.equal (5) expect (affectedRows) .to.be.greaterThan (0)
const expected = [ const expected = [
{ ID:100, name:'Some Cats...' }, { ID:100, name:'Some Cats...' },
{ ID:101, name:'Cat' }, { ID:101, name:'Cat' },

View File

@@ -1,5 +1,7 @@
const cds = require('@sap/cds/lib'); cds.User = cds.User.Privileged // skip auth
const { GET, expect } = cds.test ('serve', __dirname+'/localized-data.cds', '--in-memory')
describe('Localized Data', () => { describe('Localized Data', () => {
const { GET, expect } = require('./capire').launch('cds serve',__dirname+'/localized-data.cds')
it('serves localized $metadata documents', async () => { it('serves localized $metadata documents', async () => {
const { data } = await GET`/browse/$metadata?sap-language=de` const { data } = await GET`/browse/$metadata?sap-language=de`
@@ -72,7 +74,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' },
}) })

View File

@@ -1,8 +1,8 @@
const cds = require('@sap/cds/lib')
const cwd = process.cwd(); process.chdir (__dirname) //> only for internal CI/CD@SAP const cwd = process.cwd(); process.chdir (__dirname) //> only for internal CI/CD@SAP
const {expect} = require('./capire') const {expect} = cds.test
const cds = require ('@sap/cds')
const _model = '@capire/reviews' const _model = '@capire/reviews'
cds.User = cds.User.Privileged // hard core monkey patch
describe('Messaging', ()=>{ describe('Messaging', ()=>{
@@ -10,6 +10,7 @@ describe('Messaging', ()=>{
it ('should bootstrap sqlite in-memory db', async()=>{ it ('should bootstrap sqlite in-memory db', async()=>{
const db = await cds.deploy (_model) .to ('sqlite::memory:') const db = await cds.deploy (_model) .to ('sqlite::memory:')
await db.delete('Reviews')
expect (db.model) .not.undefined expect (db.model) .not.undefined
}) })
@@ -21,11 +22,11 @@ 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,next)=> { received.push(msg); return next() })
}) })
it ('should add more messaging event handlers', ()=>{ it ('should add more messaging event handlers', ()=>{
srv.on('reviewed', ()=> ++M) srv.on('reviewed', (_,next)=> { ++M; return next() })
}) })
it ('should add review', async ()=>{ it ('should add review', async ()=>{

View File

@@ -1,5 +1,7 @@
const cds = require('@sap/cds/lib'); cds.User = cds.User.Privileged // skip auth
const { GET, expect } = cds.test('bookshop').in(__dirname,'..')
describe('OData Protocol', () => { describe('OData Protocol', () => {
const { GET, expect } = require('./capire').launch('bookshop')
it('serves $metadata documents in v4', async () => { it('serves $metadata documents in v4', async () => {
const { headers, status, data } = await GET `/browse/$metadata` const { headers, status, data } = await GET `/browse/$metadata`

View File

@@ -1,5 +0,0 @@
{
"name": "@capire/tests",
"version": "1.0.0",
"main": "capire.js"
}