Compare commits

..

5 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
17 changed files with 129 additions and 5835 deletions

View File

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

View File

@@ -3,10 +3,9 @@
"version": "1.0.0", "version": "1.0.0",
"dependencies": { "dependencies": {
"@capire/bookshop": "*", "@capire/bookshop": "*",
"@capire/common": "*",
"@capire/orders": "*",
"@capire/reviews": "*", "@capire/reviews": "*",
"@capire/suppliers": "*", "@capire/orders": "*",
"@capire/common": "*",
"@sap/cds": "^4", "@sap/cds": "^4",
"express": "^4.17.1", "express": "^4.17.1",
"passport": "^0.4.1" "passport": "^0.4.1"
@@ -20,10 +19,6 @@
"deploy-format": "hdbtable" "deploy-format": "hdbtable"
}, },
"requires": { "requires": {
"API_BUSINESS_PARTNER": {
"kind": "odata",
"model": "@capire/suppliers"
},
"auth": { "auth": {
"strategy": "dummy" "strategy": "dummy"
}, },

View File

@@ -7,7 +7,6 @@ cds.once('bootstrap',(app)=>{
}) })
cds.once('served', require('./srv/mashup')) cds.once('served', require('./srv/mashup'))
cds.once('served', require('@capire/suppliers/srv/mashup'))
module.exports = cds.server module.exports = cds.server

View File

@@ -11,17 +11,17 @@
"@capire/hello": "./hello", "@capire/hello": "./hello",
"@capire/media": "./media", "@capire/media": "./media",
"@capire/orders": "./orders", "@capire/orders": "./orders",
"@capire/reviews": "./reviews", "@capire/reviews": "./reviews"
"@capire/suppliers": "./suppliers"
}, },
"devDependencies": { "devDependencies": {
"chai": "^4.2.0", "chai": "^4.2.0",
"chai-as-promised": "^7.1.1", "chai-as-promised": "^7.1.1",
"chai-subset": "^1.6.0", "chai-subset": "^1.6.0",
"@cucumber/cucumber": "^7.0.0",
"selenium-webdriver": "^4.0.0-beta.1",
"sqlite3": "5.0.0" "sqlite3": "5.0.0"
}, },
"scripts": { "scripts": {
"fix-antlr": "sed -i -e 's/INVALID_ALT_NUMBER = require.*/INVALID_ALT_NUMBER = 0/' node_modules/antlr4/tree/Trees.js node_modules/antlr4/RuleContext.js",
"registry": "node .registry/server.js", "registry": "node .registry/server.js",
"bookshop": "cds watch bookshop", "bookshop": "cds watch bookshop",
"fiori": "cds watch fiori", "fiori": "cds watch fiori",

View File

@@ -8,9 +8,9 @@ service ReviewsService {
action unlike (review: type of Reviews:ID); action unlike (review: type of Reviews:ID);
// Async API // Async API
event reviewed : projection on Reviews { event reviewed : {
subject, subject: type of Reviews:subject;
rating rating: Decimal(2,1)
} }
// Input validation // Input validation

View File

@@ -49,22 +49,14 @@ Each sub directory essentially is an individual npm package arranged in an [all-
- Late-cut Micro Services - Late-cut Micro Services
- As well as managed data, input validations, and authorization - As well as managed data, input validations, and authorization
## [@capire/suppliers](suppliers)
- Shows how to integrate remote services, in this case the BusinessPartner service from SAP S/4HANA.
- Extending [@capire/bookshop](bookshop) with suppliers from SAP S/4HANA
- Providing that as a pre-built integration & extension package
- Used in [@capire/fiori](fiori)
## [@capire/fiori](fiori) ## [@capire/fiori](fiori)
- A [composite app, reusing and combining](https://cap.cloud.sap/docs/guides/verticalize) these packages: - A [composite app, reusing and combining](https://cap.cloud.sap/docs/guides/verticalize) these packages:
- [@capire/bookshop](bookshop) - [@capire/bookshop](bookshop)
- [@capire/common](common)
- [@capire/orders](orders)
- [@capire/reviews](reviews) - [@capire/reviews](reviews)
- [@capire/suppliers](suppliers) - [@capire/orders](orders)
- [@capire/common](common)
- [Adds an 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 - [OData Annotations](https://cap.cloud.sap/docs/guides/fiori#adding-odata-annotations) in `.cds` files
- Support for [Fiori Draft](https://cap.cloud.sap/docs/guides/fiori#draft) - Support for [Fiori Draft](https://cap.cloud.sap/docs/guides/fiori#draft)

View File

@@ -1 +0,0 @@
using from './srv/mashup';

View File

@@ -1,24 +0,0 @@
{
"name": "@capire/suppliers",
"version": "1.0.0",
"description": "Shows integration with SAP S/4HANA, in turn provided as a reusable extension package to bookshop.",
"private": true,
"dependencies": {
"@capire/common": "*",
"@sap/cds": "^4",
"express": "^4"
},
"scripts": {
"start": "cds run --in-memory?",
"watch": "cds watch",
"mocked-s4": "cds mock API_BUSINESS_PARTNER"
},
"cds": {
"requires": {
"API_BUSINESS_PARTNER": {
"kind": "odata",
"model": "srv/external/API_BUSINESS_PARTNER"
}
}
}
}

View File

@@ -1,3 +0,0 @@
const cds = require ('@sap/cds')
cds.once('served', require('./srv/mashup'))
module.exports = cds.server

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +0,0 @@
BusinessPartner;BusinessPartnerFullName
ACME;A Company Making Everything
B4U;Books for You
S&C;Shakespeare & Co.
WSL;Waterstones
TLD;Thalia
PNG;Penguin Books
1 BusinessPartner BusinessPartnerFullName
2 ACME A Company Making Everything
3 B4U Books for You
4 S&C Shakespeare & Co.
5 WSL Waterstones
6 TLD Thalia
7 PNG Penguin Books

View File

@@ -1,5 +0,0 @@
ID;name
ACME;A Company Making Everything
B4U;Books for You
S&C;Shakespeare & Co.
WSL;Waterstones
1 ID name
2 ACME A Company Making Everything
3 B4U Books for You
4 S&C Shakespeare & Co.
5 WSL Waterstones

View File

@@ -1,25 +0,0 @@
/*
Optionally add projections to external entities, to capture what
you actually want to use from there.
*/
using {API_BUSINESS_PARTNER as S4} from './external/API_BUSINESS_PARTNER.csn';
@cds.autoexpose // or expose explicitly in Catalog and AdminService
@cds.persistence: {table,skip:false}
entity sap.capire.bookshop.Suppliers as projection on S4.A_BusinessPartner {
key BusinessPartner as ID, BusinessPartnerFullName as name
}
using { sap.capire.bookshop.Books, CatalogService } from '@capire/bookshop';
extend Books with {
supplier: Association to sap.capire.bookshop.Suppliers;
}
extend service AdminService with { // why is AdminService visible?
entity Suppliers as projection on sap.capire.bookshop.Suppliers;
}
extend projection CatalogService.ListOfBooks with {
supplier
}

View File

@@ -1,57 +0,0 @@
////////////////////////////////////////////////////////////////////////////
//
// Mashing up provided and required services...
//
module.exports = async()=>{ // called by server.js
if (!cds.services.AdminService) return //> mocking S4 service only
// Connect to services we want to mashup below...
const S4bupa = await cds.connect.to('API_BUSINESS_PARTNER') //> external S4 service
const admin = await cds.connect.to('AdminService') //> local domain service
const db = await cds.connect.to('db') //> our primary database
// Reflect CDS definition of the Suppliers entity
const { Suppliers } = S4bupa.entities
admin.prepend (()=>{ //> to ensure our .on handlers below go before the default ones
// Delegate Value Help reads for Suppliers to S4 backend
admin.on ('READ', 'Suppliers', req => {
console.log ('>> delegating to S4 service...')
return S4bupa.run(req.query)
})
// Replicate Supplier data when edited Books have suppliers
admin.on (['CREATE','UPDATE'], 'Books', ({data:{supplier}}, next) => {
// Using Promise.all(...) to parallelize local write, i.e. next(), and replication
if (supplier) return Promise.all ([ next(), async()=>{
let replicated = await db.exists (Suppliers, supplier)
if (!replicated) await replicate (supplier, 'initial')
}])
else return next() //> don't forget to pass down the interceptor stack
})
})
// Subscribe to changes in the S4 origin of Suppliers data
S4bupa.on ('BusinessPartners/Changed', async msg => { //> would be great if we had batch events from S/4
let replicas = await SELECT('ID').from (Suppliers) .where ('ID in', msg.businessPartners)
return replicate (replicas.map(each => each.ID))
})
/**
* Helper function to replicate Suppliers data.
* @param {string|string[]} IDs a single ID or an array of IDs
* @param {truthy|falsy} _initial indicates whether an insert or an update is required
*/
async function replicate (IDs,_initial) {
if (!Array.isArray(IDs)) IDs = [ IDs ]
let suppliers = await S4bupa.read (Suppliers).where('ID in',IDs)
if (_initial) return db.insert (suppliers) .into (Suppliers) //> using bulk insert
else return Promise.all(suppliers.map ( //> parallelizing updates
each => db.update (Suppliers,each.ID) .with (each)
))
}
}

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)
})