Compare commits

..

1 Commits

Author SHA1 Message Date
Vladislav Leonkev
6906e68142 add multitenancy 2025-04-25 17:33:53 +02:00
25 changed files with 229 additions and 125 deletions

View File

@@ -1,6 +1,11 @@
{
"welcomeFile": "app/bookshop/index.html",
"routes": [
{
"source": "^/-/cds/.*",
"destination": "mtx-api",
"authenticationType": "none"
},
{
"source": "^/app/(.*)$",
"target": "$1",

View File

@@ -9,7 +9,7 @@
"index.cds"
],
"devDependencies": {
"@cap-js/sqlite": ">=1"
"@cap-js/sqlite": "*"
},
"dependencies": {
"@sap/cds": ">=7",

6
bookstore/app/routes.js Normal file
View File

@@ -0,0 +1,6 @@
// Add routes to UIs from imported packages
module.exports = (app) => {
app.serve ('/bookshop') .from ('@capire/bookshop','app/vue')
app.serve ('/reviews') .from ('@capire/reviews','app/vue')
app.serve ('/orders') .from('@capire/orders','app/orders')
}

View File

@@ -11,7 +11,8 @@
"@sap-cloud-sdk/resilience": "^4",
"@sap/cds": ">=5",
"express": "^4.17.1",
"@cap-js/hana": ">=1"
"@cap-js/hana": "^1",
"@sap/cds-mtxs": "^2"
},
"cds": {
"requires": {
@@ -24,7 +25,10 @@
"model": "@capire/orders"
},
"messaging": true,
"db": true
"db": true,
"multitenancy": true,
"auth": "xsuaa"
},
"log": { "service": true }
},

View File

@@ -1,16 +1,10 @@
const cds = require ('@sap/cds')
// Add routes to UIs from imported packages
if (!cds.env.production) cds.once ('bootstrap', (app) => {
app.serve ('/bookshop') .from ('@capire/bookshop','app/vue')
app.serve ('/reviews') .from ('@capire/reviews','app/vue')
app.serve ('/orders') .from('@capire/orders','app/orders')
})
if (!cds.env.production) cds.once ('bootstrap', require('../app/routes'))
// Mashing up bookshop services with required services...
cds.once ('served', async ()=>{
cds.once ('served', async ()=>{ // called by server.js
const CatalogService = await cds.connect.to ('CatalogService')
const ReviewsService = await cds.connect.to ('ReviewsService')

View File

@@ -1,13 +0,0 @@
using { sap } from '@sap/cds/common';
extend sap.common.Currencies with {
// Currencies.code = ISO 4217 alphabetic three-letter code
// with the first two letters being equal to ISO 3166 alphabetic country codes
// See also:
// [1] https://www.iso.org/iso-4217-currency-codes.html
// [2] https://www.currency-iso.org/en/home/tables/table-a1.html
// [3] https://www.ibm.com/support/knowledgecenter/en/SSZLC2_7.0.0/com.ibm.commerce.payments.developer.doc/refs/rpylerl2mst97.htm
numcode : Integer;
exponent : Integer; //> e.g. 2 --> 1 Dollar = 10^2 Cent
minor : String; //> e.g. 'Cent'
}

View File

@@ -1,6 +0,0 @@
code;name
g;Grams
kg;Kilograms
mg;Milligrams
lb;Pounds
oz;Ounces
1 code name
2 g Grams
3 kg Kilograms
4 mg Milligrams
5 lb Pounds
6 oz Ounces

View File

@@ -1,11 +0,0 @@
code;locale;name
g;de;Gramm
kg;de;Kilogramm
mg;de;Milligramm
lb;de;Pfund
oz;de;Unze
g;fr;Grammes
kg;fr;Kilogrammes
mg;fr;Milligrammes
lb;fr;Livres
oz;fr;Onces
1 code locale name
2 g de Gramm
3 kg de Kilogramm
4 mg de Milligramm
5 lb de Pfund
6 oz de Unze
7 g fr Grammes
8 kg fr Kilogrammes
9 mg fr Milligrammes
10 lb fr Livres
11 oz fr Onces

View File

@@ -1,3 +1,45 @@
using from './currencies';
using from './regions';
using from './uom';
using { sap } from '@sap/cds/common';
extend sap.common.Currencies with {
// Currencies.code = ISO 4217 alphabetic three-letter code
// with the first two letters being equal to ISO 3166 alphabetic country codes
// See also:
// [1] https://www.iso.org/iso-4217-currency-codes.html
// [2] https://www.currency-iso.org/en/home/tables/table-a1.html
// [3] https://www.ibm.com/support/knowledgecenter/en/SSZLC2_7.0.0/com.ibm.commerce.payments.developer.doc/refs/rpylerl2mst97.htm
numcode : Integer;
exponent : Integer; //> e.g. 2 --> 1 Dollar = 10^2 Cent
minor : String; //> e.g. 'Cent'
}
/**
* The Code Lists below are designed as optional extensions to
* the base schema. Switch them on by adding an Association to
* one of the code list entities in your models or by:
* annotate sap.common.Countries with @cds.persistence.skip:false;
*/
context sap.common.countries {
extend sap.common.Countries {
regions : Composition of many Regions on regions._parent = $self.code;
}
entity Regions : sap.common.CodeList {
key code : String(5); // ISO 3166-2 alpha5 codes, e.g. DE-BW
children : Composition of many Regions on children._parent = $self.code;
cities : Composition of many Cities on cities.region = $self;
_parent : String(11);
}
entity Cities : sap.common.CodeList {
key code : String(11);
region : Association to Regions;
districts : Composition of many Districts on districts.city = $self;
}
entity Districts : sap.common.CodeList {
key code : String(11);
city : Association to Cities;
}
}

View File

@@ -1,22 +0,0 @@
using { sap.common } from '@sap/cds/common';
namespace sap.common.countries;
extend common.Countries {
regions : Composition of many Regions on regions._parent = $self.code;
}
entity Regions : common.CodeList {
key code : String(5); // ISO 3166-2 alpha5 codes, e.g. DE-BW
children : Composition of many Regions on children._parent = $self.code;
cities : Composition of many Cities on cities.region = $self;
_parent : String(11);
}
entity Cities : common.CodeList {
key code : String(11);
region : Association to Regions;
districts : Composition of many Districts on districts.city = $self;
}
entity Districts : common.CodeList {
key code : String(11);
city : Association to Cities;
}

View File

@@ -1,19 +0,0 @@
using { sap.common.CodeList } from '@sap/cds/common';
namespace sap.common.uom;
entity PackagingUnits : CodeList {
key code : String(3); // e.g. pc for Piece(s)
abbrev : localized String(3); // e.g. st for Stück
}
entity Weights : CodeList {
key code : String(3); // e.g. kg for Kilogram(s)
}
entity Lengths : CodeList {
key code : String(3); // e.g. m for Meter(s)
}
type PackagingUnit : Association to PackagingUnits;
type Length : Association to Lengths;
type Weight : Association to Weights;

1
db Symbolic link
View File

@@ -0,0 +1 @@
shared-db/db

View File

@@ -79,6 +79,8 @@ aspect Hierarchy {
LimitedDescendantCount : Integer64 = null;
DistanceFromRoot : Integer64 = null;
DrillState : String = null;
Matched : Boolean = null;
MatchedDescendantCount : Integer64 = null;
LimitedRank : Integer64 = null;
}
@@ -86,6 +88,8 @@ annotate Hierarchy with @Capabilities.FilterRestrictions.NonFilterableProperties
'LimitedDescendantCount',
'DistanceFromRoot',
'DrillState',
'Matched',
'MatchedDescendantCount',
'LimitedRank'
];
@@ -93,6 +97,8 @@ annotate Hierarchy with @Capabilities.SortRestrictions.NonSortableProperties: [
'LimitedDescendantCount',
'DistanceFromRoot',
'DrillState',
'Matched',
'MatchedDescendantCount',
'LimitedRank'
];
@@ -115,6 +121,8 @@ annotate my.Genres with @Hierarchy.RecursiveHierarchy #GenreHierarchy: {
LimitedDescendantCount: LimitedDescendantCount,
DistanceFromRoot : DistanceFromRoot,
DrillState : DrillState,
Matched : Matched,
MatchedDescendantCount: MatchedDescendantCount,
LimitedRank : LimitedRank
};

View File

@@ -7,7 +7,7 @@
"express": "^4.17.1"
},
"devDependencies": {
"@cap-js/sqlite": ">=1"
"@cap-js/sqlite": "^1"
},
"scripts": {
"start": "cds-serve",

32
fiori/server.js Normal file
View File

@@ -0,0 +1,32 @@
const cds = require('@sap/cds/lib')
if (cds.requires.db?.kind === 'sqlite') {
cds.on ('serving:AdminService', srv => srv.prepend(() => {
const {Genres} = srv.entities
// Register a simplistic handler for hierarchical queries
srv.on('READ', Genres, (req,next) => {
const q = req.query
// Expand query on a single row
if (q.SELECT.recurse?.where?.[0].ref[0] === 'Distance') {
q.SELECT.where[0] = 'parent_ID'
// Initial query
} else if (!q.SELECT.search && !is_count(q)) {
q.SELECT.where ??= [ 'parent_ID is null' ]
}
// Use scalar subselect for DrillState
q.SELECT.from.as = 'g'
q.SELECT.columns = q.SELECT.columns.map (c => {
if (c.ref == 'DrillState') return { xpr:[`
CASE WHEN ( SELECT count(1) from ${Genres} where parent_ID = g.ID ) > 0
THEN 'collapsed' ELSE 'leaf' END`
], as: 'DrillState' }
else return c
})
// Suppress error message: Feature "recurse" queries not supported.
delete q.SELECT.__proto__.recurse
delete q.SELECT.recurse
return next()
})
}))
}
const is_count = q => q.SELECT.columns?.length === 1 && q.SELECT.columns[0].func === 'count'

View File

@@ -9,7 +9,7 @@ build-parameters:
- builder: custom
commands:
- npm ci
- npx cds build shared-db --for hana --production
- npx cds build --production
- npx cds build orders --for nodejs --production --ws-pack
- npx cds build reviews --for nodejs --production
- npx cds build bookstore --for nodejs --production --ws-pack
@@ -78,14 +78,6 @@ modules:
- name: samples-auth
- name: samples-destination
- name: samples-db-deployer
type: hdb
path: shared-db/gen/db
parameters:
buildpack: nodejs_buildpack
requires:
- name: samples-db
- name: samples
type: approuter.nodejs
path: .deploy/app-router
@@ -93,6 +85,8 @@ modules:
keep-existing-routes: true
disk-quota: 256M
memory: 256M
properties:
TENANT_HOST_PATTERN: "^(.*)-${default-uri}"
requires:
- name: orders-api
group: destinations
@@ -112,7 +106,17 @@ modules:
name: bookstore-api # must be used in xs-app.json as well
url: ~{srv-url}
forwardAuthToken: true
- name: mtx-api
group: destinations
properties:
name: mtx-api # must be used in xs-app.json as well
url: ~{mtx-url}
- name: samples-auth
provides:
- name: app-api
properties:
app-protocol: ${protocol}
app-uri: ${default-uri}
- name: destination-content
type: com.sap.application.content
@@ -145,6 +149,27 @@ modules:
TokenServiceInstanceName: samples-auth
TokenServiceKeyName: xsuaa-service-key
- name: samples-mtx
type: nodejs
path: gen/mtx/sidecar
build-parameters:
builder: npm
parameters:
instances: 1
memory: 256M
disk-quota: 512M
provides:
- name: mtx-api
properties:
mtx-url: ${default-url}
requires:
- name: samples-db
- name: samples-registry
- name: samples-auth
- name: app-api
properties:
SUBSCRIPTION_URL: ~{app-protocol}://\${tenant_subdomain}-~{app-uri}
resources:
- name: samples-messaging
type: org.cloudfoundry.managed-service
@@ -156,23 +181,51 @@ resources:
emname: bookstore-${org}-${space}
namespace: cap/samples/${space}
- name: samples-db
type: com.sap.xs.hdi-container
type: org.cloudfoundry.managed-service
parameters:
service: hana
service-plan: hdi-shared
service: service-manager
service-plan: container
- name: samples-auth
type: org.cloudfoundry.managed-service
processed-after:
- samples-messaging
requires:
- name: app-api
parameters:
service: xsuaa
service-plan: application
path: ./xs-security.json
config:
xsappname: samples-${org}-${space}
tenant-mode: dedicated
tenant-mode: shared
oauth2-configuration:
redirect-uris:
- https://*-~{app-api/app-uri}/**
- name: samples-destination
type: org.cloudfoundry.managed-service
parameters:
service: destination
service-plan: lite
- name: samples-registry
type: org.cloudfoundry.managed-service
requires:
- name: mtx-api
parameters:
service: saas-registry
service-plan: application
config:
xsappname: samples-${org}-${space}
appName: samples-${org}-${space}
displayName: samples-shared-db
description: CAP Samples with shared-db and multitenancy
category: 'Samples shared-db'
appUrls:
getDependencies: ~{mtx-api/mtx-url}/-/cds/saas-provisioning/dependencies
onSubscription: ~{mtx-api/mtx-url}/-/cds/saas-provisioning/tenant/{tenantId}
onSubscriptionAsync: true
onUnSubscriptionAsync: true
onUpdateDependenciesAsync: true
callbackTimeoutMillis: 300000 # Increase if your deployments are taking longer than that

19
mtx/sidecar/package.json Normal file
View File

@@ -0,0 +1,19 @@
{
"name": "samples-mtx",
"dependencies": {
"@cap-js/hana": "^1",
"@sap/cds": "^8",
"@sap/cds-mtxs": "^2",
"@sap/xssec": "^4",
"express": "^4"
},
"devDependencies": {
"@cap-js/sqlite": "^1"
},
"scripts": {
"start": "cds-serve"
},
"cds": {
"profile": "mtx-sidecar"
}
}

View File

@@ -23,8 +23,8 @@
};
</script>
<script id="sap-ushell-bootstrap" src="https://ui5.sap.com/test-resources/sap/ushell/bootstrap/sandbox.js"></script>
<script id="sap-ui-bootstrap" src="https://ui5.sap.com/resources/sap-ui-core.js"
<script id="sap-ushell-bootstrap" src="https://sapui5.hana.ondemand.com/test-resources/sap/ushell/bootstrap/sandbox.js"></script>
<script id="sap-ui-bootstrap" src="https://sapui5.hana.ondemand.com/resources/sap-ui-core.js"
data-sap-ui-libs="sap.m, sap.ushell, sap.collaboration, sap.ui.layout"
data-sap-ui-compatVersion="edge"
data-sap-ui-theme="sap_horizon"

View File

@@ -2,15 +2,18 @@
"name": "@capire/orders",
"version": "1.0.0",
"dependencies": {
"@cap-js/hana": ">=1",
"@cap-js/hana": "^1",
"@capire/common": "*",
"@sap/cds": ">=5",
"@sap/xssec": "^4"
"@sap/xssec": "^4",
"@sap/cds-mtxs": "^2"
},
"cds": {
"requires": {
"messaging": true,
"db": true
"db": true,
"multitenancy": true,
"auth": "xsuaa"
}
},
"scripts": {

View File

@@ -5,15 +5,8 @@
"repository": "https://github.com/sap-samples/cloud-cap-samples.git",
"author": "daniel.hutzel@sap.com",
"workspaces": [
"bookshop",
"bookstore",
"common",
"fiori",
"orders",
"reviews",
"shared-db",
"etc/data-viewer",
"etc/loggers"
"./*",
"./etc/*"
],
"devDependencies": {
"@cap-js/cds-test": "^0"
@@ -40,5 +33,15 @@
"timeout": 6666
},
"license": "SEE LICENSE IN LICENSE",
"private": true
}
"private": true,
"dependencies": {
"@sap/cds-mtxs": "^2"
},
"cds": {
"profile": "with-mtx-sidecar",
"requires": {
"multitenancy": true,
"auth": "xsuaa"
}
}
}

View File

@@ -7,15 +7,18 @@
"index.cds"
],
"dependencies": {
"@cap-js/hana": ">=1",
"@cap-js/hana": "^1",
"@sap/cds": ">=5",
"@sap/xssec": "^4.2.7",
"express": "^4.17.1"
"express": "^4.17.1",
"@sap/cds-mtxs": "^2"
},
"cds": {
"requires": {
"messaging": true,
"db": true
"db": true,
"multitenancy": true,
"auth": "xsuaa"
}
},
"scripts": {

View File

@@ -3,7 +3,7 @@ module.exports = cds.service.impl (function(){
// Get the CSN definition for Reviews from the db schema for sub-sequent queries
// ( Note: we explicitly specify the namespace to support embedded reuse )
const { Reviews, Likes } = this.entities
const { Reviews, Likes } = this.entities ('sap.capire.reviews')
this.before (['CREATE','UPDATE'], 'Reviews', req => {
if (!req.data.rating) req.data.rating = Math.round(Math.random()*4)+1

View File

@@ -2,11 +2,6 @@
"name": "@capire/shared-db",
"version": "3.0.0",
"description": "CAP Sample CDS model deployment for shared-db scenario",
"dependencies": {
"@capire/bookstore": "*",
"@capire/orders": "*",
"@capire/reviews": "*"
},
"cds": {
"sql": {
"native_hana_associations": false

View File

@@ -1,5 +1,5 @@
{
"devDependencies": {
"@cap-js/sqlite": "*"
"@cap-js/sqlite": "^1"
}
}

View File

@@ -14,6 +14,13 @@
{
"name": "$XSAPPNAME.emmanagement",
"description": "Enterprise-Messaging Management Access"
},
{
"name": "$XSAPPNAME.mtcallback",
"description": "Subscription via SaaS Registry",
"grant-as-authority-to-apps": [
"$XSAPPNAME(application,sap-provisioning,tenant-onboarding)"
]
}
],
"attributes": [],