@sap/cds 7.6.4 → 7.7.1

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.
Files changed (97) hide show
  1. package/CHANGELOG.md +39 -1
  2. package/_i18n/i18n.properties +3 -0
  3. package/app/index.js +14 -8
  4. package/bin/serve.js +51 -19
  5. package/common.cds +16 -0
  6. package/lib/auth/ias-auth.js +2 -2
  7. package/lib/auth/index.js +1 -1
  8. package/lib/auth/jwt-auth.js +1 -1
  9. package/lib/compile/cdsc.js +23 -11
  10. package/lib/compile/for/nodejs.js +2 -2
  11. package/lib/compile/for/odata.js +4 -0
  12. package/lib/compile/load.js +7 -2
  13. package/lib/compile/to/sql.js +3 -0
  14. package/lib/dbs/cds-deploy.js +197 -220
  15. package/lib/env/defaults.js +2 -1
  16. package/lib/index.js +8 -2
  17. package/lib/linked/types.js +1 -0
  18. package/lib/log/format/json.js +4 -1
  19. package/lib/plugins.js +2 -2
  20. package/lib/ql/SELECT.js +8 -8
  21. package/lib/req/context.js +22 -13
  22. package/lib/req/request.js +10 -4
  23. package/lib/srv/cds-connect.js +9 -3
  24. package/lib/srv/cds-serve.js +5 -3
  25. package/lib/srv/middlewares/ctx-model.js +1 -1
  26. package/lib/srv/protocols/odata-v4.js +38 -9
  27. package/lib/srv/srv-api.js +98 -140
  28. package/lib/srv/srv-models.js +2 -2
  29. package/lib/srv/srv-tx.js +1 -0
  30. package/lib/utils/cds-utils.js +32 -23
  31. package/lib/utils/data.js +1 -1
  32. package/lib/utils/tar.js +1 -1
  33. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +1 -3
  34. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +0 -2
  35. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +18 -1
  36. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/index.js +1 -1
  37. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/utils.js +7 -3
  38. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/UriParser.js +2 -1
  39. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/index.js +5 -0
  40. package/libx/_runtime/cds-services/adapter/odata-v4/utils/data.js +71 -25
  41. package/libx/_runtime/cds-services/adapter/odata-v4/utils/metaInfo.js +10 -2
  42. package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +6 -1
  43. package/libx/_runtime/cds-services/util/assert.js +50 -240
  44. package/libx/_runtime/cds.js +5 -0
  45. package/libx/_runtime/common/aspects/any.js +53 -45
  46. package/libx/_runtime/common/generic/input.js +14 -10
  47. package/libx/_runtime/common/generic/paging.js +1 -1
  48. package/libx/_runtime/common/utils/cqn.js +1 -1
  49. package/libx/_runtime/common/utils/cqn2cqn4sql.js +1 -1
  50. package/libx/_runtime/common/utils/keys.js +1 -1
  51. package/libx/_runtime/common/utils/quotingStyles.js +1 -1
  52. package/libx/_runtime/common/utils/resolveStructured.js +4 -1
  53. package/libx/_runtime/common/utils/rewriteAsterisks.js +5 -12
  54. package/libx/_runtime/common/utils/stream.js +2 -16
  55. package/libx/_runtime/common/utils/streamProp.js +16 -6
  56. package/libx/_runtime/common/utils/ucsn.js +1 -0
  57. package/libx/_runtime/db/expand/expandCQNToJoin.js +1 -1
  58. package/libx/_runtime/db/sql-builder/InsertBuilder.js +1 -1
  59. package/libx/_runtime/db/utils/columns.js +6 -1
  60. package/libx/_runtime/fiori/generic/activate.js +11 -3
  61. package/libx/_runtime/fiori/generic/edit.js +8 -2
  62. package/libx/_runtime/fiori/lean-draft.js +94 -30
  63. package/libx/_runtime/hana/execute.js +2 -5
  64. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +12 -22
  65. package/libx/_runtime/messaging/service.js +6 -2
  66. package/libx/common/assert/index.js +232 -0
  67. package/libx/common/assert/type.js +109 -0
  68. package/libx/common/assert/utils.js +125 -0
  69. package/libx/common/assert/validation.js +109 -0
  70. package/libx/odata/index.js +5 -5
  71. package/libx/odata/middleware/create.js +83 -0
  72. package/libx/odata/middleware/delete.js +38 -0
  73. package/libx/odata/middleware/error.js +8 -0
  74. package/libx/odata/{metadata.js → middleware/metadata.js} +8 -6
  75. package/libx/odata/middleware/operation.js +78 -0
  76. package/libx/odata/middleware/parse.js +11 -0
  77. package/libx/odata/{read.js → middleware/read.js} +42 -20
  78. package/libx/odata/{service-document.js → middleware/service-document.js} +2 -1
  79. package/libx/odata/middleware/stream.js +237 -0
  80. package/libx/odata/middleware/update.js +165 -0
  81. package/libx/odata/{afterburner.js → parse/afterburner.js} +79 -29
  82. package/libx/odata/{cqn2odata.js → parse/cqn2odata.js} +5 -3
  83. package/libx/odata/{parseToCqn.js → parse/parseToCqn.js} +3 -6
  84. package/libx/odata/{utils.js → utils/index.js} +95 -9
  85. package/libx/outbox/index.js +2 -1
  86. package/libx/rest/RestAdapter.js +0 -1
  87. package/libx/rest/middleware/operation.js +6 -4
  88. package/libx/rest/middleware/parse.js +20 -2
  89. package/package.json +1 -1
  90. package/server.js +43 -71
  91. package/libx/odata/create.js +0 -44
  92. package/libx/odata/delete.js +0 -25
  93. package/libx/odata/error.js +0 -12
  94. package/libx/odata/update.js +0 -110
  95. /package/libx/odata/{grammar.peggy → parse/grammar.peggy} +0 -0
  96. /package/libx/odata/{parser.js → parse/parser.js} +0 -0
  97. /package/libx/odata/{result.js → utils/result.js} +0 -0
@@ -0,0 +1,232 @@
1
+ const { cds } = global
2
+
3
+ const typeCheckers = require('./type')
4
+ const { checkMandatory, checkEnum, checkRange, checkFormat } = require('./validation')
5
+ const { getNested, getTarget, resolveCDSType, resolveSegment } = require('./utils')
6
+
7
+ const NUMBER_TYPES = new Set(['cds.UInt8', 'cds.Int16', 'cds.Int32', 'cds.Integer', 'cds.Double'])
8
+
9
+ const _no_op = () => {}
10
+
11
+ const _reject_unknown = (_, k, def, errs) =>
12
+ errs.push(new cds.error(`Property ${k} does not exist in ${def.name}`, { statusCode: 400, code: '400' }))
13
+
14
+ const _filter_unknown = (obj, k) => delete obj[k]
15
+
16
+ const _handle_mandatories = (obj, def, errs, path) => {
17
+ for (const [k, ele] of def._mandatories) {
18
+ const v = obj[k] === undefined ? getNested(k, obj) : obj[k]
19
+ checkMandatory(v, ele, errs, path, k)
20
+ }
21
+ }
22
+
23
+ const _handle_mandatories_if_insert = (obj, def, errs, path) => {
24
+ if (!def.keys) return _handle_mandatories(obj, def, errs, path)
25
+ const allKeysProvided = Object.keys(def.keys).every(k => k in obj)
26
+ for (const [k, ele] of def._mandatories) {
27
+ const v = obj[k] === undefined ? getNested(k, obj) : obj[k]
28
+ if (!allKeysProvided || v !== undefined) checkMandatory(v, ele, errs, path, k)
29
+ }
30
+ }
31
+
32
+ function _recurse(obj, prefix, def, errs, opts) {
33
+ for (const [k, v] of Object.entries(obj)) {
34
+ if (v != null && typeof v === 'object' && !Array.isArray(v)) {
35
+ _recurse(v, prefix + k + '_', def, errs, opts)
36
+ continue
37
+ }
38
+ const flat = { [prefix + k]: v }
39
+ _process(flat, def, errs, opts)
40
+ if (!Object.keys(flat).length) delete obj[k] //> filtered out inside _process -> propagate to original object
41
+ }
42
+ }
43
+
44
+ function _process(obj, def, errs, opts) {
45
+ if (obj == null) return
46
+
47
+ if (Array.isArray(obj)) {
48
+ for (const row of obj) _process(row, def, errs, opts)
49
+ return
50
+ }
51
+
52
+ // TODO: path should be cqn
53
+ const prev = opts.path.length && opts.path[opts.path.length - 1]
54
+ if (prev?.keys || prev?.index) opts.path[opts.path.length - 1] = resolveSegment(prev, obj, def)
55
+
56
+ if (def._mandatories?.length) opts._handle_mandatories(obj, def, errs, opts.path)
57
+
58
+ for (let [k, v] of Object.entries(obj)) {
59
+ let ele = def.elements?.[k] || def.params?.[k] || def.items
60
+
61
+ /*
62
+ * TODO: should we support this? with or without transformation?
63
+ * structured vs flat
64
+ * the combination of the two cases below SHOULD cover mixed cases like
65
+ * foo: { bar: { baz: { ... } } } and foo_bar: { baz: { ... } }
66
+ * TODO: add tests!!!
67
+ */
68
+ // case 1: structured data but flat model
69
+ if (
70
+ !ele &&
71
+ typeof obj[k] === 'object' &&
72
+ !Array.isArray(obj[k]) &&
73
+ (def.elements || def.params) &&
74
+ Object.keys(def.elements || def.params).find(key => key.startsWith(`${k}_`))
75
+ ) {
76
+ _recurse(obj[k], k + '_', def, errs, opts)
77
+ continue
78
+ }
79
+ // case 2: flat data but structured model
80
+ if (!ele && k.split('_').length > 1) {
81
+ // TODO: handle stuff like foo__bar, i.e., foo_: { bar: ... }
82
+ const parts = k.split('_')
83
+ let cur = def.elements || def.params
84
+ while (cur && parts.length) cur = (cur.elements || cur.params)?.[parts.shift()]
85
+ if (cur) ele = cur
86
+ }
87
+
88
+ if (!ele) {
89
+ if (!def['@open']) opts._handle_unknown(obj, k, def, errs)
90
+ continue
91
+ }
92
+
93
+ if (ele.isAssociation) {
94
+ const keys = ele.keys?.map(k => k.ref[0]) || Object.keys(ele._target.keys)
95
+ opts.path.push(ele.is2many || Object.keys(keys).length ? { assoc: k, keys } : k)
96
+ // NOTE: the assumption is that children with all keys provided are not inserted, but updated
97
+ // -> incomplete but best we can do without roundtrip
98
+ _process(v, ele._target, errs, {
99
+ ...opts,
100
+ _handle_mandatories:
101
+ opts.mandatories === false || ele._isAssociationStrict
102
+ ? _no_op
103
+ : opts.mandatories === true
104
+ ? _handle_mandatories
105
+ : _handle_mandatories_if_insert
106
+ })
107
+ opts.path.pop()
108
+ continue
109
+ }
110
+ if (ele._isStructured) {
111
+ opts.path.push(k)
112
+ _process(v, ele, errs, opts)
113
+ opts.path.pop()
114
+ continue
115
+ }
116
+ if (ele instanceof cds.builtin.classes.array) {
117
+ for (let i = 0; i < v.length; i++) {
118
+ opts.path.push({ prop: k, index: i })
119
+ const _def = ele.items?.__proto__.elements ? ele.items.__proto__ : ele.__proto__
120
+ const _obj = _def.elements ? v[i] : { [k]: v[i] }
121
+ _process(_obj, _def, errs, opts)
122
+ opts.path.pop()
123
+ }
124
+ continue
125
+ }
126
+
127
+ if (ele.notNull && v === null) {
128
+ const target = getTarget(opts.path, k)
129
+ errs.push(new cds.error('ASSERT_NOT_NULL', { target, statusCode: 400, code: '400' }))
130
+ continue
131
+ }
132
+
133
+ const type = resolveCDSType(ele)
134
+ if (type?.match(/^cds\.hana\./)) continue
135
+
136
+ let typeChecker = typeCheckers[type]
137
+ if (typeChecker) {
138
+ if (v == null) continue
139
+
140
+ // if used in protocol adapter, adjust val/ checker if necessary
141
+ if (opts.http) {
142
+ if (typeof v !== 'boolean') {
143
+ if (NUMBER_TYPES.has(type)) v = Number(v)
144
+ else if (type === 'cds.Double') v = parseFloat(v)
145
+
146
+ // REVISIT: consider ieee754 and exp dec headers?
147
+ // const ieee = opts.http.req?.headers['content-type'].match(/IEEE754Compatible=(\w+)/i)
148
+ // const exp = opts.http.req?.headers['content-type'].match(/ExponentialDecimals=(\w+)/i)
149
+ // if (type === 'cds.Decimal') {
150
+ // TODO
151
+ // }
152
+ }
153
+ }
154
+
155
+ // use relaxed uuid check if not in strict mode
156
+ if (type === 'cds.UUID' && !opts.strict) typeChecker = typeCheckers['relaxed.UUID']
157
+
158
+ // type check
159
+ // REVISIT: all checkers should add errors themselves!
160
+ if (type === 'cds.Decimal')
161
+ typeChecker(v, ele, errs, opts.path, k) //> _checkDecimal adds error itself
162
+ else if (!typeChecker(v, ele) || (opts.strict && typeChecker.name === '_checkBuffer' && typeof v === 'string')) {
163
+ errs.push(
164
+ new cds.error('ASSERT_DATA_TYPE', {
165
+ args: [typeof obj[k] === 'string' ? `"${obj[k]}"` : obj[k], ele._type],
166
+ target: getTarget(opts.path, k),
167
+ statusCode: 400,
168
+ code: '400'
169
+ })
170
+ )
171
+ }
172
+
173
+ // propagate correction if necessary
174
+ if (obj[k] !== v) obj[k] = v
175
+
176
+ // @assert
177
+ if (ele['@assert.enum'] || (ele['@assert.range'] && ele.enum)) checkEnum(v, ele, errs, opts.path, k)
178
+ if (ele['@assert.range']) checkRange(v, ele, errs, opts.path, k)
179
+ if (ele['@assert.format']) checkFormat(v, ele, errs, opts.path, k)
180
+ // REVISIT: @assert.target? -> no because async, but maybe return the necessary query to execute?
181
+
182
+ continue
183
+ }
184
+
185
+ throw new Error(`Missing type check for "${ele.type}" (property "${k}" of "${def.name}")`)
186
+ }
187
+ }
188
+
189
+ /**
190
+ * Asserts the given data against the given CSN definition and returns an array of errors or undefined.
191
+ *
192
+ * @param {object} data - the data to be checked
193
+ * @param {LinkedCSN} definition - the CSN definition to which the data should be checked against
194
+ * @param {object} [options] - options
195
+ * @param {boolean} [options.strict] - if true, an error is thrown if a property is not defined in the CSN
196
+ * @param {boolean} [options.filter] - if true, properties not defined in the CSN are filtered out
197
+ * @param {boolean} [options.mandatories] - if false, mandatory properties are never checked.
198
+ * if true, mandatory properties are always checked.
199
+ * if undefined, mandatory properties are checked for presumed insert rows only (determined by a heuristic to avoid roundtrip).
200
+ * @param {object} [options.http] - the HTTP request object providing access to headers, etc.
201
+ * @param {*[]} [options.path] - collector for the current path, should not be set manually
202
+ * @return {Array} - an array of errors or undefined if no errors
203
+ */
204
+ module.exports = (data, definition, options = {}) => {
205
+ if (!data) throw new Error('Argument "data" was not provided')
206
+ if (typeof data !== 'object') throw new Error('Argument "data" must be an object (or an array)')
207
+
208
+ if (!definition) throw new Error('Argument "entity" was not provided')
209
+ // FIXME: definition instanceof cds.builtin.classes.any doesn't always work for some reason
210
+ if (!(definition instanceof cds.builtin.classes.any) && !(definition.kind in { entity: 1, action: 1, function: 1 })) {
211
+ throw new Error('Argument "definition" is not a valid CSN element')
212
+ }
213
+
214
+ // TODO: feature flags instead of process env vars
215
+ options.strict ??= process.env.CDS_ASSERT_STRICT === 'true'
216
+ options.filter ??= process.env.CDS_ASSERT_FILTER === 'true'
217
+ options.path ??= []
218
+
219
+ // materialize what is done ...
220
+ // ... in case of unknown elements
221
+ if (options.strict) options._handle_unknown = _reject_unknown
222
+ else if (options.filter) options._handle_unknown = _filter_unknown
223
+ else options._handle_unknown = _no_op
224
+ // ... regarding mandatory elements
225
+ if (options.mandatories === false) options._handle_mandatories = _no_op
226
+ else if (options.mandatories === true) options._handle_mandatories = _handle_mandatories
227
+ else options._handle_mandatories = _handle_mandatories_if_insert
228
+
229
+ const errs = []
230
+ _process(data, definition, errs, options)
231
+ return errs.length ? errs : undefined
232
+ }
@@ -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
+ }
@@ -1,11 +1,11 @@
1
- const cds = require('../_runtime/cds'),
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
+ }