@sap/cds 7.1.1 → 7.2.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 +68 -4
- package/apis/cds.d.ts +10 -6
- package/apis/connect.d.ts +0 -1
- package/apis/core.d.ts +54 -5
- package/apis/log.d.ts +19 -6
- package/apis/models.d.ts +0 -18
- package/apis/ql.d.ts +23 -23
- package/apis/serve.d.ts +17 -14
- package/apis/services.d.ts +40 -29
- package/apis/test.d.ts +1 -2
- package/bin/serve.js +4 -4
- package/lib/auth/basic-auth.js +1 -1
- package/lib/auth/dummy-auth.js +2 -1
- package/lib/auth/ias-auth.js +68 -2
- package/lib/auth/index.js +5 -5
- package/lib/auth/jwt-auth.js +40 -24
- package/lib/auth/mocked-users.js +0 -13
- package/lib/auth/passport-basic.js +2 -0
- package/lib/auth/passport-digest.js +2 -0
- package/lib/compile/etc/_localized.js +0 -1
- package/lib/compile/extend.js +16 -0
- package/lib/compile/for/lean_drafts.js +38 -6
- package/lib/compile/resolve.js +7 -5
- package/lib/compile/to/json.js +6 -2
- package/lib/dbs/cds-deploy.js +4 -4
- package/lib/env/cds-env.js +3 -3
- package/lib/env/cds-requires.js +1 -0
- package/lib/env/defaults.js +8 -1
- package/lib/env/schemas/cds-rc.json +27 -3
- package/lib/i18n/localize.js +3 -3
- package/lib/index.js +4 -0
- package/lib/log/cds-log.js +10 -1
- package/lib/ql/Whereable.js +7 -3
- package/lib/req/user.js +18 -16
- package/lib/srv/middlewares/sap-statistics.js +3 -3
- package/lib/srv/middlewares/trace.js +5 -4
- package/lib/srv/srv-dispatch.js +10 -9
- package/lib/utils/axios.js +3 -0
- package/lib/utils/cds-test.js +3 -0
- package/lib/utils/cds-utils.js +2 -0
- package/libx/_runtime/auth/index.js +8 -32
- package/libx/_runtime/auth/strategies/ias-auth.js +1 -77
- package/libx/_runtime/auth/strategies/mock.js +1 -12
- package/libx/_runtime/auth/strategies/xssecUtils.js +2 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +3 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +11 -9
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/ExpressionToCQN.js +5 -0
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/applyToCQN.js +5 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/UriParser.js +4 -0
- package/libx/_runtime/cds-services/services/utils/compareJson.js +0 -9
- package/libx/_runtime/cds-services/services/utils/differ.js +8 -10
- package/libx/_runtime/common/composition/data.js +10 -7
- package/libx/_runtime/common/composition/insert.js +9 -5
- package/libx/_runtime/common/composition/update.js +18 -12
- package/libx/_runtime/common/error/constants.js +6 -1
- package/libx/_runtime/common/generic/auth/requires.js +11 -3
- package/libx/_runtime/common/generic/auth/restrict.js +22 -16
- package/libx/_runtime/common/generic/auth/restrictions.js +5 -2
- package/libx/_runtime/common/generic/crud.js +6 -0
- package/libx/_runtime/common/generic/paging.js +2 -1
- package/libx/_runtime/common/utils/cqn2cqn4sql.js +3 -5
- package/libx/_runtime/common/utils/resolveView.js +3 -1
- package/libx/_runtime/common/utils/restrictions.js +47 -0
- package/libx/_runtime/db/data-conversion/post-processing.js +3 -3
- package/libx/_runtime/db/generic/input.js +1 -1
- package/libx/_runtime/db/sql-builder/ExpressionBuilder.js +0 -17
- package/libx/_runtime/db/utils/coloredTxCommands.js +5 -3
- package/libx/_runtime/fiori/lean-draft.js +24 -19
- package/libx/_runtime/hana/driver.js +2 -4
- package/libx/_runtime/hana/pool.js +53 -57
- package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +2 -2
- package/libx/_runtime/messaging/outbox/utils.js +1 -2
- package/libx/_runtime/remote/utils/client.js +1 -1
- package/libx/_runtime/sqlite/Service.js +0 -4
- package/libx/_runtime/sqlite/customBuilder/CustomFunctionBuilder.js +2 -1
- package/libx/odata/afterburner.js +6 -4
- package/libx/odata/cqn2odata.js +7 -7
- package/libx/odata/utils.js +4 -1
- package/libx/rest/RestAdapter.js +15 -16
- package/package.json +1 -1
- package/lib/auth/xsuaa-auth.js +0 -2
- package/libx/_runtime/auth/utils.js +0 -32
- package/libx/audit-log/client.cds +0 -0
- package/libx/audit-log/client.js +0 -0
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const cds = require('../../../../cds')
|
|
2
|
+
|
|
1
3
|
const odata = require('../okra/odata-server')
|
|
2
4
|
const ExpressionKind = odata.uri.Expression.ExpressionKind
|
|
3
5
|
const BinaryOperatorKind = odata.uri.BinaryExpression.OperatorKind
|
|
@@ -41,7 +43,10 @@ class ExpressionToCQN {
|
|
|
41
43
|
case EdmPrimitiveTypeKind.Int16:
|
|
42
44
|
case EdmPrimitiveTypeKind.Int32:
|
|
43
45
|
return { val: parseInt(value) }
|
|
46
|
+
case EdmPrimitiveTypeKind.Int64:
|
|
47
|
+
return { val: value.toString() }
|
|
44
48
|
case EdmPrimitiveTypeKind.Decimal:
|
|
49
|
+
return cds.env.features.compat_decimal ? { val: parseFloat(value) } : { val: value.toString() }
|
|
45
50
|
case EdmPrimitiveTypeKind.Single:
|
|
46
51
|
case EdmPrimitiveTypeKind.Double:
|
|
47
52
|
return { val: parseFloat(value) }
|
|
@@ -225,8 +225,11 @@ const applyToCQN = (transformations, entity, model) => {
|
|
|
225
225
|
case TransformationKind.BOTTOM_TOP:
|
|
226
226
|
_addBottomTopTransformation(transformation, res)
|
|
227
227
|
break
|
|
228
|
-
default:
|
|
229
|
-
|
|
228
|
+
default: {
|
|
229
|
+
const numericKind = transformation.getKind()
|
|
230
|
+
const stringKind = Object.entries(TransformationKind).find(([_, v]) => v === numericKind)?.[0]
|
|
231
|
+
throw getFeatureNotSupportedError(`Transformation "${stringKind || numericKind}" with query option $apply`)
|
|
232
|
+
}
|
|
230
233
|
}
|
|
231
234
|
}
|
|
232
235
|
|
|
@@ -175,6 +175,10 @@ class UriParser {
|
|
|
175
175
|
* @param {UriInfo} uriInfo the result of parsing
|
|
176
176
|
*/
|
|
177
177
|
parseQueryOptions (queryOptions, uriInfo) {
|
|
178
|
+
// EXPERIMENTAL FEATURE FLAGS!
|
|
179
|
+
const { okra_skip_query_options, odata_new_parser } = global.cds.env.features
|
|
180
|
+
if (okra_skip_query_options && odata_new_parser) return
|
|
181
|
+
|
|
178
182
|
const lastSegment = uriInfo.getLastSegment()
|
|
179
183
|
const crossjoinEntitySets = lastSegment.getCrossjoinEntitySets()
|
|
180
184
|
const aliases = uriInfo.getAliases()
|
|
@@ -133,11 +133,6 @@ const _addToBeDeletedEntriesToResult = (results, entity, keys, newValues, oldVal
|
|
|
133
133
|
|
|
134
134
|
const _normalizeToArray = value => (Array.isArray(value) ? value : value === null ? [] : [value])
|
|
135
135
|
|
|
136
|
-
const _addKeysToEntryIfNotExists = (keys, newEntry) => {
|
|
137
|
-
if (!newEntry) return
|
|
138
|
-
for (const key of keys) if (!(key in newEntry)) newEntry[key] = undefined
|
|
139
|
-
}
|
|
140
|
-
|
|
141
136
|
const _isUnManaged = element => {
|
|
142
137
|
return element.on && !element._isSelfManaged
|
|
143
138
|
}
|
|
@@ -207,11 +202,7 @@ const compareJsonDeep = (entity, newValue = [], oldValue = [], opts) => {
|
|
|
207
202
|
for (const newEntry of newValues) {
|
|
208
203
|
const result = {}
|
|
209
204
|
const oldEntry = _getCorrespondingEntryWithSameKeys(oldValues, newEntry, keys)
|
|
210
|
-
|
|
211
|
-
_addKeysToEntryIfNotExists(keys, newEntry)
|
|
212
|
-
|
|
213
205
|
_iteratePropsInNewEntry(newEntry, keys, result, oldEntry, entity, opts)
|
|
214
|
-
|
|
215
206
|
resultsArray.push(result)
|
|
216
207
|
}
|
|
217
208
|
|
|
@@ -37,16 +37,14 @@ module.exports = class Differ {
|
|
|
37
37
|
return columns
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
_diffDelete(req) {
|
|
40
|
+
async _diffDelete(req) {
|
|
41
41
|
const { DELETE } = cds.env.fiori.lean_draft ? req.query : (req._ && req._.query) || req.query
|
|
42
42
|
const target = DELETE._transitions?.[DELETE._transitions.length - 1]?.target || req.target
|
|
43
43
|
const query = SELECT.from(DELETE.from).columns(this._createSelectColumnsForDelete(target))
|
|
44
44
|
if (DELETE.where) query.where(DELETE.where)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
.run(query)
|
|
49
|
-
.then(dbState => compareJson(undefined, dbState, req.target, { ignoreDraftColumns: true }))
|
|
45
|
+
const dbState = await cds.tx(req).run(query)
|
|
46
|
+
const diff = compareJson(undefined, dbState, req.target, { ignoreDraftColumns: true })
|
|
47
|
+
return diff
|
|
50
48
|
}
|
|
51
49
|
|
|
52
50
|
async _addPartialPersistentState(req) {
|
|
@@ -62,7 +60,8 @@ module.exports = class Differ {
|
|
|
62
60
|
enrichDataWithKeysFromWhere(combinedData, req, this._srv)
|
|
63
61
|
const lastTransition = newQuery.UPDATE._transitions[newQuery.UPDATE._transitions.length - 1]
|
|
64
62
|
const revertedPersistent = revertData(req._.partialPersistentState, lastTransition, this._srv)
|
|
65
|
-
|
|
63
|
+
const diff = compareJson(combinedData, revertedPersistent, req.target, { ignoreDraftColumns: true })
|
|
64
|
+
return diff
|
|
66
65
|
}
|
|
67
66
|
|
|
68
67
|
async _diffPatch(req, providedData) {
|
|
@@ -87,10 +86,9 @@ module.exports = class Differ {
|
|
|
87
86
|
providedData || (req.query.INSERT.entries && req.query.INSERT.entries.length === 1)
|
|
88
87
|
? req.query.INSERT.entries[0]
|
|
89
88
|
: req.query.INSERT.entries
|
|
90
|
-
|
|
91
89
|
enrichDataWithKeysFromWhere(originalData, req, this._srv)
|
|
92
|
-
|
|
93
|
-
return
|
|
90
|
+
const diff = compareJson(originalData, undefined, req.target, { ignoreDraftColumns: true })
|
|
91
|
+
return diff
|
|
94
92
|
}
|
|
95
93
|
|
|
96
94
|
async calculate(req, providedData) {
|
|
@@ -29,6 +29,7 @@ const _isSameEntityInWhere = (where, target, persistentObj) => {
|
|
|
29
29
|
const key = where[i].ref
|
|
30
30
|
const val = where[i + 2].val
|
|
31
31
|
const sign = where[i + 1]
|
|
32
|
+
|
|
32
33
|
// eslint-disable-next-line
|
|
33
34
|
if (target.elements[key].key && key in persistentObj && sign === '=' && val !== persistentObj[key]) {
|
|
34
35
|
return false
|
|
@@ -133,7 +134,7 @@ const _subData = (data, prop) =>
|
|
|
133
134
|
data.reduce((result, entry) => {
|
|
134
135
|
if (prop in entry) {
|
|
135
136
|
const elementValue = ctUtils.val(entry[prop])
|
|
136
|
-
|
|
137
|
+
for (const val of ctUtils.array(elementValue)) result.push(val)
|
|
137
138
|
}
|
|
138
139
|
return result
|
|
139
140
|
}, [])
|
|
@@ -196,7 +197,8 @@ const _mergeResults = (result, selectData, root, model, compositionTree, entityN
|
|
|
196
197
|
if (newData[0]) selectEntry[compositionTree.name] = Object.assign(selectEntry[compositionTree.name], newData[0])
|
|
197
198
|
else selectEntry[compositionTree.name] = null
|
|
198
199
|
} else if (assoc.is2many) {
|
|
199
|
-
selectEntry[compositionTree.name]
|
|
200
|
+
const entry = selectEntry[compositionTree.name]
|
|
201
|
+
for (const val of newData) entry.push(val)
|
|
200
202
|
}
|
|
201
203
|
|
|
202
204
|
return selectEntry
|
|
@@ -254,6 +256,7 @@ const _select = ({
|
|
|
254
256
|
const _selectDeepUpdateData = async args => {
|
|
255
257
|
const { model, compositionTree, entityName, data, root, selectData, tx, selectAllColumns, where, parentKeys } = args
|
|
256
258
|
let result = []
|
|
259
|
+
|
|
257
260
|
if (!where && parentKeys && parentKeys.length && Object.keys(parentKeys[0]).length) {
|
|
258
261
|
const keys0 = Object.keys(parentKeys[0])
|
|
259
262
|
const keys = { list: keys0.map(pk => ({ ref: [pk] })) }
|
|
@@ -263,13 +266,13 @@ const _selectDeepUpdateData = async args => {
|
|
|
263
266
|
}
|
|
264
267
|
const _args = { ...args, where: [keys, 'in', values], parentKeys: undefined }
|
|
265
268
|
const selectCQN = _select(_args)
|
|
266
|
-
result.
|
|
269
|
+
result = result.concat(await tx.run(selectCQN))
|
|
267
270
|
}
|
|
268
271
|
} else if (where && !Array.isArray(where)) {
|
|
269
272
|
for (let w of Object.values(where)) {
|
|
270
273
|
const _args = { ...args, where: w }
|
|
271
274
|
const selectCQN = _select(_args)
|
|
272
|
-
result.
|
|
275
|
+
result = result.concat(await tx.run(selectCQN))
|
|
273
276
|
}
|
|
274
277
|
} else {
|
|
275
278
|
const selectCQN = _select(args)
|
|
@@ -295,9 +298,7 @@ const _selectDeepUpdateData = async args => {
|
|
|
295
298
|
root: false
|
|
296
299
|
}
|
|
297
300
|
|
|
298
|
-
// REVISIT: remove null elements
|
|
299
|
-
subs.data = subs.data.filter(d => d)
|
|
300
|
-
|
|
301
|
+
subs.data = subs.data.filter(d => d) // REVISIT: remove null elements
|
|
301
302
|
return _selectDeepUpdateData({ ...args, ...subs })
|
|
302
303
|
})
|
|
303
304
|
)
|
|
@@ -309,6 +310,7 @@ const _selectDeepUpdateData = async args => {
|
|
|
309
310
|
const _resolveOrderBy = (orderBy, transitions) => {
|
|
310
311
|
// no resolved entity found
|
|
311
312
|
if (!transitions?.length) return
|
|
313
|
+
|
|
312
314
|
// if there are no renamed fields, no need to resolve
|
|
313
315
|
if (!transitions[0].mapping.size) return
|
|
314
316
|
if (orderBy) orderBy.map(el => (el.ref[0] = transitions[0].mapping.get(el.ref[0]).ref[0]))
|
|
@@ -320,6 +322,7 @@ const _resolveOrderBy = (orderBy, transitions) => {
|
|
|
320
322
|
|
|
321
323
|
const selectDeepUpdateData = (service, model, req, selectAllColumns = false) => {
|
|
322
324
|
const query = req.query
|
|
325
|
+
|
|
323
326
|
// REVISIT this should be done somewhere before, so it is not done twice for deep updates
|
|
324
327
|
const sqlQuery = cqn2cqn4sql(query, model)
|
|
325
328
|
|
|
@@ -17,9 +17,8 @@ function _hasCompOrAssoc(entity, k) {
|
|
|
17
17
|
|
|
18
18
|
const _addSubDeepInsertCQN = (model, compositionTree, data, cqns, draft) => {
|
|
19
19
|
compositionTree.compositionElements.forEach(element => {
|
|
20
|
-
if (element.skipPersistence)
|
|
21
|
-
|
|
22
|
-
}
|
|
20
|
+
if (element.skipPersistence) return
|
|
21
|
+
|
|
23
22
|
// element source must be changed in comp tree
|
|
24
23
|
const subEntity = model.definitions[element.source]
|
|
25
24
|
const into = ctUtils.addDraftSuffix(draft, subEntity.name)
|
|
@@ -32,20 +31,25 @@ const _addSubDeepInsertCQN = (model, compositionTree, data, cqns, draft) => {
|
|
|
32
31
|
const subData = ctUtils.array(elementValue).filter(ele => Object.keys(ele).length > 0)
|
|
33
32
|
if (subData.length > 0) {
|
|
34
33
|
// REVISIT: this can make problems
|
|
35
|
-
insertCQN.INSERT.entries
|
|
36
|
-
|
|
34
|
+
const entries = insertCQN.INSERT.entries
|
|
35
|
+
for (const data of ctUtils.cleanDeepData(subEntity, subData)) entries.push(data)
|
|
36
|
+
for (const data of subData) result.push(data)
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
|
+
|
|
40
41
|
return result
|
|
41
42
|
}, [])
|
|
43
|
+
|
|
42
44
|
if (insertCQN.INSERT.entries.length > 0) {
|
|
43
45
|
cqns.push(insertCQN)
|
|
44
46
|
}
|
|
47
|
+
|
|
45
48
|
if (subData.length > 0) {
|
|
46
49
|
_addSubDeepInsertCQN(model, element, subData, cqns, draft)
|
|
47
50
|
}
|
|
48
51
|
})
|
|
52
|
+
|
|
49
53
|
return cqns
|
|
50
54
|
}
|
|
51
55
|
|
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
const cds = require('../../cds')
|
|
2
|
-
|
|
3
2
|
const { getCompositionTree } = require('./tree')
|
|
4
3
|
const { getDeepInsertCQNs } = require('./insert')
|
|
5
4
|
const { getDeepDeleteCQNs } = require('./delete')
|
|
6
5
|
const ctUtils = require('./utils')
|
|
7
|
-
|
|
8
6
|
const { ensureNoDraftsSuffix } = require('../utils/draft')
|
|
9
7
|
const { deepCopyObject } = require('../utils/copy')
|
|
10
|
-
|
|
11
8
|
const getError = require('../../common/error')
|
|
12
9
|
const { getEntityNameFromUpdateCQN } = require('../utils/cqn')
|
|
13
10
|
|
|
@@ -31,28 +28,35 @@ const _serializedKey = (entity, data) => {
|
|
|
31
28
|
|
|
32
29
|
const _dataByKey = (entity, data) => {
|
|
33
30
|
const dataByKey = new Map()
|
|
31
|
+
|
|
34
32
|
for (const entry of data) {
|
|
35
33
|
dataByKey.set(_serializedKey(entity, entry), entry)
|
|
36
34
|
}
|
|
35
|
+
|
|
37
36
|
return dataByKey
|
|
38
37
|
}
|
|
39
38
|
|
|
40
39
|
function _addSubDeepUpdateCQNForDelete({ entity, data, selectData, entityName, deleteCQNs }) {
|
|
41
40
|
const dataByKey = _dataByKey(entity, data)
|
|
41
|
+
|
|
42
42
|
if (selectData.length && selectData[0] && Object.keys(selectData[0]).length) {
|
|
43
43
|
for (let j = 0; j < selectData.length; j += CHUNK_SIZE) {
|
|
44
44
|
const deleteCQN = { DELETE: { from: entityName, where: [] } }
|
|
45
|
+
|
|
45
46
|
for (let i = j; i < j + CHUNK_SIZE && i < selectData.length; i++) {
|
|
46
47
|
const selectEntry = selectData[i]
|
|
47
48
|
if (!selectEntry) continue
|
|
49
|
+
|
|
48
50
|
const dataEntry = dataByKey.get(_serializedKey(entity, selectEntry))
|
|
49
51
|
if (!dataEntry) {
|
|
50
52
|
if (deleteCQN.DELETE.where.length > 0) {
|
|
51
53
|
deleteCQN.DELETE.where.push('or')
|
|
52
54
|
}
|
|
55
|
+
|
|
53
56
|
deleteCQN.DELETE.where.push({ xpr: [...ctUtils.whereKey(ctUtils.key(entity, selectEntry))] })
|
|
54
57
|
}
|
|
55
58
|
}
|
|
59
|
+
|
|
56
60
|
if (deleteCQN.DELETE.where.length) deleteCQNs.push(deleteCQN)
|
|
57
61
|
}
|
|
58
62
|
}
|
|
@@ -63,6 +67,7 @@ const _unwrapVal = obj => {
|
|
|
63
67
|
const value = obj[key]
|
|
64
68
|
if (value && value.val) obj[key] = value.val
|
|
65
69
|
}
|
|
70
|
+
|
|
66
71
|
return obj
|
|
67
72
|
}
|
|
68
73
|
|
|
@@ -120,6 +125,7 @@ const _diffData = (newData, oldData, entity, newEntry, oldEntry, model) => {
|
|
|
120
125
|
function _addSubDeepUpdateCQNForUpdateInsert({ entity, entityName, data, selectData, updateCQNs, insertCQN, model }) {
|
|
121
126
|
const selectDataByKey = _dataByKey(entity, selectData)
|
|
122
127
|
const deepUpdateData = []
|
|
128
|
+
|
|
123
129
|
for (const entry of data) {
|
|
124
130
|
if (entry === null) continue
|
|
125
131
|
|
|
@@ -138,26 +144,27 @@ function _addSubDeepUpdateCQNForUpdateInsert({ entity, entityName, data, selectD
|
|
|
138
144
|
// inserts are handled deep so they must not be put into deepUpdateData
|
|
139
145
|
}
|
|
140
146
|
}
|
|
147
|
+
|
|
141
148
|
return deepUpdateData
|
|
142
149
|
}
|
|
143
150
|
|
|
144
151
|
async function _addSubDeepUpdateCQNCollect(model, cqns, updateCQNs, insertCQN, deleteCQNs, req) {
|
|
145
152
|
if (updateCQNs.length > 0) {
|
|
146
153
|
cqns[0] = cqns[0] || []
|
|
147
|
-
cqns[0]
|
|
154
|
+
const cqn = cqns[0]
|
|
155
|
+
for (const updateCQN of updateCQNs) cqn.push(updateCQN)
|
|
148
156
|
}
|
|
149
157
|
|
|
150
158
|
if (insertCQN.INSERT.entries.length > 0) {
|
|
151
159
|
cqns[0] = cqns[0] || []
|
|
152
160
|
const deepInsertCQNs = getDeepInsertCQNs(model, insertCQN)
|
|
153
161
|
deepInsertCQNs.forEach(insertCQN => {
|
|
154
|
-
const intoCQN = cqns[0].find(cqn =>
|
|
155
|
-
return cqn.INSERT && cqn.INSERT.into === insertCQN.INSERT.into
|
|
156
|
-
})
|
|
162
|
+
const intoCQN = cqns[0].find(cqn => cqn.INSERT?.into === insertCQN.INSERT.into)
|
|
157
163
|
if (!intoCQN) {
|
|
158
164
|
cqns[0].push(insertCQN)
|
|
159
165
|
} else {
|
|
160
|
-
intoCQN.INSERT.entries
|
|
166
|
+
const intoCQNEntries = intoCQN.INSERT.entries
|
|
167
|
+
for (const entry of insertCQN.INSERT.entries) intoCQNEntries.push(entry)
|
|
161
168
|
}
|
|
162
169
|
})
|
|
163
170
|
}
|
|
@@ -182,7 +189,7 @@ const _addToData = (subData, entity, element, entry) => {
|
|
|
182
189
|
const value = ctUtils.val(entry[element.name])
|
|
183
190
|
const subDataEntries = ctUtils.array(value)
|
|
184
191
|
const unwrappedSubData = subDataEntries.map(entry => _unwrapIfNotArray(entry))
|
|
185
|
-
subData.push(
|
|
192
|
+
for (const val of unwrappedSubData) subData.push(val)
|
|
186
193
|
}
|
|
187
194
|
|
|
188
195
|
async function _addSubDeepUpdateCQNRecursion({ model, compositionTree, entity, data, selectData, cqns, draft, req }) {
|
|
@@ -200,6 +207,7 @@ async function _addSubDeepUpdateCQNRecursion({ model, compositionTree, entity, d
|
|
|
200
207
|
if (selectEntry[element.name] === null && entry[element.name] === null) {
|
|
201
208
|
continue
|
|
202
209
|
}
|
|
210
|
+
|
|
203
211
|
_addToData(selectSubData, entity, element, selectEntry)
|
|
204
212
|
}
|
|
205
213
|
|
|
@@ -244,7 +252,6 @@ const _addSubDeepUpdateCQN = async ({ model, compositionTree, data, selectData,
|
|
|
244
252
|
})
|
|
245
253
|
|
|
246
254
|
await _addSubDeepUpdateCQNCollect(model, cqns, updateCQNs, insertCQN, deleteCQNs, req)
|
|
247
|
-
|
|
248
255
|
if (deepUpdateData.length === 0) return Promise.resolve()
|
|
249
256
|
|
|
250
257
|
return _addSubDeepUpdateCQNRecursion({
|
|
@@ -280,7 +287,6 @@ const hasDeepUpdate = (model, cqn) => {
|
|
|
280
287
|
const getDeepUpdateCQNs = async (model, req, selectData) => {
|
|
281
288
|
const { query } = req
|
|
282
289
|
if (!Array.isArray(selectData)) selectData = [selectData]
|
|
283
|
-
|
|
284
290
|
if (selectData.length === 0) return []
|
|
285
291
|
if (selectData.length > 1) throw getError('Deep update can only be performed on a single instance')
|
|
286
292
|
|
|
@@ -310,7 +316,7 @@ const getDeepUpdateCQNs = async (model, req, selectData) => {
|
|
|
310
316
|
})
|
|
311
317
|
subCQNs.forEach((subCQNs, index) => {
|
|
312
318
|
cqns[index] = cqns[index] || []
|
|
313
|
-
cqns[index].push(
|
|
319
|
+
for (const cqn of subCQNs) cqns[index].push(cqn)
|
|
314
320
|
})
|
|
315
321
|
|
|
316
322
|
// remove empty updates and inserts
|
|
@@ -11,5 +11,10 @@ module.exports = {
|
|
|
11
11
|
DEFAULT_SEVERITY: 2,
|
|
12
12
|
MIN_SEVERITY: 1,
|
|
13
13
|
MAX_SEVERITY: 4,
|
|
14
|
-
MULTIPLE_ERRORS: 'MULTIPLE_ERRORS'
|
|
14
|
+
MULTIPLE_ERRORS: 'MULTIPLE_ERRORS',
|
|
15
|
+
/*
|
|
16
|
+
* OData
|
|
17
|
+
*/
|
|
18
|
+
ODATA_UNAUTHORIZED: { statusCode: 401, code: '401', message: 'Unauthorized' },
|
|
19
|
+
ODATA_FORBIDDEN: { statusCode: 403, code: '403', message: 'Forbidden' }
|
|
15
20
|
}
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
const { reject, getRejectReason, getAuthRelevantEntity } = require('./utils')
|
|
2
2
|
const { CRUD_EVENTS } = require('./constants')
|
|
3
3
|
|
|
4
|
-
const
|
|
4
|
+
const _getRequiresAsArray = definition =>
|
|
5
|
+
definition['@requires']
|
|
6
|
+
? Array.isArray(definition['@requires'])
|
|
7
|
+
? definition['@requires']
|
|
8
|
+
: [definition['@requires']]
|
|
9
|
+
: false
|
|
5
10
|
|
|
6
11
|
function handler(req) {
|
|
7
12
|
if (req.user._is_privileged) {
|
|
@@ -13,7 +18,7 @@ function handler(req) {
|
|
|
13
18
|
if (req.event in CRUD_EVENTS) {
|
|
14
19
|
// > CRUD
|
|
15
20
|
definition = getAuthRelevantEntity(req, this.model, ['@requires', '@restrict'])
|
|
16
|
-
} else if (req.target
|
|
21
|
+
} else if (req.target?.actions) {
|
|
17
22
|
// > bound
|
|
18
23
|
definition = req.target.actions[req.event]
|
|
19
24
|
} else {
|
|
@@ -23,7 +28,10 @@ function handler(req) {
|
|
|
23
28
|
|
|
24
29
|
if (!definition) return
|
|
25
30
|
|
|
26
|
-
|
|
31
|
+
// also check target entity for bound operations
|
|
32
|
+
const requires =
|
|
33
|
+
_getRequiresAsArray(definition) ||
|
|
34
|
+
(['action', 'function'].includes(definition.kind) && req.target && _getRequiresAsArray(req.target))
|
|
27
35
|
if (!requires || requires.some(role => req.user.is(role))) return
|
|
28
36
|
|
|
29
37
|
reject(req, getRejectReason(req, '@requires', definition))
|
|
@@ -37,11 +37,9 @@ const _getResolvedApplicables = (applicables, req) => {
|
|
|
37
37
|
return resolvedApplicables
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
const
|
|
41
|
-
return (
|
|
42
|
-
|
|
43
|
-
resolvedApplicables[0]._xpr.length === 3 &&
|
|
44
|
-
resolvedApplicables[0]._xpr.every(ele => typeof ele !== 'object' || ele.val)
|
|
40
|
+
const _getStaticAuthRestrictions = resolvedApplicables => {
|
|
41
|
+
return resolvedApplicables.filter(
|
|
42
|
+
resolved => resolved && resolved._xpr.length === 3 && resolved._xpr.every(ele => typeof ele !== 'object' || ele.val)
|
|
45
43
|
)
|
|
46
44
|
}
|
|
47
45
|
|
|
@@ -68,14 +66,14 @@ const _evalStatic = (op, vals) => {
|
|
|
68
66
|
}
|
|
69
67
|
}
|
|
70
68
|
|
|
71
|
-
const
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
69
|
+
const _handleStaticAuthRestrictions = (resolvedApplicables, req) => {
|
|
70
|
+
const isAllowed = resolvedApplicables.some(restriction => {
|
|
71
|
+
const op = restriction._xpr.find(ele => typeof ele === 'string')
|
|
72
|
+
const vals = restriction._xpr.filter(ele => typeof ele === 'object' && ele.val).map(ele => ele.val)
|
|
73
|
+
return _evalStatic(op, vals)
|
|
74
|
+
})
|
|
75
|
+
// static clause grants access => done
|
|
76
|
+
if (isAllowed) return
|
|
79
77
|
|
|
80
78
|
// static clause forbids access => forbidden
|
|
81
79
|
return reject(req)
|
|
@@ -121,7 +119,7 @@ const _addWheresToRef = (ref, model, resolvedApplicables) => {
|
|
|
121
119
|
newIdentifier.where = [{ xpr: newIdentifier.where }, 'and']
|
|
122
120
|
}
|
|
123
121
|
|
|
124
|
-
newIdentifier.where.push(
|
|
122
|
+
for (const val of _getMergedWhere(applicablesForEntity)) newIdentifier.where.push(val)
|
|
125
123
|
}
|
|
126
124
|
|
|
127
125
|
newRef.push(newIdentifier)
|
|
@@ -224,8 +222,15 @@ async function handler(req) {
|
|
|
224
222
|
return
|
|
225
223
|
}
|
|
226
224
|
|
|
225
|
+
// READ UPDATE DELETE on draft enabled entities are unrestricted, because only the owner can access them
|
|
226
|
+
const draftUnRestrictedEvents = ['READ', 'UPDATE', 'DELETE', 'CREATE']
|
|
227
|
+
if (definition.isDraft && draftUnRestrictedEvents.includes(req.event)) {
|
|
228
|
+
return
|
|
229
|
+
}
|
|
230
|
+
|
|
227
231
|
let restrictions = this.getRestrictions.call(this, definition, req.event, req.user)
|
|
228
232
|
if (restrictions instanceof Promise) restrictions = await restrictions
|
|
233
|
+
|
|
229
234
|
if (!restrictions) {
|
|
230
235
|
// > unrestricted
|
|
231
236
|
return
|
|
@@ -243,8 +248,9 @@ async function handler(req) {
|
|
|
243
248
|
const resolvedApplicables = _getResolvedApplicables(restrictions, req)
|
|
244
249
|
|
|
245
250
|
// REVISIT: support more complex statics
|
|
246
|
-
|
|
247
|
-
|
|
251
|
+
const staticAuthRestriction = _getStaticAuthRestrictions(resolvedApplicables)
|
|
252
|
+
if (staticAuthRestriction.length > 0) {
|
|
253
|
+
return _handleStaticAuthRestrictions(staticAuthRestriction, req)
|
|
248
254
|
}
|
|
249
255
|
|
|
250
256
|
if (req.event === 'READ') {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const WRITE_EVENTS = { CREATE: 1, NEW: 1, UPDATE: 1, PATCH: 1, DELETE: 1, CANCEL: 1, EDIT: 1 }
|
|
2
2
|
const CRUD = Object.assign({ READ: 1 }, WRITE_EVENTS)
|
|
3
|
+
const cds = require('../../../cds')
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Returns the applicable restrictions for the current request as follows:
|
|
@@ -109,12 +110,14 @@ const getNormalizedRestrictions = (definition, definitions) => {
|
|
|
109
110
|
return isRestricted ? restricts : null
|
|
110
111
|
}
|
|
111
112
|
|
|
112
|
-
const _isGrantAccessAllowed = (eventName, restrict) =>
|
|
113
|
+
const _isGrantAccessAllowed = (eventName, restrict) =>
|
|
114
|
+
restrict.grant === '*' || (eventName === 'EDIT' && restrict.grant === 'UPDATE') || restrict.grant === eventName
|
|
115
|
+
|
|
113
116
|
const _isToAccessAllowed = (user, restrict) => restrict.to.some(role => user.is(role))
|
|
114
117
|
|
|
115
118
|
const getApplicableRestrictions = (restrictions, event, user) => {
|
|
116
119
|
return restrictions.filter(restrict => {
|
|
117
|
-
const eventName = { NEW: 'CREATE'
|
|
120
|
+
const eventName = cds.env.fiori.lean_draft ? event : { NEW: 'CREATE' }[event] || event
|
|
118
121
|
return _isGrantAccessAllowed(eventName, restrict) && _isToAccessAllowed(user, restrict)
|
|
119
122
|
})
|
|
120
123
|
}
|
|
@@ -14,6 +14,12 @@ const _targetEntityDoesNotExist = async req => {
|
|
|
14
14
|
exports.impl = cds.service.impl(function () {
|
|
15
15
|
// eslint-disable-next-line complexity
|
|
16
16
|
this.on(['CREATE', 'READ', 'UPDATE', 'DELETE', 'UPSERT'], '*', async function (req) {
|
|
17
|
+
if (!req.query) {
|
|
18
|
+
throw getError({
|
|
19
|
+
code: 501,
|
|
20
|
+
message: 'The request has no query and cannot be served generically.'
|
|
21
|
+
})
|
|
22
|
+
}
|
|
17
23
|
if (typeof req.query !== 'string' && req.target && req.target._hasPersistenceSkip) {
|
|
18
24
|
throw getError({
|
|
19
25
|
code: 501,
|
|
@@ -10,7 +10,8 @@ const MAX = cds.env.query?.limit?.max || 1000
|
|
|
10
10
|
const _cached = Symbol('@cds.query.limit')
|
|
11
11
|
|
|
12
12
|
const getPageSize = def => {
|
|
13
|
-
|
|
13
|
+
// do not look at prototypes re cached settings
|
|
14
|
+
if (Object.hasOwn(def, _cached)) return def[_cached]
|
|
14
15
|
let max = def['@cds.query.limit.max'] ?? def._service?.['@cds.query.limit.max'] ?? MAX
|
|
15
16
|
let _default =
|
|
16
17
|
def['@cds.query.limit.default'] ??
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
const cds = require('../../cds')
|
|
4
4
|
const { SELECT, INSERT, DELETE, UPDATE } = cds.ql
|
|
5
5
|
const Query = require('../../../../lib/ql/Query')
|
|
6
|
-
|
|
7
6
|
const { resolveView } = require('./resolveView')
|
|
8
7
|
const { ensureNoDraftsSuffix, getDraftColumnsCQNForDraft, ensureDraftsSuffix } = require('./draft')
|
|
9
8
|
const { flattenStructuredSelect, OPERATIONS_MAP } = require('./structured')
|
|
@@ -807,8 +806,7 @@ const _convertSelect = (query, model, _options) => {
|
|
|
807
806
|
const cols = getColumns(target, { onlyNames: true, filterVirtual: true })
|
|
808
807
|
query.columns(cols)
|
|
809
808
|
if (target._isDraftEnabled && query._target._unresolved) {
|
|
810
|
-
query.SELECT.columns.push(...getDraftColumnsCQNForDraft(target))
|
|
811
|
-
query.SELECT.columns.push({ ref: ['DraftAdministrativeData_DraftUUID'] })
|
|
809
|
+
query.SELECT.columns.push(...getDraftColumnsCQNForDraft(target), { ref: ['DraftAdministrativeData_DraftUUID'] })
|
|
812
810
|
}
|
|
813
811
|
}
|
|
814
812
|
}
|
|
@@ -822,7 +820,8 @@ const _convertUpsert = (query, model) => {
|
|
|
822
820
|
|
|
823
821
|
const target = model.definitions[resolvedIntoClause]
|
|
824
822
|
if (!target) {
|
|
825
|
-
// if there is no target, just return original query, as a copy is not deep anyways
|
|
823
|
+
// if there is no target, just return original query, as a copy is not deep anyways
|
|
824
|
+
// and all the sub items of query.UPSERT are referenced only anyways
|
|
826
825
|
return query
|
|
827
826
|
}
|
|
828
827
|
|
|
@@ -867,7 +866,6 @@ const _convertInsert = (query, model) => {
|
|
|
867
866
|
|
|
868
867
|
const target = model.definitions[resolvedIntoClause]
|
|
869
868
|
if (!target) return insert
|
|
870
|
-
|
|
871
869
|
return resolveView(insert, model, cds.db)
|
|
872
870
|
}
|
|
873
871
|
|
|
@@ -246,7 +246,8 @@ const _newWhereRef = (newWhereElement, transition, alias, tableName, isSubSelect
|
|
|
246
246
|
if (mapped) newRef[1] = mapped.ref[0]
|
|
247
247
|
} else {
|
|
248
248
|
const mapped = transition.mapping.get(newRef[0])
|
|
249
|
-
if (isSubSelect && mapped) {
|
|
249
|
+
if (isSubSelect && mapped && newRef.length === 1) {
|
|
250
|
+
// Add a table alias prefix only for not-yet-qualified refs
|
|
250
251
|
newRef.unshift(transition.target.name)
|
|
251
252
|
newRef[1] = mapped.ref[0]
|
|
252
253
|
} else {
|
|
@@ -734,6 +735,7 @@ const resolveView = (query, model, service) => {
|
|
|
734
735
|
// restore logger and clear _event
|
|
735
736
|
LOG = _LOG
|
|
736
737
|
_event = undefined
|
|
738
|
+
|
|
737
739
|
return newQuery
|
|
738
740
|
}
|
|
739
741
|
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
const cds = require('../../cds')
|
|
2
|
+
|
|
3
|
+
const containsAnyRestrictions = srv => {
|
|
4
|
+
const accessRestrictions = getAccessRestrictions(srv)
|
|
5
|
+
if (accessRestrictions.length > 1 || accessRestrictions[0] !== 'any') return true
|
|
6
|
+
|
|
7
|
+
const entities = srv.entities
|
|
8
|
+
const entitiesKeys = Object.keys(entities)
|
|
9
|
+
|
|
10
|
+
return !!(
|
|
11
|
+
entitiesKeys.some(entity => entities[entity]['@requires'] || entities[entity]['@restrict']) ||
|
|
12
|
+
entitiesKeys.some(entity => {
|
|
13
|
+
const actions = entities[entity].actions
|
|
14
|
+
actions && Object.keys(actions).some(action => actions[action]['@requires'] || actions[action]['@restrict'])
|
|
15
|
+
}) ||
|
|
16
|
+
Object.keys(srv.operations).some(
|
|
17
|
+
operation => srv.operations[operation]['@requires'] || srv.operations[operation]['@restrict']
|
|
18
|
+
)
|
|
19
|
+
)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const getAccessRestrictions = srv => {
|
|
23
|
+
let restrictions = srv.definition['@restrict'] || srv.definition['@requires']
|
|
24
|
+
if (restrictions) {
|
|
25
|
+
if (typeof restrictions === 'string') restrictions = [restrictions]
|
|
26
|
+
else
|
|
27
|
+
restrictions = restrictions
|
|
28
|
+
.map(r => (typeof r === 'string' ? r : r.to))
|
|
29
|
+
.reduce((acc, cur) => {
|
|
30
|
+
Array.isArray(cur) ? acc.push(...cur) : acc.push(cur)
|
|
31
|
+
return acc
|
|
32
|
+
}, [])
|
|
33
|
+
} else {
|
|
34
|
+
const { restrict_all_services } = cds.env.requires.auth
|
|
35
|
+
const in_prod = process.env.NODE_ENV === 'production'
|
|
36
|
+
// REVISIT: cleanup during streamlined auth
|
|
37
|
+
const is_mocked_auth = cds.env.requires.auth._kind === 'mocked'
|
|
38
|
+
if (restrict_all_services === false || !in_prod || is_mocked_auth) restrictions = ['any']
|
|
39
|
+
else restrictions = ['authenticated-user']
|
|
40
|
+
}
|
|
41
|
+
return restrictions
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
module.exports = {
|
|
45
|
+
containsAnyRestrictions,
|
|
46
|
+
getAccessRestrictions
|
|
47
|
+
}
|
|
@@ -132,9 +132,9 @@ const _getMapperForListedElements = (conversionMap, csn, cqn) => {
|
|
|
132
132
|
* @returns {Map<any, any>}
|
|
133
133
|
* @private
|
|
134
134
|
*/
|
|
135
|
-
const getPostProcessMapper = (conversionMap, csn
|
|
136
|
-
// No mapper defined or irrelevant as no READ request
|
|
137
|
-
if (!Object.prototype.hasOwnProperty.call(cqn, 'SELECT')) {
|
|
135
|
+
const getPostProcessMapper = (conversionMap, csn, cqn) => {
|
|
136
|
+
// No mapper defined or irrelevant as no CSN, CQN or READ request
|
|
137
|
+
if (!csn || !cqn || !Object.prototype.hasOwnProperty.call(cqn, 'SELECT')) {
|
|
138
138
|
return new Map()
|
|
139
139
|
}
|
|
140
140
|
|
|
@@ -138,7 +138,7 @@ const _pickCRUD = element => {
|
|
|
138
138
|
categories.push('!default')
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
-
if (element.default && !DRAFT_COLUMNS_MAP[element.name]) {
|
|
141
|
+
if (element.default && !DRAFT_COLUMNS_MAP[element.name] && !element.isAssociation) {
|
|
142
142
|
categories.push({ category: 'default', args: element })
|
|
143
143
|
}
|
|
144
144
|
|