@sap/cds 7.6.3 → 7.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +38 -1
- package/_i18n/i18n.properties +3 -0
- package/app/index.js +18 -12
- package/bin/serve.js +51 -19
- package/common.cds +16 -0
- package/lib/auth/ias-auth.js +2 -2
- package/lib/auth/index.js +1 -1
- package/lib/auth/jwt-auth.js +1 -1
- package/lib/compile/cdsc.js +23 -11
- package/lib/compile/for/nodejs.js +2 -2
- package/lib/compile/for/odata.js +4 -0
- package/lib/compile/load.js +7 -2
- package/lib/compile/to/sql.js +3 -0
- package/lib/dbs/cds-deploy.js +197 -220
- package/lib/env/defaults.js +2 -1
- package/lib/index.js +8 -2
- package/lib/linked/types.js +1 -0
- package/lib/log/format/json.js +1 -1
- package/lib/plugins.js +2 -2
- package/lib/ql/Query.js +1 -1
- package/lib/ql/SELECT.js +8 -8
- package/lib/req/context.js +22 -13
- package/lib/req/request.js +10 -4
- package/lib/srv/cds-connect.js +9 -3
- package/lib/srv/cds-serve.js +5 -3
- package/lib/srv/middlewares/ctx-model.js +1 -1
- package/lib/srv/protocols/odata-v4.js +38 -9
- package/lib/srv/srv-api.js +98 -140
- package/lib/srv/srv-models.js +2 -2
- package/lib/srv/srv-tx.js +1 -0
- package/lib/utils/cds-utils.js +32 -23
- package/lib/utils/data.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +1 -3
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +0 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +18 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/index.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/utils.js +7 -3
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/UriParser.js +2 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/http/HttpHeaderReader.js +4 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/index.js +5 -0
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/data.js +71 -25
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/metaInfo.js +10 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +6 -1
- package/libx/_runtime/cds-services/util/assert.js +50 -240
- package/libx/_runtime/cds.js +5 -0
- package/libx/_runtime/common/aspects/any.js +53 -45
- package/libx/_runtime/common/generic/input.js +14 -10
- package/libx/_runtime/common/generic/paging.js +1 -1
- package/libx/_runtime/common/utils/cqn2cqn4sql.js +1 -1
- package/libx/_runtime/common/utils/keys.js +1 -1
- package/libx/_runtime/common/utils/quotingStyles.js +1 -1
- package/libx/_runtime/common/utils/resolveStructured.js +4 -1
- package/libx/_runtime/common/utils/rewriteAsterisks.js +5 -12
- package/libx/_runtime/common/utils/stream.js +2 -16
- package/libx/_runtime/common/utils/streamProp.js +16 -6
- package/libx/_runtime/common/utils/ucsn.js +1 -0
- package/libx/_runtime/db/utils/columns.js +6 -1
- package/libx/_runtime/fiori/generic/activate.js +11 -3
- package/libx/_runtime/fiori/generic/edit.js +8 -2
- package/libx/_runtime/fiori/lean-draft.js +99 -30
- package/libx/_runtime/hana/execute.js +2 -5
- package/libx/_runtime/messaging/service.js +6 -2
- package/libx/common/assert/index.js +232 -0
- package/libx/common/assert/type.js +109 -0
- package/libx/common/assert/utils.js +125 -0
- package/libx/common/assert/validation.js +109 -0
- package/libx/odata/index.js +5 -5
- package/libx/odata/middleware/create.js +83 -0
- package/libx/odata/middleware/delete.js +38 -0
- package/libx/odata/middleware/error.js +8 -0
- package/libx/odata/{metadata.js → middleware/metadata.js} +8 -6
- package/libx/odata/middleware/operation.js +78 -0
- package/libx/odata/middleware/parse.js +11 -0
- package/libx/odata/{read.js → middleware/read.js} +42 -20
- package/libx/odata/{service-document.js → middleware/service-document.js} +2 -1
- package/libx/odata/middleware/stream.js +237 -0
- package/libx/odata/middleware/update.js +165 -0
- package/libx/odata/{afterburner.js → parse/afterburner.js} +79 -29
- package/libx/odata/{cqn2odata.js → parse/cqn2odata.js} +5 -3
- package/libx/odata/{parseToCqn.js → parse/parseToCqn.js} +3 -6
- package/libx/odata/{utils.js → utils/index.js} +91 -9
- package/libx/outbox/index.js +5 -4
- package/libx/rest/RestAdapter.js +0 -1
- package/libx/rest/middleware/operation.js +6 -4
- package/libx/rest/middleware/parse.js +20 -2
- package/package.json +1 -1
- package/server.js +43 -71
- package/libx/odata/create.js +0 -44
- package/libx/odata/delete.js +0 -25
- package/libx/odata/error.js +0 -12
- package/libx/odata/update.js +0 -110
- /package/libx/odata/{grammar.peggy → parse/grammar.peggy} +0 -0
- /package/libx/odata/{parser.js → parse/parser.js} +0 -0
- /package/libx/odata/{result.js → utils/result.js} +0 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
const { cds } = global
|
|
2
|
+
|
|
3
|
+
const { Readable } = require('stream')
|
|
4
|
+
|
|
5
|
+
const { getNormalizedDecimal, getTarget, isBase64String } = require('./utils')
|
|
6
|
+
|
|
7
|
+
const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i //> "i" is acutally not OK, but we'll leave as is for now to avoid breaking changes
|
|
8
|
+
const RELAXED_UUID_REGEX = /^[0-9a-z]{8}-?[0-9a-z]{4}-?[0-9a-z]{4}-?[0-9a-z]{4}-?[0-9a-z]{12}$/i
|
|
9
|
+
|
|
10
|
+
const ISO_DATE_PART1 =
|
|
11
|
+
'[1-9]\\d{3}-(?:(?:0[1-9]|1[0-2])-(?:0[1-9]|1\\d|2[0-8])|(?:0[13-9]|1[0-2])-(?:29|30)|(?:0[13578]|1[02])-31)'
|
|
12
|
+
const ISO_DATE_PART2 = '(?:[1-9]\\d(?:0[48]|[2468][048]|[13579][26])|(?:[2468][048]|[13579][26])00)-02-29'
|
|
13
|
+
const ISO_DATE = `(?:${ISO_DATE_PART1}|${ISO_DATE_PART2})`
|
|
14
|
+
const ISO_TIME_NO_MILLIS = '(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d'
|
|
15
|
+
const ISO_TIME = `${ISO_TIME_NO_MILLIS}(?:\\.\\d{1,9})?`
|
|
16
|
+
const ISO_DATE_TIME = `${ISO_DATE}T${ISO_TIME_NO_MILLIS}(?:Z|[+-][01]\\d:?[0-5]\\d)`
|
|
17
|
+
const ISO_TIMESTAMP = `${ISO_DATE}T${ISO_TIME}(?:Z|[+-][01]\\d:?[0-5]\\d)`
|
|
18
|
+
const ISO_DATE_REGEX = new RegExp(`^${ISO_DATE}$`, 'i')
|
|
19
|
+
const ISO_TIME_NO_MILLIS_REGEX = new RegExp(`^${ISO_TIME_NO_MILLIS}$`, 'i')
|
|
20
|
+
const ISO_DATE_TIME_REGEX = new RegExp(`^${ISO_DATE_TIME}$`, 'i')
|
|
21
|
+
const ISO_TIMESTAMP_REGEX = new RegExp(`^${ISO_TIMESTAMP}$`, 'i')
|
|
22
|
+
|
|
23
|
+
const _checkString = value => typeof value === 'string'
|
|
24
|
+
|
|
25
|
+
const _checkNumber = value => typeof value === 'number' && !Number.isNaN(value)
|
|
26
|
+
|
|
27
|
+
const _oldCheckDecimal = (value, element) => {
|
|
28
|
+
const [left, right] = String(value).split('.')
|
|
29
|
+
return (
|
|
30
|
+
_checkNumber(value) &&
|
|
31
|
+
(!element.precision || left.length <= element.precision - (element.scale || 0)) &&
|
|
32
|
+
(!element.scale || ((right || '').length <= element.scale && parseFloat(right) !== 0))
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// REVISIT: only use a cheaper check if not in strictDecimal mode?
|
|
37
|
+
const _checkDecimal = (v, ele, errs, path, k) => {
|
|
38
|
+
if (!errs) return _oldCheckDecimal(v, ele)
|
|
39
|
+
|
|
40
|
+
const { precision, scale } = ele
|
|
41
|
+
let val = getNormalizedDecimal(v)
|
|
42
|
+
if (precision != null && scale != null) {
|
|
43
|
+
let isValid = true
|
|
44
|
+
if (!val.match(/\./)) val += '.0'
|
|
45
|
+
if (precision === scale) {
|
|
46
|
+
if (!val.match(new RegExp(`^-?0\\.\\d{0,${scale}}$`, 'g'))) isValid = false
|
|
47
|
+
} else if (scale === 0) {
|
|
48
|
+
if (!val.match(new RegExp(`^-?\\d{1,${precision - scale}}\\.0{0,1}$`, 'g'))) isValid = false
|
|
49
|
+
} else if (!val.match(new RegExp(`^-?\\d{1,${precision - scale}}\\.\\d{0,${scale}}$`, 'g'))) {
|
|
50
|
+
isValid = false
|
|
51
|
+
}
|
|
52
|
+
if (!isValid) {
|
|
53
|
+
const args = [v, `Decimal(${precision},${scale})`]
|
|
54
|
+
const target = getTarget(path, k)
|
|
55
|
+
errs.push(new cds.error('ASSERT_DATA_TYPE', { args, target, statusCode: 400, code: '400' }))
|
|
56
|
+
}
|
|
57
|
+
} else if (precision != null) {
|
|
58
|
+
if (!val.match(new RegExp(`^-?\\d{1,${precision}}$`, 'g'))) {
|
|
59
|
+
const args = [v, `Decimal(${precision})`]
|
|
60
|
+
const target = getTarget(path, k)
|
|
61
|
+
errs.push(new cds.error('ASSERT_DATA_TYPE', { args, target, statusCode: 400, code: '400' }))
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const _checkInt = value => _checkNumber(value) && parseInt(value, 10) === value
|
|
67
|
+
|
|
68
|
+
const _checkInt64 = value => typeof value === 'string' ? value.match(/^\d+$/) : _checkInt(value)
|
|
69
|
+
|
|
70
|
+
const _checkBoolean = value => typeof value === 'boolean'
|
|
71
|
+
|
|
72
|
+
const _checkBuffer = value => Buffer.isBuffer(value) || value.type === 'Buffer' || isBase64String(value)
|
|
73
|
+
|
|
74
|
+
const _checkStreamOrBuffer = value => value instanceof Readable || _checkBuffer(value)
|
|
75
|
+
|
|
76
|
+
const _checkUUID = value => _checkString(value) && UUID_REGEX.test(value)
|
|
77
|
+
|
|
78
|
+
const _checkRelaxedUUID = value => _checkString(value) && RELAXED_UUID_REGEX.test(value)
|
|
79
|
+
|
|
80
|
+
const _checkISODate = value => (_checkString(value) && ISO_DATE_REGEX.test(value)) || value instanceof Date
|
|
81
|
+
|
|
82
|
+
const _checkISOTime = value => _checkString(value) && ISO_TIME_NO_MILLIS_REGEX.test(value)
|
|
83
|
+
|
|
84
|
+
const _checkISODateTime = value => (_checkString(value) && ISO_DATE_TIME_REGEX.test(value)) || value instanceof Date
|
|
85
|
+
|
|
86
|
+
const _checkISOTimestamp = value => (_checkString(value) && ISO_TIMESTAMP_REGEX.test(value)) || value instanceof Date
|
|
87
|
+
|
|
88
|
+
module.exports = {
|
|
89
|
+
'cds.UUID': _checkUUID,
|
|
90
|
+
'relaxed.UUID': _checkRelaxedUUID,
|
|
91
|
+
'cds.Boolean': _checkBoolean,
|
|
92
|
+
'cds.Integer': _checkInt,
|
|
93
|
+
'cds.UInt8': _checkInt,
|
|
94
|
+
'cds.Int16': _checkInt,
|
|
95
|
+
'cds.Int32': _checkInt,
|
|
96
|
+
'cds.Integer64': _checkInt64,
|
|
97
|
+
'cds.Int64': _checkInt64,
|
|
98
|
+
'cds.Decimal': _checkDecimal,
|
|
99
|
+
'cds.DecimalFloat': _checkNumber,
|
|
100
|
+
'cds.Double': _checkNumber,
|
|
101
|
+
'cds.Date': _checkISODate,
|
|
102
|
+
'cds.Time': _checkISOTime,
|
|
103
|
+
'cds.DateTime': _checkISODateTime,
|
|
104
|
+
'cds.Timestamp': _checkISOTimestamp,
|
|
105
|
+
'cds.String': _checkString,
|
|
106
|
+
'cds.Binary': _checkBuffer,
|
|
107
|
+
'cds.LargeString': _checkString,
|
|
108
|
+
'cds.LargeBinary': _checkStreamOrBuffer
|
|
109
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
function getNested(k, obj) {
|
|
2
|
+
let cur = obj
|
|
3
|
+
let p = ''
|
|
4
|
+
const parts = k.split('_')
|
|
5
|
+
while (parts.length) {
|
|
6
|
+
const q = parts.shift()
|
|
7
|
+
if (q in cur) {
|
|
8
|
+
cur = cur[q]
|
|
9
|
+
p = ''
|
|
10
|
+
} else {
|
|
11
|
+
p = p ? p + '_' + q : q
|
|
12
|
+
if (p in cur) {
|
|
13
|
+
cur = cur[p]
|
|
14
|
+
p = ''
|
|
15
|
+
} else {
|
|
16
|
+
if (Object.keys(cur).some(k => k.startsWith(p + '_'))) {
|
|
17
|
+
// continue for now as there's still a chance
|
|
18
|
+
} else {
|
|
19
|
+
// abort
|
|
20
|
+
return undefined
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return cur[p] || cur !== obj ? cur : undefined
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const getNormalizedDecimal = val => {
|
|
29
|
+
let v = `${val}`
|
|
30
|
+
const cgs = v.match(/^(\d*\.*\d*)e([+|-]*)(\d*)$/)
|
|
31
|
+
if (cgs) {
|
|
32
|
+
let [l, r = ''] = cgs[1].split('.')
|
|
33
|
+
const dir = cgs[2] || '+'
|
|
34
|
+
const exp = Number(cgs[3])
|
|
35
|
+
if (dir === '+') {
|
|
36
|
+
// move decimal point to the right
|
|
37
|
+
r = r.padEnd(exp, '0')
|
|
38
|
+
l += r.substring(0, exp)
|
|
39
|
+
r = r.slice(exp)
|
|
40
|
+
v = `${l}${r ? '.' + r : ''}`
|
|
41
|
+
} else {
|
|
42
|
+
// move decimal point to the left
|
|
43
|
+
l = l.padStart(exp, '0')
|
|
44
|
+
r = l.substring(0, exp) + r
|
|
45
|
+
l = l.slice(exp)
|
|
46
|
+
v = `${l ? l : '0'}.${r}`
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return v
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function getTarget(path, k) {
|
|
53
|
+
return path.length && path[path.length - 1].match(/\[\d+\]$/) ? path.join('/') : path.concat(k).join('/')
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// non-strict mode also allows url-safe base64 strings
|
|
57
|
+
function isBase64String(string, strict = false) {
|
|
58
|
+
if (typeof string !== 'string') return false
|
|
59
|
+
|
|
60
|
+
if (strict && string.length % 4 !== 0) return false
|
|
61
|
+
|
|
62
|
+
let length = string.length
|
|
63
|
+
if (string.endsWith('==')) length -= 2
|
|
64
|
+
else if (string.endsWith('=')) length -= 1
|
|
65
|
+
|
|
66
|
+
let char
|
|
67
|
+
for (let i = 0; i < length; i++) {
|
|
68
|
+
char = string[i]
|
|
69
|
+
if (char >= 'A' && char <= 'Z') continue
|
|
70
|
+
else if (char >= 'a' && char <= 'z') continue
|
|
71
|
+
else if (char >= '0' && char <= '9') continue
|
|
72
|
+
else if (char === '+' || char === '/') continue
|
|
73
|
+
else if (!strict && (char === '-' || char === '_')) continue
|
|
74
|
+
return false
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return true
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const resolveCDSType = ele => {
|
|
81
|
+
// REVISIT: when is ele._type not set and sufficient?
|
|
82
|
+
if (ele._type?.match(/^cds\./)) return ele._type
|
|
83
|
+
if (ele.type) {
|
|
84
|
+
if (ele.type.match(/^cds\./)) return ele.type
|
|
85
|
+
return resolveCDSType(ele.__proto__)
|
|
86
|
+
}
|
|
87
|
+
if (ele.items) return resolveCDSType(ele.items)
|
|
88
|
+
return ele
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function resolveSegment(prev, obj, def) {
|
|
92
|
+
if (prev.keys) {
|
|
93
|
+
let keys = []
|
|
94
|
+
for (const k of prev.keys) {
|
|
95
|
+
let val
|
|
96
|
+
if (k in obj) val = obj[k]
|
|
97
|
+
else val = getNested(k, obj)
|
|
98
|
+
if (val == null) {
|
|
99
|
+
// in some cases, k is not given, e.g., POST into collection via navigation
|
|
100
|
+
// TODO: what to put in target? "null", "transient", ...?
|
|
101
|
+
if (k === 'IsActiveEntity')
|
|
102
|
+
keys.push(`${k}=false`) //> always false if not in obj as it must be a draft activate
|
|
103
|
+
else keys.push(`${k}=null`)
|
|
104
|
+
} else {
|
|
105
|
+
const type = resolveCDSType(def.elements[k])
|
|
106
|
+
if (type === 'cds.String') val = `'${val}'`
|
|
107
|
+
// TODO: more proper val encoding based on type
|
|
108
|
+
keys.push(`${k}=${val}`)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return `${prev.assoc}(${keys.join(',')})`
|
|
112
|
+
}
|
|
113
|
+
if (prev.index) {
|
|
114
|
+
return `${prev.prop}[${prev.index}]`
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
module.exports = {
|
|
119
|
+
getNested,
|
|
120
|
+
getNormalizedDecimal,
|
|
121
|
+
getTarget,
|
|
122
|
+
isBase64String,
|
|
123
|
+
resolveCDSType,
|
|
124
|
+
resolveSegment
|
|
125
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
const { cds } = global
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
'cds.Date': checkISODate,
|
|
5
|
+
'cds.Time': checkISOTime,
|
|
6
|
+
'cds.DateTime': checkISODateTime,
|
|
7
|
+
'cds.Timestamp': checkISOTimestamp,
|
|
8
|
+
'cds.String': checkString
|
|
9
|
+
} = require('./type')
|
|
10
|
+
const { getTarget, resolveCDSType } = require('./utils')
|
|
11
|
+
|
|
12
|
+
const _isNavigationColumn = (col, as) => col.ref?.length > 1 && (col.as === as || col.ref[col.ref.length - 1] === as)
|
|
13
|
+
|
|
14
|
+
// REVISIT: mandatory is actually not the same as not null or empty string
|
|
15
|
+
const _isNotFilled = val => val === null || val === undefined || (typeof val === 'string' && val.trim() === '')
|
|
16
|
+
|
|
17
|
+
const _getEnumElement = ele => ((ele['@assert.range'] && ele.enum) || ele['@assert.enum'] ? ele.enum : undefined)
|
|
18
|
+
|
|
19
|
+
const _enumValues = ele => {
|
|
20
|
+
return Object.keys(ele).map(enumKey => {
|
|
21
|
+
const enum_ = ele[enumKey]
|
|
22
|
+
const enumValue = enum_ && enum_.val
|
|
23
|
+
if (enumValue !== undefined) {
|
|
24
|
+
if (enumValue['=']) return enumValue['=']
|
|
25
|
+
if (enum_ && enum_.literal && enum_.literal === 'number') return Number(enumValue)
|
|
26
|
+
return enumValue
|
|
27
|
+
}
|
|
28
|
+
return enumKey
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const _checkDateValue = (val, r1, r2) => {
|
|
33
|
+
const dateVal = new Date(val)
|
|
34
|
+
return (dateVal - new Date(r1)) * (dateVal - new Date(r2)) <= 0
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const _toDate = val => `2000-01-01T${val}Z`
|
|
38
|
+
|
|
39
|
+
const _checkInRange = (val, range, type) => {
|
|
40
|
+
switch (type) {
|
|
41
|
+
case 'cds.Date':
|
|
42
|
+
return checkISODate(val) && _checkDateValue(val, range[0], range[1])
|
|
43
|
+
case 'cds.DateTime':
|
|
44
|
+
return checkISODateTime(val) && _checkDateValue(val, range[0], range[1])
|
|
45
|
+
case 'cds.Timestamp':
|
|
46
|
+
return checkISOTimestamp(val) && _checkDateValue(val, range[0], range[1])
|
|
47
|
+
case 'cds.Time':
|
|
48
|
+
return checkISOTime(val) && _checkDateValue(_toDate(val), _toDate(range[0]), _toDate(range[1]))
|
|
49
|
+
default:
|
|
50
|
+
return (val - range[0]) * (val - range[1]) <= 0
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// process.env.CDS_ASSERT_FORMAT_FLAGS is not official!
|
|
55
|
+
const _checkRegExpFormat = (val, format) =>
|
|
56
|
+
checkString(val) && val.match(new RegExp(format, process.env.CDS_ASSERT_FORMAT_FLAGS || 'u'))
|
|
57
|
+
|
|
58
|
+
const checkMandatory = (v, ele, errs, path, k) => {
|
|
59
|
+
// REVISIT: correct to not complain?
|
|
60
|
+
// do not complain about missing foreign keys in children
|
|
61
|
+
if (path.length && ele['@odata.foreignKey4']) return
|
|
62
|
+
|
|
63
|
+
// TODO: which case is this?
|
|
64
|
+
// do not complain about ???
|
|
65
|
+
if (ele.parent?.query?.SELECT?.columns?.find(col => _isNavigationColumn(col, ele.name))) return
|
|
66
|
+
|
|
67
|
+
if (!ele.default && _isNotFilled(v)) {
|
|
68
|
+
const target = getTarget(path, k)
|
|
69
|
+
errs.push(new cds.error('ASSERT_NOT_NULL', { target, statusCode: 400, code: '400' }))
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const checkEnum = (v, ele, errs, path, k) => {
|
|
74
|
+
const enumElements = _getEnumElement(ele)
|
|
75
|
+
const enumValues = enumElements && _enumValues(enumElements)
|
|
76
|
+
if (enumElements && !enumValues.includes(v)) {
|
|
77
|
+
const args =
|
|
78
|
+
typeof v === 'string'
|
|
79
|
+
? ['"' + v + '"', enumValues.map(ele => '"' + ele + '"').join(', ')]
|
|
80
|
+
: [v, enumValues.join(', ')]
|
|
81
|
+
const target = getTarget(path, k)
|
|
82
|
+
errs.push(new cds.error('ASSERT_ENUM', { args, target, statusCode: 400, code: '400' }))
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const checkRange = (v, ele, errs, path, k) => {
|
|
87
|
+
const rangeElements = ele['@assert.range'] && !_getEnumElement(ele) ? ele['@assert.range'] : undefined
|
|
88
|
+
if (rangeElements && !_checkInRange(v, rangeElements, resolveCDSType(ele))) {
|
|
89
|
+
const args = [v, ...ele['@assert.range']]
|
|
90
|
+
const target = getTarget(path, k)
|
|
91
|
+
errs.push(new cds.error('ASSERT_RANGE', { args, target, statusCode: 400, code: '400' }))
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const checkFormat = (v, ele, errs, path, k) => {
|
|
96
|
+
const formatElements = ele['@assert.format']
|
|
97
|
+
if (formatElements && !_checkRegExpFormat(v, formatElements)) {
|
|
98
|
+
const args = [v, formatElements]
|
|
99
|
+
const target = getTarget(path, k)
|
|
100
|
+
errs.push(new cds.error('ASSERT_FORMAT', { args, target, statusCode: 400, code: '400' }))
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
module.exports = {
|
|
105
|
+
checkMandatory,
|
|
106
|
+
checkEnum,
|
|
107
|
+
checkRange,
|
|
108
|
+
checkFormat
|
|
109
|
+
}
|
package/libx/odata/index.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
const cds = require('
|
|
2
|
-
{ decodeURIComponent } = cds.utils
|
|
1
|
+
const cds = require('../../')
|
|
3
2
|
const { SELECT } = cds.ql
|
|
3
|
+
const { decodeURIComponent } = cds.utils
|
|
4
4
|
|
|
5
|
-
const odata2cqn = require('./parser').parse
|
|
6
|
-
const cqn2odata = require('./cqn2odata')
|
|
5
|
+
const odata2cqn = require('./parse/parser').parse
|
|
6
|
+
const cqn2odata = require('./parse/cqn2odata')
|
|
7
7
|
|
|
8
|
-
const afterburner = require('./afterburner')
|
|
8
|
+
const afterburner = require('./parse/afterburner')
|
|
9
9
|
const { getSafeNumber: safeNumber } = require('./utils')
|
|
10
10
|
const getError = require('../_runtime/common/error')
|
|
11
11
|
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
const cds = require('../../../')
|
|
2
|
+
const { INSERT } = cds.ql
|
|
3
|
+
|
|
4
|
+
const { toODataResult } = require('../utils/result')
|
|
5
|
+
const { odataError, getKeysFromPath } = require('../utils')
|
|
6
|
+
|
|
7
|
+
const { deepCopy } = require('../../_runtime/common/utils/copy')
|
|
8
|
+
|
|
9
|
+
// REVISIT: move to or rewrite in libx/odata
|
|
10
|
+
const { readAfterWrite } = require('../../_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite')
|
|
11
|
+
const metaInfo = require('../../_runtime/cds-services/adapter/odata-v4/utils/metaInfo')
|
|
12
|
+
|
|
13
|
+
const _calculateLocationHeader = (target, srv, result) => {
|
|
14
|
+
const targetName = target.name.replace(`${srv.name}.`, '')
|
|
15
|
+
|
|
16
|
+
const keyValuePairs = Object.keys(target.keys).reduce((acc, key) => {
|
|
17
|
+
acc[key] = result[key]
|
|
18
|
+
return acc
|
|
19
|
+
}, {})
|
|
20
|
+
|
|
21
|
+
let keys
|
|
22
|
+
const entries = Object.entries(keyValuePairs)
|
|
23
|
+
if (entries.length === 1) {
|
|
24
|
+
keys = entries[0][1]
|
|
25
|
+
} else {
|
|
26
|
+
keys = entries.map(([key, value]) => `${key}=${value}`).join(',')
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return `${targetName}(${keys})`
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
module.exports = srv =>
|
|
33
|
+
function create(req, res, next) {
|
|
34
|
+
const { _query: query } = req
|
|
35
|
+
|
|
36
|
+
const {
|
|
37
|
+
SELECT: { one, from }
|
|
38
|
+
} = query
|
|
39
|
+
|
|
40
|
+
if (one) {
|
|
41
|
+
const singleton = query.target._isSingleton
|
|
42
|
+
const error = odataError('405', `Method ${req.method} not allowed for ${singleton ? 'SINGLETON' : 'ENTITY'}`)
|
|
43
|
+
return res.status(405).json(error)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const data = deepCopy(req.body)
|
|
47
|
+
|
|
48
|
+
// add keys from url into payload (overwriting if already present)
|
|
49
|
+
Object.assign(data, getKeysFromPath(from, srv))
|
|
50
|
+
|
|
51
|
+
// assert payload
|
|
52
|
+
const assertOptions = { filter: true, http: { req }, mandatories: true }
|
|
53
|
+
const errs = cds.assert(data, query.target, assertOptions)
|
|
54
|
+
if (errs) {
|
|
55
|
+
if (errs.length === 1) throw errs[0]
|
|
56
|
+
throw Object.assign(new Error('MULTIPLE_ERRORS'), { statusCode: 400, details: errs })
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const insertQuery = INSERT.into(from).entries(data)
|
|
60
|
+
|
|
61
|
+
// we need the cds request, so we can access req._.readAfterWrite
|
|
62
|
+
const cdsReq = new cds.Request({ query: insertQuery })
|
|
63
|
+
|
|
64
|
+
// rewrite event for draft-enabled entities
|
|
65
|
+
if (query.target._isDraftEnabled) cdsReq.event = 'NEW'
|
|
66
|
+
|
|
67
|
+
return srv
|
|
68
|
+
.dispatch(cdsReq)
|
|
69
|
+
.then(async result => {
|
|
70
|
+
if (cdsReq._.readAfterWrite) {
|
|
71
|
+
// TODO see if in old odata impl for other checks that should happen
|
|
72
|
+
result = await readAfterWrite(cdsReq, srv, { operation: { result } })
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (result == null || req._preferReturn === 'minimal') return res.sendStatus(204)
|
|
76
|
+
|
|
77
|
+
const location = _calculateLocationHeader(cdsReq.target, srv, result)
|
|
78
|
+
const info = metaInfo(insertQuery, 'CREATE', srv, result, req)
|
|
79
|
+
result = toODataResult(result, info)
|
|
80
|
+
res.set('location', location).status(201).send(result)
|
|
81
|
+
})
|
|
82
|
+
.catch(next)
|
|
83
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const cds = require('../../../')
|
|
2
|
+
const { UPDATE, DELETE } = cds.ql
|
|
3
|
+
|
|
4
|
+
const { odataError, getKeysFromPath } = require('../utils')
|
|
5
|
+
|
|
6
|
+
module.exports = srv =>
|
|
7
|
+
function deleete(req, res, next) {
|
|
8
|
+
if (req._preferReturn) {
|
|
9
|
+
const message = `The 'return' preference is not allowed in ${req.method} requests`
|
|
10
|
+
return res.status(400).json({ error: { code: '400', message } })
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const { _query: query } = req
|
|
14
|
+
|
|
15
|
+
const {
|
|
16
|
+
SELECT: { one, from }
|
|
17
|
+
} = query
|
|
18
|
+
|
|
19
|
+
if (!one) {
|
|
20
|
+
// REVISIT: don't use "ENTITY.COLLECTION" as that's an okra term
|
|
21
|
+
return res.status(405).json(odataError('405', `Method DELETE not allowed for ENTITY.COLLECTION`))
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// for read and delete, we provide keys in req.data
|
|
25
|
+
const data = getKeysFromPath(query.SELECT.from, srv)
|
|
26
|
+
|
|
27
|
+
// REVISIT: better
|
|
28
|
+
if (query._propertyAccess) data[query._propertyAccess] = null
|
|
29
|
+
|
|
30
|
+
// REVISIT: maybe also just dispatch a cds request here?
|
|
31
|
+
return srv
|
|
32
|
+
.run(query._propertyAccess ? UPDATE(from).set({ [query._propertyAccess]: null }) : DELETE.from(from), data)
|
|
33
|
+
.then(result => {
|
|
34
|
+
if (result === 0) return res.status(404).json({ error: { code: '404', message: 'Not Found' } })
|
|
35
|
+
res.sendStatus(204)
|
|
36
|
+
})
|
|
37
|
+
.catch(next)
|
|
38
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
const { normalizeError } = require('../../_runtime/common/error/frontend')
|
|
2
|
+
|
|
3
|
+
module.exports = _srv => (err, req, res, _next) => {
|
|
4
|
+
const { error, statusCode } = normalizeError(err, req)
|
|
5
|
+
|
|
6
|
+
// NOTE: normalizeError already does sanatization -> we can use as is
|
|
7
|
+
res.status(statusCode).json({ error })
|
|
8
|
+
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
const cds = require('
|
|
1
|
+
const cds = require('../../../')
|
|
2
2
|
const LOG = cds.log('odata')
|
|
3
|
+
|
|
3
4
|
const crypto = require('crypto')
|
|
4
|
-
|
|
5
|
+
|
|
6
|
+
const { odataError } = require('../utils')
|
|
5
7
|
|
|
6
8
|
const _requestedFormat = (queryOption, header) => {
|
|
7
9
|
if (queryOption) return queryOption.match(/json/i) ? 'json' : 'xml'
|
|
@@ -91,10 +93,10 @@ module.exports = srv =>
|
|
|
91
93
|
// REVISIT: remove check later
|
|
92
94
|
if (mpSupportsEmptyLocale()) {
|
|
93
95
|
// If no extensibility nor fts, do not provide model to mtxs
|
|
94
|
-
const modelNeeded =
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
96
|
+
const modelNeeded = cds.env.requires.extensibility || cds.context.features?.given
|
|
97
|
+
edmx =
|
|
98
|
+
metadataCache.edm ||
|
|
99
|
+
(await mps.getEdmx({ tenant, model: modelNeeded && srv.model, service: srv.definition.name }))
|
|
98
100
|
metadataCache.edm = edmx
|
|
99
101
|
const extBundle = cds.env.requires.extensibility && (await mps.getI18n({ tenant, locale }))
|
|
100
102
|
edmx = cds.localize(srv.model, locale, edmx, extBundle)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
const cds = require('../../../')
|
|
2
|
+
|
|
3
|
+
const { toODataResult } = require('../utils/result')
|
|
4
|
+
const { cds2edm } = require('../utils')
|
|
5
|
+
|
|
6
|
+
const { deepCopy } = require('../../_runtime/common/utils/copy')
|
|
7
|
+
|
|
8
|
+
// REVISIT: move to or rewrite in libx/odata
|
|
9
|
+
const metaInfo = require('../../_runtime/cds-services/adapter/odata-v4/utils/metaInfo')
|
|
10
|
+
|
|
11
|
+
module.exports = srv => (req, res, next) => {
|
|
12
|
+
let { operation, args } = req._query.SELECT.from.ref.slice(-1)[0]
|
|
13
|
+
if (!operation) return next() //> create or read
|
|
14
|
+
|
|
15
|
+
// unbound vs. bound
|
|
16
|
+
let entity
|
|
17
|
+
if (srv.model.definitions[operation]) {
|
|
18
|
+
operation = srv.model.definitions[operation]
|
|
19
|
+
} else {
|
|
20
|
+
req._query.SELECT.from.ref.pop()
|
|
21
|
+
// TODO: this does not work when navigating to the entity
|
|
22
|
+
const lastRef = req._query.SELECT.from.ref.slice(-1)[0]
|
|
23
|
+
entity = lastRef.id || lastRef
|
|
24
|
+
entity = srv.model.definitions[entity]
|
|
25
|
+
operation = entity.actions[operation]
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const data = args || deepCopy(req.body)
|
|
29
|
+
|
|
30
|
+
// assert payload
|
|
31
|
+
const assertOptions = { filter: true, http: { req }, mandatories: true }
|
|
32
|
+
const errs = cds.assert(data, operation, assertOptions)
|
|
33
|
+
if (errs) {
|
|
34
|
+
if (errs.length === 1) throw errs[0]
|
|
35
|
+
throw Object.assign(new Error('MULTIPLE_ERRORS'), { statusCode: 400, details: errs })
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// REVISIT: when is operation.name actually prefixed with the service name?
|
|
39
|
+
const event = operation.name.replace(`${srv.name}.`, '')
|
|
40
|
+
|
|
41
|
+
// TODO: params
|
|
42
|
+
const cdsReq = new cds.Request({ query: entity ? req._query : undefined, event, data, params: [] })
|
|
43
|
+
|
|
44
|
+
srv
|
|
45
|
+
.dispatch(cdsReq)
|
|
46
|
+
.then(result => {
|
|
47
|
+
// REVISIT: result === undefined valid for modelled return type?
|
|
48
|
+
if (!operation.returns || result === undefined) return res.sendStatus(204)
|
|
49
|
+
|
|
50
|
+
if (operation.returns._type?.match?.(/^cds\./)) {
|
|
51
|
+
// TODO: check result type
|
|
52
|
+
return res.json({
|
|
53
|
+
'@odata.context': `${entity ? '../' : ''}$metadata#${cds2edm[operation.returns._type]}`,
|
|
54
|
+
value: result
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const assertOptions = { mandatories: true } //> TODO: more needed?
|
|
59
|
+
// TODO: error targets are not correct if return type is "many X"
|
|
60
|
+
const assertDefinition = operation.returns.items || operation.returns
|
|
61
|
+
const errs = cds.assert(result, assertDefinition, assertOptions)
|
|
62
|
+
if (errs) {
|
|
63
|
+
// TODO: proper error handling
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const info = metaInfo(req._query, event, srv, result, req)
|
|
67
|
+
// FIXME: info.metadata.isCollection is incorrect for draftActivate
|
|
68
|
+
if (event === 'draftActivate') info.metadata.isCollection = false
|
|
69
|
+
result = toODataResult(result, info)
|
|
70
|
+
|
|
71
|
+
// TODO: toODataResult() doesn't seem to handle this case
|
|
72
|
+
if (entity && !result['@odata.context'].match(/^\.\.\//))
|
|
73
|
+
result['@odata.context'] = '../' + result['@odata.context']
|
|
74
|
+
|
|
75
|
+
res.json(result)
|
|
76
|
+
})
|
|
77
|
+
.catch(next)
|
|
78
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
const cds = require('../../../')
|
|
2
|
+
|
|
3
|
+
module.exports = srv => (req, _, next) => {
|
|
4
|
+
// if not a GET, use req.path instead of req.url to ignore query parameters
|
|
5
|
+
req._query = cds.odata.parse(req.method === 'GET' ? req.url : req.path, { service: srv, baseUrl: req.baseUrl })
|
|
6
|
+
|
|
7
|
+
const preferReturn = req.headers.prefer?.match(/\W?return=(\w+)/i)
|
|
8
|
+
if (preferReturn) req._preferReturn = preferReturn[1]
|
|
9
|
+
|
|
10
|
+
next()
|
|
11
|
+
}
|