This blog post gives an example for an eventing scenario which includes a multitenant application.
It runs on SAP Business Technology Platform, Cloud Foundry environment and uses SAP Event Mesh.
This is not an official reference application, it is just some sample code to help you in quickly setting up your project.
Quicklinks:
Quick Guide
Sample Code
Introduction
Eventing scenario:
This is – as usual – represented by sender and receiver applications.
In our example, both applications run in the cloud, i.e. in the SAP Business Technology Platform (SAP BTP).
Extension scenario:
To illustrate the technical setup, a simple example is helpful.
In this blog post we imagine an international soccer club.
It uses a SAP on-Premise enterprise system.
And there is an extension in SAP BTP, connected to the on Prem system via eventing.
Multitenancy scenario:
The interesting part is the multitenant app.
We’re assuming, the app that represents the on Prem system is a normal singletenant app.
The extension app is designed as multitenant app (SaaS – software-as-a-service).
Example scenario:
As an example, we’ve chosen an international soccer club.
To manage all the enterprise functionality like stadium, employees, customers, profits, products, etc it uses an SAP enterprise system.
Now as it has grown popular, it needs an extension to the software. It should help in managing fans and merchandising articles all over the world.
SAP BTP is ideal platform for such extension.
And using eventing is the core to keep the data in sync at real time.
To keep our example small, we just offer functionality to register new fans.
While visitors of the stadium can still register there (offline) as fans, it is now possible to use the cloud app to enter a name, and – shoot -> new fan registered.
Architecture:
We’re using 2 subaccounts in SAP BTP.
The first one is the customer account – it is the customer’s world.
Here we deploy the singletenant app, representing the onPrem system.
Let’s call it soccerclub.
The second subaccount is owned by SAP (or partner) and it is the provider account.
Here we deploy the multitenant application.
We call it fanshop – it is accessed by fans all over the world, they can purchase merchandising articles and can register as fans and subscribe to events, newsletters, all that stuff.
As it is a multitenant app, the customer subscribes to it in the customer subaccount (the first one).
The diagram shows our 2 subaccounts, the 2 deployed applications and the subscription:
Security:
To keep it really simple, we don’t support security in this first version.
We’ll cover it in next blog post.
Content
1. Create Soccerclub App
2. Create Fanshop App
3. Run the Scenario
Appendix 1: Soccerclub App
Appendix 2: Fanshop App
Prerequisites
Access to SAP BTP Cloud Foundry environment, productive environment, including 2 subaccounts.
Basic understanding of multitenancy and Cloud Foundry.
Basic Node.js skills
Preparation
Subaccounts
We need 2 subaccounts, as usual for multitenancy scenarios.
One of them will be the provider account and it needs to be entitled for creating an instance of SAP Event Mesh.
Create Project
We create a root project folder C:club containing 2 subfolders for the 2 applications
C:club
fanshop
soccerclub
Or this screenshot:
Each app folder contains a few files required for little node server apps:
C:club
fanshop
config-messaging.json
config-saasreg.json
config-security.json
manifest.yml
package.json
server.js
soccerclub
manifest.yml
package.json
server.js
For your convenience please refer to below screenshot
The content of the files can be copied from the Appendix section.
1. Create Soccerclub App
We start with the application that represents the singletenant on-Premise system.
From messaging perspective, it is the message receiver.
It is as simple as possible, it just displays the fans that are stored in the system.
In addition:
It is already aware of receiving events.
It offers a webhook endpoint for incoming events.
We will send the events in our second application.
1.0. Environment
Before we start, we make sure that we’ve chosen the proper environment, with respect to cloud account.
The “soccerclub” app represents the customer’s installation of enterprise system, as such we deploy it to the BTP-subaccount which we’ve chosen to be the customer subaccount (or consumer subaccount).
1.1. Create service instances
Our simple onPrem fake app doesn’t use any cloud services.
What we need it the Event Mesh dashboard.
Our customer wants connection based on eventing, so he needs to do some configuration for the eventing.
So we need the event mesh dashboard in the customer account.
It is subscription based, we only need the required entitlements and we can subscribe.
Subscribe to Event Mesh Dashboard
To use the dashboard, we need to subscribe to the “standard” plan.
We go you our subaccount (the one we use as customer).
The we go to Service Marketplace.
We search for Event Mesh – then choose the “Application Plan” with name “standard” and press “create”:
After the subscription is created, we can NOT press on “Go to Application”.
To access the dashboard, our user needs to have the required roles.
Roles
To assign the required roles to our user:
In our subaccount, we go to Security->Role Collections and create a new Role Collection.
We open it, press “Edit” and go to the “Roles” tab.
We open the value help of the columen “Role Name”.
To find the Event-Mesh-roles, we select “xbem-app” as filter for “Application Identifier”.
Then we can just select all roles and press “Add”.
Then we need to add our user in the “Users” section.
Note that we should use the proposal of the UI.
To add the user, we need to press the big +
Finally we press “Save” in the upper right corner.
Open
Now we can go back to the “Instances and Subscriptions” screen, choose our Event Mesh subscription and press on “Go to Application”.
We see: it is empty.
That is expected, as we haven’t created a messaging client yet.
1.2. Create app
Our simple app consists of 3 files
package.json
The app is very simple and just requires express to run a server:
{
"dependencies": {
"express": "^4.17.1"
manifest.yml
The manifest is short and basically defines the name of our app and its URL:
applications:
- name: soccerclubapp
routes:
- route: soccerclubapp.cfapps.sap.hana.ondemand.com
server.js
The application code defines an endpoint which allows for incoming POST requests.
Such requests are sent from the SAP Event Mesh webhook subscription (we’ll register our endpoint in the Event Mesh dashboard in chapter 3).
Each event that is fired from our extension app will be forwarded to our endpoint.
As such, the implementation of the endpoint takes care of accessing the payload of the event.
For simplicity reasons, we just print it to the log.
app.post('/webhook/fanshopevents', (req, res) => {
console.log(`===> [/webhook/fanshopevents] received message: ${JSON.stringify(req.body)}.`)
res.status(201).send()
The complete code can be found in the Appendix section.
That’s already it.
1.3. Deploy and Test
We can test the endpoint with a postman request as follows:
URL | https://soccerclubapp.cfapps.sap.hana.ondemand.com/webhook/fanshopevents |
Method | POST |
Headers | content-type:application/json |
Body | {“message”:”hello”} |
After firing the request, we check the log of our app and see the message printed there.
Small recap
We’ve created and deployed a little app that represents an SAP enterprise system.
It provides an endpoint that will be used for webhook subscription.
It receives events coming from SAP Event Mesh.
It reads the event payload and writes it to the console.
Now that our enterprise system is up and running and ready to receive messages, we can go ahead and create our multitenant extension app to send events.
2. Create Fanshop App
In our scenario, we write an extension app that is designed as multitenant app.
For instance, an SAP partner develops the multitenant app and deploys it to his (provider) account.
The customer (soccer club) subscribes to it in his own (customer) account.
The app can then be used by fans for online registration.
2.0. Environment
Before we start, we make sure that we’ve chosen the proper environment, with respect to cloud account.
The fanshop app represents the partner development of an extension , designed as multitenant application.
As such we deploy it to the BTP-subaccount which we’ve chosen to be the provider subaccount.
2.1. Create service instances
We need instances of Event Mesh (for sending events), SaaS registry (for multitenancy) and XSUAA (required by SaaS Registry).
The creation command uses config files which we created in the preparation section and which can be found in the appendix.
Below diagram illustrates the instances and subscriptions created in each subaccount:
Create Event Mesh instance
The important setting, to make the Event Mesh tenant-aware, is the instance-type property which needs to be set to reuse:
{
"emname": "fanshopmessagingclient",
"namespace": "zsoccerclub/scenario/fanshop",
"instanceType": "reuse",
"options": {
"management": true,
"messagingrest": true,
"messaging": true
Reminder:
make sure to switch to the provider subaccount before creating the service instances
The creation command:
cf cs enterprise-messaging default fanshopMsg -c config-messaging.json
Note:
The service plan “default” must be used, it is not available in Trial account
Note:
New “namespace” must be registered
Create instance of XSUAA
We aren’t applying security features to our apps, however, we need to create an instance of XSUAA, because it is required by SaaS Registry.
Our XSUAA instance needs to be multitenant aware:
{
"xsappname": "fanshopxsappname",
"tenant-mode": "shared"
}
cf cs xsuaa application fanshopXsuaa -c config-security.json
We need to specify the xsappname of our XSUAA instance when we configure the SaaS Registry.
Since the xsappname is generated during creation, we need to create a Service Key:
cf csk fanshopXsuaa sk
Once we have the service key, we should view the content, so we can see the generated name:
cf service-key fanshopXsuaa sk
from the service key content, we copy the value of the property xsappname, it should look like this:
Once copied the xsappname, we don’t need the service key anymore.
Poor service key….
Create instance of SaaS Registry
Before we proceed, we have to copy&paste the value of property xsappname to the config file of saas registry, as value of property appid.
In my example:
{
"appId": "fanshopxsappname!t14860",
"appName": "fanshopSaasregAppname",
"appUrls": {
"getDependencies" : https://fanshopapp.cfapps.sap.hana.ondemand.com/mtcallback/dependencies,
"onSubscription" : https://fanshopapp.cfapps.sap.hana.ondemand.com/mtcallback/{tenantId}
Note:
The URLs need to be adapted to your landscape and don’t forget to paste your xsappname.
So now we can create the saas-registry instance:
cf cs saas-registry application fanshopSaasreg -c config-saasreg.json
2.2. Create Application
After the service instances are ready, let’s go through the process of creating the multitenant app.
Again, we need 3 files.
manifest.yml
The deployment descriptor specifies the dependencies to our 3 service instances and the URL for access.
The app will be used by subscriber, as such we need to define a route which contains the subdomain of the subscribing subaccount as prefix.
e.g.
consumer-fanshopapp.cfapps.eu10.hana.ondemand.com
or more generic:
<subdomain>-<app>.cfapps.eu10.hana.ondemand.com
This is the usual way of developing multitenant applications:
The subdomain of the subscriber is concatenated with the app-URL.
We’ll see it below.
Once it comes to productive usage, the subdomain concatenation is replaced by custom domains.
In the meantime, our subscriber can only open the app if there is a route for it.
That’s why we specify the route in the manifest, so it will be created during deploy.
This is of course a little trick: we already know the subdomain that will be subscribing, because we’re in testing mode.
How to retrieve the subdomain of the subaccount?
Go to the subaccount used for customer, go to the overview page and copy the subdomain from there:
In my example, the manifest and its routes look as follows:
applications:
- name: fanshopapp
routes:
- route: fanshopapp.cfapps.sap.hana.ondemand.com
- route: consumer-fanshopapp.cfapps.sap.hana.ondemand.com
Note:
You need to adapt the first route to match your landscape,
and the second route, to match your subdomain.
Note:
If you aren’t sure, you can as well omit the second route in the manifest.
Alternatively, after deployment and subscription, you can still create and map a new route with the following command:
map-route myApp example.com –hostname myhost
Result:
myhost.example.com
Example:
cf map-route fanshopapp cfapps.sap.hana.ondemand.com –hostname consumer-fanshopapp
package.json
This application will send events to Event Mesh and it will be using the REST API, as offered by Event Mesh.
We’re using the node-fetch module for firing the request and we’re using the @sap/xssec module for fetching a JWT token.
{
"dependencies": {
"@sap/xssec": "latest",
"express": "^4.16.2",
"node-fetch": "2.6.2"
server.js
Our fanshop application is a server app which serves a very simple homepage which is reached when pressing “Go to Application” after subscription.
But first of all, we have to implement the 3 multitenancy callbacks, which are invoked when a subscriber presses the “Create Subscription” button.
Let’s have a quick look into the subscribe-callback.
The SaaS Registry calls us with an UPDATE operation and sends the info about the current subscriber (subdomain, tenant-id, etc) in the request body.
What we have to do in the code is to send back the app-URL for this specific subscriber.
Like that, every subscriber will have his own application-URL, dedicated for him.
In our example, during development/testing phase, what we do is:
We access the data in the request body.
We take the subdomain.
We prepend it in front of the normal app-URL
And at the end, we append the concrete endpoint of the app homepage (typically an index.html).
Unclear?
We want the final app URL should look like this:
<subdomain>-<appURL>/<endpoint>
So in my example, the resulting URL would be:
https://consumer-fanshopapp.cfapps.sap.hana.ondemand.com/app
This URL can only be used by the customer owning the subaccount with subdomain “consumer”.
app.put('/mtcallback/:tenant_id', (req, res) => {
const appHost = req.hostname
const subDomain = req.body.subscribedSubdomain
res.status(200).send(`https://${subDomain}-${appHost}/app`)
})
Also important to note:
The dependencies callback.
The SaaS Registry asks us if we have dependencies that need to be notified about the subscription.
If yes, then the SaaS Registry will also invoke the callbacks of the (multitenant-aware) services.
In our case: in fact, we’re using the Event Mesh service and it is tenant-aware and it needs to be notified.
We will see below, why this is important.
In the code, we just return the value of xsappname of our Event Mesh instance (which we read from binding):
app.get('/mtcallback/dependencies', (req, res) => {
res.status(200).json([{'xsappname': CREDENTIALS.uaa.xsappname }])
})
OK, assuming the subscription is done, what do we want to achieve with our application?
First, we need a homepage to which the end-user is taken when pressing “Go to Application”.
Furthermore, our app should offer just one capability:
The end-user should be able to “register” as “fan”.
Our app does nothing.
But under the hood, we send some data as message to Event Mesh – which is supposed to forward it to our enterprise system, the soccerclub application.
Our “homepage” is an endpoint that contains just a title – and a link which takes the user to the second endpoint, the /register endpoint:
app.get('/app', function(req, res){
const url = `https://${req.hostname}/register?name=JoeCool`
res.send(`<h1>Fanshop Homepage</h1>Click <a href="${url}">here</a> to register as Fan.`);
})
The /register endpoint supports one parameter, which is the name of the registered user.
To keep the UI design simple, we just hard-code the fan-name in our code (avoiding an input field).
The /register endpoint does nothing than sending a message to Event Mesh.
The message contains just one property, the fan name, and we take the value from the URL parameter.
The message is structured as JSON, however we need the JSON as a string, to send it in the HTTP request (see below).
One thing we have to note:
We’re a multitenant app, so we need to be tenant-aware.
In our prototype implementation, we extract the tenant info from the URL.
Then we pass the tenant-specific subdomain to our helper method.
app.get('/register', async (req, res) => {
const hostname = req.hostname // hostname is: consumer-fanshopapp.cfapps.sap.hana.ondemand.com
const subdomain = hostname.substring(0,hostname.lastIndexOf('-')) // subdomain is consumer
const msg = `{"name": "${req.query.name}"}`
const result = await _sendMessage(msg, 'fanshopQueue', CREDENTIALS, subdomain)
res.send(`Thank you for your registration as new Fan, '${req.query.name}'. Result: ${result}.`)
})
Note:
We pass the name of the messaging queue, which we created above.
If you’ve used a different name, you need to adapt it here.
Here we pass only the name suffix, the full queue name (including namespace) will be generated below.
The code for sending messages can be found in some helper functions.
To send the messages, we use the REST API which is provided by Event Mesh.
The API is protected with OAuth
As such, we need to first fetch a valid JWT token.
The credentials for the fetch-token request are handed over to us in the binding of our app.
We access the binding information with the help of the library @sap/xsenv.
To fetch the JWT token, we use the library @sap/xssec.
async function _fetchJwtToken (credentials, subdomain){
return new Promise ((resolve, reject) => {
xssec.requests.requestClientCredentialsToken(subdomain, credentials, null, null, (error, token)=>{
resolve(token)
The request function of the library is tenant-aware, which means that we can pass the subdomain to get a tenant-specific token.
What does that mean?
We deploy the multitenant app to our provider subaccount.
As a consequence, the URL for fetching the token corresponds to the subdomain of the provider account.
e.g.
https://provider.authentication…com
However, the application is subscribed and used by a customer and “lives” in the subaccount of the customer.
As such, the token request needs to be sent to the URL of the customer.
e.g.
https://customer.authentication…com
That’s the reason why the xssec lib needs the subdomain info, to replace it in the URL.
To send the request to the REST API, we need the URL of the API.
The URL can be found in the binding.
However, the Event Mesh supports 3 different protocols, such that 3 different sections with 3 different URLs can be found.
We need to search for the httprest protocol to get the proper URL.
We take the shortcut (not recommended) of directly accessing the third node (which is hopefully always the httprest):
const uri = msgCredentials.messaging[2].uri
Now, this uri is not enough.
We want to send a message to a queue.
As such, the final URL will also contain the name of the queue in the URL.
We need to concatenate it.
However one more preparation step is required:
The full queue name contains slashes which need to be encoded (sigh….)
Like this:
soccerclub/scenario/fanshop
==>
soccerclub%2Fscenario%2Ffanshop
The request is a POST request and the event payload is sent in the request body.
Our message is a small JSON object, but it has to be a String, to be passed to the HTTP request.
Below we can see it is a string:
const msg = `{"name": "${req.query.name}"}`
Finally, we can send the request.
const options = {
method: 'POST',
body: msg,
headers: {
Authorization: 'Bearer ' + jwtToken,
'Content-Type': 'application/json',
'x-qos' : 0 // or 1
}
}
const response = await fetch(messagingRestUrl, options)
The complete code can be found in the Appendix section.
Note:
As usual, the code is meant to be short for good overview, it needs rework obviously.
2.3. Deploy and Test
Before we deploy, we make sure that our Cloud Foundry CLI is still targetting the provider account.
After deploy we can open our application in the provider account to see that it works.
However, this is not the way how it is meant to be used.
We’ve added this route to the manifest, just to be able to test the app in the provider account.
We can open the “homepage”.
But we cannot press the link, as it would cause errors, because we’re not using tenant-specific URL.
So what we want to do is to subscribe to the multitenant app in the consumer account.
3. Run the Scenario
Subscribe
We go to our customer subaccount, open the “Service Marketplace” screen.
We search for our fanshop app, e.g. by typing “fanshop” in the filter.
Once we’ve found it, we can press “Create” in the context menu.
We again press “Create” in the dialog for creating the subscription.
Our callbacks are invoked (e.g. to notify Event Mesh about the subscription) and our fanshop app is added to the list of subscriptions.
All good – we COULD go ahead and “Go to Application” – but we don’t
Create Queue
Before we start using our subscribed app, we need to do one essential configuration step, which is to create the queue, to which we send our events.
We’ve already used the queue name in the code, but we couldn’t create the queue before the subscription of our fanshop app.
So let’s again open the Event Mesh dashboard, in the customer subaccount.
“Instances and Subscriptions” screen, choose our Event Mesh subscription and press on “Go to Application”.
Now we can see the messaging client.
After fanshop-subscription, the messaging client which we created in the provider account, has been made available in the customer account.
Note:
Have you noticed?
Type is “Subscription”, not “Instance”.
This has been made possible by 2 settings which we did earlier:
We created the event mesh service instance with “reuse”.
And we added the event mesh service in the dependencies callback.
We click on the tile, go to the “Queues” tab and create a queue with name “fanshopQueue”
Note:
The name is just a suffix which is automatically concatenated with the namespace (which we defined in the service descriptor while creating the service instance)
Test
Optionally, we can now go to the “Test” tab and send a message to the queue and see how the count is incremented
Send
Now we can go to our subscription of our fanshop app and press “Go to Applicaton”.
Our (simple) fanshop homepage is displayed.
Now we click the link to register a hardcoded dummy fan.
We’re taken to the /register endpoint, and under the hood an event has been fired.
In the response, which is displayed in the browser, we can see the name of the registered fan.
In the URL, we can see the hardcoded name-parameter
Check
To verify that the message has really arrived to Event Mesh, we can go to the dashboard, then go to the “Test” tab.
In the “Consume Messages” section, we can select our queue, check the number of messages, “Consume” the messages and view the message payload.
Create Webhook Subscription
Up to now, we’ve successfully verified the connection from the sender (which is a tenant of a multitenant app) to the Event Mesh.
Now we need to connect our receiving soccerclub app to the Event Mesh, to receive and view the events in the fake-enterprise-system app.
What we want to achieve:
Each message that is sent to the queue should be forwarded to the soccerclub app.
With other words, we want to subscribe to the queue (don’t confuse the “subscription” of a multitenant app with the subscription of messaging queue).
So now let’s go to the Event Mesh Dashboard and create a Webhook Subscription.
In the “Webhooks” tab, we press “Create Webhook”.
In the dialog, we configure the settings as described below:
Subscription Name:
We can enter anything of our choice, e.g. “SubscribeToFanshopEvents”.
Webhook URL:
The webhook URL is the one that we mentioned above, in chapter 1.3.
In my example:
https://soccerclubapp.cfapps.sap.hana.ondemand.com/webhook/fanshopevents
Note:
You might need to adapt the URL to match your landscape.
Authentication:
For our simple scenario, we choose “No Auth”.
You may refer to this blog post for secured webhook.
The other settings can be entered according to below screenshot:
After creation, we can open the “Actions” menu and run the “Trigger Handshake”.
Send events
We go to our customer subaccount and open our fanshop application.
https://consumer-fanshopapp.cfapps.sap.hana.ondemand.com/app
We click on the link to register a dummy fan and trigger an event.
Alternatively, to circumvent the hardcoded fan-name, we can directly call the endpoint with name param in the URL.
E.g.
https://consumer-fanshopapp.cfapps.sap.hana.ondemand.com/register?name=John
Receive events
As mentioned above, we don’t have any UI, so we need to verify the received event in the Cloud Foundry log.
To do so, we point our Cloud Foundry Command Line Client to the correct org and space of our customer subaccount:
cf t -o myOrg -s mySpace
then we run the command to stream the logs:
cf logs soccerclubapp
Now we can proceed sending more messages.
The result can be seen immediately in the console:
cleanup
At the end of the scenario, to remove all our artifacts, we need to first delete the subscription.
Afterwards, delete the apps and services in the respective subaccount.
1. Unsubscribe
2. Delete artifacts
For your convenience, the commands to clean up our space:
cf d soccerclubapp -f -r
cf d fanshopapp -f -r
cf dsk fanshopXsuaa sk -f
cf ds fanshopXsuaa -f
cf ds fanshopSaasreg -f
cf ds fanshopMsg -f
Troubleshooting
If things don’t work, we need to add error handling to our code.
First of all, we need to check if messages arrive in Event Mesh.
To do so, we can set the webhook subscription to “pause”.
If messages don’t arrive, we can check the response of the Event Mesh REST API in our fanshop app. The above Implementiation just returns the response status code.
To read the response body, we can add the following code to the end of the _sendMessage function:
console.log('=================>' + response.status + JSON.stringify(await response.json()) )
If there’s an error saying that “queue not found”, then the reason might be that the queue was not created in the customer subaccount.
One more hint: make sure to invoke the fanshop app in the customer subaccount, not the provider subaccount. The URL should look like this:
https://consumer-fanshopapp.cfapps.sap.hana.ondemand.com/register?name=test
Summary
In the present tutorial, we’ve created a little scenario, where 2 applications are connected via Event Mesh.
One of the applications is a multitenant app and it sends events to Event Mesh.
The receiver app is connected to Event Mesh via Webhook subscription.
The basic learning is: how to configure Event Mesh to support multitenancy.
The setting: “instanceType”: “reuse”
Quick Guide
Using Event Mesh in a multitenant app requires the following property in the configuration:
"instanceType": "reuse"
Links
SAP Help Portal:
SAP Event Mesh landing page.
SAP Event Mesh docu about JSON params.
SAP Event Mesh documentation about REST API.
SAP Event Mesh documentation about Entitlements.
Next version of this app, including security, UI and approuter.
Blog post about Webhook Subscription
XSUAA documentation about the xs-security.json parameters.
Node.js module documentation;
https://github.com/bitinn/node-fetch
https://www.npmjs.com/package/@sap/xssec
Appendix 1: Soccerclub Application
manifest.yml
---
applications:
- name: soccerclubapp
memory: 64M
routes:
- route: soccerclubapp.cfapps.sap.hana.ondemand.com
package.json
{
"dependencies": {
"express": "^4.17.1"
}
}
server.js
const express = require('express')
const app = express();
app.use(express.json())
app.post('/webhook/fanshopevents', (req, res) => {
console.log(`===> [/webhook/fanshopevents] received message: ${JSON.stringify(req.body)}.`)
res.status(201).send()
})
app.listen(process.env.PORT, () => {
console.log('===> Server running.')
})
Appendix 2: Fanshop Application
config-messaging.json
{
"emname": "fanshopmessagingclient",
"namespace": "soccerclub/scenario/fanshop",
"version": "1.1.0",
"instanceType": "reuse",
"options": {
"management": true,
"messagingrest": true,
"messaging": true
},
"rules": {
"queueRules": {
"publishFilter": [
"${namespace}/*"
],
"subscribeFilter": [
"${namespace}/*"
]
},
"topicRules": {
"publishFilter": [
"${namespace}/*"
],
"subscribeFilter": [
"${namespace}/*"
]
}
}
}
config-saasreg.json
{
"appId": "fanshopxsappname!t14860",
"appName": "fanshopSaasregAppname",
"appUrls": {
"getDependencies" : "https://fanshopapp.cfapps.sap.hana.ondemand.com/mtcallback/dependencies",
"onSubscription" : "https://fanshopapp.cfapps.sap.hana.ondemand.com/mtcallback/{tenantId}"
},
"displayName": "Fanshop MT app (based on XSUAA, SaaSreg, Enterprise-Messaging)"
}
config-security.json
{
"xsappname": "fanshopxsappname",
"tenant-mode": "shared"
}
manifest.yml
---
applications:
- name: fanshopapp
memory: 512M
routes:
- route: fanshopapp.cfapps.sap.hana.ondemand.com
- route: AsincConsumer-fanshopapp.cfapps.sap.hana.ondemand.com
services:
- fanshopMsg
- fanshopXsuaa
- fanshopSaasreg
package.json
{
"dependencies": {
"@sap/xsenv": "latest",
"@sap/xssec": "latest",
"express": "^4.16.2",
"node-fetch": "2.6.2"
}
}
server.js
const xsenv = require('@sap/xsenv')
const fetch = require('node-fetch')
const express = require('express')
const app = express()
const xssec = require('@sap/xssec')
app.use(express.json())
const CREDENTIALS = xsenv.getServices({ myMessaging: { tag: 'enterprise-messaging'} }).myMessaging
/* App server */
app.listen(process.env.PORT, () => {})
/* App endpoints */
app.get('/app', function(req, res){
const url = `https://${req.hostname}/register?name=JoeCool`
res.send(`<h1>Fanshop Homepage</h1>Click <a href="${url}">here</a> to register as Fan.`)
})
app.get('/register', async (req, res) => {
const hostname = req.hostname // hostname is: consumer-fanshopapp.cfapps.sap.hana.ondemand.com
const subdomain = hostname.substring(0,hostname.lastIndexOf('-')) // subdomain is consumer
const msg = `{"name": "${req.query.name}"}`
const result = await _sendMessage(msg, 'fanshopQueue', CREDENTIALS, subdomain)
res.send(`Thank you for your registration as new Fan, '${req.query.name}'. Result: ${result}.`)
})
/* Multi Tenancy callbacks */
app.put('/mtcallback/:tenant_id', (req, res) => {
const appHost = req.hostname
const subDomain = req.body.subscribedSubdomain
res.status(200).send(`https://${subDomain}-${appHost}/app`)
})
app.delete('/mtcallback/:tenant_id', (req, res) => {
res.status(200).end('unsubscribed')
})
app.get('/mtcallback/dependencies', (req, res) => {
res.status(200).json([{'xsappname': CREDENTIALS.uaa.xsappname }])
})
/* HELPER */
async function _fetchJwtToken (credentials, subdomain){
return new Promise ((resolve, reject) => {
xssec.requests.requestClientCredentialsToken(subdomain, credentials, null, null, (error, token)=>{
resolve(token)
})
})
}
function _composeMsgRestUrlForSendMsg (msgCredentials, queueNamePostfix){
const slash = '%2F'
const namespace = msgCredentials.namespace
const namespaceEncoded = namespace.replace(///g, slash)
const fullQueue = namespaceEncoded + slash + queueNamePostfix
const uri = msgCredentials.messaging[2].uri
return `${uri}/messagingrest/v1/queues/${fullQueue}/messages`
}
async function _sendMessage(msg, queueNamePostfix, msgCredentials, subdomain){
const uaa = msgCredentials.uaa
const jwtToken = await _fetchJwtToken(uaa, subdomain) // call subscriber account to fetch token
const messagingRestUrl = _composeMsgRestUrlForSendMsg(msgCredentials, queueNamePostfix)
const options = {
method: 'POST',
body: msg,
headers: {
Authorization: 'Bearer ' + jwtToken,
'Content-Type': 'application/json',
'x-qos' : 0 // or 1
}
}
const response = await fetch(messagingRestUrl, options)
return response.status
}