Compare commits
5 Commits
suppliers-
...
cucumber
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9ce3bb9ef6 | ||
|
|
5135276952 | ||
|
|
eb98444afb | ||
|
|
83537482b8 | ||
|
|
cf10129b68 |
@@ -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>
|
||||
{{ book.stock }} in stock
|
||||
<span class="failed"> {{ order.failed }} </span>
|
||||
<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>
|
||||
|
||||
@@ -3,10 +3,9 @@
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@capire/bookshop": "*",
|
||||
"@capire/common": "*",
|
||||
"@capire/orders": "*",
|
||||
"@capire/reviews": "*",
|
||||
"@capire/suppliers": "*",
|
||||
"@capire/orders": "*",
|
||||
"@capire/common": "*",
|
||||
"@sap/cds": "^4",
|
||||
"express": "^4.17.1",
|
||||
"passport": "^0.4.1"
|
||||
@@ -20,10 +19,6 @@
|
||||
"deploy-format": "hdbtable"
|
||||
},
|
||||
"requires": {
|
||||
"API_BUSINESS_PARTNER": {
|
||||
"kind": "odata",
|
||||
"model": "@capire/suppliers"
|
||||
},
|
||||
"auth": {
|
||||
"strategy": "dummy"
|
||||
},
|
||||
|
||||
@@ -7,7 +7,6 @@ cds.once('bootstrap',(app)=>{
|
||||
})
|
||||
|
||||
cds.once('served', require('./srv/mashup'))
|
||||
cds.once('served', require('@capire/suppliers/srv/mashup'))
|
||||
|
||||
module.exports = cds.server
|
||||
|
||||
|
||||
@@ -11,17 +11,17 @@
|
||||
"@capire/hello": "./hello",
|
||||
"@capire/media": "./media",
|
||||
"@capire/orders": "./orders",
|
||||
"@capire/reviews": "./reviews",
|
||||
"@capire/suppliers": "./suppliers"
|
||||
"@capire/reviews": "./reviews"
|
||||
},
|
||||
"devDependencies": {
|
||||
"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": {
|
||||
"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",
|
||||
"bookshop": "cds watch bookshop",
|
||||
"fiori": "cds watch fiori",
|
||||
|
||||
@@ -8,9 +8,9 @@ service ReviewsService {
|
||||
action unlike (review: type of Reviews:ID);
|
||||
|
||||
// Async API
|
||||
event reviewed : projection on Reviews {
|
||||
subject,
|
||||
rating
|
||||
event reviewed : {
|
||||
subject: type of Reviews:subject;
|
||||
rating: Decimal(2,1)
|
||||
}
|
||||
|
||||
// Input validation
|
||||
|
||||
12
samples.md
12
samples.md
@@ -49,22 +49,14 @@ Each sub directory essentially is an individual npm package arranged in an [all-
|
||||
- Late-cut Micro Services
|
||||
- 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)
|
||||
|
||||
- A [composite app, reusing and combining](https://cap.cloud.sap/docs/guides/verticalize) these packages:
|
||||
- [@capire/bookshop](bookshop)
|
||||
- [@capire/common](common)
|
||||
- [@capire/orders](orders)
|
||||
- [@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:
|
||||
- [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)
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
using from './srv/mashup';
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
const cds = require ('@sap/cds')
|
||||
cds.once('served', require('./srv/mashup'))
|
||||
module.exports = cds.server
|
||||
2425
suppliers/srv/external/API_BUSINESS_PARTNER.csn
vendored
2425
suppliers/srv/external/API_BUSINESS_PARTNER.csn
vendored
File diff suppressed because it is too large
Load Diff
3261
suppliers/srv/external/API_BUSINESS_PARTNER.edmx
vendored
3261
suppliers/srv/external/API_BUSINESS_PARTNER.edmx
vendored
File diff suppressed because it is too large
Load Diff
@@ -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,5 +0,0 @@
|
||||
ID;name
|
||||
ACME;A Company Making Everything
|
||||
B4U;Books for You
|
||||
S&C;Shakespeare & Co.
|
||||
WSL;Waterstones
|
||||
|
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
))
|
||||
}
|
||||
|
||||
}
|
||||
34
test/features/bookshop.feature
Normal file
34
test/features/bookshop.feature
Normal 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'
|
||||
82
test/features/step_definitions/bookshop_steps.js
Normal file
82
test/features/step_definitions/bookshop_steps.js
Normal 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)
|
||||
})
|
||||
Reference in New Issue
Block a user