CDS
actions and functions are an easy way to respond with a response format other than OData. These are however limited to set of
supported types. Unfortunately, Object/JSON is not part of the supported types. If you have a scenario like me (
return specific JSON structure from a service to use in a dynamic SuccessFactors tile), where your service needs to return JSON in a specific structure and OData is not an option, then there is still a solution.
Luckily, under the hood, CAP uses express.js as its web application framework and this offers the possibility to serve anything supported by that framework. The authentication of the user seems daunting outside of CDS, but
vansyckel pointed out that it's a lot less complex than one might think.
After tweaking
Sebastian's example a little bit I ended up with this simple snippet:
const proxy = require('@sap/cds-odata-v2-adapter-proxy')
const cds = require('@sap/cds')
const xsenv = require('@sap/xsenv')
const passport = require('passport')
const JWTStrategy = require('@sap/xssec').JWTStrategy
cds.on('bootstrap', (app) => {
app.use(proxy())
app.get('/favicon.ico', (req, res) => res.status(204))
app.use(
/^\/(v2\/)?(?!ci).*/, //replace 'ci' with the name of your service path
passport.initialize(),
passport.authenticate(
new JWTStrategy(xsenv.getServices({ xsuaa: { tag: 'xsuaa' } }).xsuaa),
{ session: false }
),
(req, res, next) => {
if (!req.authInfo) return res.status(401).end()
//if (!req.authInfo.checkLocalScope('<role>')) return res.status(403).end()
next()
}
)
app.get('/tile', (req, res) => {
// do stuff using the req.user.id and return whatever you'd like
res.json({
tileType: 'sap.sf.homepage3.tiles.DynamicTile',
properties: {
title: 'Flashy Tile',
subtitle: 'Best Run Company',
numberValue: '8.67',
numberState: 'Positive',
numberDigits: 2,
stateArrow: 'Up',
numberFactor: '$',
icon: 'sap-icon://line-chart',
info: 'NASDAQ',
subinfo: 'BSTR'
}
})
})
})
module.exports = cds.server
Some things to note here:
- I'm using a negative lookahead regular expression to apply the authentication strategy on any request that does not match the path of my cds service (or the v2 proxy path)
- Not sure what path name your service has?
- The app.use() where the authentication is setup, is only required once. You can have any other number of endpoints such as app.get('/somepath'), app.post('/yikes') or that are also processed (that's what next() makes sure of).
- This sample shows how this can be combined with the cds odata v2 adapter proxy, this is optional
- I like to 204 the favicon.ico requests (because I don't need a favicon for my service during testing from a browser + it makes sure this request doesn't hit any breakpoints)
- You can use req.authInfo.checkLocalScope in any endpoint where you want to check for specific roles
- I left out the extension of the strategy (to avoid collisions with cds) since the regex of the path where the passport handler is used, is excluding cds requests (I hope vansyckel can confirm this is safe/unsafe and will update the post when he does)
This example shows that CAP can still cover these exceptional scenario's where for some functionality of your app, you really need to return a different data type (or support a different incoming request) besides the built in types supported by cds.