Compare commits
4 Commits
add/custom
...
dreiklang
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c49454f44 | ||
|
|
d570ad20b2 | ||
|
|
d891a1b905 | ||
|
|
9f604b9d13 |
6
.github/workflows/node.js.yml
vendored
6
.github/workflows/node.js.yml
vendored
@@ -5,9 +5,9 @@ name: CI
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ main ]
|
branches: [ master ]
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ main ]
|
branches: [ master ]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@@ -24,5 +24,5 @@ jobs:
|
|||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
- run: npm ci
|
- run: npm install
|
||||||
- run: npm test
|
- run: npm test
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -15,5 +15,3 @@ default-env.json
|
|||||||
packages/messageBox
|
packages/messageBox
|
||||||
reviews/msg-box
|
reviews/msg-box
|
||||||
reviews/db/test.db
|
reviews/db/test.db
|
||||||
|
|
||||||
*.openapi3.json
|
|
||||||
|
|||||||
3
.npmrc
3
.npmrc
@@ -1,3 +0,0 @@
|
|||||||
# Ensure we always use public packages, i.e. avoid using local registries from ~/.npmrc
|
|
||||||
@sap:registry=https://registry.npmjs.org/
|
|
||||||
registry=https://registry.npmjs.org/
|
|
||||||
@@ -1,24 +1,18 @@
|
|||||||
const { exec } = require ('child_process')
|
const { exec } = require ('child_process')
|
||||||
const isWin = process.platform === 'win32'
|
|
||||||
const express = require ('express')
|
const express = require ('express')
|
||||||
const fs = require ('fs')
|
const fs = require ('fs')
|
||||||
const app = express()
|
const app = express()
|
||||||
|
|
||||||
const { PORT=4444 } = process.env
|
const { PORT=4444 } = process.env
|
||||||
const [,,port=PORT,scope='@capire'] = process.argv
|
const [,,port=PORT] = process.argv
|
||||||
const cwd = __dirname
|
const cwd = __dirname
|
||||||
|
|
||||||
// clean up on start (exit handler might not complete on Windows)
|
|
||||||
exec(isWin ? 'del *.tgz' : 'rm *.tgz', {cwd})
|
|
||||||
|
|
||||||
|
|
||||||
app.use('/-/:tarball', (req,res,next) => {
|
app.use('/-/:tarball', (req,res,next) => {
|
||||||
console.debug ('GET', req.params)
|
console.debug ('GET', req.params)
|
||||||
try {
|
try {
|
||||||
const { tarball } = req.params
|
const { tarball } = req.params
|
||||||
const [, pkg ] = /^\w+-(\w+)/.exec(tarball)
|
const [, pkg ] = /^capire-(\w+)/.exec(tarball)
|
||||||
fs.lstat(tarball,(err => {
|
fs.lstat(tarball,(err => {
|
||||||
if (err) console.debug (`npm pack ../${pkg}`)
|
|
||||||
if (err) exec(`npm pack ../${pkg}`,{cwd},next)
|
if (err) exec(`npm pack ../${pkg}`,{cwd},next)
|
||||||
else next()
|
else next()
|
||||||
}))
|
}))
|
||||||
@@ -31,14 +25,12 @@ app.use('/-/:tarball', (req,res,next) => {
|
|||||||
app.use('/-', express.static(__dirname))
|
app.use('/-', express.static(__dirname))
|
||||||
|
|
||||||
app.get('/*', (req,res)=>{
|
app.get('/*', (req,res)=>{
|
||||||
const urlRegex = /^\/(@\w+)\/(\w+)/
|
|
||||||
const url = decodeURIComponent(req.url)
|
const url = decodeURIComponent(req.url)
|
||||||
console.debug ('GET',url)
|
console.debug ('GET',url)
|
||||||
try {
|
try {
|
||||||
if (!urlRegex.test(url)) return res.sendStatus(404)
|
const [, capire, pkg ] = /^\/(@capire)\/(\w+)/.exec(url)
|
||||||
const [, scpe, pkg ] = urlRegex.exec(url)
|
const package = require (`${capire}/${pkg}/package.json`)
|
||||||
const package = require (`${scpe}/${pkg}/package.json`)
|
const tarball = `capire-${pkg}-${package.version}.tgz`
|
||||||
const tarball = `${scpe.slice(1)}-${pkg}-${package.version}.tgz`
|
|
||||||
// https://github.com/npm/registry/blob/master/docs/responses/package-metadata.md
|
// https://github.com/npm/registry/blob/master/docs/responses/package-metadata.md
|
||||||
res.json({
|
res.json({
|
||||||
"name": package.name,
|
"name": package.name,
|
||||||
@@ -50,30 +42,29 @@ app.get('/*', (req,res)=>{
|
|||||||
"name": package.name,
|
"name": package.name,
|
||||||
"version": package.version,
|
"version": package.version,
|
||||||
"dist": {
|
"dist": {
|
||||||
"tarball": `${server.url}/-/${tarball}`
|
"tarball": `http://localhost:${port}/-/${tarball}`
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.code === 'MODULE_NOT_FOUND') return res.sendStatus(404)
|
console.error(e)
|
||||||
console.error(e); throw e
|
res.sendStatus(404)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const server = app.listen(port, ()=>{
|
app.listen(port, ()=>{
|
||||||
const url = server.url = `http://localhost:${server.address().port}`
|
console.log (`npm set @capire:registry=http://localhost:${port}`)
|
||||||
console.log (`npm set ${scope}:registry=${url}`)
|
console.log (`@capire registry listening on http://localhost:${port}`)
|
||||||
exec(`npm set ${scope}:registry=${url}`)
|
exec(`npm set @capire:registry=http://localhost:${port}`)
|
||||||
console.log (`${scope} registry listening on ${url}`)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
const _exit = ()=>{
|
const _exit = ()=>{
|
||||||
server.close()
|
console.log ('\nnpm conf rm @capire:registry')
|
||||||
exec(`npm conf rm "${scope}:registry"`, ()=> { process.exit() })
|
exec('npm conf rm @capire:registry')
|
||||||
|
exec('rm *.tgz')
|
||||||
|
process.exit()
|
||||||
}
|
}
|
||||||
|
|
||||||
process.on ('SIGTERM',_exit)
|
process.on ('SIGTERM',_exit)
|
||||||
process.on ('SIGHUP',_exit)
|
process.on ('SIGHUP',_exit)
|
||||||
process.on ('SIGINT',_exit)
|
process.on ('SIGINT',_exit)
|
||||||
|
|||||||
@@ -68,35 +68,26 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"file": "fiori/package.json",
|
"file": "fiori/package.json",
|
||||||
"description": "#### Configuration\n\nThe `cds.requires` section in `package.json` is a place to configure which of the `db/sqlite` and `db/hana` folders are used for which database.\n\nWe use [Node.js profiles](https://cap.cloud.sap/docs/node.js/cds-env#profiles) to separate the configuration.\nIn the `development` profile, you can see that `db/sqlite` is set as the model, while the `db/hana` folder is configured in the `production` profile. `db-ext` is a pseudo datasource, its name doesn't matter.\n\nSee [`cds.resolve`](https://cap.cloud.sap/docs/node.js/cds-compile#cds-resolve) to learn more about how models are found.",
|
"description": "#### Configuration\n\nThe `cds` section in `package.json` is a place to configure which of the `db/sqlite` and `db/hana` folders are used for which database.\nWe use [Node.js profiles](https://cap.cloud.sap/docs/node.js/cds-env#profiles) to separate the configuration.\nIn the `development` profile, you can see that `db/sqlite` is set as the model, while the `db/hana` folder is configured in the `production` profile.",
|
||||||
"selection": {
|
"line": 17,
|
||||||
"start": {
|
|
||||||
"line": 41,
|
|
||||||
"character": 1
|
|
||||||
},
|
|
||||||
"end": {
|
|
||||||
"line": 48,
|
|
||||||
"character": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"title": "Configuration"
|
"title": "Configuration"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"file": "fiori/package.json",
|
"file": "fiori/package.json",
|
||||||
"description": "#### Run with SQLite\n\nTo run with `development` and an in-memory SQLite database, you don't need to do anything special, because it's activated by default. Just run:\n\n>> cds watch fiori\n\nThen open [http://localhost:4004/admin/Authors](http://localhost:4004/admin/Authors) to see the two new fields.\n",
|
"description": "#### Run with SQLite\n\nTo run with `development` and an in-memory SQLite database, you don't need to do anything special, because it's activated by default. Just run:\n\n>> cds watch fiori\n\nThen open [http://localhost:4004/admin/Authors](http://localhost:4004/admin/Authors) to see the two new fields.\n",
|
||||||
"line": 43,
|
"line": 28,
|
||||||
"title": "Run with SQLite"
|
"title": "Run with SQLite"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"file": "fiori/package.json",
|
"file": "fiori/package.json",
|
||||||
"description": "#### Deploy the CDS Model to SAP HANA\n\nTo 'activate' SAP HANA through the `production` profile, you can use the global `--production` flag:\n\n>> cd fiori; cds deploy --to hana --production\n\n[Learn more about SAP HANA deployment](https://cap.cloud.sap/docs/guides/databases#get-hana)\n\n#### Run the Application\n\n>> cd fiori; cds watch --production\n\nThe service on [http://localhost:4004/admin/Authors](http://localhost:4004/admin/Authors) is the same as before, but this time the `Authors` entity is backed by a database view with an SAP HANA function.\n\n#### More\n\nIf you don't see data, you can add some in the next step.",
|
"description": "#### Deploy the CDS Model to SAP HANA\n\nTo 'activate' SAP HANA through the `production` profile, you can use the global `--production` flag:\n\n>> cd fiori; cds deploy --to hana --production\n\n[Learn more about SAP HANA deployment](https://cap.cloud.sap/docs/guides/databases#get-hana)\n\n#### Run the Application\n\n>> cd fiori; cds watch --production\n\nThe service on [http://localhost:4004/admin/Authors](http://localhost:4004/admin/Authors) is the same as before, but this time the `Authors` entity is backed by a database view with an SAP HANA function.\n\n#### More\n\nIf you don't see data, you can add some in the next step.",
|
||||||
"line": 46,
|
"line": 31,
|
||||||
"title": "Run with SAP HANA"
|
"title": "Run with SAP HANA"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"file": "fiori/test/requests.http",
|
"file": "fiori/test/requests.http",
|
||||||
"description": "### Add More Data\n\nOptionally you can add some `Authors` data by clicking on the _Send Request_ link (provided by the [REST client](https://marketplace.visualstudio.com/items?itemName=humao.rest-client) extension).",
|
"description": "### Add More Data\n\nOptionally you can add some `Authors` data by clicking on the _Send Request_ link (provided by the [REST client](https://marketplace.visualstudio.com/items?itemName=humao.rest-client) extension).",
|
||||||
"line": 72,
|
"line": 68,
|
||||||
"selection": {
|
"selection": {
|
||||||
"start": {
|
"start": {
|
||||||
"line": 67,
|
"line": 67,
|
||||||
@@ -113,5 +104,6 @@
|
|||||||
"title": "Wrap-up",
|
"title": "Wrap-up",
|
||||||
"description": "### Summary\n\nThat's it! You have seen: \n- How to integrate database-specific functions in a CDS model.\n- How to switch between the two implementations for SQLite and SAP HANA."
|
"description": "### Summary\n\nThat's it! You have seen: \n- How to integrate database-specific functions in a CDS model.\n- How to switch between the two implementations for SQLite and SAP HANA."
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"ref": "master"
|
||||||
}
|
}
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"file": "hello/srv/world.cds",
|
"file": "hello/world.cds",
|
||||||
"description": "### Hello World!\n\nThis is 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).",
|
"description": "### Hello World!\n\nThis is 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).",
|
||||||
"line": 2,
|
"line": 2,
|
||||||
"selection": {
|
"selection": {
|
||||||
@@ -68,7 +68,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"file": "orders/db/schema.cds",
|
"file": "orders/db/schema.cds",
|
||||||
"description": "### Orders - Compositions and Serving Documents\n\nA standalone orders management service, demonstrating:\n- Using [Compositions](https://cap.cloud.sap/docs/cds/cdl#compositions) in [Domain Models](https://cap.cloud.sap/docs/guides/domain-models), along with\n- [Serving deeply nested documents](https://cap.cloud.sap/docs/guides/generic-providers#serving-structured-data)\n",
|
"description": "### Compositions and Serving Documents\n\nA standalone orders management service, demonstrating:\n- Using [Compositions](https://cap.cloud.sap/docs/cds/cdl#compositions) in [Domain Models](https://cap.cloud.sap/docs/guides/domain-models), along with\n- [Serving deeply nested documents](https://cap.cloud.sap/docs/guides/generic-providers#serving-structured-data)\n",
|
||||||
"line": 1,
|
"line": 1,
|
||||||
"selection": {
|
"selection": {
|
||||||
"start": {
|
"start": {
|
||||||
@@ -84,7 +84,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"file": "reviews/db/schema.cds",
|
"file": "reviews/db/schema.cds",
|
||||||
"description": "### Reviews - More Modularity\n\nShows how to implement a modular service to manage product reviews, including:\n- Consuming other services synchronously and asynchronously\n- Serving requests synchronously\n- Emitting events asynchronously\n- Grow as you go, with:\n- Mocking app services\n- Running service meshes\n- Late-cut Micro Services\n- As well as managed data, input validations, and authorization\n",
|
"description": "### More Modularity\n\nShows how to implement a modular service to manage product reviews, including:\n- Consuming other services synchronously and asynchronously\n- Serving requests synchronously\n- Emitting events asynchronously\n- Grow as you go, with:\n- Mocking app services\n- Running service meshes\n- Late-cut Micro Services\n- As well as managed data, input validations, and authorization\n",
|
||||||
"line": 1,
|
"line": 1,
|
||||||
"selection": {
|
"selection": {
|
||||||
"start": {
|
"start": {
|
||||||
@@ -99,12 +99,8 @@
|
|||||||
"title": "Reviews"
|
"title": "Reviews"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Bookstore",
|
"file": "fiori/app/index.cds",
|
||||||
"description": "### Bookstore - Reuse and UI\n\n- A [composite app, reusing and combining](https://cap.cloud.sap/docs/guides/reuse-and-compose) these packages:\n - [@capire/bookshop](bookshop)\n - [@capire/reviews](reviews)\n - [@capire/orders](orders)\n - [@capire/common](common)\n- [The Vue.js app](bookshop/app/vue) imported from bookshop is served as well\n- [The Vue.js app](reviews/app/vue) imported from reviews is served as well\n- [The Fiori app](orders/app) imported from orders is served as well\n- [OpenAPI export + Swagger UI](https://cap.cloud.sap/docs/advanced/openapi)"
|
"description": "### Annotations for SAP Fiori Elements\n\nA [composite app, reusing and combining](https://cap.cloud.sap/docs/guides/verticalize) these packages:\n - [@capire/bookshop](bookshop)\n - [@capire/reviews](reviews)\n - [@capire/orders](orders)\n - [@capire/common](common)\n\n[Adds a SAP Fiori elements application](https://cap.cloud.sap/docs/guides/fiori/) to bookshop, thereby introducing to:\n - [OData Annotations](https://cap.cloud.sap/docs/guides/fiori#adding-odata-annotations) in `.cds` files\n - Support for [Fiori Draft](https://cap.cloud.sap/docs/guides/fiori#draft)\n - Support for [Value Helps](https://cap.cloud.sap/docs/guides/fiori#value-help)\n - Serving SAP Fiori apps locally\n\n[The Vue.js app](bookshop/app/vue) imported from bookshop is served as well.\n",
|
||||||
},
|
|
||||||
{
|
|
||||||
"file": "fiori/app/services.cds",
|
|
||||||
"description": "### Annotations for SAP Fiori Elements\n\n- [Adds an SAP Fiori elements application](https://cap.cloud.sap/docs/guides/fiori/) to bookstore, thereby introducing to:\n- [OData Annotations](https://cap.cloud.sap/docs/guides/fiori#adding-odata-annotations) in `.cds` files\n- Support for [Fiori Draft](https://cap.cloud.sap/docs/guides/fiori#draft)\n- Support for [Value Helps](https://cap.cloud.sap/docs/guides/fiori#value-help)\n- Serving SAP Fiori apps locally\n",
|
|
||||||
"line": 1,
|
"line": 1,
|
||||||
"selection": {
|
"selection": {
|
||||||
"start": {
|
"start": {
|
||||||
@@ -121,13 +117,14 @@
|
|||||||
{
|
{
|
||||||
"file": "package.json",
|
"file": "package.json",
|
||||||
"description": "### All-in-one Monorepo\n\nEach sample sub directory essentially is a standard npm package, some with standard npm dependencies to other samples. The root folder's [package.json](package.json) has local links to the sub folders, such that an `npm install` populates a local `node_modules` folder acts like a local npm registry to the individual sample packages.\n",
|
"description": "### All-in-one Monorepo\n\nEach sample sub directory essentially is a standard npm package, some with standard npm dependencies to other samples. The root folder's [package.json](package.json) has local links to the sub folders, such that an `npm install` populates a local `node_modules` folder acts like a local npm registry to the individual sample packages.\n",
|
||||||
|
"line": 8,
|
||||||
"selection": {
|
"selection": {
|
||||||
"start": {
|
"start": {
|
||||||
"line": 8,
|
"line": 8,
|
||||||
"character": 1
|
"character": 1
|
||||||
},
|
},
|
||||||
"end": {
|
"end": {
|
||||||
"line": 16,
|
"line": 15,
|
||||||
"character": 1
|
"character": 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ Find here a collection of samples for the [SAP Cloud Application Programming Mod
|
|||||||
|
|
||||||
### Download
|
### Download
|
||||||
|
|
||||||
If you've [Git](https://git-scm.com/downloads) installed, clone this repo as shown below, otherwise [download as ZIP file](archive/main.zip).
|
If you've [Git](https://git-scm.com/downloads) installed, clone this repo as shown below, 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
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ const books = new Vue ({
|
|||||||
data: {
|
data: {
|
||||||
list: [],
|
list: [],
|
||||||
book: undefined,
|
book: undefined,
|
||||||
order: { quantity:1, succeeded:'', failed:'' }
|
order: { amount:1, succeeded:'', failed:'' }
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
@@ -26,18 +26,18 @@ const books = new Vue ({
|
|||||||
const book = books.book = books.list [eve.currentTarget.rowIndex-1]
|
const book = books.book = books.list [eve.currentTarget.rowIndex-1]
|
||||||
const res = await GET(`/Books/${book.ID}?$select=descr,stock,image`)
|
const res = await GET(`/Books/${book.ID}?$select=descr,stock,image`)
|
||||||
Object.assign (book, res.data)
|
Object.assign (book, res.data)
|
||||||
books.order = { quantity:1 }
|
books.order = { amount:1 }
|
||||||
setTimeout (()=> $('form > input').focus(), 111)
|
setTimeout (()=> $('form > input').focus(), 111)
|
||||||
},
|
},
|
||||||
|
|
||||||
async submitOrder () {
|
async submitOrder () {
|
||||||
const {book,order} = books, quantity = parseInt (order.quantity) || 1 // REVISIT: Okra should be less strict
|
const {book,order} = books, amount = parseInt (order.amount) || 1 // REVISIT: Okra should be less strict
|
||||||
try {
|
try {
|
||||||
const res = await POST(`/submitOrder`, { quantity, book: book.ID })
|
const res = await POST(`/submitOrder`, { amount, book: book.ID })
|
||||||
book.stock = res.data.stock
|
book.stock = res.data.stock
|
||||||
books.order = { quantity, succeeded: `Successfully ordered ${quantity} item(s).` }
|
books.order = { amount, succeeded: `Successfully ordered ${amount} item(s).` }
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
books.order = { quantity, failed: e.response.data.error.message }
|
books.order = { amount, failed: e.response.data.error.message }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
<td>{{ book.author }}</td>
|
<td>{{ book.author }}</td>
|
||||||
<td>{{ book.genre.name }}</td>
|
<td>{{ book.genre.name }}</td>
|
||||||
<td class="rating-stars">
|
<td class="rating-stars">
|
||||||
{{ ('★'.repeat(Math.round(book.rating))+'☆☆☆☆☆').slice(0,5) }} ({{ book.numberOfReviews }})
|
{{ ('★'.repeat(Math.round(book.rating))+'☆☆☆☆☆').slice(0,5) }}
|
||||||
</td>
|
</td>
|
||||||
<td>{{ book.currency && book.currency.symbol }} {{ book.price }}</td>
|
<td>{{ book.currency && book.currency.symbol }} {{ book.price }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
{{ 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.prevent="submitOrder" style="float:right; display:flex; flex-direction:row-reverse">
|
||||||
<input type="number" v-model="order.quantity" v-bind:class="{ failed: order.failed }" style="width:5em">
|
<input type="number" v-model="order.amount" v-bind:class="{ failed: order.failed }" style="width:5em">
|
||||||
<input type="submit" value="Order:" class="muted-button">
|
<input type="submit" value="Order:" class="muted-button">
|
||||||
</form>
|
</form>
|
||||||
<h4> {{ book.title }} </h4>
|
<h4> {{ book.title }} </h4>
|
||||||
|
|||||||
@@ -1,2 +1 @@
|
|||||||
const { CatalogService } = require('./srv/cat-service')
|
exports.CatalogService = require('./srv/cat-service')
|
||||||
module.exports = { CatalogService }
|
|
||||||
|
|||||||
@@ -8,9 +8,6 @@
|
|||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"passport": "0.4.1"
|
"passport": "0.4.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
|
||||||
"@cds/cds-plugin-openapi": "*"
|
|
||||||
},
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"genres": "cds serve test/genres.cds",
|
"genres": "cds serve test/genres.cds",
|
||||||
"start": "cds run",
|
"start": "cds run",
|
||||||
|
|||||||
@@ -11,6 +11,6 @@ service CatalogService @(path:'/browse') {
|
|||||||
} excluding { createdBy, modifiedBy };
|
} excluding { createdBy, modifiedBy };
|
||||||
|
|
||||||
@requires: 'authenticated-user'
|
@requires: 'authenticated-user'
|
||||||
action submitOrder ( book: Books:ID, quantity: Integer ) returns { stock: Integer };
|
action submitOrder ( book: Books:ID, amount: Integer ) returns { stock: Integer };
|
||||||
event OrderedBook : { book: Books:ID; quantity: Integer; buyer: String };
|
event OrderedBook : { book: Books:ID; amount: Integer; buyer: String };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
const cds = require('@sap/cds')
|
const cds = require('@sap/cds')
|
||||||
|
const { Books } = cds.entities ('sap.capire.bookshop')
|
||||||
|
|
||||||
class CatalogService extends cds.ApplicationService { init(){
|
class CatalogService extends cds.ApplicationService { init(){
|
||||||
|
|
||||||
const { Books } = cds.entities ('sap.capire.bookshop')
|
|
||||||
|
|
||||||
// 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,quantity} = req.data
|
const {book,amount} = req.data
|
||||||
if (quantity < 1) return req.reject (400,`quantity has to be 1 or more`)
|
|
||||||
let {stock} = await SELECT `stock` .from (Books,book)
|
let {stock} = await SELECT `stock` .from (Books,book)
|
||||||
if (quantity > stock) return req.reject (409,`${quantity} exceeds stock for book #${book}`)
|
if (stock >= amount) {
|
||||||
await UPDATE (Books,book) .with ({ stock: stock -= quantity })
|
await UPDATE (Books,book) .with (`stock -=`, amount)
|
||||||
await this.emit ('OrderedBook', { book, quantity, buyer:req.user.id })
|
await this.emit ('OrderedBook', { book, amount, buyer:req.user.id })
|
||||||
return { stock }
|
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
|
||||||
|
|||||||
@@ -32,19 +32,6 @@ GET {{server}}/admin/Authors?
|
|||||||
# &sap-language=de
|
# &sap-language=de
|
||||||
Authorization: Basic alice:
|
Authorization: Basic alice:
|
||||||
|
|
||||||
### ------------------------------------------------------------------------
|
|
||||||
# Create Author
|
|
||||||
POST {{server}}/admin/Authors
|
|
||||||
Content-Type: application/json;IEEE754Compatible=true
|
|
||||||
Authorization: Basic alice:
|
|
||||||
|
|
||||||
{
|
|
||||||
"ID": 112,
|
|
||||||
"name": "Shakespeeeeere",
|
|
||||||
"age": 22
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
### ------------------------------------------------------------------------
|
### ------------------------------------------------------------------------
|
||||||
# Create book
|
# Create book
|
||||||
POST {{server}}/admin/Books
|
POST {{server}}/admin/Books
|
||||||
@@ -84,7 +71,7 @@ POST {{server}}/browse/submitOrder
|
|||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
{{me}}
|
{{me}}
|
||||||
|
|
||||||
{ "book":201, "quantity":5 }
|
{ "book":201, "amount":5 }
|
||||||
|
|
||||||
|
|
||||||
### ------------------------------------------------------------------------
|
### ------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
namespace sap.capire.bookshop; //> important for reflection
|
|
||||||
using from './srv/mashup';
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@capire/bookstore",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"dependencies": {
|
|
||||||
"@capire/bookshop": "*",
|
|
||||||
"@capire/reviews": "*",
|
|
||||||
"@capire/orders": "*",
|
|
||||||
"@capire/common": "*",
|
|
||||||
"@sap/cds": "^5",
|
|
||||||
"express": "^4.17.1"
|
|
||||||
},
|
|
||||||
"cds": {
|
|
||||||
"requires": {
|
|
||||||
"ReviewsService": {
|
|
||||||
"kind": "odata",
|
|
||||||
"model": "@capire/reviews"
|
|
||||||
},
|
|
||||||
"OrdersService": {
|
|
||||||
"kind": "odata",
|
|
||||||
"model": "@capire/orders"
|
|
||||||
},
|
|
||||||
"messaging": {
|
|
||||||
"[development]": { "kind": "file-based-messaging" },
|
|
||||||
"[hybrid]": { "kind": "enterprise-messaging-shared" },
|
|
||||||
"[production]": { "kind": "enterprise-messaging" }
|
|
||||||
},
|
|
||||||
"db": {
|
|
||||||
"kind": "sql"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"log": { "service": true }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
const cds = require ('@sap/cds')
|
|
||||||
|
|
||||||
// Add mashup logic
|
|
||||||
cds.once('served', require('./srv/mashup'))
|
|
||||||
|
|
||||||
// Add routes to UIs from imported packages
|
|
||||||
cds.once('bootstrap',(app)=>{
|
|
||||||
app.serve ('/bookshop') .from ('@capire/bookshop','app/vue')
|
|
||||||
app.serve ('/reviews') .from ('@capire/reviews','app/vue')
|
|
||||||
app.serve ('/orders') .from('@capire/orders','app/orders')
|
|
||||||
})
|
|
||||||
|
|
||||||
// Add Swagger UI
|
|
||||||
require('./srv/swagger-ui')
|
|
||||||
|
|
||||||
// Returning cds.server
|
|
||||||
module.exports = cds.server
|
|
||||||
|
|
||||||
// For didactic reasons in capire
|
|
||||||
const { ReviewsService, OrdersService } = cds.requires
|
|
||||||
if (!ReviewsService.credentials && !OrdersService.credentials) cds.requires.messaging = false
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
|
|
||||||
// -----------------------------------------------------------------------
|
|
||||||
// Adding Swagger UI - see https://cap.cloud.sap/docs/advanced/openapi
|
|
||||||
const cds = require ('@sap/cds')
|
|
||||||
try {
|
|
||||||
const cds_swagger = require ('cds-swagger-ui-express')
|
|
||||||
cds.once ('bootstrap', app => app.use (cds_swagger()) )
|
|
||||||
} catch (err) {
|
|
||||||
if (err.code !== 'MODULE_NOT_FOUND') throw err
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
/* eslint-disable require-await */
|
|
||||||
const cds = require('@sap/cds'), { BuildTaskHandler } = cds.build
|
|
||||||
const cdsdk = require('@sap/cds-dk')
|
|
||||||
|
|
||||||
cds.build.register(class OpenApiHandler extends BuildTaskHandler {
|
|
||||||
static get meta() {
|
|
||||||
return {
|
|
||||||
id: 'openapi',
|
|
||||||
runWith: ['node-cf', 'java-cf'],
|
|
||||||
config: { src: cds.env.folders.srv.replace(/\/$/, '') }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
async clean() {
|
|
||||||
return this.remove('openapi-docs')
|
|
||||||
}
|
|
||||||
async build() {
|
|
||||||
const model = await this.model()
|
|
||||||
const { options } = this.task
|
|
||||||
|
|
||||||
// generate openapi files for all services
|
|
||||||
await Promise.all(cds.linked(model).services.map(service => {
|
|
||||||
const openApi = cdsdk.compile.to.openapi(model, {
|
|
||||||
service: service.name,
|
|
||||||
'openapi:diagram': String(options.diagram) === 'true'
|
|
||||||
})
|
|
||||||
this.write(openApi).to(`openapi-docs/${service.name}.openapi3.json`)
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@sap/cds-plugin-openapi",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "OpenAPI service specification build plugin",
|
|
||||||
"main": "index.js",
|
|
||||||
"dependencies": {
|
|
||||||
"@sap/cds-dk": "^4"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
2
fiori/.env
Normal file
2
fiori/.env
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# cds.requires.messaging.kind = file-based-messaging
|
||||||
|
PORT = 4004
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
using {AdminService} from '@capire/bookshop';
|
|
||||||
|
|
||||||
annotate AdminService.Authors with @odata.draft.enabled;
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
|
||||||
//
|
|
||||||
// Authors Object Page
|
|
||||||
//
|
|
||||||
annotate AdminService.Authors with @(UI : {
|
|
||||||
HeaderInfo : {
|
|
||||||
TypeName : 'Author',
|
|
||||||
TypeNamePlural : 'Authors',
|
|
||||||
Description : {Value : lifetime}
|
|
||||||
},
|
|
||||||
Facets : [
|
|
||||||
{
|
|
||||||
$Type : 'UI.ReferenceFacet',
|
|
||||||
Label : '{i18n>Details}',
|
|
||||||
Target : '@UI.FieldGroup#Details'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
$Type : 'UI.ReferenceFacet',
|
|
||||||
Label : '{i18n>Books}',
|
|
||||||
Target : 'books/@UI.LineItem'
|
|
||||||
},
|
|
||||||
],
|
|
||||||
FieldGroup #Details : {Data : [
|
|
||||||
{Value : placeOfBirth},
|
|
||||||
{Value : placeOfDeath},
|
|
||||||
{Value : dateOfBirth},
|
|
||||||
{Value : dateOfDeath},
|
|
||||||
{
|
|
||||||
Value : age,
|
|
||||||
Label : '{i18n>Age}'
|
|
||||||
},
|
|
||||||
]},
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
// Workaround to avoid errors for unknown db-specific calculated fields above
|
|
||||||
extend sap.capire.bookshop.Authors with {
|
|
||||||
virtual age : Integer;
|
|
||||||
virtual lifetime : String;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Workaround for Fiori popup for asking user to enter a new UUID on Create
|
|
||||||
annotate AdminService.Authors with { ID @Core.Computed; }
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
sap.ui.define(["sap/fe/core/AppComponent"], function (AppComponent) {
|
|
||||||
"use strict";
|
|
||||||
return AppComponent.extend("authors.Component", {
|
|
||||||
metadata: { manifest: "json" },
|
|
||||||
});
|
|
||||||
});
|
|
||||||
/* eslint no-undef:0 */
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
# This is the resource bundle of itelo
|
|
||||||
# __ldi.translation.uuid=c3431418-9caf-11e8-98d0-529269fb1459
|
|
||||||
|
|
||||||
# JCI app descriptor contains lower case TITLE
|
|
||||||
appTitle=Bookshop Authors
|
|
||||||
|
|
||||||
# JCI app descriptor contains lower case DESCRIPTION
|
|
||||||
appSubTitle=Bookshop Authors
|
|
||||||
|
|
||||||
# JCI app descriptor contains lower case DESCRIPTION
|
|
||||||
appDescription=Bookshop Authors
|
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
{
|
|
||||||
"_version": "1.28.0",
|
|
||||||
"sap.app": {
|
|
||||||
"id": "authors",
|
|
||||||
"type": "application",
|
|
||||||
"title": "Manage Authors",
|
|
||||||
"description": "Sample Application",
|
|
||||||
"i18n": "i18n/i18n.properties",
|
|
||||||
"applicationVersion": {
|
|
||||||
"version": "1.0.0"
|
|
||||||
},
|
|
||||||
"dataSources": {
|
|
||||||
"AdminService": {
|
|
||||||
"uri": "/admin/",
|
|
||||||
"type": "OData",
|
|
||||||
"settings": {
|
|
||||||
"odataVersion": "4.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"sourceTemplate": {
|
|
||||||
"id": "ui5template.basicSAPUI5ApplicationProject",
|
|
||||||
"-id": "ui5template.smartTemplate",
|
|
||||||
"version": "1.40.12"
|
|
||||||
},
|
|
||||||
"crossNavigation": {
|
|
||||||
"inbounds": {
|
|
||||||
"intent1": {
|
|
||||||
"signature": {
|
|
||||||
"parameters": {
|
|
||||||
"Books.author.ID":{
|
|
||||||
"renameTo": "ID"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"additionalParameters": "ignored"
|
|
||||||
},
|
|
||||||
"semanticObject": "Authors",
|
|
||||||
"action": "display",
|
|
||||||
"title": "{{appTitle}}",
|
|
||||||
"info": "{{appInfo}}",
|
|
||||||
"subTitle": "{{appSubTitle}}",
|
|
||||||
"icon": "sap-icon://SAP-icons-TNT/user",
|
|
||||||
"indicatorDataSource": {
|
|
||||||
"dataSource": "AdminService",
|
|
||||||
"path": "Authors/$count",
|
|
||||||
"refresh": 1800
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"sap.ui5": {
|
|
||||||
"dependencies": {
|
|
||||||
"minUI5Version": "1.81.0",
|
|
||||||
"libs": {
|
|
||||||
"sap.fe.templates": {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"models": {
|
|
||||||
"i18n": {
|
|
||||||
"type": "sap.ui.model.resource.ResourceModel",
|
|
||||||
"uri": "i18n/i18n.properties"
|
|
||||||
},
|
|
||||||
"": {
|
|
||||||
"dataSource": "AdminService",
|
|
||||||
"settings": {
|
|
||||||
"synchronizationMode": "None",
|
|
||||||
"operationMode": "Server",
|
|
||||||
"autoExpandSelect": true,
|
|
||||||
"earlyRequests": true,
|
|
||||||
"groupProperties": {
|
|
||||||
"default": {
|
|
||||||
"submit": "Auto"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"routing": {
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"pattern": ":?query:",
|
|
||||||
"name": "AuthorsList",
|
|
||||||
"target": "AuthorsList"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"pattern": "Authors({key}):?query:",
|
|
||||||
"name": "AuthorsDetails",
|
|
||||||
"target": "AuthorsDetails"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"targets": {
|
|
||||||
"AuthorsList": {
|
|
||||||
"type": "Component",
|
|
||||||
"id": "AuthorsList",
|
|
||||||
"name": "sap.fe.templates.ListReport",
|
|
||||||
"options": {
|
|
||||||
"settings": {
|
|
||||||
"entitySet": "Authors",
|
|
||||||
"initialLoad": true,
|
|
||||||
"navigation": {
|
|
||||||
"Authors": {
|
|
||||||
"detail": {
|
|
||||||
"route": "AuthorsDetails"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"AuthorsDetails": {
|
|
||||||
"type": "Component",
|
|
||||||
"id": "AuthorsDetailsList",
|
|
||||||
"name": "sap.fe.templates.ObjectPage",
|
|
||||||
"options": {
|
|
||||||
"settings": {
|
|
||||||
"entitySet": "Authors"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"contentDensities": {
|
|
||||||
"compact": true,
|
|
||||||
"cozy": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"sap.ui": {
|
|
||||||
"technology": "UI5",
|
|
||||||
"fullWidth": false,
|
|
||||||
"deviceTypes":{
|
|
||||||
"desktop": true,
|
|
||||||
"tablet": true,
|
|
||||||
"phone": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"sap.fiori": {
|
|
||||||
"registrationIds": [],
|
|
||||||
"archeType": "transactional"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -10,7 +10,32 @@
|
|||||||
<script>
|
<script>
|
||||||
window["sap-ushell-config"] = {
|
window["sap-ushell-config"] = {
|
||||||
defaultRenderer: "fiori2",
|
defaultRenderer: "fiori2",
|
||||||
applications: {}
|
applications: {
|
||||||
|
"browse-books": {
|
||||||
|
title: "Browse Books",
|
||||||
|
description: "w/ SAP Fiori Elements",
|
||||||
|
additionalInformation: "SAPUI5.Component=bookshop",
|
||||||
|
applicationType : "URL",
|
||||||
|
url: "/browse/webapp",
|
||||||
|
navigationMode: "embedded"
|
||||||
|
},
|
||||||
|
"manage-books": {
|
||||||
|
title: "Manage Books",
|
||||||
|
description: "w/ SAP Fiori Elements",
|
||||||
|
additionalInformation: "SAPUI5.Component=admin",
|
||||||
|
applicationType : "URL",
|
||||||
|
url: "/admin/webapp",
|
||||||
|
navigationMode: "embedded"
|
||||||
|
},
|
||||||
|
"manage-orders": {
|
||||||
|
title: "Manage Orders",
|
||||||
|
description: "w/ SAP Fiori Elements",
|
||||||
|
additionalInformation: "SAPUI5.Component=orders",
|
||||||
|
applicationType : "URL",
|
||||||
|
url: "/orders/webapp",
|
||||||
|
navigationMode: "embedded"
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
using { AdminService } from '@capire/bookstore';
|
using { AdminService } from '../../db/schema';
|
||||||
using from '../common'; // to help UI linter get the complete annotations
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
//
|
//
|
||||||
@@ -40,6 +39,27 @@ annotate AdminService.Books with @(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
annotate AdminService.Authors with @(
|
||||||
|
UI: {
|
||||||
|
HeaderInfo: {
|
||||||
|
Description: {Value: lifetime}
|
||||||
|
},
|
||||||
|
Facets: [
|
||||||
|
{$Type: 'UI.ReferenceFacet', Label: '{i18n>Details}', Target: '@UI.FieldGroup#Details'},
|
||||||
|
{$Type: 'UI.ReferenceFacet', Label: '{i18n>Books}', Target: 'books/@UI.LineItem'},
|
||||||
|
],
|
||||||
|
FieldGroup#Details: {
|
||||||
|
Data: [
|
||||||
|
{Value: placeOfBirth},
|
||||||
|
{Value: placeOfDeath},
|
||||||
|
{Value: dateOfBirth},
|
||||||
|
{Value: dateOfDeath},
|
||||||
|
{Value: age, Label: '{i18n>Age}'},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////
|
||||||
@@ -64,14 +84,10 @@ annotate AdminService.Books.texts with @(
|
|||||||
|
|
||||||
// Add Value Help for Locales
|
// Add Value Help for Locales
|
||||||
annotate AdminService.Books.texts {
|
annotate AdminService.Books.texts {
|
||||||
locale @ValueList:{entity:'Languages'};
|
locale @ValueList:{entity:'Languages',type:#fixed}
|
||||||
locale @Common.ValueListWithFixedValues:true; //show as drop down, not a dialog
|
|
||||||
}
|
}
|
||||||
// In addition we need to expose Languages through AdminService as a target for ValueList
|
// In addition we need to expose Languages through AdminService
|
||||||
using { sap } from '@sap/cds/common';
|
using { sap } from '@sap/cds/common';
|
||||||
extend service AdminService {
|
extend service AdminService {
|
||||||
@readonly entity Languages as projection on sap.common.Languages;
|
entity Languages as projection on sap.common.Languages;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Workaround for Fiori popup for asking user to enter a new UUID on Create
|
|
||||||
annotate AdminService.Books with { ID @Core.Computed; }
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
sap.ui.define(["sap/fe/core/AppComponent"], function(AppComponent) {
|
sap.ui.define(["sap/fe/core/AppComponent"], function(AppComponent) {
|
||||||
"use strict";
|
"use strict";
|
||||||
return AppComponent.extend("books.Component", {
|
return AppComponent.extend("admin.Component", {
|
||||||
metadata: { manifest: "json" }
|
metadata: { manifest: "json" }
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"_version": "1.8.0",
|
"_version": "1.8.0",
|
||||||
"sap.app": {
|
"sap.app": {
|
||||||
"id": "books",
|
"id": "admin",
|
||||||
"type": "application",
|
"type": "application",
|
||||||
"title": "Manage Books",
|
"title": "Manage Books",
|
||||||
"description": "Sample Application",
|
"description": "Sample Application",
|
||||||
@@ -73,7 +73,6 @@
|
|||||||
"options": {
|
"options": {
|
||||||
"settings" : {
|
"settings" : {
|
||||||
"entitySet" : "Books",
|
"entitySet" : "Books",
|
||||||
"initialLoad": true,
|
|
||||||
"navigation" : {
|
"navigation" : {
|
||||||
"Books" : {
|
"Books" : {
|
||||||
"detail" : {
|
"detail" : {
|
||||||
@@ -1,142 +0,0 @@
|
|||||||
{
|
|
||||||
"services": {
|
|
||||||
"LaunchPage": {
|
|
||||||
"adapter": {
|
|
||||||
"config": {
|
|
||||||
"catalogs": [],
|
|
||||||
"groups": [
|
|
||||||
{
|
|
||||||
"id": "Bookshop",
|
|
||||||
"title": "Bookshop",
|
|
||||||
"isPreset": true,
|
|
||||||
"isVisible": true,
|
|
||||||
"isGroupLocked": false,
|
|
||||||
"tiles": [
|
|
||||||
{
|
|
||||||
"id": "BrowseBooks",
|
|
||||||
"tileType": "sap.ushell.ui.tile.StaticTile",
|
|
||||||
"properties": {
|
|
||||||
"title": "Browse Books",
|
|
||||||
"targetURL": "#Books-display"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "Administration",
|
|
||||||
"title": "Administration",
|
|
||||||
"isPreset": true,
|
|
||||||
"isVisible": true,
|
|
||||||
"isGroupLocked": false,
|
|
||||||
"tiles": [
|
|
||||||
{
|
|
||||||
"id": "ManageBooks",
|
|
||||||
"tileType": "sap.ushell.ui.tile.StaticTile",
|
|
||||||
"properties": {
|
|
||||||
"title": "Manage Books",
|
|
||||||
"targetURL": "#Books-manage"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "ManageAuthors",
|
|
||||||
"tileType": "sap.ushell.ui.tile.StaticTile",
|
|
||||||
"properties": {
|
|
||||||
"title": "Manage Authors",
|
|
||||||
"targetURL": "#Authors-display"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "ManageOrders",
|
|
||||||
"tileType": "sap.ushell.ui.tile.StaticTile",
|
|
||||||
"properties": {
|
|
||||||
"title": "Manage Orders",
|
|
||||||
"targetURL": "#Orders-manage"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"NavTargetResolution": {
|
|
||||||
"config": {
|
|
||||||
"enableClientSideTargetResolution": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ClientSideTargetResolution": {
|
|
||||||
"adapter": {
|
|
||||||
"config": {
|
|
||||||
"inbounds": {
|
|
||||||
"BrowseBooks": {
|
|
||||||
"semanticObject": "Books",
|
|
||||||
"action": "display",
|
|
||||||
"title": "Browse Books",
|
|
||||||
"signature": {
|
|
||||||
"parameters": {
|
|
||||||
"Books.ID": {
|
|
||||||
"renameTo": "ID"
|
|
||||||
},
|
|
||||||
"Authors.books.ID": {
|
|
||||||
"renameTo": "ID"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"additionalParameters": "ignored"
|
|
||||||
},
|
|
||||||
"resolutionResult": {
|
|
||||||
"applicationType": "SAPUI5",
|
|
||||||
"additionalInformation": "SAPUI5.Component=bookshop",
|
|
||||||
"url": "/browse/webapp"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"BrowseAuthors": {
|
|
||||||
"semanticObject": "Authors",
|
|
||||||
"action": "display",
|
|
||||||
"title": "Browse Authors",
|
|
||||||
"signature": {
|
|
||||||
"parameters": {
|
|
||||||
"Books.author.ID":{
|
|
||||||
"renameTo": "ID"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"additionalParameters": "ignored"
|
|
||||||
},
|
|
||||||
"resolutionResult": {
|
|
||||||
"applicationType": "SAPUI5",
|
|
||||||
"additionalInformation": "SAPUI5.Component=authors",
|
|
||||||
"url": "/admin-authors/webapp"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ManageBooks": {
|
|
||||||
"semanticObject": "Books",
|
|
||||||
"action": "manage",
|
|
||||||
"title": "Manage Books",
|
|
||||||
"signature": {
|
|
||||||
"parameters": {},
|
|
||||||
"additionalParameters": "allowed"
|
|
||||||
},
|
|
||||||
"resolutionResult": {
|
|
||||||
"applicationType": "SAPUI5",
|
|
||||||
"additionalInformation": "SAPUI5.Component=books",
|
|
||||||
"url": "/admin-books/webapp"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ManageOrders": {
|
|
||||||
"semanticObject": "Orders",
|
|
||||||
"action": "manage",
|
|
||||||
"signature": {
|
|
||||||
"parameters": {},
|
|
||||||
"additionalParameters": "allowed"
|
|
||||||
},
|
|
||||||
"resolutionResult": {
|
|
||||||
"applicationType": "SAPUI5",
|
|
||||||
"additionalInformation": "SAPUI5.Component=orders",
|
|
||||||
"url": "/orders/webapp"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
3
fiori/app/bookshop.html
Normal file
3
fiori/app/bookshop.html
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<head>
|
||||||
|
<meta http-equiv="refresh" content="0;url=bookshop/index.html">
|
||||||
|
</head>
|
||||||
@@ -1,60 +1,50 @@
|
|||||||
using CatalogService from '@capire/bookstore';
|
using CatalogService from '@capire/bookshop';
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
//
|
//
|
||||||
// Books Object Page
|
// Books Object Page
|
||||||
//
|
//
|
||||||
annotate CatalogService.Books with @(UI : {
|
annotate CatalogService.Books with @(
|
||||||
HeaderInfo : {
|
UI: {
|
||||||
TypeName : 'Book',
|
HeaderInfo: {
|
||||||
TypeNamePlural : 'Books',
|
TypeName: 'Book',
|
||||||
Description : {Value : author}
|
TypeNamePlural: 'Books',
|
||||||
},
|
Description: {Value: author}
|
||||||
HeaderFacets : [{
|
},
|
||||||
$Type : 'UI.ReferenceFacet',
|
HeaderFacets: [
|
||||||
Label : '{i18n>Description}',
|
{$Type: 'UI.ReferenceFacet', Label: '{i18n>Description}', Target: '@UI.FieldGroup#Descr'},
|
||||||
Target : '@UI.FieldGroup#Descr'
|
],
|
||||||
}, ],
|
Facets: [
|
||||||
Facets : [{
|
{$Type: 'UI.ReferenceFacet', Label: '{i18n>Details}', Target: '@UI.FieldGroup#Price'},
|
||||||
$Type : 'UI.ReferenceFacet',
|
],
|
||||||
Label : '{i18n>Details}',
|
FieldGroup#Descr: {
|
||||||
Target : '@UI.FieldGroup#Price'
|
Data: [
|
||||||
}, ],
|
{Value: descr},
|
||||||
FieldGroup #Descr : {Data : [{Value : descr}, ]},
|
]
|
||||||
FieldGroup #Price : {Data : [
|
},
|
||||||
{Value : price},
|
FieldGroup#Price: {
|
||||||
{
|
Data: [
|
||||||
Value : currency.symbol,
|
{Value: price},
|
||||||
Label : '{i18n>Currency}'
|
{Value: currency.symbol, Label: '{i18n>Currency}'},
|
||||||
},
|
]
|
||||||
]},
|
},
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
//
|
//
|
||||||
// Books Object Page
|
// Books Object Page
|
||||||
//
|
//
|
||||||
annotate CatalogService.Books with @(UI : {
|
annotate CatalogService.Books with @(
|
||||||
SelectionFields : [
|
UI: {
|
||||||
ID,
|
SelectionFields: [ ID, price, currency_code ],
|
||||||
price,
|
LineItem: [
|
||||||
currency_code
|
{Value: title},
|
||||||
],
|
{Value: author, Label:'{i18n>Author}'},
|
||||||
LineItem : [
|
{Value: genre.name},
|
||||||
{
|
{Value: price},
|
||||||
Value : ID,
|
{Value: currency.symbol, Label:' '},
|
||||||
Label : '{i18n>Title}'
|
]
|
||||||
},
|
},
|
||||||
{
|
);
|
||||||
Value : author,
|
|
||||||
Label : '{i18n>Author}'
|
|
||||||
},
|
|
||||||
{Value : genre.name},
|
|
||||||
{Value : price},
|
|
||||||
{
|
|
||||||
Value : currency.symbol,
|
|
||||||
Label : ' '
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}, );
|
|
||||||
|
|||||||
@@ -1,14 +1,11 @@
|
|||||||
{
|
{
|
||||||
"_version": "1.28.0",
|
"_version": "1.8.0",
|
||||||
"sap.app": {
|
"sap.app": {
|
||||||
"id": "bookshop",
|
"id": "bookshop",
|
||||||
"type": "application",
|
"type": "application",
|
||||||
"title": "Browse Books",
|
"title": "Browse Books",
|
||||||
"description": "Sample Application",
|
"description": "Sample Application",
|
||||||
"i18n": "i18n/i18n.properties",
|
"i18n": "i18n/i18n.properties",
|
||||||
"applicationVersion": {
|
|
||||||
"version": "1.0.0"
|
|
||||||
},
|
|
||||||
"dataSources": {
|
"dataSources": {
|
||||||
"CatalogService": {
|
"CatalogService": {
|
||||||
"uri": "/browse/",
|
"uri": "/browse/",
|
||||||
@@ -18,43 +15,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sourceTemplate": {
|
"-sourceTemplate": {
|
||||||
"id": "ui5template.basicSAPUI5ApplicationProject",
|
"id": "ui5template.basicSAPUI5ApplicationProject",
|
||||||
"-id": "ui5template.smartTemplate",
|
"-id": "ui5template.smartTemplate",
|
||||||
"version": "1.40.12"
|
"-version": "1.40.12"
|
||||||
},
|
|
||||||
"crossNavigation": {
|
|
||||||
"inbounds": {
|
|
||||||
"intent1": {
|
|
||||||
"signature": {
|
|
||||||
"parameters": {
|
|
||||||
"Books.ID":{
|
|
||||||
"renameTo": "ID"
|
|
||||||
},
|
|
||||||
"Authors.books.ID": {
|
|
||||||
"renameTo": "ID"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"additionalParameters": "ignored"
|
|
||||||
},
|
|
||||||
"semanticObject": "Books",
|
|
||||||
"action": "display",
|
|
||||||
"title": "{{appTitle}}",
|
|
||||||
"info": "{{appInfo}}",
|
|
||||||
"subTitle": "{{appSubTitle}}",
|
|
||||||
"icon": "sap-icon://course-book",
|
|
||||||
"indicatorDataSource": {
|
|
||||||
"dataSource": "CatalogService",
|
|
||||||
"path": "Books/$count",
|
|
||||||
"refresh": 1800
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sap.ui5": {
|
"sap.ui5": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"minUI5Version": "1.81.0",
|
|
||||||
"libs": {
|
"libs": {
|
||||||
"sap.fe.templates": {}
|
"sap.fe.templates": {}
|
||||||
}
|
}
|
||||||
@@ -100,7 +68,6 @@
|
|||||||
"options": {
|
"options": {
|
||||||
"settings": {
|
"settings": {
|
||||||
"entitySet": "Books",
|
"entitySet": "Books",
|
||||||
"initialLoad": true,
|
|
||||||
"navigation": {
|
"navigation": {
|
||||||
"Books": {
|
"Books": {
|
||||||
"detail": {
|
"detail": {
|
||||||
@@ -130,12 +97,7 @@
|
|||||||
},
|
},
|
||||||
"sap.ui": {
|
"sap.ui": {
|
||||||
"technology": "UI5",
|
"technology": "UI5",
|
||||||
"fullWidth": false,
|
"fullWidth": false
|
||||||
"deviceTypes":{
|
|
||||||
"desktop": true,
|
|
||||||
"tablet": true,
|
|
||||||
"phone": true
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"sap.fiori": {
|
"sap.fiori": {
|
||||||
"registrationIds": [],
|
"registrationIds": [],
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
/*
|
/*
|
||||||
Common Annotations shared by all apps
|
Common Annotations shared by all apps
|
||||||
*/
|
*/
|
||||||
|
|
||||||
using { sap.capire.bookshop as my } from '@capire/bookstore';
|
using { sap.capire.bookshop as my } from '@capire/bookshop';
|
||||||
using { sap.common } from '@capire/common';
|
using { sap.common } from '@capire/common';
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
@@ -10,52 +10,39 @@ using { sap.common } from '@capire/common';
|
|||||||
// Books Lists
|
// Books Lists
|
||||||
//
|
//
|
||||||
annotate my.Books with @(
|
annotate my.Books with @(
|
||||||
Common.SemanticKey : [ID],
|
Common.SemanticKey: [title],
|
||||||
UI : {
|
UI: {
|
||||||
Identification : [{Value : title}],
|
Identification: [{Value:title}],
|
||||||
SelectionFields : [
|
SelectionFields: [ ID, author_ID, price, currency_code ],
|
||||||
ID,
|
LineItem: [
|
||||||
author_ID,
|
{Value: ID},
|
||||||
price,
|
{Value: title},
|
||||||
currency_code
|
{Value: author.name, Label:'{i18n>Author}'},
|
||||||
],
|
{Value: genre.name},
|
||||||
LineItem : [
|
{Value: stock},
|
||||||
{
|
{Value: price},
|
||||||
Value : ID,
|
{Value: currency.symbol, Label:' '},
|
||||||
Label : '{i18n>Title}'
|
]
|
||||||
},
|
}
|
||||||
{
|
|
||||||
Value : author.ID,
|
|
||||||
Label : '{i18n>Author}'
|
|
||||||
},
|
|
||||||
{Value : genre.name},
|
|
||||||
{Value : stock},
|
|
||||||
{Value : price},
|
|
||||||
{
|
|
||||||
Value : currency.symbol,
|
|
||||||
Label : ' '
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
) {
|
) {
|
||||||
ID @Common: {
|
author @ValueList.entity:'Authors';
|
||||||
SemanticObject : 'Books',
|
|
||||||
Text: title,
|
|
||||||
TextArrangement : #TextOnly
|
|
||||||
};
|
|
||||||
author @ValueList.entity : 'Authors';
|
|
||||||
};
|
};
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
//
|
//
|
||||||
// Books Details
|
// Books Details
|
||||||
//
|
//
|
||||||
annotate my.Books with @(UI : {HeaderInfo : {
|
annotate my.Books with @(
|
||||||
TypeName : '{i18n>Book}',
|
UI: {
|
||||||
TypeNamePlural : '{i18n>Books}',
|
HeaderInfo: {
|
||||||
Title : {Value : title},
|
TypeName: '{i18n>Book}',
|
||||||
Description : {Value : author.name}
|
TypeNamePlural: '{i18n>Books}',
|
||||||
}, });
|
Title: {Value: title},
|
||||||
|
Description: {Value: author.name}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
@@ -63,19 +50,13 @@ annotate my.Books with @(UI : {HeaderInfo : {
|
|||||||
// Books Elements
|
// Books Elements
|
||||||
//
|
//
|
||||||
annotate my.Books with {
|
annotate my.Books with {
|
||||||
ID @title : '{i18n>ID}';
|
ID @title:'{i18n>ID}' @UI.HiddenFilter;
|
||||||
title @title : '{i18n>Title}';
|
title @title:'{i18n>Title}';
|
||||||
genre @title : '{i18n>Genre}' @Common : {
|
genre @title:'{i18n>Genre}' @Common: { Text: genre.name, TextArrangement: #TextOnly };
|
||||||
Text : genre.name,
|
author @title:'{i18n>Author}' @Common: { Text: author.name, TextArrangement: #TextOnly };
|
||||||
TextArrangement : #TextOnly
|
price @title:'{i18n>Price}' @Measures.ISOCurrency: currency_code;
|
||||||
};
|
stock @title:'{i18n>Stock}';
|
||||||
author @title : '{i18n>Author}' @Common : {
|
descr @UI.MultiLineText;
|
||||||
Text : author.name,
|
|
||||||
TextArrangement : #TextOnly
|
|
||||||
};
|
|
||||||
price @title : '{i18n>Price}' @Measures.ISOCurrency : currency_code;
|
|
||||||
stock @title : '{i18n>Stock}';
|
|
||||||
descr @UI.MultiLineText;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
@@ -83,45 +64,42 @@ annotate my.Books with {
|
|||||||
// Genres List
|
// Genres List
|
||||||
//
|
//
|
||||||
annotate my.Genres with @(
|
annotate my.Genres with @(
|
||||||
Common.SemanticKey : [name],
|
Common.SemanticKey: [name],
|
||||||
UI : {
|
UI: {
|
||||||
SelectionFields : [name],
|
SelectionFields: [ name ],
|
||||||
LineItem : [
|
LineItem:[
|
||||||
{Value : name},
|
{Value: name},
|
||||||
{
|
{Value: parent.name, Label: 'Main Genre'},
|
||||||
Value : parent.name,
|
],
|
||||||
Label : 'Main Genre'
|
}
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
//
|
//
|
||||||
// Genre Details
|
// Genre Details
|
||||||
//
|
//
|
||||||
annotate my.Genres with @(UI : {
|
annotate my.Genres with @(
|
||||||
Identification : [{Value : name}],
|
UI: {
|
||||||
HeaderInfo : {
|
Identification: [{Value:name}],
|
||||||
TypeName : '{i18n>Genre}',
|
HeaderInfo: {
|
||||||
TypeNamePlural : '{i18n>Genres}',
|
TypeName: '{i18n>Genre}',
|
||||||
Title : {Value : name},
|
TypeNamePlural: '{i18n>Genres}',
|
||||||
Description : {Value : ID}
|
Title: {Value: name},
|
||||||
},
|
Description: {Value: ID}
|
||||||
Facets : [{
|
},
|
||||||
$Type : 'UI.ReferenceFacet',
|
Facets: [
|
||||||
Label : '{i18n>SubGenres}',
|
{$Type: 'UI.ReferenceFacet', Label: '{i18n>SubGenres}', Target: 'children/@UI.LineItem'},
|
||||||
Target : 'children/@UI.LineItem'
|
],
|
||||||
}, ],
|
}
|
||||||
});
|
);
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
//
|
//
|
||||||
// Genres Elements
|
// Genres Elements
|
||||||
//
|
//
|
||||||
annotate my.Genres with {
|
annotate my.Genres with {
|
||||||
ID @title : '{i18n>ID}';
|
ID @title: '{i18n>ID}';
|
||||||
name @title : '{i18n>Genre}';
|
name @title: '{i18n>Genre}';
|
||||||
}
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
@@ -129,42 +107,38 @@ annotate my.Genres with {
|
|||||||
// Authors List
|
// Authors List
|
||||||
//
|
//
|
||||||
annotate my.Authors with @(
|
annotate my.Authors with @(
|
||||||
Common.SemanticKey : [ID],
|
Common.SemanticKey: [name],
|
||||||
UI : {
|
UI: {
|
||||||
Identification : [{Value : name}],
|
Identification: [{Value:name}],
|
||||||
SelectionFields : [name],
|
SelectionFields: [ name ],
|
||||||
LineItem : [
|
LineItem:[
|
||||||
{Value : ID},
|
{Value: ID},
|
||||||
{Value : dateOfBirth},
|
{Value: name},
|
||||||
{Value : dateOfDeath},
|
{Value: dateOfBirth},
|
||||||
{Value : placeOfBirth},
|
{Value: dateOfDeath},
|
||||||
{Value : placeOfDeath},
|
{Value: placeOfBirth},
|
||||||
],
|
{Value: placeOfDeath},
|
||||||
}
|
],
|
||||||
) {
|
}
|
||||||
ID @Common: {
|
);
|
||||||
SemanticObject : 'Authors',
|
|
||||||
Text: name,
|
|
||||||
TextArrangement : #TextOnly,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
//
|
//
|
||||||
// Author Details
|
// Author Details
|
||||||
//
|
//
|
||||||
annotate my.Authors with @(UI : {
|
annotate my.Authors with @(
|
||||||
HeaderInfo : {
|
UI: {
|
||||||
TypeName : '{i18n>Author}',
|
HeaderInfo: {
|
||||||
TypeNamePlural : '{i18n>Authors}',
|
TypeName: '{i18n>Author}',
|
||||||
Title : {Value : name},
|
TypeNamePlural: '{i18n>Authors}',
|
||||||
Description : {Value : dateOfBirth}
|
Title: {Value: name},
|
||||||
},
|
Description: {Value: dateOfBirth}
|
||||||
Facets : [{
|
},
|
||||||
$Type : 'UI.ReferenceFacet',
|
Facets: [
|
||||||
Target : 'books/@UI.LineItem'
|
{$Type: 'UI.ReferenceFacet', Target: 'books/@UI.LineItem'},
|
||||||
}, ],
|
],
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
@@ -172,12 +146,12 @@ annotate my.Authors with @(UI : {
|
|||||||
// Authors Elements
|
// Authors Elements
|
||||||
//
|
//
|
||||||
annotate my.Authors with {
|
annotate my.Authors with {
|
||||||
ID @title : '{i18n>ID}';
|
ID @title:'{i18n>ID}' @UI.HiddenFilter;
|
||||||
name @title : '{i18n>Name}';
|
name @title:'{i18n>Name}';
|
||||||
dateOfBirth @title : '{i18n>DateOfBirth}';
|
dateOfBirth @title:'{i18n>DateOfBirth}';
|
||||||
dateOfDeath @title : '{i18n>DateOfDeath}';
|
dateOfDeath @title:'{i18n>DateOfDeath}';
|
||||||
placeOfBirth @title : '{i18n>PlaceOfBirth}';
|
placeOfBirth @title:'{i18n>PlaceOfBirth}';
|
||||||
placeOfDeath @title : '{i18n>PlaceOfDeath}';
|
placeOfDeath @title:'{i18n>PlaceOfDeath}';
|
||||||
}
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
@@ -185,105 +159,99 @@ annotate my.Authors with {
|
|||||||
// Languages List
|
// Languages List
|
||||||
//
|
//
|
||||||
annotate common.Languages with @(
|
annotate common.Languages with @(
|
||||||
Common.SemanticKey : [code],
|
Common.SemanticKey: [code],
|
||||||
Identification : [{Value : code}],
|
Identification: [{Value:code}],
|
||||||
UI : {
|
UI: {
|
||||||
SelectionFields : [
|
SelectionFields: [ name, descr ],
|
||||||
name,
|
LineItem:[
|
||||||
descr
|
{Value: code},
|
||||||
],
|
{Value: name},
|
||||||
LineItem : [
|
],
|
||||||
{Value : code},
|
}
|
||||||
{Value : name},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
//
|
//
|
||||||
// Language Details
|
// Language Details
|
||||||
//
|
//
|
||||||
annotate common.Languages with @(UI : {
|
annotate common.Languages with @(
|
||||||
HeaderInfo : {
|
UI: {
|
||||||
TypeName : '{i18n>Language}',
|
HeaderInfo: {
|
||||||
TypeNamePlural : '{i18n>Languages}',
|
TypeName: '{i18n>Language}',
|
||||||
Title : {Value : name},
|
TypeNamePlural: '{i18n>Languages}',
|
||||||
Description : {Value : descr}
|
Title: {Value: name},
|
||||||
},
|
Description: {Value: descr}
|
||||||
Facets : [{
|
},
|
||||||
$Type : 'UI.ReferenceFacet',
|
Facets: [
|
||||||
Label : '{i18n>Details}',
|
{$Type: 'UI.ReferenceFacet', Label: '{i18n>Details}', Target: '@UI.FieldGroup#Details'},
|
||||||
Target : '@UI.FieldGroup#Details'
|
],
|
||||||
}, ],
|
FieldGroup#Details: {
|
||||||
FieldGroup #Details : {Data : [
|
Data: [
|
||||||
{Value : code},
|
{Value: code},
|
||||||
{Value : name},
|
{Value: name},
|
||||||
{Value : descr}
|
{Value: descr}
|
||||||
]},
|
]
|
||||||
});
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
//
|
//
|
||||||
// Currencies List
|
// Currencies List
|
||||||
//
|
//
|
||||||
annotate common.Currencies with @(
|
annotate common.Currencies with @(
|
||||||
Common.SemanticKey : [code],
|
Common.SemanticKey: [code],
|
||||||
Identification : [{Value : code}],
|
Identification: [{Value:code}],
|
||||||
UI : {
|
UI: {
|
||||||
SelectionFields : [
|
SelectionFields: [ name, descr ],
|
||||||
name,
|
LineItem:[
|
||||||
descr
|
{Value: descr},
|
||||||
],
|
{Value: symbol},
|
||||||
LineItem : [
|
{Value: code},
|
||||||
{Value : descr},
|
],
|
||||||
{Value : symbol},
|
}
|
||||||
{Value : code},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
//
|
//
|
||||||
// Currency Details
|
// Currency Details
|
||||||
//
|
//
|
||||||
annotate common.Currencies with @(UI : {
|
annotate common.Currencies with @(
|
||||||
HeaderInfo : {
|
UI: {
|
||||||
TypeName : '{i18n>Currency}',
|
HeaderInfo: {
|
||||||
TypeNamePlural : '{i18n>Currencies}',
|
TypeName: '{i18n>Currency}',
|
||||||
Title : {Value : descr},
|
TypeNamePlural: '{i18n>Currencies}',
|
||||||
Description : {Value : code}
|
Title: {Value: descr},
|
||||||
},
|
Description: {Value: code}
|
||||||
Facets : [
|
},
|
||||||
{
|
Facets: [
|
||||||
$Type : 'UI.ReferenceFacet',
|
{$Type: 'UI.ReferenceFacet', Label: '{i18n>Details}', Target: '@UI.FieldGroup#Details'},
|
||||||
Label : '{i18n>Details}',
|
{$Type: 'UI.ReferenceFacet', Label: '{i18n>Extended}', Target: '@UI.FieldGroup#Extended'},
|
||||||
Target : '@UI.FieldGroup#Details'
|
],
|
||||||
},
|
FieldGroup#Details: {
|
||||||
{
|
Data: [
|
||||||
$Type : 'UI.ReferenceFacet',
|
{Value: name},
|
||||||
Label : '{i18n>Extended}',
|
{Value: symbol},
|
||||||
Target : '@UI.FieldGroup#Extended'
|
{Value: code},
|
||||||
},
|
{Value: descr}
|
||||||
],
|
]
|
||||||
FieldGroup #Details : {Data : [
|
},
|
||||||
{Value : name},
|
FieldGroup#Extended: {
|
||||||
{Value : symbol},
|
Data: [
|
||||||
{Value : code},
|
{Value: numcode},
|
||||||
{Value : descr}
|
{Value: minor},
|
||||||
]},
|
{Value: exponent}
|
||||||
FieldGroup #Extended : {Data : [
|
]
|
||||||
{Value : numcode},
|
},
|
||||||
{Value : minor},
|
}
|
||||||
{Value : exponent}
|
);
|
||||||
]},
|
|
||||||
});
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
//
|
//
|
||||||
// Currencies Elements
|
// Currencies Elements
|
||||||
//
|
//
|
||||||
annotate common.Currencies with {
|
annotate common.Currencies with {
|
||||||
numcode @title : '{i18n>NumCode}';
|
numcode @title:'{i18n>NumCode}';
|
||||||
minor @title : '{i18n>MinorUnit}';
|
minor @title:'{i18n>MinorUnit}';
|
||||||
exponent @title : '{i18n>Exponent}';
|
exponent @title:'{i18n>Exponent}';
|
||||||
}
|
}
|
||||||
|
|||||||
3
fiori/app/reviews.html
Normal file
3
fiori/app/reviews.html
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<head>
|
||||||
|
<meta http-equiv="refresh" content="0;url=reviews/index.html">
|
||||||
|
</head>
|
||||||
@@ -2,8 +2,11 @@
|
|||||||
This model controls what gets served to Fiori frontends...
|
This model controls what gets served to Fiori frontends...
|
||||||
*/
|
*/
|
||||||
|
|
||||||
using from './admin-authors/fiori-service';
|
using from './admin/fiori-service';
|
||||||
using from './admin-books/fiori-service';
|
|
||||||
using from './browse/fiori-service';
|
using from './browse/fiori-service';
|
||||||
using from './common';
|
using from './common';
|
||||||
using from '@capire/bookstore/srv/mashup';
|
|
||||||
|
using from '@capire/common';
|
||||||
|
|
||||||
|
// only works in case of embedded orders service
|
||||||
|
using from '@capire/orders/app/orders/fiori-service';
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
// Add Author.age and .lifetime with a DB-specific function
|
// Add Author.age and .lifetime with a DB-specific function
|
||||||
//
|
//
|
||||||
|
|
||||||
using { AdminService } from '@capire/bookshop';
|
using { AdminService } from '../schema';
|
||||||
|
|
||||||
extend projection AdminService.Authors with {
|
extend projection AdminService.Authors with {
|
||||||
YEARS_BETWEEN(dateOfBirth, dateOfDeath) as age: Integer,
|
YEARS_BETWEEN(dateOfBirth, dateOfDeath) as age: Integer,
|
||||||
|
|||||||
8
fiori/db/schema.cds
Normal file
8
fiori/db/schema.cds
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
using { sap.capire.bookshop } from '@capire/bookshop';
|
||||||
|
|
||||||
|
// Forward-declare calculated fields to be filled in database-specific ways
|
||||||
|
// TODO find a better way to have 'default' fields that still can be overwritten.
|
||||||
|
extend bookshop.Authors with {
|
||||||
|
virtual age: Integer;
|
||||||
|
virtual lifetime: String;
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// Add Author.age and .lifetime with a DB-specific function
|
// Add Author.age and .lifetime with a DB-specific function
|
||||||
//
|
//
|
||||||
|
|
||||||
using { AdminService } from '@capire/bookshop';
|
using { AdminService } from '../schema';
|
||||||
|
|
||||||
extend projection AdminService.Authors with {
|
extend projection AdminService.Authors with {
|
||||||
strftime('%Y',dateOfDeath)-strftime('%Y',dateOfBirth) as age: Integer,
|
strftime('%Y',dateOfDeath)-strftime('%Y',dateOfBirth) as age: Integer,
|
||||||
|
|||||||
@@ -2,7 +2,10 @@
|
|||||||
"name": "@capire/fiori",
|
"name": "@capire/fiori",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@capire/bookstore": "*",
|
"@capire/bookshop": "*",
|
||||||
|
"@capire/reviews": "*",
|
||||||
|
"@capire/orders": "*",
|
||||||
|
"@capire/common": "*",
|
||||||
"@sap/cds": "^5",
|
"@sap/cds": "^5",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"passport": "^0.4.1"
|
"passport": "^0.4.1"
|
||||||
@@ -12,6 +15,9 @@
|
|||||||
"watch": "cds watch"
|
"watch": "cds watch"
|
||||||
},
|
},
|
||||||
"cds": {
|
"cds": {
|
||||||
|
"hana": {
|
||||||
|
"deploy-format": "hdbtable"
|
||||||
|
},
|
||||||
"requires": {
|
"requires": {
|
||||||
"auth": {
|
"auth": {
|
||||||
"strategy": "dummy"
|
"strategy": "dummy"
|
||||||
@@ -24,30 +30,14 @@
|
|||||||
"kind": "odata",
|
"kind": "odata",
|
||||||
"model": "@capire/orders"
|
"model": "@capire/orders"
|
||||||
},
|
},
|
||||||
"messaging": {
|
|
||||||
"[production]": {
|
|
||||||
"kind": "enterprise-messaging"
|
|
||||||
},
|
|
||||||
"[development]": {
|
|
||||||
"kind": "file-based-messaging"
|
|
||||||
},
|
|
||||||
"[hybrid!]": {
|
|
||||||
"kind": "enterprise-messaging-shared"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"db": {
|
"db": {
|
||||||
"kind": "sql"
|
"kind": "sql",
|
||||||
},
|
|
||||||
"db-ext": {
|
|
||||||
"[development]": {
|
"[development]": {
|
||||||
"model": "db/sqlite"
|
"model": "db/sqlite"
|
||||||
},
|
},
|
||||||
"[production]": {
|
"[production]": {
|
||||||
"model": "db/hana"
|
"model": "db/hana"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"hana": {
|
|
||||||
"deploy-format": "hdbtable"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1,18 @@
|
|||||||
module.exports = require('@capire/bookstore/server.js')
|
const cds = require ('@sap/cds')
|
||||||
|
|
||||||
|
cds.once('bootstrap',(app)=>{
|
||||||
|
app.use ('/orders/webapp', _from('@capire/orders/app/orders/webapp/manifest.json'))
|
||||||
|
app.use ('/bookshop', _from('@capire/bookshop/app/vue/index.html'))
|
||||||
|
app.use ('/reviews', _from('@capire/reviews/app/vue/index.html'))
|
||||||
|
})
|
||||||
|
|
||||||
|
cds.once('served', require('./srv/mashup'))
|
||||||
|
|
||||||
|
module.exports = cds.server
|
||||||
|
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Helper for serving static content from npm-installed packages
|
||||||
|
const {static} = require('express')
|
||||||
|
const {dirname} = require('path')
|
||||||
|
const _from = target => static (dirname (require.resolve(target)))
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
//
|
//
|
||||||
// Enhancing bookshop with Reviews and Orders provided through
|
// Mashing up imported models...
|
||||||
// respective reuse packages and services
|
|
||||||
//
|
//
|
||||||
|
|
||||||
using { sap.capire.bookshop.Books } from '@capire/bookshop';
|
using { sap.capire.bookshop.Books } from '@capire/bookshop';
|
||||||
@@ -9,24 +8,18 @@ using { sap.capire.bookshop.Books } from '@capire/bookshop';
|
|||||||
//
|
//
|
||||||
// Extend Books with access to Reviews and average ratings
|
// Extend Books with access to Reviews and average ratings
|
||||||
//
|
//
|
||||||
|
|
||||||
using { ReviewsService.Reviews } from '@capire/reviews';
|
using { ReviewsService.Reviews } from '@capire/reviews';
|
||||||
extend Books with {
|
extend Books with {
|
||||||
reviews : Composition of many Reviews on reviews.subject = $self.ID;
|
reviews : Composition of many Reviews on reviews.subject = $self.ID;
|
||||||
rating : Decimal;
|
rating : Decimal;
|
||||||
numberOfReviews : Integer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// Extend Orders with Books as Products
|
// Extend Orders with Books as Products
|
||||||
//
|
//
|
||||||
|
|
||||||
using { sap.capire.orders.Orders } from '@capire/orders';
|
using { sap.capire.orders.Orders } from '@capire/orders';
|
||||||
extend Orders with {
|
extend Orders.Items with {
|
||||||
extend Items with {
|
book : Association to Books on product.ID = book.ID
|
||||||
book : Association to Books on product.ID = book.ID
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Add orders fiori app (in case of embedded orders service)
|
|
||||||
using from '@capire/orders/app/fiori';
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
//
|
//
|
||||||
// Mashing up bookshop services with required services...
|
// Mashing up provided and required services...
|
||||||
//
|
//
|
||||||
module.exports = async()=>{ // called by server.js
|
module.exports = async()=>{ // called by server.js
|
||||||
|
|
||||||
@@ -20,29 +20,30 @@ module.exports = async()=>{ // called by server.js
|
|||||||
CatalogService.prepend (srv => srv.on ('READ', 'Books/reviews', (req) => {
|
CatalogService.prepend (srv => srv.on ('READ', 'Books/reviews', (req) => {
|
||||||
console.debug ('> delegating request to ReviewsService')
|
console.debug ('> delegating request to ReviewsService')
|
||||||
const [id] = req.params, { columns, limit } = req.query.SELECT
|
const [id] = req.params, { columns, limit } = req.query.SELECT
|
||||||
return ReviewsService.read ('Reviews',columns).limit(limit).where({subject:String(id)})
|
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
|
// Create an order with the OrdersService when CatalogService signals a new order
|
||||||
//
|
//
|
||||||
CatalogService.on ('OrderedBook', async (msg) => {
|
CatalogService.on ('OrderedBook', async (msg) => {
|
||||||
const { book, quantity, buyer } = msg.data
|
const { book, amount, buyer } = msg.data
|
||||||
const { title, price } = await db.tx(msg).read (Books, book, b => { b.title, b.price })
|
const { title, price } = await db.tx(msg).read (Books, book, b => { b.title, b.price })
|
||||||
return OrdersService.tx(msg).create ('Orders').entries({
|
return OrdersService.tx(msg).create ('Orders').entries({
|
||||||
OrderNo: 'Order at '+ (new Date).toLocaleString(),
|
OrderNo: 'Order at '+ (new Date).toLocaleString(),
|
||||||
Items: [{ product:{ID:`${book}`}, title, price, quantity }],
|
Items: [{ product:{ID:`${book}`}, title, price, amount }],
|
||||||
buyer, createdBy: buyer
|
buyer, createdBy: buyer
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
//
|
//
|
||||||
// Update Books' average ratings when ReviewsService signals updated reviews
|
// Update Books' average ratings when ReviewsService signals updatd reviews
|
||||||
//
|
//
|
||||||
ReviewsService.on ('reviewed', (msg) => {
|
ReviewsService.on ('reviewed', (msg) => {
|
||||||
console.debug ('> received:', msg.event, msg.data)
|
console.debug ('> received:', msg.event, msg.data)
|
||||||
const { subject, count, rating } = msg.data
|
const { subject, rating } = msg.data
|
||||||
return UPDATE(Books,subject).with({ numberOfReviews:count, rating })
|
return UPDATE(Books,subject).with({rating})
|
||||||
|
// ^ Note: the framework will execute this and take care for db.tx
|
||||||
})
|
})
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -50,9 +51,9 @@ module.exports = async()=>{ // called by server.js
|
|||||||
//
|
//
|
||||||
OrdersService.on ('OrderChanged', (msg) => {
|
OrdersService.on ('OrderChanged', (msg) => {
|
||||||
console.debug ('> received:', msg.event, msg.data)
|
console.debug ('> received:', msg.event, msg.data)
|
||||||
const { product, deltaQuantity } = msg.data
|
const { product, deltaAmount } = msg.data
|
||||||
return UPDATE (Books) .where ('ID =', product)
|
return UPDATE (Books) .where ('ID =', product)
|
||||||
.and ('stock >=', deltaQuantity)
|
.and ('stock >=', deltaAmount)
|
||||||
.set ('stock -=', deltaQuantity)
|
.set ('stock -=', deltaAmount)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
@bookshop = http://localhost:4004
|
@bookshop = http://localhost:4004
|
||||||
@reviews-service = {{bookshop}}/reviews
|
@reviews-service = {{bookshop}}/reviews
|
||||||
# Uncomment this when running a separate reviews service
|
# Uncomment this when running a separate reviews service
|
||||||
# @reviews-service = http://localhost:4005/reviews
|
@reviews-service = http://localhost:4005/reviews
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
# Hello World Getting Started Sample
|
|
||||||
|
|
||||||
## Next Steps
|
|
||||||
|
|
||||||
- To run the JavaScript implementation, open a new terminal and run `cds watch`.
|
|
||||||
- To run the TypeScript implementation, open a new terminal and run `cds-ts watch`.
|
|
||||||
|
|
||||||
Then call the service at: http://localhost:4004/say/hello(to='world')
|
|
||||||
|
|
||||||
## Learn More
|
|
||||||
|
|
||||||
Learn more about:
|
|
||||||
|
|
||||||
- [Hello World!](https://cap.cloud.sap/docs/get-started/hello-world)
|
|
||||||
- [Using TypeScript](https://cap.cloud.sap/docs/get-started/using-typescript)
|
|
||||||
@@ -2,53 +2,6 @@
|
|||||||
"name": "@capire/hello-world",
|
"name": "@capire/hello-world",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "npx jest --silent",
|
"watch": "cds serve world.cds"
|
||||||
"start": "cds serve srv/world.cds",
|
|
||||||
"start:ts": "cds-ts serve srv/world.cds"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/jest": "^27.0.2",
|
|
||||||
"@types/node": "^16.11.6",
|
|
||||||
"ts-jest": "^27.0.2",
|
|
||||||
"typescript": "^4.3.5"
|
|
||||||
},
|
|
||||||
"jest": {
|
|
||||||
"testEnvironment": "node",
|
|
||||||
"preset": "ts-jest",
|
|
||||||
"globals": {
|
|
||||||
"ts-jest": {
|
|
||||||
"diagnostics": {
|
|
||||||
"_comment": "see https://githubmemory.com/repo/kulshekhar/ts-jest/issues/2722",
|
|
||||||
"ignoreCodes": [
|
|
||||||
151001
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"eslintConfig": {
|
|
||||||
"extends": "eslint:recommended",
|
|
||||||
"env": {
|
|
||||||
"es2020": true,
|
|
||||||
"node": true,
|
|
||||||
"jest": true,
|
|
||||||
"mocha": true
|
|
||||||
},
|
|
||||||
"globals": {
|
|
||||||
"SELECT": true,
|
|
||||||
"INSERT": true,
|
|
||||||
"UPDATE": true,
|
|
||||||
"DELETE": true,
|
|
||||||
"CREATE": true,
|
|
||||||
"DROP": true,
|
|
||||||
"CDL": true,
|
|
||||||
"CQL": true,
|
|
||||||
"CXL": true,
|
|
||||||
"cds": true
|
|
||||||
},
|
|
||||||
"rules": {
|
|
||||||
"no-console": "off",
|
|
||||||
"require-atomic-updates": "off"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import type { Request } from "@sap/cds/apis/services"
|
|
||||||
|
|
||||||
module.exports = class say {
|
|
||||||
hello(req: Request) {
|
|
||||||
return `Hello ${req.data.to} from a TypeScript file!`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
process.env.CDS_TYPESCRIPT = 'true';
|
|
||||||
import * as cds from '@sap/cds';
|
|
||||||
|
|
||||||
//@ts-ignore
|
|
||||||
const {GET} = cds.test.in(__dirname,'../srv').run('serve', 'world.cds');
|
|
||||||
|
|
||||||
describe('Hello world!', () => {
|
|
||||||
afterAll(() => { delete process.env.CDS_TYPESCRIPT; });
|
|
||||||
|
|
||||||
it('should say hello with class impl from a typescript file', async () => {
|
|
||||||
const {data} = await GET`/say/hello(to='world')`
|
|
||||||
expect(data.value).toMatch(/Hello world.*typescript.*/i)
|
|
||||||
})
|
|
||||||
|
|
||||||
})
|
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
cds.requires.messaging.kind = file-based-messaging
|
cds.requires.messaging.kind = file-based-messaging
|
||||||
PORT = 4006
|
PORT = 4006
|
||||||
|
# cds.odata.flavor = x4
|
||||||
5
orders/app/index.cds
Normal file
5
orders/app/index.cds
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
/*
|
||||||
|
This model controls what gets served to Fiori frontends...
|
||||||
|
*/
|
||||||
|
|
||||||
|
using from './orders/fiori-service';
|
||||||
@@ -10,7 +10,8 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
using { OrdersService } from '../srv/orders-service';
|
using { OrdersService } from '../../srv/orders-service';
|
||||||
|
using { sap.common } from '@capire/common';
|
||||||
|
|
||||||
|
|
||||||
@odata.draft.enabled
|
@odata.draft.enabled
|
||||||
@@ -74,19 +75,28 @@ annotate OrdersService.Orders.Items with @(
|
|||||||
{Value: product_ID, Label:'Product ID'},
|
{Value: product_ID, Label:'Product ID'},
|
||||||
{Value: title, Label:'Product Title'},
|
{Value: title, Label:'Product Title'},
|
||||||
{Value: price, Label:'Unit Price'},
|
{Value: price, Label:'Unit Price'},
|
||||||
{Value: quantity, Label:'Quantity'},
|
{Value: amount, Label:'Quantity'},
|
||||||
],
|
],
|
||||||
Identification: [ //Is the main field group
|
Identification: [ //Is the main field group
|
||||||
{Value: quantity, Label:'Quantity'},
|
{Value: product_ID, Label:'Product ID'},
|
||||||
{Value: title, Label:'Product'},
|
{Value: title, Label:'Product Title'},
|
||||||
|
{Value: amount, Label:'Amount'},
|
||||||
{Value: price, Label:'Unit Price'},
|
{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'},
|
||||||
],
|
],
|
||||||
|
HeaderInfo: {
|
||||||
|
TypeName: 'Order Item', TypeNamePlural: 'Order Items',
|
||||||
|
Title: {
|
||||||
|
Label: 'Product ID ', //A label is possible but it is not considered on the ObjectPage yet
|
||||||
|
Value: product_ID
|
||||||
|
},
|
||||||
|
Description: {Value: createdBy}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
quantity @(
|
amount @(
|
||||||
Common.FieldControl: #Mandatory
|
Common.FieldControl: #Mandatory
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
ID;up__ID;quantity;product_ID;title;price
|
ID;up__ID;amount;product_ID;title;price
|
||||||
58040e66-1dcd-4ffb-ab10-fdce32028b79;7e2f2640-6866-4dcf-8f4d-3027aa831cad;1;201;Wuthering Heights;11.11
|
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
|
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
|
e9641166-e050-4261-bfee-d1e797e6cb7f;64e718c9-ff99-47f1-8ca3-950c850777d4;2;252;Eleonora;28
|
||||||
|
@@ -3,22 +3,21 @@ namespace sap.capire.orders;
|
|||||||
|
|
||||||
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 {
|
Items : Composition of many Orders.Items on Items.up_ = $self;
|
||||||
key ID : UUID;
|
|
||||||
product : Association to Products;
|
|
||||||
quantity : Integer;
|
|
||||||
title : String; //> intentionally replicated as snapshot from product.title
|
|
||||||
price : Double; //> materialized calculated field
|
|
||||||
};
|
|
||||||
buyer : User;
|
buyer : User;
|
||||||
currency : Currency;
|
currency : Currency;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
entity Orders.Items {
|
||||||
|
/*key*/ up_ : Association to Orders; // REVISIT: 'key' doesn't work due to bug in runtime
|
||||||
|
key ID : UUID;
|
||||||
|
product : Association to Products;
|
||||||
|
amount : Integer;
|
||||||
|
title : String; //> intentionally replicated as snapshot from product.title or alike
|
||||||
|
price : Double;
|
||||||
|
}
|
||||||
|
|
||||||
/** This is a stand-in for arbitrary ordered Products */
|
/** This is a stand-in for arbitrary ordered Products */
|
||||||
entity Products @(cds.persistence.skip:'always') {
|
entity Products @(cds.persistence.skip:'always') {
|
||||||
key ID : String;
|
key ID : String;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// this is to ensure we have filled-in currencies
|
|
||||||
using from '@capire/common';
|
|
||||||
|
|||||||
31
orders/requests.http
Normal file
31
orders/requests.http
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
|
||||||
|
@Orders = http://localhost:4006/orders/Orders
|
||||||
|
|
||||||
|
|
||||||
|
### Read Orders
|
||||||
|
GET {{Orders}}?
|
||||||
|
###
|
||||||
|
GET {{Orders}}?$expand=Items
|
||||||
|
###
|
||||||
|
GET {{Orders}}(ID={{Order1}},IsActiveEntity=true)/Items
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@Order1 = 64e718c9-ff99-47f1-8ca3-950c850777d4
|
||||||
|
@Order3 = e939604c-ab83-4d4f-bdb6-95fe30b3773e
|
||||||
|
|
||||||
|
### Create order, still inactive
|
||||||
|
POST {{Orders}}
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{"ID": "{{Order3}}"}
|
||||||
|
|
||||||
|
### Get inactive order. We have to specify `IsActiveEntity`.
|
||||||
|
GET {{Orders}}(ID={{Order3}},IsActiveEntity=false)
|
||||||
|
|
||||||
|
### Activate order using `.../<servicename>.draftActivate`
|
||||||
|
POST {{Orders}}(ID={{Order3}},IsActiveEntity=false)/OrdersService.draftActivate
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
### Get active order
|
||||||
|
GET {{Orders}}(ID={{Order3}},IsActiveEntity=true)
|
||||||
@@ -7,30 +7,30 @@ class OrdersService extends cds.ApplicationService {
|
|||||||
|
|
||||||
this.before ('UPDATE', 'Orders', async function(req) {
|
this.before ('UPDATE', 'Orders', async function(req) {
|
||||||
const { ID, Items } = req.data
|
const { ID, Items } = req.data
|
||||||
if (Items) for (let { product_ID, quantity } of Items) {
|
if (Items) for (let { product_ID, amount } of Items) {
|
||||||
const { quantity:before } = await cds.tx(req).run (
|
const { amount:before } = await cds.tx(req).run (
|
||||||
SELECT.one.from (OrderItems, oi => oi.quantity) .where ({up__ID:ID, product_ID})
|
SELECT.one.from (OrderItems, oi => oi.amount) .where ({up__ID:ID, product_ID})
|
||||||
)
|
)
|
||||||
if (quantity != before) await this.orderChanged (product_ID, quantity-before)
|
if (amount != before) await this.orderChanged (product_ID, amount-before)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
this.before ('DELETE', 'Orders', async function(req) {
|
this.before ('DELETE', 'Orders', async function(req) {
|
||||||
const { ID } = req.data
|
const { ID } = req.data
|
||||||
const Items = await cds.tx(req).run (
|
const Items = await cds.tx(req).run (
|
||||||
SELECT.from (OrderItems, oi => { oi.product_ID, oi.quantity }) .where ({up__ID:ID})
|
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.quantity)))
|
if (Items) await Promise.all (Items.map(it => this.orderChanged (it.product_ID, -it.amount)))
|
||||||
})
|
})
|
||||||
|
|
||||||
return super.init()
|
return super.init()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** order changed -> broadcast event */
|
/** order changed -> broadcast event */
|
||||||
orderChanged (product, deltaQuantity) {
|
orderChanged (product, deltaAmount) {
|
||||||
// Emit events to inform subscribers about changes in orders
|
// Emit events to inform subscribers about changes in orders
|
||||||
console.log ('> emitting:', 'OrderChanged', { product, deltaQuantity })
|
console.log ('> emitting:', 'OrderChanged', { product, deltaAmount })
|
||||||
return this.emit ('OrderChanged', { product, deltaQuantity })
|
return this.emit ('OrderChanged', { product, deltaAmount })
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
13176
package-lock.json
generated
13176
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
25
package.json
25
package.json
@@ -5,45 +5,36 @@
|
|||||||
"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/bookstore": "./bookstore",
|
|
||||||
"@capire/bookshop": "./bookshop",
|
"@capire/bookshop": "./bookshop",
|
||||||
"@capire/common": "./common",
|
"@capire/common": "./common",
|
||||||
"@capire/fiori": "./fiori",
|
"@capire/fiori": "./fiori",
|
||||||
"@capire/hello": "./hello",
|
"@capire/hello": "./hello",
|
||||||
"@capire/media": "./media",
|
"@capire/media": "./media",
|
||||||
"@capire/orders": "./orders",
|
"@capire/orders": "./orders",
|
||||||
"@capire/reviews": "./reviews",
|
"@capire/reviews": "./reviews"
|
||||||
"@sap/cds": "git+https://github.tools.sap/cap/cds#add/customBuildTaskProviders"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"chai": "^4.3.4",
|
"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": "^5.0.0"
|
||||||
"@cds/cds-plugin-openapi": "./cds-plugin-openapi"
|
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"cleanup": "rm -rf node_modules && rm -rf */node_modules && rm -rf */*/node_modules",
|
"cleanup": "rm -rf node_modules && rm -rf */node_modules && rm -rf */*/node_modules",
|
||||||
"registry": "node .registry/server.js",
|
"registry": "node .registry/server.js",
|
||||||
"bookshop": "cds watch bookshop",
|
"bookshop": "cds watch bookshop",
|
||||||
"fiori": "cds watch fiori",
|
"fiori": "cds watch fiori",
|
||||||
"hello": "cds watch hello",
|
|
||||||
"media": "cds watch media",
|
"media": "cds watch media",
|
||||||
"mocha": "npx mocha || echo",
|
"mocha": "npx mocha || echo",
|
||||||
"jest": "npx jest",
|
"jest": "npx jest@^26",
|
||||||
"test": "npm run jest -- --silent",
|
"test": "npm run jest --silent"
|
||||||
"test:hello": "cd hello && npm test"
|
|
||||||
},
|
|
||||||
"jest": {
|
|
||||||
"testEnvironment": "node",
|
|
||||||
"testTimeout": 20000,
|
|
||||||
"testMatch": [
|
|
||||||
"**/*.test.js"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"mocha": {
|
"mocha": {
|
||||||
"parallel": true
|
"parallel": true
|
||||||
},
|
},
|
||||||
|
"jest": {
|
||||||
|
"testEnvironment": "node"
|
||||||
|
},
|
||||||
"license": "SAP SAMPLE CODE LICENSE",
|
"license": "SAP SAMPLE CODE LICENSE",
|
||||||
"private": true
|
"private": true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
# cds.requires.messaging.kind = file-based-messaging
|
cds.requires.messaging.kind = file-based-messaging
|
||||||
PORT = 4005
|
PORT = 4005
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
subject;rating;reviewer;title;text
|
subject;rating;title;text
|
||||||
201;5;adam;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;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;bob;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.
|
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;bob;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.
|
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;bob;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.
|
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.
|
||||||
|
@@ -10,14 +10,15 @@
|
|||||||
"@sap/cds": "^5",
|
"@sap/cds": "^5",
|
||||||
"express": "^4.17.1"
|
"express": "^4.17.1"
|
||||||
},
|
},
|
||||||
|
"scripts": {
|
||||||
|
"reviews-service": "cds watch",
|
||||||
|
"books-reviewed": "cds watch ../reviewed"
|
||||||
|
},
|
||||||
"cds": {
|
"cds": {
|
||||||
"requires": {
|
"requires": {
|
||||||
"messaging": {
|
"db": {
|
||||||
"[development]": { "kind": "file-based-messaging" },
|
"kind": "sql"
|
||||||
"[hybrid]": { "kind": "enterprise-messaging-shared" },
|
}
|
||||||
"[production]": { "kind": "enterprise-messaging" }
|
|
||||||
},
|
|
||||||
"db": { "kind": "sql" }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -8,11 +8,10 @@ service ReviewsService {
|
|||||||
action unlike (review: type of Reviews:ID);
|
action unlike (review: type of Reviews:ID);
|
||||||
|
|
||||||
// Async API
|
// Async API
|
||||||
event reviewed : {
|
event reviewed : {
|
||||||
subject : type of Reviews:subject;
|
subject: type of Reviews:subject;
|
||||||
count : Integer;
|
rating: Decimal(2,1)
|
||||||
rating : Decimal;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Input validation
|
// Input validation
|
||||||
annotate Reviews with {
|
annotate Reviews with {
|
||||||
@@ -28,7 +27,14 @@ service ReviewsService {
|
|||||||
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' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -12,11 +12,11 @@ module.exports = cds.service.impl (function(){
|
|||||||
// Emit an event to inform subscribers about new avg ratings for reviewed subjects
|
// Emit an event to inform subscribers about new avg ratings for reviewed subjects
|
||||||
this.after (['CREATE','UPDATE','DELETE'], 'Reviews', async function(_,req) {
|
this.after (['CREATE','UPDATE','DELETE'], 'Reviews', async function(_,req) {
|
||||||
const {subject} = req.data
|
const {subject} = req.data
|
||||||
const { count, rating } = await cds.tx(req) .run (
|
const {rating} = await cds.tx(req) .run (
|
||||||
SELECT.one `round(avg(rating),2) as rating, count(*) as count` .from (Reviews) .where ({subject})
|
SELECT.one (['round(avg(rating),2) as rating']) .from (Reviews) .where ({subject})
|
||||||
)
|
)
|
||||||
global.it || console.log ('< emitting:', 'reviewed', { subject, count, rating })
|
global.it || console.log ('< emitting:', 'reviewed', { subject, rating })
|
||||||
await this.emit ('reviewed', { subject, count, rating })
|
await this.emit ('reviewed', { subject, rating })
|
||||||
})
|
})
|
||||||
|
|
||||||
// Increment counter for reviews considered helpful
|
// Increment counter for reviews considered helpful
|
||||||
|
|||||||
21
samples.md
21
samples.md
@@ -7,7 +7,6 @@ Each sub directory essentially is an individual npm package arranged in an [all-
|
|||||||
## [@capire/hello-world](hello)
|
## [@capire/hello-world](hello)
|
||||||
|
|
||||||
- A simplistic [Hello World](https://cap.cloud.sap/docs/get-started/hello-world) service using [CDS](https://cap.cloud.sap/docs/cds/) and [cds.services](https://cap.cloud.sap/docs/node.js/api#services-api).
|
- A simplistic [Hello World](https://cap.cloud.sap/docs/get-started/hello-world) service using [CDS](https://cap.cloud.sap/docs/cds/) and [cds.services](https://cap.cloud.sap/docs/node.js/api#services-api).
|
||||||
- [Typescript support](https://cap.cloud.sap/docs/get-started/using-typescript)
|
|
||||||
|
|
||||||
|
|
||||||
## [@capire/bookshop](bookshop)
|
## [@capire/bookshop](bookshop)
|
||||||
@@ -51,27 +50,19 @@ Each sub directory essentially is an individual npm package arranged in an [all-
|
|||||||
- As well as managed data, input validations, and authorization
|
- As well as managed data, input validations, and authorization
|
||||||
|
|
||||||
|
|
||||||
## [@capire/bookstore](bookstore)
|
## [@capire/fiori](fiori)
|
||||||
|
|
||||||
- A [composite app, reusing and combining](https://cap.cloud.sap/docs/guides/verticalize) these packages:
|
- A [composite app, reusing and combining](https://cap.cloud.sap/docs/guides/verticalize) these packages:
|
||||||
- [@capire/bookshop](bookshop)
|
- [@capire/bookshop](bookshop)
|
||||||
- [@capire/reviews](reviews)
|
- [@capire/reviews](reviews)
|
||||||
- [@capire/orders](orders)
|
- [@capire/orders](orders)
|
||||||
- [@capire/common](common)
|
- [@capire/common](common)
|
||||||
|
- [Adds an 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
|
||||||
|
- 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)
|
||||||
|
- Serving SAP Fiori apps locally
|
||||||
- [The Vue.js app](bookshop/app/vue) imported from bookshop is served as well
|
- [The Vue.js app](bookshop/app/vue) imported from bookshop is served as well
|
||||||
- [The Vue.js app](reviews/app/vue) imported from reviews is served as well
|
|
||||||
- [The Fiori app](orders/app) imported from orders is served as well
|
|
||||||
- [OpenAPI export + Swagger UI](https://cap.cloud.sap/docs/advanced/openapi)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [@capire/fiori](fiori)
|
|
||||||
|
|
||||||
- [Adds an SAP Fiori elements application](https://cap.cloud.sap/docs/guides/fiori/) to bookstore, thereby introducing to:
|
|
||||||
- [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 [Value Helps](https://cap.cloud.sap/docs/guides/fiori#value-help)
|
|
||||||
- Serving SAP Fiori apps locally
|
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
|
|||||||
@@ -1,267 +1,96 @@
|
|||||||
|
const { expect } = require('../test')
|
||||||
const cds = require('@sap/cds/lib')
|
const cds = require('@sap/cds/lib')
|
||||||
const { expect } = cds.test
|
const CQL = ([cql]) => cds.parse.cql(cql)
|
||||||
const { cdr } = cds.ql
|
|
||||||
const Foo = { name: 'Foo' }
|
const Foo = { name: 'Foo' }
|
||||||
const Books = { name: 'capire.bookshop.Books' }
|
const Books = { name: 'capire.bookshop.Books' }
|
||||||
|
|
||||||
|
const { parse:cdr } = cds.ql
|
||||||
|
|
||||||
const STAR = cdr ? '*' : { ref: ['*'] }
|
// while jest has 'test' as alias to 'it', mocha doesn't
|
||||||
const skip = {to:{eql:()=>skip}}
|
if (!global.test) global.test = it
|
||||||
const srv = new cds.Service
|
|
||||||
let cqn
|
|
||||||
|
|
||||||
expect.plain = (cqn) => !cqn.SELECT.one && !cqn.SELECT.distinct ? expect(cqn) : skip
|
|
||||||
expect.one = (cqn) => !cqn.SELECT.distinct ? expect(cqn) : skip
|
|
||||||
|
|
||||||
describe('cds.ql → cqn', () => {
|
describe('cds.ql → cqn', () => {
|
||||||
//
|
//
|
||||||
|
let cqn
|
||||||
|
|
||||||
for (let each of ['SELECT', 'SELECT one', 'SELECT distinct']) {
|
describe.skip(`BUGS + GAPS...`, () => {
|
||||||
let SELECT; beforeEach(()=> SELECT = (
|
|
||||||
each === 'SELECT distinct' ? cds.ql.SELECT.distinct :
|
|
||||||
each === 'SELECT one' ? cds.ql.SELECT.one :
|
|
||||||
cds.ql.SELECT
|
|
||||||
))
|
|
||||||
describe(`${each}...`, () => {
|
|
||||||
|
|
||||||
test(`from Foo`, () => {
|
it('should consistently handle *', () => {
|
||||||
expect(cqn = SELECT `from Foo`)
|
expect({
|
||||||
.to.eql(SELECT.from `Foo`)
|
SELECT: { from: { ref: ['Foo'] }, columns: ['*'] },
|
||||||
.to.eql(SELECT.from('Foo'))
|
|
||||||
.to.eql(SELECT.from(Foo))
|
|
||||||
.to.eql(SELECT`Foo`)
|
|
||||||
.to.eql(SELECT('Foo'))
|
|
||||||
.to.eql(SELECT(Foo))
|
|
||||||
expect.plain(cqn)
|
|
||||||
.to.eql(CQL`SELECT from Foo`)
|
|
||||||
.to.eql(srv.read `Foo`)
|
|
||||||
.to.eql(srv.read('Foo'))
|
|
||||||
.to.eql(srv.read(Foo))
|
|
||||||
.to.eql({
|
|
||||||
SELECT: { from: { ref: ['Foo'] } },
|
|
||||||
})
|
})
|
||||||
|
.to.eql(CQL`SELECT * from Foo`)
|
||||||
|
.to.eql(CQL`SELECT from Foo{*}`)
|
||||||
|
.to.eql(SELECT('*').from(Foo))
|
||||||
|
.to.eql(SELECT.from(Foo,['*']))
|
||||||
})
|
})
|
||||||
|
|
||||||
if (each === 'SELECT')
|
|
||||||
test('SELECT ( Foo )', () => {
|
it('should consistently handle lists', () => {
|
||||||
expect({
|
const ID = 11, args = [`foo`, "'bar'", 3]
|
||||||
SELECT: { from: { ref: ['Foo'] } },
|
const cqn = CQL`SELECT from Foo where ID=11 and x in (foo,'bar',3)`
|
||||||
})
|
expect(SELECT.from(Foo).where(`ID=${ID} and x in (${args})`)).to.eql(cqn)
|
||||||
.to.eql(CQL`SELECT from Foo`)
|
expect(SELECT.from(Foo).where(`ID=`, ID, `and x in`, args)).to.eql(cqn)
|
||||||
.to.eql(SELECT(Foo))
|
expect(SELECT.from(Foo).where({ ID, x:args })).to.eql(cqn)
|
||||||
})
|
})
|
||||||
|
|
||||||
if (each === 'SELECT')
|
})
|
||||||
test('SELECT ( Foo ) .from ( Bar )', () => {
|
|
||||||
|
|
||||||
expect({
|
|
||||||
SELECT: { columns:[{ref:['Foo']}], from: { ref: ['Bar'] } },
|
|
||||||
})
|
|
||||||
.to.eql(CQL`SELECT Foo from Bar`)
|
|
||||||
.to.eql(SELECT `Foo` .from `Bar`)
|
|
||||||
.to.eql(SELECT `Foo` .from('Bar'))
|
|
||||||
.to.eql(SELECT('Foo').from('Bar'))
|
|
||||||
.to.eql(SELECT(['Foo']).from('Bar'))
|
|
||||||
.to.eql(SELECT(['Foo']).from('Bar'))
|
|
||||||
.to.eql(SELECT `Bar` .columns `Foo`)
|
|
||||||
.to.eql(SELECT `Bar` .columns ('Foo'))
|
|
||||||
.to.eql(SELECT `Bar` .columns (['Foo']))
|
|
||||||
.to.eql(SELECT.from `Bar` .columns ('Foo'))
|
|
||||||
.to.eql(SELECT.from `Bar` .columns (['Foo']))
|
|
||||||
|
|
||||||
expect({
|
|
||||||
SELECT: { columns:[
|
|
||||||
{ref:['Foo']},
|
|
||||||
{ref:['Boo']},
|
|
||||||
], from: { ref: ['Bar'] } },
|
|
||||||
})
|
|
||||||
.to.eql(CQL`SELECT Foo, Boo from Bar`)
|
|
||||||
.to.eql(SELECT `Foo, Boo` .from `Bar`)
|
|
||||||
.to.eql(SELECT `Foo, Boo` .from('Bar'))
|
|
||||||
.to.eql(SELECT('Foo','Boo').from('Bar'))
|
|
||||||
.to.eql(SELECT(['Foo','Boo']).from('Bar'))
|
|
||||||
.to.eql(SELECT `Bar` .columns `Foo, Boo`)
|
|
||||||
.to.eql(SELECT `Bar` .columns ('Foo','Boo'))
|
|
||||||
.to.eql(SELECT `Bar` .columns (['Foo','Boo']))
|
|
||||||
.to.eql(SELECT.from `Bar` .columns ('Foo','Boo'))
|
|
||||||
.to.eql(SELECT.from `Bar` .columns (['Foo','Boo']))
|
|
||||||
|
|
||||||
expect({
|
|
||||||
SELECT: { columns:[
|
|
||||||
{ref:['Foo']},
|
|
||||||
{ref:['Boo']},
|
|
||||||
{ref:['Moo']},
|
|
||||||
], from: { ref: ['Bar'] } },
|
|
||||||
})
|
|
||||||
.to.eql(CQL`SELECT Foo, Boo, Moo from Bar`)
|
|
||||||
.to.eql(SELECT `Foo, Boo, Moo` .from `Bar`)
|
|
||||||
.to.eql(SELECT `Foo, Boo, Moo` .from('Bar'))
|
|
||||||
.to.eql(SELECT('Foo','Boo','Moo').from('Bar'))
|
|
||||||
.to.eql(SELECT(['Foo','Boo','Moo']).from('Bar'))
|
|
||||||
.to.eql(SELECT `Bar` .columns `Foo, Boo, Moo`)
|
|
||||||
.to.eql(SELECT `Bar` .columns ('Foo','Boo','Moo'))
|
|
||||||
.to.eql(SELECT `Bar` .columns (['Foo','Boo','Moo']))
|
|
||||||
.to.eql(SELECT.from `Bar` .columns ('Foo','Boo','Moo'))
|
|
||||||
.to.eql(SELECT.from `Bar` .columns (['Foo','Boo','Moo']))
|
|
||||||
|
|
||||||
|
|
||||||
expect({
|
describe(`SELECT...`, () => {
|
||||||
SELECT: { one:true, columns:[{ref:['Foo']}], from: { ref: ['Bar'] } },
|
|
||||||
})
|
|
||||||
// .to.eql(CQL`SELECT one Foo from Bar`)
|
|
||||||
.to.eql(SELECT.one `Foo` .from `Bar`)
|
|
||||||
.to.eql(SELECT.one `Foo` .from('Bar'))
|
|
||||||
.to.eql(SELECT.one('Foo').from('Bar'))
|
|
||||||
.to.eql(SELECT.one(['Foo']).from('Bar'))
|
|
||||||
.to.eql(SELECT.one(['Foo']).from('Bar'))
|
|
||||||
.to.eql(SELECT.one('Bar',['Foo']))
|
|
||||||
.to.eql(SELECT.one `Bar` .columns `Foo`)
|
|
||||||
.to.eql(SELECT.one('Bar').columns('Foo'))
|
|
||||||
.to.eql(SELECT.one('Bar').columns(['Foo']))
|
|
||||||
.to.eql(SELECT.one.from('Bar',['Foo']))
|
|
||||||
.to.eql(SELECT.one.from('Bar').columns('Foo'))
|
|
||||||
.to.eql(SELECT.one.from('Bar').columns(['Foo']))
|
|
||||||
|
|
||||||
expect({
|
|
||||||
SELECT: { one:true, columns:[
|
|
||||||
{ref:['Foo']},
|
|
||||||
{ref:['Boo']},
|
|
||||||
], from: { ref: ['Bar'] } },
|
|
||||||
})
|
|
||||||
// .to.eql(CQL`SELECT Foo, Boo from Bar`)
|
|
||||||
.to.eql(SELECT.one `Foo, Boo` .from `Bar`)
|
|
||||||
.to.eql(SELECT.one `Foo, Boo` .from('Bar'))
|
|
||||||
.to.eql(SELECT.one('Foo','Boo').from('Bar'))
|
|
||||||
.to.eql(SELECT.one(['Foo','Boo']).from('Bar'))
|
|
||||||
.to.eql(SELECT.one('Bar',['Foo','Boo']))
|
|
||||||
.to.eql(SELECT.one `Bar` .columns `Foo, Boo`)
|
|
||||||
.to.eql(SELECT.one('Bar').columns('Foo','Boo'))
|
|
||||||
.to.eql(SELECT.one('Bar').columns(['Foo','Boo']))
|
|
||||||
.to.eql(SELECT.one.from('Bar',['Foo','Boo']))
|
|
||||||
.to.eql(SELECT.one.from('Bar').columns('Foo','Boo'))
|
|
||||||
.to.eql(SELECT.one.from('Bar').columns(['Foo','Boo']))
|
|
||||||
|
|
||||||
expect({
|
|
||||||
SELECT: { one:true, columns:[
|
|
||||||
{ref:['Foo']},
|
|
||||||
{ref:['Boo']},
|
|
||||||
{ref:['Moo']},
|
|
||||||
], from: { ref: ['Bar'] } },
|
|
||||||
})
|
|
||||||
// .to.eql(CQL`SELECT Foo, Boo, Moo from Bar`)
|
|
||||||
.to.eql(SELECT.one `Foo, Boo, Moo` .from `Bar`)
|
|
||||||
.to.eql(SELECT.one `Foo, Boo, Moo` .from('Bar'))
|
|
||||||
.to.eql(SELECT.one('Foo','Boo','Moo').from('Bar'))
|
|
||||||
.to.eql(SELECT.one(['Foo','Boo','Moo']).from('Bar'))
|
|
||||||
.to.eql(SELECT.one('Bar',['Foo','Boo','Moo']))
|
|
||||||
.to.eql(SELECT.one `Bar` .columns `Foo, Boo, Moo`)
|
|
||||||
.to.eql(SELECT.one('Bar').columns('Foo','Boo','Moo'))
|
|
||||||
.to.eql(SELECT.one('Bar').columns(['Foo','Boo','Moo']))
|
|
||||||
.to.eql(SELECT.one.from('Bar',['Foo','Boo','Moo']))
|
|
||||||
.to.eql(SELECT.one.from('Bar').columns('Foo','Boo','Moo'))
|
|
||||||
.to.eql(SELECT.one.from('Bar').columns(['Foo','Boo','Moo']))
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
if (each === 'SELECT')
|
|
||||||
test('from ( Foo )', () => {
|
test('from ( Foo )', () => {
|
||||||
expect({
|
expect({
|
||||||
SELECT: { from: {ref: [{ id:'Foo', where: [{val:11}] }] }}
|
SELECT: { from: { ref: ['Foo'] } },
|
||||||
})
|
})
|
||||||
.to.eql(srv.read`Foo[${11}]`)
|
.to.eql(CQL`SELECT from Foo`)
|
||||||
.to.eql(SELECT`Foo[${11}]`)
|
.to.eql(SELECT.from(Foo))
|
||||||
|
})
|
||||||
|
|
||||||
expect((cqn = SELECT`from Foo[ID=11]`))
|
test('from ( ..., <key>)', () => {
|
||||||
.to.eql(SELECT`from Foo[ID=${11}]`)
|
// Compiler
|
||||||
.to.eql(SELECT.from `Foo[ID=11]`)
|
expect(CQL`SELECT from Foo[11]`).to.eql({
|
||||||
.to.eql(SELECT.from `Foo[ID=${11}]`)
|
SELECT: {
|
||||||
.to.eql(SELECT`Foo[ID=11]`)
|
// REVISIT: add one:true?
|
||||||
expect.plain(cqn)
|
from: { ref: [{ id: 'Foo', where: [{ val: 11 }] }] },
|
||||||
.to.eql(CQL`SELECT from Foo[ID=11]`)
|
},
|
||||||
.to.eql(srv.read`Foo[ID=11]`)
|
|
||||||
.to.eql({
|
|
||||||
SELECT: { from: {
|
|
||||||
ref: [{ id: 'Foo', where: [{ ref: ['ID'] }, '=', { val: 11 }] }],
|
|
||||||
}},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if (cdr) expect.plain (cqn)
|
expect(CQL`SELECT from Foo[ID=11]`).to.eql({
|
||||||
.to.eql(SELECT`Foo[ID=${11}]`)
|
SELECT: {
|
||||||
.to.eql(srv.read`Foo[ID=${11}]`)
|
// REVISIT: add one:true
|
||||||
|
from: {
|
||||||
// Following implicitly resolve to SELECT.one
|
ref: [{ id: 'Foo', where: [{ ref: ['ID'] }, '=', { val: 11 }] }],
|
||||||
expect(cqn = SELECT.from(Foo,11))
|
|
||||||
.to.eql(SELECT.from(Foo,{ID:11}))
|
|
||||||
.to.eql(SELECT.from(Foo).byKey(11))
|
|
||||||
.to.eql(SELECT.from(Foo).byKey({ID:11}))
|
|
||||||
if (cds.version >= '5.6.0') {
|
|
||||||
expect.one(cqn)
|
|
||||||
.to.eql({
|
|
||||||
SELECT: {
|
|
||||||
one: true,
|
|
||||||
from: { ref: [{ id: 'Foo', where: [{ ref: ['ID'] }, '=', { val: 11 }] }] },
|
|
||||||
},
|
},
|
||||||
})
|
},
|
||||||
} else {
|
})
|
||||||
expect.one(cqn)
|
|
||||||
|
// Runtime ds.ql
|
||||||
|
expect(SELECT.from(Foo, 11))
|
||||||
|
.to.eql(SELECT.from(Foo, { ID: 11 }))
|
||||||
|
.to.eql(SELECT.from(Foo).byKey(11))
|
||||||
|
.to.eql(SELECT.from(Foo).byKey({ ID: 11 }))
|
||||||
|
.to.eql(SELECT.one.from(Foo).where({ ID: 11 }))
|
||||||
.to.eql({
|
.to.eql({
|
||||||
|
// REVISIT: should produce CQN as the ones above?
|
||||||
SELECT: {
|
SELECT: {
|
||||||
one: true,
|
one: true,
|
||||||
from: { ref: ['Foo'] },
|
from: { ref: ['Foo'] },
|
||||||
where: [{ ref: ['ID'] }, '=', { val: 11 }],
|
where: [{ ref: ['ID'] }, '=', { val: 11 }],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
})
|
expect(CQL`SELECT from Foo[11]{a}`).to.eql({
|
||||||
|
|
||||||
test('from Foo {...}', () => {
|
|
||||||
|
|
||||||
expect(cqn = SELECT `*,a,b as c` .from `Foo`)
|
|
||||||
.to.eql(SELECT `*,a,b as c`. from(Foo))
|
|
||||||
.to.eql(SELECT('*','a',{b:'c'}).from`Foo`)
|
|
||||||
.to.eql(SELECT('*','a',{b:'c'}).from(Foo))
|
|
||||||
.to.eql(SELECT(['*','a',{b:'c'}]).from(Foo))
|
|
||||||
.to.eql(SELECT.columns('*','a',{b:'c'}).from(Foo))
|
|
||||||
.to.eql(SELECT.columns(['*','a',{b:'c'}]).from(Foo))
|
|
||||||
.to.eql(SELECT.columns((foo) => { foo`.*`, foo.a, foo.b`as c` }).from(Foo))
|
|
||||||
.to.eql(SELECT.columns((foo) => { foo('*'), foo.a, foo.b.as('c') }).from(Foo))
|
|
||||||
.to.eql(SELECT.from(Foo).columns('*','a',{b:'c'}))
|
|
||||||
.to.eql(SELECT.from(Foo).columns(['*','a',{b:'c'}]))
|
|
||||||
.to.eql(SELECT.from(Foo).columns((foo) => { foo`.*`, foo.a, foo.b`as c` }))
|
|
||||||
.to.eql(SELECT.from(Foo).columns((foo) => { foo('*'), foo.a, foo.b.as('c') }))
|
|
||||||
.to.eql(SELECT.from(Foo,['*','a',{b:'c'}]))
|
|
||||||
.to.eql(SELECT.from(Foo, (foo) => { foo`.*`, foo.a, foo.b`as c` }))
|
|
||||||
.to.eql(SELECT.from(Foo, (foo) => { foo('*'), foo.a, foo.b.as('c') }))
|
|
||||||
|
|
||||||
expect.plain(cqn)
|
|
||||||
.to.eql({
|
|
||||||
SELECT: {
|
SELECT: {
|
||||||
from: { ref: ['Foo'] },
|
// REVISIT: add one:true?
|
||||||
columns: [ STAR, { ref: ['a'] }, { ref: ['b'], as: 'c' }],
|
from: { ref: [{ id: 'Foo', where: [{ val: 11 }] }] },
|
||||||
|
columns: [{ ref: ['a'] }],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
cdr && expect.plain(cqn)
|
expect(SELECT.from(Foo, 11, ['a']))
|
||||||
.to.eql(CQL`SELECT *,a,b as c from Foo`)
|
.to.eql(SELECT.from(Foo, 11, (foo) => foo.a))
|
||||||
.to.eql(CQL`SELECT from Foo {*,a,b as c}`)
|
|
||||||
|
|
||||||
// Test combination with key as second argument to .from
|
|
||||||
expect(cqn = SELECT.from(Foo, 11, ['a']))
|
|
||||||
.to.eql(SELECT.from(Foo, 11, foo => foo.a))
|
|
||||||
|
|
||||||
if (cds.version >= '5.6.0') {
|
|
||||||
expect.one(cqn)
|
|
||||||
.to.eql({
|
|
||||||
SELECT: {
|
|
||||||
one: true,
|
|
||||||
from: { ref: [{ id: 'Foo', where: [{ ref: ['ID'] }, '=', { val: 11 }]}] },
|
|
||||||
columns: [{ ref: ['a'] }]
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
expect.one(cqn)
|
|
||||||
.to.eql({
|
.to.eql({
|
||||||
|
// REVISIT: should produce CQN as the ones above?
|
||||||
SELECT: {
|
SELECT: {
|
||||||
one: true,
|
one: true,
|
||||||
from: { ref: ['Foo'] },
|
from: { ref: ['Foo'] },
|
||||||
@@ -269,56 +98,110 @@ describe('cds.ql → cqn', () => {
|
|||||||
where: [{ ref: ['ID'] }, '=', { val: 11 }],
|
where: [{ ref: ['ID'] }, '=', { val: 11 }],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test('with nested expands', () => {
|
test('from ( ..., => {...})', () => {
|
||||||
// SELECT from Foo { *, x, bar.*, car{*}, boo { *, moo.zoo } }
|
// single *, prefix and postfix, as array and function
|
||||||
expect(cqn =
|
let parsed, fluid
|
||||||
SELECT.from (Foo, foo => {
|
expect((parsed = CQL`SELECT * from Foo`)).to.eql(CQL`SELECT from Foo{*}`)
|
||||||
foo`*`, foo.x, foo.car`*`, foo.boo (b => {
|
//> .to.eql... FIXME: see skipped 'should handle * correctly' below
|
||||||
b`*`, b.moo.zoo(
|
expect((fluid = SELECT('*').from(Foo)))
|
||||||
x => x.y.z
|
.to.eql(SELECT.from(Foo, ['*']))
|
||||||
)
|
.to.eql(SELECT.from(Foo, (foo) => foo('*')))
|
||||||
})
|
.to.eql(SELECT.from(Foo).columns('*'))
|
||||||
|
.to.eql(SELECT.from(Foo).columns((foo) => foo('*')))
|
||||||
|
.to.eql({
|
||||||
|
SELECT: { from: { ref: ['Foo'] }, columns: [cdr ? '*' : { ref: ['*'] }] },
|
||||||
})
|
})
|
||||||
).to.eql(
|
|
||||||
SELECT.from (Foo, foo => {
|
if (cdr) expect(parsed).to.eql(fluid)
|
||||||
foo('*'), foo.x, foo.car('*'), foo.boo (b => {
|
|
||||||
b('*'), b.moo.zoo(
|
// single column, prefix and postfix, as array and function
|
||||||
x => x.y.z
|
expect(CQL`SELECT a from Foo`)
|
||||||
)
|
expect(CQL`SELECT from Foo {a}`)
|
||||||
})
|
.to.eql(SELECT.from(Foo, ['a']))
|
||||||
|
.to.eql(SELECT.from(Foo, (foo) => foo.a))
|
||||||
|
.to.eql({
|
||||||
|
SELECT: { from: { ref: ['Foo'] }, columns: [{ ref: ['a'] }] },
|
||||||
|
})
|
||||||
|
|
||||||
|
// multiple columns, prefix and postfix, as array and function
|
||||||
|
expect(CQL`SELECT a,b as c from Foo`)
|
||||||
|
|
||||||
|
expect (CQL`SELECT from Foo {a,b as c}`).to.eql(cqn = {
|
||||||
|
SELECT: {
|
||||||
|
from: { ref: ['Foo'] },
|
||||||
|
columns: [{ ref: ['a'] }, { ref: ['b'], as: 'c' }],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
expect(SELECT.from(Foo, ['a', { b: 'c' }])).to.eql(cqn)
|
||||||
|
expect(
|
||||||
|
SELECT.from(Foo, (foo) => {
|
||||||
|
foo.a, foo.b.as('c')
|
||||||
|
})
|
||||||
|
).to.eql(cqn)
|
||||||
|
expect(SELECT.from(Foo).columns('a', { b: 'c' })).to.eql(cqn)
|
||||||
|
expect(SELECT.from(Foo).columns(['a', { b: 'c' }])).to.eql(cqn)
|
||||||
|
expect(
|
||||||
|
SELECT.from(Foo).columns((foo) => {
|
||||||
|
foo.a, foo.b.as('c')
|
||||||
|
})
|
||||||
|
).to.eql(cqn)
|
||||||
|
|
||||||
|
// multiple columns and *, prefix and postfix, as array and function
|
||||||
|
expect(CQL`SELECT *,a,b from Foo`).to.eql(CQL`SELECT from Foo{*,a,b}`)
|
||||||
|
//> .to.eql... FIXME: see skipped 'should handle * correctly' below
|
||||||
|
expect(SELECT.from(Foo, ['a', 'b', '*']))
|
||||||
|
.to.eql(SELECT.from(Foo).columns('a', 'b', '*'))
|
||||||
|
.to.eql(SELECT.from(Foo).columns(['a', 'b', '*']))
|
||||||
|
.to.eql(
|
||||||
|
SELECT.from(Foo, (foo) => {
|
||||||
|
foo.a, foo.b, foo('*')
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
expect.plain(cqn)
|
|
||||||
.to.eql({
|
.to.eql({
|
||||||
|
SELECT: {
|
||||||
|
from: { ref: ['Foo'] },
|
||||||
|
columns: [{ ref: ['a'] }, { ref: ['b'] }, cdr ? '*' : { ref: ['*'] }],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('from ( ..., => _.expand ( x=>{...}))', () => {
|
||||||
|
// SELECT from Foo { *, x, bar.*, car{*}, boo { *, moo.zoo } }
|
||||||
|
expect(
|
||||||
|
SELECT.from(Foo, (foo) => {
|
||||||
|
foo('*'),
|
||||||
|
foo.x,
|
||||||
|
foo.car('*'),
|
||||||
|
foo.boo((b) => {
|
||||||
|
b('*'), b.moo.zoo((x) => x.y.z)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
).to.eql({
|
||||||
SELECT: {
|
SELECT: {
|
||||||
from: { ref: ['Foo'] },
|
from: { ref: ['Foo'] },
|
||||||
columns: [
|
columns: [
|
||||||
STAR,
|
cdr ? '*' : { ref: ['*'] },
|
||||||
{ ref: ['x'] },
|
{ ref: ['x'] },
|
||||||
{ ref: ['car'], expand: ['*'] },
|
{ ref: ['car'], expand: ['*'] },
|
||||||
{
|
{
|
||||||
ref: ['boo'],
|
ref: ['boo'],
|
||||||
expand: [ '*', { ref: ['moo', 'zoo'], expand: [{ ref: ['y', 'z'] }] }],
|
expand: ['*', { ref: ['moo', 'zoo'], expand: [{ ref: ['y', 'z'] }] }],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('from ( ..., => _.inline ( _=>{...}))', () => {
|
||||||
test('with nested inlines', () => {
|
|
||||||
// SELECT from Foo { *, x, bar.*, car{*}, boo { *, moo.zoo } }
|
// SELECT from Foo { *, x, bar.*, car{*}, boo { *, moo.zoo } }
|
||||||
expect.plain(
|
expect(
|
||||||
SELECT.from (Foo, foo => {
|
SELECT.from(Foo, (foo) => {
|
||||||
foo.bar `*`,
|
foo.bar('*'),
|
||||||
foo.bar `.*`, //> leading dot indicates inline
|
foo.bar('.*'), //> leading dot indicates inline
|
||||||
foo.boo(_ => _.moo.zoo), //> underscore arg name indicates inline
|
foo.boo((x) => x.moo.zoo),
|
||||||
foo.boo(x => x.moo.zoo)
|
foo.boo((_) => _.moo.zoo) //> underscore arg name indicates inline
|
||||||
})
|
})
|
||||||
).to.eql({
|
).to.eql({
|
||||||
SELECT: {
|
SELECT: {
|
||||||
@@ -326,142 +209,62 @@ describe('cds.ql → cqn', () => {
|
|||||||
columns: [
|
columns: [
|
||||||
{ ref: ['bar'], expand: ['*'] },
|
{ ref: ['bar'], expand: ['*'] },
|
||||||
{ ref: ['bar'], inline: ['*'] },
|
{ ref: ['bar'], inline: ['*'] },
|
||||||
{ ref: ['boo'], inline: [{ ref: ['moo', 'zoo'] }] },
|
|
||||||
{ ref: ['boo'], expand: [{ ref: ['moo', 'zoo'] }] },
|
{ ref: ['boo'], expand: [{ ref: ['moo', 'zoo'] }] },
|
||||||
|
{ ref: ['boo'], inline: [{ ref: ['moo', 'zoo'] }] },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
})}
|
test('one / distinct ...', () => {
|
||||||
|
expect(SELECT.distinct.from(Foo).SELECT)
|
||||||
|
// .to.eql(CQL(`SELECT distinct from Foo`).SELECT)
|
||||||
|
.to.eql(SELECT.distinct(Foo).SELECT)
|
||||||
|
.to.eql({ distinct: true, from: { ref: ['Foo'] } })
|
||||||
|
|
||||||
describe ('SELECT where...', ()=>{
|
expect(SELECT.one.from(Foo).SELECT)
|
||||||
|
// .to.eql(CQL(`SELECT one from Foo`).SELECT)
|
||||||
|
.to.eql(SELECT.one(Foo).SELECT)
|
||||||
|
.to.eql({ one: true, from: { ref: ['Foo'] } })
|
||||||
|
|
||||||
|
expect(SELECT.one('a').from(Foo).SELECT)
|
||||||
|
// .to.eql(CQL(`SELECT distinct a from Foo`).SELECT)
|
||||||
|
.to.eql(SELECT.one(['a']).from(Foo).SELECT)
|
||||||
|
.to.eql(SELECT.one(Foo, ['a']).SELECT)
|
||||||
|
.to.eql(SELECT.one(Foo, (foo) => foo.a).SELECT)
|
||||||
|
.to.eql(SELECT.one.from(Foo, (foo) => foo.a).SELECT)
|
||||||
|
.to.eql(SELECT.one.from(Foo, ['a']).SELECT)
|
||||||
|
.to.eql({
|
||||||
|
one: true,
|
||||||
|
from: { ref: ['Foo'] },
|
||||||
|
columns: [{ ref: ['a'] }],
|
||||||
|
})
|
||||||
|
// same for works distinct
|
||||||
|
})
|
||||||
|
|
||||||
it('should correctly handle { ... and:{...} }', () => {
|
it('should correctly handle { ... and:{...} }', () => {
|
||||||
expect(SELECT.from(Foo).where({ x: 1, and: { y: 2, or: { z: 3 } } })).to.eql({
|
expect(SELECT.from(Foo).where({ x: 1, and: { y: 2, or: { z: 3 } } })).to.eql({
|
||||||
SELECT: {
|
SELECT: {
|
||||||
from: { ref: ['Foo'] },
|
from: { ref: ['Foo'] },
|
||||||
where: cdr ? [
|
where: [
|
||||||
{ ref: ['x'] },
|
|
||||||
'=',
|
|
||||||
{ val: 1 },
|
|
||||||
'and',
|
|
||||||
// '(',
|
|
||||||
{xpr:[
|
|
||||||
{ ref: ['y'] },
|
|
||||||
'=',
|
|
||||||
{ val: 2 },
|
|
||||||
'or',
|
|
||||||
{ ref: ['z'] },
|
|
||||||
'=',
|
|
||||||
{ val: 3 },
|
|
||||||
]},
|
|
||||||
// ')',
|
|
||||||
] : [
|
|
||||||
{ ref: ['x'] },
|
{ ref: ['x'] },
|
||||||
'=',
|
'=',
|
||||||
{ val: 1 },
|
{ val: 1 },
|
||||||
'and',
|
'and',
|
||||||
'(',
|
'(',
|
||||||
// {xpr:[
|
{ ref: ['y'] },
|
||||||
{ ref: ['y'] },
|
'=',
|
||||||
'=',
|
{ val: 2 },
|
||||||
{ val: 2 },
|
'or',
|
||||||
'or',
|
{ ref: ['z'] },
|
||||||
{ ref: ['z'] },
|
'=',
|
||||||
'=',
|
{ val: 3 },
|
||||||
{ val: 3 },
|
|
||||||
// ]},
|
|
||||||
')',
|
')',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
test ("where x='*'", ()=>{
|
|
||||||
if (cdr)
|
|
||||||
expect (SELECT.from(Foo).where({x:'*'}))
|
|
||||||
.to.eql(SELECT.from(Foo).where("x='*'"))
|
|
||||||
.to.eql(SELECT.from(Foo).where("x=",'*'))
|
|
||||||
.to.eql(SELECT.from(Foo).where`x=${'*'}`)
|
|
||||||
.to.eql(
|
|
||||||
CQL`SELECT from Foo where x='*'`
|
|
||||||
)
|
|
||||||
if (cdr)
|
|
||||||
expect (SELECT.from(Foo).where({x:['*',1]}))
|
|
||||||
.to.eql(SELECT.from(Foo).where("x in ('*',1)"))
|
|
||||||
.to.eql(SELECT.from(Foo).where("x in",['*',1]))
|
|
||||||
.to.eql(SELECT.from(Foo).where`x in ${['*',1]}`)
|
|
||||||
.to.eql(
|
|
||||||
CQL`SELECT from Foo where x in ('*',1)`
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
test ('where, and, or', ()=>{
|
|
||||||
expect (
|
|
||||||
SELECT.from(Foo).where({x:1,and:{y:2}})
|
|
||||||
).to.eql (
|
|
||||||
CQL`SELECT from Foo where x=1 and y=2`
|
|
||||||
) .to.eql ({ SELECT: {
|
|
||||||
from: {ref:['Foo']},
|
|
||||||
where: [
|
|
||||||
{ref:['x']}, '=', {val:1},
|
|
||||||
'and',
|
|
||||||
{ref:['y']}, '=', {val:2}
|
|
||||||
]
|
|
||||||
}})
|
|
||||||
|
|
||||||
expect (
|
|
||||||
SELECT.from(Foo).where({x:1,or:{y:2}})
|
|
||||||
).to.eql (
|
|
||||||
CQL`SELECT from Foo where x=1 or y=2`
|
|
||||||
).to.eql ({ SELECT: {
|
|
||||||
from: {ref:['Foo']},
|
|
||||||
where: [
|
|
||||||
{ref:['x']}, '=', {val:1},
|
|
||||||
'or',
|
|
||||||
{ref:['y']}, '=', {val:2}
|
|
||||||
]
|
|
||||||
}})
|
|
||||||
|
|
||||||
expect (
|
|
||||||
SELECT.from(Foo).where({x:1,and:{y:2}}).or({z:3})
|
|
||||||
).to.eql (
|
|
||||||
CQL`SELECT from Foo where x=1 and y=2 or z=3`
|
|
||||||
)
|
|
||||||
|
|
||||||
if (cdr) expect (
|
|
||||||
SELECT.from(Foo).where({x:1}).and({y:2,or:{z:3}})
|
|
||||||
).to.eql (
|
|
||||||
CQL`SELECT from Foo where x=1 and ( y=2 or z=3 )`
|
|
||||||
)
|
|
||||||
|
|
||||||
if (cdr) expect (
|
|
||||||
SELECT.from(Foo).where({1:1}).and({x:1,or:{x:2}}).and({y:2,or:{z:3}})
|
|
||||||
).to.eql (
|
|
||||||
CQL`SELECT from Foo where 1=1 and ( x=1 or x=2 ) and ( y=2 or z=3 )`
|
|
||||||
)
|
|
||||||
|
|
||||||
if (cdr) expect (
|
|
||||||
SELECT.from(Foo).where({x:1,or:{x:2}}).and({y:2,or:{z:3}})
|
|
||||||
).to.eql (
|
|
||||||
CQL`SELECT from Foo where ( x=1 or x=2 ) and ( y=2 or z=3 )`
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('where ({x:[undefined]})', () => {
|
|
||||||
if (cdr) expect (
|
|
||||||
SELECT.from(Foo).where({x:[undefined]})
|
|
||||||
).to.eql ({ SELECT: {
|
|
||||||
from: {ref:['Foo']},
|
|
||||||
where: [
|
|
||||||
{ref:['x']},
|
|
||||||
'in',
|
|
||||||
{ list: [ {val:undefined} ] }
|
|
||||||
]
|
|
||||||
}})
|
|
||||||
})
|
|
||||||
|
|
||||||
test('where ( ... cql | {x:y} )', () => {
|
test('where ( ... cql | {x:y} )', () => {
|
||||||
const args = [`foo`, "'bar'", 3]
|
const args = [`foo`, "'bar'", 3]
|
||||||
const ID = 11
|
const ID = 11
|
||||||
@@ -477,17 +280,18 @@ describe('cds.ql → cqn', () => {
|
|||||||
).to.eql({
|
).to.eql({
|
||||||
SELECT: {
|
SELECT: {
|
||||||
from: { ref: ['Foo'] },
|
from: { ref: ['Foo'] },
|
||||||
where: cdr ? [
|
where: cds.version >= '5.3.0'
|
||||||
{ ref: ['ID'] },
|
? [
|
||||||
'=',
|
// '(', //> this one is not required
|
||||||
{ val: ID },
|
{ ref: ['ID'] },
|
||||||
'and',
|
'=',
|
||||||
{ ref: ['args'] },
|
{ val: ID },
|
||||||
'in',
|
'and',
|
||||||
{ list: args.map(val => ({ val })) },
|
{ ref: ['args'] },
|
||||||
'and',
|
'in',
|
||||||
{
|
{ list: args.map(val => ({ val })) },
|
||||||
xpr: [
|
'and',
|
||||||
|
'(', //> this one is missing, and that's changing the logic -> that's a BUG
|
||||||
{ ref: ['x'] },
|
{ ref: ['x'] },
|
||||||
'like',
|
'like',
|
||||||
{ val: '%x%' },
|
{ val: '%x%' },
|
||||||
@@ -495,28 +299,29 @@ describe('cds.ql → cqn', () => {
|
|||||||
{ ref: ['y'] },
|
{ ref: ['y'] },
|
||||||
'>=',
|
'>=',
|
||||||
{ val: 9 },
|
{ val: 9 },
|
||||||
|
')',
|
||||||
]
|
]
|
||||||
},
|
: [
|
||||||
] : [
|
// '(', //> this one is not required
|
||||||
{ ref: ['ID'] },
|
{ ref: ['ID'] },
|
||||||
'=',
|
'=',
|
||||||
{ val: ID },
|
{ val: ID },
|
||||||
'and',
|
'and',
|
||||||
{ ref: ['args'] },
|
{ ref: ['args'] },
|
||||||
'in',
|
'in',
|
||||||
{ list: args.map(val => ({ val })) },
|
{ val: args },
|
||||||
'and',
|
'and',
|
||||||
'(',
|
'(', //> this one is missing, and that's changing the logic -> that's a BUG
|
||||||
{ ref: ['x'] },
|
{ ref: ['x'] },
|
||||||
'like',
|
'like',
|
||||||
{ val: '%x%' },
|
{ val: '%x%' },
|
||||||
'or',
|
'or',
|
||||||
{ ref: ['y'] },
|
{ ref: ['y'] },
|
||||||
'>=',
|
'>=',
|
||||||
{ val: 9 },
|
{ val: 9 },
|
||||||
')',
|
')',
|
||||||
],
|
],
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// using CQL fragments -> uses cds.parse.expr
|
// using CQL fragments -> uses cds.parse.expr
|
||||||
@@ -601,32 +406,12 @@ describe('cds.ql → cqn', () => {
|
|||||||
).to.eql(cqn)
|
).to.eql(cqn)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('w/ plain SQL', () => {
|
it('w/ plain SQL', () => {
|
||||||
expect(SELECT.from(Books) + 'WHERE ...').to.eql(
|
expect(SELECT.from(Books) + 'WHERE ...').to.eql(
|
||||||
'SELECT * FROM capire_bookshop_Books WHERE ...'
|
'SELECT * FROM capire_bookshop_Books WHERE ...'
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should consistently handle *', () => {
|
|
||||||
if (!cdr) return
|
|
||||||
expect({
|
|
||||||
SELECT: { from: { ref: ['Foo'] }, columns: ['*'] },
|
|
||||||
})
|
|
||||||
.to.eql(CQL`SELECT * from Foo`)
|
|
||||||
.to.eql(CQL`SELECT from Foo{*}`)
|
|
||||||
.to.eql(SELECT('*').from(Foo))
|
|
||||||
.to.eql(SELECT.from(Foo,['*']))
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should consistently handle lists', () => {
|
|
||||||
if (!cdr) return
|
|
||||||
const ID = 11, args = [{ref:['foo']}, "bar", 3]
|
|
||||||
const cqn = CQL`SELECT from Foo where ID=11 and x in (foo,'bar',3)`
|
|
||||||
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(SELECT.from(Foo).where({ ID, x:args })).to.eql(cqn)
|
|
||||||
})
|
|
||||||
|
|
||||||
//
|
//
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -681,31 +466,21 @@ describe('cds.ql → cqn', () => {
|
|||||||
|
|
||||||
describe(`UPDATE...`, () => {
|
describe(`UPDATE...`, () => {
|
||||||
test('entity (..., <key>)', () => {
|
test('entity (..., <key>)', () => {
|
||||||
const cqnWhere = {
|
|
||||||
UPDATE: {
|
|
||||||
entity: 'capire.bookshop.Books',
|
|
||||||
where: [{ ref: ['ID'] }, '=', { val: 4711 }],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
expect(UPDATE(Books).where({ ID: 4711 }))
|
|
||||||
.to.eql(UPDATE(Books).where(`ID=`, 4711))
|
|
||||||
.to.eql(cqnWhere)
|
|
||||||
|
|
||||||
const cqnKey = (cds.version >= '5.6.0') ?
|
|
||||||
{
|
|
||||||
UPDATE: {
|
|
||||||
entity: { ref: [{ id: 'capire.bookshop.Books', where: [{ ref: ['ID'] }, '=', { val: 4711 }] }] }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
: cqnWhere
|
|
||||||
expect(UPDATE(Books, 4711))
|
expect(UPDATE(Books, 4711))
|
||||||
.to.eql(UPDATE(Books, { ID: 4711 }))
|
.to.eql(UPDATE(Books, { ID: 4711 }))
|
||||||
.to.eql(UPDATE(Books).byKey(4711))
|
.to.eql(UPDATE(Books).byKey(4711))
|
||||||
.to.eql(UPDATE(Books).byKey({ ID: 4711 }))
|
.to.eql(UPDATE(Books).byKey({ ID: 4711 }))
|
||||||
|
.to.eql(UPDATE(Books).where({ ID: 4711 }))
|
||||||
|
.to.eql(UPDATE(Books).where(`ID=`, 4711))
|
||||||
.to.eql(UPDATE.entity(Books, 4711))
|
.to.eql(UPDATE.entity(Books, 4711))
|
||||||
.to.eql(UPDATE.entity(Books, { ID: 4711 }))
|
.to.eql(UPDATE.entity(Books, { ID: 4711 }))
|
||||||
// etc...
|
// etc...
|
||||||
.to.eql(cqnKey)
|
.to.eql({
|
||||||
|
UPDATE: {
|
||||||
|
entity: 'capire.bookshop.Books',
|
||||||
|
where: [{ ref: ['ID'] }, '=', { val: 4711 }],
|
||||||
|
},
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -756,29 +531,20 @@ describe('cds.ql → cqn', () => {
|
|||||||
|
|
||||||
describe(`DELETE...`, () => {
|
describe(`DELETE...`, () => {
|
||||||
test('from (..., <key>)', () => {
|
test('from (..., <key>)', () => {
|
||||||
const cqnWhere = {
|
|
||||||
DELETE: {
|
|
||||||
from: 'capire.bookshop.Books',
|
|
||||||
where: [{ ref: ['ID'] }, '=', { val: 4711 }],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
expect(DELETE.from(Books).where({ ID: 4711 }))
|
|
||||||
.to.eql(DELETE.from(Books).where(`ID=`, 4711))
|
|
||||||
.to.eql(cqnWhere)
|
|
||||||
const cqnKey = (cds.version >= '5.6.0') ?
|
|
||||||
{
|
|
||||||
DELETE: {
|
|
||||||
from: { ref: [{ id: 'capire.bookshop.Books', where: [{ ref: ['ID'] }, '=', { val: 4711 }]}] }
|
|
||||||
},
|
|
||||||
} : cqnWhere
|
|
||||||
|
|
||||||
expect(DELETE(Books, 4711))
|
expect(DELETE(Books, 4711))
|
||||||
.to.eql(DELETE(Books, { ID: 4711 }))
|
.to.eql(DELETE(Books, { ID: 4711 }))
|
||||||
.to.eql(DELETE.from(Books, 4711))
|
.to.eql(DELETE.from(Books, 4711))
|
||||||
.to.eql(DELETE.from(Books, { ID: 4711 }))
|
.to.eql(DELETE.from(Books, { ID: 4711 }))
|
||||||
.to.eql(DELETE.from(Books).byKey(4711))
|
.to.eql(DELETE.from(Books).byKey(4711))
|
||||||
.to.eql(DELETE.from(Books).byKey({ ID: 4711 }))
|
.to.eql(DELETE.from(Books).byKey({ ID: 4711 }))
|
||||||
.to.eql(cqnKey)
|
.to.eql(DELETE.from(Books).where({ ID: 4711 }))
|
||||||
|
.to.eql(DELETE.from(Books).where(`ID=`, 4711))
|
||||||
|
.to.eql({
|
||||||
|
DELETE: {
|
||||||
|
from: 'capire.bookshop.Books',
|
||||||
|
where: [{ ref: ['ID'] }, '=', { val: 4711 }],
|
||||||
|
},
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
test('/w plain SQL', () => {
|
test('/w plain SQL', () => {
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
const cds = require('@sap/cds/lib')
|
const { expect } = require('../test') .run (
|
||||||
const { expect } = cds.test (
|
|
||||||
'serve', 'AdminService', '--from', '@capire/bookshop,@capire/common', '--in-memory'
|
'serve', 'AdminService', '--from', '@capire/bookshop,@capire/common', '--in-memory'
|
||||||
)
|
)
|
||||||
|
const cds = require('@sap/cds/lib')
|
||||||
|
|
||||||
describe('Consuming Services locally', () => {
|
describe('Consuming Services locally', () => {
|
||||||
//
|
//
|
||||||
it('bootstrapped the database successfully', ()=>{
|
it('bootrapped the database successfully', ()=>{
|
||||||
const { AdminService } = cds.services
|
const { AdminService } = cds.services
|
||||||
const { Authors } = AdminService.entities
|
const { Authors } = AdminService.entities
|
||||||
expect(AdminService).not.to.be.undefined
|
expect(AdminService).not.to.be.undefined
|
||||||
@@ -15,17 +15,17 @@ describe('Consuming Services locally', () => {
|
|||||||
it('supports targets as strings or reflected defs', async () => {
|
it('supports targets as strings or reflected defs', async () => {
|
||||||
const AdminService = await cds.connect.to('AdminService')
|
const AdminService = await cds.connect.to('AdminService')
|
||||||
const { Authors } = AdminService.entities
|
const { Authors } = AdminService.entities
|
||||||
expect (await SELECT.from(Authors))
|
const _ = expect (await AdminService.read(Authors))
|
||||||
.to.eql(await SELECT.from('Authors'))
|
|
||||||
.to.eql(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)))
|
||||||
.to.eql(await AdminService.run(SELECT.from('Authors')))
|
// temporary workaround
|
||||||
|
if (cds.version >= '4.2.0')
|
||||||
|
_.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 () => {
|
||||||
const AdminService = await cds.connect.to('AdminService')
|
const AdminService = await cds.connect.to('AdminService')
|
||||||
const authors = await AdminService.read (`Authors`, a => {
|
const query = SELECT.from('Authors', (a) => {
|
||||||
a.name,
|
a.name,
|
||||||
a.books((b) => {
|
a.books((b) => {
|
||||||
b.title,
|
b.title,
|
||||||
@@ -34,6 +34,10 @@ describe('Consuming Services locally', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
}).where(`name like`, 'E%')
|
}).where(`name like`, 'E%')
|
||||||
|
// temporary workaround
|
||||||
|
if (cds.version < '4.2.0')
|
||||||
|
query.SELECT.from.ref[0] = 'AdminService.Authors'
|
||||||
|
const authors = await AdminService.run(query)
|
||||||
expect(authors).to.containSubset([
|
expect(authors).to.containSubset([
|
||||||
{
|
{
|
||||||
name: 'Emily Brontë',
|
name: 'Emily Brontë',
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
|
const { GET, POST, expect } = require('../test') .run ('bookshop')
|
||||||
const cds = require('@sap/cds/lib')
|
const cds = require('@sap/cds/lib')
|
||||||
const { GET, POST, expect } = cds.test(__dirname+'/../bookshop')
|
|
||||||
if (cds.User.default) cds.User.default = cds.User.Privileged // hard core monkey patch
|
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
|
else cds.User = cds.User.Privileged // hard core monkey patch for older cds releases
|
||||||
|
|
||||||
describe('Custom Handlers', () => {
|
describe('Custom Handlers', () => {
|
||||||
|
|
||||||
it('should reject out-of-stock orders', async () => {
|
it('should reject out-of-stock orders', async () => {
|
||||||
await POST `/browse/submitOrder ${{ book: 201, quantity: 5 }}`
|
await POST('/browse/submitOrder', { book: 201, amount: 5 })
|
||||||
await POST `/browse/submitOrder ${{ book: 201, quantity: 5 }}`
|
await POST('/browse/submitOrder', { book: 201, amount: 5 })
|
||||||
await expect(POST `/browse/submitOrder ${{ book: 201, quantity: 5 }}`).to.be.rejectedWith(/409 - 5 exceeds stock for book #201/)
|
await expect(POST('/browse/submitOrder', { book: 201, amount: 5 })).to.be.rejectedWith(/409 - 5 exceeds stock for book #201/)
|
||||||
const { data } = await GET`/admin/Books/201/stock/$value`
|
const { data } = await GET`/admin/Books/201/stock/$value`
|
||||||
expect(data).to.equal(2)
|
expect(data).to.equal(2)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
const cds = require('@sap/cds/lib')
|
const { GET, expect } = require('../test') .run ('serve','hello/world.cds')
|
||||||
const { GET, expect } = cds.test (__dirname+'/../hello')
|
|
||||||
|
|
||||||
describe('Hello world!', () => {
|
describe('Hello world!', () => {
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
|
const {expect} = require('../test')
|
||||||
const cds = require('@sap/cds/lib')
|
const cds = require('@sap/cds/lib')
|
||||||
const {expect} = cds.test
|
|
||||||
|
const { parse:cdr } = cds.ql
|
||||||
|
|
||||||
// should become cds.compile(...) when cds5 is released
|
// should become cds.compile(...) when cds5 is released
|
||||||
const model = cds.compile.to.csn (`
|
const model = cds.compile.to.csn (`
|
||||||
@@ -74,9 +76,11 @@ describe('Hierarchical Data', ()=>{
|
|||||||
const expected = [
|
const expected = [
|
||||||
{ ID:100, name:'Some Cats...' },
|
{ ID:100, name:'Some Cats...' },
|
||||||
{ ID:101, name:'Cat' },
|
{ ID:101, name:'Cat' },
|
||||||
|
{ ID:104, name:'Aristocat' }, // REVISIT: Should be deleted as well?
|
||||||
{ ID:108, name:'Catweazle' }
|
{ ID:108, name:'Catweazle' }
|
||||||
]
|
]
|
||||||
expect ( await SELECT`ID,name`.from(Cats) ).to.eql (expected)
|
if (cdr) expect ( await SELECT.from(Cats) ).to.containSubset (expected)
|
||||||
|
else expect ( await SELECT.from(Cats) ).to.eql (expected)
|
||||||
})
|
})
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|||||||
2
test/index.js
Normal file
2
test/index.js
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
const cds = require('@sap/cds')
|
||||||
|
module.exports = cds.test.in(__dirname,'..')
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
|
const { GET, expect } = require('../test') .run ('serve', 'test/localized-data.cds', '--in-memory')
|
||||||
const cds = require('@sap/cds/lib')
|
const cds = require('@sap/cds/lib')
|
||||||
const { GET, expect } = cds.test.run ('serve', __dirname+'/localized-data.cds', '--in-memory')
|
|
||||||
if (cds.User.default) cds.User.default = cds.User.Privileged // hard core monkey patch
|
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
|
else cds.User = cds.User.Privileged // hard core monkey patch for older cds releases
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
const { expect } = require('../test')
|
||||||
const cds = require('@sap/cds/lib')
|
const cds = require('@sap/cds/lib')
|
||||||
const { expect } = cds.test
|
|
||||||
const _model = '@capire/reviews'
|
const _model = '@capire/reviews'
|
||||||
if (cds.User.default) cds.User.default = cds.User.Privileged // hard core monkey patch
|
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
|
else cds.User = cds.User.Privileged // hard core monkey patch for older cds releases
|
||||||
@@ -60,11 +60,11 @@ describe('Messaging', ()=>{
|
|||||||
expect(M).equals(N)
|
expect(M).equals(N)
|
||||||
expect(received.length).equals(N)
|
expect(received.length).equals(N)
|
||||||
expect(received.map(m=>m.data)).to.deep.equal([
|
expect(received.map(m=>m.data)).to.deep.equal([
|
||||||
{ count: 1, subject: '201', rating: 1 },
|
{ subject: '201', rating: 1 },
|
||||||
{ count: 2, subject: '201', rating: 1.5 },
|
{ subject: '201', rating: 1.5 },
|
||||||
{ count: 3, subject: '201', rating: 2 },
|
{ subject: '201', rating: 2 },
|
||||||
{ count: 4, subject: '201', rating: 2.5 },
|
{ subject: '201', rating: 2.5 },
|
||||||
{ count: 5, subject: '201', rating: 3 },
|
{ subject: '201', rating: 3 },
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
const { GET, expect } = require('../test') .run ('bookshop')
|
||||||
const cds = require('@sap/cds/lib')
|
const cds = require('@sap/cds/lib')
|
||||||
const { GET, expect } = cds.test ('@capire/bookshop')
|
|
||||||
if (cds.User.default) cds.User.default = cds.User.Privileged // hard core monkey patch
|
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
|
else cds.User = cds.User.Privileged // hard core monkey patch for older cds releases
|
||||||
|
|
||||||
@@ -18,9 +18,9 @@ describe('OData Protocol', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('supports $search in multiple fields', async () => {
|
it('supports $search in multiple fields', async () => {
|
||||||
const { data } = await GET `/browse/Books ${{
|
const { data } = await GET(`/browse/Books`, {
|
||||||
params: { $search: 'Po', $select: `title,author` },
|
params: { $search: 'Po', $select: `title,author` },
|
||||||
}}`
|
})
|
||||||
expect(data.value).to.eql([
|
expect(data.value).to.eql([
|
||||||
{ ID: 201, title: 'Wuthering Heights', author: 'Emily Brontë' },
|
{ ID: 201, title: 'Wuthering Heights', author: 'Emily Brontë' },
|
||||||
{ ID: 207, title: 'Jane Eyre', author: 'Charlotte Brontë' },
|
{ ID: 207, title: 'Jane Eyre', author: 'Charlotte Brontë' },
|
||||||
|
|||||||
@@ -1,54 +0,0 @@
|
|||||||
|
|
||||||
const { fork } = require('child_process')
|
|
||||||
const { resolve } = require('path')
|
|
||||||
const Axios = require('axios')
|
|
||||||
const verbose = process.env.CDS_TEST_VERBOSE
|
|
||||||
// ||true
|
|
||||||
|
|
||||||
describe('Local NPM registry', () => {
|
|
||||||
let registry
|
|
||||||
let axios
|
|
||||||
const cwd = resolve(__dirname, '..')
|
|
||||||
|
|
||||||
beforeAll(async ()=> {
|
|
||||||
const env = Object.assign(process.env, {PORT:'0'})
|
|
||||||
const res = await exec (resolve(cwd, '.registry/server.js'), {cwd, stdio: 'pipe', env})
|
|
||||||
registry = res.cp
|
|
||||||
axios = Axios.default.create ({ baseURL: res.url, validateStatus: (status)=>status<500 })
|
|
||||||
})
|
|
||||||
|
|
||||||
afterAll(() => { registry.kill() })
|
|
||||||
|
|
||||||
for (const mod of ['bookshop','fiori','orders','reviews']) {
|
|
||||||
it(`should serve ${mod}`, async () => {
|
|
||||||
const resp = await axios.get(`/@capire/${mod}`)
|
|
||||||
expect(resp.data).toMatchObject({name: `@capire/${mod}`, versions:{}})
|
|
||||||
const versions = Object.values(resp.data.versions)
|
|
||||||
await axios.get(versions[0].dist.tarball)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
it(`should return 404 for unknown packages`, async () => {
|
|
||||||
let resp = await axios.get(`/@capire/foo`)
|
|
||||||
expect(resp.status).toEqual(404)
|
|
||||||
resp = await axios.get(`/foo`)
|
|
||||||
expect(resp.status).toEqual(404)
|
|
||||||
})
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
function exec (script, opts) {
|
|
||||||
return new Promise((resolve, reject)=> {
|
|
||||||
const cp = fork (script, [], opts)
|
|
||||||
.on('error', err => reject(new Error(err)))
|
|
||||||
cp.stdout.on('data', chunk => {
|
|
||||||
if (verbose) console.log(chunk.toString())
|
|
||||||
if (chunk.toString().match(/listening.*(http:.*:\d+)/i)) {
|
|
||||||
resolve({cp, url:RegExp.$1})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
cp.stderr.on('data', chunk => {
|
|
||||||
if (verbose) console.error(chunk.toString())
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user