@sap/cds 6.8.1 → 6.8.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,26 @@
4
4
  - The format is based on [Keep a Changelog](http://keepachangelog.com/).
5
5
  - This project adheres to [Semantic Versioning](http://semver.org/).
6
6
 
7
+ ## Version 6.8.3 - 2023-06-13
8
+
9
+ ### Fixed
10
+
11
+ - `cds build` no longer reports CAP Java Classic runtime usage by mistake.
12
+ - `cds version` prints the local `@sap/cds` version, even if called from a different `@sap/cds` installation.
13
+ - User challenges handling in case of `cds.env.auth.restrict_all_services: false`
14
+
15
+ ## Version 6.8.2 - 2023-05-26
16
+
17
+ ### Fixed
18
+
19
+ - EDMX texts for extended tenants based on `@sap/cds-mtx` now appear correctly again.
20
+ - `@assert.range` for DateTime/Date/Time/Timestamp
21
+ - Nested `$expand` OData query to the `texts` compiler-generated composition for entities with localized elements.
22
+ For example, similar OData requests `Entity?$expand=items($expand=item($expand=texts))` now should work as expected.
23
+ - `req.subject` would occasionally be incorrect when a query had been executed prior to it.
24
+ - cds plugins are also fetched from `devDependencies`
25
+ - `cds build` now correctly resolves complex models of mtx extension projects
26
+
7
27
  ## Version 6.8.1 - 2023-05-04
8
28
 
9
29
  ### Fixed
package/README.md CHANGED
@@ -11,3 +11,4 @@ In case you find a bug, please report an [incident](https://cap.cloud.sap/docs/r
11
11
  ## License
12
12
 
13
13
  This package is provided under the terms of the [SAP Developer License Agreement](https://tools.hana.ondemand.com/developer-license-3.1.txt).
14
+
@@ -32,7 +32,7 @@ class MtxExtensionModuleBuilder extends BuildTaskHandlerInternal {
32
32
  if (model) {
33
33
  // extension CSN using parsed format
34
34
  const options = { ...this.options(), flavor: 'parsed' }
35
- const extModel = await cds.load(this.resolveModel(), options)
35
+ const extModel = await cds.load(this._resolveExtensionFiles(model), options)
36
36
  if (extModel.requires) {
37
37
  extModel.requires.length = 0
38
38
  }
@@ -79,6 +79,18 @@ class MtxExtensionModuleBuilder extends BuildTaskHandlerInternal {
79
79
  await new ResourcesTarBuilder(this).writeTarFile(path.join(this.task.dest, 'extension.tgz'), destExt)
80
80
  }
81
81
 
82
+ _resolveExtensionFiles(model) {
83
+ const node_modules = path.join(this.task.src, 'node_modules')
84
+ const paths = model['$sources'].reduce((acc, file) => {
85
+ if (file.startsWith(this.task.src) && !file.startsWith(node_modules)) {
86
+ acc.push(file)
87
+ }
88
+ return acc
89
+ }, [])
90
+
91
+ return paths
92
+ }
93
+
82
94
  _lintExtModel(extModel, model) {
83
95
  const linter = this._linter()
84
96
  if (!linter) {
package/bin/build/util.js CHANGED
@@ -88,7 +88,7 @@ async function isOldJavaStack(dirs) {
88
88
  if (files.length > 0) {
89
89
  return (await Promise.all(files.map(async file => {
90
90
  const content = await fs.promises.readFile(file, 'utf-8')
91
- return content && /<groupId>\s*com\.sap\.cloud\.servicesdk/.test(content) && !(/<groupId>\s*com\.sap\.cds\s*<\/groupId>/.test(content) && /<artifactId>\s*cds-/.test(content))
91
+ return content && /<groupId>\s*com\.sap\.cloud\.servicesdk\.prov\s*<\/groupId>/.test(content)
92
92
  }))).some(result => result)
93
93
  }
94
94
  return false
package/bin/plugins.js CHANGED
@@ -14,7 +14,8 @@ module.exports = async function load_plugins (log = console.log) {
14
14
  }
15
15
  } else
16
16
  try {
17
- const { dependencies } = require(cds.root + '/package.json')
17
+ const pkg = require(cds.root + '/package.json')
18
+ const dependencies = { ...pkg.dependencies, ...(process.env.NODE_ENV !== 'production' && pkg.devDependencies) }
18
19
  for (let each in dependencies) {
19
20
  plugins.push(_load_plugin(each + '/cds-plugin'))
20
21
  }
package/bin/version.js CHANGED
@@ -57,6 +57,7 @@ function list_versions(args, options) { //NOSONAR
57
57
  function info(o) {
58
58
  const { npmGlobalModules } = require('./utils/modules');
59
59
  const main = _findPackage (require.main.filename)
60
+ const sap_cds = require.resolve('@sap/cds/package.json', {paths:[process.cwd(), __dirname]})
60
61
  return {
61
62
  // REVISIT: Why do we need all these different hard-coded ways, including proliferation of arguments?
62
63
  ..._versions4(main, {}, true), // usually sap/cds-dk or sap/cds
@@ -66,6 +67,7 @@ function info(o) {
66
67
  ..._versions4(process.cwd(), {}, null, o),
67
68
  ..._versions4('..', {}, null, o),
68
69
  ..._findMTX(),
70
+ '@sap/cds': require(sap_cds).version, // ensure effective sap/cds version is listed
69
71
  'Node.js': process.version,
70
72
  'home': __dirname.slice(0,-4)
71
73
  }
@@ -84,6 +86,7 @@ function _versions4 (pkg_name, info, parent, o={}) {
84
86
  }
85
87
  const pkg = require(path)
86
88
  info[o.label || pkg.name] = pkg.version
89
+ // console.log(o.label || pkg.name, pkg.version, path)
87
90
  if (!parent || o.all) for (let d in pkg.dependencies) { // recurse sap packages in dependencies...
88
91
  if (!(d in info) && (d.startsWith('@sap/') || d.startsWith('@cap-js/'))) _versions4(d, info, pkg.name, o)
89
92
  }
@@ -128,7 +128,6 @@ module.exports = function cds_compile_for_lean_drafts(csn) {
128
128
  }
129
129
  draft.elements[each] = newEl
130
130
  }
131
- // TODO: Redirect associations to localized
132
131
  return draft
133
132
  }
134
133
  for (const name in csn.definitions) {
@@ -106,6 +106,7 @@ class Request extends require('./event') {
106
106
  // create key value pair without IsActiveEntity & adjust root target
107
107
  const keys = {}
108
108
  let id = item.id
109
+ if (!id) return
109
110
  for (let j = 0; j < item?.where?.length; j = j + 4) {
110
111
  const key = item.where[j].ref[0]
111
112
  const value = item.where[j + 2].val
@@ -126,7 +127,7 @@ class Request extends require('./event') {
126
127
  warn (...args) { return this._messages.add (3, ...args) }
127
128
  error (...args) { return this._errors.add (4, ...args) }
128
129
  reject (...args) {
129
- if (args[0] === 401 && this._.req?.login) return this._.req.login()
130
+ if (args[0] === 401 && this._.req?.login && !this._.req?.logIn) return this._.req.login()
130
131
  let e = this.error(...args)
131
132
  if (!e.stack) Error.captureStackTrace (e = Object.assign(new Error,e), this.reject)
132
133
  throw e
@@ -22,6 +22,8 @@ const metadata = service => {
22
22
  const { 'cds.xt.ModelProviderService': mps } = cds.services
23
23
  let edmx = mps
24
24
  ? await mps.getEdmx({ tenant, model: service.model, service: service.definition.name, locale })
25
+ : cds.mtx && cds.mtx.isExtended(tenant)
26
+ ? await cds.mtx.getEdmx(tenant, service.definition.name, locale)
25
27
  : cds.localize(
26
28
  service.model,
27
29
  locale,
@@ -112,10 +112,33 @@ const _checkISODateTime = value => (_checkString(value) && ISO_DATE_TIME_REGEX.t
112
112
 
113
113
  const _checkISOTimestamp = value => (_checkString(value) && ISO_TIMESTAMP_REGEX.test(value)) || value instanceof Date
114
114
 
115
- const _checkInRange = (val, range) => {
116
- return _checkISODate(val)
117
- ? (new Date(val) - new Date(range[0])) * (new Date(val) - new Date(range[1])) <= 0
118
- : (val - range[0]) * (val - range[1]) <= 0
115
+ const _checkDateValue = (val, r1, r2) => {
116
+ const dateVal = new Date(val)
117
+ return (dateVal - new Date(r1)) * (dateVal - new Date(r2)) <= 0
118
+ }
119
+
120
+ const _toDate = val => `2000-01-01T${val}Z`
121
+
122
+ const _checkInRange = (val, range, type) => {
123
+ switch (type) {
124
+ case 'cds.Date':
125
+ return _checkISODate(val) && _checkDateValue(val, range[0], range[1])
126
+ case 'cds.DateTime':
127
+ return _checkISODateTime(val) && _checkDateValue(val, range[0], range[1])
128
+ case 'cds.Timestamp':
129
+ return _checkISOTimestamp(val) && _checkDateValue(val, range[0], range[1])
130
+ case 'cds.Time':
131
+ return _checkISOTime(val) && _checkDateValue(_toDate(val), _toDate(range[0]), _toDate(range[1]))
132
+ default:
133
+ return (val - range[0]) * (val - range[1]) <= 0
134
+ }
135
+ }
136
+
137
+ const _resolveCDSType = element => {
138
+ if (element.type.startsWith('cds.')) return element.type
139
+ if (!element.type) return
140
+
141
+ return _resolveCDSType(element.__proto__)
119
142
  }
120
143
 
121
144
  // process.env.CDS_ASSERT_FORMAT_FLAGS not official!
@@ -227,7 +250,7 @@ const _checkEnumElement = (element, value, errors, key, pathSegmentsInfo) => {
227
250
 
228
251
  const _checkRangeElement = (element, value, errors, key, pathSegmentsInfo) => {
229
252
  const rangeElements = element['@assert.range'] && !_getEnumElement(element) ? element['@assert.range'] : undefined
230
- if (rangeElements && !_checkInRange(value, rangeElements)) {
253
+ if (rangeElements && !_checkInRange(value, rangeElements, _resolveCDSType(element))) {
231
254
  const args = [value, ...element['@assert.range']]
232
255
  errors.push(assertError({ code: ASSERT_RANGE, args }, element, value, key, pathSegmentsInfo))
233
256
  }
@@ -40,11 +40,14 @@ const _relationHandler = relation => ({
40
40
  if (newRelation) {
41
41
  target[prop] = new Proxy(_exposeRelation(newRelation), _relationHandler(newRelation))
42
42
  }
43
+
43
44
  return target[prop]
44
45
  }
46
+
45
47
  target[prop] = path.reduce((relation, value) => relation[value] || relation.csn._relations[value], relation)
46
48
  target[prop].path = path
47
49
  }
50
+
48
51
  return target[prop]
49
52
  }
50
53
  })
@@ -43,6 +43,7 @@ exports.impl = cds.service.impl(function () {
43
43
  // - INSERT has no where clause to do this in one roundtrip
44
44
  // - SELECT returns [] -> really empty collection or invalid path?
45
45
  let pathExistsQuery
46
+
46
47
  const { ref } = (req.query.INSERT && req.query.INSERT.into) || (req.query.SELECT && req.query.SELECT.from) || {}
47
48
  // REVISIT: why is copy necessary?
48
49
  if (ref && ref.length > 1) pathExistsQuery = SELECT(1).from({ ref: deepCopyArray(ref.slice(0, -1)) })
@@ -59,7 +60,6 @@ exports.impl = cds.service.impl(function () {
59
60
 
60
61
  // if no keys available, select all columns so we can delete the singleton with same content
61
62
  if (keyColumns.length) selectSingleton.columns(keyColumns)
62
-
63
63
  const singleton = await cds.tx(req).run(selectSingleton)
64
64
  if (!singleton) req.reject(404)
65
65
 
@@ -8,6 +8,7 @@ const _toRef = (alias, column) => {
8
8
  const _adaptRefs = (onCond, path, { select, join }) => {
9
9
  const _adaptEl = el => {
10
10
  const ref = el.ref
11
+
11
12
  if (ref) {
12
13
  if (ref[0] === path.join('_') && ref[1]) {
13
14
  return _toRef(select, ref.slice(1))
@@ -19,41 +20,34 @@ const _adaptRefs = (onCond, path, { select, join }) => {
19
20
  }
20
21
 
21
22
  return _toRef(join, ref.slice(0))
22
- } else if (el.xpr) {
23
- return { xpr: el.xpr.map(_adaptEl) }
24
23
  }
25
24
 
25
+ if (el.xpr) return { xpr: el.xpr.map(_adaptEl) }
26
26
  return el
27
27
  }
28
+
28
29
  return onCond.map(_adaptEl)
29
30
  }
30
31
 
31
32
  const _args = (csnElement, path, aliases) => {
32
33
  const onCond = csnElement.on
33
-
34
- if (!onCond || !onCond.length) {
35
- return []
36
- }
37
-
38
- if (onCond.length < 3) {
39
- return onCond
40
- }
41
-
34
+ if (!onCond || onCond.length === 0) return []
35
+ if (onCond.length < 3 && !onCond[0]?.xpr) return onCond
42
36
  if (!csnElement._isSelfManaged) return _adaptRefs(onCond, path, aliases)
43
37
 
44
38
  // revert join and select aliases because of backlink
45
- const oc = _newOnConditions(csnElement._backlink, [csnElement._backlink.name], {
39
+ const mutOnCond = _newOnConditions(csnElement._backlink, [csnElement._backlink.name], {
46
40
  select: aliases.join,
47
41
  join: aliases.select
48
42
  })
49
43
 
50
44
  if (onCond.some(e => e === 'and')) {
51
- // managed with ON-conditions must contain `$self`, which we replace with `oc`
52
- const onCondWithouSelf = _adaptRefs(_onCondWithout$self(onCond), path, aliases)
53
- oc.push('and', ...onCondWithouSelf)
45
+ // managed with ON-conditions must contain `$self`, which we replace with `mutOnCond`
46
+ const onCondWithoutSelf = _adaptRefs(_onCondWithout$self(onCond), path, aliases)
47
+ mutOnCond.push('and', ...onCondWithoutSelf)
54
48
  }
55
49
 
56
- return oc
50
+ return mutOnCond
57
51
  }
58
52
 
59
53
  const _isSelfRef = e => e && e.ref && e.ref[0] === '$self'
@@ -64,23 +58,27 @@ const _onCondWithout$self = onCond => {
64
58
  if (e === 'and') return _isSelfRef(on[i + 1]) || _isSelfRef(on[i + 3])
65
59
  return on[i + 1] === '=' && (_isSelfRef(e) || _isSelfRef(on[i + 2]))
66
60
  })
61
+
67
62
  onCondWithoutSelf.splice(selfIndex, 4)
68
63
  return onCondWithoutSelf
69
64
  }
70
65
 
66
+ // this is only for 2one managed w/o on-conditions, i.e. no static values are possible
71
67
  const _foreignToOn = (csnElement, path, { select, join }) => {
72
- // this is only for 2one managed w/o ON-conditions i.e. no static values are possible
73
68
  const on = []
69
+
74
70
  for (const key of csnElement._foreignKeys) {
75
71
  if (on.length !== 0) {
76
72
  on.push('and')
77
73
  }
74
+
78
75
  const prefixChild = prefixForStruct(key.childElement)
79
76
  const ref1 = _toRef(select, prefixChild + key.childElement.name)
80
77
  const structPrefix = path.length > 1 ? path.slice(0, -1) : []
81
78
  const ref2 = _toRef(join, [...structPrefix, key.parentElement.name])
82
79
  on.push(ref1, '=', ref2)
83
80
  }
81
+
84
82
  return on
85
83
  }
86
84
 
@@ -93,10 +91,8 @@ const _newOnConditions = (csnElement, path, aliases) => {
93
91
  }
94
92
 
95
93
  const getOnCond = (csnElement, path = [], aliases = { select: '', join: '' }) => {
96
- const oncond = _newOnConditions(csnElement, path, aliases)
97
- return [{ xpr: oncond }]
94
+ const onCond = _newOnConditions(csnElement, path, aliases)
95
+ return [{ xpr: onCond }]
98
96
  }
99
97
 
100
- module.exports = {
101
- getOnCond
102
- }
98
+ module.exports = { getOnCond }
@@ -117,11 +117,9 @@ class JoinCQNFromExpanded {
117
117
  _createJoinCQNFromExpanded(SELECT, toManyTree, defaultLanguage) {
118
118
  const joinArgs = SELECT.from.args
119
119
  const isJoinOfTwoSelects = joinArgs?.every(a => a.SELECT)
120
-
121
120
  const unionTableRef = this._getUnionTable(SELECT)
122
121
  const unionTable = unionTableRef?.table
123
122
  const tableAlias = this._getTableAlias(SELECT, toManyTree)
124
-
125
123
  const readToOneCQN = this._getReadToOneCQN(SELECT, isJoinOfTwoSelects ? 'filterExpand' : tableAlias)
126
124
 
127
125
  if (isJoinOfTwoSelects) {
@@ -133,6 +131,7 @@ class JoinCQNFromExpanded {
133
131
  .forEach(c => {
134
132
  mappings[c.as.replace(prefix, '')] = c.as
135
133
  })
134
+
136
135
  // expand to one
137
136
  const entity = this._csn.definitions[joinArgs[0].SELECT.from.SET.args[1].SELECT.from.ref[0]]
138
137
  this._addImplicitOrderBy(readToOneCQN, entity, tableAlias)
@@ -146,9 +145,7 @@ class JoinCQNFromExpanded {
146
145
  const entity = this._getEntityForTable(table)
147
146
  this._addImplicitOrderBy(readToOneCQN, entity, tableAlias)
148
147
  if (unionTable) readToOneCQN[IS_UNION_DRAFT] = true
149
-
150
148
  readToOneCQN[IS_ACTIVE] = isDraftTree ? this._isDraftTargetActive(table) : true
151
-
152
149
  const givenColumns = readToOneCQN.columns
153
150
  readToOneCQN.columns = []
154
151
  if (entity['@cds.localized'] === false) defaultLanguage = true
@@ -500,11 +497,11 @@ class JoinCQNFromExpanded {
500
497
  _expandedToFlat({ entity, givenColumns, readToOneCQN, tableAlias, toManyTree, defaultLanguage }) {
501
498
  const toManyColumns = []
502
499
  const mappings = this._getMappingObject(toManyTree)
503
-
504
500
  const readToOneCQNCopy = getCqnCopy(readToOneCQN)
505
501
 
506
502
  for (const column of givenColumns) {
507
503
  let navigation
504
+
508
505
  if (column.expand) {
509
506
  navigation = getNavigationIfStruct(entity, tableAlias === column.ref[0] ? column.ref.slice(1) : column.ref)
510
507
  if (this._skip(navigation && navigation._target)) continue
@@ -520,6 +517,7 @@ class JoinCQNFromExpanded {
520
517
  // Expands with to one target can be processed directly
521
518
  const navProp = column.ref[column.ref.length - 1]
522
519
  const navTarget = entity.elements[navProp]
520
+
523
521
  if (
524
522
  entity._isDraftEnabled &&
525
523
  navTarget._isAssociationStrict &&
@@ -529,6 +527,7 @@ class JoinCQNFromExpanded {
529
527
  ) {
530
528
  mappings[navProp] = { [TO_ACTIVE]: true }
531
529
  }
530
+
532
531
  this._addJoinAndElements({
533
532
  column,
534
533
  entity,
@@ -539,7 +538,8 @@ class JoinCQNFromExpanded {
539
538
  })
540
539
  } else {
541
540
  // No expand, directly add the column and its mapping.
542
- readToOneCQN.columns.push(this._addAliasToColumn(column, entity, tableAlias, mappings))
541
+ const columnAliased = this._addAliasToColumn(column, entity, tableAlias, mappings)
542
+ readToOneCQN.columns.push(columnAliased)
543
543
 
544
544
  // REVISIT required for other cqn properties as well?
545
545
  this.adjustOrderBy(readToOneCQN.orderBy, mappings, column, tableAlias)
@@ -556,6 +556,7 @@ class JoinCQNFromExpanded {
556
556
  }
557
557
  }
558
558
  }
559
+
559
560
  // only as second step handle expand to many, or else keys might still be unknown
560
561
  this._toMany({
561
562
  entity,
@@ -658,7 +659,6 @@ class JoinCQNFromExpanded {
658
659
  const extendedToManyTree = toManyTree.concat(column.ref[0] === parentAlias ? column.ref.slice(1) : column.ref)
659
660
  const tableAlias = this._createAlias(extendedToManyTree.join(':'))
660
661
  const target = this._getTarget(entity, column, parentAlias)
661
-
662
662
  const name = column.ref[column.ref.length - 1]
663
663
  const element = name && entity.elements[name]
664
664
 
@@ -765,8 +765,7 @@ class JoinCQNFromExpanded {
765
765
  const givenColumns = column.expand.map(col => {
766
766
  if (
767
767
  activeTableRequired &&
768
- col.ref &&
769
- col.ref.length &&
768
+ col.ref?.length &&
770
769
  (col.ref[0] === 'IsActiveEntity' || col.ref[0] === 'HasActiveEntity')
771
770
  ) {
772
771
  return {
@@ -974,9 +973,7 @@ class JoinCQNFromExpanded {
974
973
  */
975
974
  _addAliasToColumn(column, entity, tableAlias, mappings) {
976
975
  // No identifier for this row entry or technical column
977
- if (this._isAliasNotNeeded(column)) {
978
- return column
979
- }
976
+ if (this._isAliasNotNeeded(column)) return column
980
977
 
981
978
  if (Array.isArray(column.xpr)) {
982
979
  return this._buildNewAliasColumn(
@@ -995,6 +992,7 @@ class JoinCQNFromExpanded {
995
992
  mappings
996
993
  )
997
994
  }
995
+
998
996
  return this._buildNewAliasColumn(column, entity, tableAlias, mappings)
999
997
  }
1000
998
 
@@ -1075,10 +1073,7 @@ class JoinCQNFromExpanded {
1075
1073
  defaultLanguage,
1076
1074
  readToOneCQNCopy
1077
1075
  }) {
1078
- if (toManyColumns.length === 0) {
1079
- return
1080
- }
1081
-
1076
+ if (toManyColumns.length === 0) return
1082
1077
  this._addKeysIfNeeded({ entity, readToOneCQN, tableAlias })
1083
1078
 
1084
1079
  for (const { column, parentAlias } of toManyColumns) {
@@ -1091,6 +1086,7 @@ class JoinCQNFromExpanded {
1091
1086
  parentAlias,
1092
1087
  defaultLanguage
1093
1088
  })
1089
+
1094
1090
  this._createJoinCQNFromExpanded(select, toManyTree.concat([column.ref[column.ref.length - 1]]), defaultLanguage)
1095
1091
  }
1096
1092
  }
@@ -1162,7 +1158,6 @@ class JoinCQNFromExpanded {
1162
1158
  // eslint-disable-next-line complexity
1163
1159
  _buildExpandedCQN({ column, entity, readToOneCQN, toManyTree, mappings, parentAlias, defaultLanguage }) {
1164
1160
  const isUnion = !!readToOneCQN.from.SET
1165
-
1166
1161
  const colRef = parentAlias === column.ref[0] ? column.ref.slice(1) : column.ref.slice(0)
1167
1162
  const element = entity.elements[colRef[0]]
1168
1163
  const colTarget = ensureUnlocalized(element.target)
@@ -1170,24 +1165,21 @@ class JoinCQNFromExpanded {
1170
1165
  defaultLanguage ||
1171
1166
  entity['@cds.localized'] === false ||
1172
1167
  this._csn.definitions[colTarget]['@cds.localized'] === false
1173
-
1174
1168
  const expandActive =
1175
1169
  readToOneCQN[IS_ACTIVE] ||
1176
1170
  (element._isAssociationStrict && !element['@odata.draft.enclosed']) ||
1177
1171
  !this._csn.definitions[colTarget]._isDraftEnabled
1178
-
1179
1172
  const ref = this._refFromRefByExpand(column.ref[0], colTarget, defaultLanguageThis, expandActive)
1180
1173
  const tableAlias = this._createAlias(toManyTree.concat(colRef).join(':'))
1181
1174
  const on = entity._relations[colRef[0]].join(tableAlias, 'filterExpand')
1182
1175
  const filterExpand = this._getFilterExpandCQN(readToOneCQN, on, parentAlias, entity.keys)
1183
1176
  const expandedEntity = this._csn.definitions[colTarget]
1184
1177
  const joinColumns = this._getJoinColumnsFromOnAddToMapping(mappings[colRef[0]], parentAlias, on, entity)
1185
-
1186
1178
  let cqn = {
1187
1179
  from: {
1188
1180
  join: 'inner',
1189
1181
  args: [{ ref: [ref], as: tableAlias }, filterExpand],
1190
- on: on
1182
+ on
1191
1183
  }
1192
1184
  }
1193
1185
 
@@ -1213,16 +1205,17 @@ class JoinCQNFromExpanded {
1213
1205
  }
1214
1206
 
1215
1207
  if (column.limit) throw getError(501, 'Pagination is not supported in expand')
1216
-
1217
1208
  cqn = this._adaptWhereOrderBy(cqn, tableAlias)
1218
1209
 
1219
1210
  if (isUnion) {
1220
1211
  const cols = column.expand.filter(c => !c.expand && !(c.ref[0] in DRAFT_COLUMNS_MAP)).map(c => c.ref[0])
1212
+
1221
1213
  // ensure the join columns are selected
1222
1214
  for (const each of joinColumns) {
1223
1215
  const col = each.ref[each.ref.length - 1]
1224
1216
  if (!cols.includes(col)) cols.push(col)
1225
1217
  }
1218
+
1226
1219
  // ensure the foreign keys are selected in case of expand to one
1227
1220
  for (const each of cqn.columns) {
1228
1221
  if (each.expand) {
@@ -1240,14 +1233,17 @@ class JoinCQNFromExpanded {
1240
1233
  )
1241
1234
  const user = (cds.context && cds.context.user && cds.context.user.id) || 'anonymous'
1242
1235
  const unionFrom = getCQNUnionFrom(cols, ref.replace(/_drafts$/, ''), ref, ks, user)
1236
+
1243
1237
  for (const each of cqn.columns) {
1244
1238
  if (!each.as) continue
1239
+
1245
1240
  // replace val with ref
1246
1241
  if (each.as === 'IsActiveEntity' || each.as === 'HasActiveEntity') {
1247
1242
  delete each.val
1248
1243
  each.ref = [tableAlias, each.as]
1249
1244
  each.as = tableAlias + '_' + each.as
1250
1245
  }
1246
+
1251
1247
  // ensure the cast
1252
1248
  if (
1253
1249
  each.as.match(/IsActiveEntity$/) ||
@@ -1257,6 +1253,7 @@ class JoinCQNFromExpanded {
1257
1253
  each.cast = { type: 'cds.Boolean' }
1258
1254
  }
1259
1255
  }
1256
+
1260
1257
  const cs = cqn.columns
1261
1258
  .filter(c => !c.expand && c.ref && c.ref[0] === tableAlias)
1262
1259
  .map(c => ({ ref: [c.ref[1]] }))
@@ -1280,6 +1277,7 @@ class JoinCQNFromExpanded {
1280
1277
  const sort = element.sort
1281
1278
  if (element.args)
1282
1279
  return { func: element.func, args: this._copyOrderBy(element.args, alias, expandedEntity), sort }
1280
+
1283
1281
  const ref =
1284
1282
  element.ref[0] === alias
1285
1283
  ? [...element.ref]
@@ -1288,6 +1286,7 @@ class JoinCQNFromExpanded {
1288
1286
  : this._isPathExpressionToOne(element.ref, expandedEntity)
1289
1287
  ? [alias, ...element.ref]
1290
1288
  : [alias, element.ref[1]]
1289
+
1291
1290
  return (sort && { ref, sort }) || { ref }
1292
1291
  })
1293
1292
  }
@@ -1306,6 +1305,7 @@ class JoinCQNFromExpanded {
1306
1305
  where: where
1307
1306
  }
1308
1307
  }
1308
+
1309
1309
  return {
1310
1310
  xpr: ['case', 'when', hasDraftQuery, 'IS NOT NULL', 'then', 'true', 'else', 'false', 'end'],
1311
1311
  as: 'HasDraftEntity',
@@ -1370,6 +1370,7 @@ class JoinCQNFromExpanded {
1370
1370
  outerColumns.push(...outerCols)
1371
1371
  continue
1372
1372
  }
1373
+
1373
1374
  if (typeof entry === 'object' && entry.ref && entry.ref[0] === 'filterExpand') {
1374
1375
  columns.push(this._getColumnObjectForFilterExpand(readToOneCQN, parentAlias, entry.ref[1]))
1375
1376
  outerColumns.push({ ref: [entry.ref[1]] })
@@ -1466,6 +1467,7 @@ class JoinCQNFromExpanded {
1466
1467
  }
1467
1468
  struct = current.elements[key.replace(parentAlias + '_', '')]
1468
1469
  }
1470
+
1469
1471
  // build value for spreading (cf. mapping[GET_KEY_VALUE])
1470
1472
  value = []
1471
1473
  for (const k in struct.elements) {
@@ -1482,7 +1484,8 @@ class JoinCQNFromExpanded {
1482
1484
 
1483
1485
  _addColumNames(entity, parentAlias, columnNames) {
1484
1486
  for (const keyName in entity.keys) {
1485
- if (entity.keys[keyName].is2one || entity.keys[keyName].is2many) continue
1487
+ const key = entity.keys[keyName]
1488
+ if (key.is2one || key.is2many) continue
1486
1489
  const columnNameAlt = keyName === 'IsActiveEntity' ? 'IsActiveEntity' : `${parentAlias}_${keyName}`
1487
1490
  if (!columnNames.includes(columnNameAlt)) {
1488
1491
  columnNames.push(columnNameAlt)
@@ -1511,6 +1514,7 @@ class JoinCQNFromExpanded {
1511
1514
  columns.push(...this._getJoinColumnsFromOnAddToMapping(mapping, parentAlias, entry.xpr, entity))
1512
1515
  continue
1513
1516
  }
1517
+
1514
1518
  if (typeof entry === 'object' && entry.ref && entry.ref[0] !== 'filterExpand') {
1515
1519
  const as = entry.ref.join('_')
1516
1520
  columns.push({
@@ -1530,6 +1534,7 @@ class JoinCQNFromExpanded {
1530
1534
 
1531
1535
  for (const key of keyList) {
1532
1536
  const parts = key.split('_')
1537
+
1533
1538
  // For draft-enabled entities, associations may not take over 'IsActiveEntity', e.g.
1534
1539
  // when a draft points to an active entity
1535
1540
  if (parts[parts.length - 1] !== 'IsActiveEntity') {
@@ -1577,7 +1582,6 @@ class JoinCQNFromExpanded {
1577
1582
  this._addMissingJoinElements(columns, joinColumns)
1578
1583
  this._addMissingKeyColumns(columns, tableAlias, keys, isActive, entity)
1579
1584
  this._addMissingParentKeyColumns(columns, 'filterExpand', parentKeys, isActive)
1580
-
1581
1585
  return columns
1582
1586
  }
1583
1587
 
@@ -1606,11 +1610,13 @@ class JoinCQNFromExpanded {
1606
1610
  columns.push(this._createCalculatedBooleanColumn('IsActiveEntity', isActive))
1607
1611
  return
1608
1612
  }
1613
+
1609
1614
  if (isActive) {
1610
1615
  if (columnName === 'HasActiveEntity') {
1611
1616
  columns.push(this._createCalculatedBooleanColumn('HasActiveEntity', false))
1612
1617
  return
1613
1618
  }
1619
+
1614
1620
  if (columnName === 'HasDraftEntity') {
1615
1621
  columns.push(this._getHasDraftEntityXpr(entity, tableAlias))
1616
1622
  return
@@ -14,6 +14,9 @@ const _isLinked = req => {
14
14
  function handler(req) {
15
15
  if (typeof req.query === 'string') return
16
16
 
17
+ // invoke req.subject before it gets modified
18
+ req.subject
19
+
17
20
  if (!this.model) {
18
21
  // best-effort rewrite of path in from
19
22
  req.query = cqn2cqn4sql(req.query, { definitions: {} }, { service: this })
@@ -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', {
@@ -369,17 +392,17 @@ const Read = {
369
392
  drafts = []
370
393
  }
371
394
  }
372
- Read.merge(query._target, actives, drafts, (row, other) =>
373
- other
374
- ? Object.assign(row, other, { IsActiveEntity: true, HasActiveEntity: false, HasDraftEntity: true })
375
- : Object.assign(row, {
376
- IsActiveEntity: true,
377
- HasActiveEntity: false,
378
- HasDraftEntity: false,
379
- DraftAdministrativeData: null,
380
- DraftAdministrativeData_DraftUUID: null
381
- })
382
- )
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
+ })
383
406
  return _requested(actives, query)
384
407
  },
385
408
  unchanged: async function (run, query) {
@@ -421,12 +444,12 @@ const Read = {
421
444
  cds.context.user.id
422
445
  )
423
446
  const drafts = await run(draftsQuery)
424
- Read.merge(query._target, drafts, [], row =>
447
+ Read.merge(query._target, drafts, [], row => {
425
448
  Object.assign(row, {
426
- HasDraftEntity: false,
427
- IsActiveEntity: false
449
+ HasDraftEntity: false
428
450
  })
429
- )
451
+ _fillIsActiveEntity(row, false, query._drafts._target)
452
+ })
430
453
  return _requested(drafts, query)
431
454
  },
432
455
  all: async function (run, query) {
@@ -468,18 +491,17 @@ const Read = {
468
491
  const count = isFirstPage ? ownNewDrafts.length + (isCount ? actives[0]?.$count : actives.$count) : actives.$count
469
492
  if (isCount) return { $count: count }
470
493
 
471
- Read.merge(query._target, ownDrafts, [], row =>
494
+ Read.merge(query._target, ownDrafts, [], row => {
472
495
  Object.assign(row, {
473
- IsActiveEntity: false,
474
496
  HasDraftEntity: false
475
497
  })
476
- )
498
+ _fillIsActiveEntity(row, false, query._drafts._target)
499
+ })
477
500
  Read.delete(query._target, actives, ownEditDrafts)
478
501
  const otherEditDrafts = await Read.complementaryDrafts(run, query, actives)
479
502
  Read.merge(query._target, actives, otherEditDrafts, (row, other) => {
480
503
  if (other) {
481
504
  Object.assign(row, {
482
- IsActiveEntity: true,
483
505
  HasDraftEntity: true,
484
506
  HasActiveEntity: false,
485
507
  DraftAdministrativeData_DraftUUID: other.DraftAdministrativeData_DraftUUID,
@@ -487,13 +509,13 @@ const Read = {
487
509
  })
488
510
  } else {
489
511
  Object.assign(row, {
490
- IsActiveEntity: true,
491
512
  HasDraftEntity: false,
492
513
  HasActiveEntity: false,
493
514
  DraftAdministrativeData_DraftUUID: null,
494
515
  DraftAdministrativeData: null
495
516
  })
496
517
  }
518
+ _fillIsActiveEntity(row, true, query._target)
497
519
  })
498
520
  const res = isFirstPage ? [...ownNewDrafts, ...ownEditDrafts, ...actives] : actives
499
521
  if (query.SELECT.count) res.$count = count
@@ -519,11 +541,11 @@ const Read = {
519
541
  const actives = drafts.length
520
542
  ? await run(query.where(Read.whereIn(query._target, drafts)))
521
543
  : Object.assign([], { $count: 0 })
522
- Read.merge(query._target, actives, drafts, (row, other) =>
523
- other
524
- ? Object.assign(row, other, { IsActiveEntity: true, HasDraftEntity: true, HasActiveEntity: false })
525
- : Object.assign({ IsActiveEntity: true, HasDraftEntity: false, HasActiveEntity: false })
526
- )
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
+ })
527
549
  return _requested(actives, query)
528
550
  },
529
551
  unsavedChangesByAnotherUser: async function (run, query) {
@@ -834,14 +856,14 @@ async function onNew(req) {
834
856
  let DraftUUID
835
857
  if (isRoot) DraftUUID = cds.utils.uuid()
836
858
  else {
837
- const rootData = await this.run(
838
- 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)
839
861
  .columns([
840
862
  { ref: ['DraftAdministrativeData_DraftUUID'] },
841
863
  { ref: ['DraftAdministrativeData'], expand: [{ ref: ['InProcessByUser'] }] }
842
864
  ])
843
865
  .where(req.query.INSERT.into.ref[0].where)
844
- )
866
+ })
845
867
  if (!rootData) req.reject(404)
846
868
  if (rootData.DraftAdministrativeData?.InProcessByUser !== req.user.id)
847
869
  req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER', [rootData.DraftAdministrativeData.InProcessByUser])
@@ -889,7 +911,7 @@ async function onNew(req) {
889
911
  delete draftData.IsActiveEntity
890
912
  const draftCQN = INSERT.into(req.target).entries(draftData)
891
913
 
892
- await _promiseAll([cds.run(adminDataCQN), this.run(draftCQN)])
914
+ await _promiseAll([cds.run(adminDataCQN), _runWithContext(this, req, { query: draftCQN })])
893
915
  req._.readAfterWrite = true
894
916
  return { ...draftData, IsActiveEntity: false }
895
917
  }
@@ -935,12 +957,12 @@ async function onEdit(req) {
935
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
936
958
  // because we must trigger the app-service request on the target entity (which could be delegated to a remote service).
937
959
  // The best we can do is to catch a potential error
938
- await this.run(activeCheck).catch(_ => {})
960
+ await _runWithContext(this, req, { query: activeCheck }).catch(_ => {})
939
961
 
940
962
  const [res, draft] = await _promiseAll([
941
- this.run(activeCQN),
963
+ _runWithContext(this, req, { query: activeCQN }),
942
964
  // no user check must be done here...
943
- this.run(existingDraft)
965
+ _runWithContext(this, req, { query: existingDraft })
944
966
  ])
945
967
 
946
968
  if (!res) req.reject(404)
@@ -952,7 +974,7 @@ async function onEdit(req) {
952
974
  for (const key in req.target.drafts.keys) keys[key] = res[key]
953
975
  await _promiseAll([
954
976
  DELETE.from('DRAFT.DraftAdministrativeData').where({ DraftUUID }),
955
- this.run(DELETE.from(req.target.drafts).where(keys))
977
+ _runWithContext(this, req, { query: DELETE.from(req.target.drafts).where(keys) })
956
978
  ])
957
979
  }
958
980
 
@@ -973,7 +995,7 @@ async function onEdit(req) {
973
995
  res.DraftAdministrativeData_DraftUUID = DraftUUID
974
996
  res.HasActiveEntity = true
975
997
  delete res.DraftAdministrativeData
976
- await this.run(INSERT.into(targetDraft).entries(res))
998
+ await _runWithContext(this, req, { query: INSERT.into(targetDraft).entries(res) })
977
999
 
978
1000
  // REVISIT: we need to use okra API here because it must be set in the batched request
979
1001
  // status code must be set in handler to allow overriding for FE V2
@@ -995,18 +1017,28 @@ async function onCancel(req) {
995
1017
  ])
996
1018
  // do not add InProcessByUser restriction
997
1019
  Object.defineProperty(draftDelete, '_draftParams', { value: draftParams, enumerable: false })
998
- const draft = await this.run(draftDelete)
1020
+ const draft = await _runWithContext(this, req, { query: draftDelete })
999
1021
  if (draftParams.IsActiveEntity === false && !draft) req.reject(404)
1000
1022
  if (draft && draft.DraftAdministrativeData?.InProcessByUser !== cds.context.user.id)
1001
1023
  req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER', [draft.DraftAdministrativeData?.InProcessByUser])
1002
- const deletes = !draft ? [] : [this.run(DELETE.from({ ref: req.query.DELETE.from.ref }))]
1024
+ const queries = !draft ? [] : [_runWithContext(this, req, { query: DELETE.from({ ref: req.query.DELETE.from.ref }) })]
1003
1025
  if (draft && req.target['@Common.DraftRoot.ActivationAction'])
1004
1026
  // only for draft root
1005
- deletes.push(
1027
+ queries.push(
1006
1028
  DELETE.from('DRAFT.DraftAdministrativeData').where({ DraftUUID: draft.DraftAdministrativeData_DraftUUID })
1007
1029
  )
1008
- if (draftParams.IsActiveEntity) deletes.push(this.run(DELETE.from({ ref: activeRef })))
1009
- 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)
1010
1042
  return req.data
1011
1043
  }
1012
1044
 
@@ -1026,7 +1058,7 @@ async function onPrepare(req) {
1026
1058
  .columns(keys)
1027
1059
  .where(where)
1028
1060
  Object.defineProperty(draftQuery, '_draftParams', { value: draftParams, enumerable: false })
1029
- const data = await this.run(draftQuery)
1061
+ const data = await _runWithContext(this, req, { query: draftQuery })
1030
1062
  if (!data) req.reject(404)
1031
1063
  if (data.DraftAdministrativeData?.InProcessByUser !== req.user.id)
1032
1064
  req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER', [data.DraftAdministrativeData?.InProcessByUser])
@@ -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
 
@@ -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
  }
@@ -38,6 +38,7 @@ const _setCorrectValue = (el, data, params, kind) => {
38
38
  const _buildPartialUrlFunctions = (url, data, params, kind = 'odata-v4') => {
39
39
  const funcParams = []
40
40
  const queryOptions = []
41
+
41
42
  // REVISIT: take params from params after importer fix (the keys should not be part of params)
42
43
  for (const param in _extractParamsFromData(data, params)) {
43
44
  if (kind === 'odata-v2') {
@@ -47,6 +48,7 @@ const _buildPartialUrlFunctions = (url, data, params, kind = 'odata-v4') => {
47
48
  queryOptions.push(`@${param}=${_setCorrectValue(param, data, params, kind)}`)
48
49
  }
49
50
  }
51
+
50
52
  return kind === 'odata-v2'
51
53
  ? `${url}?${funcParams.join('&')}`
52
54
  : `${url}(${funcParams.join(',')})?${queryOptions.join('&')}`
@@ -61,9 +63,11 @@ const _extractParamsFromData = (data, params = {}) => {
61
63
 
62
64
  const _buildKeys = (req, kind) => {
63
65
  const keys = []
66
+
64
67
  if (req.params && req.params.length > 0) {
65
68
  const p1 = req.params[0]
66
69
  if (typeof p1 !== 'object') return [p1]
70
+
67
71
  for (const key in req.target.keys) {
68
72
  keys.push(`${key}=${formatVal(p1[key], key, req.target, kind)}`)
69
73
  }
@@ -73,6 +77,7 @@ const _buildKeys = (req, kind) => {
73
77
  keys.push(`${key}=${formatVal(req.data[key], key, req.target, kind)}`)
74
78
  }
75
79
  }
80
+
76
81
  return keys
77
82
  }
78
83
 
@@ -98,6 +103,7 @@ const _handleUnboundActionFunction = (srv, def, req, event) => {
98
103
  def &&
99
104
  def.returns &&
100
105
  (def.returns.type === 'cds.LargeBinary' || def.returns.type === 'cds.Binary')
106
+
101
107
  return srv.send({ method: 'POST', path: `/${event}`, data: req.data, _binary: isBinary })
102
108
  }
103
109
 
@@ -121,22 +127,26 @@ const _handleV2ActionFunction = (srv, def, req, event, kind) => {
121
127
  const _handleV2BoundActionFunction = (srv, def, req, event, kind) => {
122
128
  const params = []
123
129
  const data = req.data
130
+
124
131
  // REVISIT: take params from def.params, after importer fix (the keys should not be part of params)
125
132
  for (const param in _extractParamsFromData(req.data, def.params)) {
126
133
  params.push(`${param}=${formatVal(data[param], param, { elements: def.params }, kind)}`)
127
134
  }
135
+
128
136
  const keys = _buildKeys(req, this.kind)
129
137
  if (keys.length === 1 && typeof req.params[0] !== 'object') {
130
138
  params.push(`${Object.keys(req.target.keys)[0]}=${keys[0]}`)
131
139
  } else {
132
140
  params.push(...keys)
133
141
  }
142
+
134
143
  const url = `${`/${event}`}?${params.join('&')}`
135
144
  return _sendV2RequestActionFunction(srv, def, url)
136
145
  }
137
146
 
138
147
  const _addHandlerActionFunction = (srv, def, target) => {
139
148
  const event = def.name.match(/\w*$/)[0]
149
+
140
150
  if (target) {
141
151
  srv.on(event, target, async function (req) {
142
152
  const shortEntityName = req.target.name.replace(`${this.namespace}.`, '')
@@ -144,18 +154,18 @@ const _addHandlerActionFunction = (srv, def, target) => {
144
154
  const url = `/${shortEntityName}(${_buildKeys(req, this.kind).join(',')})/${this.namespace}.${event}`
145
155
  return _handleBoundActionFunction(srv, def, req, url)
146
156
  })
147
- } else {
148
- srv.on(event, async function (req) {
149
- if (this.kind === 'odata-v2') return _handleV2ActionFunction(srv, def, req, event, this.kind)
150
- return _handleUnboundActionFunction(srv, def, req, event)
151
- })
157
+
158
+ return
152
159
  }
153
- }
154
160
 
155
- const _selectOnlyWithAlias = q => {
156
- return q && q.SELECT && !q.SELECT._transitions && q.SELECT.columns && q.SELECT.columns.some(hasAliasedColumns)
161
+ srv.on(event, async function (req) {
162
+ if (this.kind === 'odata-v2') return _handleV2ActionFunction(srv, def, req, event, this.kind)
163
+ return _handleUnboundActionFunction(srv, def, req, event)
164
+ })
157
165
  }
158
166
 
167
+ const _selectOnlyWithAlias = q => q?.SELECT && !q.SELECT._transitions && q.SELECT?.columns?.some(hasAliasedColumns)
168
+
159
169
  const resolvedTargetOfQuery = q => {
160
170
  const transitions = (typeof q === 'object' && (q.SELECT || q.INSERT || q.UPDATE || q.DELETE)._transitions) || []
161
171
  return transitions.length && [transitions.length - 1].target
@@ -213,6 +223,7 @@ class RemoteService extends cds.Service {
213
223
  // eslint-disable-next-line cds/no-missing-dependencies -- needs to be added by app dev
214
224
  const sdkUtils = require('@sap-cloud-sdk/util')
215
225
  sdkUtils.setGlobalLogLevel('error')
226
+
216
227
  // disable sdk logger once
217
228
  sdkLoggerDisabled = true
218
229
  } catch (err) {
@@ -264,7 +275,6 @@ class RemoteService extends cds.Service {
264
275
 
265
276
  let result = await run(reqOptions, additionalOptions)
266
277
  result = typeof query === 'object' && query.SELECT?.one && Array.isArray(result) ? result[0] : result
267
-
268
278
  return result
269
279
  })
270
280
  }
@@ -312,7 +322,6 @@ class RemoteService extends cds.Service {
312
322
  // REVISIT: We need to provide target explicitly because it's cached already within ensure_target
313
323
  const newReq = new cds.Request({ query: q, target: t, headers: req.headers, _resolved: true, method: req.method })
314
324
  const result = await super.dispatch(newReq)
315
-
316
325
  return postProcess(q, result, this, true)
317
326
  }
318
327
 
@@ -306,12 +306,9 @@ const run = async (
306
306
  // > axios received status >= 400 -> gateway error
307
307
  const msg = e?.response?.data?.error?.message?.value ?? e?.response?.data?.error?.message ?? e.message
308
308
  e.message = msg ? 'Error during request to remote service: \n' + msg : 'Request to remote service failed.'
309
-
310
- const sanitizedError = _getSanitizedError(e, requestConfig, {
311
- suppressRemoteResponseBody: suppressRemoteResponseBody
312
- })
313
-
309
+ const sanitizedError = _getSanitizedError(e, requestConfig, { suppressRemoteResponseBody })
314
310
  const err = Object.assign(new Error(e.message), { statusCode: 502, reason: sanitizedError })
311
+
315
312
  LOG._warn && LOG.warn(err)
316
313
  throw err
317
314
  }
@@ -329,11 +326,7 @@ const run = async (
329
326
  ) {
330
327
  const e = new Error("Received content-type 'text/html' which is not part of accepted content types")
331
328
  e.response = response
332
-
333
- const sanitizedError = _getSanitizedError(e, requestConfig, {
334
- suppressRemoteResponseBody: suppressRemoteResponseBody
335
- })
336
-
329
+ const sanitizedError = _getSanitizedError(e, requestConfig, { suppressRemoteResponseBody })
337
330
  const err = Object.assign(new Error(`Error during request to remote service: ${e.message}`), {
338
331
  statusCode: 502,
339
332
  reason: sanitizedError
@@ -383,6 +376,7 @@ const run = async (
383
376
  if (response.data.d) {
384
377
  return _purgeODataV2(response.data, resolvedTarget, returnType, requestConfig.headers)
385
378
  }
379
+
386
380
  return _purgeODataV4(response.data)
387
381
  }
388
382
 
@@ -114,8 +114,10 @@ function executeSelectCQN(model, dbc, query, user, locale, txTimestamp) {
114
114
  ) {
115
115
  return expandV2(model, dbc, query, user, locale, txTimestamp, executeSelectCQN)
116
116
  }
117
+
117
118
  return _processExpand(model, dbc, query, user, locale, txTimestamp)
118
119
  }
120
+
119
121
  const { sql, values = [] } = sqlFactory(
120
122
  query,
121
123
  {
@@ -9,7 +9,6 @@ const { deepCopyObject } = require('../../_runtime/common/utils/copy')
9
9
 
10
10
  module.exports = async (_req, _res, next) => {
11
11
  let { _srv: srv, _query: query, _target, _data, _params } = _req
12
-
13
12
  let result,
14
13
  status = 200
15
14
 
@@ -19,6 +18,7 @@ module.exports = async (_req, _res, next) => {
19
18
  try {
20
19
  // add the data (as copy, if upsert allowed)
21
20
  query.data(UPSERT_ALLOWED ? deepCopyObject(_data) : _data)
21
+
22
22
  // REVISIT: if PUT, req.method should be PUT -> Crud2Http maps UPSERT to PUT
23
23
  result = await srv.dispatch(new RestRequest({ query, _target, method: _req.method, params: _params }))
24
24
  if (_params && result) Object.assign(result, _params[_params.length - 1])
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sap/cds",
3
- "version": "6.8.1",
3
+ "version": "6.8.3",
4
4
  "description": "SAP Cloud Application Programming Model - CDS for Node.js",
5
5
  "homepage": "https://cap.cloud.sap/",
6
6
  "keywords": [