@sap/cds 7.1.2 → 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 +55 -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 +3 -3
- 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/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/common/composition/data.js +5 -3
- package/libx/_runtime/common/composition/insert.js +6 -3
- package/libx/_runtime/common/composition/update.js +12 -8
- 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 +21 -15
- 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/fiori/lean-draft.js +6 -3
- package/libx/_runtime/hana/driver.js +2 -4
- package/libx/_runtime/hana/pool.js +1 -1
- 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 +5 -3
- 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
|
@@ -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()
|
|
@@ -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
|
|
@@ -255,6 +256,7 @@ const _select = ({
|
|
|
255
256
|
const _selectDeepUpdateData = async args => {
|
|
256
257
|
const { model, compositionTree, entityName, data, root, selectData, tx, selectAllColumns, where, parentKeys } = args
|
|
257
258
|
let result = []
|
|
259
|
+
|
|
258
260
|
if (!where && parentKeys && parentKeys.length && Object.keys(parentKeys[0]).length) {
|
|
259
261
|
const keys0 = Object.keys(parentKeys[0])
|
|
260
262
|
const keys = { list: keys0.map(pk => ({ ref: [pk] })) }
|
|
@@ -296,9 +298,7 @@ const _selectDeepUpdateData = async args => {
|
|
|
296
298
|
root: false
|
|
297
299
|
}
|
|
298
300
|
|
|
299
|
-
// REVISIT: remove null elements
|
|
300
|
-
subs.data = subs.data.filter(d => d)
|
|
301
|
-
|
|
301
|
+
subs.data = subs.data.filter(d => d) // REVISIT: remove null elements
|
|
302
302
|
return _selectDeepUpdateData({ ...args, ...subs })
|
|
303
303
|
})
|
|
304
304
|
)
|
|
@@ -310,6 +310,7 @@ const _selectDeepUpdateData = async args => {
|
|
|
310
310
|
const _resolveOrderBy = (orderBy, transitions) => {
|
|
311
311
|
// no resolved entity found
|
|
312
312
|
if (!transitions?.length) return
|
|
313
|
+
|
|
313
314
|
// if there are no renamed fields, no need to resolve
|
|
314
315
|
if (!transitions[0].mapping.size) return
|
|
315
316
|
if (orderBy) orderBy.map(el => (el.ref[0] = transitions[0].mapping.get(el.ref[0]).ref[0]))
|
|
@@ -321,6 +322,7 @@ const _resolveOrderBy = (orderBy, transitions) => {
|
|
|
321
322
|
|
|
322
323
|
const selectDeepUpdateData = (service, model, req, selectAllColumns = false) => {
|
|
323
324
|
const query = req.query
|
|
325
|
+
|
|
324
326
|
// REVISIT this should be done somewhere before, so it is not done twice for deep updates
|
|
325
327
|
const sqlQuery = cqn2cqn4sql(query, model)
|
|
326
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)
|
|
@@ -38,15 +37,19 @@ const _addSubDeepInsertCQN = (model, compositionTree, data, cqns, draft) => {
|
|
|
38
37
|
}
|
|
39
38
|
}
|
|
40
39
|
}
|
|
40
|
+
|
|
41
41
|
return result
|
|
42
42
|
}, [])
|
|
43
|
+
|
|
43
44
|
if (insertCQN.INSERT.entries.length > 0) {
|
|
44
45
|
cqns.push(insertCQN)
|
|
45
46
|
}
|
|
47
|
+
|
|
46
48
|
if (subData.length > 0) {
|
|
47
49
|
_addSubDeepInsertCQN(model, element, subData, cqns, draft)
|
|
48
50
|
}
|
|
49
51
|
})
|
|
52
|
+
|
|
50
53
|
return cqns
|
|
51
54
|
}
|
|
52
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,6 +144,7 @@ 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
|
|
|
@@ -152,9 +159,7 @@ async function _addSubDeepUpdateCQNCollect(model, cqns, updateCQNs, insertCQN, d
|
|
|
152
159
|
cqns[0] = cqns[0] || []
|
|
153
160
|
const deepInsertCQNs = getDeepInsertCQNs(model, insertCQN)
|
|
154
161
|
deepInsertCQNs.forEach(insertCQN => {
|
|
155
|
-
const intoCQN = cqns[0].find(cqn =>
|
|
156
|
-
return cqn.INSERT && cqn.INSERT.into === insertCQN.INSERT.into
|
|
157
|
-
})
|
|
162
|
+
const intoCQN = cqns[0].find(cqn => cqn.INSERT?.into === insertCQN.INSERT.into)
|
|
158
163
|
if (!intoCQN) {
|
|
159
164
|
cqns[0].push(insertCQN)
|
|
160
165
|
} else {
|
|
@@ -202,6 +207,7 @@ async function _addSubDeepUpdateCQNRecursion({ model, compositionTree, entity, d
|
|
|
202
207
|
if (selectEntry[element.name] === null && entry[element.name] === null) {
|
|
203
208
|
continue
|
|
204
209
|
}
|
|
210
|
+
|
|
205
211
|
_addToData(selectSubData, entity, element, selectEntry)
|
|
206
212
|
}
|
|
207
213
|
|
|
@@ -246,7 +252,6 @@ const _addSubDeepUpdateCQN = async ({ model, compositionTree, data, selectData,
|
|
|
246
252
|
})
|
|
247
253
|
|
|
248
254
|
await _addSubDeepUpdateCQNCollect(model, cqns, updateCQNs, insertCQN, deleteCQNs, req)
|
|
249
|
-
|
|
250
255
|
if (deepUpdateData.length === 0) return Promise.resolve()
|
|
251
256
|
|
|
252
257
|
return _addSubDeepUpdateCQNRecursion({
|
|
@@ -282,7 +287,6 @@ const hasDeepUpdate = (model, cqn) => {
|
|
|
282
287
|
const getDeepUpdateCQNs = async (model, req, selectData) => {
|
|
283
288
|
const { query } = req
|
|
284
289
|
if (!Array.isArray(selectData)) selectData = [selectData]
|
|
285
|
-
|
|
286
290
|
if (selectData.length === 0) return []
|
|
287
291
|
if (selectData.length > 1) throw getError('Deep update can only be performed on a single instance')
|
|
288
292
|
|
|
@@ -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)
|
|
@@ -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
|
|
|
@@ -157,23 +157,6 @@ class ExpressionBuilder extends BaseBuilder {
|
|
|
157
157
|
objects[i + 1].func = `not ${objects[i + 1].func}`
|
|
158
158
|
return 1
|
|
159
159
|
}
|
|
160
|
-
if (objects[i].func || (objects[i + 2] && objects[i + 2].func)) {
|
|
161
|
-
// sqlite requires leading 0 for numbers in datetime functions
|
|
162
|
-
const f = objects[i].func ? i : OPERATORS.has(objects[i + 1]) ? i + 2 : i - 2
|
|
163
|
-
const v = objects[i].val ? i : OPERATORS.has(objects[i + 1]) ? i + 2 : i - 2
|
|
164
|
-
if (
|
|
165
|
-
objects[f] &&
|
|
166
|
-
cds.db &&
|
|
167
|
-
((SQLITE_DATETIME_FUNCTIONS.has(objects[f].func) && cds.db.kind === 'sqlite') ||
|
|
168
|
-
(HANA_DATETIME_FUNCTIONS.has(objects[f].func) && cds.db.kind === 'hana'))
|
|
169
|
-
) {
|
|
170
|
-
if (objects[v] && objects[v].val !== undefined && typeof objects[v].val === 'number') {
|
|
171
|
-
objects[v] = { val: `${objects[v].val < 10 ? 0 : ''}${objects[v].val}` }
|
|
172
|
-
if (objects[f].func === 'second') objects[v].val = _fillAfterDot(objects[v].val)
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
return 0
|
|
176
|
-
}
|
|
177
160
|
|
|
178
161
|
if ((objects[i + 1] === '=' || objects[i + 1] in NOT_EQUAL) && objects[i + 2] && objects[i + 2].val === null) {
|
|
179
162
|
this._addNullOrNotNull(objects[i], objects[i + 1])
|
|
@@ -87,6 +87,7 @@ const _redirectRefToActives = (ref, model) => {
|
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
const h = cds.ApplicationService.prototype.handle
|
|
90
|
+
|
|
90
91
|
/* eslint-disable complexity */
|
|
91
92
|
cds.ApplicationService.prototype.handle = async function (req) {
|
|
92
93
|
const handle = h.bind(this)
|
|
@@ -338,8 +339,8 @@ cds.ApplicationService.prototype.handle = async function (req) {
|
|
|
338
339
|
}
|
|
339
340
|
|
|
340
341
|
req.query = query
|
|
341
|
-
|
|
342
|
-
return
|
|
342
|
+
|
|
343
|
+
return handle(req)
|
|
343
344
|
}
|
|
344
345
|
|
|
345
346
|
// REVISIT: It's not optimal to first calculate the whole result array and only later
|
|
@@ -414,7 +415,7 @@ const Read = {
|
|
|
414
415
|
draftsQuery.SELECT.orderBy = undefined
|
|
415
416
|
draftsQuery.SELECT.columns = keys.map(k => ({ ref: [k] }))
|
|
416
417
|
|
|
417
|
-
const drafts = await draftsQuery
|
|
418
|
+
const drafts = await draftsQuery.where({ HasActiveEntity: true })
|
|
418
419
|
const res = await Read.onlyActives(run, query.where(Read.whereNotIn(query._target, drafts)), {
|
|
419
420
|
ignoreDrafts: true
|
|
420
421
|
})
|
|
@@ -1092,6 +1093,7 @@ async function onPrepare(req) {
|
|
|
1092
1093
|
module.exports = {
|
|
1093
1094
|
impl() {
|
|
1094
1095
|
if (!this._datasource) this._datasource = cds.db
|
|
1096
|
+
|
|
1095
1097
|
function _wrapped(handler, isActiveEntity) {
|
|
1096
1098
|
const fn = function (req, next) {
|
|
1097
1099
|
if (!req.target?.drafts || (isActiveEntity && req.target.isDraft) || (!isActiveEntity && !req.target.isDraft))
|
|
@@ -1100,6 +1102,7 @@ module.exports = {
|
|
|
1100
1102
|
}
|
|
1101
1103
|
return fn
|
|
1102
1104
|
}
|
|
1105
|
+
|
|
1103
1106
|
// Also runs those handlers if they're annotated with @odata.draft.enabled through extensibility
|
|
1104
1107
|
this.on('NEW', '*', _wrapped(onNew, false))
|
|
1105
1108
|
this.on('EDIT', '*', _wrapped(onEdit, true))
|
|
@@ -195,11 +195,9 @@ const _getHanaDriver = name => {
|
|
|
195
195
|
LOG._debug && LOG.debug(`Failed to require "hdb" with error "${e.message}". Trying "@sap/hana-client" next.`)
|
|
196
196
|
return _getHanaDriver('@sap/hana-client')
|
|
197
197
|
} else if (isConfigured) {
|
|
198
|
-
throw new Error(`"${name}" could not be
|
|
198
|
+
throw new Error(`"${name}" could not be found. Please make sure it is installed.`)
|
|
199
199
|
} else {
|
|
200
|
-
throw new Error(
|
|
201
|
-
'Neither "hdb" nor "@sap/hana-client" could be required. Please make sure one of them is installed.'
|
|
202
|
-
)
|
|
200
|
+
throw new Error('Neither "hdb" nor "@sap/hana-client" could be found. Please make sure one of them is installed.')
|
|
203
201
|
}
|
|
204
202
|
}
|
|
205
203
|
}
|
|
@@ -4,7 +4,7 @@ const express = require('express')
|
|
|
4
4
|
const getTenantInfo = require('./getTenantInfo.js')
|
|
5
5
|
const isSecured = () => cds.requires.auth && (cds.requires.auth.impl || cds.requires.auth.credentials)
|
|
6
6
|
const _require = require('../../common/utils/require')
|
|
7
|
-
const {
|
|
7
|
+
const { ODATA_UNAUTHORIZED } = require('../../common/error/constants')
|
|
8
8
|
|
|
9
9
|
const _isAll = a => a && a.includes('all')
|
|
10
10
|
|
|
@@ -34,7 +34,7 @@ class EndpointRegistry {
|
|
|
34
34
|
paths.forEach(path => {
|
|
35
35
|
cds.app.use(path, (req, res, next) => {
|
|
36
36
|
// REVISIT: we should probably pass an error into next so that a (custom) error middleware can handle it
|
|
37
|
-
if (!req.user) res.status(401).json({ error:
|
|
37
|
+
if (!req.user) res.status(401).json({ error: ODATA_UNAUTHORIZED })
|
|
38
38
|
next()
|
|
39
39
|
})
|
|
40
40
|
})
|
|
@@ -135,8 +135,7 @@ const processMessages = async (service, tenant, _opts = {}) => {
|
|
|
135
135
|
if (!msg) continue
|
|
136
136
|
const res = {
|
|
137
137
|
process: () =>
|
|
138
|
-
|
|
139
|
-
if (userId) cds.context = { user }
|
|
138
|
+
cds._context.run({ user, tenant }, async () => {
|
|
140
139
|
try {
|
|
141
140
|
return service._emitImmediate && (await service._emitImmediate(msg))
|
|
142
141
|
} catch (e) {
|
|
@@ -416,7 +416,7 @@ const _stringToReqOptions = (query, data, target) => {
|
|
|
416
416
|
const cleanQuery = query.trim()
|
|
417
417
|
const blankIndex = cleanQuery.substring(0, 8).indexOf(' ')
|
|
418
418
|
const reqOptions = {
|
|
419
|
-
method: cleanQuery.substring(0, blankIndex).toUpperCase(),
|
|
419
|
+
method: cleanQuery.substring(0, blankIndex).toUpperCase() || 'GET',
|
|
420
420
|
url: encodeURI(formatPath(cleanQuery.substring(blankIndex, cleanQuery.length).trim()))
|
|
421
421
|
}
|
|
422
422
|
|
|
@@ -98,10 +98,6 @@ module.exports = class SQLiteDatabase extends DatabaseService {
|
|
|
98
98
|
}
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
getDbUrl(tenant) {
|
|
102
|
-
return this.url4(tenant)
|
|
103
|
-
}
|
|
104
|
-
|
|
105
101
|
url4(tenant) {
|
|
106
102
|
const credentials = this.options.credentials || this.options || {}
|
|
107
103
|
let dbUrl = credentials.database || credentials.url || credentials.host || ':memory:'
|
|
@@ -87,7 +87,7 @@ class CustomFunctionBuilder extends FunctionBuilder {
|
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
_timeFunction(functionName, args) {
|
|
90
|
-
this._outputObj.sql.push('strftime(')
|
|
90
|
+
this._outputObj.sql.push('CAST(strftime(')
|
|
91
91
|
this._outputObj.sql.push(dateTimeFunctions.get(functionName), ',')
|
|
92
92
|
if (typeof args === 'string') {
|
|
93
93
|
this._outputObj.sql.push(args, ')')
|
|
@@ -95,6 +95,7 @@ class CustomFunctionBuilder extends FunctionBuilder {
|
|
|
95
95
|
this._addFunctionArgs(args)
|
|
96
96
|
this._outputObj.sql.push(')')
|
|
97
97
|
}
|
|
98
|
+
this._outputObj.sql.push(' as decimal)')
|
|
98
99
|
}
|
|
99
100
|
}
|
|
100
101
|
|