diff --git a/.eslintrc b/.eslintrc index da867678..40fe0cb5 100644 --- a/.eslintrc +++ b/.eslintrc @@ -21,6 +21,7 @@ }, "rules": { "no-console": "off", - "require-atomic-updates": "off" + "require-atomic-updates": "off", + "require-await":"warn" } } diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index c9fb33c2..c403849a 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -16,7 +16,7 @@ jobs: strategy: matrix: - node-version: [10.x, 12.x, 14.x] + node-version: [12.x, 14.x] steps: - uses: actions/checkout@v2 diff --git a/.registry/server.js b/.registry/server.js new file mode 100644 index 00000000..c1461016 --- /dev/null +++ b/.registry/server.js @@ -0,0 +1,71 @@ +const { exec } = require ('child_process') +const express = require ('express') +const fs = require ('fs') +const app = express() + +const { PORT=4444 } = process.env +const [,,port=PORT] = process.argv + +app.use('/-/:tarball', (req,res,next) => { + const url = decodeURIComponent(req.url) + console.debug ('GET', req.params) + try { + const { tarball } = req.params + const [, pkg ] = /^capire-(\w+)/.exec(tarball) + fs.lstat(tarball,(err => { + if (err) exec(`npm pack ../${pkg}`,next) + else next() + })) + } catch (e) { + console.error(e) + res.sendStatus(500) + } +}) + +app.use('/-', express.static(__dirname)) + +app.get('/*', (req,res)=>{ + const url = decodeURIComponent(req.url) + console.debug ('GET',url) + try { + const [, capire, pkg ] = /^\/(@capire)\/(\w+)/.exec(url) + const package = require (`${capire}/${pkg}/package.json`) + const tarball = `capire-${pkg}-${package.version}.tgz` + // https://github.com/npm/registry/blob/master/docs/responses/package-metadata.md + res.json({ + "name": package.name, + "dist-tags": { + "latest": package.version + }, + "versions": { + [package.version]: { + "name": package.name, + "version": package.version, + "dist": { + "tarball": `http://localhost:${port}/-/${tarball}` + }, + } + }, + }) + } catch (e) { + console.error(e) + res.sendStatus(404) + } +}) + +app.listen(port, ()=>{ + console.log (`npm set @capire:registry=http://localhost:${port}`) + console.log (`@capire registry listening on http://localhost:${port}`) + exec(`npm set @capire:registry=http://localhost:${port}`) +}) + +const _exit = ()=>{ + console.log ('\nnpm conf rm @capire:registry') + exec('npm conf rm @capire:registry') + exec('rm *.tgz') + process.exit() +} +process.on ('SIGTERM',_exit) +process.on ('SIGHUP',_exit) +process.on ('SIGINT',_exit) +process.on ('SIGUSR2',_exit) diff --git a/.vscode/launch.json b/.vscode/launch.json index d0f0e8eb..ad51dcef 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -4,6 +4,15 @@ // 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": [ + "/**" + ], + "type": "pwa-node" + }, { "name": "bookshop", "command": "cds watch bookshop", diff --git a/LICENSES/Apache-2.0.txt b/LICENSE similarity index 100% rename from LICENSES/Apache-2.0.txt rename to LICENSE diff --git a/README.md b/README.md index 01d8fdf1..38e5cef7 100644 --- a/README.md +++ b/README.md @@ -3,18 +3,22 @@ Find here a collection of samples for the [SAP Cloud Application Programming Model](https://cap.cloud.sap) organized in a simplistic [monorepo setup](samples.md#all-in-one-monorepo). → See [**Overview** of contained samples](samples.md) ![](https://github.com/SAP-samples/cloud-cap-samples/workflows/CI/badge.svg) -[![REUSE status](https://api.reuse.software/badge/github.com/SAP-samples/cloud-cap-samples)](https://api.reuse.software/info/github.com/SAP-samples/cloud-cap-samples) + + ### Preliminaries -1. [Install @sap/cds-dk](https://cap.cloud.sap/docs/get-started/) as documented in [capire](https://cap.cloud.sap) -2. _Optional:_ [Use Visual Studio Code](https://cap.cloud.sap/docs/get-started/in-vscode) +1. Install [**@sap/cds-dk**](https://cap.cloud.sap/docs/get-started/) globally: + ```sh + npm i -g @sap/cds-dk + ``` + +2. _Optional:_ [Use Visual Studio Code](https://cap.cloud.sap/docs/get-started/tools#vscode) ### Download -Clone this repo as shown below, if you have [git](https://git-scm.com/downloads) installed, -otherwise [download as zip file](archive/master.zip). +If you have [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 @@ -39,15 +43,35 @@ cds watch bookshop After that open this link in your browser: [http://localhost:4004](http://localhost:4004) +When asked to log in, type `alice` as user and leave the password field blank, which is the [default user](https://cap.cloud.sap/docs/node.js/authentication#mocked). + ### Testing Run the provided tests with [_jest_](http://jestjs.io) or [_mocha_](http://mochajs.org), for example: + ```sh npx jest ``` > While mocha is a bit smaller and faster, jest runs tests in parallel and isolation, which allows to run all tests. +### Serve `npm` + +We've included a simple npm registry mock, which allows you to do an `npm install @capire/` locally. Use it as follows: + +1. Start the @capire registry: +```sh +npm run registry +``` +> While running this will have `@capire:registry=http://localhost:4444` set with npmrc. + +2. Install one of the @capire packages wherever you like, for example: + +```sh +npm add @capire/common @capire/bookshop +``` + + ## Get Support Check out the documentation at [https://cap.cloud.sap](https://cap.cloud.sap).
@@ -56,4 +80,4 @@ In case you have a question, find a bug, or otherwise need support, please use o ## License -Copyright (c) 2020 SAP SE or an SAP affiliate company. All rights reserved. This file is licensed under the Apache Software License, version 2.0 except as noted otherwise in the [LICENSE](LICENSES/Apache-2.0.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. diff --git a/bookshop/app/vue/app.js b/bookshop/app/vue/app.js index 3dd4a2b7..78bd342b 100644 --- a/bookshop/app/vue/app.js +++ b/bookshop/app/vue/app.js @@ -9,34 +9,36 @@ const books = new Vue ({ data: { list: [], - book: { descr:'( click on a row to see details... )' }, + book: undefined, order: { amount:1, succeeded:'', failed:'' } }, methods: { - search: ({target:{value:v}}) => books.fetch (v && '$search='+v), + search: ({target:{value:v}}) => books.fetch(v && '&$search='+v), - async fetch (_filter='') { - const columns = 'ID,title,author,price,stock', details = 'genre,currency' - const {data} = await GET(`/Books?$select=${columns}&$expand=${details}&${_filter}`) + async fetch (etc='') { + const {data} = await GET(`/ListOfBooks?$expand=genre,currency${etc}`) books.list = data.value }, - async inspect () { - const book = books.book = books.list [event.currentTarget.rowIndex-1] - book.imageSrc || await GET(`/Books/${book.ID}/image`) .then (({data}) => book.imageSrc = data ) - book.descr || await GET(`/Books/${book.ID}/descr/$value`) .then (({data}) => book.descr = data) + async inspect (eve) { + 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 = { amount:1 } setTimeout (()=> $('form > input').focus(), 111) }, - submitOrder () { event.preventDefault() - const {book,order} = books, amount = parseInt (order.amount) || 1 - POST(`/submitOrder`, { amount, book: book.ID }) - .then (()=> books.order = { amount, succeeded: `Successfully orderd ${amount} item(s).` }) - .catch (e=> books.order = { amount, failed: e.response.data.error.message }) - GET(`/Books/${book.ID}/stock/$value`).then (res => book.stock = res.data) + async submitOrder () { + const {book,order} = books, amount = parseInt (order.amount) || 1 // REVISIT: Okra should be less strict + try { + const res = await POST(`/submitOrder`, { amount, book: book.ID }) + book.stock = res.data.stock + books.order = { amount, succeeded: `Successfully orderd ${amount} item(s).` } + } catch (e) { + books.order = { amount, failed: e.response.data.error.message } + } } } diff --git a/bookshop/app/vue/index.html b/bookshop/app/vue/index.html index 51408d43..56c79a07 100644 --- a/bookshop/app/vue/index.html +++ b/bookshop/app/vue/index.html @@ -7,54 +7,56 @@
-

Capire Books

+

{{ document.title }}

- +
+ +
Book Author Genre Rating Price
{{ book.title }} {{ book.author }} {{ book.genre.name }} + {{ ('★'.repeat(Math.round(book.rating))+'☆☆☆☆☆').slice(0,5) }} + {{ book.currency.symbol }} {{ book.price }}
-
- -
- -
+
+ -
- + +
+

{{ book.title }}

+

{{ book.descr }}

+
+
+ ( click on a row to see details... )
- -

{{ book.title }}

-

{{ book.descr }}

diff --git a/bookshop/db/schema.cds b/bookshop/db/schema.cds index 99fadb5a..ec8b119a 100644 --- a/bookshop/db/schema.cds +++ b/bookshop/db/schema.cds @@ -8,7 +8,7 @@ entity Books : managed { author : Association to Authors; genre : Association to Genres; stock : Integer; - price : Decimal(9,2); + price : Decimal; currency : Currency; image : LargeBinary @Core.MediaType : 'image/png'; } diff --git a/bookshop/package.json b/bookshop/package.json index a0b3f3c9..c2f3c081 100644 --- a/bookshop/package.json +++ b/bookshop/package.json @@ -6,7 +6,8 @@ "@capire/common": "*", "@sap/cds": "^4", "cors": "^2.8.5", - "express": "^4.17.1" + "express": "^4.17.1", + "passport": "0.4.1" }, "scripts": { "genres": "cds serve test/genres.cds", diff --git a/bookshop/readme.md b/bookshop/readme.md index 5b3e9144..6a14f968 100644 --- a/bookshop/readme.md +++ b/bookshop/readme.md @@ -5,10 +5,10 @@ This stand-alone sample introduces the essential tasks in the development of CAP ## Hypothetical Use Cases 1. Build a service that allows to browse _Books_ and _Authors_. -2. Books have assigned _Genres_ which are organized hierarchically. +2. Books have assigned _Genres_, which are organized hierarchically. 3. All users may browse books without login. 4. All entries are maintained by Administrators. -5. End users may order books (the actual order mgmt being out of scope) +5. End users may order books (the actual order mgmt being out of scope). ## Running the Sample @@ -20,12 +20,12 @@ npm run watch | Links to capire | Sample files / folders | | --------------------------------------------------------------------------------------------------------- | ------------------------------------ | -| [Project Setup and Layouts](https://cap.cloud.sap/docs/get-started/projects#sharing-and-reusing-content) | [`./`](./) | -| [Defining Domain Models](https://cap.cloud.sap/docs/guides/domain-models) | [`./db/schema.cds`](./db/schema.cds) | -| [Defining Services](https://cap.cloud.sap/docs/guides/providing-services) | [`./srv/*.cds`](./srv) | -| [Single-purposed Services](https://cap.cloud.sap/docs/guides/providing-services#single-purposed-services) | [`./srv/*.cds`](./srv) | -| [Generic Providers](https://cap.cloud.sap/docs/guides/providing-services) | http://localhost:4004 | -| Using Databases | [`./db/data/*.csv`](./db/data) | +| [Project Setup & Layouts](https://cap.cloud.sap/docs/get-started/projects#sharing-and-reusing-content) | [`./`](./) | +| [Domain Modeling with CDS](https://cap.cloud.sap/docs/guides/domain-models) | [`./db/schema.cds`](./db/schema.cds) | +| [Defining Services](https://cap.cloud.sap/docs/guides/services#defining-services) | [`./srv/*.cds`](./srv) | +| [Single-purposed Services](https://cap.cloud.sap/docs/guides/services#single-purposed-services) | [`./srv/*.cds`](./srv) | +| [Providing & Consuming Providers](https://cap.cloud.sap/docs/guides/providing-services) | http://localhost:4004 | +| [Using Databases](https://cap.cloud.sap/docs/guides/databases) | [`./db/data/*.csv`](./db/data) | | [Adding Custom Logic](https://cap.cloud.sap/docs/guides/service-impl) | [`./srv/*.js`](./srv) | -| Adding Tests | [`./test`](./test) | -| [Sharing for Reuse](https://cap.cloud.sap/docs/get-started/projects#sharing-and-reusing-content) | [`./index.cds`](./index.cds) | +| Adding Tests | [`./test`](./test) | +| [Sharing for Reuse](https://cap.cloud.sap/docs/guides/reuse-and-compose) | [`./index.cds`](./index.cds) | diff --git a/bookshop/srv/admin-service.cds b/bookshop/srv/admin-service.cds index 8939262f..ea9b0731 100644 --- a/bookshop/srv/admin-service.cds +++ b/bookshop/srv/admin-service.cds @@ -1,5 +1,5 @@ using { sap.capire.bookshop as my } from '../db/schema'; -service AdminService @(requires_:'admin') { +service AdminService @(requires:'admin') { entity Books as projection on my.Books; entity Authors as projection on my.Authors; } diff --git a/bookshop/srv/admin-service.js b/bookshop/srv/admin-service.js new file mode 100644 index 00000000..0cdae4d8 --- /dev/null +++ b/bookshop/srv/admin-service.js @@ -0,0 +1,12 @@ +const cds = require('@sap/cds') + +module.exports = cds.service.impl (function(){ + this.before ('NEW','Authors', genid) + this.before ('NEW','Books', genid) +}) + +/** Generate primary keys for target entity in request */ +async function genid (req) { + const {ID} = await cds.tx(req).run (SELECT.one.from(req.target).columns('max(ID) as ID')) + req.data.ID = ID - ID % 100 + 100 + 1 +} diff --git a/bookshop/srv/cat-service.cds b/bookshop/srv/cat-service.cds index 3cbaaa8f..f6fb00cd 100644 --- a/bookshop/srv/cat-service.cds +++ b/bookshop/srv/cat-service.cds @@ -1,10 +1,14 @@ using { sap.capire.bookshop as my } from '../db/schema'; service CatalogService @(path:'/browse') { - @readonly entity Books as SELECT from my.Books {*, + @readonly entity Books as SELECT from my.Books { *, author.name as author } excluding { createdBy, modifiedBy }; - @requires_: 'authenticated-user' - action submitOrder (book : Books:ID, amount: Integer); + @readonly entity ListOfBooks as SELECT from Books + excluding { descr }; + + @requires: 'authenticated-user' + action submitOrder ( book: Books:ID, amount: Integer ) returns { stock: Integer }; + event OrderedBook : { book: Books:ID; amount: Integer; buyer: String }; } diff --git a/bookshop/srv/cat-service.js b/bookshop/srv/cat-service.js index 4352c5e7..4676dcd5 100644 --- a/bookshop/srv/cat-service.js +++ b/bookshop/srv/cat-service.js @@ -1,16 +1,18 @@ const cds = require('@sap/cds') -module.exports = async function (){ +const { Books } = cds.entities ('sap.capire.bookshop') - const db = await cds.connect.to('db') // connect to database service - const { Books } = db.entities // get reflected definitions +class CatalogService extends cds.ApplicationService { init(){ // Reduce stock of ordered books if available stock suffices this.on ('submitOrder', async req => { - const {book,amount} = req.data - const n = await UPDATE (Books, book) - .with ({ stock: {'-=': amount }}) - .where ({ stock: {'>=': amount }}) - n > 0 || req.error (409,`${amount} exceeds stock for book #${book}`) + 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,`${amount} exceeds stock for book #${book}`) }) // Add some discount for overstocked books @@ -19,4 +21,8 @@ module.exports = async function (){ each.title += ` -- 11% discount!` } }) -} + + return super.init() +}} + +module.exports = { CatalogService } diff --git a/bookshop/test/requests.http b/bookshop/test/requests.http index 6c6428a0..1fbdc0ca 100644 --- a/bookshop/test/requests.http +++ b/bookshop/test/requests.http @@ -36,6 +36,7 @@ Authorization: Basic alice: # Create book POST {{server}}/admin/Books Content-Type: application/json;IEEE754Compatible=true +Authorization: Basic alice: { "ID": 2, @@ -53,6 +54,7 @@ Content-Type: application/json;IEEE754Compatible=true # Put image to books PUT {{server}}/admin/Books(2)/image Content-Type: image/png +Authorization: Basic alice: data:image/png;base64,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 @@ -78,4 +80,3 @@ GET {{server}}/browse/Genres? # &$filter=parent_ID eq null&$select=name # &$expand=children($select=name) {{me}} - diff --git a/common/package.json b/common/package.json index d5c6dc24..c1998c24 100644 --- a/common/package.json +++ b/common/package.json @@ -1,4 +1,8 @@ { "name": "@capire/common", - "version": "1.0.0" + "description": "Provides a pre-built extension package for std @sap/cds/common", + "version": "1.0.0", + "dependencies": { + "@sap/cds": "latest" + } } diff --git a/fiori/.env b/fiori/.env new file mode 100644 index 00000000..36644fa6 --- /dev/null +++ b/fiori/.env @@ -0,0 +1,2 @@ +# cds.requires.messaging.kind = file-based-messaging +PORT = 4004 \ No newline at end of file diff --git a/fiori/app/fiori.html b/fiori/app/admin-fiori.html similarity index 83% rename from fiori/app/fiori.html rename to fiori/app/admin-fiori.html index d011797c..6c229e6b 100644 --- a/fiori/app/fiori.html +++ b/fiori/app/admin-fiori.html @@ -13,7 +13,7 @@ applications: { "browse-books": { title: "Browse Books", - description: "... testing FE v42", + description: "w/ SAP Fiori Elements", additionalInformation: "SAPUI5.Component=bookshop", applicationType : "URL", url: "/browse/webapp", @@ -21,15 +21,15 @@ }, "manage-books": { title: "Manage Books", - description: "... testing FE v42", + description: "w/ SAP Fiori Elements", additionalInformation: "SAPUI5.Component=admin", applicationType : "URL", url: "/admin/webapp", navigationMode: "embedded" }, "manage-orders": { - title: "Order Books", - description: "... testing FE v42", + title: "Manage Orders", + description: "w/ SAP Fiori Elements", additionalInformation: "SAPUI5.Component=orders", applicationType : "URL", url: "/orders/webapp", @@ -40,8 +40,7 @@ - - + + + + + + + + \ No newline at end of file diff --git a/orders/app/index.cds b/orders/app/index.cds new file mode 100644 index 00000000..d64c2905 --- /dev/null +++ b/orders/app/index.cds @@ -0,0 +1,5 @@ +/* + This model controls what gets served to Fiori frontends... +*/ + +using from './orders/fiori-service'; diff --git a/fiori/app/orders/fiori-service.cds b/orders/app/orders/fiori-service.cds similarity index 54% rename from fiori/app/orders/fiori-service.cds rename to orders/app/orders/fiori-service.cds index cced0972..e58c8ccd 100644 --- a/fiori/app/orders/fiori-service.cds +++ b/orders/app/orders/fiori-service.cds @@ -1,44 +1,27 @@ -using OrdersService from '@capire/orders/srv/orders-service'; - -annotate OrdersService.Books with { - price @Common.FieldControl: #ReadOnly; -} //////////////////////////////////////////////////////////////////////////// // -// Common +// Note: this is designed for the OrdersService being co-located with +// bookshop. It does not work if OrdersService is run as a separate +// process, and is not intended to do so. // -annotate OrdersService.OrderItems with { - book @( - Common: { - Text: book.title, - FieldControl: #Mandatory - }, - ValueList.entity:'Books', - ); - amount @( - Common.FieldControl: #Mandatory - ); -} +//////////////////////////////////////////////////////////////////////////// + + + +using { OrdersService } from '../../srv/orders-service'; @odata.draft.enabled annotate OrdersService.Orders with @( UI: { - //////////////////////////////////////////////////////////////////////////// - // - // Lists of Orders - // SelectionFields: [ createdAt, createdBy ], LineItem: [ - {Value: createdBy, Label:'Customer'}, + {Value: OrderNo, Label:'OrderNo'}, + {Value: buyer, Label:'Customer'}, {Value: createdAt, Label:'Date'} ], - //////////////////////////////////////////////////////////////////////////// - // - // Order Details - // HeaderInfo: { TypeName: 'Order', TypeNamePlural: 'Orders', Title: { @@ -62,7 +45,7 @@ annotate OrdersService.Orders with @( ], FieldGroup#Details: { Data: [ - {Value: currency_code, Label:'Currency'} + {Value: currency.code, Label:'Currency'} ] }, FieldGroup#Created: { @@ -85,36 +68,25 @@ annotate OrdersService.Orders with @( -//The enity types name is OrdersService.my_bookshop_OrderItems -//The annotations below are not generated in edmx WHY? -annotate OrdersService.OrderItems with @( +annotate OrdersService.Orders_Items with @( UI: { - HeaderInfo: { - TypeName: 'Order Item', TypeNamePlural: ' ', - Title: { - Value: book.title - }, - Description: {Value: book.descr} - }, - // There is no filterbar for items so the selctionfileds is not needed - SelectionFields: [ book_ID ], - //////////////////////////////////////////////////////////////////////////// - // - // Lists of OrderItems - // LineItem: [ - {Value: book_ID, Label:'Book'}, - //The following entry is only used to have the assoication followed in the read event - {Value: book.price, Label:'Book Price'}, + {Value: product_ID, Label:'Product ID'}, + {Value: title, Label:'Product Title'}, + {Value: price, Label:'Unit Price'}, {Value: amount, Label:'Quantity'}, ], Identification: [ //Is the main field group - //{Value: ID, Label:'ID'}, //A guid shouldn't be on the UI - {Value: book_ID, Label:'Book'}, {Value: amount, Label:'Amount'}, + {Value: title, Label:'Product'}, + {Value: price, Label:'Unit Price'}, ], Facets: [ {$Type: 'UI.ReferenceFacet', Label: '{i18n>OrderItems}', Target: '@UI.Identification'}, ], }, -); \ No newline at end of file +) { + amount @( + Common.FieldControl: #Mandatory + ); +}; diff --git a/fiori/app/orders/webapp/Component.js b/orders/app/orders/webapp/Component.js similarity index 100% rename from fiori/app/orders/webapp/Component.js rename to orders/app/orders/webapp/Component.js diff --git a/fiori/app/orders/webapp/i18n/i18n.properties b/orders/app/orders/webapp/i18n/i18n.properties similarity index 100% rename from fiori/app/orders/webapp/i18n/i18n.properties rename to orders/app/orders/webapp/i18n/i18n.properties diff --git a/fiori/app/orders/webapp/manifest.json b/orders/app/orders/webapp/manifest.json similarity index 98% rename from fiori/app/orders/webapp/manifest.json rename to orders/app/orders/webapp/manifest.json index df686462..045fa70a 100644 --- a/fiori/app/orders/webapp/manifest.json +++ b/orders/app/orders/webapp/manifest.json @@ -121,7 +121,7 @@ "name": "sap.fe.templates.ObjectPage", "options": { "settings" : { - "entitySet": "OrderItems" + "entitySet": "Orders_Items" } } }, diff --git a/orders/db/data/sap.capire.bookshop-OrderItems.csv b/orders/db/data/sap.capire.bookshop-OrderItems.csv deleted file mode 100644 index 25edab7a..00000000 --- a/orders/db/data/sap.capire.bookshop-OrderItems.csv +++ /dev/null @@ -1,4 +0,0 @@ -ID;amount;parent_ID;book_ID;netAmount -58040e66-1dcd-4ffb-ab10-fdce32028b79;1;7e2f2640-6866-4dcf-8f4d-3027aa831cad;201;11.11 -64e718c9-ff99-47f1-8ca3-950c850777d4;1;7e2f2640-6866-4dcf-8f4d-3027aa831cad;271;15 -e9641166-e050-4261-bfee-d1e797e6cb7f;2;64e718c9-ff99-47f1-8ca3-950c850777d4;252;28 \ No newline at end of file diff --git a/orders/db/data/sap.capire.bookshop-Orders.csv b/orders/db/data/sap.capire.bookshop-Orders.csv deleted file mode 100644 index 088c1e87..00000000 --- a/orders/db/data/sap.capire.bookshop-Orders.csv +++ /dev/null @@ -1,3 +0,0 @@ -ID;modifiedAt;createdAt;createdBy;modifiedBy;OrderNo;currency_code -7e2f2640-6866-4dcf-8f4d-3027aa831cad;;2019-01-31;john.doe@test.com;;1;EUR -64e718c9-ff99-47f1-8ca3-950c850777d4;;2019-01-30;jane.doe@test.com;;2;EUR \ No newline at end of file diff --git a/orders/db/data/sap.capire.orders-Orders.csv b/orders/db/data/sap.capire.orders-Orders.csv new file mode 100644 index 00000000..6ad3d700 --- /dev/null +++ b/orders/db/data/sap.capire.orders-Orders.csv @@ -0,0 +1,3 @@ +ID;createdAt;createdBy;buyer;OrderNo;currency_code +7e2f2640-6866-4dcf-8f4d-3027aa831cad;2019-01-31;john.doe@test.com;john.doe@test.com;1;EUR +64e718c9-ff99-47f1-8ca3-950c850777d4;2019-01-30;jane.doe@test.com;jane.doe@test.com;2;EUR \ No newline at end of file diff --git a/orders/db/data/sap.capire.orders-Orders_Items.csv b/orders/db/data/sap.capire.orders-Orders_Items.csv new file mode 100644 index 00000000..b3025abe --- /dev/null +++ b/orders/db/data/sap.capire.orders-Orders_Items.csv @@ -0,0 +1,4 @@ +ID;up__ID;amount;product_ID;title;price +58040e66-1dcd-4ffb-ab10-fdce32028b79;7e2f2640-6866-4dcf-8f4d-3027aa831cad;1;201;Wuthering Heights;11.11 +64e718c9-ff99-47f1-8ca3-950c850777d4;7e2f2640-6866-4dcf-8f4d-3027aa831cad;1;271;Catweazle;15 +e9641166-e050-4261-bfee-d1e797e6cb7f;64e718c9-ff99-47f1-8ca3-950c850777d4;2;252;Eleonora;28 \ No newline at end of file diff --git a/orders/db/schema.cds b/orders/db/schema.cds index 048aba28..c3b4f1c5 100644 --- a/orders/db/schema.cds +++ b/orders/db/schema.cds @@ -1,16 +1,26 @@ -using { sap.capire.bookshop.Books } from '@capire/bookshop'; -using { Currency, managed, cuid } from '@sap/cds/common'; -namespace sap.capire.bookshop; +using { Currency, User, managed, cuid } from '@sap/cds/common'; +namespace sap.capire.orders; entity Orders : cuid, managed { OrderNo : String @title:'Order Number'; //> readable key - Items : Composition of many OrderItems on Items.parent = $self; + Items : Composition of many Orders_Items on Items.up_ = $self; + buyer : User; currency : Currency; } -entity OrderItems : cuid { - parent : Association to Orders; - book : Association to Books; +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 amount : Integer; - netAmount : Decimal(9,2); + title : String; + price : Double; } + +/** This is a stand-in for arbitrary ordered Products */ +entity Products @(cds.persistence.skip:'always') { + key ID : String; +} + +// Activate extension package +using from '@capire/common'; diff --git a/orders/index.cds b/orders/index.cds new file mode 100644 index 00000000..a79e9f65 --- /dev/null +++ b/orders/index.cds @@ -0,0 +1,6 @@ +/* + This model controls what gets exposed +*/ +namespace sap.capire.orders; +using from './srv/orders-service'; +using from './db/schema'; diff --git a/orders/package.json b/orders/package.json index 242aefc4..e1c683af 100644 --- a/orders/package.json +++ b/orders/package.json @@ -1,4 +1,8 @@ { "name": "@capire/orders", - "version": "1.0.0" + "version": "1.0.0", + "dependencies": { + "@capire/common": "*", + "@sap/cds": "^4.3.0" + } } \ No newline at end of file diff --git a/orders/srv/orders-service.cds b/orders/srv/orders-service.cds index 936b5256..119373db 100644 --- a/orders/srv/orders-service.cds +++ b/orders/srv/orders-service.cds @@ -1,6 +1,5 @@ -using { sap.capire.bookshop as my } from '../db/schema'; +using { sap.capire.orders as my } from '../db/schema'; service OrdersService { entity Orders as projection on my.Orders; - entity Books as projection on my.Books; } diff --git a/orders/srv/orders-service.js b/orders/srv/orders-service.js index bc401fd4..10420410 100644 --- a/orders/srv/orders-service.js +++ b/orders/srv/orders-service.js @@ -1,21 +1,37 @@ -const cds = require('@sap/cds') +const cds = require ('@sap/cds') +class OrdersService extends cds.ApplicationService { -module.exports = cds.service.impl(function() { + /** register custom handlers */ + init(){ + const { Orders_Items:OrderItems } = this.entities - const { Books } = cds.entities + this.before ('UPDATE', 'Orders', async function(req) { + const { ID, Items } = req.data + 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 (amount != before) await this.orderChanged (product_ID, amount-before) + } + }) - // Reduce stock of ordered books if available stock suffices - this.before ('CREATE', 'Orders', (req) => { - const { Items: items } = req.data - return cds.transaction(req) .run (items.map (item => - UPDATE (Books) .where ('ID =', item.book_ID) - .and ('stock >=', item.amount) - .set ('stock -=', item.amount) - )) .then (all => all.forEach ((affectedRows,i) => { - if (affectedRows === 0) req.error (409, - `${items[i].amount} exceeds stock for book #${items[i].book_ID}` + this.before ('DELETE', 'Orders', async function(req) { + const { ID } = req.data + const Items = await cds.tx(req).run ( + SELECT.from (OrderItems, oi => { oi.product_ID, oi.amount }) .where ({up__ID:ID}) ) - })) - }) + if (Items) await Promise.all (Items.map(it => this.orderChanged (it.product_ID, -it.amount))) + }) -}) + return super.init() + } + + /** order changed -> broadcast event */ + orderChanged (product, deltaAmount) { + // Emit events to inform subscribers about changes in orders + console.log ('> emitting:', 'OrderChanged', { product, deltaAmount }) + return this.emit ('OrderChanged', { product, deltaAmount }) + } + +} +module.exports = OrdersService diff --git a/package.json b/package.json index 7423acbb..af1ea777 100644 --- a/package.json +++ b/package.json @@ -11,23 +11,23 @@ "@capire/hello": "./hello", "@capire/media": "./media", "@capire/orders": "./orders", - "@capire/reviewed": "./reviewed", "@capire/reviews": "./reviews" }, "devDependencies": { "chai": "^4.2.0", "chai-as-promised": "^7.1.1", "chai-subset": "^1.6.0", - "sqlite3": "^5", + "sqlite3": "5.0.0", "swagger-ui-express": "^4.1.4" }, "scripts": { + "registry": "cd .registry && node server.js", "bookshop": "cds watch bookshop", "fiori": "cds watch fiori", "media": "cds watch media", "mocha": "npx mocha || echo", - "jest": "npx jest --verbose", - "test": "npm run jest -s" + "jest": "npx jest", + "test": "npm run jest --silent" }, "mocha": { "parallel": true diff --git a/reviewed/.env b/reviewed/.env deleted file mode 100644 index 24a7ee31..00000000 --- a/reviewed/.env +++ /dev/null @@ -1 +0,0 @@ -cds.requires.messaging.kind = file-based-messaging \ No newline at end of file diff --git a/reviewed/db/schema.cds b/reviewed/db/schema.cds deleted file mode 100644 index 7553b3fa..00000000 --- a/reviewed/db/schema.cds +++ /dev/null @@ -1,16 +0,0 @@ -// -// Extending Books with Reviews -// - -using { sap.capire.bookshop.Books } from '@capire/bookshop'; -using { ReviewsService.Reviews } from '@capire/reviews'; - -extend Books with { - /** Access to detailed collection of Reviews */ - reviews : Composition of many Reviews on reviews.subject = $self.ID; - /** Average rating */ - rating : Reviews.rating; -} - -// Temporary workaround for cap/issues#4112: -annotate Reviews with @cds.autoexpose; diff --git a/reviewed/package.json b/reviewed/package.json deleted file mode 100644 index 363d42e3..00000000 --- a/reviewed/package.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "@capire/bookshop-with-reviews", - "version": "1.0.0", - "dependencies": { - "@capire/bookshop": "../bookshop", - "@capire/reviews": "../reviews", - "@sap/cds": "^4", - "express": "^4.17.1" - }, - "cds": { - "requires": { - "db": { - "kind": "sql" - }, - "ReviewsService": { - "kind": "odata", "model": "@capire/reviews" - } - } - } -} diff --git a/reviewed/server.js b/reviewed/server.js deleted file mode 100644 index 1d59efb9..00000000 --- a/reviewed/server.js +++ /dev/null @@ -1,33 +0,0 @@ -//////////////////////////////////////////////////////////////////////////// -// -// This is an example of using a project-local server.js to intercept -// the default bootstrapping process. -// -const cds = require ('@sap/cds') - -// Connect CatalogService and ReviewsService when all are served... -cds.once('served', async ({CatalogService}) => { - - const ReviewsService = await cds.connect.to('ReviewsService') - - // reflect entity definitions used below... - const { Books } = cds.entities('sap.capire.bookshop') - const { Reviews } = ReviewsService.entities - - // prepend the following handler so it overrides the default handler - CatalogService.prepend (srv => srv.on ('READ', 'Books/reviews', (req) => { - console.debug ('> delegating request to ReviewsService') - const [id] = req.params, { columns, limit } = req.query.SELECT - return SELECT(columns).from(Reviews).limit(limit).where({subject:String(id)}) - })) - - ReviewsService.on ('reviewed', (msg) => { - console.debug ('> received:', msg.event, msg.data) - const { subject, rating } = msg.data - return UPDATE(Books,subject).with({rating}) - }) - -}) - -// Delegate bootstrapping to built-in server.js -module.exports = cds.server diff --git a/reviewed/test/requests.http b/reviewed/test/requests.http deleted file mode 100644 index 7a9d711c..00000000 --- a/reviewed/test/requests.http +++ /dev/null @@ -1,41 +0,0 @@ -################################################# -# -# To ReviewsService -# -# move the right down: -@reviews-service = http://localhost:4004/reviews -@reviews-service = http://localhost:5005/reviews - -### Get all reviews -GET {{reviews-service}}/Reviews - -### Add a new review (with random rating) -POST {{reviews-service}}/Reviews -Content-Type: application/json;IEEE754Compatible=true - -{"subject":"201", "title":"boo"} - - - -################################################# -# -# Bookshop Requests involving reviews -# (both in-process as well as separate one) -# - -@bookshop = http://localhost:4004 - -### Request to CatalogService > delegated to ReviewsService -GET {{bookshop}}/browse/Books(201)/reviews? -&$select=rating,date,reviewer,title - -### Alternative OData URL -GET {{bookshop}}/browse/Books/201/reviews? -&$select=rating,date,title -&$top=3 - -### -GET {{bookshop}}/browse/Books(201)? -&$select=ID,title,rating -&$expand=reviews -# Note: the $expand only works in case of ReviewsService in same process diff --git a/reviews/.env b/reviews/.env index 8184d48d..b9da5c42 100644 --- a/reviews/.env +++ b/reviews/.env @@ -1,2 +1,2 @@ cds.requires.messaging.kind = file-based-messaging -PORT = 5005 \ No newline at end of file +PORT = 4005 \ No newline at end of file diff --git a/reviews/app/vue/app.js b/reviews/app/vue/app.js new file mode 100644 index 00000000..b46a4892 --- /dev/null +++ b/reviews/app/vue/app.js @@ -0,0 +1,72 @@ +/* global Vue axios */ //> from vue.html +const $ = sel => document.querySelector(sel) +const GET = (url) => axios.get('/reviews'+url) +const PUT = (cmd,data) => axios.patch('/reviews'+cmd,data) +const POST = (cmd,data) => axios.post('/reviews'+cmd,data) + +const reviews = new Vue ({ + + el:'#app', + + data: { + list: [], + review: undefined, + message: {}, + Ratings: Object.entries({ + 5 : '★★★★★', + 4 : '★★★★', + 3 : '★★★', + 2 : '★★', + 1 : '★', + }).reverse() + }, + + methods: { + + search: ({target:{value:v}}) => reviews.fetch(v && '&$search='+v), + + async fetch (etc='') { + const {data} = await GET(`/Reviews?${etc}`) + reviews.list = data.value + }, + + async inspect (eve) { + const review = reviews.review = reviews.list [eve.currentTarget.rowIndex-1] + const res = await GET(`/Reviews/${review.ID}/text/$value`) + review.text = res.data + reviews.message = {} + }, + + newReview () { + reviews.review = {} + reviews.message = {} + setTimeout (()=> $('form > input').focus(), 111) + }, + + async submitReview () { + const review = reviews.review; review.rating = parseInt (review.rating) // REVISIT: Okra should be less strict + try { + if (!review.ID) { + const res = await POST(`/Reviews`,review) + reviews.ID = res.data.ID + } else { + console.trace() + await PUT(`/Reviews/${review.ID}`,review) + } + reviews.message = { succeeded: 'Your review was submitted successfully. Thanks.' } + } catch (e) { + reviews.message = { failed: e.response.data.error.message } + } + } + + }, + + filters: { + stars: (r) => ('★'.repeat(Math.round(r))+'☆☆☆☆☆').slice(0,5), + datetime: (d) => d && new Date(d).toLocaleString(), + }, + +}) + +// initially fill list of my reviews +reviews.fetch() diff --git a/reviews/app/vue/index.html b/reviews/app/vue/index.html new file mode 100644 index 00000000..a361387f --- /dev/null +++ b/reviews/app/vue/index.html @@ -0,0 +1,62 @@ + + + + + Capire Reviews + + + + + + + +
+ +

{{ document.title }}

+ + + + + + + + + + + + + + + + +
Subject Rating Title Date
{{ review.subject }}{{ review.rating | stars }}{{ review.title }}{{ review.date | datetime }}
+ + + +
+ + + + + + {{ message.succeeded }} + {{ message.failed }} +
+
+ ( click on a row to see details... ) +
+ + +
+ + + + diff --git a/reviews/db/data/sap.capire.reviews-Reviews.csv b/reviews/db/data/sap.capire.reviews-Reviews.csv new file mode 100644 index 00000000..5e7f3d3f --- /dev/null +++ b/reviews/db/data/sap.capire.reviews-Reviews.csv @@ -0,0 +1,5 @@ +subject;rating;title;text +201;5;Intriguing;Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. +201;4;Fascinating;Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Id diam maecenas ultricies mi eget mauris pharetra et. Risus at ultrices mi tempus imperdiet nulla malesuada pellentesque. Pulvinar mattis nunc sed blandit libero. Facilisis magna etiam tempor orci eu. Nec sagittis aliquam malesuada bibendum arcu. Eu consequat ac felis donec. Ultricies tristique nulla aliquet enim tortor at auctor urna nunc. Tortor posuere ac ut consequat semper viverra nam libero. Amet nisl suscipit adipiscing bibendum est ultricies integer quis auctor. Scelerisque purus semper eget duis at tellus. Elementum tempus egestas sed sed risus pretium. Arcu dictum varius duis at. Amet luctus venenatis lectus magna fringilla urna. Eget velit aliquet sagittis id consectetur purus ut faucibus. Vitae auctor eu augue ut lectus. Fermentum iaculis eu non diam phasellus vestibulum. +207;2;What is this?;Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Libero justo laoreet sit amet cursus sit amet dictum. Nunc faucibus a pellentesque sit. Dis parturient montes nascetur ridiculus mus mauris vitae ultricies. Enim nunc faucibus a pellentesque. Commodo quis imperdiet massa tincidunt nunc pulvinar sapien. Cras ornare arcu dui vivamus. Facilisi etiam dignissim diam quis enim lobortis. Et molestie ac feugiat sed. Urna neque viverra justo nec ultrices dui. Ullamcorper a lacus vestibulum sed arcu non. Volutpat ac tincidunt vitae semper quis. Dignissim sodales ut eu sem. Feugiat in fermentum posuere urna nec. At augue eget arcu dictum varius. +251;3;It's dark...;Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Suscipit tellus mauris a diam. Velit aliquet sagittis id consectetur purus ut. Viverra adipiscing at in tellus integer. Vitae elementum curabitur vitae nunc. Mattis ullamcorper velit sed ullamcorper morbi. Diam quis enim lobortis scelerisque. Auctor neque vitae tempus quam pellentesque nec nam aliquam. Semper auctor neque vitae tempus. Quis eleifend quam adipiscing vitae proin. Neque convallis a cras semper auctor neque vitae. Imperdiet massa tincidunt nunc pulvinar sapien et ligula. Sit amet consectetur adipiscing elit ut aliquam purus. Pretium quam vulputate dignissim suspendisse. \ No newline at end of file diff --git a/reviews/db/schema.cds b/reviews/db/schema.cds index e49f90ca..456ef13d 100644 --- a/reviews/db/schema.cds +++ b/reviews/db/schema.cds @@ -17,7 +17,7 @@ entity Reviews { liked : Integer default 0; // counter for likes as helpful review (count of all _likes belonging to this review) } -type Rating : Decimal(3,2) enum { +type Rating : Integer enum { Best = 5; Good = 4; Avg = 3; diff --git a/reviews/index.cds b/reviews/index.cds index c126bf5e..ac2c4e7a 100644 --- a/reviews/index.cds +++ b/reviews/index.cds @@ -1 +1,2 @@ using from './srv/reviews-service'; +namespace sap.capire.reviews; diff --git a/reviews/srv/reviews-service.cds b/reviews/srv/reviews-service.cds index 44fa83e2..eb26d9ae 100644 --- a/reviews/srv/reviews-service.cds +++ b/reviews/srv/reviews-service.cds @@ -24,14 +24,21 @@ service ReviewsService { // Access control restrictions -annotate ReviewsService.Reviews with @restrict_:[ +annotate ReviewsService.Reviews with @restrict:[ { grant:'READ', to:'any' }, // everybody can read 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' }, ]; -annotate ReviewsService with @restrict_:[ +annotate ReviewsService with @restrict:[ { grant:'like', to:'identified-user' }, { grant:'unlike', to:'identified-user', where:'user=$user' }, ]; diff --git a/reviews/srv/reviews-service.js b/reviews/srv/reviews-service.js index 10979994..21441df6 100644 --- a/reviews/srv/reviews-service.js +++ b/reviews/srv/reviews-service.js @@ -1,5 +1,5 @@ const cds = require ('@sap/cds') -module.exports = cds.service.impl (async function(){ +module.exports = cds.service.impl (function(){ // Get the CSN definition for Reviews from the db schema for sub-sequent queries // ( Note: we explicitly specify the namespace to support embedded reuse ) @@ -16,7 +16,7 @@ module.exports = cds.service.impl (async function(){ SELECT.one (['round(avg(rating),2) as rating']) .from (Reviews) .where ({subject}) ) global.it || console.log ('< emitting:', 'reviewed', { subject, rating }) - this.emit ('reviewed', { subject, rating }) + await this.emit ('reviewed', { subject, rating }) }) // Increment counter for reviews considered helpful diff --git a/samples.md b/samples.md index efa74a61..bc5f7f81 100644 --- a/samples.md +++ b/samples.md @@ -1,28 +1,28 @@ # Overview of Samples -The list below gives an overview of the samples provided in subdirectories. -Each sub directory essentially is a individual npm package arranged in an [all-in-one monorepo](all-in-one-monorepo) umbrella setup. +The following list gives an overview of the samples provided in subdirectories. +Each sub directory essentially is an individual npm package arranged in an [all-in-one monorepo](all-in-one-monorepo) umbrella setup. -## [hello](hello) +## [@capire/hello-world](hello) - A simplistic [Hello World](https://cap.cloud.sap/docs/get-started/hello-world) service using [CDS](https://cap.cloud.sap/docs/cds/) and [cds.services](https://cap.cloud.sap/docs/node.js/api#services-api). -## [bookshop](bookshop) +## [@capire/bookshop](bookshop) - [Getting Started](https://cap.cloud.sap/docs/get-started/in-a-nutshell) with CAP, briefly introducing: - [Project Setup](https://cap.cloud.sap/docs/get-started/) and [Layouts](https://cap.cloud.sap/docs/get-started/projects) -- [Domain Modelling](https://cap.cloud.sap/docs/guides/domain-models) +- [Domain Modeling](https://cap.cloud.sap/docs/guides/domain-models) - [Defining Services](https://cap.cloud.sap/docs/guides/providing-services) - [Generic Providers](https://cap.cloud.sap/docs/guides/generic-providers) - [Adding Custom Logic](https://cap.cloud.sap/docs/guides/service-impl) - [Using Databases](https://cap.cloud.sap/docs/guides/databases) -## [common](common) +## [@capire/common](common) -- Showcases how to extend [@sap/cds/common](https://cap.cloud.sap/docs/cds/common) thereby covering... +- Showcases how to extend [@sap/cds/common](https://cap.cloud.sap/docs/cds/common) thereby covering: - Building [extension packages](https://cap.cloud.sap/docs/guides/domain-models#aspects-extensibility) - Providing [reuse packages](https://cap.cloud.sap/docs/get-started/projects#sharing-and-reusing-content) - [Verticalization](https://cap.cloud.sap/docs/cds/common#adapting-to-your-needs) @@ -30,34 +30,39 @@ Each sub directory essentially is a individual npm package arranged in an [all-i - Used in the [fiori app sample](#fiori) -## [orders](orders) +## [@capire/orders](orders) -- Adds orders to the [bookshop](#bookshop), thereby demonstrating... +- A standalone orders management service, demonstrating: - Using [Compositions](https://cap.cloud.sap/docs/cds/cdl#compositions) in [Domain Models](https://cap.cloud.sap/docs/guides/domain-models), along with - [Serving deeply nested documents](https://cap.cloud.sap/docs/guides/generic-providers#serving-structured-data) -## [reviews](reviews) +## [@capire/reviews](reviews) -- Shows how to implement a modular service to manage product reviews, including... +- Shows how to implement a modular service to manage product reviews, including: - Consuming other services synchronously and asynchronously - Serving requests synchronously - Emitting events asynchronously -- Grow as you go, with... +- Grow as you go, with: - Mocking app services - Running service meshes - Late-cut Micro Services -- As well as managed data, input validations and authorization +- As well as managed data, input validations, and authorization -## [fiori](fiori) +## [@capire/fiori](fiori) -- [Adds a Fiori elements application](https://cap.cloud.sap/docs/guides/fiori/), introducing to... -- [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 Fiori apps locally -- Combining most of the other samples through [package reuse](https://cap.cloud.sap/docs/get-started/projects#sharing-and-reusing-content) +- A [composite app, reusing and combining](https://cap.cloud.sap/docs/guides/verticalize) these packages: + - [@capire/bookshop](bookshop) + - [@capire/reviews](reviews) + - [@capire/orders](orders) + - [@capire/common](common) +- [Adds a 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
diff --git a/test/cds.ql.test.js b/test/cds.ql.test.js index 3af3327a..a8bfa7be 100644 --- a/test/cds.ql.test.js +++ b/test/cds.ql.test.js @@ -1,5 +1,5 @@ +const { expect } = require('../test') const cds = require('@sap/cds/lib') -const { expect } = cds.test const CQL = ([cql]) => cds.parse.cql(cql) const Foo = { name: 'Foo' } const Books = { name: 'capire.bookshop.Books' } @@ -325,7 +325,26 @@ describe('cds.ql → cqn', () => { }) // using CQL fragments -> uses cds.parse.expr - expect((cqn = CQL`SELECT from Foo where ID=11 and x in ( foo, 'bar', 3)`)).to.eql({ + const is_v2 = !!cds.parse.expr('(1,2)').list + if (is_v2) expect((cqn = CQL`SELECT from Foo where ID=11 and x in ( foo, 'bar', 3)`)).to.eql({ + SELECT: { + from: { ref: ['Foo'] }, + where: [ + { ref: ['ID'] }, + '=', + { val: ID }, + 'and', + { ref: ['x'] }, + 'in', + {list:[ + { ref: ['foo'] }, + { val: 'bar' }, + { val: 3 }, + ]} + ], + }, + }) + else expect((cqn = CQL`SELECT from Foo where ID=11 and x in ( foo, 'bar', 3)`)).to.eql({ SELECT: { from: { ref: ['Foo'] }, where: [ diff --git a/test/consuming-services.test.js b/test/consuming-services.test.js index ab550ef4..ff5c1b96 100644 --- a/test/consuming-services.test.js +++ b/test/consuming-services.test.js @@ -1,7 +1,7 @@ -const cds = require('@sap/cds/lib') -const { expect } = cds.test ( +const { expect } = require('../test') .run ( 'serve', 'AdminService', '--from', '@capire/bookshop,@capire/common', '--in-memory' -).in(__dirname) +) +const cds = require('@sap/cds/lib') describe('Consuming Services locally', () => { // diff --git a/test/custom-handlers.test.js b/test/custom-handlers.test.js index 8e798d97..f8541a18 100644 --- a/test/custom-handlers.test.js +++ b/test/custom-handlers.test.js @@ -1,5 +1,7 @@ +const { GET, POST, expect } = require('../test') .run ('bookshop') const cds = require('@sap/cds/lib') -const { GET, POST, expect } = cds.test('bookshop').in(__dirname,'..') +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', () => { diff --git a/test/hello-world.test.js b/test/hello-world.test.js index d8f58a80..d833dce3 100644 --- a/test/hello-world.test.js +++ b/test/hello-world.test.js @@ -1,5 +1,4 @@ -const cds = require('@sap/cds/lib') -const { GET, expect } = cds.test('serve','hello/world.cds').in(__dirname,'..') +const { GET, expect } = require('../test') .run ('serve','hello/world.cds') describe('Hello world!', () => { diff --git a/test/hierarchical-data.test.js b/test/hierarchical-data.test.js index d92dfcf9..e411eff0 100644 --- a/test/hierarchical-data.test.js +++ b/test/hierarchical-data.test.js @@ -1,6 +1,5 @@ -const cwd = process.cwd(); process.chdir (__dirname) //> only for internal CI/CD@SAP +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 @@ -25,8 +24,6 @@ describe('Hierarchical Data', ()=>{ expect (cds.db.model) .to.exist }) - after(()=> process.chdir(cwd)) - it ('supports deeply nested inserts', ()=> INSERT.into (Cats, { ID:100, name:'Some Cats...', children:[ { ID:101, name:'Cat', children:[ diff --git a/test/index.js b/test/index.js new file mode 100644 index 00000000..c44d3400 --- /dev/null +++ b/test/index.js @@ -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,'..') diff --git a/test/localized-data.test.js b/test/localized-data.test.js index ad8256b9..bfa3411f 100644 --- a/test/localized-data.test.js +++ b/test/localized-data.test.js @@ -1,5 +1,7 @@ +const { GET, expect } = require('../test') .run ('serve', 'test/localized-data.cds', '--in-memory') const cds = require('@sap/cds/lib') -const { GET, expect } = cds.test ('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 describe('Localized Data', () => { diff --git a/test/messaging.test.js b/test/messaging.test.js index a3e0df3e..42d20d38 100644 --- a/test/messaging.test.js +++ b/test/messaging.test.js @@ -1,15 +1,14 @@ +const { expect } = require('../test') const cds = require('@sap/cds/lib') -const cwd = process.cwd(); process.chdir (__dirname) //> only for internal CI/CD@SAP -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 describe('Messaging', ()=>{ - after(()=> process.chdir(cwd)) - it ('should bootstrap sqlite in-memory db', async()=>{ const db = await cds.deploy (_model) .to ('sqlite::memory:') + await db.delete('Reviews') expect (db.model) .not.undefined }) @@ -43,16 +42,16 @@ describe('Messaging', ()=>{ // { ID: 111 + (++N), subject: "201", title: "Captivating", rating: N }, // ), srv.create ('Reviews') .entries ( - { ID: 111 + (++N), subject: "201", title: "Captivating", rating: N } + { ID: String(111 + (++N)), subject: "201", title: "Captivating", rating: N } ), srv.create ('Reviews') .entries ( - { ID: 111 + (++N), subject: "201", title: "Captivating", rating: N } + { ID: String(111 + (++N)), subject: "201", title: "Captivating", rating: N } ), srv.create ('Reviews') .entries ( - { ID: 111 + (++N), subject: "201", title: "Captivating", rating: N } + { ID: String(111 + (++N)), subject: "201", title: "Captivating", rating: N } ), srv.create ('Reviews') .entries ( - { ID: 111 + (++N), subject: "201", title: "Captivating", rating: N } + { ID: String(111 + (++N)), subject: "201", title: "Captivating", rating: N } ), ])) diff --git a/test/odata.test.js b/test/odata.test.js index 4a561a95..7d8c8123 100644 --- a/test/odata.test.js +++ b/test/odata.test.js @@ -1,8 +1,11 @@ +const { GET, expect } = require('../test') .run ('bookshop') const cds = require('@sap/cds/lib') -const { GET, expect } = cds.test('bookshop').in(__dirname,'..') +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('OData Protocol', () => { + it('serves $metadata documents in v4', async () => { const { headers, status, data } = await GET `/browse/$metadata` expect(status).to.equal(200)