@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.
- package/CHANGELOG.md +53 -4
- package/app/fiori/routes.js +3 -0
- package/bin/cds.js +7 -3
- package/bin/serve.js +2 -2
- package/lib/deploy.js +1 -1
- package/lib/log/format/kibana.js +3 -3
- package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +1 -3
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +13 -0
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/ResourcePathParser.js +23 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/UriHelper.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/deserializer/ResourceJsonDeserializer.js +5 -6
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/utils/UriHelper.js +4 -1
- package/libx/_runtime/common/composition/tree.js +1 -1
- package/libx/_runtime/common/generic/crud.js +6 -4
- package/libx/_runtime/common/i18n/index.js +2 -31
- package/libx/_runtime/common/utils/csn.js +14 -2
- package/libx/_runtime/common/utils/foreignKeyPropagations.js +9 -6
- package/libx/_runtime/common/utils/generateOnCond.js +5 -5
- package/libx/_runtime/db/expand/expandCQNToJoin.js +29 -20
- package/libx/_runtime/db/utils/deep.js +10 -6
- package/libx/_runtime/hana/customBuilder/CustomSelectBuilder.js +1 -1
- package/libx/_runtime/hana/dynatrace.js +11 -5
- package/libx/_runtime/hana/execute.js +105 -6
- package/libx/_runtime/remote/utils/client.js +9 -4
- package/libx/_runtime/remote/utils/data.js +2 -1
- package/libx/gql/resolvers/crud/create.js +6 -1
- package/libx/gql/resolvers/crud/delete.js +6 -1
- package/libx/gql/resolvers/crud/read.js +6 -1
- package/libx/gql/resolvers/crud/update.js +4 -1
- 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
|
package/app/fiori/routes.js
CHANGED
|
@@ -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 =
|
|
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
|
|
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
|
|
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.
|
|
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
|
package/lib/log/format/kibana.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/ResourcePathParser.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
351
|
-
|
|
352
|
-
|
|
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 +=
|
|
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 &&
|
|
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
|
|
102
|
-
if (!req.target['@odata.singleton.nullable']) req.reject(400, 'SINGLETON_NOT_NULLABLE')
|
|
103
|
-
|
|
104
|
-
const
|
|
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
|
-
|
|
54
|
-
|
|
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
|
|
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
|
-
|
|
196
|
-
|
|
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
|
|
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
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
each.
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
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
|
-
|
|
2
|
-
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
|
225
|
+
if (_hasValues(values) || _isProcedureCall(sql)) {
|
|
127
226
|
_executeAsPreparedStatement(dbc, sql, values, reject, resolve)
|
|
128
227
|
} else {
|
|
129
|
-
dbc.exec(sql, function (err, result
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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 =>
|
|
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 =>
|
|
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 =>
|
|
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
|
|
31
|
+
resultBeforeUpdate = await tx.run(queryBeforeUpdate)
|
|
29
32
|
return tx.run(query)
|
|
30
33
|
})
|
|
31
34
|
|