@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.
Files changed (58) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/_i18n/i18n.properties +3 -0
  3. package/_i18n/i18n_cs.properties +6 -6
  4. package/_i18n/i18n_de.properties +3 -0
  5. package/_i18n/i18n_en.properties +3 -0
  6. package/_i18n/i18n_es.properties +3 -0
  7. package/_i18n/i18n_fr.properties +3 -0
  8. package/_i18n/i18n_it.properties +3 -0
  9. package/_i18n/i18n_ja.properties +3 -0
  10. package/_i18n/i18n_pl.properties +7 -4
  11. package/_i18n/i18n_pt.properties +3 -0
  12. package/_i18n/i18n_ru.properties +3 -0
  13. package/app/index.js +2 -30
  14. package/lib/compile/parse.js +1 -1
  15. package/lib/env/cds-env.js +1 -1
  16. package/lib/env/cds-requires.js +16 -9
  17. package/lib/env/schemas/cds-package.js +1 -1
  18. package/lib/env/schemas/cds-rc.js +17 -4
  19. package/lib/index.js +1 -1
  20. package/lib/ql/SELECT.js +6 -1
  21. package/lib/ql/cds.ql-predicates.js +2 -1
  22. package/lib/req/request.js +5 -2
  23. package/lib/req/validate.js +4 -2
  24. package/lib/srv/bindings.js +31 -20
  25. package/lib/srv/cds-connect.js +1 -1
  26. package/lib/srv/middlewares/auth/mocked-users.js +1 -0
  27. package/lib/srv/protocols/okra.js +5 -7
  28. package/lib/srv/srv-dispatch.js +0 -5
  29. package/lib/test/cds-test.js +34 -6
  30. package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +1 -0
  31. package/libx/_runtime/common/generic/auth/restrict.js +1 -1
  32. package/libx/_runtime/common/generic/auth/service.js +2 -2
  33. package/libx/_runtime/common/generic/auth/utils.js +2 -1
  34. package/libx/_runtime/common/generic/input.js +1 -1
  35. package/libx/_runtime/common/utils/binary.js +1 -35
  36. package/libx/_runtime/common/utils/rewriteAsterisks.js +5 -8
  37. package/libx/_runtime/fiori/lean-draft.js +2 -4
  38. package/libx/common/utils/path.js +1 -5
  39. package/libx/common/utils/streaming.js +76 -0
  40. package/libx/odata/middleware/create.js +5 -1
  41. package/libx/odata/middleware/delete.js +1 -1
  42. package/libx/odata/middleware/operation.js +48 -4
  43. package/libx/odata/middleware/read.js +1 -1
  44. package/libx/odata/middleware/stream.js +29 -101
  45. package/libx/odata/middleware/update.js +1 -1
  46. package/libx/odata/parse/afterburner.js +21 -1
  47. package/libx/odata/parse/grammar.peggy +108 -26
  48. package/libx/odata/parse/multipartToJson.js +17 -10
  49. package/libx/odata/parse/parser.js +1 -1
  50. package/libx/odata/utils/metadata.js +28 -5
  51. package/libx/odata/utils/normalizeTimeData.js +11 -8
  52. package/libx/rest/RestAdapter.js +2 -16
  53. package/libx/rest/middleware/operation.js +38 -18
  54. package/libx/rest/middleware/parse.js +5 -25
  55. package/libx/rest/post-processing.js +33 -0
  56. package/libx/rest/pre-processing.js +38 -0
  57. package/package.json +1 -1
  58. 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
- if (current.params && cur in current.params) acc[cur] = from._params[cur]
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
- if (apply.topLevels) cqn.__topLevels = apply.topLevels
207
- if (apply.ancestors) cqn.__ancestors = apply.ancestors
208
- if (apply.descendants) cqn.__descendants = apply.descendants
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
- aliasedParamEqualsVal /
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
- // normalize value
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
- aliasedParamEqualsVal = alias:aliasedParam "=" !aliasedParam value:aliasedParamVal {
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
- let lastIndex = 0
127
- changed = changed.replaceAll(/(\r\n){2,}(.+)[\r\n]+HEAD/g, (match, _, p1, index, original) => {
128
- const part = original.substring(lastIndex, index)
129
- lastIndex = index
130
- return part.match(/(PUT|POST|PATCH)\s\//g) && !part.match(/content-length/i) && !p1.startsWith('HEAD /')
131
- ? `${CRLF}content-length: ${Buffer.byteLength(p1)}${match}`
132
- : match
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.message === 'Parse Error') {
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
  }