@sap/cds
Version:
SAP Cloud Application Programming Model - CDS for Node.js
162 lines (129 loc) • 7.02 kB
JavaScript
const cds = require('../../../')
const { UPDATE } = cds.ql
const { handleSapMessages, getPreferReturnHeader, isStream } = require('../utils')
const getODataMetadata = require('../utils/metadata')
const postProcess = require('../utils/postProcess')
const readAfterWrite4 = require('../utils/readAfterWrite')
const getODataResult = require('../utils/result')
const normalizeTimeData = require('../utils/normalizeTimeData')
const odataBind = require('../utils/odataBind')
const { getKeysAndParamsFromPath } = require('../../common/utils/path')
const prepare_put_requests = require('../../http/put')
// Not supported:
// 1) If some parent entity needs to be updated, reason: we only generate one CQN statement for the target.
// 2) If the foreign key is not known, i.e. when having no key information of the immediate parent, e.g. /Root(1)/foo/bar
const upsertSupported = (pathExpression, model) => {
const pathExpressionRef = pathExpression?.ref
// not a navigation
if (pathExpressionRef.length < 2) return true
// foreign key is not known
if (!pathExpressionRef[pathExpressionRef.length - 2].where) return false
let currentEntity = model.definitions[pathExpressionRef[0].id]
let navElement
for (let i = 1; i < pathExpressionRef.length; i++) {
const id = typeof pathExpressionRef[i] === 'string' ? pathExpressionRef[i] : pathExpressionRef[i].id
navElement = currentEntity.elements[id]
currentEntity = navElement._target
}
// disallow processing of requests along associations to one and containments
if (!navElement.is2one || navElement._isContained) return true
return navElement._foreignKeys.every(foreignKey => foreignKey.parentElement.key)
}
module.exports = adapter => {
const { service } = adapter
const _readAfterWrite = readAfterWrite4(adapter, 'update')
return function update(req, res, next) {
// REVISIT: better solution for _propertyAccess
const {
SELECT: { one, from },
_propertyAccess
} = req._query
// REVISIT: patch on collection is allowed in odata 4.01
if (!one) {
throw Object.assign(new Error(`Method ${req.method} is not allowed for entity collections`), { statusCode: 405 })
}
const _isStream = isStream(req._query)
if (_propertyAccess && req.method === 'PATCH' && !_isStream) {
throw Object.assign(new Error(`Method ${req.method} is not allowed for properties`), { statusCode: 405 })
}
const model = cds.context.model ?? service.model
// payload & params
const target = cds.infer.target(req._query) // FIXME: this should not happen here but only in an event handler !
const data = _propertyAccess ? { [_propertyAccess]: req.body.value } : req.body
odataBind(data, target)
normalizeTimeData(data, model, target)
const { keys, params } = getKeysAndParamsFromPath(from, { model })
if (!_propertyAccess) {
// add keys from url into payload (overwriting if already present)
Object.assign(data, keys)
// add default values for unprovided properties
if (req.method === 'PUT') prepare_put_requests(service, target, data)
}
// query
const query = UPDATE.entity(from).with(data)
// cdsReq.headers should contain merged headers of envelope and subreq
// REVISIT: this overrides the merging mechanism in cds.Request which is meant to handle this centrally !!
const headers = { ...cds.context.http.req.headers, ...req.headers }
// we need a cds.Request for multiple reasons, incl. params, headers, sap-messages, read after write, ...
const cdsReq = adapter.request4({ query, params, headers, req, res })
// NOTES:
// - only via srv.run in combination with srv.dispatch inside,
// we automatically either use a single auto-managed tx for the req (i.e., insert and read after write in same tx)
// or the auto-managed tx opened for the respective atomicity group, if exists
// - in the then block of .run(), the transaction is committed (i.e., before sending the response) if a single auto-managed tx is used
return service
.run(() => {
return service.dispatch(cdsReq).then(result => {
// cdsReq._.readAfterWrite is only true if generic handler served the request
// If minimal requested or property access and not etag, skip read after write
if (
cdsReq._.readAfterWrite &&
(target._etag || (!_propertyAccess && getPreferReturnHeader(req) !== 'minimal'))
)
return _readAfterWrite(cdsReq)
return result
})
})
.then(result => {
if (res.headersSent) return
handleSapMessages(cdsReq, req, res)
// case: read after write returns no results, e.g., due to auth (academic but possible)
if (result == null) return res.sendStatus(204)
const preference = getPreferReturnHeader(req)
postProcess(cdsReq.target, model, result, preference === 'minimal')
if (result?.$etag) res.set('ETag', result.$etag) //> must be done after post processing
if (preference === 'minimal') res.append('Preference-Applied', 'return=minimal')
else if (preference === 'representation') res.append('Preference-Applied', 'return=representation')
if (preference === 'minimal' || (_propertyAccess && result[_propertyAccess] == null) || isStream(req._query)) {
return res.sendStatus(204)
}
const metadata = getODataMetadata(_propertyAccess ? req._query : query, { result })
result = getODataResult(result, metadata, { property: _propertyAccess })
res.send(result)
})
.catch(err => {
handleSapMessages(cdsReq, req, res)
// if UPSERT is allowed, redirect to POST
const is404 = err.code === 404 || err.status === 404 || err.statusCode === 404
const isForcedInsert =
(err.code === 412 || err.status === 412 || err.statusCode === 412) && req.get('if-none-match') === '*'
const isUpsertAllowed =
(cds.env.runtime.put_as_upsert && req.method === 'PUT') || cds.env.runtime.patch_as_upsert
if (!_propertyAccess && (is404 || isForcedInsert) && isUpsertAllowed) {
// PUT / PATCH with if-match header means "only if already exists" -> no insert if it does not
if (req.headers['if-match']) return next(Object.assign(new Error('412'), { statusCode: 412 }))
if (!upsertSupported(from, model)) return next(Object.assign(new Error('422'), { statusCode: 422 }))
// -> forward to POST
req.method = 'POST'
req.body = JSON.parse(req._raw) // REVISIT: Why do we need that?
return next()
}
// REVISIT: invoke service.on('error') for failed batch subrequests
if (cdsReq.http.req.path.startsWith('/$batch') && service.handlers._error.length) {
for (const each of service.handlers._error) each.handler.call(service, err, cdsReq)
}
// continue with caught error
next(err)
})
}
}