Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
835f450686 |
@@ -21,7 +21,6 @@
|
|||||||
},
|
},
|
||||||
"rules": {
|
"rules": {
|
||||||
"no-console": "off",
|
"no-console": "off",
|
||||||
"require-atomic-updates": "off",
|
"require-atomic-updates": "off"
|
||||||
"require-await":"warn"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
---
|
|
||||||
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
|
|
||||||
2
.github/workflows/node.js.yml
vendored
2
.github/workflows/node.js.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
|||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node-version: [10.x, 12.x, 14.x]
|
node-version: [10.x, 12.16.2]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
|
|||||||
@@ -1,71 +0,0 @@
|
|||||||
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
29
.reuse/dep5
@@ -1,29 +0,0 @@
|
|||||||
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
|
|
||||||
project’s 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
|
|
||||||
2
.vscode/extensions.json
vendored
2
.vscode/extensions.json
vendored
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
// List of extensions which should be recommended for users of this workspace.
|
// List of extensions which should be recommended for users of this workspace.
|
||||||
"recommendations": [
|
"recommendations": [
|
||||||
"SAPSE.vscode-cds",
|
// "CDS Editor !",
|
||||||
"dbaeumer.vscode-eslint",
|
"dbaeumer.vscode-eslint",
|
||||||
"esbenp.prettier-vscode",
|
"esbenp.prettier-vscode",
|
||||||
"mechatroner.rainbow-csv",
|
"mechatroner.rainbow-csv",
|
||||||
|
|||||||
27
.vscode/launch.json
vendored
27
.vscode/launch.json
vendored
@@ -4,27 +4,26 @@
|
|||||||
// 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",
|
||||||
"command": "cds watch bookshop",
|
"cwd": "${workspaceFolder}/bookshop",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"type": "node-terminal",
|
"type": "node",
|
||||||
|
"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",
|
||||||
"command": "cds watch fiori",
|
"cwd": "${workspaceFolder}/fiori",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"type": "node-terminal",
|
"type": "node",
|
||||||
|
"runtimeExecutable": "npx",
|
||||||
|
"runtimeArgs": ["-n"],
|
||||||
|
"args": ["--", "cds", "run", "--with-mocks", "--in-memory?"],
|
||||||
|
"console": "integratedTerminal",
|
||||||
"skipFiles": ["<node_internals>/**"]
|
"skipFiles": ["<node_internals>/**"]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
333
LICENSE
333
LICENSE
@@ -1,208 +1,217 @@
|
|||||||
Apache License
|
Apache License
|
||||||
|
|
||||||
Version 2.0, January 2004
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION,
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
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.
|
||||||
|
|
||||||
"License" shall mean the terms and conditions for use, reproduction, and distribution
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
as defined by Sections 1 through 9 of this document.
|
other entities that control, are controlled by, or are under common
|
||||||
|
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.
|
||||||
|
|
||||||
"Licensor" shall mean the copyright owner or entity authorized by the copyright
|
"Object" form shall mean any form resulting from mechanical
|
||||||
owner that is granting the License.
|
transformation or translation of a Source form, including but
|
||||||
|
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.
|
||||||
|
|
||||||
"Legal Entity" shall mean the union of the acting entity and all other entities
|
"Contribution" shall mean any work of authorship, including
|
||||||
that control, are controlled by, or are under common control with that entity.
|
the original version of the Work and any modifications or additions
|
||||||
For the purposes of this definition, "control" means (i) the power, direct
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
or indirect, to cause the direction or management of such entity, whether
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
by contract or otherwise, or (ii) ownership of fifty percent (50%) or more
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
of the outstanding shares, or (iii) beneficial ownership of such entity.
|
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.
|
||||||
|
|
||||||
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
granted by this License.
|
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
|
||||||
|
|
||||||
"Source" form shall mean the preferred form for making modifications, including
|
(b) You must cause any modified files to carry prominent notices
|
||||||
but not limited to software source code, documentation source, and configuration
|
stating that You changed the files; and
|
||||||
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
|
||||||
"Object" form shall mean any form resulting from mechanical transformation
|
distribution, then any Derivative Works that You distribute must
|
||||||
or translation of a Source form, including but not limited to compiled object
|
include a readable copy of the attribution notices contained
|
||||||
code, generated documentation, and conversions to other media types.
|
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
|
||||||
"Work" shall mean the work of authorship, whether in Source or Object form,
|
documentation, if provided along with the Derivative Works; or,
|
||||||
made available under the License, as indicated by a copyright notice that
|
within a display generated by the Derivative Works, if and
|
||||||
is included in or attached to the work (an example is provided in the Appendix
|
wherever such third-party notices normally appear. The contents
|
||||||
below).
|
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
|
||||||
"Derivative Works" shall mean any work, whether in Source or Object form,
|
that such additional attribution notices cannot be construed
|
||||||
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 may provide
|
You may add Your own copyright statement to Your modifications and
|
||||||
additional or different license terms and conditions for use, reproduction,
|
may provide additional or different license terms and conditions
|
||||||
or distribution of Your modifications, or for any such Derivative Works as
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
a whole, provided Your use, reproduction, and distribution of the Work otherwise
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
complies with the conditions stated in this License.
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
5. Submission of Contributions. Unless You explicitly state otherwise, any
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
Contribution intentionally submitted for inclusion in the Work by You to the
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
Licensor shall be under the terms and conditions of this License, without
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
any additional terms or conditions. Notwithstanding the above, nothing herein
|
this License, without any additional terms or conditions.
|
||||||
shall supersede or modify the terms of any separate license agreement you
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
may have executed with Licensor regarding such Contributions.
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
6. Trademarks. This License does not grant permission to use the trade names,
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
trademarks, service marks, or product names of the Licensor, except as required
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
for reasonable and customary use in describing the origin of the Work and
|
except as required for reasonable and customary use in describing the
|
||||||
reproducing the content of the NOTICE file.
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
7. Disclaimer of Warranty. Unless required by applicable law or agreed to
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
in writing, Licensor provides the Work (and each Contributor provides its
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
KIND, either express or implied, including, without limitation, any warranties
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR
|
implied, including, without limitation, any warranties or conditions
|
||||||
A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
of using or redistributing the Work and assume any risks associated with Your
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
exercise of permissions under this License.
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
8. Limitation of Liability. In no event and under no legal theory, whether
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
in tort (including negligence), contract, or otherwise, unless required by
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
applicable law (such as deliberate and grossly negligent acts) or agreed to
|
unless required by applicable law (such as deliberate and grossly
|
||||||
in writing, shall any Contributor be liable to You for damages, including
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
any direct, indirect, special, incidental, or consequential damages of any
|
liable to You for damages, including any direct, indirect, special,
|
||||||
character arising as a result of this License or out of the use or inability
|
incidental, or consequential damages of any character arising as a
|
||||||
to use the Work (including but not limited to damages for loss of goodwill,
|
result of this License or out of the use or inability to use the
|
||||||
work stoppage, computer failure or malfunction, or any and all other commercial
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
damages or losses), even if such Contributor has been advised of the possibility
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
of such damages.
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
9. Accepting Warranty or Additional Liability. While redistributing the Work
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
or Derivative Works thereof, You may choose to offer, and charge a fee for,
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
acceptance of support, warranty, indemnity, or other liability obligations
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
and/or rights consistent with this License. However, in accepting such obligations,
|
or other liability obligations and/or rights consistent with this
|
||||||
You may act only on Your own behalf and on Your sole responsibility, not on
|
License. However, in accepting such obligations, You may act only
|
||||||
behalf of any other Contributor, and only if You agree to indemnify, defend,
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
and hold each Contributor harmless for any liability incurred by, or claims
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
asserted against, such Contributor by reason of your accepting any such warranty
|
defend, and hold each Contributor harmless for any liability
|
||||||
or additional liability. END OF TERMS AND CONDITIONS
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
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 boilerplate
|
To apply the Apache License to your work, attach the following
|
||||||
notice, with the fields enclosed by brackets "[]" replaced with your own identifying
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
information. (Don't include the brackets!) The text should be enclosed in
|
replaced with your own identifying information. (Don't include
|
||||||
the appropriate comment syntax for the file format. We also recommend that
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
a file or class name and description of purpose be included on the same "printed
|
comment syntax for the file format. We also recommend that a
|
||||||
page" as the copyright notice for easier identification within third-party
|
file or class name and description of purpose be included on the
|
||||||
archives.
|
same "printed page" as the copyright notice for easier
|
||||||
|
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.
|
||||||
|
|||||||
37
README.md
37
README.md
@@ -3,22 +3,17 @@
|
|||||||
Find here a collection of samples for the [SAP Cloud Application Programming Model](https://cap.cloud.sap) organized in a simplistic [monorepo setup](samples.md#all-in-one-monorepo). → See [**Overview** of contained samples](samples.md)
|
Find here a collection of samples for the [SAP Cloud Application Programming Model](https://cap.cloud.sap) organized in a simplistic [monorepo setup](samples.md#all-in-one-monorepo). → See [**Overview** of contained samples](samples.md)
|
||||||
|
|
||||||

|

|
||||||
[](https://api.reuse.software/info/github.com/SAP-samples/cloud-cap-samples)
|
|
||||||
|
|
||||||
|
|
||||||
### Preliminaries
|
### Preliminaries
|
||||||
|
|
||||||
1. Install [**@sap/cds-dk**](https://cap.cloud.sap/docs/get-started/) globally:
|
1. [Install @sap/cds-dk](https://cap.cloud.sap/docs/get-started/) as documented in [capire](https://cap.cloud.sap)
|
||||||
|
2. _Optional:_ [Use Visual Studio Code](https://cap.cloud.sap/docs/get-started/in-vscode)
|
||||||
|
|
||||||
```sh
|
|
||||||
npm i -g @sap/cds-dk
|
|
||||||
```
|
|
||||||
|
|
||||||
2. _Optional:_ [Use Visual Studio Code](https://cap.cloud.sap/docs/get-started/tools#vscode)
|
|
||||||
|
|
||||||
### Download
|
### Download
|
||||||
|
|
||||||
If you have [Git](https://git-scm.com/downloads) installed, clone this repo as shown below, otherwise [download as ZIP file](archive/master.zip).
|
Clone this repo as shown below, if you have [git](https://git-scm.com/downloads) installed,
|
||||||
|
otherwise [download as zip file](archive/master.zip).
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
git clone https://github.com/sap-samples/cloud-cap-samples samples
|
git clone https://github.com/sap-samples/cloud-cap-samples samples
|
||||||
@@ -43,41 +38,21 @@ cds watch bookshop
|
|||||||
|
|
||||||
After that open this link in your browser: [http://localhost:4004](http://localhost:4004)
|
After that open this link in your browser: [http://localhost:4004](http://localhost:4004)
|
||||||
|
|
||||||
When asked to log in, type `alice` as user and leave the password field blank, which is the [default user](https://cap.cloud.sap/docs/node.js/authentication#mocked).
|
|
||||||
|
|
||||||
### Testing
|
### Testing
|
||||||
|
|
||||||
Run the provided tests with [_jest_](http://jestjs.io) or [_mocha_](http://mochajs.org), for example:
|
Run the provided tests with [_jest_](http://jestjs.io) or [_mocha_](http://mochajs.org), for example:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
npx jest
|
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 included a simple npm registry mock, which allows you to do an `npm install @capire/<package>` 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, for example:
|
|
||||||
|
|
||||||
```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 have a question, find a bug, or otherwise need support, please use our [community](https://answers.sap.com/tags/9f13aee1-834c-4105-8e43-ee442775e5ce).
|
In case you find a bug or need support, please [open an issue in here](https://github.com/SAP-samples/cloud-cap-samples/issues/new).
|
||||||
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Copyright (c) 2021 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.
|
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.
|
||||||
|
|||||||
1
bookshop/.env
Normal file
1
bookshop/.env
Normal file
@@ -0,0 +1 @@
|
|||||||
|
cds.features.snapi = y
|
||||||
@@ -9,36 +9,33 @@ const books = new Vue ({
|
|||||||
|
|
||||||
data: {
|
data: {
|
||||||
list: [],
|
list: [],
|
||||||
book: undefined,
|
book: { descr:'( click on a row to see details... )' },
|
||||||
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 (etc='') {
|
async fetch (_filter='') {
|
||||||
const {data} = await GET(`/ListOfBooks?$expand=genre,currency${etc}`)
|
const columns = 'ID,title,author,price,stock', details = 'genre,currency'
|
||||||
|
const {data} = await GET(`/Books?$select=${columns}&$expand=${details}&${_filter}`)
|
||||||
books.list = data.value
|
books.list = data.value
|
||||||
},
|
},
|
||||||
|
|
||||||
async inspect (eve) {
|
async inspect () {
|
||||||
const book = books.book = books.list [eve.currentTarget.rowIndex-1]
|
const book = books.book = books.list [event.currentTarget.rowIndex-1]
|
||||||
const res = await GET(`/Books/${book.ID}?$select=descr,stock,image`)
|
book.descr || await GET(`/Books/${book.ID}/descr/$value`) .then (({data}) => book.descr = data)
|
||||||
Object.assign (book, res.data)
|
|
||||||
books.order = { amount:1 }
|
books.order = { amount:1 }
|
||||||
setTimeout (()=> $('form > input').focus(), 111)
|
setTimeout (()=> $('form > input').focus(), 111)
|
||||||
},
|
},
|
||||||
|
|
||||||
async submitOrder () {
|
submitOrder () { event.preventDefault()
|
||||||
const {book,order} = books, amount = parseInt (order.amount) || 1 // REVISIT: Okra should be less strict
|
const {book,order} = books, amount = parseInt (order.amount) || 1
|
||||||
try {
|
POST(`/submitOrder`, { amount, book: book.ID })
|
||||||
const res = await POST(`/submitOrder`, { amount, book: book.ID })
|
.then (()=> books.order = { amount, succeeded: `Successfully orderd ${amount} item(s).` })
|
||||||
book.stock = res.data.stock
|
.catch (e=> books.order = { amount, failed: e.response.data.error.message })
|
||||||
books.order = { amount, succeeded: `Successfully orderd ${amount} item(s).` }
|
GET(`/Books/${book.ID}/stock/$value`).then (res => book.stock = res.data)
|
||||||
} catch (e) {
|
|
||||||
books.order = { amount, failed: e.response.data.error.message }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,56 +7,50 @@
|
|||||||
<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>
|
||||||
.hovering tr:hover td { color:cyan; background: #123; cursor: pointer; }
|
#books tr:hover { background: #f2f2f2; cursor: pointer; }
|
||||||
.rating-stars { color:teal }
|
form { float:right; display:flex; flex-direction: row-reverse }
|
||||||
.succeeded { color:teal }
|
form #amount { width: 5em }
|
||||||
.failed { color:red }
|
.is-success { color: #0d920d }
|
||||||
|
.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> {{ document.title }} </h1>
|
<h1> Capire Books </h1>
|
||||||
|
|
||||||
<input type="text" placeholder="Search..." @input="search">
|
<input type="text" placeholder="Search..." @input="search">
|
||||||
|
|
||||||
<table id='books' class="hovering">
|
<table id='books'>
|
||||||
<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">
|
<div v-if="book.title">
|
||||||
<img v-bind:src="book.image" alt=""/>
|
|
||||||
<label style="text-align:right">
|
<label style="text-align:right">
|
||||||
<span class="succeeded"> {{ order.succeeded }} </span>
|
<span class="is-success"> {{ order.succeeded }} </span>
|
||||||
<span class="failed"> {{ order.failed }} </span>
|
<span class="has-error"> {{ order.failed }} </span>
|
||||||
{{ book.stock }} in stock
|
{{ book.stock }} in stock
|
||||||
</label>
|
</label>
|
||||||
<form @submit.prevent="submitOrder" style="float:right; display:flex; flex-direction:row-reverse">
|
<form @submit="submitOrder">
|
||||||
<input type="number" v-model="order.amount" v-bind:class="{ failed: order.failed }" style="width:5em">
|
<input type="number" id="amount" v-model="order.amount" v-bind:class="{ 'has-error': order.failed }">
|
||||||
<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>
|
||||||
|
|||||||
@@ -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;21;Autobiography
|
22;20;Autobiography
|
||||||
23;20;Essay
|
23;20;Essay
|
||||||
24;20;Speech
|
24;20;Speech
|
||||||
|
|||||||
|
@@ -8,9 +8,8 @@ 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;
|
price : Decimal(9,2);
|
||||||
currency : Currency;
|
currency : Currency;
|
||||||
image : LargeBinary @Core.MediaType : 'image/png';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
entity Authors : managed {
|
entity Authors : managed {
|
||||||
|
|||||||
@@ -3,10 +3,9 @@
|
|||||||
"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": "*",
|
"@capire/common": "../common",
|
||||||
"@sap/cds": "^4",
|
"@sap/cds": ">=3.33.1",
|
||||||
"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",
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ This stand-alone sample introduces the essential tasks in the development of CAP
|
|||||||
## Hypothetical Use Cases
|
## Hypothetical Use Cases
|
||||||
|
|
||||||
1. Build a service that allows to browse _Books_ and _Authors_.
|
1. Build a service that allows to browse _Books_ and _Authors_.
|
||||||
2. Books have assigned _Genres_, which are organized hierarchically.
|
2. Books have assigned _Genres_ which are organized hierarchically.
|
||||||
3. All users may browse books without login.
|
3. All users may browse books without login.
|
||||||
4. All entries are maintained by Administrators.
|
4. All entries are maintained by Administrators.
|
||||||
5. End users may order books (the actual order mgmt being out of scope).
|
5. End users may order books (the actual order mgmt being out of scope)
|
||||||
|
|
||||||
## Running the Sample
|
## Running the Sample
|
||||||
|
|
||||||
@@ -20,12 +20,12 @@ npm run watch
|
|||||||
|
|
||||||
| Links to capire | Sample files / folders |
|
| Links to capire | Sample files / folders |
|
||||||
| --------------------------------------------------------------------------------------------------------- | ------------------------------------ |
|
| --------------------------------------------------------------------------------------------------------- | ------------------------------------ |
|
||||||
| [Project Setup & Layouts](https://cap.cloud.sap/docs/get-started/projects#sharing-and-reusing-content) | [`./`](./) |
|
| [Project Setup and Layouts](https://cap.cloud.sap/docs/get-started/projects#sharing-and-reusing-content) | [`./`](./) |
|
||||||
| [Domain Modeling with CDS](https://cap.cloud.sap/docs/guides/domain-models) | [`./db/schema.cds`](./db/schema.cds) |
|
| [Defining Domain Models](https://cap.cloud.sap/docs/guides/domain-models) | [`./db/schema.cds`](./db/schema.cds) |
|
||||||
| [Defining Services](https://cap.cloud.sap/docs/guides/services#defining-services) | [`./srv/*.cds`](./srv) |
|
| [Defining Services](https://cap.cloud.sap/docs/guides/providing-services) | [`./srv/*.cds`](./srv) |
|
||||||
| [Single-purposed Services](https://cap.cloud.sap/docs/guides/services#single-purposed-services) | [`./srv/*.cds`](./srv) |
|
| [Single-purposed Services](https://cap.cloud.sap/docs/guides/providing-services#single-purposed-services) | [`./srv/*.cds`](./srv) |
|
||||||
| [Providing & Consuming Providers](https://cap.cloud.sap/docs/guides/providing-services) | http://localhost:4004 |
|
| [Generic Providers](https://cap.cloud.sap/docs/guides/providing-services) | http://localhost:4004 |
|
||||||
| [Using Databases](https://cap.cloud.sap/docs/guides/databases) | [`./db/data/*.csv`](./db/data) |
|
| Using Databases | [`./db/data/*.csv`](./db/data) |
|
||||||
| [Adding Custom Logic](https://cap.cloud.sap/docs/guides/service-impl) | [`./srv/*.js`](./srv) |
|
| [Adding Custom Logic](https://cap.cloud.sap/docs/guides/service-impl) | [`./srv/*.js`](./srv) |
|
||||||
| Adding Tests | [`./test`](./test) |
|
| Adding Tests | [`./test`](./test) |
|
||||||
| [Sharing for Reuse](https://cap.cloud.sap/docs/guides/reuse-and-compose) | [`./index.cds`](./index.cds) |
|
| [Sharing for Reuse](https://cap.cloud.sap/docs/get-started/projects#sharing-and-reusing-content) | [`./index.cds`](./index.cds) |
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -5,10 +5,6 @@ service CatalogService @(path:'/browse') {
|
|||||||
author.name as author
|
author.name as author
|
||||||
} excluding { createdBy, modifiedBy };
|
} excluding { createdBy, modifiedBy };
|
||||||
|
|
||||||
@readonly entity ListOfBooks as SELECT from Books
|
@requires_: 'authenticated-user'
|
||||||
excluding { descr };
|
action submitOrder (book : Books.ID, amount: Integer);
|
||||||
|
|
||||||
@requires: 'authenticated-user'
|
|
||||||
action submitOrder ( book: Books:ID, amount: Integer ) returns { stock: Integer };
|
|
||||||
event OrderedBook : { book: Books:ID; amount: Integer; buyer: String };
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +1,20 @@
|
|||||||
const cds = require('@sap/cds')
|
const cds = require('@sap/cds')
|
||||||
const { Books } = cds.entities ('sap.capire.bookshop')
|
module.exports = async function (){
|
||||||
|
|
||||||
class CatalogService extends cds.ApplicationService { init(){
|
const db = await cds.connect.to('db') // connect to database service
|
||||||
|
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, tx = cds.tx(req)
|
const {book,amount} = req.data
|
||||||
let {stock} = await tx.read('stock').from(Books,book)
|
const n = await UPDATE (Books, book)
|
||||||
if (stock >= amount) {
|
.with ({ stock: {'-=': amount }})
|
||||||
await tx.update (Books,book).with ({ stock: stock -= amount })
|
.where ({ stock: {'>=': amount }})
|
||||||
await this.emit ('OrderedBook', { book, amount, buyer:req.user.id })
|
n > 0 || req.error (409,`${amount} exceeds stock for book #${book}`)
|
||||||
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) {
|
if (each.stock > 111) each.title += ` -- 11% discount!`
|
||||||
each.title += ` -- 11% discount!`
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
}
|
||||||
return super.init()
|
|
||||||
}}
|
|
||||||
|
|
||||||
module.exports = { CatalogService }
|
|
||||||
|
|||||||
@@ -32,37 +32,6 @@ 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
|
||||||
|
|||||||
@@ -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;Australian Dollar;036;Cent;2
|
AUD;$;Australian Dollar;Canadian 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,8 +1,4 @@
|
|||||||
{
|
{
|
||||||
"name": "@capire/common",
|
"name": "@capire/common",
|
||||||
"description": "Provides a pre-built extension package for std @sap/cds/common",
|
"version": "1.0.0"
|
||||||
"version": "1.0.0",
|
|
||||||
"dependencies": {
|
|
||||||
"@sap/cds": "latest"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
24
ext/.eslintrc
Normal file
24
ext/.eslintrc
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"extends": "eslint:recommended",
|
||||||
|
"env": {
|
||||||
|
"node": true,
|
||||||
|
"es6": true,
|
||||||
|
"jest": true
|
||||||
|
},
|
||||||
|
"parserOptions": {
|
||||||
|
"ecmaVersion": 2017
|
||||||
|
},
|
||||||
|
"globals": {
|
||||||
|
"SELECT": true,
|
||||||
|
"INSERT": true,
|
||||||
|
"UPDATE": true,
|
||||||
|
"DELETE": true,
|
||||||
|
"CREATE": true,
|
||||||
|
"DROP": true,
|
||||||
|
"cds": true
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"no-console": "off",
|
||||||
|
"require-atomic-updates": "off"
|
||||||
|
}
|
||||||
|
}
|
||||||
30
ext/.gitignore
vendored
Normal file
30
ext/.gitignore
vendored
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# CAP ext
|
||||||
|
_out
|
||||||
|
*.db
|
||||||
|
connection.properties
|
||||||
|
default-*.json
|
||||||
|
gen/
|
||||||
|
node_modules/
|
||||||
|
package-lock.json
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Web IDE, App Studio
|
||||||
|
.che/
|
||||||
|
.gen/
|
||||||
|
|
||||||
|
# MTA
|
||||||
|
*_mta_build_tmp
|
||||||
|
*.mtar
|
||||||
|
mta_archives/
|
||||||
|
|
||||||
|
# Other
|
||||||
|
.DS_Store
|
||||||
|
*.orig
|
||||||
|
*.log
|
||||||
|
|
||||||
|
*.iml
|
||||||
|
*.flattened-pom.xml
|
||||||
|
|
||||||
|
# IDEs
|
||||||
|
.vscode
|
||||||
|
.idea
|
||||||
6
ext/db/schema.cds
Normal file
6
ext/db/schema.cds
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
using { sap.capire.bookshop.Books } from '@capire/bookshop';
|
||||||
|
|
||||||
|
extend Books with {
|
||||||
|
ISBN : String;
|
||||||
|
discount : Decimal @assert.range:[0,1];
|
||||||
|
}
|
||||||
19
ext/package.json
Normal file
19
ext/package.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"name": "ext",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "A simple CAP project.",
|
||||||
|
"repository": "<Add your repository here>",
|
||||||
|
"license": "UNLICENSED",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@capire/bookshop": "../bookshop",
|
||||||
|
"@sap/cds": "^4",
|
||||||
|
"express": "^4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"sqlite3": "^4"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"start": "npx cds run"
|
||||||
|
}
|
||||||
|
}
|
||||||
40
ext/server.js
Normal file
40
ext/server.js
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
const cds = require ('@sap/cds')
|
||||||
|
const path = require ('path')
|
||||||
|
const fs = require ('fs')
|
||||||
|
const protected = {db:1,messaging:1,auth:1}
|
||||||
|
const { isfile } = cds.utils
|
||||||
|
|
||||||
|
cds.on('served', ()=>{
|
||||||
|
for (let each of cds.services) {
|
||||||
|
if (each.name in protected) continue
|
||||||
|
// search for local srv/<each>.js files and if exist
|
||||||
|
// activate them in a service extension sandbox
|
||||||
|
const impl = isfile (path.resolve('srv/'+each.name+'.js'))
|
||||||
|
if (impl) activate_sandboxed (each,impl)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function activate_sandboxed (srv,impl) {
|
||||||
|
console.log (`[cds] - extending ${srv.name} with sandboxed:`, {impl:path.relative(process.cwd(),impl)})
|
||||||
|
const src = fs.readFileSync (impl)
|
||||||
|
const sandbox = Object.keys(global).filter(name => name !== 'cds')
|
||||||
|
const fn = new Function (
|
||||||
|
'module','cds','global','process', ...sandbox,
|
||||||
|
src
|
||||||
|
)
|
||||||
|
// restricted sandboxed variant of 'module' and 'cds'
|
||||||
|
const module = {}
|
||||||
|
const cds = {
|
||||||
|
service: {
|
||||||
|
impl: fn=>fn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn (module,cds,undefined,undefined, ...sandbox.map((()=>(undefined))))
|
||||||
|
if (typeof module.exports === 'function') try {
|
||||||
|
module.exports.call (srv,srv)
|
||||||
|
} catch (e) {
|
||||||
|
console.log (e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = cds.server
|
||||||
10
ext/srv/AdminService.js
Normal file
10
ext/srv/AdminService.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
module.exports = cds.service.impl(function(){
|
||||||
|
|
||||||
|
this.before(['CREATE','UPDATE'],'Books', req => { //> ....
|
||||||
|
const book = req.data
|
||||||
|
if (book.stock < 10 && book.discount > 0.5) {
|
||||||
|
req.error('Hey, da sind so wenig übrig, die wollen wir nicht zu billig verticken')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
9
ext/srv/CatalogService.js
Normal file
9
ext/srv/CatalogService.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
console.log ('Böses Zeug', global, process, cds)
|
||||||
|
// process.exit()
|
||||||
|
// const fs = require('fs')
|
||||||
|
// cds.run('Böses Zeugs')
|
||||||
|
// SELECT.from ('Foo')
|
||||||
|
|
||||||
|
module.exports = cds.service.impl(function(){
|
||||||
|
this.after('READ','Books', each => each.title += ' (served through sandbox)')
|
||||||
|
})
|
||||||
@@ -1,2 +1 @@
|
|||||||
# cds.requires.messaging.kind = file-based-messaging
|
cds.features.snapi = y
|
||||||
PORT = 4004
|
|
||||||
@@ -7,19 +7,7 @@ AuthorID = Author ID
|
|||||||
Stock = Stock
|
Stock = Stock
|
||||||
Name = Name
|
Name = Name
|
||||||
AuthorName = Author's Name
|
AuthorName = Author's Name
|
||||||
DateOfBirth = Date of Birth
|
|
||||||
DateOfDeath = Date of Death
|
|
||||||
PlaceOfBirth = Place of Birth
|
|
||||||
PlaceOfDeath = Place of Death
|
|
||||||
Authors = Authors
|
Authors = Authors
|
||||||
Order = Order
|
Order = Order
|
||||||
Orders = Orders
|
Orders = Orders
|
||||||
Price = Price
|
Price = Price
|
||||||
|
|
||||||
Genre = Genre
|
|
||||||
Genres = Genres
|
|
||||||
SubGenres = Sub Genres
|
|
||||||
|
|
||||||
NumCode = Numeric Code
|
|
||||||
MinorUnit = Minor Unit
|
|
||||||
Exponent = Exponent
|
|
||||||
@@ -1,8 +1,5 @@
|
|||||||
sap.ui.define(["sap/fe/core/AppComponent"], function(AppComponent) {
|
sap.ui.define(["sap/fe/core/AppComponent"], ac => ac.extend("admin.Component", {
|
||||||
"use strict";
|
metadata:{ manifest:'json' }
|
||||||
return AppComponent.extend("admin.Component", {
|
}))
|
||||||
metadata: { manifest: "json" }
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
/* eslint no-undef:0 */
|
/* eslint no-undef:0 */
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
"sap.ui5": {
|
"sap.ui5": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"libs": {
|
"libs": {
|
||||||
"sap.fe.templates": {}
|
"sap.fe": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"models": {
|
"models": {
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
<head>
|
|
||||||
<meta http-equiv="refresh" content="0;url=vue/bookshop/index.html">
|
|
||||||
</head>
|
|
||||||
@@ -7,8 +7,6 @@ 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: [
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
sap.ui.define(["sap/fe/core/AppComponent"], function(AppComponent) {
|
sap.ui.define(["sap/fe/core/AppComponent"], ac => ac.extend("bookshop.Component", {
|
||||||
"use strict";
|
metadata:{ manifest:'json' }
|
||||||
return AppComponent.extend("bookshop.Component", {
|
}))
|
||||||
metadata: { manifest: "json" }
|
|
||||||
});
|
|
||||||
});
|
|
||||||
/* eslint no-undef:0 */
|
/* eslint no-undef:0 */
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
"sap.ui5": {
|
"sap.ui5": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"libs": {
|
"libs": {
|
||||||
"sap.fe.templates": {}
|
"sap.fe": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"models": {
|
"models": {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
using { sap.capire.bookshop as my } from '@capire/bookshop';
|
using { sap.capire.bookshop as my } from '@capire/bookshop';
|
||||||
using { sap.common } from '@capire/common';
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
//
|
//
|
||||||
@@ -28,6 +28,13 @@ annotate my.Books with @(
|
|||||||
author @ValueList.entity:'Authors';
|
author @ValueList.entity:'Authors';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
annotate my.Authors with @(
|
||||||
|
UI: {
|
||||||
|
Identification: [{Value:name}],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
//
|
//
|
||||||
// Books Details
|
// Books Details
|
||||||
@@ -59,199 +66,15 @@ 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>Name}';
|
name @title:'{i18n>AuthorName}';
|
||||||
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}';
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
applications: {
|
applications: {
|
||||||
"browse-books": {
|
"browse-books": {
|
||||||
title: "Browse Books",
|
title: "Browse Books",
|
||||||
description: "w/ SAP Fiori Elements",
|
description: "... testing FE v42",
|
||||||
additionalInformation: "SAPUI5.Component=bookshop",
|
additionalInformation: "SAPUI5.Component=bookshop",
|
||||||
applicationType : "URL",
|
applicationType : "URL",
|
||||||
url: "/browse/webapp",
|
url: "/browse/webapp",
|
||||||
@@ -21,15 +21,15 @@
|
|||||||
},
|
},
|
||||||
"manage-books": {
|
"manage-books": {
|
||||||
title: "Manage Books",
|
title: "Manage Books",
|
||||||
description: "w/ SAP Fiori Elements",
|
description: "... testing FE v42",
|
||||||
additionalInformation: "SAPUI5.Component=admin",
|
additionalInformation: "SAPUI5.Component=admin",
|
||||||
applicationType : "URL",
|
applicationType : "URL",
|
||||||
url: "/admin/webapp",
|
url: "/admin/webapp",
|
||||||
navigationMode: "embedded"
|
navigationMode: "embedded"
|
||||||
},
|
},
|
||||||
"manage-orders": {
|
"manage-orders": {
|
||||||
title: "Manage Orders",
|
title: "Order Books",
|
||||||
description: "w/ SAP Fiori Elements",
|
description: "... testing FE v42",
|
||||||
additionalInformation: "SAPUI5.Component=orders",
|
additionalInformation: "SAPUI5.Component=orders",
|
||||||
applicationType : "URL",
|
applicationType : "URL",
|
||||||
url: "/orders/webapp",
|
url: "/orders/webapp",
|
||||||
@@ -39,8 +39,8 @@
|
|||||||
};
|
};
|
||||||
</script>
|
</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/test-resources/sap/ushell/bootstrap/sandbox.js"></script>
|
||||||
<script id="sap-ui-bootstrap" src="https://sapui5.hana.ondemand.com/resources/sap-ui-core.js"
|
<script 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-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"
|
||||||
@@ -4,9 +4,7 @@
|
|||||||
|
|
||||||
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';
|
|
||||||
|
|||||||
@@ -1,27 +1,44 @@
|
|||||||
|
using OrdersService from '@capire/orders/srv/orders-service';
|
||||||
|
|
||||||
|
annotate OrdersService.Books with {
|
||||||
|
price @Common.FieldControl: #ReadOnly;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
//
|
//
|
||||||
// Note: this is designed for the OrdersService being co-located with
|
// Common
|
||||||
// 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,
|
||||||
using { OrdersService } from '../../srv/orders-service';
|
FieldControl: #Mandatory
|
||||||
|
},
|
||||||
|
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: OrderNo, Label:'OrderNo'},
|
{Value: createdBy, Label:'Customer'},
|
||||||
{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: {
|
||||||
@@ -45,7 +62,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: {
|
||||||
@@ -68,25 +85,36 @@ annotate OrdersService.Orders with @(
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
annotate OrdersService.Orders_Items with @(
|
//The enity types name is OrdersService.my_bookshop_OrderItems
|
||||||
|
//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: product_ID, Label:'Product ID'},
|
{Value: book_ID, Label:'Book'},
|
||||||
{Value: title, Label:'Product Title'},
|
//The following entry is only used to have the assoication followed in the read event
|
||||||
{Value: price, Label:'Unit Price'},
|
{Value: book.price, Label:'Book 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
|
|
||||||
);
|
);
|
||||||
};
|
|
||||||
5
fiori/app/orders/webapp/Component.js
Normal file
5
fiori/app/orders/webapp/Component.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
sap.ui.define(["sap/fe/core/AppComponent"], ac => ac.extend("orders.Component", {
|
||||||
|
metadata:{ manifest:'json' }
|
||||||
|
}))
|
||||||
|
|
||||||
|
/* eslint no-undef:0 */
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
"sap.ui5": {
|
"sap.ui5": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"libs": {
|
"libs": {
|
||||||
"sap.fe.templates": {}
|
"sap.fe": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"models": {
|
"models": {
|
||||||
@@ -121,7 +121,7 @@
|
|||||||
"name": "sap.fe.templates.ObjectPage",
|
"name": "sap.fe.templates.ObjectPage",
|
||||||
"options": {
|
"options": {
|
||||||
"settings" : {
|
"settings" : {
|
||||||
"entitySet": "Orders_Items"
|
"entitySet": "OrderItems"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
<head>
|
|
||||||
<meta http-equiv="refresh" content="0;url=vue/reviews/index.html">
|
|
||||||
</head>
|
|
||||||
3
fiori/db/schema.cds
Normal file
3
fiori/db/schema.cds
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
// Proxy for importing schema from bookshop sample
|
||||||
|
using from '@capire/bookshop';
|
||||||
|
namespace sap.capire.bookshop;
|
||||||
@@ -2,13 +2,11 @@
|
|||||||
"name": "@capire/fiori",
|
"name": "@capire/fiori",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@capire/bookshop": "*",
|
"@capire/bookshop": "../bookshop",
|
||||||
"@capire/reviews": "*",
|
"@capire/orders": "../orders",
|
||||||
"@capire/orders": "*",
|
"@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": {
|
||||||
"start": "cds run --in-memory?",
|
"start": "cds run --in-memory?",
|
||||||
@@ -16,12 +14,6 @@
|
|||||||
},
|
},
|
||||||
"cds": {
|
"cds": {
|
||||||
"requires": {
|
"requires": {
|
||||||
"ReviewsService": {
|
|
||||||
"kind": "odata", "model": "@capire/reviews"
|
|
||||||
},
|
|
||||||
"OrdersService": {
|
|
||||||
"kind": "odata", "model": "@capire/orders"
|
|
||||||
},
|
|
||||||
"db": {
|
"db": {
|
||||||
"kind": "sql"
|
"kind": "sql"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
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
|
|
||||||
3
fiori/srv/admin-service.cds
Normal file
3
fiori/srv/admin-service.cds
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
// Proxy for importing services from bookshop sample
|
||||||
|
using from '@capire/bookshop';
|
||||||
|
annotate AdminService with @impl:'srv/admin-service.js';
|
||||||
8
fiori/srv/admin-service.js
Normal file
8
fiori/srv/admin-service.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
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)
|
||||||
|
})
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
////////////////////////////////////////////////////////////////////////////
|
|
||||||
//
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
////////////////////////////////////////////////////////////////////////////
|
|
||||||
//
|
|
||||||
// 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', (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)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
|
|
||||||
@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, incl. draft choreography
|
|
||||||
#
|
|
||||||
@newOrderID = e939604c-ab83-4d4f-bdb6-95fe30b3773e
|
|
||||||
|
|
||||||
GET {{bookshop}}/orders/Orders
|
|
||||||
|
|
||||||
### Create order, still inactive
|
|
||||||
POST {{bookshop}}/orders/Orders
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
{"ID": "{{newOrderID}}"}
|
|
||||||
|
|
||||||
### Get inactive order. We have to specify `IsActiveEntity`.
|
|
||||||
GET {{bookshop}}/orders/Orders(ID={{newOrderID}},IsActiveEntity=false)
|
|
||||||
|
|
||||||
### Activate order using `.../<servicename>.draftActivate`
|
|
||||||
POST {{bookshop}}/orders/Orders(ID={{newOrderID}},IsActiveEntity=false)/OrdersService.draftActivate
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
### Get active order
|
|
||||||
GET {{bookshop}}/orders/Orders(ID={{newOrderID}},IsActiveEntity=true)
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 3.0 KiB |
@@ -1,35 +0,0 @@
|
|||||||
### 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:
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
@sap:registry=http://nexus.wdf.sap.corp:8081/nexus/repository/build.milestones.npm/
|
|
||||||
15
odata/.vscode/extensions.json
vendored
15
odata/.vscode/extensions.json
vendored
@@ -1,15 +0,0 @@
|
|||||||
{
|
|
||||||
// See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations.
|
|
||||||
// Extension identifier format: ${publisher}.${name}. Example: vscode.csharp
|
|
||||||
|
|
||||||
// List of extensions which should be recommended for users of this workspace.
|
|
||||||
"recommendations": [
|
|
||||||
"SirTobi.pegjs-language",
|
|
||||||
"tamuratak.vscode-pegjs",
|
|
||||||
"joeandaverde.vscode-pegjs-live"
|
|
||||||
],
|
|
||||||
// List of extensions recommended by VS Code that should not be recommended for users of this workspace.
|
|
||||||
"unwantedRecommendations": [
|
|
||||||
|
|
||||||
]
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,26 +0,0 @@
|
|||||||
const fs = require("fs");
|
|
||||||
const path = require("path");
|
|
||||||
const peg = require("pegjs");
|
|
||||||
const pegGrammarPath = path.join(__dirname, "/odata2cqn.pegjs");
|
|
||||||
const odataPegGrammar = fs.readFileSync(pegGrammarPath, {
|
|
||||||
encoding: "utf8",
|
|
||||||
flag: "r",
|
|
||||||
});
|
|
||||||
const parser = peg.generate(odataPegGrammar);
|
|
||||||
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
parse: {
|
|
||||||
url: parser.parse,
|
|
||||||
},
|
|
||||||
to: {
|
|
||||||
cqn: parser.parse,
|
|
||||||
url: (cqn) => pending(cqn)
|
|
||||||
},
|
|
||||||
serialize: (data) => pending(data),
|
|
||||||
deserialize: (body) => pending(body),
|
|
||||||
}
|
|
||||||
|
|
||||||
const pending = ()=>{
|
|
||||||
throw new Error ('Not yet implemented')
|
|
||||||
}
|
|
||||||
@@ -1,200 +0,0 @@
|
|||||||
/** ------------------------------------------
|
|
||||||
* This is a peg.js adaptation of the https://github.com/oasis-tcs/odata-abnf/blob/master/abnf/odata-abnf-construction-rules.txt
|
|
||||||
* which directly constructs CQN out of parsed sources.
|
|
||||||
*
|
|
||||||
* NOTE:
|
|
||||||
* In contrast to the OData ABNF source, which uses very detailedsemantic rules,
|
|
||||||
* this adaptation uses rather generic syntactic rules only, e.g. not distinguishing
|
|
||||||
* betwenn Collection Navigation or not knowing individual function names.
|
|
||||||
* This is to be open to future enhancements of the OData standard, as well as
|
|
||||||
* to improve error messages. For example a typo in a function name could be
|
|
||||||
* reported specifically instead of throwing a generic parser error.
|
|
||||||
*
|
|
||||||
* See also: https://docs.microsoft.com/en-us/odata/concepts/queryoptions-overview
|
|
||||||
* Future test cases http://docs.oasis-open.org/odata/odata/v4.0/errata03/os/complete/abnf/odata-abnf-testcases.xml
|
|
||||||
*
|
|
||||||
* Limitations: Type, Geo functions are not supported,
|
|
||||||
* maxdatetime, mindatetime, fractionalseconds,
|
|
||||||
* totaloffsetminutes, date, totalseconds,
|
|
||||||
* floor, ceiling also are not supported by CAP
|
|
||||||
*
|
|
||||||
* Examples:
|
|
||||||
* Books
|
|
||||||
* Books/201
|
|
||||||
* Books?$select=ID,title&$expand=author($select=name)&$filter=stock gt 1&$orderby=title
|
|
||||||
*/
|
|
||||||
|
|
||||||
// ---------- JavaScript Helpers -------------
|
|
||||||
{
|
|
||||||
const stack=[]
|
|
||||||
let SELECT
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------- Entity Paths ---------------
|
|
||||||
|
|
||||||
ODataRelativeURI // Note: case-sensitive!
|
|
||||||
= (p:path { SELECT = { from:p } })
|
|
||||||
( o"?"o QueryOption ( o'&'o QueryOption )* )? {
|
|
||||||
if (SELECT.expand) {
|
|
||||||
SELECT.columns = SELECT.expand
|
|
||||||
delete SELECT.expand
|
|
||||||
}
|
|
||||||
return { SELECT }
|
|
||||||
}
|
|
||||||
|
|
||||||
path
|
|
||||||
= crv:$("$count"/"$ref"/"$value") {return {ref:[crv]}}
|
|
||||||
/ head:identifier filter:(OPEN args CLOSE)? tail:( '/' p:path {return p} )? {
|
|
||||||
const ref = [ filter ? { id:head, where:filter[1] } : head ]
|
|
||||||
if (tail) ref.push (...tail.ref)
|
|
||||||
return {ref}
|
|
||||||
}
|
|
||||||
|
|
||||||
args
|
|
||||||
= val:( number / integer / string ) {return [{val}]}
|
|
||||||
/ ref:identifier o"="o val:( number / integer / string ) more:( COMMA args )? {
|
|
||||||
const args = [ {ref}, '=', {val} ]
|
|
||||||
if (more) args.push ('and', ...more[1])
|
|
||||||
return args
|
|
||||||
}
|
|
||||||
|
|
||||||
ref "a reference"
|
|
||||||
= head:identifier tail:( '/' identifier )* {
|
|
||||||
return { ref:[ head, ...tail ] }
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// ---------- Query Options ------------
|
|
||||||
|
|
||||||
QueryOption = ExpandOption
|
|
||||||
ExpandOption =
|
|
||||||
"$select=" o select ( COMMA select )* /
|
|
||||||
"$expand=" o expand ( COMMA expand )* /
|
|
||||||
"$filter=" o filter /
|
|
||||||
"$orderby=" o orderby /
|
|
||||||
"$top=" o top /
|
|
||||||
"$skip=" o skip /
|
|
||||||
"$search=" o search /
|
|
||||||
"$count=" o count
|
|
||||||
|
|
||||||
select
|
|
||||||
= col:ref {
|
|
||||||
(SELECT.expand || (SELECT.expand = [])).push(col)
|
|
||||||
return col
|
|
||||||
}
|
|
||||||
|
|
||||||
expand =
|
|
||||||
( c:select {c.expand='*'} )
|
|
||||||
( // --- nested query options, if any
|
|
||||||
(OPEN {
|
|
||||||
stack.push (SELECT)
|
|
||||||
SELECT = SELECT.expand[SELECT.expand.length-1]
|
|
||||||
SELECT.expand = []
|
|
||||||
})
|
|
||||||
ExpandOption ( o";"o ExpandOption )*
|
|
||||||
(CLOSE {
|
|
||||||
SELECT = stack.pop()
|
|
||||||
})
|
|
||||||
)? // --- end of nested query options
|
|
||||||
( COMMA expand )?
|
|
||||||
|
|
||||||
top
|
|
||||||
= val:integer {
|
|
||||||
(SELECT.limit || (SELECT.limit={})).rows = {val}
|
|
||||||
}
|
|
||||||
|
|
||||||
skip
|
|
||||||
= val:integer {
|
|
||||||
(SELECT.limit || (SELECT.limit={})).offset = {val}
|
|
||||||
}
|
|
||||||
|
|
||||||
search
|
|
||||||
= p:search_clause {SELECT.search = p}
|
|
||||||
search_clause = p:( n:NOT? {return n?[n]:[]} )(
|
|
||||||
OPEN xpr:search_clause CLOSE {p.push({xpr})}
|
|
||||||
/ val:(identifier/string) {p.push({val})}
|
|
||||||
)( ao:(AND/OR) more:search_clause {p.push(ao,...more)} )*
|
|
||||||
{return p}
|
|
||||||
|
|
||||||
filter
|
|
||||||
= p:where_clause {SELECT.where = p}
|
|
||||||
where_clause = p:( n:NOT? {return n?[n]:[]} )(
|
|
||||||
OPEN xpr:where_clause CLOSE {p.push({xpr})}
|
|
||||||
/ comp:comparison {p.push(...comp)}
|
|
||||||
/ func:boolish {p.push(func)}
|
|
||||||
)( ao:(AND/OR) more:where_clause {p.push(ao,...more)} )*
|
|
||||||
{return p}
|
|
||||||
|
|
||||||
orderby
|
|
||||||
= ref:ref sort:( _ s:$("asc"/"desc") {return s})? {
|
|
||||||
SELECT.orderby = $(ref, sort && {sort})
|
|
||||||
}
|
|
||||||
|
|
||||||
count
|
|
||||||
= c:$[^,?&()]+ { SELECT.count = true }
|
|
||||||
|
|
||||||
//
|
|
||||||
// ---------- Expressions ------------
|
|
||||||
|
|
||||||
|
|
||||||
comparison "a comparison"
|
|
||||||
= a:operand _ o:$("eq"/"ne"/"lt"/"gt"/"le"/"ge") _ b:operand {
|
|
||||||
const op = { eq:'=', ne:'!=', lt:'<', gt:'>', le:'<=', ge:'>=' }[o]||o
|
|
||||||
return [ a, op, b ]
|
|
||||||
}
|
|
||||||
|
|
||||||
operand "an operand"
|
|
||||||
= val:number {return Number.isSafeInteger(val) ? {val} : { val:String(val), literal:'number' }}
|
|
||||||
/ val:string {return {val}}
|
|
||||||
/ function
|
|
||||||
/ ref
|
|
||||||
|
|
||||||
function "a function call"
|
|
||||||
= func:$[a-z]+ OPEN a:operand more:( COMMA o:operand {return o} )* CLOSE
|
|
||||||
{ return { func, args:[a,...more] }}
|
|
||||||
|
|
||||||
boolish "a boolean function"
|
|
||||||
= func:("contains"/"endswith"/"startswith") OPEN a:operand COMMA b:operand CLOSE
|
|
||||||
{ return { func, args:[a,b] }}
|
|
||||||
|
|
||||||
NOT = o "not"i _ {return 'not'}
|
|
||||||
AND = _ "and"i _ {return 'and'}
|
|
||||||
OR = _ "or"i _ {return 'or'}
|
|
||||||
|
|
||||||
|
|
||||||
//
|
|
||||||
// ---------- Literals -----------
|
|
||||||
|
|
||||||
string "Edm.String"
|
|
||||||
= "'" s:$("''"/[^'])* "'"
|
|
||||||
{return s.replace(/''/g,"'")}
|
|
||||||
|
|
||||||
number
|
|
||||||
= x:$( [+-]? [0-9]+ ("."[0-9]+)? ("e"[0-9]+)? )
|
|
||||||
{return Number(x)}
|
|
||||||
|
|
||||||
integer
|
|
||||||
= x:$( [+-]? [0-9]+ )
|
|
||||||
{return parseInt(x)}
|
|
||||||
|
|
||||||
identifier
|
|
||||||
= $([a-zA-Z][_a-zA-Z0-9]*)
|
|
||||||
|
|
||||||
|
|
||||||
//
|
|
||||||
// ---------- Punctuation ----------
|
|
||||||
|
|
||||||
COLON = o":"o
|
|
||||||
COMMA = o","o
|
|
||||||
SEMI = o";"o
|
|
||||||
OPEN = o"("o
|
|
||||||
CLOSE = o")"
|
|
||||||
|
|
||||||
//
|
|
||||||
// ---------- Whitespaces -----------
|
|
||||||
|
|
||||||
o "optional whitespaces" = $[ \t\n]*
|
|
||||||
_ "mandatory whitespaces" = $[ \t\n]+
|
|
||||||
|
|
||||||
//
|
|
||||||
// ------------------------------------
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@sap/cds-odata",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"dependencies": {
|
|
||||||
"@sap/cds-compiler": "latest",
|
|
||||||
"@sap/cds": "^4.4.10"
|
|
||||||
},
|
|
||||||
"scripts": {
|
|
||||||
"postinstall": "rm -fr node_modules/@sap/cds/node_modules"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"pegjs": "^0.10.0"
|
|
||||||
},
|
|
||||||
"private": true
|
|
||||||
}
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
const cds = require("@sap/cds/lib"), {expect} = cds.test
|
|
||||||
cds.odata = require("../lib")
|
|
||||||
|
|
||||||
describe("$filter", () => {
|
|
||||||
|
|
||||||
describe("comparing expressions", () => {
|
|
||||||
|
|
||||||
const types = {
|
|
||||||
strings: "'some string'",
|
|
||||||
integers: 11,
|
|
||||||
decimals: 0.99,
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
it.each(Object.keys(types))("should support expressions with %s", (t) => {
|
|
||||||
expect (cds.odata.parse.url(`Foo?$filter=bar eq ${types[t]}`))
|
|
||||||
.to.eql (cds.parse.cql(`SELECT from Foo where bar = ${types[t]}`))
|
|
||||||
})
|
|
||||||
|
|
||||||
const operators = {
|
|
||||||
eq: '=',
|
|
||||||
lt: '<',
|
|
||||||
le: '<=',
|
|
||||||
gt: '>',
|
|
||||||
ge: '>=',
|
|
||||||
ne: '!=',
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
it.each(Object.keys(operators))("should support comparison operator '%s'", (op) => {
|
|
||||||
expect (cds.odata.parse.url(`Foo?$filter=bar ${op} 11`))
|
|
||||||
.to.eql (cds.parse.cql(`SELECT from Foo where bar ${operators[op]} 11`))
|
|
||||||
})
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("logical expressions", () => {
|
|
||||||
|
|
||||||
it.each(['and','or'])("should support '%s'", (t) => {
|
|
||||||
expect (cds.odata.parse.url(`Foo?$filter=bar lt 11 ${t} name eq 'some name'`))
|
|
||||||
.to.eql (cds.parse.cql(`SELECT from Foo where bar < 11 ${t} name = 'some name'`))
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should support 'not'", () => {
|
|
||||||
// REVISIT: We need to check with the Node.js team why they translated that to the equivalent of:
|
|
||||||
// not name like concat('%','sunny','%') escape '^'
|
|
||||||
expect (cds.odata.parse.url(`Foo?$filter= not contains(name,'sunny')`))
|
|
||||||
.to.eql (cds.parse.cql(`SELECT from Foo where not contains(name,'sunny')`))
|
|
||||||
});
|
|
||||||
|
|
||||||
// REVISIT: wait for compiler v2
|
|
||||||
it("should support group expr", () => {
|
|
||||||
expect (cds.odata.parse.url(`Foo?$filter= (unitPrice gt 11 and length(name) eq 12) or name eq 'Restless and Wild'`))
|
|
||||||
.to.eql (cds.parse.cql(`SELECT from Foo where (unitPrice > 11 and length(name) = 12) or name = 'Restless and Wild'`))
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("function expressions", () => {
|
|
||||||
|
|
||||||
it("should support contains", () => {
|
|
||||||
expect (cds.odata.parse.url(`Foo?$filter= contains(name,'sunny')`))
|
|
||||||
.to.eql (cds.parse.cql(`SELECT from Foo where contains(name,'sunny')`))
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should support startswith", () => {
|
|
||||||
expect (cds.odata.parse.url(`Foo?$filter= startswith(name,'sunny')`))
|
|
||||||
.to.eql (cds.parse.cql(`SELECT from Foo where startswith(name,'sunny')`))
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should support endswith", () => {
|
|
||||||
expect (cds.odata.parse.url(`Foo?$filter= endswith(name,'sunny')`))
|
|
||||||
.to.eql (cds.parse.cql(`SELECT from Foo where endswith(name,'sunny')`))
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should support length", () => {
|
|
||||||
expect (cds.odata.parse.url(`Foo?$filter= length(name) lt 11`))
|
|
||||||
.to.eql (cds.parse.cql(`SELECT from Foo where length(name) < 11`))
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should support indexof", () => {
|
|
||||||
expect (cds.odata.parse.url(`Foo?$filter= indexof(name,'x') eq 11`))
|
|
||||||
.to.eql (cds.parse.cql(`SELECT from Foo where indexof(name,'x') = 11`))
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should support substring", () => {
|
|
||||||
expect (cds.odata.parse.url(`Foo?$filter= substring(name,1) eq 'foo'`))
|
|
||||||
.to.eql (cds.parse.cql(`SELECT from Foo where substring(name,1) = 'foo'`))
|
|
||||||
});
|
|
||||||
|
|
||||||
it.each(['tolower','toupper','trim'])("should support '%s'", (fn) => {
|
|
||||||
expect (cds.odata.parse.url(`Foo?$filter= ${fn}(name) eq 'foo'`))
|
|
||||||
.to.eql (cds.parse.cql(`SELECT from Foo where ${fn}(name) = 'foo'`))
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should support 'day'", () => {
|
|
||||||
expect (cds.odata.parse.url(`Foo?$filter= day(name) eq 11`))
|
|
||||||
.to.eql (cds.parse.cql(`SELECT from Foo where day(name) = 11`))
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should support concat", () => {
|
|
||||||
expect (cds.odata.parse.url(`Foo?$filter= concat(name,'o') eq 'foo'`))
|
|
||||||
.to.eql (cds.parse.cql(`SELECT from Foo where concat(name,'o') = 'foo'`))
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
cds.requires.messaging.kind = file-based-messaging
|
|
||||||
PORT = 4006
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
<!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>
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
/*
|
|
||||||
This model controls what gets served to Fiori frontends...
|
|
||||||
*/
|
|
||||||
|
|
||||||
using from './orders/fiori-service';
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
sap.ui.define(["sap/fe/core/AppComponent"], function(AppComponent) {
|
|
||||||
"use strict";
|
|
||||||
return AppComponent.extend("orders.Component", {
|
|
||||||
metadata: { manifest: "json" }
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
/* eslint no-undef:0 */
|
|
||||||
4
orders/db/data/sap.capire.bookshop-OrderItems.csv
Normal file
4
orders/db/data/sap.capire.bookshop-OrderItems.csv
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
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
|
||||||
|
3
orders/db/data/sap.capire.bookshop-Orders.csv
Normal file
3
orders/db/data/sap.capire.bookshop-Orders.csv
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
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,3 +0,0 @@
|
|||||||
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,4 +0,0 @@
|
|||||||
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,26 +1,16 @@
|
|||||||
using { Currency, User, managed, cuid } from '@sap/cds/common';
|
using { sap.capire.bookshop.Books } from '@capire/bookshop';
|
||||||
namespace sap.capire.orders;
|
using { Currency, managed, cuid } from '@sap/cds/common';
|
||||||
|
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 Orders_Items on Items.up_ = $self;
|
Items : Composition of many OrderItems on Items.parent = $self;
|
||||||
buyer : User;
|
|
||||||
currency : Currency;
|
currency : Currency;
|
||||||
}
|
}
|
||||||
|
|
||||||
entity Orders_Items {
|
entity OrderItems : cuid {
|
||||||
key ID : UUID;
|
parent : Association to Orders;
|
||||||
up_ : Association to Orders;
|
book : Association to Books;
|
||||||
product : Association to Products @assert.integrity:false; // REVISIT: this is a temporary workaround for a glitch in cds-runtime
|
|
||||||
amount : Integer;
|
amount : Integer;
|
||||||
title : String;
|
netAmount : Decimal(9,2);
|
||||||
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';
|
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
/*
|
|
||||||
This model controls what gets exposed
|
|
||||||
*/
|
|
||||||
namespace sap.capire.orders;
|
|
||||||
using from './srv/orders-service';
|
|
||||||
using from './db/schema';
|
|
||||||
@@ -1,8 +1,4 @@
|
|||||||
{
|
{
|
||||||
"name": "@capire/orders",
|
"name": "@capire/orders",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0"
|
||||||
"dependencies": {
|
|
||||||
"@capire/common": "*",
|
|
||||||
"@sap/cds": "^4.3.0"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
using { sap.capire.orders as my } from '../db/schema';
|
using { sap.capire.bookshop 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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,37 +1,21 @@
|
|||||||
const cds = require('@sap/cds')
|
const cds = require('@sap/cds')
|
||||||
class OrdersService extends cds.ApplicationService {
|
|
||||||
|
|
||||||
/** register custom handlers */
|
module.exports = cds.service.impl(function() {
|
||||||
init(){
|
|
||||||
const { Orders_Items:OrderItems } = this.entities
|
|
||||||
|
|
||||||
this.before ('UPDATE', 'Orders', async function(req) {
|
const { Books } = cds.entities
|
||||||
const { ID, Items } = req.data
|
|
||||||
if (Items) for (let { product_ID, amount } of Items) {
|
// Reduce stock of ordered books if available stock suffices
|
||||||
const { amount:before } = await cds.tx(req).run (
|
this.before ('CREATE', 'Orders', (req) => {
|
||||||
SELECT.one.from (OrderItems, oi => oi.amount) .where ({up__ID:ID, product_ID})
|
const { Items: OrderItems } = req.data
|
||||||
|
return cds.transaction(req) .run (()=> OrderItems.map (order =>
|
||||||
|
UPDATE (Books) .where ('ID =', order.book_ID)
|
||||||
|
.and ('stock >=', order.amount)
|
||||||
|
.set ('stock -=', order.amount)
|
||||||
|
)) .then (all => all.forEach ((affectedRows,i) => {
|
||||||
|
if (affectedRows === 0) req.error (409,
|
||||||
|
`${OrderItems[i].amount} exceeds stock for book #${OrderItems[i].book_ID}`
|
||||||
)
|
)
|
||||||
if (amount != before) await 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) await Promise.all (Items.map(it => 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
|
|
||||||
|
|||||||
27
package.json
27
package.json
@@ -5,32 +5,23 @@
|
|||||||
"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": "./bookshop",
|
"@capire/bookshop": "file:bookshop",
|
||||||
"@capire/common": "./common",
|
"@capire/common": "file:common",
|
||||||
"@capire/fiori": "./fiori",
|
"@capire/fiori": "file:fiori",
|
||||||
"@capire/hello": "./hello",
|
"@capire/orders": "file:orders",
|
||||||
"@capire/media": "./media",
|
"@capire/reviews": "file:reviews",
|
||||||
"@capire/orders": "./orders",
|
"@capire/tests": "file:test"
|
||||||
"@capire/reviews": "./reviews"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"pegjs": "^0.10.0",
|
|
||||||
"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": "^5"
|
"sqlite3": "^4"
|
||||||
},
|
},
|
||||||
"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",
|
"jest": "npx jest --verbose",
|
||||||
"test": "npm run jest --silent"
|
"test": "npm run jest -s"
|
||||||
},
|
|
||||||
"mocha": {
|
|
||||||
"parallel": true
|
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
"testEnvironment": "node"
|
"testEnvironment": "node"
|
||||||
|
|||||||
@@ -1,2 +1 @@
|
|||||||
cds.requires.messaging.kind = file-based-messaging
|
PORT = 5005
|
||||||
PORT = 4005
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
/* 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 = {}
|
|
||||||
},
|
|
||||||
|
|
||||||
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()
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
<!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>
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
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.
|
|
||||||
|
@@ -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 : Integer enum {
|
type Rating : Decimal(3,2) enum {
|
||||||
Best = 5;
|
Best = 5;
|
||||||
Good = 4;
|
Good = 4;
|
||||||
Avg = 3;
|
Avg = 3;
|
||||||
|
|||||||
@@ -1,2 +1 @@
|
|||||||
using from './srv/reviews-service';
|
using from './srv/reviews-service';
|
||||||
namespace sap.capire.reviews;
|
|
||||||
|
|||||||
@@ -7,17 +7,20 @@
|
|||||||
"index.cds"
|
"index.cds"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sap/cds": "^4",
|
"@sap/cds": ">=3.33.1",
|
||||||
"express": "^4.17.1"
|
"express": "^4.17.1"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"reviews-service": "cds watch",
|
"reviews-service": "cds watch",
|
||||||
"books-reviewed": "cds watch ../reviewed"
|
"bookshop": "cds watch test/bookshop"
|
||||||
},
|
},
|
||||||
"cds": {
|
"cds": {
|
||||||
"requires": {
|
"requires": {
|
||||||
"db": {
|
"db": {
|
||||||
"kind": "sql"
|
"kind": "sql"
|
||||||
|
},
|
||||||
|
"messaging": {
|
||||||
|
"kind": "file-based-messaging"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,34 +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 @assert.enum;
|
rating @mandatory @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' },
|
||||||
//
|
|
||||||
// Temporarily disabling this due to glitch in CAP Node.js runtime:
|
|
||||||
// { grant:'UPDATE', to:'authenticated-user', where:'reviewer=$user' },
|
|
||||||
// -> reenable it when the issue is fixed
|
|
||||||
{ grant:'UPDATE', to:'authenticated-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' },
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -10,20 +10,23 @@ module.exports = cds.service.impl (function(){
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Emit an event to inform subscribers about new avg ratings for reviewed subjects
|
// Emit an event to inform subscribers about new avg ratings for reviewed subjects
|
||||||
this.after (['CREATE','UPDATE','DELETE'], 'Reviews', async function(_,req) {
|
// ( Note: req.on.succeeded ensures we only do that if there's no error )
|
||||||
|
this.after (['CREATE','UPDATE','DELETE'], 'Reviews', async(_,req) => {
|
||||||
const {subject} = req.data
|
const {subject} = req.data
|
||||||
const {rating} = await cds.tx(req) .run (
|
const {rating} = await cds.transaction(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 })
|
||||||
await 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.tx(req)
|
const tx = cds.transaction(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})
|
||||||
@@ -34,7 +37,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.tx(req)
|
const tx = cds.transaction(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}))
|
||||||
})
|
})
|
||||||
|
|||||||
36
reviews/test/4004.http
Normal file
36
reviews/test/4004.http
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
#################################################
|
||||||
|
#
|
||||||
|
# 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
|
||||||
13
reviews/test/5005.http
Normal file
13
reviews/test/5005.http
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
#################################################
|
||||||
|
#
|
||||||
|
# To ReviewsService running as separate process
|
||||||
|
#
|
||||||
|
|
||||||
|
GET http://localhost:5005/reviews/Reviews?
|
||||||
|
|
||||||
|
###
|
||||||
|
|
||||||
|
POST http://localhost:5005/reviews/Reviews
|
||||||
|
Content-Type: application/json;IEEE754Compatible=true
|
||||||
|
|
||||||
|
{"subject":"201", "title":"boo"}
|
||||||
23
reviews/test/bookshop/package.json
Normal file
23
reviews/test/bookshop/package.json
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
16
reviews/test/bookshop/schema.cds
Normal file
16
reviews/test/bookshop/schema.cds
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
//
|
||||||
|
// 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;
|
||||||
51
reviews/test/bookshop/server.js
Normal file
51
reviews/test/bookshop/server.js
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
//
|
||||||
|
// This is an example of using a project-local server.js to intercept
|
||||||
|
// the default bootstrapping process.
|
||||||
|
//
|
||||||
|
|
||||||
|
const cds = require ('@sap/cds')
|
||||||
|
|
||||||
|
// Mashup services after all are served...
|
||||||
|
cds.once('served', async()=>{
|
||||||
|
|
||||||
|
// react on event messages from reviews service
|
||||||
|
const ReviewsService = await cds.connect.to ('ReviewsService')
|
||||||
|
const db = await cds.connect.to ('db')
|
||||||
|
ReviewsService.on ('reviewed', (msg) => {
|
||||||
|
console.debug ('> received:', msg.event, msg.data)
|
||||||
|
const { Books } = db.entities('sap.capire.bookshop')
|
||||||
|
const { subject, rating } = msg.data
|
||||||
|
const tx = db.tx (msg) // TODO: db.tx(msg) fully implemented?
|
||||||
|
return tx.update (Books,subject) .with ({rating})
|
||||||
|
})
|
||||||
|
|
||||||
|
// delegate requests to read reviews to ReviewsService
|
||||||
|
const CatalogService = await cds.connect.to ('CatalogService')
|
||||||
|
CatalogService.impl (srv => srv.on ('READ', 'Books/reviews', (req) => {
|
||||||
|
console.debug ('> delegating to ReviewsService')
|
||||||
|
const { Reviews } = ReviewsService.entities
|
||||||
|
const [ subject ] = req.params
|
||||||
|
const tx = ReviewsService.tx (req)
|
||||||
|
return tx.read (Reviews) .where({subject}) .columns (req.query.SELECT.columns)
|
||||||
|
}))
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
// Other bootstrapping events you could hook in to...
|
||||||
|
/* eslint-disable no-unused-vars */
|
||||||
|
cds.on('bootstrap',(app) => {/* ... */})
|
||||||
|
cds.on('loaded', (model) => {/* ... */})
|
||||||
|
cds.on('connect', (srv) => {/* ... */})
|
||||||
|
cds.on('serving', (srv) => {/* ... */})
|
||||||
|
cds.once('listening', ({server,url}) => {/* ... */})
|
||||||
|
|
||||||
|
|
||||||
|
// Delegate bootstrapping to built-in server.js
|
||||||
|
module.exports = cds.server
|
||||||
|
|
||||||
|
// Monkey-patching older releases
|
||||||
|
if (cds.version < '3.33.4') cds.once('listening', ()=> cds.emit('served'))
|
||||||
|
|
||||||
|
// Launch server if started directly from command-line
|
||||||
|
if (!module.parent) cds.server()
|
||||||
39
samples.md
39
samples.md
@@ -1,28 +1,28 @@
|
|||||||
# Overview of Samples
|
# Overview of Samples
|
||||||
|
|
||||||
The following list gives an overview of the samples provided in subdirectories.
|
The list below gives an overview of the samples provided in subdirectories.
|
||||||
Each sub directory essentially is an 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.
|
||||||
|
|
||||||
|
|
||||||
## [@capire/hello-world](hello)
|
## [hello](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).
|
||||||
|
|
||||||
|
|
||||||
## [@capire/bookshop](bookshop)
|
## [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)
|
||||||
- [Domain Modeling](https://cap.cloud.sap/docs/guides/domain-models)
|
- [Domain Modelling](https://cap.cloud.sap/docs/guides/domain-models)
|
||||||
- [Defining Services](https://cap.cloud.sap/docs/guides/providing-services)
|
- [Defining Services](https://cap.cloud.sap/docs/guides/providing-services)
|
||||||
- [Generic Providers](https://cap.cloud.sap/docs/guides/generic-providers)
|
- [Generic Providers](https://cap.cloud.sap/docs/guides/generic-providers)
|
||||||
- [Adding Custom Logic](https://cap.cloud.sap/docs/guides/service-impl)
|
- [Adding Custom Logic](https://cap.cloud.sap/docs/guides/service-impl)
|
||||||
- [Using Databases](https://cap.cloud.sap/docs/guides/databases)
|
- [Using Databases](https://cap.cloud.sap/docs/guides/databases)
|
||||||
|
|
||||||
|
|
||||||
## [@capire/common](common)
|
## [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)
|
||||||
- Providing [reuse packages](https://cap.cloud.sap/docs/get-started/projects#sharing-and-reusing-content)
|
- Providing [reuse packages](https://cap.cloud.sap/docs/get-started/projects#sharing-and-reusing-content)
|
||||||
- [Verticalization](https://cap.cloud.sap/docs/cds/common#adapting-to-your-needs)
|
- [Verticalization](https://cap.cloud.sap/docs/cds/common#adapting-to-your-needs)
|
||||||
@@ -30,39 +30,34 @@ Each sub directory essentially is an individual npm package arranged in an [all-
|
|||||||
- Used in the [fiori app sample](#fiori)
|
- Used in the [fiori app sample](#fiori)
|
||||||
|
|
||||||
|
|
||||||
## [@capire/orders](orders)
|
## [orders](orders)
|
||||||
|
|
||||||
- A standalone orders management service, demonstrating:
|
- Adds orders to the [bookshop](#bookshop), thereby 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)
|
||||||
|
|
||||||
|
|
||||||
## [@capire/reviews](reviews)
|
## [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
|
||||||
- Serving requests synchronously
|
- Serving requests synchronously
|
||||||
- Emitting events asynchronously
|
- Emitting events asynchronously
|
||||||
- Grow as you go, with:
|
- Grow as you go, with...
|
||||||
- Mocking app services
|
- Mocking app services
|
||||||
- Running service meshes
|
- Running service meshes
|
||||||
- Late-cut Micro Services
|
- Late-cut Micro Services
|
||||||
- As well as managed data, input validations, and authorization
|
- As well as managed data, input validations and authorization
|
||||||
|
|
||||||
|
|
||||||
## [@capire/fiori](fiori)
|
## [fiori](fiori)
|
||||||
|
|
||||||
- A [composite app, reusing and combining](https://cap.cloud.sap/docs/guides/verticalize) these packages:
|
- [Adds a Fiori elements application](https://cap.cloud.sap/docs/guides/fiori/), introducing to...
|
||||||
- [@capire/bookshop](bookshop)
|
|
||||||
- [@capire/reviews](reviews)
|
|
||||||
- [@capire/orders](orders)
|
|
||||||
- [@capire/common](common)
|
|
||||||
- [Adds a SAP 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 SAP Fiori apps locally
|
- Serving Fiori apps locally
|
||||||
- [The Vue.js app](bookshop/app/vue) imported from bookshop is served as well
|
- Combining most of the other samples through [package reuse](https://cap.cloud.sap/docs/get-started/projects#sharing-and-reusing-content)
|
||||||
|
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|||||||
151
test/_cds_tests.js
Normal file
151
test/_cds_tests.js
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
//
|
||||||
|
// 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
|
||||||
1
test/capire.js
Normal file
1
test/capire.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
module.exports = require('./_cds_tests').for(__dirname,'..')
|
||||||
@@ -1,9 +1,15 @@
|
|||||||
const { expect } = require('../test')
|
const { expect } = require('./capire')
|
||||||
const cds = require('@sap/cds/lib')
|
const cds = require('@sap/cds')
|
||||||
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
|
||||||
@@ -13,9 +19,8 @@ describe('cds.ql → cqn', () => {
|
|||||||
//
|
//
|
||||||
let cqn
|
let cqn
|
||||||
|
|
||||||
describe.skip(`BUGS + GAPS...`, () => {
|
describe(`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: ['*'] },
|
||||||
})
|
})
|
||||||
@@ -25,17 +30,29 @@ describe('cds.ql → cqn', () => {
|
|||||||
.to.eql(SELECT.from(Foo, ['*']))
|
.to.eql(SELECT.from(Foo, ['*']))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
!(cdr ? it : it.skip)('should correctly handle { ... and:{...} }', () => {
|
||||||
it('should consistently handle lists', () => {
|
expect(SELECT.from(Foo).where({ x: 1, and: { y: 2, or: { z: 3 } } })).to.eql({
|
||||||
const ID = 11, args = [`foo`, "'bar'", 3]
|
SELECT: {
|
||||||
const cqn = CQL`SELECT from Foo where ID=11 and x in (foo,'bar',3)`
|
from: { ref: ['Foo'] },
|
||||||
expect(SELECT.from(Foo).where(`ID=${ID} and x in (${args})`)).to.eql(cqn)
|
where: [
|
||||||
expect(SELECT.from(Foo).where(`ID=`, ID, `and x in`, args)).to.eql(cqn)
|
{ ref: ['x'] },
|
||||||
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 )', () => {
|
||||||
@@ -167,6 +184,7 @@ 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(
|
||||||
@@ -194,6 +212,7 @@ 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(
|
||||||
@@ -242,30 +261,7 @@ describe('cds.ql → cqn', () => {
|
|||||||
// same for works distinct
|
// same for works distinct
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should correctly handle { ... and:{...} }', () => {
|
xtest('where ( ... cql | {x:y} )', () => {
|
||||||
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
|
||||||
|
|
||||||
@@ -302,7 +298,7 @@ describe('cds.ql → cqn', () => {
|
|||||||
')',
|
')',
|
||||||
]
|
]
|
||||||
: [
|
: [
|
||||||
// '(', //> this one is not required
|
'(', //> this one is not required
|
||||||
{ ref: ['ID'] },
|
{ ref: ['ID'] },
|
||||||
'=',
|
'=',
|
||||||
{ val: ID },
|
{ val: ID },
|
||||||
@@ -311,7 +307,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%' },
|
||||||
@@ -325,26 +321,7 @@ describe('cds.ql → cqn', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// using CQL fragments -> uses cds.parse.expr
|
// using CQL fragments -> uses cds.parse.expr
|
||||||
const is_v2 = !!cds.parse.expr('(1,2)').list
|
expect((cqn = CQL`SELECT from Foo where ID=11 and x in ( foo, 'bar', 3)`)).to.eql({
|
||||||
if (is_v2) expect((cqn = CQL`SELECT from Foo where ID=11 and x in ( foo, 'bar', 3)`)).to.eql({
|
|
||||||
SELECT: {
|
|
||||||
from: { ref: ['Foo'] },
|
|
||||||
where: [
|
|
||||||
{ ref: ['ID'] },
|
|
||||||
'=',
|
|
||||||
{ val: ID },
|
|
||||||
'and',
|
|
||||||
{ ref: ['x'] },
|
|
||||||
'in',
|
|
||||||
{list:[
|
|
||||||
{ ref: ['foo'] },
|
|
||||||
{ val: 'bar' },
|
|
||||||
{ val: 3 },
|
|
||||||
]}
|
|
||||||
],
|
|
||||||
},
|
|
||||||
})
|
|
||||||
else expect((cqn = CQL`SELECT from Foo where ID=11 and x in ( foo, 'bar', 3)`)).to.eql({
|
|
||||||
SELECT: {
|
SELECT: {
|
||||||
from: { ref: ['Foo'] },
|
from: { ref: ['Foo'] },
|
||||||
where: [
|
where: [
|
||||||
@@ -364,6 +341,8 @@ 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, `)`)
|
||||||
@@ -487,38 +466,47 @@ 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 + data', () => {
|
test('with', () => {
|
||||||
if (cds.version < '4.1.0') return
|
expect(UPDATE(Foo).with(`foo=`, 11, `bar-=`, 22))
|
||||||
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: {
|
data: { foo: 11 },
|
||||||
car: "foo's bar, car",
|
|
||||||
},
|
|
||||||
with: {
|
with: {
|
||||||
bar: { func: 'coalesce', args: [{ ref: ['x'] }, { ref: ['y'] }] },
|
bar: { xpr: [{ ref: ['bar'] }, '-', { val: 22 }] },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,26 +1,30 @@
|
|||||||
const { expect } = require('../test') .run (
|
const { expect } = require('./capire')
|
||||||
'serve', 'AdminService', '--from', '@capire/bookshop,@capire/common', '--in-memory'
|
const cds = require('@sap/cds')
|
||||||
)
|
|
||||||
const cds = require('@sap/cds/lib')
|
|
||||||
|
|
||||||
describe('Consuming Services locally', () => {
|
describe('Consuming Services locally', () => {
|
||||||
//
|
//
|
||||||
it('bootrapped the database successfully', ()=>{
|
before('bootstrap db and services', async () => {
|
||||||
const { AdminService } = cds.services
|
const model = await cds.load(['@capire/bookshop', '@capire/common'])
|
||||||
|
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
|
||||||
const _ = expect (await AdminService.read(Authors))
|
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 (cds.version >= '4.2.0')
|
if (AdminService.read.fix_50)
|
||||||
_.to.eql(await AdminService.run(SELECT.from('Authors')))
|
expect(await AdminService.run(SELECT.from(Authors))).to.eql(
|
||||||
|
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 () => {
|
||||||
@@ -35,8 +39,9 @@ describe('Consuming Services locally', () => {
|
|||||||
})
|
})
|
||||||
}).where(`name like`, 'E%')
|
}).where(`name like`, 'E%')
|
||||||
// temporary workaround
|
// temporary workaround
|
||||||
if (cds.version < '4.2.0')
|
if (!AdminService.read.fix_50) {
|
||||||
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([
|
||||||
{
|
{
|
||||||
@@ -74,14 +79,17 @@ 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%'))
|
.to.eql(await db.read(Authors, projection).where(`name like`, 'E%')) // FIXME!!
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
const { GET, POST, expect } = require('../test') .run ('bookshop')
|
|
||||||
const cds = require('@sap/cds/lib')
|
|
||||||
if (cds.User.default) cds.User.default = cds.User.Privileged // hard core monkey patch
|
|
||||||
else cds.User = cds.User.Privileged // hard core monkey patch for older cds releases
|
|
||||||
|
|
||||||
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(
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user