@sap/cds 7.2.0 → 7.3.0
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 +174 -126
- package/README.md +1 -1
- package/apis/connect.d.ts +1 -1
- package/apis/core.d.ts +6 -4
- package/apis/serve.d.ts +1 -1
- package/apis/services.d.ts +51 -31
- package/apis/test.d.ts +24 -10
- package/bin/serve.js +4 -3
- package/common.cds +4 -4
- 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/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 +1 -1
- 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/generic/paging.js +1 -0
- package/libx/_runtime/common/i18n/messages.properties +1 -0
- 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 +23 -24
- 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 +11 -26
- package/libx/_runtime/remote/utils/client.js +3 -2
- 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
|
@@ -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
|
}
|
|
@@ -83,6 +83,7 @@ CRUD_VIA_NAVIGATION_NOT_SUPPORTED=CRUD via navigations is not yet supported
|
|
|
83
83
|
|
|
84
84
|
# draft
|
|
85
85
|
DRAFT_ALREADY_EXISTS=A draft for this entity already exists
|
|
86
|
+
DRAFT_NOT_EXISTING=No draft for this entity exists
|
|
86
87
|
DRAFT_LOCKED_BY_ANOTHER_USER=The entity is locked by user "{0}"
|
|
87
88
|
DRAFT_MODIFICATION_ONLY_VIA_ROOT=A draft can only be modified via its root entity
|
|
88
89
|
|
|
@@ -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
|
|
|
@@ -48,6 +48,10 @@ const _promiseAll = async array => {
|
|
|
48
48
|
|
|
49
49
|
const _isCount = query => query.SELECT.columns?.length === 1 && query.SELECT.columns[0].func === 'count'
|
|
50
50
|
|
|
51
|
+
const entity_keys = e => {
|
|
52
|
+
return Object_keys(e.keys).filter(k => k !== 'IsActiveEntity' && !e.keys[k].isAssociation)
|
|
53
|
+
}
|
|
54
|
+
|
|
51
55
|
const _inProcessByUserXpr = lockShiftedNow => ({
|
|
52
56
|
xpr: [
|
|
53
57
|
'case',
|
|
@@ -257,7 +261,7 @@ cds.ApplicationService.prototype.handle = async function (req) {
|
|
|
257
261
|
{ ref: ['DraftAdministrativeData'], expand: [{ ref: ['InProcessByUser'] }] }
|
|
258
262
|
])
|
|
259
263
|
)
|
|
260
|
-
if (!res) req.reject(_etagValidationType ? 412 : 404)
|
|
264
|
+
if (!res) req.reject(_etagValidationType ? 412 : { code: 'DRAFT_NOT_EXISTING', status: 404 })
|
|
261
265
|
if (res.DraftAdministrativeData?.InProcessByUser !== cds.context.user.id)
|
|
262
266
|
req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER', [res.DraftAdministrativeData?.InProcessByUser])
|
|
263
267
|
const DraftAdministrativeData_DraftUUID = res.DraftAdministrativeData_DraftUUID
|
|
@@ -265,11 +269,10 @@ cds.ApplicationService.prototype.handle = async function (req) {
|
|
|
265
269
|
delete res.DraftAdministrativeData
|
|
266
270
|
const HasActiveEntity = res.HasActiveEntity
|
|
267
271
|
delete res.HasActiveEntity
|
|
268
|
-
delete res.IsActiveEntity
|
|
269
272
|
// First run the handlers as they might need access to DraftAdministrativeData or the draft entities
|
|
270
273
|
const result = await run(
|
|
271
274
|
HasActiveEntity ? UPDATE(req.target).data(res).where(targetWhere) : INSERT.into(req.target).entries(res),
|
|
272
|
-
{ headers: { 'if-match': '*' } }
|
|
275
|
+
{ headers: Object.assign({}, req.headers, { 'if-match': '*' }) }
|
|
273
276
|
)
|
|
274
277
|
await _promiseAll([
|
|
275
278
|
DELETE.from(targetDraft).where(targetWhere),
|
|
@@ -287,9 +290,7 @@ cds.ApplicationService.prototype.handle = async function (req) {
|
|
|
287
290
|
if (req.target.actions?.[req.event] && draftParams.IsActiveEntity === false) {
|
|
288
291
|
if (query.SELECT?.from?.ref) query.SELECT.from.ref = _redirectRefToDrafts(query.SELECT.from.ref, this.model)
|
|
289
292
|
const rootQuery = query.clone()
|
|
290
|
-
const columns =
|
|
291
|
-
.filter(k => k !== 'IsActiveEntity')
|
|
292
|
-
.map(k => ({ ref: [k] }))
|
|
293
|
+
const columns = entity_keys(query._target).map(k => ({ ref: [k] }))
|
|
293
294
|
columns.push({ ref: ['DraftAdministrativeData'], expand: [{ ref: ['InProcessByUser'] }] })
|
|
294
295
|
rootQuery.SELECT.columns = columns
|
|
295
296
|
rootQuery.SELECT.one = true
|
|
@@ -377,7 +378,7 @@ const Read = {
|
|
|
377
378
|
if (query._target.name.endsWith('.DraftAdministrativeData')) return run(query._drafts)
|
|
378
379
|
if (!query._target._isDraftEnabled) return run(query)
|
|
379
380
|
if (!query.SELECT.groupBy && query.SELECT.columns && !query.SELECT.columns.some(c => c === '*')) {
|
|
380
|
-
const keys =
|
|
381
|
+
const keys = entity_keys(query._target)
|
|
381
382
|
for (const key of keys) {
|
|
382
383
|
if (!query.SELECT.columns.some(c => c.ref?.[0] === key)) query.SELECT.columns.push({ ref: [key] })
|
|
383
384
|
}
|
|
@@ -409,11 +410,10 @@ const Read = {
|
|
|
409
410
|
unchanged: async function (run, query) {
|
|
410
411
|
LOG.debug('List Editing Status: Unchanged')
|
|
411
412
|
const draftsQuery = query._drafts
|
|
412
|
-
const keys = Object_keys(query._target.keys).filter(k => k !== 'IsActiveEntity')
|
|
413
413
|
draftsQuery.SELECT.count = undefined
|
|
414
|
-
draftsQuery.SELECT.limit = undefined
|
|
415
414
|
draftsQuery.SELECT.orderBy = undefined
|
|
416
|
-
draftsQuery.SELECT.
|
|
415
|
+
draftsQuery.SELECT.limit = false
|
|
416
|
+
draftsQuery.SELECT.columns = entity_keys(query._target).map(k => ({ ref: [k] }))
|
|
417
417
|
|
|
418
418
|
const drafts = await draftsQuery.where({ HasActiveEntity: true })
|
|
419
419
|
const res = await Read.onlyActives(run, query.where(Read.whereNotIn(query._target, drafts)), {
|
|
@@ -457,11 +457,10 @@ const Read = {
|
|
|
457
457
|
LOG.debug('List Editing Status: All')
|
|
458
458
|
if (!query._drafts) return []
|
|
459
459
|
query._drafts.SELECT.count = false
|
|
460
|
-
query._drafts.SELECT.limit =
|
|
460
|
+
query._drafts.SELECT.limit = false // We need all entries for the keys to properly select actives (count)
|
|
461
461
|
const isCount = _isCount(query._drafts)
|
|
462
462
|
if (isCount) {
|
|
463
|
-
|
|
464
|
-
query._drafts.SELECT.columns = keys.map(k => ({ ref: [k] }))
|
|
463
|
+
query._drafts.SELECT.columns = entity_keys(query._target).map(k => ({ ref: [k] }))
|
|
465
464
|
}
|
|
466
465
|
if (!query._drafts.SELECT.columns) query._drafts.SELECT.columns = ['*']
|
|
467
466
|
if (!query._drafts.SELECT.columns.some(c => c.ref?.[0] === 'HasActiveEntity'))
|
|
@@ -525,13 +524,14 @@ const Read = {
|
|
|
525
524
|
},
|
|
526
525
|
activesFromDrafts: async function (run, query, { isLocked = true }) {
|
|
527
526
|
const draftsQuery = query._drafts
|
|
528
|
-
const keys = Object_keys(query._target.keys).filter(k => k !== 'IsActiveEntity')
|
|
529
527
|
const additionalCols = draftsQuery.SELECT.columns
|
|
530
528
|
? draftsQuery.SELECT.columns.filter(
|
|
531
529
|
c => c.ref && ['DraftAdministrativeData', 'DraftAdministrativeData_DraftUUID'].includes(c.ref[0])
|
|
532
530
|
)
|
|
533
531
|
: [{ ref: ['DraftAdministrativeData_DraftUUID'] }]
|
|
534
|
-
draftsQuery.SELECT.columns =
|
|
532
|
+
draftsQuery.SELECT.columns = entity_keys(query._target)
|
|
533
|
+
.map(k => ({ ref: [k] }))
|
|
534
|
+
.concat(additionalCols)
|
|
535
535
|
draftsQuery.where({
|
|
536
536
|
HasActiveEntity: true,
|
|
537
537
|
'DraftAdministrativeData.InProcessByUser': { '!=': cds.context.user.id },
|
|
@@ -560,7 +560,7 @@ const Read = {
|
|
|
560
560
|
},
|
|
561
561
|
whereNotIn: (target, data) => Read.whereIn(target, data, true),
|
|
562
562
|
whereIn: (target, data, not = false) => {
|
|
563
|
-
const keys =
|
|
563
|
+
const keys = entity_keys(target)
|
|
564
564
|
const dataArray = data ? (Array.isArray(data) ? data : [data]) : []
|
|
565
565
|
if (not && !dataArray.length) return []
|
|
566
566
|
const left = { list: keys.map(k => ({ ref: [k] })) }
|
|
@@ -573,9 +573,7 @@ const Read = {
|
|
|
573
573
|
if (!actives.length) return []
|
|
574
574
|
const drafts = cds.ql.clone(query._drafts)
|
|
575
575
|
drafts.SELECT.where = Read.whereIn(query._target, actives)
|
|
576
|
-
const newColumns =
|
|
577
|
-
.filter(k => k !== 'IsActiveEntity')
|
|
578
|
-
.map(k => ({ ref: [k] }))
|
|
576
|
+
const newColumns = entity_keys(query._target).map(k => ({ ref: [k] }))
|
|
579
577
|
if (
|
|
580
578
|
!drafts.SELECT.columns ||
|
|
581
579
|
drafts.SELECT.columns.some(c => c === '*' || c.ref?.[0] === 'DraftAdministrativeData_DraftUUID')
|
|
@@ -594,8 +592,10 @@ const Read = {
|
|
|
594
592
|
// Indexes the data for fast key access
|
|
595
593
|
const dataArray = Read._makeArray(data)
|
|
596
594
|
if (!dataArray.length) return
|
|
597
|
-
const
|
|
598
|
-
|
|
595
|
+
const hash = row =>
|
|
596
|
+
entity_keys(target)
|
|
597
|
+
.map(k => row[k])
|
|
598
|
+
.reduce((res, curr) => res + '|$|' + curr, '')
|
|
599
599
|
const hashMap = new Map()
|
|
600
600
|
for (const row of dataArray) hashMap.set(hash(row), row)
|
|
601
601
|
return { hashMap, hash }
|
|
@@ -1053,7 +1053,7 @@ async function onCancel(req) {
|
|
|
1053
1053
|
)
|
|
1054
1054
|
else
|
|
1055
1055
|
queries.push(
|
|
1056
|
-
UPDATE('
|
|
1056
|
+
UPDATE('DRAFT.DraftAdministrativeData')
|
|
1057
1057
|
.data({
|
|
1058
1058
|
InProcessByUser: cds.context.user.id,
|
|
1059
1059
|
LastChangedByUser: cds.context.user.id,
|
|
@@ -1074,12 +1074,11 @@ async function onPrepare(req) {
|
|
|
1074
1074
|
}
|
|
1075
1075
|
const where = req.query.SELECT.from.ref[0].where
|
|
1076
1076
|
|
|
1077
|
-
const keys = Object_keys(req.target.keys).filter(k => k !== 'IsActiveEntity')
|
|
1078
1077
|
const draftQuery = SELECT.one
|
|
1079
1078
|
.from(req.target, d => {
|
|
1080
1079
|
d.DraftAdministrativeData(a => a.InProcessByUser)
|
|
1081
1080
|
})
|
|
1082
|
-
.columns(
|
|
1081
|
+
.columns(entity_keys(req.target))
|
|
1083
1082
|
.where(where)
|
|
1084
1083
|
draftQuery[DRAFT_PARAMS] = draftParams
|
|
1085
1084
|
const data = await draftQuery
|
|
@@ -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 {
|