UNPKG

@sap/cds

Version:

SAP Cloud Application Programming Model - CDS for Node.js

130 lines (113 loc) 4.83 kB
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