@sap/cds
Version:
SAP Cloud Application Programming Model - CDS for Node.js
130 lines (113 loc) • 4.83 kB
JavaScript
const cds = require('../../lib')
const LOG = cds.log('odata')
const express = require('express')
const star = express.application.del ? '*' : '{*splat}'
const HttpAdapter = require('../../lib/srv/protocols/http')
const $describe = require('./middleware/service-document')
const $metadata = require('./middleware/metadata')
const $batch = require('./middleware/batch')
const parse4 = require('./middleware/parse')
const operation4 = require('./middleware/operation')
const create4 = require('./middleware/create')
const stream4 = require('./middleware/stream')
const read4 = require('./middleware/read')
const update4 = require('./middleware/update')
const delete4 = require('./middleware/delete')
class ODataAdapter extends HttpAdapter {
/**
* Construct express Router for OData service
*/
get router() {
const router = super.router
router.use(this.log_request)
router.all('/\\$metadata', $metadata(this))
router.all('/', $describe(this))
router.use(parse4(this))
router.all('/\\$batch', $batch(this))
router.head(star, (_, res) => res.status(405).end())
router.all(star, operation4(this))
router.post(star, create4(this))
router.get(star, stream4(this), read4(this))
router.put(star, update4(this), create4(this, 'upsert'))
router.patch(star, update4(this), create4(this, 'upsert'))
router.delete(star, delete4(this))
return router
}
/**
* Middleware to log all incoming requests and set OData-Version header.
* @type {express.RequestHandler}
*/
log_request(req, res, next) {
if (req.__proto__.method in { PUT: 1, PATCH: 1 }) return next() // in case of upsert
let details = Object.keys(req.query).length ? { ...req.query } : ''
if (req._subrequest) LOG.info('> ' + req.method, req.path, details)
else LOG.info(req.method, decodeURI(req.baseUrl + req.path), details)
res.set('OData-Version', '4.0')
next()
}
error(err, req, res, next) {
let content_id = req.get('content-id')
if (content_id && typeof err === 'object') err.content_id = content_id
return super.error(err, req, res, next)
}
normalized(err) {
// Unwrap first of multiple errors for Fiori, and keep the rest as details
// Note: has to happen before super.normalized(), to ensure proper
// normalization of the main error's code.
if (err.details?.map && cds.env.fiori.wrap_multiple_errors == false) {
const [first, ...tail] = err.details
err = Object.assign(err, first, {
message: first.message, // ensure first message override, even if empty
details: tail.length ? tail : undefined
})
}
// Normalize error using super, which will ensure inferred `code` property
err = super.normalized(err) // this.normalized() modifies err in-place
// Ensure all errors have a string code, as expected by OData specification
for (let e of err.details?.map ? [err, ...err.details] : [err])
if (typeof e.code !== 'string') e.code = String(e.code || e.status || '')
if (err.reason) err.innererror = err.reason
return err
}
cleansed(err, content_id = err.content_id) {
// Extract known OData error properties from the original error object
const {
message,
code,
target,
innererror,
details,
additionalTargets,
numericSeverity = 4,
...other // all other unknown ones
} = err
// Construct and return a new spec-compliant error object
const error = { message, code, target, innererror }
if (details) error.details = details.map?.(e => this.cleansed(e, content_id)) ?? details
if (additionalTargets) error['@Common.additionalTargets'] = additionalTargets
if (numericSeverity) error['@Common.numericSeverity'] = numericSeverity
if (content_id) error['@Core.ContentID'] = content_id
for (let k in other) if (k[0] === '@') error[k] = other[k]
return error
}
request4(args) {
return new ODataRequest(args)
}
sap_messages4(cdsReq, req, res) {
if (res.headersSent || !cdsReq.messages?.length) return
const messages = cdsReq.messages.map(err => {
if (!err.code) err.code = '' // default to empty string if not provided
const { code, message, numericSeverity, longtextUrl, target, additionalTargets } = this.normalized(err)
// only return allowed subset of properties
return { code, message, numericSeverity, longtextUrl, target, additionalTargets }
})
const msgs = JSON.stringify(messages).replace(
/[\u007F-\uFFFF]/g,
c => '\\u' + c.charCodeAt(0).toString(16).padStart(4, '0')
)
res.setHeader('sap-messages', msgs)
}
}
class ODataRequest extends cds.Request {}
ODataRequest.prototype.protocol = 'odata' // for batch subrequests where prototype is not properly set
module.exports = ODataAdapter