@sap/cds 6.5.0 → 6.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +53 -2
- package/README.md +5 -0
- package/apis/services.d.ts +5 -0
- package/bin/build/buildTaskEngine.js +0 -2
- package/bin/build/buildTaskFactory.js +1 -1
- package/bin/build/buildTaskHandler.js +1 -1
- package/bin/build/provider/buildTaskProviderInternal.js +10 -6
- package/bin/build/provider/fiori/index.js +5 -10
- package/bin/build/provider/hana/2migration.js +11 -2
- package/bin/build/provider/hana/index.js +17 -14
- package/bin/build/provider/hana/template/.hdiconfig-hanacloud +137 -0
- package/bin/build/provider/mtx-extension/index.js +18 -1
- package/bin/build/provider/mtx-sidecar/index.js +1 -1
- package/bin/build/util.js +1 -1
- package/bin/cds.js +1 -5
- package/bin/deploy/to-hana/hana.js +10 -3
- package/bin/deploy/to-hana/hdiDeployUtil.js +24 -12
- package/bin/serve.js +32 -20
- package/lib/auth/jwt-auth.js +4 -4
- package/lib/compile/for/lean_drafts.js +55 -6
- package/lib/dbs/cds-deploy.js +6 -8
- package/lib/env/schemas/cds-rc.json +4 -0
- package/lib/index.js +4 -2
- package/lib/req/cds-context.js +3 -3
- package/lib/srv/bindings.js +1 -2
- package/lib/srv/cds-serve.js +2 -1
- package/lib/srv/middlewares/trace.js +31 -15
- package/lib/srv/protocols/odata-v2-proxy.js +8 -8
- package/lib/srv/srv-handlers.js +26 -7
- package/lib/srv/srv-methods.js +2 -2
- package/lib/srv/srv-models.js +4 -3
- package/lib/utils/cds-test.js +7 -5
- package/libx/_runtime/auth/strategies/ias-auth.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/OData.js +6 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +26 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +1 -0
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/ExpressionToCQN.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/readToCQN.js +11 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/utils/PrimitiveValueDecoder.js +8 -8
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/utils/ValueConverter.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/validator/ValueValidator.js +14 -14
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/deserializer/DeserializerFactory.js +7 -8
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/serializer/ResourceJsonSerializer.js +3 -0
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/utils/UriHelper.js +2 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/metaInfo.js +3 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +7 -0
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/stream.js +0 -3
- package/libx/_runtime/cds-services/services/Service.js +8 -19
- package/libx/_runtime/cds-services/services/utils/columns.js +7 -4
- package/libx/_runtime/cds-services/util/assert.js +7 -1
- package/libx/_runtime/common/code-ext/WorkerReq.js +3 -1
- package/libx/_runtime/common/code-ext/execute.js +9 -2
- package/libx/_runtime/common/code-ext/handlers.js +2 -2
- package/libx/_runtime/common/code-ext/worker.js +9 -5
- package/libx/_runtime/common/code-ext/workerQueryExecutor.js +5 -2
- package/libx/_runtime/common/composition/data.js +5 -2
- package/libx/_runtime/common/composition/tree.js +2 -0
- package/libx/_runtime/common/generic/auth/restrict.js +1 -1
- package/libx/_runtime/common/generic/etag.js +22 -10
- package/libx/_runtime/common/generic/input.js +12 -14
- package/libx/_runtime/common/utils/cqn2cqn4sql.js +31 -11
- package/libx/_runtime/common/utils/path.js +0 -1
- package/libx/_runtime/common/utils/search2cqn4sql.js +4 -1
- package/libx/_runtime/common/utils/structured.js +1 -0
- package/libx/_runtime/common/utils/templateProcessorPathSerializer.js +19 -13
- package/libx/_runtime/db/data-conversion/post-processing.js +1 -1
- package/libx/_runtime/db/expand/expandCQNToJoin.js +5 -3
- package/libx/_runtime/db/expand/rawToExpanded.js +3 -2
- package/libx/_runtime/db/generic/input.js +2 -2
- package/libx/_runtime/db/generic/integrity.js +1 -0
- package/libx/_runtime/db/generic/virtual.js +1 -0
- package/libx/_runtime/db/query/read.js +3 -2
- package/libx/_runtime/fiori/generic/activate.js +3 -1
- package/libx/_runtime/fiori/generic/before.js +1 -0
- package/libx/_runtime/fiori/generic/edit.js +6 -1
- package/libx/_runtime/fiori/generic/new.js +2 -0
- package/libx/_runtime/fiori/generic/patch.js +2 -0
- package/libx/_runtime/fiori/generic/prepare.js +2 -0
- package/libx/_runtime/fiori/generic/read.js +8 -2
- package/libx/_runtime/fiori/generic/readOverDraft.js +2 -0
- package/libx/_runtime/fiori/lean-draft.js +498 -245
- package/libx/_runtime/fiori/utils/delete.js +2 -0
- package/libx/_runtime/messaging/Outbox.js +1 -1
- package/libx/_runtime/messaging/enterprise-messaging-utils/getTenantInfo.js +1 -0
- package/libx/_runtime/messaging/enterprise-messaging.js +2 -6
- package/libx/_runtime/messaging/file-based.js +1 -2
- package/libx/_runtime/messaging/outbox/OutboxRunner.js +1 -1
- package/libx/_runtime/messaging/outbox/utils.js +1 -1
- package/libx/_runtime/messaging/service.js +0 -1
- package/libx/_runtime/remote/Service.js +1 -0
- package/libx/_runtime/sqlite/convertDraftAdminPathExpression.js +19 -3
- package/libx/_runtime/sqlite/customBuilder/CustomExpressionBuilder.js +0 -18
- package/libx/_runtime/sqlite/customBuilder/CustomFunctionBuilder.js +0 -18
- package/libx/_runtime/sqlite/customBuilder/CustomSelectBuilder.js +0 -24
- package/libx/_runtime/sqlite/customBuilder/CustomUpsertBuilder.js +2 -1
- package/libx/_runtime/sqlite/customBuilder/index.js +47 -32
- package/libx/odata/afterburner.js +17 -5
- package/libx/odata/grammar.pegjs +3 -4
- package/libx/odata/index.js +5 -1
- package/libx/odata/parseToCqn.js +3 -3
- package/libx/odata/parser.js +1 -1
- package/libx/odata/utils.js +58 -1
- package/package.json +1 -1
- package/server.js +1 -1
- package/libx/_runtime/sqlite/customBuilder/CustomDeleteBuilder.js +0 -17
- package/libx/_runtime/sqlite/customBuilder/CustomReferenceBuilder.js +0 -11
- package/libx/_runtime/sqlite/customBuilder/CustomUpdateBuilder.js +0 -17
- /package/bin/build/provider/hana/template/{.hdiconfig → .hdiconfig-haas} +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const cds = require('../cds'),
|
|
2
|
-
{ Object_keys
|
|
2
|
+
{ Object_keys } = cds.utils
|
|
3
3
|
const LOG = cds.log('fiori|drafts')
|
|
4
4
|
|
|
5
5
|
const DRAFT_ELEMENTS = new Set([
|
|
@@ -16,48 +16,255 @@ const REDUCED_DRAFT_ELEMENTS = new Set(['IsActiveEntity', 'HasDraftEntity', 'Sib
|
|
|
16
16
|
const DRAFT_ADMIN_ELEMENTS = [
|
|
17
17
|
'DraftUUID',
|
|
18
18
|
'LastChangedByUser',
|
|
19
|
+
'LastChangeDateTime',
|
|
19
20
|
'CreatedByUser',
|
|
21
|
+
'CreationDateTime',
|
|
20
22
|
'InProcessByUser',
|
|
21
23
|
'DraftIsCreatedByMe',
|
|
22
24
|
'DraftIsProcessedByMe'
|
|
23
25
|
]
|
|
24
26
|
|
|
27
|
+
const _inProcessByUserXpr = lockShiftedNow => ({
|
|
28
|
+
xpr: [
|
|
29
|
+
'case',
|
|
30
|
+
'when',
|
|
31
|
+
{ ref: ['LastChangeDateTime'] },
|
|
32
|
+
'<',
|
|
33
|
+
{ val: lockShiftedNow },
|
|
34
|
+
'then',
|
|
35
|
+
{ val: '' },
|
|
36
|
+
'else',
|
|
37
|
+
{ ref: ['InProcessByUser'] },
|
|
38
|
+
'end'
|
|
39
|
+
],
|
|
40
|
+
as: 'InProcessByUser',
|
|
41
|
+
cast: { type: 'cds.String' }
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
const _lock = {
|
|
45
|
+
get shiftedNow() {
|
|
46
|
+
return new Date(Math.max(0, Date.now() - DRAFT_CANCEL_TIMEOUT_IN_MIN() * 60 * 1000)).toISOString()
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
25
50
|
const DRAFT_CANCEL_TIMEOUT_IN_MIN = () =>
|
|
26
51
|
(cds.env.drafts?.cancellationTimeout && Number(cds.env.drafts?.cancellationTimeout)) || 15
|
|
27
52
|
|
|
53
|
+
const _redirectRefToDrafts = (ref, model) => {
|
|
54
|
+
const [root, ...tail] = ref
|
|
55
|
+
const draft = model.definitions[root.id || root].drafts
|
|
56
|
+
return [root.id ? { ...root, id: draft.name } : draft.name, ...tail]
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const _redirectRefToActives = (ref, model) => {
|
|
60
|
+
const [root, ...tail] = ref
|
|
61
|
+
const active = model.definitions[root.id || root].actives
|
|
62
|
+
return [root.id ? { ...root, id: active.name } : active.name, ...tail]
|
|
63
|
+
}
|
|
64
|
+
|
|
28
65
|
const h = cds.ApplicationService.prototype.handle
|
|
29
|
-
|
|
66
|
+
/* eslint-disable complexity */
|
|
67
|
+
cds.ApplicationService.prototype.handle = async function (req) {
|
|
30
68
|
const handle = h.bind(this)
|
|
31
69
|
|
|
32
|
-
|
|
70
|
+
if (
|
|
71
|
+
!req.query ||
|
|
72
|
+
(!req.query.SELECT && !req.query.INSERT && !req.query.UPDATE && !req.query.DELETE) ||
|
|
73
|
+
req.query._draftParams
|
|
74
|
+
)
|
|
75
|
+
return handle(req)
|
|
76
|
+
const query = _cleansed(req.query, this.model)
|
|
77
|
+
_cleanseParams(req.params)
|
|
78
|
+
const draftParams = query._draftParams
|
|
79
|
+
|
|
80
|
+
const _newReq = (req, query, draftParams, event) => {
|
|
33
81
|
// REVISIT: This is a bit hacky -> better way?
|
|
34
82
|
query._target = undefined
|
|
83
|
+
query._draftParams = draftParams
|
|
35
84
|
cds.infer(query, this.model.definitions)
|
|
36
85
|
const _req = cds.Request.for(req._) // REVISIT: this causes req._.data of WRITE reqs copied to READ reqs
|
|
37
86
|
if (query.SELECT) delete _req.data // which we fix here -> but this is an ugly workaround
|
|
38
87
|
_req.query = query
|
|
39
|
-
_req.event =
|
|
88
|
+
_req.event =
|
|
89
|
+
event ||
|
|
90
|
+
(query.SELECT && 'READ') ||
|
|
91
|
+
(query.INSERT && 'CREATE') ||
|
|
92
|
+
(query.UPDATE && 'UPDATE') ||
|
|
93
|
+
(query.DELETE && 'DELETE') ||
|
|
94
|
+
req.event
|
|
40
95
|
_req.target = query._target
|
|
41
96
|
_req._.params = req.params
|
|
97
|
+
_req.params = req.params
|
|
42
98
|
_req._.query = query
|
|
43
99
|
_req._ = req._
|
|
100
|
+
_req.data = _req.query.UPDATE?.data || _req.query.INSERT?.entries?.[0]
|
|
101
|
+
|
|
102
|
+
// Dirty hack: delegate messages to original request by binding the getter _messages to req
|
|
103
|
+
let proto = req
|
|
104
|
+
let _messagesDescr
|
|
105
|
+
while (proto && !(_messagesDescr = Object.getOwnPropertyDescriptor(proto, '_messages')))
|
|
106
|
+
proto = Object.getPrototypeOf(proto)
|
|
107
|
+
Object.defineProperty(_req, '_messages', { ..._messagesDescr, get: _messagesDescr.get.bind(req) })
|
|
108
|
+
|
|
44
109
|
return _req
|
|
45
110
|
}
|
|
46
111
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const draftParams = query._draftParams
|
|
50
|
-
if (req.event !== 'READ') {
|
|
51
|
-
const _req = _newReq(req, query)
|
|
112
|
+
const run = async query => {
|
|
113
|
+
const _req = _newReq(req, query, draftParams)
|
|
52
114
|
return handle(_req)
|
|
53
115
|
}
|
|
54
116
|
|
|
55
|
-
|
|
56
|
-
draftParams.IsActiveEntity
|
|
117
|
+
if (req.event === 'NEW' || req.event === 'CANCEL' || req.event === 'draftPrepare') {
|
|
118
|
+
if (draftParams.IsActiveEntity) req.reject(501)
|
|
119
|
+
req.target = req.target.drafts
|
|
120
|
+
|
|
121
|
+
if (query.INSERT?.into) {
|
|
122
|
+
if (typeof query.INSERT.into === 'string') query.INSERT.into = req.target.name
|
|
123
|
+
else if (query.INSERT.into.ref) query.INSERT.into.ref = _redirectRefToDrafts(query.INSERT.into.ref, this.model)
|
|
124
|
+
} else if (query.DELETE?.from?.ref) query.DELETE.from.ref = _redirectRefToDrafts(query.DELETE.from.ref, this.model)
|
|
125
|
+
else if (query.SELECT?.from?.ref) query.SELECT.from.ref = _redirectRefToDrafts(query.SELECT.from.ref, this.model)
|
|
126
|
+
const _req = _newReq(req, query, draftParams, req.event)
|
|
127
|
+
const result = await handle(_req)
|
|
128
|
+
return result
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (req.event === 'DELETE' && draftParams.IsActiveEntity) {
|
|
132
|
+
const draftsRef = _redirectRefToDrafts(query.DELETE.from.ref, this.model)
|
|
133
|
+
// NOTE: Check if draft is locked!
|
|
134
|
+
const draft = await run(
|
|
135
|
+
SELECT.one.from({ ref: draftsRef }).columns([
|
|
136
|
+
{ ref: ['DraftAdministrativeData_DraftUUID'] },
|
|
137
|
+
{
|
|
138
|
+
ref: ['DraftAdministrativeData'],
|
|
139
|
+
expand: [_inProcessByUserXpr(_lock.shiftedNow)]
|
|
140
|
+
}
|
|
141
|
+
])
|
|
142
|
+
)
|
|
143
|
+
const inProcessByUser = draft?.DraftAdministrativeData?.InProcessByUser
|
|
144
|
+
if (inProcessByUser && inProcessByUser !== cds.context.user.id) req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER')
|
|
145
|
+
const deletes = [run(DELETE.from({ ref: query.DELETE.from.ref }))]
|
|
146
|
+
if (draft)
|
|
147
|
+
deletes.push(
|
|
148
|
+
run(
|
|
149
|
+
DELETE.from(req.target.drafts).where({
|
|
150
|
+
DraftAdministrativeData_DraftUUID: draft.DraftAdministrativeData_DraftUUID
|
|
151
|
+
})
|
|
152
|
+
)
|
|
153
|
+
)
|
|
154
|
+
if (draft && req.target['@Common.DraftRoot.ActivationAction'])
|
|
155
|
+
deletes.push(
|
|
156
|
+
DELETE.from('DRAFT.DraftAdministrativeData').where({ DraftUUID: draft.DraftAdministrativeData_DraftUUID })
|
|
157
|
+
)
|
|
158
|
+
await Promise.all(deletes)
|
|
159
|
+
return req.data
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (req.event === 'draftActivate') {
|
|
163
|
+
LOG.debug('activate draft')
|
|
164
|
+
// It would be great if we'd have a SELECT ** to deeply expand the entity (along compositions), that should
|
|
165
|
+
// be implemented in expand implementation.
|
|
166
|
+
if (req.query.SELECT.from.ref.length > 1 || draftParams.IsActiveEntity !== false) {
|
|
167
|
+
req.reject(400, 'Action "draftActivate" can only be called on the root draft entity')
|
|
168
|
+
}
|
|
169
|
+
const targetDraft = req.target.drafts
|
|
170
|
+
const targetWhere = query.SELECT.from.ref[0].where
|
|
171
|
+
const cols = expandStarStar(targetDraft)
|
|
172
|
+
const res = await run(
|
|
173
|
+
SELECT.one
|
|
174
|
+
.from(targetDraft)
|
|
175
|
+
.columns(cols)
|
|
176
|
+
.columns([
|
|
177
|
+
'HasActiveEntity',
|
|
178
|
+
'DraftAdministrativeData_DraftUUID',
|
|
179
|
+
{ ref: ['DraftAdministrativeData'], expand: [{ ref: ['InProcessByUser'] }] }
|
|
180
|
+
])
|
|
181
|
+
.where(targetWhere)
|
|
182
|
+
)
|
|
183
|
+
if (!res) req.reject(404)
|
|
184
|
+
if (res.DraftAdministrativeData?.InProcessByUser !== cds.context.user.id)
|
|
185
|
+
req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER')
|
|
186
|
+
const DraftAdministrativeData_DraftUUID = res.DraftAdministrativeData_DraftUUID
|
|
187
|
+
delete res.DraftAdministrativeData_DraftUUID
|
|
188
|
+
delete res.DraftAdministrativeData
|
|
189
|
+
const HasActiveEntity = res.HasActiveEntity
|
|
190
|
+
delete res.HasActiveEntity
|
|
191
|
+
await Promise.all([
|
|
192
|
+
run(DELETE.from(targetDraft).where(targetWhere)),
|
|
193
|
+
DELETE.from('DRAFT.DraftAdministrativeData').where({ DraftUUID: DraftAdministrativeData_DraftUUID })
|
|
194
|
+
])
|
|
195
|
+
|
|
196
|
+
const result = await run(
|
|
197
|
+
HasActiveEntity ? UPDATE(req.target).data(res).where(targetWhere) : INSERT.into(req.target).entries(res)
|
|
198
|
+
)
|
|
199
|
+
req?._?.odataRes.setStatusCode(201)
|
|
200
|
+
|
|
201
|
+
return Object.assign(result, { IsActiveEntity: true })
|
|
202
|
+
|
|
203
|
+
// REVISIT: we need to use okra API here because it must be set in the batched request
|
|
204
|
+
// status code must be set in handler to allow overriding for FE V2
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (req.target.actions?.[req.event] && draftParams.IsActiveEntity === false) {
|
|
208
|
+
if (query.SELECT?.from?.ref) query.SELECT.from.ref = _redirectRefToDrafts(query.SELECT.from.ref, this.model)
|
|
209
|
+
const rootQuery = query.clone()
|
|
210
|
+
rootQuery.SELECT.columns = [{ ref: ['DraftAdministrativeData'], expand: [{ ref: ['InProcessByUser'] }] }]
|
|
211
|
+
rootQuery.SELECT.one = true
|
|
212
|
+
const root = await run(rootQuery)
|
|
213
|
+
if (!root) req.reject(404)
|
|
214
|
+
if (root.DraftAdministrativeData?.InProcessByUser !== cds.context.user.id) req.reject(403)
|
|
215
|
+
const _req = _newReq(req, query, draftParams, req.event)
|
|
216
|
+
const result = await handle(_req)
|
|
217
|
+
return result
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (req.event === 'PATCH') {
|
|
221
|
+
if (draftParams.IsActiveEntity) req.reject(501)
|
|
222
|
+
if (req.event === 'PATCH' && !('IsActiveEntity' in draftParams)) {
|
|
223
|
+
const res = await run(
|
|
224
|
+
SELECT.one.from({ ref: req.UPDATE.entity.ref }).columns('DraftAdministrativeData_DraftUUID')
|
|
225
|
+
)
|
|
226
|
+
if (res) req.reject(403, 'DRAFT_ALREADY_EXISTS')
|
|
227
|
+
const _req = _newReq(req, query, draftParams, 'UPDATE')
|
|
228
|
+
const result = await handle(_req)
|
|
229
|
+
return result
|
|
230
|
+
}
|
|
231
|
+
if (req.event === 'PATCH' && draftParams.IsActiveEntity === false) {
|
|
232
|
+
LOG.debug('patch draft')
|
|
233
|
+
if (req.target?.name.endsWith('DraftAdministrativeData')) req.reject(405)
|
|
234
|
+
const draftsRef = _redirectRefToDrafts(query.UPDATE.entity.ref, this.model)
|
|
235
|
+
const res = await run(
|
|
236
|
+
SELECT.one.from({ ref: draftsRef }).columns('DraftAdministrativeData_DraftUUID', {
|
|
237
|
+
ref: ['DraftAdministrativeData'],
|
|
238
|
+
expand: [{ ref: ['InProcessByUser'] }]
|
|
239
|
+
})
|
|
240
|
+
)
|
|
241
|
+
if (!res) req.reject(404)
|
|
242
|
+
if (res.DraftAdministrativeData?.InProcessByUser !== cds.context.user.id)
|
|
243
|
+
req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER')
|
|
244
|
+
await UPDATE('DRAFT.DraftAdministrativeData')
|
|
245
|
+
.data({
|
|
246
|
+
InProcessByUser: req.user.id,
|
|
247
|
+
LastChangedByUser: req.user.id,
|
|
248
|
+
LastChangeDateTime: new Date()
|
|
249
|
+
})
|
|
250
|
+
.where({ DraftUUID: res.DraftAdministrativeData_DraftUUID })
|
|
251
|
+
|
|
252
|
+
const updateData = { ...req.data }
|
|
253
|
+
delete updateData.IsActiveEntity
|
|
254
|
+
await run(UPDATE({ ref: draftsRef }).data(updateData))
|
|
255
|
+
return Object.assign(req.data, { IsActiveEntity: false })
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (req.event === 'READ') {
|
|
260
|
+
const read = req.query._target.name.endsWith('.drafts')
|
|
261
|
+
? Read.ownDrafts
|
|
262
|
+
: draftParams.IsActiveEntity === false && draftParams.SiblingEntity_IsActiveEntity === null
|
|
57
263
|
? Read.all
|
|
58
264
|
: draftParams.IsActiveEntity === true &&
|
|
59
265
|
draftParams.SiblingEntity_IsActiveEntity === null &&
|
|
60
|
-
draftParams.DraftAdministrativeData_InProcessByUser === 'not null'
|
|
266
|
+
(draftParams.DraftAdministrativeData_InProcessByUser === 'not null' ||
|
|
267
|
+
draftParams.DraftAdministrativeData_InProcessByUser === 'not ')
|
|
61
268
|
? Read.lockedByAnotherUser
|
|
62
269
|
: draftParams.IsActiveEntity === true &&
|
|
63
270
|
draftParams.SiblingEntity_IsActiveEntity === null &&
|
|
@@ -70,15 +277,18 @@ cds.ApplicationService.prototype.handle = function (req) {
|
|
|
70
277
|
: draftParams.IsActiveEntity === false
|
|
71
278
|
? Read.ownDrafts
|
|
72
279
|
: Read.onlyActives
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
return handle(_req)
|
|
280
|
+
const result = await read(run, query)
|
|
281
|
+
return result
|
|
76
282
|
}
|
|
77
|
-
|
|
283
|
+
|
|
284
|
+
const _req = _newReq(req, query, draftParams, req.event)
|
|
285
|
+
const result = await handle(_req)
|
|
286
|
+
return result
|
|
78
287
|
}
|
|
79
288
|
|
|
80
289
|
const Read = {
|
|
81
290
|
onlyActives: async function (run, query, { ignoreDrafts } = {}) {
|
|
291
|
+
LOG.debug('List Editing Status: Only Active')
|
|
82
292
|
// DraftAdministrativeData is only accessible via drafts
|
|
83
293
|
if (query._target.name.endsWith('.DraftAdministrativeData')) return run(query._drafts)
|
|
84
294
|
const actives = await run(query)
|
|
@@ -106,6 +316,7 @@ const Read = {
|
|
|
106
316
|
return actives
|
|
107
317
|
},
|
|
108
318
|
unchanged: async function (run, query) {
|
|
319
|
+
LOG.debug('List Editing Status: Unchanged')
|
|
109
320
|
const draftsQuery = query._drafts
|
|
110
321
|
const keys = Object_keys(query._target.keys).filter(k => k !== 'IsActiveEntity')
|
|
111
322
|
draftsQuery.SELECT.count = undefined
|
|
@@ -120,10 +331,29 @@ const Read = {
|
|
|
120
331
|
return res
|
|
121
332
|
},
|
|
122
333
|
ownDrafts: async function (run, query) {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
334
|
+
LOG.debug('List Editing Status: Own Draft')
|
|
335
|
+
|
|
336
|
+
// read active from draft
|
|
337
|
+
if (!query._drafts._target?.name.endsWith('.drafts')) {
|
|
338
|
+
const result = await run(query._drafts)
|
|
339
|
+
|
|
340
|
+
// active entity is draft enabled, draft columns have to be removed
|
|
341
|
+
if (query._drafts._target?.drafts) {
|
|
342
|
+
Read.merge(query._drafts._target, result, [], row => {
|
|
343
|
+
delete row.IsActiveEntity
|
|
344
|
+
delete row.HasDraftEntity
|
|
345
|
+
delete row.HasActiveEntity
|
|
346
|
+
delete row.DraftAdministrativeData_DraftUUID
|
|
347
|
+
})
|
|
348
|
+
}
|
|
349
|
+
return result
|
|
350
|
+
}
|
|
351
|
+
const draftsQuery = query._drafts.where(
|
|
352
|
+
{ ref: ['DraftAdministrativeData', 'InProcessByUser'] },
|
|
353
|
+
'=',
|
|
354
|
+
cds.context.user.id
|
|
126
355
|
)
|
|
356
|
+
const drafts = await run(draftsQuery)
|
|
127
357
|
Read.merge(query._target, drafts, [], row =>
|
|
128
358
|
Object.assign(row, {
|
|
129
359
|
IsActiveEntity: false,
|
|
@@ -133,52 +363,67 @@ const Read = {
|
|
|
133
363
|
return drafts
|
|
134
364
|
},
|
|
135
365
|
all: async function (run, query) {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
366
|
+
LOG.debug('List Editing Status: All')
|
|
367
|
+
query._drafts.SELECT.count = false
|
|
368
|
+
query._drafts.SELECT.limit = undefined // We need all entries for the keys to properly select actives (count)
|
|
369
|
+
const isCount = query._drafts.SELECT.columns?.[0]?.func === 'count'
|
|
370
|
+
if (isCount) {
|
|
371
|
+
const keys = Object_keys(query._target.keys).filter(k => k !== 'IsActiveEntity')
|
|
372
|
+
query._drafts.SELECT.columns = keys.map(k => ({ ref: [k] }))
|
|
373
|
+
}
|
|
374
|
+
if (!query._drafts.SELECT.columns) query._drafts.SELECT.columns = ['*']
|
|
375
|
+
if (!query._drafts.SELECT.columns.some(c => c.ref?.[0] === 'HasActiveEntity'))
|
|
376
|
+
query._drafts.SELECT.columns.push({ ref: ['HasActiveEntity'] })
|
|
377
|
+
|
|
378
|
+
const isFirstPage = !query.SELECT.limit?.offset?.val
|
|
379
|
+
|
|
380
|
+
const [ownDrafts, actives] = await Promise.all([
|
|
381
|
+
isFirstPage
|
|
382
|
+
? run(query._drafts.where({ ref: ['DraftAdministrativeData', 'InProcessByUser'] }, '=', cds.context.user.id))
|
|
383
|
+
: [], // we only show drafts on the first page
|
|
384
|
+
run(query)
|
|
385
|
+
])
|
|
386
|
+
|
|
387
|
+
const ownNewDrafts = []
|
|
388
|
+
const ownEditDrafts = []
|
|
389
|
+
for (const draft of ownDrafts) {
|
|
390
|
+
if (draft.HasActiveEntity) ownEditDrafts.push(draft)
|
|
391
|
+
else ownNewDrafts.push(draft)
|
|
392
|
+
}
|
|
143
393
|
|
|
144
|
-
const
|
|
394
|
+
const count = isFirstPage ? ownNewDrafts.length + (isCount ? actives[0]?.$count : actives.$count) : actives.$count
|
|
395
|
+
if (isCount) return { $count: count }
|
|
145
396
|
|
|
146
|
-
|
|
147
|
-
let actives
|
|
148
|
-
if (topActives === 0) {
|
|
149
|
-
actives = []
|
|
150
|
-
} else {
|
|
151
|
-
const skipActives = skip && skip - appliedSkip
|
|
152
|
-
query.SELECT.count = true
|
|
153
|
-
actives = await run(query.limit(topActives, skipActives).where(Read.whereNotIn(query._target, drafts)))
|
|
154
|
-
const draftsFromActives = await Read.complementaryDrafts(run, query, actives)
|
|
155
|
-
Read.merge(query._target, actives, draftsFromActives, (row, other) => {
|
|
156
|
-
if (other) {
|
|
157
|
-
Object.assign(row, other, {
|
|
158
|
-
IsActiveEntity: true,
|
|
159
|
-
HasDraftEntity: true,
|
|
160
|
-
HasActiveEntity: false
|
|
161
|
-
})
|
|
162
|
-
} else {
|
|
163
|
-
Object.assign(row, {
|
|
164
|
-
IsActiveEntity: true,
|
|
165
|
-
HasDraftEntity: false,
|
|
166
|
-
HasActiveEntity: false,
|
|
167
|
-
DraftAdministrativeData_DraftUUID: null,
|
|
168
|
-
DraftAdministrativeData: null
|
|
169
|
-
})
|
|
170
|
-
}
|
|
171
|
-
})
|
|
172
|
-
}
|
|
173
|
-
Read.merge(query._target, drafts, [], row =>
|
|
397
|
+
Read.merge(query._target, ownDrafts, [], row =>
|
|
174
398
|
Object.assign(row, {
|
|
175
399
|
IsActiveEntity: false,
|
|
176
400
|
HasDraftEntity: false
|
|
177
401
|
})
|
|
178
402
|
)
|
|
179
|
-
|
|
180
|
-
const
|
|
181
|
-
|
|
403
|
+
Read.delete(query._target, actives, ownEditDrafts)
|
|
404
|
+
const otherEditDrafts = await Read.complementaryDrafts(run, query, actives)
|
|
405
|
+
Read.merge(query._target, actives, otherEditDrafts, (row, other) => {
|
|
406
|
+
if (other) {
|
|
407
|
+
Object.assign(row, {
|
|
408
|
+
IsActiveEntity: true,
|
|
409
|
+
HasDraftEntity: true,
|
|
410
|
+
HasActiveEntity: false,
|
|
411
|
+
DraftAdministrativeData_DraftUUID: other.DraftAdministrativeData_DraftUUID,
|
|
412
|
+
DraftAdministrativeData: other.DraftAdministrativeData
|
|
413
|
+
})
|
|
414
|
+
} else {
|
|
415
|
+
Object.assign(row, {
|
|
416
|
+
IsActiveEntity: true,
|
|
417
|
+
HasDraftEntity: false,
|
|
418
|
+
HasActiveEntity: false,
|
|
419
|
+
DraftAdministrativeData_DraftUUID: null,
|
|
420
|
+
DraftAdministrativeData: null
|
|
421
|
+
})
|
|
422
|
+
}
|
|
423
|
+
})
|
|
424
|
+
const res = isFirstPage ? [...ownNewDrafts, ...ownEditDrafts, ...actives] : actives
|
|
425
|
+
if (query.SELECT.count) res.$count = count
|
|
426
|
+
return res
|
|
182
427
|
},
|
|
183
428
|
activesFromDrafts: async function (run, query, { isLocked = true }) {
|
|
184
429
|
const draftsQuery = query._drafts
|
|
@@ -193,7 +438,7 @@ const Read = {
|
|
|
193
438
|
HasActiveEntity: true,
|
|
194
439
|
'DraftAdministrativeData.InProcessByUser': { '!=': cds.context.user.id },
|
|
195
440
|
'DraftAdministrativeData.LastChangeDateTime': {
|
|
196
|
-
[isLocked ? '>' : '<']:
|
|
441
|
+
[isLocked ? '>' : '<']: _lock.shiftedNow
|
|
197
442
|
}
|
|
198
443
|
})
|
|
199
444
|
const drafts = await run(draftsQuery)
|
|
@@ -208,14 +453,13 @@ const Read = {
|
|
|
208
453
|
return actives
|
|
209
454
|
},
|
|
210
455
|
unsavedChangesByAnotherUser: async function (run, query) {
|
|
456
|
+
LOG.debug('List Editing Status: Unsaved Changes by Another User')
|
|
211
457
|
return Read.activesFromDrafts(run, query, { isLocked: false })
|
|
212
458
|
},
|
|
213
459
|
lockedByAnotherUser: async function (run, query) {
|
|
460
|
+
LOG.debug('List Editing Status: Locked by Another User')
|
|
214
461
|
return Read.activesFromDrafts(run, query, { isLocked: true })
|
|
215
462
|
},
|
|
216
|
-
get lockshiftedNow() {
|
|
217
|
-
return new Date(Date.now() - DRAFT_CANCEL_TIMEOUT_IN_MIN() * 60 * 1000).toISOString()
|
|
218
|
-
},
|
|
219
463
|
whereNotIn: (target, data) => Read.whereIn(target, data, true),
|
|
220
464
|
whereIn: (target, data, not = false) => {
|
|
221
465
|
const keys = Object_keys(target.keys).filter(k => k !== 'IsActiveEntity')
|
|
@@ -231,6 +475,12 @@ const Read = {
|
|
|
231
475
|
if (!actives.length) return []
|
|
232
476
|
const drafts = cds.ql.clone(query._drafts)
|
|
233
477
|
drafts.SELECT.where = Read.whereIn(query._target, actives)
|
|
478
|
+
if (drafts.SELECT.columns?.some(c => c === '*')) {
|
|
479
|
+
drafts.SELECT.columns = drafts.SELECT.columns.filter(c => c !== '*')
|
|
480
|
+
if (!drafts.SELECT.columns.some(c => c.ref?.[0] === 'DraftAdministrativeData_DraftUUID')) {
|
|
481
|
+
drafts.SELECT.columns.push({ ref: ['DraftAdministrativeData_DraftUUID'] })
|
|
482
|
+
}
|
|
483
|
+
}
|
|
234
484
|
const relevantColumns = ['DraftAdministrativeData', 'DraftAdministrativeData_DraftUUID']
|
|
235
485
|
drafts.SELECT.columns = (
|
|
236
486
|
drafts.SELECT.columns?.filter(c => c.ref && relevantColumns.includes(c.ref[0])) ||
|
|
@@ -241,60 +491,100 @@ const Read = {
|
|
|
241
491
|
.map(k => ({ ref: [k] }))
|
|
242
492
|
)
|
|
243
493
|
drafts.SELECT.count = undefined
|
|
494
|
+
drafts.SELECT.search = undefined
|
|
244
495
|
drafts.SELECT.one = undefined
|
|
245
496
|
return run(drafts)
|
|
246
497
|
},
|
|
498
|
+
_makeArray: data => (Array.isArray(data) ? data : data ? [data] : []),
|
|
499
|
+
_index: (target, data) => {
|
|
500
|
+
// Indexes the data for fast key access
|
|
501
|
+
const dataArray = Read._makeArray(data)
|
|
502
|
+
if (!dataArray.length) return
|
|
503
|
+
const _keys = Object_keys(target.keys).filter(k => k !== 'IsActiveEntity')
|
|
504
|
+
const hash = row => _keys.map(k => row[k]).reduce((res, curr) => res + '|$|' + curr, '')
|
|
505
|
+
const hashMap = new Map()
|
|
506
|
+
for (const row of dataArray) hashMap.set(hash(row), row)
|
|
507
|
+
return { hashMap, hash }
|
|
508
|
+
},
|
|
509
|
+
// Calls `cb` for each entry of data with a potential counterpart in otherData
|
|
247
510
|
merge: (target, data, otherData, cb) => {
|
|
248
|
-
|
|
249
|
-
const dataArray = Array.isArray(data) ? data : [data]
|
|
511
|
+
const dataArray = Read._makeArray(data)
|
|
250
512
|
if (!dataArray.length) return
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
const
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
513
|
+
|
|
514
|
+
const index = Read._index(target, otherData)
|
|
515
|
+
for (const row of dataArray) {
|
|
516
|
+
const other = index?.hashMap.get(index.hash(row))
|
|
517
|
+
cb(row, other)
|
|
518
|
+
}
|
|
519
|
+
},
|
|
520
|
+
// Deletes entries of data with a counterpart in otherData
|
|
521
|
+
delete: (target, data, otherData) => {
|
|
522
|
+
if (!Array.isArray(data) || !data.length) return
|
|
523
|
+
|
|
524
|
+
const index = Read._index(target, otherData)
|
|
525
|
+
let i = data.length
|
|
526
|
+
while (i--) {
|
|
527
|
+
if (index?.hashMap.get(index.hash(data[i]))) data.splice(i, 1)
|
|
265
528
|
}
|
|
266
529
|
}
|
|
267
530
|
}
|
|
268
531
|
|
|
532
|
+
function _cleanseParams(params) {
|
|
533
|
+
if (Array.isArray(params)) {
|
|
534
|
+
for (const param of params) _cleanseParams(param)
|
|
535
|
+
return
|
|
536
|
+
}
|
|
537
|
+
if (typeof params === 'object') {
|
|
538
|
+
for (const key in params) {
|
|
539
|
+
if (key === 'IsActiveEntity') delete params[key]
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function _cleanseCols(columns, elements) {
|
|
545
|
+
if (typeof columns?.filter !== 'function') return columns
|
|
546
|
+
return (
|
|
547
|
+
columns &&
|
|
548
|
+
columns
|
|
549
|
+
.filter(c => !elements.has(c.ref?.[0]))
|
|
550
|
+
.map(c => {
|
|
551
|
+
if (c.expand) return { ...c, expand: _cleanseCols(c.expand, elements) }
|
|
552
|
+
return c
|
|
553
|
+
})
|
|
554
|
+
)
|
|
555
|
+
}
|
|
556
|
+
|
|
269
557
|
/**
|
|
270
558
|
* Creates a clone of the query, cleanses and collects all draft parameters into ._draftParams.
|
|
271
559
|
*/
|
|
272
|
-
function _cleansed(query, model
|
|
560
|
+
function _cleansed(query, model) {
|
|
273
561
|
const draftParams = {} //> used to collect draft filter criteria
|
|
274
562
|
const q = _cleanseQuery(query, draftParams)
|
|
275
563
|
if (query.SELECT) {
|
|
276
|
-
let cache
|
|
277
564
|
const getDrafts = () => {
|
|
278
|
-
if (cache) return cache
|
|
279
565
|
const draftsQuery = _cleanseQuery(query, {}) // could just clone `q` but the latter is ruined by database layer
|
|
566
|
+
draftsQuery._target = undefined
|
|
280
567
|
const [root, ...tail] = draftsQuery.SELECT.from.ref
|
|
281
568
|
const draft = model.definitions[root.id || root].drafts
|
|
282
569
|
draftsQuery.SELECT.from = {
|
|
283
570
|
ref: [root.id ? { ...root, id: draft.name } : draft.name, ...tail]
|
|
284
571
|
}
|
|
572
|
+
cds.infer(draftsQuery, model.definitions)
|
|
573
|
+
// draftsQuery._target = draftsQuery._target?.drafts || draftsQuery._target
|
|
285
574
|
if (query.SELECT.columns && query._target.drafts)
|
|
286
|
-
draftsQuery.SELECT.columns = query.SELECT.columns
|
|
575
|
+
draftsQuery.SELECT.columns = _cleanseCols(query.SELECT.columns, REDUCED_DRAFT_ELEMENTS)
|
|
287
576
|
|
|
288
577
|
if (draftsQuery._target.name.endsWith('.DraftAdministrativeData')) {
|
|
289
578
|
draftsQuery.SELECT.columns = _tweakAdminCols(draftsQuery.SELECT.columns)
|
|
290
|
-
} else if (
|
|
579
|
+
} else if (draftsQuery._target?.name.endsWith('.drafts')) {
|
|
291
580
|
draftsQuery.SELECT.columns = _tweakAdminExpand(draftsQuery.SELECT.columns)
|
|
292
581
|
}
|
|
293
582
|
Object.defineProperty(draftsQuery, '_draftParams', { value: draftParams, enumerable: false })
|
|
294
|
-
|
|
583
|
+
Object.defineProperty(q, '_drafts', { value: draftsQuery })
|
|
295
584
|
return draftsQuery
|
|
296
585
|
}
|
|
297
586
|
Object.defineProperty(q, '_drafts', {
|
|
587
|
+
configurable: true,
|
|
298
588
|
get() {
|
|
299
589
|
return getDrafts()
|
|
300
590
|
}
|
|
@@ -325,7 +615,7 @@ function _cleansed(query, model, target) {
|
|
|
325
615
|
}
|
|
326
616
|
|
|
327
617
|
if (cqn.where) cqn.where = _cleanseWhere(cqn.where, draftParams)
|
|
328
|
-
if (cqn.columns) cqn.columns = q.SELECT.columns
|
|
618
|
+
if (cqn.columns) cqn.columns = _cleanseCols(q.SELECT.columns, DRAFT_ELEMENTS)
|
|
329
619
|
if (cqn.orderBy) cqn.orderBy = _cleanseWhere(cqn.orderBy, {})
|
|
330
620
|
|
|
331
621
|
return q
|
|
@@ -342,7 +632,7 @@ function _cleansed(query, model, target) {
|
|
|
342
632
|
}
|
|
343
633
|
|
|
344
634
|
function _tweakAdminCols(columns) {
|
|
345
|
-
if (!columns) columns = DRAFT_ADMIN_ELEMENTS.map(k => ({ ref: [k] }))
|
|
635
|
+
if (!columns || columns.some(c => c === '*')) columns = DRAFT_ADMIN_ELEMENTS.map(k => ({ ref: [k] }))
|
|
346
636
|
return columns.map(col => {
|
|
347
637
|
const name = col.ref?.[0]
|
|
348
638
|
if (!name) return col
|
|
@@ -367,22 +657,7 @@ function _cleansed(query, model, target) {
|
|
|
367
657
|
cast: { type: 'cds.Boolean' }
|
|
368
658
|
}
|
|
369
659
|
case 'InProcessByUser':
|
|
370
|
-
return
|
|
371
|
-
xpr: [
|
|
372
|
-
'case',
|
|
373
|
-
'when',
|
|
374
|
-
{ ref: ['LastChangeDateTime'] },
|
|
375
|
-
'<',
|
|
376
|
-
{ val: Read.lockshiftedNow },
|
|
377
|
-
'then',
|
|
378
|
-
{ val: '' },
|
|
379
|
-
'else',
|
|
380
|
-
{ ref: ['InProcessByUser'] },
|
|
381
|
-
'end'
|
|
382
|
-
],
|
|
383
|
-
as: 'InProcessByUser',
|
|
384
|
-
cast: { type: 'cds.String' }
|
|
385
|
-
}
|
|
660
|
+
return _inProcessByUserXpr(_lock.shiftedNow)
|
|
386
661
|
case 'DraftIsProcessedByMe':
|
|
387
662
|
return {
|
|
388
663
|
xpr: [
|
|
@@ -391,6 +666,10 @@ function _cleansed(query, model, target) {
|
|
|
391
666
|
{ ref: ['InProcessByUser'] },
|
|
392
667
|
'=',
|
|
393
668
|
{ val: cds.context.user.id },
|
|
669
|
+
'and',
|
|
670
|
+
{ ref: ['LastChangeDateTime'] },
|
|
671
|
+
'>',
|
|
672
|
+
{ val: _lock.shiftedNow },
|
|
394
673
|
'then',
|
|
395
674
|
{ val: true },
|
|
396
675
|
'else',
|
|
@@ -411,9 +690,12 @@ function _cleansed(query, model, target) {
|
|
|
411
690
|
for (let i = 0; i < xpr.length; ++i) {
|
|
412
691
|
let x = xpr[i],
|
|
413
692
|
e = x.ref?.[0]
|
|
414
|
-
if (DRAFT_ELEMENTS.has(e)) {
|
|
693
|
+
if (DRAFT_ELEMENTS.has(e) && !xpr[i + 2]) {
|
|
694
|
+
continue
|
|
695
|
+
}
|
|
696
|
+
if (DRAFT_ELEMENTS.has(e) && xpr[i + 2]) {
|
|
415
697
|
let { val } = xpr[i + 2]
|
|
416
|
-
draftParams[x.ref.join('_')] = xpr[i + 1] === '!=' ? 'not ' + val : val
|
|
698
|
+
draftParams[x.ref.join('_')] = xpr[i + 1] === '!=' ? (typeof val === 'boolean' ? !val : 'not ' + val) : val
|
|
417
699
|
i += 3
|
|
418
700
|
continue
|
|
419
701
|
}
|
|
@@ -432,27 +714,46 @@ function _cleansed(query, model, target) {
|
|
|
432
714
|
}
|
|
433
715
|
}
|
|
434
716
|
|
|
435
|
-
function
|
|
436
|
-
|
|
717
|
+
// This function is better defined on DB layer
|
|
718
|
+
function expandStarStar(target, recursion = new Map()) {
|
|
719
|
+
const MAX_RECURSION_DEPTH = (cds.env.features.recursion_depth && Number(cds.env.features.recursion_depth)) || 4
|
|
720
|
+
const columns = []
|
|
721
|
+
for (const el in target.elements) {
|
|
722
|
+
const element = target.elements[el]
|
|
723
|
+
if (!element.isAssociation && !DRAFT_ELEMENTS.has(el)) columns.push({ ref: [el] })
|
|
724
|
+
if (!element.isComposition || element._target['@odata.draft.enabled'] === false) continue // happens for texts if not @fiori.draft.enabled
|
|
725
|
+
const _key = target.name + ':' + el
|
|
726
|
+
let cache = recursion.get(_key)
|
|
727
|
+
if (!cache) {
|
|
728
|
+
cache = 1
|
|
729
|
+
recursion.set(_key, cache)
|
|
730
|
+
} else {
|
|
731
|
+
cache++
|
|
732
|
+
recursion.set(_key, cache)
|
|
733
|
+
}
|
|
734
|
+
if (cache >= MAX_RECURSION_DEPTH) return
|
|
735
|
+
const expand = expandStarStar(element._target, recursion)
|
|
736
|
+
if (expand) columns.push({ ref: [el], expand })
|
|
737
|
+
}
|
|
738
|
+
return columns
|
|
437
739
|
}
|
|
438
740
|
|
|
439
|
-
async function
|
|
741
|
+
async function onNew(req) {
|
|
440
742
|
LOG.debug('new draft')
|
|
441
743
|
const isRoot = typeof req.query.INSERT.into === 'string'
|
|
442
744
|
let DraftUUID
|
|
443
745
|
if (isRoot) DraftUUID = cds.utils.uuid()
|
|
444
746
|
else {
|
|
445
|
-
const rootData = await
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
if (
|
|
453
|
-
!rootData.DraftAdministrativeData.InProcessByUser === req.user.id &&
|
|
454
|
-
_draftIsLocked(rootData.DraftAdministrativeData.LastChangeDateTime)
|
|
747
|
+
const rootData = await this.run(
|
|
748
|
+
SELECT.one(req.query.INSERT.into.ref[0].id)
|
|
749
|
+
.columns([
|
|
750
|
+
{ ref: ['DraftAdministrativeData_DraftUUID'] },
|
|
751
|
+
{ ref: ['DraftAdministrativeData'], expand: [{ ref: ['InProcessByUser'] }] }
|
|
752
|
+
])
|
|
753
|
+
.where(req.query.INSERT.into.ref[0].where)
|
|
455
754
|
)
|
|
755
|
+
if (!rootData) req.reject(404)
|
|
756
|
+
if (rootData.DraftAdministrativeData?.InProcessByUser !== req.user.id)
|
|
456
757
|
req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER')
|
|
457
758
|
DraftUUID = rootData.DraftAdministrativeData_DraftUUID
|
|
458
759
|
}
|
|
@@ -480,60 +781,21 @@ async function onNewDraft(req) {
|
|
|
480
781
|
{ DraftAdministrativeData_DraftUUID: DraftUUID, HasActiveEntity: false },
|
|
481
782
|
req.query.INSERT.entries[0]
|
|
482
783
|
)
|
|
784
|
+
|
|
483
785
|
delete draftData.IsActiveEntity
|
|
484
|
-
const draftCQN = INSERT.into(req.target
|
|
786
|
+
const draftCQN = INSERT.into(req.target).entries(draftData)
|
|
485
787
|
|
|
486
|
-
|
|
487
|
-
await Promise.all([adminDataCQN, draftCQN].map(cqn => cds.run(cqn)))
|
|
788
|
+
await Promise.all([cds.run(adminDataCQN), this.run(draftCQN)])
|
|
488
789
|
req._.readAfterWrite = true
|
|
489
790
|
return { ...draftData, IsActiveEntity: false }
|
|
490
791
|
}
|
|
491
|
-
exports.onNewDraft = onNewDraft
|
|
492
|
-
|
|
493
|
-
async function onDraftPrepare(req) {
|
|
494
|
-
LOG.debug('prepare draft')
|
|
495
|
-
|
|
496
|
-
const draftParams = req.query._draftParams
|
|
497
|
-
const where = req.query.SELECT.from.ref[0].where
|
|
498
|
-
if (req.query.SELECT.from.ref.length > 1 || draftParams.IsActiveEntity !== false) {
|
|
499
|
-
req.reject(400, 'Action "draftPrepare" can only be called on the root draft entity')
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
const keys = Object_keys(req.target.keys).filter(k => k !== 'IsActiveEntity')
|
|
503
|
-
const data = await SELECT.one
|
|
504
|
-
.from(req.target.drafts, d => {
|
|
505
|
-
d.DraftAdministrativeData(a => a.InProcessByUser)
|
|
506
|
-
})
|
|
507
|
-
.columns(keys)
|
|
508
|
-
.where(where)
|
|
509
|
-
if (!data) req.reject(404)
|
|
510
|
-
if (data.DraftAdministrativeData.InProcessByUser !== req.user.id) req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER')
|
|
511
|
-
delete data.DraftAdministrativeData
|
|
512
|
-
return { ...data, IsActiveEntity: false }
|
|
513
|
-
}
|
|
514
|
-
exports.onDraftPrepare = onDraftPrepare
|
|
515
|
-
|
|
516
|
-
// This function is better defined on DB layer
|
|
517
|
-
function expandStarStar(target, recursion = new Map()) {
|
|
518
|
-
const MAX_RECURSION_DEPTH = (cds.env.features.recursion_depth && Number(cds.env.features.recursion_depth)) || 4
|
|
519
|
-
// TODO: Remove draftcolumns from payload
|
|
520
|
-
const columns = []
|
|
521
|
-
for (const el in target.elements) {
|
|
522
|
-
const element = target.elements[el]
|
|
523
|
-
if (!element.isAssociation && !DRAFT_ELEMENTS.has(el)) columns.push({ ref: [el] })
|
|
524
|
-
if (!element.isComposition) continue
|
|
525
|
-
const _key = target.name + ':' + el
|
|
526
|
-
let cache = recursion.get(_key)
|
|
527
|
-
if (!cache) cache = 1 && recursion.set(_key, cache)
|
|
528
|
-
if (cache >= MAX_RECURSION_DEPTH) return
|
|
529
|
-
columns.push({ ref: [el], expand: expandStarStar(element._target, recursion) })
|
|
530
|
-
}
|
|
531
|
-
return columns
|
|
532
|
-
}
|
|
533
792
|
|
|
534
|
-
async function
|
|
793
|
+
async function onEdit(req) {
|
|
535
794
|
LOG.debug('edit active')
|
|
536
795
|
const draftParams = req.query._draftParams
|
|
796
|
+
if (req.query.SELECT.from.ref.length > 1 || draftParams.IsActiveEntity !== true) {
|
|
797
|
+
req.reject(400, 'Action "draftEdit" can only be called on the root entity')
|
|
798
|
+
}
|
|
537
799
|
const targetWhere = req.query.SELECT.from.ref[0].where
|
|
538
800
|
|
|
539
801
|
if (draftParams.IsActiveEntity !== true) req.reject(400)
|
|
@@ -554,13 +816,30 @@ async function onDraftEdit(req) {
|
|
|
554
816
|
}
|
|
555
817
|
}
|
|
556
818
|
_addDraftColumns(req.target, cols)
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
819
|
+
const draftsCheck = SELECT.one(req.target.drafts)
|
|
820
|
+
.columns({ ref: ['DraftAdministrativeData'], expand: [_inProcessByUserXpr(_lock.shiftedNow)] })
|
|
821
|
+
.where(targetWhere)
|
|
822
|
+
.forUpdate({ wait: 0 })
|
|
823
|
+
// prevent service to check for own user
|
|
824
|
+
Object.defineProperty(draftsCheck, '_draftParams', { value: draftParams, enumerable: false })
|
|
825
|
+
|
|
826
|
+
const [res, draft] = await Promise.all([
|
|
827
|
+
this.run(SELECT.one.from(req.target).columns(cols).where(targetWhere).forUpdate({ wait: 0 })),
|
|
828
|
+
// no user check must be done here...
|
|
829
|
+
this.run(draftsCheck)
|
|
561
830
|
])
|
|
562
831
|
if (!res) req.reject(404)
|
|
563
|
-
|
|
832
|
+
const preserveChanges = req.context?.data?.PreserveChanges
|
|
833
|
+
const inProcessByUser = draft?.DraftAdministrativeData?.InProcessByUser
|
|
834
|
+
if (draft) {
|
|
835
|
+
if (inProcessByUser || preserveChanges) req.reject(409, 'DRAFT_ALREADY_EXISTS')
|
|
836
|
+
const keys = {}
|
|
837
|
+
for (const key in req.target.drafts.keys) keys[key] = res[key]
|
|
838
|
+
await Promise.all([
|
|
839
|
+
DELETE.from('DRAFT.DraftAdministrativeData').where({ DraftUUID }),
|
|
840
|
+
this.run(DELETE.from(req.target.drafts).where(keys))
|
|
841
|
+
])
|
|
842
|
+
}
|
|
564
843
|
|
|
565
844
|
const timestamp = cds.context.timestamp.toISOString() // REVISIT: toISOString should be done on db layer
|
|
566
845
|
await INSERT.into('DRAFT.DraftAdministrativeData').entries({
|
|
@@ -575,7 +854,11 @@ async function onDraftEdit(req) {
|
|
|
575
854
|
})
|
|
576
855
|
|
|
577
856
|
const targetDraft = req.target.drafts
|
|
578
|
-
|
|
857
|
+
// is set to `null` on srv layer
|
|
858
|
+
res.DraftAdministrativeData_DraftUUID = DraftUUID
|
|
859
|
+
res.HasActiveEntity = true
|
|
860
|
+
delete res.DraftAdministrativeData
|
|
861
|
+
await this.run(INSERT.into(targetDraft).entries(res))
|
|
579
862
|
|
|
580
863
|
// REVISIT: we need to use okra API here because it must be set in the batched request
|
|
581
864
|
// status code must be set in handler to allow overriding for FE V2
|
|
@@ -583,86 +866,56 @@ async function onDraftEdit(req) {
|
|
|
583
866
|
|
|
584
867
|
return { ...res, IsActiveEntity: false } // REVISIT: Flatten?
|
|
585
868
|
}
|
|
586
|
-
exports.onDraftEdit = onDraftEdit
|
|
587
|
-
|
|
588
|
-
async function onDraftActivate(req) {
|
|
589
|
-
LOG.debug('activate draft')
|
|
590
|
-
// TODO: Read all drafts, send deep update/insert (based on HasActiveEntity, which should be fille with new!)
|
|
591
|
-
// It would be great if we'd have a SELECT ** to deeply expand the entity (along compositions), that should
|
|
592
|
-
// be implemented in expand implementation.
|
|
593
|
-
const targetDraft = req.target.drafts
|
|
594
|
-
const targetWhere = req.query.SELECT.from.ref[0].where
|
|
595
|
-
const res = await SELECT.one
|
|
596
|
-
.from(targetDraft)
|
|
597
|
-
.columns(expandStarStar(targetDraft))
|
|
598
|
-
.columns(['HasActiveEntity', 'DraftAdministrativeData_DraftUUID'])
|
|
599
|
-
.where(targetWhere)
|
|
600
|
-
const DraftAdministrativeData_DraftUUID = res.DraftAdministrativeData_DraftUUID
|
|
601
|
-
delete res.DraftAdministrativeData_DraftUUID
|
|
602
|
-
const HasActiveEntity = res.HasActiveEntity
|
|
603
|
-
delete res.HasActiveEntity
|
|
604
|
-
// TODO: Deep delete!
|
|
605
|
-
await Promise.all([
|
|
606
|
-
DELETE.from(targetDraft).where(targetWhere),
|
|
607
|
-
DELETE.from('DRAFT.DraftAdministrativeData').where({ DraftUUID: DraftAdministrativeData_DraftUUID })
|
|
608
|
-
])
|
|
609
|
-
// TODO: Check for InProcessByUser
|
|
610
|
-
|
|
611
|
-
let event, query
|
|
612
|
-
if (HasActiveEntity) {
|
|
613
|
-
query = UPDATE(req.target).data(res).where(targetWhere)
|
|
614
|
-
event = 'UPDATE'
|
|
615
|
-
} else {
|
|
616
|
-
query = INSERT.into(req.target).entries(res)
|
|
617
|
-
event = 'CREATE'
|
|
618
|
-
}
|
|
619
|
-
const r = new cds.Request({ event, query, data: res })
|
|
620
|
-
const result = await this.dispatch(r)
|
|
621
|
-
|
|
622
|
-
// REVISIT: we need to use okra API here because it must be set in the batched request
|
|
623
|
-
// status code must be set in handler to allow overriding for FE V2
|
|
624
|
-
req?._?.odataRes.setStatusCode(201)
|
|
625
|
-
|
|
626
|
-
return Object.assign(result, { IsActiveEntity: true })
|
|
627
|
-
}
|
|
628
|
-
exports.onDraftActivate = onDraftActivate
|
|
629
869
|
|
|
630
|
-
async function
|
|
631
|
-
LOG.debug('
|
|
632
|
-
const
|
|
633
|
-
const
|
|
634
|
-
const res = await SELECT.one.from(targetDraft).columns('DraftAdministrativeData_DraftUUID').where(targetWhere)
|
|
635
|
-
if (!res) req.reject(404)
|
|
636
|
-
await UPDATE('DRAFT.DraftAdministrativeData')
|
|
637
|
-
.data({
|
|
638
|
-
InProcessByUser: req.user.id,
|
|
639
|
-
LastChangedByUser: req.user.id,
|
|
640
|
-
LastChangeDateTime: new Date()
|
|
641
|
-
})
|
|
642
|
-
.where({ DraftUUID: res.DraftAdministrativeData_DraftUUID })
|
|
870
|
+
async function onCancel(req) {
|
|
871
|
+
LOG.debug('delete draft')
|
|
872
|
+
const activeRef = _redirectRefToActives(req.query.DELETE.from.ref, this.model)
|
|
873
|
+
const draftParams = req.query._draftParams
|
|
643
874
|
|
|
644
|
-
const
|
|
645
|
-
|
|
646
|
-
|
|
875
|
+
const draftDelete = SELECT.one
|
|
876
|
+
.from({ ref: req.query.DELETE.from.ref })
|
|
877
|
+
.columns([
|
|
878
|
+
'DraftAdministrativeData_DraftUUID',
|
|
879
|
+
{ ref: ['DraftAdministrativeData'], expand: [{ ref: ['InProcessByUser'] }] }
|
|
880
|
+
])
|
|
881
|
+
// do not add InProcessByUser restriction
|
|
882
|
+
Object.defineProperty(draftDelete, '_draftParams', { value: draftParams, enumerable: false })
|
|
883
|
+
const draft = await this.run(draftDelete)
|
|
884
|
+
if (draftParams.IsActiveEntity === false && !draft) req.reject(404)
|
|
885
|
+
if (draft && draft.DraftAdministrativeData?.InProcessByUser !== cds.context.user.id)
|
|
886
|
+
req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER')
|
|
887
|
+
const deletes = !draft ? [] : [this.run(DELETE.from({ ref: req.query.DELETE.from.ref }))]
|
|
888
|
+
if (draft && req.target['@Common.DraftRoot.ActivationAction'])
|
|
889
|
+
// only for draft root
|
|
890
|
+
deletes.push(
|
|
891
|
+
DELETE.from('DRAFT.DraftAdministrativeData').where({ DraftUUID: draft.DraftAdministrativeData_DraftUUID })
|
|
892
|
+
)
|
|
893
|
+
if (draftParams.IsActiveEntity) deletes.push(this.run(DELETE.from({ ref: activeRef })))
|
|
894
|
+
await Promise.all(deletes)
|
|
647
895
|
return req.data
|
|
648
896
|
}
|
|
649
|
-
exports.onPatch = onPatch
|
|
650
897
|
|
|
651
|
-
async function
|
|
652
|
-
LOG.debug('
|
|
653
|
-
const targetDraft = req.target.drafts
|
|
898
|
+
async function onPrepare(req) {
|
|
899
|
+
LOG.debug('prepare draft')
|
|
654
900
|
const draftParams = req.query._draftParams
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
const
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
901
|
+
if (req.query.SELECT.from.ref.length > 1 || draftParams.IsActiveEntity !== false) {
|
|
902
|
+
req.reject(400, 'Action "draftPrepare" can only be called on the root draft entity')
|
|
903
|
+
}
|
|
904
|
+
const where = req.query.SELECT.from.ref[0].where
|
|
905
|
+
|
|
906
|
+
const keys = Object_keys(req.target.keys).filter(k => k !== 'IsActiveEntity')
|
|
907
|
+
const draftQuery = SELECT.one
|
|
908
|
+
.from(req.target, d => {
|
|
909
|
+
d.DraftAdministrativeData(a => a.InProcessByUser)
|
|
910
|
+
})
|
|
911
|
+
.columns(keys)
|
|
912
|
+
.where(where)
|
|
913
|
+
Object.defineProperty(draftQuery, '_draftParams', { value: draftParams, enumerable: false })
|
|
914
|
+
const data = await this.run(draftQuery)
|
|
915
|
+
if (!data) req.reject(404)
|
|
916
|
+
if (data.DraftAdministrativeData?.InProcessByUser !== req.user.id) req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER')
|
|
917
|
+
delete data.DraftAdministrativeData
|
|
918
|
+
return { ...data, IsActiveEntity: false }
|
|
667
919
|
}
|
|
668
|
-
|
|
920
|
+
|
|
921
|
+
module.exports = { onNew, onEdit, onCancel, onPrepare }
|