Compare commits
2 Commits
graphql
...
cds.deploy
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
adaeb6ab05 | ||
|
|
6e88453dea |
6
.github/workflows/node.js.yml
vendored
6
.github/workflows/node.js.yml
vendored
@@ -5,9 +5,9 @@ name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
branches: [ master ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -24,5 +24,5 @@ jobs:
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- run: npm ci
|
||||
- run: npm install
|
||||
- run: npm test
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -15,5 +15,3 @@ default-env.json
|
||||
packages/messageBox
|
||||
reviews/msg-box
|
||||
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,19 @@
|
||||
const { exec } = require ('child_process')
|
||||
const isWin = process.platform === 'win32'
|
||||
const express = require ('express')
|
||||
const fs = require ('fs')
|
||||
const app = express()
|
||||
|
||||
const { PORT=4444 } = process.env
|
||||
const [,,port=PORT,scope='@capire'] = process.argv
|
||||
const cwd = __dirname
|
||||
|
||||
// clean up on start (exit handler might not complete on Windows)
|
||||
exec(isWin ? 'del *.tgz' : 'rm *.tgz', {cwd})
|
||||
|
||||
const [,,port=PORT] = process.argv
|
||||
process.chdir(__dirname)
|
||||
|
||||
app.use('/-/:tarball', (req,res,next) => {
|
||||
console.debug ('GET', req.params)
|
||||
try {
|
||||
const { tarball } = req.params
|
||||
const [, pkg ] = /^\w+-(\w+)/.exec(tarball)
|
||||
const [, pkg ] = /^capire-(\w+)/.exec(tarball)
|
||||
fs.lstat(tarball,(err => {
|
||||
if (err) exec(`npm pack ../${pkg}`,{cwd},next)
|
||||
if (err) exec(`npm pack ../${pkg}`,next)
|
||||
else next()
|
||||
}))
|
||||
} catch (e) {
|
||||
@@ -30,14 +25,12 @@ app.use('/-/:tarball', (req,res,next) => {
|
||||
app.use('/-', express.static(__dirname))
|
||||
|
||||
app.get('/*', (req,res)=>{
|
||||
const urlRegex = /^\/(@\w+)\/(\w+)/
|
||||
const url = decodeURIComponent(req.url)
|
||||
console.debug ('GET',url)
|
||||
try {
|
||||
if (!urlRegex.test(url)) return res.sendStatus(404)
|
||||
const [, scpe, pkg ] = urlRegex.exec(url)
|
||||
const package = require (`${scpe}/${pkg}/package.json`)
|
||||
const tarball = `${scpe.slice(1)}-${pkg}-${package.version}.tgz`
|
||||
const [, capire, pkg ] = /^\/(@capire)\/(\w+)/.exec(url)
|
||||
const package = require (`${capire}/${pkg}/package.json`)
|
||||
const tarball = `capire-${pkg}-${package.version}.tgz`
|
||||
// https://github.com/npm/registry/blob/master/docs/responses/package-metadata.md
|
||||
res.json({
|
||||
"name": package.name,
|
||||
@@ -49,30 +42,29 @@ app.get('/*', (req,res)=>{
|
||||
"name": package.name,
|
||||
"version": package.version,
|
||||
"dist": {
|
||||
"tarball": `/-/${tarball}`
|
||||
"tarball": `http://localhost:${port}/-/${tarball}`
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
} catch (e) {
|
||||
if (e.code === 'MODULE_NOT_FOUND') return res.sendStatus(404)
|
||||
console.error(e); throw e
|
||||
console.error(e)
|
||||
res.sendStatus(404)
|
||||
}
|
||||
})
|
||||
|
||||
const server = app.listen(port, ()=>{
|
||||
const url = `http://localhost:${server.address().port}`
|
||||
console.log (`npm set ${scope}:registry=${url}`)
|
||||
exec(`npm set ${scope}:registry=${url}`)
|
||||
console.log (`${scope} registry listening on ${url}`)
|
||||
app.listen(port, ()=>{
|
||||
console.log (`npm set @capire:registry=http://localhost:${port}`)
|
||||
console.log (`@capire registry listening on http://localhost:${port}`)
|
||||
exec(`npm set @capire:registry=http://localhost:${port}`)
|
||||
})
|
||||
|
||||
|
||||
const _exit = ()=>{
|
||||
server.close()
|
||||
exec(`npm conf rm "${scope}:registry"`, ()=> { process.exit() })
|
||||
console.log ('\nnpm conf rm @capire:registry')
|
||||
exec('npm conf rm @capire:registry')
|
||||
exec('rm *.tgz')
|
||||
process.exit()
|
||||
}
|
||||
|
||||
process.on ('SIGTERM',_exit)
|
||||
process.on ('SIGHUP',_exit)
|
||||
process.on ('SIGINT',_exit)
|
||||
|
||||
@@ -105,5 +105,5 @@
|
||||
"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": "main"
|
||||
"ref": "master"
|
||||
}
|
||||
37
.vscode/launch.json
vendored
37
.vscode/launch.json
vendored
@@ -4,31 +4,28 @@
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Attach by Process ID",
|
||||
"processId": "${command:PickProcess}",
|
||||
"request": "attach",
|
||||
"skipFiles": [
|
||||
"<node_internals>/**"
|
||||
],
|
||||
"type": "pwa-node"
|
||||
},
|
||||
{
|
||||
"name": "bookshop",
|
||||
"command": "npx cds watch bookshop",
|
||||
"type": "node-terminal",
|
||||
"command": "cds watch bookshop",
|
||||
"request": "launch",
|
||||
"skipFiles": [
|
||||
"<node_internals>/**",
|
||||
"**/node_modules/**",
|
||||
"**/cds/lib/lazy.js",
|
||||
"**/cds/lib/req/cls.js",
|
||||
"**/odata-v4/okra/**"
|
||||
]
|
||||
"type": "node-terminal",
|
||||
"skipFiles": ["<node_internals>/**"]
|
||||
},
|
||||
{
|
||||
"name": "Fiori App",
|
||||
"command": "npx cds watch fiori",
|
||||
"type": "node-terminal",
|
||||
"name": "Fiori app",
|
||||
"command": "cds watch fiori",
|
||||
"request": "launch",
|
||||
"skipFiles": [
|
||||
"<node_internals>/**",
|
||||
"**/node_modules/**",
|
||||
"**/cds/lib/lazy.js",
|
||||
"**/cds/lib/req/cls.js",
|
||||
"**/odata-v4/okra/**"
|
||||
]
|
||||
"type": "node-terminal",
|
||||
"skipFiles": ["<node_internals>/**"]
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
@@ -36,7 +33,7 @@
|
||||
"type": "pickString",
|
||||
"id": "sample",
|
||||
"description": "Which sample do you want to start?",
|
||||
"options": [ "bookshop", "fiori", "reviews", "reviews" ],
|
||||
"options": ["bookshop", "fiori", "reviews", "reviews/test/bookshop"],
|
||||
"default": "bookshop"
|
||||
}
|
||||
]
|
||||
|
||||
9
.vscode/settings.json
vendored
9
.vscode/settings.json
vendored
@@ -4,14 +4,5 @@
|
||||
"**/.gitignore": true,
|
||||
"**/.vscode": true,
|
||||
"LICENSES/**": true
|
||||
},
|
||||
"debug.javascript.terminalOptions": {
|
||||
"skipFiles": [
|
||||
"<node_internals>/**",
|
||||
"**/node_modules/**",
|
||||
"**/cds/lib/lazy.js",
|
||||
"**/cds/lib/req/cls.js",
|
||||
"**/odata-v4/okra/**"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ Find here a collection of samples for the [SAP Cloud Application Programming Mod
|
||||
|
||||
### 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
|
||||
git clone https://github.com/sap-samples/cloud-cap-samples samples
|
||||
@@ -83,4 +83,4 @@ In case you've a question, find a bug, or otherwise need support, use our [commu
|
||||
|
||||
## License
|
||||
|
||||
Copyright (c) 2021 SAP SE or an SAP affiliate company. All rights reserved. This file is licensed under the Apache Software License, version 2.0 except as noted otherwise in the [LICENSE](LICENSE.txt) file.
|
||||
Copyright (c) 2021 SAP SE or an SAP affiliate company. All rights reserved. This file is licensed under the Apache Software License, version 2.0 except as noted otherwise in the [LICENSE](LICENSES/Apache-2.0.txt) file.
|
||||
|
||||
@@ -10,7 +10,7 @@ const books = new Vue ({
|
||||
data: {
|
||||
list: [],
|
||||
book: undefined,
|
||||
order: { quantity:1, succeeded:'', failed:'' }
|
||||
order: { amount:1, succeeded:'', failed:'' }
|
||||
},
|
||||
|
||||
methods: {
|
||||
@@ -26,18 +26,18 @@ const books = new Vue ({
|
||||
const book = books.book = books.list [eve.currentTarget.rowIndex-1]
|
||||
const res = await GET(`/Books/${book.ID}?$select=descr,stock,image`)
|
||||
Object.assign (book, res.data)
|
||||
books.order = { quantity:1 }
|
||||
books.order = { amount:1 }
|
||||
setTimeout (()=> $('form > input').focus(), 111)
|
||||
},
|
||||
|
||||
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 {
|
||||
const res = await POST(`/submitOrder`, { quantity, book: book.ID })
|
||||
const res = await POST(`/submitOrder`, { amount, book: book.ID })
|
||||
book.stock = res.data.stock
|
||||
books.order = { quantity, succeeded: `Successfully ordered ${quantity} item(s).` }
|
||||
books.order = { amount, succeeded: `Successfully orderd ${amount} item(s).` }
|
||||
} 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.genre.name }}</td>
|
||||
<td class="rating-stars">
|
||||
{{ ('★'.repeat(Math.round(book.rating))+'☆☆☆☆☆').slice(0,5) }} ({{ book.numberOfReviews }})
|
||||
{{ ('★'.repeat(Math.round(book.rating))+'☆☆☆☆☆').slice(0,5) }}
|
||||
</td>
|
||||
<td>{{ book.currency && book.currency.symbol }} {{ book.price }}</td>
|
||||
</tr>
|
||||
@@ -48,7 +48,7 @@
|
||||
{{ book.stock }} in stock
|
||||
</label>
|
||||
<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">
|
||||
</form>
|
||||
<h4> {{ book.title }} </h4>
|
||||
|
||||
@@ -3,4 +3,4 @@ ID;title;descr;author_ID;stock;price;currency_code;genre_ID
|
||||
207;Jane Eyre;"Jane Eyre /ɛər/ (originally published as Jane Eyre: An Autobiography) is a novel by English writer Charlotte Brontë, published under the pen name ""Currer Bell"", on 16 October 1847, by Smith, Elder & Co. of London. The first American edition was published the following year by Harper & Brothers of New York. Primarily a bildungsroman, Jane Eyre follows the experiences of its eponymous heroine, including her growth to adulthood and her love for Mr. Rochester, the brooding master of Thornfield Hall. The novel revolutionised prose fiction in that the focus on Jane's moral and spiritual development is told through an intimate, first-person narrative, where actions and events are coloured by a psychological intensity. The book contains elements of social criticism, with a strong sense of Christian morality at its core and is considered by many to be ahead of its time because of Jane's individualistic character and how the novel approaches the topics of class, sexuality, religion and feminism.";107;11;12.34;GBP;11
|
||||
251;The Raven;"""The Raven"" is a narrative poem by American writer Edgar Allan Poe. First published in January 1845, the poem is often noted for its musicality, stylized language, and supernatural atmosphere. It tells of a talking raven's mysterious visit to a distraught lover, tracing the man's slow fall into madness. The lover, often identified as being a student, is lamenting the loss of his love, Lenore. Sitting on a bust of Pallas, the raven seems to further distress the protagonist with its constant repetition of the word ""Nevermore"". The poem makes use of folk, mythological, religious, and classical references.";150;333;13.13;USD;16
|
||||
252;Eleonora;"""Eleonora"" is a short story by Edgar Allan Poe, first published in 1842 in Philadelphia in the literary annual The Gift. It is often regarded as somewhat autobiographical and has a relatively ""happy"" ending.";150;555;14;USD;16
|
||||
271;Catweazle;Catweazle is a British fantasy television series, starring Geoffrey Bayldon in the title role, and created by Richard Carpenter for London Weekend Television. The first series, produced and directed by Quentin Lawrence, was screened in the UK on ITV in 1970. The second series, directed by David Reid and David Lane, was shown in 1971. Each series had thirteen episodes, most but not all written by Carpenter, who also published two books based on the scripts.;170;22;150;JPY;13
|
||||
271;Catweazle;Catweazle is a British fantasy television series, starring Geoffrey Bayldon in the title role, and created by Richard Carpenter for London Weekend Television. The first series, produced and directed by Quentin Lawrence, was screened in the UK on ITV in 1970. The second series, directed by David Reid and David Lane, was shown in 1971. Each series had thirteen episodes, most but not all written by Carpenter, who also published two books based on the scripts.;170;22;15;EUR;13
|
||||
|
@@ -1,2 +1 @@
|
||||
const { CatalogService } = require('./srv/cat-service')
|
||||
module.exports = { CatalogService }
|
||||
exports.CatalogService = require('./srv/cat-service')
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"description": "A simple self-contained bookshop service.",
|
||||
"dependencies": {
|
||||
"@capire/common": "*",
|
||||
"@sap/cds": "^5.0.4",
|
||||
"@sap/cds": ">=4",
|
||||
"express": "^4.17.1",
|
||||
"passport": "0.4.1"
|
||||
},
|
||||
@@ -16,7 +16,8 @@
|
||||
"cds": {
|
||||
"requires": {
|
||||
"db": {
|
||||
"kind": "sql"
|
||||
"kind": "sqlite",
|
||||
"model": "*"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BIN
bookshop/sqlite.db
Normal file
BIN
bookshop/sqlite.db
Normal file
Binary file not shown.
@@ -1,16 +1,14 @@
|
||||
using { sap.capire.bookshop as my } from '../db/schema';
|
||||
service CatalogService @(path:'/browse') {
|
||||
|
||||
/** For displaying lists of Books */
|
||||
@readonly entity ListOfBooks as projection on Books
|
||||
excluding { descr };
|
||||
|
||||
/** For display in details pages */
|
||||
@readonly entity Books as projection on my.Books { *,
|
||||
@readonly entity Books as SELECT from my.Books { *,
|
||||
author.name as author
|
||||
} excluding { createdBy, modifiedBy };
|
||||
|
||||
@readonly entity ListOfBooks as SELECT from Books
|
||||
excluding { descr };
|
||||
|
||||
@requires: 'authenticated-user'
|
||||
action submitOrder ( book: Books:ID, quantity: Integer ) returns { stock: Integer };
|
||||
event OrderedBook : { book: Books:ID; quantity: Integer; buyer: String };
|
||||
action submitOrder ( book: Books:ID, amount: Integer ) returns { stock: Integer };
|
||||
event OrderedBook : { book: Books:ID; amount: Integer; buyer: String };
|
||||
}
|
||||
|
||||
@@ -1,24 +1,25 @@
|
||||
const cds = require('@sap/cds')
|
||||
const { Books } = cds.entities ('sap.capire.bookshop')
|
||||
|
||||
class CatalogService extends cds.ApplicationService { init(){
|
||||
|
||||
const { Books } = cds.entities ('sap.capire.bookshop')
|
||||
|
||||
// Reduce stock of ordered books if available stock suffices
|
||||
this.on ('submitOrder', async req => {
|
||||
const {book,quantity} = req.data
|
||||
let {stock} = await SELECT `stock` .from (Books,book)
|
||||
if (stock >= quantity) {
|
||||
await UPDATE (Books,book) .with (`stock -=`, quantity)
|
||||
await this.emit ('OrderedBook', { book, quantity, buyer:req.user.id })
|
||||
const {book,amount} = req.data, tx = cds.tx(req)
|
||||
let {stock} = await tx.read('stock').from(Books,book)
|
||||
if (stock >= amount) {
|
||||
await tx.update (Books,book).with ({ stock: stock -= amount })
|
||||
await this.emit ('OrderedBook', { book, amount, buyer:req.user.id })
|
||||
return { stock }
|
||||
}
|
||||
else return req.error (409,`${quantity} exceeds stock for book #${book}`)
|
||||
else return req.error (409,`${amount} exceeds stock for book #${book}`)
|
||||
})
|
||||
|
||||
// Add some discount for overstocked books
|
||||
this.after ('READ','ListOfBooks', each => {
|
||||
if (each.stock > 111) each.title += ` -- 11% discount!`
|
||||
this.after ('READ','Books', each => {
|
||||
if (each.stock > 111) {
|
||||
each.title += ` -- 11% discount!`
|
||||
}
|
||||
})
|
||||
|
||||
return super.init()
|
||||
|
||||
19
bookshop/srv/server.js
Normal file
19
bookshop/srv/server.js
Normal file
@@ -0,0 +1,19 @@
|
||||
const cds = require ('@sap/cds')
|
||||
|
||||
cds.on('listening', async ()=>{ try { //> just to see the output at the end; will later on be in db service impl
|
||||
console.log ('\nFilling database with initial data...')
|
||||
const m = cds.model.minified() //> we likely should do that by default
|
||||
const tx = cds.tx()
|
||||
tx.run ([ //> this will be replaced by
|
||||
DELETE.from ('sap.capire.bookshop.Authors'),
|
||||
DELETE.from ('sap.capire.bookshop.Books'),
|
||||
DELETE.from ('sap.capire.bookshop.Books.texts'),
|
||||
DELETE.from ('sap.capire.bookshop.Genres'),
|
||||
DELETE.from ('sap.common.Currencies'),
|
||||
DELETE.from ('sap.common.Currencies.texts'),
|
||||
])
|
||||
await cds.deploy(m).to(tx,{ddl:false})
|
||||
await tx.commit()
|
||||
} catch(e) { console.error(e) }})
|
||||
|
||||
module.exports = cds.server
|
||||
@@ -71,7 +71,7 @@ POST {{server}}/browse/submitOrder
|
||||
Content-Type: application/json
|
||||
{{me}}
|
||||
|
||||
{ "book":201, "quantity":5 }
|
||||
{ "book":201, "amount":5 }
|
||||
|
||||
|
||||
### ------------------------------------------------------------------------
|
||||
|
||||
@@ -1,39 +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",
|
||||
"[development]": {
|
||||
"model": "db/sqlite"
|
||||
},
|
||||
"[production]": {
|
||||
"model": "db/hana"
|
||||
}
|
||||
}
|
||||
},
|
||||
"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
|
||||
}
|
||||
@@ -5,11 +5,9 @@ CAD;de;Kanadischer Dollar;Kanadischer Dollar
|
||||
AUD;de;Australischer Dollar;Australischer Dollar
|
||||
GBP;de;Pfund;Britische Pfund
|
||||
ILS;de;Schekel;Israelische Schekel
|
||||
JPY;de;Yen;Japanische Yen
|
||||
EUR;fr;euro;de la Zone euro
|
||||
USD;fr;dollar;dollar des États-Unis
|
||||
CAD;fr;dollar canadien;dollar canadien
|
||||
AUD;fr;dollar australien;dollar australien
|
||||
GBP;fr;livre sterling;pound sterling
|
||||
ILS;fr;Shekel;shekel israelien
|
||||
JPY;fr;Yen;Yen japonais
|
||||
|
2
fiori/.env
Normal file
2
fiori/.env
Normal file
@@ -0,0 +1,2 @@
|
||||
# cds.requires.messaging.kind = file-based-messaging
|
||||
PORT = 4004
|
||||
@@ -1,5 +1,4 @@
|
||||
using { AdminService } from '@capire/bookshop';
|
||||
using from '../common'; // to help UI linter get the complete annotations
|
||||
using { AdminService } from '../../db/schema';
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
@@ -71,7 +70,7 @@ annotate AdminService.Authors with @(
|
||||
annotate sap.capire.bookshop.Books with @fiori.draft.enabled;
|
||||
annotate AdminService.Books with @odata.draft.enabled;
|
||||
|
||||
annotate AdminService.Books.texts with @(
|
||||
annotate AdminService.Books_texts with @(
|
||||
UI: {
|
||||
Identification: [{Value:title}],
|
||||
SelectionFields: [ locale, title ],
|
||||
@@ -84,11 +83,11 @@ annotate AdminService.Books.texts with @(
|
||||
);
|
||||
|
||||
// Add Value Help for Locales
|
||||
annotate AdminService.Books.texts {
|
||||
annotate AdminService.Books_texts {
|
||||
locale @ValueList:{entity:'Languages',type:#fixed}
|
||||
}
|
||||
// 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';
|
||||
extend service AdminService {
|
||||
@readonly entity Languages as projection on sap.common.Languages;
|
||||
entity Languages as projection on sap.common.Languages;
|
||||
}
|
||||
|
||||
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>
|
||||
@@ -6,11 +6,11 @@ using CatalogService from '@capire/bookshop';
|
||||
//
|
||||
annotate CatalogService.Books with @(
|
||||
UI: {
|
||||
HeaderInfo: {
|
||||
HeaderInfo: {
|
||||
TypeName: 'Book',
|
||||
TypeNamePlural: 'Books',
|
||||
Description: {Value: author}
|
||||
},
|
||||
Description: {Value: author}
|
||||
},
|
||||
HeaderFacets: [
|
||||
{$Type: 'UI.ReferenceFacet', Label: '{i18n>Description}', Target: '@UI.FieldGroup#Descr'},
|
||||
],
|
||||
@@ -38,7 +38,7 @@ annotate CatalogService.Books with @(
|
||||
//
|
||||
annotate CatalogService.Books with @(
|
||||
UI: {
|
||||
SelectionFields: [ ID, price, currency_code ],
|
||||
SelectionFields: [ ID, price, currency_code ],
|
||||
LineItem: [
|
||||
{Value: title},
|
||||
{Value: author, Label:'{i18n>Author}'},
|
||||
|
||||
@@ -54,7 +54,7 @@ annotate my.Books with {
|
||||
title @title:'{i18n>Title}';
|
||||
genre @title:'{i18n>Genre}' @Common: { Text: genre.name, TextArrangement: #TextOnly };
|
||||
author @title:'{i18n>Author}' @Common: { Text: author.name, TextArrangement: #TextOnly };
|
||||
price @title:'{i18n>Price}' @Measures.ISOCurrency: currency_code;
|
||||
price @title:'{i18n>Price}';
|
||||
stock @title:'{i18n>Stock}';
|
||||
descr @UI.MultiLineText;
|
||||
}
|
||||
|
||||
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>
|
||||
@@ -6,4 +6,7 @@ using from './admin/fiori-service';
|
||||
using from './browse/fiori-service';
|
||||
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,8 +2,11 @@
|
||||
"name": "@capire/fiori",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@capire/bookstore": "*",
|
||||
"@sap/cds": "^5",
|
||||
"@capire/bookshop": "*",
|
||||
"@capire/reviews": "*",
|
||||
"@capire/orders": "*",
|
||||
"@capire/common": "*",
|
||||
"@sap/cds": ">=4",
|
||||
"express": "^4.17.1",
|
||||
"passport": "^0.4.1"
|
||||
},
|
||||
@@ -12,6 +15,9 @@
|
||||
"watch": "cds watch"
|
||||
},
|
||||
"cds": {
|
||||
"hana": {
|
||||
"deploy-format": "hdbtable"
|
||||
},
|
||||
"requires": {
|
||||
"auth": {
|
||||
"strategy": "dummy"
|
||||
@@ -24,13 +30,14 @@
|
||||
"kind": "odata",
|
||||
"model": "@capire/orders"
|
||||
},
|
||||
"messaging": {
|
||||
"[production]": { "kind": "enterprise-messaging" },
|
||||
"[development]": { "kind": "file-based-messaging" },
|
||||
"[hybrid!]": { "kind": "enterprise-messaging-shared" }
|
||||
},
|
||||
"hana": {
|
||||
"deploy-format": "hdbtable"
|
||||
"db": {
|
||||
"kind": "sql",
|
||||
"[development]": {
|
||||
"model": "db/sqlite"
|
||||
},
|
||||
"[production]": {
|
||||
"model": "db/hana"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
// respective reuse packages and services
|
||||
// Mashing up imported models...
|
||||
//
|
||||
|
||||
using { sap.capire.bookshop.Books } from '@capire/bookshop';
|
||||
@@ -9,22 +8,18 @@ using { sap.capire.bookshop.Books } from '@capire/bookshop';
|
||||
//
|
||||
// Extend Books with access to Reviews and average ratings
|
||||
//
|
||||
|
||||
using { ReviewsService.Reviews } from '@capire/reviews';
|
||||
extend Books with {
|
||||
reviews : Composition of many Reviews on reviews.subject = $self.ID;
|
||||
rating : Decimal;
|
||||
numberOfReviews : Integer;
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// Extend Orders with Books as Products
|
||||
//
|
||||
|
||||
using { sap.capire.orders.Orders_Items } from '@capire/orders';
|
||||
extend Orders_Items with {
|
||||
book : Association to Books on product.ID = book.ID
|
||||
}
|
||||
|
||||
|
||||
// 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
|
||||
|
||||
@@ -20,29 +20,30 @@ module.exports = async()=>{ // called by server.js
|
||||
CatalogService.prepend (srv => srv.on ('READ', 'Books/reviews', (req) => {
|
||||
console.debug ('> delegating request to ReviewsService')
|
||||
const [id] = req.params, { columns, limit } = req.query.SELECT
|
||||
return ReviewsService.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
|
||||
//
|
||||
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 })
|
||||
return OrdersService.tx(msg).create ('Orders').entries({
|
||||
OrderNo: 'Order at '+ (new Date).toLocaleString(),
|
||||
Items: [{ product:{ID:`${book}`}, title, price, quantity }],
|
||||
Items: [{ product:{ID:`${book}`}, title, price, amount }],
|
||||
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) => {
|
||||
console.debug ('> received:', msg.event, msg.data)
|
||||
const { subject, count, rating } = msg.data
|
||||
return UPDATE(Books,subject).with({ numberOfReviews:count, rating })
|
||||
const { subject, rating } = msg.data
|
||||
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) => {
|
||||
console.debug ('> received:', msg.event, msg.data)
|
||||
const { product, deltaQuantity } = msg.data
|
||||
const { product, deltaAmount } = msg.data
|
||||
return UPDATE (Books) .where ('ID =', product)
|
||||
.and ('stock >=', deltaQuantity)
|
||||
.set ('stock -=', deltaQuantity)
|
||||
.and ('stock >=', deltaAmount)
|
||||
.set ('stock -=', deltaAmount)
|
||||
})
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
@bookshop = http://localhost:4004
|
||||
@reviews-service = {{bookshop}}/reviews
|
||||
# Uncomment this when running a separate reviews service
|
||||
# @reviews-service = http://localhost:4005/reviews
|
||||
@reviews-service = http://localhost:4005/reviews
|
||||
|
||||
|
||||
|
||||
@@ -38,11 +38,7 @@ GET {{bookshop}}/browse/Books(201)?
|
||||
&$select=ID,title,rating
|
||||
&$expand=reviews
|
||||
|
||||
###
|
||||
|
||||
GET {{bookshop}}/browse/Books?
|
||||
&$select=title,author&$expand=currency
|
||||
Accept-Language: de
|
||||
|
||||
#################################################
|
||||
#
|
||||
@@ -1 +0,0 @@
|
||||
PORT = 4007
|
||||
@@ -1,4 +0,0 @@
|
||||
book_ID;number;title
|
||||
201;1;Chapter 1
|
||||
201;2;Chapter 2
|
||||
201;3;Chapter 3
|
||||
|
@@ -1,26 +0,0 @@
|
||||
using {
|
||||
cuid,
|
||||
managed
|
||||
} from '@sap/cds/common';
|
||||
using {sap.capire.bookshop} from '@capire/bookshop';
|
||||
|
||||
namespace sap.capire.graphql;
|
||||
|
||||
extend bookshop.Books with {
|
||||
chapters : Composition of many Chapters
|
||||
on chapters.book = $self;
|
||||
}
|
||||
|
||||
entity Chapters : managed {
|
||||
key book : Association to bookshop.Books;
|
||||
key number : Integer;
|
||||
title : String;
|
||||
}
|
||||
|
||||
entity Orders : cuid, managed {
|
||||
@mandatory
|
||||
book : Association to bookshop.Books;
|
||||
@mandatory
|
||||
@assert.range : [ 1, 5 ]
|
||||
quantity : Integer;
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
# GraphQL
|
||||
GET http://localhost:4007/graphql?query={BookshopService{Books{title,author{name},chapters{number,title}}}}
|
||||
|
||||
###
|
||||
|
||||
# OData
|
||||
GET http://localhost:4007/bookshop/Books?$select=title&$expand=author($select=name),chapters($select=number,title)
|
||||
@@ -1,16 +0,0 @@
|
||||
1. open `http://localhost:4007/graphql`
|
||||
2. paste into left field:
|
||||
```graphql
|
||||
{
|
||||
BookshopService {
|
||||
Books {
|
||||
title
|
||||
chapters {
|
||||
number
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
3. press play button
|
||||
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"name": "@capire/graphql",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@capire/bookshop": "*",
|
||||
"@graphql-tools/schema": "^8.3.1",
|
||||
"@sap/cds": "^5.6",
|
||||
"express-graphql": "^0.12.0",
|
||||
"graphql": "^16.0.1"
|
||||
},
|
||||
"cds": {
|
||||
"features": {
|
||||
"graphql": true
|
||||
},
|
||||
"requires": {
|
||||
"auth": {
|
||||
"kind": "dummy-auth"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
using {
|
||||
sap.capire.bookshop,
|
||||
sap.capire.graphql
|
||||
} from '../db/schema';
|
||||
|
||||
service BookshopService {
|
||||
entity Books as projection on bookshop.Books;
|
||||
entity Authors as projection on bookshop.Authors;
|
||||
entity Chapters as projection on graphql.Chapters;
|
||||
entity Orders as projection on graphql.Orders;
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
module.exports = function() {
|
||||
const { Orders, Books } = this.entities
|
||||
|
||||
this.before('CREATE', Orders, async function(req) {
|
||||
const { book_ID, quantity } = req.data
|
||||
|
||||
// reduce the stock, if enough are available, else reject the order
|
||||
const applied = await UPDATE(Books, book_ID).set({ stock: { '-=': quantity } }).where({ stock: { '>=': quantity }})
|
||||
if (!applied) req.reject(400, `Sorry, ${quantity} are not in stock`)
|
||||
})
|
||||
}
|
||||
@@ -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",
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
"test": "npx jest --silent",
|
||||
"start": "cds serve srv/world.cds",
|
||||
"start:ts": "cds-ts serve srv/world.cds"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^26.0.23",
|
||||
"@types/node": "^15.12.0",
|
||||
"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"
|
||||
}
|
||||
"watch": "cds serve world.cds"
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
module.exports = class say {
|
||||
hello(req: any) {
|
||||
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)
|
||||
})
|
||||
|
||||
})
|
||||
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,7 @@
|
||||
|
||||
|
||||
|
||||
using { OrdersService } from '../srv/orders-service';
|
||||
using { OrdersService } from '../../srv/orders-service';
|
||||
|
||||
|
||||
@odata.draft.enabled
|
||||
@@ -74,10 +74,10 @@ annotate OrdersService.Orders_Items with @(
|
||||
{Value: product_ID, Label:'Product ID'},
|
||||
{Value: title, Label:'Product Title'},
|
||||
{Value: price, Label:'Unit Price'},
|
||||
{Value: quantity, Label:'Quantity'},
|
||||
{Value: amount, Label:'Quantity'},
|
||||
],
|
||||
Identification: [ //Is the main field group
|
||||
{Value: quantity, Label:'Quantity'},
|
||||
{Value: amount, Label:'Amount'},
|
||||
{Value: title, Label:'Product'},
|
||||
{Value: price, Label:'Unit Price'},
|
||||
],
|
||||
@@ -86,7 +86,7 @@ annotate OrdersService.Orders_Items with @(
|
||||
],
|
||||
},
|
||||
) {
|
||||
quantity @(
|
||||
amount @(
|
||||
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
|
||||
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
|
||||
|
@@ -12,8 +12,8 @@ entity Orders_Items {
|
||||
key ID : UUID;
|
||||
up_ : Association to Orders;
|
||||
product : Association to Products @assert.integrity:false; // REVISIT: this is a temporary workaround for a glitch in cds-runtime
|
||||
quantity : Integer;
|
||||
title : String; //> intentionally replicated as snapshot from product.title
|
||||
amount : Integer;
|
||||
title : String;
|
||||
price : Double;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,6 @@
|
||||
"name": "@capire/orders",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@sap/cds": "^5"
|
||||
"@sap/cds": ">=4.3.0"
|
||||
}
|
||||
}
|
||||
@@ -7,30 +7,30 @@ class OrdersService extends cds.ApplicationService {
|
||||
|
||||
this.before ('UPDATE', 'Orders', async function(req) {
|
||||
const { ID, Items } = req.data
|
||||
if (Items) for (let { product_ID, quantity } of Items) {
|
||||
const { quantity:before } = await cds.tx(req).run (
|
||||
SELECT.one.from (OrderItems, oi => oi.quantity) .where ({up__ID:ID, product_ID})
|
||||
if (Items) for (let { product_ID, amount } of Items) {
|
||||
const { amount:before } = await cds.tx(req).run (
|
||||
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) {
|
||||
const { ID } = req.data
|
||||
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()
|
||||
}
|
||||
|
||||
/** order changed -> broadcast event */
|
||||
orderChanged (product, deltaQuantity) {
|
||||
orderChanged (product, deltaAmount) {
|
||||
// Emit events to inform subscribers about changes in orders
|
||||
console.log ('> emitting:', 'OrderChanged', { product, deltaQuantity })
|
||||
return this.emit ('OrderChanged', { product, deltaQuantity })
|
||||
console.log ('> emitting:', 'OrderChanged', { product, deltaAmount })
|
||||
return this.emit ('OrderChanged', { product, deltaAmount })
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
24
package.json
24
package.json
@@ -5,45 +5,35 @@
|
||||
"repository": "https://github.com/sap-samples/cloud-cap-samples.git",
|
||||
"author": "daniel.hutzel@sap.com",
|
||||
"dependencies": {
|
||||
"@capire/bookstore": "./bookstore",
|
||||
"@capire/bookshop": "./bookshop",
|
||||
"@capire/common": "./common",
|
||||
"@capire/fiori": "./fiori",
|
||||
"@capire/graphql": "./graphql",
|
||||
"@capire/hello": "./hello",
|
||||
"@capire/media": "./media",
|
||||
"@capire/orders": "./orders",
|
||||
"@capire/reviews": "./reviews",
|
||||
"@sap/cds": "^5.6"
|
||||
"@capire/reviews": "./reviews"
|
||||
},
|
||||
"devDependencies": {
|
||||
"chai": "^4.3.4",
|
||||
"chai": "^4.2.0",
|
||||
"chai-as-promised": "^7.1.1",
|
||||
"chai-subset": "^1.6.0",
|
||||
"sqlite3": "^5"
|
||||
"sqlite3": "^5.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"cleanup": "rm -rf node_modules && rm -rf */node_modules && rm -rf */*/node_modules",
|
||||
"registry": "node .registry/server.js",
|
||||
"bookshop": "cds watch bookshop",
|
||||
"fiori": "cds watch fiori",
|
||||
"hello": "cds watch hello",
|
||||
"media": "cds watch media",
|
||||
"mocha": "npx mocha || echo",
|
||||
"jest": "npx jest",
|
||||
"test": "npm run jest -- --silent",
|
||||
"test:hello": "cd hello && npm test"
|
||||
},
|
||||
"jest": {
|
||||
"testEnvironment": "node",
|
||||
"testTimeout": 20000,
|
||||
"testMatch": [
|
||||
"**/*.test.js"
|
||||
]
|
||||
"test": "npm run jest --silent"
|
||||
},
|
||||
"mocha": {
|
||||
"parallel": true
|
||||
},
|
||||
"jest": {
|
||||
"testEnvironment": "node"
|
||||
},
|
||||
"license": "SAP SAMPLE CODE LICENSE",
|
||||
"private": true
|
||||
}
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
# cds.requires.messaging.kind = file-based-messaging
|
||||
cds.requires.messaging.kind = file-based-messaging
|
||||
PORT = 4005
|
||||
@@ -7,17 +7,18 @@
|
||||
"index.cds"
|
||||
],
|
||||
"dependencies": {
|
||||
"@sap/cds": "^5",
|
||||
"@sap/cds": ">=4",
|
||||
"express": "^4.17.1"
|
||||
},
|
||||
"scripts": {
|
||||
"reviews-service": "cds watch",
|
||||
"books-reviewed": "cds watch ../reviewed"
|
||||
},
|
||||
"cds": {
|
||||
"requires": {
|
||||
"messaging": {
|
||||
"[development]": { "kind": "file-based-messaging" },
|
||||
"[hybrid]": { "kind": "enterprise-messaging-shared" },
|
||||
"[production]": { "kind": "enterprise-messaging" }
|
||||
},
|
||||
"db": { "kind": "sql" }
|
||||
"db": {
|
||||
"kind": "sql"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,10 +2,10 @@
|
||||
|
||||
## Run all-in-one
|
||||
|
||||
Open a terminal window and run the `fiori` app in it:
|
||||
Open a terminal window and run the bookshop in it:
|
||||
|
||||
```sh
|
||||
npm run fiori
|
||||
npm run bookshop
|
||||
```
|
||||
|
||||
|
||||
@@ -17,8 +17,8 @@ Open two terminal windows. In the first one start the reviews service stand-alon
|
||||
npm run reviews-service
|
||||
```
|
||||
|
||||
In the second one start the `fiori` app:
|
||||
In the second one start the bookshop:
|
||||
|
||||
```sh
|
||||
npm run fiori
|
||||
npm run bookshop
|
||||
```
|
||||
|
||||
@@ -8,11 +8,10 @@ service ReviewsService {
|
||||
action unlike (review: type of Reviews:ID);
|
||||
|
||||
// Async API
|
||||
event reviewed : {
|
||||
subject : type of Reviews:subject;
|
||||
count : Integer;
|
||||
rating : Decimal;
|
||||
}
|
||||
event reviewed : {
|
||||
subject: type of Reviews:subject;
|
||||
rating: Decimal(2,1)
|
||||
}
|
||||
|
||||
// Input validation
|
||||
annotate Reviews with {
|
||||
|
||||
@@ -12,11 +12,11 @@ module.exports = cds.service.impl (function(){
|
||||
// Emit an event to inform subscribers about new avg ratings for reviewed subjects
|
||||
this.after (['CREATE','UPDATE','DELETE'], 'Reviews', async function(_,req) {
|
||||
const {subject} = req.data
|
||||
const { count, rating } = await cds.tx(req) .run (
|
||||
SELECT.one `round(avg(rating),2) as rating, count(*) as count` .from (Reviews) .where ({subject})
|
||||
const {rating} = await cds.tx(req) .run (
|
||||
SELECT.one (['round(avg(rating),2) as rating']) .from (Reviews) .where ({subject})
|
||||
)
|
||||
global.it || console.log ('< emitting:', 'reviewed', { subject, count, rating })
|
||||
await this.emit ('reviewed', { subject, count, rating })
|
||||
global.it || console.log ('< emitting:', 'reviewed', { subject, rating })
|
||||
await this.emit ('reviewed', { subject, rating })
|
||||
})
|
||||
|
||||
// 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)
|
||||
|
||||
- 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)
|
||||
@@ -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
|
||||
|
||||
|
||||
## [@capire/bookstore](bookstore)
|
||||
## [@capire/fiori](fiori)
|
||||
|
||||
- A [composite app, reusing and combining](https://cap.cloud.sap/docs/guides/verticalize) these packages:
|
||||
- [@capire/bookshop](bookshop)
|
||||
- [@capire/reviews](reviews)
|
||||
- [@capire/orders](orders)
|
||||
- [@capire/common](common)
|
||||
- [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 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
|
||||
- [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
|
||||
|
||||
<br>
|
||||
|
||||
|
||||
@@ -1,159 +1,96 @@
|
||||
const { expect } = require('../test')
|
||||
const cds = require('@sap/cds/lib')
|
||||
const { expect } = cds.test
|
||||
const { cdr } = cds.ql
|
||||
const CQL = ([cql]) => cds.parse.cql(cql)
|
||||
const Foo = { name: 'Foo' }
|
||||
const Books = { name: 'capire.bookshop.Books' }
|
||||
|
||||
const { parse:cdr } = cds.ql
|
||||
|
||||
const STAR = cdr ? '*' : { ref: ['*'] }
|
||||
const skip = {to:{eql:()=>skip}}
|
||||
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
|
||||
// while jest has 'test' as alias to 'it', mocha doesn't
|
||||
if (!global.test) global.test = it
|
||||
|
||||
describe('cds.ql → cqn', () => {
|
||||
//
|
||||
let cqn
|
||||
|
||||
for (let each of ['SELECT', 'SELECT one', 'SELECT distinct']) {
|
||||
let SELECT; beforeEach(()=> SELECT = (
|
||||
each === 'SELECT distinct' ? cds.ql.SELECT.distinct :
|
||||
each === 'SELECT one' ? cds.ql.SELECT.one :
|
||||
cds.ql.SELECT
|
||||
))
|
||||
describe(`${each}...`, () => {
|
||||
describe.skip(`BUGS + GAPS...`, () => {
|
||||
|
||||
test(`from Foo`, () => {
|
||||
expect(cqn = SELECT `from Foo`)
|
||||
.to.eql(SELECT.from `Foo`)
|
||||
.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'] } },
|
||||
it('should consistently handle *', () => {
|
||||
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,['*']))
|
||||
})
|
||||
|
||||
|
||||
test('from Foo [<key>]', () => {
|
||||
it('should consistently handle lists', () => {
|
||||
const ID = 11, args = [`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)
|
||||
})
|
||||
|
||||
expect(cqn = SELECT`from Foo[11]`)
|
||||
.to.eql(SELECT`from Foo[${11}]`)
|
||||
.to.eql(SELECT.from `Foo[11]`)
|
||||
.to.eql(SELECT.from `Foo[${11}]`)
|
||||
.to.eql(SELECT`Foo[11]`)
|
||||
expect.plain(cqn)
|
||||
.to.eql(CQL`SELECT from Foo[11]`)
|
||||
.to.eql(srv.read`Foo[11]`)
|
||||
.to.eql({
|
||||
SELECT: { from: {
|
||||
ref: [{ id: 'Foo', where: [{ val: 11 }] }]
|
||||
}},
|
||||
})
|
||||
|
||||
|
||||
describe(`SELECT...`, () => {
|
||||
test('from ( Foo )', () => {
|
||||
expect({
|
||||
SELECT: { from: { ref: ['Foo'] } },
|
||||
})
|
||||
.to.eql(CQL`SELECT from Foo`)
|
||||
.to.eql(SELECT.from(Foo))
|
||||
})
|
||||
|
||||
test('from ( ..., <key>)', () => {
|
||||
// Compiler
|
||||
expect(CQL`SELECT from Foo[11]`).to.eql({
|
||||
SELECT: {
|
||||
// REVISIT: add one:true?
|
||||
from: { ref: [{ id: 'Foo', where: [{ val: 11 }] }] },
|
||||
},
|
||||
})
|
||||
|
||||
if (cdr) expect.plain (cqn)
|
||||
.to.eql(srv.read`Foo[${11}]`)
|
||||
.to.eql(SELECT`Foo[${11}]`)
|
||||
|
||||
expect((cqn = SELECT`from Foo[ID=11]`))
|
||||
.to.eql(SELECT`from Foo[ID=${11}]`)
|
||||
.to.eql(SELECT.from `Foo[ID=11]`)
|
||||
.to.eql(SELECT.from `Foo[ID=${11}]`)
|
||||
.to.eql(SELECT`Foo[ID=11]`)
|
||||
expect.plain(cqn)
|
||||
.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)
|
||||
.to.eql(SELECT`Foo[ID=${11}]`)
|
||||
.to.eql(srv.read`Foo[ID=${11}]`)
|
||||
|
||||
// Following implicitly resolve to SELECT.one
|
||||
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 }] }] },
|
||||
expect(CQL`SELECT from Foo[ID=11]`).to.eql({
|
||||
SELECT: {
|
||||
// REVISIT: add 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({
|
||||
// REVISIT: should produce CQN as the ones above?
|
||||
SELECT: {
|
||||
one: true,
|
||||
from: { ref: ['Foo'] },
|
||||
where: [{ ref: ['ID'] }, '=', { val: 11 }],
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
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({
|
||||
expect(CQL`SELECT from Foo[11]{a}`).to.eql({
|
||||
SELECT: {
|
||||
from: { ref: ['Foo'] },
|
||||
columns: [ STAR, { ref: ['a'] }, { ref: ['b'], as: 'c' }],
|
||||
// REVISIT: add one:true?
|
||||
from: { ref: [{ id: 'Foo', where: [{ val: 11 }] }] },
|
||||
columns: [{ ref: ['a'] }],
|
||||
},
|
||||
})
|
||||
|
||||
cdr && expect.plain(cqn)
|
||||
.to.eql(CQL`SELECT *,a,b as c from Foo`)
|
||||
.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)
|
||||
expect(SELECT.from(Foo, 11, ['a']))
|
||||
.to.eql(SELECT.from(Foo, 11, (foo) => foo.a))
|
||||
.to.eql({
|
||||
// REVISIT: should produce CQN as the ones above?
|
||||
SELECT: {
|
||||
one: true,
|
||||
from: { ref: ['Foo'] },
|
||||
@@ -161,199 +98,173 @@ describe('cds.ql → cqn', () => {
|
||||
where: [{ ref: ['ID'] }, '=', { val: 11 }],
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
test('with nested expands', () => {
|
||||
// SELECT from Foo { *, x, bar.*, car{*}, boo { *, moo.zoo } }
|
||||
expect(cqn =
|
||||
SELECT.from (Foo, foo => {
|
||||
foo`*`, foo.x, foo.car`*`, foo.boo (b => {
|
||||
b`*`, b.moo.zoo(
|
||||
x => x.y.z
|
||||
)
|
||||
})
|
||||
test('from ( ..., => {...})', () => {
|
||||
// single *, prefix and postfix, as array and function
|
||||
let parsed, fluid
|
||||
expect((parsed = CQL`SELECT * from Foo`)).to.eql(CQL`SELECT from Foo{*}`)
|
||||
//> .to.eql... FIXME: see skipped 'should handle * correctly' below
|
||||
expect((fluid = SELECT('*').from(Foo)))
|
||||
.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 => {
|
||||
foo('*'), foo.x, foo.car('*'), foo.boo (b => {
|
||||
b('*'), b.moo.zoo(
|
||||
x => x.y.z
|
||||
)
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
expect.plain(cqn)
|
||||
.to.eql({
|
||||
if (cdr) expect(parsed).to.eql(fluid)
|
||||
|
||||
// single column, prefix and postfix, as array and function
|
||||
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: [
|
||||
STAR,
|
||||
{ ref: ['x'] },
|
||||
{ ref: ['car'], expand: ['*'] },
|
||||
{
|
||||
ref: ['boo'],
|
||||
expand: [ '*', { ref: ['moo', 'zoo'], expand: [{ ref: ['y', 'z'] }] }],
|
||||
},
|
||||
],
|
||||
columns: [{ ref: ['a'] }, { ref: ['b'], as: 'c' }],
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
test('with nested inlines', () => {
|
||||
// SELECT from Foo { *, x, bar.*, car{*}, boo { *, moo.zoo } }
|
||||
expect.plain(
|
||||
SELECT.from (Foo, foo => {
|
||||
foo.bar `*`,
|
||||
foo.bar `.*`, //> leading dot indicates inline
|
||||
foo.boo(_ => _.moo.zoo), //> underscore arg name indicates inline
|
||||
foo.boo(x => x.moo.zoo)
|
||||
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('*')
|
||||
})
|
||||
)
|
||||
.to.eql({
|
||||
SELECT: {
|
||||
from: { ref: ['Foo'] },
|
||||
columns: [{ ref: ['a'] }, { ref: ['b'] }, cdr ? '*' : { ref: ['*'] }],
|
||||
},
|
||||
})
|
||||
).to.eql({
|
||||
SELECT: {
|
||||
from: { ref: ['Foo'] },
|
||||
columns: [
|
||||
{ ref: ['bar'], expand: ['*'] },
|
||||
{ ref: ['bar'], inline: ['*'] },
|
||||
{ ref: ['boo'], inline: [{ ref: ['moo', 'zoo'] }] },
|
||||
{ ref: ['boo'], expand: [{ ref: ['moo', 'zoo'] }] },
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
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: {
|
||||
from: { ref: ['Foo'] },
|
||||
columns: [
|
||||
cdr ? '*' : { ref: ['*'] },
|
||||
{ ref: ['x'] },
|
||||
{ ref: ['car'], expand: ['*'] },
|
||||
{
|
||||
ref: ['boo'],
|
||||
expand: ['*', { ref: ['moo', 'zoo'], expand: [{ ref: ['y', 'z'] }] }],
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test('from ( ..., => _.inline ( _=>{...}))', () => {
|
||||
// SELECT from Foo { *, x, bar.*, car{*}, boo { *, moo.zoo } }
|
||||
expect(
|
||||
SELECT.from(Foo, (foo) => {
|
||||
foo.bar('*'),
|
||||
foo.bar('.*'), //> leading dot indicates inline
|
||||
foo.boo((x) => x.moo.zoo),
|
||||
foo.boo((_) => _.moo.zoo) //> underscore arg name indicates inline
|
||||
})
|
||||
).to.eql({
|
||||
SELECT: {
|
||||
from: { ref: ['Foo'] },
|
||||
columns: [
|
||||
{ ref: ['bar'], expand: ['*'] },
|
||||
{ ref: ['bar'], inline: ['*'] },
|
||||
{ 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'] } })
|
||||
|
||||
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
|
||||
})
|
||||
|
||||
})}
|
||||
|
||||
describe ('SELECT where...', ()=>{
|
||||
|
||||
it('should correctly handle { ... and:{...} }', () => {
|
||||
expect(SELECT.from(Foo).where({ x: 1, and: { y: 2, or: { z: 3 } } })).to.eql({
|
||||
SELECT: {
|
||||
from: { ref: ['Foo'] },
|
||||
where: cdr ? [
|
||||
{ ref: ['x'] },
|
||||
'=',
|
||||
{ val: 1 },
|
||||
'and',
|
||||
// '(',
|
||||
{xpr:[
|
||||
{ ref: ['y'] },
|
||||
'=',
|
||||
{ val: 2 },
|
||||
'or',
|
||||
{ ref: ['z'] },
|
||||
'=',
|
||||
{ val: 3 },
|
||||
]},
|
||||
// ')',
|
||||
] : [
|
||||
where: [
|
||||
{ ref: ['x'] },
|
||||
'=',
|
||||
{ val: 1 },
|
||||
'and',
|
||||
'(',
|
||||
// {xpr:[
|
||||
{ ref: ['y'] },
|
||||
'=',
|
||||
{ val: 2 },
|
||||
'or',
|
||||
{ ref: ['z'] },
|
||||
'=',
|
||||
{ val: 3 },
|
||||
// ]},
|
||||
{ ref: ['y'] },
|
||||
'=',
|
||||
{ val: 2 },
|
||||
'or',
|
||||
{ ref: ['z'] },
|
||||
'=',
|
||||
{ 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} )', () => {
|
||||
const args = [`foo`, "'bar'", 3]
|
||||
const ID = 11
|
||||
@@ -369,17 +280,18 @@ describe('cds.ql → cqn', () => {
|
||||
).to.eql({
|
||||
SELECT: {
|
||||
from: { ref: ['Foo'] },
|
||||
where: cdr ? [
|
||||
{ ref: ['ID'] },
|
||||
'=',
|
||||
{ val: ID },
|
||||
'and',
|
||||
{ ref: ['args'] },
|
||||
'in',
|
||||
{ list: args.map(val => ({ val })) },
|
||||
'and',
|
||||
{
|
||||
xpr: [
|
||||
where: cdr
|
||||
? [
|
||||
// '(', //> this one is not required
|
||||
{ ref: ['ID'] },
|
||||
'=',
|
||||
{ val: ID },
|
||||
'and',
|
||||
{ ref: ['args'] },
|
||||
'in',
|
||||
{ val: args },
|
||||
'and',
|
||||
'(', //> this one is missing, and that's changing the logic -> that's a BUG
|
||||
{ ref: ['x'] },
|
||||
'like',
|
||||
{ val: '%x%' },
|
||||
@@ -387,28 +299,29 @@ describe('cds.ql → cqn', () => {
|
||||
{ ref: ['y'] },
|
||||
'>=',
|
||||
{ val: 9 },
|
||||
')',
|
||||
]
|
||||
},
|
||||
] : [
|
||||
{ ref: ['ID'] },
|
||||
'=',
|
||||
{ val: ID },
|
||||
'and',
|
||||
{ ref: ['args'] },
|
||||
'in',
|
||||
{ list: args.map(val => ({ val })) },
|
||||
'and',
|
||||
'(',
|
||||
{ ref: ['x'] },
|
||||
'like',
|
||||
{ val: '%x%' },
|
||||
'or',
|
||||
{ ref: ['y'] },
|
||||
'>=',
|
||||
{ val: 9 },
|
||||
')',
|
||||
],
|
||||
}
|
||||
: [
|
||||
// '(', //> this one is not required
|
||||
{ ref: ['ID'] },
|
||||
'=',
|
||||
{ val: ID },
|
||||
'and',
|
||||
{ ref: ['args'] },
|
||||
'in',
|
||||
{ val: args },
|
||||
'and',
|
||||
'(', //> this one is missing, and that's changing the logic -> that's a BUG
|
||||
{ ref: ['x'] },
|
||||
'like',
|
||||
{ val: '%x%' },
|
||||
'or',
|
||||
{ ref: ['y'] },
|
||||
'>=',
|
||||
{ val: 9 },
|
||||
')',
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
// using CQL fragments -> uses cds.parse.expr
|
||||
@@ -493,32 +406,12 @@ describe('cds.ql → cqn', () => {
|
||||
).to.eql(cqn)
|
||||
})
|
||||
|
||||
test('w/ plain SQL', () => {
|
||||
it('w/ plain SQL', () => {
|
||||
expect(SELECT.from(Books) + 'WHERE ...').to.eql(
|
||||
'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)
|
||||
})
|
||||
|
||||
//
|
||||
})
|
||||
|
||||
@@ -573,31 +466,21 @@ describe('cds.ql → cqn', () => {
|
||||
|
||||
describe(`UPDATE...`, () => {
|
||||
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))
|
||||
.to.eql(UPDATE(Books, { ID: 4711 }))
|
||||
.to.eql(UPDATE(Books).byKey(4711))
|
||||
.to.eql(UPDATE(Books).byKey({ ID: 4711 }))
|
||||
.to.eql(UPDATE(Books).where({ ID: 4711 }))
|
||||
.to.eql(UPDATE(Books).where(`ID=`, 4711))
|
||||
.to.eql(UPDATE.entity(Books, 4711))
|
||||
.to.eql(UPDATE.entity(Books, { ID: 4711 }))
|
||||
// etc...
|
||||
.to.eql(cqnKey)
|
||||
.to.eql({
|
||||
UPDATE: {
|
||||
entity: 'capire.bookshop.Books',
|
||||
where: [{ ref: ['ID'] }, '=', { val: 4711 }],
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
/*
|
||||
@@ -648,29 +531,20 @@ describe('cds.ql → cqn', () => {
|
||||
|
||||
describe(`DELETE...`, () => {
|
||||
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))
|
||||
.to.eql(DELETE(Books, { ID: 4711 }))
|
||||
.to.eql(DELETE.from(Books, 4711))
|
||||
.to.eql(DELETE.from(Books, { ID: 4711 }))
|
||||
.to.eql(DELETE.from(Books).byKey(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', () => {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
const cds = require('@sap/cds/lib')
|
||||
const { expect } = cds.test (
|
||||
const { expect } = require('../test') .run (
|
||||
'serve', 'AdminService', '--from', '@capire/bookshop,@capire/common', '--in-memory'
|
||||
)
|
||||
const cds = require('@sap/cds/lib')
|
||||
|
||||
describe('Consuming Services locally', () => {
|
||||
//
|
||||
it('bootstrapped the database successfully', ()=>{
|
||||
it('bootrapped the database successfully', ()=>{
|
||||
const { AdminService } = cds.services
|
||||
const { Authors } = AdminService.entities
|
||||
expect(AdminService).not.to.be.undefined
|
||||
@@ -15,17 +15,17 @@ describe('Consuming Services locally', () => {
|
||||
it('supports targets as strings or reflected defs', async () => {
|
||||
const AdminService = await cds.connect.to('AdminService')
|
||||
const { Authors } = AdminService.entities
|
||||
expect (await SELECT.from(Authors))
|
||||
.to.eql(await SELECT.from('Authors'))
|
||||
.to.eql(await AdminService.read(Authors))
|
||||
const _ = expect (await AdminService.read(Authors))
|
||||
.to.eql(await AdminService.read('Authors'))
|
||||
.to.eql(await AdminService.run(SELECT.from(Authors)))
|
||||
.to.eql(await AdminService.run(SELECT.from('Authors')))
|
||||
// temporary workaround
|
||||
if (cds.version >= '4.2.0')
|
||||
_.to.eql(await AdminService.run(SELECT.from('Authors')))
|
||||
})
|
||||
|
||||
it('allows reading from local services using cds.ql', async () => {
|
||||
const AdminService = await cds.connect.to('AdminService')
|
||||
const authors = await AdminService.read (`Authors`, a => {
|
||||
const query = SELECT.from('Authors', (a) => {
|
||||
a.name,
|
||||
a.books((b) => {
|
||||
b.title,
|
||||
@@ -34,6 +34,10 @@ describe('Consuming Services locally', () => {
|
||||
})
|
||||
})
|
||||
}).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([
|
||||
{
|
||||
name: 'Emily Brontë',
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
const { GET, POST, expect } = require('../test') .run ('bookshop')
|
||||
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
|
||||
else cds.User = cds.User.Privileged // hard core monkey patch for older cds releases
|
||||
|
||||
describe('Custom Handlers', () => {
|
||||
|
||||
it('should reject out-of-stock orders', async () => {
|
||||
await POST `/browse/submitOrder ${{ book: 201, quantity: 5 }}`
|
||||
await POST `/browse/submitOrder ${{ book: 201, quantity: 5 }}`
|
||||
await expect(POST `/browse/submitOrder ${{ book: 201, quantity: 5 }}`).to.be.rejectedWith(/409 - 5 exceeds stock for book #201/)
|
||||
await expect(
|
||||
Promise.all([
|
||||
POST('/browse/submitOrder', { book: 201, amount: 5 }),
|
||||
POST('/browse/submitOrder', { book: 201, amount: 5 }),
|
||||
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`
|
||||
expect(data).to.equal(2)
|
||||
})
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
const cds = require('@sap/cds/lib')
|
||||
const { GET, expect } = cds.test (__dirname+'/../hello')
|
||||
const { GET, expect } = require('../test') .run ('serve','hello/world.cds')
|
||||
|
||||
describe('Hello world!', () => {
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
const {expect} = require('../test')
|
||||
const cds = require('@sap/cds/lib')
|
||||
const {expect} = cds.test
|
||||
|
||||
// monkey patching older releases:
|
||||
if (!cds.compile.cdl) cds.compile.cdl = cds.parse
|
||||
const { parse:cdr } = cds.ql
|
||||
|
||||
// should become cds.compile(...) when cds5 is released
|
||||
const model = cds.compile.to.csn (`
|
||||
const model = cds.compile.cdl (`
|
||||
entity Categories {
|
||||
key ID : Integer;
|
||||
name : String;
|
||||
@@ -76,9 +77,9 @@ describe('Hierarchical Data', ()=>{
|
||||
const expected = [
|
||||
{ ID:100, name:'Some Cats...' },
|
||||
{ ID:101, name:'Cat' },
|
||||
{ ID:104, name:'Aristocat' }, // REVISIT: Should be deleted as well?
|
||||
{ ID:108, name:'Catweazle' }
|
||||
]
|
||||
return 'skipped as this will be fixed in a newer cds version'
|
||||
if (cdr) expect ( await SELECT.from(Cats) ).to.containSubset (expected)
|
||||
else expect ( await SELECT.from(Cats) ).to.eql (expected)
|
||||
})
|
||||
|
||||
6
test/index.js
Normal file
6
test/index.js
Normal file
@@ -0,0 +1,6 @@
|
||||
|
||||
const test = require('@sap/cds/lib/utils/tests').in(__dirname,'..')
|
||||
module.exports = Object.assign(test,{run:test})
|
||||
|
||||
// REVISIT: With upcoming release of @sap/cds this should become:
|
||||
// module.exports = require('@sap/cds/tests').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 { 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
|
||||
else cds.User = cds.User.Privileged // hard core monkey patch for older cds releases
|
||||
|
||||
@@ -43,7 +43,7 @@ describe('Localized Data', () => {
|
||||
{ title: 'Jane Eyre', author: 'Charlotte Brontë', currency: { name: 'Pfund' } },
|
||||
{ title: 'The Raven', author: 'Edgar Allen Poe', currency: { name: 'US-Dollar' } },
|
||||
{ title: 'Eleonora', author: 'Edgar Allen Poe', currency: { name: 'US-Dollar' } },
|
||||
{ title: 'Catweazle', author: 'Richard Carpenter', currency: { name: 'Yen' } },
|
||||
{ title: 'Catweazle', author: 'Richard Carpenter', currency: { name: 'Euro' } },
|
||||
])
|
||||
})
|
||||
|
||||
@@ -85,7 +85,7 @@ describe('Localized Data', () => {
|
||||
{ title: 'Jane Eyre', currency: { name: 'British Pound' } },
|
||||
{ title: 'The Raven', currency: { name: 'US Dollar' } },
|
||||
{ title: 'Eleonora', currency: { name: 'US Dollar' } },
|
||||
{ title: 'Catweazle', currency: { name: 'Yen' } },
|
||||
{ title: 'Catweazle', currency: { name: 'Euro' } },
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const { expect } = require('../test')
|
||||
const cds = require('@sap/cds/lib')
|
||||
const { expect } = cds.test
|
||||
const _model = '@capire/reviews'
|
||||
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
|
||||
@@ -20,11 +20,11 @@ describe('Messaging', ()=>{
|
||||
|
||||
let N=0, received=[], M=0
|
||||
it ('should add messaging event handlers', ()=>{
|
||||
srv.on('reviewed', (msg)=> received.push(msg))
|
||||
srv.on('reviewed', (msg,next)=> { received.push(msg); return next() })
|
||||
})
|
||||
|
||||
it ('should add more messaging event handlers', ()=>{
|
||||
srv.on('reviewed', ()=> ++M)
|
||||
srv.on('reviewed', (_,next)=> { ++M; return next() })
|
||||
})
|
||||
|
||||
it ('should add review', async ()=>{
|
||||
@@ -60,11 +60,11 @@ describe('Messaging', ()=>{
|
||||
expect(M).equals(N)
|
||||
expect(received.length).equals(N)
|
||||
expect(received.map(m=>m.data)).to.deep.equal([
|
||||
{ count: 1, subject: '201', rating: 1 },
|
||||
{ count: 2, subject: '201', rating: 1.5 },
|
||||
{ count: 3, subject: '201', rating: 2 },
|
||||
{ count: 4, subject: '201', rating: 2.5 },
|
||||
{ count: 5, subject: '201', rating: 3 },
|
||||
{ subject: '201', rating: 1 },
|
||||
{ subject: '201', rating: 1.5 },
|
||||
{ subject: '201', rating: 2 },
|
||||
{ subject: '201', rating: 2.5 },
|
||||
{ subject: '201', rating: 3 },
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const { GET, expect } = require('../test') .run ('bookshop')
|
||||
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
|
||||
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 () => {
|
||||
const { data } = await GET `/browse/Books ${{
|
||||
const { data } = await GET(`/browse/Books`, {
|
||||
params: { $search: 'Po', $select: `title,author` },
|
||||
}}`
|
||||
})
|
||||
expect(data.value).to.eql([
|
||||
{ ID: 201, title: 'Wuthering Heights', author: 'Emily 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