@sap/cds 8.2.3 → 8.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +35 -3
- package/bin/test.js +1 -1
- package/lib/compile/etc/csv.js +1 -1
- package/lib/dbs/cds-deploy.js +8 -5
- package/lib/env/cds-requires.js +0 -13
- package/lib/log/cds-error.js +10 -7
- package/lib/plugins.js +8 -3
- package/lib/srv/middlewares/errors.js +5 -3
- package/lib/srv/protocols/index.js +4 -4
- package/lib/srv/srv-methods.js +1 -0
- package/lib/utils/cds-test.js +2 -1
- package/lib/utils/cds-utils.js +14 -1
- package/lib/utils/colors.js +45 -44
- package/libx/_runtime/common/composition/data.js +4 -2
- package/libx/_runtime/common/composition/index.js +1 -2
- package/libx/_runtime/common/composition/tree.js +1 -24
- package/libx/_runtime/common/generic/auth/restrict.js +29 -4
- package/libx/_runtime/common/generic/auth/restrictions.js +29 -36
- package/libx/_runtime/common/i18n/messages.properties +1 -1
- package/libx/_runtime/common/utils/cqn.js +0 -26
- package/libx/_runtime/common/utils/csn.js +0 -14
- package/libx/_runtime/common/utils/differ.js +1 -0
- package/libx/_runtime/common/utils/resolveView.js +28 -9
- package/libx/_runtime/common/utils/templateProcessor.js +3 -0
- package/libx/_runtime/fiori/lean-draft.js +30 -12
- package/libx/_runtime/types/api.js +1 -1
- package/libx/_runtime/ucl/Service.js +2 -2
- package/libx/common/utils/path.js +1 -4
- package/libx/odata/ODataAdapter.js +6 -0
- package/libx/odata/middleware/batch.js +7 -9
- package/libx/odata/middleware/create.js +4 -2
- package/libx/odata/middleware/delete.js +3 -1
- package/libx/odata/middleware/operation.js +7 -5
- package/libx/odata/middleware/read.js +14 -10
- package/libx/odata/middleware/service-document.js +1 -1
- package/libx/odata/middleware/stream.js +1 -0
- package/libx/odata/middleware/update.js +5 -3
- package/libx/odata/parse/afterburner.js +37 -49
- package/libx/odata/utils/postProcess.js +3 -8
- package/package.json +1 -1
- package/libx/_runtime/cds-services/services/utils/compareJson.js +0 -2
- package/libx/_runtime/messaging/event-broker.js +0 -317
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
const cds = require('../../../cds')
|
|
2
|
+
|
|
1
3
|
const WRITE_EVENTS = { CREATE: 1, NEW: 1, UPDATE: 1, PATCH: 1, DELETE: 1, CANCEL: 1, EDIT: 1 }
|
|
2
4
|
const CRUD = Object.assign({ READ: 1 }, WRITE_EVENTS)
|
|
3
|
-
|
|
5
|
+
const ACTION_TYPES = { action: 1, function: 1 }
|
|
4
6
|
/**
|
|
5
7
|
* Returns the applicable restrictions for the current request as follows:
|
|
6
8
|
* - null: unrestricted access
|
|
@@ -15,24 +17,12 @@ const CRUD = Object.assign({ READ: 1 }, WRITE_EVENTS)
|
|
|
15
17
|
* @returns {Promise | Array | null}
|
|
16
18
|
*/
|
|
17
19
|
function getRestrictions(definition, event, user) {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
if (!restrictions && (event in CRUD || !definition.parent)) {
|
|
21
|
-
// > unrestricted entity or unbound
|
|
22
|
-
return null
|
|
23
|
-
}
|
|
20
|
+
let restrictions = getNormalizedRestrictions(definition)
|
|
21
|
+
if (!restrictions) return null
|
|
24
22
|
if (event in CRUD && restrictions.length && restrictions.every(r => r.grant !== '*' && !(r.grant in CRUD))) {
|
|
25
23
|
// > only bounds are restricted
|
|
26
24
|
return null
|
|
27
25
|
}
|
|
28
|
-
if (!(event in CRUD) && !restrictions && definition.parent) {
|
|
29
|
-
// > bound without own restrictions -> get from parent
|
|
30
|
-
restrictions = getNormalizedRestrictions(definition.parent, model.definitions, event)
|
|
31
|
-
if (!restrictions) {
|
|
32
|
-
// > unrestricted bound
|
|
33
|
-
return null
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
26
|
// return the applicable restrictions (grant and to fit to request and user)
|
|
37
27
|
return getApplicableRestrictions(restrictions, event, user)
|
|
38
28
|
}
|
|
@@ -41,6 +31,13 @@ const _getLocalName = definition => {
|
|
|
41
31
|
return definition._service ? definition.name.replace(`${definition._service.name}.`, '') : definition.name
|
|
42
32
|
}
|
|
43
33
|
|
|
34
|
+
const _isStaticWhere = where => {
|
|
35
|
+
if (typeof where === 'string') where = cds.parse.expr(where)
|
|
36
|
+
return (
|
|
37
|
+
where?.xpr?.length === 3 && where.xpr.every(ele => typeof ele !== 'object' || ele.val || ele.ref?.[0] === '$user')
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
44
41
|
const _getRestrictWithEventRewrite = (grant, to, where, target) => {
|
|
45
42
|
// REVISIT: req.event should be 'SAVE' and 'PREPARE'
|
|
46
43
|
if (grant === 'SAVE') grant = 'draftActivate'
|
|
@@ -72,35 +69,31 @@ const _addNormalizedRestrict = (restrict, restricts, definition) => {
|
|
|
72
69
|
restrict.grant.forEach(grant => _addNormalizedRestrictPerGrant(grant, where, restrict, restricts, definition))
|
|
73
70
|
}
|
|
74
71
|
|
|
75
|
-
const getNormalizedRestrictions =
|
|
72
|
+
const getNormalizedRestrictions = definition => {
|
|
76
73
|
const restricts = []
|
|
77
74
|
let isRestricted = false
|
|
78
75
|
|
|
79
76
|
// own
|
|
80
77
|
if (definition['@restrict']) {
|
|
81
78
|
isRestricted = true
|
|
82
|
-
definition['@restrict'].forEach(restrict =>
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const action = actions[k]
|
|
91
|
-
|
|
92
|
-
if (action['@restrict']) {
|
|
93
|
-
const restrictions = getNormalizedRestrictions(action, definitions)
|
|
94
|
-
if (restrictions) {
|
|
95
|
-
isRestricted = true
|
|
96
|
-
restricts.push(...restrictions)
|
|
79
|
+
definition['@restrict'].forEach(restrict => {
|
|
80
|
+
if (definition.kind in ACTION_TYPES) {
|
|
81
|
+
const to = restrict.to ? (Array.isArray(restrict.to) ? restrict.to : [restrict.to]) : ['any']
|
|
82
|
+
if (definition.parent?.kind === 'entity') {
|
|
83
|
+
restrict = { grant: definition.name, to }
|
|
84
|
+
} else {
|
|
85
|
+
const where = _isStaticWhere(restrict.where) && restrict.where
|
|
86
|
+
restrict = { grant: _getLocalName(definition), to, where }
|
|
97
87
|
}
|
|
98
|
-
} else if (!definition['@restrict']) {
|
|
99
|
-
// > no entity-level restrictions => unrestricted action
|
|
100
|
-
isRestricted = true
|
|
101
|
-
restricts.push({ grant: action.name, to: ['any'], target: action.parent })
|
|
102
88
|
}
|
|
103
|
-
|
|
89
|
+
_addNormalizedRestrict(restrict, restricts, definition)
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// parent - in case of bound actions/functions
|
|
94
|
+
if (definition.kind in ACTION_TYPES && definition.parent && definition.parent['@restrict']) {
|
|
95
|
+
isRestricted = true
|
|
96
|
+
definition.parent['@restrict'].forEach(restrict => _addNormalizedRestrict(restrict, restricts, definition.parent))
|
|
104
97
|
}
|
|
105
98
|
|
|
106
99
|
return isRestricted ? restricts : null
|
|
@@ -91,7 +91,7 @@ BATCH_TOO_MANY_REQ=Batch request contains too many requests
|
|
|
91
91
|
DRAFT_ALREADY_EXISTS=A draft for this entity already exists
|
|
92
92
|
DRAFT_NOT_EXISTING=No draft for this entity exists
|
|
93
93
|
DRAFT_LOCKED_BY_ANOTHER_USER=The entity is locked by user "{0}"
|
|
94
|
-
DRAFT_MODIFICATION_ONLY_VIA_ROOT=A draft can only be modified via its root entity
|
|
94
|
+
DRAFT_MODIFICATION_ONLY_VIA_ROOT=A draft-enabled entity can only be modified via its root entity
|
|
95
95
|
ACTIVE_MODIFICATION_VIA_DRAFT=Active entities cannot be modified via draft request
|
|
96
96
|
DRAFT_ACTIVE_DELETE_FORBIDDEN_DRAFT_EXISTS=Entity cannot be deleted because a draft exists
|
|
97
97
|
|
|
@@ -81,31 +81,6 @@ function targetFromPath(from, model) {
|
|
|
81
81
|
return { last, path, target, isTargetComposition: isContained }
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
function isPathToDraft(path, model) {
|
|
85
|
-
const { definitions } = model
|
|
86
|
-
let draft = false
|
|
87
|
-
let current
|
|
88
|
-
for (const r of path) {
|
|
89
|
-
if (r.id) {
|
|
90
|
-
const isaIndex = r.where.findIndex(ele => ele.ref && ele.ref[0] === 'IsActiveEntity')
|
|
91
|
-
if (isaIndex !== -1) draft = !r.where[isaIndex + 2].val
|
|
92
|
-
current = current ? definitions[current.elements[r.id].target] : definitions[r.id]
|
|
93
|
-
} else {
|
|
94
|
-
if (r === 'SiblingEntity') draft = !!draft
|
|
95
|
-
else {
|
|
96
|
-
const next = current.elements[r]
|
|
97
|
-
if (next.isAssociation) {
|
|
98
|
-
if (next._isAssociationStrict) draft = false
|
|
99
|
-
current = definitions[next.target]
|
|
100
|
-
} else {
|
|
101
|
-
current = next
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
return draft
|
|
107
|
-
}
|
|
108
|
-
|
|
109
84
|
const resolveFromSelect = query => {
|
|
110
85
|
const __protoSelect = Object.getPrototypeOf(SELECT())
|
|
111
86
|
if (!(query instanceof __protoSelect.constructor)) Object.setPrototypeOf(query, __protoSelect)
|
|
@@ -118,6 +93,5 @@ module.exports = {
|
|
|
118
93
|
getEntityNameFromUpdateCQN,
|
|
119
94
|
where2obj,
|
|
120
95
|
targetFromPath,
|
|
121
|
-
isPathToDraft,
|
|
122
96
|
resolveFromSelect
|
|
123
97
|
}
|
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
const cds = require('../../cds')
|
|
2
2
|
|
|
3
|
-
const getEtagElement = entity => {
|
|
4
|
-
return Object.values(entity.elements).find(element => element['@odata.etag'])
|
|
5
|
-
}
|
|
6
|
-
|
|
7
3
|
const getComp2oneParents = (entity, model) => {
|
|
8
4
|
if (!entity) return []
|
|
9
5
|
return _getUps(entity, model).filter(element => element.is2one && element.isComposition)
|
|
@@ -106,14 +102,6 @@ const findCsnTargetFor = (edmName, model, namespace) => {
|
|
|
106
102
|
return edm2csnMap[edmName]
|
|
107
103
|
}
|
|
108
104
|
|
|
109
|
-
const getElementDeep = (entity, ref) => {
|
|
110
|
-
let current = entity
|
|
111
|
-
for (const r of ref) {
|
|
112
|
-
current = current && current.elements && current.elements[r]
|
|
113
|
-
}
|
|
114
|
-
return current
|
|
115
|
-
}
|
|
116
|
-
|
|
117
105
|
const prefixForStruct = element => {
|
|
118
106
|
const prefixes = []
|
|
119
107
|
let parent = element.parent
|
|
@@ -153,9 +141,7 @@ function getDraftTreeRoot(entity, model) {
|
|
|
153
141
|
}
|
|
154
142
|
|
|
155
143
|
module.exports = {
|
|
156
|
-
getEtagElement,
|
|
157
144
|
findCsnTargetFor,
|
|
158
|
-
getElementDeep,
|
|
159
145
|
getComp2oneParents,
|
|
160
146
|
prefixForStruct,
|
|
161
147
|
getDraftTreeRoot,
|
|
@@ -54,6 +54,7 @@ module.exports = class Differ {
|
|
|
54
54
|
|
|
55
55
|
async _diffUpdate(req, providedData) {
|
|
56
56
|
if (cds.db) await this._addPartialPersistentState(req)
|
|
57
|
+
// REVISIT: this is done (better) in the new db services
|
|
57
58
|
const newQuery = cqn2cqn4sql(req.query, this._srv.model)
|
|
58
59
|
const combinedData = providedData || Object.assign({}, req.query.UPDATE.data || {}, req.query.UPDATE.with || {})
|
|
59
60
|
enrichDataWithKeysFromWhere(combinedData, req, this._srv)
|
|
@@ -184,6 +184,8 @@ const _newColumns = (columns = [], transition, service, withAlias = false) => {
|
|
|
184
184
|
newColumn = {}
|
|
185
185
|
newColumn.as = column.ref[0]
|
|
186
186
|
newColumn.val = mapped.val
|
|
187
|
+
} else if (column.ref && !transition.target.elements[column.ref[0]]) {
|
|
188
|
+
return // ignore columns which are not part of the entity
|
|
187
189
|
} else {
|
|
188
190
|
newColumn = column
|
|
189
191
|
}
|
|
@@ -239,16 +241,33 @@ const _newInsertColumns = (columns = [], transition) => {
|
|
|
239
241
|
const _newWhereRef = (newWhereElement, transition, alias, tableName) => {
|
|
240
242
|
const newRef = Array.isArray(newWhereElement.ref) ? [...newWhereElement.ref] : [newWhereElement.ref]
|
|
241
243
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
244
|
+
const [firstRef, secondRef] = [newRef[0].id || newRef[0], newRef[1]?.id || newRef[1]]
|
|
245
|
+
|
|
246
|
+
const updateRef = (index, value) => {
|
|
247
|
+
// REVISIT: this should be done instead of ignoring where for a ref step
|
|
248
|
+
// --> there are no transitions calculated for infix filters though
|
|
249
|
+
// if (newRef[index].id) {
|
|
250
|
+
// newRef[index].id = value;
|
|
251
|
+
// } else {
|
|
252
|
+
// newRef[index] = value;
|
|
253
|
+
// }
|
|
254
|
+
|
|
255
|
+
newRef[index] = value
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (secondRef) {
|
|
259
|
+
if (firstRef === alias) {
|
|
260
|
+
const mapped = transition.mapping.get(secondRef)
|
|
261
|
+
if (mapped) updateRef(1, mapped.ref.join('_'))
|
|
262
|
+
} else if (firstRef === tableName) {
|
|
263
|
+
updateRef(0, transition.target.name)
|
|
264
|
+
|
|
265
|
+
const mapped = transition.mapping.get(secondRef)
|
|
266
|
+
if (mapped) updateRef(1, mapped.ref.join('_'))
|
|
267
|
+
}
|
|
249
268
|
} else if (newRef.length === 1) {
|
|
250
|
-
const mapped = transition.mapping.get(
|
|
251
|
-
if (mapped)
|
|
269
|
+
const mapped = transition.mapping.get(firstRef)
|
|
270
|
+
if (mapped) updateRef(0, mapped.ref.join('_'))
|
|
252
271
|
}
|
|
253
272
|
|
|
254
273
|
newWhereElement.ref = newRef
|
|
@@ -63,6 +63,9 @@ const _processComplex = (processFn, row, template, key, pathOptions) => {
|
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
/**
|
|
67
|
+
* @param {import("../../types/api").TemplateProcessor} args
|
|
68
|
+
*/
|
|
66
69
|
const templateProcessor = ({ processFn, data, template, isRoot = true, pathOptions = {} }) => {
|
|
67
70
|
if (!template || !template.elements.size || !data || typeof data !== 'object') return
|
|
68
71
|
const dataArr = Array.isArray(data) ? data : [data]
|
|
@@ -226,17 +226,34 @@ const _cleanUpOldDrafts = (service, tenant) => {
|
|
|
226
226
|
if (!expiredDrafts.length) return
|
|
227
227
|
|
|
228
228
|
const expiredDraftsIds = expiredDrafts.map(el => el.DraftUUID)
|
|
229
|
-
const
|
|
229
|
+
const promises = []
|
|
230
230
|
|
|
231
|
-
|
|
231
|
+
const draftRoots = []
|
|
232
|
+
|
|
233
|
+
for (const name in service.model.definitions) {
|
|
232
234
|
const target = service.model.definitions[name]
|
|
233
235
|
if (target.drafts && target['@Common.DraftRoot.ActivationAction']) {
|
|
234
|
-
|
|
236
|
+
draftRoots.push(target.drafts)
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const draftRootIds = await Promise.all(
|
|
241
|
+
draftRoots.map(draftRoot =>
|
|
242
|
+
SELECT.from(draftRoot, entity_keys(draftRoot)).where(`DraftAdministrativeData_DraftUUID IN`, expiredDraftsIds)
|
|
243
|
+
)
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
for (let i = 0; i < draftRoots.length; i++) {
|
|
247
|
+
const ids = draftRootIds[i]
|
|
248
|
+
if (!ids.length) continue
|
|
249
|
+
const srv = await cds.connect.to(draftRoots[i]._service.name).catch(() => {})
|
|
250
|
+
if (!srv) continue // srv might not be loaded
|
|
251
|
+
for (const idObj of ids) {
|
|
252
|
+
promises.push(srv.send({ event: 'CANCEL', query: DELETE.from(draftRoots[i], idObj), data: idObj }))
|
|
235
253
|
}
|
|
236
254
|
}
|
|
237
255
|
|
|
238
|
-
|
|
239
|
-
return await _promiseAll(cqns)
|
|
256
|
+
await Promise.allSettled(promises)
|
|
240
257
|
})
|
|
241
258
|
|
|
242
259
|
lastCheckMap.set(tenant, Date.now())
|
|
@@ -523,7 +540,7 @@ cds.ApplicationService.prototype.handle = async function (req) {
|
|
|
523
540
|
// Deletion of active instance inside draft tree, need to check that no draft exists
|
|
524
541
|
const draft = await draftQuery
|
|
525
542
|
const inProcessByUser = draft?.DraftAdministrativeData?.InProcessByUser
|
|
526
|
-
if (inProcessByUser && inProcessByUser !== cds.context.user.id)
|
|
543
|
+
if (!cds.context.user._is_privileged && inProcessByUser && inProcessByUser !== cds.context.user.id)
|
|
527
544
|
req.reject({ code: 403, statusCode: 403, message: 'DRAFT_LOCKED_BY_ANOTHER_USER', args: [inProcessByUser] })
|
|
528
545
|
if (draft) req.reject({ code: 403, statusCode: 403, message: 'DRAFT_ACTIVE_DELETE_FORBIDDEN_DRAFT_EXISTS' })
|
|
529
546
|
await run(query)
|
|
@@ -565,7 +582,7 @@ cds.ApplicationService.prototype.handle = async function (req) {
|
|
|
565
582
|
const res = await run(draftQuery)
|
|
566
583
|
if (!res)
|
|
567
584
|
req.reject(_etagValidationType ? { code: 412, statusCode: 412 } : { code: 'DRAFT_NOT_EXISTING', statusCode: 404 })
|
|
568
|
-
if (res.DraftAdministrativeData?.InProcessByUser !== cds.context.user.id) {
|
|
585
|
+
if (!cds.context.user._is_privileged && res.DraftAdministrativeData?.InProcessByUser !== cds.context.user.id) {
|
|
569
586
|
req.reject({
|
|
570
587
|
code: 403,
|
|
571
588
|
statusCode: 403,
|
|
@@ -650,7 +667,7 @@ cds.ApplicationService.prototype.handle = async function (req) {
|
|
|
650
667
|
expand: [{ ref: ['InProcessByUser'] }]
|
|
651
668
|
})
|
|
652
669
|
if (!res) req.reject({ code: 404, statusCode: 404 })
|
|
653
|
-
if (res.DraftAdministrativeData?.InProcessByUser !== cds.context.user.id) {
|
|
670
|
+
if (!cds.context.user._is_privileged && res.DraftAdministrativeData?.InProcessByUser !== cds.context.user.id) {
|
|
654
671
|
req.reject({
|
|
655
672
|
code: 403,
|
|
656
673
|
statusCode: 403,
|
|
@@ -1486,7 +1503,7 @@ async function onNew(req) {
|
|
|
1486
1503
|
])
|
|
1487
1504
|
.where(req.query.INSERT.into.ref[0].where)
|
|
1488
1505
|
if (!rootData) req.reject({ code: 404, statusCode: 404 })
|
|
1489
|
-
if (rootData.DraftAdministrativeData?.InProcessByUser !== req.user.id)
|
|
1506
|
+
if (!cds.context.user._is_privileged && rootData.DraftAdministrativeData?.InProcessByUser !== req.user.id)
|
|
1490
1507
|
req.reject({
|
|
1491
1508
|
code: 403,
|
|
1492
1509
|
statusCode: 403,
|
|
@@ -1756,7 +1773,7 @@ async function onCancel(req) {
|
|
|
1756
1773
|
if (draftParams.IsActiveEntity === false && !draft) req.reject(req._etagValidationType ? 412 : 404)
|
|
1757
1774
|
if (draft) {
|
|
1758
1775
|
const processByUser = draft.DraftAdministrativeData?.InProcessByUser
|
|
1759
|
-
if (processByUser && processByUser !== cds.context.user.id)
|
|
1776
|
+
if (!cds.context.user._is_privileged && processByUser && processByUser !== cds.context.user.id)
|
|
1760
1777
|
req.reject({ code: 403, statusCode: 403, message: 'DRAFT_LOCKED_BY_ANOTHER_USER', args: [processByUser] })
|
|
1761
1778
|
}
|
|
1762
1779
|
const draftDeleteQuery = DELETE.from({ ref: req.query.DELETE.from.ref })
|
|
@@ -1778,7 +1795,8 @@ async function onCancel(req) {
|
|
|
1778
1795
|
})
|
|
1779
1796
|
.where({ DraftUUID: draft.DraftAdministrativeData_DraftUUID })
|
|
1780
1797
|
)
|
|
1781
|
-
if (draftParams.IsActiveEntity !== false
|
|
1798
|
+
if (draftParams.IsActiveEntity !== false && !req.target.isDraft)
|
|
1799
|
+
queries.push(this.run(DELETE.from({ ref: activeRef })))
|
|
1782
1800
|
await _promiseAll(queries)
|
|
1783
1801
|
return req.data
|
|
1784
1802
|
}
|
|
@@ -1804,7 +1822,7 @@ async function onPrepare(req) {
|
|
|
1804
1822
|
draftQuery[DRAFT_PARAMS] = draftParams
|
|
1805
1823
|
const data = await draftQuery
|
|
1806
1824
|
if (!data) req.reject({ code: 404, statusCode: 404 })
|
|
1807
|
-
if (data.DraftAdministrativeData?.InProcessByUser !== req.user.id)
|
|
1825
|
+
if (!cds.context.user._is_privileged && data.DraftAdministrativeData?.InProcessByUser !== req.user.id)
|
|
1808
1826
|
req.reject({
|
|
1809
1827
|
code: 403,
|
|
1810
1828
|
statusCode: 403,
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
/**
|
|
59
59
|
* @typedef {object} TemplateProcessor
|
|
60
60
|
* @property {Function} processFn
|
|
61
|
-
* @property {object}
|
|
61
|
+
* @property {object} data
|
|
62
62
|
* @property {TemplateProcessorInfo} template
|
|
63
63
|
* @property {boolean} [isRoot=true]
|
|
64
64
|
* @property {TemplateProcessorPathOptions} [pathOptions=null]
|
|
@@ -95,7 +95,7 @@ class UCLService extends cds.Service {
|
|
|
95
95
|
}
|
|
96
96
|
placeholders: [
|
|
97
97
|
{ name: "subdomain", description: "The subdomain of the consumer tenant" }
|
|
98
|
-
{ name: "tenant-id", description: "The tenant id as it's known in the product's domain", jsonPath: "$.
|
|
98
|
+
{ name: "tenant-id", description: "The tenant id as it's known in the product's domain", jsonPath: "$.subscribedTenantId" }
|
|
99
99
|
]
|
|
100
100
|
labels: {
|
|
101
101
|
managed_app_provisioning: true
|
|
@@ -203,7 +203,7 @@ class UCLService extends cds.Service {
|
|
|
203
203
|
applicationNamespace: "${this.options.namespace}"
|
|
204
204
|
placeholders: [
|
|
205
205
|
{ name: "subdomain", description: "The subdomain of the consumer tenant" }
|
|
206
|
-
{ name: "tenant-id", description: "The tenant id as it's known in the product's domain", jsonPath: "$.
|
|
206
|
+
{ name: "tenant-id", description: "The tenant id as it's known in the product's domain", jsonPath: "$.subscribedTenantId" }
|
|
207
207
|
]
|
|
208
208
|
accessLevel: GLOBAL
|
|
209
209
|
}
|
|
@@ -1,17 +1,14 @@
|
|
|
1
|
-
const cds = require('../../_runtime/cds')
|
|
2
1
|
const { where2obj } = require('../../_runtime/common/utils/cqn')
|
|
3
2
|
const { getKeysForNavigationFromRefPath } = require('../../_runtime/common/utils/keys')
|
|
4
3
|
|
|
5
4
|
// REVISIT: do we already have something like this _without using okra api_?
|
|
6
5
|
// REVISIT: should we still support process.env.CDS_FEATURES_PARAMS? probably nobody uses it...
|
|
7
|
-
const getKeysAndParamsFromPath = (from,
|
|
6
|
+
const getKeysAndParamsFromPath = (from, { model }) => {
|
|
8
7
|
if (!from.ref || !from.ref.length) return {}
|
|
9
8
|
|
|
10
9
|
const keys = {}
|
|
11
10
|
const params = []
|
|
12
11
|
|
|
13
|
-
const model = cds.context.model ?? srv.model
|
|
14
|
-
|
|
15
12
|
let cur = model.definitions
|
|
16
13
|
let lastElement
|
|
17
14
|
|
|
@@ -112,6 +112,12 @@ class ODataAdapter extends HttpAdapter {
|
|
|
112
112
|
|
|
113
113
|
// REVISIT: ugly hack -> eliminate
|
|
114
114
|
class NoaRequest extends cds.Request {
|
|
115
|
+
constructor(args) {
|
|
116
|
+
super(args)
|
|
117
|
+
this.req = args.req
|
|
118
|
+
this.res = args.res
|
|
119
|
+
}
|
|
120
|
+
|
|
115
121
|
// REVISIT: all usages of .protocol are very bad style, violating modularization
|
|
116
122
|
get protocol() {
|
|
117
123
|
return 'odata'
|
|
@@ -9,6 +9,8 @@ const { URL } = require('url')
|
|
|
9
9
|
const multipartToJson = require('../parse/multipartToJson')
|
|
10
10
|
const { getBoundary } = require('../utils')
|
|
11
11
|
|
|
12
|
+
const { normalizeError } = require('../../_runtime/common/error/frontend')
|
|
13
|
+
|
|
12
14
|
const HTTP_METHODS = { GET: 1, POST: 1, PUT: 1, PATCH: 1, DELETE: 1 }
|
|
13
15
|
const CT = { JSON: 'application/json', MULTIPART: 'multipart/mixed' }
|
|
14
16
|
const CRLF = '\r\n'
|
|
@@ -263,12 +265,8 @@ const _tx_done = async (tx, responses, isJson) => {
|
|
|
263
265
|
} catch (e) {
|
|
264
266
|
// here, the commit was rejected even though all requests were successful (e.g., by custom handler or db consistency check)
|
|
265
267
|
rejected = 'rejected'
|
|
266
|
-
// construct commit error
|
|
267
|
-
|
|
268
|
-
if (isNaN(statusCode)) statusCode = 500
|
|
269
|
-
const code = String(e.code || statusCode)
|
|
270
|
-
const message = e.message || 'Internal Server Error'
|
|
271
|
-
const error = { error: { ...e, code, message } }
|
|
268
|
+
// construct commit error (without modifying original error)
|
|
269
|
+
const { error, statusCode } = normalizeError(Object.create(e), { locale: cds.context.locale })
|
|
272
270
|
// replace all responses with commit error
|
|
273
271
|
for (const res of responses) {
|
|
274
272
|
res.status = 'fail'
|
|
@@ -278,15 +276,15 @@ const _tx_done = async (tx, responses, isJson) => {
|
|
|
278
276
|
for (let i = 0; i < res.txt.length; i++) txt += Buffer.isBuffer(res.txt[i]) ? res.txt[i].toString() : res.txt[i]
|
|
279
277
|
txt = JSON.parse(txt)
|
|
280
278
|
txt.status = statusCode
|
|
281
|
-
txt.body = error
|
|
279
|
+
txt.body = { error }
|
|
282
280
|
// REVISIT: content-length needed? not there in multipart case...
|
|
283
281
|
delete txt.headers['content-length']
|
|
284
282
|
res.txt = [JSON.stringify(txt)]
|
|
285
283
|
} else {
|
|
286
284
|
let txt = res.txt[0]
|
|
287
|
-
txt = txt.replace(/HTTP\/1\.1 \d\d\d \w+/, `HTTP/1.1 ${statusCode} ${
|
|
285
|
+
txt = txt.replace(/HTTP\/1\.1 \d\d\d \w+/, `HTTP/1.1 ${statusCode} ${STATUS_CODES[statusCode]}`)
|
|
288
286
|
txt = txt.split(/\r\n/)
|
|
289
|
-
txt.splice(-1, 1, JSON.stringify(error))
|
|
287
|
+
txt.splice(-1, 1, JSON.stringify({ error }))
|
|
290
288
|
txt = txt.join('\r\n')
|
|
291
289
|
res.txt = [txt]
|
|
292
290
|
}
|
|
@@ -28,9 +28,11 @@ module.exports = (adapter, isUpsert) => {
|
|
|
28
28
|
throw Object.assign(new Error(msg), { statusCode: 405 })
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
const model = cds.context.model ?? service.model
|
|
32
|
+
|
|
31
33
|
// payload & params
|
|
32
34
|
const data = req.body
|
|
33
|
-
const { keys, params } = getKeysAndParamsFromPath(from,
|
|
35
|
+
const { keys, params } = getKeysAndParamsFromPath(from, { model })
|
|
34
36
|
// add keys from url into payload (overwriting if already present)
|
|
35
37
|
Object.assign(data, keys)
|
|
36
38
|
|
|
@@ -74,7 +76,7 @@ module.exports = (adapter, isUpsert) => {
|
|
|
74
76
|
}
|
|
75
77
|
|
|
76
78
|
const isMinimal = getPreferReturnHeader(req) === 'minimal'
|
|
77
|
-
postProcess(cdsReq.target,
|
|
79
|
+
postProcess(cdsReq.target, model, result, isMinimal)
|
|
78
80
|
if (result?.$etag) res.set('ETag', result.$etag) //> must be done after post processing
|
|
79
81
|
if (isMinimal) return res.sendStatus(204)
|
|
80
82
|
|
|
@@ -25,8 +25,10 @@ module.exports = adapter => {
|
|
|
25
25
|
throw Object.assign(new Error('Method DELETE is not allowed for entity collections'), { statusCode: 405 })
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
const model = cds.context.model ?? service.model
|
|
29
|
+
|
|
28
30
|
// payload & params
|
|
29
|
-
const { keys, params } = getKeysAndParamsFromPath(from,
|
|
31
|
+
const { keys, params } = getKeysAndParamsFromPath(from, { model })
|
|
30
32
|
const data = keys //> for read and delete, we provide keys in req.data
|
|
31
33
|
if (_propertyAccess) data[_propertyAccess] = null //> delete of property -> set to null
|
|
32
34
|
|
|
@@ -51,20 +51,22 @@ module.exports = adapter => {
|
|
|
51
51
|
let { operation, args } = req._query.SELECT?.from.ref?.slice(-1)[0] || {}
|
|
52
52
|
if (!operation) return next() //> create or read
|
|
53
53
|
|
|
54
|
+
const model = cds.context.model ?? service.model
|
|
55
|
+
|
|
54
56
|
// unbound vs. bound
|
|
55
57
|
let entity, params
|
|
56
|
-
if (
|
|
57
|
-
operation =
|
|
58
|
+
if (model.definitions[operation]) {
|
|
59
|
+
operation = model.definitions[operation]
|
|
58
60
|
} else {
|
|
59
61
|
req._query.SELECT.from.ref.pop()
|
|
60
|
-
let cur = { elements:
|
|
62
|
+
let cur = { elements: model.definitions }
|
|
61
63
|
for (const each of req._query.SELECT.from.ref) {
|
|
62
64
|
cur = cur.elements[each.id || each]
|
|
63
65
|
if (cur._target) cur = cur._target
|
|
64
66
|
}
|
|
65
67
|
operation = cur.actions[operation]
|
|
66
68
|
entity = cur
|
|
67
|
-
const keysAndParams = getKeysAndParamsFromPath(req._query.SELECT.from,
|
|
69
|
+
const keysAndParams = getKeysAndParamsFromPath(req._query.SELECT.from, { model })
|
|
68
70
|
params = keysAndParams.params
|
|
69
71
|
}
|
|
70
72
|
|
|
@@ -109,7 +111,7 @@ module.exports = adapter => {
|
|
|
109
111
|
}
|
|
110
112
|
|
|
111
113
|
if (operation.returns) {
|
|
112
|
-
postProcess(operation.returns,
|
|
114
|
+
postProcess(operation.returns, model, result)
|
|
113
115
|
if (result?.$etag) res.set('ETag', result.$etag) //> must be done after post processing
|
|
114
116
|
}
|
|
115
117
|
|
|
@@ -12,19 +12,19 @@ const { getKeysAndParamsFromPath } = require('../../common/utils')
|
|
|
12
12
|
const { getPageSize } = require('../../_runtime/common/generic/paging')
|
|
13
13
|
const { handleStreamProperties } = require('../../_runtime/common/utils/streamProp')
|
|
14
14
|
|
|
15
|
-
const _getCount = result =>
|
|
16
|
-
Array.isArray(result)
|
|
17
|
-
? result.reduce((acc, val) => {
|
|
18
|
-
return acc + ((val && (val.$count || val._counted_)) || (val[0] && (val[0].$count || val[0]._counted_))) || 0
|
|
19
|
-
}, 0)
|
|
20
|
-
: result.$count || result._counted_ || 0
|
|
15
|
+
const _getCount = result => (Array.isArray(result) && result.length ? result[0].$count || 0 : result.$count || 0)
|
|
21
16
|
|
|
22
17
|
const _setNextLink = (req, result) => {
|
|
23
18
|
const $skiptoken = result.$nextLink ?? _calculateSkiptoken(req, result)
|
|
24
19
|
if (!$skiptoken) return
|
|
25
20
|
|
|
26
21
|
const queryParamsWithSkipToken = { ...req.req.query, $skiptoken }
|
|
27
|
-
|
|
22
|
+
const encodedQueryParams = querystring.stringify(queryParamsWithSkipToken)
|
|
23
|
+
|
|
24
|
+
// percent-encode all path segments with key values inside parentheses, but keep Navigation Properties untouched
|
|
25
|
+
const encodedPath = req.req.path.slice(1).replace(/\('([^']*)'\)/g, (match, key) => `('${encodeURIComponent(key)}')`)
|
|
26
|
+
|
|
27
|
+
result.$nextLink = `${encodedPath}?${encodedQueryParams}`
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
const _calculateSkiptoken = (req, result) => {
|
|
@@ -184,6 +184,8 @@ module.exports = adapter => {
|
|
|
184
184
|
// $apply with concat -> multiple queries with special handling
|
|
185
185
|
if (Array.isArray(req._query)) return _handleArrayOfQueries(req, res, next)
|
|
186
186
|
|
|
187
|
+
const model = cds.context.model ?? service.model
|
|
188
|
+
|
|
187
189
|
// REVISIT: better solution for _propertyAccess
|
|
188
190
|
let {
|
|
189
191
|
SELECT: { from, one },
|
|
@@ -193,7 +195,7 @@ module.exports = adapter => {
|
|
|
193
195
|
const { _query: query } = req
|
|
194
196
|
|
|
195
197
|
// payload & params
|
|
196
|
-
const { keys, params } = getKeysAndParamsFromPath(from,
|
|
198
|
+
const { keys, params } = getKeysAndParamsFromPath(from, { model })
|
|
197
199
|
const data = keys //> for read and delete, we provide keys in req.data
|
|
198
200
|
|
|
199
201
|
// cdsReq.headers should contain merged headers of envelope and subreq
|
|
@@ -212,7 +214,7 @@ module.exports = adapter => {
|
|
|
212
214
|
|
|
213
215
|
if (!query.SELECT.columns) query.SELECT.columns = ['*']
|
|
214
216
|
|
|
215
|
-
handleStreamProperties(target, query.SELECT.columns,
|
|
217
|
+
handleStreamProperties(target, query.SELECT.columns, model)
|
|
216
218
|
|
|
217
219
|
// REVISIT: what is this for? some tests fail without it... we should find a better solution!
|
|
218
220
|
Object.defineProperty(query.SELECT, '_4odata', { value: true })
|
|
@@ -246,10 +248,12 @@ module.exports = adapter => {
|
|
|
246
248
|
if (result == null) {
|
|
247
249
|
result = []
|
|
248
250
|
if (req.query.$count) result.$count = 0
|
|
251
|
+
} else if (query.SELECT.count && !result.$count) {
|
|
252
|
+
result.$count = 0
|
|
249
253
|
}
|
|
250
254
|
|
|
251
255
|
if (!one) _setNextLink(cdsReq, result)
|
|
252
|
-
postProcess(cdsReq.target,
|
|
256
|
+
postProcess(cdsReq.target, model, result)
|
|
253
257
|
if (result?.$etag) res.set('ETag', result.$etag) //> must be done after post processing
|
|
254
258
|
|
|
255
259
|
const lastSeg = req.path.split('/').slice(-1)[0]
|
|
@@ -22,7 +22,7 @@ module.exports = adapter => {
|
|
|
22
22
|
throw Object.assign(new Error(msg), { statusCode: 405 })
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
const model = cds.context.model
|
|
25
|
+
const model = cds.context.model ?? service.model
|
|
26
26
|
const csnService = model.definitions[service.definition.name]
|
|
27
27
|
|
|
28
28
|
if (req.headers['if-match']) {
|
|
@@ -170,6 +170,7 @@ module.exports = adapter => {
|
|
|
170
170
|
|
|
171
171
|
if (isRedirect(query)) {
|
|
172
172
|
const cdsReq = adapter.request4({ query, req, res })
|
|
173
|
+
|
|
173
174
|
service.dispatch(cdsReq).then(result => {
|
|
174
175
|
if (result[query._propertyAccess]) res.set('Location', result[query._propertyAccess])
|
|
175
176
|
return res.sendStatus(307)
|