@sap/cds 6.0.3 → 6.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/CHANGELOG.md +165 -18
  2. package/apis/cds.d.ts +11 -7
  3. package/apis/log.d.ts +46 -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 +14 -34
  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 +12 -9
  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 +60 -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/deploy/to-hana/cfUtil.js +7 -2
  19. package/bin/deploy/to-hana/hana.js +20 -25
  20. package/bin/deploy/to-hana/hdiDeployUtil.js +13 -2
  21. package/bin/serve.js +7 -4
  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} +9 -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 +614 -0
  34. package/lib/index.js +19 -16
  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/Query.js +9 -3
  38. package/lib/ql/SELECT.js +2 -2
  39. package/lib/ql/{index.js → cds-ql.js} +0 -9
  40. package/lib/req/context.js +49 -17
  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 +1 -1
  47. package/lib/{serve/Service-api.js → srv/srv-api.js} +22 -6
  48. package/lib/{serve/Service-dispatch.js → srv/srv-dispatch.js} +13 -8
  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 +207 -0
  52. package/lib/{serve/Transaction.js → srv/srv-tx.js} +57 -40
  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 -145
  56. package/lib/utils/jest.js +43 -0
  57. package/lib/utils/resources/index.js +15 -25
  58. package/lib/utils/resources/tar.js +18 -41
  59. package/libx/_runtime/auth/index.js +14 -11
  60. package/libx/_runtime/cds-services/adapter/odata-v4/OData.js +7 -19
  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 +1 -4
  63. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/create.js +1 -4
  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 +1 -1
  69. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +1 -4
  70. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/utils/PrimitiveValueDecoder.js +0 -2
  71. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/deserializer/DeserializerFactory.js +3 -1
  72. package/libx/_runtime/cds-services/adapter/odata-v4/to.js +38 -4
  73. package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +1 -3
  74. package/libx/_runtime/cds-services/services/utils/differ.js +4 -0
  75. package/libx/_runtime/cds-services/util/errors.js +1 -29
  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/binary.js +3 -4
  79. package/libx/_runtime/common/utils/cqn2cqn4sql.js +0 -1
  80. package/libx/_runtime/common/utils/entityFromCqn.js +8 -5
  81. package/libx/_runtime/common/utils/resolveView.js +1 -1
  82. package/libx/_runtime/common/utils/template.js +1 -1
  83. package/libx/_runtime/db/Service.js +2 -14
  84. package/libx/_runtime/db/expand/expandCQNToJoin.js +28 -25
  85. package/libx/_runtime/db/expand/rawToExpanded.js +7 -6
  86. package/libx/_runtime/db/generic/input.js +8 -1
  87. package/libx/_runtime/db/sql-builder/SelectBuilder.js +37 -18
  88. package/libx/_runtime/extensibility/activate.js +47 -47
  89. package/libx/_runtime/extensibility/add.js +22 -13
  90. package/libx/_runtime/extensibility/addExtension.js +17 -13
  91. package/libx/_runtime/extensibility/defaults.js +25 -30
  92. package/libx/_runtime/extensibility/handler/transformREAD.js +20 -18
  93. package/libx/_runtime/extensibility/linter/allowlist_checker.js +373 -0
  94. package/libx/_runtime/extensibility/linter/annotations_checker.js +113 -0
  95. package/libx/_runtime/extensibility/linter/checker_base.js +20 -0
  96. package/libx/_runtime/extensibility/linter/namespace_checker.js +180 -0
  97. package/libx/_runtime/extensibility/linter.js +32 -0
  98. package/libx/_runtime/extensibility/push.js +77 -20
  99. package/libx/_runtime/extensibility/service.js +29 -12
  100. package/libx/_runtime/extensibility/token.js +56 -0
  101. package/libx/_runtime/extensibility/utils.js +8 -6
  102. package/libx/_runtime/extensibility/validation.js +6 -9
  103. package/libx/_runtime/fiori/generic/new.js +0 -11
  104. package/libx/_runtime/hana/Service.js +0 -1
  105. package/libx/_runtime/hana/conversion.js +12 -1
  106. package/libx/_runtime/hana/customBuilder/CustomFunctionBuilder.js +4 -3
  107. package/libx/_runtime/hana/customBuilder/CustomSelectBuilder.js +5 -0
  108. package/libx/_runtime/hana/pool.js +6 -10
  109. package/libx/_runtime/hana/search2Contains.js +0 -5
  110. package/libx/_runtime/hana/search2cqn4sql.js +1 -0
  111. package/libx/_runtime/messaging/outbox/utils.js +1 -1
  112. package/libx/_runtime/messaging/service.js +11 -6
  113. package/libx/_runtime/remote/utils/data.js +5 -0
  114. package/libx/_runtime/sqlite/Service.js +7 -6
  115. package/libx/_runtime/sqlite/execute.js +41 -28
  116. package/libx/odata/afterburner.js +79 -2
  117. package/libx/odata/cqn2odata.js +9 -7
  118. package/libx/odata/grammar.pegjs +157 -76
  119. package/libx/odata/index.js +9 -3
  120. package/libx/odata/parser.js +1 -1
  121. package/libx/odata/utils.js +39 -5
  122. package/libx/rest/RestAdapter.js +3 -7
  123. package/libx/rest/middleware/delete.js +4 -5
  124. package/libx/rest/middleware/parse.js +3 -2
  125. package/package.json +3 -3
  126. package/server.js +1 -1
  127. package/srv/extensibility-service.cds +6 -3
  128. package/srv/model-provider.cds +3 -1
  129. package/srv/model-provider.js +84 -104
  130. package/srv/mtx.js +7 -1
  131. package/libx/_runtime/cds-services/adapter/odata-v4/Dispatcher.js +0 -240
@@ -57,7 +57,7 @@ const _log = (req, challenges) => {
57
57
  LOG.debug(`User "${req.user.id}" request URL`, req.url, '\n', ...challengesLog)
58
58
  }
59
59
 
60
- const _authCallback = (req, res, next, internalError, user, challenges) => {
60
+ const cap_auth_callback = (req, res, next, internalError, user, challenges) => {
61
61
  // An internal error occurs during the authentication process
62
62
  if (internalError) {
63
63
  // REVISIT: What to do? Security log?
@@ -83,7 +83,7 @@ const _authCallback = (req, res, next, internalError, user, challenges) => {
83
83
 
84
84
  const _mountCustomAuth = (srv, app, config) => {
85
85
  const impl = cds.resolve(config.impl)
86
- app.use(srv.path, _require(impl))
86
+ app.use(_require(impl))
87
87
  }
88
88
 
89
89
  const _mountMockAuth = (srv, app, strategy, config) => {
@@ -92,12 +92,12 @@ const _mountMockAuth = (srv, app, strategy, config) => {
92
92
  ? new (require('./strategies/dummy'))()
93
93
  : new (require('./strategies/mock'))(config, `mock_${srv.name}`)
94
94
 
95
- app.use(srv.path, (req, res, next) => {
95
+ app.use(function cap_auth(req, res, next) {
96
96
  let user, challenge
97
97
  impl.success = arg => (user = arg)
98
98
  impl.fail = arg => (challenge = arg)
99
99
  impl.authenticate(req)
100
- _authCallback(req, res, next, undefined, user, [challenge])
100
+ cap_auth_callback(req, res, next, undefined, user, [challenge])
101
101
  })
102
102
  }
103
103
 
@@ -119,10 +119,10 @@ const _mountPassportAuth = (srv, app, strategy, config) => {
119
119
  }
120
120
 
121
121
  // authenticate
122
- app.use(srv.path, passport.initialize())
123
- app.use(srv.path, (req, res, next) => {
122
+ app.use(passport.initialize())
123
+ app.use((req, res, next) => {
124
124
  const options = { session: false, failWithError: true }
125
- const callback = _authCallback.bind(undefined, req, res, next)
125
+ const callback = cap_auth_callback.bind(undefined, req, res, next)
126
126
  passport.authenticate(strategy === 'jwt' ? 'JWT' : strategy, options, callback)(req, res, next)
127
127
  })
128
128
  }
@@ -131,7 +131,9 @@ const _mountPassportAuth = (srv, app, strategy, config) => {
131
131
  * export authentication middleware
132
132
  */
133
133
  // eslint-disable-next-line complexity
134
- module.exports = (srv, app, options) => {
134
+ module.exports = (srv, options = srv.options) => {
135
+ const handlers = [],
136
+ app = { use: h => handlers.push(h) }
135
137
  // NOTE: options.auth is not an official API
136
138
  let config = 'auth' in options ? options.auth : cds.env.requires.auth
137
139
  if (!config) {
@@ -147,7 +149,7 @@ module.exports = (srv, app, options) => {
147
149
  LOG._warn && LOG.warn(`No authentication configured. This is not recommended in production.`)
148
150
  }
149
151
  // no auth wanted > return
150
- return
152
+ return handlers
151
153
  }
152
154
 
153
155
  // cds.env.requires.auth = { kind: 'xsuaa-auth' } was briefly documented on capire -> also support
@@ -181,11 +183,12 @@ module.exports = (srv, app, options) => {
181
183
  (process.env.NODE_ENV === 'production' && config.credentials && config.restrict_all_services)
182
184
  ) {
183
185
  if (!logged) LOG._debug && LOG.debug(`Enforcing authenticated users for all services`)
184
- app.use(srv.path, _enforce_authenticated_user)
186
+ app.use(cap_enforce_login)
185
187
  }
186
188
 
187
189
  // so we don't log the same stuff multiple times
188
190
  logged = true
191
+ return handlers
189
192
  }
190
193
 
191
194
  const _strategy4 = config => {
@@ -196,7 +199,7 @@ const _strategy4 = config => {
196
199
  throw new Error(`Authentication kind "${config.kind}" is not supported`)
197
200
  }
198
201
 
199
- const _enforce_authenticated_user = (req, res, next) => {
202
+ const cap_enforce_login = (req, res, next) => {
200
203
  if (req.user && req.user.is('authenticated-user')) return next() // pass if user is authenticated
201
204
  if (!req.user || req.user._is_anonymous) {
202
205
  if (req.user && req.user._challenges) res.set('WWW-Authenticate', req.user._challenges.join(';'))
@@ -107,8 +107,7 @@ class OData {
107
107
  constructor(edm, csn, options = {}) {
108
108
  this._validateEdm(edm)
109
109
  this._options = options
110
- this._csn = csn
111
- this._createOdataService(edm)
110
+ this._createOdataService(edm, csn)
112
111
  }
113
112
 
114
113
  _validateEdm(edm) {
@@ -118,7 +117,7 @@ class OData {
118
117
  }
119
118
  }
120
119
 
121
- _createOdataService(edm) {
120
+ _createOdataService(edm, csn) {
122
121
  const ServiceFactory = require('./okra/odata-server').ServiceFactory
123
122
 
124
123
  // skip okra's validation in production or implicitly for w4 and x4
@@ -133,7 +132,7 @@ class OData {
133
132
  effective.odata.proxies ||
134
133
  effective.odata.xrefs
135
134
 
136
- this._odataService = ServiceFactory.createService(edm, _config(edm, this._csn, this._options)).trust(isTrusted)
135
+ this._odataService = ServiceFactory.createService(edm, _config(edm, csn, this._options)).trust(isTrusted)
137
136
 
138
137
  // will be added to express app like app.use('/base/path/', service) and odata-v4 wants app.use('/', service) if basePath is set
139
138
  this._odataService.setBasePath('/')
@@ -165,8 +164,7 @@ class OData {
165
164
  req,
166
165
  res
167
166
  } = data
168
- // REVISIT: _model should not be necessary
169
- const tx = (txs[odataContext.id] = cdsService.tx({ user, req, res, _model: cdsService.model }))
167
+ const tx = (txs[odataContext.id] = cdsService.tx({ user, req, res }))
170
168
  cds.context = tx.context
171
169
  // for collecting results and errors
172
170
  data.results = data.results || {}
@@ -183,19 +181,8 @@ class OData {
183
181
  if (errors) {
184
182
  // rollback without errors to not trigger srv.on('error') with array
185
183
  await tx.rollback()
186
- // invoke srv.on('error') for each error and build failedRequests that reflects error modifications
187
- errors = odataContext.applicationData.errors[odataContext.id]
188
- const failedRequests = {}
189
-
190
- for (const e of errors) {
191
- const { error: err, req } = e
192
- for (const each of cdsService._handlers._error) each.handler.call(cdsService, err, req)
193
- const requestId = req._.odataReq.getOdataRequestId()
194
- const { error, statusCode } = normalizeError(err, req)
195
- failedRequests[requestId] = Object.assign(error, { statusCode })
196
- }
197
-
198
- done(new Error(`Atomicity group "${odataContext.id}" failed`), { failedRequests })
184
+
185
+ done()
199
186
  return
200
187
  }
201
188
 
@@ -226,6 +213,7 @@ class OData {
226
213
  this._odataService.use(DATA_DELETE_HANDLER, _delete(cdsService))
227
214
 
228
215
  this._odataService.use(ACTION_EXECUTE_HANDLER, _action(cdsService))
216
+ return this
229
217
  }
230
218
 
231
219
  /**
@@ -163,12 +163,9 @@ class ODataRequest extends cds.Request {
163
163
  */
164
164
  const { user } = req
165
165
 
166
- // REVISIT: _model should not be necessary
167
- const _model = service.model
168
-
169
166
  // REVISIT: public API for query options (express style req.query already in use)?
170
167
  const _queryOptions = odataReq.getQueryOptions()
171
- super({ event, target, data, query, user, method, headers, req, res, _model, _queryOptions })
168
+ super({ event, target, data, query, user, method, headers, req, res, _queryOptions })
172
169
  }
173
170
 
174
171
  /*
@@ -77,10 +77,7 @@ const action = service => {
77
77
  } catch (e) {
78
78
  err = e
79
79
 
80
- if (changeset) {
81
- // for passing into rollback
82
- odataReq.getBatchApplicationData().errors[changeset].push({ error: e, req })
83
- } else {
80
+ if (!changeset) {
84
81
  // REVISIT: rollback needed if an error occurred before commit attempted -> how to distinguish?
85
82
  await tx.rollback(e).catch(() => {})
86
83
  }
@@ -63,10 +63,7 @@ const create = service => {
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()
@@ -174,10 +174,7 @@ const update = service => {
174
174
  } catch (e) {
175
175
  err = e
176
176
 
177
- if (changeset) {
178
- // for passing into rollback
179
- odataReq.getBatchApplicationData().errors[changeset].push({ error: e, req })
180
- } else {
177
+ if (!changeset) {
181
178
  // REVISIT: rollback needed if an error occurred before commit attempted -> how to distinguish?
182
179
  await tx.rollback(e).catch(() => {})
183
180
  }
@@ -68,8 +68,6 @@ const MULTI_LINE_STRING_VALIDATION = new RegExp('^' + SRID + 'MultiLineString\\(
68
68
  const MULTI_POLYGON_VALIDATION = new RegExp('^' + SRID + 'MultiPolygon\\((' + MULTI_POLYGON + ')\\)$', 'i')
69
69
  const COLLECTION_VALIDATION = new RegExp('^' + SRID + 'Collection\\((' + MULTI_GEO_LITERAL + ')\\)$', 'i')
70
70
 
71
- const BASE64 = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{4}|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}={2})$/
72
-
73
71
  function _getBase64(val) {
74
72
  if (isInvalidBase64string(val)) return
75
73
  // convert url-safe to standard base64
@@ -40,8 +40,10 @@ class DeserializerFactory {
40
40
  let additionalInformation = { hasDelta: false }
41
41
  const deserializer = new ResourceJsonDeserializer(edm, jsonContentTypeInfo)
42
42
  return (edmObject, value) => {
43
+ const body = deserializer[name](edmObject, value, expand, additionalInformation)
44
+
43
45
  return {
44
- body: deserializer[name](edmObject, value, expand, additionalInformation),
46
+ body,
45
47
  expand,
46
48
  additionalInformation
47
49
  }
@@ -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
@@ -22,10 +22,8 @@ const _isNoAccessError = e => Number(e.code) === 403 || Number(e.code) === 401
22
22
  const _isNotFoundError = e => Number(e.code) === 404
23
23
  const _isEntityNotReadableError = e => Number(e.code) === 405
24
24
 
25
- const _handleReadError = (err, req) => {
25
+ const _handleReadError = err => {
26
26
  if (!(_isNoAccessError(err) || _isEntityNotReadableError(err) || _isNotFoundError(err))) throw err
27
- const log = Object.assign(err, { level: 'ERROR', message: normalizeError(err, req).error.message })
28
- process.env.NODE_ENV !== 'production' && LOG._warn && LOG.warn(log)
29
27
  }
30
28
 
31
29
  const _getOperationQueryColumns = urlQueryOptions => {
@@ -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
  }
@@ -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
  }
@@ -1,8 +1,6 @@
1
1
  const getTemplate = require('./template')
2
2
  const templateProcessor = require('./templateProcessor')
3
3
 
4
- const BASE64 = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{4}|[A-Za-z0-9+/]{3}={0,1}|[A-Za-z0-9+/]{2}={0,2})$/
5
-
6
4
  // convert the standard base64 encoding to the URL-safe variant
7
5
  const toBase64url = value =>
8
6
  (Buffer.isBuffer(value) ? value.toString('base64') : value).replace(/\//g, '_').replace(/\+/g, '-')
@@ -17,12 +15,13 @@ const isInvalidBase64string = value => {
17
15
  if (Buffer.isBuffer(value)) return // ok
18
16
 
19
17
  // convert to standard base64 string; let it crash if typeof value !== 'string'
20
- const base64 = value.replace(/_/g, '/').replace(/-/g, '+')
18
+ const base64value = value.replace(/_/g, '/').replace(/-/g, '+')
21
19
  const normalized = normalizeBase64string(value)
22
20
 
23
21
  // example of invalid base64 string --> 'WTGTdDsD/k21LnFRb+uNcAi=' <-- '...i=' must be '...g='
24
22
  // see https://datatracker.ietf.org/doc/html/rfc4648#section-4
25
- return !base64.match(BASE64) || base64.replace(/=/g, '') !== normalized.replace(/=/g, '')
23
+ if (base64value.replace(/=/g, '') !== normalized.replace(/=/g, '')) return true
24
+ return base64value.length > normalized.length
26
25
  }
27
26
 
28
27
  const _picker = element => {
@@ -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
 
@@ -612,7 +612,7 @@ const _newQuery = (query, event, model, service) => {
612
612
  }[event]
613
613
  const newQuery = Object.create(query)
614
614
  const transitions = _entityTransitionsForTarget(query[event][_prop], model, service)
615
- newQuery[event] = (transitions[0] && _func(newQuery, transitions, service)) || { ...query[event] }
615
+ newQuery[event] = (transitions?.[0] && _func(newQuery, transitions, service)) || { ...query[event] }
616
616
  return newQuery
617
617
  }
618
618
 
@@ -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