@sap/cds 7.1.1 → 7.2.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 (84) hide show
  1. package/CHANGELOG.md +68 -4
  2. package/apis/cds.d.ts +10 -6
  3. package/apis/connect.d.ts +0 -1
  4. package/apis/core.d.ts +54 -5
  5. package/apis/log.d.ts +19 -6
  6. package/apis/models.d.ts +0 -18
  7. package/apis/ql.d.ts +23 -23
  8. package/apis/serve.d.ts +17 -14
  9. package/apis/services.d.ts +40 -29
  10. package/apis/test.d.ts +1 -2
  11. package/bin/serve.js +4 -4
  12. package/lib/auth/basic-auth.js +1 -1
  13. package/lib/auth/dummy-auth.js +2 -1
  14. package/lib/auth/ias-auth.js +68 -2
  15. package/lib/auth/index.js +5 -5
  16. package/lib/auth/jwt-auth.js +40 -24
  17. package/lib/auth/mocked-users.js +0 -13
  18. package/lib/auth/passport-basic.js +2 -0
  19. package/lib/auth/passport-digest.js +2 -0
  20. package/lib/compile/etc/_localized.js +0 -1
  21. package/lib/compile/extend.js +16 -0
  22. package/lib/compile/for/lean_drafts.js +38 -6
  23. package/lib/compile/resolve.js +7 -5
  24. package/lib/compile/to/json.js +6 -2
  25. package/lib/dbs/cds-deploy.js +4 -4
  26. package/lib/env/cds-env.js +3 -3
  27. package/lib/env/cds-requires.js +1 -0
  28. package/lib/env/defaults.js +8 -1
  29. package/lib/env/schemas/cds-rc.json +27 -3
  30. package/lib/i18n/localize.js +3 -3
  31. package/lib/index.js +4 -0
  32. package/lib/log/cds-log.js +10 -1
  33. package/lib/ql/Whereable.js +7 -3
  34. package/lib/req/user.js +18 -16
  35. package/lib/srv/middlewares/sap-statistics.js +3 -3
  36. package/lib/srv/middlewares/trace.js +5 -4
  37. package/lib/srv/srv-dispatch.js +10 -9
  38. package/lib/utils/axios.js +3 -0
  39. package/lib/utils/cds-test.js +3 -0
  40. package/lib/utils/cds-utils.js +2 -0
  41. package/libx/_runtime/auth/index.js +8 -32
  42. package/libx/_runtime/auth/strategies/ias-auth.js +1 -77
  43. package/libx/_runtime/auth/strategies/mock.js +1 -12
  44. package/libx/_runtime/auth/strategies/xssecUtils.js +2 -2
  45. package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +3 -1
  46. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +11 -9
  47. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/ExpressionToCQN.js +5 -0
  48. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/applyToCQN.js +5 -2
  49. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/UriParser.js +4 -0
  50. package/libx/_runtime/cds-services/services/utils/compareJson.js +0 -9
  51. package/libx/_runtime/cds-services/services/utils/differ.js +8 -10
  52. package/libx/_runtime/common/composition/data.js +10 -7
  53. package/libx/_runtime/common/composition/insert.js +9 -5
  54. package/libx/_runtime/common/composition/update.js +18 -12
  55. package/libx/_runtime/common/error/constants.js +6 -1
  56. package/libx/_runtime/common/generic/auth/requires.js +11 -3
  57. package/libx/_runtime/common/generic/auth/restrict.js +22 -16
  58. package/libx/_runtime/common/generic/auth/restrictions.js +5 -2
  59. package/libx/_runtime/common/generic/crud.js +6 -0
  60. package/libx/_runtime/common/generic/paging.js +2 -1
  61. package/libx/_runtime/common/utils/cqn2cqn4sql.js +3 -5
  62. package/libx/_runtime/common/utils/resolveView.js +3 -1
  63. package/libx/_runtime/common/utils/restrictions.js +47 -0
  64. package/libx/_runtime/db/data-conversion/post-processing.js +3 -3
  65. package/libx/_runtime/db/generic/input.js +1 -1
  66. package/libx/_runtime/db/sql-builder/ExpressionBuilder.js +0 -17
  67. package/libx/_runtime/db/utils/coloredTxCommands.js +5 -3
  68. package/libx/_runtime/fiori/lean-draft.js +24 -19
  69. package/libx/_runtime/hana/driver.js +2 -4
  70. package/libx/_runtime/hana/pool.js +53 -57
  71. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +2 -2
  72. package/libx/_runtime/messaging/outbox/utils.js +1 -2
  73. package/libx/_runtime/remote/utils/client.js +1 -1
  74. package/libx/_runtime/sqlite/Service.js +0 -4
  75. package/libx/_runtime/sqlite/customBuilder/CustomFunctionBuilder.js +2 -1
  76. package/libx/odata/afterburner.js +6 -4
  77. package/libx/odata/cqn2odata.js +7 -7
  78. package/libx/odata/utils.js +4 -1
  79. package/libx/rest/RestAdapter.js +15 -16
  80. package/package.json +1 -1
  81. package/lib/auth/xsuaa-auth.js +0 -2
  82. package/libx/_runtime/auth/utils.js +0 -32
  83. package/libx/audit-log/client.cds +0 -0
  84. package/libx/audit-log/client.js +0 -0
@@ -1,3 +1,5 @@
1
+ const cds = require('../../../../cds')
2
+
1
3
  const odata = require('../okra/odata-server')
2
4
  const ExpressionKind = odata.uri.Expression.ExpressionKind
3
5
  const BinaryOperatorKind = odata.uri.BinaryExpression.OperatorKind
@@ -41,7 +43,10 @@ class ExpressionToCQN {
41
43
  case EdmPrimitiveTypeKind.Int16:
42
44
  case EdmPrimitiveTypeKind.Int32:
43
45
  return { val: parseInt(value) }
46
+ case EdmPrimitiveTypeKind.Int64:
47
+ return { val: value.toString() }
44
48
  case EdmPrimitiveTypeKind.Decimal:
49
+ return cds.env.features.compat_decimal ? { val: parseFloat(value) } : { val: value.toString() }
45
50
  case EdmPrimitiveTypeKind.Single:
46
51
  case EdmPrimitiveTypeKind.Double:
47
52
  return { val: parseFloat(value) }
@@ -225,8 +225,11 @@ const applyToCQN = (transformations, entity, model) => {
225
225
  case TransformationKind.BOTTOM_TOP:
226
226
  _addBottomTopTransformation(transformation, res)
227
227
  break
228
- default:
229
- throw getFeatureNotSupportedError(`Transformation "${transformation.getKind()}" with query option $apply`)
228
+ default: {
229
+ const numericKind = transformation.getKind()
230
+ const stringKind = Object.entries(TransformationKind).find(([_, v]) => v === numericKind)?.[0]
231
+ throw getFeatureNotSupportedError(`Transformation "${stringKind || numericKind}" with query option $apply`)
232
+ }
230
233
  }
231
234
  }
232
235
 
@@ -175,6 +175,10 @@ class UriParser {
175
175
  * @param {UriInfo} uriInfo the result of parsing
176
176
  */
177
177
  parseQueryOptions (queryOptions, uriInfo) {
178
+ // EXPERIMENTAL FEATURE FLAGS!
179
+ const { okra_skip_query_options, odata_new_parser } = global.cds.env.features
180
+ if (okra_skip_query_options && odata_new_parser) return
181
+
178
182
  const lastSegment = uriInfo.getLastSegment()
179
183
  const crossjoinEntitySets = lastSegment.getCrossjoinEntitySets()
180
184
  const aliases = uriInfo.getAliases()
@@ -133,11 +133,6 @@ const _addToBeDeletedEntriesToResult = (results, entity, keys, newValues, oldVal
133
133
 
134
134
  const _normalizeToArray = value => (Array.isArray(value) ? value : value === null ? [] : [value])
135
135
 
136
- const _addKeysToEntryIfNotExists = (keys, newEntry) => {
137
- if (!newEntry) return
138
- for (const key of keys) if (!(key in newEntry)) newEntry[key] = undefined
139
- }
140
-
141
136
  const _isUnManaged = element => {
142
137
  return element.on && !element._isSelfManaged
143
138
  }
@@ -207,11 +202,7 @@ const compareJsonDeep = (entity, newValue = [], oldValue = [], opts) => {
207
202
  for (const newEntry of newValues) {
208
203
  const result = {}
209
204
  const oldEntry = _getCorrespondingEntryWithSameKeys(oldValues, newEntry, keys)
210
-
211
- _addKeysToEntryIfNotExists(keys, newEntry)
212
-
213
205
  _iteratePropsInNewEntry(newEntry, keys, result, oldEntry, entity, opts)
214
-
215
206
  resultsArray.push(result)
216
207
  }
217
208
 
@@ -37,16 +37,14 @@ module.exports = class Differ {
37
37
  return columns
38
38
  }
39
39
 
40
- _diffDelete(req) {
40
+ async _diffDelete(req) {
41
41
  const { DELETE } = cds.env.fiori.lean_draft ? req.query : (req._ && req._.query) || req.query
42
42
  const target = DELETE._transitions?.[DELETE._transitions.length - 1]?.target || req.target
43
43
  const query = SELECT.from(DELETE.from).columns(this._createSelectColumnsForDelete(target))
44
44
  if (DELETE.where) query.where(DELETE.where)
45
-
46
- return cds
47
- .tx(req)
48
- .run(query)
49
- .then(dbState => compareJson(undefined, dbState, req.target, { ignoreDraftColumns: true }))
45
+ const dbState = await cds.tx(req).run(query)
46
+ const diff = compareJson(undefined, dbState, req.target, { ignoreDraftColumns: true })
47
+ return diff
50
48
  }
51
49
 
52
50
  async _addPartialPersistentState(req) {
@@ -62,7 +60,8 @@ module.exports = class Differ {
62
60
  enrichDataWithKeysFromWhere(combinedData, req, this._srv)
63
61
  const lastTransition = newQuery.UPDATE._transitions[newQuery.UPDATE._transitions.length - 1]
64
62
  const revertedPersistent = revertData(req._.partialPersistentState, lastTransition, this._srv)
65
- return compareJson(combinedData, revertedPersistent, req.target, { ignoreDraftColumns: true })
63
+ const diff = compareJson(combinedData, revertedPersistent, req.target, { ignoreDraftColumns: true })
64
+ return diff
66
65
  }
67
66
 
68
67
  async _diffPatch(req, providedData) {
@@ -87,10 +86,9 @@ module.exports = class Differ {
87
86
  providedData || (req.query.INSERT.entries && req.query.INSERT.entries.length === 1)
88
87
  ? req.query.INSERT.entries[0]
89
88
  : req.query.INSERT.entries
90
-
91
89
  enrichDataWithKeysFromWhere(originalData, req, this._srv)
92
-
93
- return compareJson(originalData, undefined, req.target, { ignoreDraftColumns: true })
90
+ const diff = compareJson(originalData, undefined, req.target, { ignoreDraftColumns: true })
91
+ return diff
94
92
  }
95
93
 
96
94
  async calculate(req, providedData) {
@@ -29,6 +29,7 @@ const _isSameEntityInWhere = (where, target, persistentObj) => {
29
29
  const key = where[i].ref
30
30
  const val = where[i + 2].val
31
31
  const sign = where[i + 1]
32
+
32
33
  // eslint-disable-next-line
33
34
  if (target.elements[key].key && key in persistentObj && sign === '=' && val !== persistentObj[key]) {
34
35
  return false
@@ -133,7 +134,7 @@ const _subData = (data, prop) =>
133
134
  data.reduce((result, entry) => {
134
135
  if (prop in entry) {
135
136
  const elementValue = ctUtils.val(entry[prop])
136
- result.push(...ctUtils.array(elementValue))
137
+ for (const val of ctUtils.array(elementValue)) result.push(val)
137
138
  }
138
139
  return result
139
140
  }, [])
@@ -196,7 +197,8 @@ const _mergeResults = (result, selectData, root, model, compositionTree, entityN
196
197
  if (newData[0]) selectEntry[compositionTree.name] = Object.assign(selectEntry[compositionTree.name], newData[0])
197
198
  else selectEntry[compositionTree.name] = null
198
199
  } else if (assoc.is2many) {
199
- selectEntry[compositionTree.name].push(...newData)
200
+ const entry = selectEntry[compositionTree.name]
201
+ for (const val of newData) entry.push(val)
200
202
  }
201
203
 
202
204
  return selectEntry
@@ -254,6 +256,7 @@ const _select = ({
254
256
  const _selectDeepUpdateData = async args => {
255
257
  const { model, compositionTree, entityName, data, root, selectData, tx, selectAllColumns, where, parentKeys } = args
256
258
  let result = []
259
+
257
260
  if (!where && parentKeys && parentKeys.length && Object.keys(parentKeys[0]).length) {
258
261
  const keys0 = Object.keys(parentKeys[0])
259
262
  const keys = { list: keys0.map(pk => ({ ref: [pk] })) }
@@ -263,13 +266,13 @@ const _selectDeepUpdateData = async args => {
263
266
  }
264
267
  const _args = { ...args, where: [keys, 'in', values], parentKeys: undefined }
265
268
  const selectCQN = _select(_args)
266
- result.push(...(await tx.run(selectCQN)))
269
+ result = result.concat(await tx.run(selectCQN))
267
270
  }
268
271
  } else if (where && !Array.isArray(where)) {
269
272
  for (let w of Object.values(where)) {
270
273
  const _args = { ...args, where: w }
271
274
  const selectCQN = _select(_args)
272
- result.push(...(await tx.run(selectCQN)))
275
+ result = result.concat(await tx.run(selectCQN))
273
276
  }
274
277
  } else {
275
278
  const selectCQN = _select(args)
@@ -295,9 +298,7 @@ const _selectDeepUpdateData = async args => {
295
298
  root: false
296
299
  }
297
300
 
298
- // REVISIT: remove null elements
299
- subs.data = subs.data.filter(d => d)
300
-
301
+ subs.data = subs.data.filter(d => d) // REVISIT: remove null elements
301
302
  return _selectDeepUpdateData({ ...args, ...subs })
302
303
  })
303
304
  )
@@ -309,6 +310,7 @@ const _selectDeepUpdateData = async args => {
309
310
  const _resolveOrderBy = (orderBy, transitions) => {
310
311
  // no resolved entity found
311
312
  if (!transitions?.length) return
313
+
312
314
  // if there are no renamed fields, no need to resolve
313
315
  if (!transitions[0].mapping.size) return
314
316
  if (orderBy) orderBy.map(el => (el.ref[0] = transitions[0].mapping.get(el.ref[0]).ref[0]))
@@ -320,6 +322,7 @@ const _resolveOrderBy = (orderBy, transitions) => {
320
322
 
321
323
  const selectDeepUpdateData = (service, model, req, selectAllColumns = false) => {
322
324
  const query = req.query
325
+
323
326
  // REVISIT this should be done somewhere before, so it is not done twice for deep updates
324
327
  const sqlQuery = cqn2cqn4sql(query, model)
325
328
 
@@ -17,9 +17,8 @@ function _hasCompOrAssoc(entity, k) {
17
17
 
18
18
  const _addSubDeepInsertCQN = (model, compositionTree, data, cqns, draft) => {
19
19
  compositionTree.compositionElements.forEach(element => {
20
- if (element.skipPersistence) {
21
- return
22
- }
20
+ if (element.skipPersistence) return
21
+
23
22
  // element source must be changed in comp tree
24
23
  const subEntity = model.definitions[element.source]
25
24
  const into = ctUtils.addDraftSuffix(draft, subEntity.name)
@@ -32,20 +31,25 @@ const _addSubDeepInsertCQN = (model, compositionTree, data, cqns, draft) => {
32
31
  const subData = ctUtils.array(elementValue).filter(ele => Object.keys(ele).length > 0)
33
32
  if (subData.length > 0) {
34
33
  // REVISIT: this can make problems
35
- insertCQN.INSERT.entries.push(...ctUtils.cleanDeepData(subEntity, subData))
36
- result.push(...subData)
34
+ const entries = insertCQN.INSERT.entries
35
+ for (const data of ctUtils.cleanDeepData(subEntity, subData)) entries.push(data)
36
+ for (const data of subData) result.push(data)
37
37
  }
38
38
  }
39
39
  }
40
+
40
41
  return result
41
42
  }, [])
43
+
42
44
  if (insertCQN.INSERT.entries.length > 0) {
43
45
  cqns.push(insertCQN)
44
46
  }
47
+
45
48
  if (subData.length > 0) {
46
49
  _addSubDeepInsertCQN(model, element, subData, cqns, draft)
47
50
  }
48
51
  })
52
+
49
53
  return cqns
50
54
  }
51
55
 
@@ -1,13 +1,10 @@
1
1
  const cds = require('../../cds')
2
-
3
2
  const { getCompositionTree } = require('./tree')
4
3
  const { getDeepInsertCQNs } = require('./insert')
5
4
  const { getDeepDeleteCQNs } = require('./delete')
6
5
  const ctUtils = require('./utils')
7
-
8
6
  const { ensureNoDraftsSuffix } = require('../utils/draft')
9
7
  const { deepCopyObject } = require('../utils/copy')
10
-
11
8
  const getError = require('../../common/error')
12
9
  const { getEntityNameFromUpdateCQN } = require('../utils/cqn')
13
10
 
@@ -31,28 +28,35 @@ const _serializedKey = (entity, data) => {
31
28
 
32
29
  const _dataByKey = (entity, data) => {
33
30
  const dataByKey = new Map()
31
+
34
32
  for (const entry of data) {
35
33
  dataByKey.set(_serializedKey(entity, entry), entry)
36
34
  }
35
+
37
36
  return dataByKey
38
37
  }
39
38
 
40
39
  function _addSubDeepUpdateCQNForDelete({ entity, data, selectData, entityName, deleteCQNs }) {
41
40
  const dataByKey = _dataByKey(entity, data)
41
+
42
42
  if (selectData.length && selectData[0] && Object.keys(selectData[0]).length) {
43
43
  for (let j = 0; j < selectData.length; j += CHUNK_SIZE) {
44
44
  const deleteCQN = { DELETE: { from: entityName, where: [] } }
45
+
45
46
  for (let i = j; i < j + CHUNK_SIZE && i < selectData.length; i++) {
46
47
  const selectEntry = selectData[i]
47
48
  if (!selectEntry) continue
49
+
48
50
  const dataEntry = dataByKey.get(_serializedKey(entity, selectEntry))
49
51
  if (!dataEntry) {
50
52
  if (deleteCQN.DELETE.where.length > 0) {
51
53
  deleteCQN.DELETE.where.push('or')
52
54
  }
55
+
53
56
  deleteCQN.DELETE.where.push({ xpr: [...ctUtils.whereKey(ctUtils.key(entity, selectEntry))] })
54
57
  }
55
58
  }
59
+
56
60
  if (deleteCQN.DELETE.where.length) deleteCQNs.push(deleteCQN)
57
61
  }
58
62
  }
@@ -63,6 +67,7 @@ const _unwrapVal = obj => {
63
67
  const value = obj[key]
64
68
  if (value && value.val) obj[key] = value.val
65
69
  }
70
+
66
71
  return obj
67
72
  }
68
73
 
@@ -120,6 +125,7 @@ const _diffData = (newData, oldData, entity, newEntry, oldEntry, model) => {
120
125
  function _addSubDeepUpdateCQNForUpdateInsert({ entity, entityName, data, selectData, updateCQNs, insertCQN, model }) {
121
126
  const selectDataByKey = _dataByKey(entity, selectData)
122
127
  const deepUpdateData = []
128
+
123
129
  for (const entry of data) {
124
130
  if (entry === null) continue
125
131
 
@@ -138,26 +144,27 @@ function _addSubDeepUpdateCQNForUpdateInsert({ entity, entityName, data, selectD
138
144
  // inserts are handled deep so they must not be put into deepUpdateData
139
145
  }
140
146
  }
147
+
141
148
  return deepUpdateData
142
149
  }
143
150
 
144
151
  async function _addSubDeepUpdateCQNCollect(model, cqns, updateCQNs, insertCQN, deleteCQNs, req) {
145
152
  if (updateCQNs.length > 0) {
146
153
  cqns[0] = cqns[0] || []
147
- cqns[0].push(...updateCQNs)
154
+ const cqn = cqns[0]
155
+ for (const updateCQN of updateCQNs) cqn.push(updateCQN)
148
156
  }
149
157
 
150
158
  if (insertCQN.INSERT.entries.length > 0) {
151
159
  cqns[0] = cqns[0] || []
152
160
  const deepInsertCQNs = getDeepInsertCQNs(model, insertCQN)
153
161
  deepInsertCQNs.forEach(insertCQN => {
154
- const intoCQN = cqns[0].find(cqn => {
155
- return cqn.INSERT && cqn.INSERT.into === insertCQN.INSERT.into
156
- })
162
+ const intoCQN = cqns[0].find(cqn => cqn.INSERT?.into === insertCQN.INSERT.into)
157
163
  if (!intoCQN) {
158
164
  cqns[0].push(insertCQN)
159
165
  } else {
160
- intoCQN.INSERT.entries.push(...insertCQN.INSERT.entries)
166
+ const intoCQNEntries = intoCQN.INSERT.entries
167
+ for (const entry of insertCQN.INSERT.entries) intoCQNEntries.push(entry)
161
168
  }
162
169
  })
163
170
  }
@@ -182,7 +189,7 @@ const _addToData = (subData, entity, element, entry) => {
182
189
  const value = ctUtils.val(entry[element.name])
183
190
  const subDataEntries = ctUtils.array(value)
184
191
  const unwrappedSubData = subDataEntries.map(entry => _unwrapIfNotArray(entry))
185
- subData.push(...unwrappedSubData)
192
+ for (const val of unwrappedSubData) subData.push(val)
186
193
  }
187
194
 
188
195
  async function _addSubDeepUpdateCQNRecursion({ model, compositionTree, entity, data, selectData, cqns, draft, req }) {
@@ -200,6 +207,7 @@ async function _addSubDeepUpdateCQNRecursion({ model, compositionTree, entity, d
200
207
  if (selectEntry[element.name] === null && entry[element.name] === null) {
201
208
  continue
202
209
  }
210
+
203
211
  _addToData(selectSubData, entity, element, selectEntry)
204
212
  }
205
213
 
@@ -244,7 +252,6 @@ const _addSubDeepUpdateCQN = async ({ model, compositionTree, data, selectData,
244
252
  })
245
253
 
246
254
  await _addSubDeepUpdateCQNCollect(model, cqns, updateCQNs, insertCQN, deleteCQNs, req)
247
-
248
255
  if (deepUpdateData.length === 0) return Promise.resolve()
249
256
 
250
257
  return _addSubDeepUpdateCQNRecursion({
@@ -280,7 +287,6 @@ const hasDeepUpdate = (model, cqn) => {
280
287
  const getDeepUpdateCQNs = async (model, req, selectData) => {
281
288
  const { query } = req
282
289
  if (!Array.isArray(selectData)) selectData = [selectData]
283
-
284
290
  if (selectData.length === 0) return []
285
291
  if (selectData.length > 1) throw getError('Deep update can only be performed on a single instance')
286
292
 
@@ -310,7 +316,7 @@ const getDeepUpdateCQNs = async (model, req, selectData) => {
310
316
  })
311
317
  subCQNs.forEach((subCQNs, index) => {
312
318
  cqns[index] = cqns[index] || []
313
- cqns[index].push(...subCQNs)
319
+ for (const cqn of subCQNs) cqns[index].push(cqn)
314
320
  })
315
321
 
316
322
  // remove empty updates and inserts
@@ -11,5 +11,10 @@ module.exports = {
11
11
  DEFAULT_SEVERITY: 2,
12
12
  MIN_SEVERITY: 1,
13
13
  MAX_SEVERITY: 4,
14
- MULTIPLE_ERRORS: 'MULTIPLE_ERRORS'
14
+ MULTIPLE_ERRORS: 'MULTIPLE_ERRORS',
15
+ /*
16
+ * OData
17
+ */
18
+ ODATA_UNAUTHORIZED: { statusCode: 401, code: '401', message: 'Unauthorized' },
19
+ ODATA_FORBIDDEN: { statusCode: 403, code: '403', message: 'Forbidden' }
15
20
  }
@@ -1,7 +1,12 @@
1
1
  const { reject, getRejectReason, getAuthRelevantEntity } = require('./utils')
2
2
  const { CRUD_EVENTS } = require('./constants')
3
3
 
4
- const { getRequiresAsArray } = require('../../../auth/utils')
4
+ const _getRequiresAsArray = definition =>
5
+ definition['@requires']
6
+ ? Array.isArray(definition['@requires'])
7
+ ? definition['@requires']
8
+ : [definition['@requires']]
9
+ : false
5
10
 
6
11
  function handler(req) {
7
12
  if (req.user._is_privileged) {
@@ -13,7 +18,7 @@ function handler(req) {
13
18
  if (req.event in CRUD_EVENTS) {
14
19
  // > CRUD
15
20
  definition = getAuthRelevantEntity(req, this.model, ['@requires', '@restrict'])
16
- } else if (req.target && req.target.actions) {
21
+ } else if (req.target?.actions) {
17
22
  // > bound
18
23
  definition = req.target.actions[req.event]
19
24
  } else {
@@ -23,7 +28,10 @@ function handler(req) {
23
28
 
24
29
  if (!definition) return
25
30
 
26
- const requires = getRequiresAsArray(definition)
31
+ // also check target entity for bound operations
32
+ const requires =
33
+ _getRequiresAsArray(definition) ||
34
+ (['action', 'function'].includes(definition.kind) && req.target && _getRequiresAsArray(req.target))
27
35
  if (!requires || requires.some(role => req.user.is(role))) return
28
36
 
29
37
  reject(req, getRejectReason(req, '@requires', definition))
@@ -37,11 +37,9 @@ const _getResolvedApplicables = (applicables, req) => {
37
37
  return resolvedApplicables
38
38
  }
39
39
 
40
- const _isStaticAuth = resolvedApplicables => {
41
- return (
42
- resolvedApplicables.length === 1 &&
43
- resolvedApplicables[0]._xpr.length === 3 &&
44
- resolvedApplicables[0]._xpr.every(ele => typeof ele !== 'object' || ele.val)
40
+ const _getStaticAuthRestrictions = resolvedApplicables => {
41
+ return resolvedApplicables.filter(
42
+ resolved => resolved && resolved._xpr.length === 3 && resolved._xpr.every(ele => typeof ele !== 'object' || ele.val)
45
43
  )
46
44
  }
47
45
 
@@ -68,14 +66,14 @@ const _evalStatic = (op, vals) => {
68
66
  }
69
67
  }
70
68
 
71
- const _handleStaticAuth = (resolvedApplicables, req) => {
72
- const op = resolvedApplicables[0]._xpr.find(ele => typeof ele === 'string')
73
- const vals = resolvedApplicables[0]._xpr.filter(ele => typeof ele === 'object' && ele.val).map(ele => ele.val)
74
-
75
- if (_evalStatic(op, vals)) {
76
- // static clause grants access => done
77
- return
78
- }
69
+ const _handleStaticAuthRestrictions = (resolvedApplicables, req) => {
70
+ const isAllowed = resolvedApplicables.some(restriction => {
71
+ const op = restriction._xpr.find(ele => typeof ele === 'string')
72
+ const vals = restriction._xpr.filter(ele => typeof ele === 'object' && ele.val).map(ele => ele.val)
73
+ return _evalStatic(op, vals)
74
+ })
75
+ // static clause grants access => done
76
+ if (isAllowed) return
79
77
 
80
78
  // static clause forbids access => forbidden
81
79
  return reject(req)
@@ -121,7 +119,7 @@ const _addWheresToRef = (ref, model, resolvedApplicables) => {
121
119
  newIdentifier.where = [{ xpr: newIdentifier.where }, 'and']
122
120
  }
123
121
 
124
- newIdentifier.where.push(..._getMergedWhere(applicablesForEntity))
122
+ for (const val of _getMergedWhere(applicablesForEntity)) newIdentifier.where.push(val)
125
123
  }
126
124
 
127
125
  newRef.push(newIdentifier)
@@ -224,8 +222,15 @@ async function handler(req) {
224
222
  return
225
223
  }
226
224
 
225
+ // READ UPDATE DELETE on draft enabled entities are unrestricted, because only the owner can access them
226
+ const draftUnRestrictedEvents = ['READ', 'UPDATE', 'DELETE', 'CREATE']
227
+ if (definition.isDraft && draftUnRestrictedEvents.includes(req.event)) {
228
+ return
229
+ }
230
+
227
231
  let restrictions = this.getRestrictions.call(this, definition, req.event, req.user)
228
232
  if (restrictions instanceof Promise) restrictions = await restrictions
233
+
229
234
  if (!restrictions) {
230
235
  // > unrestricted
231
236
  return
@@ -243,8 +248,9 @@ async function handler(req) {
243
248
  const resolvedApplicables = _getResolvedApplicables(restrictions, req)
244
249
 
245
250
  // REVISIT: support more complex statics
246
- if (_isStaticAuth(resolvedApplicables)) {
247
- return _handleStaticAuth(resolvedApplicables, req)
251
+ const staticAuthRestriction = _getStaticAuthRestrictions(resolvedApplicables)
252
+ if (staticAuthRestriction.length > 0) {
253
+ return _handleStaticAuthRestrictions(staticAuthRestriction, req)
248
254
  }
249
255
 
250
256
  if (req.event === 'READ') {
@@ -1,5 +1,6 @@
1
1
  const WRITE_EVENTS = { CREATE: 1, NEW: 1, UPDATE: 1, PATCH: 1, DELETE: 1, CANCEL: 1, EDIT: 1 }
2
2
  const CRUD = Object.assign({ READ: 1 }, WRITE_EVENTS)
3
+ const cds = require('../../../cds')
3
4
 
4
5
  /**
5
6
  * Returns the applicable restrictions for the current request as follows:
@@ -109,12 +110,14 @@ const getNormalizedRestrictions = (definition, definitions) => {
109
110
  return isRestricted ? restricts : null
110
111
  }
111
112
 
112
- const _isGrantAccessAllowed = (eventName, restrict) => restrict.grant === '*' || restrict.grant === eventName
113
+ const _isGrantAccessAllowed = (eventName, restrict) =>
114
+ restrict.grant === '*' || (eventName === 'EDIT' && restrict.grant === 'UPDATE') || restrict.grant === eventName
115
+
113
116
  const _isToAccessAllowed = (user, restrict) => restrict.to.some(role => user.is(role))
114
117
 
115
118
  const getApplicableRestrictions = (restrictions, event, user) => {
116
119
  return restrictions.filter(restrict => {
117
- const eventName = { NEW: 'CREATE', EDIT: 'UPDATE' }[event] || event
120
+ const eventName = cds.env.fiori.lean_draft ? event : { NEW: 'CREATE' }[event] || event
118
121
  return _isGrantAccessAllowed(eventName, restrict) && _isToAccessAllowed(user, restrict)
119
122
  })
120
123
  }
@@ -14,6 +14,12 @@ const _targetEntityDoesNotExist = async req => {
14
14
  exports.impl = cds.service.impl(function () {
15
15
  // eslint-disable-next-line complexity
16
16
  this.on(['CREATE', 'READ', 'UPDATE', 'DELETE', 'UPSERT'], '*', async function (req) {
17
+ if (!req.query) {
18
+ throw getError({
19
+ code: 501,
20
+ message: 'The request has no query and cannot be served generically.'
21
+ })
22
+ }
17
23
  if (typeof req.query !== 'string' && req.target && req.target._hasPersistenceSkip) {
18
24
  throw getError({
19
25
  code: 501,
@@ -10,7 +10,8 @@ const MAX = cds.env.query?.limit?.max || 1000
10
10
  const _cached = Symbol('@cds.query.limit')
11
11
 
12
12
  const getPageSize = def => {
13
- if (_cached in def) return def[_cached]
13
+ // do not look at prototypes re cached settings
14
+ if (Object.hasOwn(def, _cached)) return def[_cached]
14
15
  let max = def['@cds.query.limit.max'] ?? def._service?.['@cds.query.limit.max'] ?? MAX
15
16
  let _default =
16
17
  def['@cds.query.limit.default'] ??
@@ -3,7 +3,6 @@
3
3
  const cds = require('../../cds')
4
4
  const { SELECT, INSERT, DELETE, UPDATE } = cds.ql
5
5
  const Query = require('../../../../lib/ql/Query')
6
-
7
6
  const { resolveView } = require('./resolveView')
8
7
  const { ensureNoDraftsSuffix, getDraftColumnsCQNForDraft, ensureDraftsSuffix } = require('./draft')
9
8
  const { flattenStructuredSelect, OPERATIONS_MAP } = require('./structured')
@@ -807,8 +806,7 @@ const _convertSelect = (query, model, _options) => {
807
806
  const cols = getColumns(target, { onlyNames: true, filterVirtual: true })
808
807
  query.columns(cols)
809
808
  if (target._isDraftEnabled && query._target._unresolved) {
810
- query.SELECT.columns.push(...getDraftColumnsCQNForDraft(target))
811
- query.SELECT.columns.push({ ref: ['DraftAdministrativeData_DraftUUID'] })
809
+ query.SELECT.columns.push(...getDraftColumnsCQNForDraft(target), { ref: ['DraftAdministrativeData_DraftUUID'] })
812
810
  }
813
811
  }
814
812
  }
@@ -822,7 +820,8 @@ const _convertUpsert = (query, model) => {
822
820
 
823
821
  const target = model.definitions[resolvedIntoClause]
824
822
  if (!target) {
825
- // if there is no target, just return original query, as a copy is not deep anyways and all the sub items of query.UPSERT are referenced only anyways
823
+ // if there is no target, just return original query, as a copy is not deep anyways
824
+ // and all the sub items of query.UPSERT are referenced only anyways
826
825
  return query
827
826
  }
828
827
 
@@ -867,7 +866,6 @@ const _convertInsert = (query, model) => {
867
866
 
868
867
  const target = model.definitions[resolvedIntoClause]
869
868
  if (!target) return insert
870
-
871
869
  return resolveView(insert, model, cds.db)
872
870
  }
873
871
 
@@ -246,7 +246,8 @@ const _newWhereRef = (newWhereElement, transition, alias, tableName, isSubSelect
246
246
  if (mapped) newRef[1] = mapped.ref[0]
247
247
  } else {
248
248
  const mapped = transition.mapping.get(newRef[0])
249
- if (isSubSelect && mapped) {
249
+ if (isSubSelect && mapped && newRef.length === 1) {
250
+ // Add a table alias prefix only for not-yet-qualified refs
250
251
  newRef.unshift(transition.target.name)
251
252
  newRef[1] = mapped.ref[0]
252
253
  } else {
@@ -734,6 +735,7 @@ const resolveView = (query, model, service) => {
734
735
  // restore logger and clear _event
735
736
  LOG = _LOG
736
737
  _event = undefined
738
+
737
739
  return newQuery
738
740
  }
739
741
 
@@ -0,0 +1,47 @@
1
+ const cds = require('../../cds')
2
+
3
+ const containsAnyRestrictions = srv => {
4
+ const accessRestrictions = getAccessRestrictions(srv)
5
+ if (accessRestrictions.length > 1 || accessRestrictions[0] !== 'any') return true
6
+
7
+ const entities = srv.entities
8
+ const entitiesKeys = Object.keys(entities)
9
+
10
+ return !!(
11
+ entitiesKeys.some(entity => entities[entity]['@requires'] || entities[entity]['@restrict']) ||
12
+ entitiesKeys.some(entity => {
13
+ const actions = entities[entity].actions
14
+ actions && Object.keys(actions).some(action => actions[action]['@requires'] || actions[action]['@restrict'])
15
+ }) ||
16
+ Object.keys(srv.operations).some(
17
+ operation => srv.operations[operation]['@requires'] || srv.operations[operation]['@restrict']
18
+ )
19
+ )
20
+ }
21
+
22
+ const getAccessRestrictions = srv => {
23
+ let restrictions = srv.definition['@restrict'] || srv.definition['@requires']
24
+ if (restrictions) {
25
+ if (typeof restrictions === 'string') restrictions = [restrictions]
26
+ else
27
+ restrictions = restrictions
28
+ .map(r => (typeof r === 'string' ? r : r.to))
29
+ .reduce((acc, cur) => {
30
+ Array.isArray(cur) ? acc.push(...cur) : acc.push(cur)
31
+ return acc
32
+ }, [])
33
+ } else {
34
+ const { restrict_all_services } = cds.env.requires.auth
35
+ const in_prod = process.env.NODE_ENV === 'production'
36
+ // REVISIT: cleanup during streamlined auth
37
+ const is_mocked_auth = cds.env.requires.auth._kind === 'mocked'
38
+ if (restrict_all_services === false || !in_prod || is_mocked_auth) restrictions = ['any']
39
+ else restrictions = ['authenticated-user']
40
+ }
41
+ return restrictions
42
+ }
43
+
44
+ module.exports = {
45
+ containsAnyRestrictions,
46
+ getAccessRestrictions
47
+ }
@@ -132,9 +132,9 @@ const _getMapperForListedElements = (conversionMap, csn, cqn) => {
132
132
  * @returns {Map<any, any>}
133
133
  * @private
134
134
  */
135
- const getPostProcessMapper = (conversionMap, csn = {}, cqn = {}) => {
136
- // No mapper defined or irrelevant as no READ request
137
- if (!Object.prototype.hasOwnProperty.call(cqn, 'SELECT')) {
135
+ const getPostProcessMapper = (conversionMap, csn, cqn) => {
136
+ // No mapper defined or irrelevant as no CSN, CQN or READ request
137
+ if (!csn || !cqn || !Object.prototype.hasOwnProperty.call(cqn, 'SELECT')) {
138
138
  return new Map()
139
139
  }
140
140
 
@@ -138,7 +138,7 @@ const _pickCRUD = element => {
138
138
  categories.push('!default')
139
139
  }
140
140
 
141
- if (element.default && !DRAFT_COLUMNS_MAP[element.name]) {
141
+ if (element.default && !DRAFT_COLUMNS_MAP[element.name] && !element.isAssociation) {
142
142
  categories.push({ category: 'default', args: element })
143
143
  }
144
144