Merge branch 'main' into add/customBuildTask

This commit is contained in:
Lothar Bender
2021-12-06 10:10:14 +01:00
25 changed files with 174 additions and 184 deletions

View File

@@ -37,7 +37,7 @@
"title": "Authors service"
},
{
"file": "bookstore/db/sqlite/index.cds",
"file": "fiori/db/sqlite/index.cds",
"description": "#### SQLite Implementation\n\nHere's the first implementation for SQLite. It computes the two fields `age` and `lifetime` through SQLite's [strftime](https://sqlite.org/lang_datefunc.html) function.\n\nThrough the [`extend projection`](https://cap.cloud.sap/docs/cds/cdl#extend-view) clause you can add additional fields to projection entities. These are deployed as database views, which is why we can integrate the database functions in the first place.\n",
"selection": {
"start": {
@@ -52,8 +52,8 @@
"title": "SQLite implementation"
},
{
"file": "bookstore/db/hana/index.cds",
"description": "#### SAP HANA Implementation\n\nThis is the second implementation for SAP HANA. It computes the same two fields `age` and `lifetime` through the [YEARS_BETWEEN](https://help.sap.com/viewer/7c78579ce9b14a669c1f3295b0d8ca16/Cloud/en-US/7c0d2c161ea34def86de3f5eadd6a0af.html) and [YEAR](https://help.sap.com/viewer/7c78579ce9b14a669c1f3295b0d8ca16/Cloud/en-US/20f5fac6751910148dabd3c6821f907d.html) functions of SAP HANA.\n\n#### File Layout and Code Structure\n\nNote the path of the `.cds` file we are in: it's in a subfolder of `db`, so that it's _not_ automatically picked up when we start the application. The same is true for the SQLite implementation: it's in a separate `db/sqlite/` folder as well. In the next step, you'll see how these files are loaded.\n\nAlso, we choose to implement all of that as an extension of the original bookshop here in the _bookstore_ package. See the first [CAP Samples] code tour for more details on the different packages of this repository.",
"file": "fiori/db/hana/index.cds",
"description": "#### SAP HANA Implementation\n\nThis is the second implementation for SAP HANA. It computes the same two fields `age` and `lifetime` through the [YEARS_BETWEEN](https://help.sap.com/viewer/7c78579ce9b14a669c1f3295b0d8ca16/Cloud/en-US/7c0d2c161ea34def86de3f5eadd6a0af.html) and [YEAR](https://help.sap.com/viewer/7c78579ce9b14a669c1f3295b0d8ca16/Cloud/en-US/20f5fac6751910148dabd3c6821f907d.html) functions of SAP HANA.\n\n#### File Layout and Code Structure\n\nNote the path of the `.cds` file we are in: it's in a subfolder of `db`, so that it's _not_ automatically picked up when we start the application. The same is true for the SQLite implementation: it's in a separate `db/sqlite/` folder as well. In the next step, you'll see how these files are loaded.\n\nAlso, we choose to implement all of that as an extension of the original bookshop here in the _fiori_ package. See the first [CAP Samples] code tour for more details on the different packages of this repository.",
"selection": {
"start": {
"line": 7,
@@ -67,25 +67,34 @@
"title": "SAP HANA implementation"
},
{
"file": "bookstore/package.json",
"description": "#### Configuration\n\nThe `cds` section in `package.json` is a place to configure which of the `db/sqlite` and `db/hana` folders are used for which database.\nWe use [Node.js profiles](https://cap.cloud.sap/docs/node.js/cds-env#profiles) to separate the configuration.\nIn the `development` profile, you can see that `db/sqlite` is set as the model, while the `db/hana` folder is configured in the `production` profile.",
"line": 12,
"file": "fiori/package.json",
"description": "#### Configuration\n\nThe `cds.requires` section in `package.json` is a place to configure which of the `db/sqlite` and `db/hana` folders are used for which database.\n\nWe use [Node.js profiles](https://cap.cloud.sap/docs/node.js/cds-env#profiles) to separate the configuration.\nIn the `development` profile, you can see that `db/sqlite` is set as the model, while the `db/hana` folder is configured in the `production` profile. `db-ext` is a pseudo datasource, its name doesn't matter.\n\nSee [`cds.resolve`](https://cap.cloud.sap/docs/node.js/cds-compile#cds-resolve) to learn more about how models are found.",
"selection": {
"start": {
"line": 41,
"character": 1
},
"end": {
"line": 48,
"character": 1
}
},
"title": "Configuration"
},
{
"file": "bookstore/package.json",
"description": "#### Run with SQLite\n\nTo run with `development` and an in-memory SQLite database, you don't need to do anything special, because it's activated by default. Just run:\n\n>> cds watch bookstore\n\nThen open [http://localhost:4004/admin/Authors](http://localhost:4004/admin/Authors) to see the two new fields.\n",
"line": 30,
"file": "fiori/package.json",
"description": "#### Run with SQLite\n\nTo run with `development` and an in-memory SQLite database, you don't need to do anything special, because it's activated by default. Just run:\n\n>> cds watch fiori\n\nThen open [http://localhost:4004/admin/Authors](http://localhost:4004/admin/Authors) to see the two new fields.\n",
"line": 43,
"title": "Run with SQLite"
},
{
"file": "bookstore/package.json",
"description": "#### Deploy the CDS Model to SAP HANA\n\nTo 'activate' SAP HANA through the `production` profile, you can use the global `--production` flag:\n\n>> cd bookstore; cds deploy --to hana --production\n\n[Learn more about SAP HANA deployment](https://cap.cloud.sap/docs/guides/databases#get-hana)\n\n#### Run the Application\n\n>> cd bookstore; cds watch --production\n\nThe service on [http://localhost:4004/admin/Authors](http://localhost:4004/admin/Authors) is the same as before, but this time the `Authors` entity is backed by a database view with an SAP HANA function.\n\n#### More\n\nIf you don't see data, you can add some in the next step.",
"line": 33,
"file": "fiori/package.json",
"description": "#### Deploy the CDS Model to SAP HANA\n\nTo 'activate' SAP HANA through the `production` profile, you can use the global `--production` flag:\n\n>> cd fiori; cds deploy --to hana --production\n\n[Learn more about SAP HANA deployment](https://cap.cloud.sap/docs/guides/databases#get-hana)\n\n#### Run the Application\n\n>> cd fiori; cds watch --production\n\nThe service on [http://localhost:4004/admin/Authors](http://localhost:4004/admin/Authors) is the same as before, but this time the `Authors` entity is backed by a database view with an SAP HANA function.\n\n#### More\n\nIf you don't see data, you can add some in the next step.",
"line": 46,
"title": "Run with SAP HANA"
},
{
"file": "bookstore/test/requests.http",
"file": "fiori/test/requests.http",
"description": "### Add More Data\n\nOptionally you can add some `Authors` data by clicking on the _Send Request_ link (provided by the [REST client](https://marketplace.visualstudio.com/items?itemName=humao.rest-client) extension).",
"line": 72,
"selection": {
@@ -104,6 +113,5 @@
"title": "Wrap-up",
"description": "### Summary\n\nThat's it! You have seen: \n- How to integrate database-specific functions in a CDS model.\n- How to switch between the two implementations for SQLite and SAP HANA."
}
],
"ref": "main"
]
}

View File

@@ -3,7 +3,3 @@ service AdminService @(requires:'admin') {
entity Books as projection on my.Books;
entity Authors as projection on my.Authors;
}
//Since ID is computed, we can hide the popup for ID on Create
annotate AdminService.Books with { ID @Core.Computed; }
annotate AdminService.Authors with { ID @Core.Computed; }

View File

@@ -1,42 +1,16 @@
using {sap.capire.bookshop as my} from '../db/schema';
using { sap.capire.bookshop as my } from '../db/schema';
service CatalogService @(path:'/browse') {
service CatalogService @(path : '/browse') {
/** For displaying lists of Books */
@readonly entity ListOfBooks as projection on Books
excluding { descr };
/**
* For displaying lists of Books
*/
@readonly
entity ListOfBooks as projection on Books excluding {
descr
};
/** For display in details pages */
@readonly entity Books as projection on my.Books { *,
author.name as author
} excluding { createdBy, modifiedBy };
/**
* For display in details pages
*/
@readonly
entity Books as projection on my.Books {
* , author.name as authorName
} excluding {
createdBy,
modifiedBy
};
@readonly
entity Authors as projection on my.Authors {
* , books : redirected to Books
} excluding {
createdBy,
modifiedBy
};
@requires : 'authenticated-user'
action submitOrder(book : Books:ID, quantity : Integer) returns {
stock : Integer
};
event OrderedBook : {
book : Books:ID;
quantity : Integer;
buyer : String
};
@requires: 'authenticated-user'
action submitOrder ( book: Books:ID, quantity: Integer ) returns { stock: Integer };
event OrderedBook : { book: Books:ID; quantity: Integer; buyer: String };
}

View File

@@ -7,13 +7,12 @@ class CatalogService extends cds.ApplicationService { init(){
// Reduce stock of ordered books if available stock suffices
this.on ('submitOrder', async req => {
const {book,quantity} = req.data
if (quantity < 1) return req.reject (400,`quantity has to be 1 or more`)
let {stock} = await SELECT `stock` .from (Books,book)
if (stock >= quantity) {
await UPDATE (Books,book) .with (`stock -=`, quantity)
await this.emit ('OrderedBook', { book, quantity, buyer:req.user.id })
return { stock }
}
else return req.error (409,`${quantity} exceeds stock for book #${book}`)
if (quantity > stock) return req.reject (409,`${quantity} exceeds stock for book #${book}`)
await UPDATE (Books,book) .with ({ stock: stock -= quantity })
await this.emit ('OrderedBook', { book, quantity, buyer:req.user.id })
return { stock }
})
// Add some discount for overstocked books

View File

@@ -32,6 +32,19 @@ GET {{server}}/admin/Authors?
# &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",
"age": 22
}
### ------------------------------------------------------------------------
# Create book
POST {{server}}/admin/Books

View File

@@ -1,8 +0,0 @@
using { sap.capire.bookshop } from '@capire/bookshop';
// Forward-declare calculated fields to be filled in database-specific ways
// TODO find a better way to have 'default' fields that still can be overwritten.
extend bookshop.Authors with {
virtual age: Integer;
virtual lifetime: String;
}

View File

@@ -1,4 +1,2 @@
namespace sap.capire.bookshop; //> important for reflection
using from '@capire/bookshop';
using from './db/schema';
using from './srv/mashup';

View File

@@ -25,13 +25,7 @@
"[production]": { "kind": "enterprise-messaging" }
},
"db": {
"kind": "sql",
"[development]": {
"model": "db/sqlite"
},
"[production]": {
"model": "db/hana"
}
"kind": "sql"
}
},
"log": { "service": true }

View File

@@ -0,0 +1,47 @@
using {AdminService} from '@capire/bookshop';
annotate AdminService.Authors with @odata.draft.enabled;
////////////////////////////////////////////////////////////////////////////
//
// Authors Object Page
//
annotate AdminService.Authors with @(UI : {
HeaderInfo : {
TypeName : 'Author',
TypeNamePlural : 'Authors',
Description : {Value : lifetime}
},
Facets : [
{
$Type : 'UI.ReferenceFacet',
Label : '{i18n>Details}',
Target : '@UI.FieldGroup#Details'
},
{
$Type : 'UI.ReferenceFacet',
Label : '{i18n>Books}',
Target : 'books/@UI.LineItem'
},
],
FieldGroup #Details : {Data : [
{Value : placeOfBirth},
{Value : placeOfDeath},
{Value : dateOfBirth},
{Value : dateOfDeath},
{
Value : age,
Label : '{i18n>Age}'
},
]},
});
// Workaround to avoid errors for unknown db-specific calculated fields above
extend sap.capire.bookshop.Authors with {
virtual age : Integer;
virtual lifetime : String;
}
// Workaround for Fiori popup for asking user to enter a new UUID on Create
annotate AdminService.Authors with { ID @Core.Computed; }

View File

@@ -3,15 +3,15 @@
"sap.app": {
"id": "authors",
"type": "application",
"title": "Browse Authors",
"title": "Manage Authors",
"description": "Sample Application",
"i18n": "i18n/i18n.properties",
"applicationVersion": {
"version": "1.0.0"
},
"dataSources": {
"CatalogService": {
"uri": "/browse/",
"AdminService": {
"uri": "/admin/",
"type": "OData",
"settings": {
"odataVersion": "4.0"
@@ -41,7 +41,7 @@
"subTitle": "{{appSubTitle}}",
"icon": "sap-icon://SAP-icons-TNT/user",
"indicatorDataSource": {
"dataSource": "CatalogService",
"dataSource": "AdminService",
"path": "Authors/$count",
"refresh": 1800
}
@@ -62,7 +62,7 @@
"uri": "i18n/i18n.properties"
},
"": {
"dataSource": "CatalogService",
"dataSource": "AdminService",
"settings": {
"synchronizationMode": "None",
"operationMode": "Server",

View File

@@ -40,27 +40,6 @@ annotate AdminService.Books with @(
}
);
annotate AdminService.Authors with @(
UI: {
HeaderInfo: {
Description: {Value: lifetime}
},
Facets: [
{$Type: 'UI.ReferenceFacet', Label: '{i18n>Details}', Target: '@UI.FieldGroup#Details'},
{$Type: 'UI.ReferenceFacet', Label: '{i18n>Books}', Target: 'books/@UI.LineItem'},
],
FieldGroup#Details: {
Data: [
{Value: placeOfBirth},
{Value: placeOfDeath},
{Value: dateOfBirth},
{Value: dateOfDeath},
{Value: age, Label: '{i18n>Age}'},
]
},
}
);
////////////////////////////////////////////////////////////
@@ -93,3 +72,6 @@ using { sap } from '@sap/cds/common';
extend service AdminService {
@readonly entity Languages as projection on sap.common.Languages;
}
// Workaround for Fiori popup for asking user to enter a new UUID on Create
annotate AdminService.Books with { ID @Core.Computed; }

View File

@@ -1,6 +1,6 @@
sap.ui.define(["sap/fe/core/AppComponent"], function(AppComponent) {
"use strict";
return AppComponent.extend("admin.Component", {
return AppComponent.extend("books.Component", {
metadata: { manifest: "json" }
});
});

View File

@@ -1,7 +1,7 @@
{
"_version": "1.8.0",
"sap.app": {
"id": "admin",
"id": "books",
"type": "application",
"title": "Manage Books",
"description": "Sample Application",

View File

@@ -19,14 +19,6 @@
"title": "Browse Books",
"targetURL": "#Books-display"
}
},
{
"id": "BrowseAuthors",
"tileType": "sap.ushell.ui.tile.StaticTile",
"properties": {
"title": "Browse Authors",
"targetURL": "#Authors-display"
}
}
]
},
@@ -45,6 +37,14 @@
"targetURL": "#Books-manage"
}
},
{
"id": "ManageAuthors",
"tileType": "sap.ushell.ui.tile.StaticTile",
"properties": {
"title": "Manage Authors",
"targetURL": "#Authors-display"
}
},
{
"id": "ManageOrders",
"tileType": "sap.ushell.ui.tile.StaticTile",
@@ -75,10 +75,10 @@
"signature": {
"parameters": {
"Books.ID": {
"renameTo": "ID"
"renameTo": "ID"
},
"Authors.books.ID": {
"renameTo": "ID"
"renameTo": "ID"
}
},
"additionalParameters": "ignored"
@@ -104,7 +104,7 @@
"resolutionResult": {
"applicationType": "SAPUI5",
"additionalInformation": "SAPUI5.Component=authors",
"url": "/authors/webapp"
"url": "/admin-authors/webapp"
}
},
"ManageBooks": {
@@ -117,8 +117,8 @@
},
"resolutionResult": {
"applicationType": "SAPUI5",
"additionalInformation": "SAPUI5.Component=admin",
"url": "/admin/webapp"
"additionalInformation": "SAPUI5.Component=books",
"url": "/admin-books/webapp"
}
},
"ManageOrders": {

View File

@@ -1,30 +0,0 @@
using CatalogService from '@capire/bookshop';
////////////////////////////////////////////////////////////////////////////
//
// Authors Object Page
//
annotate CatalogService.Authors with @(UI : {
HeaderInfo : {
TypeName : 'Author',
TypeNamePlural : 'Authors',
Description : {Value : name}
},
HeaderFacets : [{
$Type : 'UI.ReferenceFacet',
Label : '{i18n>Description}',
Target : '@UI.FieldGroup#Descr'
}, ],
Facets : [{
$Type : 'UI.ReferenceFacet',
Label : '{i18n>Details}',
Target : 'books/@UI.LineItem'
}, ],
FieldGroup #Descr : {Data : [
{Value : name},
{Value : dateOfBirth},
{Value : dateOfDeath},
{Value : placeOfBirth},
{Value : placeOfDeath},
]},
});

View File

@@ -8,7 +8,7 @@ annotate CatalogService.Books with @(UI : {
HeaderInfo : {
TypeName : 'Book',
TypeNamePlural : 'Books',
Description : {Value : authorName}
Description : {Value : author}
},
HeaderFacets : [{
$Type : 'UI.ReferenceFacet',
@@ -47,7 +47,7 @@ annotate CatalogService.Books with @(UI : {
Label : '{i18n>Title}'
},
{
Value : author.ID,
Value : author,
Label : '{i18n>Author}'
},
{Value : genre.name},

View File

@@ -2,8 +2,8 @@
This model controls what gets served to Fiori frontends...
*/
using from './admin/fiori-service';
using from './admin-authors/fiori-service';
using from './admin-books/fiori-service';
using from './browse/fiori-service';
using from './authors/fiori-service';
using from './common';
using from '@capire/bookstore/srv/mashup';

View File

@@ -2,7 +2,7 @@
// Add Author.age and .lifetime with a DB-specific function
//
using { AdminService } from '../schema';
using { AdminService } from '@capire/bookshop';
extend projection AdminService.Authors with {
YEARS_BETWEEN(dateOfBirth, dateOfDeath) as age: Integer,

View File

@@ -2,7 +2,7 @@
// Add Author.age and .lifetime with a DB-specific function
//
using { AdminService } from '../schema';
using { AdminService } from '@capire/bookshop';
extend projection AdminService.Authors with {
strftime('%Y',dateOfDeath)-strftime('%Y',dateOfBirth) as age: Integer,

View File

@@ -25,9 +25,26 @@
"model": "@capire/orders"
},
"messaging": {
"[production]": { "kind": "enterprise-messaging" },
"[development]": { "kind": "file-based-messaging" },
"[hybrid!]": { "kind": "enterprise-messaging-shared" }
"[production]": {
"kind": "enterprise-messaging"
},
"[development]": {
"kind": "file-based-messaging"
},
"[hybrid!]": {
"kind": "enterprise-messaging-shared"
}
},
"db": {
"kind": "sql"
},
"db-ext": {
"[development]": {
"model": "db/sqlite"
},
"[production]": {
"model": "db/hana"
}
},
"hana": {
"deploy-format": "hdbtable"

View File

@@ -11,39 +11,39 @@ describe('Localized Data', () => {
})
it('supports sap-language param', async () => {
const { data } = await GET(`/browse/Books?$select=title,authorName` + '&sap-language=de')
const { data } = await GET(`/browse/Books?$select=title,author` + '&sap-language=de')
expect(data.value).to.containSubset([
{ title: 'Sturmhöhe', authorName: 'Emily Brontë' },
{ title: 'Jane Eyre', authorName: 'Charlotte Brontë' },
{ title: 'The Raven', authorName: 'Edgar Allen Poe' },
{ title: 'Eleonora', authorName: 'Edgar Allen Poe' },
{ title: 'Catweazle', authorName: 'Richard Carpenter' },
{ 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 accept-language header', async () => {
const { data } = await GET(`/browse/Books?$select=title,authorName`, {
const { data } = await GET(`/browse/Books?$select=title,author`, {
headers: { 'Accept-Language': 'de' },
})
expect(data.value).to.containSubset([
{ title: 'Sturmhöhe', authorName: 'Emily Brontë' },
{ title: 'Jane Eyre', authorName: 'Charlotte Brontë' },
{ title: 'The Raven', authorName: 'Edgar Allen Poe' },
{ title: 'Eleonora', authorName: 'Edgar Allen Poe' },
{ title: 'Catweazle', authorName: 'Richard Carpenter' },
{ 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,authorName&$expand=currency`, {
const { data } = await GET(`/browse/Books?&$select=title,author&$expand=currency`, {
headers: { 'Accept-Language': 'de' },
})
expect(data.value).to.containSubset([
{ title: 'Sturmhöhe', authorName: 'Emily Brontë', currency: { name: 'Pfund' } },
{ title: 'Jane Eyre', authorName: 'Charlotte Brontë', currency: { name: 'Pfund' } },
{ title: 'The Raven', authorName: 'Edgar Allen Poe', currency: { name: 'US-Dollar' } },
{ title: 'Eleonora', authorName: 'Edgar Allen Poe', currency: { name: 'US-Dollar' } },
{ title: 'Catweazle', authorName: 'Richard Carpenter', currency: { name: '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

@@ -19,13 +19,13 @@ describe('OData Protocol', () => {
it('supports $search in multiple fields', async () => {
const { data } = await GET `/browse/Books ${{
params: { $search: 'Po', $select: `title,authorName` },
params: { $search: 'Po', $select: `title,author` },
}}`
expect(data.value).to.eql([
{ ID: 201, title: 'Wuthering Heights', authorName: 'Emily Brontë' },
{ ID: 207, title: 'Jane Eyre', authorName: 'Charlotte Brontë' },
{ ID: 251, title: 'The Raven', authorName: 'Edgar Allen Poe' },
{ ID: 252, title: 'Eleonora', authorName: 'Edgar Allen Poe' },
{ 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' },
])
})