@sap/cds 9.5.1 → 9.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.
@@ -8,14 +8,19 @@ const taskRunner = new TaskRunner()
8
8
 
9
9
  const { expBkfFix: waitingTime } = require('../_runtime/common/utils/waitingTime')
10
10
 
11
+ const QUEUE = 'queue'
11
12
  const PROCESSING = 'processing'
13
+
12
14
  const INTERNAL_USER = 'cds.internal.user'
15
+ // prettier-ignore
16
+ const TO_COPY = ['inbound', 'event', 'data', 'headers', 'queue', 'results', 'method', 'path', 'params', 'entity', 'service']
13
17
 
14
18
  const $taskProcessorRegistered = Symbol('task processor registered')
15
19
  const $queued = Symbol('queued')
16
20
  const $unqueued = Symbol('unqueued')
17
21
  const $stored_reqs = Symbol('stored_reqs')
18
22
  const $error = Symbol('error')
23
+ const $service = Symbol('service')
19
24
 
20
25
  const _get100NanosecondTimestampISOString = (offset = 0) => {
21
26
  const [now, nanoseconds] = [new Date(Date.now() + offset), process.hrtime()[1]]
@@ -58,31 +63,30 @@ const _targetName = (name, opts) => (opts.targetPrefix ? opts.targetPrefix + nam
58
63
  // Process messages (in parallel or sequentially)
59
64
  // tx2 (legacyLocking: tx1): Update/Delete messages based on outcome, set status to null
60
65
  //
61
- const processTasks = (service, tenant, _opts = {}) => {
66
+ const _processTasks = (target, tenant, _opts = {}) => {
62
67
  const opts = Object.assign({ attempt: 0 }, _opts)
63
68
  if (!opts.parallel) opts.chunkSize = 1
64
69
 
65
70
  // Add the current done callback if provided
66
71
  if (opts.done) {
67
- taskRunner.addCallback({ name: service.name, tenant }, opts.done)
72
+ taskRunner.addCallback({ name: target, tenant }, opts.done)
68
73
  delete opts.done // Remove to avoid re-adding in recursive calls
69
74
  }
70
75
 
71
- const name = service.name
72
76
  const tasksEntity = _getTasksEntity()
73
77
 
74
78
  let letAppCrash = false
75
79
 
76
80
  const __done = () => {
77
81
  if (letAppCrash) cds.exit(1)
78
- taskRunner.end({ name, tenant }, () => processTasks(service, tenant, opts))
82
+ taskRunner.end({ name: target, tenant }, () => _processTasks(target, tenant, opts))
79
83
  }
80
84
  const _done = () => {
81
85
  if (!opts.legacyLocking) __done()
82
86
  // else will be handled in spawn
83
87
  }
84
88
 
85
- return taskRunner.run({ name, tenant }, () => {
89
+ return taskRunner.run({ name: target, tenant }, () => {
86
90
  const config = tenant ? { tenant, user: cds.User.privileged } : { user: cds.User.privileged }
87
91
  config.after = 1 // make sure spawn puts its cb on the `timer` queue (via setTimeout), which is also used by `taskRunner`
88
92
 
@@ -102,8 +106,12 @@ const processTasks = (service, tenant, _opts = {}) => {
102
106
  else currMinWaitingTime = Math.min(currMinWaitingTime, time)
103
107
  }
104
108
 
109
+ const whereClause = { target: _targetName(target, opts) }
110
+ // Fallback: To flush messages which have been stored with target = service.name
111
+ if (target === QUEUE && opts[$service]) whereClause.or = { target: _targetName(opts[$service], opts) }
112
+
105
113
  const tasksQuery = SELECT.from(tasksEntity)
106
- .where({ target: _targetName(name, opts) })
114
+ .where(whereClause)
107
115
  .orderBy(opts.parallel ? ['status', 'timestamp', 'ID'] : ['timestamp', 'status', 'ID'])
108
116
  .limit(opts.chunkSize)
109
117
  .forUpdate()
@@ -117,7 +125,7 @@ const processTasks = (service, tenant, _opts = {}) => {
117
125
  // it would (in the worst case) select <chunkSize> messages which are all in process
118
126
  // and not yet timed out, hence only <chunkSize> messages could be processed at any given time.
119
127
  }
120
- LOG._debug && LOG.debug(`${name}: Fetch messages`)
128
+ LOG._debug && LOG.debug(`${target}: Fetch messages`)
121
129
  try {
122
130
  // Use dedicated transaction to fetch relevant messages
123
131
  // and _immediately_ set their status to 'processing' and commit
@@ -154,7 +162,7 @@ const processTasks = (service, tenant, _opts = {}) => {
154
162
  // Note: There's also no scheduling for tasks which are not yet timed out.
155
163
  if (!processableTasks.length) return [] // all in process
156
164
  // prettier-ignore
157
- LOG._debug && LOG.debug(`${name}: Process ${processableTasks.length} ${processableTasks.length > 1 ? 'messages' : 'message'}`)
165
+ LOG._debug && LOG.debug(`${target}: Process ${processableTasks.length} ${processableTasks.length > 1 ? 'messages' : 'message'}`)
158
166
  if (!opts.legacyLocking) {
159
167
  await UPDATE(tasksEntity)
160
168
  .set({ status: PROCESSING })
@@ -166,44 +174,53 @@ const processTasks = (service, tenant, _opts = {}) => {
166
174
  // could potentially be a timeout
167
175
  const _waitingTime = waitingTime(opts.attempt)
168
176
  // prettier-ignore
169
- LOG.error(`${name}: Message retrieval failed`, e, `Retry${_waitingTime > 0 ? ` in ${Math.round(_waitingTime / 1000)} s` : ''}`)
177
+ LOG.error(`${target}: Message retrieval failed`, e, `Retry${_waitingTime > 0 ? ` in ${Math.round(_waitingTime / 1000)} s` : ''}`)
170
178
  taskRunner.plan(
171
179
  {
172
- name,
180
+ name: target,
173
181
  tenant,
174
182
  waitingTime: _waitingTime
175
183
  },
176
- () => processTasks(service, tenant, { ...opts, attempt: opts.attempt + 1 })
184
+ () => _processTasks(target, tenant, { ...opts, attempt: opts.attempt + 1 })
177
185
  )
178
186
  return _done()
179
187
  }
188
+
180
189
  const tasksGen = function* () {
181
- for (const task of selectedTasks) {
182
- const _msg = _safeJSONParse(task.msg)
190
+ for (const _task of selectedTasks) {
191
+ const _msg = _safeJSONParse(_task.msg)
192
+
183
193
  const context = _msg.context || {}
184
- const userId = _msg[INTERNAL_USER]
185
- if (_msg.query) {
186
- const q = (_msg.query = cds.ql(_msg.query))
187
- q.bind(service)
188
- _msg.target = cds.infer.target(q)
189
- }
194
+ delete _msg.context
190
195
 
191
- const msg = _msg._fromSend ? new cds.Request(_msg) : new cds.Event(_msg)
192
- delete msg._fromSend
193
- delete msg[INTERNAL_USER]
196
+ const userId = _msg[INTERNAL_USER]
197
+ delete _msg[INTERNAL_USER]
194
198
  const user = new cds.User.Privileged(userId)
195
199
  context.user = user
200
+
201
+ // The latter is for backwards compatibility where `service` was not stored and `target` was the service name
202
+ const srvName = _msg.service || _task.target
203
+ delete _msg.service
204
+
205
+ const fromSend = _msg._fromSend
206
+ delete _msg._fromSend
207
+
208
+ const msg = fromSend ? new cds.Request(_msg) : new cds.Event(_msg)
196
209
  if (!msg) continue
197
- const res = {
198
- ID: task.ID,
210
+
211
+ const task = {
212
+ ID: _task.ID,
199
213
  msg,
200
214
  context,
201
- attempts: task.attempts || 0
215
+ srvName,
216
+ attempts: _task.attempts || 0
202
217
  }
203
- yield res
218
+
219
+ yield task
204
220
  }
205
221
  }
206
222
 
223
+ const succeeded = []
207
224
  const toBeDeleted = []
208
225
  const toBeUpdated = []
209
226
  const toBeCreated = []
@@ -212,17 +229,26 @@ const processTasks = (service, tenant, _opts = {}) => {
212
229
  let minAttemptFailed
213
230
  try {
214
231
  const _handleWithErr = async task => {
232
+ const service = cds.unqueued(await cds.connect.to(task.srvName))
215
233
  try {
234
+ if (task.msg.query) {
235
+ const q = (task.msg.query = cds.ql(task.msg.query))
236
+ q.bind(service)
237
+ task.msg.target = cds.infer.target(q)
238
+ }
239
+
216
240
  // REVISIT: Shouldn't that work like a standard inbound adapter? I.e. either of:
217
241
  // - cds._with({...}, ()=> srv.dispatch(task.msg)) // instead of srv.handle(task.msg)
218
242
  // - cds.tx({...}, ()=> srv.dispatch(task.msg)) // instead of srv.handle(task.msg)
219
243
  // Problem: If task involves db, dedicated transactions will block on SQLite if an outer transaction is open
220
244
  const _run = opts.legacyLocking && cds.db?.kind === 'sqlite' ? cds._with : service.tx.bind(service)
221
245
  const result = await _run({ ...task.context, tenant }, async () => {
222
- return opts.handle ? await opts.handle.call(service, task.msg) : await service.handle(task.msg)
246
+ const result = opts.handle ? await opts.handle.call(service, task.msg) : await service.handle(task.msg)
247
+ await DELETE.from(tasksEntity).where({ ID: task.ID })
248
+ return result
223
249
  })
224
250
  task.results = result
225
- toBeDeleted.push(task)
251
+ succeeded.push(task)
226
252
  } catch (e) {
227
253
  if (!minAttemptFailed) minAttemptFailed = task.attempts
228
254
  else minAttemptFailed = Math.min(minAttemptFailed, task.attempts)
@@ -253,7 +279,7 @@ const processTasks = (service, tenant, _opts = {}) => {
253
279
  const res = await Promise.allSettled([...tasks].map(_handleWithErr))
254
280
  const errors = res.filter(r => r.status === 'rejected').map(r => r.reason)
255
281
  if (errors.length) {
256
- throw new Error(`${service.name}: Programming errors detected.`)
282
+ throw new Error(`${target}: Programming errors detected.`)
257
283
  }
258
284
  } else {
259
285
  // In principle, this branch is not needed as for `parallel == false`, there's only one chunk at a time,
@@ -279,7 +305,10 @@ const processTasks = (service, tenant, _opts = {}) => {
279
305
 
280
306
  // There can be tasks which are not updated / deleted, their status must be set back to `null`
281
307
  const updateTasks = selectedTasks.filter(
282
- task => !toBeDeleted.some(t => t.ID === task.ID) && !toBeUpdated.some(t => t.ID === task.ID)
308
+ task =>
309
+ !toBeDeleted.some(t => t.ID === task.ID) &&
310
+ !succeeded.some(t => t.ID === task.ID) &&
311
+ !toBeUpdated.some(t => t.ID === task.ID)
283
312
  )
284
313
  if (updateTasks.length) {
285
314
  queries.push(
@@ -290,7 +319,7 @@ const processTasks = (service, tenant, _opts = {}) => {
290
319
  }
291
320
 
292
321
  for (const each of toBeUpdated) {
293
- if (toBeDeleted.some(d => d.ID === each.ID)) continue
322
+ if (toBeDeleted.some(d => d.ID === each.ID) || succeeded.some(d => d.ID === each.ID)) continue
294
323
  each.updateData.status = null
295
324
  if (opts.storeLastError !== false) each.updateData.lastError = inspect(each[$error])
296
325
  if (each.updateData.lastError && typeof each.updateData.lastError !== 'string') {
@@ -312,7 +341,7 @@ const processTasks = (service, tenant, _opts = {}) => {
312
341
  return newMsg
313
342
  }
314
343
 
315
- const _failed = task => {
344
+ const _failed = async task => {
316
345
  const msg = _newMsgFrom(task.msg)
317
346
  msg.event = msg.event + '/#failed'
318
347
  const _errorToObj = error => {
@@ -326,25 +355,27 @@ const processTasks = (service, tenant, _opts = {}) => {
326
355
  }
327
356
  }
328
357
  msg.results = _errorToObj(task[$error])
358
+ const service = await cds.connect.to(task.srvName)
329
359
  if (service.handlers.on.some(h => h.for(msg)) || service.handlers.after.some(h => h.for(msg))) {
330
- toBeCreated.push(_createTask(service.name, msg, task.context, opts))
360
+ toBeCreated.push(_createTask(target, msg, task.context, task.srvName, opts))
331
361
  }
332
362
  }
333
363
 
334
- const _succeeded = task => {
364
+ const _succeeded = async task => {
335
365
  const msg = _newMsgFrom(task.msg)
336
366
  msg.event = msg.event + '/#succeeded'
367
+ const service = await cds.connect.to(task.srvName)
337
368
  if (service.handlers.on.some(h => h.for(msg)) || service.handlers.after.some(h => h.for(msg))) {
338
- toBeCreated.push(_createTask(service.name, msg, task.context, opts))
369
+ toBeCreated.push(_createTask(target, msg, task.context, task.srvName, opts))
339
370
  }
340
371
  }
341
372
 
342
- for (const task of toBeDeleted) {
373
+ for (const task of succeeded) {
343
374
  // invoke succeeded handlers
344
375
  if (!task.msg.event.endsWith('/#succeeded') && !task.msg.event.endsWith('/#failed')) {
345
376
  if (!task.error) {
346
377
  // skip programming errors & unrecoverable without maxAttempts
347
- _succeeded(task)
378
+ await _succeeded(task)
348
379
  }
349
380
  }
350
381
 
@@ -352,7 +383,7 @@ const processTasks = (service, tenant, _opts = {}) => {
352
383
  if (task.msg.queue?.every) {
353
384
  const _m = { ...task.msg }
354
385
  _m._fromSend = task.msg instanceof cds.Request
355
- const _task = _createTask(service.name, _m, task.context, opts)
386
+ const _task = _createTask(target, _m, task.context, task.srvName, opts)
356
387
  _task.timestamp = _get100NanosecondTimestampISOString(task.msg.queue.every)
357
388
  toBeCreated.push(_task)
358
389
  }
@@ -365,7 +396,7 @@ const processTasks = (service, tenant, _opts = {}) => {
365
396
  opts.maxAttempts &&
366
397
  task.updateData.attempts >= opts.maxAttempts
367
398
  ) {
368
- _failed(task)
399
+ await _failed(task)
369
400
  }
370
401
  }
371
402
 
@@ -374,40 +405,40 @@ const processTasks = (service, tenant, _opts = {}) => {
374
405
  if (queries.length) {
375
406
  await _tx(() => Promise.all(queries))
376
407
  // prettier-ignore
377
- LOG._debug && LOG.debug(`${name}: Messages modified (-${toBeDeleted.length}, ~${toBeUpdated.length + updateTasks.length}, +${toBeCreated.length})`)
408
+ LOG._debug && LOG.debug(`${target}: Messages modified (-${toBeDeleted.length + succeeded.length}, ~${toBeUpdated.length + updateTasks.length}, +${toBeCreated.length})`)
378
409
  }
379
410
 
380
411
  if (letAppCrash) return _done()
381
412
 
382
413
  if (toBeUpdated.length) {
383
- LOG.error(`${name}: Some messages could not be processed`)
414
+ LOG.error(`${target}: Some messages could not be processed`)
384
415
  _setWaitingTime(waitingTime(minAttemptFailed + 1))
385
416
  }
386
- if (toBeDeleted.length === opts.chunkSize || toBeCreated.length) {
417
+ if (toBeDeleted.length + succeeded.length === opts.chunkSize || toBeCreated.length) {
387
418
  _setWaitingTime(0)
388
419
  }
389
420
  if (currMinWaitingTime !== undefined) {
390
421
  // prettier-ignore
391
- LOG._debug && LOG.debug(`${name}: Process${currMinWaitingTime > 0 ? ` in ${Math.round(currMinWaitingTime / 1000)} s` : ''}`)
422
+ LOG._debug && LOG.debug(`${target}: Process${currMinWaitingTime > 0 ? ` in ${Math.round(currMinWaitingTime / 1000)} s` : ''}`)
392
423
  taskRunner.plan(
393
424
  {
394
- name,
425
+ name: target,
395
426
  tenant,
396
427
  waitingTime: currMinWaitingTime
397
428
  },
398
- () => processTasks(service, tenant, opts)
429
+ () => _processTasks(target, tenant, opts)
399
430
  )
400
431
  return _done()
401
432
  }
402
433
 
403
- LOG._debug && LOG.debug(`${name}: Done`)
434
+ LOG._debug && LOG.debug(`${target}: Done`)
404
435
  return _done()
405
436
  }, config)
406
437
  _end(spawn)
407
438
  })
408
439
  }
409
440
 
410
- const registerTaskProcessor = (name, context) => {
441
+ const _registerTaskProcessor = (name, context) => {
411
442
  const registry = context[$taskProcessorRegistered] || (context[$taskProcessorRegistered] = new Set())
412
443
  if (!registry.has(name)) {
413
444
  registry.add(name)
@@ -416,8 +447,13 @@ const registerTaskProcessor = (name, context) => {
416
447
  return false
417
448
  }
418
449
 
419
- const _createTask = (name, msg, context, taskOpts) => {
420
- const _msg = { [INTERNAL_USER]: context.user.id }
450
+ const _createTask = (name, msg, context, serviceName, taskOpts) => {
451
+ // REVISIT: use something like $cds for control data
452
+ const _msg = {
453
+ [INTERNAL_USER]: context.user.id,
454
+ service: serviceName
455
+ }
456
+
421
457
  const _newContext = {}
422
458
  for (const key in context) {
423
459
  if (!taskOpts.ignoredContext.includes(key)) _newContext[key] = context[key]
@@ -425,9 +461,9 @@ const _createTask = (name, msg, context, taskOpts) => {
425
461
  _msg.context = _newContext
426
462
 
427
463
  if (msg._fromSend || msg.reply) _msg._fromSend = true // send or emit?
428
- for (const prop of ['inbound', 'event', 'data', 'headers', 'queue', 'results', 'method', 'path', 'params', 'entity']) {
429
- if (msg[prop]) _msg[prop] = msg[prop]
430
- }
464
+
465
+ for (const prop of TO_COPY) if (msg[prop]) _msg[prop] = msg[prop]
466
+
431
467
  if (msg.query) {
432
468
  _msg.query = typeof msg.query.flat === 'function' ? msg.query.flat() : msg.query
433
469
  delete _msg.query._target
@@ -435,17 +471,19 @@ const _createTask = (name, msg, context, taskOpts) => {
435
471
  delete _msg.query.target
436
472
  delete _msg.data // `req.data` should be a getter to whatever is in `req.query`
437
473
  }
474
+
438
475
  const taskMsg = {
439
476
  ID: cds.utils.uuid(),
440
477
  target: _targetName(name, taskOpts),
441
478
  timestamp: _get100NanosecondTimestampISOString(msg.queue?.after), // needs to be different for each emit
442
479
  msg: JSON.stringify(_msg)
443
480
  }
481
+
444
482
  return taskMsg
445
483
  }
446
484
 
447
- const writeInQueue = async (name, msg, context, taskOpts) => {
448
- const taskMsg = _createTask(name, msg, context, taskOpts)
485
+ const _writeInQueue = async (name, msg, context, serviceName, taskOpts) => {
486
+ const taskMsg = _createTask(name, msg, context, serviceName, taskOpts)
449
487
  const tasksEntity = _getTasksEntity()
450
488
  LOG._debug && LOG.debug(`${name}: Write message to queue`)
451
489
  return INSERT.into(tasksEntity).entries(taskMsg)
@@ -455,7 +493,14 @@ exports.unqueued = function unqueued(srv) {
455
493
  return srv[$unqueued] || srv
456
494
  }
457
495
 
458
- exports.queued = function queued(srv, customOpts) {
496
+ exports.queued = function queued(srv, opts) {
497
+ // REVISIT: officially deprecate `outbox`
498
+ if ('outbox' in cds.requires) {
499
+ if (typeof cds.requires.outbox === 'object') Object.assign(cds.requires.queue, cds.requires.outbox)
500
+ else cds.requires.queue = cds.requires.outbox
501
+ }
502
+ if ('outbox' in srv.options) srv.options.outboxed = srv.options.outbox
503
+
459
504
  // queue max. once
460
505
  if (!new.target) {
461
506
  const former = srv[$queued]
@@ -468,28 +513,17 @@ exports.queued = function queued(srv, customOpts) {
468
513
 
469
514
  if (!new.target) Object.defineProperty(srv, $queued, { value: queuedSrv })
470
515
 
471
- let requiresOpts = cds.requires.queue
472
- if (cds.requires.outbox !== undefined) {
473
- cds.utils.deprecated({ old: 'cds.requires.outbox', use: 'cds.requires.queue' })
474
- requiresOpts = cds.requires.outbox ? Object.assign({}, requiresOpts, cds.requires.outbox) : false
475
- }
476
-
477
- let serviceOpts = srv.options?.queued ?? srv.options?.outboxed
478
- if (srv.options?.outbox !== undefined) {
479
- // REVISIT: cds.utils.deprecated({ old: 'cds.requires.<srv>.outbox', use: 'cds.requires.<srv>.queued or cds.requires.<srv>.outboxed' })
480
- serviceOpts = srv.options.outbox ? Object.assign({}, serviceOpts, srv.options.outbox) : false
516
+ const queueCfg = srv.options?.queued ?? srv.options?.outboxed
517
+ if (queueCfg && typeof queueCfg === 'object') {
518
+ if (opts) opts = Object.assign(queueCfg, opts)
519
+ else opts = queueCfg
481
520
  }
521
+ const queueOpts = opts
522
+ ? Object.assign({}, cds.requires.queue, opts)
523
+ : cds.requires[typeof queueCfg === 'string' ? queueCfg : QUEUE]
524
+ const queueName = opts ? srv.name : typeof queueCfg === 'string' ? queueCfg : QUEUE // Generated name for the queue of that service
482
525
 
483
- if (typeof requiresOpts === 'string') requiresOpts = { kind: requiresOpts }
484
- if (typeof serviceOpts === 'string') serviceOpts = { kind: serviceOpts }
485
-
486
- const queueOpts = Object.assign(
487
- {},
488
- (typeof requiresOpts === 'object' && requiresOpts) || {},
489
- (typeof serviceOpts === 'object' && serviceOpts) || {},
490
- customOpts || {}
491
- )
492
-
526
+ // REVISIT: shouldn't it be "queued"? -> normalize during bootstrapping and here (for ad-hoc queue config)
493
527
  queuedSrv.outboxed = queueOpts // Store effective outbox configuration (e.g. used in telemetry)
494
528
 
495
529
  queuedSrv.handle = async function (req) {
@@ -499,15 +533,15 @@ exports.queued = function queued(srv, customOpts) {
499
533
  _hasPersistentQueue(context.tenant)
500
534
  ) {
501
535
  // returns true if not yet registered
502
- if (registerTaskProcessor(srv.name, context)) {
536
+ if (_registerTaskProcessor(srv.name, context)) {
503
537
  // NOTE: What if there are different queue options for the same service?!
504
538
  // There could be tasks for srv1 with { maxAttempts: 1 }
505
539
  // and tasks for srv1 with { maxAttempts: 9 }.
506
540
  // How would they be processed? I'd rather not have dedicated
507
541
  // service names or store serialized options for each task.
508
- context.on('succeeded', () => processTasks(originalSrv, context.tenant, queueOpts))
542
+ context.on('succeeded', () => _processTasks(queueName, context.tenant, queueOpts))
509
543
  }
510
- await writeInQueue(srv.name, req, context, queueOpts)
544
+ await _writeInQueue(queueName, req, context, srv.name, queueOpts)
511
545
  return
512
546
  }
513
547
 
@@ -533,13 +567,22 @@ exports.queued = function queued(srv, customOpts) {
533
567
  context[$stored_reqs].push(req)
534
568
  }
535
569
 
536
- queuedSrv.flush = function flush(tenant = cds.context?.tenant, opts) {
537
- // Flush resolves once there's nothing to do _now_. But there can still be
538
- // messages with _future_ timestamps. Those are then planned with the scheduler service.
539
- return new Promise(resolve =>
540
- processTasks(originalSrv, tenant, Object.assign({ done: resolve }, queueOpts, opts))
541
- )
570
+ // REVISIT:
571
+ // Not great to have `flush` on the service, because it will flush the corresponding queue.
572
+ // A better API would be: (await cds.connect.to('queueName')).flush()
573
+ // But the current logic of connect.to doesn't allow having a model but no service.
574
+ queuedSrv.flush = function flush(opts) {
575
+ queueOpts[$service] = srv.name
576
+ const tenant = cds.context?.tenant
577
+ return new Promise(resolve => _processTasks(queueName, tenant, Object.assign({ done: resolve }, queueOpts, opts)))
542
578
  }
543
579
 
544
580
  return queuedSrv
545
581
  }
582
+
583
+ exports.cdsFlush = function cdsFlush(target, opts) {
584
+ if (typeof target === 'object') [opts, target] = [target, QUEUE]
585
+ const tenant = cds.context?.tenant
586
+ const queueOpts = Object.assign({}, cds.requires.queue, cds.requires[target] || {}, opts || {})
587
+ return new Promise(resolve => _processTasks(target, tenant, Object.assign({ done: resolve }, queueOpts)))
588
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sap/cds",
3
- "version": "9.5.1",
3
+ "version": "9.6.0",
4
4
  "description": "SAP Cloud Application Programming Model - CDS for Node.js",
5
5
  "homepage": "https://cap.cloud.sap/",
6
6
  "keywords": [
@@ -34,7 +34,7 @@
34
34
  "dependencies": {
35
35
  "@sap/cds-compiler": "^6.3",
36
36
  "@sap/cds-fiori": "^2",
37
- "js-yaml": "^4.1.0"
37
+ "js-yaml": "^4.1.1"
38
38
  },
39
39
  "peerDependencies": {
40
40
  "@eslint/js": "^9",
package/srv/outbox.cds CHANGED
@@ -10,4 +10,6 @@ entity Messages {
10
10
  lastError : LargeString;
11
11
  lastAttemptTimestamp : Timestamp @cds.on.update: $now;
12
12
  status : String(23);
13
+ task : String(255);
14
+ appid : String(255);
13
15
  }
@@ -5,12 +5,14 @@ const authorizedRequest = require('../libx/_runtime/messaging/common-utils/autho
5
5
  const cds = require('../libx/_runtime/cds.js')
6
6
  const LOG = cds.log('messaging')
7
7
 
8
- const VCAPSERVICESstr = process.env.VCAP_SERVICES
9
- if (!VCAPSERVICESstr) throw new Error('Please provide environment variable `VCAP_SERVICES`')
10
- const VCAPSERVICES = JSON.parse(process.env.VCAP_SERVICES)
8
+ let vcap_services = process.env.VCAP_SERVICES_FILE_PATH
9
+ ? cds.utils.fs.readFileSync(process.env.VCAP_SERVICES_FILE_PATH, 'utf-8')
10
+ : process.env.VCAP_SERVICES
11
+ if (!vcap_services) throw new Error('Please provide environment variable `VCAP_SERVICES`')
12
+ vcap_services = JSON.parse(vcap_services)
11
13
 
12
- const xsuaaSrv = Object.keys(VCAPSERVICES)
13
- .map(k => VCAPSERVICES[k])
14
+ const xsuaaSrv = Object.keys(vcap_services)
15
+ .map(k => vcap_services[k])
14
16
  .map(e => e[0])
15
17
  .find(srv => srv.label === 'xsuaa')
16
18
 
@@ -1,13 +0,0 @@
1
- const { performance } = require ('perf_hooks')
2
-
3
- module.exports = (prec = 1000) => function sap_statistics (req, res, next) {
4
- if (req.query['sap-statistics'] || req.headers['sap-statistics']) {
5
- const { writeHead } = res, t0 = performance.now()
6
- res.writeHead = function (...args) {
7
- const total = ((performance.now() - t0) / prec).toFixed(2)
8
- if (res.statusCode < 400) res.setHeader('sap-statistics', `total=${total}`)
9
- writeHead.call(this, ...args)
10
- }
11
- }
12
- next()
13
- }
@@ -1,2 +0,0 @@
1
- // for compatibility with old releases of cds-lint
2
- module.exports = require('./cds-utils')
@@ -1,21 +0,0 @@
1
- const cds = require('../../cds')
2
-
3
- // register after input.js in order to write content-type also for @Core.Computed fields
4
- module.exports = cds.service.impl(function () {
5
- this.before('UPDATE', '*', function fill_media_types(req) {
6
- if (!req.data || !req.target) return
7
- if (_is_field_request(req)) {
8
- for (let e in req.data) {
9
- let ref = _media_type_ref4(req.target.elements[e])
10
- if (ref) {
11
- let content_type = req.req?.get('content-type')
12
- if (_is_valid(content_type)) req.data[ref] = content_type
13
- }
14
- }
15
- }
16
- })
17
- })
18
-
19
- const _media_type_ref4 = media_element => media_element['@Core.MediaType']?.['=']
20
- const _is_field_request = req => req.req?._query?._propertyAccess
21
- const _is_valid = content_type => content_type && !content_type.includes('multipart')
@@ -1,79 +0,0 @@
1
- //
2
- // REVISIT: Not used any longer -> move to @sap/cds-attic ...
3
- //
4
- const fs = require('fs')
5
- const path = require('path')
6
-
7
- const cds = require('../../cds')
8
-
9
- const dirs = (cds.env.i18n && cds.env.i18n.folders) || []
10
-
11
- const i18ns = {}
12
-
13
- function exists(args, locale) {
14
- const file = path.join(cds.root, ...args, locale ? `messages_${locale}.properties` : 'messages.properties')
15
- return fs.existsSync(file) ? file : undefined
16
- }
17
-
18
- function findFile(locale) {
19
- // lookup all paths to model files
20
- const prefixes = new Set()
21
- if (cds.env.folders && cds.env.folders.srv) prefixes.add(cds.env.folders.srv.replace(/\/$/, ''))
22
- if (cds.services) {
23
- for (const outer in cds.services) {
24
- if (cds.services[outer].definition && cds.services[outer].definition['@source']) {
25
- prefixes.add(path.dirname(cds.services[outer].definition['@source']))
26
- }
27
- }
28
- }
29
-
30
- let file
31
- // find first messages_${locale}.properties file in cds.env.i18n.folders
32
- for (const dir of dirs) {
33
- // w/o prefix
34
- file = exists([dir], locale)
35
- if (file) break
36
-
37
- // w/ prefix
38
- for (const prefix of prefixes.keys()) {
39
- file = exists([prefix, dir], locale)
40
- if (file) break
41
- }
42
-
43
- if (file) break
44
- }
45
-
46
- return file
47
- }
48
-
49
- function init(locale, file) {
50
- if (!i18ns[locale]) i18ns[locale] = {}
51
-
52
- if (!file) file = findFile(locale)
53
- if (!file) return
54
-
55
- const props = cds.load.properties(file)
56
- i18ns[locale] = props
57
- }
58
-
59
- init('default', path.resolve(__dirname, '../../../../_i18n/messages.properties'))
60
- init('')
61
-
62
- module.exports = (key, locale = '', args = {}) => {
63
- if (typeof locale !== 'string') {
64
- args = locale
65
- locale = ''
66
- }
67
-
68
- // initialize locale if not yet done
69
- if (!i18ns[locale]) {
70
- init(locale)
71
- }
72
-
73
- // for locale OR app default OR cds default
74
- let text = i18ns[locale][key] || i18ns[''][key] || i18ns.default[key]
75
- return text?.replace(/{(\w+)}/g, (_, k) => {
76
- let x = args[k]
77
- return i18ns[locale][x] || i18ns[''][x] || i18ns.default[x] || (x ?? 'NULL') // REVISIT: i'm afraid this twofold localization is a rather bad idea
78
- })
79
- }