@sap/cds 8.4.2 → 8.5.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 (70) hide show
  1. package/CHANGELOG.md +35 -1
  2. package/_i18n/messages.properties +99 -0
  3. package/bin/serve.js +2 -2
  4. package/lib/compile/cdsc.js +9 -4
  5. package/lib/compile/to/srvinfo.js +4 -4
  6. package/lib/core/entities.js +1 -0
  7. package/lib/core/types.js +1 -1
  8. package/lib/dbs/cds-deploy.js +4 -1
  9. package/lib/env/defaults.js +7 -6
  10. package/lib/env/schemas/cds-rc.js +132 -22
  11. package/lib/i18n/bundles.js +111 -0
  12. package/lib/i18n/files.js +134 -0
  13. package/lib/i18n/index.js +63 -0
  14. package/lib/i18n/localize.js +101 -237
  15. package/lib/i18n/resources.js +150 -0
  16. package/lib/index.js +1 -0
  17. package/lib/log/format/aspects/cls.js +6 -1
  18. package/lib/log/format/json.js +1 -1
  19. package/lib/ql/CREATE.js +1 -0
  20. package/lib/ql/DELETE.js +1 -0
  21. package/lib/ql/DROP.js +1 -0
  22. package/lib/ql/INSERT.js +9 -8
  23. package/lib/ql/Query.js +18 -8
  24. package/lib/ql/SELECT.js +1 -0
  25. package/lib/ql/UPDATE.js +2 -1
  26. package/lib/ql/UPSERT.js +1 -1
  27. package/lib/ql/Whereable.js +3 -3
  28. package/lib/ql/cds-ql.js +12 -18
  29. package/lib/req/user.js +1 -0
  30. package/lib/req/validate.js +12 -3
  31. package/lib/srv/factory.js +2 -2
  32. package/lib/{auth → srv/middlewares/auth}/basic-auth.js +1 -1
  33. package/lib/{auth → srv/middlewares/auth}/dummy-auth.js +1 -1
  34. package/lib/srv/middlewares/auth/ias-auth.js +96 -0
  35. package/lib/{auth → srv/middlewares/auth}/index.js +2 -2
  36. package/lib/srv/middlewares/auth/jwt-auth.js +62 -0
  37. package/lib/{auth → srv/middlewares/auth}/mocked-users.js +1 -1
  38. package/lib/srv/middlewares/auth/xssec.js +7 -0
  39. package/lib/srv/middlewares/index.js +1 -1
  40. package/lib/utils/cds-utils.js +15 -19
  41. package/lib/utils/tar.js +2 -2
  42. package/libx/_runtime/cds-services/adapter/odata-v4/OData.js +2 -2
  43. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +1 -0
  44. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +1 -1
  45. package/libx/_runtime/common/error/frontend.js +2 -6
  46. package/libx/_runtime/common/error/log.js +7 -8
  47. package/libx/_runtime/common/error/utils.js +3 -7
  48. package/libx/_runtime/common/generic/auth/capabilities.js +1 -1
  49. package/libx/_runtime/common/generic/input.js +41 -6
  50. package/libx/_runtime/common/i18n/index.js +8 -15
  51. package/libx/_runtime/common/utils/compareJson.js +10 -1
  52. package/libx/_runtime/common/utils/resolveView.js +1 -1
  53. package/libx/_runtime/fiori/lean-draft.js +77 -26
  54. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +5 -1
  55. package/libx/_runtime/messaging/kafka.js +1 -1
  56. package/libx/odata/index.js +3 -0
  57. package/libx/odata/middleware/create.js +8 -6
  58. package/libx/odata/middleware/update.js +24 -21
  59. package/libx/odata/parse/afterburner.js +15 -2
  60. package/libx/odata/parse/grammar.peggy +24 -7
  61. package/libx/odata/parse/parser.js +1 -1
  62. package/libx/odata/utils/postProcess.js +4 -1
  63. package/libx/rest/RestAdapter.js +2 -1
  64. package/libx/rest/middleware/error.js +0 -50
  65. package/package.json +1 -1
  66. package/lib/auth/ias-auth.js +0 -68
  67. package/lib/auth/ias-claims.js +0 -34
  68. package/lib/auth/jwt-auth.js +0 -70
  69. package/libx/_runtime/common/i18n/messages.properties +0 -99
  70. package/libx/_runtime/common/utils/require.js +0 -9
@@ -39,7 +39,7 @@ function handler(req) {
39
39
 
40
40
  if (!req.target || !annotation) return
41
41
 
42
- const action = annotation.split('.').pop().toUpperCase()
42
+ const action = annotation.split('.').pop().toLowerCase()
43
43
  const from = cqnFrom(req)
44
44
  const nav = _getNav(from)
45
45
 
@@ -10,6 +10,8 @@
10
10
  const cds = require('../../cds')
11
11
  const LOG = cds.log('app')
12
12
 
13
+ const { Readable } = require('node:stream')
14
+
13
15
  const { enrichDataWithKeysFromWhere } = require('../utils/keys')
14
16
  const { DRAFT_COLUMNS_MAP } = require('../../common/constants/draft')
15
17
  const propagateForeignKeys = require('../utils/propagateForeignKeys')
@@ -27,6 +29,13 @@ const _shouldSuppressErrorPropagation = (event, value) => {
27
29
  )
28
30
  }
29
31
 
32
+ const _sliceBase64 = function* (str) {
33
+ const chunkSize = 1 << 16
34
+ for (let i = 0; i < str.length; i += chunkSize) {
35
+ yield Buffer.from(str.slice(i, i + chunkSize), 'base64')
36
+ }
37
+ }
38
+
30
39
  const _getSimpleCategory = category => {
31
40
  if (typeof category === 'object') {
32
41
  category = category.category
@@ -142,6 +151,16 @@ const _processCategory = (req, category, value, elementInfo, assertMap) => {
142
151
  if ((event === 'UPDATE' || event === 'CREATE') && category === '@assert.target') {
143
152
  _preProcessAssertTarget(elementInfo, assertMap)
144
153
  }
154
+
155
+ if (category === 'binary' && typeof row[key] === 'string') {
156
+ row[key] = Buffer.from(row[key], 'base64')
157
+ return
158
+ }
159
+
160
+ if (category === 'largebinary' && typeof row[key] === 'string') {
161
+ row[key] = Readable.from(_sliceBase64(row[key]), { objectMode: false })
162
+ return
163
+ }
145
164
  }
146
165
 
147
166
  const _getProcessorFn = (req, errors, assertMap) => {
@@ -192,6 +211,7 @@ const _pick = element => {
192
211
  // REVISIT: cleanse @Core.Immutable
193
212
  // should be a db feature, as we cannot handle completely on service level (cf. deep update)
194
213
  // -> add to attic env behavior once new dbs handle this
214
+ // also happens in validate but because of draft activate we have to do it twice (where cleansing is suppressed)
195
215
  if (element['@Core.Immutable']) {
196
216
  categories.push('immutable')
197
217
  }
@@ -200,10 +220,6 @@ const _pick = element => {
200
220
  categories.push('uuid')
201
221
  }
202
222
 
203
- if (element['@Core.IsMediaType']) {
204
- categories.push('stream')
205
- }
206
-
207
223
  if (
208
224
  element._isAssociationStrict &&
209
225
  !element.on && // managed assoc
@@ -213,6 +229,14 @@ const _pick = element => {
213
229
  categories.push('@assert.target')
214
230
  }
215
231
 
232
+ if (element.type === 'cds.Binary' && !cds.env.features.base64_binaries) {
233
+ categories.push('binary')
234
+ }
235
+
236
+ if (element.type === 'cds.LargeBinary' && !cds.env.features.base64_binaries) {
237
+ categories.push('largebinary')
238
+ }
239
+
216
240
  if (categories.length) return { categories }
217
241
  }
218
242
 
@@ -226,7 +250,7 @@ async function commonGenericInput(req) {
226
250
 
227
251
  // validate data
228
252
  if (cds.env.features.cds_validate) {
229
- const assertOptions = { mandatories: req.event === 'CREATE' || req.req?.method === 'PUT' }
253
+ const assertOptions = { mandatories: req.event === 'CREATE' || req.method === 'PUT' }
230
254
 
231
255
  const _is_activate = req._?.event === 'draftActivate' && cds.env.features.preserve_computed !== false
232
256
  const _is_create_after_new = req.target.isDraft && req.event === 'CREATE'
@@ -346,7 +370,10 @@ function _actionFunctionHandler(req) {
346
370
 
347
371
  // validate data
348
372
  if (cds.env.features.cds_validate) {
349
- const assertOptions = { mandatories: true }
373
+ const assertOptions = {
374
+ mandatories: true,
375
+ cleanse: !(operation.kind === 'action' || operation.kind === 'function')
376
+ }
350
377
  let errs = cds.validate(data, operation, assertOptions)
351
378
  if (errs) {
352
379
  if (errs.length === 1) throw errs[0]
@@ -361,6 +388,14 @@ function _actionFunctionHandler(req) {
361
388
  const arrayData = Array.isArray(data) ? data : [data]
362
389
  for (const row of arrayData) _processActionFunction(row, operation.params, errors, req.event, this)
363
390
  if (errors.length) for (const error of errors) req.error(error)
391
+
392
+ // convert binaries
393
+ operation.params &&
394
+ !cds.env.features.base64_binaries &&
395
+ Object.keys(operation.params).forEach(key => {
396
+ if (operation.params[key].type === 'cds.Binary' && typeof data[key] === 'string')
397
+ data[key] = Buffer.from(data[key], 'base64')
398
+ })
364
399
  }
365
400
 
366
401
  commonGenericInput._initial = true
@@ -1,3 +1,6 @@
1
+ //
2
+ // REVISIT: Not used any longer -> move to @sap/cds-attic ...
3
+ //
1
4
  const fs = require('fs')
2
5
  const path = require('path')
3
6
 
@@ -53,7 +56,7 @@ function init(locale, file) {
53
56
  i18ns[locale] = props
54
57
  }
55
58
 
56
- init('default', path.join(__dirname, 'messages.properties'))
59
+ init('default', path.resolve(__dirname, '../../../../_i18n/messages.properties'))
57
60
  init('')
58
61
 
59
62
  module.exports = (key, locale = '', args = {}) => {
@@ -69,18 +72,8 @@ module.exports = (key, locale = '', args = {}) => {
69
72
 
70
73
  // for locale OR app default OR cds default
71
74
  let text = i18ns[locale][key] || i18ns[''][key] || i18ns.default[key]
72
- if (!text) return
73
- // best effort replacement
74
- try {
75
- const matches = text.match(/\{[\w][\w]*\}/g) || []
76
- for (const match of matches) {
77
- const arg = args[match.slice(1, -1)]
78
- const argtext = i18ns[locale][arg] || i18ns[''][arg] || i18ns.default[arg]
79
- text = text.replace(match, argtext || (arg != null ? arg : 'NULL'))
80
- }
81
- } catch {
82
- // nothing to do
83
- }
84
-
85
- return text
75
+ return text?.replace(/{(\w+)}/g, (_, k) => {
76
+ let x = args[k]
77
+ return i18ns[locale][x] || i18ns[''][x] || i18ns.default[x] || (x ?? 'NULL') // REVISIT: i'm afraid this twofold localization is a rather bad idea
78
+ })
86
79
  }
@@ -2,6 +2,7 @@ const cds = require('../../cds')
2
2
  const { DRAFT_COLUMNS_MAP } = require('../constants/draft')
3
3
 
4
4
  const _deepEqual = (val1, val2) => {
5
+ if (Buffer.isBuffer(val1) && Buffer.isBuffer(val2)) return val1.equals(val2)
5
6
  if (val1 && typeof val1 === 'object' && val2 && typeof val2 === 'object') {
6
7
  for (const key in val1) {
7
8
  if (!_deepEqual(val1[key], val2[key])) return false
@@ -179,7 +180,15 @@ const _iteratePropsInNewEntry = (newEntry, keys, result, oldEntry, entity, opts,
179
180
  }
180
181
 
181
182
  // if value did not change --> ignored
182
- if (newEntry[prop] === (oldEntry && oldEntry[prop])) continue
183
+ if (
184
+ (Buffer.isBuffer(newEntry[prop]) &&
185
+ oldEntry &&
186
+ Buffer.isBuffer(oldEntry[prop]) &&
187
+ newEntry[prop].equals(oldEntry[prop])) ||
188
+ newEntry[prop] === oldEntry?.[prop]
189
+ ) {
190
+ continue
191
+ }
183
192
 
184
193
  // existing immutable --> ignored
185
194
  if (oldEntry && cache.immutables.includes(prop)) continue
@@ -715,7 +715,7 @@ const resolveView = (query, model, service) => {
715
715
 
716
716
  // If the query is a projection, one must follow it
717
717
  // to let the underlying service know its true entity.
718
- if (query.cmd) _event = query.cmd
718
+ if (query.kind) _event = query.kind
719
719
  else if (query.SELECT) _event = 'SELECT'
720
720
  else if (query.INSERT) _event = 'INSERT'
721
721
  else if (query.UPSERT) _event = 'UPSERT'
@@ -624,20 +624,20 @@ cds.ApplicationService.prototype.handle = async function (req) {
624
624
  }
625
625
  }
626
626
 
627
- if (cds.env.features.odata_new_adapter) {
627
+ if (cds.env.features.odata_new_adapter && req.res) {
628
628
  const read_result = await _readAfterDraftAction.bind(this)({
629
629
  req,
630
630
  payload: res,
631
631
  action: 'draftActivate'
632
632
  })
633
- req.res?.set(
633
+ req.res.set(
634
634
  'location',
635
635
  '../' + calculateLocationHeader(req.target, this, read_result || { ...res, IsActiveEntity: true })
636
636
  )
637
637
  return read_result
638
- } else {
639
- return Object.assign(result, { IsActiveEntity: true })
640
638
  }
639
+
640
+ return Object.assign(result, { IsActiveEntity: true })
641
641
  }
642
642
 
643
643
  if (req.target.actions?.[req.event] && draftParams.IsActiveEntity === false) {
@@ -826,8 +826,12 @@ const Read = {
826
826
  }
827
827
  }
828
828
  Read.merge(query._target, actives, drafts, (row, other) => {
829
- if (other) Object.assign(row, other, { HasActiveEntity: false, HasDraftEntity: true })
830
- else
829
+ if (other) {
830
+ if ('DraftAdministrativeData' in other) row.DraftAdministrativeData = other.DraftAdministrativeData
831
+ if ('DraftAdministrativeData_DraftUUID' in other)
832
+ row.DraftAdministrativeData_DraftUUID = other.DraftAdministrativeData_DraftUUID
833
+ Object.assign(row, { HasActiveEntity: false, HasDraftEntity: true })
834
+ } else
831
835
  Object.assign(row, {
832
836
  HasActiveEntity: false,
833
837
  HasDraftEntity: false,
@@ -1235,44 +1239,61 @@ function _cleansed(query, model) {
1235
1239
  const q = _cleanseQuery(query, draftParams, model)
1236
1240
  if (query.SELECT) {
1237
1241
  const getDrafts = () => {
1238
- const draftsQuery = _cleanseQuery(query, {}, model) // could just clone `q` but the latter is ruined by database layer
1239
- draftsQuery._target = undefined
1240
- if (!draftsQuery.SELECT.from.ref) return // invalid draft request
1241
- const [root, ...tail] = draftsQuery.SELECT.from.ref
1242
+ // could just clone `query` but the latter is ruined by database layer
1243
+ const draftsQuery = _cleanseQuery(query, {}, model)
1244
+
1245
+ // set the target to null to ensure cds.infer(...) correctly infer the
1246
+ // target after query modifications
1247
+ draftsQuery._target = null
1248
+ let draftSelect = draftsQuery.SELECT
1249
+ let querySelect = query.SELECT
1250
+
1251
+ // in the $apply scenario, only the most inner nested SELECT data structure must be cleansed
1252
+ while (draftSelect.from.SELECT) {
1253
+ draftSelect = draftSelect.from.SELECT
1254
+ querySelect = querySelect.from.SELECT
1255
+ }
1256
+
1257
+ if (!draftSelect.from.ref) return // invalid draft request
1258
+
1259
+ const [root, ...tail] = draftSelect.from.ref
1242
1260
  const draft = model.definitions[root.id || root].drafts
1243
1261
  if (!draft) return
1244
- draftsQuery.SELECT.from = {
1262
+ draftSelect.from = {
1245
1263
  ref: [root.id ? { ...root, id: draft.name } : draft.name, ...tail]
1246
1264
  }
1247
1265
  cds.infer(draftsQuery, model.definitions)
1266
+
1248
1267
  // draftsQuery._target = draftsQuery._target?.drafts || draftsQuery._target
1249
- if (query.SELECT.columns && query._target.drafts) {
1268
+ if (querySelect.columns && query._target.drafts) {
1250
1269
  if (draftsQuery._target.isDraft)
1251
- draftsQuery.SELECT.columns = _cleanseCols(query.SELECT.columns, REDUCED_DRAFT_ELEMENTS, draft)
1252
- else draftsQuery.SELECT.columns = _cleanseCols(query.SELECT.columns, DRAFT_ELEMENTS, draft)
1270
+ draftSelect.columns = _cleanseCols(querySelect.columns, REDUCED_DRAFT_ELEMENTS, draft)
1271
+ else draftSelect.columns = _cleanseCols(querySelect.columns, DRAFT_ELEMENTS, draft)
1253
1272
  }
1254
1273
 
1255
- if (query.SELECT.where && query._target.drafts) {
1274
+ if (querySelect.where && query._target.drafts) {
1256
1275
  if (draftsQuery._target.isDraft)
1257
- draftsQuery.SELECT.where = _cleanseWhere(query.SELECT.where, {}, DRAFT_ELEMENTS_WITHOUT_HASACTIVE)
1258
- else draftsQuery.SELECT.where = _cleanseWhere(query.SELECT.where, {}, DRAFT_ELEMENTS)
1276
+ draftSelect.where = _cleanseWhere(querySelect.where, {}, DRAFT_ELEMENTS_WITHOUT_HASACTIVE)
1277
+ else draftSelect.where = _cleanseWhere(querySelect.where, {}, DRAFT_ELEMENTS)
1259
1278
  }
1260
1279
 
1261
- if (query.SELECT.orderBy && query._target.drafts) {
1280
+ if (querySelect.orderBy && query._target.drafts) {
1262
1281
  if (draftsQuery._target.isDraft)
1263
- draftsQuery.SELECT.orderBy = _cleanseWhere(query.SELECT.orderBy, {}, REDUCED_DRAFT_ELEMENTS)
1264
- else draftsQuery.SELECT.orderBy = _cleanseWhere(query.SELECT.orderBy, {}, DRAFT_ELEMENTS)
1282
+ draftSelect.orderBy = _cleanseWhere(querySelect.orderBy, {}, REDUCED_DRAFT_ELEMENTS)
1283
+ else draftSelect.orderBy = _cleanseWhere(querySelect.orderBy, {}, DRAFT_ELEMENTS)
1265
1284
  }
1266
1285
 
1267
1286
  if (draftsQuery._target.name.endsWith('.DraftAdministrativeData')) {
1268
- draftsQuery.SELECT.columns = _tweakAdminCols(draftsQuery.SELECT.columns)
1287
+ draftSelect.columns = _tweakAdminCols(draftSelect.columns)
1269
1288
  } else if (draftsQuery._target?.name.endsWith('.drafts')) {
1270
- draftsQuery.SELECT.columns = _tweakAdminExpand(draftsQuery.SELECT.columns)
1289
+ draftSelect.columns = _tweakAdminExpand(draftSelect.columns)
1271
1290
  }
1291
+
1272
1292
  draftsQuery[DRAFT_PARAMS] = draftParams
1273
1293
  Object.defineProperty(q, '_drafts', { value: draftsQuery })
1274
1294
  return draftsQuery
1275
1295
  }
1296
+
1276
1297
  Object.defineProperty(q, '_drafts', {
1277
1298
  configurable: true,
1278
1299
  get() {
@@ -1481,6 +1502,10 @@ function expandStarStar(target, draftActivate, recursion = new Map()) {
1481
1502
  return columns
1482
1503
  }
1483
1504
 
1505
+ async function onNewCleanse(req) {
1506
+ cds.validate(req.data, req.target, {})
1507
+ }
1508
+ onNewCleanse._initial = true
1484
1509
  async function onNew(req) {
1485
1510
  LOG.debug('new draft')
1486
1511
 
@@ -1658,17 +1683,17 @@ async function onEdit(req) {
1658
1683
  activeLockCQN[DRAFT_PARAMS] = draftParams
1659
1684
 
1660
1685
  try {
1661
- await this.run(activeLockCQN)
1686
+ await activeLockCQN
1662
1687
  } catch (error) {
1663
1688
  LOG._debug && LOG.debug('Failed to acquire database lock:', error)
1664
- const draft = await this.run(existingDraft)
1689
+ const draft = await existingDraft
1665
1690
  if (draft) req.reject({ code: 409, statusCode: 409, message: 'DRAFT_ALREADY_EXISTS' })
1666
1691
  req.reject({ code: 409, statusCode: 409, message: 'ENTITY_LOCKED' })
1667
1692
  }
1668
1693
 
1669
1694
  const cqns = [
1670
1695
  cds.env.fiori.read_actives_from_db ? this._datasource.run(selectActiveCQN) : this.run(selectActiveCQN),
1671
- this.run(existingDraft)
1696
+ existingDraft
1672
1697
  ]
1673
1698
 
1674
1699
  ;[res, draft] = await _promiseAll(cqns)
@@ -1739,7 +1764,7 @@ async function onEdit(req) {
1739
1764
  req.res.status(201)
1740
1765
  }
1741
1766
 
1742
- if (cds.env.features.odata_new_adapter) {
1767
+ if (cds.env.features.odata_new_adapter && req.res) {
1743
1768
  const read_result = await _readAfterDraftAction.bind(this)({
1744
1769
  req,
1745
1770
  payload: res,
@@ -1877,6 +1902,30 @@ const _readAfterDraftAction = async function ({ req, payload, action }) {
1877
1902
 
1878
1903
  module.exports = {
1879
1904
  impl() {
1905
+ if (!this.new)
1906
+ this.new = function (entity, data) {
1907
+ return this.send({ event: 'NEW', query: INSERT.into(entity).entries(data ?? {}) })
1908
+ }
1909
+ if (!this.cancel)
1910
+ this.cancel = function (entity, data) {
1911
+ return this.send({ event: 'CANCEL', query: DELETE(entity, data) })
1912
+ }
1913
+ if (!this.edit)
1914
+ this.edit = function (entity, data) {
1915
+ if ((typeof entity === 'string' && entity.endsWith('.drafts')) || entity.isDraft)
1916
+ throw new Error('Action `edit` must be called on the active entity')
1917
+ return this.send({ event: 'EDIT', query: SELECT.from(entity, data).where({ IsActiveEntity: true }) })
1918
+ // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ makes sure draftParams are set
1919
+ }
1920
+ if (!this.save)
1921
+ this.save = function (entity, data) {
1922
+ // a bit fishy to demand registering it on drafts since `SAVE` is usually just a shortcut for `['CREATE', 'UPDATE']`
1923
+ // and is typically registered for active entities. Hence, we allow to register it on both and redirect to drafts
1924
+ const _entity =
1925
+ typeof entity === 'string' ? (entity.endsWith('.drafts') ? entity : entity + '.drafts') : entity?.drafts
1926
+ return this.send({ event: 'draftActivate', query: SELECT.from(_entity, data) })
1927
+ }
1928
+
1880
1929
  if (!this._datasource) this._datasource = cds.db
1881
1930
 
1882
1931
  function _wrapped(handler, isActiveEntity) {
@@ -1885,10 +1934,12 @@ module.exports = {
1885
1934
  return next.call(this)
1886
1935
  return handler.call(this, req, next)
1887
1936
  }
1937
+ if (handler._initial) fn._initial = true
1888
1938
  return fn
1889
1939
  }
1890
1940
 
1891
1941
  // Also runs those handlers if they're annotated with @odata.draft.enabled through extensibility
1942
+ this.before('NEW', '*', _wrapped(onNewCleanse, false))
1892
1943
  this.on('NEW', '*', _wrapped(onNew, false))
1893
1944
  this.on('EDIT', '*', _wrapped(onEdit, true))
1894
1945
  this.on('CANCEL', '*', _wrapped(onCancel, false))
@@ -15,9 +15,13 @@ class EndpointRegistry {
15
15
  if (cds.requires.auth.impl) {
16
16
  cds.app.use(basePath, cds.middlewares.before) // contains auth, trace, context
17
17
  } else {
18
- const jwt_auth = require('../../../../lib/auth/jwt-auth.js')
18
+ const jwt_auth = require('../../../../lib/srv/middlewares/auth/jwt-auth.js')
19
19
  cds.app.use(basePath, cds.middlewares.context())
20
20
  cds.app.use(basePath, jwt_auth(cds.requires.auth))
21
+ cds.app.use(basePath, (err, req, res, next) => {
22
+ if (err === 401) res.send(401)
23
+ else next(err)
24
+ })
21
25
  }
22
26
  // unsuccessful auth doesn't automatically reject!
23
27
  cds.app.use(basePath, (req, res, next) => {
@@ -18,7 +18,7 @@ class KafkaService extends cds.MessagingService {
18
18
 
19
19
  if (!this.options.local && !this.options.credentials) {
20
20
  throw new Error(
21
- 'No Kafka credentials found.\n\nHint: You need to bind your application to a Kafa service instance.'
21
+ 'No Kafka credentials found.\n\nHint: You need to bind your application to a Kafka service instance.'
22
22
  )
23
23
  }
24
24
 
@@ -144,6 +144,9 @@ module.exports = {
144
144
  cqn = enhanceCqn(cqn, options)
145
145
  }
146
146
 
147
+ // REVISIT: SELECT.from._params is a temporary hack
148
+ if (cqn.SELECT?.from?._params) delete cqn.SELECT.from._params
149
+
147
150
  return cqn
148
151
  },
149
152
 
@@ -58,9 +58,10 @@ module.exports = (adapter, isUpsert) => {
58
58
  return service
59
59
  .run(() => {
60
60
  return service.dispatch(cdsReq).then(result => {
61
- // REVISIT: shouldn't read after write be the default behavior (that could be suppressed)?
62
- // generic handlers indicate that read after write is required
63
- if (cdsReq._.readAfterWrite) return _readAfterWrite(cdsReq)
61
+ // cdsReq._.readAfterWrite is only true if generic handler served the request
62
+ // If minimal requested and not etag, skip read after write
63
+ if (cdsReq._.readAfterWrite && (target._etag || getPreferReturnHeader(req) !== 'minimal'))
64
+ return _readAfterWrite(cdsReq)
64
65
  return result
65
66
  })
66
67
  })
@@ -75,10 +76,11 @@ module.exports = (adapter, isUpsert) => {
75
76
  res.set('location', calculateLocationHeader(cdsReq.target, service, result || cdsReq.data))
76
77
  }
77
78
 
78
- const isMinimal = getPreferReturnHeader(req) === 'minimal'
79
- postProcess(cdsReq.target, model, result, isMinimal)
79
+ const preference = getPreferReturnHeader(req)
80
+ postProcess(cdsReq.target, model, result, preference === 'minimal')
80
81
  if (result?.$etag) res.set('ETag', result.$etag) //> must be done after post processing
81
- if (isMinimal) return res.sendStatus(204)
82
+ if (preference === 'minimal') return res.append('Preference-Applied', 'return=minimal').sendStatus(204)
83
+ else if (preference === 'representation') res.append('Preference-Applied', 'return=representation')
82
84
 
83
85
  const metadata = getODataMetadata(query, { result })
84
86
  result = getODataResult(result, metadata)
@@ -1,13 +1,7 @@
1
1
  const cds = require('../../../')
2
2
  const { UPDATE } = cds.ql
3
3
 
4
- const {
5
- calculateLocationHeader,
6
- handleSapMessages,
7
- getPreferReturnHeader,
8
- extractIfNoneMatch,
9
- isStream
10
- } = require('../utils')
4
+ const { handleSapMessages, getPreferReturnHeader, extractIfNoneMatch, isStream } = require('../utils')
11
5
  const getODataMetadata = require('../utils/metadata')
12
6
  const postProcess = require('../utils/postProcess')
13
7
  const readAfterWrite4 = require('../utils/readAfterWrite')
@@ -65,7 +59,9 @@ module.exports = adapter => {
65
59
  throw Object.assign(new Error(`Method ${req.method} is not allowed for entity collections`), { statusCode: 405 })
66
60
  }
67
61
 
68
- if (_propertyAccess && req.method === 'PATCH') {
62
+ const _isStream = isStream(req._query)
63
+
64
+ if (_propertyAccess && req.method === 'PATCH' && !_isStream) {
69
65
  throw Object.assign(new Error(`Method ${req.method} is not allowed for properties`), { statusCode: 405 })
70
66
  }
71
67
 
@@ -78,7 +74,6 @@ module.exports = adapter => {
78
74
  if (!_propertyAccess) Object.assign(data, keys)
79
75
 
80
76
  const _isDraft = target.drafts && data.IsActiveEntity !== true
81
-
82
77
  // query
83
78
  let query = UPDATE.entity(from).with(data)
84
79
 
@@ -86,7 +81,14 @@ module.exports = adapter => {
86
81
  const headers = { ...cds.context.http.req.headers, ...req.headers }
87
82
 
88
83
  // we need a cds.Request for multiple reasons, incl. params, headers, sap-messages, read after write, ...
89
- const cdsReq = adapter.request4({ method: req.method, query, params, headers, req, res })
84
+ const cdsReq = adapter.request4({
85
+ method: _propertyAccess ? 'PATCH' : req.method,
86
+ query,
87
+ params,
88
+ headers,
89
+ req,
90
+ res
91
+ })
90
92
 
91
93
  // rewrite event for draft-enabled entities
92
94
  if (_isDraft) cdsReq.event = 'PATCH'
@@ -99,10 +101,13 @@ module.exports = adapter => {
99
101
  return service
100
102
  .run(() => {
101
103
  return service.dispatch(cdsReq).then(result => {
102
- // REVISIT: shouldn't read after write be the default behavior (that could be suppressed)?
103
- // generic handlers indicate that read after write is required
104
- // REVISIT: what does target._etag have to do with it?
105
- if (cdsReq._.readAfterWrite && !(_propertyAccess && !target._etag)) return _readAfterWrite(cdsReq)
104
+ // cdsReq._.readAfterWrite is only true if generic handler served the request
105
+ // If minimal requested or property access and not etag, skip read after write
106
+ if (
107
+ cdsReq._.readAfterWrite &&
108
+ (target._etag || (!_propertyAccess && getPreferReturnHeader(req) !== 'minimal'))
109
+ )
110
+ return _readAfterWrite(cdsReq)
106
111
  return result
107
112
  })
108
113
  })
@@ -112,16 +117,14 @@ module.exports = adapter => {
112
117
  // case: read after write returns no results, e.g., due to auth (academic but possible)
113
118
  if (result == null) return res.sendStatus(204)
114
119
 
115
- const isMinimal = getPreferReturnHeader(req) === 'minimal'
116
- postProcess(cdsReq.target, model, result, isMinimal)
120
+ const preference = getPreferReturnHeader(req)
121
+ postProcess(cdsReq.target, model, result, preference === 'minimal')
117
122
 
118
- if (isMinimal && !target._isSingleton) {
119
- // determine calculation based on result with req.data as fallback
120
- res.set('location', calculateLocationHeader(cdsReq.target, service, result || cdsReq.data))
121
- }
122
123
  if (result?.$etag) res.set('ETag', result.$etag) //> must be done after post processing
123
124
 
124
- if (isMinimal || (_propertyAccess && result[_propertyAccess] == null) || isStream(req._query)) {
125
+ if (preference === 'minimal') res.append('Preference-Applied', 'return=minimal')
126
+ else if (preference === 'representation') res.append('Preference-Applied', 'return=representation')
127
+ if (preference === 'minimal' || (_propertyAccess && result[_propertyAccess] == null) || isStream(req._query)) {
125
128
  return res.sendStatus(204)
126
129
  }
127
130
 
@@ -328,6 +328,10 @@ function _processSegments(from, model, namespace, cqn, protocol) {
328
328
  keyCount += addRefToWhereIfNecessary(ref[i].where, current)
329
329
  _resolveAliasesInXpr(ref[i].where, current)
330
330
  _processWhere(ref[i].where, current)
331
+ } else {
332
+ // parentheses are missing
333
+ const msg = `Invalid call to "${current.name}". Parentheses are missing`
334
+ throw cds.error(msg, { code: '400', statusCode: 400 })
331
335
  }
332
336
 
333
337
  _addDefaultParams(ref[i], current)
@@ -381,7 +385,18 @@ function _processSegments(from, model, namespace, cqn, protocol) {
381
385
  }
382
386
 
383
387
  ref[i] = { operation: current.name }
388
+
384
389
  if (params) ref[i].args = _getDataFromParams(params, current)
390
+ // REVISIT: SELECT.from._params is a temporary hack
391
+ else if (from._params && current.kind === 'function') {
392
+ // only take known params to allow additional instructions like sap-language, etc.
393
+ ref[i].args = current['@open']
394
+ ? Object.assign({}, from._params)
395
+ : Object.keys(from._params).reduce((acc, cur) => {
396
+ if (current.params && cur in current.params) acc[cur] = from._params[cur]
397
+ return acc
398
+ }, {})
399
+ }
385
400
  if (current.returns && current.returns._type) one = true
386
401
 
387
402
  if (current.returns) {
@@ -572,8 +587,6 @@ const _checkAllKeysProvided = (params, entity) => {
572
587
  // view with params
573
588
  if (params === undefined) {
574
589
  throw cds.error(`Invalid call to "${entity.name}". You need to navigate to Set`, { code: '400', statusCode: 400 })
575
- } else if (Object.keys(params).length === 0) {
576
- throw new Error('KEY_EXPECTED')
577
590
  }
578
591
 
579
592
  keysOfEntity = Object.keys(entity.params)
@@ -504,11 +504,11 @@
504
504
  where_clause = p:( n:NOT? {return n?[n]:[]} )(
505
505
  OPEN xpr:where_clause CLOSE {p.push({xpr})}
506
506
  / comp:comparison {p.push(...comp)}
507
- / lambda:lambda {
508
- if (p[p.length - 1] === 'not' && lambda[0] === 'not') {
509
- p.push('(', ...lambda, ')')
507
+ / xpr:lambda {
508
+ if (p[p.length - 1] === 'not' && xpr[0] === 'not') {
509
+ p.push({ xpr })
510
510
  } else {
511
- p.push(...lambda)
511
+ p.push(...xpr)
512
512
  }
513
513
  }
514
514
  / func:boolish {p.push(func)}
@@ -631,8 +631,25 @@
631
631
 
632
632
  aliasedParamVal = val / jsonObject / jsonArray / "[" list:innerListParam "]" { return { list } }
633
633
 
634
- custom
635
- = [a-zA-Z0-9-_.~!]+ ("=" [^&]*)?
634
+ custom = k:$([a-zA-Z0-9-_.~!\[\]]+) "="? v:$([^&]*)? {
635
+ // normalize value
636
+ if (v === 'null') v = null
637
+ else if (v === 'true') v = true
638
+ else if (v === 'false') v = false
639
+ // set value in structure
640
+ // REVISIT: SELECT.from._params is a temporary hack
641
+ const params = SELECT.from._params ??= {}
642
+ let t = params
643
+ let x = k.match(/^(\w+)\[(.*)\]$/)
644
+ while (x) {
645
+ if (!(x[1] in t)) t[x[1]] = x[2] === '' ? [] : {}
646
+ t = t[x[1]]
647
+ k = x[2]
648
+ x = k.match(/^(\w+)\[(.*)\]$/)
649
+ }
650
+ if (Array.isArray(t)) t.push(v)
651
+ else t[k] = v
652
+ }
636
653
 
637
654
  aliasedParam "an aliased parameter (@param)" = "@" i:identifier { return "@" + i }
638
655
  aliasedParamEqualsVal = alias:aliasedParam "=" !aliasedParam value:aliasedParamVal {
@@ -912,7 +929,7 @@
912
929
  = $( [a-zA-Z0-9-"."_~!$'()*+,;=:@"/""?"]+ )
913
930
 
914
931
  binary "a binary" // > url-safe base64
915
- = "binary'" s:$([a-zA-Z0-9-_]+ ("=="/"=")?) "'" { return standardBase64(s) }
932
+ = "binary'" s:$([a-zA-Z0-9-_]+ ("=="/"=")?) "'" { return cds.env.features.base64_binaries ? standardBase64(s) : Buffer.from(s, 'base64') }
916
933
 
917
934
  //
918
935
  // ---------- Punctuation ----------