@sap/cds 6.7.2 → 6.8.2

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 (106) hide show
  1. package/CHANGELOG.md +53 -1
  2. package/README.md +1 -0
  3. package/_i18n/i18n.properties +9 -6
  4. package/_i18n/i18n_ar.properties +6 -6
  5. package/_i18n/i18n_cs.properties +6 -6
  6. package/_i18n/i18n_da.properties +6 -6
  7. package/_i18n/i18n_de.properties +6 -6
  8. package/_i18n/i18n_en.properties +6 -6
  9. package/_i18n/i18n_es.properties +6 -6
  10. package/_i18n/i18n_fi.properties +6 -6
  11. package/_i18n/i18n_fr.properties +6 -6
  12. package/_i18n/i18n_hu.properties +6 -6
  13. package/_i18n/i18n_it.properties +6 -6
  14. package/_i18n/i18n_ja.properties +6 -6
  15. package/_i18n/i18n_ko.properties +6 -6
  16. package/_i18n/i18n_ms.properties +6 -6
  17. package/_i18n/i18n_nl.properties +6 -6
  18. package/_i18n/i18n_no.properties +6 -6
  19. package/_i18n/i18n_pl.properties +6 -6
  20. package/_i18n/i18n_pt.properties +6 -6
  21. package/_i18n/i18n_ro.properties +6 -6
  22. package/_i18n/i18n_ru.properties +6 -6
  23. package/_i18n/i18n_sv.properties +6 -6
  24. package/_i18n/i18n_th.properties +6 -6
  25. package/_i18n/i18n_tr.properties +8 -8
  26. package/_i18n/i18n_zh_CN.properties +3 -3
  27. package/_i18n/i18n_zh_TW.properties +6 -6
  28. package/apis/core.d.ts +30 -31
  29. package/apis/csn.d.ts +1 -1
  30. package/apis/ql.d.ts +69 -39
  31. package/apis/serve.d.ts +4 -3
  32. package/apis/services.d.ts +20 -7
  33. package/bin/build/buildTaskEngine.js +1 -1
  34. package/bin/build/index.js +1 -1
  35. package/bin/build/provider/buildTaskProviderInternal.js +9 -6
  36. package/bin/build/provider/hana/index.js +11 -4
  37. package/bin/build/provider/mtx-extension/index.js +13 -1
  38. package/bin/build/provider/mtx-sidecar/index.js +3 -3
  39. package/bin/build/provider/nodejs/index.js +23 -0
  40. package/bin/plugins.js +2 -1
  41. package/bin/version.js +3 -2
  42. package/common.cds +3 -2
  43. package/lib/auth/index.js +3 -0
  44. package/lib/auth/mocked-users.js +13 -0
  45. package/lib/compile/etc/_localized.js +3 -0
  46. package/lib/compile/for/lean_drafts.js +0 -1
  47. package/lib/core/entities.js +7 -3
  48. package/lib/dbs/cds-deploy.js +36 -12
  49. package/lib/env/cds-env.js +47 -14
  50. package/lib/env/cds-requires.js +16 -7
  51. package/lib/env/defaults.js +2 -2
  52. package/lib/env/schemas/cds-rc.json +1 -8
  53. package/lib/index.js +1 -1
  54. package/lib/ql/STREAM.js +89 -0
  55. package/lib/ql/cds-ql.js +2 -1
  56. package/lib/req/request.js +6 -2
  57. package/lib/req/user.js +1 -1
  58. package/lib/srv/middlewares/index.js +9 -7
  59. package/lib/srv/middlewares/trace.js +6 -5
  60. package/lib/srv/srv-api.js +1 -0
  61. package/lib/utils/cds-utils.js +1 -1
  62. package/lib/utils/tar.js +30 -31
  63. package/libx/_runtime/audit/Service.js +96 -37
  64. package/libx/_runtime/audit/generic/personal/utils.js +26 -13
  65. package/libx/_runtime/audit/utils/v2.js +21 -22
  66. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +1 -1
  67. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/create.js +1 -1
  68. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +2 -0
  69. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +2 -3
  70. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +1 -1
  71. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/batch/BatchProcessor.js +2 -0
  72. package/libx/_runtime/cds-services/adapter/odata-v4/utils/handlerUtils.js +2 -1
  73. package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +10 -3
  74. package/libx/_runtime/cds-services/services/Service.js +2 -7
  75. package/libx/_runtime/cds-services/services/utils/differ.js +1 -1
  76. package/libx/_runtime/cds-services/util/assert.js +28 -5
  77. package/libx/_runtime/common/aspects/any.js +4 -1
  78. package/libx/_runtime/common/generic/auth/utils.js +30 -41
  79. package/libx/_runtime/common/generic/crud.js +1 -1
  80. package/libx/_runtime/common/i18n/messages.properties +1 -1
  81. package/libx/_runtime/common/utils/generateOnCond.js +18 -22
  82. package/libx/_runtime/db/expand/expandCQNToJoin.js +49 -41
  83. package/libx/_runtime/db/expand/rawToExpanded.js +3 -5
  84. package/libx/_runtime/db/generic/rewrite.js +3 -0
  85. package/libx/_runtime/db/utils/generateAliases.js +1 -1
  86. package/libx/_runtime/fiori/generic/activate.js +1 -1
  87. package/libx/_runtime/fiori/generic/before.js +18 -19
  88. package/libx/_runtime/fiori/generic/prepare.js +1 -1
  89. package/libx/_runtime/fiori/generic/read.js +1 -1
  90. package/libx/_runtime/fiori/lean-draft.js +87 -53
  91. package/libx/_runtime/fiori/utils/handler.js +0 -6
  92. package/libx/_runtime/hana/customBuilder/CustomFunctionBuilder.js +1 -1
  93. package/libx/_runtime/hana/customBuilder/CustomReferenceBuilder.js +2 -1
  94. package/libx/_runtime/hana/customBuilder/CustomSelectBuilder.js +0 -5
  95. package/libx/_runtime/hana/execute.js +18 -11
  96. package/libx/_runtime/hana/pool.js +26 -18
  97. package/libx/_runtime/hana/search2Contains.js +1 -1
  98. package/libx/_runtime/hana/search2cqn4sql.js +26 -18
  99. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +23 -16
  100. package/libx/_runtime/messaging/outbox/utils.js +6 -1
  101. package/libx/_runtime/remote/Service.js +83 -48
  102. package/libx/_runtime/remote/utils/client.js +17 -19
  103. package/libx/_runtime/sqlite/execute.js +2 -0
  104. package/libx/rest/middleware/read.js +2 -1
  105. package/libx/rest/middleware/update.js +1 -1
  106. package/package.json +1 -1
@@ -25,6 +25,30 @@ const DRAFT_ADMIN_ELEMENTS = [
25
25
  'DraftIsProcessedByMe'
26
26
  ]
27
27
 
28
+ const _fillIsActiveEntity = (row, IsActiveEntity, target) => {
29
+ if (target.drafts) row.IsActiveEntity = IsActiveEntity
30
+ for (const key in target.associations) {
31
+ const prop = row[key]
32
+ if (!prop) continue
33
+ const el = target.elements[key]
34
+ const childIsActiveEntity = el._target.isDraft ? IsActiveEntity : true
35
+ if (Array.isArray(prop)) prop.map(r => _fillIsActiveEntity(r, childIsActiveEntity, el._target))
36
+ else if (typeof prop === 'object') _fillIsActiveEntity(prop, childIsActiveEntity, el._target)
37
+ }
38
+ }
39
+
40
+ // REVISIT: should not be necessary
41
+ const _runWithContext = (srv, req, obj) => {
42
+ const r = new cds.Request(obj)
43
+ r.event // invoke getter
44
+ r._ = Object.assign(r._, req._)
45
+ if (req.getUriInfo) r.getUriInfo = () => req.getUriInfo()
46
+ if (req.getUrlObject) r.getUrlObject = () => req.getUrlObject()
47
+ r._.params = req.params
48
+ r._.query = req.query
49
+ return srv.dispatch(r)
50
+ }
51
+
28
52
  /// It's important to wait for the completion of all promises, otherwise a rollback might happen too soon
29
53
  const _promiseAll = async array => {
30
54
  const results = await Promise.allSettled(array)
@@ -109,16 +133,15 @@ cds.ApplicationService.prototype.handle = async function (req) {
109
133
  (query.DELETE && 'DELETE') ||
110
134
  req.event
111
135
  _req.target = query._target
112
- _req._ = Object.assign({}, req._ || {}) // don't share the same `_` object
113
- _req._.params = req.params
114
136
  _req.params = req.params
137
+ _req._ = Object.assign({}, req._ || {})
138
+ _req._.params = req.params
115
139
  _req._.query = query
140
+ const props = ['_isRest', '_isOData', 'isConcurrentResource', 'isConditional', 'validateEtag']
141
+ props.forEach(p => {
142
+ if (req[p]) _req.p = req[p]
143
+ })
116
144
  _req._ = req._
117
- _req._isRest = req._isRest
118
- _req._isOData = req._isOData
119
- _req.isConcurrentResource = req.isConcurrentResource
120
- _req.isConditional = req.isConditional
121
- _req.validateEtag = req.validateEtag
122
145
  const cqnData = _req.query.UPDATE?.data || _req.query.INSERT?.entries?.[0]
123
146
  if (cqnData) _req.data = cqnData // must point to the same object
124
147
  Object.defineProperty(_req, '_messages', {
@@ -162,7 +185,8 @@ cds.ApplicationService.prototype.handle = async function (req) {
162
185
  ])
163
186
  )
164
187
  const inProcessByUser = draft?.DraftAdministrativeData?.InProcessByUser
165
- if (inProcessByUser && inProcessByUser !== cds.context.user.id) req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER')
188
+ if (inProcessByUser && inProcessByUser !== cds.context.user.id)
189
+ req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER', [inProcessByUser])
166
190
  const deletes = [run(DELETE.from({ ref: query.DELETE.from.ref }))]
167
191
  if (draft)
168
192
  deletes.push(
@@ -203,7 +227,7 @@ cds.ApplicationService.prototype.handle = async function (req) {
203
227
  )
204
228
  if (!res) req.reject(404)
205
229
  if (res.DraftAdministrativeData?.InProcessByUser !== cds.context.user.id)
206
- req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER')
230
+ req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER', [res.DraftAdministrativeData?.InProcessByUser])
207
231
  const DraftAdministrativeData_DraftUUID = res.DraftAdministrativeData_DraftUUID
208
232
  delete res.DraftAdministrativeData_DraftUUID
209
233
  delete res.DraftAdministrativeData
@@ -263,7 +287,7 @@ cds.ApplicationService.prototype.handle = async function (req) {
263
287
  )
264
288
  if (!res) req.reject(404)
265
289
  if (res.DraftAdministrativeData?.InProcessByUser !== cds.context.user.id)
266
- req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER')
290
+ req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER', [res.DraftAdministrativeData?.InProcessByUser])
267
291
  await UPDATE('DRAFT.DraftAdministrativeData')
268
292
  .data({
269
293
  InProcessByUser: req.user.id,
@@ -368,17 +392,17 @@ const Read = {
368
392
  drafts = []
369
393
  }
370
394
  }
371
- Read.merge(query._target, actives, drafts, (row, other) =>
372
- other
373
- ? Object.assign(row, other, { IsActiveEntity: true, HasActiveEntity: false, HasDraftEntity: true })
374
- : Object.assign(row, {
375
- IsActiveEntity: true,
376
- HasActiveEntity: false,
377
- HasDraftEntity: false,
378
- DraftAdministrativeData: null,
379
- DraftAdministrativeData_DraftUUID: null
380
- })
381
- )
395
+ Read.merge(query._target, actives, drafts, (row, other) => {
396
+ if (other) Object.assign(row, other, { HasActiveEntity: false, HasDraftEntity: true })
397
+ else
398
+ Object.assign(row, {
399
+ HasActiveEntity: false,
400
+ HasDraftEntity: false,
401
+ DraftAdministrativeData: null,
402
+ DraftAdministrativeData_DraftUUID: null
403
+ })
404
+ _fillIsActiveEntity(row, true, query._target)
405
+ })
382
406
  return _requested(actives, query)
383
407
  },
384
408
  unchanged: async function (run, query) {
@@ -420,12 +444,12 @@ const Read = {
420
444
  cds.context.user.id
421
445
  )
422
446
  const drafts = await run(draftsQuery)
423
- Read.merge(query._target, drafts, [], row =>
447
+ Read.merge(query._target, drafts, [], row => {
424
448
  Object.assign(row, {
425
- HasDraftEntity: false,
426
- IsActiveEntity: false
449
+ HasDraftEntity: false
427
450
  })
428
- )
451
+ _fillIsActiveEntity(row, false, query._drafts._target)
452
+ })
429
453
  return _requested(drafts, query)
430
454
  },
431
455
  all: async function (run, query) {
@@ -467,18 +491,17 @@ const Read = {
467
491
  const count = isFirstPage ? ownNewDrafts.length + (isCount ? actives[0]?.$count : actives.$count) : actives.$count
468
492
  if (isCount) return { $count: count }
469
493
 
470
- Read.merge(query._target, ownDrafts, [], row =>
494
+ Read.merge(query._target, ownDrafts, [], row => {
471
495
  Object.assign(row, {
472
- IsActiveEntity: false,
473
496
  HasDraftEntity: false
474
497
  })
475
- )
498
+ _fillIsActiveEntity(row, false, query._drafts._target)
499
+ })
476
500
  Read.delete(query._target, actives, ownEditDrafts)
477
501
  const otherEditDrafts = await Read.complementaryDrafts(run, query, actives)
478
502
  Read.merge(query._target, actives, otherEditDrafts, (row, other) => {
479
503
  if (other) {
480
504
  Object.assign(row, {
481
- IsActiveEntity: true,
482
505
  HasDraftEntity: true,
483
506
  HasActiveEntity: false,
484
507
  DraftAdministrativeData_DraftUUID: other.DraftAdministrativeData_DraftUUID,
@@ -486,13 +509,13 @@ const Read = {
486
509
  })
487
510
  } else {
488
511
  Object.assign(row, {
489
- IsActiveEntity: true,
490
512
  HasDraftEntity: false,
491
513
  HasActiveEntity: false,
492
514
  DraftAdministrativeData_DraftUUID: null,
493
515
  DraftAdministrativeData: null
494
516
  })
495
517
  }
518
+ _fillIsActiveEntity(row, true, query._target)
496
519
  })
497
520
  const res = isFirstPage ? [...ownNewDrafts, ...ownEditDrafts, ...actives] : actives
498
521
  if (query.SELECT.count) res.$count = count
@@ -518,11 +541,11 @@ const Read = {
518
541
  const actives = drafts.length
519
542
  ? await run(query.where(Read.whereIn(query._target, drafts)))
520
543
  : Object.assign([], { $count: 0 })
521
- Read.merge(query._target, actives, drafts, (row, other) =>
522
- other
523
- ? Object.assign(row, other, { IsActiveEntity: true, HasDraftEntity: true, HasActiveEntity: false })
524
- : Object.assign({ IsActiveEntity: true, HasDraftEntity: false, HasActiveEntity: false })
525
- )
544
+ Read.merge(query._target, actives, drafts, (row, other) => {
545
+ if (other) Object.assign(row, other, { HasDraftEntity: true, HasActiveEntity: false })
546
+ else Object.assign({ HasDraftEntity: false, HasActiveEntity: false })
547
+ _fillIsActiveEntity(row, true, query._target)
548
+ })
526
549
  return _requested(actives, query)
527
550
  },
528
551
  unsavedChangesByAnotherUser: async function (run, query) {
@@ -833,17 +856,17 @@ async function onNew(req) {
833
856
  let DraftUUID
834
857
  if (isRoot) DraftUUID = cds.utils.uuid()
835
858
  else {
836
- const rootData = await this.run(
837
- SELECT.one(req.query.INSERT.into.ref[0].id)
859
+ const rootData = await _runWithContext(this, req, {
860
+ query: SELECT.one(req.query.INSERT.into.ref[0].id)
838
861
  .columns([
839
862
  { ref: ['DraftAdministrativeData_DraftUUID'] },
840
863
  { ref: ['DraftAdministrativeData'], expand: [{ ref: ['InProcessByUser'] }] }
841
864
  ])
842
865
  .where(req.query.INSERT.into.ref[0].where)
843
- )
866
+ })
844
867
  if (!rootData) req.reject(404)
845
868
  if (rootData.DraftAdministrativeData?.InProcessByUser !== req.user.id)
846
- req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER')
869
+ req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER', [rootData.DraftAdministrativeData.InProcessByUser])
847
870
  DraftUUID = rootData.DraftAdministrativeData_DraftUUID
848
871
  }
849
872
  const timestamp = cds.context.timestamp.toISOString() // REVISIT: toISOString should be done on db layer
@@ -888,7 +911,7 @@ async function onNew(req) {
888
911
  delete draftData.IsActiveEntity
889
912
  const draftCQN = INSERT.into(req.target).entries(draftData)
890
913
 
891
- await _promiseAll([cds.run(adminDataCQN), this.run(draftCQN)])
914
+ await _promiseAll([cds.run(adminDataCQN), _runWithContext(this, req, { query: draftCQN })])
892
915
  req._.readAfterWrite = true
893
916
  return { ...draftData, IsActiveEntity: false }
894
917
  }
@@ -934,12 +957,12 @@ async function onEdit(req) {
934
957
  // It's not possible to use `FOR UPDATE` in HANA if the view contains joins/unions. Unfortunately, we can't resolve the table entity
935
958
  // because we must trigger the app-service request on the target entity (which could be delegated to a remote service).
936
959
  // The best we can do is to catch a potential error
937
- await this.run(activeCheck).catch(_ => {})
960
+ await _runWithContext(this, req, { query: activeCheck }).catch(_ => {})
938
961
 
939
962
  const [res, draft] = await _promiseAll([
940
- this.run(activeCQN),
963
+ _runWithContext(this, req, { query: activeCQN }),
941
964
  // no user check must be done here...
942
- this.run(existingDraft)
965
+ _runWithContext(this, req, { query: existingDraft })
943
966
  ])
944
967
 
945
968
  if (!res) req.reject(404)
@@ -951,7 +974,7 @@ async function onEdit(req) {
951
974
  for (const key in req.target.drafts.keys) keys[key] = res[key]
952
975
  await _promiseAll([
953
976
  DELETE.from('DRAFT.DraftAdministrativeData').where({ DraftUUID }),
954
- this.run(DELETE.from(req.target.drafts).where(keys))
977
+ _runWithContext(this, req, { query: DELETE.from(req.target.drafts).where(keys) })
955
978
  ])
956
979
  }
957
980
 
@@ -972,7 +995,7 @@ async function onEdit(req) {
972
995
  res.DraftAdministrativeData_DraftUUID = DraftUUID
973
996
  res.HasActiveEntity = true
974
997
  delete res.DraftAdministrativeData
975
- await this.run(INSERT.into(targetDraft).entries(res))
998
+ await _runWithContext(this, req, { query: INSERT.into(targetDraft).entries(res) })
976
999
 
977
1000
  // REVISIT: we need to use okra API here because it must be set in the batched request
978
1001
  // status code must be set in handler to allow overriding for FE V2
@@ -994,18 +1017,28 @@ async function onCancel(req) {
994
1017
  ])
995
1018
  // do not add InProcessByUser restriction
996
1019
  Object.defineProperty(draftDelete, '_draftParams', { value: draftParams, enumerable: false })
997
- const draft = await this.run(draftDelete)
1020
+ const draft = await _runWithContext(this, req, { query: draftDelete })
998
1021
  if (draftParams.IsActiveEntity === false && !draft) req.reject(404)
999
1022
  if (draft && draft.DraftAdministrativeData?.InProcessByUser !== cds.context.user.id)
1000
- req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER')
1001
- const deletes = !draft ? [] : [this.run(DELETE.from({ ref: req.query.DELETE.from.ref }))]
1023
+ req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER', [draft.DraftAdministrativeData?.InProcessByUser])
1024
+ const queries = !draft ? [] : [_runWithContext(this, req, { query: DELETE.from({ ref: req.query.DELETE.from.ref }) })]
1002
1025
  if (draft && req.target['@Common.DraftRoot.ActivationAction'])
1003
1026
  // only for draft root
1004
- deletes.push(
1027
+ queries.push(
1005
1028
  DELETE.from('DRAFT.DraftAdministrativeData').where({ DraftUUID: draft.DraftAdministrativeData_DraftUUID })
1006
1029
  )
1007
- if (draftParams.IsActiveEntity) deletes.push(this.run(DELETE.from({ ref: activeRef })))
1008
- await _promiseAll(deletes)
1030
+ else
1031
+ queries.push(
1032
+ UPDATE('Draft.DraftAdministrativeData')
1033
+ .data({
1034
+ InProcessByUser: cds.context.user.id,
1035
+ LastChangedByUser: cds.context.user.id,
1036
+ LastChangeDateTime: cds.context.timestamp.toISOString()
1037
+ })
1038
+ .where({ DraftUUID: draft.DraftAdministrativeData_DraftUUID })
1039
+ )
1040
+ if (draftParams.IsActiveEntity) queries.push(_runWithContext(this, req, { query: DELETE.from({ ref: activeRef }) }))
1041
+ await _promiseAll(queries)
1009
1042
  return req.data
1010
1043
  }
1011
1044
 
@@ -1025,9 +1058,10 @@ async function onPrepare(req) {
1025
1058
  .columns(keys)
1026
1059
  .where(where)
1027
1060
  Object.defineProperty(draftQuery, '_draftParams', { value: draftParams, enumerable: false })
1028
- const data = await this.run(draftQuery)
1061
+ const data = await _runWithContext(this, req, { query: draftQuery })
1029
1062
  if (!data) req.reject(404)
1030
- if (data.DraftAdministrativeData?.InProcessByUser !== req.user.id) req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER')
1063
+ if (data.DraftAdministrativeData?.InProcessByUser !== req.user.id)
1064
+ req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER', [data.DraftAdministrativeData?.InProcessByUser])
1031
1065
  delete data.DraftAdministrativeData
1032
1066
  return { ...data, IsActiveEntity: false }
1033
1067
  }
@@ -188,11 +188,6 @@ const removeDraftUUIDIfNecessary = req =>
188
188
  ? () => {}
189
189
  : result => delete result.DraftAdministrativeData_DraftUUID
190
190
 
191
- const isDraftActivateAction = req => {
192
- // REVISIT: get rid of getUrlObject
193
- if (req.getUrlObject) return req.getUrlObject().pathname.endsWith('draftActivate')
194
- }
195
-
196
191
  const addColumnAlias = (columns, alias) => {
197
192
  if (!alias) {
198
193
  return columns
@@ -258,7 +253,6 @@ module.exports = {
258
253
  getUpdateDraftAdminCQN,
259
254
  getEnrichedCQN,
260
255
  removeDraftUUIDIfNecessary,
261
- isDraftActivateAction,
262
256
  ensureDraftsSuffix,
263
257
  ensureNoDraftsSuffix,
264
258
  ensureUnlocalized,
@@ -21,7 +21,7 @@ class CustomFunctionBuilder extends FunctionBuilder {
21
21
 
22
22
  _handleContains(args) {
23
23
  // fuzzy search has three arguments, must not be converted to like expressions
24
- if (args.length > 2 || this._options.$searchUsingContains) {
24
+ if (args.length > 2 || this._obj.searchUsingContains) {
25
25
  this._outputObj.sql.push('CONTAINS')
26
26
  this._addFunctionArgs(args, true)
27
27
  return
@@ -25,7 +25,8 @@ class CustomReferenceBuilder extends ReferenceBuilder {
25
25
  return
26
26
  }
27
27
 
28
- this._outputObj.sql.push(ref.map(el => this._quoteElement(el)).join('.'))
28
+ const sql = ref.map(el => this._quoteElement(el)).join('.')
29
+ this._outputObj.sql.push(sql)
29
30
  }
30
31
  }
31
32
 
@@ -4,11 +4,6 @@ const LOG = cds.log('hana|db|sql')
4
4
  const SelectBuilder = require('../../db/sql-builder').SelectBuilder
5
5
 
6
6
  class CustomSelectBuilder extends SelectBuilder {
7
- constructor(obj, options, csn) {
8
- super(obj, options, csn)
9
- // $searchUsingContains property is set in the sub SELECT of the query, optimized search case
10
- if (this._obj?._$searchUsingContains) this._options.$searchUsingContains = this._obj._$searchUsingContains
11
- }
12
7
  get FunctionBuilder() {
13
8
  const FunctionBuilder = require('./CustomFunctionBuilder')
14
9
  Object.defineProperty(this, 'FunctionBuilder', { value: FunctionBuilder })
@@ -58,6 +58,7 @@ function _getProcedureName(sql) {
58
58
  function _hdbGetResultForProcedure(rows, args, outParameters) {
59
59
  // on hdb, rows already contains results for scalar params
60
60
  const result = rows || {}
61
+
61
62
  // merge table output params into scalar params
62
63
  if (args && args.length && outParameters) {
63
64
  const params = outParameters.filter(md => !(md.PARAMETER_NAME in rows))
@@ -65,6 +66,7 @@ function _hdbGetResultForProcedure(rows, args, outParameters) {
65
66
  result[params[i].PARAMETER_NAME] = args[i]
66
67
  }
67
68
  }
69
+
68
70
  return result
69
71
  }
70
72
 
@@ -79,6 +81,7 @@ function _hcGetResultForProcedure(stmt, resultSet, outParameters) {
79
81
  }
80
82
  }
81
83
  }
84
+
82
85
  // merge table output params into scalar params
83
86
  const params = Array.isArray(outParameters) && outParameters.filter(md => !(md.PARAMETER_NAME in result))
84
87
  if (params && params.length) {
@@ -91,6 +94,7 @@ function _hcGetResultForProcedure(stmt, resultSet, outParameters) {
91
94
  resultSet.nextResult()
92
95
  }
93
96
  }
97
+
94
98
  return result
95
99
  }
96
100
 
@@ -160,7 +164,6 @@ function _executeAsPreparedStatement(dbc, sql, values, reject, resolve) {
160
164
  }
161
165
 
162
166
  stmt.drop(() => {})
163
-
164
167
  resolve(result)
165
168
  })
166
169
  })
@@ -180,6 +183,7 @@ function _executeSimpleSQL(dbc, sql, values) {
180
183
  if (dbc.name !== 'hdb' && typeof values === 'object') {
181
184
  values = Object.values(values)
182
185
  }
186
+
183
187
  // ensure that stored procedure with parameters is always executed as prepared
184
188
  if (_hasValues(values) || !!_getProcedureName(sql)) {
185
189
  _executeAsPreparedStatement(dbc, sql, values, reject, resolve)
@@ -230,12 +234,12 @@ function executeSelectCQN(model, dbc, query, user, locale, txTimestamp) {
230
234
  ) {
231
235
  return expandV2(model, dbc, query, user, locale, txTimestamp, executeSelectCQN)
232
236
  }
237
+
233
238
  return _processExpand(model, dbc, query, user, locale, txTimestamp)
234
239
  }
235
240
 
236
241
  const { sql, values = [] } = _cqnToSQL(model, query, user, locale, txTimestamp)
237
242
  const postProcessMapper = getPostProcessMapper(HANA_TYPE_CONVERSION_MAP, model, query)
238
-
239
243
  return _executeSelectSQL(dbc, sql, values, query.SELECT.one, postProcessMapper)
240
244
  }
241
245
 
@@ -245,17 +249,17 @@ function _getValuesProxy(values) {
245
249
  if (prop.length > 1 && prop.startsWith(':')) {
246
250
  return Object.getOwnPropertyDescriptor(obj, prop.slice(1))
247
251
  }
252
+
248
253
  return Object.getOwnPropertyDescriptor(obj, prop)
249
254
  },
250
255
  get: (obj, prop) => {
251
256
  if (prop.length > 1 && prop.startsWith(':')) {
252
257
  return obj[prop.slice(1)]
253
258
  }
259
+
254
260
  return obj[prop]
255
261
  },
256
- ownKeys: target => {
257
- return Reflect.ownKeys(target).map(key => `:${key}`)
258
- }
262
+ ownKeys: target => Reflect.ownKeys(target).map(key => `:${key}`)
259
263
  })
260
264
  }
261
265
 
@@ -276,6 +280,7 @@ function executeInsertCQN(model, dbc, query, user, locale, txTimestamp) {
276
280
  if (dbc.name === 'hdb') {
277
281
  return writeStreamWithHdb(dbc, sql, values)
278
282
  }
283
+
279
284
  return writeStreamWithHanaClient(dbc, sql, values)
280
285
  }
281
286
 
@@ -284,6 +289,7 @@ function executeInsertCQN(model, dbc, query, user, locale, txTimestamp) {
284
289
  const affectedRowsCount = Array.isArray(affectedRows)
285
290
  ? affectedRows.reduce((sum, rows) => sum + rows, 0)
286
291
  : affectedRows
292
+
287
293
  if (entriesOrRows && entriesOrRows.length !== affectedRowsCount) {
288
294
  LOG._warn &&
289
295
  LOG.warn(
@@ -295,13 +301,17 @@ function executeInsertCQN(model, dbc, query, user, locale, txTimestamp) {
295
301
  query
296
302
  }
297
303
  )
304
+
298
305
  throw new Error('Possible data loss by INSERT into HANA db. Please, update a corresponding HANA driver.')
299
306
  }
307
+
300
308
  // InsertResult needs an object per row with its values
301
309
  // query.INSERT.values -> one row
302
310
  if (query.INSERT.values) return [{ affectedRows: 1, values: [values] }]
311
+
303
312
  // query.INSERT.entries or .rows -> multiple rows
304
313
  if (entriesOrRows) return values.map(v => ({ affectedRows: 1, values: v }))
314
+
305
315
  // INSERT into SELECT
306
316
  return [{ affectedRows }]
307
317
  })
@@ -331,20 +341,17 @@ function executeGenericCQN(model, dbc, query, user, locale, txTimestamp) {
331
341
  async function executeSelectStreamCQN(model, dbc, query, user, locale, txTimestamp) {
332
342
  const { sql, values = [] } = _cqnToSQL(model, query, user, locale, txTimestamp)
333
343
  let result
344
+
334
345
  if (dbc.name === 'hdb') {
335
346
  result = await readStreamWithHdb(dbc, sql, values)
336
347
  } else {
337
348
  result = await readStreamWithHanaClient(dbc, sql, values)
338
349
  }
339
350
 
340
- if (result.length === 0) {
341
- return
342
- }
351
+ if (result.length === 0) return
343
352
 
344
353
  const val = Object.values(result[0])[0]
345
- if (val === null) {
346
- return null
347
- }
354
+ if (val === null) return null
348
355
 
349
356
  return { value: val }
350
357
  }
@@ -17,6 +17,7 @@ function multiTenantServiceManager() {
17
17
  if (e.code === 'MODULE_NOT_FOUND') return null
18
18
  else throw e
19
19
  }
20
+
20
21
  const oldIm =
21
22
  cds.requires.multitenancy?.['old-instance-manager'] ??
22
23
  cds.env.requires?.['cds.xt.DeploymentService']?.['old-instance-manager']
@@ -93,8 +94,10 @@ async function credentials4(tenant, db) {
93
94
  return new Promise((resolve, reject) => {
94
95
  db._instance_manager.get(tenant, (err, res) => {
95
96
  if (err) return reject(err)
96
- if (!res)
97
+ if (!res) {
97
98
  return reject(Object.assign(new Error(`There is no instance for tenant "${tenant}"`), { statusCode: 404 }))
99
+ }
100
+
98
101
  resolve(res.credentials)
99
102
  })
100
103
  })
@@ -102,15 +105,9 @@ async function credentials4(tenant, db) {
102
105
 
103
106
  function factory4(creds, tenant) {
104
107
  return {
105
- create: function () {
106
- return hana.__connect(creds, tenant)
107
- },
108
- destroy: function (client) {
109
- return hana.__disconnect(client)
110
- },
111
- validate: function (client) {
112
- return hana.__isConnected(client)
113
- }
108
+ create: () => hana.__connect(creds, tenant),
109
+ destroy: client => hana.__disconnect(client),
110
+ validate: client => hana.__isConnected(client)
114
111
  }
115
112
  }
116
113
 
@@ -121,7 +118,6 @@ const defaultConfig = { min: 0, max: 100, testOnBorrow: true, fifo: false }
121
118
 
122
119
  const _getPoolConfig = function () {
123
120
  const { pool: poolConfig } = cds.env.requires.db
124
-
125
121
  const mergedConfig = Object.assign({}, defaultConfig, poolConfig)
126
122
 
127
123
  // defaults
@@ -154,15 +150,19 @@ const _getMassagedCreds = function (creds) {
154
150
  if (!('ca' in creds) && creds.certificate) {
155
151
  creds.ca = creds.certificate
156
152
  }
153
+
157
154
  if ('encrypt' in creds && !('useTLS' in creds)) {
158
155
  creds.useTLS = creds.encrypt
159
156
  }
157
+
160
158
  if ('hostname_in_certificate' in creds && !('sslHostNameInCertificate' in creds)) {
161
159
  creds.sslHostNameInCertificate = creds.hostname_in_certificate
162
160
  }
161
+
163
162
  if ('validate_certificate' in creds && !('sslValidateCertificate' in creds)) {
164
163
  creds.sslValidateCertificate = creds.validate_certificate
165
164
  }
165
+
166
166
  return creds
167
167
  }
168
168
 
@@ -185,9 +185,11 @@ async function pool4(tenant, db) {
185
185
  )
186
186
 
187
187
  /*
188
- * The error listener for "factoryCreateError" is registered in order to find out failed connection attempts.
189
- * If it fails due to invalid credentials, we delete the current pool from the pools map and overwrite the pool factory create function.
190
- * Background is that generic-pool will continue to try to open a connection by calling the factory create function until the "acquireTimeoutMillis" is reached.
188
+ * The error listener for "factoryCreateError" is registered to find out failed connection attempts.
189
+ * If it fails due to invalid credentials, we delete the current pool from the pools map and overwrite the
190
+ * pool factory create function.
191
+ * Background is that generic-pool will continue to try to open a connection by calling the factory create
192
+ * function until the "acquireTimeoutMillis" is reached.
191
193
  * This ends up in many connection attempts for one request even though the credentials are invalid.
192
194
  * Because of the deletion in the map, subsequent requests will fetch the credentials again.
193
195
  */
@@ -218,6 +220,7 @@ async function pool4(tenant, db) {
218
220
  })
219
221
  )
220
222
  }
223
+
221
224
  if ('then' in pools.get(tenant)) {
222
225
  pools.set(tenant, await pools.get(tenant))
223
226
  }
@@ -240,12 +243,17 @@ async function resilientAcquire(pool, attempts = 1) {
240
243
  attempt++
241
244
  }
242
245
  }
246
+
243
247
  if (client) return client
248
+
244
249
  const { borrowed, pending, size, available, max } = pool
250
+ const message =
251
+ 'Acquiring client from pool timed out. Please review your system setup, transaction handling, and pool configuration. ' +
252
+ `Pool State: borrowed: ${borrowed}, pending: ${pending}, size: ${size}, available: ${available}, max: ${max}`
245
253
  err = getError(
246
254
  Object.assign(err, {
247
255
  statusCode: 503,
248
- message: `Acquiring client from pool timed out. Please review your system setup, transaction handling, and pool configuration. Pool State: borrowed: ${borrowed}, pending: ${pending}, size: ${size}, available: ${available}, max: ${max}`
256
+ message
249
257
  })
250
258
  )
251
259
  err._attempts = attempt
@@ -262,9 +270,9 @@ module.exports = {
262
270
  client._pool = pool
263
271
  return client
264
272
  },
265
- release: client => {
266
- return client._pool.release(client)
267
- },
273
+
274
+ release: client => client._pool.release(client),
275
+
268
276
  drain: async tenant => {
269
277
  const pool = pools.get(tenant)
270
278
  if (!pool) return
@@ -62,7 +62,7 @@ const search2Contains = (cqnSearchPhrase, columns) => {
62
62
  const isContainsPredicateSupported = (query, entity, columns2Search) => {
63
63
  const cqnSearchPhrase = query.SELECT.search
64
64
 
65
- if (cqnSearchPhrase && cqnSearchPhrase[0] && cqnSearchPhrase[0].val === ' ') return false
65
+ if (cqnSearchPhrase?.[0]?.val === ' ') return false
66
66
 
67
67
  // REVISIT: In the future, to further optimize search queries, you might
68
68
  // want to remove the following condition(s).