Compare commits
147 Commits
eslint-fla
...
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
|
AuthorName = Author's Name
|
||||||
Authors = Authors
|
Authors = Authors
|
||||||
Order = Order
|
Order = Order
|
||||||
|
OrderItems = Order Items
|
||||||
Orders = Orders
|
Orders = Orders
|
||||||
Price = Price
|
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;
|
ID @title:'{i18n>ID}' @UI.HiddenFilter;
|
||||||
name @title:'{i18n>AuthorName}';
|
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}';
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ using AdminService from '../../srv/admin-service';
|
|||||||
annotate AdminService.Books with {
|
annotate AdminService.Books with {
|
||||||
price @Common.FieldControl : #ReadOnly;
|
price @Common.FieldControl : #ReadOnly;
|
||||||
}
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
//
|
//
|
||||||
// Common
|
// Common
|
||||||
@@ -15,29 +16,82 @@ annotate AdminService.OrderItems with {
|
|||||||
},
|
},
|
||||||
ValueList.entity : 'Books',
|
ValueList.entity : 'Books',
|
||||||
);
|
);
|
||||||
amount @(
|
amount @(Common.FieldControl : #Mandatory);
|
||||||
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: {
|
//
|
||||||
|
// UI
|
||||||
|
//
|
||||||
|
annotate AdminService.Orders with @(UI : {
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
//
|
//
|
||||||
// Lists of Orders
|
// Lists of Orders
|
||||||
//
|
//
|
||||||
SelectionFields: [ createdAt, createdBy ],
|
SelectionFields : [
|
||||||
|
createdAt,
|
||||||
|
createdBy
|
||||||
|
],
|
||||||
LineItem : [
|
LineItem : [
|
||||||
{Value: createdBy, Label:'Customer'},
|
{
|
||||||
{Value: createdAt, Label:'Date'}
|
Value : createdBy,
|
||||||
|
Label : 'Customer'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Value : createdAt,
|
||||||
|
Label : 'Date'
|
||||||
|
}
|
||||||
],
|
],
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
//
|
//
|
||||||
// Order Details
|
// Order Details
|
||||||
//
|
//
|
||||||
HeaderInfo : {
|
HeaderInfo : {
|
||||||
TypeName: 'Order', TypeNamePlural: 'Orders',
|
TypeName : 'Order',
|
||||||
|
TypeNamePlural : 'Orders',
|
||||||
Title : {
|
Title : {
|
||||||
Label : 'Order number ', //A label is possible but it is not considered on the ObjectPage yet
|
Label : 'Order number ', //A label is possible but it is not considered on the ObjectPage yet
|
||||||
Value : OrderNo
|
Value : OrderNo
|
||||||
@@ -45,52 +99,108 @@ annotate AdminService.Orders with @(
|
|||||||
Description : {Value : createdBy}
|
Description : {Value : createdBy}
|
||||||
},
|
},
|
||||||
Identification : [ //Is the main field group
|
Identification : [ //Is the main field group
|
||||||
{Value: createdBy, Label:'Customer'},
|
// labels not considered
|
||||||
{Value: createdAt, Label:'Date'},
|
{
|
||||||
|
Value : createdBy,
|
||||||
|
Label : 'Customer'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Value : createdAt,
|
||||||
|
Label : 'Date'
|
||||||
|
},
|
||||||
{Value : OrderNo},
|
{Value : OrderNo},
|
||||||
|
{
|
||||||
|
Value : 'shippingAddress_ID',
|
||||||
|
Label : 'Address ID'
|
||||||
|
}
|
||||||
],
|
],
|
||||||
HeaderFacets : [
|
HeaderFacets : [
|
||||||
{$Type: 'UI.ReferenceFacet', Label: '{i18n>Created}', Target: '@UI.FieldGroup#Created'},
|
{
|
||||||
{$Type: 'UI.ReferenceFacet', Label: '{i18n>Modified}', Target: '@UI.FieldGroup#Modified'},
|
$Type : 'UI.ReferenceFacet',
|
||||||
|
Label : '{i18n>Created}',
|
||||||
|
Target : '@UI.FieldGroup#Created'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$Type : 'UI.ReferenceFacet',
|
||||||
|
Label : '{i18n>Modified}',
|
||||||
|
Target : '@UI.FieldGroup#Modified'
|
||||||
|
},
|
||||||
],
|
],
|
||||||
Facets : [
|
Facets : [
|
||||||
{$Type: 'UI.ReferenceFacet', Label: '{i18n>Details}', Target: '@UI.FieldGroup#Details'},
|
{
|
||||||
{$Type: 'UI.ReferenceFacet', Label: '{i18n>OrderItems}', Target: 'Items/@UI.LineItem'},
|
$Type : 'UI.ReferenceFacet',
|
||||||
],
|
Label : '{i18n>shippingAddress}',
|
||||||
FieldGroup#Details: {
|
Target : '@UI.FieldGroup#ShippingAddress'
|
||||||
Data: [
|
|
||||||
{Value: currency_code, Label:'Currency'}
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
FieldGroup#Created: {
|
{
|
||||||
Data: [
|
$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 : createdBy},
|
||||||
{Value : createdAt},
|
{Value : createdAt},
|
||||||
]
|
]},
|
||||||
},
|
FieldGroup #Modified : {Data : [
|
||||||
FieldGroup#Modified: {
|
|
||||||
Data: [
|
|
||||||
{Value : modifiedBy},
|
{Value : modifiedBy},
|
||||||
{Value : modifiedAt},
|
{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;
|
createdAt @UI.HiddenFilter : false;
|
||||||
createdBy @UI.HiddenFilter : false;
|
createdBy @UI.HiddenFilter : false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//The enity types name is AdminService.my_bookshop_OrderItems
|
//The enity types name is AdminService.my_bookshop_OrderItems
|
||||||
//The annotations below are not generated in edmx WHY?
|
//The annotations below are not generated in edmx WHY?
|
||||||
annotate AdminService.OrderItems with @(
|
annotate AdminService.OrderItems with @(UI : {
|
||||||
UI: {
|
|
||||||
HeaderInfo : {
|
HeaderInfo : {
|
||||||
TypeName: 'Order Item', TypeNamePlural: ' ',
|
TypeName : 'Order Item',
|
||||||
Title: {
|
TypeNamePlural : ' ',
|
||||||
Value: book.title
|
Title : {Value : book.title},
|
||||||
},
|
|
||||||
Description : {Value : book.descr}
|
Description : {Value : book.descr}
|
||||||
},
|
},
|
||||||
// There is no filterbar for items so the selctionfileds is not needed
|
// There is no filterbar for items so the selctionfileds is not needed
|
||||||
@@ -100,18 +210,34 @@ annotate AdminService.OrderItems with @(
|
|||||||
// Lists of OrderItems
|
// Lists of OrderItems
|
||||||
//
|
//
|
||||||
LineItem : [
|
LineItem : [
|
||||||
{Value: book_ID, Label:'Book'},
|
{
|
||||||
|
Value : book_ID,
|
||||||
|
Label : 'Book'
|
||||||
|
},
|
||||||
//The following entry is only used to have the assoication followed in the read event
|
//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'},
|
Value : book.price,
|
||||||
|
Label : 'Book Price'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Value : amount,
|
||||||
|
Label : 'Quantity'
|
||||||
|
},
|
||||||
],
|
],
|
||||||
Identification : [ //Is the main field group
|
Identification : [ //Is the main field group
|
||||||
//{Value: ID, Label:'ID'}, //A guid shouldn't be on the UI
|
//{Value: ID, Label:'ID'}, //A guid shouldn't be on the UI
|
||||||
{Value: book_ID, Label:'Book'},
|
{
|
||||||
{Value: amount, Label:'Amount'},
|
Value : book_ID,
|
||||||
],
|
Label : 'Book'
|
||||||
Facets: [
|
|
||||||
{$Type: 'UI.ReferenceFacet', Label: '{i18n>OrderItems}', Target: '@UI.Identification'},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
);
|
{
|
||||||
|
Value : amount,
|
||||||
|
Label : 'Amount'
|
||||||
|
},
|
||||||
|
],
|
||||||
|
Facets : [{
|
||||||
|
$Type : 'UI.ReferenceFacet',
|
||||||
|
Label : '{i18n>OrderItems}',
|
||||||
|
Target : '@UI.Identification'
|
||||||
|
}, ],
|
||||||
|
}, );
|
||||||
@@ -5,10 +5,43 @@
|
|||||||
"license": "SAP SAMPLE CODE LICENSE",
|
"license": "SAP SAMPLE CODE LICENSE",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sap/cds": "latest",
|
"@sap/cds": "latest",
|
||||||
"express": "*"
|
"@sap/xb-msg-amqp-v100": "^0.9.31-SNAPSHOT",
|
||||||
|
"express": "*",
|
||||||
|
"passport": "^0.4.0",
|
||||||
|
"sqlite3": "^4.1.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "cds run --in-memory?",
|
"start": "cds run --in-memory?",
|
||||||
"watch": "cds watch"
|
"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';
|
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 Books as projection on my.Books;
|
||||||
entity Authors as projection on my.Authors;
|
entity Authors as projection on my.Authors;
|
||||||
entity Orders as select from my.Orders;
|
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')
|
||||||
@@ -15,12 +15,15 @@ function _addDiscount2 (each,discount) {
|
|||||||
/** Reduce stock of ordered books if available stock suffices */
|
/** Reduce stock of ordered books if available stock suffices */
|
||||||
async function _reduceStock (req) {
|
async function _reduceStock (req) {
|
||||||
const { Items: OrderItems } = req.data
|
const { Items: OrderItems } = req.data
|
||||||
return cds.transaction(req) .run (()=> OrderItems.map (order =>
|
const all = await cds.transaction(req).run(() =>
|
||||||
UPDATE (Books) .set ('stock -=', order.amount)
|
OrderItems.map(order =>
|
||||||
.where ('ID =', order.book_ID) .and ('stock >=', order.amount)
|
UPDATE(Books)
|
||||||
)) .then (all => all.forEach ((affectedRows,i) => {
|
.set('stock -=', order.amount)
|
||||||
if (affectedRows === 0) req.error (409,
|
.where('ID =', order.book_ID)
|
||||||
`${OrderItems[i].amount} exceeds stock for book #${OrderItems[i].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