Compare commits

...

17 Commits

Author SHA1 Message Date
Daniel
9ce3bb9ef6 typo 2021-03-03 23:25:45 +01:00
Daniel
5135276952 copy error 2021-03-03 23:22:43 +01:00
Daniel
eb98444afb one more 2021-03-03 23:11:41 +01:00
Daniel
83537482b8 Adding cucumber tests 2021-03-03 22:47:23 +01:00
Daniel
cf10129b68 Prepare for compiler v2 2021-03-02 17:14:38 +01:00
Iwona Hahn
861094cc83 Merge pull request #206 from SAP-samples/promote-code-tour
promote code tours
2021-03-02 09:15:19 +01:00
Christian Georgi
2368913612 Dummy change 2021-03-02 09:09:51 +01:00
Iwona Hahn
22ece2e586 typo 2021-03-01 20:44:05 +01:00
Iwona Hahn
ae788414f8 promote code tours 2021-03-01 20:42:43 +01:00
Iwona Hahn
ebc7017ded UA review 2021-03-01 20:42:23 +01:00
Christian Georgi
876ff94782 Merge pull request #205 from SAP-samples/db-codetour
Codetour for DB functions
2021-03-01 17:29:31 +01:00
Iwona Hahn
76d1b1865b minor edits 2021-03-01 17:24:53 +01:00
Christian Georgi
85dd8558f4 Some more explanation around file structure 2021-03-01 14:38:00 +01:00
Iwona Hahn
d0d95f3c42 first review, minor edits 2021-02-26 17:14:49 +01:00
Christian Georgi
c46a82d0f8 Codetour for DB functions 2021-02-26 16:40:57 +01:00
Iwona Hahn
7654012292 cosmetics codetour 2021-02-26 09:29:51 +01:00
Christian Georgi
65c8c82f74 Showcase how to integrate DB-specific functions (#201)
Augment `AdminService.Author` by 2 fields `age` and `lifetime` whose
values are computed by SQLite and HANA-specific functions.

Don't do this in bookshop directly, to keep it simple.  Instead, use
the fiori module, which also allows to integrate the fields in the UI.

Co-authored-by: Daniel <daniel.hutzel@sap.com>
2021-02-24 17:26:29 +01:00
18 changed files with 329 additions and 19 deletions

109
.tours/db-native.tour Normal file
View File

@@ -0,0 +1,109 @@
{
"$schema": "https://aka.ms/codetour-schema",
"title": "Database Functions",
"steps": [
{
"title": "Introduction",
"description": "### Database Functions in CDS Models\n\nIn this tour, you'll learn how to add database-specific functions to CDS models in your application."
},
{
"file": "bookshop/db/schema.cds",
"description": "#### Basic Schema\n\nWe want to add two fields to the `Authors` entity, one for the author's age and one for the span of years that she or he lived.\n\nThese two fields can be computed out of the existing `dateOfBirth` and `dateOfDeath` fields.",
"selection": {
"start": {
"line": 19,
"character": 1
},
"end": {
"line": 21,
"character": 1
}
},
"title": "Base fields in Author"
},
{
"file": "bookshop/srv/admin-service.cds",
"description": "This is how the `Authors` entity gets exposed in an OData or REST service.\n\nIn the next step, you'll see how we extend this projection.",
"selection": {
"start": {
"line": 4,
"character": 1
},
"end": {
"line": 5,
"character": 1
}
},
"title": "Authors service"
},
{
"file": "fiori/db/sqlite/index.cds",
"description": "#### SQLite Implementation\n\nHere's the first implementation for SQLite. It computes the two fields `age` and `lifetime` through SQLite's [strftime](https://sqlite.org/lang_datefunc.html) function.\n\nThrough the [`extend projection`](https://cap.cloud.sap/docs/cds/cdl#extend-view) clause you can add additional fields to projection entities. These are deployed as database views, which is why we can integrate the database functions in the first place.\n",
"selection": {
"start": {
"line": 7,
"character": 1
},
"end": {
"line": 11,
"character": 1
}
},
"title": "SQLite implementation"
},
{
"file": "fiori/db/hana/index.cds",
"description": "#### SAP HANA Implementation\n\nThis is the second implementation for SAP HANA. It computes the same two fields `age` and `lifetime` through the [YEARS_BETWEEN](https://help.sap.com/viewer/7c78579ce9b14a669c1f3295b0d8ca16/Cloud/en-US/7c0d2c161ea34def86de3f5eadd6a0af.html) and [YEAR](https://help.sap.com/viewer/7c78579ce9b14a669c1f3295b0d8ca16/Cloud/en-US/20f5fac6751910148dabd3c6821f907d.html) functions of SAP HANA.\n\n#### File Layout and Code Structure\n\nNote the path of the `.cds` file we are in: it's in a subfolder of `db`, so that it's _not_ automatically picked up when we start the application. The same is true for the SQLite implementation: it's in a separate `db/sqlite/` folder as well. In the next step, you'll see how these files are loaded.\n\nAlso, we choose to implement all of that as an extension of the original bookshop here in the _fiori_ package. See the first [CAP Samples] code tour for more details on the different packages of this repository.",
"selection": {
"start": {
"line": 7,
"character": 1
},
"end": {
"line": 11,
"character": 1
}
},
"title": "SAP HANA implementation"
},
{
"file": "fiori/package.json",
"description": "#### Configuration\n\nThe `cds` section in `package.json` is a place to configure which of the `db/sqlite` and `db/hana` folders are used for which database.\nWe use [Node.js profiles](https://cap.cloud.sap/docs/node.js/cds-env#profiles) to separate the configuration.\nIn the `development` profile, you can see that `db/sqlite` is set as the model, while the `db/hana` folder is configured in the `production` profile.",
"line": 17,
"title": "Configuration"
},
{
"file": "fiori/package.json",
"description": "#### Run with SQLite\n\nTo run with `development` and an in-memory SQLite database, you don't need to do anything special, because it's activated by default. Just run:\n\n>> cds watch fiori\n\nThen open [http://localhost:4004/admin/Authors](http://localhost:4004/admin/Authors) to see the two new fields.\n",
"line": 28,
"title": "Run with SQLite"
},
{
"file": "fiori/package.json",
"description": "#### Deploy the CDS Model to SAP HANA\n\nTo 'activate' SAP HANA through the `production` profile, you can use the global `--production` flag:\n\n>> cd fiori; cds deploy --to hana --production\n\n[Learn more about SAP HANA deployment](https://cap.cloud.sap/docs/guides/databases#get-hana)\n\n#### Run the Application\n\n>> cd fiori; cds watch --production\n\nThe service on [http://localhost:4004/admin/Authors](http://localhost:4004/admin/Authors) is the same as before, but this time the `Authors` entity is backed by a database view with an SAP HANA function.\n\n#### More\n\nIf you don't see data, you can add some in the next step.",
"line": 31,
"title": "Run with SAP HANA"
},
{
"file": "fiori/test/requests.http",
"description": "### Add More Data\n\nOptionally you can add some `Authors` data by clicking on the _Send Request_ link (provided by the [REST client](https://marketplace.visualstudio.com/items?itemName=humao.rest-client) extension).",
"line": 68,
"selection": {
"start": {
"line": 67,
"character": 1
},
"end": {
"line": 73,
"character": 1
}
},
"title": "Add Data"
},
{
"title": "Wrap-up",
"description": "### Summary\n\nThat's it! You have seen: \n- How to integrate database-specific functions in a CDS model.\n- How to switch between the two implementations for SQLite and SAP HANA."
}
],
"ref": "master"
}

View File

@@ -5,7 +5,7 @@
{
"title": "Welcome",
"file": "README.md",
"description": "### Welcome to CAP Samples!\n\nThis tour leads you through a collection of samples for the [SAP Cloud Application Programming Model](https://cap.cloud.sap)\nYou will learn which features of the programming models are demonstrated in which sample.\n\nLet's start!",
"description": "### Welcome to CAP Samples!\n\nThis tour leads you through a collection of samples for the [SAP Cloud Application Programming Model (CAP)](https://cap.cloud.sap).\nYou will learn which features of the programming model are demonstrated in which sample.\n\nLet's start!",
"line": 2,
"selection": {
"start": {
@@ -133,4 +133,4 @@
],
"isPrimary": true,
"description": "Overview of CAP Samples for Node.js"
}
}

View File

@@ -18,7 +18,7 @@ Find here a collection of samples for the [SAP Cloud Application Programming Mod
### Download
If you have [Git](https://git-scm.com/downloads) installed, clone this repo as shown below, otherwise [download as ZIP file](archive/master.zip).
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
@@ -72,10 +72,14 @@ npm add @capire/common @capire/bookshop
```
## Code Tours
Take one of the [guided tours](.tours) in VS Code through our CAP samples and learn which CAP features are showcased by the different parts of the repository. Just install the [CodeTour extension](https://marketplace.visualstudio.com/items?itemName=vsls-contrib.codetour) for VS Code. We'll add more code tours in the future. Stay tuned!
## Get Support
Check out the documentation at [https://cap.cloud.sap](https://cap.cloud.sap). <br>
In case you have a question, find a bug, or otherwise need support, please use our [community](https://answers.sap.com/tags/9f13aee1-834c-4105-8e43-ee442775e5ce).
In case you've a question, find a bug, or otherwise need support, use our [community](https://answers.sap.com/tags/9f13aee1-834c-4105-8e43-ee442775e5ce) to get more visibility.
## License

View File

@@ -44,11 +44,11 @@
<img v-bind:src="book.image" alt=""/>
<label style="text-align:right">
<span class="succeeded"> {{ order.succeeded }} </span>
<span class="failed"> {{ order.failed }} </span>
&nbsp;&nbsp; {{ book.stock }} in stock
<span class="failed"> {{ order.failed }} </span> &nbsp;&nbsp;
<span id="stock"> {{ book.stock }} in stock </span>
</label>
<form @submit.prevent="submitOrder" style="float:right; display:flex; flex-direction:row-reverse">
<input type="number" v-model="order.amount" v-bind:class="{ failed: order.failed }" style="width:5em">
<input id="amount" 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>

View File

@@ -11,6 +11,7 @@ DateOfBirth = Date of Birth
DateOfDeath = Date of Death
PlaceOfBirth = Place of Birth
PlaceOfDeath = Place of Death
Age = Age
Authors = Authors
Order = Order
Orders = Orders

View File

@@ -6,6 +6,7 @@ Authors = Autoren
Author = Autor
AuthorID = ID des Autors
AuthorName = Name des Autors
Age = Alter
Name = Name
Stock = Bestand
Order = Bestellung

View File

@@ -1,4 +1,4 @@
using AdminService from '@capire/bookshop';
using { AdminService } from '../../db';
////////////////////////////////////////////////////////////////////////////
//
@@ -39,6 +39,27 @@ annotate AdminService.Books with @(
}
);
annotate AdminService.Authors with @(
UI: {
HeaderInfo: {
Description: {Value: lifetime}
},
Facets: [
{$Type: 'UI.ReferenceFacet', Label: '{i18n>Details}', Target: '@UI.FieldGroup#Details'},
{$Type: 'UI.ReferenceFacet', Label: '{i18n>Books}', Target: 'books/@UI.LineItem'},
],
FieldGroup#Details: {
Data: [
{Value: placeOfBirth},
{Value: placeOfDeath},
{Value: dateOfBirth},
{Value: dateOfDeath},
{Value: age, Label: '{i18n>Age}'},
]
},
}
);
////////////////////////////////////////////////////////////

10
fiori/db/hana/index.cds Normal file
View File

@@ -0,0 +1,10 @@
//
// Add Author.age and .lifetime with a DB-specific function
//
using { AdminService } from '..';
extend projection AdminService.Authors with {
YEARS_BETWEEN(dateOfBirth, dateOfDeath) as age: Integer,
YEAR(dateOfBirth) || ' ' || YEAR(dateOfDeath) as lifetime : String
}

8
fiori/db/index.cds Normal file
View File

@@ -0,0 +1,8 @@
using { sap.capire.bookshop } from '@capire/bookshop';
// Forward-declare calculated fields to be filled in database-specific ways
// TODO find a better way to have 'default' fields that still can be overwritten.
extend bookshop.Authors with {
virtual age: Integer;
virtual lifetime: String;
}

10
fiori/db/sqlite/index.cds Normal file
View File

@@ -0,0 +1,10 @@
//
// Add Author.age and .lifetime with a DB-specific function
//
using { AdminService } from '..';
extend projection AdminService.Authors with {
strftime('%Y',dateOfDeath)-strftime('%Y',dateOfBirth) as age: Integer,
strftime('%Y',dateOfBirth) || ' ' || strftime('%Y',dateOfDeath) as lifetime : String
}

View File

@@ -8,23 +8,37 @@
"@capire/common": "*",
"@sap/cds": "^4",
"express": "^4.17.1",
"passport": "0.4.1"
"passport": "^0.4.1"
},
"scripts": {
"start": "cds run --in-memory?",
"watch": "cds watch"
},
"cds": {
"hana": {
"deploy-format": "hdbtable"
},
"requires": {
"auth": {
"strategy": "dummy"
},
"ReviewsService": {
"kind": "odata", "model": "@capire/reviews"
"kind": "odata",
"model": "@capire/reviews"
},
"OrdersService": {
"kind": "odata", "model": "@capire/orders"
"kind": "odata",
"model": "@capire/orders"
},
"db": {
"kind": "sql"
"kind": "sql",
"[development]": {
"model": "db/sqlite"
},
"[production]": {
"model": "db/hana"
}
}
}
}
}
}

View File

@@ -63,3 +63,15 @@ Content-Type: application/json
### Get active order
GET {{bookshop}}/orders/Orders(ID={{newOrderID}},IsActiveEntity=true)
### Create author
POST {{bookshop}}/admin/Authors
Content-Type: application/json
Authorization: Basic alice:
{
"ID": 200,
"name": "William Shakespeare",
"dateOfBirth": "1564-04-26",
"dateOfDeath": "1616-04-23"
}

View File

@@ -17,6 +17,8 @@
"chai": "^4.2.0",
"chai-as-promised": "^7.1.1",
"chai-subset": "^1.6.0",
"@cucumber/cucumber": "^7.0.0",
"selenium-webdriver": "^4.0.0-beta.1",
"sqlite3": "5.0.0"
},
"scripts": {

View File

@@ -3,19 +3,22 @@
## Run all-in-one
Open a terminal window and run the bookshop in it:
```sh
npm run bookshop
```
## Run as separate services
## Run as Separate Services
Open two terminal windows. In the first one start the reviews service stand-alone:
Open two terminal windows, and in the first one start the reviews service stand-alone:
```sh
npm run reviews-service
```
In the the second one start the bookshop:
In the second one start the bookshop:
```sh
npm run bookshop
```

View File

@@ -57,14 +57,13 @@ Each sub directory essentially is an individual npm package arranged in an [all-
- [@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:
- [Adds an SAP Fiori elements application](https://cap.cloud.sap/docs/guides/fiori/) to bookshop, thereby introducing to:
- [OData Annotations](https://cap.cloud.sap/docs/guides/fiori#adding-odata-annotations) in `.cds` files
- Support for [Fiori Draft](https://cap.cloud.sap/docs/guides/fiori#draft)
- Support for [Value Helps](https://cap.cloud.sap/docs/guides/fiori#value-help)
- Serving SAP Fiori apps locally
- [The Vue.js app](bookshop/app/vue) imported from bookshop is served as well
<br>
# All-in-one Monorepo

View File

@@ -365,7 +365,7 @@ describe('cds.ql → cqn', () => {
},
})
expect(
if (!is_v2) expect(
SELECT.from(Foo).where(`x=`, 1, `or y.z is null and (a>`, 2, `or b=`, 3, `)`)
).to.eql(CQL`SELECT from Foo where x=1 or y.z is null and (a>2 or b=3)`)

View File

@@ -0,0 +1,34 @@
Feature: List Books using Vue.js UI
Scenario: Launch cds server for bookshop
When we run the 'bookshop' server
And wait for 1s
Then it should listen at 'http://localhost:4004'
Scenario: Display Books List
When we open page '/vue/index.html'
And wait for 1s
Then it should list these rows in table 'books':
| Wuthering Heights | Emily Brontë |
| Jane Eyre | Charlotte Brontë |
| The Raven | Edgar Allen Poe |
| Eleonora | Edgar Allen Poe |
| Catweazle | Richard Carpenter |
Scenario: Select a Book
When we click on the 1st row in table 'books'
Then it shows '12' in 'stock'
Scenario: Order One Book
When we click on button 'Order:'
Then it succeeds with 'ordered 1 item(s)'
Scenario: Order Four Books
When we enter '4' into 'amount'
And we click on button 'Order:'
Then it succeeds with 'ordered 4 item(s)'
Scenario: Order Amount Exceeding Stock
When we enter '9' into 'amount'
And we click on button 'Order:'
Then it fails with '9 exceeds stock'

View File

@@ -0,0 +1,82 @@
const { Given, When, Then, AfterAll } = require('@cucumber/cucumber')
const { Builder, By } = require('selenium-webdriver')
const browser = (new Builder).forBrowser('safari').build()
const cds = require ('@sap/cds/lib')
process.env.cds_requires_auth_strategy = 'dummy'
let axios = require('axios').default
let {display} = browser
When('we run the {string} server', project => cds.exec('watch', project))
Then('it should listen at {string}', baseURL => {
axios = axios.create({ baseURL })
display = url => browser.get (baseURL+url)
return axios.head()
})
Then('terminate the server', ()=> process.exit())
When(/wait for (\d+)\s*(\w+)/, {timeout:60*1000}, (delay, unit, done) => {
const factor = {
ms: 1,
s: 1000, sec: 1000, second: 1000, seconds: 1000,
m: 60*1000, min: 60*1000, minute: 60*1000, minutes: 60*1000,
h: 60*60*1000, hr: 60*60*1000, hour: 60*60*1000, hours: 60*60*1000,
}[unit]
if (!factor) throw `Unknown duration unit: ${unit}`
setTimeout (done, delay * factor)
})
AfterAll(()=> setTimeout (process.exit, 111))
When('we open page {string}', page => display(page))
Then('it should list these rows in table {string}:', async (id,data) => {
let rows = await browser.findElements(By.css(`#${id} tr`)); rows.shift()
await Promise.all (data.rawTable.map (async (row,i)=>{
const tr = await rows[i].getText()
for (let each of row) if (!tr.match(each)) throw `Didn't find '${each}' in web page as expected`
}))
})
When(/we click on the (\d+)(?:st|nd|rd|th) row in table '(\w+)'/, async (row, id) => {
let rows = await browser.findElements(By.css(`#${id} tr`))
let td = await rows[row].findElement(By.css('td'))
return td.click()
})
When('we enter {string} into {string}', async (value,id) => {
const field = await browser.findElement(By.css(`input#${id}`))
return field.sendKeys('\b\b\b\b\b\b',value)
})
When('we click on button {string}', async (text) => {
const button = await browser.findElement(By.css(`input[value='${text}']`))
return button.click()
})
Then('it succeeds with {string}', async message => {
const element = await browser.wait (browser.findElement(By.css(`span.succeeded`)))
return (await element.getText()).includes(message)
})
Then('it fails with {string}', async message => {
const element = await browser.wait (browser.findElement(By.css(`span.failed`)))
return (await element.getText()).includes(message)
})
Then('it shows {string} in {string}', async (message,id) => {
const element = await browser.wait (browser.findElement(By.id(id)))
return (await element.getText()).includes(message)
})
Given('we login as {string}, {string}', async (username, password) => {
const alert = await browser.switchTo().alert()
return alert.authenticateAs(username, password)
})