MessageBox.success('You will be navigated back to the Fiori Launchpad', {
onClose: () => {
const oCrossAppNavigator = sap.ushell.Container.getService("CrossApplicationNavigation")
oCrossAppNavigator.toExternal({
target: {
semanticObject: "#"
}
})
}
})
Category: SAP
SAP
[CAP] Multitenant Job Scheduler – Request timeout after 15 seconds
For Jobs running longer than 15 seconds, you have to manually inform the Job Scheduler if your operation succeeded or not. Else, your job will only stay in status COMPLETED/UNKNOWN due to the timeout.
Informing the Job Scheduler about your succeeded operation can be done vie REST API Endpoint Update Job Run Log. You can read more about Long-Running (Async) Jobs here. I therefore wrote a function named updateJobStatus, which I call at the end of every long-running endpoint. It checks if the endpoint is called manually or via Job Scheduler service and updates the Job Run Log using the @sap/jobs-client if required.
const cds = require('@sap/cds')
const LOG = cds.log('JobService')
const xsenv = require("@sap/xsenv")
const JobSchedulerClient = require("@sap/jobs-client")
async function fetchAccessToken(url, creds) {
const response = await fetch(`${url}/oauth/token`, {
method: 'POST',
body: 'grant_type=client_credentials&client_id=' + creds.uaa.clientid + '&client_secret=' + creds.uaa.clientsecret,
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
})
return await response.json()
}
async function getJobscheduler(req) {
xsenv.loadEnv()
const services = xsenv.getServices({
jobscheduler: { tags: "jobscheduler" }
})
if (!services.jobscheduler) req.reject("no jobscheduler service instance found")
const subdomain = (process.env.NODE_ENV === 'production') ? req.http.req.authInfo.getSubdomain() : 'customer1' // workaround for local testing
const domain = `https://${subdomain}.${services.jobscheduler.uaa.uaadomain}`
const token = await fetchAccessToken(domain, services.jobscheduler)
const options = {
baseURL: services.jobscheduler.url,
token: token.access_token
}
return new JobSchedulerClient.Scheduler(options)
}
async function updateJobStatus(req) {
const jobId = req.headers['x-sap-job-id']
const scheduleId = req.headers['x-sap-job-schedule-id']
const runId = req.headers['x-sap-job-run-id']
if (!jobId || !scheduleId || !runId) return
LOG.info('Endpoint is called via Job Scheduler')
const scheduler = await getJobscheduler(req)
const payload = {
jobId: jobId,
scheduleId: scheduleId,
runId: runId,
data: { success: true, message: 'The endpoint has successfully executed the long-running job' }
}
scheduler.updateJobRunLog(payload, function (err, result) {
if (err) return LOG.error('Error updating run log: %s', err)
//Run log updated successfully
LOG.info('Run log updated successfully')
})
}
module.exports = {
updateJobStatus
}
[CAP] Multitenant Job Scheduler – Fixing Scope issue
When I was integrating the Job Scheduler service into my Multitenant Application, I ran into the following JWT Token issue, when the Job Scheduler was calling my CAP action. Means the job creation was already working fine and was also displaying the right tenant for my job, but the Job Scheduler was not able to successfully call the given endpoint. This is the error I got in the logs:
Error: Jwt token with audience: [
'sb-a1e9d3b8-2bee-47db-xxxx-07e5a54aec1e!b180208|sap-jobscheduler!b3',
'uaa'
] is not issued for these clientIds: [
'sb-MyApp-mtdev-App!t180208',
'MyAp-mtdev-App!t180208'
].
After reading some of the great blogs from Carlos Roggan, I noticed that I forgot to grant the Job Scheduler the necessary authority to actual call my CAP action. So I added the following lines to the xs-security.json file
{
"name": "$XSAPPNAME.jobscheduler",
"description": "Scope for Job Scheduler",
"grant-as-authority-to-apps": [
"$XSSERVICENAME(job-scheduler)"
]
}
and also annotated my CAP action using the new scope @(requires: ['jobscheduler'])
.
I redeployed everything, but the issue still persists. 🙁
Turned out, for the standard plan, tokens are cached in Job Scheduler up to 12 hours.
https://help.sap.com/docs/job-scheduling/sap-job-scheduling-service/secure-access?locale=en-US
After waiting 12 hours, the endpoint was successfully called by the Job Scheduler. 🙂
[CAP] Timeout on long-running endpoint
In my application, I have a function that can take quite a long time to process, depending on the data selected. Two external systems were involved in the processing, so a lot of round trips were made. Of course, I tried to parallelize the calls to the external systems as much as possible, but it still took a long time. During the development in BAS everything worked fine, but during the deployment in BTP I encountered some errors, depending on the amount of data selected.
In the console I could see, that it was a 504 Gateway Timeout.
Luckily, the CAP docs are already explaining the possible reason for this. The approuter has a default timeout of 30 seconds for destinations. This matched my observation, that this issue only occurred when deployed.
https://www.npmjs.com/package/@sap/approuter#destinations
In my case, the destination for my backend service is configured in the mta.yaml directly on the approuter. By simply adding the timeout property and by increasing the timeout from 30 seconds to 2 minutes, I could get rid of the errors.
- name: my-approuter
type: approuter.nodejs
path: app/approute
build-parameters:
builder: npm-ci
ignore:
- "node_modules/"
- "default-env.json"
- "manifest*.yml"
requires:
- name: srv-api
group: destinations
properties:
name: srv-api
url: ~{srv-url}
forwardAuthToken: true
timeout: 120000 # <--------------------------------- add timeout to your cap service destination
- name: my-xsuaa
- name: my-destination
- name: my-html5-repo-runtime
[CAP] Get subdomain in a multitenant scenario
Until recently, I was always decoded the JWT to get the subdomain of a subscribed tenant like this:
const jwt = retrieveJwt(req)
const subdomain = decodeJwt(jwt).ext_attr.zdn
I now noticed that you can also use the original request object and the getSubdomain
function of the authInfo
object. It’s provided by @sap/xssec and is only available when using XSUAA, means not with mocked authentication. This way you can get the subdomain in a single line:
const subdomain = req.http.req.authInfo.getSubdomain()
And there are some more helpful functions, that are documented here:
[CAP] Get service and approuter URLs of your running CAP application in a service handler
In your mta.yaml
you can define environment variables, which are filled during deployment. They can be filled with MTA Development and Deployment Parameters. Click here for an overview.
To get the URL of your deployed CAP service, simply use the ${default-url} parameter and pass the value to a variable below the properties attribute, e.g. SRV_URL
. The approuter has to provide its default-url, which can then be used in the service for a variable, e.g. APPROUTER_URL
.
#####################################################################################################################
# Approuter
#####################################################################################################################
- name: approuter
type: approuter.nodejs
path: app/approuter
provides:
- name: app-api
properties:
app-url: ${default-url} # <---------------------------- provides approuter url
app-uri: ${default-uri} # <---------------------------- provides approuter uri/hostname
app-protocol: ${protocol} # <---------------------------- provides approuter protocol
#####################################################################################################################
# Business Service Module
#####################################################################################################################
- name: mySrv
type: nodejs
path: gen/srv
requires:
- name: app-api # <---------------------------------------- required to access the provided variables of the approuter
properties:
SRV_URL: ${default-url}
APPROUTER_URL: ~{app-api/app-url}
SUBSCRIPTION_URL: ~{app-api/app-protocol}://\${tenant_subdomain}-~{app-api/app-uri}
After deployment, you can now access the URL via process.env.SRV_URL
in a service handler. During development, simply use the .env file to provide the SRV_URL
value.
You can check all the variables via the BTP Cockpit: Subaccount → Space → Select application → User-Provided Variables
[SAPUI5] Add your own Logout functionality to the Launchpad Sandbox
Use the attachLogoutEvent of the ushell container to trigger your approuter logout endpoint, that needs to be configured in your xs-app.json
. The code in my launchpad.html
looks like this:
<script>
sap.ui.getCore().attachInit(() => {
sap.ushell.Container.createRenderer('fiori2', true).then(renderer => renderer.placeAt("content"))
sap.ushell.Container.attachLogoutEvent(e => {
e.preventDefault()
window.location.replace('/do/logout')
}, false)
})
</script>
This way, you can reuse the default logout dialog logic the launchpad provides.
For completeness, find also my approuter configuration and custom logout page below.
My xs-app.json
:
{
"welcomeFile": "index.html",
"authenticationMethod": "route",
"logout": {
"logoutEndpoint": "/do/logout",
"logoutPage": "/logged-out.html"
},
"sessionTimeout": 60,
"routes": [
{
"source": "^/logged-out.html$",
"localDir": ".",
"authenticationType": "none"
},
{
"source": "^/launchpad.html$",
"localDir": ".",
"authenticationType": "xsuaa",
"cacheControl": "no-cache, no-store, must-revalidate"
},
{
"source": "^/appconfig/(.*)$",
"localDir": ".",
"authenticationType": "xsuaa"
},
{
"source": "^/user-api(.*)",
"target": "$1",
"service": "sap-approuter-userapi",
"authenticationType": "xsuaa"
},
...
]
}
My logged-out.html
file, placed in the approuter folder next to the launchpad.html:
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Logged out</title>
<style>
h2 {
font-family: "Arial", sans-serif;
}
.centered {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
</style>
</head>
<body>
<div class="centered">
<h2>You are now logged out</h2>
</div>
</body>
</html>
[SAPUI5] Enable Variant Management in Launchpad Sandbox
https://sapui5.hana.ondemand.com/sdk/#/topic/642dab291a7b47ec9d46c39b3c482aba
data-sap-ui-flexibilityServices='[{"connector": "LocalStorageConnector"}]'
[CAP] Select exists
So far I have not found a better/shorter way to check if a table entry exists than the following:
let exists = await SELECT(1).from(Books,201)
return (exists .length > 0) ? true : false
Found here: https://cap.cloud.sap/docs/node.js/cds-tx#cds-tx-ctx-fn
[SAPUI5] Add custom header parameter to all oData queries
Simply go to your Component.js file and add this line to the init
function:
this.getModel().setHeaders({"myCustomParameter": "test"})
In a CAP Backend, you get this parameter from the express req object, which can be accessed via the req.http property:
req.http.req.headers['myCustomParameter']
And here is a nice code snippet, on how to read this header parameter in an ABAP system: https://answers.sap.com/answers/425621/view.html