Compare commits

..

2 Commits

Author SHA1 Message Date
Jörg Mann
55e4c92a60 renaming 2024-05-03 14:23:10 +02:00
Jörg Mann
7738f85036 refactor(lint): simplify eslint config 2024-05-03 13:53:43 +02:00
65 changed files with 1278 additions and 1260 deletions

View File

@@ -15,6 +15,3 @@ updates:
- dependency-name: "chai"
# chai 5 doesn't work atm w/ cds.test, TODO fix that in cds.test
versions: ["5.x"]
- dependency-name: "chai-as-promised"
# chai-as-promised 8 doesn't work atm w/ cds.test, TODO fix that in cds.test
versions: ["8.x"]

View File

@@ -15,7 +15,7 @@ jobs:
strategy:
matrix:
node-version: [22.x, 20.x, 18.x]
node-version: [20.x, 18.x]
steps:
- uses: actions/checkout@v4
@@ -33,6 +33,6 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22.x
node-version: 20.x
- run: npm ci
- run: npm run lint

10
.vscode/settings.json vendored
View File

@@ -13,5 +13,13 @@
"**/cds/lib/req/cds-context.js",
"**/odata-v4/okra/**"
]
}
},
"eslint.probe": [
"cds",
"csn",
"csv",
"csv (semicolon)",
"tsv",
"tab"
]
}

View File

@@ -13,7 +13,7 @@ Find here a collection of samples for the [SAP Cloud Application Programming Mod
### Preliminaries
1. Ensure you have the latest LTS version of Node.js installed (see [Getting Started](https://cap.cloud.sap/docs/get-started/jumpstart))
1. Ensure you have the latest LTS version of Node.js installed (see [Getting Started](https://cap.cloud.sap/docs/get-started/))
2. Install [**@sap/cds-dk**](https://cap.cloud.sap/docs/get-started/) globally:
```sh
@@ -36,7 +36,7 @@ cd samples
In the samples folder run:
```sh
npm ci
npm install
```
### Run

View File

@@ -19,13 +19,13 @@ const books = Vue.createApp ({
search: ({target:{value:v}}) => books.fetch(v && '&$search='+v),
async fetch (etc='') {
const {data} = await GET(`/Books?${etc}`)
const {data} = await GET(`/ListOfBooks?$expand=genre,currency${etc}`)
books.list = data.value
},
async inspect (eve) {
const book = books.book = books.list [eve.currentTarget.rowIndex-1]
const res = await GET(`/Book/${book.ID}?$select=descr,stock,image`)
const res = await GET(`/Books/${book.ID}?$select=descr,stock,image`)
Object.assign (book, res.data)
books.order = { quantity:1 }
setTimeout (()=> $('form > input').focus(), 111)
@@ -77,7 +77,7 @@ function csrfToken (request) {
document.csrfToken = token
request.headers['x-csrf-token'] = document.csrfToken
return request
}).catch(() => {
}).catch(_ => {
document.csrfToken = null // set mark to not try again
return request
})

View File

@@ -45,11 +45,11 @@
<tr v-for="book in list" v-bind:id="book.ID" v-on:click="inspect">
<td>{{ book.title }}</td>
<td>{{ book.author }}</td>
<td>{{ book.genre }}</td>
<td>{{ book.genre.name }}</td>
<td class="rating-stars">
{{ ('★'.repeat(Math.round(book.rating))+'☆☆☆☆☆').slice(0,5) }} ({{ book.numberOfReviews }})
</td>
<td>{{ book.currency }} {{ book.price }}</td>
<td>{{ book.currency && book.currency.symbol }} {{ book.price }}</td>
</tr>
</table>

View File

@@ -1,5 +1,3 @@
const cds = require('@sap/cds')
/**
* In order to keep basic bookshop sample as simple as possible, we don't add
* reuse dependencies. This db/init.js ensures we still have a minimum set of
@@ -8,7 +6,7 @@ const cds = require('@sap/cds')
// NOTE: We use cds.on('served') to delay the UPSERTs after the db init
// to run after all INSERTs from .csv files happened.
module.exports = cds.on('served', ()=>
module.exports = cds.on('served', ()=> cds.run(
UPSERT.into ('sap.common.Currencies') .columns (
[ 'code', 'symbol', 'name' ]
) .rows (
@@ -18,4 +16,4 @@ module.exports = cds.on('served', ()=>
[ 'ILS', '₪', 'Shekel' ],
[ 'JPY', '¥', 'Yen' ],
)
)
))

2
bookshop/index.js Normal file
View File

@@ -0,0 +1,2 @@
const { CatalogService } = require('./srv/cat-service')
module.exports = { CatalogService }

View File

@@ -1,5 +1,5 @@
using { sap.capire.bookshop as my } from '../db/schema';
service AdminService @(requires:'admin', path:'/admin') {
entity Books as projection on my.Books;
// entity Authors as projection on my.Authors;
entity Authors as projection on my.Authors;
}

View File

@@ -1,14 +1,13 @@
const cds = require('@sap/cds')
const cds = require('@sap/cds/lib')
module.exports = class AdminService extends cds.ApplicationService { init(){
this.before (['NEW','CREATE'],'Authors', genid)
this.before (['NEW','CREATE'],'Books', genid)
this.before ('NEW','Authors', genid)
this.before ('NEW','Books', genid)
return super.init()
}}
/** Generate primary keys for target entity in request */
async function genid (req) {
if (req.data.ID) return
const {id} = await SELECT.one.from(req.target).columns('max(ID) as id')
req.data.ID = id + 4 // Note: that is not safe! ok for this sample only.
req.data.ID = id - id % 100 + 100 + 1
}

View File

@@ -2,20 +2,15 @@ using { sap.capire.bookshop as my } from '../db/schema';
service CatalogService @(path:'/browse') {
/** For displaying lists of Books */
@readonly entity Books as projection on Book excluding { descr };
@readonly entity ListOfBooks as projection on Books
excluding { descr };
/** For display in details pages */
@readonly entity Book as projection on my.Books { *,
currency.name as currencyName, // flattened
currency.symbol as currency, // flattened
author.name as author, // flattened
genre.name as genre, // flattened
} excluding {
createdBy, modifiedBy, // as end users shouldn't see them
localized, texts, // as end users don't need them
};
@readonly entity Books as projection on my.Books { *,
author.name as author
} excluding { createdBy, modifiedBy };
@requires: 'authenticated-user'
action submitOrder ( book: Book:ID, quantity: Integer ) returns { stock: Integer };
event OrderedBook : { book: Book:ID; quantity: Integer; buyer: String };
action submitOrder ( book: Books:ID, quantity: Integer ) returns { stock: Integer };
event OrderedBook : { book: Books:ID; quantity: Integer; buyer: String };
}

View File

@@ -1,12 +1,10 @@
const cds = require('@sap/cds')
class CatalogService extends cds.ApplicationService { init() {
module.exports = class CatalogService extends cds.ApplicationService { init() {
const { Books } = cds.entities('sap.capire.bookshop')
const { Books:Book } = this.entities
const { ListOfBooks } = this.entities
// Add some discount for overstocked books
this.after('each', Book, book => {
this.after('each', ListOfBooks, book => {
if (book.stock > 111) book.title += ` -- 11% discount!`
})
@@ -34,5 +32,3 @@ class CatalogService extends cds.ApplicationService { init() {
// Delegate requests to the underlying generic service
return super.init()
}}
module.exports = CatalogService

View File

@@ -3,15 +3,15 @@
# Genres
#
GET http://localhost:4004/odata/v4/test/Genres?
GET http://localhost:4004/test/Genres?
###
GET http://localhost:4004/odata/v4/test/Genres?
GET http://localhost:4004/test/Genres?
&$filter=parent_ID eq null&$select=name
&$expand=children($select=name)
###
POST http://localhost:4004/odata/v4/test/Genres?
POST http://localhost:4004/test/Genres?
Content-Type: application/json
{ "ID":100, "name":"Some Sample Genres...", "children":[
@@ -26,13 +26,13 @@ Content-Type: application/json
]}
###
GET http://localhost:4004/odata/v4/test/Genres(100)?
GET http://localhost:4004/test/Genres(100)?
# &$expand=children
# &$expand=children($expand=children($expand=children($expand=children)))
###
DELETE http://localhost:4004/odata/v4/test/Genres(103)
DELETE http://localhost:4004/test/Genres(103)
###
DELETE http://localhost:4004/odata/v4/test/Genres(100)
DELETE http://localhost:4004/test/Genres(100)
###

View File

@@ -1,6 +1,5 @@
@server = http://localhost:4004
@me = Authorization: Basic {{$processEnv USER}}:
@alice = Authorization: Basic alice:
### ------------------------------------------------------------------------
@@ -17,11 +16,11 @@ GET {{server}}/browse/$metadata
### ------------------------------------------------------------------------
# Browse Books as any user
GET {{server}}/admin/Books?
GET {{server}}/browse/ListOfBooks?
# &$select=title,stock
&$expand=genre
# &sap-language=de
{{alice}}
{{me}}
### ------------------------------------------------------------------------
@@ -31,13 +30,13 @@ GET {{server}}/admin/Authors?
# &$expand=books($select=title;$expand=currency)
# &$filter=ID eq 101
# &sap-language=de
{{alice}}
Authorization: Basic alice:
### ------------------------------------------------------------------------
# Create Author
POST {{server}}/admin/Authors
Content-Type: application/json;IEEE754Compatible=true
{{alice}}
Authorization: Basic alice:
{
"ID": 112,
@@ -50,7 +49,7 @@ Content-Type: application/json;IEEE754Compatible=true
# Create book
POST {{server}}/admin/Books
Content-Type: application/json;IEEE754Compatible=true
{{alice}}
Authorization: Basic alice:
{
"ID": 2,
@@ -68,7 +67,7 @@ Content-Type: application/json;IEEE754Compatible=true
# Put image to books
PUT {{server}}/admin/Books(2)/image
Content-Type: image/png
{{alice}}
Authorization: Basic alice:
data:image/png;base64,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

View File

@@ -1,12 +0,0 @@
Books = Bücher
Book = Buch
Title = Titel
Description = Beschreibung
Stock = Bestand
Image = Bild
Price = Preis
Currency = Währung
Authors = Autoren
Author = Autor
AuthorID = ID des Autors

View File

@@ -1,21 +0,0 @@
Books = Books
Book = Book
ID = ID
Title = Title
Description = Description
Stock = Stock
Image = Image
Price = Price
Currency = Currency
Authors = Authors
Author = Author
AuthorID = Author''s ID
Name = Name
DateOfBirth = Date of Birth
DateOfDeath = Date of Death
PlaceOfBirth = Place of Birth
PlaceOfDeath = Place of Death
Genres = Genres
Genre = Genre

View File

@@ -1,20 +0,0 @@
Books = Livres
Book = Livre
Title = Titre
Description = Description
Stock = Action
Image = Image
Price = Prix
Currency = Devise
Authors = Auteurs
Author = Auteur
AuthorID = ID de l''auteur
Name = Nom
DateOfBirth = Date de naissance
DateOfDeath = Date de décès
PlaceOfBirth = Lieu de naissance
PlaceOfDeath = Lieu de décès
Genres = Genre
Genre = Genre

View File

@@ -1,34 +0,0 @@
400 = Ungültige Anfrage
401 = Nicht autorisiert
403 = Verboten
404 = Nicht gefunden
405 = Methode nicht zulässig
406 = Nicht akzeptabel
407 = Proxy-Authentifizierung erforderlich
408 = Anfrage-Timeout
409 = Konflikt
410 = Weg
411 = Erforderliche Länge
412 = Vorbedingung fehlgeschlagen
413 = Nutzlast zu groß
414 = URI zu lang
415 = Nicht unterstützter Medientyp
416 = Bereich nicht erfüllbar
417 = Erwartung fehlgeschlagen
422 = Nicht verarbeitbarer Inhalt
424 = Fehlgeschlagene Abhängigkeit
428 = Vorbedingung erforderlich
429 = Zu viele Anfragen
431 = Anfrage-Headerfelder zu groß
451 = Aus rechtlichen Gründen nicht verfügbar
500 = Interner Serverfehler
501 = Der Server unterstützt die zur Erfüllung der Anfrage erforderliche Funktionalität nicht
502 = Ungültiges Gateway
503 = Dienst nicht verfügbar
504 = Gateway-Timeout
ASSERT_RANGE = Wert {0} liegt nicht im angegebenen Bereich [{1}, {2}]
ASSERT_FORMAT = Wert "{0}" liegt nicht im angegebenen Format "{1}"
ASSERT_ARRAY = Wert muss ein Array sein
ASSERT_ENUM = Wert {0} ist gemäß Enumerationsdeklaration {{1}} ungültig
ASSERT_NOT_NULL = Wert ist erforderlich

View File

@@ -1,34 +0,0 @@
400 = Bad Request
401 = Unauthorized
403 = Forbidden
404 = Not Found
405 = Method Not Allowed
406 = Not Acceptable
407 = Proxy Authentication Required
408 = Request Timeout
409 = Conflict
410 = Gone
411 = Length Required
412 = Precondition Failed
413 = Payload Too Large
414 = URI Too Long
415 = Unsupported Media Type
416 = Range Not Satisfiable
417 = Expectation Failed
422 = Unprocessable Content
424 = Failed Dependency
428 = Precondition Required
429 = Too Many Requests
431 = Request Header Fields Too Large
451 = Unavailable For Legal Reasons
500 = Internal Server Error
501 = The server does not support the functionality required to fulfill the request
502 = Bad Gateway
503 = Service Unavailable
504 = Gateway Timeout
ASSERT_RANGE = Value {0} is not in specified range [{1}, {2}]
ASSERT_FORMAT = Value "{0}" is not in specified format "{1}"
ASSERT_ARRAY = Value must be an array
ASSERT_ENUM = Value {0} is invalid according to enum declaration {{1}}
ASSERT_NOT_NULL = Value is required

View File

@@ -1,34 +0,0 @@
400 = Requête incorrecte
401 = Non autorisée
403 = Interdite
404 = Introuvable
405 = Méthode non autorisée
406 = Non acceptable
407 = Authentification proxy requise
408 = Délai d''expiration de la requête
409 = Conflit
410 = Disparu
411 = Longueur requise
412 = Échec de la condition préalable
413 = Charge utile trop importante
414 = URI trop longue
415 = Type de média non pris en charge
416 = Plage non satisfaisante
417 = Échec de l''attente
422 = Contenu non traitable
424 = Dépendance échouée
428 = Condition préalable requise
429 = Trop de requêtes
431 = Champs d''en-tête de requête trop importants
451 = Indisponible pour des raisons juridiques
500 = Erreur interne du serveur
501 = Le serveur ne prend pas en charge la fonctionnalité requise pour répondre à la requête
502 = Passerelle incorrecte
503 = Service indisponible
504 = Délai d''attente de la passerelle
ASSERT_RANGE = La valeur {0} n''est pas dans la plage spécifiée [{1}, {2}]
ASSERT_FORMAT = La valeur "{0}" n''est pas au format spécifié "{1}"
ASSERT_ARRAY = La valeur doit être un tableau
ASSERT_ENUM = La valeur {0} n''est pas valide selon la déclaration d''énumération {{1}}
ASSERT_NOT_NULL = La valeur est obligatoire

View File

@@ -5,15 +5,10 @@ cds.once('served', require('./srv/mashup'))
// Add routes to UIs from imported packages
cds.once('bootstrap',(app)=>{
try {
app.serve ('/bookshop') .from ('@capire/bookshop','app/vue')
app.serve ('/reviews') .from ('@capire/reviews','app/vue')
app.serve ('/orders') .from('@capire/orders','app/orders')
app.serve ('/data') .from('@capire/data-viewer','app/viewer')
} catch (err) {
if (err.code === 'MODULE_NOT_FOUND') throw new Error('Run "npm ci" to install the required dependencies', { cause: err })
throw err
}
})
// Add Swagger UI

View File

@@ -18,7 +18,7 @@ module.exports = async()=>{ // called by server.js
// Note: prepend is neccessary to intercept generic default handler
//
CatalogService.prepend (srv => srv.on ('READ', 'Books/reviews', (req) => {
console.debug ('> delegating request to ReviewsService') // eslint-disable-line no-console
console.debug ('> delegating request to ReviewsService')
const [id] = req.params, { columns, limit } = req.query.SELECT
return ReviewsService.read ('Reviews',columns).limit(limit).where({subject:String(id)})
}))
@@ -40,7 +40,7 @@ module.exports = async()=>{ // called by server.js
// Update Books' average ratings when ReviewsService signals updated reviews
//
ReviewsService.on ('reviewed', (msg) => {
console.debug ('> received:', msg.event, msg.data) // eslint-disable-line no-console
console.debug ('> received:', msg.event, msg.data)
const { subject, count, rating } = msg.data
return UPDATE(Books,subject).with({ numberOfReviews:count, rating })
})
@@ -49,7 +49,7 @@ module.exports = async()=>{ // called by server.js
// Reduce stock of ordered books for orders are created from Orders admin UI
//
OrdersService.on ('OrderChanged', (msg) => {
console.debug ('> received:', msg.event, msg.data) // eslint-disable-line no-console
console.debug ('> received:', msg.event, msg.data)
const { product, deltaQuantity } = msg.data
return UPDATE (Books) .where ('ID =', product)
.and ('stock >=', deltaQuantity)

View File

@@ -12,12 +12,11 @@
#
GET {{reviews-service}}/Reviews
Authorization: Basic me:
###
POST {{reviews-service}}/Reviews
Authorization: Basic me:
Authorization: Basic {{$processEnv USER}}:
Content-Type: application/json
{"subject":"201", "title":"boo", "rating":3 }
@@ -42,7 +41,7 @@ GET {{bookshop}}/browse/Books(201)?
###
GET {{bookshop}}/browse/Books?
&$select=title,author&$expand=currency
&$select=title,author&$expand=currency
Accept-Language: de
#################################################
@@ -51,23 +50,23 @@ Accept-Language: de
#
@newOrderID = e939604c-ab83-4d4f-bdb6-95fe30b3773e
GET {{bookshop}}/odata/v4/orders/Orders
GET {{bookshop}}/orders/Orders
### Create order, still inactive
POST {{bookshop}}/odata/v4/orders/Orders
POST {{bookshop}}/orders/Orders
Content-Type: application/json
{"ID": "{{newOrderID}}"}
### Get inactive order. We have to specify `IsActiveEntity`.
GET {{bookshop}}/odata/v4/orders/Orders(ID={{newOrderID}},IsActiveEntity=false)
GET {{bookshop}}/orders/Orders(ID={{newOrderID}},IsActiveEntity=false)
### Activate order using `.../<servicename>.draftActivate`
POST {{bookshop}}/odata/v4/orders/Orders(ID={{newOrderID}},IsActiveEntity=false)/OrdersService.draftActivate
POST {{bookshop}}/orders/Orders(ID={{newOrderID}},IsActiveEntity=false)/OrdersService.draftActivate
Content-Type: application/json
### Get active order
GET {{bookshop}}/odata/v4/orders/Orders(ID={{newOrderID}},IsActiveEntity=true)
GET {{bookshop}}/orders/Orders(ID={{newOrderID}},IsActiveEntity=true)
### Create author
POST {{bookshop}}/admin/Authors

View File

@@ -1 +0,0 @@
// dummy to auto-load the plugin

View File

@@ -4,12 +4,5 @@
"version": "1.0.0",
"dependencies": {
"@sap/cds": "*"
},
"cds": {
"requires": {
"@capire/common/data": {
"model": "@capire/common"
}
}
}
}

View File

@@ -18,7 +18,7 @@ class DataService extends cds.ApplicationService { init(){
.sort((e1, e2) => e1.name.localeCompare(e2.name))
.map(e => {
const columns = Object.entries(e.elements)
.filter(([,el]) => !(el instanceof cds.Association)) // exclude assocs+compositions
.filter(([_, el]) => !(el instanceof cds.Association)) // exclude assocs+compositions
.map(([name, el]) => { return { name, type: el.type, isKey:!!el.key }})
return { name: e.name, columns }
})
@@ -49,7 +49,6 @@ class DataService extends cds.ApplicationService { init(){
module.exports = { DataService }
/** @returns {cds.Service} */
function findDataSource(dataSourceName, entityName) {
for (let srv of Object.values(cds.services)) { // all connected services
if (!srv.name) continue // FIXME intermediate/pending in cds.services ?

27
eslint.config.js Normal file
View File

@@ -0,0 +1,27 @@
const eslintCds = require('@sap/eslint-plugin-cds')
const eslintJs = require('@eslint/js')
const globals = require('globals')
module.exports = [
eslintJs.configs.recommended,
eslintCds.configs.recommended,
{
languageOptions: {
globals: {
sap: true,
...globals.es2022,
...globals.browser,
...globals.node,
...globals.jest,
...globals.mocha,
...eslintCds.configs.recommended.languageOptions.globals
}
},
rules: {
'no-console': 'off',
'no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
'require-atomic-updates': 'off',
'require-await': 'warn'
}
}
]

View File

@@ -1,20 +0,0 @@
import cds from '@sap/cds/eslint.config.mjs'
export default [ ...cds.recommended ]
//
// The above is the recommended minimalistic eslint config, just using
// recommended defaults provided by cds. Alternatively, go for enhanced
// project-specific options, but only if really required, like this:
//
// export default [ ...cds.recommended, {
// rules: {
// 'complexity': [ 'warn', 66 ],
// 'require-await': 'warn',
// 'no-await-in-loop': 'warn',
// },
// }, {
// ignores: [
// '**/webapp/**'
// ]
// }]
//

View File

@@ -0,0 +1,43 @@
Books = Books
Book = Book
ID = ID
Title = Title
Author = Author
AuthorID = Author ID
Stock = Stock
Name = Name
Description = Description
Image = Image
AuthorName = Author's Name
DateOfBirth = Date of Birth
DateOfDeath = Date of Death
PlaceOfBirth = Place of Birth
PlaceOfDeath = Place of Death
Age = Age
Lifetime = Lifetime
Authors = Authors
Order = Order
Orders = Orders
OrderNo = Order Number
OrderItems = Order Items
Customer = Customer
Product = Product
ProductID = Product ID
ProductTitle = Product Title
UnitPrice = Unit Price
Quantity = Quantity
Price = Price
Currency = Currency
Date = Date
Rating = Rating
NumberOfReviews = Number of Reviews
Genre = Genre
Genres = Genres
SubGenres = Sub Genres
NumCode = Numeric Code
MinorUnit = Minor Unit
Exponent = Exponent

View File

@@ -1,5 +1,14 @@
Books = Bücher
Book = Buch
ID = ID
Title = Titel
Authors = Autoren
Author = Autor
AuthorID = ID des Autors
AuthorName = Name des Autors
Age = Alter
rder = Bestellung
Name = Name
Stock = Bestand
Order = Bestellung
Orders = Bestellungen
Price = Preis

View File

@@ -1,8 +0,0 @@
Age = Age
Lifetime = Lifetime
SubGenres = Sub Genres
NumCode = Numeric Code
MinorUnit = Minor Unit
Exponent = Exponent

View File

@@ -4,10 +4,10 @@ using CatalogService from '@capire/bookstore';
//
// Books Object Page
//
annotate CatalogService.Book with @(UI : {
annotate CatalogService.Books with @(UI : {
HeaderInfo : {
TypeName : '{i18n>Book}',
TypeNamePlural : '{i18n>Books}',
TypeName : 'Book',
TypeNamePlural : 'Books',
Description : {Value : author}
},
HeaderFacets : [{
@@ -24,7 +24,7 @@ annotate CatalogService.Book with @(UI : {
FieldGroup #Price : {Data : [
{Value : price},
{
Value : currencyName,
Value : currency.symbol,
Label : '{i18n>Currency}'
},
]},
@@ -35,11 +35,11 @@ annotate CatalogService.Book with @(UI : {
//
// Books List Page
//
annotate CatalogService.Book with @(UI : {
annotate CatalogService.Books with @(UI : {
SelectionFields : [
ID,
price,
currencyName
currency_code
],
LineItem : [
{
@@ -50,10 +50,8 @@ annotate CatalogService.Book with @(UI : {
Value : author,
Label : '{i18n>Author}'
},
{Value : genre},
{Value : genre.name},
{Value : price},
{Value : currencyName},
{Value : currency.symbol},
]
}) {
currencyName @Common.Label : '{i18n>Currency}';
};
}, );

View File

@@ -18,6 +18,7 @@ annotate my.Books with @(
ID,
author_ID,
price,
currency_code
],
LineItem : [
{ Value: ID, Label: '{i18n>Title}' },
@@ -25,6 +26,7 @@ annotate my.Books with @(
{ Value: genre.name },
{ Value: stock },
{ Value: price },
{ Value: currency.symbol },
]
}
) {
@@ -61,7 +63,7 @@ annotate my.Books with {
title @title: '{i18n>Title}';
genre @title: '{i18n>Genre}' @Common: { Text: genre.name, TextArrangement: #TextOnly };
author @title: '{i18n>Author}' @Common: { Text: author.name, TextArrangement: #TextOnly };
price @title: '{i18n>Price}' @Measures.ISOCurrency : currency;
price @title: '{i18n>Price}' @Measures.ISOCurrency : currency_code;
stock @title: '{i18n>Stock}';
descr @title: '{i18n>Description}' @UI.MultiLineText;
image @title: '{i18n>Image}';

View File

@@ -1,7 +1,7 @@
<!doctype html>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
@@ -10,22 +10,21 @@
<script>
window["sap-ushell-config"] = {
defaultRenderer: "fiori2",
applications: {},
applications: {}
};
</script>
<script id="sap-ushell-bootstrap" src="https://ui5.sap.com/test-resources/sap/ushell/bootstrap/sandbox.js"></script>
<script id="sap-ui-bootstrap" src="https://ui5.sap.com/resources/sap-ui-core.js"
data-sap-ui-libs="sap.m, sap.ushell, sap.collaboration, sap.ui.layout" data-sap-ui-compatVersion="edge"
data-sap-ui-theme="sap_horizon"></script>
<script id="sap-ushell-bootstrap" src="https://sapui5.hana.ondemand.com/test-resources/sap/ushell/bootstrap/sandbox.js"></script>
<script id="sap-ui-bootstrap" src="https://sapui5.hana.ondemand.com/resources/sap-ui-core.js"
data-sap-ui-libs="sap.m, sap.ushell, sap.collaboration, sap.ui.layout"
data-sap-ui-compatVersion="edge"
data-sap-ui-theme="sap_horizon"
data-sap-ui-frameOptions="allow"
></script>
<script>
sap.ui.getCore().attachInit(() =>
sap.ushell.Container.createRenderer().placeAt("content")
);
sap.ui.getCore().attachInit(()=> sap.ushell.Container.createRenderer().placeAt("content"))
</script>
</head>
<body class="sapUiBody" id="content">
</body>
<body class="sapUiBody" id="content"></body>
</html>

8
fiori/server.js Normal file
View File

@@ -0,0 +1,8 @@
// install OData v2 adapter
const cds = require("@sap/cds")
const proxy = require('@cap-js-community/odata-v2-adapter')
const opts = global.it ? { target:'auto' } : {} // for tests, set 'auto' to detect port dynamically
cds.on('bootstrap', app => app.use(proxy(opts))) // install proxy
// cds.log('cov2ap','silent') // suppress anoying log outpout, e.g. for `npm run mocha -- --reporter nyan`
module.exports = require('@capire/bookstore/server.js')

View File

@@ -3,10 +3,40 @@
"version": "1.0.0",
"scripts": {
"test": "npx jest --silent",
"start": "cds-serve srv/world.cds",
"start": "cds serve srv/world.cds",
"start:ts": "cds-ts serve srv/world.cds"
},
"dependencies": {
"@sap/cds": ">=5.0.4"
},
"devDependencies": {
"@types/jest": "*",
"@types/node": "*",
"typescript": ">=4.3.5"
},
"eslintConfig": {
"extends": "eslint:recommended",
"env": {
"es2020": true,
"node": true,
"jest": true,
"mocha": true
},
"globals": {
"SELECT": true,
"INSERT": true,
"UPDATE": true,
"DELETE": true,
"CREATE": true,
"DROP": true,
"CDL": true,
"CQL": true,
"CXL": true,
"cds": true
},
"rules": {
"no-console": "off",
"require-atomic-updates": "off"
}
}
}

View File

@@ -1,3 +1,3 @@
service say {
service say @(path: '/say') {
function hello (to:String) returns String;
}

View File

@@ -0,0 +1,13 @@
const cds = require ('@sap/cds')
describe('Hello world!', () => {
beforeAll (()=> process.env.CDS_TYPESCRIPT = true)
afterAll (()=> delete process.env.CDS_TYPESCRIPT)
const {GET} = cds.test.in(__dirname,'../srv').run('serve', 'world.cds')
it('should say hello with class impl', async () => {
const {data} = await GET`/say/hello(to='world')`
expect(data.value).toMatch(/Hello world.*typescript.*/i)
})
})

View File

@@ -1,5 +1 @@
GET http://localhost:4004/odata/v4/say/hello
###
GET http://localhost:4004/odata/v4/say/hello(to='me')
###
GET http://localhost:4004/say/hello(to='world')

View File

@@ -13,5 +13,10 @@
"scripts": {
"start": "cds-serve",
"watch": "cds watch"
},
"cds": {
"requires": {
"db": "sql"
}
}
}

View File

@@ -1,4 +1,4 @@
const cds = require ('@sap/cds')
const cds = require ('@sap/cds/lib')
const LOG = cds.log('cds.log')
module.exports = class LogService extends cds.Service {

View File

@@ -1,7 +1,6 @@
using { sap.capire.media as db } from '../db/data-model';
namespace sap.capire.media;
@path: '/media-server'
service MediaServer {
entity Media as projection on db.Media ;
}

View File

@@ -10,7 +10,7 @@ module.exports = srv => {
})
srv.on('UPDATE', 'Media', (req, next) => {
const url = req.path
const url = req._.req.path
if (url.includes('content')) {
const id = req.data.id
const obj = mediaDB.get(id)
@@ -32,7 +32,7 @@ module.exports = srv => {
})
srv.on('READ', 'Media', (req, next) => {
const url = req.path
const url = req._.req.path
if (url.includes('content')) {
const id = req.data.id
const mediaObj = mediaDB.get(id)

View File

@@ -1,3 +0,0 @@
Order = Bestellung
Orders = Bestellungen
Price = Preis

View File

@@ -1,10 +0,0 @@
Order = Order
Orders = Orders
OrderNo = Order Number
OrderItems = Order Items
Customer = Customer
Product = Product
ProductID = Product ID
ProductTitle = Product Title
UnitPrice = Unit Price
Quantity = Quantity

View File

@@ -1,10 +0,0 @@
Order = Order
Orders = Orders
OrderNo = Numéro de commande
OrderItems = Articles de la commande
Customer = Client
Product = Produit
ProductID = ID du produit
ProductTitle = Titre du produit
UnitPrice = Prix unitaire
Quantity = Quantité

View File

@@ -2,7 +2,7 @@ using { Currency, User, managed, cuid } from '@sap/cds/common';
namespace sap.capire.orders;
entity Orders : cuid, managed {
OrderNo : String(44) @title:'Order Number'; //> readable key
OrderNo : String(22) @title:'Order Number'; //> readable key
Items : Composition of many {
key ID : UUID;
product : Association to Products;

View File

@@ -29,7 +29,7 @@ class OrdersService extends cds.ApplicationService {
/** order changed -> broadcast event */
orderChanged (product, deltaQuantity) {
// Emit events to inform subscribers about changes in orders
console.log ('> emitting:', 'OrderChanged', { product, deltaQuantity }) // eslint-disable-line no-console
console.log ('> emitting:', 'OrderChanged', { product, deltaQuantity })
return this.emit ('OrderChanged', { product, deltaQuantity })
}

1592
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +1,11 @@
{
"name": "@capire/samples",
"version": "2.1.0",
"version": "2.0.0",
"description": "A monorepo with several samples for CAP.",
"repository": "https://github.com/sap-samples/cloud-cap-samples.git",
"author": "daniel.hutzel@sap.com",
"dependencies": {
"@sap/cds": ">=8"
"@sap/cds": ">=7"
},
"workspaces": [
"./bookshop",
@@ -20,8 +20,8 @@
"./reviews"
],
"devDependencies": {
"@cap-js/cds-types": "^0",
"@cap-js/sqlite": "^1",
"@sap/eslint-plugin-cds": "^3",
"axios": "^1",
"chai": "^4.3.4",
"chai-as-promised": "^7.1.1",
@@ -30,16 +30,23 @@
"semver": "^7"
},
"scripts": {
"cleanup": "rm -rf node_modules && rm -rf */node_modules && rm -rf */*/node_modules",
"bookshop": "cds watch bookshop",
"start": "cds watch fiori",
"fiori": "cds watch fiori",
"hello": "cds watch hello",
"media": "cds watch media",
"lint": "eslint",
"test": "npx jest --silent",
"jest": "npx jest --silent",
"mocha": "CDS_TEST_SILENT=y npx mocha",
"test:hello": "cd hello && npm test"
"jest": "npx jest --silent",
"start": "cds watch fiori",
"test": "npm run jest -- --silent",
"test:hello": "cd hello && npm test",
"lint": "eslint ."
},
"jest": {
"testTimeout": 20000,
"testMatch": [
"**/*.test.js"
]
},
"mocha": {
"recursive": true,
@@ -48,4 +55,4 @@
},
"license": "SEE LICENSE IN LICENSE",
"private": true
}
}

View File

@@ -1,3 +0,0 @@
Date = Datum
Rating = Bewertung
NumberOfReviews = Anzahl der Reviews

View File

@@ -1,5 +0,0 @@
Reviews = Reviews
Review = Review
Date = Date
Rating = Rating
NumberOfReviews = Number of Reviews

View File

@@ -1,5 +0,0 @@
Reviews = Avis
Review = Avis
Date = Date
Rating = Note
NumberOfReviews = Nombre d''avis

View File

@@ -50,6 +50,7 @@ const reviews = Vue.createApp ({
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.' }

View File

@@ -15,7 +15,7 @@ module.exports = cds.service.impl (function(){
const { count, rating } = await cds.tx(req) .run (
SELECT.one `round(avg(rating),2) as rating, count(*) as count` .from (Reviews) .where ({subject})
)
global.it || console.log ('< emitting:', 'reviewed', { subject, count, rating }) // eslint-disable-line no-console
global.it || console.log ('< emitting:', 'reviewed', { subject, count, rating })
await this.emit ('reviewed', { subject, count, rating })
})

View File

@@ -1,8 +1,7 @@
const cds = require('@sap/cds')
const { expect } = cds.test
describe('cds.ql → cqn', () => {
const cds = require('@sap/cds/lib')
const { expect } = cds.test
const Foo = { name: 'Foo' }
const Books = { name: 'capire.bookshop.Books' }
@@ -677,7 +676,7 @@ describe('cds.ql → cqn', () => {
.to.eql(INSERT.into(Foo).entries(...entries))
.to.eql(INSERT.into(Foo).entries(entries))
.to.eql({
INSERT: { into: { ref: ['Foo'] }, entries },
INSERT: { into: cds.env.ql.quirks_mode ? 'Foo' : { ref: ['Foo'] }, entries },
})
})
@@ -693,7 +692,7 @@ describe('cds.ql → cqn', () => {
.to.eql(INSERT.into(Foo).columns('a', 'b').rows([1, 2], [3, 4]))
.to.eql({
INSERT: {
into: { ref: ['Foo'] },
into: cds.env.ql.quirks_mode ? 'Foo' : { ref: ['Foo'] },
columns: ['a', 'b'],
rows: [
[1, 2],
@@ -707,7 +706,7 @@ describe('cds.ql → cqn', () => {
expect(INSERT.into(Foo).columns('a', 'b').values([1, 2]))
.to.eql(INSERT.into(Foo).columns('a', 'b').values(1, 2))
.to.eql({
INSERT: { into: { ref: ['Foo'] }, columns: ['a', 'b'], values: [1, 2] },
INSERT: { into: cds.env.ql.quirks_mode ? 'Foo' : { ref: ['Foo'] }, columns: ['a', 'b'], values: [1, 2] },
})
})
@@ -722,7 +721,7 @@ describe('cds.ql → cqn', () => {
test('entity (..., <key>)', () => {
const cqnWhere = {
UPDATE: {
entity: { ref: ['capire.bookshop.Books'] },
entity: cds.env.ql.quirks_mode ? 'capire.bookshop.Books' : { ref: ['capire.bookshop.Books'] },
where: [{ ref: ['ID'] }, '=', { val: 4711 }],
},
}
@@ -766,7 +765,7 @@ describe('cds.ql → cqn', () => {
.to.eql(UPDATE(Foo).with({ foo: 11, bar: { '-=': 22 } }))
.to.eql({
UPDATE: {
entity: { ref: ['Foo'] },
entity: cds.env.ql.quirks_mode ? 'Foo' : { ref: ['Foo'] },
data: { foo: 11 },
with: {
bar: { xpr: [{ ref: ['bar'] }, '-', { val: 22 }] },
@@ -777,7 +776,7 @@ describe('cds.ql → cqn', () => {
// some more
expect(UPDATE(Foo).with(`bar = coalesce(x,y), car = 'foo''s bar, car'`)).to.eql({
UPDATE: {
entity: { ref: ['Foo'] },
entity: cds.env.ql.quirks_mode ? 'Foo' : { ref: ['Foo'] },
data: {
car: "foo's bar, car",
},
@@ -797,7 +796,7 @@ describe('cds.ql → cqn', () => {
test('from (..., <key>)', () => {
const cqnWhere = {
DELETE: {
from: { ref: ['capire.bookshop.Books'] },
from: cds.env.ql.quirks_mode ? 'capire.bookshop.Books' : { ref: ['capire.bookshop.Books'] },
where: [{ ref: ['ID'] }, '=', { val: 4711 }],
},
}

View File

@@ -1,53 +0,0 @@
const cds = require("@sap/cds");
const { expect } = cds.test(
"serve",
"CatalogService",
"--from",
"@capire/bookshop,@capire/common",
"--in-memory"
);
describe("Consuming actions locally", () => {
let cats, CatalogService, Books, stockBefore;
const BOOK_ID = 251;
const QUANTITY = 1;
before("bootstrap the database", async () => {
CatalogService = cds.services.CatalogService;
expect(CatalogService).not.to.be.undefined;
Books = CatalogService.entities.Books;
expect(Books).not.to.be.undefined;
cats = await cds.connect.to("CatalogService");
});
beforeEach(async () => {
// Read the stock before the action is called
stockBefore = (await cats.get(Books, BOOK_ID)).stock;
});
it("calls unbound actions - basic variant using srv.send", async () => {
// Use a managed transaction to create a continuation with an authenticated user
const res1 = await cats.tx({ user: "alice" }, () => {
return cats.send("submitOrder", { book: BOOK_ID, quantity: QUANTITY });
});
expect(res1.stock).to.eql(stockBefore - QUANTITY);
});
it("calls unbound actions - named args variant", async () => {
// Use a managed transaction to create a continuation with an authenticated user
const res2 = await cats.tx({ user: "alice" }, () => {
return cats.submitOrder({ book: BOOK_ID, quantity: QUANTITY });
});
expect(res2.stock).to.eql(stockBefore - QUANTITY);
});
it("calls unbound actions - positional args variant", async () => {
// Use a managed transaction to create a continuation with an authenticated user
const res3 = await cats.tx({ user: "alice" }, () => {
return cats.submitOrder(BOOK_ID, QUANTITY);
});
expect(res3.stock).to.eql(stockBefore - QUANTITY);
});
});

View File

@@ -1,4 +1,4 @@
const cds = require('@sap/cds')
const cds = require('@sap/cds/lib')
describe('cap/samples - Consuming Services locally', () => {

View File

@@ -1,4 +1,4 @@
const cds = require('@sap/cds')
const cds = require('@sap/cds/lib')
describe('cap/samples - Custom Handlers', () => {
@@ -8,10 +8,9 @@ describe('cap/samples - Custom Handlers', () => {
})
it('should reject out-of-stock orders', async () => {
await expect(POST `/browse/submitOrder ${{ book: 201, quantity: 5 }}`).to.be.fulfilled
await expect(POST `/browse/submitOrder ${{ book: 201, quantity: 5 }}`).to.be.fulfilled
await expect(POST `/browse/submitOrder ${{ book: 201, quantity: 5 }}`).to.be.rejectedWith(
/409 - 5 exceeds stock for book #201/)
await POST `/browse/submitOrder ${{ book: 201, quantity: 5 }}`
await POST `/browse/submitOrder ${{ book: 201, quantity: 5 }}`
await expect(POST `/browse/submitOrder ${{ book: 201, quantity: 5 }}`).to.be.rejectedWith(/409 - 5 exceeds stock for book #201/)
const { data } = await GET`/admin/Books/201/stock/$value`
expect(data).to.equal(2)
})

View File

@@ -1,8 +1,4 @@
// Quick hack: suppress deprecation warnings w/ Node22 caused by http-proxy (used by OData v2 proxy)
// See also: https://github.com/http-party/node-http-proxy/pull/1666
require('util')._extend = Object.assign
const cds = require('@sap/cds')
const cds = require('@sap/cds/lib')
describe('cap/samples - Fiori APIs - v2', function() {

View File

@@ -1,11 +1,11 @@
const cds = require('@sap/cds')
const cds = require('@sap/cds/lib')
describe('cap/samples - Hello world!', () => {
const { GET, expect } = cds.test (__dirname+'/../hello')
it('should say hello with class impl', async () => {
const {data} = await GET `/odata/v4/say/hello(to='world')`
const {data} = await GET `/say/hello(to='world')`
expect(data.value).to.eql('Hello world!')
})

View File

@@ -1,4 +1,4 @@
const cds = require('@sap/cds')
const cds = require('@sap/cds/lib')
const { expect } = cds.test.in(__dirname,'..')
describe('cap/samples - Hierarchical Data', ()=>{
@@ -77,27 +77,29 @@ describe('cap/samples - Hierarchical Data', ()=>{
)
})
it ('supports nested reads', ()=> expect (
it ('supports nested reads', async()=>{
expect (await
SELECT.one.from (Cats, c=>{
c.ID, c.name.as('parent'), c.children (c=>{
c.name.as('child')
})
}) .where ({name:'Cat'})
) .to.eventually.eql (
) .to.eql (
{ ID:101, parent:'Cat', children:[
{ child:'Kitty' },
{ child:'Catwoman' },
]}
))
)
})
it ('supports deeply nested reads', ()=> expect (
SELECT.one.from (Cats, c=>{
it ('supports deeply nested reads', async()=>{
expect (await SELECT.one.from (Cats, c=>{
c.ID, c.name, c.children (
c => { c.name },
{levels:3}
)
}) .where ({name:'Cat'})
) .to.eventually.eql (
) .to.eql (
{ ID:101, name:'Cat', children:[
{ name:'Kitty', children:[
{ name:'Kitty Cat', children:[
@@ -106,16 +108,18 @@ describe('cap/samples - Hierarchical Data', ()=>{
{ name:'Catwoman', children:[
{ name:'Catalina', children:[] } ]},
]}
))
)
})
it ('supports cascaded deletes', async()=>{
const affectedRows = await DELETE.from (Cats) .where ({ID:[102,106]})
expect (affectedRows) .to.be.greaterThan (0)
await expect (SELECT`ID,name`.from(Cats) ).to.eventually.eql ([
const expected = [
{ ID:100, name:'Some Cats...' },
{ ID:101, name:'Cat' },
{ ID:108, name:'Catweazle' }
])
]
expect ( await SELECT`ID,name`.from(Cats) ).to.eql (expected)
})
})

View File

@@ -1,4 +1,4 @@
const cds = require('@sap/cds')
const cds = require('@sap/cds/lib')
describe('cap/samples - Localized Data', () => {
@@ -27,15 +27,15 @@ describe('cap/samples - Localized Data', () => {
})
it('supports queries with $expand', async () => {
const { data } = await GET(`/browse/Book?&$select=title,author,currencyName`, {
const { data } = await GET(`/browse/Books?&$select=title,author&$expand=currency`, {
headers: { 'Accept-Language': 'de' },
})
expect(data.value).to.containSubset([
{ title: 'Sturmhöhe', author: 'Emily Brontë', currencyName: 'Pfund' },
{ title: 'Jane Eyre', author: 'Charlotte Brontë', currencyName: 'Pfund' },
{ title: 'The Raven', author: 'Edgar Allen Poe', currencyName: 'US-Dollar' },
{ title: 'Eleonora', author: 'Edgar Allen Poe', currencyName: 'US-Dollar' },
{ title: 'Catweazle', author: 'Richard Carpenter', currencyName: 'Yen' },
{ title: 'Sturmhöhe', author: 'Emily Brontë', currency: { name: 'Pfund' } },
{ title: 'Jane Eyre', author: 'Charlotte Brontë', currency: { name: 'Pfund' } },
{ title: 'The Raven', author: 'Edgar Allen Poe', currency: { name: 'US-Dollar' } },
{ title: 'Eleonora', author: 'Edgar Allen Poe', currency: { name: 'US-Dollar' } },
{ title: 'Catweazle', author: 'Richard Carpenter', currency: { name: 'Yen' } },
])
})

View File

@@ -1,4 +1,4 @@
const cds = require('@sap/cds')
const cds = require('@sap/cds/lib')
describe('cap/samples - Messaging', ()=>{

View File

@@ -1,4 +1,4 @@
const cds = require('@sap/cds')
const cds = require('@sap/cds/lib')
describe('cap/samples - Bookshop APIs', () => {
const { GET, expect, axios } = cds.test ('@capire/bookshop')
@@ -8,29 +8,26 @@ describe('cap/samples - Bookshop APIs', () => {
const { headers, status, data } = await GET `/browse/$metadata`
expect(status).to.equal(200)
expect(headers).to.contain({
// 'content-type': 'application/xml', //> fails with 'application/xml;charset=utf-8', which is set by express
'content-type': 'application/xml',
'odata-version': '4.0',
})
expect(headers['content-type']).to.match(/application\/xml/)
expect(data).to.contain('<EntitySet Name="Books" EntityType="CatalogService.Books"/>')
expect(data).to.contain('<Annotation Term="Common.Label" String="Currency Symbol"/>')
expect(data).to.contain('<EntitySet Name="Books" EntityType="CatalogService.Books">')
expect(data).to.contain('<Annotation Term="Common.Label" String="Currency"/>')
})
it('serves Books?$expand=genre,currency', async () => {
it('serves ListOfBooks?$expand=genre,currency', async () => {
const Mystery = { ID: 16, name: 'Mystery', descr: null, parent_ID: 10 }
const Romance = { ID: 15, name: 'Romance', descr: null, parent_ID: 10 }
const USD = { code: 'USD', name: 'US Dollar', descr: null, symbol: '$' }
const { data } = await GET `/admin/Books ${{
const { data } = await GET `/browse/ListOfBooks ${{
params: { $search: 'Po', $select: `title,author`, $expand:`genre,currency` },
}}`
expect(data.value).to.containSubset([
{ ID: 251, title: 'The Raven', author_ID: 150, genre:Mystery, currency:USD },
{ ID: 252, title: 'Eleonora', author_ID: 150, genre:Romance, currency:USD },
{ ID: 251, title: 'The Raven', author: 'Edgar Allen Poe', genre:Mystery, currency:USD },
{ ID: 252, title: 'Eleonora', author: 'Edgar Allen Poe', genre:Romance, currency:USD },
])
})
describe('query options...', () => {
it('supports $search in multiple fields', async () => {
const { data } = await GET `/browse/Books ${{
params: { $search: 'Po', $select: `title,author` },
@@ -89,7 +86,6 @@ describe('cap/samples - Bookshop APIs', () => {
{ ID: 271, title: 'Catweazle' },
])
})
})
it('serves user info', async () => {
const { data: alice } = await GET `/user/me`