@sap/cds 6.7.0 → 6.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 CHANGED
@@ -4,6 +4,19 @@
4
4
  - The format is based on [Keep a Changelog](http://keepachangelog.com/).
5
5
  - This project adheres to [Semantic Versioning](http://semver.org/).
6
6
 
7
+ ## Version 6.7.1 - 2023-04-14
8
+
9
+ ### Changed
10
+
11
+ - Calling a parameterized view without params error now results in the status code 400 with an improved error message.
12
+
13
+ ### Fixed
14
+
15
+ - cds build error CreateListFromArrayLike
16
+ - Disabling of arbitrary user config in mock auth config using `"users": { "*": false }`
17
+ - Various fixes for `cds.fiori.lean_draft`
18
+ - User attributes that look like numbers are quoted in SQL clause for `@restrict`
19
+
7
20
  ## Version 6.7.0 - 2023-03-28
8
21
 
9
22
  ### Added
package/bin/cds.js CHANGED
@@ -14,8 +14,10 @@ const cli = { //NOSONAR
14
14
 
15
15
  const task = this.load ('./'+cmd)
16
16
 
17
- let args
18
- try { args = task && cmd !== 'build' && this.args(task, argv) }
17
+ let args = []
18
+ try {
19
+ if (task && cmd !== 'build') args = this.args(task, argv)
20
+ }
19
21
  catch (err) { process.exitCode = 1; return console.error(err) }
20
22
 
21
23
  if (task && cmd !== 'build') return task.apply (this, args)
@@ -108,6 +108,7 @@ module.exports = function cds_compile_for_lean_drafts(csn) {
108
108
  Object.defineProperty(model.definitions, _draftEntity, { value: draft })
109
109
  Object.defineProperty(active, 'drafts', { value: draft })
110
110
  Object.defineProperty(draft, 'actives', { value: active })
111
+ Object.defineProperty(draft, 'isDraft', { value: true })
111
112
  draft['@cds.persistence.table'] = _draftEntity
112
113
 
113
114
  for (const key in draft) {
@@ -16,7 +16,7 @@ class MockStrategy {
16
16
  if (!base64) return this.fail(CHALLENGE)
17
17
 
18
18
  const [id, password] = Buffer.from(base64, 'base64').toString().split(':')
19
- const user = this.users[id] || ('*' in this.users && { id })
19
+ const user = this.users[id] || (this.users['*'] && { id })
20
20
  if (!user) return this.fail(CHALLENGE)
21
21
  if (user.password && user.password !== password) return this.fail(CHALLENGE)
22
22
 
@@ -81,7 +81,7 @@ const action = service => {
81
81
  await tx.rollback(e).catch(() => {})
82
82
  }
83
83
  } finally {
84
- req.messages && odataRes.setHeader('sap-messages', getSapMessages(req.messages, req.http.req))
84
+ req.messages?.length && odataRes.setHeader('sap-messages', getSapMessages(req.messages, req.http.req))
85
85
 
86
86
  if (err) next(err)
87
87
  else next(null, toODataResult(result, req))
@@ -68,7 +68,7 @@ const create = service => {
68
68
  await tx.rollback(e).catch(() => {})
69
69
  }
70
70
  } finally {
71
- req.messages && odataRes.setHeader('sap-messages', getSapMessages(req.messages, req.http.req))
71
+ req.messages?.length && odataRes.setHeader('sap-messages', getSapMessages(req.messages, req.http.req))
72
72
 
73
73
  if (err) next(err)
74
74
  else next(null, toODataResult(result, req))
@@ -49,7 +49,7 @@ const del = service => {
49
49
  await tx.rollback(e).catch(() => {})
50
50
  }
51
51
  } finally {
52
- req.messages && odataRes.setHeader('sap-messages', getSapMessages(req.messages, req.http.req))
52
+ req.messages?.length && odataRes.setHeader('sap-messages', getSapMessages(req.messages, req.http.req))
53
53
 
54
54
  if (err) next(err)
55
55
  else next(null, null)
@@ -523,7 +523,7 @@ const read = service => {
523
523
  await tx.rollback(e).catch(() => {})
524
524
  }
525
525
  } finally {
526
- req.messages && odataRes.setHeader('sap-messages', getSapMessages(req.messages, req.http.req))
526
+ req.messages?.length && odataRes.setHeader('sap-messages', getSapMessages(req.messages, req.http.req))
527
527
 
528
528
  if (err) next(err)
529
529
  else next(null, result, additional)
@@ -179,7 +179,7 @@ const update = service => {
179
179
  await tx.rollback(e).catch(() => {})
180
180
  }
181
181
  } finally {
182
- req.messages && odataRes.setHeader('sap-messages', getSapMessages(req.messages, req.http.req))
182
+ req.messages?.length && odataRes.setHeader('sap-messages', getSapMessages(req.messages, req.http.req))
183
183
 
184
184
  if (err) next(err)
185
185
  else if (primitive && result) {
@@ -223,12 +223,12 @@ const _checkViewWithParamCall = (isView, segments, kind, name) => {
223
223
  }
224
224
 
225
225
  if (segments.length < 2) {
226
- throw new Error(`Incorrect call to a view with parameter "${name}"`)
226
+ throw cds.error(`Invalid call to "${name}". You need to navigate to Set`, { status: 400, code: 400 })
227
227
  }
228
228
 
229
229
  // if the last segment is count, check if previous segment is Set, otherwise check if the last segment equals Set
230
230
  if (!_isSet(segments[segments.length - (_isCount(kind) ? 2 : 1)])) {
231
- throw new Error(`Incorrect call to a view with parameter "${name}"`)
231
+ throw cds.error(`Invalid call to "${name}". You need to navigate to Set`, { status: 400, code: 400 })
232
232
  }
233
233
  }
234
234
 
@@ -62,6 +62,33 @@ class ApplicationService extends cds.Service {
62
62
  this.on('EDIT', each, onEdit)
63
63
  this.on('CANCEL', each.drafts, onCancel)
64
64
  this.on('draftPrepare', each.drafts, onPrepare)
65
+ if (cds.env.fiori.draft_compat) {
66
+ // register after read handlers to add `IsActiveEntity`,
67
+ // so stakeholders have access to it when calling next()
68
+
69
+ // to check if data contains a key value
70
+ let _key
71
+ for (const key in each.keys) {
72
+ if (key === 'IsActiveEntity') continue
73
+ _key = key
74
+ break
75
+ }
76
+ const _addIsActiveEntity = (data, IsActiveEntity) => {
77
+ if (!data) return
78
+ if (Array.isArray(data)) return data.map(d => _addIsActiveEntity(d, IsActiveEntity))
79
+ if (_key in data) data.IsActiveEntity = IsActiveEntity
80
+ }
81
+ this.on('READ', each, async (_, next) => {
82
+ const data = await next()
83
+ _addIsActiveEntity(data, true)
84
+ return data
85
+ })
86
+ this.on('READ', each, async (_, next) => {
87
+ const data = await next()
88
+ _addIsActiveEntity(data, false)
89
+ return data
90
+ })
91
+ }
65
92
  }
66
93
  } else return require('../../fiori/generic').impl.call(this)
67
94
  }
@@ -141,8 +141,11 @@ const resolveUserAttrs = (restrict, req) => {
141
141
  attr = parts.shift()
142
142
  }
143
143
 
144
- if (!skip)
145
- restrict.where = restrict.where.replace(next[0], val === undefined ? null : val).replace('in null', 'is null')
144
+ if (!skip) {
145
+ const v = val === undefined ? null : typeof val === 'string' && val.match(/^\d*$/) ? `'${val}'` : val
146
+ restrict.where = restrict.where.replace(next[0], v).replace('in null', 'is null')
147
+ }
148
+
146
149
  next = _getNext(restrict.where)
147
150
  }
148
151
 
@@ -280,7 +280,7 @@ const _createWindowCQN = (SELECT, model) => {
280
280
  }
281
281
  }
282
282
 
283
- delete SELECT.groupBy
283
+ SELECT.groupBy = undefined
284
284
  }
285
285
 
286
286
  const _unshiftRefsWithNavigation = nav => el => {
@@ -42,6 +42,14 @@ const _inProcessByUserXpr = lockShiftedNow => ({
42
42
  cast: { type: 'cds.String' }
43
43
  })
44
44
 
45
+ /// It's important to wait for the completion of all promises, otherwise a rollback might happen too soon
46
+ const _promiseAll = async array => {
47
+ const results = await Promise.allSettled(array)
48
+ const e = results.find(r => r.status === 'rejected')
49
+ if (e) throw e.reason
50
+ return results.map(r => r.value)
51
+ }
52
+
45
53
  const _lock = {
46
54
  get shiftedNow() {
47
55
  return new Date(Math.max(0, Date.now() - DRAFT_CANCEL_TIMEOUT_IN_MIN() * 60 * 1000)).toISOString()
@@ -111,15 +119,11 @@ cds.ApplicationService.prototype.handle = async function (req) {
111
119
  _req.validateEtag = req.validateEtag
112
120
  const cqnData = _req.query.UPDATE?.data || _req.query.INSERT?.entries?.[0]
113
121
  if (cqnData) _req.data = cqnData // must point to the same object
114
-
115
- // Dirty hack: delegate messages to original request by binding the getter _messages to req
116
- let proto = req
117
- let _messagesDescr
118
- while (proto && !(_messagesDescr = Object.getOwnPropertyDescriptor(proto, '_messages')))
119
- proto = Object.getPrototypeOf(proto)
120
- if (_messagesDescr)
121
- Object.defineProperty(_req, '_messages', { ..._messagesDescr, get: _messagesDescr.get.bind(req) })
122
-
122
+ Object.defineProperty(_req, '_messages', {
123
+ get: function () {
124
+ return req._messages
125
+ }
126
+ })
123
127
  if (req.tx) _req.tx = req.tx
124
128
  return _req
125
129
  }
@@ -170,7 +174,7 @@ cds.ApplicationService.prototype.handle = async function (req) {
170
174
  deletes.push(
171
175
  DELETE.from('DRAFT.DraftAdministrativeData').where({ DraftUUID: draft.DraftAdministrativeData_DraftUUID })
172
176
  )
173
- await Promise.all(deletes)
177
+ await _promiseAll(deletes)
174
178
  return req.data
175
179
  }
176
180
 
@@ -203,7 +207,8 @@ cds.ApplicationService.prototype.handle = async function (req) {
203
207
  delete res.DraftAdministrativeData
204
208
  const HasActiveEntity = res.HasActiveEntity
205
209
  delete res.HasActiveEntity
206
- await Promise.all([
210
+ delete res.IsActiveEntity
211
+ await _promiseAll([
207
212
  run(DELETE.from(targetDraft).where(targetWhere)),
208
213
  DELETE.from('DRAFT.DraftAdministrativeData').where({ DraftUUID: DraftAdministrativeData_DraftUUID })
209
214
  ])
@@ -342,7 +347,7 @@ const Read = {
342
347
  // DraftAdministrativeData is only accessible via drafts
343
348
  if (query._target.name.endsWith('.DraftAdministrativeData')) return run(query._drafts)
344
349
  if (!query._target._isDraftEnabled) return run(query)
345
- if (query.SELECT.columns && !query.SELECT.columns.some(c => c === '*')) {
350
+ if (!query.SELECT.groupBy && query.SELECT.columns && !query.SELECT.columns.some(c => c === '*')) {
346
351
  const keys = Object_keys(query._target.keys).filter(k => k !== 'IsActiveEntity')
347
352
  for (const key of keys) {
348
353
  if (!query.SELECT.columns.some(c => c.ref?.[0] === key)) query.SELECT.columns.push({ ref: [key] })
@@ -382,7 +387,7 @@ const Read = {
382
387
  draftsQuery.SELECT.columns = keys.map(k => ({ ref: [k] }))
383
388
 
384
389
  const drafts = await run(draftsQuery)
385
- const res = Read.onlyActives(run, query.where(Read.whereNotIn(query._target, drafts)), {
390
+ const res = await Read.onlyActives(run, query.where(Read.whereNotIn(query._target, drafts)), {
386
391
  ignoreDrafts: true
387
392
  })
388
393
  return _requested(res, query)
@@ -528,6 +533,7 @@ const Read = {
528
533
  whereIn: (target, data, not = false) => {
529
534
  const keys = Object_keys(target.keys).filter(k => k !== 'IsActiveEntity')
530
535
  const dataArray = data ? (Array.isArray(data) ? data : [data]) : []
536
+ if (not && !dataArray.length) return []
531
537
  return [
532
538
  { list: keys.map(k => ({ ref: [k] })) },
533
539
  not ? 'not in' : 'in',
@@ -724,9 +730,9 @@ function _cleansed(query, model) {
724
730
  '=',
725
731
  { val: cds.context.user.id },
726
732
  'then',
727
- { val: true },
733
+ 'true',
728
734
  'else',
729
- { val: false },
735
+ 'false',
730
736
  'end'
731
737
  ],
732
738
  as: 'DraftIsCreatedByMe',
@@ -747,9 +753,9 @@ function _cleansed(query, model) {
747
753
  '>',
748
754
  { val: _lock.shiftedNow },
749
755
  'then',
750
- { val: true },
756
+ 'true',
751
757
  'else',
752
- { val: false },
758
+ 'false',
753
759
  'end'
754
760
  ],
755
761
  as: 'DraftIsProcessedByMe',
@@ -862,7 +868,10 @@ async function onNew(req) {
862
868
 
863
869
  // Also support deep insertions
864
870
  for (const key in newObj) {
865
- if (typeof newObj[key] === 'object' && target.elements[key]?.isComposition) {
871
+ if (!target.elements[key]?.isComposition) continue
872
+ if (Array.isArray(newObj[key]))
873
+ newObj[key] = newObj[key].map(v => _assignDraftData(v, target.elements[key]._target))
874
+ else if (typeof newObj[key] === 'object') {
866
875
  newObj[key] = _assignDraftData(newObj[key], target.elements[key]._target)
867
876
  }
868
877
  }
@@ -875,7 +884,7 @@ async function onNew(req) {
875
884
  delete draftData.IsActiveEntity
876
885
  const draftCQN = INSERT.into(req.target).entries(draftData)
877
886
 
878
- await Promise.all([cds.run(adminDataCQN), this.run(draftCQN)])
887
+ await _promiseAll([cds.run(adminDataCQN), this.run(draftCQN)])
879
888
  req._.readAfterWrite = true
880
889
  return { ...draftData, IsActiveEntity: false }
881
890
  }
@@ -906,20 +915,29 @@ async function onEdit(req) {
906
915
  }
907
916
  }
908
917
  _addDraftColumns(req.target, cols)
909
- const draftsCheck = SELECT.one(req.target.drafts)
918
+
919
+ const existingDraft = SELECT.one(req.target.drafts)
910
920
  .columns({ ref: ['DraftAdministrativeData'], expand: [_inProcessByUserXpr(_lock.shiftedNow)] })
911
921
  .where(targetWhere)
912
- .forUpdate({ wait: 0 })
913
922
  // prevent service to check for own user
914
- Object.defineProperty(draftsCheck, '_draftParams', { value: draftParams, enumerable: false })
923
+ Object.defineProperty(existingDraft, '_draftParams', { value: draftParams, enumerable: false })
915
924
 
916
- const activeCQN = SELECT.one.from(req.target).columns(cols).where(targetWhere).forUpdate({ wait: 0 })
925
+ const activeCQN = SELECT.one.from(req.target).columns(cols).where(targetWhere)
917
926
  activeCQN._suppressLocalization = true // in the future we should be able to just set activeCQN.SELECT.localized = false
918
- const [res, draft] = await Promise.all([
927
+
928
+ const activeCheck = SELECT.one(req.target).columns([1]).where(targetWhere).forUpdate()
929
+ Object.defineProperty(activeCheck, '_draftParams', { value: draftParams, enumerable: false })
930
+ // It's not possible to use `FOR UPDATE` in HANA if the view contains joins/unions. Unfortunately, we can't resolve the table entity
931
+ // because we must trigger the app-service request on the target entity (which could be delegated to a remote service).
932
+ // The best we can do is to catch a potential error
933
+ await this.run(activeCheck).catch(_ => {})
934
+
935
+ const [res, draft] = await _promiseAll([
919
936
  this.run(activeCQN),
920
937
  // no user check must be done here...
921
- this.run(draftsCheck)
938
+ this.run(existingDraft)
922
939
  ])
940
+
923
941
  if (!res) req.reject(404)
924
942
  const preserveChanges = req.context?.data?.PreserveChanges
925
943
  const inProcessByUser = draft?.DraftAdministrativeData?.InProcessByUser
@@ -927,7 +945,7 @@ async function onEdit(req) {
927
945
  if (inProcessByUser || preserveChanges) req.reject(409, 'DRAFT_ALREADY_EXISTS')
928
946
  const keys = {}
929
947
  for (const key in req.target.drafts.keys) keys[key] = res[key]
930
- await Promise.all([
948
+ await _promiseAll([
931
949
  DELETE.from('DRAFT.DraftAdministrativeData').where({ DraftUUID }),
932
950
  this.run(DELETE.from(req.target.drafts).where(keys))
933
951
  ])
@@ -983,7 +1001,7 @@ async function onCancel(req) {
983
1001
  DELETE.from('DRAFT.DraftAdministrativeData').where({ DraftUUID: draft.DraftAdministrativeData_DraftUUID })
984
1002
  )
985
1003
  if (draftParams.IsActiveEntity) deletes.push(this.run(DELETE.from({ ref: activeRef })))
986
- await Promise.all(deletes)
1004
+ await _promiseAll(deletes)
987
1005
  return req.data
988
1006
  }
989
1007
 
@@ -6,7 +6,7 @@ const { addAliasToExpression } = require('../db/utils/generateAliases')
6
6
  const targetAlias = 'Target'
7
7
  const textsAlias = 'Texts'
8
8
  const _generateKeysWhereCondition = (entity, alias1, alias2) => {
9
- const keys = Object.keys(entity.keys).filter(k => !entity.keys[k].isAssociation)
9
+ const keys = Object.keys(entity.keys).filter(k => !entity.keys[k].isAssociation && !entity.keys[k].virtual)
10
10
  const where = []
11
11
  keys.forEach(key => {
12
12
  if (where.length > 0) where.push('and')
@@ -266,7 +266,7 @@ function _processSegments(from, model, namespace, cqn) {
266
266
  ref[i].where = undefined
267
267
  if (ref[i + 1] !== 'Set') {
268
268
  // /Set is missing
269
- throw new Error(`Incorrect call to a view with parameter "${current.name}"`)
269
+ throw cds.error(`Invalid call to "${current.name}". You need to navigate to Set`, { status: 400, code: 400 })
270
270
  }
271
271
  ref[++i] = null
272
272
  } else if (current.kind === 'entity') {
@@ -372,11 +372,11 @@ function _processSegments(from, model, namespace, cqn) {
372
372
  const AGGREGATION_DEFAULT = '@Aggregation.default'
373
373
 
374
374
  function _addKeys(columns, target) {
375
- let hasAggregatedColumn = false, hasStarColumn = false
375
+ let hasAggregatedColumn = false,
376
+ hasStarColumn = false
376
377
  for (let k = 0; k < columns.length; k++) {
377
378
  if (columns[k] === '*') hasStarColumn = true
378
379
  else if (columns[k].func || columns[k].func === null) hasAggregatedColumn = true
379
-
380
380
  // Add keys to (sub-)expands
381
381
  else if (columns[k].expand && columns[k].expand[0] !== '*')
382
382
  _addKeys(columns[k].expand, target.elements[columns[k].ref]._target)
@@ -444,7 +444,7 @@ const _checkAllKeysProvided = (params, entity) => {
444
444
  if (isView) {
445
445
  // view with params
446
446
  if (params === undefined) {
447
- throw new Error(`Incorrect call to a view with parameter "${entity.name}"`)
447
+ throw cds.error(`Invalid call to "${entity.name}". You need to navigate to Set`, { status: 400, code: 400 })
448
448
  } else if (Object.keys(params).length === 0) {
449
449
  throw new Error('KEY_EXPECTED')
450
450
  }
@@ -1,4 +1,5 @@
1
1
  const { formatVal } = require('./utils')
2
+ const cds = require('../_runtime/cds')
2
3
 
3
4
  const OPERATORS = {
4
5
  '=': 'eq',
@@ -253,7 +254,7 @@ function _getQueryTarget(entity, propOrEntity, model) {
253
254
 
254
255
  const _params = (args, kind, target) => {
255
256
  if (!args) {
256
- throw new Error(`Incorrect call to a view with parameter "${target.name}"`)
257
+ throw cds.error(`Invalid call to "${target.name}". You need to navigate to Set`, { status: 400, code: 400 })
257
258
  }
258
259
  const params = Object.keys(args)
259
260
  if (params.length !== Object.keys(target.params).length) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sap/cds",
3
- "version": "6.7.0",
3
+ "version": "6.7.1",
4
4
  "description": "SAP Cloud Application Programming Model - CDS for Node.js",
5
5
  "homepage": "https://cap.cloud.sap/",
6
6
  "keywords": [