chore: Merged fiori into bookstore and tests into subprojects

This commit is contained in:
Daniel Hutzel
2025-07-17 13:54:32 +02:00
parent dcb0c0394a
commit c10ca17546
83 changed files with 2817 additions and 538 deletions

View File

@@ -0,0 +1,94 @@
@server = http://localhost:4004
@me = Authorization: Basic {{$processEnv USER}}:
### ------------------------------------------------------------------------
# Get service info
GET {{server}}/browse
{{me}}
### ------------------------------------------------------------------------
# Get $metadata document
GET {{server}}/browse/$metadata
{{me}}
### ------------------------------------------------------------------------
# Browse Books as any user
GET {{server}}/browse/ListOfBooks?
# &$select=title,stock
&$expand=genre
# &sap-language=de
{{me}}
### ------------------------------------------------------------------------
# Fetch Authors as admin
GET {{server}}/admin/Authors?
# &$select=name,dateOfBirth,placeOfBirth
# &$expand=books($select=title;$expand=currency)
# &$filter=ID eq 101
# &sap-language=de
Authorization: Basic alice:
### ------------------------------------------------------------------------
# Create Author
POST {{server}}/admin/Authors
Content-Type: application/json;IEEE754Compatible=true
Authorization: Basic alice:
{
"ID": 112,
"name": "Shakespeeeeere"
}
### ------------------------------------------------------------------------
# Create book
POST {{server}}/admin/Books
Content-Type: application/json;IEEE754Compatible=true
Authorization: Basic alice:
{
"ID": 2,
"title": "Poems : Pocket Poets",
"descr": "The Everyman's Library Pocket Poets hardcover series is popular for its compact size and reasonable price which does not compromise content. Poems: Bronte contains poems that demonstrate a sensibility elemental in its force with an imaginative discipline and flexibility of the highest order. Also included are an Editor's Note and an index of first lines.",
"author": { "ID": 101 },
"genre": { "ID": "12aaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" },
"stock": 5,
"price": "12.05",
"currency": { "code": "USD" }
}
### ------------------------------------------------------------------------
# Put image to books
PUT {{server}}/admin/Books(2)/image
Content-Type: image/png
Authorization: Basic alice:
data:image/png;base64,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
### ------------------------------------------------------------------------
# Reading image from from the server directly
GET {{server}}/browse/Books(2)/image
### ------------------------------------------------------------------------
# Submit Order as authenticated user
# (send that three times to get out-of-stock message)
POST {{server}}/browse/submitOrder
Content-Type: application/json
{{me}}
{ "book":201, "quantity":5 }
### ------------------------------------------------------------------------
# Browse Genres
GET {{server}}/browse/Genres?
# &$filter=parent_ID eq null&$select=name
# &$expand=children($select=name)
{{me}}

View File

@@ -0,0 +1,126 @@
const cds = require('@sap/cds/lib')
const { GET, expect, axios } = cds.test(__dirname)
// Fetch API disallows GET|HEAD requests with body
if (axios.constructor.name === 'Naxios') it = it.skip
describe ('GET w/ query in body', () => {
it ('serves CQN query objects in body', async () => {
const {data:books} = await GET ('/hcql/admin', {
headers: { 'Content-Type': 'application/json' },
data: cds.ql `SELECT from Books`
})
expect(books).to.be.an('array').of.length(5)
})
it ('serves plain CQL strings in body', async () => {
const {data:books} = await GET ('/hcql/admin', {
headers: { 'Content-Type': 'text/plain' },
data: `SELECT from Books`
})
expect(books).to.be.an('array').of.length(5)
})
it ('serves complex and deep queries', async () => {
const {data:books} = await GET ('/hcql/admin', {
headers: { 'Content-Type': 'text/plain' },
data: `SELECT from Authors {
name,
books [order by title] {
title,
genre.name as genre
}
}`
})
expect(books).to.deep.equal([
{
name: "Emily Brontë",
books: [
{ title: "Wuthering Heights", genre: 'Drama' }
]
},
{
name: "Charlotte Brontë",
books: [
{ title: "Jane Eyre", genre: 'Drama' }
]
},
{
name: "Edgar Allen Poe",
books: [
{ title: "Eleonora", genre: 'Romance' },
{ title: "The Raven", genre: 'Mystery' },
]
},
{
name: "Richard Carpenter",
books: [
{ title: "Catweazle", genre: 'Fantasy' }
]
}
])
})
})
describe ('Sluggified variants', () => {
test ('GET /Books', async () => {
const {data:books} = await GET ('/hcql/admin/Books')
expect(books).to.be.an('array').of.length(5)
expect(books.length).to.eql(5) //.of.length(5)
})
test ('GET /Books/201', async () => {
const {data:book} = await GET ('/hcql/admin/Books/201')
expect(book).to.be.an('object')
expect(book).to.have.property ('title', "Wuthering Heights")
})
test ('GET /Books { title, author.name as author }' , async () => {
const {data:books} = await GET ('/hcql/admin/Books { title, author.name as author } order by ID')
expect(books).to.deep.equal ([
{ title: "Wuthering Heights", author: "Emily Brontë" },
{ title: "Jane Eyre", author: "Charlotte Brontë" },
{ title: "The Raven", author: "Edgar Allen Poe" },
{ title: "Eleonora", author: "Edgar Allen Poe" },
{ title: "Catweazle", author: "Richard Carpenter" }
])
})
test ('GET /Books/201 w/ CQL tail in URL' , async () => {
const {data:book} = await GET ('/hcql/admin/Books/201 { title, author.name as author } order by ID')
expect(book).to.deep.equal ({ title: "Wuthering Heights", author: "Emily Brontë" })
})
it ('GET /Books/201 w/ CQL fragment in body' , async () => {
const {data:book} = await GET ('/hcql/admin/Books/201', {
headers: { 'Content-Type': 'text/plain' },
data: `{ title, author.name as author }`
})
expect(book).to.deep.equal ({ title: "Wuthering Heights", author: "Emily Brontë" })
})
it ('GET /Books/201 w/ CQN fragment in body' , async () => {
const {data:book} = await GET ('/hcql/admin/Books/201', {
data: cds.ql `SELECT title, author.name as author` .SELECT
})
expect(book).to.deep.equal ({ title: "Wuthering Heights", author: "Emily Brontë" })
})
it ('GET /Books/201 w/ tail in URL plus CQL/CQN fragments in body' , async () => {
const {data:[b1]} = await GET ('/hcql/admin/Books where ID=201', {
data: cds.ql `SELECT title, author.name as author` .SELECT
})
expect(b1).to.deep.equal ({ title: "Wuthering Heights", author: "Emily Brontë" })
const {data:[b2]} = await GET ('/hcql/admin/Books where ID=201', {
headers: { 'Content-Type': 'text/plain' },
data: `{ title, author.name as author }`
})
expect(b2).to.deep.equal ({ title: "Wuthering Heights", author: "Emily Brontë" })
})
})

View File

@@ -0,0 +1,225 @@
@server = http://localhost:4004
GET {{server}}/odata/v4/admin/Authors?
&$select=ID,name
&$expand=books($select=ID,title)
&$count=true
###
#
# The basic variant expects a CQN object passed as an application/json body
# to a POST request. This is also the fastest one, as it doesn't need CQL parsing.
# Note: $count is returned in X-Total-Count response header
#
GET {{server}}/hcql/admin
Content-Type: application/json
# Accept-Language: de
{ "SELECT": {
"from": { "ref": [ "Authors" ] },
"columns": [
{ "ref": [ "name" ] },
{ "ref": [ "books" ], "expand": [
{ "ref": [ "ID" ] },
{ "ref": [ "title" ] }
]}
],
"count": true
}}
###
POST {{server}}/hcql/browse/submitOrder?book=201&quantity=2
Authorization: Basic alice:
###
POST {{server}}/hcql/browse/submitOrder
Authorization: Basic alice:
Content-Type: application/json
{
"book": 201,
"quantity": 2
}
###
GET {{server}}/hcql/browse/submitOrder?book=201&quantity=2
Authorization: Basic alice:
###
#
# Alternatively you can pass a CQL string as plain/text body
#
GET {{server}}/hcql/admin
Content-Type: text/plain
# X-Total-Count: true
SELECT from Authors { name, books { title }}
# SELECT from Books { title, currency }
###
#
# In addition we offer convenience slug routes...
# .e.g. /srv/entity routes
#
GET {{server}}/hcql/admin/Books
###
GET {{server}}/hcql/admin/Books/201
###
GET {{server}}/hcql/admin/Books { ID, title, author.name as author }
###
GET {{server}}/hcql/admin/Books order by stock desc
Content-Type: text/plain
{ title, stock }
###
GET {{server}}/hcql/admin/Books/201 { ID, title, author.name }
###
GET {{server}}/hcql/admin/Books/201 { ID, title, author{name} }
###
POST {{server}}/hcql/admin/Books?title=The Black Cat&author_ID=101
###
POST {{server}}/hcql/admin/Books?title=The Black Cat
Content-Type: application/json
{
"author_ID": 101
}
###
POST {{server}}/hcql/admin/Books
Content-Type: application/json
{
"title": "The Black Cat",
"author": { "ID": 101 }
}
###
PUT {{server}}/hcql/admin/Books/275?title=Catastrophe
###
PATCH {{server}}/hcql/admin/Books/275
Content-Type: application/json
{
"title": "Catastrophe"
}
###
GET {{server}}/hcql/admin/Authors { name, books { ID, title }}
###
GET {{server}}/hcql/admin/Books { ID, title, author.name as author } order by ID desc
###
// ------------------------------------
POST {{server}}/hcql/admin
Content-Type: application/json
{"SELECT": { "from": { "ref": ["Books"] }}}
###
POST {{server}}/hcql/admin
Content-Type: text/plain
SELECT from Authors {
name as author,
books {
title,
stock,
price,
currency { * }
}
}
where name like '%Bro%'
order by name asc
###
#
# Simple REST-style URLs as supported as well
#
GET {{server}}/hcql/admin/Books
###
GET {{server}}/hcql/admin/Books/201
###
#
# REST-style URLs can be combined with trailing CQL in the path, in plain
# text body, or with projections sent as application/json array
#
GET {{server}}/hcql/admin/Books order by stock desc
###
GET {{server}}/hcql/admin/Books { title as book, stock } order by stock desc
###
GET {{server}}/hcql/admin/Authors
Content-Type: text/plain
Accept-Language: fr
{
ID, name as author,
books {
title,
stock,
currency { * }
}
}
where name like '%Bro%'
order by name asc
###
GET {{server}}/hcql/admin/Books/201 { title, stock }
###
GET {{server}}/hcql/admin/Books order by stock desc
Content-Type: text/plain
{ title, stock }
###
#
# CQL adaptor also provides access to the underlying CSN schema
#
GET {{server}}/hcql/admin/$csn
###
#
# CQL adaptor also supports INSERTs, UPDATEs, DELETEs ...
#
POST {{server}}/hcql/admin
Content-Type: application/jsonin wonderland
{ "INSERT": {
"into": "Books",
"entries": [{
"title": "The Black Cat",
"author": { "ID": 150 }
}]
}}
###

View File

@@ -0,0 +1,26 @@
@server = http://localhost:4004
GET {{server}}/odata/v2/admin/Authors
Authorization: Basic alice:
###
GET {{server}}/odata/v2/admin/Authors?$select=ID,name&$expand=books($select=ID,title)
Authorization: Basic alice:
###
GET {{server}}/odata/v4/admin/Authors
Authorization: Basic alice:
###
GET {{server}}/odata/v4/admin/Authors?$select=ID,name&$expand=books($select=ID,title)
Authorization: Basic alice:
###
GET {{server}}/rest/admin/Authors
Authorization: Basic alice:
###
GET {{server}}/rest/admin/Authors?$select=ID,name&$expand=books($select=ID,title)
Authorization: Basic alice:
###

View File

@@ -0,0 +1,9 @@
@server = http://localhost:4004
GET {{server}}/rest/admin/Authors
Authorization: Basic alice:
###
GET {{server}}/rest/admin/Authors?$select=ID,name&$expand=books($select=ID,title)
Authorization: Basic alice:
###

View File

@@ -0,0 +1,4 @@
using { CatalogService, AdminService } from '@capire/bookstore';
annotate CatalogService with @hcql @odata @path:'browse' @requires:[];
annotate AdminService with @hcql @odata @path:'admin';