Compare commits
147 Commits
openSAP-we
...
dkom
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a2febd57db | ||
|
|
5073af37d3 | ||
|
|
e8cda443ba | ||
|
|
da63d6c287 | ||
|
|
009dfc53c5 | ||
|
|
d760516e3e | ||
|
|
261d238738 | ||
|
|
136eae69eb | ||
|
|
e0306850b4 | ||
|
|
b7f9b78988 | ||
|
|
4e5f31a29d | ||
|
|
2d7efafd30 | ||
|
|
cf0b9c2a52 | ||
|
|
2053450404 | ||
|
|
920600a5ff | ||
|
|
dc1ea91d9e | ||
|
|
9ba5aae999 | ||
|
|
29aee15cf3 | ||
|
|
2ca5a79c7e | ||
|
|
857b28aad0 | ||
|
|
91d1a9bcb2 | ||
|
|
a33dbff74e | ||
|
|
7f65729dc9 | ||
|
|
3c4aa7427e | ||
|
|
28859993ba | ||
|
|
e503f157f6 | ||
|
|
4eebfb5df4 | ||
|
|
28362cc835 | ||
|
|
78e6138718 | ||
|
|
7db2c6e781 | ||
|
|
afc5be2610 | ||
|
|
5bc8d4dde0 | ||
|
|
60563dc816 | ||
|
|
4a1887f424 | ||
|
|
c28287f9e4 | ||
|
|
8cadac0051 | ||
|
|
034c3b84f7 | ||
|
|
b155754a72 | ||
|
|
782e8a6696 | ||
|
|
f3c14a0625 | ||
|
|
4cffa85079 | ||
|
|
837e6bf1c8 | ||
|
|
b394dbd234 | ||
|
|
fdd0a256c4 | ||
|
|
5b5d9da82e | ||
|
|
2af54a520c | ||
|
|
128213aba7 | ||
|
|
4bc2257cea | ||
|
|
e16a343ce3 | ||
|
|
1f01bdf202 | ||
|
|
45843ab7bd | ||
|
|
421cea9f2b | ||
|
|
aa919f9d62 | ||
|
|
ed814a1f75 | ||
|
|
c04c93cca6 | ||
|
|
2cde812edd | ||
|
|
2f576dbb1b | ||
|
|
7f7cd43bff | ||
|
|
294f9feb36 | ||
|
|
2ebfcd8871 | ||
|
|
963d0fbb6c | ||
|
|
eebdd74bfe | ||
|
|
37810c9027 | ||
|
|
48a086e9a1 | ||
|
|
659c347c71 | ||
|
|
0bbb8e3d3b | ||
|
|
b3abcbcaae | ||
|
|
3716d4d5e3 | ||
|
|
226094e85c | ||
|
|
de149ea9b3 | ||
|
|
b6c1610817 | ||
|
|
3df0981992 | ||
|
|
8c8c5f3f9d | ||
|
|
2c0f69a161 | ||
|
|
54d0c8b35d | ||
|
|
3027a7a1e5 | ||
|
|
8eaf34f5d3 | ||
|
|
0b0a22d126 | ||
|
|
c0e1fb38ac | ||
|
|
74c155ca62 | ||
|
|
db16577235 | ||
|
|
53989cf609 | ||
|
|
d678b51320 | ||
|
|
5720d73b76 | ||
|
|
06a6ac2201 | ||
|
|
125edc34e2 | ||
|
|
dc8e8c55df | ||
|
|
f89acc00dd | ||
|
|
3e725bcc26 | ||
|
|
dfea19334d | ||
|
|
8f11de5430 | ||
|
|
38ce94d5cd | ||
|
|
ffe633a493 | ||
|
|
e081182a7c | ||
|
|
e4f8f13dbf | ||
|
|
cc698ec23f | ||
|
|
382a4c562d | ||
|
|
811694cdf1 | ||
|
|
83653bd095 | ||
|
|
e27275d29a | ||
|
|
b9330d7f77 | ||
|
|
f413b45e24 | ||
|
|
4a21b9edc3 | ||
|
|
4bfd4430e1 | ||
|
|
0bb3144aea | ||
|
|
c1d2c4caef | ||
|
|
59f68c0f28 | ||
|
|
5ba3458b27 | ||
|
|
199b2c8045 | ||
|
|
3b4abf5600 | ||
|
|
f69c0ae190 | ||
|
|
f48cd1cc2f | ||
|
|
6bded9df98 | ||
|
|
5a659774b5 | ||
|
|
17d6dc8cf8 | ||
|
|
1a1686e340 | ||
|
|
b298c9b708 | ||
|
|
348a7b191e | ||
|
|
f56d4fe093 | ||
|
|
02942f5e1a | ||
|
|
1aa9237d20 | ||
|
|
7a760cfaf8 | ||
|
|
6c0d8fa444 | ||
|
|
3b06003328 | ||
|
|
e686b1819b | ||
|
|
d36c2a97fa | ||
|
|
f4e119342b | ||
|
|
ddd02b52f2 | ||
|
|
e6d5183cce | ||
|
|
a191ecf88d | ||
|
|
140db39cd4 | ||
|
|
68ee29598a | ||
|
|
7deae997bb | ||
|
|
12aee3e38c | ||
|
|
dfe876e2cf | ||
|
|
9e2c7a0974 | ||
|
|
30b2854fac | ||
|
|
4ca7e425ec | ||
|
|
28a51f4837 | ||
|
|
692a360065 | ||
|
|
b3d9fdb8b3 | ||
|
|
e9d10986ff | ||
|
|
7191f61806 | ||
|
|
e688e7ecee | ||
|
|
4a2139a5f2 | ||
|
|
ed3ecd502f | ||
|
|
8f3d112558 |
3
packages/bookshop/.prettierrc
Normal file
3
packages/bookshop/.prettierrc
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"printWidth": 120
|
||||
}
|
||||
65
packages/bookshop/README.md
Normal file
65
packages/bookshop/README.md
Normal file
@@ -0,0 +1,65 @@
|
||||
# Bookshop With Address Data From SAP S/4HANA
|
||||
|
||||
This is an extended bookshop with business-partner address data from SAP S/4HANA.
|
||||
When the user creates an order and uses the value help of the shipping address,
|
||||
a synchronous request to SAP S/4HANA is triggered yielding all possible addresses
|
||||
belonging to this business partner. Once an address is selected, its data
|
||||
is replicated into a local database. To keep data in sync, an event handler
|
||||
is registered which listens to all changes of business partners and updates the
|
||||
local database table.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
`@sap/cds` >= 1.30
|
||||
|
||||
|
||||
## Running With Mocks
|
||||
Just execute the following command in the `bookshop` folder.
|
||||
```
|
||||
cds run --in-memory --with-mocks
|
||||
```
|
||||
|
||||
|
||||
|
||||
## Running With an S/4HANA Backend
|
||||
|
||||
To run your app in non-mock mode you need an SAP S/4HANA Cloud system and connect it to your SAP Cloud Platform. You can use the
|
||||
[SAP Cloud Platform Extension Factory](https://help.sap.com/viewer/65de2977205c403bbc107264b8eccf4b/Cloud/en-US/346864df64f24011b49abee07bbd79af.html) to automate parts of this task. You need to enable synchronous APIs as well as events that are sent whenever business partners are changed.
|
||||
|
||||
To run the app locally, you need to create a `default-env.json` file in the `bookshop` folder containing the binding information (credentials of Enterprise Messaging as well as the destination to the business-partner service).
|
||||
|
||||
Provide the credentials in the `cds.requires` section of the `package.json` file in the `bookshop` folder, e.g.
|
||||
|
||||
```json
|
||||
"cds": {
|
||||
"requires": {
|
||||
"API_BUSINESS_PARTNER": {
|
||||
"kind": "odata",
|
||||
"model": "srv/external",
|
||||
"credentials": {
|
||||
"destination": "cap-api098"
|
||||
}
|
||||
},
|
||||
"messaging": {
|
||||
"kind": "enterprise-messaging",
|
||||
"credentials": {
|
||||
"prefix": "sap/S4HANAOD/c098/BO"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Here, `destination` is the destination of your business-partner service and `prefix` is the prefix
|
||||
of the topic of the events.
|
||||
|
||||
Then simply run the following command in the `bookshop` folder.
|
||||
```
|
||||
cds run --in-memory
|
||||
```
|
||||
|
||||
## User Flow
|
||||
After starting the app, go to http://localhost:4004/fiori.html#Shell-home and open the app `Manage Orders` to create an order.
|
||||
Use the value help of the shipping address to select an address. Create an order item and save the order.
|
||||
Then change the address of your business partner (in the mocked case you can trigger the PATCH request in `req.http` ). Refresh
|
||||
the object page of your order and see the change.
|
||||
@@ -9,5 +9,14 @@ Name = Name
|
||||
AuthorName = Author's Name
|
||||
Authors = Authors
|
||||
Order = Order
|
||||
OrderItems = Order Items
|
||||
Orders = Orders
|
||||
Price = Price
|
||||
shippingAddress = Shipping Address
|
||||
cityName = City Name
|
||||
houseNumber = House Number
|
||||
streetName = Street Name
|
||||
postalCode = Postal Code
|
||||
country = Country
|
||||
AddressID = Address ID
|
||||
contact = Contact
|
||||
|
||||
@@ -72,3 +72,13 @@ annotate my.Authors with {
|
||||
ID @title:'{i18n>ID}' @UI.HiddenFilter;
|
||||
name @title:'{i18n>AuthorName}';
|
||||
}
|
||||
|
||||
annotate my.Addresses with {
|
||||
ID @title:'{i18n>AddressID}';
|
||||
contact @title:'{i18n>contact}';
|
||||
@readonly cityName @title:'{i18n>cityName}';
|
||||
@readonly streetName @title:'{i18n>streetName}';
|
||||
@readonly postalCode @title:'{i18n>postalCode}';
|
||||
@readonly country @title:'{i18n>country}';
|
||||
@readonly houseNumber @title:'{i18n>houseNumber}';
|
||||
}
|
||||
@@ -1,117 +1,243 @@
|
||||
using AdminService from '../../srv/admin-service';
|
||||
|
||||
annotate AdminService.Books with {
|
||||
price @Common.FieldControl: #ReadOnly;
|
||||
price @Common.FieldControl : #ReadOnly;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Common
|
||||
//
|
||||
annotate AdminService.OrderItems with {
|
||||
book @(
|
||||
Common: {
|
||||
Text: book.title,
|
||||
FieldControl: #Mandatory
|
||||
},
|
||||
ValueList.entity:'Books',
|
||||
);
|
||||
amount @(
|
||||
Common.FieldControl: #Mandatory
|
||||
);
|
||||
book @(
|
||||
Common : {
|
||||
Text : book.title,
|
||||
FieldControl : #Mandatory
|
||||
},
|
||||
ValueList.entity : 'Books',
|
||||
);
|
||||
amount @(Common.FieldControl : #Mandatory);
|
||||
}
|
||||
|
||||
annotate AdminService.Orders with {
|
||||
shippingAddress @(Common : {
|
||||
FieldControl : #Mandatory,
|
||||
ValueList : {
|
||||
CollectionPath : 'Addresses',
|
||||
Label : 'Addresses',
|
||||
SearchSupported : 'true',
|
||||
Parameters : [
|
||||
{
|
||||
$Type : 'Common.ValueListParameterOut',
|
||||
LocalDataProperty : 'shippingAddress_ID',
|
||||
ValueListProperty : 'ID'
|
||||
},
|
||||
{
|
||||
$Type : 'Common.ValueListParameterOut',
|
||||
LocalDataProperty : 'shippingAddress_contact',
|
||||
ValueListProperty : 'contact'
|
||||
},
|
||||
{
|
||||
$Type : 'Common.ValueListParameterDisplayOnly',
|
||||
ValueListProperty : 'postalCode'
|
||||
},
|
||||
{
|
||||
$Type : 'Common.ValueListParameterDisplayOnly',
|
||||
ValueListProperty : 'cityName'
|
||||
},
|
||||
{
|
||||
$Type : 'Common.ValueListParameterDisplayOnly',
|
||||
ValueListProperty : 'country'
|
||||
},
|
||||
{
|
||||
$Type : 'Common.ValueListParameterDisplayOnly',
|
||||
ValueListProperty : 'streetName'
|
||||
},
|
||||
{
|
||||
$Type : 'Common.ValueListParameterDisplayOnly',
|
||||
ValueListProperty : 'houseNumber'
|
||||
},
|
||||
]
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
annotate AdminService.Orders with @(
|
||||
UI: {
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Lists of Orders
|
||||
//
|
||||
SelectionFields: [ createdAt, createdBy ],
|
||||
LineItem: [
|
||||
{Value: createdBy, Label:'Customer'},
|
||||
{Value: createdAt, Label:'Date'}
|
||||
],
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Order Details
|
||||
//
|
||||
HeaderInfo: {
|
||||
TypeName: 'Order', TypeNamePlural: 'Orders',
|
||||
Title: {
|
||||
Label: 'Order number ', //A label is possible but it is not considered on the ObjectPage yet
|
||||
Value: OrderNo
|
||||
},
|
||||
Description: {Value: createdBy}
|
||||
},
|
||||
Identification: [ //Is the main field group
|
||||
{Value: createdBy, Label:'Customer'},
|
||||
{Value: createdAt, Label:'Date'},
|
||||
{Value: OrderNo },
|
||||
],
|
||||
HeaderFacets: [
|
||||
{$Type: 'UI.ReferenceFacet', Label: '{i18n>Created}', Target: '@UI.FieldGroup#Created'},
|
||||
{$Type: 'UI.ReferenceFacet', Label: '{i18n>Modified}', Target: '@UI.FieldGroup#Modified'},
|
||||
],
|
||||
Facets: [
|
||||
{$Type: 'UI.ReferenceFacet', Label: '{i18n>Details}', Target: '@UI.FieldGroup#Details'},
|
||||
{$Type: 'UI.ReferenceFacet', Label: '{i18n>OrderItems}', Target: 'Items/@UI.LineItem'},
|
||||
],
|
||||
FieldGroup#Details: {
|
||||
Data: [
|
||||
{Value: currency_code, Label:'Currency'}
|
||||
]
|
||||
},
|
||||
FieldGroup#Created: {
|
||||
Data: [
|
||||
{Value: createdBy},
|
||||
{Value: createdAt},
|
||||
]
|
||||
},
|
||||
FieldGroup#Modified: {
|
||||
Data: [
|
||||
{Value: modifiedBy},
|
||||
{Value: modifiedAt},
|
||||
]
|
||||
},
|
||||
},
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// UI
|
||||
//
|
||||
annotate AdminService.Orders with @(UI : {
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Lists of Orders
|
||||
//
|
||||
SelectionFields : [
|
||||
createdAt,
|
||||
createdBy
|
||||
],
|
||||
LineItem : [
|
||||
{
|
||||
Value : createdBy,
|
||||
Label : 'Customer'
|
||||
},
|
||||
{
|
||||
Value : createdAt,
|
||||
Label : 'Date'
|
||||
}
|
||||
],
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Order Details
|
||||
//
|
||||
HeaderInfo : {
|
||||
TypeName : 'Order',
|
||||
TypeNamePlural : 'Orders',
|
||||
Title : {
|
||||
Label : 'Order number ', //A label is possible but it is not considered on the ObjectPage yet
|
||||
Value : OrderNo
|
||||
},
|
||||
Description : {Value : createdBy}
|
||||
},
|
||||
Identification : [ //Is the main field group
|
||||
// labels not considered
|
||||
{
|
||||
Value : createdBy,
|
||||
Label : 'Customer'
|
||||
},
|
||||
{
|
||||
Value : createdAt,
|
||||
Label : 'Date'
|
||||
},
|
||||
{Value : OrderNo},
|
||||
{
|
||||
Value : 'shippingAddress_ID',
|
||||
Label : 'Address ID'
|
||||
}
|
||||
],
|
||||
HeaderFacets : [
|
||||
{
|
||||
$Type : 'UI.ReferenceFacet',
|
||||
Label : '{i18n>Created}',
|
||||
Target : '@UI.FieldGroup#Created'
|
||||
},
|
||||
{
|
||||
$Type : 'UI.ReferenceFacet',
|
||||
Label : '{i18n>Modified}',
|
||||
Target : '@UI.FieldGroup#Modified'
|
||||
},
|
||||
],
|
||||
Facets : [
|
||||
{
|
||||
$Type : 'UI.ReferenceFacet',
|
||||
Label : '{i18n>shippingAddress}',
|
||||
Target : '@UI.FieldGroup#ShippingAddress'
|
||||
},
|
||||
{
|
||||
$Type : 'UI.ReferenceFacet',
|
||||
Label : '{i18n>Details}',
|
||||
Target : '@UI.FieldGroup#Details'
|
||||
},
|
||||
{
|
||||
$Type : 'UI.ReferenceFacet',
|
||||
Label : '{i18n>OrderItems}',
|
||||
Target : 'Items/@UI.LineItem'
|
||||
},
|
||||
],
|
||||
FieldGroup #Details : {Data : [{
|
||||
Value : currency_code,
|
||||
Label : 'Currency'
|
||||
}]},
|
||||
FieldGroup #Created : {Data : [
|
||||
{Value : createdBy},
|
||||
{Value : createdAt},
|
||||
]},
|
||||
FieldGroup #Modified : {Data : [
|
||||
{Value : modifiedBy},
|
||||
{Value : modifiedAt},
|
||||
]},
|
||||
FieldGroup #ShippingAddress : {Data : [
|
||||
{
|
||||
Value : shippingAddress_ID,
|
||||
Label : '{i18n>shippingAddress}'
|
||||
},
|
||||
{
|
||||
Value : shippingAddress.houseNumber,
|
||||
Label : '{i18n>houseNumber}'
|
||||
},
|
||||
{
|
||||
Value : shippingAddress.streetName,
|
||||
Label : '{i18n>streetName}'
|
||||
},
|
||||
{
|
||||
Value : shippingAddress.cityName,
|
||||
Label : '{i18n>cityName}'
|
||||
},
|
||||
{
|
||||
Value : shippingAddress.postalCode,
|
||||
Label : '{i18n>postalCode}'
|
||||
},
|
||||
]},
|
||||
},
|
||||
Common.SideEffects : {
|
||||
EffectTypes : #ValueChange,
|
||||
SourceProperties : [shippingAddress_ID],
|
||||
TargetProperties : [
|
||||
shippingAddress.country,
|
||||
shippingAddress.houseNumber,
|
||||
shippingAddress.streetName,
|
||||
shippingAddress.cityName,
|
||||
shippingAddress.postalCode
|
||||
]
|
||||
},
|
||||
) {
|
||||
createdAt @UI.HiddenFilter:false;
|
||||
createdBy @UI.HiddenFilter:false;
|
||||
createdAt @UI.HiddenFilter : false;
|
||||
createdBy @UI.HiddenFilter : false;
|
||||
};
|
||||
|
||||
|
||||
|
||||
//The enity types name is AdminService.my_bookshop_OrderItems
|
||||
//The annotations below are not generated in edmx WHY?
|
||||
annotate AdminService.OrderItems with @(
|
||||
UI: {
|
||||
HeaderInfo: {
|
||||
TypeName: 'Order Item', TypeNamePlural: ' ',
|
||||
Title: {
|
||||
Value: book.title
|
||||
},
|
||||
Description: {Value: book.descr}
|
||||
},
|
||||
// There is no filterbar for items so the selctionfileds is not needed
|
||||
SelectionFields: [ book_ID ],
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Lists of OrderItems
|
||||
//
|
||||
LineItem: [
|
||||
{Value: book_ID, Label:'Book'},
|
||||
//The following entry is only used to have the assoication followed in the read event
|
||||
{Value: book.price, Label:'Book Price'},
|
||||
{Value: amount, Label:'Quantity'},
|
||||
],
|
||||
Identification: [ //Is the main field group
|
||||
//{Value: ID, Label:'ID'}, //A guid shouldn't be on the UI
|
||||
{Value: book_ID, Label:'Book'},
|
||||
{Value: amount, Label:'Amount'},
|
||||
],
|
||||
Facets: [
|
||||
{$Type: 'UI.ReferenceFacet', Label: '{i18n>OrderItems}', Target: '@UI.Identification'},
|
||||
],
|
||||
},
|
||||
);
|
||||
annotate AdminService.OrderItems with @(UI : {
|
||||
HeaderInfo : {
|
||||
TypeName : 'Order Item',
|
||||
TypeNamePlural : ' ',
|
||||
Title : {Value : book.title},
|
||||
Description : {Value : book.descr}
|
||||
},
|
||||
// There is no filterbar for items so the selctionfileds is not needed
|
||||
SelectionFields : [book_ID],
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Lists of OrderItems
|
||||
//
|
||||
LineItem : [
|
||||
{
|
||||
Value : book_ID,
|
||||
Label : 'Book'
|
||||
},
|
||||
//The following entry is only used to have the assoication followed in the read event
|
||||
{
|
||||
Value : book.price,
|
||||
Label : 'Book Price'
|
||||
},
|
||||
{
|
||||
Value : amount,
|
||||
Label : 'Quantity'
|
||||
},
|
||||
],
|
||||
Identification : [ //Is the main field group
|
||||
//{Value: ID, Label:'ID'}, //A guid shouldn't be on the UI
|
||||
{
|
||||
Value : book_ID,
|
||||
Label : 'Book'
|
||||
},
|
||||
{
|
||||
Value : amount,
|
||||
Label : 'Amount'
|
||||
},
|
||||
],
|
||||
Facets : [{
|
||||
$Type : 'UI.ReferenceFacet',
|
||||
Label : '{i18n>OrderItems}',
|
||||
Target : '@UI.Identification'
|
||||
}, ],
|
||||
}, );
|
||||
@@ -5,10 +5,43 @@
|
||||
"license": "SAP SAMPLE CODE LICENSE",
|
||||
"dependencies": {
|
||||
"@sap/cds": "latest",
|
||||
"express": "*"
|
||||
"@sap/xb-msg-amqp-v100": "^0.9.31-SNAPSHOT",
|
||||
"express": "*",
|
||||
"passport": "^0.4.0",
|
||||
"sqlite3": "^4.1.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "cds run --in-memory?",
|
||||
"watch": "cds watch"
|
||||
},
|
||||
"cds": {
|
||||
"requires": {
|
||||
"API_BUSINESS_PARTNER": {
|
||||
"kind": "odata",
|
||||
"model": "srv/external",
|
||||
"--credentials": {
|
||||
"destination": "cap-api098"
|
||||
}
|
||||
},
|
||||
"--messaging": {
|
||||
"kind": "enterprise-messaging",
|
||||
"credentials": {
|
||||
"prefix": "sap/S4HANAOD/c098/BO"
|
||||
}
|
||||
}
|
||||
},
|
||||
"auth": {
|
||||
"passport": {
|
||||
"strategy": "mock",
|
||||
"users": {
|
||||
"alice": {
|
||||
"roles": [
|
||||
"admin"
|
||||
],
|
||||
"ID": "ALICE"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
19
packages/bookshop/req.http
Normal file
19
packages/bookshop/req.http
Normal file
@@ -0,0 +1,19 @@
|
||||
PATCH http://localhost:4004/api-business-partner/A_BusinessPartnerAddress(BusinessPartner='ALICE',AddressID='62640')
|
||||
Content-Type: application/json
|
||||
Authorization: Basic QUxJQ0Utc2VjcmV0
|
||||
|
||||
{
|
||||
"PostalCode": "123456",
|
||||
"CityName": "AlteredTown"
|
||||
}
|
||||
|
||||
###
|
||||
|
||||
GET http://localhost:4004/admin/Orders(ID=7e2f2640-6866-4dcf-8f4d-3027aa831cad,IsActiveEntity=false)?
|
||||
&$expand=shippingAddress($select=cityName,houseNumber,postalCode,streetName)
|
||||
Authorization: Basic QUxJQ0U6c2VjcmV0
|
||||
|
||||
|
||||
###
|
||||
|
||||
DELETE http://localhost:4004/api-business-partner/A_BusinessPartnerAddress(BusinessPartner='ALICE',AddressID='62640')
|
||||
@@ -1,6 +1,6 @@
|
||||
using { sap.capire.bookshop as my } from '../db/schema';
|
||||
|
||||
service AdminService @(_requires:'authenticated-user') {
|
||||
service AdminService @(requires:'admin') {
|
||||
entity Books as projection on my.Books;
|
||||
entity Authors as projection on my.Authors;
|
||||
entity Orders as select from my.Orders;
|
||||
|
||||
77
packages/bookshop/srv/admin-service.js
Normal file
77
packages/bookshop/srv/admin-service.js
Normal file
@@ -0,0 +1,77 @@
|
||||
const cds = require('@sap/cds')
|
||||
module.exports = cds.service.impl(async () => {
|
||||
// We are mashing up three services...
|
||||
const bupa = await cds.connect.to('API_BUSINESS_PARTNER')
|
||||
const admin = await cds.connect.to('AdminService')
|
||||
const db = await cds.connect.to('db')
|
||||
|
||||
// Using reflected definitions from connected services/database
|
||||
const { Addresses: externalAddresses } = bupa.entities // projection on external addresses
|
||||
const { Books, Addresses } = db.entities('sap.capire.bookshop') // entities in local database
|
||||
|
||||
// Delegate ValueHelp requests to S/4 backend, fetching current user's addresses from there
|
||||
admin.on('READ', 'Addresses', req => {
|
||||
console.log('Delegating to S/4 bupa service...')
|
||||
const UsersAddresses = SELECT.from(externalAddresses)
|
||||
.where({ contact: req.user.id })
|
||||
.and(req.query.SELECT.where)
|
||||
return bupa.tx(req).run(UsersAddresses)
|
||||
})
|
||||
|
||||
// Replicate chosen addresses from S/4 when filling orders.
|
||||
admin.before('PATCH', 'Orders', async req => {
|
||||
const assigned = { ID: req.data.shippingAddress_ID, contact: req.user.id }
|
||||
if (!assigned.ID) return //> something else
|
||||
const local = db.transaction(req)
|
||||
const [replica] = await local.read(Addresses).where(assigned)
|
||||
if (replica) return //> already replicated
|
||||
const [address] = await bupa.tx(req).run(SELECT.from(externalAddresses).where(assigned))
|
||||
if (address) return local.create(Addresses).entries(address)
|
||||
})
|
||||
|
||||
// Subscribe to S/4 event to update local replicas when sources change in S/4.
|
||||
bupa.on('BusinessPartner/Changed', async msg => {
|
||||
console.log('>> received:', msg.data)
|
||||
|
||||
const BuPaID = msg.data.KEY[0].BUSINESSPARTNER //> S/4HANA's weird payload format
|
||||
const { SELECT, UPDATE } = cds.ql(msg) //> convenient alternative to <srv>.transaction(req).run(SELECT...)
|
||||
|
||||
// fetch affected entries from local replicas
|
||||
const replicas = await SELECT.from(Addresses).where({ contact: BuPaID })
|
||||
if (replicas.length === 0) return //> not affected
|
||||
|
||||
// fetch changed data from S/4 -> might be less than local due to deletes
|
||||
const changed = await SELECT.from(externalAddresses).where({
|
||||
contact: BuPaID,
|
||||
ID: replicas.map(({ ID }) => ID)
|
||||
})
|
||||
|
||||
// update local replicas with changes from S/4
|
||||
const local = db.transaction(msg) //> using that variant to benefit from bulk runs
|
||||
return local.run(changed.map(a => UPDATE(Addresses, a.ID).with(a)))
|
||||
})
|
||||
|
||||
// Validate incoming orders and reduce books' stocks.
|
||||
admin.before('CREATE', 'Orders', async req => {
|
||||
const { Items } = req.data
|
||||
|
||||
// validate input...
|
||||
if (!Items || Items.length === 0) return req.reject('Please order at least one item.')
|
||||
if (!req.data.shippingAddress_ID) return req.reject('Please enter a valid shipping address.', 'shippingAddress_ID')
|
||||
|
||||
// reduce stock on ordered books...
|
||||
const all = await db.tx(req).run(
|
||||
Items.map(each =>
|
||||
UPDATE(Books)
|
||||
.where('ID =', each.book_ID)
|
||||
.and('stock >=', each.amount)
|
||||
.set('stock -=', each.amount)
|
||||
)
|
||||
)
|
||||
all.forEach(
|
||||
(affectedRows, i) =>
|
||||
affectedRows > 0 || req.error(409, `${Items[i].amount} exceeds stock for book #${Items[i].book_ID}`)
|
||||
)
|
||||
})
|
||||
})
|
||||
require('./utils')
|
||||
@@ -2,25 +2,28 @@ const cds = require('@sap/cds')
|
||||
const { Books } = cds.entities
|
||||
|
||||
/** Service implementation for CatalogService */
|
||||
module.exports = cds.service.impl(function() {
|
||||
this.after ('READ', 'Books', each => each.stock > 111 && _addDiscount2(each,11))
|
||||
this.before ('CREATE', 'Orders', _reduceStock)
|
||||
module.exports = cds.service.impl(function () {
|
||||
this.after('READ', 'Books', each => each.stock > 111 && _addDiscount2(each, 11))
|
||||
this.before('CREATE', 'Orders', _reduceStock)
|
||||
})
|
||||
|
||||
/** Add some discount for overstocked books */
|
||||
function _addDiscount2 (each,discount) {
|
||||
function _addDiscount2 (each, discount) {
|
||||
each.title += ` -- ${discount}% discount!`
|
||||
}
|
||||
|
||||
/** Reduce stock of ordered books if available stock suffices */
|
||||
async function _reduceStock (req) {
|
||||
const { Items: OrderItems } = req.data
|
||||
return cds.transaction(req) .run (()=> OrderItems.map (order =>
|
||||
UPDATE (Books) .set ('stock -=', order.amount)
|
||||
.where ('ID =', order.book_ID) .and ('stock >=', order.amount)
|
||||
)) .then (all => all.forEach ((affectedRows,i) => {
|
||||
if (affectedRows === 0) req.error (409,
|
||||
`${OrderItems[i].amount} exceeds stock for book #${OrderItems[i].book_ID}`
|
||||
const all = await cds.transaction(req).run(() =>
|
||||
OrderItems.map(order =>
|
||||
UPDATE(Books)
|
||||
.set('stock -=', order.amount)
|
||||
.where('ID =', order.book_ID)
|
||||
.and('stock >=', order.amount)
|
||||
)
|
||||
}))
|
||||
)
|
||||
all.forEach((affectedRows, i) => {
|
||||
if (affectedRows === 0) req.error(409, `${OrderItems[i].amount} exceeds stock for book #${OrderItems[i].book_ID}`)
|
||||
})
|
||||
}
|
||||
|
||||
39
packages/bookshop/srv/external.cds
Normal file
39
packages/bookshop/srv/external.cds
Normal file
@@ -0,0 +1,39 @@
|
||||
using { API_BUSINESS_PARTNER as external } from './external/API_BUSINESS_PARTNER.csn';
|
||||
|
||||
/**
|
||||
* Tailor the imported API to our needs...
|
||||
*/
|
||||
extend service external with {
|
||||
|
||||
/**
|
||||
* Simplified view on external addresses
|
||||
*/
|
||||
// @cds.persistence.skip: false
|
||||
@mashup entity Addresses as projection on external.A_BusinessPartnerAddress {
|
||||
key AddressID as ID,
|
||||
key BusinessPartner as contact,
|
||||
Country as country,
|
||||
CityName as cityName,
|
||||
PostalCode as postalCode,
|
||||
StreetName as streetName,
|
||||
HouseNumber as houseNumber
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add an entity to replicate external address data for quick access,
|
||||
* e.g. when displaying lists of orders.
|
||||
*/
|
||||
@cds.persistence:{table,skip:false} //> create a table with the view's inferred signature
|
||||
@cds.autoexpose //> auto-expose in services as targets for ValueHelps and joins
|
||||
entity sap.capire.bookshop.Addresses as projection on external.Addresses;
|
||||
|
||||
|
||||
/**
|
||||
* Extend Orders with references to replicated external Addresses
|
||||
*/
|
||||
using { sap.capire.bookshop } from '../db/schema';
|
||||
extend bookshop.Orders with {
|
||||
shippingAddress : Association to bookshop.Addresses;
|
||||
}
|
||||
2426
packages/bookshop/srv/external/API_BUSINESS_PARTNER.csn
vendored
Normal file
2426
packages/bookshop/srv/external/API_BUSINESS_PARTNER.csn
vendored
Normal file
File diff suppressed because it is too large
Load Diff
3261
packages/bookshop/srv/external/API_BUSINESS_PARTNER.edmx
vendored
Normal file
3261
packages/bookshop/srv/external/API_BUSINESS_PARTNER.edmx
vendored
Normal file
File diff suppressed because it is too large
Load Diff
11
packages/bookshop/srv/external/API_BUSINESS_PARTNER.js
vendored
Normal file
11
packages/bookshop/srv/external/API_BUSINESS_PARTNER.js
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
module.exports = srv => {
|
||||
srv.on(['CREATE', 'UPDATE', 'DELETE'], req => {
|
||||
|
||||
const payload = {
|
||||
KEY: [{ BUSINESSPARTNER: req.data.BusinessPartner }]
|
||||
}
|
||||
console.log('<< emitting:', payload)
|
||||
srv.emit('BusinessPartner/Changed', payload)
|
||||
|
||||
})
|
||||
}
|
||||
5
packages/bookshop/srv/external/data/API_BUSINESS_PARTNER-A_BusinessPartnerAddress.csv
vendored
Normal file
5
packages/bookshop/srv/external/data/API_BUSINESS_PARTNER-A_BusinessPartnerAddress.csv
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
BusinessPartner;AddressID;CityName;PostalCode;Country;StreetName;HouseNumber
|
||||
ALICE;62640;Walldorf;69190;GER;Dietmer-Hopp-Allee;16
|
||||
ALICE;62641;Berlin;69390;GER;Berlin-Street;19
|
||||
BOB;62341;Karlsruhe;61390;GER;Karlsruhe-Street;19
|
||||
anonymous;61321;Sometown;61290;GER;Sometown-Street;19
|
||||
|
27
packages/bookshop/srv/utils.js
Normal file
27
packages/bookshop/srv/utils.js
Normal file
@@ -0,0 +1,27 @@
|
||||
// Hack for SAP Application Studio
|
||||
process.env['http_proxy'] = ''
|
||||
process.env['https_proxy'] = ''
|
||||
process.env['HTTP_PROXY'] = ''
|
||||
process.env['HTTPS_PROXY'] = ''
|
||||
|
||||
const diff = (obj1, obj2) =>
|
||||
Object.keys(obj1).reduce((res, curr) => (obj1[curr] === obj2[curr] ? res : (res[curr] = obj2[curr]) && res), {})
|
||||
|
||||
const queriesToUpdateDifferences = (entity, ownEntries, otherEntries) =>
|
||||
ownEntries
|
||||
.map(ownEntry => {
|
||||
const otherEntry = otherEntries.find(otherEntry =>
|
||||
Object.keys(entity.keys).reduce((res, curr) => res && otherEntry[curr] === ownEntry[curr], true)
|
||||
)
|
||||
if (otherEntry) {
|
||||
const differences = diff(ownEntry, otherEntry)
|
||||
if (Object.keys(differences).length) {
|
||||
return UPDATE(entity)
|
||||
.set(differences)
|
||||
.where(Object.keys(entity.keys).reduce((res, curr) => (res[curr] = ownEntry[curr]) && res, {}))
|
||||
}
|
||||
}
|
||||
})
|
||||
.filter(el => el)
|
||||
|
||||
module.exports = { diff, queriesToUpdateDifferences }
|
||||
Reference in New Issue
Block a user