chore: Merged fiori into bookstore and tests into subprojects
This commit is contained in:
53
bookshop/test/consuming-actions.test.js
Normal file
53
bookshop/test/consuming-actions.test.js
Normal file
@@ -0,0 +1,53 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
82
bookshop/test/consuming-services.test.js
Normal file
82
bookshop/test/consuming-services.test.js
Normal file
@@ -0,0 +1,82 @@
|
||||
const cds = require('@sap/cds')
|
||||
const { expect } = cds.test ('@capire/bookshop')
|
||||
cds.User.default = cds.User.privileged // disable auth checks
|
||||
|
||||
describe('cap/samples - Consuming Services locally', () => {
|
||||
|
||||
it('bootstrapped the database successfully', ()=>{
|
||||
const { AdminService } = cds.services
|
||||
const { Authors } = AdminService.entities
|
||||
expect(AdminService).to.exist
|
||||
expect(Authors).to.exist
|
||||
})
|
||||
|
||||
it('supports targets as strings or reflected defs', async () => {
|
||||
const AdminService = await cds.connect.to('AdminService')
|
||||
const { Authors } = AdminService.entities
|
||||
expect (await SELECT.from(Authors))
|
||||
// .to.eql(await SELECT.from('Authors'))
|
||||
.to.eql(await AdminService.read(Authors))
|
||||
.to.eql(await AdminService.read('Authors'))
|
||||
.to.eql(await AdminService.run(SELECT.from(Authors)))
|
||||
.to.eql(await AdminService.run(SELECT.from('Authors')))
|
||||
})
|
||||
|
||||
it('allows reading from local services using cds.ql', async () => {
|
||||
const AdminService = await cds.connect.to('AdminService')
|
||||
const authors = await AdminService.read (`Authors`, a => {
|
||||
a.name,
|
||||
a.books((b) => {
|
||||
b.title,
|
||||
b.currency((c) => {
|
||||
c.name, c.symbol
|
||||
})
|
||||
})
|
||||
}).where(`name like`, 'E%')
|
||||
expect(authors).to.containSubset([
|
||||
{
|
||||
name: 'Emily Brontë',
|
||||
books: [
|
||||
{
|
||||
title: 'Wuthering Heights',
|
||||
currency: { name: 'British Pound', symbol: '£' },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Edgar Allen Poe',
|
||||
books: [
|
||||
{ title: 'The Raven', currency: { name: 'US Dollar', symbol: '$' } },
|
||||
{ title: 'Eleonora', currency: { name: 'US Dollar', symbol: '$' } },
|
||||
],
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('provides CRUD-style convenience methods', async () => {})
|
||||
|
||||
it('uses same methods for all kind of services, including dbs', async () => {
|
||||
const srv = await cds.connect.to('AdminService')
|
||||
const db = await cds.connect.to('db')
|
||||
const { Authors } = srv.entities
|
||||
const projection = (a) => {
|
||||
a.name,
|
||||
a.books((b) => {
|
||||
b.title,
|
||||
b.currency((c) => {
|
||||
c.name, c.symbol
|
||||
})
|
||||
})
|
||||
}
|
||||
const query1 = SELECT.from(Authors, projection).where(`name like`, 'E%')
|
||||
const query2 = cds.read(Authors, projection).where(`name like`, 'E%')
|
||||
expect(await cds.run(query1))
|
||||
.to.eql(await db.run(query1))
|
||||
.to.eql(await srv.run(query1))
|
||||
.to.eql(await srv.read(Authors, projection).where(`name like`, 'E%'))
|
||||
.to.eql(await cds.run(query2))
|
||||
.to.eql(await db.run(query2))
|
||||
.to.eql(await srv.run(query2))
|
||||
.to.eql(await db.read(Authors, projection).where(`name like`, 'E%'))
|
||||
})
|
||||
})
|
||||
15
bookshop/test/custom-handlers.test.js
Normal file
15
bookshop/test/custom-handlers.test.js
Normal file
@@ -0,0 +1,15 @@
|
||||
const cds = require('@sap/cds')
|
||||
const { GET, POST, expect } = cds.test(__dirname+'/..')
|
||||
cds.User.default = cds.User.Privileged // hard core monkey patch
|
||||
|
||||
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/)
|
||||
const { data } = await GET`/admin/Books/201/stock/$value`
|
||||
expect(data).to.equal(2)
|
||||
})
|
||||
})
|
||||
123
bookshop/test/hierarchical-data/hierarchical-data.test.js
Normal file
123
bookshop/test/hierarchical-data/hierarchical-data.test.js
Normal file
@@ -0,0 +1,123 @@
|
||||
const cds = require('@sap/cds')
|
||||
const { expect } = cds.test.in(__dirname,'..','..')
|
||||
|
||||
describe('cap/samples - Hierarchical Data', ()=>{
|
||||
|
||||
const csn = CDL`
|
||||
entity Categories {
|
||||
key ID : Integer;
|
||||
name : String;
|
||||
children : Composition of many Categories on children.parent = $self;
|
||||
parent : Association to Categories;
|
||||
}
|
||||
`
|
||||
const model = cds.compile.for.nodejs(csn)
|
||||
const {Categories:Cats} = model.definitions
|
||||
|
||||
before ('bootstrap sqlite in-memory db...', async()=>{
|
||||
await cds.deploy (csn) .to ('sqlite::memory:') // REVISIT: cds.compile.to.sql should accept cds.compiled.for.nodejs models
|
||||
expect (cds.db) .to.exist
|
||||
expect (cds.db.model) .to.exist
|
||||
})
|
||||
|
||||
it ('supports deeply nested inserts', ()=> INSERT.into (Cats,
|
||||
{ ID:100, name:'Some Cats...', children:[
|
||||
{ ID:101, name:'Cat', children:[
|
||||
{ ID:102, name:'Kitty', children:[
|
||||
{ ID:103, name:'Kitty Cat', children:[
|
||||
{ ID:104, name:'Aristocat' } ]},
|
||||
{ ID:105, name:'Kitty Bat' } ]},
|
||||
{ ID:106, name:'Catwoman', children:[
|
||||
{ ID:107, name:'Catalina' } ]} ]},
|
||||
{ ID:108, name:'Catweazle' }
|
||||
]}
|
||||
))
|
||||
|
||||
it ('should generate correct queries for expands', ()=>{
|
||||
let q = SELECT.from (Cats, c => { c.ID, c.name, c.children (c => c.name) })
|
||||
expect (q) .to.eql ({
|
||||
SELECT: {
|
||||
from: { ref:[ "Categories" ] },
|
||||
columns: [
|
||||
{ ref: [ "ID" ] },
|
||||
{ ref: [ "name" ] },
|
||||
{ ref: [ "children" ], expand: [ {ref:['name']} ] },
|
||||
]
|
||||
}
|
||||
})
|
||||
/* temp skip for release
|
||||
if (q.forSQL) expect (q.forSQL()) .to.eql ({
|
||||
SELECT: {
|
||||
from: { ref:[ "Categories" ], as: "Categories" },
|
||||
columns: [
|
||||
{ ref: [ "Categories", "ID" ] },
|
||||
{ ref: [ "Categories", "name" ] },
|
||||
{ as: "children", SELECT: { expand: true,
|
||||
one: false,
|
||||
columns: [{ ref: [ "children", "name" ]}],
|
||||
from: { ref:["Categories"], as: "children" },
|
||||
where: [
|
||||
{ref:[ "Categories", "ID" ]}, "=", {ref:[ "children", "parent_ID" ]}
|
||||
],
|
||||
}},
|
||||
],
|
||||
}
|
||||
})
|
||||
if (q.toSql) expect (q.toSql()) .to.eql (
|
||||
`SELECT json_insert('{}',` +
|
||||
`'$."ID"',ID,` +
|
||||
`'$."name"',name,` +
|
||||
`'$."children"',children->'$'` +
|
||||
`) as _json_ FROM (` +
|
||||
`SELECT Categories.ID,Categories.name,(` +
|
||||
`SELECT jsonb_group_array(jsonb_insert('{}','$."name"',name)) as _json_ FROM (` +
|
||||
`SELECT children.name FROM Categories as children WHERE Categories.ID = children.parent_ID` +
|
||||
`)` +
|
||||
`) as children FROM Categories as Categories` +
|
||||
`)`
|
||||
)
|
||||
*/
|
||||
})
|
||||
|
||||
it ('supports nested reads', ()=> expect (
|
||||
SELECT.one.from (Cats, c=>{
|
||||
c.ID, c.name.as('parent'), c.children (c=>{
|
||||
c.name.as('child')
|
||||
})
|
||||
}) .where ({name:'Cat'})
|
||||
) .to.eventually.eql (
|
||||
{ ID:101, parent:'Cat', children:[
|
||||
{ child:'Kitty' },
|
||||
{ child:'Catwoman' },
|
||||
]}
|
||||
))
|
||||
|
||||
it ('supports deeply nested reads', ()=> expect (
|
||||
SELECT.one.from (Cats, c=>{
|
||||
c.ID, c.name, c.children (
|
||||
c => { c.name },
|
||||
{levels:3}
|
||||
)
|
||||
}) .where ({name:'Cat'})
|
||||
) .to.eventually.eql (
|
||||
{ ID:101, name:'Cat', children:[
|
||||
{ name:'Kitty', children:[
|
||||
{ name:'Kitty Cat', children:[
|
||||
{ name:'Aristocat' }, ]}, // level 3
|
||||
{ name:'Kitty Bat', children:[] }, ]},
|
||||
{ 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 ([
|
||||
{ ID:100, name:'Some Cats...' },
|
||||
{ ID:101, name:'Cat' },
|
||||
{ ID:108, name:'Catweazle' }
|
||||
])
|
||||
})
|
||||
|
||||
})
|
||||
@@ -1,4 +1,4 @@
|
||||
using { sap.capire.bookshop as my } from '../db/schema';
|
||||
using { sap.capire.bookshop as my } from '../../db/schema';
|
||||
service TestService {
|
||||
entity Genres as projection on my.Genres;
|
||||
}
|
||||
5
bookshop/test/localized-data/package.json
Normal file
5
bookshop/test/localized-data/package.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"devDependencies": {
|
||||
"@cap-js/sqlite": "*"
|
||||
}
|
||||
}
|
||||
12
bookshop/test/localized-data/services.cds
Normal file
12
bookshop/test/localized-data/services.cds
Normal file
@@ -0,0 +1,12 @@
|
||||
using { CatalogService, sap.capire.bookshop as my } from '@capire/bookshop';
|
||||
using from '@capire/common';
|
||||
|
||||
extend service CatalogService with {
|
||||
@cds.localized:false
|
||||
entity BooksSans as projection on my.Books {
|
||||
*, //> non-localized defaults, e.g. title
|
||||
key ID,
|
||||
texts.title as localized_title,
|
||||
texts.locale
|
||||
};
|
||||
}
|
||||
79
bookshop/test/localized-data/services.test.js
Normal file
79
bookshop/test/localized-data/services.test.js
Normal file
@@ -0,0 +1,79 @@
|
||||
const cds = require('@sap/cds')
|
||||
const { GET, expect } = cds.test (__dirname)
|
||||
cds.User.default = cds.User.Privileged // hard core monkey patch
|
||||
|
||||
describe('cap/samples - Localized Data', () => {
|
||||
|
||||
it('serves localized $metadata documents', async () => {
|
||||
const { data } = await GET(`/browse/$metadata?sap-language=de`, { headers: { 'accept-language': 'de' }})
|
||||
expect(data).to.contain('<Annotation Term="Common.Label" String="Währung"/>')
|
||||
})
|
||||
|
||||
it('supports accept-language header', async () => {
|
||||
const { data } = await GET(`/browse/Books?$select=title,author`, {
|
||||
headers: { 'Accept-Language': 'de' },
|
||||
})
|
||||
expect(data.value).to.containSubset([
|
||||
{ title: 'Sturmhöhe', 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' },
|
||||
])
|
||||
})
|
||||
|
||||
it('supports queries with $expand', async () => {
|
||||
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ë', 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' } },
|
||||
])
|
||||
})
|
||||
|
||||
it('supports queries with nested $expand', async () => {
|
||||
const { data } = await GET(`/admin/Authors`, {
|
||||
params: {
|
||||
$filter: `startswith(name,'E')`,
|
||||
$expand: `books(
|
||||
$select=title;
|
||||
$expand=currency(
|
||||
$select=name,symbol
|
||||
)
|
||||
)`.replace(/\s/g, ''),
|
||||
$select: `name`,
|
||||
},
|
||||
headers: { 'Accept-Language': 'de' },
|
||||
})
|
||||
expect(data.value).to.containSubset([
|
||||
{
|
||||
name: 'Emily Brontë',
|
||||
books: [{ title: 'Sturmhöhe', currency: { name: 'Pfund', symbol: '£' } }],
|
||||
},
|
||||
{
|
||||
name: 'Edgar Allen Poe',
|
||||
books: [
|
||||
{ title: 'The Raven', currency: { name: 'US-Dollar', symbol: '$' } },
|
||||
{ title: 'Eleonora', currency: { name: 'US-Dollar', symbol: '$' } },
|
||||
],
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('supports @cds.localized:false', async ()=>{
|
||||
const { data } = await GET(`/browse/BooksSans?&$select=title,localized_title&$expand=currency&$filter=locale eq 'de' or locale eq null`, {
|
||||
headers: { 'Accept-Language': 'de' },
|
||||
})
|
||||
expect(data.value).to.containSubset([
|
||||
{ title: 'Wuthering Heights', localized_title: 'Sturmhöhe', currency: { name: 'British Pound' } },
|
||||
{ title: 'Jane Eyre', currency: { name: 'British Pound' } },
|
||||
{ title: 'The Raven', currency: { name: 'US Dollar' } },
|
||||
{ title: 'Eleonora', currency: { name: 'US Dollar' } },
|
||||
{ title: 'Catweazle', currency: { name: 'Yen' } },
|
||||
])
|
||||
})
|
||||
})
|
||||
74
bookshop/test/messaging.test.js
Normal file
74
bookshop/test/messaging.test.js
Normal file
@@ -0,0 +1,74 @@
|
||||
const cds = require('@sap/cds')
|
||||
const { expect } = cds.test.in(__dirname,'..')
|
||||
|
||||
describe('cap/samples - Messaging', ()=>{
|
||||
|
||||
const _model = '@capire/reviews'
|
||||
const Reviews = 'sap.capire.reviews.Reviews'
|
||||
beforeAll(()=>{
|
||||
cds.User.default = cds.User.Privileged // hard core monkey patch
|
||||
})
|
||||
|
||||
it ('should bootstrap sqlite in-memory db', async()=>{
|
||||
const db = await cds.deploy (_model) .to ('sqlite::memory:')
|
||||
await db.delete(Reviews)
|
||||
expect (db.model) .not.undefined
|
||||
})
|
||||
|
||||
let srv
|
||||
it ('should serve ReviewsService', async()=>{
|
||||
srv = await cds.serve('ReviewsService') .from (_model)
|
||||
expect (srv.name) .to.match (/ReviewsService/)
|
||||
})
|
||||
|
||||
let N=0, received=[], M=0
|
||||
it ('should add messaging event handlers', ()=>{
|
||||
srv.on('reviewed', (msg)=> received.push(msg))
|
||||
})
|
||||
|
||||
it ('should add more messaging event handlers', ()=>{
|
||||
srv.on('reviewed', ()=> ++M)
|
||||
})
|
||||
|
||||
it ('should add review', async ()=>{
|
||||
const review = { subject: "201", title: "Captivating", rating: ++N }
|
||||
cds._debug = 1
|
||||
const response = await srv.create ('Reviews') .entries (review)
|
||||
expect (response) .to.containSubset (review)
|
||||
})
|
||||
|
||||
it ('should add more reviews', ()=> Promise.all ([
|
||||
// REVISIT: mass operation should trigger one message per entry
|
||||
// srv.create('Reviews').entries(
|
||||
// { ID: 111 + (++N), subject: "201", title: "Captivating", rating: N },
|
||||
// { ID: 111 + (++N), subject: "201", title: "Captivating", rating: N },
|
||||
// { ID: 111 + (++N), subject: "201", title: "Captivating", rating: N },
|
||||
// { ID: 111 + (++N), subject: "201", title: "Captivating", rating: N },
|
||||
// ),
|
||||
srv.create ('Reviews') .entries (
|
||||
{ ID: String(111 + (++N)), subject: "201", title: "Captivating", rating: N }
|
||||
),
|
||||
srv.create ('Reviews') .entries (
|
||||
{ ID: String(111 + (++N)), subject: "201", title: "Captivating", rating: N }
|
||||
),
|
||||
srv.create ('Reviews') .entries (
|
||||
{ ID: String(111 + (++N)), subject: "201", title: "Captivating", rating: N }
|
||||
),
|
||||
srv.create ('Reviews') .entries (
|
||||
{ ID: String(111 + (++N)), subject: "201", title: "Captivating", rating: N }
|
||||
),
|
||||
]))
|
||||
|
||||
it ('should have received all messages', async()=> {
|
||||
await new Promise((done)=>setImmediate(done))
|
||||
expect(M).equals(N)
|
||||
expect(received.length).equals(N)
|
||||
expect(received.map(m=>m.data)).to.deep.equal([
|
||||
{ count: 1, subject: '201', rating: 1 },
|
||||
{ count: 2, subject: '201', rating: 1.5 },
|
||||
{ count: 3, subject: '201', rating: 2 },
|
||||
{ count: 4, subject: '201', rating: 2.5 },
|
||||
{ count: 5, subject: '201', rating: 3 },
|
||||
])
|
||||
})
|
||||
})
|
||||
101
bookshop/test/odata.test.js
Normal file
101
bookshop/test/odata.test.js
Normal file
@@ -0,0 +1,101 @@
|
||||
const cds = require('@sap/cds')
|
||||
const { GET, expect, axios } = cds.test ('@capire/bookshop')
|
||||
axios.defaults.auth = { username: 'alice', password: 'admin' }
|
||||
|
||||
describe('cap/samples - Bookshop APIs', () => {
|
||||
|
||||
it('serves $metadata documents in v4', async () => {
|
||||
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
|
||||
'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"/>')
|
||||
})
|
||||
|
||||
it('serves ListOfBooks?$expand=genre,currency', async () => {
|
||||
const Mystery = { name: 'Mystery' }
|
||||
const Romance = { name: 'Romance' }
|
||||
const USD = { code: 'USD', name: 'US Dollar', descr: null, symbol: '$' }
|
||||
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: '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` },
|
||||
}}`
|
||||
expect(data.value).to.containSubset([
|
||||
{ ID: 201, title: 'Wuthering Heights', author: 'Emily Brontë' },
|
||||
{ ID: 207, title: 'Jane Eyre', author: 'Charlotte Brontë' },
|
||||
{ ID: 251, title: 'The Raven', author: 'Edgar Allen Poe' },
|
||||
{ ID: 252, title: 'Eleonora', author: 'Edgar Allen Poe' },
|
||||
])
|
||||
})
|
||||
|
||||
it('supports $select', async () => {
|
||||
const { data } = await GET(`/browse/Books`, {
|
||||
params: { $select: `ID,title` },
|
||||
})
|
||||
expect(data.value).to.containSubset([
|
||||
{ ID: 201, title: 'Wuthering Heights' },
|
||||
{ ID: 207, title: 'Jane Eyre' },
|
||||
{ ID: 251, title: 'The Raven' },
|
||||
{ ID: 252, title: 'Eleonora' },
|
||||
{ ID: 271, title: 'Catweazle' },
|
||||
])
|
||||
})
|
||||
|
||||
it('supports $expand', async () => {
|
||||
const { data } = await GET(`/admin/Authors`, {
|
||||
params: {
|
||||
$select: `name`,
|
||||
$expand: `books($select=title)`,
|
||||
},
|
||||
})
|
||||
expect(data.value).to.containSubset([
|
||||
{ name: 'Emily Brontë', books: [{ title: 'Wuthering Heights' }] },
|
||||
{ name: 'Charlotte Brontë', books: [{ title: 'Jane Eyre' }] },
|
||||
{ name: 'Edgar Allen Poe', books: [{ title: 'The Raven' }, { title: 'Eleonora' }] },
|
||||
{ name: 'Richard Carpenter', books: [{ title: 'Catweazle' }] },
|
||||
])
|
||||
})
|
||||
|
||||
it('supports $value requests', async () => {
|
||||
const { data } = await GET`/admin/Books/201/stock/$value`
|
||||
expect(data).to.equal(12)
|
||||
})
|
||||
|
||||
it('supports $top/$skip paging', async () => {
|
||||
const { data: p1 } = await GET`/browse/Books?$select=title&$top=3`
|
||||
expect(p1.value).to.containSubset([
|
||||
{ ID: 201, title: 'Wuthering Heights' },
|
||||
{ ID: 207, title: 'Jane Eyre' },
|
||||
{ ID: 251, title: 'The Raven' },
|
||||
])
|
||||
const { data: p2 } = await GET`/browse/Books?$select=title&$skip=3`
|
||||
expect(p2.value).to.containSubset([
|
||||
{ ID: 252, title: 'Eleonora' },
|
||||
{ ID: 271, title: 'Catweazle' },
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
it('serves user info', async () => {
|
||||
const { data: alice } = await GET `/user/me`
|
||||
expect(alice).to.containSubset({ id: 'alice' })
|
||||
const { data: joe } = await GET (`/user/me`, {auth: { username: 'joe' }})
|
||||
expect(joe).to.containSubset({ id: 'joe' })
|
||||
})
|
||||
|
||||
})
|
||||
126
bookshop/test/protocols/hcql-adapter.test.js
Normal file
126
bookshop/test/protocols/hcql-adapter.test.js
Normal 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ë" })
|
||||
})
|
||||
|
||||
})
|
||||
225
bookshop/test/protocols/hcql.http
Normal file
225
bookshop/test/protocols/hcql.http
Normal 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 }
|
||||
}]
|
||||
}}
|
||||
###
|
||||
26
bookshop/test/protocols/odata.http
Normal file
26
bookshop/test/protocols/odata.http
Normal 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:
|
||||
###
|
||||
9
bookshop/test/protocols/rest.http
Normal file
9
bookshop/test/protocols/rest.http
Normal 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:
|
||||
###
|
||||
4
bookshop/test/protocols/services.cds
Normal file
4
bookshop/test/protocols/services.cds
Normal 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';
|
||||
Reference in New Issue
Block a user