@sap/cds 7.6.3 → 7.7.0

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 (94) hide show
  1. package/CHANGELOG.md +38 -1
  2. package/_i18n/i18n.properties +3 -0
  3. package/app/index.js +18 -12
  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 +1 -1
  19. package/lib/plugins.js +2 -2
  20. package/lib/ql/Query.js +1 -1
  21. package/lib/ql/SELECT.js +8 -8
  22. package/lib/req/context.js +22 -13
  23. package/lib/req/request.js +10 -4
  24. package/lib/srv/cds-connect.js +9 -3
  25. package/lib/srv/cds-serve.js +5 -3
  26. package/lib/srv/middlewares/ctx-model.js +1 -1
  27. package/lib/srv/protocols/odata-v4.js +38 -9
  28. package/lib/srv/srv-api.js +98 -140
  29. package/lib/srv/srv-models.js +2 -2
  30. package/lib/srv/srv-tx.js +1 -0
  31. package/lib/utils/cds-utils.js +32 -23
  32. package/lib/utils/data.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/http/HttpHeaderReader.js +4 -2
  40. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/index.js +5 -0
  41. package/libx/_runtime/cds-services/adapter/odata-v4/utils/data.js +71 -25
  42. package/libx/_runtime/cds-services/adapter/odata-v4/utils/metaInfo.js +10 -2
  43. package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +6 -1
  44. package/libx/_runtime/cds-services/util/assert.js +50 -240
  45. package/libx/_runtime/cds.js +5 -0
  46. package/libx/_runtime/common/aspects/any.js +53 -45
  47. package/libx/_runtime/common/generic/input.js +14 -10
  48. package/libx/_runtime/common/generic/paging.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/utils/columns.js +6 -1
  58. package/libx/_runtime/fiori/generic/activate.js +11 -3
  59. package/libx/_runtime/fiori/generic/edit.js +8 -2
  60. package/libx/_runtime/fiori/lean-draft.js +99 -30
  61. package/libx/_runtime/hana/execute.js +2 -5
  62. package/libx/_runtime/messaging/service.js +6 -2
  63. package/libx/common/assert/index.js +232 -0
  64. package/libx/common/assert/type.js +109 -0
  65. package/libx/common/assert/utils.js +125 -0
  66. package/libx/common/assert/validation.js +109 -0
  67. package/libx/odata/index.js +5 -5
  68. package/libx/odata/middleware/create.js +83 -0
  69. package/libx/odata/middleware/delete.js +38 -0
  70. package/libx/odata/middleware/error.js +8 -0
  71. package/libx/odata/{metadata.js → middleware/metadata.js} +8 -6
  72. package/libx/odata/middleware/operation.js +78 -0
  73. package/libx/odata/middleware/parse.js +11 -0
  74. package/libx/odata/{read.js → middleware/read.js} +42 -20
  75. package/libx/odata/{service-document.js → middleware/service-document.js} +2 -1
  76. package/libx/odata/middleware/stream.js +237 -0
  77. package/libx/odata/middleware/update.js +165 -0
  78. package/libx/odata/{afterburner.js → parse/afterburner.js} +79 -29
  79. package/libx/odata/{cqn2odata.js → parse/cqn2odata.js} +5 -3
  80. package/libx/odata/{parseToCqn.js → parse/parseToCqn.js} +3 -6
  81. package/libx/odata/{utils.js → utils/index.js} +91 -9
  82. package/libx/outbox/index.js +5 -4
  83. package/libx/rest/RestAdapter.js +0 -1
  84. package/libx/rest/middleware/operation.js +6 -4
  85. package/libx/rest/middleware/parse.js +20 -2
  86. package/package.json +1 -1
  87. package/server.js +43 -71
  88. package/libx/odata/create.js +0 -44
  89. package/libx/odata/delete.js +0 -25
  90. package/libx/odata/error.js +0 -12
  91. package/libx/odata/update.js +0 -110
  92. /package/libx/odata/{grammar.peggy → parse/grammar.peggy} +0 -0
  93. /package/libx/odata/{parser.js → parse/parser.js} +0 -0
  94. /package/libx/odata/{result.js → utils/result.js} +0 -0
@@ -2,7 +2,8 @@ const cds = require('../../cds')
2
2
  const { ensureNoDraftsSuffix, ensureUnlocalized } = require('../../fiori/utils/handler')
3
3
  const { isDuplicate } = require('./rewriteAsterisks')
4
4
 
5
- const _addColumn = (name, type, columns) => {
5
+ const _addColumn = (name, type, columns, url) => {
6
+ if (cds.env.features.odata_new_adapter) return
6
7
  if (typeof type === 'object') {
7
8
  let mType = type['='].replaceAll(/\./g, '_')
8
9
  const ref = {
@@ -14,13 +15,22 @@ const _addColumn = (name, type, columns) => {
14
15
  const val = { val: type, as: `${name}@odata.mediaContentType` }
15
16
  if (!columns.find(isDuplicate(val))) columns.push(val)
16
17
  }
18
+
19
+ if (url) {
20
+ const ref = {
21
+ ref: [name],
22
+ as: `${name}@odata.mediaReadLink`
23
+ }
24
+ if (!columns.find(isDuplicate(ref))) columns.push(ref)
25
+ }
17
26
  }
18
27
 
19
28
  const _addColumns = (target, columns) => {
29
+ if (cds.env.features.odata_new_adapter) return
20
30
  for (const k in target.elements) {
21
31
  const el = target.elements[k]
22
32
  if (el['@Core.MediaType']) {
23
- _addColumn(el.name, el['@Core.MediaType'], columns)
33
+ _addColumn(el.name, el['@Core.MediaType'], columns, el['@Core.IsURL'])
24
34
  }
25
35
  }
26
36
  }
@@ -38,11 +48,11 @@ const handleStreamProperties = (target, columns, model) => {
38
48
 
39
49
  if (col === '*') {
40
50
  _addColumns(target, columns)
41
- } else if (col.ref && type === 'cds.LargeBinary') {
51
+ } else if (col.ref && (type === 'cds.LargeBinary' || mediaType)) {
42
52
  if (mediaType) {
43
- _addColumn(name, mediaType, columns)
44
- }
45
- if ((mediaType || !cds.env.features.stream_compat) && !element['@Core.IsURL']) {
53
+ _addColumn(name, mediaType, columns, element['@Core.IsURL'])
54
+ columns.splice(index, 1)
55
+ } else if (!cds.env.features.stream_compat) {
46
56
  columns.splice(index, 1)
47
57
  }
48
58
  } else if (col.expand && col.ref) {
@@ -100,6 +100,7 @@ const _cleanup = (row, definition, cleanupNull, cleanupStruct, errors, prefix =
100
100
  }
101
101
  }
102
102
 
103
+ // REVISIT: when needed?
103
104
  function convertStructured(service, definition, data, { cleanupNull = false, cleanupStruct = false, errors } = {}) {
104
105
  if (!definition) return
105
106
  // REVISIT check `structs` mode only for now as uCSN is not yet available
@@ -3,6 +3,10 @@ const resolveStructured = require('../../common/utils/resolveStructured')
3
3
 
4
4
  const { DRAFT_COLUMNS_MAP } = require('../../common/constants/draft')
5
5
 
6
+ const _isStreamProperty = element => {
7
+ return element.type === 'cds.LargeBinary' || element['@Core.IsURL']
8
+ }
9
+
6
10
  /**
7
11
  * This method gets all columns for an entity.
8
12
  * It includes the generated foreign keys from managed associations, structured elements and complex and custom types.
@@ -11,7 +15,7 @@ const { DRAFT_COLUMNS_MAP } = require('../../common/constants/draft')
11
15
  * @param entity - the csn entity
12
16
  * @returns {Array} - array of columns
13
17
  */
14
- const getColumns = (entity, { _4db, onlyKeys } = { _4db: true, onlyKeys: false }) => {
18
+ const getColumns = (entity, { _4db, onlyKeys, omitStream } = { _4db: true, onlyKeys: false, omitStream: false }) => {
15
19
  // REVISIT is this correct or just a problem that occurs because of new structure we do not deal with yet?
16
20
  if (!(entity && entity.elements)) return []
17
21
  const columnNames = []
@@ -23,6 +27,7 @@ const getColumns = (entity, { _4db, onlyKeys } = { _4db: true, onlyKeys: false }
23
27
  const element = elements[elementName]
24
28
  if (element['@cds.api.ignore']) continue
25
29
  if (onlyKeys && !element.key) continue
30
+ if (omitStream && _isStreamProperty(element)) continue
26
31
  if (element.isAssociation) continue
27
32
  if (!lean_draft && _4db && entity._isDraftEnabled && elementName in DRAFT_COLUMNS_MAP) continue
28
33
  if (structs && element.elements) {
@@ -171,9 +171,17 @@ const fioriGenericActivate = async function (req, next) {
171
171
  })
172
172
  ])
173
173
 
174
- // REVISIT: we need to use okra API here because it must be set in the batched request
175
- // status code must be set in handler to allow overriding for FE V2
176
- if (event === 'CREATE') req?._?.odataRes?.setStatusCode(201, { overwrite: true })
174
+ if (event === 'CREATE') {
175
+ // REVISIT: we need to use okra API here because it must be set in the batched request
176
+ // status code must be set in handler to allow overriding for FE V2
177
+ // REVISIT: needs reworking for new adapter, especially re $batch
178
+ if (req._?.odataRes) {
179
+ req._?.odataRes?.setStatusCode(201, { overwrite: true })
180
+ } else if (req.http?.res) {
181
+ req.http.res.status(201)
182
+ }
183
+ }
184
+
177
185
  return result
178
186
  }
179
187
 
@@ -141,8 +141,14 @@ const fioriGenericEdit = async function (req, next) {
141
141
  await Promise.all(insertCQNs.map(CQN => dbtx.run(CQN)))
142
142
 
143
143
  // REVISIT: we need to use okra API here because it must be set in the batched request
144
- // status code must be set in handler to allow overriding for FE V2
145
- req?._?.odataRes?.setStatusCode(201, { overwrite: true })
144
+ // status code must be set in handler to allow overriding for FE V2
145
+ // REVISIT: needs reworking for new adapter, especially re $batch
146
+ if (req._?.odataRes) {
147
+ req._?.odataRes?.setStatusCode(201, { overwrite: true })
148
+ } else if (req.http?.res) {
149
+ req.http.res.status(201)
150
+ }
151
+
146
152
  return results[0][0]
147
153
  }
148
154
 
@@ -8,31 +8,71 @@ const original = Symbol('original')
8
8
  const DRAFT_PARAMS = Symbol('draftParams')
9
9
  const AGGREGATION_FUNCTIONS = ['sum', 'min', 'max', 'avg', 'count']
10
10
 
11
+ const calcTimeMs = timeout => {
12
+ const match = timeout.match(/^([0-9]+)(w|d|h|hrs|min)$/)
13
+ if (!match) return
14
+ const [, val, t] = match
15
+ switch (t) {
16
+ case 'w':
17
+ return val * 1000 * 3600 * 24 * 7
18
+ case 'd':
19
+ return val * 1000 * 3600 * 24
20
+ case 'h':
21
+ case 'hrs':
22
+ return val * 1000 * 3600
23
+ case 'min':
24
+ return val * 1000 * 60
25
+ default:
26
+ return val
27
+ }
28
+ }
29
+
30
+ const _config_to_ms = (config, _default) => {
31
+ const timeout = cds.env.fiori?.[config]
32
+ let timeout_ms
33
+ if (timeout === true) {
34
+ timeout_ms = calcTimeMs(_default)
35
+ } else if (typeof timeout === 'string') {
36
+ timeout_ms = calcTimeMs(timeout)
37
+ if (!timeout_ms)
38
+ throw new Error(`
39
+ ${timeout} is an invalid value for \`cds.fiori.${config}\`.
40
+ Please provide a value in format /^([0-9]+)(w|d|h|hrs|min)$/.
41
+ `)
42
+ } else {
43
+ timeout_ms = timeout
44
+ }
45
+
46
+ return timeout_ms
47
+ }
48
+
11
49
  const DEL_TIMEOUT = {
12
50
  get value() {
13
- const delTimeout = cds.env.fiori?.draft_deletion_timeout
14
- const timeout = delTimeout && delTimeout !== false && delTimeout === true ? '30d' : delTimeout
15
- let parts
16
- if (typeof timeout === 'string') {
17
- parts = timeout.match(/^([0-9]+)(d|h)$/)
18
- if (!parts && !Number(timeout)) throw new Error('Invalid value for `cds.fiori.draft_deletion_timeout`')
51
+ const timeout_ms = _config_to_ms('draft_deletion_timeout', '30d')
52
+ Object.defineProperty(DEL_TIMEOUT, 'value', { value: timeout_ms })
53
+ return timeout_ms
54
+ }
55
+ }
56
+
57
+ const LOCK_TIMEOUT = {
58
+ get value() {
59
+ let timeout_ms = _config_to_ms('draft_lock_timeout', '15min')
60
+
61
+ const deprecated = cds.env.drafts?.cancellationTimeout // in min
62
+ if (deprecated) {
63
+ // in order to still support legacy use cases for tests, e. g. 0.000001
64
+ timeout_ms = deprecated * 1000 * 60
19
65
  }
20
- const result =
21
- parts && parts.length
22
- ? parts[2] === 'd'
23
- ? Number(parts[1]) * 1000 * 3600 * 24
24
- : Number(parts[1]) * 1000 * 3600
25
- : Number(timeout) || 0
26
-
27
- Object.defineProperty(DEL_TIMEOUT, 'value', { value: result })
28
- return result
66
+
67
+ Object.defineProperty(LOCK_TIMEOUT, 'value', { value: timeout_ms })
68
+ return timeout_ms
29
69
  }
30
70
  }
31
71
 
32
72
  const reject_bypassed_draft = req => {
33
73
  const msg =
34
74
  !cds.profiles?.includes('production') &&
35
- '`cds.env.fiori.bypass_draft` must be enabled to support the directly modification of active instances.'
75
+ '`cds.env.fiori.bypass_draft` must be enabled or the entity must be annotated with `@odata.draft.bypass` to support the directly modification of active instances.'
36
76
  return req.reject(501, msg)
37
77
  }
38
78
 
@@ -116,13 +156,10 @@ const _inProcessByUserXpr = lockShiftedNow => ({
116
156
 
117
157
  const _lock = {
118
158
  get shiftedNow() {
119
- return new Date(Math.max(0, Date.now() - DRAFT_CANCEL_TIMEOUT_IN_MIN() * 60 * 1000)).toISOString()
159
+ return new Date(Math.max(0, Date.now() - LOCK_TIMEOUT.value)).toISOString()
120
160
  }
121
161
  }
122
162
 
123
- const DRAFT_CANCEL_TIMEOUT_IN_MIN = () =>
124
- (cds.env.drafts?.cancellationTimeout && Number(cds.env.drafts?.cancellationTimeout)) || 15
125
-
126
163
  const _redirectRefToDrafts = (ref, model) => {
127
164
  const [root, ...tail] = ref
128
165
  const draft = model.definitions[root.id || root].drafts
@@ -281,7 +318,7 @@ cds.ApplicationService.prototype.handle = async function (req) {
281
318
  if (req.event === 'NEW' || req.event === 'CANCEL' || req.event === 'draftPrepare') {
282
319
  if (req.event === 'draftPrepare' && draftParams.IsActiveEntity) req.reject(400)
283
320
  if (req.event === 'NEW' && req.data?.IsActiveEntity === true) {
284
- if (!cds.env.fiori.bypass_draft) return reject_bypassed_draft(req)
321
+ if (!cds.env.fiori.bypass_draft && !req.target['@odata.draft.bypass']) return reject_bypassed_draft(req)
285
322
  const containsDraftRoot =
286
323
  this.model.definitions[query.INSERT.into?.ref?.[0]?.id || query.INSERT.into?.ref?.[0] || query.INSERT.into][
287
324
  '@Common.DraftRoot.ActivationAction'
@@ -390,6 +427,19 @@ cds.ApplicationService.prototype.handle = async function (req) {
390
427
  const HasActiveEntity = res.HasActiveEntity
391
428
  delete res.HasActiveEntity
392
429
 
430
+ if (cds.env.features.cds_assert) {
431
+ const assertOptions = { path: [req.target.actions[req.event]['@cds.odata.bindingparameter.name'] || 'in'] }
432
+ const errs = cds.assert(res, req.target, assertOptions)
433
+ if (errs) {
434
+ if (errs.length === 1) throw Object.assign(errs[0], { '@Common.numericSeverity': 4 })
435
+ throw Object.assign(new Error('MULTIPLE_ERRORS'), {
436
+ statusCode: 400,
437
+ details: errs,
438
+ '@Common.numericSeverity': 4 //> TODO: should not be needed here
439
+ })
440
+ }
441
+ }
442
+
393
443
  // First run the handlers as they might need access to DraftAdministrativeData or the draft entities
394
444
  const result = await run(
395
445
  HasActiveEntity
@@ -402,12 +452,18 @@ cds.ApplicationService.prototype.handle = async function (req) {
402
452
  DELETE.from('DRAFT.DraftAdministrativeData').where({ DraftUUID: DraftAdministrativeData_DraftUUID })
403
453
  ])
404
454
 
405
- if (!HasActiveEntity) req?._?.odataRes?.setStatusCode(201, { overwrite: true })
455
+ if (!HasActiveEntity) {
456
+ // REVISIT: we need to use okra API here because it must be set in the batched request
457
+ // status code must be set in handler to allow overriding for FE V2
458
+ // REVISIT: needs reworking for new adapter, especially re $batch
459
+ if (req._?.odataRes) {
460
+ req._?.odataRes?.setStatusCode(201, { overwrite: true })
461
+ } else if (req.http?.res) {
462
+ req.http.res.status(201)
463
+ }
464
+ }
406
465
 
407
466
  return Object.assign(result, { IsActiveEntity: true })
408
-
409
- // REVISIT: we need to use okra API here because it must be set in the batched request
410
- // status code must be set in handler to allow overriding for FE V2
411
467
  }
412
468
 
413
469
  if (req.target.actions?.[req.event] && draftParams.IsActiveEntity === false) {
@@ -457,7 +513,7 @@ cds.ApplicationService.prototype.handle = async function (req) {
457
513
 
458
514
  LOG.debug('patch active')
459
515
 
460
- if (!cds.env.fiori.bypass_draft) return reject_bypassed_draft(req)
516
+ if (!cds.env.fiori.bypass_draft && !req.target['@odata.draft.bypass']) return reject_bypassed_draft(req)
461
517
 
462
518
  const entityRef = query.UPDATE.entity.ref
463
519
 
@@ -549,6 +605,7 @@ const Read = {
549
605
  unchanged: async function (run, query) {
550
606
  LOG.debug('List Editing Status: Unchanged')
551
607
  const draftsQuery = query._drafts
608
+ if (!draftsQuery) throw new Error('Invalid draft request')
552
609
  draftsQuery.SELECT.count = undefined
553
610
  draftsQuery.SELECT.orderBy = undefined
554
611
  draftsQuery.SELECT.limit = null
@@ -564,6 +621,8 @@ const Read = {
564
621
  ownDrafts: async function (run, query) {
565
622
  LOG.debug('List Editing Status: Own Draft')
566
623
 
624
+ if (!query._drafts) throw new Error('Invalid draft request')
625
+
567
626
  // read active from draft
568
627
  if (!query._drafts._target?.name.endsWith('.drafts')) {
569
628
  const result = await run(query._drafts)
@@ -758,6 +817,8 @@ const Read = {
758
817
 
759
818
  activesFromDrafts: async function (run, query, { isLocked = true }) {
760
819
  const draftsQuery = query._drafts
820
+ if (!draftsQuery) throw new Error('Invalid draft request')
821
+
761
822
  const additionalCols = draftsQuery.SELECT.columns
762
823
  ? draftsQuery.SELECT.columns.filter(
763
824
  c => c.ref && ['DraftAdministrativeData', 'DraftAdministrativeData_DraftUUID'].includes(c.ref[0])
@@ -1051,7 +1112,10 @@ function _cleansed(query, model) {
1051
1112
  }
1052
1113
  if (ignoredElements.has(e) && xpr[i + 2]) {
1053
1114
  let { val } = xpr[i + 2]
1054
- draftParams[x.ref.join('_')] = xpr[i + 1] === '!=' ? (typeof val === 'boolean' ? !val : 'not ' + val) : val
1115
+ const param = x.ref.join('_')
1116
+ // outer-most parameters win
1117
+ if (draftParams[param] === undefined)
1118
+ draftParams[param] = xpr[i + 1] === '!=' ? (typeof val === 'boolean' ? !val : 'not ' + val) : val
1055
1119
  i += 2
1056
1120
  const last = cleansed[cleansed.length - 1]
1057
1121
  if (last === 'and' || last === 'or') cleansed.pop()
@@ -1293,7 +1357,7 @@ async function onEdit(req) {
1293
1357
  }
1294
1358
 
1295
1359
  if (!res) req.reject(404)
1296
- const preserveChanges = req.context?.data?.PreserveChanges
1360
+ const preserveChanges = req.data?.PreserveChanges
1297
1361
  const inProcessByUser = draft?.DraftAdministrativeData?.InProcessByUser
1298
1362
 
1299
1363
  if (draft) {
@@ -1327,8 +1391,13 @@ async function onEdit(req) {
1327
1391
  await INSERT.into(targetDraft).entries(res)
1328
1392
 
1329
1393
  // REVISIT: we need to use okra API here because it must be set in the batched request
1330
- // status code must be set in handler to allow overriding for FE V2
1331
- req?._?.odataRes?.setStatusCode(201, { overwrite: true })
1394
+ // status code must be set in handler to allow overriding for FE V2
1395
+ // REVISIT: needs reworking for new adapter, especially re $batch
1396
+ if (req._?.odataRes) {
1397
+ req._?.odataRes?.setStatusCode(201, { overwrite: true })
1398
+ } else if (req.http?.res) {
1399
+ req.http.res.status(201)
1400
+ }
1332
1401
 
1333
1402
  return { ...res, IsActiveEntity: false } // REVISIT: Flatten?
1334
1403
  }
@@ -15,6 +15,7 @@ const {
15
15
  readStreamWithHdb
16
16
  } = require('./streaming')
17
17
  const { convertStream } = require('../db/utils/stream')
18
+ const { isBase64String } = require('../../common/assert/utils')
18
19
 
19
20
  function _cqnToSQL(model, query, user, locale, txTimestamp) {
20
21
  return sqlFactory(
@@ -48,8 +49,6 @@ function _getBinaries(stmt) {
48
49
  }, [])
49
50
  }
50
51
 
51
- const BASE64 = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{4}|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}={2})$/
52
-
53
52
  function _getProcedureNameAndSchema(sql) {
54
53
  // name delimited with "" allows any character
55
54
  const match = sql
@@ -142,9 +141,7 @@ function _executeAsPreparedStatement(dbc, sql, values, reject, resolve) {
142
141
  const vals = Array.isArray(values[0]) ? values : [values]
143
142
  for (const i of binaries) {
144
143
  for (const row of vals) {
145
- if (row[i] && typeof row[i] === 'string' && row[i].match(BASE64)) {
146
- row[i] = Buffer.from(row[i], 'base64')
147
- }
144
+ if (row[i] && isBase64String(row[i])) row[i] = Buffer.from(row[i], 'base64')
148
145
  }
149
146
  }
150
147
  }
@@ -126,8 +126,12 @@ class MessagingService extends cds.Service {
126
126
  const subscribedEvent =
127
127
  this.subscribedTopics.get(_msg.event) ||
128
128
  (this.wildcarded && this.subscribedTopics.get(this.wildcarded(_msg.event)))
129
- if (!subscribedEvent && !this._listenToAll.value)
130
- throw new Error(`No handler for incoming message with topic '${_msg.event}' found.`)
129
+ if (!subscribedEvent && !this._listenToAll.value) {
130
+ const err = new Error(`No handler for incoming message with topic '${_msg.event}' found.`)
131
+ err.code = 'NO_HANDLER_FOUND' // consumers might want to react to that
132
+ throw err
133
+ }
134
+
131
135
  _msg.event = subscribedEvent || _msg.event
132
136
  }
133
137
  return _msg
@@ -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
+ }