@sap/cds 6.3.1 → 6.4.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.
Files changed (114) hide show
  1. package/CHANGELOG.md +87 -0
  2. package/apis/cds.d.ts +1 -1
  3. package/apis/core.d.ts +118 -90
  4. package/apis/cqn.d.ts +11 -2
  5. package/apis/internal/inference.d.ts +7 -2
  6. package/apis/ql.d.ts +45 -11
  7. package/apis/serve.d.ts +8 -1
  8. package/apis/services.d.ts +303 -305
  9. package/bin/build/buildTaskEngine.js +28 -36
  10. package/bin/build/buildTaskFactory.js +32 -81
  11. package/bin/build/buildTaskHandler.js +3 -2
  12. package/bin/build/buildTaskProvider.js +2 -2
  13. package/bin/build/buildTaskProviderFactory.js +5 -14
  14. package/bin/build/constants.js +0 -1
  15. package/bin/build/provider/buildTaskHandlerEdmx.js +7 -6
  16. package/bin/build/provider/buildTaskHandlerFeatureToggles.js +6 -5
  17. package/bin/build/provider/buildTaskHandlerInternal.js +9 -30
  18. package/bin/build/provider/buildTaskProviderInternal.js +70 -58
  19. package/bin/build/provider/fiori/index.js +6 -5
  20. package/bin/build/provider/hana/2migration.js +20 -3
  21. package/bin/build/provider/hana/2tabledata.js +1 -0
  22. package/bin/build/provider/hana/index.js +40 -17
  23. package/bin/build/provider/java/index.js +10 -10
  24. package/bin/build/provider/mtx/index.js +25 -16
  25. package/bin/build/provider/mtx/resourcesTarBuilder.js +22 -27
  26. package/bin/build/provider/mtx-extension/index.js +3 -2
  27. package/bin/build/provider/mtx-sidecar/index.js +16 -15
  28. package/bin/build/provider/nodejs/index.js +14 -56
  29. package/bin/build/util.js +56 -16
  30. package/bin/deploy/to-hana/cfUtil.js +4 -1
  31. package/bin/deploy/to-hana/gitUtil.js +1 -1
  32. package/bin/deploy/to-hana/hana.js +45 -38
  33. package/bin/deploy/to-hana/hdiDeployUtil.js +8 -9
  34. package/bin/deploy/to-hana/mtaUtil.js +13 -14
  35. package/bin/mtx/in-cds.js +3 -1
  36. package/bin/serve.js +1 -1
  37. package/bin/version.js +2 -1
  38. package/lib/compile/cds-compile.js +1 -0
  39. package/lib/compile/cdsc.js +1 -0
  40. package/lib/compile/etc/_localized.js +2 -2
  41. package/lib/compile/for/lean_drafts.js +83 -0
  42. package/lib/compile/for/nodejs.js +1 -0
  43. package/lib/compile/minify.js +2 -1
  44. package/lib/compile/parse.js +2 -1
  45. package/lib/compile/to/gql.js +1 -1
  46. package/lib/compile/to/sql.js +11 -1
  47. package/lib/core/entities.js +1 -1
  48. package/lib/core/index.js +8 -9
  49. package/lib/core/infer.js +1 -0
  50. package/lib/dbs/cds-deploy.js +97 -41
  51. package/lib/env/cds-env.js +9 -10
  52. package/lib/env/cds-requires.js +8 -2
  53. package/lib/env/defaults.js +0 -4
  54. package/lib/env/schemas/cds-rc.json +38 -0
  55. package/lib/ql/SELECT.js +10 -4
  56. package/lib/srv/bindings.js +1 -1
  57. package/lib/srv/factory.js +1 -1
  58. package/lib/srv/protocols/index.js +3 -1
  59. package/lib/srv/srv-methods.js +1 -1
  60. package/lib/utils/cds-utils.js +11 -0
  61. package/lib/utils/inflect.js +13 -12
  62. package/lib/utils/tar.js +53 -10
  63. package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +2 -2
  64. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +1 -1
  65. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/create.js +1 -1
  66. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/delete.js +1 -1
  67. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +1 -15
  68. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +1 -1
  69. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +1 -1
  70. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/errors/UriSyntaxError.js +1 -1
  71. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/UriParser.js +6 -1
  72. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/utils/BufferedWriter.js +1 -1
  73. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/validator/ConditionalRequestValidator.js +0 -12
  74. package/libx/_runtime/cds-services/adapter/odata-v4/utils/oDataConfiguration.js +1 -7
  75. package/libx/_runtime/cds-services/adapter/odata-v4/utils/result.js +4 -0
  76. package/libx/_runtime/cds-services/services/Service.js +23 -1
  77. package/libx/_runtime/cds-services/util/assert.js +0 -41
  78. package/libx/_runtime/common/composition/data.js +5 -1
  79. package/libx/_runtime/common/generic/auth/utils.js +3 -3
  80. package/libx/_runtime/common/generic/input.js +4 -24
  81. package/libx/_runtime/common/generic/paging.js +3 -3
  82. package/libx/_runtime/common/utils/csn.js +21 -15
  83. package/libx/_runtime/common/utils/draft.js +2 -1
  84. package/libx/_runtime/common/utils/resolveView.js +25 -4
  85. package/libx/_runtime/common/utils/rewriteAsterisks.js +3 -1
  86. package/libx/_runtime/common/utils/rowUUIDGenerator.js +21 -0
  87. package/libx/_runtime/common/utils/templateProcessor.js +12 -15
  88. package/libx/_runtime/common/utils/templateProcessorPathSerializer.js +23 -0
  89. package/libx/_runtime/db/expand/expandCQNToJoin.js +29 -12
  90. package/libx/_runtime/db/generic/input.js +7 -13
  91. package/libx/_runtime/db/sql-builder/UpsertBuilder.js +47 -0
  92. package/libx/_runtime/db/sql-builder/index.js +2 -0
  93. package/libx/_runtime/db/sql-builder/sqlFactory.js +9 -0
  94. package/libx/_runtime/db/utils/columns.js +4 -2
  95. package/libx/_runtime/fiori/generic/read.js +1 -12
  96. package/libx/_runtime/fiori/lean-draft.js +657 -0
  97. package/libx/_runtime/fiori/utils/handler.js +1 -1
  98. package/libx/_runtime/hana/pool.js +16 -1
  99. package/libx/_runtime/messaging/enterprise-messaging-utils/getTenantInfo.js +2 -1
  100. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +1 -1
  101. package/libx/_runtime/messaging/enterprise-messaging.js +2 -3
  102. package/libx/_runtime/messaging/outbox/utils.js +109 -70
  103. package/libx/_runtime/messaging/service.js +16 -7
  104. package/libx/_runtime/remote/Service.js +15 -2
  105. package/libx/_runtime/remote/utils/client.js +41 -11
  106. package/libx/_runtime/sqlite/Service.js +3 -0
  107. package/libx/_runtime/sqlite/convertDraftAdminPathExpression.js +56 -0
  108. package/libx/_runtime/sqlite/customBuilder/CustomUpsertBuilder.js +59 -0
  109. package/libx/_runtime/sqlite/customBuilder/index.js +5 -0
  110. package/libx/_runtime/sqlite/execute.js +1 -1
  111. package/libx/_runtime/types/api.js +2 -2
  112. package/libx/rest/RestAdapter.js +15 -13
  113. package/package.json +1 -1
  114. package/server.js +1 -0
@@ -0,0 +1,657 @@
1
+ const cds = require('../cds'),
2
+ { Object_keys, results } = cds.utils
3
+ const LOG = cds.log('fiori|drafts')
4
+
5
+ const DRAFT_ELEMENTS = new Set([
6
+ 'IsActiveEntity',
7
+ 'HasDraftEntity',
8
+ 'HasActiveEntity',
9
+ 'DraftAdministrativeData',
10
+ 'DraftAdministrativeData_DraftUUID',
11
+ 'SiblingEntity'
12
+ ])
13
+
14
+ const REDUCED_DRAFT_ELEMENTS = new Set(['IsActiveEntity', 'HasDraftEntity', 'SiblingEntity'])
15
+
16
+ const DRAFT_ADMIN_ELEMENTS = [
17
+ 'DraftUUID',
18
+ 'LastChangedByUser',
19
+ 'CreatedByUser',
20
+ 'InProcessByUser',
21
+ 'DraftIsCreatedByMe',
22
+ 'DraftIsProcessedByMe'
23
+ ]
24
+
25
+ const DRAFT_CANCEL_TIMEOUT_IN_MIN = () =>
26
+ (cds.env.drafts?.cancellationTimeout && Number(cds.env.drafts?.cancellationTimeout)) || 15
27
+
28
+ const h = cds.ApplicationService.prototype.handle
29
+ cds.ApplicationService.prototype.handle = function (req) {
30
+ const handle = h.bind(this)
31
+
32
+ const _newReq = (req, query) => {
33
+ // REVISIT: This is a bit hacky -> better way?
34
+ query._target = undefined
35
+ cds.infer(query, this.model.definitions)
36
+ const _req = cds.Request.for(req._)
37
+ _req.query = query
38
+ _req.event = req.event
39
+ _req.target = query._target
40
+ _req._.params = req.params
41
+ _req._.query = query
42
+ _req._ = req._
43
+ return _req
44
+ }
45
+
46
+ if (!req.query || req.query._draftParams) return handle(req)
47
+ const query = _cleansed(req.query, this.model)
48
+ const draftParams = query._draftParams
49
+ if (req.event !== 'READ') {
50
+ const _req = _newReq(req, query)
51
+ return handle(_req)
52
+ }
53
+
54
+ const read =
55
+ draftParams.IsActiveEntity === false && draftParams.SiblingEntity_IsActiveEntity === null
56
+ ? Read.all
57
+ : draftParams.IsActiveEntity === true &&
58
+ draftParams.SiblingEntity_IsActiveEntity === null &&
59
+ draftParams.DraftAdministrativeData_InProcessByUser === 'not null'
60
+ ? Read.lockedByAnotherUser
61
+ : draftParams.IsActiveEntity === true &&
62
+ draftParams.SiblingEntity_IsActiveEntity === null &&
63
+ draftParams.DraftAdministrativeData_InProcessByUser === ''
64
+ ? Read.unsavedChangesByAnotherUser
65
+ : draftParams.IsActiveEntity === true && draftParams.HasDraftEntity === false
66
+ ? Read.unchanged
67
+ : draftParams.IsActiveEntity === true
68
+ ? Read.onlyActives
69
+ : draftParams.IsActiveEntity === false
70
+ ? Read.ownDrafts
71
+ : Read.onlyActives
72
+ const run = async query => {
73
+ const _req = _newReq(req, query)
74
+ return handle(_req)
75
+ }
76
+ return read(run, query)
77
+ }
78
+
79
+ const Read = {
80
+ onlyActives: async function (run, query, { ignoreDrafts } = {}) {
81
+ // DraftAdministrativeData is only accessible via drafts
82
+ if (query._target.name.endsWith('.DraftAdministrativeData')) return run(query._drafts)
83
+ const actives = await run(query)
84
+ if (!actives || (Array.isArray(actives) && !actives.length) || !query._target.drafts) return actives
85
+ let drafts
86
+ if (ignoreDrafts) drafts = []
87
+ else {
88
+ try {
89
+ drafts = await Read.complementaryDrafts(run, query, actives)
90
+ } catch (e) {
91
+ drafts = []
92
+ }
93
+ }
94
+ Read.merge(query._target, actives, drafts, (row, other) =>
95
+ other
96
+ ? Object.assign(row, other, { IsActiveEntity: true, HasActiveEntity: false, HasDraftEntity: true })
97
+ : Object.assign(row, {
98
+ IsActiveEntity: true,
99
+ HasActiveEntity: false,
100
+ HasDraftEntity: false,
101
+ DraftAdministrativeData: null,
102
+ DraftAdministrativeData_DraftUUID: null
103
+ })
104
+ )
105
+ return actives
106
+ },
107
+ unchanged: async function (run, query) {
108
+ const draftsQuery = query._drafts
109
+ const keys = Object_keys(query._target.keys).filter(k => k !== 'IsActiveEntity')
110
+ draftsQuery.SELECT.count = undefined
111
+ draftsQuery.SELECT.limit = undefined
112
+ draftsQuery.SELECT.orderBy = undefined
113
+ draftsQuery.SELECT.columns = keys.map(k => ({ ref: [k] }))
114
+
115
+ const drafts = await run(draftsQuery)
116
+ const res = Read.onlyActives(run, query.where(Read.whereNotIn(query._target, drafts)), {
117
+ ignoreDrafts: true
118
+ })
119
+ return res
120
+ },
121
+ ownDrafts: async function (run, query) {
122
+ if (!query._target.drafts) return run(query._drafts)
123
+ const drafts = await run(
124
+ query._drafts.where({ ref: ['DraftAdministrativeData', 'InProcessByUser'] }, '=', cds.context.user.id)
125
+ )
126
+ Read.merge(query._target, drafts, [], row =>
127
+ Object.assign(row, {
128
+ IsActiveEntity: false,
129
+ HasDraftEntity: false
130
+ })
131
+ )
132
+ return drafts
133
+ },
134
+ all: async function (run, query) {
135
+ query._drafts.SELECT.count = true
136
+ const drafts = await run(
137
+ query._drafts.where({ ref: ['DraftAdministrativeData', 'InProcessByUser'] }, '=', cds.context.user.id)
138
+ )
139
+
140
+ const skip = query.SELECT.limit?.offset?.val
141
+ const top = query.SELECT.limit?.rows?.val
142
+
143
+ const appliedSkip = drafts.length ? skip : drafts.$count
144
+
145
+ const topActives = top && Math.max(0, top - drafts.length)
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 =>
173
+ Object.assign(row, {
174
+ IsActiveEntity: false,
175
+ HasDraftEntity: false
176
+ })
177
+ )
178
+ const totalCount = actives.$count + drafts.$count
179
+ const result = Object.assign([...drafts, ...actives], { $count: totalCount })
180
+ return result
181
+ },
182
+ activesFromDrafts: async function (run, query, { isLocked = true }) {
183
+ const draftsQuery = query._drafts
184
+ const keys = Object_keys(query._target.keys).filter(k => k !== 'IsActiveEntity')
185
+ const additionalCols = draftsQuery.SELECT.columns
186
+ ? draftsQuery.SELECT.columns.filter(
187
+ c => c.ref && ['DraftAdministrativeData', 'DraftAdministrativeData_DraftUUID'].includes(c.ref[0])
188
+ )
189
+ : [{ ref: ['DraftAdministrativeData_DraftUUID'] }]
190
+ draftsQuery.SELECT.columns = keys.map(k => ({ ref: [k] })).concat(additionalCols)
191
+ draftsQuery.where({
192
+ HasActiveEntity: true,
193
+ 'DraftAdministrativeData.InProcessByUser': { '!=': cds.context.user.id },
194
+ 'DraftAdministrativeData.LastChangeDateTime': {
195
+ [isLocked ? '>' : '<']: Read.lockshiftedNow
196
+ }
197
+ })
198
+ const drafts = await run(draftsQuery)
199
+ const actives = drafts.length
200
+ ? await run(query.where(Read.whereIn(query._target, drafts)))
201
+ : Object.assign([], { $count: 0 })
202
+ Read.merge(query._target, actives, drafts, (row, other) =>
203
+ other
204
+ ? Object.assign(row, other, { IsActiveEntity: true, HasDraftEntity: true, HasActiveEntity: false })
205
+ : Object.assign({ IsActiveEntity: true, HasDraftEntity: false, HasActiveEntity: false })
206
+ )
207
+ return actives
208
+ },
209
+ unsavedChangesByAnotherUser: async function (run, query) {
210
+ return Read.activesFromDrafts(run, query, { isLocked: false })
211
+ },
212
+ lockedByAnotherUser: async function (run, query) {
213
+ return Read.activesFromDrafts(run, query, { isLocked: true })
214
+ },
215
+ get lockshiftedNow() {
216
+ return new Date(Date.now() - DRAFT_CANCEL_TIMEOUT_IN_MIN() * 60 * 1000).toISOString()
217
+ },
218
+ whereNotIn: (target, data) => Read.whereIn(target, data, true),
219
+ whereIn: (target, data, not = false) => {
220
+ const keys = Object_keys(target.keys).filter(k => k !== 'IsActiveEntity')
221
+ const dataArray = data ? (Array.isArray(data) ? data : [data]) : []
222
+ return [
223
+ { list: keys.map(k => ({ ref: [k] })) },
224
+ not ? 'not in' : 'in',
225
+ { list: dataArray.map(r => ({ list: keys.map(k => ({ val: r[k] })) })) }
226
+ ]
227
+ },
228
+ complementaryDrafts: (run, query, _actives) => {
229
+ const actives = Array.isArray(_actives) ? _actives : [_actives]
230
+ if (!actives.length) return []
231
+ const drafts = cds.ql.clone(query._drafts)
232
+ drafts.SELECT.where = Read.whereIn(query._target, actives)
233
+ const relevantColumns = ['DraftAdministrativeData', 'DraftAdministrativeData_DraftUUID']
234
+ drafts.SELECT.columns = (
235
+ drafts.SELECT.columns?.filter(c => c.ref && relevantColumns.includes(c.ref[0])) ||
236
+ relevantColumns.map(k => ({ ref: [k] }))
237
+ ).concat(
238
+ Object_keys(query._target.keys)
239
+ .filter(k => k !== 'IsActiveEntity')
240
+ .map(k => ({ ref: [k] }))
241
+ )
242
+ drafts.SELECT.count = undefined
243
+ drafts.SELECT.one = undefined
244
+ return run(drafts)
245
+ },
246
+ merge: (target, data, otherData, cb) => {
247
+ if (!data) return
248
+ const dataArray = Array.isArray(data) ? data : [data]
249
+ if (!dataArray.length) return
250
+ const otherDataArray = Array.isArray(otherData) ? otherData : otherData ? [otherData] : []
251
+ if (otherDataArray.length) {
252
+ const _keys = Object_keys(target.keys).filter(k => k !== 'IsActiveEntity')
253
+ const _hash = row => _keys.map(k => row[k]).reduce((res, curr) => res + '|$|' + curr, '')
254
+ const d2hash = new Map()
255
+ for (const row of otherDataArray) d2hash.set(_hash(row), row)
256
+ for (const row of dataArray) {
257
+ const other = d2hash.get(_hash(row))
258
+ cb(row, other)
259
+ }
260
+ } else {
261
+ for (const row of dataArray) {
262
+ cb(row, undefined)
263
+ }
264
+ }
265
+ }
266
+ }
267
+
268
+ /**
269
+ * Creates a clone of the query, cleanses and collects all draft parameters into ._draftParams.
270
+ */
271
+ function _cleansed(query, model, target) {
272
+ const draftParams = {} //> used to collect draft filter criteria
273
+ const q = _cleanseQuery(query, draftParams)
274
+ if (query.SELECT) {
275
+ let cache
276
+ const getDrafts = () => {
277
+ if (cache) return cache
278
+ const draftsQuery = _cleanseQuery(query, {}) // could just clone `q` but the latter is ruined by database layer
279
+ const [root, ...tail] = draftsQuery.SELECT.from.ref
280
+ const draft = model.definitions[root.id || root].drafts
281
+ draftsQuery.SELECT.from = {
282
+ ref: [root.id ? { ...root, id: draft.name } : draft.name, ...tail]
283
+ }
284
+ if (query.SELECT.columns && query._target.drafts)
285
+ draftsQuery.SELECT.columns = query.SELECT.columns.filter(c => !REDUCED_DRAFT_ELEMENTS.has(c.ref?.[0]))
286
+
287
+ if (draftsQuery._target.name.endsWith('.DraftAdministrativeData')) {
288
+ draftsQuery.SELECT.columns = _tweakAdminCols(draftsQuery.SELECT.columns)
289
+ } else if (q._target.drafts) {
290
+ draftsQuery.SELECT.columns = _tweakAdminExpand(draftsQuery.SELECT.columns)
291
+ }
292
+ Object.defineProperty(draftsQuery, '_draftParams', { value: draftParams, enumerable: false })
293
+ cache = draftsQuery
294
+ return draftsQuery
295
+ }
296
+ Object.defineProperty(q, '_drafts', {
297
+ get() {
298
+ return getDrafts()
299
+ }
300
+ })
301
+ }
302
+
303
+ Object.defineProperty(q, '_draftParams', { value: draftParams, enumerable: false })
304
+ return q
305
+
306
+ function _cleanseQuery(query, draftParams) {
307
+ const q = cds.ql.clone(query)
308
+
309
+ const ref = q.SELECT?.from.ref || q.UPDATE?.entity.ref || q.INSERT?.into.ref || q.DELETE?.from.ref
310
+ const cqn = q.SELECT || q.UPDATE || q.INSERT || q.DELETE
311
+
312
+ if (ref) {
313
+ const cleansedRef = ref.map(r => (r.where ? { ...r, where: _cleanseWhere(r.where, draftParams) } : r))
314
+ if (q.SELECT) q.SELECT.from = { ...q.SELECT.from, ref: cleansedRef }
315
+ else if (q.DELETE) q.DELETE.from = { ...q.DELETE.from, ref: cleansedRef }
316
+ else if (q.UPDATE) q.UPDATE.entity = { ...q.UPDATE.entity, ref: cleansedRef }
317
+ else if (q.INSERT) q.INSERT.into = { ...q.INSERT.into, ref: cleansedRef }
318
+
319
+ const siblingIdx = cleansedRef.findIndex(r => r === 'SiblingEntity')
320
+ if (siblingIdx !== -1) {
321
+ cleansedRef.splice(siblingIdx, 1)
322
+ draftParams.IsActiveEntity = !draftParams.IsActiveEntity
323
+ }
324
+ }
325
+
326
+ if (cqn.where) cqn.where = _cleanseWhere(cqn.where, draftParams)
327
+ if (cqn.columns) cqn.columns = q.SELECT.columns.filter(c => !DRAFT_ELEMENTS.has(c.ref?.[0]))
328
+ if (cqn.orderBy) cqn.orderBy = _cleanseWhere(cqn.orderBy, {})
329
+
330
+ return q
331
+ }
332
+
333
+ function _tweakAdminExpand(columns) {
334
+ if (!columns) return columns
335
+ return columns.map(col => {
336
+ if (col.ref?.[0] === 'DraftAdministrativeData') {
337
+ return { ...col, expand: _tweakAdminCols(col.expand) }
338
+ }
339
+ return col
340
+ })
341
+ }
342
+
343
+ function _tweakAdminCols(columns) {
344
+ if (!columns) columns = DRAFT_ADMIN_ELEMENTS.map(k => ({ ref: [k] }))
345
+ return columns.map(col => {
346
+ const name = col.ref?.[0]
347
+ if (!name) return col
348
+ switch (name) {
349
+ case 'DraftAdministrativeData':
350
+ return { ...col, expand: _tweakAdminCols(col.expand) }
351
+ case 'DraftIsCreatedByMe':
352
+ return {
353
+ xpr: [
354
+ 'case',
355
+ 'when',
356
+ { ref: ['CreatedByUser'] },
357
+ '=',
358
+ { val: cds.context.user.id },
359
+ 'then',
360
+ { val: true },
361
+ 'else',
362
+ { val: false },
363
+ 'end'
364
+ ],
365
+ as: 'DraftIsCreatedByMe',
366
+ cast: { type: 'cds.Boolean' }
367
+ }
368
+ 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
+ }
385
+ case 'DraftIsProcessedByMe':
386
+ return {
387
+ xpr: [
388
+ 'case',
389
+ 'when',
390
+ { ref: ['InProcessByUser'] },
391
+ '=',
392
+ { val: cds.context.user.id },
393
+ 'then',
394
+ { val: true },
395
+ 'else',
396
+ { val: false },
397
+ 'end'
398
+ ],
399
+ as: 'DraftIsProcessedByMe',
400
+ cast: { type: 'cds.Boolean' }
401
+ }
402
+ default:
403
+ return col
404
+ }
405
+ })
406
+ }
407
+
408
+ function _cleanseWhere(xpr, draftParams) {
409
+ const cleansed = []
410
+ for (let i = 0; i < xpr.length; ++i) {
411
+ let x = xpr[i],
412
+ e = x.ref?.[0]
413
+ if (DRAFT_ELEMENTS.has(e)) {
414
+ let { val } = xpr[i + 2]
415
+ draftParams[x.ref.join('_')] = xpr[i + 1] === '!=' ? 'not ' + val : val
416
+ i += 3
417
+ continue
418
+ }
419
+ if (x.xpr) {
420
+ x = { xpr: _cleanseWhere(x.xpr, draftParams) }
421
+ if (!x.xpr) {
422
+ i += 1
423
+ continue
424
+ }
425
+ }
426
+ cleansed.push(x)
427
+ }
428
+ const last = cleansed[cleansed.length - 1]
429
+ if (last === 'and' || last === 'or') cleansed.pop()
430
+ if (cleansed.length) return cleansed
431
+ }
432
+ }
433
+
434
+ function _draftIsLocked(LastChangeDateTime) {
435
+ return DRAFT_CANCEL_TIMEOUT_IN_MIN() * 60 * 1000 > Date.now() - Date.parse(LastChangeDateTime)
436
+ }
437
+
438
+ async function onNewDraft(req) {
439
+ LOG.debug('new draft')
440
+ const isRoot = typeof req.query.INSERT.into === 'string'
441
+ let DraftUUID
442
+ if (isRoot) DraftUUID = cds.utils.uuid()
443
+ else {
444
+ const rootData = await SELECT.one(req.query.INSERT.into.ref[0].id + '.drafts', d => {
445
+ d.DraftAdministrativeData_DraftUUID,
446
+ d.DraftAdministrativeData(a => {
447
+ a.LastChangeDateTime, a.InProcessByUser
448
+ })
449
+ }).where(req.query.INSERT.into.ref[0].where)
450
+ if (!rootData) req.reject(404)
451
+ if (
452
+ !rootData.DraftAdministrativeData.InProcessByUser === req.user.id &&
453
+ _draftIsLocked(rootData.DraftAdministrativeData.LastChangeDateTime)
454
+ )
455
+ req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER')
456
+ DraftUUID = rootData.DraftAdministrativeData_DraftUUID
457
+ }
458
+ const timestamp = cds.context.timestamp.toISOString() // REVISIT: toISOString should be done on db layer
459
+ const adminDataCQN = isRoot
460
+ ? INSERT.into('DRAFT.DraftAdministrativeData').entries({
461
+ DraftUUID,
462
+ CreationDateTime: timestamp,
463
+ CreatedByUser: req.user.id,
464
+ LastChangeDateTime: timestamp,
465
+ LastChangedByUser: req.user.id,
466
+ DraftIsCreatedByMe: true, // Dummy values
467
+ DraftIsProcessedByMe: true, // Dummy values
468
+ InProcessByUser: req.user.id
469
+ })
470
+ : UPDATE('DRAFT.DraftAdministrativeData')
471
+ .data({
472
+ InProcessByUser: req.user.id,
473
+ LastChangedByUser: req.user.id,
474
+ LastChangeDateTime: timestamp
475
+ })
476
+ .where({ DraftUUID })
477
+
478
+ const draftData = Object.assign(
479
+ { DraftAdministrativeData_DraftUUID: DraftUUID, HasActiveEntity: false },
480
+ req.query.INSERT.entries[0]
481
+ )
482
+ delete draftData.IsActiveEntity
483
+ const draftCQN = INSERT.into(req.target.drafts).entries(draftData)
484
+
485
+ // TODO: do this? req.query._draftParams.IsActiveEntity = false
486
+ await Promise.all([adminDataCQN, draftCQN].map(cqn => cds.run(cqn)))
487
+ req._.readAfterWrite = true
488
+ return { ...draftData, IsActiveEntity: false }
489
+ }
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
+
533
+ async function onDraftEdit(req) {
534
+ LOG.debug('edit active')
535
+ const draftParams = req.query._draftParams
536
+ const targetWhere = req.query.SELECT.from.ref[0].where
537
+
538
+ if (draftParams.IsActiveEntity !== true) req.reject(400)
539
+
540
+ const DraftUUID = cds.utils.uuid()
541
+
542
+ const cols = expandStarStar(req.target)
543
+ const _addDraftColumns = (target, columns) => {
544
+ if (target.drafts) {
545
+ columns.push({ val: true, as: 'HasActiveEntity' })
546
+ columns.push({ val: DraftUUID, as: 'DraftAdministrativeData_DraftUUID' })
547
+ }
548
+ for (const col of columns) {
549
+ if (col.expand) {
550
+ const el = target.elements[col.ref[0]]
551
+ _addDraftColumns(el._target, col.expand)
552
+ }
553
+ }
554
+ }
555
+ _addDraftColumns(req.target, cols)
556
+
557
+ const [res, draftExists] = await Promise.all([
558
+ SELECT.one.from(req.target).columns(cols).where(targetWhere),
559
+ SELECT.one(req.target.drafts).columns('1 as _exists').where(targetWhere)
560
+ ])
561
+ if (!res) req.reject(404)
562
+ if (draftExists) req.reject(409, 'DRAFT_ALREADY_EXISTS')
563
+
564
+ const timestamp = cds.context.timestamp.toISOString() // REVISIT: toISOString should be done on db layer
565
+ await INSERT.into('DRAFT.DraftAdministrativeData').entries({
566
+ DraftUUID,
567
+ CreationDateTime: timestamp,
568
+ CreatedByUser: req.user.id,
569
+ LastChangeDateTime: timestamp,
570
+ LastChangedByUser: req.user.id,
571
+ DraftIsCreatedByMe: true, // Dummy values
572
+ DraftIsProcessedByMe: true, // Dummy values
573
+ InProcessByUser: req.user.id
574
+ })
575
+
576
+ const targetDraft = req.target.drafts
577
+ await INSERT.into(targetDraft).entries(res)
578
+ return { ...res, IsActiveEntity: false } // REVISIT: Flatten?
579
+ }
580
+ exports.onDraftEdit = onDraftEdit
581
+
582
+ async function onDraftActivate(req) {
583
+ LOG.debug('activate draft')
584
+ // TODO: Read all drafts, send deep update/insert (based on HasActiveEntity, which should be fille with new!)
585
+ // It would be great if we'd have a SELECT ** to deeply expand the entity (along compositions), that should
586
+ // be implemented in expand implementation.
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 })
616
+ }
617
+ exports.onDraftActivate = onDraftActivate
618
+
619
+ async function onPatch(req) {
620
+ LOG.debug('patch draft')
621
+ const targetDraft = req.target.drafts
622
+ const targetWhere = req.query.UPDATE.entity.ref[0].where
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 })
632
+
633
+ const updateData = { ...req.data }
634
+ delete updateData.IsActiveEntity
635
+ await UPDATE(targetDraft).data(updateData).where(targetWhere)
636
+ return req.data
637
+ }
638
+ exports.onPatch = onPatch
639
+
640
+ async function onDelete(req) {
641
+ LOG.debug('delete')
642
+ const targetDraft = req.target.drafts
643
+ const draftParams = req.query._draftParams
644
+ const targetWhere = req.query.DELETE.from.ref[0].where
645
+ const res = await SELECT.one.from(targetDraft).columns('DraftAdministrativeData_DraftUUID').where(targetWhere)
646
+ if (!res && draftParams.IsActiveEntity === false) req.reject(404)
647
+ const deletes = !res
648
+ ? []
649
+ : [
650
+ DELETE.from('DRAFT.DraftAdministrativeData').where({ DraftUUID: res.DraftAdministrativeData_DraftUUID }),
651
+ DELETE.from(targetDraft).where(targetWhere)
652
+ ]
653
+ if (draftParams.IsActiveEntity) deletes.push(DELETE.from(req.target).where(targetWhere))
654
+ await Promise.all(deletes)
655
+ return req.data
656
+ }
657
+ exports.onDelete = onDelete
@@ -184,7 +184,7 @@ const _aliased = (arr, columns, alias) =>
184
184
 
185
185
  // Only works for root entity, otherwise the relative position needs to be adapted
186
186
  const removeDraftUUIDIfNecessary = req =>
187
- req._.req && req._.req.headers && req._.req.headers['x-cds-odata-version'] === 'v2'
187
+ req.http?.req?.headers?.['x-cds-odata-version'] === 'v2'
188
188
  ? () => {}
189
189
  : result => delete result.DraftAdministrativeData_DraftUUID
190
190
 
@@ -7,6 +7,17 @@ const hana = require('./driver')
7
7
  const _require = require('../common/utils/require')
8
8
  const getError = require('../common/error')
9
9
 
10
+ function multiTenantServiceManager() {
11
+ try {
12
+ // Make sure cds-mtxs APIs are loaded
13
+ require('@sap/cds-mtxs/lib') // eslint-disable-line cds/no-missing-dependencies
14
+ } catch (e) {
15
+ if (e.code === 'MODULE_NOT_FOUND') return null
16
+ else throw e
17
+ }
18
+ return cds.env.requires['cds.xt.DeploymentService']?.['old-instance-manager'] ? null : cds.xt?.serviceManager
19
+ }
20
+
10
21
  function multiTenantInstanceManager(config = cds.env.requires.db) {
11
22
  const { credentials } = config
12
23
 
@@ -63,10 +74,14 @@ async function credentials4(tenant, db) {
63
74
  if (!db._instance_manager) {
64
75
  const opts = db.options && db.options.credentials ? db.options : undefined
65
76
  db._instance_manager = cds.requires.multitenancy
66
- ? await multiTenantInstanceManager(opts)
77
+ ? multiTenantServiceManager() ?? (await multiTenantInstanceManager(opts))
67
78
  : singleTenantInstanceManager(opts)
68
79
  }
69
80
 
81
+ if (cds.xt?.serviceManager && !cds.env.requires['cds.xt.DeploymentService']?.['old-instance-manager']) {
82
+ return (await db._instance_manager.get(tenant)).credentials
83
+ }
84
+
70
85
  return new Promise((resolve, reject) => {
71
86
  db._instance_manager.get(tenant, (err, res) => {
72
87
  if (err) return reject(err)