@sap/cds 6.0.2 → 6.1.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 (134) hide show
  1. package/CHANGELOG.md +153 -19
  2. package/apis/cds.d.ts +11 -7
  3. package/apis/log.d.ts +48 -0
  4. package/apis/ql.d.ts +72 -15
  5. package/bin/build/buildTaskHandler.js +5 -2
  6. package/bin/build/constants.js +4 -1
  7. package/bin/build/provider/buildTaskHandlerEdmx.js +11 -39
  8. package/bin/build/provider/buildTaskHandlerFeatureToggles.js +13 -32
  9. package/bin/build/provider/buildTaskHandlerInternal.js +56 -4
  10. package/bin/build/provider/buildTaskProviderInternal.js +22 -14
  11. package/bin/build/provider/hana/index.js +8 -7
  12. package/bin/build/provider/java/index.js +18 -8
  13. package/bin/build/provider/mtx/index.js +7 -4
  14. package/bin/build/provider/mtx/resourcesTarBuilder.js +64 -35
  15. package/bin/build/provider/mtx-extension/index.js +57 -0
  16. package/bin/build/provider/mtx-sidecar/index.js +46 -18
  17. package/bin/build/provider/nodejs/index.js +34 -13
  18. package/bin/build/util.js +6 -4
  19. package/bin/deploy/to-hana/cfUtil.js +7 -2
  20. package/bin/deploy/to-hana/hana.js +6 -3
  21. package/bin/serve.js +8 -13
  22. package/lib/compile/{index.js → cds-compile.js} +0 -0
  23. package/lib/compile/extend.js +15 -5
  24. package/lib/compile/minify.js +1 -15
  25. package/lib/compile/parse.js +1 -1
  26. package/lib/compile/resolve.js +2 -2
  27. package/lib/compile/to/srvinfo.js +6 -4
  28. package/lib/{deploy.js → dbs/cds-deploy.js} +8 -8
  29. package/lib/env/{index.js → cds-env.js} +1 -17
  30. package/lib/env/{requires.js → cds-requires.js} +24 -3
  31. package/lib/env/defaults.js +7 -1
  32. package/lib/env/schemas/cds-package.json +11 -0
  33. package/lib/env/schemas/cds-rc.json +605 -0
  34. package/lib/index.js +20 -17
  35. package/lib/log/{errors.js → cds-error.js} +1 -1
  36. package/lib/log/{index.js → cds-log.js} +0 -0
  37. package/lib/ql/SELECT.js +1 -1
  38. package/lib/ql/{index.js → cds-ql.js} +0 -0
  39. package/lib/req/cds-context.js +1 -1
  40. package/lib/req/context.js +35 -7
  41. package/lib/req/locale.js +5 -1
  42. package/lib/{serve → srv}/adapters.js +23 -19
  43. package/lib/{connect → srv}/bindings.js +0 -0
  44. package/lib/{connect/index.js → srv/cds-connect.js} +1 -1
  45. package/lib/{serve/index.js → srv/cds-serve.js} +1 -1
  46. package/lib/{serve → srv}/factory.js +2 -3
  47. package/lib/{serve/Service-api.js → srv/srv-api.js} +14 -6
  48. package/lib/{serve/Service-dispatch.js → srv/srv-dispatch.js} +3 -2
  49. package/lib/{serve/Service-handlers.js → srv/srv-handlers.js} +10 -0
  50. package/lib/{serve/Service-methods.js → srv/srv-methods.js} +10 -8
  51. package/lib/srv/srv-models.js +206 -0
  52. package/lib/{serve/Transaction.js → srv/srv-tx.js} +6 -1
  53. package/lib/utils/{tests.js → cds-test.js} +2 -2
  54. package/lib/utils/cds-utils.js +146 -0
  55. package/lib/utils/index.js +2 -136
  56. package/lib/utils/jest.js +43 -0
  57. package/lib/utils/resources/index.js +14 -24
  58. package/lib/utils/resources/tar.js +18 -41
  59. package/libx/_runtime/auth/index.js +13 -10
  60. package/libx/_runtime/cds-services/adapter/odata-v4/OData.js +9 -20
  61. package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +1 -4
  62. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +19 -7
  63. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/create.js +8 -11
  64. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/delete.js +1 -4
  65. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/error.js +2 -2
  66. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +6 -19
  67. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +1 -4
  68. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +2 -2
  69. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +8 -10
  70. package/libx/_runtime/cds-services/adapter/odata-v4/to.js +38 -4
  71. package/libx/_runtime/cds-services/adapter/odata-v4/utils/handlerUtils.js +2 -6
  72. package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +8 -5
  73. package/libx/_runtime/cds-services/services/utils/differ.js +4 -0
  74. package/libx/_runtime/cds-services/util/errors.js +1 -29
  75. package/libx/_runtime/common/constants/events.js +1 -3
  76. package/libx/_runtime/common/i18n/messages.properties +2 -1
  77. package/libx/_runtime/common/perf/index.js +10 -15
  78. package/libx/_runtime/common/utils/cqn2cqn4sql.js +0 -1
  79. package/libx/_runtime/common/utils/entityFromCqn.js +8 -5
  80. package/libx/_runtime/common/utils/template.js +1 -1
  81. package/libx/_runtime/db/Service.js +2 -14
  82. package/libx/_runtime/db/expand/expandCQNToJoin.js +28 -25
  83. package/libx/_runtime/db/generic/input.js +4 -0
  84. package/libx/_runtime/db/sql-builder/SelectBuilder.js +37 -18
  85. package/libx/_runtime/extensibility/activate.js +47 -47
  86. package/libx/_runtime/extensibility/add.js +19 -13
  87. package/libx/_runtime/extensibility/addExtension.js +17 -13
  88. package/libx/_runtime/extensibility/defaults.js +25 -30
  89. package/libx/_runtime/extensibility/linter/allowlist_checker.js +373 -0
  90. package/libx/_runtime/extensibility/linter/annotations_checker.js +113 -0
  91. package/libx/_runtime/extensibility/linter/checker_base.js +20 -0
  92. package/libx/_runtime/extensibility/linter/namespace_checker.js +180 -0
  93. package/libx/_runtime/extensibility/linter.js +32 -0
  94. package/libx/_runtime/extensibility/push.js +78 -21
  95. package/libx/_runtime/extensibility/service.js +29 -12
  96. package/libx/_runtime/extensibility/token.js +56 -0
  97. package/libx/_runtime/extensibility/validation.js +6 -9
  98. package/libx/_runtime/fiori/generic/activate.js +0 -4
  99. package/libx/_runtime/fiori/generic/edit.js +1 -9
  100. package/libx/_runtime/fiori/generic/new.js +3 -28
  101. package/libx/_runtime/fiori/generic/patch.js +6 -7
  102. package/libx/_runtime/fiori/generic/prepare.js +11 -18
  103. package/libx/_runtime/fiori/generic/read.js +11 -1
  104. package/libx/_runtime/fiori/utils/handler.js +0 -17
  105. package/libx/_runtime/hana/Service.js +0 -1
  106. package/libx/_runtime/hana/conversion.js +12 -1
  107. package/libx/_runtime/hana/customBuilder/CustomFunctionBuilder.js +4 -3
  108. package/libx/_runtime/hana/customBuilder/CustomSelectBuilder.js +5 -0
  109. package/libx/_runtime/hana/pool.js +6 -10
  110. package/libx/_runtime/hana/search2Contains.js +0 -5
  111. package/libx/_runtime/hana/search2cqn4sql.js +1 -0
  112. package/libx/_runtime/messaging/AMQPWebhookMessaging.js +18 -19
  113. package/libx/_runtime/messaging/file-based.js +1 -0
  114. package/libx/_runtime/messaging/outbox/utils.js +1 -1
  115. package/libx/_runtime/messaging/service.js +11 -6
  116. package/libx/_runtime/remote/utils/client.js +6 -2
  117. package/libx/_runtime/remote/utils/data.js +5 -0
  118. package/libx/_runtime/sqlite/Service.js +0 -1
  119. package/libx/odata/afterburner.js +79 -2
  120. package/libx/odata/cqn2odata.js +9 -7
  121. package/libx/odata/grammar.pegjs +161 -77
  122. package/libx/odata/index.js +9 -3
  123. package/libx/odata/parser.js +1 -1
  124. package/libx/odata/utils.js +39 -5
  125. package/libx/rest/RestAdapter.js +1 -2
  126. package/libx/rest/middleware/delete.js +4 -5
  127. package/libx/rest/middleware/parse.js +3 -2
  128. package/package.json +3 -3
  129. package/server.js +1 -1
  130. package/srv/extensibility-service.cds +6 -3
  131. package/srv/model-provider.cds +3 -1
  132. package/srv/model-provider.js +84 -104
  133. package/srv/mtx.js +7 -1
  134. package/libx/_runtime/cds-services/adapter/odata-v4/Dispatcher.js +0 -240
@@ -7,16 +7,16 @@ const {
7
7
  } = require('../okra/odata-server')
8
8
 
9
9
  const { getSapMessages } = require('../../../../common/error/frontend')
10
- const { getActionOrFunctionReturnType } = require('../utils/handlerUtils')
10
+ const { getActionOrFunctionReturnType, isReturnMinimal } = require('../utils/handlerUtils')
11
11
  const { validateResourcePath } = require('../utils/request')
12
12
  const { toODataResult, postProcess } = require('../utils/result')
13
- const { DRAFT_MOD_EVENTS } = require('../../../../common/constants/events')
13
+ const { DRAFT_EVENTS } = require('../../../../common/constants/events')
14
14
  const { readAfterWrite } = require('../utils/readAfterWrite')
15
15
 
16
16
  const _postProcess = async (req, odataReq, odataRes, tx, result) => {
17
17
  const returnType = getActionOrFunctionReturnType(odataReq.getUriInfo().getPathSegments(), tx.model.definitions)
18
18
  // as of spec meeting: no generic support of $select/$expand for custom actions/functions
19
- if (returnType && returnType.kind === 'entity' && req.event in DRAFT_MOD_EVENTS) {
19
+ if (returnType && returnType.kind === 'entity' && (req.event in DRAFT_EVENTS || req.event === 'EDIT')) {
20
20
  result = await readAfterWrite(req, tx, { operation: { result, returnType } })
21
21
  if (result && !('IsActiveEntity' in result)) result.IsActiveEntity = req.event === 'draftActivate'
22
22
  }
@@ -59,13 +59,25 @@ const action = service => {
59
59
  } else {
60
60
  await tx.commit(result)
61
61
  }
62
+
63
+ if (isReturnMinimal(req) || result === null) odataRes.setStatusCode(204)
64
+ else if (req.event === 'draftActivate' || req.event === 'EDIT') {
65
+ odataRes.setStatusCode(201)
66
+ const keys = Object.keys(req.target.keys).filter(k => {
67
+ return k !== 'IsActiveEntity' && !req.target.keys[k]._isAssociationStrict
68
+ })
69
+ const keysString = keys.map(key => `${key}=${result[key]}`).join(',')
70
+ odataRes.setHeader(
71
+ 'location',
72
+ `../${req.target.name.replace(`${service.name}.`, '')}(${keysString},IsActiveEntity=${
73
+ req.event === 'draftActivate'
74
+ })`
75
+ )
76
+ }
62
77
  } catch (e) {
63
78
  err = e
64
79
 
65
- if (changeset) {
66
- // for passing into rollback
67
- odataReq.getBatchApplicationData().errors[changeset].push({ error: e, req })
68
- } else {
80
+ if (!changeset) {
69
81
  // REVISIT: rollback needed if an error occurred before commit attempted -> how to distinguish?
70
82
  await tx.rollback(e).catch(() => {})
71
83
  }
@@ -38,15 +38,15 @@ const create = service => {
38
38
  try {
39
39
  result = await tx.dispatch(req)
40
40
 
41
- // REVISIT: Return post processing once we implement srv read after write
41
+ // REVISIT:
42
+ // Performance: For `isReturnMinimal` it's enough to just read the etag.
43
+ // Note: Without read access, one cannot return the etag.
44
+ if (req._.readAfterWrite) {
45
+ result = await readAfterWrite(req, service, { operation: { result } })
46
+ }
42
47
  if (isReturnMinimal(req)) {
43
48
  result = postProcessMinimal(req, service, result)
44
49
  } else {
45
- // REVISIT: find better solution
46
- if (req._.readAfterWrite) {
47
- result = await readAfterWrite(req, service)
48
- }
49
-
50
50
  postProcess(req, odataRes, service, result)
51
51
  }
52
52
 
@@ -57,16 +57,13 @@ const create = service => {
57
57
  await tx.commit(result)
58
58
  }
59
59
 
60
- if (isReturnMinimal(req)) {
60
+ if (isReturnMinimal(req) || result === null) {
61
61
  odataRes.setStatusCode(204)
62
62
  }
63
63
  } catch (e) {
64
64
  err = e
65
65
 
66
- if (changeset) {
67
- // for passing into rollback
68
- odataReq.getBatchApplicationData().errors[changeset].push({ error: e, req })
69
- } else {
66
+ if (!changeset) {
70
67
  // REVISIT: rollback needed if an error occurred before commit attempted -> how to distinguish?
71
68
  await tx.rollback(e).catch(() => {})
72
69
  }
@@ -44,10 +44,7 @@ const del = service => {
44
44
  } catch (e) {
45
45
  err = e
46
46
 
47
- if (changeset) {
48
- // for passing into rollback
49
- odataReq.getBatchApplicationData().errors[changeset].push({ error: e, req })
50
- } else {
47
+ if (!changeset) {
51
48
  // REVISIT: rollback needed if an error occurred before commit attempted -> how to distinguish?
52
49
  await tx.rollback(e).catch(() => {})
53
50
  }
@@ -114,13 +114,13 @@ const getErrorHandler = (crashOnError = true, srv) => {
114
114
 
115
115
  // invoke srv.on('error', function (err, req) { ... }) here in special situations
116
116
  // REVISIT: if for compat reasons, remove once cds^5.1
117
- if (srv._handlers._error) {
117
+ if (srv._handlers._error.length) {
118
118
  let ctx = cds.context
119
119
  if (!ctx) {
120
120
  // > error before req was dispatched
121
121
  ctx = new cds.Request({ req, res: req.res, user: req.user || new cds.User.Anonymous() })
122
122
  for (const each of srv._handlers._error) each.handler.call(srv, err, ctx)
123
- } else if (err.getRootCause) {
123
+ } else {
124
124
  // > error after req was dispatched, e.g., serialization error in okra
125
125
  for (const each of srv._handlers._error) each.handler.call(srv, err, ctx)
126
126
  }
@@ -5,10 +5,6 @@ const { toODataResult } = require('../utils/result')
5
5
  const { normalizeError } = require('../../../../common/error/frontend')
6
6
  const getError = require('../../../../common/error')
7
7
 
8
- const _getMetadata4Tenant = async (tenant, locale, service) => {
9
- return await cds.mtx.getEdmx(tenant, service.name, locale)
10
- }
11
-
12
8
  /**
13
9
  * Provide localized metadata handler.
14
10
  *
@@ -23,21 +19,12 @@ const metadata = service => {
23
19
  const locale = odataRes.getContract().getLocale()
24
20
 
25
21
  try {
26
- let edmx
27
-
28
- if (cds.mtx && service._isExtended) {
29
- edmx = await _getMetadata4Tenant(tenant, locale, service)
30
- }
31
-
32
- if (!edmx) {
33
- edmx = cds.localize(
34
- service.model,
35
- locale,
36
- // REVISIT: we could cache this in a weak map
37
- cds.compile.to.edmx(service.model, { service: service.definition.name })
38
- )
39
- }
40
-
22
+ let edmx = cds.localize(
23
+ service.model,
24
+ locale,
25
+ // REVISIT: we could cache this in model._cached
26
+ cds.compile.to.edmx(service.model, { service: service.definition.name })
27
+ )
41
28
  return next(null, toODataResult(edmx))
42
29
  } catch (e) {
43
30
  if (LOG._error) {
@@ -484,10 +484,7 @@ const read = service => {
484
484
  } catch (e) {
485
485
  err = e
486
486
 
487
- if (changeset) {
488
- // for passing into rollback
489
- odataReq.getBatchApplicationData().errors[changeset].push({ error: e, req })
490
- } else {
487
+ if (!changeset) {
491
488
  // REVISIT: rollback needed if an error occurred before commit attempted -> how to distinguish?
492
489
  await tx.rollback(e).catch(() => {})
493
490
  }
@@ -6,7 +6,7 @@ module.exports = srv => {
6
6
  const requires = getRequiresAsArray(srv.definition)
7
7
  const restricted = isRestricted(srv)
8
8
 
9
- return (odataReq, odataRes, next) => {
9
+ return function ODataRequestHandler(odataReq, odataRes, next) {
10
10
  const req = odataReq.getBatchApplicationData()
11
11
  ? odataReq.getBatchApplicationData().req
12
12
  : odataReq.getIncomingRequest()
@@ -51,7 +51,7 @@ module.exports = srv => {
51
51
  })
52
52
  }
53
53
 
54
- odataReq.setApplicationData({ req })
54
+ odataReq.setApplicationData({ req, res })
55
55
  }
56
56
 
57
57
  next()
@@ -149,12 +149,13 @@ const update = service => {
149
149
  // try UPDATE and, on 404 error, try CREATE
150
150
  ;[result, req] = await _updateThenCreate(req, odataReq, odataRes, tx)
151
151
 
152
+ if (!primitive && req._.readAfterWrite) {
153
+ // REVISIT:
154
+ // Performance: For `isReturnMinimal` it's enough to just read the etag.
155
+ // Note: Without read access, one cannot return the etag.
156
+ result = await readAfterWrite(req, service, { operation: { result } })
157
+ }
152
158
  if (!isReturnMinimal(req)) {
153
- // REVISIT: find better solution
154
- if (!primitive && req._.readAfterWrite) {
155
- result = await readAfterWrite(req, service)
156
- }
157
-
158
159
  postProcess(req, odataRes, service, result, previousResult)
159
160
  } else {
160
161
  result = postProcessMinimal(req, service, result)
@@ -167,16 +168,13 @@ const update = service => {
167
168
  await tx.commit(result)
168
169
  }
169
170
 
170
- if (isReturnMinimal(req)) {
171
+ if (isReturnMinimal(req) || result === null) {
171
172
  odataRes.setStatusCode(204)
172
173
  }
173
174
  } catch (e) {
174
175
  err = e
175
176
 
176
- if (changeset) {
177
- // for passing into rollback
178
- odataReq.getBatchApplicationData().errors[changeset].push({ error: e, req })
179
- } else {
177
+ if (!changeset) {
180
178
  // REVISIT: rollback needed if an error occurred before commit attempted -> how to distinguish?
181
179
  await tx.rollback(e).catch(() => {})
182
180
  }
@@ -1,7 +1,41 @@
1
- const Dispatcher = require('./Dispatcher')
1
+ const { alias2ref } = require('../../../common/utils/csn') // REVISIT: eliminate that
2
+ const cds = require('../../../cds')
3
+ const OData = require('./OData')
2
4
 
3
- const to = service => {
4
- return new Dispatcher(service).getService()
5
+ /**
6
+ * This is the express handler for a specific OData endpoint.
7
+ * Note: the same service can be served at different endpoints.
8
+ */
9
+ module.exports = srv => {
10
+ const okra = new OkraAdapter(srv)
11
+ return okra.process.bind(okra)
5
12
  }
6
13
 
7
- module.exports = to
14
+ function OkraAdapter(srv, model = srv.model) {
15
+ const edm = cds.compile.to.edm(model, { service: srv.definition?.name || srv.name })
16
+ alias2ref(srv, edm) // REVISIT: eliminate that -> done again and again -> search for _alias2ref
17
+ return new OData(edm, model, srv.options).addCDSServiceToChannel(srv)
18
+ }
19
+
20
+ //////////////////////////////////////////////////////////////////////////////
21
+ //
22
+ // REVISIT: Move to ExtensibilityService
23
+ //
24
+ if (cds.mtx || cds.requires.extensibility || cds.requires.toggles)
25
+ module.exports = srv => {
26
+ const id = `${++unique} - ${srv.path}` // REVISIT: this is to allow running multiple express apps serving same endpoints, as done by some questionable tests
27
+ return function ODataAdapter(req, res) {
28
+ const model = cds.context?.model || srv.model
29
+ if (!model._cached) Object.defineProperty(model, '_cached', { value: {} })
30
+
31
+ // Note: cache is attached to model cache so they get disposed when models are evicted from cache
32
+ let adapters = model._cached._odata_adapters || (model._cached._odata_adapters = {})
33
+ let okra = adapters[id]
34
+ if (!okra) {
35
+ const _srv = { __proto__: srv, _real_srv: srv, model } // REVISIT: we need to do that better in new adapters
36
+ okra = adapters[id] = new OkraAdapter(_srv, model)
37
+ }
38
+ return okra.process(req, res)
39
+ }
40
+ }
41
+ let unique = 0
@@ -16,9 +16,7 @@ const _keysOf = (row, target) => {
16
16
  if (!keyElements.length) return
17
17
  const keys = {}
18
18
  for (const key of keyElements) {
19
- if (key._isAssociationStrict || key.name in DRAFT_COLUMNS_MAP) {
20
- continue
21
- }
19
+ if (key._isAssociationStrict) continue
22
20
  keys[key.name] = key.elements ? { val: JSON.stringify(row[key.name]) } : row[key.name]
23
21
  }
24
22
  return keys
@@ -111,10 +109,8 @@ const _getColumns = (target, data, prefix = []) => {
111
109
  * (depth determined by req.data)
112
110
  */
113
111
  const getDeepSelect = req => {
114
- // REVISIT: Why do we do such expensive deep reads after write at all ???
115
- const { target, data } = req
112
+ let { target, data } = req
116
113
  const columns = _getColumns(target, data)
117
-
118
114
  return getSimpleSelectCQN(target, data, columns)
119
115
  }
120
116
 
@@ -1,3 +1,4 @@
1
+ const { removeDraftUUIDIfNecessary } = require('../../../../fiori/utils/handler')
1
2
  const cds = require('../../../../cds')
2
3
  const {
3
4
  Request,
@@ -8,7 +9,7 @@ const { normalizeError } = require('../../../../common/error/frontend')
8
9
 
9
10
  const { getDeepSelect, getSimpleSelectCQN } = require('./handlerUtils')
10
11
  const { hasDeepUpdate } = require('../../../../common/composition/update')
11
- const { WRITE_EVENTS, CDS_EVENTS, DRAFT_MOD_EVENTS } = require('../../../../common/constants/events')
12
+ const { WRITE_EVENTS, CDS_EVENTS } = require('../../../../common/constants/events')
12
13
 
13
14
  const setLocationHeader = (req, { model }) => {
14
15
  const { odataRes } = req._
@@ -21,10 +22,8 @@ const _isNoAccessError = e => Number(e.code) === 403 || Number(e.code) === 401
21
22
  const _isNotFoundError = e => Number(e.code) === 404
22
23
  const _isEntityNotReadableError = e => Number(e.code) === 405
23
24
 
24
- const _handleReadError = (err, req) => {
25
+ const _handleReadError = err => {
25
26
  if (!(_isNoAccessError(err) || _isEntityNotReadableError(err) || _isNotFoundError(err))) throw err
26
- const log = Object.assign(err, { level: 'ERROR', message: normalizeError(err, req).error.message })
27
- process.env.NODE_ENV !== 'production' && LOG._warn && LOG.warn(log)
28
27
  }
29
28
 
30
29
  const _getOperationQueryColumns = urlQueryOptions => {
@@ -38,7 +37,7 @@ const _getOperationQueryColumns = urlQueryOptions => {
38
37
  return columns
39
38
  }
40
39
 
41
- const _isDraftAction = req => req.event in DRAFT_MOD_EVENTS
40
+ const _isDraftAction = req => req.event in { draftActivate: 1, EDIT: 1, draftPrepare: 1 }
42
41
  const _isActionOrFunction = req => !(req.event in CDS_EVENTS) || _isDraftAction(req)
43
42
  const _isWriteWithResponse = req => req.event in WRITE_EVENTS && !(req.event in { CANCEL: 1, DELETE: 1 })
44
43
 
@@ -49,6 +48,9 @@ const readAfterWrite = async (req, srv, { operation, isBefore } = { isBefore: fa
49
48
  const { result, returnType } = operation
50
49
  query = getSimpleSelectCQN(returnType, result, _getOperationQueryColumns(req._queryOptions))
51
50
  if (_isDraftAction(req)) query.where({ IsActiveEntity: req.event === 'draftActivate' })
51
+ } else if (req.event === 'NEW' || req.event === 'PATCH') {
52
+ const { result } = operation
53
+ query = getSimpleSelectCQN(req.target, result)
52
54
  } else if (req.event === 'UPDATE' && !hasDeepUpdate(srv.model, req.query)) {
53
55
  query = Array.isArray(req.data) ? SELECT.from(req.query.UPDATE.entity) : SELECT.one(req.query.UPDATE.entity)
54
56
  } else {
@@ -60,6 +62,7 @@ const readAfterWrite = async (req, srv, { operation, isBefore } = { isBefore: fa
60
62
  try {
61
63
  const _req = new Request({ query, event: 'READ', _: req._ })
62
64
  result = await srv.dispatch(_req)
65
+ if (result && req.target._isDraftEnabled) removeDraftUUIDIfNecessary(req)(result)
63
66
  if (result === null && !isBefore && (_isWriteWithResponse(req) || _isDraftAction(req))) {
64
67
  // > something must be written and no READ error <=> @restrict or static where
65
68
  _req.reject({
@@ -18,6 +18,10 @@ module.exports = class Differ {
18
18
  _createSelectColumnsForDelete(entity) {
19
19
  const columns = []
20
20
  for (const element of Object.values(entity.elements)) {
21
+ // Don't take into account virtual or computed properties to make the diff result
22
+ // consistent with the ones for UPDATE/CREATE (where we don't have access to that
23
+ // information).
24
+ if (!element.key && (element.virtual || element['@Core.Computed'])) continue
21
25
  if (element.isComposition) {
22
26
  if (element._target._hasPersistenceSkip) continue
23
27
  columns.push({
@@ -9,35 +9,7 @@ const getFeatureNotSupportedError = message => {
9
9
  return getError(501, `Feature is not supported: ${message}`)
10
10
  }
11
11
 
12
- const getAuditLogNotWrittenError = (rootCauseError, phase, event) => {
13
- const errorMessage =
14
- !phase || event === 'READ' ? 'Audit log could not be written' : `Audit log could not be written ${phase}`
15
- const error = new Error(errorMessage)
16
- error.rootCause = rootCauseError
17
- return error
18
- }
19
-
20
- const hasBeenCalledError = (method, query) => {
21
- return new Error(`Method ${method} has been called before. Invalid CQN: ${JSON.stringify(query)}`)
22
- }
23
-
24
- const unexpectedFunctionCallError = (functionName, expectedFunction) => {
25
- return new Error(`Cannot build CQN object. Invalid call of "${functionName}" before "${expectedFunction}"`)
26
- }
27
-
28
- const invalidFunctionArgumentError = (statement, arg) => {
29
- const details = JSON.stringify(arg, (key, value) => (value === undefined ? '__undefined__' : value)).replace(
30
- /"__undefined__"/g,
31
- 'undefined'
32
- )
33
- return new Error(`Cannot build ${statement} statement. Invalid data provided: ${details}`)
34
- }
35
-
36
12
  module.exports = {
37
13
  getModelNotDefinedError,
38
- getFeatureNotSupportedError,
39
- getAuditLogNotWrittenError,
40
- hasBeenCalledError,
41
- unexpectedFunctionCallError,
42
- invalidFunctionArgumentError
14
+ getFeatureNotSupportedError
43
15
  }
@@ -2,7 +2,6 @@ const MOD_EVENTS = { UPDATE: 1, DELETE: 1, EDIT: 1 }
2
2
  const WRITE_EVENTS = Object.assign({ CREATE: 1, NEW: 1, PATCH: 1, CANCEL: 1 }, MOD_EVENTS)
3
3
  const CRUD_EVENTS = Object.assign({ READ: 1 }, WRITE_EVENTS)
4
4
  const DRAFT_EVENTS = { PATCH: 1, CANCEL: 1, draftActivate: 1, draftPrepare: 1 }
5
- const DRAFT_MOD_EVENTS = { draftActivate: 1, EDIT: 1 }
6
5
  const CDS_EVENTS = Object.assign({}, CRUD_EVENTS, DRAFT_EVENTS)
7
6
 
8
7
  module.exports = {
@@ -10,6 +9,5 @@ module.exports = {
10
9
  WRITE_EVENTS,
11
10
  CRUD_EVENTS,
12
11
  DRAFT_EVENTS,
13
- CDS_EVENTS,
14
- DRAFT_MOD_EVENTS
12
+ CDS_EVENTS
15
13
  }
@@ -74,8 +74,9 @@ ENTITY_IS_NOT_CRUD=Entity "{0}" is not {1}
74
74
  ENTITY_IS_NOT_CRUD_VIA_NAVIGATION=Entity "{0}" is not {1} via navigation "{2}"
75
75
  ENTITY_IS_AUTOEXPOSED=Entity "{0}" is not explicitly exposed as part of the service
76
76
  EXPAND_IS_RESTRICTED=Navigation property "{0}" is not allowed for expand operation
77
- EXPAND_COUNT_UNSUPPORTED="$count" is not supported for expand operation
77
+ EXPAND_COUNT_UNSUPPORTED="/$count" is not supported for expand operation
78
78
  ORDERBY_LAMBDA_UNSUPPORTED="$orderby" does not support lambda
79
+ EXPAND_APPLY_UNSUPPORTED="$apply" is not supported for expand operation
79
80
 
80
81
  # rest protocol adapter
81
82
  INVALID_RESOURCE="{0}" is not a valid resource
@@ -4,21 +4,16 @@ const _statisticsRequested = req =>
4
4
  (req.query && req.query['sap-statistics'] === 'true') ||
5
5
  (req.headers && req.headers['sap-statistics'] === 'true' && (!req.query || !req.query['sap-statistics']))
6
6
 
7
- module.exports = app => {
8
- if (app._perf_measured) return
9
- else app._perf_measured = true
7
+ module.exports = function sap_statistics(req, res, next) {
8
+ if (!_statisticsRequested(req)) return next()
10
9
 
11
- app.use((req, res, next) => {
12
- if (!_statisticsRequested(req)) return next()
10
+ const t0 = performance.now()
11
+ const { writeHead } = res
12
+ res.writeHead = function (...args) {
13
+ const total = Number((performance.now() - t0) / 1000).toFixed(2)
14
+ if (res.statusCode < 400) res.setHeader('sap-statistics', `total=${total}`)
15
+ writeHead.call(this, ...args)
16
+ }
13
17
 
14
- const t0 = performance.now()
15
- const { writeHead } = res
16
- res.writeHead = function (...args) {
17
- const total = Number((performance.now() - t0) / 1000).toFixed(2)
18
- if (res.statusCode < 400) res.setHeader('sap-statistics', `total=${total}`)
19
- writeHead.call(this, ...args)
20
- }
21
-
22
- next()
23
- })
18
+ next()
24
19
  }
@@ -698,7 +698,6 @@ const _convertToOneEqNullInFilter = (query, target) => {
698
698
  }
699
699
  }
700
700
  }
701
-
702
701
  // eslint-disable-next-line complexity
703
702
  const _convertSelect = (query, model, _options) => {
704
703
  const _4db = _options.service instanceof cds.DatabaseService
@@ -1,25 +1,28 @@
1
1
  const { ensureNoDraftsSuffix } = require('../../common/utils/draft')
2
2
 
3
- const traverseFroms = (cqn, cb) => {
3
+ const traverseFroms = (cqn, cb, aliasForSet) => {
4
4
  while (cqn.SELECT) cqn = cqn.SELECT.from
5
5
 
6
6
  // Do the most likely first -> {ref}
7
7
  if (cqn.ref) {
8
- return cb(cqn)
8
+ return cb(cqn, aliasForSet)
9
9
  }
10
10
 
11
11
  if (cqn.SET) {
12
- return cqn.SET.args.map(a => traverseFroms(a, cb))
12
+ // if a union has an alias, we should use it for the columns we get out of the union
13
+ return cqn.SET.args.map(a => traverseFroms(a, cb, cqn.as))
13
14
  }
14
15
 
15
16
  if (cqn.join) {
16
- return cqn.args.map(a => traverseFroms(a, cb))
17
+ return cqn.args.map(a => traverseFroms(a, cb, aliasForSet))
17
18
  }
18
19
  }
19
20
 
20
21
  const getEntityNameFromCQN = cqn => {
21
22
  const res = []
22
- traverseFroms(cqn, from => res.push({ entityName: from.ref[0].id || from.ref[0], alias: from.as }))
23
+ traverseFroms(cqn, (from, aliasForSet) =>
24
+ res.push({ entityName: from.ref[0].id || from.ref[0], alias: aliasForSet || from.as })
25
+ )
23
26
  return res.length === 1 ? res[0] : res.find(n => n.entityName !== 'DRAFT.DraftAdministrativeData') || {}
24
27
  }
25
28
 
@@ -134,7 +134,7 @@ const getCache = (anything, cache, newCacheFn) => {
134
134
  }
135
135
 
136
136
  module.exports = (usecase, tx, target, ...args) => {
137
- // get model first as it may be added to tx (cf. "_ensureModel")
137
+ // get model first as it may be added to tx (cf. "_ensureModel") // REVISIT: _ensureModel is gone
138
138
  const model = tx.model
139
139
  if (!model) return
140
140
 
@@ -24,28 +24,16 @@ class DatabaseService extends cds.Service {
24
24
  this[`_${each}`] = generic[each]
25
25
  }
26
26
 
27
- // REVISIT: ensures tenant-aware this.model if this is a transaction -> this should be fixed in mtx integration, not here
28
- this._ensureModel = function (req) {
29
- if (this.context) {
30
- // if the tx was initiated in messaging, then this.context._model is not unfolded
31
- // -> use this.context._model._4odata if present
32
- const { _model } = this.context
33
- if (_model) this.model = _model._4odata || _model
34
- else this.model = req._model
35
- }
36
- }
37
- this._ensureModel._initial = true
38
-
39
27
  // REVISIT: how to generic handler registration?
40
28
  }
41
29
 
42
30
  /** Database services don't support custom-defined operations */
43
- operations() {
31
+ get operations() {
44
32
  return []
45
33
  }
46
34
 
47
35
  /** Database services don't support custom-defined events */
48
- events() {
36
+ get events() {
49
37
  return []
50
38
  }
51
39
 
@@ -12,6 +12,8 @@ const { DRAFT_COLUMNS_MAP } = require('../../common/constants/draft')
12
12
 
13
13
  const { filterKeys } = require('../../fiori/utils/handler')
14
14
 
15
+ const getError = require('../../common/error')
16
+
15
17
  // Symbols are used to add extra information in response structure
16
18
  const GET_KEY_VALUE = Symbol.for('sap.cds.getKeyValue')
17
19
  const TO_MANY = Symbol.for('sap.cds.toMany')
@@ -94,6 +96,16 @@ class JoinCQNFromExpanded {
94
96
  return this._isDraft
95
97
  }
96
98
 
99
+ // There can be a limit/offset for the target entity.
100
+ // The current expand implementation applys a `DISTINCT` on
101
+ // `filterExpand`, which changes the sorting in absence of `ORDER BY`.
102
+ // Therefore, add an implicit `ORDER BY` in those cases.
103
+ _addImplicitOrderBy(cqn, entity, alias) {
104
+ if (cqn.orderBy || !cqn.limit || !entity) return // not needed
105
+ const orderByColumns = cqn.groupBy || getAllKeys(entity).map(key => ({ ref: [alias, key] }))
106
+ cqn.orderBy = orderByColumns
107
+ }
108
+
97
109
  /**
98
110
  * Build first level of expanding regarding to many and all to one if not part of a nested to many expand.
99
111
  *
@@ -123,6 +135,7 @@ class JoinCQNFromExpanded {
123
135
  })
124
136
  // expand to one
125
137
  const entity = this._csn.definitions[joinArgs[0].SELECT.from.SET.args[1].SELECT.from.ref[0]]
138
+ this._addImplicitOrderBy(readToOneCQN, entity, tableAlias)
126
139
  const givenColumns = readToOneCQN.columns
127
140
  readToOneCQN.columns = []
128
141
  this._expandedToFlat({ entity, givenColumns, readToOneCQN, tableAlias, toManyTree, defaultLanguage })
@@ -130,7 +143,7 @@ class JoinCQNFromExpanded {
130
143
  const table = unionTable || this._getRef(SELECT).table
131
144
  const isDraftTree = this._isDraftTree(table)
132
145
  const entity = this._getEntityForTable(table)
133
-
146
+ this._addImplicitOrderBy(readToOneCQN, entity, tableAlias)
134
147
  if (unionTable) readToOneCQN[IS_UNION_DRAFT] = true
135
148
 
136
149
  readToOneCQN[IS_ACTIVE] = isDraftTree ? this._isDraftTargetActive(table) : true
@@ -455,14 +468,10 @@ class JoinCQNFromExpanded {
455
468
 
456
469
  if (element.ref) {
457
470
  element.ref[1] = Object.assign({}, element.ref[1])
458
- element.ref[1].args = element.ref[1].args.map(arg => {
459
- return this._mapArg(arg, cqn, tableAlias)
460
- })
471
+ element.ref[1].args = element.ref[1].args.map(arg => this._mapArg(arg, cqn, tableAlias))
461
472
  } else {
462
473
  element.args = element.args.slice(0)
463
- element.args = element.args.map(arg => {
464
- return this._mapArg(arg, cqn, tableAlias)
465
- })
474
+ element.args = element.args.map(arg => this._mapArg(arg, cqn, tableAlias))
466
475
  }
467
476
  }
468
477
 
@@ -684,6 +693,17 @@ class JoinCQNFromExpanded {
684
693
  c => !expandedEntity.keys[c].isAssociation && !(c in DRAFT_COLUMNS_MAP)
685
694
  )
686
695
  const user = (cds.context && cds.context.user && cds.context.user.id) || 'anonymous'
696
+
697
+ const assoc = entity.associations[column.ref[0]]
698
+ if (assoc.is2one && assoc.on) {
699
+ const onCond = expandedEntity._relations[assoc.name].join('target', 'source')
700
+ const xpr = onCond[0].xpr
701
+ const fks = (xpr && xpr.filter(e => e.ref && e.ref[0] === 'target').map(e => e.ref[1])) || []
702
+ for (const k of fks) {
703
+ if (!cols.includes(k)) cols.push(k)
704
+ }
705
+ }
706
+
687
707
  const unionFrom = getCQNUnionFrom(cols, expandedEntity.name, expandedEntity.name + '.drafts', ks, user)
688
708
  readToOneCQN.from.args[1] = {
689
709
  SELECT: {
@@ -1162,7 +1182,7 @@ class JoinCQNFromExpanded {
1162
1182
  cqn.orderBy = this._copyOrderBy(column.orderBy, tableAlias, expandedEntity)
1163
1183
  }
1164
1184
 
1165
- if (column.limit) cqn.limit = column.limit
1185
+ if (column.limit) throw getError(501, 'Pagination is not supported in expand')
1166
1186
 
1167
1187
  cqn = this._adaptWhereOrderBy(cqn, tableAlias)
1168
1188
 
@@ -1227,23 +1247,6 @@ class JoinCQNFromExpanded {
1227
1247
  return assoc.target + '_drafts'
1228
1248
  }
1229
1249
 
1230
- _getLimitInSelect(cqn, columns, limit, orderBy, expandedEntity) {
1231
- const select = {
1232
- SELECT: {
1233
- columns: this._copyColumns(columns, 'limitFilter'),
1234
- from: { ref: [cqn.from.args[0].ref[0]], as: 'limitFilter' },
1235
- where: this._convertOnToWhere(cqn.from.on, cqn.from.args[0].as, 'limitFilter'),
1236
- limit: limit
1237
- }
1238
- }
1239
-
1240
- if (orderBy) {
1241
- select.SELECT.orderBy = this._copyOrderBy(orderBy, 'limitFilter', expandedEntity)
1242
- }
1243
-
1244
- return select
1245
- }
1246
-
1247
1250
  _isPathExpressionToOne(ref, entity) {
1248
1251
  const ref0 = ref[0]
1249
1252
  const el = entity.elements[ref0]