@sap/cds 6.4.1 → 6.6.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 +79 -6
- package/README.md +5 -0
- package/apis/cqn.d.ts +14 -3
- package/apis/ql.d.ts +8 -8
- package/apis/services.d.ts +37 -65
- package/apis/test.d.ts +7 -0
- package/bin/build/buildTaskEngine.js +9 -14
- package/bin/build/buildTaskFactory.js +1 -1
- package/bin/build/buildTaskHandler.js +3 -14
- package/bin/build/index.js +8 -2
- package/bin/build/provider/buildTaskProviderInternal.js +18 -13
- 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/hana/template/package.json +3 -0
- package/bin/build/provider/mtx/resourcesTarBuilder.js +12 -3
- package/bin/build/provider/mtx-extension/index.js +57 -37
- package/bin/build/provider/mtx-sidecar/index.js +1 -1
- package/bin/build/util.js +18 -1
- package/bin/cds.js +1 -5
- package/bin/deploy/to-hana/hana.js +10 -3
- package/bin/serve.js +36 -20
- package/common.cds +7 -0
- package/lib/auth/jwt-auth.js +8 -7
- package/lib/compile/for/lean_drafts.js +55 -6
- package/lib/compile/minify.js +3 -3
- package/lib/dbs/cds-deploy.js +18 -17
- package/lib/env/cds-requires.js +1 -1
- package/lib/env/defaults.js +5 -1
- package/lib/env/schemas/cds-rc.json +74 -3
- package/lib/index.js +4 -2
- package/lib/lazy.js +6 -8
- package/lib/log/cds-error.js +2 -2
- package/lib/ql/Whereable.js +22 -11
- package/lib/ql/cds-ql.js +1 -1
- package/lib/req/cds-context.js +3 -3
- package/lib/req/response.js +8 -3
- package/lib/req/user.js +12 -2
- 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 +8 -3
- package/lib/utils/cds-test.js +7 -5
- package/lib/utils/cds-utils.js +3 -1
- package/lib/utils/tar.js +6 -3
- package/libx/_runtime/auth/strategies/JWT.js +1 -0
- package/libx/_runtime/auth/strategies/ias-auth.js +3 -2
- package/libx/_runtime/auth/strategies/mock.js +12 -1
- package/libx/_runtime/auth/strategies/xssecUtils.js +7 -8
- package/libx/_runtime/auth/strategies/xsuaa.js +1 -0
- package/libx/_runtime/cds-services/adapter/odata-v4/OData.js +6 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +1 -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 +8 -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 +1 -0
- 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 +11 -19
- package/libx/_runtime/cds-services/services/utils/columns.js +42 -40
- package/libx/_runtime/cds-services/util/assert.js +7 -1
- package/libx/_runtime/common/code-ext/WorkerReq.js +81 -0
- package/libx/_runtime/common/code-ext/config.js +13 -0
- package/libx/_runtime/common/code-ext/execute.js +113 -0
- package/libx/_runtime/common/code-ext/handlers.js +49 -0
- package/libx/_runtime/common/code-ext/worker.js +40 -0
- package/libx/_runtime/common/code-ext/workerQuery.js +45 -0
- package/libx/_runtime/common/code-ext/workerQueryExecutor.js +36 -0
- 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/crud.js +4 -0
- package/libx/_runtime/common/generic/etag.js +3 -1
- package/libx/_runtime/common/generic/input.js +12 -14
- package/libx/_runtime/common/i18n/index.js +1 -1
- package/libx/_runtime/common/utils/cqn2cqn4sql.js +47 -22
- package/libx/_runtime/common/utils/path.js +5 -26
- package/libx/_runtime/common/utils/search2cqn4sql.js +16 -9
- 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 +7 -4
- 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/db/utils/localized.js +1 -1
- package/libx/_runtime/fiori/generic/activate.js +7 -1
- package/libx/_runtime/fiori/generic/before.js +9 -1
- package/libx/_runtime/fiori/generic/edit.js +8 -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 +16 -5
- package/libx/_runtime/fiori/generic/readOverDraft.js +2 -0
- package/libx/_runtime/fiori/lean-draft.js +505 -241
- package/libx/_runtime/fiori/utils/delete.js +2 -0
- package/libx/_runtime/hana/customBuilder/CustomSelectBuilder.js +5 -5
- package/libx/_runtime/hana/pool.js +1 -1
- package/libx/_runtime/hana/search2cqn4sql.js +51 -51
- 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 +23 -8
- package/libx/odata/cqn2odata.js +1 -1
- 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/libx/rest/middleware/parse.js +26 -4
- 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,47 +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
|
-
const _req = cds.Request.for(req._)
|
|
85
|
+
const _req = cds.Request.for(req._) // REVISIT: this causes req._.data of WRITE reqs copied to READ reqs
|
|
86
|
+
if (query.SELECT) delete _req.data // which we fix here -> but this is an ugly workaround
|
|
37
87
|
_req.query = query
|
|
38
|
-
_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
|
|
39
95
|
_req.target = query._target
|
|
40
96
|
_req._.params = req.params
|
|
97
|
+
_req.params = req.params
|
|
41
98
|
_req._.query = query
|
|
42
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
|
+
|
|
43
109
|
return _req
|
|
44
110
|
}
|
|
45
111
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const draftParams = query._draftParams
|
|
49
|
-
if (req.event !== 'READ') {
|
|
50
|
-
const _req = _newReq(req, query)
|
|
112
|
+
const run = async query => {
|
|
113
|
+
const _req = _newReq(req, query, draftParams)
|
|
51
114
|
return handle(_req)
|
|
52
115
|
}
|
|
53
116
|
|
|
54
|
-
|
|
55
|
-
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
|
|
56
263
|
? Read.all
|
|
57
264
|
: draftParams.IsActiveEntity === true &&
|
|
58
265
|
draftParams.SiblingEntity_IsActiveEntity === null &&
|
|
59
|
-
draftParams.DraftAdministrativeData_InProcessByUser === 'not null'
|
|
266
|
+
(draftParams.DraftAdministrativeData_InProcessByUser === 'not null' ||
|
|
267
|
+
draftParams.DraftAdministrativeData_InProcessByUser === 'not ')
|
|
60
268
|
? Read.lockedByAnotherUser
|
|
61
269
|
: draftParams.IsActiveEntity === true &&
|
|
62
270
|
draftParams.SiblingEntity_IsActiveEntity === null &&
|
|
@@ -69,15 +277,18 @@ cds.ApplicationService.prototype.handle = function (req) {
|
|
|
69
277
|
: draftParams.IsActiveEntity === false
|
|
70
278
|
? Read.ownDrafts
|
|
71
279
|
: Read.onlyActives
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
return handle(_req)
|
|
280
|
+
const result = await read(run, query)
|
|
281
|
+
return result
|
|
75
282
|
}
|
|
76
|
-
|
|
283
|
+
|
|
284
|
+
const _req = _newReq(req, query, draftParams, req.event)
|
|
285
|
+
const result = await handle(_req)
|
|
286
|
+
return result
|
|
77
287
|
}
|
|
78
288
|
|
|
79
289
|
const Read = {
|
|
80
290
|
onlyActives: async function (run, query, { ignoreDrafts } = {}) {
|
|
291
|
+
LOG.debug('List Editing Status: Only Active')
|
|
81
292
|
// DraftAdministrativeData is only accessible via drafts
|
|
82
293
|
if (query._target.name.endsWith('.DraftAdministrativeData')) return run(query._drafts)
|
|
83
294
|
const actives = await run(query)
|
|
@@ -105,6 +316,7 @@ const Read = {
|
|
|
105
316
|
return actives
|
|
106
317
|
},
|
|
107
318
|
unchanged: async function (run, query) {
|
|
319
|
+
LOG.debug('List Editing Status: Unchanged')
|
|
108
320
|
const draftsQuery = query._drafts
|
|
109
321
|
const keys = Object_keys(query._target.keys).filter(k => k !== 'IsActiveEntity')
|
|
110
322
|
draftsQuery.SELECT.count = undefined
|
|
@@ -119,10 +331,29 @@ const Read = {
|
|
|
119
331
|
return res
|
|
120
332
|
},
|
|
121
333
|
ownDrafts: async function (run, query) {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
|
125
355
|
)
|
|
356
|
+
const drafts = await run(draftsQuery)
|
|
126
357
|
Read.merge(query._target, drafts, [], row =>
|
|
127
358
|
Object.assign(row, {
|
|
128
359
|
IsActiveEntity: false,
|
|
@@ -132,52 +363,67 @@ const Read = {
|
|
|
132
363
|
return drafts
|
|
133
364
|
},
|
|
134
365
|
all: async function (run, query) {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
+
}
|
|
142
393
|
|
|
143
|
-
const
|
|
394
|
+
const count = isFirstPage ? ownNewDrafts.length + (isCount ? actives[0]?.$count : actives.$count) : actives.$count
|
|
395
|
+
if (isCount) return { $count: count }
|
|
144
396
|
|
|
145
|
-
|
|
146
|
-
let actives
|
|
147
|
-
if (topActives === 0) {
|
|
148
|
-
actives = []
|
|
149
|
-
} else {
|
|
150
|
-
const skipActives = skip && skip - appliedSkip
|
|
151
|
-
query.SELECT.count = true
|
|
152
|
-
actives = await run(query.limit(topActives, skipActives).where(Read.whereNotIn(query._target, drafts)))
|
|
153
|
-
const draftsFromActives = await Read.complementaryDrafts(run, query, actives)
|
|
154
|
-
Read.merge(query._target, actives, draftsFromActives, (row, other) => {
|
|
155
|
-
if (other) {
|
|
156
|
-
Object.assign(row, other, {
|
|
157
|
-
IsActiveEntity: true,
|
|
158
|
-
HasDraftEntity: true,
|
|
159
|
-
HasActiveEntity: false
|
|
160
|
-
})
|
|
161
|
-
} else {
|
|
162
|
-
Object.assign(row, {
|
|
163
|
-
IsActiveEntity: true,
|
|
164
|
-
HasDraftEntity: false,
|
|
165
|
-
HasActiveEntity: false,
|
|
166
|
-
DraftAdministrativeData_DraftUUID: null,
|
|
167
|
-
DraftAdministrativeData: null
|
|
168
|
-
})
|
|
169
|
-
}
|
|
170
|
-
})
|
|
171
|
-
}
|
|
172
|
-
Read.merge(query._target, drafts, [], row =>
|
|
397
|
+
Read.merge(query._target, ownDrafts, [], row =>
|
|
173
398
|
Object.assign(row, {
|
|
174
399
|
IsActiveEntity: false,
|
|
175
400
|
HasDraftEntity: false
|
|
176
401
|
})
|
|
177
402
|
)
|
|
178
|
-
|
|
179
|
-
const
|
|
180
|
-
|
|
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
|
|
181
427
|
},
|
|
182
428
|
activesFromDrafts: async function (run, query, { isLocked = true }) {
|
|
183
429
|
const draftsQuery = query._drafts
|
|
@@ -192,7 +438,7 @@ const Read = {
|
|
|
192
438
|
HasActiveEntity: true,
|
|
193
439
|
'DraftAdministrativeData.InProcessByUser': { '!=': cds.context.user.id },
|
|
194
440
|
'DraftAdministrativeData.LastChangeDateTime': {
|
|
195
|
-
[isLocked ? '>' : '<']:
|
|
441
|
+
[isLocked ? '>' : '<']: _lock.shiftedNow
|
|
196
442
|
}
|
|
197
443
|
})
|
|
198
444
|
const drafts = await run(draftsQuery)
|
|
@@ -207,14 +453,13 @@ const Read = {
|
|
|
207
453
|
return actives
|
|
208
454
|
},
|
|
209
455
|
unsavedChangesByAnotherUser: async function (run, query) {
|
|
456
|
+
LOG.debug('List Editing Status: Unsaved Changes by Another User')
|
|
210
457
|
return Read.activesFromDrafts(run, query, { isLocked: false })
|
|
211
458
|
},
|
|
212
459
|
lockedByAnotherUser: async function (run, query) {
|
|
460
|
+
LOG.debug('List Editing Status: Locked by Another User')
|
|
213
461
|
return Read.activesFromDrafts(run, query, { isLocked: true })
|
|
214
462
|
},
|
|
215
|
-
get lockshiftedNow() {
|
|
216
|
-
return new Date(Date.now() - DRAFT_CANCEL_TIMEOUT_IN_MIN() * 60 * 1000).toISOString()
|
|
217
|
-
},
|
|
218
463
|
whereNotIn: (target, data) => Read.whereIn(target, data, true),
|
|
219
464
|
whereIn: (target, data, not = false) => {
|
|
220
465
|
const keys = Object_keys(target.keys).filter(k => k !== 'IsActiveEntity')
|
|
@@ -230,6 +475,12 @@ const Read = {
|
|
|
230
475
|
if (!actives.length) return []
|
|
231
476
|
const drafts = cds.ql.clone(query._drafts)
|
|
232
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
|
+
}
|
|
233
484
|
const relevantColumns = ['DraftAdministrativeData', 'DraftAdministrativeData_DraftUUID']
|
|
234
485
|
drafts.SELECT.columns = (
|
|
235
486
|
drafts.SELECT.columns?.filter(c => c.ref && relevantColumns.includes(c.ref[0])) ||
|
|
@@ -240,60 +491,100 @@ const Read = {
|
|
|
240
491
|
.map(k => ({ ref: [k] }))
|
|
241
492
|
)
|
|
242
493
|
drafts.SELECT.count = undefined
|
|
494
|
+
drafts.SELECT.search = undefined
|
|
243
495
|
drafts.SELECT.one = undefined
|
|
244
496
|
return run(drafts)
|
|
245
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
|
|
246
510
|
merge: (target, data, otherData, cb) => {
|
|
247
|
-
|
|
248
|
-
const dataArray = Array.isArray(data) ? data : [data]
|
|
511
|
+
const dataArray = Read._makeArray(data)
|
|
249
512
|
if (!dataArray.length) return
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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)
|
|
264
528
|
}
|
|
265
529
|
}
|
|
266
530
|
}
|
|
267
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
|
+
|
|
268
557
|
/**
|
|
269
558
|
* Creates a clone of the query, cleanses and collects all draft parameters into ._draftParams.
|
|
270
559
|
*/
|
|
271
|
-
function _cleansed(query, model
|
|
560
|
+
function _cleansed(query, model) {
|
|
272
561
|
const draftParams = {} //> used to collect draft filter criteria
|
|
273
562
|
const q = _cleanseQuery(query, draftParams)
|
|
274
563
|
if (query.SELECT) {
|
|
275
|
-
let cache
|
|
276
564
|
const getDrafts = () => {
|
|
277
|
-
if (cache) return cache
|
|
278
565
|
const draftsQuery = _cleanseQuery(query, {}) // could just clone `q` but the latter is ruined by database layer
|
|
566
|
+
draftsQuery._target = undefined
|
|
279
567
|
const [root, ...tail] = draftsQuery.SELECT.from.ref
|
|
280
568
|
const draft = model.definitions[root.id || root].drafts
|
|
281
569
|
draftsQuery.SELECT.from = {
|
|
282
570
|
ref: [root.id ? { ...root, id: draft.name } : draft.name, ...tail]
|
|
283
571
|
}
|
|
572
|
+
cds.infer(draftsQuery, model.definitions)
|
|
573
|
+
// draftsQuery._target = draftsQuery._target?.drafts || draftsQuery._target
|
|
284
574
|
if (query.SELECT.columns && query._target.drafts)
|
|
285
|
-
draftsQuery.SELECT.columns = query.SELECT.columns
|
|
575
|
+
draftsQuery.SELECT.columns = _cleanseCols(query.SELECT.columns, REDUCED_DRAFT_ELEMENTS)
|
|
286
576
|
|
|
287
577
|
if (draftsQuery._target.name.endsWith('.DraftAdministrativeData')) {
|
|
288
578
|
draftsQuery.SELECT.columns = _tweakAdminCols(draftsQuery.SELECT.columns)
|
|
289
|
-
} else if (
|
|
579
|
+
} else if (draftsQuery._target?.name.endsWith('.drafts')) {
|
|
290
580
|
draftsQuery.SELECT.columns = _tweakAdminExpand(draftsQuery.SELECT.columns)
|
|
291
581
|
}
|
|
292
582
|
Object.defineProperty(draftsQuery, '_draftParams', { value: draftParams, enumerable: false })
|
|
293
|
-
|
|
583
|
+
Object.defineProperty(q, '_drafts', { value: draftsQuery })
|
|
294
584
|
return draftsQuery
|
|
295
585
|
}
|
|
296
586
|
Object.defineProperty(q, '_drafts', {
|
|
587
|
+
configurable: true,
|
|
297
588
|
get() {
|
|
298
589
|
return getDrafts()
|
|
299
590
|
}
|
|
@@ -324,7 +615,7 @@ function _cleansed(query, model, target) {
|
|
|
324
615
|
}
|
|
325
616
|
|
|
326
617
|
if (cqn.where) cqn.where = _cleanseWhere(cqn.where, draftParams)
|
|
327
|
-
if (cqn.columns) cqn.columns = q.SELECT.columns
|
|
618
|
+
if (cqn.columns) cqn.columns = _cleanseCols(q.SELECT.columns, DRAFT_ELEMENTS)
|
|
328
619
|
if (cqn.orderBy) cqn.orderBy = _cleanseWhere(cqn.orderBy, {})
|
|
329
620
|
|
|
330
621
|
return q
|
|
@@ -341,7 +632,7 @@ function _cleansed(query, model, target) {
|
|
|
341
632
|
}
|
|
342
633
|
|
|
343
634
|
function _tweakAdminCols(columns) {
|
|
344
|
-
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] }))
|
|
345
636
|
return columns.map(col => {
|
|
346
637
|
const name = col.ref?.[0]
|
|
347
638
|
if (!name) return col
|
|
@@ -366,22 +657,7 @@ function _cleansed(query, model, target) {
|
|
|
366
657
|
cast: { type: 'cds.Boolean' }
|
|
367
658
|
}
|
|
368
659
|
case 'InProcessByUser':
|
|
369
|
-
return
|
|
370
|
-
xpr: [
|
|
371
|
-
'case',
|
|
372
|
-
'when',
|
|
373
|
-
{ ref: ['LastChangeDateTime'] },
|
|
374
|
-
'<',
|
|
375
|
-
{ val: Read.lockshiftedNow },
|
|
376
|
-
'then',
|
|
377
|
-
{ val: '' },
|
|
378
|
-
'else',
|
|
379
|
-
{ ref: ['InProcessByUser'] },
|
|
380
|
-
'end'
|
|
381
|
-
],
|
|
382
|
-
as: 'InProcessByUser',
|
|
383
|
-
cast: { type: 'cds.String' }
|
|
384
|
-
}
|
|
660
|
+
return _inProcessByUserXpr(_lock.shiftedNow)
|
|
385
661
|
case 'DraftIsProcessedByMe':
|
|
386
662
|
return {
|
|
387
663
|
xpr: [
|
|
@@ -390,6 +666,10 @@ function _cleansed(query, model, target) {
|
|
|
390
666
|
{ ref: ['InProcessByUser'] },
|
|
391
667
|
'=',
|
|
392
668
|
{ val: cds.context.user.id },
|
|
669
|
+
'and',
|
|
670
|
+
{ ref: ['LastChangeDateTime'] },
|
|
671
|
+
'>',
|
|
672
|
+
{ val: _lock.shiftedNow },
|
|
393
673
|
'then',
|
|
394
674
|
{ val: true },
|
|
395
675
|
'else',
|
|
@@ -410,9 +690,12 @@ function _cleansed(query, model, target) {
|
|
|
410
690
|
for (let i = 0; i < xpr.length; ++i) {
|
|
411
691
|
let x = xpr[i],
|
|
412
692
|
e = x.ref?.[0]
|
|
413
|
-
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]) {
|
|
414
697
|
let { val } = xpr[i + 2]
|
|
415
|
-
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
|
|
416
699
|
i += 3
|
|
417
700
|
continue
|
|
418
701
|
}
|
|
@@ -431,27 +714,46 @@ function _cleansed(query, model, target) {
|
|
|
431
714
|
}
|
|
432
715
|
}
|
|
433
716
|
|
|
434
|
-
function
|
|
435
|
-
|
|
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
|
|
436
739
|
}
|
|
437
740
|
|
|
438
|
-
async function
|
|
741
|
+
async function onNew(req) {
|
|
439
742
|
LOG.debug('new draft')
|
|
440
743
|
const isRoot = typeof req.query.INSERT.into === 'string'
|
|
441
744
|
let DraftUUID
|
|
442
745
|
if (isRoot) DraftUUID = cds.utils.uuid()
|
|
443
746
|
else {
|
|
444
|
-
const rootData = await
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
if (
|
|
452
|
-
!rootData.DraftAdministrativeData.InProcessByUser === req.user.id &&
|
|
453
|
-
_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)
|
|
454
754
|
)
|
|
755
|
+
if (!rootData) req.reject(404)
|
|
756
|
+
if (rootData.DraftAdministrativeData?.InProcessByUser !== req.user.id)
|
|
455
757
|
req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER')
|
|
456
758
|
DraftUUID = rootData.DraftAdministrativeData_DraftUUID
|
|
457
759
|
}
|
|
@@ -479,60 +781,21 @@ async function onNewDraft(req) {
|
|
|
479
781
|
{ DraftAdministrativeData_DraftUUID: DraftUUID, HasActiveEntity: false },
|
|
480
782
|
req.query.INSERT.entries[0]
|
|
481
783
|
)
|
|
784
|
+
|
|
482
785
|
delete draftData.IsActiveEntity
|
|
483
|
-
const draftCQN = INSERT.into(req.target
|
|
786
|
+
const draftCQN = INSERT.into(req.target).entries(draftData)
|
|
484
787
|
|
|
485
|
-
|
|
486
|
-
await Promise.all([adminDataCQN, draftCQN].map(cqn => cds.run(cqn)))
|
|
788
|
+
await Promise.all([cds.run(adminDataCQN), this.run(draftCQN)])
|
|
487
789
|
req._.readAfterWrite = true
|
|
488
790
|
return { ...draftData, IsActiveEntity: false }
|
|
489
791
|
}
|
|
490
|
-
exports.onNewDraft = onNewDraft
|
|
491
|
-
|
|
492
|
-
async function onDraftPrepare(req) {
|
|
493
|
-
LOG.debug('prepare draft')
|
|
494
|
-
|
|
495
|
-
const draftParams = req.query._draftParams
|
|
496
|
-
const where = req.query.SELECT.from.ref[0].where
|
|
497
|
-
if (req.query.SELECT.from.ref.length > 1 || draftParams.IsActiveEntity !== false) {
|
|
498
|
-
req.reject(400, 'Action "draftPrepare" can only be called on the root draft entity')
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
const keys = Object_keys(req.target.keys).filter(k => k !== 'IsActiveEntity')
|
|
502
|
-
const data = await SELECT.one
|
|
503
|
-
.from(req.target.drafts, d => {
|
|
504
|
-
d.DraftAdministrativeData(a => a.InProcessByUser)
|
|
505
|
-
})
|
|
506
|
-
.columns(keys)
|
|
507
|
-
.where(where)
|
|
508
|
-
if (!data) req.reject(404)
|
|
509
|
-
if (data.DraftAdministrativeData.InProcessByUser !== req.user.id) req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER')
|
|
510
|
-
delete data.DraftAdministrativeData
|
|
511
|
-
return { ...data, IsActiveEntity: false }
|
|
512
|
-
}
|
|
513
|
-
exports.onDraftPrepare = onDraftPrepare
|
|
514
|
-
|
|
515
|
-
// This function is better defined on DB layer
|
|
516
|
-
function expandStarStar(target, recursion = new Map()) {
|
|
517
|
-
const MAX_RECURSION_DEPTH = (cds.env.features.recursion_depth && Number(cds.env.features.recursion_depth)) || 4
|
|
518
|
-
// TODO: Remove draftcolumns from payload
|
|
519
|
-
const columns = []
|
|
520
|
-
for (const el in target.elements) {
|
|
521
|
-
const element = target.elements[el]
|
|
522
|
-
if (!element.isAssociation && !DRAFT_ELEMENTS.has(el)) columns.push({ ref: [el] })
|
|
523
|
-
if (!element.isComposition) continue
|
|
524
|
-
const _key = target.name + ':' + el
|
|
525
|
-
let cache = recursion.get(_key)
|
|
526
|
-
if (!cache) cache = 1 && recursion.set(_key, cache)
|
|
527
|
-
if (cache >= MAX_RECURSION_DEPTH) return
|
|
528
|
-
columns.push({ ref: [el], expand: expandStarStar(element._target, recursion) })
|
|
529
|
-
}
|
|
530
|
-
return columns
|
|
531
|
-
}
|
|
532
792
|
|
|
533
|
-
async function
|
|
793
|
+
async function onEdit(req) {
|
|
534
794
|
LOG.debug('edit active')
|
|
535
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
|
+
}
|
|
536
799
|
const targetWhere = req.query.SELECT.from.ref[0].where
|
|
537
800
|
|
|
538
801
|
if (draftParams.IsActiveEntity !== true) req.reject(400)
|
|
@@ -553,13 +816,30 @@ async function onDraftEdit(req) {
|
|
|
553
816
|
}
|
|
554
817
|
}
|
|
555
818
|
_addDraftColumns(req.target, cols)
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
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)
|
|
560
830
|
])
|
|
561
831
|
if (!res) req.reject(404)
|
|
562
|
-
|
|
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
|
+
}
|
|
563
843
|
|
|
564
844
|
const timestamp = cds.context.timestamp.toISOString() // REVISIT: toISOString should be done on db layer
|
|
565
845
|
await INSERT.into('DRAFT.DraftAdministrativeData').entries({
|
|
@@ -574,84 +854,68 @@ async function onDraftEdit(req) {
|
|
|
574
854
|
})
|
|
575
855
|
|
|
576
856
|
const targetDraft = req.target.drafts
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
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))
|
|
581
862
|
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
const targetDraft = req.target.drafts
|
|
588
|
-
const targetWhere = req.query.SELECT.from.ref[0].where
|
|
589
|
-
const res = await SELECT.one
|
|
590
|
-
.from(targetDraft)
|
|
591
|
-
.columns(expandStarStar(targetDraft))
|
|
592
|
-
.columns(['HasActiveEntity', 'DraftAdministrativeData_DraftUUID'])
|
|
593
|
-
.where(targetWhere)
|
|
594
|
-
const DraftAdministrativeData_DraftUUID = res.DraftAdministrativeData_DraftUUID
|
|
595
|
-
delete res.DraftAdministrativeData_DraftUUID
|
|
596
|
-
const HasActiveEntity = res.HasActiveEntity
|
|
597
|
-
delete res.HasActiveEntity
|
|
598
|
-
// TODO: Deep delete!
|
|
599
|
-
await Promise.all([
|
|
600
|
-
DELETE.from(targetDraft).where(targetWhere),
|
|
601
|
-
DELETE.from('DRAFT.DraftAdministrativeData').where({ DraftUUID: DraftAdministrativeData_DraftUUID })
|
|
602
|
-
])
|
|
603
|
-
// TODO: Check for InProcessByUser
|
|
604
|
-
|
|
605
|
-
let event, query
|
|
606
|
-
if (HasActiveEntity) {
|
|
607
|
-
query = UPDATE(req.target).data(res).where(targetWhere)
|
|
608
|
-
event = 'UPDATE'
|
|
609
|
-
} else {
|
|
610
|
-
query = INSERT.into(req.target).entries(res)
|
|
611
|
-
event = 'CREATE'
|
|
612
|
-
}
|
|
613
|
-
const r = new cds.Request({ event, query, data: res })
|
|
614
|
-
const result = await this.dispatch(r)
|
|
615
|
-
return Object.assign(result, { IsActiveEntity: true })
|
|
863
|
+
// REVISIT: we need to use okra API here because it must be set in the batched request
|
|
864
|
+
// status code must be set in handler to allow overriding for FE V2
|
|
865
|
+
req?._?.odataRes.setStatusCode(201)
|
|
866
|
+
|
|
867
|
+
return { ...res, IsActiveEntity: false } // REVISIT: Flatten?
|
|
616
868
|
}
|
|
617
|
-
exports.onDraftActivate = onDraftActivate
|
|
618
869
|
|
|
619
|
-
async function
|
|
620
|
-
LOG.debug('
|
|
621
|
-
const
|
|
622
|
-
const
|
|
623
|
-
const res = await SELECT.one.from(targetDraft).columns('DraftAdministrativeData_DraftUUID').where(targetWhere)
|
|
624
|
-
if (!res) req.reject(404)
|
|
625
|
-
await UPDATE('DRAFT.DraftAdministrativeData')
|
|
626
|
-
.data({
|
|
627
|
-
InProcessByUser: req.user.id,
|
|
628
|
-
LastChangedByUser: req.user.id,
|
|
629
|
-
LastChangeDateTime: new Date()
|
|
630
|
-
})
|
|
631
|
-
.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
|
|
632
874
|
|
|
633
|
-
const
|
|
634
|
-
|
|
635
|
-
|
|
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)
|
|
636
895
|
return req.data
|
|
637
896
|
}
|
|
638
|
-
exports.onPatch = onPatch
|
|
639
897
|
|
|
640
|
-
async function
|
|
641
|
-
LOG.debug('
|
|
642
|
-
const targetDraft = req.target.drafts
|
|
898
|
+
async function onPrepare(req) {
|
|
899
|
+
LOG.debug('prepare draft')
|
|
643
900
|
const draftParams = req.query._draftParams
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
const
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
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 }
|
|
656
919
|
}
|
|
657
|
-
|
|
920
|
+
|
|
921
|
+
module.exports = { onNew, onEdit, onCancel, onPrepare }
|