@sap/cds
Version:
SAP Cloud Application Programming Model - CDS for Node.js
182 lines (152 loc) • 6.11 kB
JavaScript
const cds = require('../../cds')
const LOG = cds.log('app')
const templatePathSerializer = require('../../common/utils/templateProcessorPathSerializer')
const typeCheckers = require('../../../common/assert/type-strict')
const { 'cds.Decimal': checkDecimal } = typeCheckers
const { checkMandatory, checkEnum, checkRange, checkFormat } = require('../../../common/assert/validation')
const ASSERT_RANGE = 'ASSERT_RANGE'
const ASSERT_FORMAT = 'ASSERT_FORMAT'
const ASSERT_DATA_TYPE = 'ASSERT_DATA_TYPE'
const ASSERT_ENUM = 'ASSERT_ENUM'
const ASSERT_NOT_NULL = 'ASSERT_NOT_NULL'
const _enumValues = element => {
return Object.keys(element).map(enumKey => {
const enum_ = element[enumKey]
const enumValue = enum_ && enum_.val
if (enumValue !== undefined) {
if (enumValue['=']) return enumValue['=']
if (enum_ && enum_.literal && enum_.literal === 'number') return Number(enumValue)
return enumValue
}
return enumKey
})
}
// REVISIT: this needs a cleanup!
const assertError = (code, element, value, key, path) => {
let args
if (typeof code === 'object') {
args = code.args
code = code.code
}
const { name, type, precision, scale } = element
const error = new Error()
const errorEntry = {
code,
message: code,
target: path ?? element.name ?? key,
args: args ?? [name ?? key]
}
const assertError = Object.assign(error, errorEntry)
Object.assign(assertError, {
entity: element.parent && element.parent.name,
element: name, // > REVISIT: when is error.element needed?
type: element.items ? element.items._type : type,
status: 400,
value
})
if (element.enum) assertError.enum = _enumValues(element)
if (precision) assertError.precision = precision
if (scale) assertError.scale = scale
if (element.target) {
// REVISIT: when does this case apply?
assertError.target = element.target
}
return assertError
}
/**
* @param {import('../../types/api').InputConstraints} constraints
*/
const checkInputConstraints = ({ element, value, errors, key, pathSegmentsInfo }) => {
if (!element) return errors
let path
if (pathSegmentsInfo?.length) path = templatePathSerializer(element.name || key, pathSegmentsInfo)
// not nice, but best option for keeping new cds.assert() clean
if (element._isMandatory) {
const mandatoryErrors = []
checkMandatory(value, element, mandatoryErrors, [], key)
if (mandatoryErrors.length) {
errors.push(...mandatoryErrors.map(() => assertError({ code: ASSERT_NOT_NULL }, element, value, key, path)))
}
}
if (value == null) return errors
// not nice, but best option for keeping new cds.assert() clean
const enumErrors = []
checkEnum(value, element, enumErrors, [], key)
if (enumErrors.length) {
errors.push(...enumErrors.map(e => assertError({ code: ASSERT_ENUM, args: e.args }, element, value, key, path)))
}
// not nice, but best option for keeping new cds.assert() clean
const rangeErrors = []
checkRange(value, element, rangeErrors, [], key)
if (rangeErrors.length) {
errors.push(...rangeErrors.map(e => assertError({ code: ASSERT_RANGE, args: e.args }, element, value, key, path)))
}
// not nice, but best option for keeping new cds.assert() clean
const formatErrors = []
checkFormat(value, element, formatErrors, [], key)
if (formatErrors.length) {
errors.push(...formatErrors.map(e => assertError({ code: ASSERT_FORMAT, args: e.args }, element, value, key, path)))
}
if (element.type === 'cds.Decimal') {
// not nice, but best option for keeping new cds.assert() clean
const decimalErrors = []
checkDecimal(value, element, decimalErrors, [], key)
if (decimalErrors.length) {
errors.push(
...decimalErrors.map(e => assertError({ code: ASSERT_DATA_TYPE, args: e.args }, element, value, key, path))
)
}
}
return errors
}
/**
* Check whether the target entity referenced by the association (the reference's target) exists and assert an error if
* the the reference's target doesn't exist.
*
* In other words, use this annotation to check whether a non-null foreign key input in a table has a corresponding
* primary key (also known as a parent key) in the associated/referenced target table (also known as a parent table).
*
* @param {import('../../types/api').assertTargetMap} assertMap
* @param {array} errors An array to appends the possible errors.
* @see {@link https://cap.cloud.sap/docs/guides/providing-services#assert-target @assert.target} for
* further information.
*/
const assertTargets = async (assertMap, errors) => {
const { targets: targetsMap, allTargets } = assertMap
if (targetsMap.size === 0) return
const targets = Array.from(targetsMap.values())
const transactions = targets.map(({ keys, entity }) => {
const where = Object.assign({}, ...keys)
return cds.db.exists(entity, where).forShareLock()
})
const targetsExistsResults = await Promise.allSettled(transactions)
targetsExistsResults.forEach((txPromise, index) => {
const isPromiseRejected = txPromise.status === 'rejected'
const shouldAssertError = (txPromise.status === 'fulfilled' && txPromise.value == null) || isPromiseRejected
if (!shouldAssertError) return
const target = targets[index]
const { element } = target.assocInfo
if (isPromiseRejected) {
LOG._debug &&
LOG.debug(
`The transaction to check the @assert.target constraint for foreign key "${element.name}" failed`,
txPromise.reason
)
throw new Error(txPromise.reason.message)
}
allTargets
.filter(t => t.key === target.key)
.forEach(target => {
const { row, pathSegmentsInfo } = target.assocInfo
const key = target.foreignKey.name
let path
if (pathSegmentsInfo?.length) path = templatePathSerializer(key, pathSegmentsInfo)
const error = assertError('ASSERT_TARGET', target.foreignKey, row[key], key, path)
errors.push(error)
})
})
}
module.exports = {
checkInputConstraints,
assertTargets
}