@sap/cds 9.5.2 → 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.
- package/CHANGELOG.md +39 -1
- package/bin/args.js +3 -3
- package/bin/serve.js +18 -14
- package/lib/compile/for/lean_drafts.js +1 -1
- package/lib/compile/for/nodejs.js +2 -0
- package/lib/compile/parse.js +1 -1
- package/lib/core/linked-csn.js +23 -13
- package/lib/env/cds-env.js +6 -0
- package/lib/env/defaults.js +3 -1
- package/lib/index.js +2 -1
- package/lib/log/format/aspects/als.js +5 -1
- package/lib/log/format/aspects/cls.js +7 -3
- package/lib/log/service/index.js +5 -1
- package/lib/req/validate.js +1 -1
- package/lib/srv/cds.Service.js +37 -5
- package/libx/_runtime/common/generic/assert.js +1 -0
- package/libx/_runtime/common/generic/input.js +8 -2
- package/libx/_runtime/fiori/lean-draft.js +7 -3
- package/libx/_runtime/messaging/kafka.js +1 -0
- package/libx/odata/ODataAdapter.js +2 -1
- package/libx/odata/middleware/service-document.js +1 -1
- package/libx/odata/parse/afterburner.js +6 -2
- package/libx/odata/parse/cqn2odata.js +9 -9
- package/libx/odata/parse/grammar.peggy +6 -8
- package/libx/odata/parse/parser.js +1 -1
- package/libx/odata/utils/index.js +6 -2
- package/libx/queue/index.js +127 -84
- package/package.json +2 -2
- package/srv/outbox.cds +2 -0
- package/tasks/enterprise-messaging-deploy.js +7 -5
- package/lib/srv/middlewares/sap-statistics.js +0 -13
- package/lib/utils/index.js +0 -2
- package/libx/_runtime/common/generic/stream.js +0 -21
- package/libx/_runtime/common/i18n/index.js +0 -79
package/libx/queue/index.js
CHANGED
|
@@ -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
|
|
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:
|
|
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 }, () =>
|
|
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(
|
|
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(`${
|
|
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(`${
|
|
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(`${
|
|
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
|
-
() =>
|
|
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
|
|
182
|
-
const _msg = _safeJSONParse(
|
|
190
|
+
for (const _task of selectedTasks) {
|
|
191
|
+
const _msg = _safeJSONParse(_task.msg)
|
|
192
|
+
|
|
183
193
|
const context = _msg.context || {}
|
|
184
|
-
|
|
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
|
|
192
|
-
delete
|
|
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
|
-
|
|
198
|
-
|
|
210
|
+
|
|
211
|
+
const task = {
|
|
212
|
+
ID: _task.ID,
|
|
199
213
|
msg,
|
|
200
214
|
context,
|
|
201
|
-
|
|
215
|
+
srvName,
|
|
216
|
+
attempts: _task.attempts || 0
|
|
202
217
|
}
|
|
203
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(`${
|
|
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 =>
|
|
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(
|
|
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(
|
|
369
|
+
toBeCreated.push(_createTask(target, msg, task.context, task.srvName, opts))
|
|
339
370
|
}
|
|
340
371
|
}
|
|
341
372
|
|
|
342
|
-
for (const task of
|
|
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(
|
|
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(`${
|
|
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(`${
|
|
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(`${
|
|
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
|
-
() =>
|
|
429
|
+
() => _processTasks(target, tenant, opts)
|
|
399
430
|
)
|
|
400
431
|
return _done()
|
|
401
432
|
}
|
|
402
433
|
|
|
403
|
-
LOG._debug && LOG.debug(`${
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
429
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
472
|
-
if (
|
|
473
|
-
|
|
474
|
-
|
|
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
|
-
|
|
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 (
|
|
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', () =>
|
|
542
|
+
context.on('succeeded', () => _processTasks(queueName, context.tenant, queueOpts))
|
|
509
543
|
}
|
|
510
|
-
await
|
|
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
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
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.
|
|
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.
|
|
37
|
+
"js-yaml": "^4.1.1"
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
40
40
|
"@eslint/js": "^9",
|
package/srv/outbox.cds
CHANGED
|
@@ -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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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(
|
|
13
|
-
.map(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
|
-
}
|
package/lib/utils/index.js
DELETED
|
@@ -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
|
-
}
|