@sap/cds 6.7.1 → 6.8.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 +50 -1
- package/_i18n/i18n.properties +9 -6
- package/_i18n/i18n_ar.properties +6 -6
- package/_i18n/i18n_cs.properties +6 -6
- package/_i18n/i18n_da.properties +6 -6
- package/_i18n/i18n_de.properties +6 -6
- package/_i18n/i18n_en.properties +6 -6
- package/_i18n/i18n_es.properties +6 -6
- package/_i18n/i18n_fi.properties +6 -6
- package/_i18n/i18n_fr.properties +6 -6
- package/_i18n/i18n_hu.properties +6 -6
- package/_i18n/i18n_it.properties +6 -6
- package/_i18n/i18n_ja.properties +6 -6
- package/_i18n/i18n_ko.properties +6 -6
- package/_i18n/i18n_ms.properties +6 -6
- package/_i18n/i18n_nl.properties +6 -6
- package/_i18n/i18n_no.properties +6 -6
- package/_i18n/i18n_pl.properties +6 -6
- package/_i18n/i18n_pt.properties +6 -6
- package/_i18n/i18n_ro.properties +6 -6
- package/_i18n/i18n_ru.properties +6 -6
- package/_i18n/i18n_sv.properties +6 -6
- package/_i18n/i18n_th.properties +6 -6
- package/_i18n/i18n_tr.properties +8 -8
- package/_i18n/i18n_zh_CN.properties +3 -3
- package/_i18n/i18n_zh_TW.properties +6 -6
- package/apis/core.d.ts +30 -31
- package/apis/csn.d.ts +1 -1
- package/apis/ql.d.ts +69 -39
- package/apis/serve.d.ts +4 -3
- package/apis/services.d.ts +20 -7
- package/bin/build/buildTaskEngine.js +1 -1
- package/bin/build/index.js +1 -1
- package/bin/build/provider/buildTaskProviderInternal.js +9 -6
- package/bin/build/provider/hana/index.js +11 -4
- package/bin/build/provider/mtx-sidecar/index.js +3 -3
- package/bin/build/provider/nodejs/index.js +23 -0
- package/bin/version.js +3 -2
- package/common.cds +3 -2
- package/lib/auth/index.js +3 -0
- package/lib/auth/mocked-users.js +13 -0
- package/lib/compile/etc/_localized.js +3 -0
- package/lib/core/entities.js +7 -3
- package/lib/dbs/cds-deploy.js +36 -12
- package/lib/env/cds-env.js +47 -14
- package/lib/env/cds-requires.js +16 -7
- package/lib/env/defaults.js +2 -2
- package/lib/env/schemas/cds-rc.json +1 -8
- package/lib/index.js +1 -1
- package/lib/ql/STREAM.js +89 -0
- package/lib/ql/cds-ql.js +2 -1
- package/lib/req/request.js +5 -2
- package/lib/req/user.js +1 -1
- package/lib/srv/middlewares/index.js +9 -7
- package/lib/srv/middlewares/trace.js +6 -5
- package/lib/srv/srv-api.js +1 -0
- package/lib/utils/cds-utils.js +1 -1
- package/lib/utils/tar.js +30 -31
- package/libx/_runtime/audit/Service.js +96 -37
- package/libx/_runtime/audit/generic/personal/utils.js +26 -13
- package/libx/_runtime/audit/utils/v2.js +21 -22
- package/libx/_runtime/cds-services/adapter/odata-v4/OData.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/create.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +7 -6
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/batch/BatchProcessor.js +2 -0
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/handlerUtils.js +2 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +10 -3
- package/libx/_runtime/cds-services/services/Service.js +2 -7
- package/libx/_runtime/cds-services/services/utils/differ.js +1 -1
- package/libx/_runtime/common/aspects/any.js +1 -1
- package/libx/_runtime/common/generic/auth/utils.js +30 -41
- package/libx/_runtime/common/generic/input.js +14 -8
- package/libx/_runtime/common/i18n/messages.properties +1 -1
- package/libx/_runtime/db/expand/expandCQNToJoin.js +19 -17
- package/libx/_runtime/db/expand/rawToExpanded.js +3 -5
- package/libx/_runtime/db/utils/generateAliases.js +1 -1
- package/libx/_runtime/fiori/generic/activate.js +1 -1
- package/libx/_runtime/fiori/generic/before.js +18 -19
- package/libx/_runtime/fiori/generic/prepare.js +1 -1
- package/libx/_runtime/fiori/generic/read.js +1 -1
- package/libx/_runtime/fiori/lean-draft.js +27 -21
- package/libx/_runtime/fiori/utils/handler.js +0 -6
- package/libx/_runtime/hana/customBuilder/CustomFunctionBuilder.js +1 -1
- package/libx/_runtime/hana/customBuilder/CustomSelectBuilder.js +0 -5
- package/libx/_runtime/hana/pool.js +26 -18
- package/libx/_runtime/hana/search2Contains.js +1 -1
- package/libx/_runtime/hana/search2cqn4sql.js +26 -18
- package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +23 -16
- package/libx/_runtime/messaging/outbox/utils.js +6 -1
- package/libx/_runtime/remote/Service.js +64 -38
- package/libx/_runtime/remote/utils/client.js +13 -9
- package/libx/rest/middleware/read.js +2 -1
- package/package.json +1 -1
|
@@ -40,54 +40,51 @@ const _getCurrentSubClause = (next, restrict) => {
|
|
|
40
40
|
const escaped = next[0].replace(/\$/g, '\\$').replace(/\./g, '\\.')
|
|
41
41
|
const re1 = new RegExp(`([\\w\\.']*)\\s*=\\s*(${escaped})|(${escaped})\\s*=\\s*([\\w\\.']*)`)
|
|
42
42
|
const re2 = new RegExp(`([\\w\\.']*)\\s*in\\s*(${escaped})|(${escaped})\\s*in\\s*([\\w\\.']*)`)
|
|
43
|
-
const
|
|
43
|
+
const re3 = new RegExp(`(${escaped})\\s*is\\s*null`)
|
|
44
|
+
const re4 = new RegExp(`(${escaped})\\s*is\\s*not\\s*null`)
|
|
45
|
+
const clause =
|
|
46
|
+
restrict.where.match(re3) || restrict.where.match(re4) || restrict.where.match(re1) || restrict.where.match(re2)
|
|
44
47
|
|
|
45
48
|
if (clause) return clause
|
|
46
49
|
|
|
47
50
|
// NOTE: arrayed attr with "=" as operator is some kind of legacy case
|
|
48
|
-
throw new Error('user attribute array must be used with operator "=" or "
|
|
51
|
+
throw new Error('user attribute array must be used with operator "=", "in", "is null", or "is not null"')
|
|
49
52
|
}
|
|
50
53
|
|
|
51
|
-
const
|
|
54
|
+
const _isNull = (userAttrs, attr) =>
|
|
55
|
+
userAttrs[attr] == null || (Array.isArray(userAttrs[attr]) && userAttrs[attr].length === 0)
|
|
56
|
+
const _isNotNull = (userAttrs, attr) =>
|
|
57
|
+
userAttrs[attr] != null && Array.isArray(userAttrs[attr]) && userAttrs[attr].length > 0
|
|
58
|
+
|
|
59
|
+
const _processUserAttr = (next, restrict, userAttrs, attr) => {
|
|
52
60
|
const clause = _getCurrentSubClause(next, restrict)
|
|
53
61
|
const valOrRef = clause[1] || clause[4]
|
|
54
62
|
|
|
55
|
-
if (clause[0].match(/
|
|
56
|
-
|
|
63
|
+
if (clause[0].match(/ is\s*null/)) {
|
|
64
|
+
restrict.where = restrict.where.replace(clause[0], _isNull(userAttrs, attr) ? '1 = 1' : '1 = 2')
|
|
65
|
+
} else if (clause[0].match(/ is\s*not\s*null/)) {
|
|
66
|
+
restrict.where = restrict.where.replace(clause[0], _isNotNull(userAttrs, attr) ? '1 = 1' : '1 = 2')
|
|
67
|
+
} else {
|
|
68
|
+
if (_isNull(userAttrs, attr)) {
|
|
57
69
|
restrict.where = restrict.where.replace(clause[0], '1 = 2')
|
|
58
|
-
} else if (
|
|
59
|
-
|
|
70
|
+
} else if (clause[0].match(/ in /)) {
|
|
71
|
+
if (userAttrs[attr].length === 1) {
|
|
72
|
+
restrict.where = restrict.where.replace(clause[0], `${valOrRef} = '${userAttrs[attr][0]}'`)
|
|
73
|
+
} else {
|
|
74
|
+
restrict.where = restrict.where.replace(
|
|
75
|
+
clause[0],
|
|
76
|
+
`${valOrRef} in (${userAttrs[attr].map(ele => `'${ele}'`).join(', ')})`
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
} else if (valOrRef.startsWith("'") && userAttrs[attr].includes(valOrRef.split("'")[1])) {
|
|
80
|
+
restrict.where = restrict.where.replace(clause[0], `${valOrRef} = ${valOrRef}`)
|
|
60
81
|
} else {
|
|
61
82
|
restrict.where = restrict.where.replace(
|
|
62
83
|
clause[0],
|
|
63
|
-
|
|
84
|
+
`(${userAttrs[attr].map(ele => `${valOrRef} = '${ele}'`).join(' or ')})`
|
|
64
85
|
)
|
|
65
86
|
}
|
|
66
|
-
} else if (valOrRef.startsWith("'") && user[attr].includes(valOrRef.split("'")[1])) {
|
|
67
|
-
restrict.where = restrict.where.replace(clause[0], `${valOrRef} = ${valOrRef}`)
|
|
68
|
-
} else {
|
|
69
|
-
restrict.where = restrict.where.replace(
|
|
70
|
-
clause[0],
|
|
71
|
-
`(${user[attr].map(ele => `${valOrRef} = '${ele}'`).join(' or ')})`
|
|
72
|
-
)
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const _getShortcut = (attrs, attr) => {
|
|
77
|
-
// undefined
|
|
78
|
-
if (attrs[attr] === undefined) {
|
|
79
|
-
return '1 = 2'
|
|
80
87
|
}
|
|
81
|
-
|
|
82
|
-
// $UNRESTRICTED
|
|
83
|
-
if (
|
|
84
|
-
(typeof attrs[attr] === 'string' && attrs[attr].match(/\$UNRESTRICTED/i)) ||
|
|
85
|
-
(Array.isArray(attrs[attr]) && attrs[attr].some(a => a.match(/\$UNRESTRICTED/i)))
|
|
86
|
-
) {
|
|
87
|
-
return '1 = 1'
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
return null
|
|
91
88
|
}
|
|
92
89
|
|
|
93
90
|
/*
|
|
@@ -120,15 +117,7 @@ const resolveUserAttrs = (restrict, req) => {
|
|
|
120
117
|
let attr = parts.shift()
|
|
121
118
|
|
|
122
119
|
while (attr) {
|
|
123
|
-
|
|
124
|
-
if (shortcut) {
|
|
125
|
-
const clause = _getCurrentSubClause(next, restrict)
|
|
126
|
-
restrict.where = restrict.where.replace(clause[0], shortcut)
|
|
127
|
-
skip = true
|
|
128
|
-
break
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
if (Array.isArray(attrs[attr])) {
|
|
120
|
+
if (attrs[attr] === undefined || Array.isArray(attrs[attr])) {
|
|
132
121
|
_processUserAttr(next, restrict, attrs, attr)
|
|
133
122
|
skip = true
|
|
134
123
|
break
|
|
@@ -47,8 +47,12 @@ const _isStreamingProperty = (elements, row, property) =>
|
|
|
47
47
|
element => element['@Core.MediaType'] && element['@Core.MediaType']['='] === property && row[element.name]
|
|
48
48
|
)
|
|
49
49
|
|
|
50
|
-
const _getMediaTypeValue =
|
|
51
|
-
|
|
50
|
+
const _getMediaTypeValue = () => {
|
|
51
|
+
const ctx = cds.context
|
|
52
|
+
return (
|
|
53
|
+
!ctx?.http?.req?.headers?.['content-type']?.match(/json|multipart/i) && ctx?.http?.req?.headers?.['content-type']
|
|
54
|
+
)
|
|
55
|
+
}
|
|
52
56
|
|
|
53
57
|
const _preProcessAssertTarget = (assocInfo, assertMap) => {
|
|
54
58
|
const { element: assoc, row } = assocInfo
|
|
@@ -145,9 +149,12 @@ const _processCategory = (req, category, value, elementInfo, assertMap) => {
|
|
|
145
149
|
}
|
|
146
150
|
|
|
147
151
|
// set media type from content-type header if streaming
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
152
|
+
if (category === 'stream') {
|
|
153
|
+
if (_isStreamingProperty(element.parent.elements, row, key)) {
|
|
154
|
+
const mtValue = _getMediaTypeValue()
|
|
155
|
+
if (mtValue) row[key] = mtValue
|
|
156
|
+
}
|
|
157
|
+
}
|
|
151
158
|
}
|
|
152
159
|
|
|
153
160
|
const _getProcessorFn = (req, errors, assertMap) => {
|
|
@@ -219,7 +226,7 @@ const _callError = (req, errors) => {
|
|
|
219
226
|
for (const error of errors) req.error(error)
|
|
220
227
|
}
|
|
221
228
|
|
|
222
|
-
const _getBoundAction = req => req.target.actions?.[req.context?.event]
|
|
229
|
+
const _getBoundAction = req => req.target.actions?.[req._?.event || req.context?.event]
|
|
223
230
|
const _getBoundActionBindingParameter = action => action['@cds.odata.bindingparameter.name'] || 'in'
|
|
224
231
|
|
|
225
232
|
async function commonGenericInput(req) {
|
|
@@ -248,9 +255,8 @@ async function commonGenericInput(req) {
|
|
|
248
255
|
|
|
249
256
|
if (boundAction) {
|
|
250
257
|
const pathSegment = _getBoundActionBindingParameter(boundAction)
|
|
251
|
-
const keys = req._ && req._.params && req._.params[0]
|
|
252
258
|
if (pathSegment) pathOptions.pathSegmentsInfo.push(pathSegment)
|
|
253
|
-
|
|
259
|
+
const keys = req._?.params?.[0]
|
|
254
260
|
if (keys && 'IsActiveEntity' in keys) {
|
|
255
261
|
pathOptions.draftKeys = { IsActiveEntity: keys.IsActiveEntity }
|
|
256
262
|
}
|
|
@@ -85,7 +85,7 @@ CRUD_VIA_NAVIGATION_NOT_SUPPORTED=CRUD via navigations is not yet supported
|
|
|
85
85
|
|
|
86
86
|
# draft
|
|
87
87
|
DRAFT_ALREADY_EXISTS=A draft for this entity already exists
|
|
88
|
-
DRAFT_LOCKED_BY_ANOTHER_USER=The entity is locked by
|
|
88
|
+
DRAFT_LOCKED_BY_ANOTHER_USER=The entity is locked by user "{0}"
|
|
89
89
|
DRAFT_MODIFICATION_ONLY_VIA_ROOT=A draft can only be modified via its root entity
|
|
90
90
|
|
|
91
91
|
# singleton
|
|
@@ -66,7 +66,6 @@ class JoinCQNFromExpanded {
|
|
|
66
66
|
|
|
67
67
|
// Get first level of expanding regarding to many and all to one if not part of a nested to many expand.
|
|
68
68
|
this._createJoinCQNFromExpanded(this._SELECT, [])
|
|
69
|
-
|
|
70
69
|
return this
|
|
71
70
|
}
|
|
72
71
|
|
|
@@ -117,10 +116,10 @@ class JoinCQNFromExpanded {
|
|
|
117
116
|
*/
|
|
118
117
|
_createJoinCQNFromExpanded(SELECT, toManyTree, defaultLanguage) {
|
|
119
118
|
const joinArgs = SELECT.from.args
|
|
120
|
-
const isJoinOfTwoSelects = joinArgs
|
|
119
|
+
const isJoinOfTwoSelects = joinArgs?.every(a => a.SELECT)
|
|
121
120
|
|
|
122
121
|
const unionTableRef = this._getUnionTable(SELECT)
|
|
123
|
-
const unionTable = unionTableRef
|
|
122
|
+
const unionTable = unionTableRef?.table
|
|
124
123
|
const tableAlias = this._getTableAlias(SELECT, toManyTree)
|
|
125
124
|
|
|
126
125
|
const readToOneCQN = this._getReadToOneCQN(SELECT, isJoinOfTwoSelects ? 'filterExpand' : tableAlias)
|
|
@@ -178,7 +177,8 @@ class JoinCQNFromExpanded {
|
|
|
178
177
|
* @private
|
|
179
178
|
*/
|
|
180
179
|
_getTableAlias(SELECT, toManyTree) {
|
|
181
|
-
|
|
180
|
+
const ref = this._getRef(SELECT)
|
|
181
|
+
return this._createAlias(toManyTree.length === 0 ? ref.table : toManyTree.join(':'), ref.as)
|
|
182
182
|
}
|
|
183
183
|
|
|
184
184
|
_getRef(SELECT) {
|
|
@@ -212,15 +212,21 @@ class JoinCQNFromExpanded {
|
|
|
212
212
|
* Create an alias from value.
|
|
213
213
|
*
|
|
214
214
|
* @param {string} value
|
|
215
|
+
* @param {string} [alias]
|
|
215
216
|
* @returns {string}
|
|
216
217
|
* @private
|
|
217
218
|
*/
|
|
218
|
-
_createAlias(value) {
|
|
219
|
+
_createAlias(value, alias) {
|
|
219
220
|
if (!this._aliases) {
|
|
220
221
|
this._aliases = {}
|
|
221
222
|
}
|
|
222
223
|
|
|
223
224
|
if (!this._aliases[value]) {
|
|
225
|
+
if (alias) {
|
|
226
|
+
this._aliases[value] = alias
|
|
227
|
+
return alias
|
|
228
|
+
}
|
|
229
|
+
|
|
224
230
|
const aliasNum = Object.keys(this._aliases).length
|
|
225
231
|
|
|
226
232
|
if (aliasNum < 26) {
|
|
@@ -319,6 +325,7 @@ class JoinCQNFromExpanded {
|
|
|
319
325
|
list: element.list.map(element => this._checkOrderByWhereElementRecursive(cqn, element, tableAlias))
|
|
320
326
|
})
|
|
321
327
|
}
|
|
328
|
+
|
|
322
329
|
return this._checkOrderByWhereElementRecursive(cqn, element, tableAlias)
|
|
323
330
|
}
|
|
324
331
|
|
|
@@ -333,9 +340,7 @@ class JoinCQNFromExpanded {
|
|
|
333
340
|
*/
|
|
334
341
|
_adaptWhereOrderBy(cqn, tableAlias) {
|
|
335
342
|
if (cqn.where) {
|
|
336
|
-
cqn.where = cqn.where.map(element =>
|
|
337
|
-
return this._adaptWhereElement(element, cqn, tableAlias)
|
|
338
|
-
})
|
|
343
|
+
cqn.where = cqn.where.map(element => this._adaptWhereElement(element, cqn, tableAlias))
|
|
339
344
|
}
|
|
340
345
|
|
|
341
346
|
if (cqn.having) {
|
|
@@ -343,15 +348,11 @@ class JoinCQNFromExpanded {
|
|
|
343
348
|
}
|
|
344
349
|
|
|
345
350
|
if (cqn.orderBy) {
|
|
346
|
-
cqn.orderBy = cqn.orderBy.map(element =>
|
|
347
|
-
return this._checkOrderByWhereElementRecursive(cqn, element, tableAlias)
|
|
348
|
-
})
|
|
351
|
+
cqn.orderBy = cqn.orderBy.map(element => this._checkOrderByWhereElementRecursive(cqn, element, tableAlias))
|
|
349
352
|
}
|
|
350
353
|
|
|
351
354
|
if (cqn.groupBy) {
|
|
352
|
-
cqn.groupBy = cqn.groupBy.map(element =>
|
|
353
|
-
return this._checkOrderByWhereElementRecursive(cqn, element, tableAlias)
|
|
354
|
-
})
|
|
355
|
+
cqn.groupBy = cqn.groupBy.map(element => this._checkOrderByWhereElementRecursive(cqn, element, tableAlias))
|
|
355
356
|
}
|
|
356
357
|
|
|
357
358
|
return cqn
|
|
@@ -393,7 +394,7 @@ class JoinCQNFromExpanded {
|
|
|
393
394
|
element.xpr = element.xpr.map(nestedElement => {
|
|
394
395
|
return this._checkOrderByWhereElementRecursive(cqn, nestedElement, tableAlias)
|
|
395
396
|
})
|
|
396
|
-
} else if (element.SELECT
|
|
397
|
+
} else if (element.SELECT?.where) {
|
|
397
398
|
element = {
|
|
398
399
|
SELECT: Object.assign({}, element.SELECT, {
|
|
399
400
|
where: this._adaptWhereSELECT(this._getRef(cqn), element.SELECT.where, tableAlias)
|
|
@@ -416,6 +417,7 @@ class JoinCQNFromExpanded {
|
|
|
416
417
|
if (element.xpr) {
|
|
417
418
|
return { xpr: this._adaptWhereSELECT(aliasedTable, element.xpr, tableAlias) }
|
|
418
419
|
}
|
|
420
|
+
|
|
419
421
|
return this._elementAliasNeedsReplacement(element, aliasedTable)
|
|
420
422
|
? Object.assign({}, element, { ref: [tableAlias, element.ref[1]] })
|
|
421
423
|
: element
|
|
@@ -847,7 +849,7 @@ class JoinCQNFromExpanded {
|
|
|
847
849
|
continue
|
|
848
850
|
}
|
|
849
851
|
|
|
850
|
-
if (arg.SELECT
|
|
852
|
+
if (arg.SELECT?.columns.some(column => column[IDENTIFIER])) {
|
|
851
853
|
return arg.SELECT.columns
|
|
852
854
|
}
|
|
853
855
|
|
|
@@ -1434,7 +1436,7 @@ class JoinCQNFromExpanded {
|
|
|
1434
1436
|
}
|
|
1435
1437
|
|
|
1436
1438
|
_getValueFromEntry(entry, parentAlias, key, struct) {
|
|
1437
|
-
let value = entry[key]
|
|
1439
|
+
let value = entry[key] ?? entry[key.toUpperCase()]
|
|
1438
1440
|
if (value === undefined) {
|
|
1439
1441
|
value = entry[`${parentAlias}_${key}`] || entry[`${parentAlias}_${key}`.toUpperCase()]
|
|
1440
1442
|
}
|
|
@@ -158,9 +158,7 @@ class RawToExpanded {
|
|
|
158
158
|
}
|
|
159
159
|
|
|
160
160
|
// No property holds any value. A to null must have failed.
|
|
161
|
-
if (isEntityNull)
|
|
162
|
-
return
|
|
163
|
-
}
|
|
161
|
+
if (isEntityNull) return
|
|
164
162
|
|
|
165
163
|
return row
|
|
166
164
|
}
|
|
@@ -175,10 +173,10 @@ class RawToExpanded {
|
|
|
175
173
|
*/
|
|
176
174
|
_isNull(isEntityNull, value, key) {
|
|
177
175
|
if (isEntityNull === undefined) {
|
|
178
|
-
return value
|
|
176
|
+
return value == null || key === 'IsActiveEntity'
|
|
179
177
|
}
|
|
180
178
|
|
|
181
|
-
return isEntityNull === true && (value
|
|
179
|
+
return isEntityNull === true && (value == null || key === 'IsActiveEntity')
|
|
182
180
|
}
|
|
183
181
|
|
|
184
182
|
/**
|
|
@@ -122,7 +122,7 @@ const fioriGenericActivate = async function (req) {
|
|
|
122
122
|
if (!draftData) req.reject(404)
|
|
123
123
|
if (adminData.InProcessByUser !== req.user.id) {
|
|
124
124
|
// REVISIT: security log?
|
|
125
|
-
req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER')
|
|
125
|
+
req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER', [adminData.InProcessByUser])
|
|
126
126
|
}
|
|
127
127
|
|
|
128
128
|
/*
|
|
@@ -4,7 +4,7 @@ const { SELECT } = cds.ql
|
|
|
4
4
|
|
|
5
5
|
const { isNavigationToMany } = require('../utils/req')
|
|
6
6
|
const { getKeysCondition, removeIsActiveEntityRecursively } = require('../utils/where')
|
|
7
|
-
const {
|
|
7
|
+
const { ensureNoDraftsSuffix, ensureDraftsSuffix, draftIsLocked } = require('../utils/handler')
|
|
8
8
|
|
|
9
9
|
const { DRAFT_COLUMNS_ADMIN_MAP } = require('../../common/constants/draft')
|
|
10
10
|
const { deepCopyArray } = require('../../common/utils/copy')
|
|
@@ -32,7 +32,7 @@ const _validateDraft = (req, draftResult, isBoundAction) => {
|
|
|
32
32
|
// user than the one who locked the entity and the configured drafts cancellation
|
|
33
33
|
// timeout timer has expired
|
|
34
34
|
if (draftIsLocked(draftAdminData.LastChangeDateTime)) {
|
|
35
|
-
req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER')
|
|
35
|
+
req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER', [draftAdminData.CreatedByUser])
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
// At this point, the request user ID isn't the owner of the draft.
|
|
@@ -60,16 +60,19 @@ const _getSelectDraftDataCqn = (entityName, where) => {
|
|
|
60
60
|
|
|
61
61
|
const _getRoot = req => {
|
|
62
62
|
if (!req.query) return
|
|
63
|
+
|
|
63
64
|
const refObj = req.query.SELECT?.from || req.query.UPDATE?.entity || req.query.INSERT?.into || req.query.DELETE?.from
|
|
65
|
+
const ref0 = refObj.ref[0]
|
|
66
|
+
|
|
64
67
|
const root = {
|
|
65
|
-
entityName: ensureDraftsSuffix(
|
|
66
|
-
where: removeIsActiveEntityRecursively(deepCopyArray(
|
|
68
|
+
entityName: ensureDraftsSuffix(ref0.id),
|
|
69
|
+
where: removeIsActiveEntityRecursively(deepCopyArray(ref0.where))
|
|
67
70
|
}
|
|
68
71
|
|
|
69
|
-
for (const item of
|
|
72
|
+
for (const item of ref0.where) {
|
|
70
73
|
if (item.ref && item.ref[item.ref.length - 1] === 'IsActiveEntity') {
|
|
71
|
-
const index =
|
|
72
|
-
root.IsActiveEntity =
|
|
74
|
+
const index = ref0.where.indexOf(item)
|
|
75
|
+
root.IsActiveEntity = ref0.where[index + 2].val
|
|
73
76
|
break
|
|
74
77
|
}
|
|
75
78
|
}
|
|
@@ -115,14 +118,12 @@ const _addDraftDataFromExistingDraft = async req => {
|
|
|
115
118
|
* Generic Handler for before NEW requests.
|
|
116
119
|
*/
|
|
117
120
|
const _new = async function (req) {
|
|
118
|
-
if (isDraftActivateAction(req)) return // REVISIT: How can NEW be draftActivate???
|
|
119
|
-
|
|
120
121
|
if (isNavigationToMany(req)) {
|
|
121
|
-
// REVISIT: How can NEW be a navigation to many?
|
|
122
122
|
const result = await _addDraftDataFromExistingDraft(req)
|
|
123
123
|
|
|
124
124
|
// in order to fix corner case where active subitems are created in draft case
|
|
125
125
|
if (result.length === 0) req.reject(404)
|
|
126
|
+
|
|
126
127
|
return
|
|
127
128
|
}
|
|
128
129
|
|
|
@@ -134,19 +135,17 @@ const _new = async function (req) {
|
|
|
134
135
|
/**
|
|
135
136
|
* Generic Handler for before PATCH and UPDATE requests.
|
|
136
137
|
*/
|
|
137
|
-
const
|
|
138
|
-
if (isDraftActivateAction(req)) return
|
|
139
|
-
|
|
138
|
+
const _patch = async function (req) {
|
|
140
139
|
const result = await _addDraftDataFromExistingDraft(req)
|
|
141
140
|
|
|
142
|
-
// means that the draft does not
|
|
141
|
+
// no result means that the draft does not exist
|
|
143
142
|
if (result.length === 0) req.reject(404)
|
|
144
143
|
}
|
|
145
144
|
|
|
146
145
|
/**
|
|
147
146
|
* Generic Handler for before DELETE and CANCEL requests.
|
|
148
147
|
*/
|
|
149
|
-
const
|
|
148
|
+
const _cancel = async function (req) {
|
|
150
149
|
await _addDraftDataFromExistingDraft(req)
|
|
151
150
|
}
|
|
152
151
|
|
|
@@ -181,12 +180,12 @@ const _registerBoundActionHandlers = function (entityName, actions) {
|
|
|
181
180
|
}
|
|
182
181
|
|
|
183
182
|
_new._initial = true
|
|
184
|
-
|
|
185
|
-
|
|
183
|
+
_patch._initial = true
|
|
184
|
+
_cancel._initial = true
|
|
186
185
|
|
|
187
186
|
module.exports = cds.service.impl((srv, entity) => {
|
|
188
187
|
srv.before('NEW', entity, _new)
|
|
189
|
-
srv.before(
|
|
190
|
-
srv.before(
|
|
188
|
+
srv.before('PATCH', entity, _patch)
|
|
189
|
+
srv.before('CANCEL', entity, _cancel)
|
|
191
190
|
_registerBoundActionHandlers.call(srv, entity.name, entity.actions)
|
|
192
191
|
})
|
|
@@ -39,7 +39,7 @@ const fioriGenericPrepare = async function (req) {
|
|
|
39
39
|
if (!result) req.reject(404)
|
|
40
40
|
if (result.draftAdmin_inProcessByUser !== req.user.id) {
|
|
41
41
|
// REVISIT: security log?
|
|
42
|
-
req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER')
|
|
42
|
+
req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER', [result.draftAdmin_inProcessByUser])
|
|
43
43
|
}
|
|
44
44
|
delete result.draftAdmin_inProcessByUser
|
|
45
45
|
return result
|
|
@@ -787,7 +787,7 @@ const _getOrderByEnrichedColumns = (orderBy, columns, entity) => {
|
|
|
787
787
|
const enrichedCol = []
|
|
788
788
|
|
|
789
789
|
if (orderBy && orderBy.length > 1) {
|
|
790
|
-
const colNames = columns.map(el => el.ref[el.ref.length - 1])
|
|
790
|
+
const colNames = columns.filter(el => el.ref).map(el => el.ref[el.ref.length - 1])
|
|
791
791
|
|
|
792
792
|
// REVISIT: GET Books?$select=title&$expand=NotBooks($select=pages)&$orderby=NotBooks/title - what's then?
|
|
793
793
|
for (const el of orderBy) {
|
|
@@ -25,6 +25,16 @@ const DRAFT_ADMIN_ELEMENTS = [
|
|
|
25
25
|
'DraftIsProcessedByMe'
|
|
26
26
|
]
|
|
27
27
|
|
|
28
|
+
/// It's important to wait for the completion of all promises, otherwise a rollback might happen too soon
|
|
29
|
+
const _promiseAll = async array => {
|
|
30
|
+
const results = await Promise.allSettled(array)
|
|
31
|
+
const e = results.find(r => r.status === 'rejected')
|
|
32
|
+
if (e) throw e.reason
|
|
33
|
+
return results.map(r => r.value)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const _isCount = query => query.SELECT.columns?.length === 1 && query.SELECT.columns[0].func === 'count'
|
|
37
|
+
|
|
28
38
|
const _inProcessByUserXpr = lockShiftedNow => ({
|
|
29
39
|
xpr: [
|
|
30
40
|
'case',
|
|
@@ -42,14 +52,6 @@ const _inProcessByUserXpr = lockShiftedNow => ({
|
|
|
42
52
|
cast: { type: 'cds.String' }
|
|
43
53
|
})
|
|
44
54
|
|
|
45
|
-
/// It's important to wait for the completion of all promises, otherwise a rollback might happen too soon
|
|
46
|
-
const _promiseAll = async array => {
|
|
47
|
-
const results = await Promise.allSettled(array)
|
|
48
|
-
const e = results.find(r => r.status === 'rejected')
|
|
49
|
-
if (e) throw e.reason
|
|
50
|
-
return results.map(r => r.value)
|
|
51
|
-
}
|
|
52
|
-
|
|
53
55
|
const _lock = {
|
|
54
56
|
get shiftedNow() {
|
|
55
57
|
return new Date(Math.max(0, Date.now() - DRAFT_CANCEL_TIMEOUT_IN_MIN() * 60 * 1000)).toISOString()
|
|
@@ -160,7 +162,8 @@ cds.ApplicationService.prototype.handle = async function (req) {
|
|
|
160
162
|
])
|
|
161
163
|
)
|
|
162
164
|
const inProcessByUser = draft?.DraftAdministrativeData?.InProcessByUser
|
|
163
|
-
if (inProcessByUser && inProcessByUser !== cds.context.user.id)
|
|
165
|
+
if (inProcessByUser && inProcessByUser !== cds.context.user.id)
|
|
166
|
+
req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER', [inProcessByUser])
|
|
164
167
|
const deletes = [run(DELETE.from({ ref: query.DELETE.from.ref }))]
|
|
165
168
|
if (draft)
|
|
166
169
|
deletes.push(
|
|
@@ -201,21 +204,22 @@ cds.ApplicationService.prototype.handle = async function (req) {
|
|
|
201
204
|
)
|
|
202
205
|
if (!res) req.reject(404)
|
|
203
206
|
if (res.DraftAdministrativeData?.InProcessByUser !== cds.context.user.id)
|
|
204
|
-
req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER')
|
|
207
|
+
req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER', [res.DraftAdministrativeData?.InProcessByUser])
|
|
205
208
|
const DraftAdministrativeData_DraftUUID = res.DraftAdministrativeData_DraftUUID
|
|
206
209
|
delete res.DraftAdministrativeData_DraftUUID
|
|
207
210
|
delete res.DraftAdministrativeData
|
|
208
211
|
const HasActiveEntity = res.HasActiveEntity
|
|
209
212
|
delete res.HasActiveEntity
|
|
210
213
|
delete res.IsActiveEntity
|
|
214
|
+
// First run the handlers as they might need access to DraftAdministrativeData or the draft entities
|
|
215
|
+
const result = await run(
|
|
216
|
+
HasActiveEntity ? UPDATE(req.target).data(res).where(targetWhere) : INSERT.into(req.target).entries(res)
|
|
217
|
+
)
|
|
211
218
|
await _promiseAll([
|
|
212
219
|
run(DELETE.from(targetDraft).where(targetWhere)),
|
|
213
220
|
DELETE.from('DRAFT.DraftAdministrativeData').where({ DraftUUID: DraftAdministrativeData_DraftUUID })
|
|
214
221
|
])
|
|
215
222
|
|
|
216
|
-
const result = await run(
|
|
217
|
-
HasActiveEntity ? UPDATE(req.target).data(res).where(targetWhere) : INSERT.into(req.target).entries(res)
|
|
218
|
-
)
|
|
219
223
|
req?._?.odataRes?.setStatusCode(201)
|
|
220
224
|
|
|
221
225
|
return Object.assign(result, { IsActiveEntity: true })
|
|
@@ -239,7 +243,7 @@ cds.ApplicationService.prototype.handle = async function (req) {
|
|
|
239
243
|
|
|
240
244
|
if (req.event === 'PATCH') {
|
|
241
245
|
if (draftParams.IsActiveEntity) req.reject(501)
|
|
242
|
-
if (
|
|
246
|
+
if (!('IsActiveEntity' in draftParams)) {
|
|
243
247
|
const res = await run(
|
|
244
248
|
SELECT.one.from({ ref: req.UPDATE.entity.ref }).columns('DraftAdministrativeData_DraftUUID')
|
|
245
249
|
)
|
|
@@ -248,7 +252,7 @@ cds.ApplicationService.prototype.handle = async function (req) {
|
|
|
248
252
|
const result = await handle(_req)
|
|
249
253
|
return result
|
|
250
254
|
}
|
|
251
|
-
if (
|
|
255
|
+
if (draftParams.IsActiveEntity === false) {
|
|
252
256
|
LOG.debug('patch draft')
|
|
253
257
|
if (req.target?.name.endsWith('DraftAdministrativeData')) req.reject(405)
|
|
254
258
|
const draftsRef = _redirectRefToDrafts(query.UPDATE.entity.ref, this.model)
|
|
@@ -260,7 +264,7 @@ cds.ApplicationService.prototype.handle = async function (req) {
|
|
|
260
264
|
)
|
|
261
265
|
if (!res) req.reject(404)
|
|
262
266
|
if (res.DraftAdministrativeData?.InProcessByUser !== cds.context.user.id)
|
|
263
|
-
req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER')
|
|
267
|
+
req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER', [res.DraftAdministrativeData?.InProcessByUser])
|
|
264
268
|
await UPDATE('DRAFT.DraftAdministrativeData')
|
|
265
269
|
.data({
|
|
266
270
|
InProcessByUser: req.user.id,
|
|
@@ -345,6 +349,7 @@ const Read = {
|
|
|
345
349
|
onlyActives: async function (run, query, { ignoreDrafts } = {}) {
|
|
346
350
|
LOG.debug('List Editing Status: Only Active')
|
|
347
351
|
// DraftAdministrativeData is only accessible via drafts
|
|
352
|
+
if (_isCount(query)) return run(query)
|
|
348
353
|
if (query._target.name.endsWith('.DraftAdministrativeData')) return run(query._drafts)
|
|
349
354
|
if (!query._target._isDraftEnabled) return run(query)
|
|
350
355
|
if (!query.SELECT.groupBy && query.SELECT.columns && !query.SELECT.columns.some(c => c === '*')) {
|
|
@@ -428,7 +433,7 @@ const Read = {
|
|
|
428
433
|
LOG.debug('List Editing Status: All')
|
|
429
434
|
query._drafts.SELECT.count = false
|
|
430
435
|
query._drafts.SELECT.limit = undefined // We need all entries for the keys to properly select actives (count)
|
|
431
|
-
const isCount = query._drafts
|
|
436
|
+
const isCount = _isCount(query._drafts)
|
|
432
437
|
if (isCount) {
|
|
433
438
|
const keys = Object_keys(query._target.keys).filter(k => k !== 'IsActiveEntity')
|
|
434
439
|
query._drafts.SELECT.columns = keys.map(k => ({ ref: [k] }))
|
|
@@ -839,7 +844,7 @@ async function onNew(req) {
|
|
|
839
844
|
)
|
|
840
845
|
if (!rootData) req.reject(404)
|
|
841
846
|
if (rootData.DraftAdministrativeData?.InProcessByUser !== req.user.id)
|
|
842
|
-
req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER')
|
|
847
|
+
req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER', [rootData.DraftAdministrativeData.InProcessByUser])
|
|
843
848
|
DraftUUID = rootData.DraftAdministrativeData_DraftUUID
|
|
844
849
|
}
|
|
845
850
|
const timestamp = cds.context.timestamp.toISOString() // REVISIT: toISOString should be done on db layer
|
|
@@ -863,7 +868,7 @@ async function onNew(req) {
|
|
|
863
868
|
.where({ DraftUUID })
|
|
864
869
|
|
|
865
870
|
const _assignDraftData = (obj, target) => {
|
|
866
|
-
const newObj = Object.assign({ DraftAdministrativeData_DraftUUID: DraftUUID, HasActiveEntity: false }
|
|
871
|
+
const newObj = Object.assign({}, obj, { DraftAdministrativeData_DraftUUID: DraftUUID, HasActiveEntity: false })
|
|
867
872
|
if (!target) return newObj
|
|
868
873
|
|
|
869
874
|
// Also support deep insertions
|
|
@@ -993,7 +998,7 @@ async function onCancel(req) {
|
|
|
993
998
|
const draft = await this.run(draftDelete)
|
|
994
999
|
if (draftParams.IsActiveEntity === false && !draft) req.reject(404)
|
|
995
1000
|
if (draft && draft.DraftAdministrativeData?.InProcessByUser !== cds.context.user.id)
|
|
996
|
-
req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER')
|
|
1001
|
+
req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER', [draft.DraftAdministrativeData?.InProcessByUser])
|
|
997
1002
|
const deletes = !draft ? [] : [this.run(DELETE.from({ ref: req.query.DELETE.from.ref }))]
|
|
998
1003
|
if (draft && req.target['@Common.DraftRoot.ActivationAction'])
|
|
999
1004
|
// only for draft root
|
|
@@ -1023,7 +1028,8 @@ async function onPrepare(req) {
|
|
|
1023
1028
|
Object.defineProperty(draftQuery, '_draftParams', { value: draftParams, enumerable: false })
|
|
1024
1029
|
const data = await this.run(draftQuery)
|
|
1025
1030
|
if (!data) req.reject(404)
|
|
1026
|
-
if (data.DraftAdministrativeData?.InProcessByUser !== req.user.id)
|
|
1031
|
+
if (data.DraftAdministrativeData?.InProcessByUser !== req.user.id)
|
|
1032
|
+
req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER', [data.DraftAdministrativeData?.InProcessByUser])
|
|
1027
1033
|
delete data.DraftAdministrativeData
|
|
1028
1034
|
return { ...data, IsActiveEntity: false }
|
|
1029
1035
|
}
|
|
@@ -188,11 +188,6 @@ const removeDraftUUIDIfNecessary = req =>
|
|
|
188
188
|
? () => {}
|
|
189
189
|
: result => delete result.DraftAdministrativeData_DraftUUID
|
|
190
190
|
|
|
191
|
-
const isDraftActivateAction = req => {
|
|
192
|
-
// REVISIT: get rid of getUrlObject
|
|
193
|
-
if (req.getUrlObject) return req.getUrlObject().pathname.endsWith('draftActivate')
|
|
194
|
-
}
|
|
195
|
-
|
|
196
191
|
const addColumnAlias = (columns, alias) => {
|
|
197
192
|
if (!alias) {
|
|
198
193
|
return columns
|
|
@@ -258,7 +253,6 @@ module.exports = {
|
|
|
258
253
|
getUpdateDraftAdminCQN,
|
|
259
254
|
getEnrichedCQN,
|
|
260
255
|
removeDraftUUIDIfNecessary,
|
|
261
|
-
isDraftActivateAction,
|
|
262
256
|
ensureDraftsSuffix,
|
|
263
257
|
ensureNoDraftsSuffix,
|
|
264
258
|
ensureUnlocalized,
|
|
@@ -21,7 +21,7 @@ class CustomFunctionBuilder extends FunctionBuilder {
|
|
|
21
21
|
|
|
22
22
|
_handleContains(args) {
|
|
23
23
|
// fuzzy search has three arguments, must not be converted to like expressions
|
|
24
|
-
if (args.length > 2 || this.
|
|
24
|
+
if (args.length > 2 || this._obj.searchUsingContains) {
|
|
25
25
|
this._outputObj.sql.push('CONTAINS')
|
|
26
26
|
this._addFunctionArgs(args, true)
|
|
27
27
|
return
|
|
@@ -4,11 +4,6 @@ const LOG = cds.log('hana|db|sql')
|
|
|
4
4
|
const SelectBuilder = require('../../db/sql-builder').SelectBuilder
|
|
5
5
|
|
|
6
6
|
class CustomSelectBuilder extends SelectBuilder {
|
|
7
|
-
constructor(obj, options, csn) {
|
|
8
|
-
super(obj, options, csn)
|
|
9
|
-
// $searchUsingContains property is set in the sub SELECT of the query, optimized search case
|
|
10
|
-
if (this._obj?._$searchUsingContains) this._options.$searchUsingContains = this._obj._$searchUsingContains
|
|
11
|
-
}
|
|
12
7
|
get FunctionBuilder() {
|
|
13
8
|
const FunctionBuilder = require('./CustomFunctionBuilder')
|
|
14
9
|
Object.defineProperty(this, 'FunctionBuilder', { value: FunctionBuilder })
|