@sap/cds 6.6.2 → 6.7.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 (130) hide show
  1. package/CHANGELOG.md +59 -2
  2. package/README.md +1 -1
  3. package/apis/connect.d.ts +11 -4
  4. package/apis/core.d.ts +1 -1
  5. package/apis/csn.d.ts +1 -0
  6. package/apis/internal/inference.d.ts +15 -2
  7. package/apis/log.d.ts +10 -0
  8. package/apis/serve.d.ts +4 -9
  9. package/apis/services.d.ts +86 -19
  10. package/bin/build/buildTaskEngine.js +16 -42
  11. package/bin/build/constants.js +4 -2
  12. package/bin/build/provider/buildTaskProviderInternal.js +117 -85
  13. package/bin/build/provider/hana/index.js +6 -1
  14. package/bin/build/provider/mtx-extension/index.js +74 -34
  15. package/bin/build/provider/mtx-sidecar/index.js +3 -3
  16. package/bin/build/provider/nodejs/index.js +2 -2
  17. package/bin/build/util.js +63 -14
  18. package/bin/cds-serve.js +6 -0
  19. package/bin/cds.js +20 -4
  20. package/bin/deploy/to-hana/cfUtil.js +15 -1
  21. package/bin/mtx/in-cds.js +2 -9
  22. package/bin/plugins.js +31 -0
  23. package/bin/serve.js +12 -12
  24. package/lib/compile/etc/_localized.js +1 -1
  25. package/lib/compile/for/lean_drafts.js +22 -6
  26. package/lib/compile/for/nodejs.js +4 -1
  27. package/lib/compile/load.js +4 -2
  28. package/lib/core/index.js +35 -15
  29. package/lib/dbs/cds-deploy.js +129 -133
  30. package/lib/env/cds-env.js +25 -17
  31. package/lib/env/cds-requires.js +10 -40
  32. package/lib/env/compat.js +12 -0
  33. package/lib/env/defaults.js +17 -9
  34. package/lib/env/plugins.js +29 -0
  35. package/lib/env/schemas/cds-rc.json +14 -0
  36. package/lib/index.js +3 -0
  37. package/lib/log/cds-log.js +7 -4
  38. package/lib/ql/CREATE.js +1 -1
  39. package/lib/ql/DELETE.js +1 -1
  40. package/lib/ql/DROP.js +3 -3
  41. package/lib/ql/INSERT.js +1 -1
  42. package/lib/ql/Query.js +14 -6
  43. package/lib/ql/SELECT.js +8 -2
  44. package/lib/ql/UPDATE.js +1 -1
  45. package/lib/ql/Whereable.js +1 -1
  46. package/lib/ql/cds-ql.js +1 -9
  47. package/lib/req/cds-context.js +1 -4
  48. package/lib/req/request.js +63 -2
  49. package/lib/req/response.js +3 -2
  50. package/lib/srv/bindings.js +69 -71
  51. package/lib/srv/cds-connect.js +4 -1
  52. package/lib/srv/cds-serve.js +4 -0
  53. package/lib/srv/middlewares/index.js +37 -6
  54. package/lib/srv/protocols/_legacy.js +1 -1
  55. package/lib/srv/protocols/index.js +1 -1
  56. package/lib/srv/srv-api.js +4 -6
  57. package/lib/srv/srv-dispatch.js +4 -3
  58. package/lib/srv/srv-handlers.js +1 -1
  59. package/lib/srv/srv-methods.js +8 -2
  60. package/lib/utils/cds-test.js +4 -1
  61. package/libx/_runtime/audit/Service.js +8 -9
  62. package/libx/_runtime/audit/generic/personal/index.js +1 -1
  63. package/libx/_runtime/audit/generic/personal/utils.js +1 -1
  64. package/libx/_runtime/audit/utils/v2.js +17 -20
  65. package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +2 -0
  66. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +11 -4
  67. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +0 -1
  68. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/readToCQN.js +3 -3
  69. package/libx/_runtime/cds-services/adapter/odata-v4/utils/data.js +1 -1
  70. package/libx/_runtime/cds-services/adapter/odata-v4/utils/metaInfo.js +4 -4
  71. package/libx/_runtime/cds-services/adapter/odata-v4/utils/oDataConfiguration.js +2 -2
  72. package/libx/_runtime/cds-services/adapter/odata-v4/utils/result.js +1 -1
  73. package/libx/_runtime/cds-services/services/Service.js +1 -1
  74. package/libx/_runtime/cds-services/util/assert.js +41 -65
  75. package/libx/_runtime/common/code-ext/WorkerPool.js +90 -0
  76. package/libx/_runtime/common/code-ext/WorkerReq.js +0 -4
  77. package/libx/_runtime/common/code-ext/execute.js +28 -18
  78. package/libx/_runtime/common/code-ext/handlers.js +5 -4
  79. package/libx/_runtime/common/code-ext/worker.js +45 -3
  80. package/libx/_runtime/common/code-ext/workerQueryExecutor.js +8 -7
  81. package/libx/_runtime/common/composition/delete.js +1 -1
  82. package/libx/_runtime/common/composition/update.js +3 -5
  83. package/libx/_runtime/common/generic/auth/expand.js +1 -1
  84. package/libx/_runtime/common/generic/auth/readOnly.js +5 -4
  85. package/libx/_runtime/common/generic/auth/restrict.js +7 -2
  86. package/libx/_runtime/common/generic/crud.js +12 -1
  87. package/libx/_runtime/common/generic/etag.js +11 -3
  88. package/libx/_runtime/common/generic/input.js +8 -6
  89. package/libx/_runtime/common/generic/paging.js +25 -8
  90. package/libx/_runtime/common/generic/put.js +1 -1
  91. package/libx/_runtime/common/generic/sorting.js +0 -1
  92. package/libx/_runtime/common/i18n/messages.properties +1 -0
  93. package/libx/_runtime/common/utils/cqn.js +5 -1
  94. package/libx/_runtime/common/utils/cqn2cqn4sql.js +2 -2
  95. package/libx/_runtime/common/utils/resolveView.js +14 -10
  96. package/libx/_runtime/common/utils/rewriteAsterisks.js +2 -3
  97. package/libx/_runtime/common/utils/templateProcessor.js +15 -17
  98. package/libx/_runtime/common/utils/templateProcessorPathSerializer.js +18 -6
  99. package/libx/_runtime/db/Service.js +1 -0
  100. package/libx/_runtime/db/data-conversion/post-processing.js +0 -18
  101. package/libx/_runtime/db/expand/expand-v2.js +2 -2
  102. package/libx/_runtime/db/expand/rawToExpanded.js +6 -6
  103. package/libx/_runtime/db/generic/integrity.js +1 -1
  104. package/libx/_runtime/db/utils/columns.js +5 -5
  105. package/libx/_runtime/fiori/generic/activate.js +3 -3
  106. package/libx/_runtime/fiori/generic/edit.js +1 -1
  107. package/libx/_runtime/fiori/generic/new.js +4 -0
  108. package/libx/_runtime/fiori/lean-draft.js +138 -46
  109. package/libx/_runtime/hana/execute.js +3 -1
  110. package/libx/_runtime/hana/pool.js +10 -2
  111. package/libx/_runtime/messaging/common-utils/AMQPClient.js +6 -1
  112. package/libx/_runtime/messaging/enterprise-messaging.js +1 -0
  113. package/libx/_runtime/remote/Service.js +16 -13
  114. package/libx/_runtime/remote/utils/client.js +6 -1
  115. package/libx/_runtime/sqlite/Service.js +5 -59
  116. package/libx/_runtime/sqlite/convertDraftAdminPathExpression.js +1 -0
  117. package/libx/_runtime/sqlite/customBuilder/CustomUpsertBuilder.js +2 -2
  118. package/libx/_runtime/sqlite/execute.js +3 -1
  119. package/libx/_runtime/types/api.js +12 -3
  120. package/libx/odata/afterburner.js +36 -0
  121. package/libx/odata/cqn2odata.js +1 -1
  122. package/libx/odata/grammar.pegjs +5 -3
  123. package/libx/odata/parser.js +1 -1
  124. package/libx/odata/utils.js +1 -1
  125. package/libx/rest/RestAdapter.js +1 -1
  126. package/libx/rest/RestRequest.js +1 -0
  127. package/package.json +5 -2
  128. package/libx/_runtime/common/code-ext/workerQuery.js +0 -45
  129. package/libx/_runtime/common/constants/limit.js +0 -12
  130. package/libx/_runtime/common/utils/page.js +0 -39
@@ -1,6 +1,7 @@
1
1
  const cds = require('../cds'),
2
2
  { Object_keys } = cds.utils
3
3
  const LOG = cds.log('fiori|drafts')
4
+ const original = Symbol('original')
4
5
 
5
6
  const DRAFT_ELEMENTS = new Set([
6
7
  'IsActiveEntity',
@@ -69,12 +70,14 @@ cds.ApplicationService.prototype.handle = async function (req) {
69
70
 
70
71
  if (
71
72
  !req.query ||
73
+ req.query.UPSERT || // skip UPSERTs (might have an additional INSERT)
72
74
  (!req.query.SELECT && !req.query.INSERT && !req.query.UPDATE && !req.query.DELETE) ||
73
75
  req.query._draftParams
74
76
  )
75
77
  return handle(req)
76
78
  const query = _cleansed(req.query, this.model)
77
- _cleanseParams(req.params)
79
+ _cleanseParams(req.params, req.target)
80
+ if (req.data) _cleanseParams(req.data, req.target)
78
81
  const draftParams = query._draftParams
79
82
 
80
83
  const _newReq = (req, query, draftParams, event) => {
@@ -82,8 +85,11 @@ cds.ApplicationService.prototype.handle = async function (req) {
82
85
  query._target = undefined
83
86
  query._draftParams = draftParams
84
87
  cds.infer(query, this.model.definitions)
88
+
89
+ // REVISIT: This is extremely bad. We should be able to just create a copy without such hacks.
85
90
  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
91
+ // If we create a `READ` event based on a modifying request, we delete data
92
+ if (event === 'READ' && req.event !== 'READ') delete _req.data // which we fix here -> but this is an ugly workaround
87
93
  _req.query = query
88
94
  _req.event =
89
95
  event ||
@@ -93,19 +99,28 @@ cds.ApplicationService.prototype.handle = async function (req) {
93
99
  (query.DELETE && 'DELETE') ||
94
100
  req.event
95
101
  _req.target = query._target
102
+ _req._ = Object.assign({}, req._ || {}) // don't share the same `_` object
96
103
  _req._.params = req.params
97
104
  _req.params = req.params
98
105
  _req._.query = query
99
106
  _req._ = req._
100
- _req.data = _req.query.UPDATE?.data || _req.query.INSERT?.entries?.[0]
107
+ _req._isRest = req._isRest
108
+ _req._isOData = req._isOData
109
+ _req.isConcurrentResource = req.isConcurrentResource
110
+ _req.isConditional = req.isConditional
111
+ _req.validateEtag = req.validateEtag
112
+ const cqnData = _req.query.UPDATE?.data || _req.query.INSERT?.entries?.[0]
113
+ if (cqnData) _req.data = cqnData // must point to the same object
101
114
 
102
115
  // Dirty hack: delegate messages to original request by binding the getter _messages to req
103
116
  let proto = req
104
117
  let _messagesDescr
105
118
  while (proto && !(_messagesDescr = Object.getOwnPropertyDescriptor(proto, '_messages')))
106
119
  proto = Object.getPrototypeOf(proto)
107
- Object.defineProperty(_req, '_messages', { ..._messagesDescr, get: _messagesDescr.get.bind(req) })
120
+ if (_messagesDescr)
121
+ Object.defineProperty(_req, '_messages', { ..._messagesDescr, get: _messagesDescr.get.bind(req) })
108
122
 
123
+ if (req.tx) _req.tx = req.tx
109
124
  return _req
110
125
  }
111
126
 
@@ -196,7 +211,7 @@ cds.ApplicationService.prototype.handle = async function (req) {
196
211
  const result = await run(
197
212
  HasActiveEntity ? UPDATE(req.target).data(res).where(targetWhere) : INSERT.into(req.target).entries(res)
198
213
  )
199
- req?._?.odataRes.setStatusCode(201)
214
+ req?._?.odataRes?.setStatusCode(201)
200
215
 
201
216
  return Object.assign(result, { IsActiveEntity: true })
202
217
 
@@ -252,11 +267,20 @@ cds.ApplicationService.prototype.handle = async function (req) {
252
267
  const updateData = { ...req.data }
253
268
  delete updateData.IsActiveEntity
254
269
  await run(UPDATE({ ref: draftsRef }).data(updateData))
255
- return Object.assign(req.data, { IsActiveEntity: false })
270
+ req.data.IsActiveEntity = false
271
+ return req.data
256
272
  }
257
273
  }
258
274
 
259
275
  if (req.event === 'READ') {
276
+ if (
277
+ !Object.keys(draftParams).length &&
278
+ !req.query._target.name?.endsWith('DraftAdministrativeData') &&
279
+ !req.query._target.drafts
280
+ ) {
281
+ req.query = query
282
+ return handle(req)
283
+ }
260
284
  const read = req.query._target.name.endsWith('.drafts')
261
285
  ? Read.ownDrafts
262
286
  : draftParams.IsActiveEntity === false && draftParams.SiblingEntity_IsActiveEntity === null
@@ -281,8 +305,34 @@ cds.ApplicationService.prototype.handle = async function (req) {
281
305
  return result
282
306
  }
283
307
 
284
- const _req = _newReq(req, query, draftParams, req.event)
285
- const result = await handle(_req)
308
+ req.query = query
309
+ const result = await handle(req)
310
+ return result
311
+ }
312
+
313
+ // REVISIT: It's not optimal to first calculate the whole result array and only later
314
+ // delete unrequested properties. However, as a first step, we do it that way,
315
+ // especially since the current db driver always adds those fields.
316
+ // Once we switch to the new driver, we'll adapt it.
317
+ const _requested = (result, query) => {
318
+ const originalQuery = query[original]
319
+ if (!result || !originalQuery) return result
320
+ const all = ['HasActiveEntity', 'HasDraftEntity']
321
+
322
+ const ignoredCols = new Set(all.concat('DraftAdministrativeData'))
323
+ const _isODataV2 = cds.context?.http?.req?.headers?.['x-cds-odata-version'] === 'v2'
324
+ if (!_isODataV2) ignoredCols.add('DraftAdministrativeData_DraftUUID')
325
+ for (const col of originalQuery.SELECT.columns || ['*']) {
326
+ const name = col.as || col.ref?.[0] || col
327
+ if (all.includes(name) || name === 'DraftAdministrativeData' || name === 'DraftAdministrativeData_DraftUUID')
328
+ ignoredCols.delete(name)
329
+ if (name === '*') all.forEach(c => ignoredCols.delete(c))
330
+ }
331
+ if (!ignoredCols.size) return result
332
+ const resArray = Array.isArray(result) ? result : [result]
333
+ for (const row of resArray) {
334
+ for (const ignoredCol of ignoredCols) delete row[ignoredCol]
335
+ }
286
336
  return result
287
337
  }
288
338
 
@@ -291,6 +341,13 @@ const Read = {
291
341
  LOG.debug('List Editing Status: Only Active')
292
342
  // DraftAdministrativeData is only accessible via drafts
293
343
  if (query._target.name.endsWith('.DraftAdministrativeData')) return run(query._drafts)
344
+ if (!query._target._isDraftEnabled) return run(query)
345
+ if (query.SELECT.columns && !query.SELECT.columns.some(c => c === '*')) {
346
+ const keys = Object_keys(query._target.keys).filter(k => k !== 'IsActiveEntity')
347
+ for (const key of keys) {
348
+ if (!query.SELECT.columns.some(c => c.ref?.[0] === key)) query.SELECT.columns.push({ ref: [key] })
349
+ }
350
+ }
294
351
  const actives = await run(query)
295
352
  if (!actives || (Array.isArray(actives) && !actives.length) || !query._target.drafts) return actives
296
353
  let drafts
@@ -313,7 +370,7 @@ const Read = {
313
370
  DraftAdministrativeData_DraftUUID: null
314
371
  })
315
372
  )
316
- return actives
373
+ return _requested(actives, query)
317
374
  },
318
375
  unchanged: async function (run, query) {
319
376
  LOG.debug('List Editing Status: Unchanged')
@@ -328,7 +385,7 @@ const Read = {
328
385
  const res = Read.onlyActives(run, query.where(Read.whereNotIn(query._target, drafts)), {
329
386
  ignoreDrafts: true
330
387
  })
331
- return res
388
+ return _requested(res, query)
332
389
  },
333
390
  ownDrafts: async function (run, query) {
334
391
  LOG.debug('List Editing Status: Own Draft')
@@ -356,11 +413,11 @@ const Read = {
356
413
  const drafts = await run(draftsQuery)
357
414
  Read.merge(query._target, drafts, [], row =>
358
415
  Object.assign(row, {
359
- IsActiveEntity: false,
360
- HasDraftEntity: false
416
+ HasDraftEntity: false,
417
+ IsActiveEntity: false
361
418
  })
362
419
  )
363
- return drafts
420
+ return _requested(drafts, query)
364
421
  },
365
422
  all: async function (run, query) {
366
423
  LOG.debug('List Editing Status: All')
@@ -391,6 +448,13 @@ const Read = {
391
448
  else ownNewDrafts.push(draft)
392
449
  }
393
450
 
451
+ // We can't properly calculate `count`:
452
+ // - Not all actives are retrieved (e.g. top = 0), hence there could be more deletes if more actives are requested,
453
+ // hence we cannot count deletions based on data.
454
+ // - We can't rely on the fact that `HasActiveEntity` always has an active counterpart because the filter
455
+ // is applied on draft and active data respectively (you could fetch a draft but not an active instance).
456
+ // However, there's not much we can do, so we use use this as a best guess.
457
+
394
458
  const count = isFirstPage ? ownNewDrafts.length + (isCount ? actives[0]?.$count : actives.$count) : actives.$count
395
459
  if (isCount) return { $count: count }
396
460
 
@@ -423,7 +487,7 @@ const Read = {
423
487
  })
424
488
  const res = isFirstPage ? [...ownNewDrafts, ...ownEditDrafts, ...actives] : actives
425
489
  if (query.SELECT.count) res.$count = count
426
- return res
490
+ return _requested(res, query)
427
491
  },
428
492
  activesFromDrafts: async function (run, query, { isLocked = true }) {
429
493
  const draftsQuery = query._drafts
@@ -450,7 +514,7 @@ const Read = {
450
514
  ? Object.assign(row, other, { IsActiveEntity: true, HasDraftEntity: true, HasActiveEntity: false })
451
515
  : Object.assign({ IsActiveEntity: true, HasDraftEntity: false, HasActiveEntity: false })
452
516
  )
453
- return actives
517
+ return _requested(actives, query)
454
518
  },
455
519
  unsavedChangesByAnotherUser: async function (run, query) {
456
520
  LOG.debug('List Editing Status: Unsaved Changes by Another User')
@@ -529,29 +593,33 @@ const Read = {
529
593
  }
530
594
  }
531
595
 
532
- function _cleanseParams(params) {
596
+ function _cleanseParams(params, target) {
597
+ if (!target?.drafts) return
533
598
  if (Array.isArray(params)) {
534
- for (const param of params) _cleanseParams(param)
599
+ for (const param of params) _cleanseParams(param, target)
535
600
  return
536
601
  }
537
602
  if (typeof params === 'object') {
538
603
  for (const key in params) {
539
- if (key === 'IsActiveEntity') delete params[key]
604
+ if (key === 'IsActiveEntity') {
605
+ const value = params[key]
606
+ delete params[key]
607
+ if (cds.env.fiori?.draft_compat) Object.defineProperty(params, key, { value, enumerable: false })
608
+ }
540
609
  }
541
610
  }
542
611
  }
543
612
 
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
- )
613
+ function _cleanseCols(columns, elements, target) {
614
+ // TODO: sometimes target is undefined
615
+ if (!target || typeof columns?.filter !== 'function') return columns
616
+ const filtered = target?.drafts ? columns.filter(c => !elements.has(c.ref?.[0])) : columns
617
+ return filtered.map(c => {
618
+ if (c.expand && c.ref) {
619
+ return { ...c, expand: _cleanseCols(c.expand, elements, target.elements[c.ref[0]]?._target) }
620
+ }
621
+ return c
622
+ })
555
623
  }
556
624
 
557
625
  /**
@@ -559,10 +627,10 @@ function _cleanseCols(columns, elements) {
559
627
  */
560
628
  function _cleansed(query, model) {
561
629
  const draftParams = {} //> used to collect draft filter criteria
562
- const q = _cleanseQuery(query, draftParams)
630
+ const q = _cleanseQuery(query, draftParams, model)
563
631
  if (query.SELECT) {
564
632
  const getDrafts = () => {
565
- const draftsQuery = _cleanseQuery(query, {}) // could just clone `q` but the latter is ruined by database layer
633
+ const draftsQuery = _cleanseQuery(query, {}, model) // could just clone `q` but the latter is ruined by database layer
566
634
  draftsQuery._target = undefined
567
635
  const [root, ...tail] = draftsQuery.SELECT.from.ref
568
636
  const draft = model.definitions[root.id || root].drafts
@@ -572,7 +640,7 @@ function _cleansed(query, model) {
572
640
  cds.infer(draftsQuery, model.definitions)
573
641
  // draftsQuery._target = draftsQuery._target?.drafts || draftsQuery._target
574
642
  if (query.SELECT.columns && query._target.drafts)
575
- draftsQuery.SELECT.columns = _cleanseCols(query.SELECT.columns, REDUCED_DRAFT_ELEMENTS)
643
+ draftsQuery.SELECT.columns = _cleanseCols(query.SELECT.columns, REDUCED_DRAFT_ELEMENTS, draft)
576
644
 
577
645
  if (draftsQuery._target.name.endsWith('.DraftAdministrativeData')) {
578
646
  draftsQuery.SELECT.columns = _tweakAdminCols(draftsQuery.SELECT.columns)
@@ -592,21 +660,30 @@ function _cleansed(query, model) {
592
660
  }
593
661
 
594
662
  Object.defineProperty(q, '_draftParams', { value: draftParams, enumerable: false })
663
+ q[original] = query
595
664
  return q
596
665
 
597
- function _cleanseQuery(query, draftParams) {
666
+ function _cleanseQuery(query, draftParams, model) {
667
+ const target = query._target
598
668
  const q = cds.ql.clone(query)
599
669
 
600
670
  const ref = q.SELECT?.from.ref || q.UPDATE?.entity.ref || q.INSERT?.into.ref || q.DELETE?.from.ref
601
671
  const cqn = q.SELECT || q.UPDATE || q.INSERT || q.DELETE
602
672
 
603
673
  if (ref) {
604
- const cleansedRef = ref.map(r => (r.where ? { ...r, where: _cleanseWhere(r.where, draftParams) } : r))
674
+ let entity
675
+ const cleansedRef = ref.map(r => {
676
+ entity = (entity && entity.elements[r.id || r]._target) || model.definitions[r.id || r]
677
+ if (!entity?.drafts) return r
678
+ return r.where ? { ...r, where: _cleanseWhere(r.where, draftParams) } : r
679
+ })
605
680
  if (q.SELECT) q.SELECT.from = { ...q.SELECT.from, ref: cleansedRef }
606
681
  else if (q.DELETE) q.DELETE.from = { ...q.DELETE.from, ref: cleansedRef }
607
682
  else if (q.UPDATE) q.UPDATE.entity = { ...q.UPDATE.entity, ref: cleansedRef }
608
683
  else if (q.INSERT) q.INSERT.into = { ...q.INSERT.into, ref: cleansedRef }
609
684
 
685
+ // This only works for simple cases of `SiblingEntity`, e.g. `root(ID=1,IsActiveEntity=false)/SiblingEntity`
686
+ // , check if there are more complicated use cases
610
687
  const siblingIdx = cleansedRef.findIndex(r => r === 'SiblingEntity')
611
688
  if (siblingIdx !== -1) {
612
689
  cleansedRef.splice(siblingIdx, 1)
@@ -614,10 +691,9 @@ function _cleansed(query, model) {
614
691
  }
615
692
  }
616
693
 
617
- if (cqn.where) cqn.where = _cleanseWhere(cqn.where, draftParams)
618
- if (cqn.columns) cqn.columns = _cleanseCols(q.SELECT.columns, DRAFT_ELEMENTS)
619
- if (cqn.orderBy) cqn.orderBy = _cleanseWhere(cqn.orderBy, {})
620
-
694
+ if (target.drafts && cqn.where) cqn.where = _cleanseWhere(cqn.where, draftParams)
695
+ if (target.drafts && cqn.orderBy) cqn.orderBy = _cleanseWhere(cqn.orderBy, {})
696
+ if (cqn.columns) cqn.columns = _cleanseCols(cqn.columns, DRAFT_ELEMENTS, target)
621
697
  return q
622
698
  }
623
699
 
@@ -740,7 +816,10 @@ function expandStarStar(target, recursion = new Map()) {
740
816
 
741
817
  async function onNew(req) {
742
818
  LOG.debug('new draft')
743
- const isRoot = typeof req.query.INSERT.into === 'string'
819
+ const isRoot = typeof req.query.INSERT.into === 'string' || req.query.INSERT.into.ref?.length === 1
820
+ // Only allowed for pseudo draft roots (entities with this action)
821
+ if (isRoot && !req.target.actives['@Common.DraftRoot.ActivationAction'])
822
+ req.reject(403, 'DRAFT_MODIFICATION_ONLY_VIA_ROOT')
744
823
  let DraftUUID
745
824
  if (isRoot) DraftUUID = cds.utils.uuid()
746
825
  else {
@@ -777,10 +856,21 @@ async function onNew(req) {
777
856
  })
778
857
  .where({ DraftUUID })
779
858
 
780
- const draftData = Object.assign(
781
- { DraftAdministrativeData_DraftUUID: DraftUUID, HasActiveEntity: false },
782
- req.query.INSERT.entries[0]
783
- )
859
+ const _assignDraftData = (obj, target) => {
860
+ const newObj = Object.assign({ DraftAdministrativeData_DraftUUID: DraftUUID, HasActiveEntity: false }, obj)
861
+ if (!target) return newObj
862
+
863
+ // Also support deep insertions
864
+ for (const key in newObj) {
865
+ if (typeof newObj[key] === 'object' && target.elements[key]?.isComposition) {
866
+ newObj[key] = _assignDraftData(newObj[key], target.elements[key]._target)
867
+ }
868
+ }
869
+
870
+ return newObj
871
+ }
872
+
873
+ const draftData = _assignDraftData(req.query.INSERT.entries[0], req.target)
784
874
 
785
875
  delete draftData.IsActiveEntity
786
876
  const draftCQN = INSERT.into(req.target).entries(draftData)
@@ -794,7 +884,7 @@ async function onEdit(req) {
794
884
  LOG.debug('edit active')
795
885
  const draftParams = req.query._draftParams
796
886
  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')
887
+ req.reject(400, 'Action "draftEdit" can only be called on the root active entity')
798
888
  }
799
889
  const targetWhere = req.query.SELECT.from.ref[0].where
800
890
 
@@ -823,8 +913,10 @@ async function onEdit(req) {
823
913
  // prevent service to check for own user
824
914
  Object.defineProperty(draftsCheck, '_draftParams', { value: draftParams, enumerable: false })
825
915
 
916
+ const activeCQN = SELECT.one.from(req.target).columns(cols).where(targetWhere).forUpdate({ wait: 0 })
917
+ activeCQN._suppressLocalization = true // in the future we should be able to just set activeCQN.SELECT.localized = false
826
918
  const [res, draft] = await Promise.all([
827
- this.run(SELECT.one.from(req.target).columns(cols).where(targetWhere).forUpdate({ wait: 0 })),
919
+ this.run(activeCQN),
828
920
  // no user check must be done here...
829
921
  this.run(draftsCheck)
830
922
  ])
@@ -862,7 +954,7 @@ async function onEdit(req) {
862
954
 
863
955
  // REVISIT: we need to use okra API here because it must be set in the batched request
864
956
  // status code must be set in handler to allow overriding for FE V2
865
- req?._?.odataRes.setStatusCode(201)
957
+ req?._?.odataRes?.setStatusCode(201)
866
958
 
867
959
  return { ...res, IsActiveEntity: false } // REVISIT: Flatten?
868
960
  }
@@ -225,7 +225,9 @@ function _processExpand(model, dbc, cqn, user, locale, txTimestamp) {
225
225
  function executeSelectCQN(model, dbc, query, user, locale, txTimestamp) {
226
226
  if (hasExpand(query)) {
227
227
  // expand: '**' or '*3' is handled by new impl
228
- if (query.SELECT.columns.some(c => c.expand && typeof c.expand === 'string' && /^\*{1}[\d|*]+/.test(c.expand))) {
228
+ if (
229
+ query.SELECT.columns.some(c => c.expand && typeof c.expand[0] === 'string' && /^\*{1}[\d|*]+/.test(c.expand[0]))
230
+ ) {
229
231
  return expandV2(model, dbc, query, user, locale, txTimestamp, executeSelectCQN)
230
232
  }
231
233
  return _processExpand(model, dbc, query, user, locale, txTimestamp)
@@ -1,3 +1,5 @@
1
+ // REVISIT: Remove @sap/instance-manager compat with CDS 7
2
+
1
3
  const cds = require('../cds')
2
4
  const LOG = cds.log('pool|db')
3
5
 
@@ -15,7 +17,10 @@ function multiTenantServiceManager() {
15
17
  if (e.code === 'MODULE_NOT_FOUND') return null
16
18
  else throw e
17
19
  }
18
- return cds.env.requires['cds.xt.DeploymentService']?.['old-instance-manager'] ? null : cds.xt?.serviceManager
20
+ const oldIm =
21
+ cds.requires.multitenancy?.['old-instance-manager'] ??
22
+ cds.env.requires?.['cds.xt.DeploymentService']?.['old-instance-manager']
23
+ return oldIm ? null : cds.xt?.serviceManager
19
24
  }
20
25
 
21
26
  function multiTenantInstanceManager(config = cds.env.requires.db) {
@@ -78,7 +83,10 @@ async function credentials4(tenant, db) {
78
83
  : singleTenantInstanceManager(opts)
79
84
  }
80
85
 
81
- if (cds.xt?.serviceManager && !cds.env.requires['cds.xt.DeploymentService']?.['old-instance-manager']) {
86
+ const oldIm =
87
+ cds.requires.multitenancy?.['old-instance-manager'] ??
88
+ cds.env.requires?.['cds.xt.DeploymentService']?.['old-instance-manager']
89
+ if (cds.xt?.serviceManager && !oldIm) {
82
90
  return (await db._instance_manager.get(tenant, { disableCache: true })).credentials
83
91
  }
84
92
 
@@ -31,7 +31,12 @@ const emit = ({ data, event: topic, headers = {} }, stream, prefix, LOG) =>
31
31
  new Promise((resolve, reject) => {
32
32
  LOG._info && LOG.info('Emit', { topic })
33
33
  const message = { ...headers, data }
34
- const payload = { chunks: [Buffer.from(JSON.stringify(message))], type: 'application/json' }
34
+ const payload = {
35
+ chunks: [Buffer.from(JSON.stringify(message))],
36
+ type: ['id', 'source', 'specversion', 'type'].every(el => el in headers)
37
+ ? 'application/cloudevents+json'
38
+ : 'application/json'
39
+ }
35
40
  const msg = {
36
41
  done: resolve,
37
42
  failed: e => {
@@ -82,6 +82,7 @@ class EnterpriseMessaging extends AMQPWebhookMessaging {
82
82
 
83
83
  // New mtx based on @sap/cds-mtxs
84
84
  async addMTXSHandlers() {
85
+ // REVISIT: Is that tested with MTX services in sidecar?
85
86
  const deploymentSrv = await cds.connect.to('cds.xt.DeploymentService')
86
87
  const provisioningSrv = await cds.connect.to('cds.xt.SaasProvisioningService')
87
88
  deploymentSrv.impl(() => {
@@ -1,19 +1,6 @@
1
1
  const cds = require('../cds')
2
2
  const LOG = cds.log('remote')
3
3
 
4
- // REVISIT: use cds.log's logger in cloud sdk
5
-
6
- // disable sdk logger if not in debug mode
7
- if (!LOG._debug) {
8
- try {
9
- // eslint-disable-next-line cds/no-missing-dependencies -- needs to be added by app dev
10
- const sdkUtils = require('@sap-cloud-sdk/util')
11
- sdkUtils.setGlobalLogLevel('error')
12
- } catch (err) {
13
- /* might fail in cds repl due to winston's exception handler, see cap/issues#10134 */
14
- }
15
- }
16
-
17
4
  const { resolveView, getTransition, restoreLink, findQueryTarget } = require('../common/utils/resolveView')
18
5
  const { postProcess } = require('../common/utils/postProcessing')
19
6
  const { getKind, run, getDestination, getAdditionalOptions, getReqOptions } = require('./utils/client')
@@ -165,6 +152,7 @@ const resolvedTargetOfQuery = q => {
165
152
  return transitions.length && [transitions.length - 1].target
166
153
  }
167
154
  let logged
155
+ let sdkLoggerDisabled
168
156
  class RemoteService extends cds.Service {
169
157
  init() {
170
158
  if (!this.options.credentials) {
@@ -186,6 +174,21 @@ class RemoteService extends cds.Service {
186
174
  'Configuration option "cds.env.features.fetch_csrf" is deprecated.\n Please use "csrf"/"csrfInBatch" as described in https://cap.cloud.sap/docs/node.js/remote-services'
187
175
  )
188
176
  }
177
+
178
+ // REVISIT: use cds.log's logger in cloud sdk
179
+
180
+ // disable sdk logger if not in debug mode
181
+ if (!LOG._debug && !sdkLoggerDisabled) {
182
+ try {
183
+ // eslint-disable-next-line cds/no-missing-dependencies -- needs to be added by app dev
184
+ const sdkUtils = require('@sap-cloud-sdk/util')
185
+ sdkUtils.setGlobalLogLevel('error')
186
+ // disable sdk logger once
187
+ sdkLoggerDisabled = true
188
+ } catch (err) {
189
+ /* might fail in cds repl due to winston's exception handler, see cap/issues#10134 */
190
+ }
191
+ }
189
192
  // REVISIT: remove cds.env.features.fetch_csrf in next major ^7
190
193
  this.csrf = cds.env.features.fetch_csrf || this.options.csrf
191
194
  this.csrfInBatch = this.options.csrfInBatch
@@ -483,7 +483,12 @@ const getReqOptions = (req, query, service) => {
483
483
  for (const k in originalHeaders) if (k.match(/^dwc-/)) reqOptions.headers[k] = originalHeaders[k]
484
484
  }
485
485
 
486
- if (reqOptions.data && reqOptions.method !== 'GET' && reqOptions.method !== 'HEAD') {
486
+ if (
487
+ reqOptions.data &&
488
+ reqOptions.method !== 'GET' &&
489
+ reqOptions.method !== 'HEAD' &&
490
+ !(reqOptions.data instanceof require('stream').Readable)
491
+ ) {
487
492
  if (typeof reqOptions.data === 'object' && !Buffer.isBuffer(reqOptions.data)) {
488
493
  reqOptions.headers['content-type'] = 'application/json'
489
494
  reqOptions.headers['content-length'] = Buffer.byteLength(JSON.stringify(reqOptions.data))
@@ -75,8 +75,7 @@ module.exports = class SQLiteDatabase extends DatabaseService {
75
75
  this.before(['CREATE', 'UPDATE'], '*', this._input) // > has to run before rewrite
76
76
  this.before(['CREATE', 'READ', 'UPDATE', 'DELETE', 'UPSERT'], '*', this._rewrite)
77
77
 
78
- if (cds.env.features.lean_draft && cds.db?.kind !== 'better-sqlite')
79
- this.before('READ', '*', convertDraftAdminPathExpression)
78
+ if (cds.env.fiori.lean_draft && !cds.db?.cqn2sql) this.before('READ', '*', convertDraftAdminPathExpression)
80
79
  this.before('READ', '*', convertAssocToOneManaged)
81
80
  this.before('READ', '*', localized) // > has to run after rewrite
82
81
  this.before('READ', '*', this._virtual)
@@ -103,6 +102,9 @@ module.exports = class SQLiteDatabase extends DatabaseService {
103
102
  }
104
103
 
105
104
  getDbUrl(tenant) {
105
+ return this.url4(tenant)
106
+ }
107
+ url4(tenant) {
106
108
  const credentials = this.options.credentials || this.options || {}
107
109
  let dbUrl = credentials.database || credentials.url || credentials.host || ':memory:'
108
110
 
@@ -123,7 +125,7 @@ module.exports = class SQLiteDatabase extends DatabaseService {
123
125
  const tenant = isMultitenant && arg && (typeof arg === 'string' ? arg : arg.tenant || (arg.user && arg.user.tenant))
124
126
  let dbc = this.dbcs.get(tenant)
125
127
  if (!dbc) {
126
- const dbUrl = this.getDbUrl(tenant)
128
+ const dbUrl = this.url4(tenant)
127
129
 
128
130
  dbc = await _new(dbUrl)
129
131
 
@@ -152,62 +154,6 @@ module.exports = class SQLiteDatabase extends DatabaseService {
152
154
  else dbc._busy = false
153
155
  }
154
156
 
155
- /*
156
- * deploy
157
- */
158
- // REVISIT: make tenant aware
159
- async deploy(model, options = {}) {
160
- let createEntities = cds.compile.to.sql(model, options)
161
- if (createEntities.length === 0) return // > nothing to deploy
162
-
163
- let dropViews = []
164
- let dropTables = []
165
- for (const each of createEntities) {
166
- const [, table, entity] = each.match(/^CREATE (?:(TABLE)|VIEW)\s+"?([^\s"(]+)"?/im) || []
167
- if (table) dropTables.push({ DROP: { entity } })
168
- else dropViews.push({ DROP: { view: entity } })
169
- }
170
-
171
- // H2 is picky on the order
172
- dropTables.reverse()
173
- dropViews.reverse()
174
-
175
- if (options.dry) {
176
- // do not use cds.log() here!
177
- const log = console.log // eslint-disable-line no-console
178
- for (const {
179
- DROP: { view }
180
- } of dropViews) {
181
- log('DROP VIEW IF EXISTS ' + view + ';')
182
- }
183
- log()
184
- for (const {
185
- DROP: { entity }
186
- } of dropTables) {
187
- log('DROP TABLE IF EXISTS ' + entity + ';')
188
- }
189
- log()
190
- for (const each of createEntities) log(each + '\n')
191
- return
192
- }
193
-
194
- await this.run(async tx => {
195
- // This starts a new transaction if called from CLI, while joining
196
- // existing root tx, e.g. when called from DeploymenrService
197
- const [ext] = await tx.run(`SELECT 1 from sqlite_master where name='cds_xt_Extensions'`)
198
- if (ext) {
199
- // Poor man's schema evolution for MTX upgrade operations
200
- createEntities = createEntities.filter(ct => !ct.match(/^CREATE TABLE cds_xt_Extensions/im))
201
- dropTables = dropTables.filter(dt => dt.DROP.entity !== 'cds_xt_Extensions')
202
- }
203
- await tx.run(dropViews)
204
- await tx.run(dropTables)
205
- await tx.run(createEntities)
206
- })
207
-
208
- return true
209
- }
210
-
211
157
  async disconnect(tenant) {
212
158
  this.dbcs.delete(tenant)
213
159
  }
@@ -1,6 +1,7 @@
1
1
  const cds = require('../cds')
2
2
 
3
3
  function sqliteConvertDraftAdminPathExpression(req) {
4
+ if (req.query?.SELECT?.from?.SET) return // not supported, won't happen in draft
4
5
  if (
5
6
  !req.query?.SELECT ||
6
7
  !req.query?._target?.name?.endsWith('.drafts') ||
@@ -31,8 +31,8 @@ class CustomUpsertBuilder extends InsertBuilder {
31
31
  if (!keys.includes(col_)) updates.push(`${sqlColumn}=excluded.${sqlColumn}`)
32
32
  })
33
33
  const conflict = updates.length
34
- ? ` ON CONFLICT(${keys}) DO UPDATE SET ` + updates.join(', ')
35
- : ` ON CONFLICT(${keys}) DO NOTHING`
34
+ ? ` ON CONFLICT (${keys}) DO UPDATE SET ` + updates.join(', ')
35
+ : ` ON CONFLICT (${keys}) DO NOTHING`
36
36
 
37
37
  this._outputObj.sql = this._outputObj.sql + conflict
38
38
  return this._outputObj
@@ -109,7 +109,9 @@ function _processExpand(model, dbc, cqn, user, locale, txTimestamp) {
109
109
  function executeSelectCQN(model, dbc, query, user, locale, txTimestamp) {
110
110
  if (hasExpand(query)) {
111
111
  // expand: '**' or '*3' is handled by new impl
112
- if (query.SELECT.columns.some(c => c.expand && typeof c.expand === 'string' && /^\*{1}[\d|*]+/.test(c.expand))) {
112
+ if (
113
+ query.SELECT.columns.some(c => c.expand && typeof c.expand[0] === 'string' && /^\*{1}[\d|*]+/.test(c.expand[0]))
114
+ ) {
113
115
  return expandV2(model, dbc, query, user, locale, txTimestamp, executeSelectCQN)
114
116
  }
115
117
  return _processExpand(model, dbc, query, user, locale, txTimestamp)