@sap/cds 8.7.2 → 8.8.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 +50 -0
- package/_i18n/i18n.properties +3 -0
- package/_i18n/i18n_cs.properties +6 -6
- package/_i18n/i18n_de.properties +3 -0
- package/_i18n/i18n_en.properties +3 -0
- package/_i18n/i18n_es.properties +3 -0
- package/_i18n/i18n_fr.properties +3 -0
- package/_i18n/i18n_it.properties +3 -0
- package/_i18n/i18n_ja.properties +3 -0
- package/_i18n/i18n_pl.properties +7 -4
- package/_i18n/i18n_pt.properties +3 -0
- package/_i18n/i18n_ru.properties +3 -0
- package/app/index.js +2 -30
- package/lib/compile/parse.js +1 -1
- package/lib/env/cds-env.js +1 -1
- package/lib/env/cds-requires.js +16 -9
- package/lib/env/schemas/cds-package.js +1 -1
- package/lib/env/schemas/cds-rc.js +17 -4
- package/lib/index.js +1 -1
- package/lib/ql/SELECT.js +6 -1
- package/lib/ql/cds.ql-predicates.js +2 -1
- package/lib/req/request.js +5 -2
- package/lib/req/validate.js +4 -2
- package/lib/srv/bindings.js +31 -20
- package/lib/srv/cds-connect.js +1 -1
- package/lib/srv/middlewares/auth/mocked-users.js +1 -0
- package/lib/srv/protocols/okra.js +5 -7
- package/lib/srv/srv-dispatch.js +0 -5
- package/lib/test/cds-test.js +34 -6
- package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +1 -0
- package/libx/_runtime/common/generic/auth/restrict.js +1 -1
- package/libx/_runtime/common/generic/auth/service.js +2 -2
- package/libx/_runtime/common/generic/auth/utils.js +2 -1
- package/libx/_runtime/common/generic/input.js +1 -1
- package/libx/_runtime/common/utils/binary.js +1 -35
- package/libx/_runtime/common/utils/rewriteAsterisks.js +5 -8
- package/libx/_runtime/fiori/lean-draft.js +2 -4
- package/libx/common/utils/path.js +1 -5
- package/libx/common/utils/streaming.js +76 -0
- package/libx/odata/middleware/create.js +5 -1
- package/libx/odata/middleware/delete.js +1 -1
- package/libx/odata/middleware/operation.js +48 -4
- package/libx/odata/middleware/read.js +1 -1
- package/libx/odata/middleware/stream.js +29 -101
- package/libx/odata/middleware/update.js +1 -1
- package/libx/odata/parse/afterburner.js +21 -1
- package/libx/odata/parse/grammar.peggy +108 -26
- package/libx/odata/parse/multipartToJson.js +17 -10
- package/libx/odata/parse/parser.js +1 -1
- package/libx/odata/utils/metadata.js +28 -5
- package/libx/odata/utils/normalizeTimeData.js +11 -8
- package/libx/rest/RestAdapter.js +2 -16
- package/libx/rest/middleware/operation.js +38 -18
- package/libx/rest/middleware/parse.js +5 -25
- package/libx/rest/post-processing.js +33 -0
- package/libx/rest/pre-processing.js +38 -0
- package/package.json +1 -1
- package/libx/common/utils/index.js +0 -5
|
@@ -411,9 +411,11 @@ function _processSegments(from, model, namespace, cqn, protocol) {
|
|
|
411
411
|
ref[i].args = current['@open']
|
|
412
412
|
? Object.assign({}, from._params)
|
|
413
413
|
: Object.keys(from._params).reduce((acc, cur) => {
|
|
414
|
-
|
|
414
|
+
const param = cur.startsWith('@') ? cur.slice(1) : cur
|
|
415
|
+
if (current.params && param in current.params) acc[param] = from._params[cur]
|
|
415
416
|
return acc
|
|
416
417
|
}, {})
|
|
418
|
+
ref[i].args = _getDataFromParams(ref[i].args, current) //resolve parameter if Object or Array
|
|
417
419
|
_resolveImplicitFunctionParameters(ref[i].args)
|
|
418
420
|
}
|
|
419
421
|
}
|
|
@@ -803,6 +805,24 @@ module.exports = (cqn, model, namespace, protocol) => {
|
|
|
803
805
|
// one?
|
|
804
806
|
if (one) cqn.SELECT.one = true
|
|
805
807
|
|
|
808
|
+
// hierarchy requests, quick check to avoid unnecessary traversing
|
|
809
|
+
// REVISIT: Should be done via annotation on backlink, would make lookup easier
|
|
810
|
+
if (target?.elements?.LimitedDescendantCount) {
|
|
811
|
+
let uplinkName
|
|
812
|
+
for (const key in target) {
|
|
813
|
+
if (key.match(/@Aggregation\.RecursiveHierarchy#.*\.ParentNavigationProperty/)) {
|
|
814
|
+
// Qualifiers are bad for lookups
|
|
815
|
+
uplinkName = target[key]['=']
|
|
816
|
+
break
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
const setRecurseRef = SELECT => {
|
|
820
|
+
if (SELECT.from.SELECT) setRecurseRef(SELECT.from.SELECT)
|
|
821
|
+
if (SELECT.recurse) SELECT.recurse.ref[0] = uplinkName
|
|
822
|
+
}
|
|
823
|
+
if (uplinkName) setRecurseRef(cqn.SELECT)
|
|
824
|
+
}
|
|
825
|
+
|
|
806
826
|
// REVISIT: better
|
|
807
827
|
// set target (csn definition) for later retrieval
|
|
808
828
|
cqn.__target = current.parent?.kind === 'entity' ? `${current.parent.name}:$:${current.name}` : current.name
|
|
@@ -203,9 +203,71 @@
|
|
|
203
203
|
) {
|
|
204
204
|
cqn.from = { SELECT: { ...cqn } }
|
|
205
205
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
206
|
+
|
|
207
|
+
const _toplevels = cqn => {
|
|
208
|
+
cqn.recurse = {
|
|
209
|
+
ref: ['parent'],
|
|
210
|
+
|
|
211
|
+
}
|
|
212
|
+
if (apply.topLevels.levels) {
|
|
213
|
+
cqn.recurse.where = [{ ref: ['DistanceFromRoot'] }, '<=', { val: apply.topLevels.levels }]
|
|
214
|
+
}
|
|
215
|
+
if (apply.topLevels.expandLevels) {
|
|
216
|
+
if (!cqn.recurse.where) cqn.recurse.where = []
|
|
217
|
+
for (const expandLevel of apply.topLevels.expandLevels) {
|
|
218
|
+
if (cqn.recurse.where.length !== 0) cqn.recurse.where.push('or')
|
|
219
|
+
cqn.recurse.where.push({ ref: [apply.topLevels.nodeProperty] }, '=', { val: expandLevel.nodeID }, 'and', { ref: ['Distance'] }, 'between', { val: 0 }, 'and', { val: expandLevel.levels } )
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
const _descendants = cqn => {
|
|
224
|
+
return _ancestors(cqn, apply.descendants, 1)
|
|
225
|
+
}
|
|
226
|
+
const _ancestors = (cqn, info = apply.ancestors, direction = -1) => {
|
|
227
|
+
if (info.nodes) {
|
|
228
|
+
for (const node of info.nodes) {
|
|
229
|
+
if (node.filter) cqn.where = node.filter // should we accept $filter?
|
|
230
|
+
else if (node.search) cqn.search = node.search
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
const startVal = { val: info.keepStart ? 0 : direction * 1 }
|
|
234
|
+
const endVal = info.distance && { val: direction * info.distance }
|
|
235
|
+
const where = endVal ?
|
|
236
|
+
startVal.val === endVal.val ?
|
|
237
|
+
[{ ref: ['Distance'] }, '=', startVal ] :
|
|
238
|
+
[{ ref: ['Distance'] }, 'between', startVal, 'and', endVal] :
|
|
239
|
+
[{ ref: ['Distance'] }, direction < 0 ? '<=' : '>=', startVal ]
|
|
240
|
+
cqn.recurse = {
|
|
241
|
+
ref: ['parent'],
|
|
242
|
+
where
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (apply.ancestors && apply.topLevels) {
|
|
247
|
+
const inner = {
|
|
248
|
+
SELECT: {
|
|
249
|
+
from: cqn.from,
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
_ancestors(inner.SELECT)
|
|
253
|
+
cqn.from = inner
|
|
254
|
+
_toplevels(cqn)
|
|
255
|
+
} else if (apply.ancestors && apply.descendants) {
|
|
256
|
+
const inner = {
|
|
257
|
+
SELECT: {
|
|
258
|
+
from: cqn.from
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
_ancestors(inner.SELECT)
|
|
262
|
+
cqn.from = inner
|
|
263
|
+
_descendants(cqn)
|
|
264
|
+
} else if (apply.topLevels) {
|
|
265
|
+
_toplevels(cqn)
|
|
266
|
+
} else if (apply.ancestors) {
|
|
267
|
+
_ancestors(apply)
|
|
268
|
+
} else if (apply.descendants) {
|
|
269
|
+
_descendants(cqn)
|
|
270
|
+
}
|
|
209
271
|
if (apply.where) cqn.where = apply.where
|
|
210
272
|
if (apply.search) cqn.search = apply.search
|
|
211
273
|
if (apply.groupBy) {
|
|
@@ -262,17 +324,41 @@
|
|
|
262
324
|
return elements
|
|
263
325
|
}
|
|
264
326
|
|
|
327
|
+
const _custom = (k, v) => {
|
|
328
|
+
// normalize value
|
|
329
|
+
if (v === 'null') v = null
|
|
330
|
+
else if (v === 'true') v = true
|
|
331
|
+
else if (v === 'false') v = false
|
|
332
|
+
// set value in structure
|
|
333
|
+
// REVISIT: SELECT.from._params is a temporary hack
|
|
334
|
+
const params = SELECT.from._params ??= {}
|
|
335
|
+
let t = params
|
|
336
|
+
let x = k.match(/^(\w+)\[(.*)\]$/)
|
|
337
|
+
while (x) {
|
|
338
|
+
if (!(x[1] in t)) t[x[1]] = x[2] === '' ? [] : {}
|
|
339
|
+
t = t[x[1]]
|
|
340
|
+
k = x[2]
|
|
341
|
+
x = k.match(/^(\w+)\[(.*)\]$/)
|
|
342
|
+
}
|
|
343
|
+
if (Array.isArray(t)) t.push(v)
|
|
344
|
+
else t[k] = v
|
|
345
|
+
}
|
|
346
|
+
|
|
265
347
|
const _replaceAliasedInWhere = (where, alias, value, isFromWhere = false) => {
|
|
348
|
+
let isAliased = false
|
|
266
349
|
where?.forEach(element => {
|
|
267
350
|
if (element.val === alias) {
|
|
268
351
|
// TODO check if we want to store replaced aliases/values for req.data in actions/functions in CQN
|
|
269
352
|
element.val = 'list' in value ? value.list.map(ele => ele.val) : value.val
|
|
353
|
+
isAliased = true
|
|
270
354
|
} else if (element.list === alias) {
|
|
271
355
|
element.list = value.list
|
|
356
|
+
isAliased = true
|
|
272
357
|
} else if (element.func) {
|
|
273
358
|
element.args.forEach((arg, i) => {
|
|
274
359
|
if (arg.val === alias) {
|
|
275
360
|
arg.val = value.val
|
|
361
|
+
isAliased = true
|
|
276
362
|
} else if (arg.func) {
|
|
277
363
|
_replaceAliasedInWhere(arg.args, alias, value, isFromWhere)
|
|
278
364
|
}
|
|
@@ -281,14 +367,17 @@
|
|
|
281
367
|
_replaceAliased(element.SELECT, alias, value, isFromWhere)
|
|
282
368
|
}
|
|
283
369
|
});
|
|
370
|
+
return isAliased
|
|
284
371
|
}
|
|
285
372
|
const _replaceAliased = (select, alias, value, isFromWhere = false) => {
|
|
373
|
+
let isAliased = false
|
|
286
374
|
const {where, from} = select
|
|
287
|
-
_replaceAliasedInWhere(where, alias, value);
|
|
375
|
+
isAliased =_replaceAliasedInWhere(where, alias, value);
|
|
288
376
|
|
|
289
377
|
from?.ref?.forEach(element => {
|
|
290
|
-
_replaceAliasedInWhere(element.where, alias, value, true);
|
|
378
|
+
isAliased = _replaceAliasedInWhere(element.where, alias, value, true);
|
|
291
379
|
})
|
|
380
|
+
return isAliased
|
|
292
381
|
}
|
|
293
382
|
}
|
|
294
383
|
|
|
@@ -404,7 +493,7 @@
|
|
|
404
493
|
temporal /
|
|
405
494
|
format /
|
|
406
495
|
custom /
|
|
407
|
-
|
|
496
|
+
aliasedParamEqualsValOrPrefixParam /
|
|
408
497
|
deltaToken
|
|
409
498
|
// @OData spec for $expand:
|
|
410
499
|
// "Allowed system query options are $filter, $select, $orderby, $skip, $top, $count, $search, $expand and
|
|
@@ -646,29 +735,21 @@
|
|
|
646
735
|
|
|
647
736
|
aliasedParamVal = val / jsonObject / jsonArray / "[" list:innerListParam "]" { return { list } }
|
|
648
737
|
|
|
649
|
-
custom = k:$([a-zA-Z0-9-_.~!\[\]]+) "="? v:$([^&]*)? {
|
|
650
|
-
|
|
651
|
-
if (v === 'null') v = null
|
|
652
|
-
else if (v === 'true') v = true
|
|
653
|
-
else if (v === 'false') v = false
|
|
654
|
-
// set value in structure
|
|
655
|
-
// REVISIT: SELECT.from._params is a temporary hack
|
|
656
|
-
const params = SELECT.from._params ??= {}
|
|
657
|
-
let t = params
|
|
658
|
-
let x = k.match(/^(\w+)\[(.*)\]$/)
|
|
659
|
-
while (x) {
|
|
660
|
-
if (!(x[1] in t)) t[x[1]] = x[2] === '' ? [] : {}
|
|
661
|
-
t = t[x[1]]
|
|
662
|
-
k = x[2]
|
|
663
|
-
x = k.match(/^(\w+)\[(.*)\]$/)
|
|
664
|
-
}
|
|
665
|
-
if (Array.isArray(t)) t.push(v)
|
|
666
|
-
else t[k] = v
|
|
738
|
+
custom = k:$([[a-zA-Z0-9-_.~!\[\]]+) "="? v:$([^&]*)? {
|
|
739
|
+
_custom(k, v)
|
|
667
740
|
}
|
|
668
741
|
|
|
669
742
|
aliasedParam "an aliased parameter (@param)" = "@" i:identifier { return "@" + i }
|
|
670
|
-
|
|
671
|
-
_replaceAliased(SELECT, alias, value);
|
|
743
|
+
aliasedParamEqualsValOrPrefixParam = alias:aliasedParam "=" !aliasedParam value:aliasedParamVal {
|
|
744
|
+
const isAliased = _replaceAliased(SELECT, alias, value);
|
|
745
|
+
if (!isAliased) { // custom parameters with "@" prefix (not aliased)
|
|
746
|
+
if (value.val) {
|
|
747
|
+
value = value.val
|
|
748
|
+
} else if (value.list && Array.isArray(value.list)) {
|
|
749
|
+
value = value.list.map(obj => obj.val)
|
|
750
|
+
}
|
|
751
|
+
_custom(alias, value)
|
|
752
|
+
}
|
|
672
753
|
}
|
|
673
754
|
|
|
674
755
|
format = "$format=" f:$([^&]*) {
|
|
@@ -947,6 +1028,7 @@
|
|
|
947
1028
|
hierarchy: h,
|
|
948
1029
|
id: e,
|
|
949
1030
|
nodes: t,
|
|
1031
|
+
distance: d,
|
|
950
1032
|
keepStart: k?.keepStart || false
|
|
951
1033
|
}
|
|
952
1034
|
}
|
|
@@ -116,28 +116,35 @@ const _parseStream = async function* (body, boundary) {
|
|
|
116
116
|
.toString()
|
|
117
117
|
.replace(/^--(.*)$/gm, (_, g) => `HEAD /${g} HTTP/1.1${g.slice(-2) === '--' ? CRLF : ''}`)
|
|
118
118
|
// correct content-length for non-HEAD requests is inserted below
|
|
119
|
-
.replace(/content-length: \d+\r\n/gim, '')
|
|
119
|
+
.replace(/content-length: \d+\r\n/gim, '') // if content-length is given it should be taken
|
|
120
120
|
.replace(/ \$/g, ' /$')
|
|
121
121
|
|
|
122
122
|
// HACKS!!!
|
|
123
123
|
// ensure URLs start with slashes
|
|
124
124
|
changed = changed.replaceAll(/\r\n(GET|PUT|POST|PATCH|DELETE) (\w)/g, `\r\n$1 /$2`)
|
|
125
125
|
// add content-length headers
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
126
|
+
changed = changed
|
|
127
|
+
.split(CRLF + CRLF)
|
|
128
|
+
.map((line, i, arr) => {
|
|
129
|
+
if (/^(PUT|POST|PATCH) /.test(line) && !/content-length/i.test(line)) {
|
|
130
|
+
const body = arr[i + 1].split('\r\nHEAD')[0]
|
|
131
|
+
if (body) return `${line}${CRLF}content-length: ${Buffer.byteLength(body)}`
|
|
132
|
+
}
|
|
133
|
+
return line
|
|
134
|
+
})
|
|
135
|
+
.join(CRLF + CRLF)
|
|
134
136
|
// remove strange "Group ID" appendix
|
|
135
137
|
changed = changed.split(`${CRLF}Group ID`)[0] + CRLF
|
|
136
138
|
|
|
137
139
|
let ret = parser.execute(Buffer.from(changed))
|
|
138
140
|
|
|
139
141
|
if (typeof ret !== 'number') {
|
|
140
|
-
if (ret.
|
|
142
|
+
if (ret.code === 'HPE_HEADER_OVERFLOW') {
|
|
143
|
+
// same error conversion as node http server
|
|
144
|
+
ret.status = 431
|
|
145
|
+
ret.code = '431'
|
|
146
|
+
ret.message = 'Request Header Fields Too Large'
|
|
147
|
+
} else if (ret.message === 'Parse Error') {
|
|
141
148
|
ret.statusCode = 400
|
|
142
149
|
ret.message = `Error while parsing batch body at position ${ret.bytesParsed}: ${ret.reason}`
|
|
143
150
|
}
|