Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
11f3b357e4 |
@@ -1,22 +1,17 @@
|
|||||||
const { exec } = require ('child_process')
|
const { exec } = require ('child_process')
|
||||||
const isWin = process.platform === 'win32'
|
|
||||||
const express = require ('express')
|
const express = require ('express')
|
||||||
const fs = require ('fs')
|
const fs = require ('fs')
|
||||||
const app = express()
|
const app = express()
|
||||||
|
|
||||||
const { PORT=4444 } = process.env
|
const { PORT=4444 } = process.env
|
||||||
const [,,port=PORT,scope='@capire'] = process.argv
|
const [,,port=PORT] = process.argv
|
||||||
const cwd = __dirname
|
const cwd = __dirname
|
||||||
|
|
||||||
// clean up on start (exit handler might not complete on Windows)
|
|
||||||
exec(isWin ? 'del *.tgz' : 'rm *.tgz', {cwd})
|
|
||||||
|
|
||||||
|
|
||||||
app.use('/-/:tarball', (req,res,next) => {
|
app.use('/-/:tarball', (req,res,next) => {
|
||||||
console.debug ('GET', req.params)
|
console.debug ('GET', req.params)
|
||||||
try {
|
try {
|
||||||
const { tarball } = req.params
|
const { tarball } = req.params
|
||||||
const [, pkg ] = /^\w+-(\w+)/.exec(tarball)
|
const [, pkg ] = /^capire-(\w+)/.exec(tarball)
|
||||||
fs.lstat(tarball,(err => {
|
fs.lstat(tarball,(err => {
|
||||||
if (err) exec(`npm pack ../${pkg}`,{cwd},next)
|
if (err) exec(`npm pack ../${pkg}`,{cwd},next)
|
||||||
else next()
|
else next()
|
||||||
@@ -30,14 +25,12 @@ app.use('/-/:tarball', (req,res,next) => {
|
|||||||
app.use('/-', express.static(__dirname))
|
app.use('/-', express.static(__dirname))
|
||||||
|
|
||||||
app.get('/*', (req,res)=>{
|
app.get('/*', (req,res)=>{
|
||||||
const urlRegex = /^\/(@\w+)\/(\w+)/
|
|
||||||
const url = decodeURIComponent(req.url)
|
const url = decodeURIComponent(req.url)
|
||||||
console.debug ('GET',url)
|
console.debug ('GET',url)
|
||||||
try {
|
try {
|
||||||
if (!urlRegex.test(url)) return res.sendStatus(404)
|
const [, capire, pkg ] = /^\/(@capire)\/(\w+)/.exec(url)
|
||||||
const [, scpe, pkg ] = urlRegex.exec(url)
|
const package = require (`${capire}/${pkg}/package.json`)
|
||||||
const package = require (`${scpe}/${pkg}/package.json`)
|
const tarball = `capire-${pkg}-${package.version}.tgz`
|
||||||
const tarball = `${scpe.slice(1)}-${pkg}-${package.version}.tgz`
|
|
||||||
// https://github.com/npm/registry/blob/master/docs/responses/package-metadata.md
|
// https://github.com/npm/registry/blob/master/docs/responses/package-metadata.md
|
||||||
res.json({
|
res.json({
|
||||||
"name": package.name,
|
"name": package.name,
|
||||||
@@ -49,30 +42,29 @@ app.get('/*', (req,res)=>{
|
|||||||
"name": package.name,
|
"name": package.name,
|
||||||
"version": package.version,
|
"version": package.version,
|
||||||
"dist": {
|
"dist": {
|
||||||
"tarball": `/-/${tarball}`
|
"tarball": `http://localhost:${port}/-/${tarball}`
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.code === 'MODULE_NOT_FOUND') return res.sendStatus(404)
|
console.error(e)
|
||||||
console.error(e); throw e
|
res.sendStatus(404)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const server = app.listen(port, ()=>{
|
app.listen(port, ()=>{
|
||||||
const url = `http://localhost:${server.address().port}`
|
console.log (`npm set @capire:registry=http://localhost:${port}`)
|
||||||
console.log (`npm set ${scope}:registry=${url}`)
|
console.log (`@capire registry listening on http://localhost:${port}`)
|
||||||
exec(`npm set ${scope}:registry=${url}`)
|
exec(`npm set @capire:registry=http://localhost:${port}`)
|
||||||
console.log (`${scope} registry listening on ${url}`)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
const _exit = ()=>{
|
const _exit = ()=>{
|
||||||
server.close()
|
console.log ('\nnpm conf rm @capire:registry')
|
||||||
exec(`npm conf rm "${scope}:registry"`, ()=> { process.exit() })
|
exec('npm conf rm @capire:registry')
|
||||||
|
exec('rm *.tgz')
|
||||||
|
process.exit()
|
||||||
}
|
}
|
||||||
|
|
||||||
process.on ('SIGTERM',_exit)
|
process.on ('SIGTERM',_exit)
|
||||||
process.on ('SIGHUP',_exit)
|
process.on ('SIGHUP',_exit)
|
||||||
process.on ('SIGINT',_exit)
|
process.on ('SIGINT',_exit)
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
<td>{{ book.author }}</td>
|
<td>{{ book.author }}</td>
|
||||||
<td>{{ book.genre.name }}</td>
|
<td>{{ book.genre.name }}</td>
|
||||||
<td class="rating-stars">
|
<td class="rating-stars">
|
||||||
{{ ('★'.repeat(Math.round(book.rating))+'☆☆☆☆☆').slice(0,5) }} ({{ book.numberOfReviews }})
|
{{ ('★'.repeat(Math.round(book.rating))+'☆☆☆☆☆').slice(0,5) }}
|
||||||
</td>
|
</td>
|
||||||
<td>{{ book.currency && book.currency.symbol }} {{ book.price }}</td>
|
<td>{{ book.currency && book.currency.symbol }} {{ book.price }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -1,2 +1 @@
|
|||||||
const { CatalogService } = require('./srv/cat-service')
|
exports.CatalogService = require('./srv/cat-service')
|
||||||
module.exports = { CatalogService }
|
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
const cds = require('@sap/cds')
|
const cds = require('@sap/cds')
|
||||||
|
const { Books } = cds.entities ('sap.capire.bookshop')
|
||||||
|
|
||||||
class CatalogService extends cds.ApplicationService { init(){
|
class CatalogService extends cds.ApplicationService { init(){
|
||||||
|
|
||||||
const { Books } = cds.entities ('sap.capire.bookshop')
|
|
||||||
|
|
||||||
// Reduce stock of ordered books if available stock suffices
|
// Reduce stock of ordered books if available stock suffices
|
||||||
this.on ('submitOrder', async req => {
|
this.on ('submitOrder', async req => {
|
||||||
const {book,quantity} = req.data
|
const {book,quantity} = req.data
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@capire/bookstore",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"dependencies": {
|
|
||||||
"@capire/bookshop": "*",
|
|
||||||
"@capire/reviews": "*",
|
|
||||||
"@capire/orders": "*",
|
|
||||||
"@capire/common": "*",
|
|
||||||
"@sap/cds": "^5",
|
|
||||||
"express": "^4.17.1"
|
|
||||||
},
|
|
||||||
"cds": {
|
|
||||||
"requires": {
|
|
||||||
"ReviewsService": {
|
|
||||||
"kind": "odata",
|
|
||||||
"model": "@capire/reviews"
|
|
||||||
},
|
|
||||||
"OrdersService": {
|
|
||||||
"kind": "odata",
|
|
||||||
"model": "@capire/orders"
|
|
||||||
},
|
|
||||||
"messaging": {
|
|
||||||
"[development]": { "kind": "file-based-messaging" },
|
|
||||||
"[hybrid]": { "kind": "enterprise-messaging-shared" },
|
|
||||||
"[production]": { "kind": "enterprise-messaging" }
|
|
||||||
},
|
|
||||||
"db": {
|
|
||||||
"kind": "sql",
|
|
||||||
"[development]": {
|
|
||||||
"model": "db/sqlite"
|
|
||||||
},
|
|
||||||
"[production]": {
|
|
||||||
"model": "db/hana"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"log": { "service": true }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
const cds = require ('@sap/cds')
|
|
||||||
|
|
||||||
// Add mashup logic
|
|
||||||
cds.once('served', require('./srv/mashup'))
|
|
||||||
|
|
||||||
// Add routes to UIs from imported packages
|
|
||||||
cds.once('bootstrap',(app)=>{
|
|
||||||
app.serve ('/bookshop') .from ('@capire/bookshop','app/vue')
|
|
||||||
app.serve ('/reviews') .from ('@capire/reviews','app/vue')
|
|
||||||
app.serve ('/orders') .from('@capire/orders','app/orders')
|
|
||||||
})
|
|
||||||
|
|
||||||
// Add Swagger UI
|
|
||||||
require('./srv/swagger-ui')
|
|
||||||
|
|
||||||
// Returning cds.server
|
|
||||||
module.exports = cds.server
|
|
||||||
|
|
||||||
// For didactic reasons in capire
|
|
||||||
const { ReviewsService, OrdersService } = cds.requires
|
|
||||||
if (!ReviewsService.credentials && !OrdersService.credentials) cds.requires.messaging = false
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
|
|
||||||
// -----------------------------------------------------------------------
|
|
||||||
// Adding Swagger UI - see https://cap.cloud.sap/docs/advanced/openapi
|
|
||||||
const cds = require ('@sap/cds')
|
|
||||||
try {
|
|
||||||
const cds_swagger = require ('cds-swagger-ui-express')
|
|
||||||
cds.once ('bootstrap', app => app.use (cds_swagger()) )
|
|
||||||
} catch (err) {
|
|
||||||
if (err.code !== 'MODULE_NOT_FOUND') throw err
|
|
||||||
}
|
|
||||||
2
fiori/.env
Normal file
2
fiori/.env
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# cds.requires.messaging.kind = file-based-messaging
|
||||||
|
PORT = 4004
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using { AdminService } from '@capire/bookshop';
|
using { AdminService } from '../../db/schema';
|
||||||
using from '../common'; // to help UI linter get the complete annotations
|
using from '../common'; // to help UI linter get the complete annotations
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
|||||||
3
fiori/app/bookshop.html
Normal file
3
fiori/app/bookshop.html
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<head>
|
||||||
|
<meta http-equiv="refresh" content="0;url=bookshop/index.html">
|
||||||
|
</head>
|
||||||
3
fiori/app/reviews.html
Normal file
3
fiori/app/reviews.html
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<head>
|
||||||
|
<meta http-equiv="refresh" content="0;url=reviews/index.html">
|
||||||
|
</head>
|
||||||
@@ -6,4 +6,7 @@ using from './admin/fiori-service';
|
|||||||
using from './browse/fiori-service';
|
using from './browse/fiori-service';
|
||||||
using from './common';
|
using from './common';
|
||||||
|
|
||||||
using from '@capire/bookstore/srv/mashup';
|
using from '@capire/common';
|
||||||
|
|
||||||
|
// only works in case of embedded orders service
|
||||||
|
using from '@capire/orders/app/orders/fiori-service';
|
||||||
|
|||||||
@@ -2,7 +2,10 @@
|
|||||||
"name": "@capire/fiori",
|
"name": "@capire/fiori",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@capire/bookstore": "*",
|
"@capire/bookshop": "*",
|
||||||
|
"@capire/reviews": "*",
|
||||||
|
"@capire/orders": "*",
|
||||||
|
"@capire/common": "*",
|
||||||
"@sap/cds": "^5",
|
"@sap/cds": "^5",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"passport": "^0.4.1"
|
"passport": "^0.4.1"
|
||||||
@@ -12,6 +15,9 @@
|
|||||||
"watch": "cds watch"
|
"watch": "cds watch"
|
||||||
},
|
},
|
||||||
"cds": {
|
"cds": {
|
||||||
|
"hana": {
|
||||||
|
"deploy-format": "hdbtable"
|
||||||
|
},
|
||||||
"requires": {
|
"requires": {
|
||||||
"auth": {
|
"auth": {
|
||||||
"strategy": "dummy"
|
"strategy": "dummy"
|
||||||
@@ -24,13 +30,14 @@
|
|||||||
"kind": "odata",
|
"kind": "odata",
|
||||||
"model": "@capire/orders"
|
"model": "@capire/orders"
|
||||||
},
|
},
|
||||||
"messaging": {
|
"db": {
|
||||||
"[production]": { "kind": "enterprise-messaging" },
|
"kind": "sql",
|
||||||
"[development]": { "kind": "file-based-messaging" },
|
"[development]": {
|
||||||
"[hybrid!]": { "kind": "enterprise-messaging-shared" }
|
"model": "db/sqlite"
|
||||||
},
|
},
|
||||||
"hana": {
|
"[production]": {
|
||||||
"deploy-format": "hdbtable"
|
"model": "db/hana"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1,25 @@
|
|||||||
module.exports = require('@capire/bookstore/server.js')
|
const cds = require ('@sap/cds')
|
||||||
|
module.exports = cds.server
|
||||||
|
|
||||||
|
cds.once('bootstrap',(app)=>{
|
||||||
|
app.use ('/orders/webapp', _from('@capire/orders/app/orders/webapp/manifest.json'))
|
||||||
|
app.use ('/bookshop', _from('@capire/bookshop/app/vue/index.html'))
|
||||||
|
app.use ('/reviews', _from('@capire/reviews/app/vue/index.html'))
|
||||||
|
})
|
||||||
|
|
||||||
|
cds.once('served', require('./srv/mashup'))
|
||||||
|
|
||||||
|
// Swagger UI - see https://cap.cloud.sap/docs/advanced/openapi
|
||||||
|
try {
|
||||||
|
const cds_swagger = require ('cds-swagger-ui-express')
|
||||||
|
cds.once ('bootstrap', app => app.use (cds_swagger()) )
|
||||||
|
} catch (err) {
|
||||||
|
if (err.code !== 'MODULE_NOT_FOUND') throw err
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Helper for serving static content from npm-installed packages
|
||||||
|
const {static} = require('express')
|
||||||
|
const {dirname} = require('path')
|
||||||
|
const _from = target => static (dirname (require.resolve(target)))
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
//
|
//
|
||||||
// Enhancing bookshop with Reviews and Orders provided through
|
// Mashing up imported models...
|
||||||
// respective reuse packages and services
|
|
||||||
//
|
//
|
||||||
|
|
||||||
using { sap.capire.bookshop.Books } from '@capire/bookshop';
|
using { sap.capire.bookshop.Books } from '@capire/bookshop';
|
||||||
@@ -9,22 +8,18 @@ using { sap.capire.bookshop.Books } from '@capire/bookshop';
|
|||||||
//
|
//
|
||||||
// Extend Books with access to Reviews and average ratings
|
// Extend Books with access to Reviews and average ratings
|
||||||
//
|
//
|
||||||
|
|
||||||
using { ReviewsService.Reviews } from '@capire/reviews';
|
using { ReviewsService.Reviews } from '@capire/reviews';
|
||||||
extend Books with {
|
extend Books with {
|
||||||
reviews : Composition of many Reviews on reviews.subject = $self.ID;
|
reviews : Composition of many Reviews on reviews.subject = $self.ID;
|
||||||
rating : Decimal;
|
rating : Decimal;
|
||||||
numberOfReviews : Integer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// Extend Orders with Books as Products
|
// Extend Orders with Books as Products
|
||||||
//
|
//
|
||||||
|
|
||||||
using { sap.capire.orders.Orders_Items } from '@capire/orders';
|
using { sap.capire.orders.Orders_Items } from '@capire/orders';
|
||||||
extend Orders_Items with {
|
extend Orders_Items with {
|
||||||
book : Association to Books on product.ID = book.ID
|
book : Association to Books on product.ID = book.ID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Add orders fiori app (in case of embedded orders service)
|
|
||||||
using from '@capire/orders/app/fiori';
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
//
|
//
|
||||||
// Mashing up bookshop services with required services...
|
// Mashing up provided and required services...
|
||||||
//
|
//
|
||||||
module.exports = async()=>{ // called by server.js
|
module.exports = async()=>{ // called by server.js
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ module.exports = async()=>{ // called by server.js
|
|||||||
CatalogService.prepend (srv => srv.on ('READ', 'Books/reviews', (req) => {
|
CatalogService.prepend (srv => srv.on ('READ', 'Books/reviews', (req) => {
|
||||||
console.debug ('> delegating request to ReviewsService')
|
console.debug ('> delegating request to ReviewsService')
|
||||||
const [id] = req.params, { columns, limit } = req.query.SELECT
|
const [id] = req.params, { columns, limit } = req.query.SELECT
|
||||||
return ReviewsService.read ('Reviews',columns).limit(limit).where({subject:String(id)})
|
return ReviewsService.tx(req).read ('Reviews',columns).limit(limit).where({subject:String(id)})
|
||||||
}))
|
}))
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -37,12 +37,13 @@ module.exports = async()=>{ // called by server.js
|
|||||||
})
|
})
|
||||||
|
|
||||||
//
|
//
|
||||||
// Update Books' average ratings when ReviewsService signals updated reviews
|
// Update Books' average ratings when ReviewsService signals updatd reviews
|
||||||
//
|
//
|
||||||
ReviewsService.on ('reviewed', (msg) => {
|
ReviewsService.on ('reviewed', (msg) => {
|
||||||
console.debug ('> received:', msg.event, msg.data)
|
console.debug ('> received:', msg.event, msg.data)
|
||||||
const { subject, count, rating } = msg.data
|
const { subject, rating } = msg.data
|
||||||
return UPDATE(Books,subject).with({ numberOfReviews:count, rating })
|
return UPDATE(Books,subject).with({rating})
|
||||||
|
// ^ Note: the framework will execute this and take care for db.tx
|
||||||
})
|
})
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
ID;createdAt;createdBy;buyer;OrderNo;currency_code
|
ID;createdAt;createdBy;buyer;OrderNo;currency_code;status_code
|
||||||
7e2f2640-6866-4dcf-8f4d-3027aa831cad;2019-01-31;john.doe@test.com;john.doe@test.com;1;EUR
|
7e2f2640-6866-4dcf-8f4d-3027aa831cad;2019-01-31;john.doe@test.com;john.doe@test.com;1;EUR;O
|
||||||
64e718c9-ff99-47f1-8ca3-950c850777d4;2019-01-30;jane.doe@test.com;jane.doe@test.com;2;EUR
|
64e718c9-ff99-47f1-8ca3-950c850777d4;2019-01-30;jane.doe@test.com;jane.doe@test.com;2;EUR;C
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
@bookshop = http://localhost:4004
|
@bookshop = http://localhost:4004
|
||||||
@reviews-service = {{bookshop}}/reviews
|
@reviews-service = {{bookshop}}/reviews
|
||||||
# Uncomment this when running a separate reviews service
|
# Uncomment this when running a separate reviews service
|
||||||
# @reviews-service = http://localhost:4005/reviews
|
@reviews-service = http://localhost:4005/reviews
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
PORT = 4007
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
book_ID;number;title
|
|
||||||
201;1;Chapter 1
|
|
||||||
201;2;Chapter 2
|
|
||||||
201;3;Chapter 3
|
|
||||||
|
@@ -1,26 +0,0 @@
|
|||||||
using {
|
|
||||||
cuid,
|
|
||||||
managed
|
|
||||||
} from '@sap/cds/common';
|
|
||||||
using {sap.capire.bookshop} from '@capire/bookshop';
|
|
||||||
|
|
||||||
namespace sap.capire.graphql;
|
|
||||||
|
|
||||||
extend bookshop.Books with {
|
|
||||||
chapters : Composition of many Chapters
|
|
||||||
on chapters.book = $self;
|
|
||||||
}
|
|
||||||
|
|
||||||
entity Chapters : managed {
|
|
||||||
key book : Association to bookshop.Books;
|
|
||||||
key number : Integer;
|
|
||||||
title : String;
|
|
||||||
}
|
|
||||||
|
|
||||||
entity Orders : cuid, managed {
|
|
||||||
@mandatory
|
|
||||||
book : Association to bookshop.Books;
|
|
||||||
@mandatory
|
|
||||||
@assert.range : [ 1, 5 ]
|
|
||||||
quantity : Integer;
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
# GraphQL
|
|
||||||
GET http://localhost:4007/graphql?query={BookshopService{Books{title,author{name},chapters{number,title}}}}
|
|
||||||
|
|
||||||
###
|
|
||||||
|
|
||||||
# OData
|
|
||||||
GET http://localhost:4007/bookshop/Books?$select=title&$expand=author($select=name),chapters($select=number,title)
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
1. open `http://localhost:4007/graphql`
|
|
||||||
2. paste into left field:
|
|
||||||
```graphql
|
|
||||||
{
|
|
||||||
BookshopService {
|
|
||||||
Books {
|
|
||||||
title
|
|
||||||
chapters {
|
|
||||||
number
|
|
||||||
title
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
3. press play button
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@capire/graphql",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"dependencies": {
|
|
||||||
"@capire/bookshop": "*",
|
|
||||||
"@graphql-tools/schema": "^8.3.1",
|
|
||||||
"@sap/cds": "^5.6",
|
|
||||||
"express-graphql": "^0.12.0",
|
|
||||||
"graphql": "^16.0.1"
|
|
||||||
},
|
|
||||||
"cds": {
|
|
||||||
"features": {
|
|
||||||
"graphql": true
|
|
||||||
},
|
|
||||||
"requires": {
|
|
||||||
"auth": {
|
|
||||||
"kind": "dummy-auth"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
using {
|
|
||||||
sap.capire.bookshop,
|
|
||||||
sap.capire.graphql
|
|
||||||
} from '../db/schema';
|
|
||||||
|
|
||||||
service BookshopService {
|
|
||||||
entity Books as projection on bookshop.Books;
|
|
||||||
entity Authors as projection on bookshop.Authors;
|
|
||||||
entity Chapters as projection on graphql.Chapters;
|
|
||||||
entity Orders as projection on graphql.Orders;
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
module.exports = function() {
|
|
||||||
const { Orders, Books } = this.entities
|
|
||||||
|
|
||||||
this.before('CREATE', Orders, async function(req) {
|
|
||||||
const { book_ID, quantity } = req.data
|
|
||||||
|
|
||||||
// reduce the stock, if enough are available, else reject the order
|
|
||||||
const applied = await UPDATE(Books, book_ID).set({ stock: { '-=': quantity } }).where({ stock: { '>=': quantity }})
|
|
||||||
if (!applied) req.reject(400, `Sorry, ${quantity} are not in stock`)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
5
orders/app/index.cds
Normal file
5
orders/app/index.cds
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
/*
|
||||||
|
This model controls what gets served to Fiori frontends...
|
||||||
|
*/
|
||||||
|
|
||||||
|
using from './orders/fiori-service';
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
using { OrdersService } from '../srv/orders-service';
|
using { OrdersService } from '../../srv/orders-service';
|
||||||
|
|
||||||
|
|
||||||
@odata.draft.enabled
|
@odata.draft.enabled
|
||||||
@@ -45,7 +45,8 @@ annotate OrdersService.Orders with @(
|
|||||||
],
|
],
|
||||||
FieldGroup#Details: {
|
FieldGroup#Details: {
|
||||||
Data: [
|
Data: [
|
||||||
{Value: currency.code, Label:'Currency'}
|
{Value: currency.code, Label:'Currency'},
|
||||||
|
{Value: status.code, Label:'Status'},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
FieldGroup#Created: {
|
FieldGroup#Created: {
|
||||||
@@ -66,6 +67,9 @@ annotate OrdersService.Orders with @(
|
|||||||
createdBy @UI.HiddenFilter:false;
|
createdBy @UI.HiddenFilter:false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
annotate OrdersService.OrderStatus with {
|
||||||
|
code @Common: { Text: name, TextArrangement: #TextOnly };
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
annotate OrdersService.Orders_Items with @(
|
annotate OrdersService.Orders_Items with @(
|
||||||
4
orders/db/data/sap.capire.orders-OrderStatus.csv
Normal file
4
orders/db/data/sap.capire.orders-OrderStatus.csv
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
code;name;descr
|
||||||
|
O;Open;Order is open
|
||||||
|
P;In Process;Order is about to be processed
|
||||||
|
C;Closed;Order is closed
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
using { Currency, User, managed, cuid } from '@sap/cds/common';
|
using { Currency, User, managed, cuid, sap.common.CodeList } from '@sap/cds/common';
|
||||||
namespace sap.capire.orders;
|
namespace sap.capire.orders;
|
||||||
|
|
||||||
entity Orders : cuid, managed {
|
entity Orders : cuid, managed {
|
||||||
@@ -6,8 +6,12 @@ entity Orders : cuid, managed {
|
|||||||
Items : Composition of many Orders_Items on Items.up_ = $self;
|
Items : Composition of many Orders_Items on Items.up_ = $self;
|
||||||
buyer : User;
|
buyer : User;
|
||||||
currency : Currency;
|
currency : Currency;
|
||||||
|
status : Association to OrderStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@cds.persistence.data.kind: 'config'
|
||||||
|
entity OrderStatus : CodeList { key code: String(1) }
|
||||||
|
|
||||||
entity Orders_Items {
|
entity Orders_Items {
|
||||||
key ID : UUID;
|
key ID : UUID;
|
||||||
up_ : Association to Orders;
|
up_ : Association to Orders;
|
||||||
|
|||||||
2634
package-lock.json
generated
Normal file
2634
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -5,16 +5,14 @@
|
|||||||
"repository": "https://github.com/sap-samples/cloud-cap-samples.git",
|
"repository": "https://github.com/sap-samples/cloud-cap-samples.git",
|
||||||
"author": "daniel.hutzel@sap.com",
|
"author": "daniel.hutzel@sap.com",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@capire/bookstore": "./bookstore",
|
|
||||||
"@capire/bookshop": "./bookshop",
|
"@capire/bookshop": "./bookshop",
|
||||||
"@capire/common": "./common",
|
"@capire/common": "./common",
|
||||||
"@capire/fiori": "./fiori",
|
"@capire/fiori": "./fiori",
|
||||||
"@capire/graphql": "./graphql",
|
|
||||||
"@capire/hello": "./hello",
|
"@capire/hello": "./hello",
|
||||||
"@capire/media": "./media",
|
"@capire/media": "./media",
|
||||||
"@capire/orders": "./orders",
|
"@capire/orders": "./orders",
|
||||||
"@capire/reviews": "./reviews",
|
"@capire/reviews": "./reviews",
|
||||||
"@sap/cds": "^5.6"
|
"@sap/cds": "^5.5.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"chai": "^4.3.4",
|
"chai": "^4.3.4",
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
# cds.requires.messaging.kind = file-based-messaging
|
cds.requires.messaging.kind = file-based-messaging
|
||||||
PORT = 4005
|
PORT = 4005
|
||||||
@@ -10,14 +10,15 @@
|
|||||||
"@sap/cds": "^5",
|
"@sap/cds": "^5",
|
||||||
"express": "^4.17.1"
|
"express": "^4.17.1"
|
||||||
},
|
},
|
||||||
|
"scripts": {
|
||||||
|
"reviews-service": "cds watch",
|
||||||
|
"books-reviewed": "cds watch ../reviewed"
|
||||||
|
},
|
||||||
"cds": {
|
"cds": {
|
||||||
"requires": {
|
"requires": {
|
||||||
"messaging": {
|
"db": {
|
||||||
"[development]": { "kind": "file-based-messaging" },
|
"kind": "sql"
|
||||||
"[hybrid]": { "kind": "enterprise-messaging-shared" },
|
}
|
||||||
"[production]": { "kind": "enterprise-messaging" }
|
|
||||||
},
|
|
||||||
"db": { "kind": "sql" }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -10,8 +10,7 @@ service ReviewsService {
|
|||||||
// Async API
|
// Async API
|
||||||
event reviewed : {
|
event reviewed : {
|
||||||
subject: type of Reviews:subject;
|
subject: type of Reviews:subject;
|
||||||
count : Integer;
|
rating: Decimal(2,1)
|
||||||
rating : Decimal;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Input validation
|
// Input validation
|
||||||
|
|||||||
@@ -12,11 +12,11 @@ module.exports = cds.service.impl (function(){
|
|||||||
// Emit an event to inform subscribers about new avg ratings for reviewed subjects
|
// Emit an event to inform subscribers about new avg ratings for reviewed subjects
|
||||||
this.after (['CREATE','UPDATE','DELETE'], 'Reviews', async function(_,req) {
|
this.after (['CREATE','UPDATE','DELETE'], 'Reviews', async function(_,req) {
|
||||||
const {subject} = req.data
|
const {subject} = req.data
|
||||||
const { count, rating } = await cds.tx(req) .run (
|
const {rating} = await cds.tx(req) .run (
|
||||||
SELECT.one `round(avg(rating),2) as rating, count(*) as count` .from (Reviews) .where ({subject})
|
SELECT.one (['round(avg(rating),2) as rating']) .from (Reviews) .where ({subject})
|
||||||
)
|
)
|
||||||
global.it || console.log ('< emitting:', 'reviewed', { subject, count, rating })
|
global.it || console.log ('< emitting:', 'reviewed', { subject, rating })
|
||||||
await this.emit ('reviewed', { subject, count, rating })
|
await this.emit ('reviewed', { subject, rating })
|
||||||
})
|
})
|
||||||
|
|
||||||
// Increment counter for reviews considered helpful
|
// Increment counter for reviews considered helpful
|
||||||
|
|||||||
13
samples.md
13
samples.md
@@ -51,27 +51,20 @@ Each sub directory essentially is an individual npm package arranged in an [all-
|
|||||||
- As well as managed data, input validations, and authorization
|
- As well as managed data, input validations, and authorization
|
||||||
|
|
||||||
|
|
||||||
## [@capire/bookstore](bookstore)
|
## [@capire/fiori](fiori)
|
||||||
|
|
||||||
- A [composite app, reusing and combining](https://cap.cloud.sap/docs/guides/verticalize) these packages:
|
- A [composite app, reusing and combining](https://cap.cloud.sap/docs/guides/verticalize) these packages:
|
||||||
- [@capire/bookshop](bookshop)
|
- [@capire/bookshop](bookshop)
|
||||||
- [@capire/reviews](reviews)
|
- [@capire/reviews](reviews)
|
||||||
- [@capire/orders](orders)
|
- [@capire/orders](orders)
|
||||||
- [@capire/common](common)
|
- [@capire/common](common)
|
||||||
- [The Vue.js app](bookshop/app/vue) imported from bookshop is served as well
|
|
||||||
- [The Vue.js app](reviews/app/vue) imported from reviews is served as well
|
|
||||||
- [The Fiori app](orders/app) imported from orders is served as well
|
|
||||||
- [OpenAPI export + Swagger UI](https://cap.cloud.sap/docs/advanced/openapi)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [@capire/fiori](fiori)
|
|
||||||
|
|
||||||
- [Adds an SAP Fiori elements application](https://cap.cloud.sap/docs/guides/fiori/) to bookshop, thereby introducing to:
|
- [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)
|
||||||
- Support for [Value Helps](https://cap.cloud.sap/docs/guides/fiori#value-help)
|
- Support for [Value Helps](https://cap.cloud.sap/docs/guides/fiori#value-help)
|
||||||
- Serving SAP Fiori apps locally
|
- Serving SAP Fiori apps locally
|
||||||
|
- [The Vue.js app](bookshop/app/vue) imported from bookshop is served as well
|
||||||
|
- [OpenAPI export + Swagger UI](https://cap.cloud.sap/docs/advanced/openapi)
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
|
|||||||
@@ -86,15 +86,6 @@ describe('cds.ql → cqn', () => {
|
|||||||
.to.eql(SELECT.from(Foo,{ID:11}))
|
.to.eql(SELECT.from(Foo,{ID:11}))
|
||||||
.to.eql(SELECT.from(Foo).byKey(11))
|
.to.eql(SELECT.from(Foo).byKey(11))
|
||||||
.to.eql(SELECT.from(Foo).byKey({ID:11}))
|
.to.eql(SELECT.from(Foo).byKey({ID:11}))
|
||||||
if (cds.version >= '5.6.0') {
|
|
||||||
expect.one(cqn)
|
|
||||||
.to.eql({
|
|
||||||
SELECT: {
|
|
||||||
one: true,
|
|
||||||
from: { ref: [{ id: 'Foo', where: [{ ref: ['ID'] }, '=', { val: 11 }] }] },
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
expect.one(cqn)
|
expect.one(cqn)
|
||||||
.to.eql({
|
.to.eql({
|
||||||
SELECT: {
|
SELECT: {
|
||||||
@@ -103,7 +94,6 @@ describe('cds.ql → cqn', () => {
|
|||||||
where: [{ ref: ['ID'] }, '=', { val: 11 }],
|
where: [{ ref: ['ID'] }, '=', { val: 11 }],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -141,17 +131,6 @@ describe('cds.ql → cqn', () => {
|
|||||||
// Test combination with key as second argument to .from
|
// Test combination with key as second argument to .from
|
||||||
expect(cqn = SELECT.from(Foo, 11, ['a']))
|
expect(cqn = SELECT.from(Foo, 11, ['a']))
|
||||||
.to.eql(SELECT.from(Foo, 11, foo => foo.a))
|
.to.eql(SELECT.from(Foo, 11, foo => foo.a))
|
||||||
|
|
||||||
if (cds.version >= '5.6.0') {
|
|
||||||
expect.one(cqn)
|
|
||||||
.to.eql({
|
|
||||||
SELECT: {
|
|
||||||
one: true,
|
|
||||||
from: { ref: [{ id: 'Foo', where: [{ ref: ['ID'] }, '=', { val: 11 }]}] },
|
|
||||||
columns: [{ ref: ['a'] }]
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
expect.one(cqn)
|
expect.one(cqn)
|
||||||
.to.eql({
|
.to.eql({
|
||||||
SELECT: {
|
SELECT: {
|
||||||
@@ -161,7 +140,6 @@ describe('cds.ql → cqn', () => {
|
|||||||
where: [{ ref: ['ID'] }, '=', { val: 11 }],
|
where: [{ ref: ['ID'] }, '=', { val: 11 }],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -573,31 +551,21 @@ describe('cds.ql → cqn', () => {
|
|||||||
|
|
||||||
describe(`UPDATE...`, () => {
|
describe(`UPDATE...`, () => {
|
||||||
test('entity (..., <key>)', () => {
|
test('entity (..., <key>)', () => {
|
||||||
const cqnWhere = {
|
|
||||||
UPDATE: {
|
|
||||||
entity: 'capire.bookshop.Books',
|
|
||||||
where: [{ ref: ['ID'] }, '=', { val: 4711 }],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
expect(UPDATE(Books).where({ ID: 4711 }))
|
|
||||||
.to.eql(UPDATE(Books).where(`ID=`, 4711))
|
|
||||||
.to.eql(cqnWhere)
|
|
||||||
|
|
||||||
const cqnKey = (cds.version >= '5.6.0') ?
|
|
||||||
{
|
|
||||||
UPDATE: {
|
|
||||||
entity: { ref: [{ id: 'capire.bookshop.Books', where: [{ ref: ['ID'] }, '=', { val: 4711 }] }] }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
: cqnWhere
|
|
||||||
expect(UPDATE(Books, 4711))
|
expect(UPDATE(Books, 4711))
|
||||||
.to.eql(UPDATE(Books, { ID: 4711 }))
|
.to.eql(UPDATE(Books, { ID: 4711 }))
|
||||||
.to.eql(UPDATE(Books).byKey(4711))
|
.to.eql(UPDATE(Books).byKey(4711))
|
||||||
.to.eql(UPDATE(Books).byKey({ ID: 4711 }))
|
.to.eql(UPDATE(Books).byKey({ ID: 4711 }))
|
||||||
|
.to.eql(UPDATE(Books).where({ ID: 4711 }))
|
||||||
|
.to.eql(UPDATE(Books).where(`ID=`, 4711))
|
||||||
.to.eql(UPDATE.entity(Books, 4711))
|
.to.eql(UPDATE.entity(Books, 4711))
|
||||||
.to.eql(UPDATE.entity(Books, { ID: 4711 }))
|
.to.eql(UPDATE.entity(Books, { ID: 4711 }))
|
||||||
// etc...
|
// etc...
|
||||||
.to.eql(cqnKey)
|
.to.eql({
|
||||||
|
UPDATE: {
|
||||||
|
entity: 'capire.bookshop.Books',
|
||||||
|
where: [{ ref: ['ID'] }, '=', { val: 4711 }],
|
||||||
|
},
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -648,29 +616,20 @@ describe('cds.ql → cqn', () => {
|
|||||||
|
|
||||||
describe(`DELETE...`, () => {
|
describe(`DELETE...`, () => {
|
||||||
test('from (..., <key>)', () => {
|
test('from (..., <key>)', () => {
|
||||||
const cqnWhere = {
|
|
||||||
DELETE: {
|
|
||||||
from: 'capire.bookshop.Books',
|
|
||||||
where: [{ ref: ['ID'] }, '=', { val: 4711 }],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
expect(DELETE.from(Books).where({ ID: 4711 }))
|
|
||||||
.to.eql(DELETE.from(Books).where(`ID=`, 4711))
|
|
||||||
.to.eql(cqnWhere)
|
|
||||||
const cqnKey = (cds.version >= '5.6.0') ?
|
|
||||||
{
|
|
||||||
DELETE: {
|
|
||||||
from: { ref: [{ id: 'capire.bookshop.Books', where: [{ ref: ['ID'] }, '=', { val: 4711 }]}] }
|
|
||||||
},
|
|
||||||
} : cqnWhere
|
|
||||||
|
|
||||||
expect(DELETE(Books, 4711))
|
expect(DELETE(Books, 4711))
|
||||||
.to.eql(DELETE(Books, { ID: 4711 }))
|
.to.eql(DELETE(Books, { ID: 4711 }))
|
||||||
.to.eql(DELETE.from(Books, 4711))
|
.to.eql(DELETE.from(Books, 4711))
|
||||||
.to.eql(DELETE.from(Books, { ID: 4711 }))
|
.to.eql(DELETE.from(Books, { ID: 4711 }))
|
||||||
.to.eql(DELETE.from(Books).byKey(4711))
|
.to.eql(DELETE.from(Books).byKey(4711))
|
||||||
.to.eql(DELETE.from(Books).byKey({ ID: 4711 }))
|
.to.eql(DELETE.from(Books).byKey({ ID: 4711 }))
|
||||||
.to.eql(cqnKey)
|
.to.eql(DELETE.from(Books).where({ ID: 4711 }))
|
||||||
|
.to.eql(DELETE.from(Books).where(`ID=`, 4711))
|
||||||
|
.to.eql({
|
||||||
|
DELETE: {
|
||||||
|
from: 'capire.bookshop.Books',
|
||||||
|
where: [{ ref: ['ID'] }, '=', { val: 4711 }],
|
||||||
|
},
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
test('/w plain SQL', () => {
|
test('/w plain SQL', () => {
|
||||||
|
|||||||
@@ -60,11 +60,11 @@ describe('Messaging', ()=>{
|
|||||||
expect(M).equals(N)
|
expect(M).equals(N)
|
||||||
expect(received.length).equals(N)
|
expect(received.length).equals(N)
|
||||||
expect(received.map(m=>m.data)).to.deep.equal([
|
expect(received.map(m=>m.data)).to.deep.equal([
|
||||||
{ count: 1, subject: '201', rating: 1 },
|
{ subject: '201', rating: 1 },
|
||||||
{ count: 2, subject: '201', rating: 1.5 },
|
{ subject: '201', rating: 1.5 },
|
||||||
{ count: 3, subject: '201', rating: 2 },
|
{ subject: '201', rating: 2 },
|
||||||
{ count: 4, subject: '201', rating: 2.5 },
|
{ subject: '201', rating: 2.5 },
|
||||||
{ count: 5, subject: '201', rating: 3 },
|
{ subject: '201', rating: 3 },
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,54 +0,0 @@
|
|||||||
|
|
||||||
const { fork } = require('child_process')
|
|
||||||
const { resolve } = require('path')
|
|
||||||
const Axios = require('axios')
|
|
||||||
const verbose = process.env.CDS_TEST_VERBOSE
|
|
||||||
// ||true
|
|
||||||
|
|
||||||
describe('Local NPM registry', () => {
|
|
||||||
let registry
|
|
||||||
let axios
|
|
||||||
const cwd = resolve(__dirname, '..')
|
|
||||||
|
|
||||||
beforeAll(async ()=> {
|
|
||||||
const env = Object.assign(process.env, {PORT:'0'})
|
|
||||||
const res = await exec (resolve(cwd, '.registry/server.js'), {cwd, stdio: 'pipe', env})
|
|
||||||
registry = res.cp
|
|
||||||
axios = Axios.default.create ({ baseURL: res.url, validateStatus: (status)=>status<500 })
|
|
||||||
})
|
|
||||||
|
|
||||||
afterAll(() => { registry.kill() })
|
|
||||||
|
|
||||||
for (const mod of ['bookshop','fiori','orders','reviews']) {
|
|
||||||
it(`should serve ${mod}`, async () => {
|
|
||||||
const resp = await axios.get(`/@capire/${mod}`)
|
|
||||||
expect(resp.data).toMatchObject({name: `@capire/${mod}`, versions:{}})
|
|
||||||
const versions = Object.values(resp.data.versions)
|
|
||||||
await axios.get(versions[0].dist.tarball)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
it(`should return 404 for unknown packages`, async () => {
|
|
||||||
let resp = await axios.get(`/@capire/foo`)
|
|
||||||
expect(resp.status).toEqual(404)
|
|
||||||
resp = await axios.get(`/foo`)
|
|
||||||
expect(resp.status).toEqual(404)
|
|
||||||
})
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
function exec (script, opts) {
|
|
||||||
return new Promise((resolve, reject)=> {
|
|
||||||
const cp = fork (script, [], opts)
|
|
||||||
.on('error', err => reject(new Error(err)))
|
|
||||||
cp.stdout.on('data', chunk => {
|
|
||||||
if (verbose) console.log(chunk.toString())
|
|
||||||
if (chunk.toString().match(/listening.*(http:.*:\d+)/i)) {
|
|
||||||
resolve({cp, url:RegExp.$1})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
cp.stderr.on('data', chunk => {
|
|
||||||
if (verbose) console.error(chunk.toString())
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user