@sap/cds 9.1.0 → 9.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/bin/deploy.js +29 -0
  3. package/bin/serve.js +1 -5
  4. package/lib/compile/etc/csv.js +11 -6
  5. package/lib/compile/load.js +8 -5
  6. package/lib/compile/to/hdbtabledata.js +1 -1
  7. package/lib/dbs/cds-deploy.js +0 -31
  8. package/lib/env/cds-env.js +2 -1
  9. package/lib/env/cds-requires.js +3 -0
  10. package/lib/env/schemas/cds-rc.js +4 -0
  11. package/lib/index.js +38 -38
  12. package/lib/log/cds-error.js +12 -11
  13. package/lib/log/format/json.js +1 -1
  14. package/lib/ql/SELECT.js +31 -0
  15. package/lib/ql/UPDATE.js +3 -1
  16. package/lib/ql/resolve.js +1 -1
  17. package/lib/req/context.js +1 -1
  18. package/lib/req/validate.js +16 -17
  19. package/lib/srv/cds.Service.js +18 -28
  20. package/lib/srv/middlewares/auth/ias-auth.js +31 -4
  21. package/lib/srv/middlewares/auth/jwt-auth.js +11 -1
  22. package/lib/srv/srv-models.js +1 -1
  23. package/lib/srv/srv-tx.js +2 -2
  24. package/lib/utils/cds-utils.js +35 -2
  25. package/lib/utils/csv-reader.js +1 -1
  26. package/lib/utils/version.js +18 -0
  27. package/libx/_runtime/cds.js +1 -1
  28. package/libx/_runtime/common/aspects/any.js +1 -23
  29. package/libx/_runtime/common/generic/input.js +111 -50
  30. package/libx/_runtime/common/generic/sorting.js +1 -1
  31. package/libx/_runtime/common/utils/draft.js +1 -1
  32. package/libx/_runtime/common/utils/entityFromCqn.js +1 -1
  33. package/libx/_runtime/common/utils/propagateForeignKeys.js +1 -1
  34. package/libx/_runtime/common/utils/resolveView.js +2 -2
  35. package/libx/_runtime/common/utils/rewriteAsterisks.js +2 -2
  36. package/libx/_runtime/common/utils/structured.js +2 -2
  37. package/libx/_runtime/common/utils/templateProcessor.js +0 -5
  38. package/libx/_runtime/common/utils/vcap.js +1 -1
  39. package/libx/_runtime/fiori/lean-draft.js +63 -23
  40. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +3 -2
  41. package/libx/_runtime/messaging/file-based.js +2 -1
  42. package/libx/_runtime/messaging/service.js +1 -1
  43. package/libx/_runtime/remote/utils/client.js +1 -1
  44. package/libx/common/assert/utils.js +2 -12
  45. package/libx/common/utils/streaming.js +4 -9
  46. package/libx/http/location.js +1 -0
  47. package/libx/odata/index.js +1 -1
  48. package/libx/odata/middleware/batch.js +6 -1
  49. package/libx/odata/middleware/create.js +1 -1
  50. package/libx/odata/middleware/error.js +22 -19
  51. package/libx/odata/middleware/stream.js +1 -1
  52. package/libx/odata/parse/cqn2odata.js +16 -10
  53. package/libx/odata/parse/grammar.peggy +8 -4
  54. package/libx/odata/parse/parser.js +1 -1
  55. package/libx/odata/utils/index.js +1 -1
  56. package/libx/queue/index.js +3 -3
  57. package/libx/rest/RestAdapter.js +1 -2
  58. package/libx/rest/middleware/create.js +5 -2
  59. package/package.json +2 -2
  60. package/server.js +1 -1
  61. package/bin/deploy/to-hana.js +0 -1
  62. package/lib/utils/check-version.js +0 -9
  63. package/lib/utils/unit.js +0 -19
  64. package/libx/_runtime/cds-services/util/assert.js +0 -181
  65. package/libx/_runtime/types/api.js +0 -129
  66. package/libx/common/assert/validation.js +0 -109
@@ -24,9 +24,9 @@ const _config_to_ms = (config, _default) => {
24
24
  const timeout = cds.env.fiori?.[config]
25
25
  let timeout_ms
26
26
  if (timeout === true) {
27
- timeout_ms = cds.utils._unit.time2ms(_default)
27
+ timeout_ms = cds.utils.ms4(_default)
28
28
  } else if (typeof timeout === 'string') {
29
- timeout_ms = cds.utils._unit.time2ms(timeout)
29
+ timeout_ms = cds.utils.ms4(timeout)
30
30
  if (!timeout_ms)
31
31
  throw new Error(`
32
32
  ${timeout} is an invalid value for \`cds.fiori.${config}\`.
@@ -384,26 +384,29 @@ const _replaceStreams = result => {
384
384
 
385
385
  const _compileUpdatedDraftMessages = (newMessages, persistedMessages, requestData, draftsRef) => {
386
386
  const prefixRef = []
387
- const simplePathElements = []
388
- let targetEntity = cds.infer.ref([draftsRef[0]], cds.context.tx.model)
389
387
 
390
388
  // Determine the path prefix required for a fully qualified validation message 'target'
389
+ let targetEntity = cds.context.tx.model.definitions[draftsRef[0].id || draftsRef[0]]
391
390
  for (let refIdx = 0; refIdx < draftsRef.length; refIdx++) {
392
- let dRef = draftsRef[refIdx]
393
- if (refIdx > 0) targetEntity = targetEntity.elements[draftsRef[refIdx].id || draftsRef[refIdx]]._target
391
+ const dRef = draftsRef[refIdx],
392
+ dRefId = dRef.id ?? dRef
394
393
 
394
+ // Determine entity, referenced by the processesd segment of 'draftsRef'
395
+ if (refIdx > 0) targetEntity = targetEntity.elements[dRefId]._target
396
+
397
+ // Construct 'prefixRef' segment
395
398
  const pRef = { where: [] }
396
399
 
397
- const dRefId = dRef.id ?? dRef
398
400
  if (dRefId === targetEntity.name) {
399
401
  if (!targetEntity.isDraft) pRef.id = targetEntity.name.replace(`${targetEntity._service.name}.`, '')
400
402
  else pRef.id = targetEntity.actives.name.replace(`${targetEntity.actives._service.name}.`, '')
401
403
  } else pRef.id = dRefId
402
- simplePathElements.push(pRef.id)
403
404
 
404
405
  if (targetEntity.isDraft) targetEntity = targetEntity.actives
405
406
 
406
407
  if (typeof dRef === 'string' || !dRef.where?.length) {
408
+ // In case of CREATE: The WHERE for the created entity must be constructed for use in 'prefixRef'
409
+
407
410
  for (const k in targetEntity.keys) {
408
411
  const key = targetEntity.keys[k]
409
412
  let v = requestData[key.name]
@@ -414,6 +417,7 @@ const _compileUpdatedDraftMessages = (newMessages, persistedMessages, requestDat
414
417
  pRef.where.push(key.name, '=', v)
415
418
  }
416
419
  } else {
420
+ // 'dRef.where' regards the draft and will never contain 'IsActiveEntity=false'
417
421
  pRef.where.push(...dRef.where, 'and', 'IsActiveEntity', '=', false)
418
422
  }
419
423
 
@@ -424,23 +428,30 @@ const _compileUpdatedDraftMessages = (newMessages, persistedMessages, requestDat
424
428
 
425
429
  // Collect messages that were created during the most recent validation run
426
430
  const newMessagesByCodeAndTarget = newMessages.reduce((acc, message) => {
427
- message.simplePathElements = [...simplePathElements]
431
+ message.numericSeverity ??= 4
428
432
 
429
- const messagePrefixRef = [...prefixRef]
433
+ // Handle validation messages produced during draftActivate, that went through error normalization already
434
+ // > We must not store pre-localized data in DraftAdministrativeData.DraftMessages
430
435
  const messageTarget = message.target.startsWith('in/') ? message.target.slice(3) : message.target
436
+ if (message.code) {
437
+ message.message = message.code
438
+ delete message.code
439
+ }
440
+
441
+ // Process the message target produced by validation
442
+ // > The message target contains the relative path of the erroneous entity
443
+ // > For a message specific 'prefixRef', this info must be added to the entity 'prefixRef'
444
+ const messagePrefixRef = [...prefixRef]
431
445
  const messageTargetRef = cds.odata.parse(messageTarget).SELECT.from.ref
432
446
  message.target = messageTargetRef.pop()
433
447
 
434
448
  for (const tRef of messageTargetRef) {
435
- message.simplePathElements.push(tRef.id)
436
449
  messagePrefixRef.push(tRef)
437
450
  if ((tRef.where ??= []).some(w => w === 'IsActiveEntity' || w.ref?.[0] === 'IsActiveEntity')) continue
438
- if (tRef.where.length > 0) tRef.where.push('and')
439
- tRef.where.push('IsActiveEntity', '=', false)
451
+ if (tRef.where.length > 0) tRef.where.push('and', 'IsActiveEntity', '=', false)
440
452
  }
441
453
 
442
454
  message.prefix = cds.odata.urlify({ SELECT: { from: { ref: messagePrefixRef } } }).path
443
- message.numericSeverity ??= 4
444
455
 
445
456
  nextMessages.push(message)
446
457
 
@@ -450,14 +461,18 @@ const _compileUpdatedDraftMessages = (newMessages, persistedMessages, requestDat
450
461
  const draftMessageTargetPrefix = cds.odata.urlify({ SELECT: { from: { ref: prefixRef } } }).path
451
462
 
452
463
  // Merge new messages with persisted ones & eliminate outdated ones
464
+ const simplePathElements = prefixRef.map(pRef => pRef.id)
453
465
  for (const message of persistedMessages) {
466
+ // Drop persisted draft messages that are replaced by new ones
454
467
  if (newMessagesByCodeAndTarget.has(`${message.message}:${message.prefix}:${message.target}`)) continue
468
+
469
+ // Drop persisted draft messages where the value of the target field changed without a new error
455
470
  if (message.prefix === draftMessageTargetPrefix && requestData[message.target] !== undefined) continue
456
- if (
457
- message.simplePathElements.length > simplePathElements.length &&
458
- requestData[message.simplePathElements[simplePathElements.length]] !== undefined
459
- )
460
- continue
471
+
472
+ // Drop persisted draft messages, whose target's use navigations, where the value of the target field changed without a new error
473
+ const messageSimplePathElements = message.prefix.replaceAll(/\([^(]*\)/g, '').split('/')
474
+ if (messageSimplePathElements.length > simplePathElements.length)
475
+ if (requestData[messageSimplePathElements[simplePathElements.length]] !== undefined) continue
461
476
 
462
477
  nextMessages.push(message)
463
478
  }
@@ -843,6 +858,7 @@ const handle = async function (req) {
843
858
 
844
859
  // Remove draft artefacts from persistedDraft entry
845
860
  const DraftAdministrativeData_DraftUUID = res.DraftAdministrativeData_DraftUUID
861
+ const persistedDraftMessages = res.DraftAdministrativeData?.DraftMessages || []
846
862
  delete res.DraftAdministrativeData_DraftUUID
847
863
  delete res.DraftAdministrativeData
848
864
  const HasActiveEntity = res.HasActiveEntity
@@ -866,7 +882,29 @@ const handle = async function (req) {
866
882
  const upsertOptions = { headers: Object.assign({}, req.headers, { 'if-match': '*' }) }
867
883
 
868
884
  const _req = _newReq(req, upsertQuery, draftParams, upsertOptions)
869
- const result = await h.call(this, _req)
885
+
886
+ let result
887
+ try {
888
+ result = await h.call(this, _req)
889
+ } catch (error) {
890
+ if (cds.model.definitions['DRAFT.DraftAdministrativeData'].elements.DraftMessages && _req.errors)
891
+ _req.on('failed', async () => {
892
+ const nextDraftMessages = _compileUpdatedDraftMessages(
893
+ _req.errors.map(e => ({ ...e })),
894
+ persistedDraftMessages,
895
+ {},
896
+ draftRef
897
+ )
898
+
899
+ await cds.tx(async () => {
900
+ await UPDATE('DRAFT.DraftAdministrativeData')
901
+ .set({ DraftMessages: nextDraftMessages })
902
+ .where({ DraftUUID: DraftAdministrativeData_DraftUUID })
903
+ })
904
+ })
905
+
906
+ throw error
907
+ }
870
908
 
871
909
  // REVISIT: Remove feature flag dependency
872
910
  if (cds.env.fiori.move_media_data_in_db) {
@@ -1203,7 +1241,9 @@ const Read = {
1203
1241
  _fillIsActiveEntity(row, false, query._drafts._target)
1204
1242
 
1205
1243
  if (cds.model.definitions['DRAFT.DraftAdministrativeData'].elements.DraftMessages && row.DraftMessages?.length) {
1206
- const simplifiedMessagePrefix = query._drafts.SELECT.from.ref
1244
+ // Reduce persisted draft messages to the set of those that are part of the queried entities tree
1245
+
1246
+ const simplePath = query._drafts.SELECT.from.ref
1207
1247
  .map(refElement => {
1208
1248
  refElement = (refElement.id ?? refElement).split('.')
1209
1249
  if (refElement.length === 1) return refElement[0]
@@ -1213,7 +1253,8 @@ const Read = {
1213
1253
  .join('/')
1214
1254
 
1215
1255
  row.DraftMessages = row.DraftMessages.reduce((acc, msg) => {
1216
- if (!msg.simplePathElements.join('/').startsWith(simplifiedMessagePrefix)) return acc
1256
+ const messageSimplePath = msg.prefix.replaceAll(/\([^(]*\)/g, '')
1257
+ if (!messageSimplePath.startsWith(simplePath)) return acc
1217
1258
  msg.target = `/${msg.prefix}/${msg.target}`
1218
1259
  delete msg.prefix
1219
1260
  delete msg.simplePathElements
@@ -1221,7 +1262,6 @@ const Read = {
1221
1262
  return acc
1222
1263
  }, [])
1223
1264
 
1224
- // TODO: Messsages use i18n to map codes to textual error messages -> Replace!
1225
1265
  row.DraftMessages = getLocalizedMessages(row.DraftMessages, cds.context.http.req)
1226
1266
  }
1227
1267
  })
@@ -29,9 +29,10 @@ class EndpointRegistry {
29
29
  if (cds.context.user._is_anonymous) return res.status(401).end()
30
30
  next()
31
31
  })
32
- } else if (process.env.NODE_ENV === 'production') {
33
- LOG.warn('Messaging endpoints not secured')
34
32
  } else {
33
+ if (process.env.NODE_ENV === 'production') {
34
+ LOG.warn('Messaging endpoints not secured')
35
+ }
35
36
  // auth middlewares set cds.context.user
36
37
  cds.app.use(basePath, cds.middlewares.context())
37
38
  }
@@ -28,10 +28,11 @@ class FileBasedMessaging extends MessagingService {
28
28
  this.LOG._debug && this.LOG.debug('Emit', { topic: e, file: this.file })
29
29
  try {
30
30
  await fs.appendFile(this.file, `\n${e} ${JSON.stringify(_msg)}`)
31
+ unlock(this.file)
31
32
  } catch (e) {
32
33
  this.LOG._debug && this.LOG.debug('Error', e)
33
- } finally {
34
34
  unlock(this.file)
35
+ throw e
35
36
  }
36
37
  }
37
38
 
@@ -48,7 +48,7 @@ module.exports = class MessagingService extends cds.Service {
48
48
  srv.on(event, async msg => {
49
49
  const { data, headers } = msg
50
50
  const messaging = await cds.connect.to('messaging') // needed for potential outbox
51
- return messaging.tx(msg).emit({ event: topic, data, headers })
51
+ return messaging.emit({ event: topic, data, headers })
52
52
  })
53
53
  }
54
54
  })
@@ -231,7 +231,7 @@ const run = async (requestConfig, options) => {
231
231
  e.message = msg ? 'Error during request to remote service: ' + msg : 'Request to remote service failed.'
232
232
  const sanitizedError = _getSanitizedError(e, requestConfig, { suppressRemoteResponseBody })
233
233
  const err = Object.assign(new Error(e.message), { statusCode: 502, reason: sanitizedError })
234
- LOG._warn && LOG.warn(err)
234
+ cds.repl || LOG.warn(err)
235
235
  throw err
236
236
  }
237
237
 
@@ -50,21 +50,11 @@ function isBase64String(string, strict = false) {
50
50
  return true
51
51
  }
52
52
 
53
- const resolveCDSType = ele => {
54
- // REVISIT: when is ele._type not set and sufficient?
55
- if (ele._type?.match(/^cds\./)) return ele._type
56
- if (ele.type) {
57
- if (ele.type.match(/^cds\./)) return ele.type
58
- return resolveCDSType(ele.__proto__)
59
- }
60
- if (ele.items) return resolveCDSType(ele.items)
61
- return ele
62
- }
53
+
63
54
 
64
55
 
65
56
  module.exports = {
66
57
  getNormalizedDecimal,
67
58
  getTarget,
68
- isBase64String,
69
- resolveCDSType
59
+ isBase64String
70
60
  }
@@ -64,14 +64,9 @@ exports.collectStreamMetadata = (result, operation, query) => {
64
64
  }
65
65
 
66
66
  exports.getReadable = function readable4 (result) {
67
- if (result == null) return
68
- if (typeof result !== 'object') {
69
- const stream = new Readable()
70
- stream.push(result)
71
- stream.push(null)
72
- return stream
73
- }
67
+ if (result == null) return null
74
68
  if (result instanceof Readable) return result
75
- if (result.value) return readable4 (result.value) // REVISIT: for OData legacy only?
76
- if (Array.isArray(result)) return readable4 (result[0]) // compat // REVISIT: remove ?
69
+ if (Array.isArray(result)) return readable4 (result[0]) // compat
70
+ if (typeof result === 'object' && 'value' in result) return readable4 (result.value)
71
+ cds.error(500, `Unexpected result type for streaming: Expected stream.Readable or null but got ${typeof result}`)
77
72
  }
@@ -1,4 +1,5 @@
1
1
  module.exports = (target, srv, result, keys_as_segments) => {
2
+ if (typeof result !== 'object' || result == null || Array.isArray(result)) return
2
3
  const targetName = target.name.replace(`${srv.definition.name}.`, '')
3
4
  if (!target.keys) return targetName
4
5
 
@@ -1,6 +1,6 @@
1
1
  /** @typedef {import('../../lib/srv/cds.Service')} Service */
2
2
 
3
- const cds = require('../../')
3
+ const cds = require('../..')
4
4
  const { decodeURIComponent } = cds.utils
5
5
 
6
6
  const odata2cqn = require('./parse/parser').parse
@@ -259,7 +259,12 @@ const _tx_done = async (tx, responses, isJson) => {
259
259
  // here, the commit was rejected even though all requests were successful (e.g., by custom handler or db consistency check)
260
260
  rejected = 'rejected'
261
261
  // construct commit error (without modifying original error)
262
- const error = normalizeError(Object.create(e), { locale: cds.context.locale })
262
+ const error = normalizeError(Object.create(e), {
263
+ get locale() {
264
+ return cds.context.locale
265
+ },
266
+ get() {}
267
+ })
263
268
  // replace all responses with commit error
264
269
  for (const res of responses) {
265
270
  res.status = 'fail'
@@ -72,7 +72,7 @@ module.exports = (adapter, isUpsert) => {
72
72
 
73
73
  handleSapMessages(cdsReq, req, res)
74
74
 
75
- if (!target._isSingleton) {
75
+ if (!target._isSingleton && !res.hasHeader('location')) {
76
76
  res.set('location', location4(cdsReq.target, service, result || cdsReq.data))
77
77
  }
78
78
 
@@ -4,17 +4,8 @@ const { shutdown_on_uncaught_errors } = cds.env.server
4
4
  exports = module.exports = () =>
5
5
  function odata_error(err, req, res, next) {
6
6
  if (exports.pass_through(err)) return next(err)
7
+ else req._is_odata = true
7
8
  if (err.details) err = _fioritized(err)
8
- const content_id = req.headers['content-id']
9
- if (content_id) err['@Core.ContentID'] = content_id
10
- err['@Common.numericSeverity'] ??= err.numericSeverity || 4
11
-
12
- // propagate content-id and set @Common.numericSeverity
13
- err.details?.forEach(e => {
14
- if (content_id) e['@Core.ContentID'] = content_id
15
- e['@Common.numericSeverity'] ??= e.numericSeverity || 4 // Fiori expects this in error.details
16
- })
17
-
18
9
  exports.normalizeError(err, req)
19
10
  return next(err)
20
11
  }
@@ -25,21 +16,18 @@ exports.pass_through = err => {
25
16
  }
26
17
 
27
18
  exports.normalizeError = (err, req, cleanse = ODATA_PROPERTIES) => {
28
- const locale = cds.i18n.locale.from(req)
29
- err.status = _normalize(err, locale, cleanse) || 500
19
+ err.status = _normalize(err, req, cleanse) || 500
30
20
  return err
31
21
  }
32
22
 
33
- // TODO: This should go somewhere else ...
34
23
  exports.getLocalizedMessages = (messages, req) => {
35
24
  const locale = cds.i18n.locale.from(req)
36
- for (let m of messages) _normalize(m, locale, SAP_MSG_PROPERTIES)
25
+ for (let m of messages) _normalize(m, req, SAP_MSG_PROPERTIES, locale)
37
26
  return messages
38
27
  }
39
28
 
40
29
  exports.getSapMessages = (messages, req) => {
41
- const locale = cds.i18n.locale.from(req)
42
- for (let m of messages) _normalize(m, locale, SAP_MSG_PROPERTIES)
30
+ messages = exports.getLocalizedMessages(messages, req)
43
31
  return JSON.stringify(messages).replace(
44
32
  /[\u007F-\uFFFF]/g,
45
33
  c => '\\u' + c.charCodeAt(0).toString(16).padStart(4, '0')
@@ -52,10 +40,13 @@ const SAP_MSG_PROPERTIES = { ...ODATA_PROPERTIES, longtextUrl: 2, transition: 2,
52
40
  const BAD_REQUESTS = { ENTITY_ALREADY_EXISTS: 1, FK_CONSTRAINT_VIOLATION: 2, UNIQUE_CONSTRAINT_VIOLATION: 3 }
53
41
 
54
42
  // prettier-ignore
55
- const _normalize = (err, locale = cds.env.i18n.default_language, keep) => {
43
+ const _normalize = (err, req, keep,
44
+ locale = cds.i18n.locale.from(req) || cds.env.i18n.default_language,
45
+ content_id = req.get('content-id')
46
+ ) => {
56
47
 
57
48
  // Determine status code if not already set
58
- const details = err.details?.map?.(each => _normalize (each, locale, keep))
49
+ const details = err.details?.map?.(each => _normalize (each, req, keep, locale, content_id))
59
50
  const status = err.status || err.statusCode || _status4(err) || _reduce(details)
60
51
 
61
52
  // Determine error code and message
@@ -71,7 +62,11 @@ const _normalize = (err, locale = cds.env.i18n.default_language, keep) => {
71
62
  Object.defineProperty(err, 'toJSON', { value: function() {
72
63
  const that = keep ? {} : {...this}
73
64
  if (keep) for (let k in this) if (k in keep || k[0] === '@') that[k] = this[k]
74
- if (locale) that.message = i18n.messages.at (key, locale, this.args)
65
+ if (req._is_odata && keep !== SAP_MSG_PROPERTIES) {
66
+ that['@Common.numericSeverity'] ??= err.numericSeverity || 4
67
+ if (content_id) that['@Core.ContentID'] = content_id
68
+ }
69
+ if (locale) that.message = _message4 (err, key, locale)
75
70
  if (!that.message) that.message = this.message
76
71
  return that
77
72
  }})
@@ -83,6 +78,14 @@ const _status4 = err => {
83
78
  if (err.message in BAD_REQUESTS) return 400 // REVISIT: should we use 409 or 500 instead?
84
79
  }
85
80
 
81
+ const _message4 = (err, key, locale) => {
82
+ if (err.i18n) {
83
+ key = /{i18n>(.*)}/.exec(err.i18n)?.[1]
84
+ if (!key) return err.i18n
85
+ }
86
+ return i18n.messages.at(key, locale, err.args)
87
+ }
88
+
86
89
  const _reduce = details => {
87
90
  const unique = [...new Set(details)]
88
91
  if (unique.length === 1) return unique[0] // if only one unique status exists, we use that
@@ -149,7 +149,7 @@ module.exports = adapter => {
149
149
  }
150
150
 
151
151
  const stream = getReadable(result)
152
- if (!stream) return res.sendStatus(204)
152
+ if (stream == null) return res.sendStatus(204)
153
153
 
154
154
  validateMimetypeIsAcceptedOrThrow(req.headers, mimetype)
155
155
  if (!res.get('content-type')) res.set('content-type', mimetype)
@@ -1,4 +1,4 @@
1
- const cds = require('../../../')
1
+ const cds = require('../../..')
2
2
 
3
3
  const { formatVal } = require('../utils')
4
4
 
@@ -65,10 +65,7 @@ function hasValidProps(obj, ...names) {
65
65
  for (const propName of names) {
66
66
  const validate = validators[propName]
67
67
  const isValid = validate && validate(obj[propName])
68
-
69
- if (!isValid) {
70
- return false
71
- }
68
+ if (!isValid) return false
72
69
  }
73
70
 
74
71
  return true
@@ -183,23 +180,32 @@ function _xpr(expr, target, kind, isLambda, navPrefix = []) {
183
180
  if (inExpr) res.push(`(${inExpr})`)
184
181
  i += 1
185
182
  } else if (_isLambda(cur, expr[i + 1])) {
183
+ // Process 'exists' expression
184
+
186
185
  const { where } = expr[i + 1].ref.at(-1)
187
- const nav = expr[i + 1].ref.map(ref => ref?.id ?? ref).join('/')
186
+ const navSegments = expr[i + 1].ref.map(ref => ref?.id ?? ref)
187
+ const nav = navSegments.join('/')
188
188
 
189
189
  if (kind === 'odata-v2') {
190
190
  // odata-v2 does not support lambda expressions but successfactors allows filter like for to-one assocs
191
191
  cds.log('remote').info(`OData V2 does not support lambda expressions. Using path expression as best effort.`)
192
192
  isLambda = false
193
193
  res.push(_xpr(where, target, kind, isLambda, [...navPrefix, nav]))
194
- } else if (!where) {
195
- res.push(`${nav}/any()`)
196
- } else {
194
+ } else if (res.at(-1) === 'not' && where.length === 2 && where[0] === 'not' && where[1].xpr) {
195
+ // Convert double negation 'not exists where not' to 'all'
196
+ // > reverting the mirrored transformation performed in grammar.peggy/lambda
197
+
198
+ res.pop()
199
+ res.push(`${nav}/all(${LAMBDA_VARIABLE}:${_xpr(where[1].xpr, target, kind, true, navPrefix)})`)
200
+ } else if (where) {
197
201
  res.push(
198
202
  `${nav}/any(${LAMBDA_VARIABLE}:${_xpr(where, target?.elements[nav]._target, kind, true, navPrefix)})`
199
203
  )
204
+ } else {
205
+ res.push(`${nav}/any()`)
200
206
  }
201
207
 
202
- i++
208
+ i += 1
203
209
  } else {
204
210
  res.push(OPERATORS[cur] || cur.toLowerCase())
205
211
  }
@@ -190,7 +190,9 @@
190
190
  if (newCqn.SELECT?.recurse && cqn.where) {
191
191
  const where = cqn.where
192
192
  delete cqn.where
193
- newCqn = { SELECT: { from: newCqn, where } }
193
+ const columns = newCqn.SELECT.columns
194
+ delete newCqn.SELECT.columns
195
+ newCqn = { SELECT: { from: newCqn, where, columns } }
194
196
  }
195
197
  return newCqn
196
198
  }
@@ -206,7 +208,7 @@
206
208
  if (
207
209
  (topCqn.columns && topCqn.columns[0].as === '$count') ||
208
210
  //In QUERY_WITH_AGGREGATION topCqn.where is a having and thus no further nesting needed
209
- (!QUERY_WITH_AGGREGATION && topCqn.where && (cqn.SELECT.where || cqn.SELECT.limit)) ||
211
+ (!QUERY_WITH_AGGREGATION && topCqn.where && (cqn.SELECT.where || cqn.SELECT.limit)) ||
210
212
  (cqn.SELECT.limit && topCqn.limit) ||
211
213
  (cqn.SELECT.orderBy && topCqn.orderBy) ||
212
214
  (cqn.SELECT.search && topCqn.search)
@@ -710,6 +712,8 @@
710
712
  = (
711
713
  c:('*' / ref) {
712
714
  const col = c === '*' ? {} : c
715
+ if (col.ref?.length > 1) throw Object.assign(new Error(`navigation "${col.ref.join('/')}" in "$expand" is not supported`), { statusCode: 400 })
716
+
713
717
  col.expand = ['*']
714
718
  if (!Array.isArray(SELECT.expand)) SELECT.expand = []
715
719
  if (!SELECT.expand.find(_compareRefs(col))) SELECT.expand.push(col)
@@ -984,7 +988,7 @@
984
988
  // Loop through each element, add it to current level, if element is already part of result, increase level
985
989
  for(let trafos of additionalTransformation) {
986
990
  for(const transformation in trafos) {
987
- if (transformation === 'where' && (mainTransformation.groupBy || mainTransformation.aggregate) && !mainTransformation.having) {
991
+ if (transformation === 'where' && (mainTransformation.groupBy || mainTransformation.aggregate) && !mainTransformation.having) {
988
992
  //When a group by or aggregate preceed a where, the where is a having
989
993
  mainTransformation.having = trafos[transformation]
990
994
  }
@@ -1216,7 +1220,7 @@
1216
1220
 
1217
1221
  doubleQuotedString "a doubled quoted string"
1218
1222
  = '"' s:$('\\"' / '\\\\' / [^"])* '"'
1219
- {return s.replace(/\\\\/g,"\\").replace(/\\"/g,'"')}
1223
+ {return s.replace(/\\(["\\])/g, '$1')}
1220
1224
 
1221
1225
  word "a string"
1222
1226
  = $([^ \t\n()"&;]+)