@sap/cds 7.2.1 → 7.3.1
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 +168 -126
- package/README.md +1 -1
- package/apis/core.d.ts +6 -4
- package/apis/services.d.ts +24 -4
- package/apis/test.d.ts +24 -10
- package/bin/serve.js +4 -3
- package/lib/auth/ias-auth.js +7 -8
- package/lib/compile/cdsc.js +5 -7
- package/lib/compile/etc/csv.js +22 -11
- package/lib/compile/for/lean_drafts.js +1 -1
- package/lib/dbs/cds-deploy.js +1 -2
- package/lib/env/cds-env.js +26 -20
- package/lib/env/defaults.js +4 -3
- package/lib/env/schema.js +9 -0
- package/lib/i18n/localize.js +83 -77
- package/lib/index.js +6 -2
- package/lib/linked/classes.js +13 -13
- package/lib/plugins.js +41 -45
- package/lib/req/user.js +2 -2
- package/lib/srv/protocols/_legacy.js +0 -1
- package/lib/srv/protocols/odata-v4.js +4 -0
- package/lib/utils/axios.js +7 -1
- package/lib/utils/cds-test.js +140 -133
- package/lib/utils/cds-utils.js +10 -3
- package/lib/utils/check-version.js +6 -0
- package/lib/utils/data.js +19 -6
- package/libx/_runtime/cds-services/adapter/odata-v4/OData.js +20 -19
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +10 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/error.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +2 -3
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/utils/ValueConverter.js +0 -14
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/core/OdataRequest.js +1 -0
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/deserializer/BatchRequestListBuilder.js +5 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/handler/MetadataHandler.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/handler/ServiceHandler.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/invocation/DispatcherCommand.js +2 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/utils/BufferedWriter.js +1 -3
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/metaInfo.js +1 -1
- package/libx/_runtime/common/composition/update.js +18 -2
- package/libx/_runtime/common/error/frontend.js +46 -34
- package/libx/_runtime/common/generic/auth/capabilities.js +33 -14
- package/libx/_runtime/common/generic/input.js +1 -1
- package/libx/_runtime/common/utils/cqn2cqn4sql.js +3 -3
- package/libx/_runtime/db/query/update.js +48 -30
- package/libx/_runtime/fiori/lean-draft.js +2 -3
- package/libx/_runtime/hana/conversion.js +3 -2
- package/libx/_runtime/messaging/enterprise-messaging-utils/getTenantInfo.js +1 -1
- package/libx/_runtime/messaging/outbox/utils.js +1 -1
- package/libx/_runtime/remote/Service.js +1 -17
- package/libx/_runtime/remote/utils/client.js +3 -3
- package/libx/_runtime/remote/utils/data.js +5 -7
- package/libx/odata/{grammar.pegjs → grammar.peggy} +1 -1
- package/libx/odata/metadata.js +121 -0
- package/libx/odata/parser.js +1 -1
- package/libx/odata/service-document.js +61 -0
- package/libx/odata/utils.js +102 -48
- package/libx/rest/RestAdapter.js +2 -2
- package/libx/rest/middleware/error.js +1 -1
- package/package.json +1 -1
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const parentDataSymbol = Symbol('parentData')
|
|
2
|
+
const cqnSymbol = Symbol('cqn')
|
|
1
3
|
const cds = require('../../cds')
|
|
2
4
|
const { getCompositionTree } = require('./tree')
|
|
3
5
|
const { getDeepInsertCQNs } = require('./insert')
|
|
@@ -138,7 +140,13 @@ function _addSubDeepUpdateCQNForUpdateInsert({ entity, entityName, data, selectD
|
|
|
138
140
|
const oldData = ctUtils.cleanDeepData(entity, selectEntry)
|
|
139
141
|
const diff = _diffData(newData, oldData, entity, entry, selectEntry, model)
|
|
140
142
|
// empty updates will be removed later
|
|
141
|
-
updateCQNs.push({
|
|
143
|
+
updateCQNs.push({
|
|
144
|
+
UPDATE: { entity: entityName, data: Object.assign({}, key, diff), where: ctUtils.whereKey(key) },
|
|
145
|
+
// We take tree information from data and store
|
|
146
|
+
// it in the `updateCQN` array (which itself is just a flat list)
|
|
147
|
+
parent: entry[parentDataSymbol]?.[cqnSymbol]
|
|
148
|
+
})
|
|
149
|
+
entry[cqnSymbol] = updateCQNs[updateCQNs.length - 1]
|
|
142
150
|
} else {
|
|
143
151
|
insertCQN.INSERT.entries.push(entry)
|
|
144
152
|
// inserts are handled deep so they must not be put into deepUpdateData
|
|
@@ -189,7 +197,15 @@ const _addToData = (subData, entity, element, entry) => {
|
|
|
189
197
|
const value = ctUtils.val(entry[element.name])
|
|
190
198
|
const subDataEntries = ctUtils.array(value)
|
|
191
199
|
const unwrappedSubData = subDataEntries.map(entry => _unwrapIfNotArray(entry))
|
|
192
|
-
for (const val of unwrappedSubData)
|
|
200
|
+
for (const val of unwrappedSubData) {
|
|
201
|
+
if (val != null) {
|
|
202
|
+
// We need to conserve the tree information which gets
|
|
203
|
+
// lost because we're creating flat arrays of layers.
|
|
204
|
+
Object.defineProperty(val, parentDataSymbol, { value: entry })
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
subData.push(val)
|
|
208
|
+
}
|
|
193
209
|
}
|
|
194
210
|
|
|
195
211
|
async function _addSubDeepUpdateCQNRecursion({ model, compositionTree, entity, data, selectData, cqns, draft, req }) {
|
|
@@ -39,42 +39,48 @@ const _getFiltered = err => {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
const _rewriteDBError = error => {
|
|
42
|
+
let { code, message } = error
|
|
43
|
+
code = String(code || 'null')
|
|
44
|
+
|
|
42
45
|
// REVISIT: db stuff probably shouldn't be here
|
|
43
|
-
if (
|
|
46
|
+
if (code === 'SQLITE_ERROR') {
|
|
44
47
|
error.code = 'null'
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (
|
|
52
|
+
(code.startsWith('SQLITE_CONSTRAINT') && (message.match(/COMMIT/) || message.match(/FOREIGN KEY/))) ||
|
|
53
|
+
(code === '155' && message.match(/fk constraint violation/))
|
|
49
54
|
) {
|
|
50
55
|
// > foreign key constaint violation no sqlite/ hana
|
|
51
56
|
error.code = '400'
|
|
52
|
-
error.message =
|
|
53
|
-
|
|
57
|
+
error.message = 'FK_CONSTRAINT_VIOLATION'
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (code.startsWith('ASSERT_')) {
|
|
54
62
|
error.code = '400'
|
|
63
|
+
return
|
|
55
64
|
}
|
|
56
65
|
}
|
|
57
66
|
|
|
58
|
-
const _normalize = (err, locale,
|
|
67
|
+
const _normalize = (err, locale, formatterFn = _getFiltered) => {
|
|
68
|
+
// REVISIT: code and message rewriting
|
|
69
|
+
_rewriteDBError(err)
|
|
70
|
+
|
|
59
71
|
// message (i18n)
|
|
60
72
|
err.message = getErrorMessage(err, locale)
|
|
61
73
|
|
|
62
|
-
// only allowed properties
|
|
63
|
-
const error = _getFiltered(err)
|
|
64
|
-
|
|
65
74
|
// ensure code is set and a string
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
// REVISIT: code and message rewriting
|
|
69
|
-
_rewriteDBError(error)
|
|
75
|
+
err.code = String(err.code || 'null')
|
|
70
76
|
|
|
71
|
-
let statusCode = err.status || err.statusCode || (_isAllowedError(
|
|
77
|
+
let statusCode = err.status || err.statusCode || (_isAllowedError(err.code) && err.code)
|
|
72
78
|
|
|
73
79
|
// details
|
|
74
|
-
if (
|
|
80
|
+
if (err.details) {
|
|
75
81
|
const childErrorCodes = new Set()
|
|
76
|
-
|
|
77
|
-
const { error: childError, statusCode: childStatusCode } = _normalize(ele, locale,
|
|
82
|
+
err.details = err.details.map(ele => {
|
|
83
|
+
const { error: childError, statusCode: childStatusCode } = _normalize(ele, locale, formatterFn)
|
|
78
84
|
childErrorCodes.add(childStatusCode)
|
|
79
85
|
return childError
|
|
80
86
|
})
|
|
@@ -84,12 +90,13 @@ const _normalize = (err, locale, inner = false) => {
|
|
|
84
90
|
// make sure it's a number if set, otherwise will be assigned as 500 in normalizeError
|
|
85
91
|
statusCode = statusCode ? Number(statusCode) : undefined
|
|
86
92
|
|
|
93
|
+
// only allowed properties
|
|
94
|
+
const error = formatterFn(err)
|
|
95
|
+
|
|
87
96
|
return { error, statusCode }
|
|
88
97
|
}
|
|
89
98
|
|
|
90
|
-
const _isAllowedError = errorCode =>
|
|
91
|
-
return errorCode >= 300 && errorCode < 505
|
|
92
|
-
}
|
|
99
|
+
const _isAllowedError = errorCode => errorCode >= 300 && errorCode < 505
|
|
93
100
|
|
|
94
101
|
// - for one unique value, we use it
|
|
95
102
|
// - if at least one 5xx exists, we use 500
|
|
@@ -100,32 +107,37 @@ const _statusCodeFromDetails = uniqueStatusCodes => {
|
|
|
100
107
|
if ([...uniqueStatusCodes].some(s => s >= 400)) return 400
|
|
101
108
|
}
|
|
102
109
|
|
|
103
|
-
const
|
|
110
|
+
const _getSanitizedError = (statusCode, locale) => ({
|
|
111
|
+
error: { code: String(statusCode), message: i18n(statusCode, locale) },
|
|
112
|
+
statusCode
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
const normalizeError = (err, req, formatterFn) => {
|
|
104
116
|
const locale = req.locale || (req.locale = localeFrom(req))
|
|
105
|
-
let { error, statusCode } = _normalize(err, locale)
|
|
117
|
+
let { error, statusCode } = _normalize(err, locale, formatterFn)
|
|
106
118
|
|
|
107
|
-
if (
|
|
119
|
+
if (process.env.NODE_ENV === 'production') {
|
|
108
120
|
// > return sanitized error to client
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
121
|
+
|
|
122
|
+
if (!statusCode) return _getSanitizedError(500, locale)
|
|
123
|
+
|
|
124
|
+
if (statusCode >= 500 && err._thrownByFramework) {
|
|
125
|
+
return _getSanitizedError(statusCode, locale)
|
|
112
126
|
}
|
|
113
127
|
}
|
|
114
128
|
|
|
115
129
|
if (!statusCode) statusCode = 500
|
|
116
130
|
|
|
117
131
|
// no top level null codes
|
|
118
|
-
if (error.code === 'null')
|
|
119
|
-
error.code = String(statusCode)
|
|
120
|
-
}
|
|
132
|
+
if (error.code === 'null') error.code = String(statusCode)
|
|
121
133
|
|
|
122
134
|
return { error, statusCode }
|
|
123
135
|
}
|
|
124
136
|
|
|
125
137
|
const _ensureSeverity = arg => {
|
|
126
|
-
if (typeof arg
|
|
127
|
-
|
|
128
|
-
|
|
138
|
+
if (typeof arg !== 'number') return DEFAULT_SEVERITY
|
|
139
|
+
|
|
140
|
+
if (arg >= MIN_SEVERITY && arg <= MAX_SEVERITY) return arg
|
|
129
141
|
|
|
130
142
|
return DEFAULT_SEVERITY
|
|
131
143
|
}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
const { cqnFrom } = require('./utils')
|
|
2
2
|
const { RESTRICTIONS } = require('./constants')
|
|
3
3
|
|
|
4
|
-
const
|
|
5
|
-
if (capabilityReadByKey !== undefined && req.query.SELECT
|
|
6
|
-
return capabilityReadByKey
|
|
4
|
+
const _getRestriction = (req, capability, capabilityReadByKey) => {
|
|
5
|
+
if (capabilityReadByKey !== undefined && req.query.SELECT?.one) {
|
|
6
|
+
return capabilityReadByKey
|
|
7
7
|
}
|
|
8
|
-
return capability
|
|
8
|
+
return capability
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
const
|
|
11
|
+
const _getNavigationRestriction = (target, path, annotation, req) => {
|
|
12
12
|
if (!target) return
|
|
13
13
|
if (!Array.isArray(target['@Capabilities.NavigationRestrictions.RestrictedProperties'])) return
|
|
14
14
|
|
|
@@ -17,7 +17,7 @@ const _isNavigationRestricted = (target, path, annotation, req) => {
|
|
|
17
17
|
// prefix check to support both notations: { InsertRestrictions: { Insertable: false } } and { InsertRestrictions.Insertable: false }
|
|
18
18
|
if (r.NavigationProperty['='] === path && Object.keys(r).some(k => k.startsWith(restriction))) {
|
|
19
19
|
const capability = r[annotation] ?? r[restriction]?.[operation]
|
|
20
|
-
return
|
|
20
|
+
return _getRestriction(req, capability, r.ReadRestrictions?.['ReadByKeyRestrictions.Readable'])
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
}
|
|
@@ -32,22 +32,41 @@ function handler(req) {
|
|
|
32
32
|
|
|
33
33
|
const action = annotation.split('.').pop().toUpperCase()
|
|
34
34
|
const from = cqnFrom(req)
|
|
35
|
-
const nav = (from
|
|
35
|
+
const nav = (from?.ref && from.ref.map(el => el.id || el)) || []
|
|
36
36
|
|
|
37
|
+
let navRestriction
|
|
37
38
|
if (nav.length > 1) {
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
const navs = nav.slice(1)
|
|
40
|
+
let lastTarget, target, element, navigation, path
|
|
41
|
+
target = this.model.definitions[nav[0]]
|
|
42
|
+
for (let i = 0; i < navs.length && target; i++) {
|
|
43
|
+
element = !element || element.isAssociation ? target.elements[navs[i]] : element.elements[navs[i]]
|
|
44
|
+
if (element.isAssociation) {
|
|
45
|
+
navigation = path ? `${path}.${navs[i]}` : navs[i]
|
|
46
|
+
path = undefined
|
|
47
|
+
lastTarget = target
|
|
48
|
+
target = this.model.definitions[element.target]
|
|
49
|
+
} else {
|
|
50
|
+
path = path ? `${path}.${navs[i]}` : navs[i]
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (lastTarget && navigation) {
|
|
54
|
+
navRestriction = _getNavigationRestriction(lastTarget, navigation, annotation, req)
|
|
55
|
+
}
|
|
56
|
+
if (navRestriction === false) {
|
|
41
57
|
// REVISIT: rework exception with using target
|
|
42
|
-
const trgt = `${_localName(
|
|
58
|
+
const trgt = `${_localName(lastTarget)}.${navs.join('.')}`
|
|
43
59
|
req.reject(405, 'ENTITY_IS_NOT_CRUD_VIA_NAVIGATION', [_localName(req.target), action, trgt])
|
|
44
60
|
}
|
|
45
|
-
}
|
|
46
|
-
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (
|
|
64
|
+
!navRestriction &&
|
|
65
|
+
_getRestriction(
|
|
47
66
|
req,
|
|
48
67
|
req.target['@Capabilities.' + annotation],
|
|
49
68
|
req.target['@Capabilities.' + RESTRICTIONS.READABLE_BY_KEY]
|
|
50
|
-
)
|
|
69
|
+
) === false
|
|
51
70
|
) {
|
|
52
71
|
req.reject(405, 'ENTITY_IS_NOT_CRUD', [_localName(req.target), action])
|
|
53
72
|
}
|
|
@@ -246,7 +246,7 @@ async function commonGenericInput(req) {
|
|
|
246
246
|
if (boundAction) {
|
|
247
247
|
const pathSegment = _getBoundActionBindingParameter(boundAction)
|
|
248
248
|
if (pathSegment) pathOptions.pathSegmentsInfo.push(pathSegment)
|
|
249
|
-
const keys = req.
|
|
249
|
+
const keys = req.params?.[0]
|
|
250
250
|
if (keys && 'IsActiveEntity' in keys) {
|
|
251
251
|
pathOptions.draftKeys = { IsActiveEntity: keys.IsActiveEntity }
|
|
252
252
|
}
|
|
@@ -729,9 +729,6 @@ const _convertSelect = (query, model, _options) => {
|
|
|
729
729
|
}
|
|
730
730
|
}
|
|
731
731
|
|
|
732
|
-
// lambda functions
|
|
733
|
-
convertWhereExists(query.SELECT, model, options)
|
|
734
|
-
|
|
735
732
|
// add 'or is null' in case of not equal: '!=' or '<>'
|
|
736
733
|
if (query.SELECT._4odata) {
|
|
737
734
|
_convertNotEqual(query.SELECT, 'where')
|
|
@@ -739,6 +736,9 @@ const _convertSelect = (query, model, _options) => {
|
|
|
739
736
|
}
|
|
740
737
|
|
|
741
738
|
_convertPathExpression(query, model, options)
|
|
739
|
+
|
|
740
|
+
convertWhereExists(query.SELECT, model, options)
|
|
741
|
+
|
|
742
742
|
rewriteAsterisks(query, model, options)
|
|
743
743
|
const entity =
|
|
744
744
|
(query.SELECT.from.ref && (query.SELECT.from.ref[0].id || query.SELECT.from.ref[0])) || query.SELECT.from
|
|
@@ -1,48 +1,64 @@
|
|
|
1
1
|
const cds = require('../../cds')
|
|
2
|
-
|
|
2
|
+
const preservedSymbol = Symbol('preserved')
|
|
3
3
|
const { hasDeepUpdate, getDeepUpdateCQNs, selectDeepUpdateData } = require('../../common/composition')
|
|
4
4
|
const { getFlatArray, processCQNs } = require('../utils/deep')
|
|
5
5
|
const normalizeTimestamp = require('../../common/utils/normalizeTimestamp')
|
|
6
|
+
const onlyKeysRemain = require('../../common/utils/onlyKeysRemain')
|
|
7
|
+
|
|
8
|
+
const isMoreThanManaged = (cqn, entity) => {
|
|
9
|
+
return (
|
|
10
|
+
cqn[preservedSymbol] ||
|
|
11
|
+
Object.keys(cqn.UPDATE.data).some(
|
|
12
|
+
key =>
|
|
13
|
+
!(key in entity.keys) &&
|
|
14
|
+
(entity.elements[key]['@cds.on.update'] === undefined || !cqn.UPDATE.data[key]?.startsWith('$'))
|
|
15
|
+
)
|
|
16
|
+
)
|
|
17
|
+
}
|
|
6
18
|
|
|
7
|
-
const
|
|
8
|
-
return
|
|
9
|
-
|
|
10
|
-
|
|
19
|
+
const isManaged = (cqn, entity) => {
|
|
20
|
+
return Object.keys(cqn.UPDATE.data).some(
|
|
21
|
+
key => entity.elements[key]['@cds.on.update'] && cqn.UPDATE.data[key] !== undefined
|
|
22
|
+
)
|
|
11
23
|
}
|
|
12
24
|
|
|
13
25
|
const _getFilteredCqns = (cqns, model) => {
|
|
14
26
|
// right to left processing necessary!
|
|
15
|
-
// no need to process first (= root)
|
|
16
27
|
for (let i = cqns.length - 1; i > 0; i--) {
|
|
17
28
|
const cqn = cqns[i]
|
|
18
29
|
|
|
19
30
|
const entity = model && cqn.UPDATE && model.definitions[cqn.UPDATE.entity]
|
|
20
|
-
if (!entity)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
// with $) a composition target is updated as well
|
|
25
|
-
let moreThanManaged = Object.keys(cqn.UPDATE.data).some(
|
|
26
|
-
key => entity.elements[key]['@cds.on.update'] === undefined || !cqn.UPDATE.data[key]?.startsWith('$')
|
|
27
|
-
)
|
|
31
|
+
if (!entity) {
|
|
32
|
+
Object.defineProperty(cqn, preservedSymbol, { value: true })
|
|
33
|
+
continue
|
|
34
|
+
}
|
|
28
35
|
|
|
29
|
-
if (
|
|
36
|
+
// do not filter if there is a property that is not a key or managed by us (its value starts with $)
|
|
37
|
+
let moreThanManaged = isMoreThanManaged(cqn, entity)
|
|
30
38
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
39
|
+
if (moreThanManaged) {
|
|
40
|
+
Object.defineProperty(cqn, preservedSymbol, { value: true })
|
|
41
|
+
const parentEntity = cqn.parent?.UPDATE && model.definitions[cqn.parent.UPDATE.entity]
|
|
42
|
+
if (parentEntity && isManaged(cqn.parent, parentEntity)) {
|
|
43
|
+
Object.defineProperty(cqn.parent, preservedSymbol, { value: true })
|
|
36
44
|
}
|
|
45
|
+
continue
|
|
37
46
|
}
|
|
47
|
+
}
|
|
38
48
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
//
|
|
42
|
-
|
|
49
|
+
const rootCqn = cqns[0]
|
|
50
|
+
if (rootCqn) {
|
|
51
|
+
// no need to process first (= root)
|
|
52
|
+
Object.defineProperty(rootCqn, preservedSymbol, { value: true })
|
|
43
53
|
}
|
|
44
54
|
|
|
45
|
-
return cqns
|
|
55
|
+
return cqns.filter(cqn => {
|
|
56
|
+
const entity = model && cqn.UPDATE && model.definitions[cqn.UPDATE.entity]
|
|
57
|
+
if (entity && onlyKeysRemain({ query: cqn, target: entity, data: cqn.UPDATE.data })) {
|
|
58
|
+
return false
|
|
59
|
+
}
|
|
60
|
+
return cqn[preservedSymbol]
|
|
61
|
+
})
|
|
46
62
|
}
|
|
47
63
|
|
|
48
64
|
const update = executeUpdateCQN => async (model, dbc, req) => {
|
|
@@ -52,20 +68,22 @@ const update = executeUpdateCQN => async (model, dbc, req) => {
|
|
|
52
68
|
if (model && hasDeepUpdate(model, query)) {
|
|
53
69
|
// REVISIT: avoid additional read
|
|
54
70
|
const selectData = await selectDeepUpdateData(cds.db, model, req)
|
|
55
|
-
|
|
71
|
+
const cqns = await getDeepUpdateCQNs(model, req, selectData)
|
|
56
72
|
|
|
57
73
|
// the delete chunks, i.e., how many deletes can be processed in parallel
|
|
58
74
|
const chunks = []
|
|
59
75
|
for (const each of cqns) chunks.push(each.filter(e => e.DELETE).length)
|
|
60
76
|
|
|
61
77
|
// remove child queries that only want to update @cds.on.update properties
|
|
62
|
-
|
|
78
|
+
let _cqns = Array.from(cqns)
|
|
79
|
+
_cqns = getFlatArray(_cqns)
|
|
80
|
+
_cqns = _getFilteredCqns(_cqns, model)
|
|
63
81
|
|
|
64
|
-
if (
|
|
65
|
-
const results = await processCQNs(executeUpdateCQN,
|
|
82
|
+
if (_cqns.length === 0) return 0
|
|
83
|
+
const results = await processCQNs(executeUpdateCQN, _cqns, model, dbc, user, locale, isoTs, chunks)
|
|
66
84
|
|
|
67
85
|
// return number of affected rows of "root cqn", if an update, 1 otherwise (as not update of root but its children)
|
|
68
|
-
if (
|
|
86
|
+
if (_cqns[0].UPDATE) return results[0]
|
|
69
87
|
return 1
|
|
70
88
|
}
|
|
71
89
|
|
|
@@ -269,11 +269,10 @@ cds.ApplicationService.prototype.handle = async function (req) {
|
|
|
269
269
|
delete res.DraftAdministrativeData
|
|
270
270
|
const HasActiveEntity = res.HasActiveEntity
|
|
271
271
|
delete res.HasActiveEntity
|
|
272
|
-
delete res.IsActiveEntity
|
|
273
272
|
// First run the handlers as they might need access to DraftAdministrativeData or the draft entities
|
|
274
273
|
const result = await run(
|
|
275
274
|
HasActiveEntity ? UPDATE(req.target).data(res).where(targetWhere) : INSERT.into(req.target).entries(res),
|
|
276
|
-
{ headers: { 'if-match': '*' } }
|
|
275
|
+
{ headers: Object.assign({}, req.headers, { 'if-match': '*' }) }
|
|
277
276
|
)
|
|
278
277
|
await _promiseAll([
|
|
279
278
|
DELETE.from(targetDraft).where(targetWhere),
|
|
@@ -1054,7 +1053,7 @@ async function onCancel(req) {
|
|
|
1054
1053
|
)
|
|
1055
1054
|
else
|
|
1056
1055
|
queries.push(
|
|
1057
|
-
UPDATE('
|
|
1056
|
+
UPDATE('DRAFT.DraftAdministrativeData')
|
|
1058
1057
|
.data({
|
|
1059
1058
|
InProcessByUser: cds.context.user.id,
|
|
1060
1059
|
LastChangedByUser: cds.context.user.id,
|
|
@@ -21,15 +21,16 @@ const convertInt64ToString = int64 => {
|
|
|
21
21
|
const convertToISO = element => {
|
|
22
22
|
if (!element) return null
|
|
23
23
|
|
|
24
|
+
let dateTime = element.replace(' ', 'T')
|
|
24
25
|
if (cds.env.features.precise_timestamps) {
|
|
25
|
-
|
|
26
|
+
dateTime = dateTime.slice(0, 19)
|
|
26
27
|
let millis = element.slice(20)
|
|
27
28
|
if (millis.at(-1) === 'Z') millis = millis.slice(0, -1)
|
|
28
29
|
millis = millis.slice(0, 7).padEnd(7, '0')
|
|
29
30
|
return dateTime + '.' + millis + 'Z'
|
|
30
31
|
}
|
|
31
32
|
|
|
32
|
-
return new Date(
|
|
33
|
+
return new Date(dateTime + 'Z').toISOString()
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
const convertToISONoMillis = element => {
|
|
@@ -4,7 +4,7 @@ const _transform = o => ({ subdomain: o.subscribedSubdomain, tenant: o.subscribe
|
|
|
4
4
|
// REVISIT: Looks ugly -> can we simplify that?
|
|
5
5
|
const getTenantInfo = async tenant => {
|
|
6
6
|
const provisioning = await cds.connect.to('cds.xt.SaasProvisioningService')
|
|
7
|
-
const tx = provisioning.tx({ user:
|
|
7
|
+
const tx = provisioning.tx({ user: cds.User.privileged })
|
|
8
8
|
try {
|
|
9
9
|
const result = tenant
|
|
10
10
|
? _transform(await tx.get('/tenant', { ['subscribedTenantId']: tenant }))
|
|
@@ -99,7 +99,7 @@ const processMessages = async (service, tenant, _opts = {}) => {
|
|
|
99
99
|
|
|
100
100
|
outboxRunner.run({ name, tenant }, () => {
|
|
101
101
|
let letAppCrash = false
|
|
102
|
-
const config = tenant ? { tenant, user:
|
|
102
|
+
const config = tenant ? { tenant, user: cds.User.privileged } : { user: cds.User.privileged }
|
|
103
103
|
const spawn = cds.spawn(async () => {
|
|
104
104
|
let messages
|
|
105
105
|
try {
|
|
@@ -34,6 +34,7 @@ const _setHeaders = (defaultHeaders, req) => {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
const _setCorrectValue = (el, data, params, kind) => {
|
|
37
|
+
if (data[el] === undefined) return "'undefined'"
|
|
37
38
|
return typeof data[el] === 'object' && kind !== 'odata-v2'
|
|
38
39
|
? JSON.stringify(data[el])
|
|
39
40
|
: formatVal(data[el], el, { elements: params }, kind)
|
|
@@ -180,7 +181,6 @@ const resolvedTargetOfQuery = q => {
|
|
|
180
181
|
}
|
|
181
182
|
|
|
182
183
|
let logged
|
|
183
|
-
let sdkLoggerDisabled
|
|
184
184
|
|
|
185
185
|
const _resolveSelectionStrategy = options => {
|
|
186
186
|
if (typeof options?.selectionStrategy !== 'string') return
|
|
@@ -228,22 +228,6 @@ class RemoteService extends cds.Service {
|
|
|
228
228
|
'Configuration option "cds.env.features.fetch_csrf" is deprecated.\n Please use "csrf"/"csrfInBatch" as described in https://cap.cloud.sap/docs/node.js/remote-services'
|
|
229
229
|
)
|
|
230
230
|
}
|
|
231
|
-
|
|
232
|
-
// REVISIT: use cds.log's logger in cloud sdk
|
|
233
|
-
|
|
234
|
-
// disable sdk logger if not in debug mode
|
|
235
|
-
if (!LOG._debug && !sdkLoggerDisabled) {
|
|
236
|
-
try {
|
|
237
|
-
// eslint-disable-next-line cds/no-missing-dependencies -- needs to be added by app dev
|
|
238
|
-
const sdkUtils = require('@sap-cloud-sdk/util')
|
|
239
|
-
sdkUtils.setGlobalLogLevel('error')
|
|
240
|
-
|
|
241
|
-
// disable sdk logger once
|
|
242
|
-
sdkLoggerDisabled = true
|
|
243
|
-
} catch (err) {
|
|
244
|
-
/* might fail in cds repl due to winston's exception handler, see cap/issues#10134 */
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
231
|
} else if ([...this.entities].length || [...this.operations].length) {
|
|
248
232
|
throw new Error(`No credentials configured for "${this.name}".`)
|
|
249
233
|
}
|
|
@@ -427,13 +427,13 @@ const _stringToReqOptions = (query, data, target) => {
|
|
|
427
427
|
return reqOptions
|
|
428
428
|
}
|
|
429
429
|
|
|
430
|
-
const _pathToReqOptions = (method, path, data, target,
|
|
430
|
+
const _pathToReqOptions = (method, path, data, target, srvName) => {
|
|
431
431
|
let url = path
|
|
432
432
|
if (!url.startsWith('/')) {
|
|
433
433
|
// extract entity name and instance identifier (either in "()" or after "/") from fully qualified path
|
|
434
434
|
const parts = path.match(/([\w.]*)([\W.]*)(.*)/)
|
|
435
435
|
if (!parts) url = '/' + path.match(/\w*$/)[0]
|
|
436
|
-
else if (url.startsWith(
|
|
436
|
+
else if (url.startsWith(srvName)) url = '/' + parts[1].replace(srvName + '.', '') + parts[2] + parts[3]
|
|
437
437
|
else url = '/' + parts[1].match(/\w*$/)[0] + parts[2] + parts[3]
|
|
438
438
|
|
|
439
439
|
// normalize in case parts[2] already starts with /
|
|
@@ -460,7 +460,7 @@ const getReqOptions = (req, query, service) => {
|
|
|
460
460
|
? _cqnToReqOptions(query, service, req)
|
|
461
461
|
: typeof query === 'string'
|
|
462
462
|
? _stringToReqOptions(query, req.data, req.target)
|
|
463
|
-
: _pathToReqOptions(req.method, req.path, req.data, req.target, service.
|
|
463
|
+
: _pathToReqOptions(req.method, req.path, req.data, req.target, service.name)
|
|
464
464
|
|
|
465
465
|
if (service.kind === 'odata-v2' && req.event === 'READ' && reqOptions.url?.match(/(\/any\()|(\/all\()/)) {
|
|
466
466
|
req.reject(501, 'Lambda expressions are not supported in OData v2')
|
|
@@ -66,12 +66,9 @@ const _convertActionFuncResponse = (returnType, convertValueFn) => data => {
|
|
|
66
66
|
|
|
67
67
|
// eslint-disable-next-line complexity
|
|
68
68
|
const _convertValue = (ieee754Compatible, exponentialDecimals) => (value, element) => {
|
|
69
|
-
if (value == null)
|
|
70
|
-
return value
|
|
71
|
-
}
|
|
69
|
+
if (value == null) return value
|
|
72
70
|
|
|
73
71
|
const type = _elementType(element)
|
|
74
|
-
|
|
75
72
|
if (type === 'cds.Boolean') {
|
|
76
73
|
if (value === 'true') {
|
|
77
74
|
value = true
|
|
@@ -119,18 +116,19 @@ const _convertValue = (ieee754Compatible, exponentialDecimals) => (value, elemen
|
|
|
119
116
|
|
|
120
117
|
return value
|
|
121
118
|
}
|
|
119
|
+
|
|
122
120
|
const _PT = ([hh, mm, ss]) => `PT${hh}H${mm}M${ss}S`
|
|
123
121
|
|
|
124
122
|
const _convertPayloadValue = (value, element) => {
|
|
125
|
-
|
|
123
|
+
if (value == null) return value
|
|
126
124
|
|
|
127
125
|
// see https://www.odata.org/documentation/odata-version-2-0/json-format/
|
|
128
|
-
|
|
126
|
+
const type = _elementType(element)
|
|
129
127
|
switch (type) {
|
|
130
128
|
case 'cds.Time':
|
|
131
129
|
return value.match(/^(PT)([H,M,S,0-9])*$/) ? value : _PT(value.split(':'))
|
|
132
130
|
case 'cds.Decimal':
|
|
133
|
-
return typeof value === 'string' ? value :
|
|
131
|
+
return typeof value === 'string' ? value : `${value}`
|
|
134
132
|
case 'cds.Date':
|
|
135
133
|
case 'cds.DateTime':
|
|
136
134
|
return `/Date(${new Date(value).getTime()})/`
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
const stack = []
|
|
34
34
|
let SELECT, count
|
|
35
35
|
const TECHNICAL_OPTS = ['$value'] // odata parts to be handled somewhere else
|
|
36
|
-
// we keep that here to allow for usage in https://
|
|
36
|
+
// we keep that here to allow for usage in https://peggyjs.org/online
|
|
37
37
|
const safeNumber =
|
|
38
38
|
options.safeNumber ||
|
|
39
39
|
function (str) {
|