@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.
- package/CHANGELOG.md +39 -1
- package/_i18n/i18n.properties +3 -0
- package/app/index.js +14 -8
- 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 +4 -1
- package/lib/plugins.js +2 -2
- 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/lib/utils/tar.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/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/cqn.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/expand/expandCQNToJoin.js +1 -1
- package/libx/_runtime/db/sql-builder/InsertBuilder.js +1 -1
- 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 +94 -30
- package/libx/_runtime/hana/execute.js +2 -5
- package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +12 -22
- 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} +95 -9
- package/libx/outbox/index.js +2 -1
- 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,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
|
+
}
|
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
|
+
}
|