@sap/cds 6.4.1 → 6.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/CHANGELOG.md +79 -6
  2. package/README.md +5 -0
  3. package/apis/cqn.d.ts +14 -3
  4. package/apis/ql.d.ts +8 -8
  5. package/apis/services.d.ts +37 -65
  6. package/apis/test.d.ts +7 -0
  7. package/bin/build/buildTaskEngine.js +9 -14
  8. package/bin/build/buildTaskFactory.js +1 -1
  9. package/bin/build/buildTaskHandler.js +3 -14
  10. package/bin/build/index.js +8 -2
  11. package/bin/build/provider/buildTaskProviderInternal.js +18 -13
  12. package/bin/build/provider/fiori/index.js +5 -10
  13. package/bin/build/provider/hana/2migration.js +11 -2
  14. package/bin/build/provider/hana/index.js +17 -14
  15. package/bin/build/provider/hana/template/.hdiconfig-hanacloud +137 -0
  16. package/bin/build/provider/hana/template/package.json +3 -0
  17. package/bin/build/provider/mtx/resourcesTarBuilder.js +12 -3
  18. package/bin/build/provider/mtx-extension/index.js +57 -37
  19. package/bin/build/provider/mtx-sidecar/index.js +1 -1
  20. package/bin/build/util.js +18 -1
  21. package/bin/cds.js +1 -5
  22. package/bin/deploy/to-hana/hana.js +10 -3
  23. package/bin/serve.js +36 -20
  24. package/common.cds +7 -0
  25. package/lib/auth/jwt-auth.js +8 -7
  26. package/lib/compile/for/lean_drafts.js +55 -6
  27. package/lib/compile/minify.js +3 -3
  28. package/lib/dbs/cds-deploy.js +18 -17
  29. package/lib/env/cds-requires.js +1 -1
  30. package/lib/env/defaults.js +5 -1
  31. package/lib/env/schemas/cds-rc.json +74 -3
  32. package/lib/index.js +4 -2
  33. package/lib/lazy.js +6 -8
  34. package/lib/log/cds-error.js +2 -2
  35. package/lib/ql/Whereable.js +22 -11
  36. package/lib/ql/cds-ql.js +1 -1
  37. package/lib/req/cds-context.js +3 -3
  38. package/lib/req/response.js +8 -3
  39. package/lib/req/user.js +12 -2
  40. package/lib/srv/bindings.js +1 -2
  41. package/lib/srv/cds-serve.js +2 -1
  42. package/lib/srv/middlewares/trace.js +31 -15
  43. package/lib/srv/protocols/odata-v2-proxy.js +8 -8
  44. package/lib/srv/srv-handlers.js +26 -7
  45. package/lib/srv/srv-methods.js +2 -2
  46. package/lib/srv/srv-models.js +8 -3
  47. package/lib/utils/cds-test.js +7 -5
  48. package/lib/utils/cds-utils.js +3 -1
  49. package/lib/utils/tar.js +6 -3
  50. package/libx/_runtime/auth/strategies/JWT.js +1 -0
  51. package/libx/_runtime/auth/strategies/ias-auth.js +3 -2
  52. package/libx/_runtime/auth/strategies/mock.js +12 -1
  53. package/libx/_runtime/auth/strategies/xssecUtils.js +7 -8
  54. package/libx/_runtime/auth/strategies/xsuaa.js +1 -0
  55. package/libx/_runtime/cds-services/adapter/odata-v4/OData.js +6 -2
  56. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +1 -2
  57. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +26 -1
  58. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +8 -0
  59. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/ExpressionToCQN.js +1 -1
  60. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/readToCQN.js +11 -2
  61. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/utils/PrimitiveValueDecoder.js +8 -8
  62. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/utils/ValueConverter.js +1 -1
  63. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/validator/ValueValidator.js +14 -14
  64. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/deserializer/DeserializerFactory.js +1 -0
  65. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/serializer/ResourceJsonSerializer.js +3 -0
  66. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/utils/UriHelper.js +2 -1
  67. package/libx/_runtime/cds-services/adapter/odata-v4/utils/metaInfo.js +3 -2
  68. package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +7 -0
  69. package/libx/_runtime/cds-services/adapter/odata-v4/utils/stream.js +0 -3
  70. package/libx/_runtime/cds-services/services/Service.js +11 -19
  71. package/libx/_runtime/cds-services/services/utils/columns.js +42 -40
  72. package/libx/_runtime/cds-services/util/assert.js +7 -1
  73. package/libx/_runtime/common/code-ext/WorkerReq.js +81 -0
  74. package/libx/_runtime/common/code-ext/config.js +13 -0
  75. package/libx/_runtime/common/code-ext/execute.js +113 -0
  76. package/libx/_runtime/common/code-ext/handlers.js +49 -0
  77. package/libx/_runtime/common/code-ext/worker.js +40 -0
  78. package/libx/_runtime/common/code-ext/workerQuery.js +45 -0
  79. package/libx/_runtime/common/code-ext/workerQueryExecutor.js +36 -0
  80. package/libx/_runtime/common/composition/data.js +5 -2
  81. package/libx/_runtime/common/composition/tree.js +2 -0
  82. package/libx/_runtime/common/generic/auth/restrict.js +1 -1
  83. package/libx/_runtime/common/generic/crud.js +4 -0
  84. package/libx/_runtime/common/generic/etag.js +3 -1
  85. package/libx/_runtime/common/generic/input.js +12 -14
  86. package/libx/_runtime/common/i18n/index.js +1 -1
  87. package/libx/_runtime/common/utils/cqn2cqn4sql.js +47 -22
  88. package/libx/_runtime/common/utils/path.js +5 -26
  89. package/libx/_runtime/common/utils/search2cqn4sql.js +16 -9
  90. package/libx/_runtime/common/utils/templateProcessorPathSerializer.js +19 -13
  91. package/libx/_runtime/db/data-conversion/post-processing.js +1 -1
  92. package/libx/_runtime/db/expand/expandCQNToJoin.js +7 -4
  93. package/libx/_runtime/db/expand/rawToExpanded.js +3 -2
  94. package/libx/_runtime/db/generic/input.js +2 -2
  95. package/libx/_runtime/db/generic/integrity.js +1 -0
  96. package/libx/_runtime/db/generic/virtual.js +1 -0
  97. package/libx/_runtime/db/query/read.js +3 -2
  98. package/libx/_runtime/db/utils/localized.js +1 -1
  99. package/libx/_runtime/fiori/generic/activate.js +7 -1
  100. package/libx/_runtime/fiori/generic/before.js +9 -1
  101. package/libx/_runtime/fiori/generic/edit.js +8 -1
  102. package/libx/_runtime/fiori/generic/new.js +2 -0
  103. package/libx/_runtime/fiori/generic/patch.js +2 -0
  104. package/libx/_runtime/fiori/generic/prepare.js +2 -0
  105. package/libx/_runtime/fiori/generic/read.js +16 -5
  106. package/libx/_runtime/fiori/generic/readOverDraft.js +2 -0
  107. package/libx/_runtime/fiori/lean-draft.js +505 -241
  108. package/libx/_runtime/fiori/utils/delete.js +2 -0
  109. package/libx/_runtime/hana/customBuilder/CustomSelectBuilder.js +5 -5
  110. package/libx/_runtime/hana/pool.js +1 -1
  111. package/libx/_runtime/hana/search2cqn4sql.js +51 -51
  112. package/libx/_runtime/messaging/Outbox.js +1 -1
  113. package/libx/_runtime/messaging/enterprise-messaging-utils/getTenantInfo.js +1 -0
  114. package/libx/_runtime/messaging/enterprise-messaging.js +2 -6
  115. package/libx/_runtime/messaging/file-based.js +1 -2
  116. package/libx/_runtime/messaging/outbox/OutboxRunner.js +1 -1
  117. package/libx/_runtime/messaging/outbox/utils.js +1 -1
  118. package/libx/_runtime/messaging/service.js +0 -1
  119. package/libx/_runtime/remote/Service.js +1 -0
  120. package/libx/_runtime/sqlite/convertDraftAdminPathExpression.js +19 -3
  121. package/libx/_runtime/sqlite/customBuilder/CustomExpressionBuilder.js +0 -18
  122. package/libx/_runtime/sqlite/customBuilder/CustomFunctionBuilder.js +0 -18
  123. package/libx/_runtime/sqlite/customBuilder/CustomSelectBuilder.js +0 -24
  124. package/libx/_runtime/sqlite/customBuilder/CustomUpsertBuilder.js +2 -1
  125. package/libx/_runtime/sqlite/customBuilder/index.js +47 -32
  126. package/libx/odata/afterburner.js +23 -8
  127. package/libx/odata/cqn2odata.js +1 -1
  128. package/libx/odata/grammar.pegjs +3 -4
  129. package/libx/odata/index.js +5 -1
  130. package/libx/odata/parseToCqn.js +3 -3
  131. package/libx/odata/parser.js +1 -1
  132. package/libx/odata/utils.js +58 -1
  133. package/libx/rest/middleware/parse.js +26 -4
  134. package/package.json +1 -1
  135. package/server.js +1 -1
  136. package/libx/_runtime/sqlite/customBuilder/CustomDeleteBuilder.js +0 -17
  137. package/libx/_runtime/sqlite/customBuilder/CustomReferenceBuilder.js +0 -11
  138. package/libx/_runtime/sqlite/customBuilder/CustomUpdateBuilder.js +0 -17
  139. /package/bin/build/provider/hana/template/{.hdiconfig → .hdiconfig-haas} +0 -0
@@ -938,7 +938,7 @@ function cov2ap(options = {}) {
938
938
  }
939
939
  }
940
940
 
941
- Object.entries(headers).forEach(([name, value]) => {
941
+ Object.keys(headers).forEach(name => {
942
942
  if (
943
943
  name === "dataserviceversion" ||
944
944
  name === "DataServiceVersion" ||
@@ -1171,7 +1171,7 @@ function cov2ap(options = {}) {
1171
1171
  return req.context;
1172
1172
  }
1173
1173
 
1174
- function convertUrlLinks(url, req) {
1174
+ function convertUrlLinks(url) {
1175
1175
  url.contextPath = url.contextPath.replace(/\/\$links\//gi, "/");
1176
1176
  }
1177
1177
 
@@ -1630,7 +1630,7 @@ function cov2ap(options = {}) {
1630
1630
  }
1631
1631
  }
1632
1632
 
1633
- function convertFilter(url, req) {
1633
+ function convertFilter(url) {
1634
1634
  const _ = "§§";
1635
1635
 
1636
1636
  let filter = url.query["$filter"];
@@ -1702,7 +1702,7 @@ function cov2ap(options = {}) {
1702
1702
  }
1703
1703
  }
1704
1704
 
1705
- function convertSearch(url, req) {
1705
+ function convertSearch(url) {
1706
1706
  if (url.query.search) {
1707
1707
  let search = url.query.search;
1708
1708
  if (quoteSearch) {
@@ -2789,7 +2789,7 @@ function cov2ap(options = {}) {
2789
2789
  }
2790
2790
  }
2791
2791
 
2792
- function removeMetadata(data, headers, definition, elements, body, req) {
2792
+ function removeMetadata(data) {
2793
2793
  Object.keys(data).forEach((key) => {
2794
2794
  if (key.startsWith("@") || key.startsWith("odata.") || key.includes("@odata.")) {
2795
2795
  delete data[key];
@@ -2797,7 +2797,7 @@ function cov2ap(options = {}) {
2797
2797
  });
2798
2798
  }
2799
2799
 
2800
- function convertMedia(data, headers, definition, elements, proxyBody, req) {
2800
+ function convertMedia(data) {
2801
2801
  Object.keys(data).forEach((key) => {
2802
2802
  if (key.endsWith("@odata.mediaReadLink")) {
2803
2803
  data[key.split("@odata.mediaReadLink")[0]] = data[key];
@@ -2807,7 +2807,7 @@ function cov2ap(options = {}) {
2807
2807
  });
2808
2808
  }
2809
2809
 
2810
- function removeAnnotations(data, headers, definition, elements, proxyBody, req) {
2810
+ function removeAnnotations(data) {
2811
2811
  Object.keys(data).forEach((key) => {
2812
2812
  if (key.startsWith("@")) {
2813
2813
  delete data[key];
@@ -2999,7 +2999,7 @@ function cov2ap(options = {}) {
2999
2999
  return `PT${timeParts[0] || "00"}H${timeParts[1] || "00"}M${timeParts[2] || "00"}S`;
3000
3000
  }
3001
3001
 
3002
- function addResultsNesting(data, headers, definition, elements, root, req) {
3002
+ function addResultsNesting(data, headers, definition, elements) {
3003
3003
  if (!returnCollectionNested) {
3004
3004
  return;
3005
3005
  }
@@ -16,13 +16,19 @@ class EventHandlers {
16
16
  )}
17
17
 
18
18
  async prepend (...impl_functions) {
19
- // IMPORTANT: We might be called in parallel -> the ._handlers._handlers
20
- // game below avoids loosing registrations due to race conditions
21
- const _handlers = this._handlers._handlers || this._handlers
22
- const _new = this._handlers = { _handlers, on:[], before:[], after:[], _initial:[], _error:[] }
23
- await Promise.all (impl_functions.map (fn => is_impl(fn) && fn.call (this,this)))
24
- for (let each in _new) if (_new[each].length) _handlers[each] = [ ..._new[each], ..._handlers[each] ]
25
- this._handlers = _handlers
19
+ // IMPORTANT: We might be called in parallel -> the ._handlers._real
20
+ // game below avoids loosing registrations due to race conditions.
21
+ // Note also that {__proto__:this, _handlers:_new} doesn't work as
22
+ // usages frequently look like that: srv.prepend(()=>srv.on(...)),
23
+ // which means the derived srv instance would be bypassed.
24
+ const _real = this._handlers._real || this._handlers
25
+ const _new = { on:[], before:[], after:[], _initial:[], _error:[], _real }
26
+ await Promise.all (impl_functions.map (fn => { if (is_impl(fn)) {
27
+ this._handlers = _new
28
+ return fn.call (this,this)
29
+ }}))
30
+ for (let handlers in _new) if (_new[handlers].length) _real[handlers] = [ ..._new[handlers], ..._real[handlers] ]
31
+ this._handlers = _real
26
32
  return this
27
33
  }
28
34
 
@@ -85,6 +91,19 @@ const _register = function (srv, phase, event, path, handler) { //NOSONAR
85
91
  if (!path.startsWith(srv.name+'.')) path = `${srv.name}.${path}`
86
92
  }
87
93
 
94
+ if (cds.env.features.lean_draft && cds.env.features.lean_draft_compatibility) {
95
+ const entity = path && srv.model?.definitions[path.name || path]
96
+ if (['PATCH', 'CANCEL', 'NEW'].includes(event)) {
97
+ // delegate to drafts
98
+ path = typeof path === 'string' && path !== '*' && !path.endsWith('.drafts') ? path + '.drafts' : typeof path === 'object' && path.drafts || path
99
+ if (event === 'PATCH') event = 'UPDATE'
100
+ }
101
+ else if (entity && (event === 'READ' || entity.actions?.[event]) && (entity.drafts && !entity.name.endsWith('.drafts'))) {
102
+ // additionally add drafts for READ and bound actions/functions
103
+ _register(srv, phase, event, entity.name + '.drafts', handler)
104
+ }
105
+ }
106
+
88
107
  // Finally register with a filter function to match requests to be handled
89
108
  const _handlers = srv._handlers [event === 'error' ? '_error' : (handler._initial ? '_initial' : phase)] // REVISIT: remove _initial handlers
90
109
  _handlers.push (new EventHandler (phase, event, path, handler))
@@ -4,8 +4,8 @@ const LOG = cds.log('cds.serve',{label:'cds'})
4
4
 
5
5
  module.exports = (srv) => {
6
6
  if (srv.model && ( //> we only support that for app services
7
- srv instanceof cds.ApplicationService ||
8
- srv instanceof cds.RemoteService ||
7
+ srv.isAppService ||
8
+ srv.isExternal ||
9
9
  srv._add_stub_methods
10
10
  )) {
11
11
  for (const each of srv.operations) {
@@ -57,7 +57,13 @@ class ExtendedModels {
57
57
  else return cache[key] = (async()=>{ // temporarily add promise to cache to avoid race conditions...
58
58
 
59
59
  // If tenant doesn't have extensions check cache with tenant = undefined
60
- const _has_extensions = tenant && extensibility && await _is_extended(tenant)
60
+ let _has_extensions = false
61
+ try {
62
+ _has_extensions = tenant && extensibility && await _is_extended(tenant)
63
+ } catch (error) {
64
+ // Better error message for client
65
+ cds.error('`extensibility: true` is configured but table "cds.xt.Extensions" does not exist. Please redeploy.', error)
66
+ }
61
67
  if (!_has_extensions) {
62
68
  let k = cache.key4 (tenant = undefined, features)
63
69
  let cached = cache.at(k); if (cached) return cached
@@ -146,8 +152,7 @@ class ExtendedModels {
146
152
  if (Date.now() - m._cached.touched > ExtendedModels.sentinelInterval) {
147
153
  delete this [key]
148
154
  }
149
- }}, ExtendedModels.sentinelInterval)
150
- cds.on('shutdown', ()=> clearInterval(this.sentinel))
155
+ }}, ExtendedModels.sentinelInterval).unref()
151
156
  }
152
157
 
153
158
 
@@ -29,10 +29,8 @@ class Test extends require('./axios') {
29
29
  catch (e) { if (is_mocha) console.error(e) } // eslint-disable-line no-console
30
30
  })
31
31
 
32
- // shutdown cds server...
33
- after (done => {
34
- this.server ? this.server.close (done) : done && done()
35
- })
32
+ // gracefully shutdown cds server...
33
+ after (() => this.server && cds.shutdown())
36
34
 
37
35
  beforeEach (async () => {
38
36
  if (this.data._autoReset) await this.data.reset()
@@ -80,7 +78,11 @@ class Test extends require('./axios') {
80
78
 
81
79
  then(r) {
82
80
  const {cds} = this
83
- cds.once('listening',r)
81
+ if (this.server) {
82
+ r({ server: this.server, url: this.url })
83
+ } else {
84
+ cds.once('listening', r)
85
+ }
84
86
  }
85
87
  }
86
88
 
@@ -63,6 +63,7 @@ exports.exists = function exists (x) {
63
63
  }
64
64
  }
65
65
 
66
+ // REVISIT naming: doesn't return boolean
66
67
  exports.isdir = function isdir (x) {
67
68
  if (x) try {
68
69
  const y = resolve (cds.root,x)
@@ -72,6 +73,7 @@ exports.isdir = function isdir (x) {
72
73
  } catch(e){/* ignore */}
73
74
  }
74
75
 
76
+ // REVISIT naming: doesn't return boolean
75
77
  exports.isfile = function isfile (x) {
76
78
  if (x) try {
77
79
  const y = resolve (cds.root,x)
@@ -100,7 +102,7 @@ exports.read = async function read (file, _encoding) {
100
102
  exports.write = function write (file, data, o) {
101
103
  if (arguments.length === 1) return {to:(...path) => write(join(...path),file)}
102
104
  if (typeof data === 'object' && !Buffer.isBuffer(data))
103
- data = JSON.stringify(data, null, ' '.repeat(o && o.spaces))
105
+ data = JSON.stringify(data, null, ' '.repeat(o && o.spaces)) + require('os').EOL
104
106
  const f = resolve (cds.root,file)
105
107
  return fs.mkdirp (dirname(f)).then (()=> fs.promises.writeFile (f,data,o))
106
108
  }
package/lib/utils/tar.js CHANGED
@@ -96,8 +96,11 @@ exports.create = async (dir='.', ...args) => {
96
96
  c = spawnDir(dir, args)
97
97
  }
98
98
  } else {
99
- if (Array.isArray(args[0])) args.push (...args.shift().map (f => path.isAbsolute(f) ? path.relative(dir,f) : f))
100
- else args.push('.')
99
+ if (Array.isArray(args[0])) {
100
+ args.push (...args.shift().map (f => path.isAbsolute(f) ? path.relative(dir,f) : f))
101
+ } else {
102
+ args.push('.')
103
+ }
101
104
 
102
105
  c = spawn ('tar', ['c', '-C', dir, ...args])
103
106
  }
@@ -236,5 +239,5 @@ exports.t = tar.tf = tar.list
236
239
  // ---------------------------------------------------------------------------------
237
240
  // Compatibility...
238
241
 
239
- exports.packTarArchive = (files,d) => d ? tar.cz (d,files) : tar.cz (files)
242
+ exports.packTarArchive = (resources,d) => d ? tar.cz (d,resources) : tar.cz (resources)
240
243
  exports.unpackTarArchive = (x,dir) => tar.xz(x).to(dir)
@@ -24,6 +24,7 @@ class JWTStrategy extends JS {
24
24
  roles: xssecUtils.getRoles(['any', 'identified-user'], info, credentials),
25
25
  attr: xssecUtils.getAttrForJWT(info)
26
26
  })
27
+ xssecUtils.addRolesFromGrantType(user, info, credentials)
27
28
  const tenant = xssecUtils.getTenant(info)
28
29
  if (tenant) user.tenant = tenant
29
30
  // call "super.success"
@@ -42,9 +42,10 @@ module.exports = function ias_auth(config) {
42
42
  if (req.tokenInfo.getClientId() === req.tokenInfo.getSubject()) {
43
43
  req.user = new cds.User({
44
44
  id: 'system',
45
- roles: ['system-user', 'authenticated-user'],
45
+ roles: ['authenticated-user'],
46
46
  attr: {}
47
47
  })
48
+ req.user._is_system = true
48
49
  } else {
49
50
  // add all unknown attributes to req.user.attr in order to keep public API small
50
51
  const payload = req.tokenInfo.getPayload()
@@ -65,7 +66,7 @@ module.exports = function ias_auth(config) {
65
66
  req.tenant = req.tokenInfo.getZoneId()
66
67
  next()
67
68
  })
68
- .use((err, req, res, next) => {
69
+ .use((err, req, res, _next) => {
69
70
  if (req.tokenInfo) {
70
71
  LOG?.debug('error during token validation', req.tokenInfo.getErrorObject())
71
72
  }
@@ -21,6 +21,17 @@ class MockStrategy {
21
21
  if (user.password && user.password !== password) return this.fail(CHALLENGE)
22
22
 
23
23
  const { features } = req.headers
24
+ // Only in the mock strategy the pseudo roles are kept in the role list.
25
+ // In all other cases pseudo roles are filtered out.
26
+ if (user.roles) {
27
+ if (Array.isArray(user.roles)) {
28
+ if (user.roles.includes('system-user')) user._is_system = true
29
+ if (user.roles.includes('internal-user')) user._is_internal = true
30
+ } else {
31
+ if ('system-user' in user.roles) user._is_system = true
32
+ if ('internal-user' in user.roles) user._is_internal = true
33
+ }
34
+ }
24
35
  this.success(new cds.User(features ? { ...user, features } : user))
25
36
  }
26
37
  }
@@ -44,7 +55,7 @@ const _init_users = (users, tenants = {}) => {
44
55
  Array.isArray(user.roles) ? user.roles.push(...scopes) : (user.roles = scopes)
45
56
  }
46
57
  if (user.jwt.grant_type === 'client_credentials' || user.jwt.grant_type === 'client_x509') {
47
- user.roles.push('system-user')
58
+ user._is_system = true
48
59
  }
49
60
  if (!user.tenant && user.jwt.zid) user.tenant = user.jwt.zid
50
61
  }
@@ -5,21 +5,19 @@ const getUserId = (user, info) => {
5
5
  return user.id || (info && info.getClientId && info.getClientId())
6
6
  }
7
7
 
8
- const _addRolesFromGrantType = (roles, info, credentials) => {
8
+ const addRolesFromGrantType = (user, info, credentials) => {
9
9
  const grantType = info && (info.grantType || (info.getGrantType && info.getGrantType()))
10
10
  if (grantType) {
11
11
  // > not "weak"
12
- roles.push('authenticated-user')
12
+ user.roles['authenticated-user'] = true
13
13
  if (grantType in CLIENT) {
14
- roles.push('system-user')
15
- if (info.getClientId() === credentials.clientid) roles.push('internal-user')
14
+ user._is_system = true
15
+ if (info.getClientId() === credentials.clientid) user._is_internal = true
16
16
  }
17
17
  }
18
18
  }
19
19
 
20
- const getRoles = (roles, info, credentials) => {
21
- _addRolesFromGrantType(roles, info, credentials)
22
-
20
+ const getRoles = (roles, info) => {
23
21
  // convert to object
24
22
  roles = Object.assign(...roles.map(ele => ({ [ele]: true })))
25
23
 
@@ -90,5 +88,6 @@ module.exports = {
90
88
  getRoles,
91
89
  getAttrForJWT,
92
90
  getAttrForXSSEC,
93
- getTenant
91
+ getTenant,
92
+ addRolesFromGrantType
94
93
  }
@@ -25,6 +25,7 @@ class XSUAAStrategy extends JS {
25
25
  roles: xssecUtils.getRoles(['any', 'identified-user'], info, credentials),
26
26
  attr: xssecUtils.getAttrForXSSEC(info)
27
27
  })
28
+ xssecUtils.addRolesFromGrantType(user, info, credentials)
28
29
  const tenant = xssecUtils.getTenant(info)
29
30
  if (tenant) user.tenant = tenant
30
31
  // call "super.success"
@@ -253,8 +253,12 @@ class OData {
253
253
  this._odataService.process(req, res).catch(err => {
254
254
  LOG.warn(err)
255
255
  // REVISIT: use i18n
256
- //do not reply with error, if response already processed (streaming)
257
- if (!res.headersSent) {
256
+ // do not reply with error, if response already processed (streaming)
257
+ // destroy response socket instead
258
+ if (res.headersSent) {
259
+ // REVISIT: temp solution until streaming is switched to express middlewares
260
+ res.socket.destroy()
261
+ } else {
258
262
  const { error, statusCode } = normalizeError(err, req)
259
263
  res.status(statusCode).send({ error })
260
264
  }
@@ -60,9 +60,8 @@ const action = service => {
60
60
  await tx.commit(result)
61
61
  }
62
62
 
63
- if (isReturnMinimal(req) || result === null) odataRes.setStatusCode(204)
63
+ if (isReturnMinimal(req) || result === null) odataRes.setStatusCode(204, { overwrite: true })
64
64
  else if (req.event === 'draftActivate' || req.event === 'EDIT') {
65
- odataRes.setStatusCode(201)
66
65
  const keys = Object.keys(req.target.keys).filter(k => {
67
66
  return k !== 'IsActiveEntity' && !req.target.keys[k]._isAssociationStrict
68
67
  })
@@ -251,6 +251,14 @@ const _readEntityOrProperty = async (tx, req, segments) => {
251
251
  return odataResult
252
252
  }
253
253
 
254
+ const _reliablePagingPossible = req => {
255
+ if (req.target._isDraftEnabled) return false
256
+ if (cds.context?.http.req.query.$apply) return false
257
+ if (req.query.SELECT.limit.offset?.val ?? req.query.SELECT.limit.offset > 0) return false
258
+ if (req.query.SELECT.orderBy.some(o => !o.ref)) return false
259
+ return req.query.SELECT.orderBy.every(o => req.query.SELECT.columns.some(c => o.ref[0] === c.ref[0]))
260
+ }
261
+
254
262
  /**
255
263
  * Read an entity collection without including the count of the total amount of entities.
256
264
  *
@@ -260,6 +268,7 @@ const _readEntityOrProperty = async (tx, req, segments) => {
260
268
  * @returns {Promise}
261
269
  * @private
262
270
  */
271
+ // eslint-disable-next-line complexity
263
272
  const _readCollection = async (tx, req, odataReq) => {
264
273
  const result = (await tx.dispatch(req)) || []
265
274
  if (Array.isArray(req.query)) {
@@ -284,7 +293,23 @@ const _readCollection = async (tx, req, odataReq) => {
284
293
  const top = odataReq.getUriInfo().getQueryOption(QueryOptions.TOP)
285
294
  if (limit && limit === result.length && limit !== top && !('$nextLink' in result)) {
286
295
  const token = odataReq.getUriInfo().getQueryOption(QueryOptions.SKIPTOKEN)
287
- result.$nextLink = (token ? parseInt(token) : 0) + limit
296
+ if (cds.env.query.limit.reliablePaging && _reliablePagingPossible(req)) {
297
+ const decoded = token && JSON.parse(Buffer.from(token, 'base64').toString())
298
+ const skipToken = {
299
+ r: (decoded?.r || 0) + limit,
300
+ c: req.query.SELECT.orderBy.map(o => ({
301
+ a: o.sort ? o.sort === 'asc' : true,
302
+ k: o.ref[0],
303
+ v: result[result.length - 1][o.ref[0]]
304
+ }))
305
+ }
306
+
307
+ if (limit + (decoded?.r || 0) !== top) {
308
+ result.$nextLink = Buffer.from(JSON.stringify(skipToken)).toString('base64')
309
+ }
310
+ } else {
311
+ result.$nextLink = (token ? parseInt(token) : 0) + limit
312
+ }
288
313
  }
289
314
 
290
315
  const odataResult = toODataResult(result, req)
@@ -11,6 +11,7 @@ const { isReturnMinimal } = require('../utils/handlerUtils')
11
11
  const { readAfterWrite } = require('../utils/readAfterWrite')
12
12
  const { toODataResult, postProcess, postProcessMinimal } = require('../utils/result')
13
13
  const { hasOmitValuesPreference } = require('../utils/omitValues')
14
+ const { isStreaming } = require('../utils/stream')
14
15
 
15
16
  const { getSapMessages } = require('../../../../common/error/frontend')
16
17
 
@@ -146,6 +147,13 @@ const update = service => {
146
147
  previousResult = await readAfterWrite(req, service, { isBefore: true })
147
148
  }
148
149
 
150
+ // in case of express errors in streaming do rollback
151
+ const segments = odataReq.getUriInfo().getPathSegments()
152
+ if (isStreaming(segments)) {
153
+ odataReq.getIncomingRequest().on('error', async err => {
154
+ await tx.rollback(err).catch(() => {})
155
+ })
156
+ }
149
157
  // try UPDATE and, on 404 error, try CREATE
150
158
  ;[result, req] = await _updateThenCreate(req, odataReq, odataRes, tx)
151
159
 
@@ -187,7 +187,7 @@ class ExpressionToCQN {
187
187
  // ignore
188
188
  }
189
189
  }
190
- return { func: `${operator ? `${operator} ` : ''}${methodName}`, args }
190
+ return operator ? [operator, { func: `${methodName}`, args }] : { func: `${methodName}`, args }
191
191
  }
192
192
 
193
193
  /* eslint-disable complexity */
@@ -41,6 +41,8 @@ const { isAsteriskColumn } = require('../../../../common/utils/rewriteAsterisks'
41
41
 
42
42
  const { ensureUnlocalized } = require('../../../../fiori/utils/handler')
43
43
 
44
+ const { skipToken: handleSkipToken } = require('../../../../../odata/utils')
45
+
44
46
  const _applyOnlyContainsFilter = apply => Object.keys(apply).length === 1 && apply.filter
45
47
 
46
48
  /**
@@ -136,9 +138,9 @@ const _apply = (uriInfo, queryOptions, entity, model) => {
136
138
  }
137
139
 
138
140
  const _topSkip = (queryOptions, target, cqn) => {
139
- if (queryOptions && (queryOptions.$top || queryOptions.$skip || queryOptions.$skiptoken)) {
141
+ if (queryOptions && (queryOptions.$top || queryOptions.$skip)) {
140
142
  const top = queryOptions.$top ? parseInt(queryOptions.$top) : getDefaultPageSize(target)
141
- const skip = parseInt(queryOptions.$skip || 0) + parseInt(queryOptions.$skiptoken || 0)
143
+ const skip = parseInt(queryOptions.$skip || 0)
142
144
  cqn.limit(Math.min(top, getMaxPageSize(target)), skip)
143
145
  }
144
146
  }
@@ -324,6 +326,10 @@ const _handleApply = (apply, select) => {
324
326
  select.push(...mergedArray)
325
327
  }
326
328
 
329
+ const _skipToken = (token, cqn) => {
330
+ handleSkipToken(token, cqn)
331
+ }
332
+
327
333
  /**
328
334
  * Transform odata READ request into a CQN object.
329
335
  *
@@ -393,6 +399,9 @@ const readToCQN = (service, target, odataReq) => {
393
399
  if (isCollectionOrToMany) {
394
400
  _topSkip(queryOptions, target, cqn)
395
401
  _orderby(uriInfo, cqn)
402
+
403
+ const skipToken = queryOptions?.$skiptoken
404
+ if (skipToken) _skipToken(skipToken, cqn)
396
405
  }
397
406
 
398
407
  if (!isCollectionOrToMany || entity._isSingleton) cqn.SELECT.one = true
@@ -166,7 +166,7 @@ class PrimitiveValueDecoder {
166
166
  throw new IllegalArgumentError(
167
167
  'Invalid value ' +
168
168
  value +
169
- ' (JavaScript ' +
169
+ ' (' +
170
170
  typeof value +
171
171
  '). ' +
172
172
  'A JSON string must be specified as value for type ' +
@@ -180,7 +180,7 @@ class PrimitiveValueDecoder {
180
180
  throw new IllegalArgumentError(
181
181
  'Invalid value ' +
182
182
  value +
183
- ' (JavaScript ' +
183
+ ' (' +
184
184
  typeof value +
185
185
  ') ' +
186
186
  'as value for type ' +
@@ -209,7 +209,7 @@ class PrimitiveValueDecoder {
209
209
  throw new IllegalArgumentError(
210
210
  'Invalid value ' +
211
211
  value +
212
- ' (JavaScript ' +
212
+ ' (' +
213
213
  typeof value +
214
214
  ') ' +
215
215
  'as value for type ' +
@@ -226,7 +226,7 @@ class PrimitiveValueDecoder {
226
226
  throw new IllegalArgumentError(
227
227
  'Invalid value ' +
228
228
  value +
229
- ' (JavaScript ' +
229
+ ' (' +
230
230
  typeof value +
231
231
  ') ' +
232
232
  'as value for type ' +
@@ -247,7 +247,7 @@ class PrimitiveValueDecoder {
247
247
  throw new IllegalArgumentError(
248
248
  'Invalid value ' +
249
249
  value +
250
- ' (JavaScript ' +
250
+ ' (' +
251
251
  typeof value +
252
252
  '). A JSON string must be specified ' +
253
253
  'as value for type ' +
@@ -262,7 +262,7 @@ class PrimitiveValueDecoder {
262
262
  throw new IllegalArgumentError(
263
263
  'Invalid value ' +
264
264
  value +
265
- ' (JavaScript ' +
265
+ ' (' +
266
266
  typeof value +
267
267
  ') ' +
268
268
  'as value for type ' +
@@ -311,7 +311,7 @@ class PrimitiveValueDecoder {
311
311
  throw new IllegalArgumentError(
312
312
  'Invalid value ' +
313
313
  value +
314
- ' (JavaScript ' +
314
+ ' (' +
315
315
  typeof value +
316
316
  '). A JSON ' +
317
317
  kind +
@@ -339,7 +339,7 @@ class PrimitiveValueDecoder {
339
339
  throw new IllegalArgumentError(
340
340
  'Invalid value ' +
341
341
  value +
342
- ' (JavaScript ' +
342
+ ' (' +
343
343
  typeof value +
344
344
  ') for enumeration type ' +
345
345
  type.getFullQualifiedName() +
@@ -544,7 +544,7 @@ class ValueConverter {
544
544
  throw new IllegalArgumentError(
545
545
  'Invalid value ' +
546
546
  value +
547
- ' (JavaScript ' +
547
+ ' (' +
548
548
  typeof value +
549
549
  ') ' +
550
550
  'for enumeration type ' +