@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
@@ -1,10 +1,10 @@
1
- const cds = require('../_runtime/cds')
1
+ const cds = require('../../../')
2
2
 
3
- const { where2obj, resolveFromSelect } = require('../_runtime/common/utils/cqn')
4
- const { findCsnTargetFor } = require('../_runtime/common/utils/csn')
5
- const normalizeTimestamp = require('../_runtime/common/utils/normalizeTimestamp')
6
- const { rewriteExpandAsterisk } = require('../_runtime/common/utils/rewriteAsterisks')
7
- const resolveStructured = require('../_runtime/common/utils/resolveStructured')
3
+ const { where2obj, resolveFromSelect } = require('../../_runtime/common/utils/cqn')
4
+ const { findCsnTargetFor } = require('../../_runtime/common/utils/csn')
5
+ const normalizeTimestamp = require('../../_runtime/common/utils/normalizeTimestamp')
6
+ const { rewriteExpandAsterisk } = require('../../_runtime/common/utils/rewriteAsterisks')
7
+ const resolveStructured = require('../../_runtime/common/utils/resolveStructured')
8
8
 
9
9
  const _addKeysDeep = (keys, keysCollector, ignoreManagedBacklinks) => {
10
10
  for (const keyName in keys) {
@@ -165,28 +165,28 @@ function _processWhere(where, entity) {
165
165
 
166
166
  if (element) {
167
167
  i += 2
168
- where[valIndex].val = _convertVal(element, where[valIndex].val)
168
+ where[valIndex].val = _convertVal(where[valIndex].val, element)
169
169
  }
170
170
  }
171
171
  }
172
172
 
173
- function _convertVal(element, value) {
173
+ function _convertVal(value, element) {
174
174
  if (value === null) return value
175
175
  switch (element._type) {
176
+ // numbers
176
177
  case 'cds.UInt8':
177
178
  case 'cds.Integer':
178
179
  case 'cds.Int16':
179
180
  case 'cds.Int32':
180
- if (!/^-?\+?\d+$/.test(value)) throw new Error('Not a valid integer')
181
+ if (!/^-?\+?\d+$/.test(value))
182
+ throw Object.assign(new Error(`${element.name} does not contain a valid Integer`), { statusCode: 400 })
181
183
  // eslint-disable-next-line no-case-declarations
182
184
  const n = Number(value)
183
- if (!Number.isSafeInteger(n)) throw new Error('Not a valid integer')
184
- if (element._type === 'cds.UInt8' && n < 0) throw new Error('Not a positive integer')
185
+ if (!Number.isSafeInteger(n))
186
+ throw Object.assign(new Error(`${element.name} does not contain a valid Integer`), { statusCode: 400 })
187
+ if (element._type === 'cds.UInt8' && n < 0)
188
+ throw Object.assign(new Error(`${element.name} does not contain a valid positive Integer`), { statusCode: 400 })
185
189
  return n
186
-
187
- case 'cds.String':
188
- case 'cds.LargeString':
189
- return String(value)
190
190
  case 'cds.Double':
191
191
  return parseFloat(value)
192
192
  case 'cds.Decimal':
@@ -195,18 +195,44 @@ function _convertVal(element, value) {
195
195
  case 'cds.Integer64':
196
196
  if (typeof value === 'string') return value
197
197
  return String(value)
198
+ // others
199
+ case 'cds.String':
200
+ case 'cds.LargeString':
201
+ return String(value)
198
202
  case 'cds.Boolean':
199
203
  return typeof value === 'string' ? value === 'true' : value
200
-
201
204
  case 'cds.Timestamp':
202
205
  return normalizeTimestamp(value)
203
-
204
206
  default:
205
207
  return value
206
208
  }
207
209
  }
208
210
 
209
- function _processSegments(from, model, namespace, cqn) {
211
+ const getStructRef = (element, ref = []) => {
212
+ if (element.kind === 'element') {
213
+ if (element.parent.kind === 'element') {
214
+ getStructRef(element.parent, ref)
215
+ ref.push(element.name)
216
+ }
217
+ if (element.parent.kind === 'entity') {
218
+ ref.push(element.name)
219
+ }
220
+ }
221
+ return ref
222
+ }
223
+
224
+ const getStructTargetName = element => {
225
+ if (element.kind === 'element') {
226
+ if (element.parent.kind === 'element') {
227
+ return getStructTargetName(element.parent)
228
+ }
229
+ if (element.elements && element.parent.kind === 'entity') {
230
+ return element.parent.name
231
+ }
232
+ }
233
+ }
234
+
235
+ function _processSegments(from, model, namespace, cqn, protocol) {
210
236
  const { ref } = from
211
237
 
212
238
  let current = model
@@ -247,7 +273,7 @@ function _processSegments(from, model, namespace, cqn) {
247
273
 
248
274
  if (incompleteKeys) {
249
275
  // > key
250
- keys = keys || _keysOf(current, cds.env.features.odata_new_parser) // if odata, skip backlinks as key as they are used from structure
276
+ keys = keys || _keysOf(current, protocol !== 'rest') // if odata, skip backlinks as key as they are used from structure
251
277
  let key = keys[keyCount++]
252
278
  one = true
253
279
  const element = current.elements[key]
@@ -262,7 +288,7 @@ function _processSegments(from, model, namespace, cqn) {
262
288
  .join(',')})`
263
289
  base.where.push({ ref: [key] }, '=', { val })
264
290
  } else {
265
- const val = _convertVal(element, seg)
291
+ const val = _convertVal(seg, element)
266
292
  base.where.push({ ref: [key] }, '=', { val })
267
293
  }
268
294
  ref[i] = null
@@ -306,7 +332,10 @@ function _processSegments(from, model, namespace, cqn) {
306
332
  ref[i].where = undefined
307
333
  if (ref[i + 1] !== 'Set') {
308
334
  // /Set is missing
309
- throw cds.error(`Invalid call to "${current.name}". You need to navigate to Set`, { status: 400, code: 400 })
335
+ throw cds.error(`Invalid call to "${current.name}". You need to navigate to Set`, {
336
+ statusCode: 400,
337
+ code: 400
338
+ })
310
339
  }
311
340
  ref[++i] = null
312
341
  } else if (current.kind === 'entity') {
@@ -352,13 +381,32 @@ function _processSegments(from, model, namespace, cqn) {
352
381
  _resolveAliasesInXpr(ref[i].where, current)
353
382
  _processWhere(ref[i].where, current)
354
383
  }
384
+ } else if (current.kind === 'element' && current.elements && i < ref.length - 1) {
385
+ // > structured
386
+ continue
355
387
  } else {
356
388
  // > property
357
389
  // we do not support navigations from properties yet
358
390
  one = true
359
391
  // if the last segment is a property, it must be removed and pushed to columns
360
392
  target = target || _getDefinition(model, ref[0].id, namespace)
361
- if (Object.keys(target.elements).includes(current.name)) {
393
+ if (getStructTargetName(current) === target.name) {
394
+ // TODO add simple isStructured check before
395
+ if (!cqn.SELECT.columns) cqn.SELECT.columns = []
396
+ const ref = getStructRef(current)
397
+ cqn.SELECT.columns.push({ ref }) // store struct as ref
398
+ // we need the keys to generate the correct @odata.context
399
+ for (const key in target.keys || {}) {
400
+ if (key !== 'IsActiveEntity' && !cqn.SELECT.columns.some(c => c.ref?.[0] === key))
401
+ cqn.SELECT.columns.push({ ref: [key] })
402
+ }
403
+ Object.defineProperty(cqn, '_propertyAccess', { value: current.name, enumerable: false })
404
+ // if we end up with structured, keep path as is, if we end up with property in structured, cut off property
405
+ if (!current.elements) {
406
+ from.ref.splice(-1)
407
+ }
408
+ break
409
+ } else if (Object.keys(target.elements).includes(current.name)) {
362
410
  if (!cqn.SELECT.columns) cqn.SELECT.columns = []
363
411
  cqn.SELECT.columns.push({ ref: ref.slice(i) })
364
412
  // we need the keys to generate the correct @odata.context
@@ -366,6 +414,7 @@ function _processSegments(from, model, namespace, cqn) {
366
414
  if (key !== 'IsActiveEntity' && !cqn.SELECT.columns.some(c => c.ref?.[0] === key))
367
415
  cqn.SELECT.columns.push({ ref: [key] })
368
416
  }
417
+ // REVISIT: remove hacky _propertyAccess
369
418
  Object.defineProperty(cqn, '_propertyAccess', { value: current.name, enumerable: false })
370
419
  from.ref.splice(i)
371
420
  break
@@ -382,7 +431,7 @@ function _processSegments(from, model, namespace, cqn) {
382
431
  keyCount === 1 ? 'was' : 'were'
383
432
  } provided.`
384
433
  ),
385
- { status: 400 }
434
+ { statusCode: 400 }
386
435
  )
387
436
  }
388
437
 
@@ -430,7 +479,7 @@ function _removeDuplicateAsterisk(columns) {
430
479
  }
431
480
  }
432
481
 
433
- function _processColumns(cqn, target) {
482
+ function _processColumns(cqn, target, protocol) {
434
483
  if (cqn.SELECT.from.SELECT) _processColumns(cqn.SELECT.from, target)
435
484
 
436
485
  let columns = cqn.SELECT.columns
@@ -445,7 +494,7 @@ function _processColumns(cqn, target) {
445
494
  _removeDuplicateAsterisk(columns)
446
495
 
447
496
  rewriteExpandAsterisk(columns, entity)
448
- if (cds.env.features.odata_new_parser) _addKeys(columns, entity)
497
+ if (protocol !== 'rest') _addKeys(columns, entity)
449
498
  }
450
499
 
451
500
  if (!Array.isArray(columns)) return
@@ -481,7 +530,7 @@ const _checkAllKeysProvided = (params, entity) => {
481
530
  if (isView) {
482
531
  // view with params
483
532
  if (params === undefined) {
484
- throw cds.error(`Invalid call to "${entity.name}". You need to navigate to Set`, { status: 400, code: 400 })
533
+ throw cds.error(`Invalid call to "${entity.name}". You need to navigate to Set`, { statusCode: 400, code: 400 })
485
534
  } else if (Object.keys(params).length === 0) {
486
535
  throw new Error('KEY_EXPECTED')
487
536
  }
@@ -505,7 +554,7 @@ const _checkAllKeysProvided = (params, entity) => {
505
554
  entity.name
506
555
  }"`
507
556
  ),
508
- { status: 400 }
557
+ { statusCode: 400 }
509
558
  )
510
559
  }
511
560
  }
@@ -548,6 +597,7 @@ function _cleanupIgnored(SELECT, ignoredColumns, target, isOne) {
548
597
 
549
598
  function _4service(service) {
550
599
  const { namespace, model } = service
600
+ const protocol = service.options?.to
551
601
  if (!model) return cqn => cqn
552
602
 
553
603
  return cqn => {
@@ -575,7 +625,7 @@ function _4service(service) {
575
625
  /*
576
626
  * key vs. path segments (/Books/1/author/books/2/...) and more
577
627
  */
578
- const { one, current, target } = _processSegments(from, model, namespace, cqn)
628
+ const { one, current, target } = _processSegments(from, model, namespace, cqn, protocol)
579
629
 
580
630
  if (cqn.SELECT.where) {
581
631
  _processWhere(cqn.SELECT.where, root)
@@ -594,7 +644,7 @@ function _4service(service) {
594
644
  /*
595
645
  * add default aggregation function (and alias)
596
646
  */
597
- _processColumns(cqn, current)
647
+ _processColumns(cqn, current, protocol)
598
648
 
599
649
  const ignoredColumns = Object.values(root.elements ?? {})
600
650
  .filter(element => element['@cds.api.ignore'])
@@ -1,5 +1,6 @@
1
- const { formatVal } = require('./utils')
2
- const cds = require('../_runtime/cds')
1
+ const cds = require('../../../')
2
+
3
+ const { formatVal } = require('../utils')
3
4
 
4
5
  const OPERATORS = {
5
6
  '=': 'eq',
@@ -119,7 +120,8 @@ const _format = (cur, elementName, target, kind, isLambda, func) => {
119
120
  if (typeof cur !== 'object') return encodeURIComponent(formatVal(cur, elementName, target, kind))
120
121
  if (hasValidProps(cur, 'ref'))
121
122
  return encodeURIComponent(isLambda ? [LAMBDA_VARIABLE, ...cur.ref].join('/') : cur.ref[0].id || cur.ref.join('/'))
122
- if (hasValidProps(cur, 'val')) return encodeURIComponent(formatVal(cur.val, elementName, target, kind, func, cur.literal))
123
+ if (hasValidProps(cur, 'val'))
124
+ return encodeURIComponent(formatVal(cur.val, elementName, target, kind, func, cur.literal))
123
125
  if (hasValidProps(cur, 'xpr')) return `(${_xpr(cur.xpr, target, kind, isLambda)})`
124
126
  // REVISIT: How to detect the types for all functions?
125
127
  if (hasValidProps(cur, 'func')) {
@@ -1,12 +1,10 @@
1
- const cds = require('../../lib')
1
+ const cds = require('../../../')
2
2
 
3
3
  module.exports = (component, service, target, data, odataReq, upsert) => {
4
4
  let query = cds.odata.parse(odataReq, { service })
5
5
 
6
- //For concat
7
- if (component === 'READ' && Array.isArray(query)) {
8
- return query
9
- }
6
+ // for concat
7
+ if (component === 'READ' && Array.isArray(query)) return query
10
8
 
11
9
  const _target = query.SELECT && query.SELECT.from
12
10
 
@@ -22,7 +20,6 @@ module.exports = (component, service, target, data, odataReq, upsert) => {
22
20
  return INSERT.into(_target).entries(data)
23
21
  case 'DELETE':
24
22
  if (!one) cds.error('DELETE not allowed on collection', { code: 400 })
25
-
26
23
  // eslint-disable-next-line no-case-declarations
27
24
  const last = query._propertyAccess || (_target.ref && _target.ref[_target.ref.length - 1])
28
25
  if (target.elements[last] || target.elements[query._propertyAccess]) {
@@ -1,7 +1,43 @@
1
- const { toBase64url } = require('../_runtime/common/utils/binary')
2
- const cds = require('../_runtime/cds')
1
+ // TODO: split into multiple files
3
2
 
4
- const MATH_FUNC = { round: 1, floor: 1, ceiling: 1 }
3
+ const cds = require('../../../')
4
+
5
+ const { toBase64url } = require('../../_runtime/common/utils/binary')
6
+ const { where2obj } = require('../../_runtime/common/utils/cqn')
7
+
8
+ // copied from cds-compiler/lib/edm/edmUtils.js
9
+ const cds2edm = {
10
+ 'cds.String': 'Edm.String',
11
+ // 'cds.hana.NCHAR': 'Edm.String',
12
+ 'cds.LargeString': 'Edm.String',
13
+ // 'cds.hana.VARCHAR': 'Edm.String',
14
+ // 'cds.hana.CHAR': 'Edm.String',
15
+ // 'cds.hana.CLOB': 'Edm.String',
16
+ 'cds.Binary': 'Edm.Binary',
17
+ // 'cds.hana.BINARY': 'Edm.Binary',
18
+ 'cds.LargeBinary': 'Edm.Binary',
19
+ 'cds.Decimal': 'Edm.Decimal',
20
+ 'cds.DecimalFloat': 'Edm.Decimal',
21
+ // 'cds.hana.SMALLDECIMAL': 'Edm.Decimal', // V4: Scale="floating" Precision="16"
22
+ 'cds.Integer64': 'Edm.Int64',
23
+ 'cds.Integer': 'Edm.Int32',
24
+ 'cds.Int64': 'Edm.Int64',
25
+ 'cds.Int32': 'Edm.Int32',
26
+ 'cds.Int16': 'Edm.Int16',
27
+ 'cds.UInt8': 'Edm.Byte',
28
+ // 'cds.hana.SMALLINT': 'Edm.Int16',
29
+ // 'cds.hana.TINYINT': 'Edm.Byte',
30
+ 'cds.Double': 'Edm.Double',
31
+ // 'cds.hana.REAL': 'Edm.Single',
32
+ 'cds.Date': 'Edm.Date',
33
+ 'cds.Time': 'Edm.TimeOfDay',
34
+ 'cds.DateTime': 'Edm.DateTimeOffset',
35
+ 'cds.Timestamp': 'Edm.DateTimeOffset',
36
+ 'cds.Boolean': 'Edm.Boolean',
37
+ 'cds.UUID': 'Edm.Guid'
38
+ // 'cds.hana.ST_POINT': 'Edm.GeometryPoint',
39
+ // 'cds.hana.ST_GEOMETRY': 'Edm.Geometry',
40
+ }
5
41
 
6
42
  const odataError = (code, message) => ({ error: { code, message } })
7
43
 
@@ -26,11 +62,6 @@ const getSafeNumber = inputString => {
26
62
  return inputString
27
63
  }
28
64
 
29
- const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
30
- const _PT = ([hh, mm, ss]) => `PT${hh}H${mm}M${ss}S`
31
- const _isTimestamp = val =>
32
- /^\d+-\d\d-\d\d(T\d\d:\d\d(:\d\d(\.\d+)?)?(Z|([+-]{1}\d\d:\d\d))?)?$/.test(val) && !isNaN(Date.parse(val))
33
-
34
65
  const _getElement = (csnTarget, key) => {
35
66
  if (csnTarget) {
36
67
  if (csnTarget.elements) {
@@ -68,6 +99,8 @@ const _getElement = (csnTarget, key) => {
68
99
  }
69
100
  }
70
101
 
102
+ const _PT = ([hh, mm, ss]) => `PT${hh}H${mm}M${ss}S`
103
+
71
104
  const _v2 = (val, element) => {
72
105
  switch (element.type) {
73
106
  case 'cds.UUID':
@@ -155,6 +188,12 @@ const _v4 = (val, element) => {
155
188
  }
156
189
  }
157
190
 
191
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
192
+ const MATH_FUNC = { round: 1, floor: 1, ceiling: 1 }
193
+
194
+ const _isTimestamp = val =>
195
+ /^\d+-\d\d-\d\d(T\d\d:\d\d(:\d\d(\.\d+)?)?(Z|([+-]{1}\d\d:\d\d))?)?$/.test(val) && !isNaN(Date.parse(val))
196
+
158
197
  const formatVal = (val, elementName, csnTarget, kind, func, literal) => {
159
198
  if (val === null || val === 'null') return 'null'
160
199
  if (typeof val === 'boolean') return val
@@ -230,9 +269,56 @@ const skipToken = (token, cqn) => {
230
269
  }
231
270
  }
232
271
 
272
+ // REVISIT: do we already have something like this _without using okra api_?
273
+ const getKeysFromPath = (from, srv) => {
274
+ const keys = {}
275
+ // own
276
+ if (from.ref[from.ref.length - 1].where) {
277
+ Object.assign(keys, where2obj(from.ref[from.ref.length - 1].where))
278
+ }
279
+ // previous path segments
280
+ if (from.ref.length > 1) {
281
+ const entities = []
282
+ let cur = srv.model.definitions
283
+ for (let i = 0; i < from.ref.length; i++) {
284
+ const id = from.ref[i].id || from.ref[i]
285
+ const t = cur[id]._target || cur[id]
286
+ entities.push(t)
287
+ cur = t.elements
288
+ }
289
+ for (let i = from.ref.length - 2; i >= 0; i--) {
290
+ const ref = from.ref[i]
291
+ if (ref.where) {
292
+ const relation = entities[i]._relations[from.ref[i + 1].id || from.ref[i + 1]].join('target', 'source')
293
+ const seg_keys = where2obj(ref.where)
294
+ if (relation?.[0].xpr) {
295
+ const join = [...relation[0].xpr]
296
+ while (join.length >= 3) {
297
+ const [left, _, right] = join.splice(0, 4)
298
+ if (left.ref?.[0] === 'target') {
299
+ if (left.ref[1] in keys) break // we already added the foreign key for the last segment
300
+ keys[left.ref[1]] = 'val' in right ? right.val : seg_keys[right.ref[1]]
301
+ } else if (right.ref?.[0] === 'target') {
302
+ if (right.ref[1] in keys) break // we already added the foreign key for the last segment
303
+ keys[right.ref[1]] = 'val' in left ? left.val : seg_keys[left.ref[1]]
304
+ } else {
305
+ // REVISIT: what to do here?
306
+ }
307
+ }
308
+ } else {
309
+ // REVISIT: what to do here?
310
+ }
311
+ }
312
+ }
313
+ }
314
+ return keys
315
+ }
316
+
233
317
  module.exports = {
318
+ cds2edm,
234
319
  getSafeNumber,
235
320
  formatVal,
236
321
  skipToken,
237
- odataError
322
+ odataError,
323
+ getKeysFromPath
238
324
  }
@@ -31,7 +31,7 @@ const _getMessagesEntity = () => {
31
31
  // REVISIT: Is this always a reliable way to identify the provider tenant?
32
32
  // Are there scenarios where the credentials have a different format?
33
33
  const _isProviderTenant = tenant =>
34
- cds.requires.auth && cds.requires.auth.credentials && cds.requires.auth.credentials.identityzoneid === tenant
34
+ cds.requires.auth && cds.requires.auth.credentials && cds.requires.auth.credentials.identityzoneid === tenant || cds.requires.multitenancy.t0 === tenant
35
35
 
36
36
  const hasPersistentOutbox = tenant => {
37
37
  if (!cds.requires.outbox || cds.requires.outbox.kind !== 'persistent-outbox') return false
@@ -275,6 +275,7 @@ function outboxed(srv, customOpts) {
275
275
  await writeInOutbox(srv.name, req, context)
276
276
  return
277
277
  }
278
+
278
279
  if (!context[$stored_reqs]) {
279
280
  context[$stored_reqs] = []
280
281
  context.on('succeeded', async () => {
@@ -135,7 +135,6 @@ const RestAdapter = function (srv) {
135
135
  case 'PATCH':
136
136
  ;({ result, status } = await update(req))
137
137
  break
138
-
139
138
  case 'DELETE':
140
139
  ;({ result, status } = await deleet(req))
141
140
  break
@@ -2,9 +2,11 @@ const cds = require('../../_runtime/cds')
2
2
 
3
3
  const RestRequest = require('../RestRequest')
4
4
 
5
- const { checkStatic, CDS_TYPE_CHECKS } = require('../../_runtime/cds-services/util/assert')
5
+ const { checkStatic } = require('../../_runtime/cds-services/util/assert')
6
6
  const getError = require('../../_runtime/common/error')
7
7
 
8
+ const typeCheckers = require('../../common/assert/type')
9
+
8
10
  // REVISIT: use i18n
9
11
  const _enrichErrorDetails = (isPrimitive, error) => {
10
12
  const element = error.target ? ` '${error.target}' ` : ' '
@@ -38,10 +40,10 @@ const _validateReturnType = (operation, data) => {
38
40
  // Return type contains primitives
39
41
  // eslint-disable-next-line no-proto
40
42
  const _type = typeof returnType._type === 'object' ? returnType.__proto__._type : returnType._type // REVISIT: super dirty hack for compiler's to.edmx polluting the csn definitions with ._type -> please use Symbols instead
41
- const check = CDS_TYPE_CHECKS[_type] // IMPORTANT: use ._type
42
- if (check) {
43
+ const typeChecker = typeCheckers[_type] // IMPORTANT: use ._type
44
+ if (typeChecker) {
43
45
  const array = Array.isArray(data) ? data : [data]
44
- checkResult = array.filter(value => !check(value)).map(value => ({ type: _type, value }))
46
+ checkResult = array.filter(value => !typeChecker(value)).map(value => ({ type: _type, value }))
45
47
  } else {
46
48
  if (typeof data !== 'object') {
47
49
  throw new Error(
@@ -20,7 +20,11 @@ module.exports = srv => (req, res, next) => {
20
20
  __target: definition,
21
21
  SELECT: { one }
22
22
  } = query
23
- if (typeof definition === 'string') definition = srv.model.definitions[definition] || srv.model.definitions[definition.split(':$:')[0]].actions[definition.split(':$:')[1]]
23
+ if (typeof definition === 'string') {
24
+ definition =
25
+ srv.model.definitions[definition] ||
26
+ srv.model.definitions[definition.split(':$:')[0]].actions[definition.split(':$:')[1]]
27
+ }
24
28
  delete query.__target
25
29
 
26
30
  // REVISIT: hack for actions and functions
@@ -101,8 +105,22 @@ module.exports = srv => (req, res, next) => {
101
105
  if (operation && (operation.kind === 'action' || operation.kind === 'function') && !operation.params) {
102
106
  req._data = {}
103
107
  } else {
108
+ // TODO: add keys from url into payload (overwriting if already present) -> document this behavior, also for OData
104
109
  const payload = deepCopy(args || req.body)
105
- convertStructured(srv, operation || definition, payload, { cleanupStruct: cds.env.features.rest_struct_data })
110
+ if (cds.env.features.cds_assert) {
111
+ const assertOptions = {
112
+ filter: true,
113
+ http: { req },
114
+ mandatories: req.method === 'POST' || req.method === 'PUT' || undefined
115
+ }
116
+ const errs = cds.assert(payload, definition, assertOptions)
117
+ if (errs) {
118
+ if (errs.length === 1) throw errs[0]
119
+ throw Object.assign(new Error('MULTIPLE_ERRORS'), { statusCode: 400, details: errs })
120
+ }
121
+ } else {
122
+ convertStructured(srv, operation || definition, payload, { cleanupStruct: cds.env.features.rest_struct_data })
123
+ }
106
124
  req._data = payload
107
125
  }
108
126
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sap/cds",
3
- "version": "7.6.4",
3
+ "version": "7.7.1",
4
4
  "description": "SAP Cloud Application Programming Model - CDS for Node.js",
5
5
  "homepage": "https://cap.cloud.sap/",
6
6
  "keywords": [