@sap/cds 6.2.3 → 6.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 +57 -0
- package/apis/connect.d.ts +1 -1
- package/apis/cqn.d.ts +1 -1
- package/apis/internal/inference.d.ts +14 -0
- package/apis/ql.d.ts +40 -36
- package/apis/services.d.ts +23 -6
- package/bin/build/buildTaskEngine.js +15 -12
- package/bin/build/buildTaskHandler.js +3 -3
- package/bin/build/constants.js +2 -0
- package/bin/build/provider/buildTaskHandlerEdmx.js +1 -1
- package/bin/build/provider/buildTaskHandlerFeatureToggles.js +4 -3
- package/bin/build/provider/buildTaskHandlerInternal.js +2 -2
- package/bin/build/provider/java/index.js +2 -1
- package/bin/build/provider/mtx/index.js +2 -1
- package/bin/build/provider/mtx/resourcesTarBuilder.js +3 -2
- package/bin/build/provider/mtx-extension/index.js +2 -1
- package/bin/build/provider/mtx-sidecar/index.js +3 -1
- package/bin/build/util.js +2 -2
- package/bin/deploy/to-hana/cfUtil.js +46 -62
- package/lib/auth/index.js +2 -1
- package/lib/auth/jwt-auth.js +64 -3
- package/lib/auth/xsuaa-auth.js +2 -3
- package/lib/compile/cdsc.js +1 -0
- package/lib/compile/etc/_localized.js +1 -0
- package/lib/dbs/cds-deploy.js +2 -1
- package/lib/env/cds-env.js +14 -49
- package/lib/env/cds-requires.js +13 -7
- package/lib/env/defaults.js +4 -0
- package/lib/i18n/localize.js +11 -8
- package/lib/index.js +1 -1
- package/lib/log/cds-log.js +2 -2
- package/lib/log/format/cf.js +16 -0
- package/lib/log/format/kibana.js +15 -2
- package/lib/ql/INSERT.js +12 -11
- package/lib/ql/Query.js +14 -7
- package/lib/ql/UPSERT.js +1 -0
- package/lib/ql/Whereable.js +6 -2
- package/lib/ql/cds-ql.js +2 -4
- package/lib/req/request.js +2 -0
- package/lib/srv/bindings.js +1 -0
- package/lib/srv/middlewares/cds-context.js +1 -1
- package/lib/srv/srv-dispatch.js +1 -0
- package/lib/srv/srv-tx.js +3 -3
- package/lib/utils/cds-utils.js +75 -30
- package/lib/utils/inflect.js +24 -0
- package/libx/_runtime/auth/strategies/ias-auth.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/error.js +9 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +23 -6
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +1 -0
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/validator/ValueValidator.js +27 -15
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +1 -1
- package/libx/_runtime/cds-services/services/utils/compareJson.js +11 -10
- package/libx/_runtime/cds-services/services/utils/differ.js +6 -4
- package/libx/_runtime/common/composition/data.js +29 -40
- package/libx/_runtime/common/composition/update.js +6 -19
- package/libx/_runtime/common/generic/paging.js +1 -1
- package/libx/_runtime/common/utils/resolveView.js +7 -13
- package/libx/_runtime/db/utils/generateAliases.js +1 -0
- package/libx/_runtime/fiori/generic/before.js +5 -2
- package/libx/_runtime/fiori/generic/read.js +11 -4
- package/libx/_runtime/hana/execute.js +2 -2
- package/libx/_runtime/hana/search2Contains.js +3 -1
- package/libx/_runtime/hana/search2cqn4sql.js +1 -0
- package/libx/_runtime/messaging/AMQPWebhookMessaging.js +1 -1
- package/libx/_runtime/messaging/enterprise-messaging-utils/EMManagement.js +5 -2
- package/libx/_runtime/messaging/enterprise-messaging.js +7 -1
- package/libx/_runtime/messaging/file-based.js +1 -1
- package/libx/_runtime/messaging/message-queuing.js +5 -2
- package/libx/_runtime/messaging/outbox/utils.js +1 -1
- package/libx/_runtime/messaging/service.js +5 -3
- package/libx/odata/cqn2odata.js +4 -1
- package/libx/odata/utils.js +8 -7
- package/libx/rest/RestAdapter.js +1 -4
- package/package.json +1 -1
|
@@ -77,7 +77,7 @@ const _hasOpDeep = (entry, element) => {
|
|
|
77
77
|
return false
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
const _addCompositionsToResult = (result, entity, prop, newValue, oldValue) => {
|
|
80
|
+
const _addCompositionsToResult = (result, entity, prop, newValue, oldValue, opts) => {
|
|
81
81
|
/*
|
|
82
82
|
* REVISIT: the current impl results in {} instead of keeping null for compo to one.
|
|
83
83
|
* unfortunately, many follow-up errors occur (e.g., prop in null checks) if changed.
|
|
@@ -89,9 +89,9 @@ const _addCompositionsToResult = (result, entity, prop, newValue, oldValue) => {
|
|
|
89
89
|
!Array.isArray(newValue[prop]) &&
|
|
90
90
|
Object.keys(newValue[prop]).length === 0
|
|
91
91
|
) {
|
|
92
|
-
composition = compareJsonDeep(entity.elements[prop]._target, undefined, oldValue && oldValue[prop])
|
|
92
|
+
composition = compareJsonDeep(entity.elements[prop]._target, undefined, oldValue && oldValue[prop], opts)
|
|
93
93
|
} else {
|
|
94
|
-
composition = compareJsonDeep(entity.elements[prop]._target, newValue[prop], oldValue && oldValue[prop])
|
|
94
|
+
composition = compareJsonDeep(entity.elements[prop]._target, newValue[prop], oldValue && oldValue[prop], opts)
|
|
95
95
|
}
|
|
96
96
|
if (composition.some(c => _hasOpDeep(c, entity.elements[prop]))) {
|
|
97
97
|
result[prop] = entity.elements[prop].is2one ? composition[0] : composition
|
|
@@ -154,7 +154,7 @@ const _skipToMany = (entity, prop) => {
|
|
|
154
154
|
return entity.elements[prop] && entity.elements[prop].is2many && _skip(entity, prop)
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
-
const _iteratePropsInNewEntry = (newEntry, keys, result, oldEntry, entity) => {
|
|
157
|
+
const _iteratePropsInNewEntry = (newEntry, keys, result, oldEntry, entity, opts) => {
|
|
158
158
|
// On app-service layer, generated foreign keys are not enumerable,
|
|
159
159
|
// include them here too.
|
|
160
160
|
for (const prop of Object.getOwnPropertyNames(newEntry)) {
|
|
@@ -164,7 +164,7 @@ const _iteratePropsInNewEntry = (newEntry, keys, result, oldEntry, entity) => {
|
|
|
164
164
|
}
|
|
165
165
|
|
|
166
166
|
// if value did not change --> ignored
|
|
167
|
-
if (newEntry[prop] === (oldEntry && oldEntry[prop]) || prop in DRAFT_COLUMNS_MAP) {
|
|
167
|
+
if (newEntry[prop] === (oldEntry && oldEntry[prop]) || (opts.ignoreDraftColumns && prop in DRAFT_COLUMNS_MAP)) {
|
|
168
168
|
continue
|
|
169
169
|
}
|
|
170
170
|
|
|
@@ -177,7 +177,7 @@ const _iteratePropsInNewEntry = (newEntry, keys, result, oldEntry, entity) => {
|
|
|
177
177
|
}
|
|
178
178
|
|
|
179
179
|
if (entity.elements[prop] && entity.elements[prop].isComposition) {
|
|
180
|
-
_addCompositionsToResult(result, entity, prop, newEntry, oldEntry)
|
|
180
|
+
_addCompositionsToResult(result, entity, prop, newEntry, oldEntry, opts)
|
|
181
181
|
continue
|
|
182
182
|
}
|
|
183
183
|
|
|
@@ -185,7 +185,7 @@ const _iteratePropsInNewEntry = (newEntry, keys, result, oldEntry, entity) => {
|
|
|
185
185
|
}
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
-
const compareJsonDeep = (entity, newValue = [], oldValue = []) => {
|
|
188
|
+
const compareJsonDeep = (entity, newValue = [], oldValue = [], opts) => {
|
|
189
189
|
const resultsArray = []
|
|
190
190
|
const keys = _getKeysOfEntity(entity)
|
|
191
191
|
|
|
@@ -200,7 +200,7 @@ const compareJsonDeep = (entity, newValue = [], oldValue = []) => {
|
|
|
200
200
|
|
|
201
201
|
_addKeysToEntryIfNotExists(keys, newEntry)
|
|
202
202
|
|
|
203
|
-
_iteratePropsInNewEntry(newEntry, keys, result, oldEntry, entity)
|
|
203
|
+
_iteratePropsInNewEntry(newEntry, keys, result, oldEntry, entity, opts)
|
|
204
204
|
|
|
205
205
|
resultsArray.push(result)
|
|
206
206
|
}
|
|
@@ -259,8 +259,9 @@ const compareJsonDeep = (entity, newValue = [], oldValue = []) => {
|
|
|
259
259
|
*
|
|
260
260
|
* @returns {Array}
|
|
261
261
|
*/
|
|
262
|
-
const compareJson = (newValue, oldValue, entity) => {
|
|
263
|
-
const
|
|
262
|
+
const compareJson = (newValue, oldValue, entity, opts = {}) => {
|
|
263
|
+
const options = Object.assign({ ignoreDraftColumns: false }, opts)
|
|
264
|
+
const result = compareJsonDeep(entity, newValue, oldValue, options)
|
|
264
265
|
|
|
265
266
|
// in case of batch insert, result is an array
|
|
266
267
|
// in all other cases it is an array with just one entry
|
|
@@ -44,7 +44,7 @@ module.exports = class Differ {
|
|
|
44
44
|
return cds
|
|
45
45
|
.tx(req)
|
|
46
46
|
.run(query)
|
|
47
|
-
.then(dbState => compareJson(undefined, dbState, req.target))
|
|
47
|
+
.then(dbState => compareJson(undefined, dbState, req.target, { ignoreDraftColumns: true }))
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
async _addPartialPersistentState(req) {
|
|
@@ -59,7 +59,7 @@ module.exports = class Differ {
|
|
|
59
59
|
const combinedData = providedData || Object.assign({}, req.query.UPDATE.data || {}, req.query.UPDATE.with || {})
|
|
60
60
|
const lastTransition = newQuery.UPDATE._transitions[newQuery.UPDATE._transitions.length - 1]
|
|
61
61
|
const revertedPersistent = revertData(req._.partialPersistentState, lastTransition, this._srv)
|
|
62
|
-
return compareJson(combinedData, revertedPersistent, req.target)
|
|
62
|
+
return compareJson(combinedData, revertedPersistent, req.target, { ignoreDraftColumns: true })
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
async _diffPatch(req, providedData) {
|
|
@@ -73,7 +73,9 @@ module.exports = class Differ {
|
|
|
73
73
|
.tx(req)
|
|
74
74
|
.run(SELECT.from(draftRef).where(removeIsActiveEntityRecursively(where)).limit(1))
|
|
75
75
|
|
|
76
|
-
return compareJson(providedData || req.data, req._.partialPersistentState, req.target
|
|
76
|
+
return compareJson(providedData || req.data, req._.partialPersistentState, req.target, {
|
|
77
|
+
ignoreDraftColumns: true
|
|
78
|
+
})
|
|
77
79
|
}
|
|
78
80
|
}
|
|
79
81
|
|
|
@@ -83,7 +85,7 @@ module.exports = class Differ {
|
|
|
83
85
|
? req.query.INSERT.entries[0]
|
|
84
86
|
: req.query.INSERT.entries
|
|
85
87
|
|
|
86
|
-
return compareJson(originalData, undefined, req.target)
|
|
88
|
+
return compareJson(originalData, undefined, req.target, { ignoreDraftColumns: true })
|
|
87
89
|
}
|
|
88
90
|
|
|
89
91
|
async calculate(req, providedData) {
|
|
@@ -8,6 +8,8 @@ const { cqn2cqn4sql } = require('../utils/cqn2cqn4sql')
|
|
|
8
8
|
const cds = require('../../cds')
|
|
9
9
|
const { SELECT } = cds.ql
|
|
10
10
|
|
|
11
|
+
const CHUNK_SIZE = cds.env.features.chunk_deep || Number.MAX_VALUE
|
|
12
|
+
|
|
11
13
|
/*
|
|
12
14
|
* own utils
|
|
13
15
|
*/
|
|
@@ -62,13 +64,17 @@ const _getLinksOfCompTree = compositionTree => {
|
|
|
62
64
|
return links
|
|
63
65
|
}
|
|
64
66
|
|
|
65
|
-
const _whereKeys =
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
67
|
+
const _whereKeys = keySet => {
|
|
68
|
+
if (!keySet.length || !Object.keys(keySet[0]).length) return []
|
|
69
|
+
|
|
70
|
+
const keys0 = Object.keys(keySet[0])
|
|
71
|
+
const keys = { list: keys0.map(pk => ({ ref: [pk] })) }
|
|
72
|
+
const values = {
|
|
73
|
+
list: keySet.map(row => ({
|
|
74
|
+
list: keys0.map(k => ({ val: row[k] }))
|
|
75
|
+
}))
|
|
76
|
+
}
|
|
77
|
+
return [keys, 'in', values]
|
|
72
78
|
}
|
|
73
79
|
|
|
74
80
|
const _parentKey = (element, key) => {
|
|
@@ -136,28 +142,19 @@ const _subWhere = (result, element) => {
|
|
|
136
142
|
const links = [...element.backLinks, ...element.customBackLinks]
|
|
137
143
|
if (result.length && links && links.length > 0) {
|
|
138
144
|
where = {}
|
|
139
|
-
const chunkSize = cds.env.features.chunk_deep
|
|
140
145
|
const keys0 = Object.keys(_getWhereObj(result[0], links))
|
|
141
|
-
if (
|
|
146
|
+
if (keys0.length) {
|
|
142
147
|
const keys = { list: keys0.map(pk => ({ ref: [pk] })) }
|
|
143
|
-
for (let i = 0; i < result.length; i +=
|
|
148
|
+
for (let i = 0; i < result.length; i += CHUNK_SIZE) {
|
|
144
149
|
const values = {
|
|
145
|
-
list: result
|
|
146
|
-
.
|
|
147
|
-
|
|
150
|
+
list: result.slice(i, i + CHUNK_SIZE).map(row => ({
|
|
151
|
+
list: keys0.map(k => {
|
|
152
|
+
return { val: _getWhereObj(row, links)[k] }
|
|
153
|
+
})
|
|
154
|
+
}))
|
|
148
155
|
}
|
|
149
156
|
where[i] = [keys, 'in', values]
|
|
150
157
|
}
|
|
151
|
-
} else {
|
|
152
|
-
where = []
|
|
153
|
-
for (const row of result) {
|
|
154
|
-
const whereObj = _getWhereObj(row, links)
|
|
155
|
-
const whereCQN = ctUtils.whereKey(whereObj)
|
|
156
|
-
if (whereCQN.length) {
|
|
157
|
-
if (where.length > 0) where.push('or')
|
|
158
|
-
where.push({ xpr: [...whereCQN] })
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
158
|
}
|
|
162
159
|
}
|
|
163
160
|
return where
|
|
@@ -232,29 +229,22 @@ const _select = ({
|
|
|
232
229
|
}
|
|
233
230
|
|
|
234
231
|
const _selectDeepUpdateData = async args => {
|
|
235
|
-
const { model, compositionTree, entityName, data, root, selectData, tx, selectAllColumns } = args
|
|
232
|
+
const { model, compositionTree, entityName, data, root, selectData, tx, selectAllColumns, where, parentKeys } = args
|
|
236
233
|
let result = []
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
chunkSize &&
|
|
240
|
-
!args.where &&
|
|
241
|
-
args.parentKeys &&
|
|
242
|
-
args.parentKeys.length > chunkSize &&
|
|
243
|
-
Object.keys(args.parentKeys[0]).length
|
|
244
|
-
) {
|
|
245
|
-
const keys0 = Object.keys(args.parentKeys[0])
|
|
234
|
+
if (!where && parentKeys && parentKeys.length && Object.keys(parentKeys[0]).length) {
|
|
235
|
+
const keys0 = Object.keys(parentKeys[0])
|
|
246
236
|
const keys = { list: keys0.map(pk => ({ ref: [pk] })) }
|
|
247
|
-
for (let i = 0; i <
|
|
237
|
+
for (let i = 0; i < parentKeys.length; i += CHUNK_SIZE) {
|
|
248
238
|
const values = {
|
|
249
|
-
list:
|
|
239
|
+
list: parentKeys.slice(i, i + CHUNK_SIZE).map(row => ({ list: keys0.map(k => ({ val: row[k] })) }))
|
|
250
240
|
}
|
|
251
|
-
const _args = { ...args, where: [keys, 'in', values] }
|
|
241
|
+
const _args = { ...args, where: [keys, 'in', values], parentKeys: undefined }
|
|
252
242
|
const selectCQN = _select(_args)
|
|
253
243
|
result.push(...(await tx.run(selectCQN)))
|
|
254
244
|
}
|
|
255
|
-
} else if (
|
|
256
|
-
for (let
|
|
257
|
-
const _args = { ...args, where }
|
|
245
|
+
} else if (where && !Array.isArray(where)) {
|
|
246
|
+
for (let w of Object.values(where)) {
|
|
247
|
+
const _args = { ...args, where: w }
|
|
258
248
|
const selectCQN = _select(_args)
|
|
259
249
|
result.push(...(await tx.run(selectCQN)))
|
|
260
250
|
}
|
|
@@ -262,7 +252,6 @@ const _selectDeepUpdateData = async args => {
|
|
|
262
252
|
const selectCQN = _select(args)
|
|
263
253
|
result = await tx.run(selectCQN)
|
|
264
254
|
}
|
|
265
|
-
|
|
266
255
|
if (!result.length) return Promise.resolve(result)
|
|
267
256
|
|
|
268
257
|
const keys = _keys(model.definitions[entityName], result)
|
|
@@ -10,6 +10,8 @@ const { deepCopyObject } = require('../utils/copy')
|
|
|
10
10
|
|
|
11
11
|
const getError = require('../../common/error')
|
|
12
12
|
|
|
13
|
+
const CHUNK_SIZE = cds.env.features.chunk_deep || Number.MAX_VALUE
|
|
14
|
+
|
|
13
15
|
/*
|
|
14
16
|
* own utils
|
|
15
17
|
*/
|
|
@@ -35,13 +37,11 @@ const _dataByKey = (entity, data) => {
|
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
function _addSubDeepUpdateCQNForDelete({ entity, data, selectData, entityName, deleteCQNs }) {
|
|
38
|
-
const chunkSize = cds.env.features.chunk_deep
|
|
39
40
|
const dataByKey = _dataByKey(entity, data)
|
|
40
|
-
if (
|
|
41
|
-
|
|
42
|
-
for (let j = 0; j < selectData.length; j += chunkSize) {
|
|
41
|
+
if (selectData.length && selectData[0] && Object.keys(selectData[0]).length) {
|
|
42
|
+
for (let j = 0; j < selectData.length; j += CHUNK_SIZE) {
|
|
43
43
|
const deleteCQN = { DELETE: { from: entityName, where: [] } }
|
|
44
|
-
for (let i = j; i < j +
|
|
44
|
+
for (let i = j; i < j + CHUNK_SIZE && i < selectData.length; i++) {
|
|
45
45
|
const selectEntry = selectData[i]
|
|
46
46
|
if (!selectEntry) continue
|
|
47
47
|
const dataEntry = dataByKey.get(_serializedKey(entity, selectEntry))
|
|
@@ -52,21 +52,8 @@ function _addSubDeepUpdateCQNForDelete({ entity, data, selectData, entityName, d
|
|
|
52
52
|
deleteCQN.DELETE.where.push({ xpr: [...ctUtils.whereKey(ctUtils.key(entity, selectEntry))] })
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
|
-
deleteCQNs.push(deleteCQN)
|
|
56
|
-
}
|
|
57
|
-
} else {
|
|
58
|
-
const deleteCQN = { DELETE: { from: entityName, where: [] } }
|
|
59
|
-
for (const selectEntry of selectData) {
|
|
60
|
-
if (!selectEntry) continue
|
|
61
|
-
const dataEntry = dataByKey.get(_serializedKey(entity, selectEntry))
|
|
62
|
-
if (!dataEntry) {
|
|
63
|
-
if (deleteCQN.DELETE.where.length > 0) {
|
|
64
|
-
deleteCQN.DELETE.where.push('or')
|
|
65
|
-
}
|
|
66
|
-
deleteCQN.DELETE.where.push({ xpr: [...ctUtils.whereKey(ctUtils.key(entity, selectEntry))] })
|
|
67
|
-
}
|
|
55
|
+
if (deleteCQN.DELETE.where.length) deleteCQNs.push(deleteCQN)
|
|
68
56
|
}
|
|
69
|
-
deleteCQNs.push(deleteCQN)
|
|
70
57
|
}
|
|
71
58
|
}
|
|
72
59
|
|
|
@@ -3,7 +3,7 @@ const { getDefaultPageSize, getMaxPageSize } = require('../utils/page')
|
|
|
3
3
|
|
|
4
4
|
const commonGenericPaging = function (req) {
|
|
5
5
|
// only if http request
|
|
6
|
-
if (!req._.req) return
|
|
6
|
+
if (!(req.http?.req || req._.req)) return
|
|
7
7
|
|
|
8
8
|
// target === null if view with parameters
|
|
9
9
|
if (!req.target || !req.query.SELECT || req.query.SELECT.one) return
|
|
@@ -353,7 +353,7 @@ const _newSelect = (query, transitions, service) => {
|
|
|
353
353
|
if (!newSelect.columns && targetTransition.mapping.size) newSelect.columns = _initialColumns(targetTransition)
|
|
354
354
|
if (newSelect.columns) {
|
|
355
355
|
rewriteAsterisks({ SELECT: query.SELECT }, service.model, {
|
|
356
|
-
_4db: service instanceof cds.DatabaseService,
|
|
356
|
+
_4db: service instanceof cds.DatabaseService || service.kind === 'better-sqlite',
|
|
357
357
|
target: targetTransition.queryTarget
|
|
358
358
|
})
|
|
359
359
|
newSelect.columns = _newColumns(newSelect.columns, targetTransition, service, service.kind !== 'app-service')
|
|
@@ -517,26 +517,20 @@ const _getTransitionData = (target, columns, service, skipForbiddenViewCheck) =>
|
|
|
517
517
|
if (!skipForbiddenViewCheck) _checkForForbiddenViews(target)
|
|
518
518
|
const targetStartsWithSrvName = service.namespace && target.name.startsWith(`${service.namespace}.`)
|
|
519
519
|
const persistenceTable = _isPersistenceTable(target)
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
!(service instanceof cds.DatabaseService) && !targetStartsWithSrvName
|
|
525
|
-
)
|
|
526
|
-
if (persistenceTable && service instanceof cds.DatabaseService) {
|
|
520
|
+
const isDatabaseService = service instanceof cds.DatabaseService || service.kind === 'better-sqlite'
|
|
521
|
+
columns = _queryColumns(target, columns, persistenceTable, !isDatabaseService && !targetStartsWithSrvName)
|
|
522
|
+
// REVISIT: Change once we expose database service
|
|
523
|
+
if (persistenceTable && isDatabaseService) {
|
|
527
524
|
return { target, transitionColumns: columns }
|
|
528
525
|
}
|
|
529
526
|
// stop projection resolving if it starts with the service name prefix
|
|
530
|
-
if (!
|
|
527
|
+
if (!isDatabaseService && targetStartsWithSrvName) {
|
|
531
528
|
return { target, transitionColumns: columns }
|
|
532
529
|
}
|
|
533
530
|
// continue projection resolving if the target is a projection
|
|
534
531
|
if (target.query && target.query._target) {
|
|
535
532
|
const newTarget = target.query._target
|
|
536
|
-
if (
|
|
537
|
-
service instanceof cds.DatabaseService ||
|
|
538
|
-
!(service.namespace && newTarget.name.startsWith(`${service.namespace}.`))
|
|
539
|
-
) {
|
|
533
|
+
if (isDatabaseService || !(service.namespace && newTarget.name.startsWith(`${service.namespace}.`))) {
|
|
540
534
|
return _getTransitionData(newTarget, columns, service, skipForbiddenViewCheck)
|
|
541
535
|
}
|
|
542
536
|
return { target: newTarget, transitionColumns: columns }
|
|
@@ -57,6 +57,7 @@ const _generateAliases = (partialCqn, aliasMap = new Map()) => {
|
|
|
57
57
|
_redirectXpr(partialCqn.SELECT.having, selectMap)
|
|
58
58
|
_redirectXpr(partialCqn.SELECT.columns, selectMap)
|
|
59
59
|
_redirectXpr(partialCqn.SELECT.groupBy, selectMap)
|
|
60
|
+
_redirectXpr(partialCqn.SELECT.orderBy, selectMap)
|
|
60
61
|
return
|
|
61
62
|
}
|
|
62
63
|
|
|
@@ -77,7 +77,7 @@ const _getRoot = req => {
|
|
|
77
77
|
return root
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
const _getDraftDataFromExistingDraft = async (req, root) => {
|
|
80
|
+
const _getDraftDataFromExistingDraft = async (req, root, isBoundAction) => {
|
|
81
81
|
if (!root) return []
|
|
82
82
|
if (root?.IsActiveEntity === false) {
|
|
83
83
|
const query = _getSelectDraftDataCqn(root.entityName, root.where)
|
|
@@ -85,6 +85,9 @@ const _getDraftDataFromExistingDraft = async (req, root) => {
|
|
|
85
85
|
return result
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
// do not expect validate draft ownership for action call on active instances
|
|
89
|
+
if (isBoundAction) return []
|
|
90
|
+
|
|
88
91
|
const rootWhere = getKeysCondition(req)
|
|
89
92
|
const query = _getSelectDraftDataCqn(ensureNoDraftsSuffix(req.target.name), rootWhere)
|
|
90
93
|
const result = await cds.tx(req).run(query)
|
|
@@ -147,8 +150,8 @@ const _deleteCancel = async function (req) {
|
|
|
147
150
|
}
|
|
148
151
|
|
|
149
152
|
const _validateDraftBoundAction = async function (req) {
|
|
150
|
-
const result = await _getDraftDataFromExistingDraft(req, _getRoot(req))
|
|
151
153
|
const isBoundAction = true
|
|
154
|
+
const result = await _getDraftDataFromExistingDraft(req, _getRoot(req), isBoundAction)
|
|
152
155
|
if (result && result.length > 0) _validateDraft(req, result, isBoundAction)
|
|
153
156
|
}
|
|
154
157
|
|
|
@@ -187,16 +187,23 @@ const _getDraftPropertiesDetermineDraft = (req, where, tableName, calcDraftUUID
|
|
|
187
187
|
}
|
|
188
188
|
|
|
189
189
|
function _copyCQNPartial(partial) {
|
|
190
|
-
if (partial.SELECT
|
|
190
|
+
if (partial.SELECT) {
|
|
191
191
|
const newPartial = Object.assign({}, partial)
|
|
192
192
|
const newSELECT = Object.assign({}, partial.SELECT)
|
|
193
193
|
newSELECT.from = _copyCQNPartial(partial.SELECT.from)
|
|
194
194
|
newPartial.SELECT = newSELECT
|
|
195
|
+
if (partial.SELECT._4odata) newSELECT._4odata = true
|
|
195
196
|
if (partial.SELECT.columns) newPartial.SELECT.columns = _copyArray(partial.SELECT.columns)
|
|
196
197
|
if (partial.SELECT.where) newPartial.SELECT.where = _copyArray(partial.SELECT.where)
|
|
197
198
|
return newPartial
|
|
198
199
|
}
|
|
199
200
|
|
|
201
|
+
if (partial.id) {
|
|
202
|
+
const res = Object.assign({}, partial)
|
|
203
|
+
if (res.where) res.where = _copyArray(res.where)
|
|
204
|
+
return res
|
|
205
|
+
}
|
|
206
|
+
|
|
200
207
|
if (partial.ref) {
|
|
201
208
|
return Object.assign({}, partial, { ref: _copyArray(partial.ref) })
|
|
202
209
|
}
|
|
@@ -1272,12 +1279,12 @@ const fioriGenericRead = async function (req) {
|
|
|
1272
1279
|
req.target = _getLocalizedEntity(this.model, req.target, req.user) || req.target
|
|
1273
1280
|
|
|
1274
1281
|
// REVISIT DRAFT HANDLING: cqn2cqn4sql must not be called here
|
|
1275
|
-
const query4sql = cqn2cqn4sql(req.query, this.model, { _4fiori: true })
|
|
1282
|
+
const query4sql = cqn2cqn4sql(_copyCQNPartial(req.query), this.model, { _4fiori: true })
|
|
1276
1283
|
|
|
1277
1284
|
// Clone the request. Do not clone with Object.assign as that would skip all non-enumerable properties.
|
|
1278
1285
|
// REVISIT: query4sql.clone() doesn't really clone the original query, hence _generateCQN will heavily modify
|
|
1279
1286
|
// it, e.g. IsActiveEntity is stripped. This is a problem for subsequent handlers which rely on this information.
|
|
1280
|
-
const reqClone = { __proto__: req, query: query4sql
|
|
1287
|
+
const reqClone = { __proto__: req, query: query4sql }
|
|
1281
1288
|
// Clone draft restrictions to the cloned query.
|
|
1282
1289
|
reqClone.query._draftRestrictions = query._draftRestrictions
|
|
1283
1290
|
|
|
@@ -1320,7 +1327,7 @@ const fioriGenericRead = async function (req) {
|
|
|
1320
1327
|
_adaptColumns4readAfterWrite(req, cqnScenario, query4sql)
|
|
1321
1328
|
|
|
1322
1329
|
const result = await cds.tx(req).send({ query: cqnScenario.cqn, target: req.target })
|
|
1323
|
-
return _postProcess(result,
|
|
1330
|
+
return _postProcess(result, reqClone, cqnScenario, enhancedWithLastChangeDateTime)
|
|
1324
1331
|
}
|
|
1325
1332
|
|
|
1326
1333
|
module.exports = cds.service.impl(function (srv, entity) {
|
|
@@ -51,8 +51,8 @@ const BASE64 = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{4}|[A-Za-z0-9+/]{3}=|[A-Z
|
|
|
51
51
|
|
|
52
52
|
function _getProcedureName(sql) {
|
|
53
53
|
// name delimited with "" allows any character
|
|
54
|
-
const match = sql.trim().match(/^call \s*(("(?<delimited>.+)")|(?<undelimited>\w+))\s*\(/i)
|
|
55
|
-
return match && (match.groups.undelimited
|
|
54
|
+
const match = sql.trim().match(/^call \s*(("\w+"\.)?("(?<delimited>.+)")|(\w+\.)?(?<undelimited>\w+))\s*\(/i)
|
|
55
|
+
return match && (match.groups.undelimited ?? match.groups.delimited)
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
function _hdbGetResultForProcedure(rows, args, outParameters) {
|
|
@@ -95,7 +95,9 @@ const isContainsPredicateSupported = (query, entity, columns2Search) => {
|
|
|
95
95
|
const _isColumnFunc = (columns2Search, columnsDefs) =>
|
|
96
96
|
columns2Search.some(column2Search => {
|
|
97
97
|
if (column2Search.func) return true
|
|
98
|
-
return columnsDefs?.some(
|
|
98
|
+
return columnsDefs?.some(
|
|
99
|
+
columnDef => (columnDef.func && columnDef.as === column2Search.ref[0]) || columnDef?.xpr?.some(xpr => xpr.func)
|
|
100
|
+
)
|
|
99
101
|
})
|
|
100
102
|
|
|
101
103
|
module.exports = {
|
|
@@ -82,6 +82,7 @@ const _addAliasToQuery = (query, entity, columnsToBeSearched) => {
|
|
|
82
82
|
SELECT.columns = addAliasToExpression(SELECT.columns, getEntityName)
|
|
83
83
|
columnsToBeSearched = addAliasToExpression(columnsToBeSearched, getEntityName)
|
|
84
84
|
SELECT.groupBy = addAliasToExpression(SELECT.groupBy, getEntityName)
|
|
85
|
+
SELECT.orderBy = addAliasToExpression(SELECT.orderBy, getEntityName)
|
|
85
86
|
SELECT.where = addAliasToExpression(SELECT.where, getEntityName)
|
|
86
87
|
return columnsToBeSearched
|
|
87
88
|
}
|
|
@@ -38,7 +38,7 @@ class AMQPWebhookMessaging extends MessagingService {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
startListening(opt = {}) {
|
|
41
|
-
if (!this.subscribedTopics.size) return
|
|
41
|
+
if (!this._listenToAll && !this.subscribedTopics.size) return
|
|
42
42
|
if (!opt.doNotDeploy) {
|
|
43
43
|
const management = this.getManagement()
|
|
44
44
|
this.queued(management.createQueueAndSubscriptions.bind(management))()
|
|
@@ -87,12 +87,15 @@ class EMManagement {
|
|
|
87
87
|
this.subdomain ? { queue: queueName, subdomain: this.subdomain } : { queue: queueName }
|
|
88
88
|
)
|
|
89
89
|
try {
|
|
90
|
+
const queueConfig = this.queueConfig && { ...this.queueConfig }
|
|
91
|
+
if (queueConfig?.deadMsgQueue)
|
|
92
|
+
queueConfig.deadMsgQueue = queueConfig.deadMsgQueue.replace(/\$namespace/g, this.namespace)
|
|
90
93
|
const res = await authorizedRequest({
|
|
91
94
|
method: 'PUT',
|
|
92
95
|
uri: this.options.uri,
|
|
93
96
|
path: `/hub/rest/api/v1/management/messaging/queues/${encodeURIComponent(queueName)}`,
|
|
94
97
|
oa2: this.options.oa2,
|
|
95
|
-
dataObj:
|
|
98
|
+
dataObj: queueConfig,
|
|
96
99
|
tokenStore: this
|
|
97
100
|
})
|
|
98
101
|
if (res.statusCode === 201) return true
|
|
@@ -389,7 +392,7 @@ class EMManagement {
|
|
|
389
392
|
this.LOG._info && this.LOG.info('Unchanged subscriptions', unchangedSubs, ' ', this.subdomainInfo)
|
|
390
393
|
await Promise.all([
|
|
391
394
|
...obsoleteSubs.map(s => this.deleteSubscription(s)),
|
|
392
|
-
...additionalSubs.map(async
|
|
395
|
+
...additionalSubs.map(async t => this.createSubscription(t))
|
|
393
396
|
])
|
|
394
397
|
return
|
|
395
398
|
}
|
|
@@ -139,7 +139,7 @@ class EnterpriseMessaging extends AMQPWebhookMessaging {
|
|
|
139
139
|
const doNotDeploy = _multitenancyEnabled() && !this.options.deployForProvider
|
|
140
140
|
if (doNotDeploy) this.LOG._info && this.LOG.info('Skipping deployment of messaging artifacts for provider account')
|
|
141
141
|
super.startListening({ doNotDeploy })
|
|
142
|
-
if (!doNotDeploy && this.subscribedTopics.size) {
|
|
142
|
+
if (!doNotDeploy && (this._listenToAll || this.subscribedTopics.size)) {
|
|
143
143
|
const management = this.getManagement()
|
|
144
144
|
// Webhooks will perform an OPTIONS call on creation to check the availability of the app.
|
|
145
145
|
// On systems like Cloud Foundry the app URL will only be advertised once
|
|
@@ -218,6 +218,11 @@ class EnterpriseMessaging extends AMQPWebhookMessaging {
|
|
|
218
218
|
const topic = msg.event
|
|
219
219
|
const message = { ...(msg.headers || {}), data: msg.data }
|
|
220
220
|
|
|
221
|
+
const contentType =
|
|
222
|
+
msg.headers && ['id', 'source', 'specversion', 'type'].every(el => el in msg.headers)
|
|
223
|
+
? 'application/cloudevents+json'
|
|
224
|
+
: 'application/json'
|
|
225
|
+
|
|
221
226
|
await this.queued(() => {})()
|
|
222
227
|
|
|
223
228
|
try {
|
|
@@ -229,6 +234,7 @@ class EnterpriseMessaging extends AMQPWebhookMessaging {
|
|
|
229
234
|
tenant,
|
|
230
235
|
dataObj: message,
|
|
231
236
|
headers: {
|
|
237
|
+
'Content-Type': contentType,
|
|
232
238
|
'x-qos': 1
|
|
233
239
|
},
|
|
234
240
|
tokenStore: {}
|
|
@@ -35,7 +35,7 @@ class FileBasedMessaging extends MessagingService {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
startWatching() {
|
|
38
|
-
if (!this.subscribedTopics.size) return
|
|
38
|
+
if (!this._listenToAll && !this.subscribedTopics.size) return
|
|
39
39
|
const watcher = async () => {
|
|
40
40
|
if (!(await touched(this.file, this.recent))) return // > not touched since last check
|
|
41
41
|
// REVISIT: Bad if lock file wasn't cleaned up (due to crashes...)
|
|
@@ -61,12 +61,15 @@ class MQManagement {
|
|
|
61
61
|
async createQueue(queueName = this.queueName) {
|
|
62
62
|
this.LOG._info && this.LOG.info('Create queue', { queue: queueName })
|
|
63
63
|
try {
|
|
64
|
+
const queueConfig = this.queueConfig && { ...this.queueConfig }
|
|
65
|
+
if (queueConfig?.deadMessageQueue)
|
|
66
|
+
queueConfig.deadMessageQueue = queueConfig.deadMessageQueue.replace(/\$namespace/g, '')
|
|
64
67
|
const res = await authorizedRequest({
|
|
65
68
|
method: 'PUT',
|
|
66
69
|
uri: this.options.url,
|
|
67
70
|
path: `/v1/management/queues/${encodeURIComponent(queueName)}`,
|
|
68
71
|
oa2: this.options.auth.oauth2,
|
|
69
|
-
dataObj:
|
|
72
|
+
dataObj: queueConfig,
|
|
70
73
|
tokenStore: this
|
|
71
74
|
})
|
|
72
75
|
if (res.statusCode === 201) return true
|
|
@@ -182,7 +185,7 @@ class MQManagement {
|
|
|
182
185
|
.filter(s => !existingSubscriptions.some(e => s === e))
|
|
183
186
|
await Promise.all([
|
|
184
187
|
...obsoleteSubs.map(s => this.deleteSubscription(s)),
|
|
185
|
-
...additionalSubs.map(
|
|
188
|
+
...additionalSubs.map(t => this.createSubscription(t))
|
|
186
189
|
])
|
|
187
190
|
return
|
|
188
191
|
}
|
|
@@ -50,7 +50,7 @@ const _processSingleMessage = async (service, message, succeededMessages) => {
|
|
|
50
50
|
// Promise resolve is necessary because we want to set `cds.context` only
|
|
51
51
|
// inside this call
|
|
52
52
|
return Promise.resolve().then(async () => {
|
|
53
|
-
if (userId) cds.context
|
|
53
|
+
if (userId) cds.context = { user: new cds.User.Privileged(userId) }
|
|
54
54
|
try {
|
|
55
55
|
service._emitImmediate && (await service._emitImmediate(msg))
|
|
56
56
|
succeededMessages.push(message.ID)
|
|
@@ -97,7 +97,8 @@ class MessagingService extends OutboxService {
|
|
|
97
97
|
on(event, cb) {
|
|
98
98
|
const _event = _warnAndStripTopicPrefix(event, this.LOG)
|
|
99
99
|
// save all subscribed topics (not needed for local-messaging)
|
|
100
|
-
this.subscribedTopics.set(this.prepareTopic(_event, true), _event)
|
|
100
|
+
if (event !== '*') this.subscribedTopics.set(this.prepareTopic(_event, true), _event)
|
|
101
|
+
else this._listenToAll = true
|
|
101
102
|
return super.on(_event, cb)
|
|
102
103
|
}
|
|
103
104
|
|
|
@@ -134,8 +135,9 @@ class MessagingService extends OutboxService {
|
|
|
134
135
|
const subscribedEvent =
|
|
135
136
|
this.subscribedTopics.get(_msg.event) ||
|
|
136
137
|
(this.wildcarded && this.subscribedTopics.get(this.wildcarded(_msg.event)))
|
|
137
|
-
if (!subscribedEvent
|
|
138
|
-
|
|
138
|
+
if (!subscribedEvent && !this._listenToAll)
|
|
139
|
+
throw new Error(`No handler for incoming message with topic '${_msg.event}' found.`)
|
|
140
|
+
_msg.event = subscribedEvent || _msg.event
|
|
139
141
|
}
|
|
140
142
|
return _msg
|
|
141
143
|
}
|
package/libx/odata/cqn2odata.js
CHANGED
|
@@ -516,12 +516,15 @@ const _select = (cqn, kind, model) => {
|
|
|
516
516
|
const _insert = (cqn, kind, model) => {
|
|
517
517
|
const INSERT = getProp(cqn, 'INSERT')
|
|
518
518
|
const { url } = _from(getProp(INSERT, 'into'), kind, model)
|
|
519
|
-
const body =
|
|
519
|
+
const body = _copyData(
|
|
520
|
+
Array.isArray(INSERT.entries) && INSERT.entries.length === 1 ? INSERT.entries[0] : INSERT.entries
|
|
521
|
+
)
|
|
520
522
|
return { method: 'POST', path: url, body }
|
|
521
523
|
}
|
|
522
524
|
|
|
523
525
|
const _copyData = data => {
|
|
524
526
|
// only works on flat structures
|
|
527
|
+
if (Array.isArray(data)) return data.map(_copyData)
|
|
525
528
|
const copied = {}
|
|
526
529
|
for (const property in data) {
|
|
527
530
|
copied[property] =
|
package/libx/odata/utils.js
CHANGED
|
@@ -52,10 +52,9 @@ const formatVal = (val, elementName, csnTarget, kind) => {
|
|
|
52
52
|
if (typeof val === 'boolean') return val
|
|
53
53
|
if (typeof val === 'number') return getSafeNumber(val)
|
|
54
54
|
if (!csnTarget && typeof val === 'string' && UUID.test(val)) return kind === 'odata-v2' ? `guid'${val}'` : val
|
|
55
|
-
const
|
|
56
|
-
|
|
55
|
+
const element = _getElement(csnTarget, elementName)
|
|
57
56
|
if (kind === 'odata-v2') {
|
|
58
|
-
switch (type) {
|
|
57
|
+
switch (element.type) {
|
|
59
58
|
case 'cds.Decimal':
|
|
60
59
|
case 'cds.Integer64':
|
|
61
60
|
case 'cds.Int64':
|
|
@@ -64,20 +63,22 @@ const formatVal = (val, elementName, csnTarget, kind) => {
|
|
|
64
63
|
case 'cds.LargeBinary':
|
|
65
64
|
return `binary'${toBase64url(val)}'`
|
|
66
65
|
case 'cds.Date':
|
|
67
|
-
return
|
|
66
|
+
return element['@odata.Type'] === 'Edm.DateTimeOffset'
|
|
67
|
+
? `datetimeoffset'${val}T00:00:00'`
|
|
68
|
+
: `datetime'${val}T00:00:00'`
|
|
68
69
|
case 'cds.DateTime':
|
|
69
|
-
return `datetime'${val}'`
|
|
70
|
+
return element['@odata.Type'] === 'Edm.DateTimeOffset' ? `datetimeoffset'${val}'` : `datetime'${val}'`
|
|
70
71
|
case 'cds.Time':
|
|
71
72
|
return `time'${_PT(val.split(':'))}'`
|
|
72
73
|
case 'cds.Timestamp':
|
|
73
|
-
return `datetimeoffset'${val}'`
|
|
74
|
+
return element['@odata.Type'] === 'Edm.DateTime' ? `datetime'${val}'` : `datetimeoffset'${val}'`
|
|
74
75
|
case 'cds.UUID':
|
|
75
76
|
return `guid'${val}'`
|
|
76
77
|
default:
|
|
77
78
|
return `'${val}'`
|
|
78
79
|
}
|
|
79
80
|
} else {
|
|
80
|
-
switch (type) {
|
|
81
|
+
switch (element.type) {
|
|
81
82
|
case 'cds.Binary':
|
|
82
83
|
case 'cds.LargeBinary':
|
|
83
84
|
return `binary'${toBase64url(val)}'`
|