@sap/cds 6.5.0 → 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 (105) hide show
  1. package/CHANGELOG.md +38 -2
  2. package/README.md +5 -0
  3. package/apis/services.d.ts +5 -0
  4. package/bin/build/buildTaskEngine.js +0 -2
  5. package/bin/build/buildTaskFactory.js +1 -1
  6. package/bin/build/buildTaskHandler.js +1 -1
  7. package/bin/build/provider/buildTaskProviderInternal.js +10 -6
  8. package/bin/build/provider/fiori/index.js +5 -10
  9. package/bin/build/provider/hana/2migration.js +11 -2
  10. package/bin/build/provider/hana/index.js +17 -14
  11. package/bin/build/provider/hana/template/.hdiconfig-hanacloud +137 -0
  12. package/bin/build/provider/mtx-extension/index.js +18 -1
  13. package/bin/build/provider/mtx-sidecar/index.js +1 -1
  14. package/bin/build/util.js +1 -1
  15. package/bin/cds.js +1 -5
  16. package/bin/deploy/to-hana/hana.js +10 -3
  17. package/bin/serve.js +32 -20
  18. package/lib/auth/jwt-auth.js +4 -4
  19. package/lib/compile/for/lean_drafts.js +55 -6
  20. package/lib/dbs/cds-deploy.js +6 -8
  21. package/lib/index.js +4 -2
  22. package/lib/req/cds-context.js +3 -3
  23. package/lib/srv/bindings.js +1 -2
  24. package/lib/srv/cds-serve.js +2 -1
  25. package/lib/srv/middlewares/trace.js +31 -15
  26. package/lib/srv/protocols/odata-v2-proxy.js +8 -8
  27. package/lib/srv/srv-handlers.js +26 -7
  28. package/lib/srv/srv-methods.js +2 -2
  29. package/lib/srv/srv-models.js +3 -3
  30. package/lib/utils/cds-test.js +7 -5
  31. package/libx/_runtime/auth/strategies/ias-auth.js +1 -1
  32. package/libx/_runtime/cds-services/adapter/odata-v4/OData.js +6 -2
  33. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +26 -1
  34. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +8 -0
  35. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/ExpressionToCQN.js +1 -1
  36. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/readToCQN.js +11 -2
  37. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/utils/PrimitiveValueDecoder.js +8 -8
  38. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/utils/ValueConverter.js +1 -1
  39. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/validator/ValueValidator.js +14 -14
  40. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/deserializer/DeserializerFactory.js +1 -0
  41. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/serializer/ResourceJsonSerializer.js +3 -0
  42. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/utils/UriHelper.js +2 -1
  43. package/libx/_runtime/cds-services/adapter/odata-v4/utils/metaInfo.js +3 -2
  44. package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +7 -0
  45. package/libx/_runtime/cds-services/adapter/odata-v4/utils/stream.js +0 -3
  46. package/libx/_runtime/cds-services/services/Service.js +8 -19
  47. package/libx/_runtime/cds-services/services/utils/columns.js +7 -4
  48. package/libx/_runtime/cds-services/util/assert.js +7 -1
  49. package/libx/_runtime/common/code-ext/WorkerReq.js +3 -1
  50. package/libx/_runtime/common/code-ext/execute.js +9 -2
  51. package/libx/_runtime/common/code-ext/handlers.js +2 -2
  52. package/libx/_runtime/common/code-ext/worker.js +9 -5
  53. package/libx/_runtime/common/code-ext/workerQueryExecutor.js +5 -2
  54. package/libx/_runtime/common/composition/data.js +5 -2
  55. package/libx/_runtime/common/composition/tree.js +2 -0
  56. package/libx/_runtime/common/generic/auth/restrict.js +1 -1
  57. package/libx/_runtime/common/generic/etag.js +3 -1
  58. package/libx/_runtime/common/generic/input.js +12 -14
  59. package/libx/_runtime/common/utils/cqn2cqn4sql.js +31 -11
  60. package/libx/_runtime/common/utils/path.js +0 -1
  61. package/libx/_runtime/common/utils/search2cqn4sql.js +4 -1
  62. package/libx/_runtime/common/utils/templateProcessorPathSerializer.js +19 -13
  63. package/libx/_runtime/db/data-conversion/post-processing.js +1 -1
  64. package/libx/_runtime/db/expand/expandCQNToJoin.js +5 -3
  65. package/libx/_runtime/db/expand/rawToExpanded.js +3 -2
  66. package/libx/_runtime/db/generic/input.js +2 -2
  67. package/libx/_runtime/db/generic/integrity.js +1 -0
  68. package/libx/_runtime/db/generic/virtual.js +1 -0
  69. package/libx/_runtime/db/query/read.js +3 -2
  70. package/libx/_runtime/fiori/generic/activate.js +3 -1
  71. package/libx/_runtime/fiori/generic/before.js +1 -0
  72. package/libx/_runtime/fiori/generic/edit.js +3 -1
  73. package/libx/_runtime/fiori/generic/new.js +2 -0
  74. package/libx/_runtime/fiori/generic/patch.js +2 -0
  75. package/libx/_runtime/fiori/generic/prepare.js +2 -0
  76. package/libx/_runtime/fiori/generic/read.js +8 -2
  77. package/libx/_runtime/fiori/generic/readOverDraft.js +2 -0
  78. package/libx/_runtime/fiori/lean-draft.js +498 -245
  79. package/libx/_runtime/fiori/utils/delete.js +2 -0
  80. package/libx/_runtime/messaging/Outbox.js +1 -1
  81. package/libx/_runtime/messaging/enterprise-messaging-utils/getTenantInfo.js +1 -0
  82. package/libx/_runtime/messaging/enterprise-messaging.js +2 -6
  83. package/libx/_runtime/messaging/file-based.js +1 -2
  84. package/libx/_runtime/messaging/outbox/OutboxRunner.js +1 -1
  85. package/libx/_runtime/messaging/outbox/utils.js +1 -1
  86. package/libx/_runtime/messaging/service.js +0 -1
  87. package/libx/_runtime/remote/Service.js +1 -0
  88. package/libx/_runtime/sqlite/convertDraftAdminPathExpression.js +19 -3
  89. package/libx/_runtime/sqlite/customBuilder/CustomExpressionBuilder.js +0 -18
  90. package/libx/_runtime/sqlite/customBuilder/CustomFunctionBuilder.js +0 -18
  91. package/libx/_runtime/sqlite/customBuilder/CustomSelectBuilder.js +0 -24
  92. package/libx/_runtime/sqlite/customBuilder/CustomUpsertBuilder.js +2 -1
  93. package/libx/_runtime/sqlite/customBuilder/index.js +47 -32
  94. package/libx/odata/afterburner.js +17 -5
  95. package/libx/odata/grammar.pegjs +3 -4
  96. package/libx/odata/index.js +5 -1
  97. package/libx/odata/parseToCqn.js +3 -3
  98. package/libx/odata/parser.js +1 -1
  99. package/libx/odata/utils.js +58 -1
  100. package/package.json +1 -1
  101. package/server.js +1 -1
  102. package/libx/_runtime/sqlite/customBuilder/CustomDeleteBuilder.js +0 -17
  103. package/libx/_runtime/sqlite/customBuilder/CustomReferenceBuilder.js +0 -11
  104. package/libx/_runtime/sqlite/customBuilder/CustomUpdateBuilder.js +0 -17
  105. /package/bin/build/provider/hana/template/{.hdiconfig → .hdiconfig-haas} +0 -0
package/bin/serve.js CHANGED
@@ -131,14 +131,14 @@ const cds = require('../lib'), { exists, isfile, local, path } = cds.utils
131
131
 
132
132
  // provisional loggers, see _prepare_logging
133
133
  let log = console.log
134
- let debug = false
135
134
 
136
135
 
137
136
  /**
138
137
  * The main function which dispatches into the respective usage variants.
139
138
  * @param {string[]} all - project folder, model filenames, or service name
140
139
  */
141
- async function serve (all=[], o={}) { // NOSONAR
140
+ async function serve (all=[], o={}) {
141
+
142
142
  // canonicalize options to ease subsequent tasks...
143
143
  cds.options = o
144
144
  const [pms] = all // project folder, model filenames, or service name
@@ -200,17 +200,8 @@ async function serve (all=[], o={}) { // NOSONAR
200
200
 
201
201
  server.listening ? _started(server) : server.once('listening',_started)
202
202
  server.on ('error',_reject) // startup errors like EADDRINUSE
203
- server.on ('close', ()=> shutdown()) // in case of server.close() was called, like in cds.test
204
-
205
- process.once('SIGTERM', shutdown)
206
- process.once('SIGINT', shutdown)
207
- process.once('SIGHUP', shutdown)
208
- process.once('SIGUSR2', shutdown) // by nodemon
209
- process.on('beforeExit', shutdown) //> when event loop empties
210
- process.on('message', (msg) => { if (msg.close||msg.exit) shutdown() }) // by `cds watch` on Windows
211
-
212
- return server
213
-
203
+ // server.on ('close', _shutdown) // IMPORTANT: Don't do that as that would be a very strange loop
204
+ // process.on ('exit', _shutdown) // IMPORTANT: Don't do that as that would be a very strange loop
214
205
  async function _started() {
215
206
  _warn_if_cds_was_loaded_from_different_locations()
216
207
  const url = cds.server.url = `http://localhost:${server.address().port}`
@@ -218,13 +209,34 @@ async function serve (all=[], o={}) { // NOSONAR
218
209
  _resolve (server)
219
210
  }
220
211
 
221
- async function shutdown (sig) {
222
- if (shutdown.called) return; else shutdown.called = true // only do that once
223
- global.it || cds.watched || console.log() // blank line makes the ^C look pretty in terminals
224
- debug && debug(`${sig}, shutting down, calling ${cds.listeners('shutdown').length} listeners`)
225
- await Promise.all(cds.listeners('shutdown').map((fn) => fn()))
226
- if (process.env.NODE_ENV !== 'test' && !global.it) process.exit()
212
+ cds.shutdown = _shutdown //> for programmatic invocation
213
+ process.on('unhandledRejection', (_,p) => _shutdown (console.error('❗️Uncaught',p)))
214
+ process.on('uncaughtException', (e) => _shutdown (console.error('❗️Uncaught',e)))
215
+ process.on('SIGINT', cds.watched ? _shutdown : (s,n)=>_shutdown(s,n,console.log())) //> newline after ^C
216
+ process.on('SIGHUP', _shutdown)
217
+ process.on('SIGHUP2', _shutdown)
218
+ process.on('SIGTERM', _shutdown)
219
+
220
+ async function _shutdown (signal,n) {
221
+ if (signal) DEBUG?.('⚡️',signal,n, 'received by cds serve')
222
+ await Promise.all(cds.listeners('shutdown').map(fn => fn()))
223
+ server.close(()=>{/* it's ok if closed already */}) // first, we try stopping server and process the nice way
224
+ if (!global.it) setTimeout(process.exit,1111).unref() // after ~1 sec, we force-exit it, unless in test mode
227
225
  }
226
+
227
+ const DEBUG = cds.debug('cli')
228
+ if (DEBUG) {
229
+ cds.on('shutdown', () => DEBUG ('⚡️','cds serve - cds.shutdown'))
230
+ server.on('close', () => DEBUG ('⚡️','cds serve - server.close(d)'))
231
+ process.on('exit', () => DEBUG ('⚡️','cds serve - process.exit'))
232
+ process.on('beforeExit', ()=> DEBUG ('⚡️','cds serve - process.beforeExit'))
233
+ }
234
+
235
+ if (process.platform === 'win32') {
236
+ process.on('message', msg => msg.close && _shutdown()) // by `cds watch` on Windows
237
+ }
238
+
239
+ return server
228
240
  })
229
241
  }
230
242
 
@@ -269,7 +281,7 @@ function _prepare_logging () { // NOSONAR
269
281
 
270
282
  // print info when we are finally on air
271
283
  cds.once ('listening', ({url})=>{
272
- LOG.info ()
284
+ console.log()
273
285
  LOG.info ('server listening on',{url})
274
286
  _timer && console.timeEnd (_timer)
275
287
  if (process.stdin.isTTY) LOG.info (`[ terminate with ^C ]\n`)
@@ -24,16 +24,16 @@ module.exports = function jwt_auth(config) {
24
24
  .use(passport.authenticate(config.kind, { session: false }))
25
25
  .use((req, res, next) => {
26
26
  const payload = req.tokenInfo.getPayload()
27
-
27
+
28
28
  let id = req.user.id
29
29
  let _is_system, _is_internal
30
-
30
+
31
31
  let roles = payload.scope.map(s => s.replace(new RegExp(`^(${config.credentials.xsappname + '.'})`), ''))
32
32
  roles.push('identified-user')
33
33
  if (payload.grant_type) {
34
34
  // > not "weak"
35
35
  roles.push('authenticated-user')
36
-
36
+
37
37
  const CLIENT = { client_credentials: 1, client_x509: 1 }
38
38
  if (payload.grant_type in CLIENT) {
39
39
  id = 'system'
@@ -54,7 +54,7 @@ module.exports = function jwt_auth(config) {
54
54
  req.tenant = req.tokenInfo.getZoneId?.()
55
55
  next()
56
56
  })
57
- .use((err, req, res, next) => {
57
+ .use((err, req, res, _next) => {
58
58
  if (req.tokenInfo) {
59
59
  LOG?.debug('error during token validation', req.tokenInfo.getErrorObject())
60
60
  }
@@ -1,5 +1,43 @@
1
- const cds = require ('../../index')
2
- module.exports = function cds_compile_for_lean_drafts(csn, o) {
1
+ const cds = require('../../index')
2
+
3
+ function _getBacklinkName(on) {
4
+ const i = on.findIndex(e => e.ref && e.ref[0] === '$self')
5
+ if (i === -1) return
6
+ let ref
7
+ if (on[i + 1] && on[i + 1] === '=') ref = on[i + 2].ref
8
+ if (on[i - 1] && on[i - 1] === '=') ref = on[i - 2].ref
9
+ return ref && ref[ref.length - 1]
10
+ }
11
+
12
+ function _isCompositionBacklink(e) {
13
+ if (!e.isAssociation) return
14
+ if (!e._target?.associations) return
15
+ if (!(!e.isComposition && (e.keys || e.on))) return
16
+ for (const anchor of Object.values(e._target.associations)) {
17
+ if (!(anchor.isComposition && anchor.on?.length > 2)) continue
18
+ if (_getBacklinkName(anchor.on) === e.name && anchor.target === e.parent.name) return anchor
19
+ }
20
+ }
21
+
22
+ const IGNORED_ANNOTATIONS = [
23
+ '@assert.range',
24
+ '@assert.enum',
25
+ '@assert.format',
26
+ '@assert.target',
27
+ '@mandatory',
28
+ '@Core.Immutable',
29
+ '@readonly',
30
+ '@cds.on.update',
31
+ '@cds.on.insert',
32
+ '@Core.Computed',
33
+ '@Common.FieldControl.Readonly',
34
+ '@Common.FieldControl.Mandatory',
35
+ '@FieldControl.Mandatory',
36
+ '@FieldControl.ReadOnly',
37
+ '@Common.FieldControl'
38
+ ]
39
+
40
+ module.exports = function cds_compile_for_lean_drafts(csn) {
3
41
  const DRAFT_ELEMENTS = new Set([
4
42
  'IsActiveEntity',
5
43
  'HasDraftEntity',
@@ -22,6 +60,13 @@ module.exports = function cds_compile_for_lean_drafts(csn, o) {
22
60
  return on
23
61
  }
24
62
 
63
+ function _isDraft(def) {
64
+ return (
65
+ def.associations?.DraftAdministrativeData ||
66
+ (def.own('@odata.draft.enabled') && def.own('@Common.DraftRoot.ActivationAction'))
67
+ )
68
+ }
69
+
25
70
  const { Draft } = cds.linked(`
26
71
  entity ActiveEntity { key ID: UUID; }
27
72
  entity Draft {
@@ -49,15 +94,21 @@ module.exports = function cds_compile_for_lean_drafts(csn, o) {
49
94
  const draft = { __proto__: active, name: _draftEntity, elements: { ...active.elements, ...Draft.elements } }
50
95
  Object.defineProperty(model.definitions, _draftEntity, { value: draft })
51
96
  Object.defineProperty(active, 'drafts', { value: draft })
97
+ Object.defineProperty(draft, 'actives', { value: active })
52
98
  draft['@cds.persistence.table'] = _draftEntity
99
+ if (draft['@restrict']) draft['@restrict'] = undefined
53
100
  // Recursively add drafts for compositions
54
101
  for (const each in draft.elements) {
55
102
  const e = draft.elements[each]
56
103
  const newEl = Object.create(e)
57
- if (e.isComposition || (e.isAssociation && e['@odata.draft.enclosed']) || e._isCompositionBacklink) {
104
+ if (e.isComposition || (e.isAssociation && e['@odata.draft.enclosed']) || _isCompositionBacklink(e)) {
105
+ if (e._target['@odata.draft.enabled'] === false) continue // happens for texts if @fiori.draft.enabled is not set
58
106
  _redirect(newEl, draftEntity(e._target, model))
59
107
  }
60
108
  newEl.parent = draft
109
+ for (const ignoredAnno of IGNORED_ANNOTATIONS) {
110
+ if (newEl[ignoredAnno]) newEl[ignoredAnno] = undefined
111
+ }
61
112
  draft.elements[each] = newEl
62
113
  }
63
114
  // TODO: Redirect associations to localized
@@ -65,9 +116,7 @@ module.exports = function cds_compile_for_lean_drafts(csn, o) {
65
116
  }
66
117
  for (const name in csn.definitions) {
67
118
  const def = csn.definitions[name]
68
- if (!def._isDraftEnabled || def.name.endsWith('.DraftAdministrativeData'))
69
- continue
70
- // so that database ignores them
119
+ if (!_isDraft(def)) continue
71
120
  ;[
72
121
  'IsActiveEntity',
73
122
  'HasDraftEntity',
@@ -107,9 +107,7 @@ exports.exclude_external_entities_in = function (csn) { // NOSONAR
107
107
 
108
108
  function getSqls(db, csn, o, beforeCsn) {
109
109
  const schemaEvo = (db.options?.schema_evolution === 'auto' || o.schema_evolution === 'auto')
110
- const creds = db.options?.credentials
111
- const in_memory = (creds?.url || creds?.database) === ':memory:';
112
- if (!in_memory && schemaEvo) {
110
+ if (schemaEvo) {
113
111
  const { afterImage: afterCsn, drops, createsAndAlters: creas } = cds.compile.to.sql.delta (csn, o, beforeCsn);
114
112
  if(beforeCsn === undefined) {
115
113
  // If this is the first deployment done with automatic schema evolution, generate everything as if it was a drop create
@@ -138,9 +136,8 @@ exports.create = async function (db, csn=db.model, o) {
138
136
  return db.deploy(csn, o);
139
137
  }
140
138
 
141
- const in_memory = db.options?.credentials?.url === ':memory:';
142
139
  let beforeCsn
143
- if (!in_memory && schemaEvo) try {
140
+ if (schemaEvo) try {
144
141
  const [{ csn }] = await db.read('cds.Model')
145
142
  beforeCsn = JSON.parse(csn);
146
143
  } catch(e) {
@@ -160,7 +157,7 @@ exports.create = async function (db, csn=db.model, o) {
160
157
  } else return db.run (async tx => {
161
158
  await tx.run(drops)
162
159
  await tx.run(creas)
163
- if (!in_memory && schemaEvo) {
160
+ if (schemaEvo) {
164
161
  await tx.update('cds.Model').with({ csn: JSON.stringify(afterCsn) })
165
162
  }
166
163
  return true
@@ -169,10 +166,11 @@ exports.create = async function (db, csn=db.model, o) {
169
166
 
170
167
 
171
168
  exports.init = (db, csn=db.model, o, csvs, log=()=>{}) => db.run (async tx => {
169
+
170
+ const {tenant} = cds.context; if (tenant && tenant === cds.requires.multitenancy?.t0) return
171
+ const schemaEvo = db.options?.schema_evolution === 'auto' || o?.schema_evolution === 'auto'
172
172
  const resources = await exports.resources(csn, {testdata: cds.env.features.test_data})
173
173
  const inits=[]
174
- const schemaEvo = (db.options?.schema_evolution === 'auto' || o?.schema_evolution === 'auto')
175
-
176
174
 
177
175
  if (csvs) {
178
176
  const ccsn = cds.compile.for['nodejs'](csn) // compile to calculate keys for newly added entities
package/lib/index.js CHANGED
@@ -12,8 +12,9 @@ const facade = class cds extends require('events') {
12
12
  set context(_){ this._context._for(this,_) }
13
13
  get spawn() { return super.spawn = this._context.spawn }
14
14
 
15
- emit (eve, ...args) {
16
- if (eve === 'served') return Promise.all (this.listeners(eve).map (l => l.call(this,...args)))
15
+ async emit (eve, ...args) {
16
+ // if (eve === 'served') return Promise.all (this.listeners(eve).map (l => l.call(this,...args)))
17
+ if (eve === 'served') for (let l of this.listeners(eve)) await l.call(this,...args)
17
18
  else return super.emit (eve, ...args)
18
19
  }
19
20
  }
@@ -93,6 +94,7 @@ const cds = module.exports = extend (new facade) .with ({
93
94
  test: require ('./utils/cds-test'),
94
95
  log: require ('./log/cds-log'), debug: lazy => cds.log.debug,
95
96
  exec: require ('../bin/cds'),
97
+ exit: (code) => cds.shutdown ? cds.shutdown() : process.exit(code),
96
98
  clone: m => JSON.parse (JSON.stringify(m)),
97
99
  lazified, lazify,
98
100
 
@@ -47,9 +47,9 @@ module.exports = new class extends AsyncLocalStorage {
47
47
  })
48
48
  }
49
49
  const em = new EventEmitter; em.timer = (
50
- (o && o.after) ? setTimeout(fx, o.after) :
51
- (o && o.every) ? setInterval(fx, o.every) :
52
- setImmediate(fx)
50
+ (o && o.after) ? setTimeout(fx, o.after).unref() :
51
+ (o && o.every) ? setInterval(fx, o.every).unref() :
52
+ setImmediate(fx).unref()
53
53
  )
54
54
  return em
55
55
  }
@@ -68,8 +68,7 @@ module.exports = class Bindings {
68
68
  }
69
69
  }
70
70
  }
71
- cds.on ('shutdown', ()=>this.purge())
72
- process.on ('exit', ()=>this.purge()) // last resort e.g. in case of errors
71
+ process.on ('exit', ()=>this.purge())
73
72
  return this.store()
74
73
  }
75
74
 
@@ -56,7 +56,8 @@ module.exports = function cds_serve (som, _options) { // NOSONAR
56
56
  if (o.service && o.service !== 'all') {
57
57
  // skip services not chosen by o.service, if specified
58
58
  const specified = o.service.split(/\s*,\s*/).map (s => required[s] && required[s].service || s )
59
- services = services.filter (s => specified.some (n => s.name.endsWith(n)))
59
+ // matching exact or unqualified name
60
+ services = services.filter (s => specified.some (n => s.name === n || s.name.endsWith('.'+n)))
60
61
  if (!services.length) throw cds.error (`No such service: '${o.service}'`)
61
62
  }
62
63
  services = services.filter (d => !(
@@ -66,15 +66,22 @@ function _instrument_sqlite (_get_perf) {
66
66
  try { require.resolve('sqlite3') } catch { return }
67
67
  // eslint-disable-next-line cds/no-missing-dependencies
68
68
  const sqlite = require('sqlite3').Database.prototype
69
- for (let each of ['all', 'get', 'run', 'prepare']) {
70
- const _super = sqlite[each]
71
- sqlite[each] = function (q, ..._) {
72
- const perf = _get_perf(q)
69
+ for (let each of ['all', 'get', 'run', 'prepare']) _wrap(each,sqlite)
70
+ function _wrap (op,sqlite) {
71
+ const _super = sqlite[op]
72
+ sqlite[op] = function (q, ..._) {
73
+ const perf = _get_perf(q) //> q is a SQL command like BEGIN, COMMIT, ROLLBACK, SELECT ...
73
74
  if (perf) {
74
- const pe = perf.log ('sqlite -', q)
75
- const callback = _[_.length-1]; _[_.length-1] = function(){
76
- perf.done(pe)
77
- callback.apply (this, arguments)
75
+ const pe = perf.log ('sqlite3', '-', q)
76
+ const callback = _[_.length-1]; _[_.length-1] = function(ps){
77
+ if (op === 'prepare') callback.apply (this, {
78
+ all: _wrap('all',ps),
79
+ get: _wrap('get',ps),
80
+ run: _wrap('run',ps),
81
+ }); else {
82
+ perf.done(pe)
83
+ callback.apply (this, arguments)
84
+ }
78
85
  }
79
86
  }
80
87
  return _super.call (this, q, ..._)
@@ -87,14 +94,23 @@ function _instrument_better_sqlite (_get_perf) {
87
94
  try { require.resolve('better-sqlite3') } catch { return }
88
95
  // eslint-disable-next-line cds/no-missing-dependencies
89
96
  const sqlite = require('better-sqlite3').prototype
90
- for (let each of ['exec', 'prepare']) {
91
- const _super = sqlite[each]
92
- sqlite[each] = function (q, ..._) {
93
- const perf = _get_perf(q)
97
+ for (let each of ['exec', 'prepare']) _wrap(each,sqlite)
98
+ function _wrap (op,sqlite) {
99
+ const _super = sqlite[op]
100
+ sqlite[op] = function (q, ..._) {
101
+ const perf = _get_perf(q) //> q is a SQL command like BEGIN, COMMIT, ROLLBACK, SELECT ...
94
102
  if (perf) {
95
- const pe = perf.log ('sqlite -', q)
96
- try { return _super.call (this, q, ..._) }
97
- finally { perf.done(pe) }
103
+ const pe = perf.log ('better-sqlite3', '-', q)
104
+ try {
105
+ const x = _super.call (this, q, ..._)
106
+ if (op === 'prepare') return {
107
+ all(..._){ try { return x.all(..._) } finally { perf.done(pe) }},
108
+ get(..._){ try { return x.get(..._) } finally { perf.done(pe) }},
109
+ run(..._){ try { return x.run(..._) } finally { perf.done(pe) }},
110
+ }
111
+ else return x
112
+ }
113
+ catch(e) { perf.done(pe); throw e }
98
114
  }
99
115
  else return _super.call (this, q, ..._)
100
116
  }
@@ -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) {
@@ -61,7 +61,8 @@ class ExtendedModels {
61
61
  try {
62
62
  _has_extensions = tenant && extensibility && await _is_extended(tenant)
63
63
  } catch (error) {
64
- LOG.error('`extensibility: true` is configured but table "cds.xt.Extensions" does not exist. Please redeploy.', 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)
65
66
  }
66
67
  if (!_has_extensions) {
67
68
  let k = cache.key4 (tenant = undefined, features)
@@ -151,8 +152,7 @@ class ExtendedModels {
151
152
  if (Date.now() - m._cached.touched > ExtendedModels.sentinelInterval) {
152
153
  delete this [key]
153
154
  }
154
- }}, ExtendedModels.sentinelInterval)
155
- cds.on('shutdown', ()=> clearInterval(this.sentinel))
155
+ }}, ExtendedModels.sentinelInterval).unref()
156
156
  }
157
157
 
158
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
 
@@ -66,7 +66,7 @@ module.exports = function ias_auth(config) {
66
66
  req.tenant = req.tokenInfo.getZoneId()
67
67
  next()
68
68
  })
69
- .use((err, req, res, next) => {
69
+ .use((err, req, res, _next) => {
70
70
  if (req.tokenInfo) {
71
71
  LOG?.debug('error during token validation', req.tokenInfo.getErrorObject())
72
72
  }
@@ -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
  }
@@ -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