@sap/cds 5.8.1 → 5.8.4

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 (30) hide show
  1. package/CHANGELOG.md +53 -4
  2. package/app/fiori/routes.js +3 -0
  3. package/bin/cds.js +7 -3
  4. package/bin/serve.js +2 -2
  5. package/lib/deploy.js +1 -1
  6. package/lib/log/format/kibana.js +3 -3
  7. package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +1 -3
  8. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +13 -0
  9. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/ResourcePathParser.js +23 -2
  10. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/UriHelper.js +1 -1
  11. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/deserializer/ResourceJsonDeserializer.js +5 -6
  12. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/utils/UriHelper.js +4 -1
  13. package/libx/_runtime/common/composition/tree.js +1 -1
  14. package/libx/_runtime/common/generic/crud.js +6 -4
  15. package/libx/_runtime/common/i18n/index.js +2 -31
  16. package/libx/_runtime/common/utils/csn.js +14 -2
  17. package/libx/_runtime/common/utils/foreignKeyPropagations.js +9 -6
  18. package/libx/_runtime/common/utils/generateOnCond.js +5 -5
  19. package/libx/_runtime/db/expand/expandCQNToJoin.js +29 -20
  20. package/libx/_runtime/db/utils/deep.js +10 -6
  21. package/libx/_runtime/hana/customBuilder/CustomSelectBuilder.js +1 -1
  22. package/libx/_runtime/hana/dynatrace.js +11 -5
  23. package/libx/_runtime/hana/execute.js +105 -6
  24. package/libx/_runtime/remote/utils/client.js +9 -4
  25. package/libx/_runtime/remote/utils/data.js +2 -1
  26. package/libx/gql/resolvers/crud/create.js +6 -1
  27. package/libx/gql/resolvers/crud/delete.js +6 -1
  28. package/libx/gql/resolvers/crud/read.js +6 -1
  29. package/libx/gql/resolvers/crud/update.js +4 -1
  30. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,52 @@
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 5.8.4 - 2022-03-17
8
+
9
+ ### Fixed
10
+
11
+ - `UPDATE` singleton entity does not require to provide singleton keys in a payload
12
+ - CQN queries with operator expressions (`xpr`) in ON-conditions of unmanaged associations and compositions
13
+
14
+ ## Version 5.8.3 - 2022-03-01
15
+
16
+ ### Fixed
17
+
18
+ - `queries` property for application defined destinations of remote services
19
+ - `cds serve --watch` no longer fails if `@sap/cds-dk` is installed only globally
20
+ - `cds serve` during development longer redirects URLs with similar path segments from different services, like `/service/one` and `/service`
21
+ - `cds deploy --to sqlite` now ignores a `_texts.csv` file again if there is a language-specific file like `_texts_en.csv` present
22
+ - Using logical blocks (surrounded with `(` and `)`) in ON-conditions of unmanaged associations and compositions
23
+ - Skip "with parameters" clause if no order by clause or all columns in the order by clause are not strings when using parametrized views on hana
24
+ - Limited support for binary data in OData
25
+ + Using of `base64` string values in `WHERE IN` on hana
26
+ + `base64url` values in `@odata.context` annotation
27
+ - `cds.context` is set in GraphQL adapter
28
+ - Using payloads with `@odata.type` annotating primitive properties no longer crashes the application. `#` in type value may be ommitted. Example:
29
+ ```json
30
+ {
31
+ "ID": 201,
32
+ "title@odata.type": "#String",
33
+ "title": "Wuthering Heights",
34
+ "stock@odata.type": "Int32",
35
+ "stock": 12
36
+ }
37
+ ```
38
+ - Unicode support for i18n bundles
39
+
40
+ ## Version 5.8.2 - 2022-02-22
41
+
42
+ ### Fixed
43
+
44
+ - Crash if error does not have a stack in kibana logging
45
+ - Allow short names for bound operations in odata-server
46
+ - Performance issue during deep operations
47
+ - Resolving views with parameters
48
+ - Expanding association-to-many within draft union scenario
49
+ - Erroneous invalidation of deep `INSERT|UPDATE|DELETE` operations if root entity has managed to-one association to non-writable view
50
+ - Handling of falsy results when sending requests to remote services
51
+ - Resolving foreign key propagations for views with union
52
+
7
53
  ## Version 5.8.1 - 2022-02-11
8
54
 
9
55
  ### Fixed
@@ -23,7 +69,7 @@
23
69
 
24
70
  ### Added
25
71
 
26
- - Custom `server.js` don't have to export `cds.server` anymore -> we use that by default now.
72
+ - Custom `server.js` don't have to export `cds.server` anymore -> we use that by default now.
27
73
  - In `cds.requires`: Support to replace primitive values with objects
28
74
  - Support filter functions on renamed properties from external service
29
75
  - Results of database queries use `big.js` for values of type `cds.Decimal` and `cds.Integer64` if enabled via `cds.env.features.bigjs`
@@ -36,9 +82,6 @@
36
82
  - Restrict access to all services via `cds.env.requires.auth.restrict_all_services = true`
37
83
  + That is, all unrestricted services (i.e., w/o own `@requires`) are treated as having `@requires: 'authenticated-user'`
38
84
  - Threshold for automatically sending GET requests as `$batch` (beta, cf. @sap/cds@5.6.0) can be configured per remote service via `cds.env.requires.<srv>.max_get_url_length` (if not configured on service, the global config applies)
39
- - Alpha out-of-the-box support for DwC
40
- + Authentication based on headers set by Jupiter router via `cds.env.requires.auth.kind = 'dwc-auth'`
41
- + All DwC headers are forwarded to remote service via `cds.env.requires.<srv>.forward_dwc_headers = true`
42
85
  - Limited support for binary data in OData
43
86
  + In payloads, the binary data must be a base64 encoded string
44
87
  + In URLs, the binary data must have the following format: `binary'<url-safe base64 encoded>'`, e.g., `$filter=ID eq binary'Q0FQIE5vZGUuanM='`
@@ -91,6 +134,12 @@
91
134
  + Request data properties of types `cds.Date`, `cds.DateTime` and `cds.Timestamp` are converted accordingly to OData V2 specification
92
135
  + Response data properties of types `cds.Decimal`, `cds.DecimalFloat` (deprecated) and `cds.Integer64` are handled properly when using `Accept` header with `IEEE754Compatible=true/false` and `ExponentialDecimals=true/false` format parameters
93
136
 
137
+ ## Version 5.7.6 - 2022-02-23
138
+
139
+ ### Fixed
140
+
141
+ - `draftActivate` action does not return children if not requested
142
+
94
143
  ## Version 5.7.5 - 2022-01-14
95
144
 
96
145
  ### Fixed
@@ -15,6 +15,9 @@ cds.on ('bootstrap', app => {
15
15
  app.use('*/'+uri, ({originalUrl}, res, next)=> { // */browse/webapp[/prefix]/browse/
16
16
  // any of our special URLs ($fiori-, $api-docs) ? -> next
17
17
  if (originalUrl.startsWith('/$')) return next()
18
+ // is there a service starting with the URL? -> next
19
+ if (cds.service.providers.find (srv => originalUrl.startsWith(srv.path))) return next()
20
+
18
21
  // is there a service for '[prefix]/browse' ?
19
22
  const srv = serviceForUri[uri] || (serviceForUri[uri] =
20
23
  cds.service.providers.find (srv => ('/'+uri).lastIndexOf(srv.path) >=0))
package/bin/cds.js CHANGED
@@ -15,13 +15,17 @@ const cli = { //NOSONAR
15
15
  if (!argv.length) argv = process.argv.slice(3)
16
16
  if (cmd in this.Shortcuts) cmd = process.argv[2] = this.Shortcuts[cmd]
17
17
  if (process.env.NODE_ENV !== 'test') this.errorHandlers()
18
- const task = _require ('./'+cmd)
19
- || _require ('@sap/cds-dk/bin/'+cmd) // if dk is in installed modules
20
- || _require (_npmGlobalModules()+'/@sap/cds-dk/bin/'+cmd) // needed for running cds in npm scripts
18
+ const task = this.load(cmd)
21
19
  if (!task) return _requires_cdsdk (cmd)
22
20
  return task.apply (this, this.args(task,argv))
23
21
  },
24
22
 
23
+ load (cmd) {
24
+ return _require ('./'+cmd)
25
+ || _require ('@sap/cds-dk/bin/'+cmd) // if dk is in installed modules
26
+ || _require (_npmGlobalModules()+'/@sap/cds-dk/bin/'+cmd) // needed for running cds in npm scripts
27
+ },
28
+
25
29
  args (task, argv) {
26
30
 
27
31
  const { options:o=[], flags:f=[], shortcuts:s=[] } = task
package/bin/serve.js CHANGED
@@ -143,7 +143,7 @@ async function serve (all=[], o={}) { // NOSONAR
143
143
 
144
144
  // IMPORTANT: never load any @sap/cds modules before the chdir above happened!
145
145
  // handle --watch and --project
146
- if (o.watch) return _watch (o.project,o) // cds serve --watch <project>
146
+ if (o.watch) return _watch.call(this, o.project,o) // cds serve --watch <project>
147
147
  if (o.project) _chdir_to (o.project) // cds run --project <project>
148
148
  if (!o.silent) _prepare_logging ()
149
149
 
@@ -240,7 +240,7 @@ function _prepare_logging () { // NOSONAR
240
240
  /** handles --watch option */
241
241
  function _watch (project,o) {
242
242
  o.args = process.argv.slice(2) .filter (a => a !== '--watch' && a !== '-w')
243
- return require('@sap/cds-dk/bin/watch')([project],o)
243
+ return this.load('watch')([project],o)
244
244
  }
245
245
 
246
246
 
package/lib/deploy.js CHANGED
@@ -152,7 +152,7 @@ function init_from_json (db, csn, SILENT) {
152
152
  */
153
153
  function prefer_translated_texts (file, all) {
154
154
  if (/[._]texts\.(json|csv)$/.test (file)) {
155
- const pattern = new RegExp('^'+ path.basename(file) +'_')
155
+ const pattern = new RegExp('^'+ path.parse(file).name +'_')
156
156
  const translated = all.filter (f => pattern.test(f))
157
157
  if (translated.length > 0) {
158
158
  DEBUG && DEBUG (`ignoring '${file}' in favor of [${translated}]`) // eslint-disable-line
@@ -1,4 +1,4 @@
1
- const cds = require ('../../')
1
+ const cds = require('../../')
2
2
  const util = require('util')
3
3
 
4
4
  const _l2l = { 1: 'error', 2: 'warn', 3: 'info', 4: 'debug', 5: 'trace' }
@@ -8,7 +8,7 @@ const _l2l = { 1: 'error', 2: 'warn', 3: 'info', 4: 'debug', 5: 'trace' }
8
8
  */
9
9
  module.exports = (module, level, ...args) => {
10
10
  // config
11
- const { user: log_user , kibana_custom_fields } = cds.env.log
11
+ const { user: log_user, kibana_custom_fields } = cds.env.log
12
12
 
13
13
  // build the object to log
14
14
  const toLog = {
@@ -36,7 +36,7 @@ module.exports = (module, level, ...args) => {
36
36
  if (args.length && typeof args[0] === 'object' && args[0].message) {
37
37
  const err = args.shift()
38
38
  toLog.msg = err.message
39
- if (err instanceof Error) toLog.stacktrace = err.stack.split(/\s*\r?\n\s*/)
39
+ if (typeof err.stack === 'string') toLog.stacktrace = err.stack.split(/\s*\r?\n\s*/)
40
40
  Object.assign(toLog, err, { level: toLog.level })
41
41
  }
42
42
 
@@ -45,9 +45,7 @@ function _getTarget(service, segments) {
45
45
  : last.getEdmType().csdlStructuredType.name
46
46
 
47
47
  // autoexposed entities now used . in csn and _ in edm
48
- const target =
49
- findCsnTargetFor(name, service.model, namespace) ||
50
- (name.endsWith('Parameters') && service.model.definitions[namespace + '.' + name.replace(/Parameters$/, '')])
48
+ const target = findCsnTargetFor(name, service.model, namespace)
51
49
 
52
50
  if (target && target.kind === 'entity') {
53
51
  return target
@@ -14,6 +14,12 @@ const { setStatusCodeAndHeader, getKeyProperty } = require('../../../../fiori/ut
14
14
  const { toODataResult, postProcess } = require('../utils/result')
15
15
  const { mergeJson } = require('../../../services/utils/compareJson')
16
16
 
17
+ const _isAssocOrCompNotLocalized = (reqTarget, el) => {
18
+ return (
19
+ reqTarget.elements[el].isAssociation && (!reqTarget.texts || reqTarget.elements[el].target !== reqTarget.texts.name)
20
+ )
21
+ }
22
+
17
23
  const _postProcessDraftActivate = async (req, result, service) => {
18
24
  // update req.data (keys needed in readAfterWrite)
19
25
  req.data = result
@@ -25,6 +31,13 @@ const _postProcessDraftActivate = async (req, result, service) => {
25
31
  result.HasActiveEntity = false
26
32
  result.HasDraftEntity = false
27
33
 
34
+ // remove children from result, excluding localized composition 'text'
35
+ if (!cds.env.effective.odata.structs) {
36
+ for (const k in req.target.elements) {
37
+ if (_isAssocOrCompNotLocalized(req.target, k)) delete result[k]
38
+ }
39
+ }
40
+
28
41
  return result
29
42
  }
30
43
 
@@ -396,7 +396,18 @@ class ResourcePathParser {
396
396
  throw new UriSyntaxError(UriSyntaxError.Message.PREVIOUS_TYPE_HAS_NO_MEDIA, currentType.getName())
397
397
  }
398
398
 
399
- const uriResources = this._parsePropertyPath(uriPathSegments, currentResource, tokenizer)
399
+ let uriResources
400
+ try {
401
+ uriResources = this._parsePropertyPath(uriPathSegments, currentResource, tokenizer)
402
+ } catch (e) {
403
+ try {
404
+ uriResources = this._parseBoundOperation(uriPathSegments, currentResource, tokenizer)
405
+ } catch (e1) {
406
+ // throw first error
407
+ throw e
408
+ }
409
+ }
410
+
400
411
  return result.concat(uriResources)
401
412
  }
402
413
 
@@ -409,7 +420,17 @@ class ResourcePathParser {
409
420
  * @private
410
421
  */
411
422
  _parseBoundOperation (uriPathSegments, currentResource, tokenizer) {
412
- const fqn = FullQualifiedName.createFromNameSpaceAndName(tokenizer.getText())
423
+ // allow short names for bound operations
424
+ let name = tokenizer.getText()
425
+ if (typeof name === 'string' && !name.match(/\./)) {
426
+ const namespace = currentResource._entitySet &&
427
+ currentResource._entitySet._target &&
428
+ currentResource._entitySet._target.type &&
429
+ currentResource._entitySet._target.type.namespace
430
+ if (namespace) name = namespace + '.' + name
431
+ }
432
+
433
+ const fqn = FullQualifiedName.createFromNameSpaceAndName(name)
413
434
  const bindingParamTypeFqn = currentResource.getEdmType().getFullQualifiedName()
414
435
 
415
436
  // parse bound action
@@ -50,7 +50,7 @@ class UriHelper {
50
50
  if (value === null) return 'null'
51
51
  if (edmType === EdmPrimitiveTypeKind.String) return "'" + value.replace(REGEXP_SINGLE_QUOTE, "''") + "'"
52
52
  if (edmType === EdmPrimitiveTypeKind.Duration) return "duration'" + value + "'"
53
- if (edmType === EdmPrimitiveTypeKind.Binary) return "binary'" + value + "'"
53
+ if (edmType === EdmPrimitiveTypeKind.Binary) return "binary'" + value.replace(/\//g, '_').replace(/\+/g, '-') + "'"
54
54
  if (edmType.getKind() === EdmTypeKind.DEFINITION) {
55
55
  return UriHelper.toUriLiteral(value, edmType.getUnderlyingType())
56
56
  }
@@ -347,12 +347,9 @@ class ResourceJsonDeserializer {
347
347
  }
348
348
  const expectedTypeName =
349
349
  expectedType.getKind() === EdmTypeKind.PRIMITIVE ? expectedType.getName() : expectedType.getFullQualifiedName()
350
- const typeName =
351
- typeof value === 'string' &&
352
- value.startsWith(isCollection ? '#Collection(' : '#') &&
353
- value.endsWith(isCollection ? ')' : '')
354
- ? value.substring(isCollection ? 12 : 1, value.length - (isCollection ? 1 : 0))
355
- : null
350
+ const typeNameRegex = isCollection ? /^#?Collection\((.*)\)$/ : /^#?(.*)$/
351
+ const matchedTypeName = typeof value === 'string' && value.match(typeNameRegex)
352
+ const typeName = matchedTypeName && matchedTypeName[1]
356
353
  // The type name could be an alias-qualified name; for that case we have to do an EDM look-up.
357
354
  const fullQualifiedName =
358
355
  typeName && typeName.indexOf('.') > 0 && typeName.lastIndexOf('.') < typeName.length - 1
@@ -369,6 +366,8 @@ class ResourceJsonDeserializer {
369
366
  throw new DeserializationError(
370
367
  "The value of '" + name + "' must describe correctly the type '" + expectedType.getFullQualifiedName() + "'."
371
368
  )
369
+ } else {
370
+ delete structureValue[name]
372
371
  }
373
372
  } else if (name.endsWith(JsonAnnotations.BIND) && !isDelta) {
374
373
  const navigationPropertyName = name.substring(0, name.length - JsonAnnotations.BIND.length)
@@ -6,6 +6,7 @@ const AbstractError = commons.errors.AbstractError
6
6
  const UriSyntaxError = commons.errors.UriSyntaxError
7
7
  const IllegalArgumentError = commons.errors.IllegalArgumentError
8
8
  const NotImplementedError = commons.errors.NotImplementedError
9
+ const EdmPrimitiveTypeKind = require('../../odata-commons/edm/EdmPrimitiveTypeKind')
9
10
 
10
11
  /**
11
12
  * UriHelper has utility methods for reading and constructing URIs.
@@ -103,7 +104,9 @@ class UriHelper {
103
104
  for (const key of keys) {
104
105
  url = url ? url + ',' : ''
105
106
  if (keys.length > 1) url += encodeURIComponent(key.name) + '='
106
- url += encodeURIComponent(CommonsUriHelper.toUriLiteral(key.value, key.type))
107
+ url += key.type === EdmPrimitiveTypeKind.Binary
108
+ ? CommonsUriHelper.toUriLiteral(key.value, key.type)
109
+ : encodeURIComponent(CommonsUriHelper.toUriLiteral(key.value, key.type))
107
110
  }
108
111
  return '(' + url + ')'
109
112
  }
@@ -38,7 +38,7 @@ const _foreignKeysToLinks = (element, inverse) =>
38
38
  const _resolvedElement = (element, service) => {
39
39
  if (!element.target) return element
40
40
  // skip forbidden view check if association to view with foreign key in target
41
- const skipForbiddenViewCheck = element._isAssociationStrict && element.on && !element['@odata.contained']
41
+ const skipForbiddenViewCheck = element._isAssociationStrict && !element['@odata.contained']
42
42
  const { target, mapping } = getTransition(element._target, service, skipForbiddenViewCheck)
43
43
  const newElement = { target: target.name, _target: target }
44
44
  Object.setPrototypeOf(newElement, element)
@@ -7,6 +7,7 @@ const replaceManagedData = require('../utils/dollar')
7
7
  const { deepCopyArray } = require('../utils/copy')
8
8
 
9
9
  const onlyKeysRemain = require('../utils/onlyKeysRemain')
10
+ const { getColumns } = require('../../cds-services/services/utils/columns')
10
11
 
11
12
  const _targetEntityDoesNotExist = async req => {
12
13
  const { query } = req
@@ -98,10 +99,11 @@ module.exports = cds.service.impl(function () {
98
99
  result = req.data
99
100
  }
100
101
 
101
- if (req.event === 'DELETE' && req.target._isSingleton) {
102
- if (!req.target['@odata.singleton.nullable']) req.reject(400, 'SINGLETON_NOT_NULLABLE')
103
-
104
- const singleton = await cds.tx(req).run(SELECT.one(req.target))
102
+ if (req.event in { DELETE: 1, UPDATE: 1 } && req.target && req.target._isSingleton) {
103
+ if (req.event === 'DELETE' && !req.target['@odata.singleton.nullable']) req.reject(400, 'SINGLETON_NOT_NULLABLE')
104
+ const keyColumns = getColumns(req.target, { onlyNames: true, keysOnly: true })
105
+ const selectSingleton = SELECT.one(req.target).columns(keyColumns)
106
+ const singleton = await cds.tx(req).run(selectSingleton)
105
107
  if (!singleton) req.reject(404)
106
108
  req.query.where(singleton)
107
109
  }
@@ -2,7 +2,6 @@ const fs = require('fs')
2
2
  const path = require('path')
3
3
 
4
4
  const cds = require('../../cds')
5
- const LOG = cds.log('app')
6
5
 
7
6
  const dirs = (cds.env.i18n && cds.env.i18n.folders) || []
8
7
 
@@ -50,36 +49,8 @@ function init(locale, file) {
50
49
  if (!file) file = findFile(locale)
51
50
  if (!file) return
52
51
 
53
- let raw
54
- try {
55
- raw = fs.readFileSync(file, 'utf-8')
56
- } catch (e) {
57
- if (LOG._warn) {
58
- e.message = `Unable to load file "${file}" for locale "${locale}" due to error: ` + e.message
59
- LOG.warn(e)
60
- }
61
- return
62
- }
63
-
64
- try {
65
- const pairs = raw
66
- .replace(/\r/g, '')
67
- .split(/\n/)
68
- .map(ele => ele.trim())
69
- .filter(ele => ele && !ele.startsWith('#'))
70
- .map(ele => {
71
- const del = ele.indexOf('=')
72
- return [ele.slice(0, del), ele.slice(del + 1)].map(ele => ele.trim())
73
- })
74
- for (const [key, value] of pairs) {
75
- i18ns[locale][key] = value
76
- }
77
- } catch (e) {
78
- if (LOG._warn) {
79
- e.message = `Unable to process file "${file}" for locale "${locale}" due to error: ` + e.message
80
- LOG.warn(e)
81
- }
82
- }
52
+ const props = cds.load.properties(file)
53
+ i18ns[locale] = props
83
54
  }
84
55
 
85
56
  init('default', path.join(__dirname, 'messages.properties'))
@@ -79,8 +79,20 @@ const getDataSubject = (entity, model, role) => {
79
79
  return entity.set(hash, dataSubject)
80
80
  }
81
81
 
82
- const _resolve = (name, model, namespace) =>
83
- model.entities(namespace)[name] || model.definitions[`${namespace}.${name}`]
82
+ const _findInModel = (name, model, namespace) => {
83
+ return model.entities(namespace)[name] || model.definitions[`${namespace}.${name}`]
84
+ }
85
+
86
+ const _resolve = (name, model, namespace) => {
87
+ const resolved = _findInModel(name, model, namespace)
88
+ // the edm name has an additional suffix 'Parameters' in case of views with parameters
89
+ if (!resolved && name.endsWith('Parameters')) {
90
+ const viewWithParam = _findInModel(name.replace(/Parameters$/, ''), model, namespace)
91
+ if (!viewWithParam || !viewWithParam.params) return
92
+ return viewWithParam
93
+ }
94
+ return resolved
95
+ }
84
96
 
85
97
  const _findRootEntity = (model, edmName, namespace) => {
86
98
  const parts = edmName.split('_')
@@ -178,6 +178,12 @@ const _resolveTargetForeignKey = targetKey => {
178
178
  return { targetName, propagation }
179
179
  }
180
180
 
181
+ const _resolveColumnsFromQuery = query => {
182
+ if (query && query.SET) return _resolveColumnsFromQuery(query.SET.args[0])
183
+ if (query && query.SELECT && query.SELECT.columns) return query.SELECT.columns
184
+ return []
185
+ }
186
+
181
187
  const _resolvedKeys = (foreignKeys, targetKeys, fillChild) => {
182
188
  const foreignKeyPropagations = []
183
189
 
@@ -191,12 +197,9 @@ const _resolvedKeys = (foreignKeys, targetKeys, fillChild) => {
191
197
  * Once you have the full path, you can find it in the target entity.
192
198
  * NOTE: There can be projections upon projections and renamings in every projection. -> not yet covered!!!
193
199
  */
194
- const tkCol =
195
- targetKeys[i].parent.query &&
196
- targetKeys[i].parent.query.SELECT.columns &&
197
- targetKeys[i].parent.query.SELECT.columns.find(
198
- c => c.ref && `${fk['@odata.foreignKey4']}_${c.ref.join('_')}` === fk.name
199
- )
200
+ const tkCol = _resolveColumnsFromQuery(targetKeys[i].parent.query).find(
201
+ c => c.ref && `${fk['@odata.foreignKey4']}_${c.ref.join('_')}` === fk.name
202
+ )
200
203
  tk = tkCol && targetKeys.find(tk => tk.name === (tkCol.as ? tkCol.as : tkCol.ref.join('_')))
201
204
  // with composition of aspects, the lookup fails -> we need this final fallback
202
205
  if (!tk) tk = targetKeys[i]
@@ -4,9 +4,8 @@ const _toRef = (alias, column) => {
4
4
  }
5
5
 
6
6
  const _adaptRefs = (onCond, path, { select, join }) => {
7
- const adaptedOnCondition = onCond.map(el => {
7
+ const _adaptEl = el => {
8
8
  const ref = el.ref
9
-
10
9
  if (ref) {
11
10
  if (ref[0] === path.join('_') && ref[1]) {
12
11
  return _toRef(select, ref.slice(1))
@@ -18,12 +17,13 @@ const _adaptRefs = (onCond, path, { select, join }) => {
18
17
  }
19
18
 
20
19
  return _toRef(join, ref.slice(0))
20
+ } else if (el.xpr) {
21
+ return { xpr: el.xpr.map(_adaptEl) }
21
22
  }
22
23
 
23
24
  return el
24
- })
25
-
26
- return adaptedOnCondition
25
+ }
26
+ return onCond.map(_adaptEl)
27
27
  }
28
28
 
29
29
  const _args = (csnElement, path, aliases) => {
@@ -1151,28 +1151,37 @@ class JoinCQNFromExpanded {
1151
1151
  }
1152
1152
  }
1153
1153
  }
1154
- const ks = Object.keys(expandedEntity.keys).filter(
1155
- c => !expandedEntity.keys[c].isAssociation && !DRAFT_COLUMNS.includes(c)
1156
- )
1157
- const user = (cds.context && cds.context.user && cds.context.user.id) || 'anonymous'
1158
- const unionFrom = getCQNUnionFrom(cols, ref.replace(/_drafts$/, ''), ref, ks, user)
1159
- for (const each of cqn.columns) {
1160
- if (!each.as) continue
1161
- // replace val with ref
1162
- if (each.as === 'IsActiveEntity' || each.as === 'HasActiveEntity') {
1163
- delete each.val
1164
- each.ref = [tableAlias, each.as]
1165
- each.as = tableAlias + '_' + each.as
1166
- }
1167
- // ensure the cast
1168
- if (each.as.match(/IsActiveEntity$/) || each.as.match(/HasActiveEntity$/) || each.as.match(/HasDraftEntity$/)) {
1169
- each.cast = { type: 'cds.Boolean' }
1154
+
1155
+ if (!cqn[IS_ACTIVE]) {
1156
+ const ks = Object.keys(expandedEntity.keys).filter(
1157
+ c => !expandedEntity.keys[c].isAssociation && !DRAFT_COLUMNS.includes(c)
1158
+ )
1159
+ const user = (cds.context && cds.context.user && cds.context.user.id) || 'anonymous'
1160
+ const unionFrom = getCQNUnionFrom(cols, ref.replace(/_drafts$/, ''), ref, ks, user)
1161
+ for (const each of cqn.columns) {
1162
+ if (!each.as) continue
1163
+ // replace val with ref
1164
+ if (each.as === 'IsActiveEntity' || each.as === 'HasActiveEntity') {
1165
+ delete each.val
1166
+ each.ref = [tableAlias, each.as]
1167
+ each.as = tableAlias + '_' + each.as
1168
+ }
1169
+ // ensure the cast
1170
+ if (
1171
+ each.as.match(/IsActiveEntity$/) ||
1172
+ each.as.match(/HasActiveEntity$/) ||
1173
+ each.as.match(/HasDraftEntity$/)
1174
+ ) {
1175
+ each.cast = { type: 'cds.Boolean' }
1176
+ }
1170
1177
  }
1178
+ const cs = cqn.columns
1179
+ .filter(c => !c.expand && c.ref && c.ref[0] === tableAlias)
1180
+ .map(c => ({ ref: [c.ref[1]] }))
1181
+ const unionArgs = cqn.from.args
1182
+ unionArgs[0].SELECT = { columns: cs, from: unionFrom, distinct: true }
1183
+ delete unionArgs[0].ref
1171
1184
  }
1172
- const cs = cqn.columns.filter(c => !c.expand && c.ref && c.ref[0] === tableAlias).map(c => ({ ref: [c.ref[1]] }))
1173
- const unionArgs = cqn.from.args
1174
- unionArgs[0].SELECT = { columns: cs, from: unionFrom, distinct: true }
1175
- delete unionArgs[0].ref
1176
1185
  }
1177
1186
 
1178
1187
  return cqn
@@ -1,14 +1,18 @@
1
- function _flattenDeep(arr) {
2
- return arr.reduce((acc, val) => acc.concat(Array.isArray(val) ? _flattenDeep(val) : val), [])
1
+ const _flattenDeep = (arr, res) => {
2
+ if (!Array.isArray(arr)) {
3
+ res.push(arr)
4
+ return res
5
+ }
6
+ for (const a of arr) {
7
+ _flattenDeep(a, res)
8
+ }
9
+ return res
3
10
  }
4
11
 
5
12
  /*
6
13
  * flatten with a dfs approach. this is important!!!
7
14
  */
8
- function getFlatArray(arg) {
9
- if (!Array.isArray(arg)) return [arg]
10
- return _flattenDeep(arg)
11
- }
15
+ const getFlatArray = arg => _flattenDeep(arg, [])
12
16
 
13
17
  async function _processChunk(processFn, model, dbc, cqns, user, locale, ts, indexes, results) {
14
18
  const promises = []
@@ -48,7 +48,7 @@ class CustomSelectBuilder extends SelectBuilder {
48
48
  select.from.ref &&
49
49
  select.from.ref.length === 1 &&
50
50
  // REVISIT this does not work with join and draft!
51
- this._csn.definitions[select.from.ref[0]]
51
+ this._csn.definitions[select.from.ref[0].id || select.from.ref[0]]
52
52
  // TODO FIXME
53
53
  skip =
54
54
  !select.orderBy ||
@@ -7,11 +7,12 @@ try {
7
7
  }
8
8
 
9
9
  const isDynatraceEnabled = () => {
10
- return dynatrace.sdk !== undefined
10
+ return dynatrace.sdk !== undefined && !process.env.CDS_SKIP_DYNATRACE
11
11
  }
12
12
 
13
13
  const _dynatraceResultCallback = function (tracer, cb) {
14
- return function (err, results, fields) {
14
+ return function (err, ...args) {
15
+ const results = args.shift()
15
16
  if (err) {
16
17
  tracer.error(err)
17
18
  } else {
@@ -19,7 +20,7 @@ const _dynatraceResultCallback = function (tracer, cb) {
19
20
  rowsReturned: (results && results.length) || results
20
21
  })
21
22
  }
22
- tracer.end(cb, err, results, fields)
23
+ tracer.end(cb, err, results, ...args)
23
24
  }
24
25
  }
25
26
 
@@ -73,9 +74,14 @@ const dynatraceClient = (client, credentials, tenant) => {
73
74
  // hana-client does not like decorating.
74
75
  // because of that, we need to override the fn and pass the original fn for execution
75
76
  const originalExecFn = client.exec
76
- const originalPrepareFn = client.prepare
77
77
  client.exec = _execUsingDynatrace(client, originalExecFn, dbInfo)
78
- client.prepare = _preparedStmtUsingDynatrace(client, originalPrepareFn, dbInfo)
78
+ const originalPrepareFn = client.prepare
79
+ if (client.name === '@sap/hana-client') {
80
+ // client.prepare = ... doesn't work for hana-client
81
+ Object.defineProperty(client, 'prepare', { value: _preparedStmtUsingDynatrace(client, originalPrepareFn, dbInfo) })
82
+ } else {
83
+ client.prepare = _preparedStmtUsingDynatrace(client, originalPrepareFn, dbInfo)
84
+ }
79
85
 
80
86
  return client
81
87
  }
@@ -51,7 +51,8 @@ function _getOutputParameters(stmt) {
51
51
  const BINARY_TYPES = {
52
52
  12: 'BINARY',
53
53
  13: 'VARBINARY',
54
- 27: 'BLOB'
54
+ 27: 'BLOB',
55
+ 33: 'BSTRING'
55
56
  }
56
57
 
57
58
  function _getBinaries(stmt) {
@@ -66,15 +67,73 @@ function _getBinaries(stmt) {
66
67
 
67
68
  const BASE64 = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{4}|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}={2})$/
68
69
 
70
+ function _isProcedureCall(sql) {
71
+ return sql.trim().match(/^call \s*"{0,1}\w*/i)
72
+ }
73
+
74
+ function _getProcedureName(sql) {
75
+ const match = sql.trim().match(/^call \s*"{0,1}(\w*)/i)
76
+ return match && match[1]
77
+ }
78
+
79
+ function _hdbGetResultForProcedure(rows, args, outParameters) {
80
+ // on hdb, rows already contains results for scalar params
81
+ const result = rows || {}
82
+ // merge table output params into scalar params
83
+ if (args && args.length && outParameters) {
84
+ const params = outParameters.filter(md => !(md.PARAMETER_NAME in rows))
85
+ for (let i = 0; i < args.length; i++) {
86
+ result[params[i].PARAMETER_NAME] = args[i]
87
+ }
88
+ }
89
+ return result
90
+ }
91
+
92
+ function _hcGetResultForProcedure(stmt, resultSet, outParameters) {
93
+ const result = {}
94
+ // build result from scalar params
95
+ const paramInfo = stmt.getParameterInfo()
96
+ if (paramInfo.some(p => p.direction > 1)) {
97
+ for (let i = 0; i < paramInfo.length; i++) {
98
+ if (paramInfo[i].direction > 1) {
99
+ result[paramInfo[i].name] = stmt.getParameterValue(i)
100
+ }
101
+ }
102
+ }
103
+ // merge table output params into scalar params
104
+ if (outParameters && outParameters.length) {
105
+ const params = outParameters.filter(md => !(md.PARAMETER_NAME in result))
106
+ let i = 0
107
+ while (resultSet.next()) {
108
+ result[params[i].PARAMETER_NAME] = [resultSet.getValues()]
109
+ resultSet.nextResult()
110
+ i++
111
+ }
112
+ }
113
+ return result
114
+ }
115
+
116
+ function _getProcedureMetadata(procedureName, dbc) {
117
+ return new Promise((resolve, reject) => {
118
+ dbc.exec(
119
+ `SELECT PARAMETER_NAME FROM SYS.PROCEDURE_PARAMETERS WHERE SCHEMA_NAME = CURRENT_SCHEMA AND PROCEDURE_NAME = '${procedureName}' AND PARAMETER_TYPE IN ('OUT', 'INOUT') ORDER BY POSITION`,
120
+ (err, res) => {
121
+ if (err) reject(err)
122
+ else resolve(res)
123
+ }
124
+ )
125
+ })
126
+ }
127
+
69
128
  function _executeAsPreparedStatement(dbc, sql, values, reject, resolve) {
70
- dbc.prepare(sql, function (err, stmt) {
129
+ dbc.prepare(sql, async function (err, stmt) {
71
130
  if (err) {
72
131
  err.query = sql
73
132
  if (values) err.values = SANITIZE_VALUES ? ['***'] : values
74
133
  return reject(err)
75
134
  }
76
135
 
77
- // convert binary strings to buffers ()
136
+ // convert binary strings to buffers
78
137
  if (cds.env.hana.base64_to_buffer !== false && _hasValues(values)) {
79
138
  const binaries = _getBinaries(stmt)
80
139
  if (binaries.length) {
@@ -89,6 +148,46 @@ function _executeAsPreparedStatement(dbc, sql, values, reject, resolve) {
89
148
  }
90
149
  }
91
150
 
151
+ if (cds.env.features.new_call_prodecure) {
152
+ // procedure call metadata
153
+ let outParameters
154
+ const isProcedureCall = _isProcedureCall(sql)
155
+ if (isProcedureCall) {
156
+ try {
157
+ const procedureName = _getProcedureName(sql)
158
+ outParameters = await _getProcedureMetadata(procedureName, dbc)
159
+ } catch (e) {
160
+ LOG._warn && LOG.warn('Unable to fetch procedure metadata due to error:', e)
161
+ }
162
+ }
163
+
164
+ // on @sap/hana-client, we need to use execQuery in case of calling procedures
165
+ stmt[isProcedureCall && dbc.name !== 'hdb' ? 'execQuery' : 'exec'](values, function (err, rows, ...args) {
166
+ if (err) {
167
+ stmt.drop(() => {})
168
+ err.query = sql
169
+ if (values) err.values = SANITIZE_VALUES ? ['***'] : values
170
+ return reject(err)
171
+ }
172
+
173
+ let result
174
+ if (isProcedureCall) {
175
+ result =
176
+ dbc.name === 'hdb'
177
+ ? _hdbGetResultForProcedure(rows, args, outParameters)
178
+ : _hcGetResultForProcedure(stmt, rows, outParameters)
179
+ } else {
180
+ result = rows
181
+ }
182
+
183
+ stmt.drop(() => {})
184
+
185
+ resolve(result)
186
+ })
187
+
188
+ return
189
+ }
190
+
92
191
  stmt.exec(values, function (err, rows, procedureReturn) {
93
192
  if (err) {
94
193
  stmt.drop(() => {})
@@ -123,15 +222,15 @@ function _executeSimpleSQL(dbc, sql, values) {
123
222
  values = Object.values(values)
124
223
  }
125
224
  // ensure that stored procedure with parameters is always executed as prepared
126
- if (_hasValues(values) || sql.match(/^call.*?\?.*$/i)) {
225
+ if (_hasValues(values) || _isProcedureCall(sql)) {
127
226
  _executeAsPreparedStatement(dbc, sql, values, reject, resolve)
128
227
  } else {
129
- dbc.exec(sql, function (err, result, procedureReturn) {
228
+ dbc.exec(sql, function (err, result) {
130
229
  if (err) {
131
230
  err.query = sql
132
231
  return reject(err)
133
232
  }
134
- resolve(procedureReturn || result)
233
+ resolve(result)
135
234
  })
136
235
  }
137
236
  })
@@ -67,6 +67,11 @@ const getDestination = (name, credentials) => {
67
67
  throw new Error(`"url" or "destination" property must be configured in "credentials" of "${name}".`)
68
68
  }
69
69
 
70
+ // Cloud SDK wants property "queryParameters" but we have documented "queries"
71
+ if (credentials.queries && !credentials.queryParameters) {
72
+ credentials.queryParameters = credentials.queries
73
+ }
74
+
70
75
  return { name, ...credentials }
71
76
  }
72
77
 
@@ -142,8 +147,8 @@ function _defineProperty(obj, property, value) {
142
147
  }
143
148
 
144
149
  function _normalizeMetadata(prefix, data, results) {
145
- const target = results || data
146
- if (typeof target !== 'object') return target
150
+ const target = results !== undefined ? results : data
151
+ if (typeof target !== 'object' || target === null) return target
147
152
  const metadataKeys = Object.keys(data).filter(k => prefix.test(k))
148
153
  for (const k of metadataKeys) {
149
154
  const $ = k.replace(prefix, '$')
@@ -169,7 +174,7 @@ const _purgeODataV2 = (data, target, reqHeaders) => {
169
174
  data = data.d
170
175
  const ieee754Compatible = reqHeaders.accept && reqHeaders.accept.includes('IEEE754Compatible=true')
171
176
  const exponentialDecimals = ieee754Compatible && reqHeaders.accept.includes('ExponentialDecimals=true')
172
- const purgedResponse = data.results || data
177
+ const purgedResponse = 'results' in data ? data.results : data
173
178
  const convertedResponse = convertV2ResponseData(purgedResponse, target, ieee754Compatible, exponentialDecimals)
174
179
  return _normalizeMetadata(/^__/, data, convertedResponse)
175
180
  }
@@ -177,7 +182,7 @@ const _purgeODataV2 = (data, target, reqHeaders) => {
177
182
  const _purgeODataV4 = data => {
178
183
  if (typeof data !== 'object') return data
179
184
 
180
- const purgedResponse = data.value || data
185
+ const purgedResponse = 'value' in data ? data.value : data
181
186
  return _normalizeMetadata(/^@odata\./, data, purgedResponse)
182
187
  }
183
188
 
@@ -39,7 +39,8 @@ const _getConvertRecordFn = (target, convertValueFn) => record => {
39
39
  if (!element) continue
40
40
 
41
41
  const recordValue = record[key]
42
- const value = (recordValue && recordValue.results) || recordValue
42
+ const value =
43
+ (recordValue && typeof recordValue === 'object' && 'results' in recordValue && recordValue.results) || recordValue
43
44
 
44
45
  if (value && (element.isAssociation || Array.isArray(value))) {
45
46
  record[key] = _convertData(value, element._target, convertValueFn)
@@ -1,3 +1,5 @@
1
+ const cds = require('../../../../lib')
2
+
1
3
  const { ARGUMENT } = require('../../constants/adapter')
2
4
  const { getArgumentByName, astToEntries } = require('../parse/ast2cqn')
3
5
  const { entriesStructureToEntityStructure } = require('./utils')
@@ -9,7 +11,10 @@ module.exports = async (service, entityFQN, selection) => {
9
11
  const entries = entriesStructureToEntityStructure(service, entityFQN, astToEntries(input))
10
12
  query.entries(entries)
11
13
 
12
- const result = await service.tx(tx => tx.run(query))
14
+ const result = await service.tx(tx => {
15
+ cds.context = tx
16
+ return tx.run(query)
17
+ })
13
18
 
14
19
  return Array.isArray(result) ? result : [result]
15
20
  }
@@ -1,3 +1,5 @@
1
+ const cds = require('../../../../lib')
2
+
1
3
  const { ARGUMENT } = require('../../constants/adapter')
2
4
  const { getArgumentByName, astToWhere } = require('../parse/ast2cqn')
3
5
 
@@ -11,7 +13,10 @@ module.exports = async (service, entityFQN, selection) => {
11
13
 
12
14
  let result
13
15
  try {
14
- result = await service.tx(tx => tx.run(query))
16
+ result = await service.tx(tx => {
17
+ cds.context = tx
18
+ return tx.run(query)
19
+ })
15
20
  } catch (e) {
16
21
  if (e.code === 404) {
17
22
  result = 0
@@ -1,3 +1,5 @@
1
+ const cds = require('../../../../lib')
2
+
1
3
  const { ARGUMENT } = require('../../constants/adapter')
2
4
  const { getArgumentByName, astToColumns, astToWhere, astToOrderBy, astToLimit } = require('../parse/ast2cqn')
3
5
 
@@ -21,5 +23,8 @@ module.exports = async (service, entityFQN, selection) => {
21
23
  query.limit(astToLimit(top, skip))
22
24
  }
23
25
 
24
- return await service.tx(tx => tx.run(query))
26
+ return await service.tx(tx => {
27
+ cds.context = tx
28
+ return tx.run(query)
29
+ })
25
30
  }
@@ -1,3 +1,5 @@
1
+ const cds = require('../../../../lib')
2
+
1
3
  const { ARGUMENT } = require('../../constants/adapter')
2
4
  const { getArgumentByName, astToColumns, astToWhere, astToEntries } = require('../parse/ast2cqn')
3
5
  const { entriesStructureToEntityStructure } = require('./utils')
@@ -24,8 +26,9 @@ module.exports = async (service, entityFQN, selection) => {
24
26
 
25
27
  let resultBeforeUpdate
26
28
  const result = await service.tx(async tx => {
29
+ cds.context = tx
27
30
  // read needs to be done before the update, otherwise the where clause might become invalid (case that properties in where clause are updated by the mutation)
28
- resultBeforeUpdate = await service.tx(tx => tx.run(queryBeforeUpdate))
31
+ resultBeforeUpdate = await tx.run(queryBeforeUpdate)
29
32
  return tx.run(query)
30
33
  })
31
34
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sap/cds",
3
- "version": "5.8.1",
3
+ "version": "5.8.4",
4
4
  "description": "SAP Cloud Application Programming Model - CDS for Node.js",
5
5
  "homepage": "https://cap.cloud.sap/",
6
6
  "keywords": [