@sap/cds 6.1.2 → 6.1.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 +15 -0
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +0 -2
- package/libx/_runtime/cds-services/services/utils/compareJson.js +3 -1
- package/libx/_runtime/cds-services/util/assert.js +3 -0
- package/libx/_runtime/common/generic/input.js +17 -2
- package/libx/_runtime/common/generic/put.js +4 -1
- package/libx/_runtime/common/generic/temporal.js +0 -3
- package/libx/_runtime/common/utils/propagateForeignKeys.js +122 -0
- package/libx/_runtime/common/utils/template.js +2 -3
- package/libx/_runtime/db/generic/input.js +3 -3
- package/libx/_runtime/fiori/generic/new.js +1 -3
- package/libx/_runtime/fiori/generic/patch.js +1 -7
- package/libx/_runtime/remote/utils/client.js +29 -10
- package/libx/rest/middleware/input.js +2 -3
- package/package.json +1 -1
- package/srv/extensibility-service.cds +4 -3
- package/srv/mtx.js +18 -9
- package/libx/_runtime/db/utils/propagateForeignKeys.js +0 -93
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,21 @@
|
|
|
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.1.3 - 2022-09-13
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Configuration to change maximum body size in bytes for remote requests: `cds.env.remote.max_body_length: 1000` sets it to 1 MB
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- For structured input, foreign keys are generated as non-enumerable properties on application-service layer
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- Deep insert/update/upsert requests where the key of an association is provided in a structured format will not be rejected anymore if the target has default or mandatory fields
|
|
20
|
+
- For some configurations, mtxs services were bootstrapped twice
|
|
21
|
+
|
|
7
22
|
## Version 6.1.2 - 2022-09-05
|
|
8
23
|
|
|
9
24
|
### Fixed
|
|
@@ -4,8 +4,6 @@ const {
|
|
|
4
4
|
Request,
|
|
5
5
|
ql: { SELECT }
|
|
6
6
|
} = cds
|
|
7
|
-
const LOG = cds.log()
|
|
8
|
-
const { normalizeError } = require('../../../../common/error/frontend')
|
|
9
7
|
|
|
10
8
|
const { getDeepSelect, getSimpleSelectCQN } = require('./handlerUtils')
|
|
11
9
|
const { hasDeepUpdate } = require('../../../../common/composition/update')
|
|
@@ -155,7 +155,9 @@ const _skipToMany = (entity, prop) => {
|
|
|
155
155
|
}
|
|
156
156
|
|
|
157
157
|
const _iteratePropsInNewEntry = (newEntry, keys, result, oldEntry, entity) => {
|
|
158
|
-
|
|
158
|
+
// On app-service layer, generated foreign keys are not enumerable,
|
|
159
|
+
// include them here too.
|
|
160
|
+
for (const prop of Object.getOwnPropertyNames(newEntry)) {
|
|
159
161
|
if (keys.includes(prop)) {
|
|
160
162
|
_addKeysToResult(result, prop, newEntry, oldEntry)
|
|
161
163
|
continue
|
|
@@ -277,6 +277,9 @@ const checkIfAssocDeep = (element, value, req) => {
|
|
|
277
277
|
// managed to one
|
|
278
278
|
Object.keys(value).forEach(prop => {
|
|
279
279
|
if (typeof value[prop] !== 'object') {
|
|
280
|
+
const foreignKey = element._foreignKeys.find(fk => fk.childElement.name === prop)
|
|
281
|
+
if (foreignKey) return
|
|
282
|
+
|
|
280
283
|
const key = element.keys.find(element => element.ref[0] === prop)
|
|
281
284
|
if (key) return
|
|
282
285
|
|
|
@@ -11,6 +11,7 @@ const cds = require('../../cds')
|
|
|
11
11
|
const LOG = cds.log('app')
|
|
12
12
|
const { enrichDataWithKeysFromWhere } = require('../utils/keys')
|
|
13
13
|
const { DRAFT_COLUMNS_MAP } = require('../../common/constants/draft')
|
|
14
|
+
const propagateForeignKeys = require('../utils/propagateForeignKeys')
|
|
14
15
|
const { checkInputConstraints, assertTargets } = require('../../cds-services/util/assert')
|
|
15
16
|
const getTemplate = require('../utils/template')
|
|
16
17
|
const templateProcessor = require('../utils/templateProcessor')
|
|
@@ -126,6 +127,11 @@ const _processCategory = (req, category, value, elementInfo, assertMap) => {
|
|
|
126
127
|
const { row, key, element, isRoot } = elementInfo
|
|
127
128
|
category = _getSimpleCategory(category)
|
|
128
129
|
|
|
130
|
+
if (category === 'propagateForeignKeys') {
|
|
131
|
+
propagateForeignKeys(key, row, element._foreignKeys, element.isComposition, { enumerable: false })
|
|
132
|
+
return
|
|
133
|
+
}
|
|
134
|
+
|
|
129
135
|
// remember mandatory
|
|
130
136
|
if (category === 'mandatory') {
|
|
131
137
|
value.mandatory = true
|
|
@@ -186,6 +192,11 @@ const _pick = element => {
|
|
|
186
192
|
// collect actions to apply
|
|
187
193
|
const categories = []
|
|
188
194
|
|
|
195
|
+
// REVISIT: element._foreignKeys.length seems to be a very broad check
|
|
196
|
+
if (element.isAssociation && element._foreignKeys.length) {
|
|
197
|
+
categories.push({ category: 'propagateForeignKeys' })
|
|
198
|
+
}
|
|
199
|
+
|
|
189
200
|
if (element['@assert.range'] || element['@assert.enum'] || element['@assert.format']) {
|
|
190
201
|
categories.push('assert')
|
|
191
202
|
}
|
|
@@ -241,7 +252,10 @@ async function _handler(req) {
|
|
|
241
252
|
if (!req.query) return // FIXME: the code below expects req.query to be defined
|
|
242
253
|
if (!req.target) return
|
|
243
254
|
|
|
244
|
-
const template = getTemplate('app-input', this, req.target, {
|
|
255
|
+
const template = getTemplate('app-input', this, req.target, {
|
|
256
|
+
pick: _pick,
|
|
257
|
+
ignore: element => element._isAssociationStrict
|
|
258
|
+
})
|
|
245
259
|
if (template.elements.size === 0) return
|
|
246
260
|
|
|
247
261
|
const errors = []
|
|
@@ -301,7 +315,8 @@ const _processActionFunctionRow = (row, param, key, errors, event, service) => {
|
|
|
301
315
|
|
|
302
316
|
// structured
|
|
303
317
|
const template = getTemplate('app-input-operation', service, param, {
|
|
304
|
-
pick: _pick
|
|
318
|
+
pick: _pick,
|
|
319
|
+
ignore: element => element._isAssociationStrict
|
|
305
320
|
})
|
|
306
321
|
|
|
307
322
|
if (template && template.elements.size) {
|
|
@@ -64,7 +64,10 @@ function _handler(req) {
|
|
|
64
64
|
const { elements } = req.target
|
|
65
65
|
for (const k in req.data) if (k in elements && elements[k]['@Core.MediaType']) return
|
|
66
66
|
|
|
67
|
-
const template = getTemplate('app-put', this, req.target, {
|
|
67
|
+
const template = getTemplate('app-put', this, req.target, {
|
|
68
|
+
pick: _pick,
|
|
69
|
+
ignore: element => element._isAssociationStrict
|
|
70
|
+
})
|
|
68
71
|
if (template.elements.size === 0) return
|
|
69
72
|
|
|
70
73
|
// REVISIT: req.data should point into req.query
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
const cds = require('../../cds')
|
|
2
|
-
const LOG = cds.log('app')
|
|
3
2
|
|
|
4
3
|
const _getDateFromQueryOptions = str => {
|
|
5
4
|
if (str) {
|
|
@@ -11,8 +10,6 @@ const _getDateFromQueryOptions = str => {
|
|
|
11
10
|
|
|
12
11
|
const _isDate = dateStr => !dateStr.includes(':')
|
|
13
12
|
const _isTimestamp = dateStr => dateStr.includes('.')
|
|
14
|
-
const _isWarningRequired = (warning, queryOptions) =>
|
|
15
|
-
!warning && queryOptions && (queryOptions['sap-valid-from'] || queryOptions['sap-valid-to'])
|
|
16
13
|
const _isAsOfNow = queryOptions =>
|
|
17
14
|
!queryOptions || (!queryOptions['sap-valid-at'] && !queryOptions['sap-valid-to'] && !queryOptions['sap-valid-from'])
|
|
18
15
|
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
const cds = require('../../cds')
|
|
2
|
+
|
|
3
|
+
const { prefixForStruct } = require('../../common/utils/csn')
|
|
4
|
+
|
|
5
|
+
const _autoGenerate = e => e && e.isUUID && e.key
|
|
6
|
+
|
|
7
|
+
const _set = (row, value, element, enumerable) => {
|
|
8
|
+
if (!element.parent.elements[element.name]) return // only when in model
|
|
9
|
+
if (!enumerable && element.foreignKeySource) {
|
|
10
|
+
// only for foreign keys
|
|
11
|
+
Object.defineProperty(row, element.name, {
|
|
12
|
+
get() {
|
|
13
|
+
return value
|
|
14
|
+
},
|
|
15
|
+
set(v) {
|
|
16
|
+
// Make sure that it becomes enumerable again if set manually afterwards
|
|
17
|
+
Object.defineProperty(row, element.name, { value: v, configurable: true, enumerable: true })
|
|
18
|
+
},
|
|
19
|
+
enumerable: false,
|
|
20
|
+
configurable: true
|
|
21
|
+
})
|
|
22
|
+
} else {
|
|
23
|
+
row[element.name] = value
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const _generateParentField = ({ parentElement }, row, enumerable) => {
|
|
28
|
+
if (_autoGenerate(parentElement) && !row[parentElement.name]) {
|
|
29
|
+
_set(row, cds.utils.uuid(), parentElement, enumerable)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const _generateChildField = ({ deep, childElement }, childRow, enumerable) => {
|
|
34
|
+
if (deep) {
|
|
35
|
+
_generateChildField(deep.propagation, childRow[deep.targetName], enumerable)
|
|
36
|
+
} else if (_autoGenerate(childElement) && childRow && !childRow[childElement.name]) {
|
|
37
|
+
_set(childRow, cds.utils.uuid(), childElement, enumerable)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const _getNestedVal = (row, prefix) => {
|
|
42
|
+
let val = row
|
|
43
|
+
const splitted = prefix.split('_')
|
|
44
|
+
splitted.pop() // remove last `_`
|
|
45
|
+
let k = ''
|
|
46
|
+
|
|
47
|
+
while (splitted.length > 0) {
|
|
48
|
+
k += splitted.shift()
|
|
49
|
+
if (k in val) {
|
|
50
|
+
val = val[k]
|
|
51
|
+
k = ''
|
|
52
|
+
} else {
|
|
53
|
+
k += '_'
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return val
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const _propagateToChild = ({ parentElement, childElement, parentFieldValue }, row, childRow, enumerable) => {
|
|
61
|
+
if (!childElement || !childElement.parent.elements[childElement.name]) return
|
|
62
|
+
if (parentElement) {
|
|
63
|
+
const prefix = prefixForStruct(parentElement)
|
|
64
|
+
if (prefix) {
|
|
65
|
+
const nested = _getNestedVal(row, prefix)
|
|
66
|
+
_set(childRow, nested[parentElement.name], childElement, enumerable)
|
|
67
|
+
} else {
|
|
68
|
+
_set(childRow, row[parentElement.name], childElement, enumerable)
|
|
69
|
+
}
|
|
70
|
+
} else if (parentFieldValue !== undefined) {
|
|
71
|
+
_set(childRow, parentFieldValue, childElement, enumerable)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const _propagateToParent = ({ parentElement, childElement, deep }, childRow, row, enumerable) => {
|
|
76
|
+
if (deep) {
|
|
77
|
+
_propagateToParent(deep.propagation, childRow[deep.targetName], childRow, enumerable)
|
|
78
|
+
}
|
|
79
|
+
if (parentElement && childElement && childRow && childElement.name in childRow) {
|
|
80
|
+
_set(row, childRow[childElement.name], parentElement, enumerable)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
module.exports = (
|
|
85
|
+
tKey,
|
|
86
|
+
row,
|
|
87
|
+
foreignKeyPropagations,
|
|
88
|
+
isCompositionEffective,
|
|
89
|
+
{ deleteAssocs = false, enumerable = true } = {}
|
|
90
|
+
) => {
|
|
91
|
+
if (!row || !(tKey in row)) return
|
|
92
|
+
if (row[tKey] === null) {
|
|
93
|
+
for (const foreignKeyPropagation of foreignKeyPropagations) {
|
|
94
|
+
if (!foreignKeyPropagation.fillChild) {
|
|
95
|
+
_set(row, null, foreignKeyPropagation.parentElement, enumerable)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (deleteAssocs && !isCompositionEffective) delete row[tKey]
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const childRows = Array.isArray(row[tKey]) ? row[tKey] : [row[tKey]]
|
|
103
|
+
|
|
104
|
+
for (const childRow of childRows) {
|
|
105
|
+
if (!childRow) return
|
|
106
|
+
|
|
107
|
+
for (const foreignKeyPropagation of foreignKeyPropagations) {
|
|
108
|
+
if (foreignKeyPropagation.fillChild) {
|
|
109
|
+
// propagate or generate in parent
|
|
110
|
+
const pk = foreignKeyPropagation.parentElement && foreignKeyPropagation.parentElement.name
|
|
111
|
+
if (pk && !(pk in row)) _propagateToParent(foreignKeyPropagation, childRow, row, enumerable)
|
|
112
|
+
if (!(pk in row)) _generateParentField(foreignKeyPropagation, row, enumerable)
|
|
113
|
+
|
|
114
|
+
if (isCompositionEffective) _propagateToChild(foreignKeyPropagation, row, childRow, enumerable)
|
|
115
|
+
} else {
|
|
116
|
+
_generateChildField(foreignKeyPropagation, childRow, enumerable)
|
|
117
|
+
_propagateToParent(foreignKeyPropagation, childRow, row, enumerable)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (deleteAssocs && !isCompositionEffective) delete row[tKey]
|
|
122
|
+
}
|
|
@@ -67,7 +67,7 @@ const _getNextTarget = (model, element, currentPath = []) => {
|
|
|
67
67
|
* @param {object} targetEntity The target entity which needs to be traversed
|
|
68
68
|
* @param {object} callbacks
|
|
69
69
|
* @param {function} callbacks.pick Callback function to pick elements. If it returns a truthy value, the element will be picked. The returned value is part of the template.
|
|
70
|
-
* @param {function} callbacks.ignore Callback function to ignore
|
|
70
|
+
* @param {function} callbacks.ignore Callback function to ignore the target of an element. If it returns a truthy value, the element's target will be ignored.
|
|
71
71
|
* @param {object} [parent=null] The parent entity
|
|
72
72
|
* @param {Map} [_entityMap] This parameter is an implementation side-effect — don't use it
|
|
73
73
|
* @param {array} [targetPath=[]]
|
|
@@ -92,8 +92,6 @@ function _getTemplate(model, cache, targetEntity, callbacks, parent = null, _ent
|
|
|
92
92
|
|
|
93
93
|
for (const elementName in elements) {
|
|
94
94
|
const element = elements[elementName]
|
|
95
|
-
if (ignore && ignore(element, targetEntity, parent)) continue
|
|
96
|
-
|
|
97
95
|
_pick(pick, element, targetEntity, parent, templateElements, elementName)
|
|
98
96
|
|
|
99
97
|
if (element.items) {
|
|
@@ -101,6 +99,7 @@ function _getTemplate(model, cache, targetEntity, callbacks, parent = null, _ent
|
|
|
101
99
|
}
|
|
102
100
|
|
|
103
101
|
const { nextTargetName, nextTarget } = _getNextTarget(model, element, currentPath)
|
|
102
|
+
if (ignore && ignore(element)) continue
|
|
104
103
|
const nextTargetCached = _entityMap.get(nextTargetName)
|
|
105
104
|
|
|
106
105
|
if (nextTargetCached) {
|
|
@@ -14,7 +14,7 @@ const cds = require('../../cds')
|
|
|
14
14
|
const normalizeTimeData = require('../utils/normalizeTimeData')
|
|
15
15
|
|
|
16
16
|
const { enrichDataWithKeysFromWhere } = require('../../common/utils/keys')
|
|
17
|
-
const propagateForeignKeys = require('
|
|
17
|
+
const propagateForeignKeys = require('../../common/utils/propagateForeignKeys')
|
|
18
18
|
const getTemplate = require('../../common/utils/template')
|
|
19
19
|
const templateProcessor = require('../../common/utils/templateProcessor')
|
|
20
20
|
|
|
@@ -32,8 +32,8 @@ const _processComplexCategory = ({ row, key, val, category, req, element }) => {
|
|
|
32
32
|
category = category.category
|
|
33
33
|
|
|
34
34
|
// propagate keys
|
|
35
|
-
if (category === 'propagateForeignKeys'
|
|
36
|
-
propagateForeignKeys(key, row, element._foreignKeys, element.isComposition)
|
|
35
|
+
if (category === 'propagateForeignKeys') {
|
|
36
|
+
propagateForeignKeys(key, row, element._foreignKeys, element.isComposition, { deleteAssocs: true })
|
|
37
37
|
return
|
|
38
38
|
}
|
|
39
39
|
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
const cds = require('../../cds')
|
|
2
|
-
const { INSERT,
|
|
2
|
+
const { INSERT, UPDATE } = cds.ql
|
|
3
3
|
|
|
4
4
|
const onDraftActivate = require('./activate')._handler
|
|
5
5
|
const { isNavigationToMany } = require('../utils/req')
|
|
6
|
-
const { getKeysCondition } = require('../utils/where')
|
|
7
6
|
const { ensureDraftsSuffix } = require('../utils/handler')
|
|
8
|
-
const { DRAFT_COLUMNS_MAP } = require('../../common/constants/draft')
|
|
9
7
|
|
|
10
8
|
const _getUpdateDraftAdminCQN = ({ user, timestamp }, draftUUID) => {
|
|
11
9
|
return UPDATE('DRAFT.DraftAdministrativeData')
|
|
@@ -1,13 +1,7 @@
|
|
|
1
1
|
const cds = require('../../cds')
|
|
2
2
|
const { UPDATE, SELECT } = cds.ql
|
|
3
3
|
|
|
4
|
-
const {
|
|
5
|
-
getUpdateDraftAdminCQN,
|
|
6
|
-
removeDraftUUIDIfNecessary,
|
|
7
|
-
ensureDraftsSuffix,
|
|
8
|
-
ensureNoDraftsSuffix,
|
|
9
|
-
addColumnAlias
|
|
10
|
-
} = require('../utils/handler')
|
|
4
|
+
const { getUpdateDraftAdminCQN, ensureDraftsSuffix, ensureNoDraftsSuffix, addColumnAlias } = require('../utils/handler')
|
|
11
5
|
const { getKeysCondition } = require('../utils/where')
|
|
12
6
|
const { getColumns } = require('../../cds-services/services/utils/columns')
|
|
13
7
|
const { DRAFT_COLUMNS_CQN } = require('../../common/constants/draft')
|
|
@@ -24,8 +24,8 @@ const _sanitizeHeaders = headers => {
|
|
|
24
24
|
|
|
25
25
|
const _executeHttpRequest = async ({ requestConfig, destination, destinationOptions, jwt }) => {
|
|
26
26
|
const { executeHttpRequestWithOrigin } = cloudSdk()
|
|
27
|
-
|
|
28
27
|
const destinationName = typeof destination === 'string' && destination
|
|
28
|
+
|
|
29
29
|
if (destinationName) {
|
|
30
30
|
destination = { destinationName, ...(resolveDestinationOptions(destinationOptions, jwt) || {}) }
|
|
31
31
|
} else if (destination.forwardAuthToken) {
|
|
@@ -59,7 +59,13 @@ const _executeHttpRequest = async ({ requestConfig, destination, destinationOpti
|
|
|
59
59
|
|
|
60
60
|
// cloud sdk requires a new mechanism to differentiate the priority of headers
|
|
61
61
|
// "custom" keeps the highest priority as before
|
|
62
|
-
requestConfig = {
|
|
62
|
+
requestConfig = {
|
|
63
|
+
...(cds.env?.remote?.max_body_length && { maxBodyLength: cds.env.remote.max_body_length }),
|
|
64
|
+
...requestConfig,
|
|
65
|
+
headers: {
|
|
66
|
+
custom: { ...requestConfig.headers }
|
|
67
|
+
}
|
|
68
|
+
}
|
|
63
69
|
|
|
64
70
|
return executeHttpRequestWithOrigin(destination, requestConfig, requestOptions)
|
|
65
71
|
}
|
|
@@ -145,10 +151,12 @@ function _defineProperty(obj, property, value) {
|
|
|
145
151
|
const map = (..._) => _defineProperty(_map.call(obj, ..._), property, value)
|
|
146
152
|
props.map = { value: map, enumerable: false, configurable: true, writable: true }
|
|
147
153
|
}
|
|
154
|
+
|
|
148
155
|
props[property] = { value: value, enumerable: false, configurable: true, writable: true }
|
|
149
156
|
for (const prop in props) {
|
|
150
157
|
Object.defineProperty(obj, prop, props[prop])
|
|
151
158
|
}
|
|
159
|
+
|
|
152
160
|
return obj
|
|
153
161
|
}
|
|
154
162
|
|
|
@@ -156,14 +164,17 @@ function _normalizeMetadata(prefix, data, results) {
|
|
|
156
164
|
const target = results !== undefined ? results : data
|
|
157
165
|
if (typeof target !== 'object' || target === null) return target
|
|
158
166
|
const metadataKeys = Object.keys(data).filter(k => prefix.test(k))
|
|
167
|
+
|
|
159
168
|
for (const k of metadataKeys) {
|
|
160
169
|
const $ = k.replace(prefix, '$')
|
|
161
170
|
_defineProperty(target, $, data[k])
|
|
162
171
|
delete target[k]
|
|
163
172
|
}
|
|
173
|
+
|
|
164
174
|
if (Array.isArray(target)) {
|
|
165
175
|
return target.map(row => _normalizeMetadata(prefix, row))
|
|
166
176
|
}
|
|
177
|
+
|
|
167
178
|
// check properties for all and prop.results for odata v2
|
|
168
179
|
for (const [key, value] of Object.entries(target)) {
|
|
169
180
|
if (value && typeof value === 'object') {
|
|
@@ -171,6 +182,7 @@ function _normalizeMetadata(prefix, data, results) {
|
|
|
171
182
|
target[key] = _normalizeMetadata(prefix, value, nestedResults)
|
|
172
183
|
}
|
|
173
184
|
}
|
|
185
|
+
|
|
174
186
|
return target
|
|
175
187
|
}
|
|
176
188
|
const _getPurgedRespActionFunc = (data, returnType) => {
|
|
@@ -181,6 +193,7 @@ const _getPurgedRespActionFunc = (data, returnType) => {
|
|
|
181
193
|
return data[key]
|
|
182
194
|
}
|
|
183
195
|
}
|
|
196
|
+
|
|
184
197
|
return data
|
|
185
198
|
}
|
|
186
199
|
|
|
@@ -198,6 +211,7 @@ const _purgeODataV2 = (data, target, returnType, reqHeaders) => {
|
|
|
198
211
|
ieee754Compatible,
|
|
199
212
|
exponentialDecimals
|
|
200
213
|
)
|
|
214
|
+
|
|
201
215
|
return _normalizeMetadata(/^__/, data, convertedResponse)
|
|
202
216
|
}
|
|
203
217
|
|
|
@@ -217,6 +231,7 @@ const _getSanitizedError = (e, reqOptions, options = { suppressRemoteResponseBod
|
|
|
217
231
|
url: e.config ? e.config.baseURL + e.config.url : reqOptions.url,
|
|
218
232
|
headers: e.config ? e.config.headers : reqOptions.headers
|
|
219
233
|
}
|
|
234
|
+
|
|
220
235
|
if (options.batchRequest) {
|
|
221
236
|
e.request.body = reqOptions.data
|
|
222
237
|
}
|
|
@@ -227,9 +242,11 @@ const _getSanitizedError = (e, reqOptions, options = { suppressRemoteResponseBod
|
|
|
227
242
|
statusText: e.response.statusText,
|
|
228
243
|
headers: e.response.headers
|
|
229
244
|
}
|
|
245
|
+
|
|
230
246
|
if (e.response.data && !options.suppressRemoteResponseBody) {
|
|
231
247
|
response.body = e.response.data
|
|
232
248
|
}
|
|
249
|
+
|
|
233
250
|
e.response = response
|
|
234
251
|
}
|
|
235
252
|
|
|
@@ -270,12 +287,7 @@ const run = async (
|
|
|
270
287
|
response = await _executeHttpRequest({ requestConfig, destination, destinationOptions, jwt })
|
|
271
288
|
} catch (e) {
|
|
272
289
|
// > axios received status >= 400 -> gateway error
|
|
273
|
-
const msg =
|
|
274
|
-
(e.response &&
|
|
275
|
-
e.response.data &&
|
|
276
|
-
e.response.data.error &&
|
|
277
|
-
((e.response.data.error.message && e.response.data.error.message.value) || e.response.data.error.message)) ||
|
|
278
|
-
e.message
|
|
290
|
+
const msg = e?.response?.data?.error?.message?.value ?? e?.response?.data?.error?.message ?? e.message
|
|
279
291
|
e.message = msg ? 'Error during request to remote service: \n' + msg : 'Request to remote service failed.'
|
|
280
292
|
|
|
281
293
|
const sanitizedError = _getSanitizedError(e, requestConfig, {
|
|
@@ -284,7 +296,6 @@ const run = async (
|
|
|
284
296
|
|
|
285
297
|
const err = Object.assign(new Error(e.message), { statusCode: 502, reason: sanitizedError })
|
|
286
298
|
LOG._warn && LOG.warn(err)
|
|
287
|
-
|
|
288
299
|
throw err
|
|
289
300
|
}
|
|
290
301
|
|
|
@@ -312,7 +323,6 @@ const run = async (
|
|
|
312
323
|
})
|
|
313
324
|
|
|
314
325
|
LOG._warn && LOG.warn(err)
|
|
315
|
-
|
|
316
326
|
throw err
|
|
317
327
|
}
|
|
318
328
|
|
|
@@ -331,6 +341,7 @@ const run = async (
|
|
|
331
341
|
if (responseDataSplitted[1].startsWith('HTTP/1.1 2')) {
|
|
332
342
|
response.data = contentJSON
|
|
333
343
|
}
|
|
344
|
+
|
|
334
345
|
if (responseDataSplitted[1].startsWith('HTTP/1.1 4') || responseDataSplitted[1].startsWith('HTTP/1.1 5')) {
|
|
335
346
|
const innerError = contentJSON.error || contentJSON
|
|
336
347
|
innerError.status = Number(responseDataSplitted[1].match(/HTTP.*(\d{3})/m)[1])
|
|
@@ -357,6 +368,7 @@ const run = async (
|
|
|
357
368
|
}
|
|
358
369
|
return _purgeODataV4(response.data)
|
|
359
370
|
}
|
|
371
|
+
|
|
360
372
|
return response.data
|
|
361
373
|
}
|
|
362
374
|
|
|
@@ -368,6 +380,7 @@ const getJwt = req => {
|
|
|
368
380
|
return token[1]
|
|
369
381
|
}
|
|
370
382
|
}
|
|
383
|
+
|
|
371
384
|
return null
|
|
372
385
|
}
|
|
373
386
|
|
|
@@ -382,9 +395,11 @@ const _cqnToReqOptions = (query, service, req) => {
|
|
|
382
395
|
.replace(/\( /g, '(')
|
|
383
396
|
.replace(/ \)/g, ')')
|
|
384
397
|
}
|
|
398
|
+
|
|
385
399
|
if (queryObject.method !== 'GET' && queryObject.method !== 'HEAD') {
|
|
386
400
|
reqOptions.data = kind === 'odata-v2' ? convertV2PayloadData(queryObject.body, req.target) : queryObject.body
|
|
387
401
|
}
|
|
402
|
+
|
|
388
403
|
return reqOptions
|
|
389
404
|
}
|
|
390
405
|
|
|
@@ -395,9 +410,11 @@ const _stringToReqOptions = (query, data, target) => {
|
|
|
395
410
|
method: cleanQuery.substring(0, blankIndex).toUpperCase(),
|
|
396
411
|
url: encodeURI(formatPath(cleanQuery.substring(blankIndex, cleanQuery.length).trim()))
|
|
397
412
|
}
|
|
413
|
+
|
|
398
414
|
if (data && reqOptions.method !== 'GET' && reqOptions.method !== 'HEAD') {
|
|
399
415
|
reqOptions.data = this.kind === 'odata-v2' ? Object.assign({}, convertV2PayloadData(data, target)) : data
|
|
400
416
|
}
|
|
417
|
+
|
|
401
418
|
return reqOptions
|
|
402
419
|
}
|
|
403
420
|
|
|
@@ -412,10 +429,12 @@ const _pathToReqOptions = (method, path, data, target) => {
|
|
|
412
429
|
// normalize in case parts[2] already starts with /
|
|
413
430
|
url = url.replace(/^\/\//, '/')
|
|
414
431
|
}
|
|
432
|
+
|
|
415
433
|
const reqOptions = { method, url }
|
|
416
434
|
if (data && reqOptions.method !== 'GET' && reqOptions.method !== 'HEAD') {
|
|
417
435
|
reqOptions.data = this.kind === 'odata-v2' ? Object.assign({}, convertV2PayloadData(data, target)) : data
|
|
418
436
|
}
|
|
437
|
+
|
|
419
438
|
return reqOptions
|
|
420
439
|
}
|
|
421
440
|
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
// const cds = require('../../_runtime/cds')
|
|
2
2
|
const getTemplate = require('../../_runtime/common/utils/template')
|
|
3
3
|
const templateProcessor = require('../../_runtime/common/utils/templateProcessor')
|
|
4
|
-
const { checkStaticElementByKey
|
|
4
|
+
const { checkStaticElementByKey } = require('../../_runtime/cds-services/util/assert')
|
|
5
5
|
const { MULTIPLE_ERRORS } = require('../../_runtime/common/error/constants')
|
|
6
6
|
|
|
7
|
-
|
|
8
7
|
//
|
|
9
8
|
// REVISIT: We need to decipher what we are doing here...
|
|
10
9
|
//
|
|
@@ -20,7 +19,7 @@ const _picker = () => {
|
|
|
20
19
|
}
|
|
21
20
|
|
|
22
21
|
const _processorFn = errors => {
|
|
23
|
-
return ({ row, key, plain: categories, target
|
|
22
|
+
return ({ row, key, plain: categories, target }) => {
|
|
24
23
|
// REVISIT move validation to generic asserter => see PR 717
|
|
25
24
|
if (categories['static_validation'] && row[key] != null) {
|
|
26
25
|
const validations = checkStaticElementByKey(target, key, row[key])
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
// using { cds.xt.TAR } from './model-provider'; //> IMPORTANT: don't add this as it will cause services loaded twice
|
|
1
2
|
using { cds.xt.Extensions } from './extensions';
|
|
2
|
-
using { cds.xt.TAR } from './model-provider';
|
|
3
3
|
|
|
4
4
|
@protocol: 'rest'
|
|
5
5
|
@(requires : 'authenticated-user')
|
|
@@ -8,7 +8,8 @@ service cds.xt.ExtensibilityService @(path:'/-/cds/extensibility', impl:'@sap/cd
|
|
|
8
8
|
// TODO: async jobs
|
|
9
9
|
|
|
10
10
|
type ActivationLevel : Extensions:activated;
|
|
11
|
-
type
|
|
11
|
+
type TAR : LargeBinary;
|
|
12
|
+
type CSN : String; // REVISIT: should reuse cds.xt.CSN
|
|
12
13
|
type CSN_OR_CDL: String;
|
|
13
14
|
|
|
14
15
|
// UIFLEX API
|
|
@@ -46,7 +47,7 @@ service cds.xt.ExtensibilityService @(path:'/-/cds/extensibility', impl:'@sap/cd
|
|
|
46
47
|
|
|
47
48
|
@(requires : ['cds.ExtensionDeveloper'])
|
|
48
49
|
action push (
|
|
49
|
-
extension : TAR
|
|
50
|
+
extension : LargeBinary, // REVISIT: Using TAR here leads to a strange type check failure
|
|
50
51
|
tag : Extensions:tag
|
|
51
52
|
// activate : ActivationLevel
|
|
52
53
|
);
|
package/srv/mtx.js
CHANGED
|
@@ -8,19 +8,28 @@ class MTXServices extends cds.Service { async init(){
|
|
|
8
8
|
}
|
|
9
9
|
// else...
|
|
10
10
|
DEBUG && DEBUG ('bootstrapping MTX services...')
|
|
11
|
+
let defs = cds.model.definitions
|
|
11
12
|
let sources = []
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
|
|
14
|
+
if (cds.requires.multitenancy && !('cds.xt.SaasProvisioningService' in defs)) {
|
|
15
|
+
sources.push('@sap/cds-mtxs/srv/cf/saas-provisioning-service')
|
|
16
|
+
sources.push('@sap/cds/srv/model-provider')
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (cds.requires.multitenancy && !('cds.xt.DeploymentService' in defs)) {
|
|
20
|
+
sources.push('@sap/cds-mtxs/srv/deployment-service')
|
|
21
|
+
sources.push('@sap/cds/srv/model-provider')
|
|
17
22
|
}
|
|
18
|
-
|
|
19
|
-
|
|
23
|
+
|
|
24
|
+
if (cds.requires.extensibility && !('cds.xt.ExtensibilityService' in defs)) {
|
|
25
|
+
sources.push('@sap/cds/srv/extensibility-service')
|
|
26
|
+
sources.push('@sap/cds/srv/model-provider')
|
|
20
27
|
}
|
|
21
|
-
|
|
22
|
-
|
|
28
|
+
|
|
29
|
+
if (cds.requires.toggles && !('cds.xt.ModelProviderService' in defs)) {
|
|
30
|
+
sources.push('@sap/cds/srv/model-provider')
|
|
23
31
|
}
|
|
32
|
+
|
|
24
33
|
let models = cds.resolve(sources); if (!models) return
|
|
25
34
|
let base = cds.model.$sources; models = models.filter(m => !base.includes(m))
|
|
26
35
|
if (models.length) return cds.serve(models).in(cds.app)
|
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
const cds = require('../../cds')
|
|
2
|
-
|
|
3
|
-
const { prefixForStruct } = require('../../common/utils/csn')
|
|
4
|
-
|
|
5
|
-
const _autoGenerate = e => e && e.isUUID && e.key
|
|
6
|
-
|
|
7
|
-
const _generateParentField = ({ parentElement }, row) => {
|
|
8
|
-
if (_autoGenerate(parentElement) && !row[parentElement.name]) {
|
|
9
|
-
row[parentElement.name] = cds.utils.uuid()
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
const _generateChildField = ({ deep, childElement }, childRow) => {
|
|
14
|
-
if (deep) {
|
|
15
|
-
_generateChildField(deep.propagation, childRow[deep.targetName])
|
|
16
|
-
} else if (_autoGenerate(childElement) && childRow && !childRow[childElement.name]) {
|
|
17
|
-
childRow[childElement.name] = cds.utils.uuid()
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const _getNestedVal = (row, prefix) => {
|
|
22
|
-
let val = row
|
|
23
|
-
const splitted = prefix.split('_')
|
|
24
|
-
splitted.pop() // remove last `_`
|
|
25
|
-
let k = ''
|
|
26
|
-
|
|
27
|
-
while (splitted.length > 0) {
|
|
28
|
-
k += splitted.shift()
|
|
29
|
-
if (k in val) {
|
|
30
|
-
val = val[k]
|
|
31
|
-
k = ''
|
|
32
|
-
} else {
|
|
33
|
-
k += '_'
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
return val
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const _propagateToChid = ({ parentElement, childElement, parentFieldValue }, row, childRow) => {
|
|
41
|
-
if (!childElement) return
|
|
42
|
-
if (parentElement) {
|
|
43
|
-
const prefix = prefixForStruct(parentElement)
|
|
44
|
-
if (prefix) {
|
|
45
|
-
const nested = _getNestedVal(row, prefix)
|
|
46
|
-
childRow[childElement.name] = nested[parentElement.name]
|
|
47
|
-
} else {
|
|
48
|
-
childRow[childElement.name] = row[parentElement.name]
|
|
49
|
-
}
|
|
50
|
-
} else if (parentFieldValue !== undefined) {
|
|
51
|
-
childRow[childElement.name] = parentFieldValue
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const _propagateToParent = ({ parentElement, childElement, deep }, childRow, row) => {
|
|
56
|
-
if (deep) {
|
|
57
|
-
_propagateToParent(deep.propagation, childRow[deep.targetName], childRow)
|
|
58
|
-
}
|
|
59
|
-
if (parentElement && childElement && childRow && childElement.name in childRow) {
|
|
60
|
-
row[parentElement.name] = childRow[childElement.name]
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
module.exports = (tKey, row, foreignKeyPropagations, isCompositionEffective) => {
|
|
65
|
-
if (row[tKey] === null) {
|
|
66
|
-
for (const foreignKeyPropagation of foreignKeyPropagations) {
|
|
67
|
-
if (!foreignKeyPropagation.fillChild) row[foreignKeyPropagation.parentElement.name] = null
|
|
68
|
-
}
|
|
69
|
-
if (!isCompositionEffective) delete row[tKey]
|
|
70
|
-
return
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const childRows = Array.isArray(row[tKey]) ? row[tKey] : [row[tKey]]
|
|
74
|
-
|
|
75
|
-
for (const childRow of childRows) {
|
|
76
|
-
if (!childRow) return
|
|
77
|
-
|
|
78
|
-
for (const foreignKeyPropagation of foreignKeyPropagations) {
|
|
79
|
-
if (foreignKeyPropagation.fillChild) {
|
|
80
|
-
// propagate or generate in parent
|
|
81
|
-
const pk = foreignKeyPropagation.parentElement && foreignKeyPropagation.parentElement.name
|
|
82
|
-
if (pk && !(pk in row)) _propagateToParent(foreignKeyPropagation, childRow, row)
|
|
83
|
-
if (!(pk in row)) _generateParentField(foreignKeyPropagation, row)
|
|
84
|
-
|
|
85
|
-
if (isCompositionEffective) _propagateToChid(foreignKeyPropagation, row, childRow)
|
|
86
|
-
} else {
|
|
87
|
-
_generateChildField(foreignKeyPropagation, childRow)
|
|
88
|
-
_propagateToParent(foreignKeyPropagation, childRow, row)
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
if (!isCompositionEffective) delete row[tKey]
|
|
93
|
-
}
|