@sap/cds 9.8.0 → 9.8.2
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 +14 -0
- package/lib/compile/cds-compile.js +1 -0
- package/lib/compile/etc/properties.js +1 -0
- package/lib/compile/for/direct_crud.js +23 -0
- package/lib/compile/for/odata.js +1 -18
- package/lib/compile/to/edm.js +1 -0
- package/lib/env/cds-env.js +1 -1
- package/lib/i18n/index.js +2 -0
- package/lib/ql/cds.ql-infer.js +2 -0
- package/lib/req/response.js +2 -0
- package/lib/srv/middlewares/auth/mocked-users.js +1 -1
- package/lib/utils/cds-utils.js +1 -1
- package/libx/_runtime/messaging/redis-messaging.js +1 -1
- package/libx/_runtime/messaging/service.js +3 -1
- package/libx/odata/middleware/batch.js +19 -12
- package/libx/odata/parse/afterburner.js +0 -1
- package/libx/queue/index.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,20 @@
|
|
|
4
4
|
- The format is based on [Keep a Changelog](https://keepachangelog.com/).
|
|
5
5
|
- This project adheres to [Semantic Versioning](https://semver.org/).
|
|
6
6
|
|
|
7
|
+
## Version 9.8.2 - 2026-03-10
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Compatibility with `@eslint/js^10`
|
|
12
|
+
|
|
13
|
+
## Version 9.8.1 - 2026-03-09
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- OData batch parallel processing: Preserve request sequence for OData v2
|
|
18
|
+
- In inbound messaging, only load the extended model if there is a tenant
|
|
19
|
+
- Ensure `cds.fiori.direct_crud` is considered during `cds build`
|
|
20
|
+
|
|
7
21
|
## Version 9.8.0 - 2026-03-06
|
|
8
22
|
|
|
9
23
|
### Added
|
|
@@ -6,6 +6,7 @@ const compile = module.exports = Object.assign (cds_compile, {
|
|
|
6
6
|
for: new class {
|
|
7
7
|
get java(){ return super.java = require('./for/java') }
|
|
8
8
|
get nodejs() { return super.nodejs = require('./for/nodejs') }
|
|
9
|
+
get direct_crud() { return super.direct_crud = require('./for/direct_crud') }
|
|
9
10
|
get lean_drafts() { return super.lean_drafts = require('./for/lean_drafts') }
|
|
10
11
|
get flows() { return super.flows = require('./for/flows') }
|
|
11
12
|
get assert() { return super.assert = require('./for/assert') }
|
|
@@ -9,6 +9,7 @@ exports.read = function read (res, ext = '.properties', options) {
|
|
|
9
9
|
const src = fs.readFileSync(path.resolve(res),'utf-8')
|
|
10
10
|
return Object.defineProperty (exports.parse(src,options), '_source', {value:res})
|
|
11
11
|
} catch (e) {
|
|
12
|
+
// eslint-disable-next-line preserve-caught-error
|
|
12
13
|
if (e.code !== 'ENOENT') throw new Error (`Corrupt ${ext} file: ${res+ext}`)
|
|
13
14
|
}
|
|
14
15
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const $compiled_for_direct_crud = Symbol('compiled_for_direct_crud')
|
|
2
|
+
const DRAFT_NEW = 'draftNew'
|
|
3
|
+
|
|
4
|
+
module.exports = function cds_compile_for_direct_crud(csn) {
|
|
5
|
+
if (csn[$compiled_for_direct_crud]) return csn
|
|
6
|
+
csn[$compiled_for_direct_crud] = true
|
|
7
|
+
|
|
8
|
+
for (const each in csn.definitions) {
|
|
9
|
+
const def = csn.definitions[each]
|
|
10
|
+
if (!def['@Common.DraftRoot.NewAction'] && def['@odata.draft.enabled']) {
|
|
11
|
+
const srvName = Object.keys(csn.definitions)
|
|
12
|
+
.filter(k => csn.definitions[k].kind === 'service')
|
|
13
|
+
.find(k => each.startsWith(`${k}.`))
|
|
14
|
+
def['@Common.DraftRoot.NewAction'] = `${srvName}.${DRAFT_NEW}`
|
|
15
|
+
const params = { in: { items: { type: '$self' } } }
|
|
16
|
+
// for UI pop-up asking for values for non-UUID keys
|
|
17
|
+
Object.keys(def.elements)
|
|
18
|
+
.filter(k => k !== 'IsActiveEntity' && def.elements[k].key && def.elements[k].type !== 'cds.UUID')
|
|
19
|
+
.forEach(k => (params[k] = { type: def.elements[k].type }))
|
|
20
|
+
def.actions[DRAFT_NEW] = { kind: 'action', params, returns: { type: each } }
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
package/lib/compile/for/odata.js
CHANGED
|
@@ -9,24 +9,7 @@ module.exports = function cds_compile_for_odata (csn,_o) {
|
|
|
9
9
|
let dsn = compile.for.odata (csn,o)
|
|
10
10
|
if (o.sql_mapping) dsn['@sql_mapping'] = o.sql_mapping //> compat4 old Java stack
|
|
11
11
|
|
|
12
|
-
if (cds.env.fiori.direct_crud)
|
|
13
|
-
const DRAFT_NEW = 'draftNew'
|
|
14
|
-
for (const each in dsn.definitions) {
|
|
15
|
-
const def = dsn.definitions[each]
|
|
16
|
-
if (!def['@Common.DraftRoot.NewAction'] && def['@Common.DraftRoot.ActivationAction']) {
|
|
17
|
-
const srvName = Object.keys(dsn.definitions)
|
|
18
|
-
.filter(k => dsn.definitions[k].kind === 'service')
|
|
19
|
-
.find(k => each.startsWith(`${k}.`))
|
|
20
|
-
def['@Common.DraftRoot.NewAction'] = `${srvName}.${DRAFT_NEW}`
|
|
21
|
-
const params = { in: { items: { type: '$self' } } }
|
|
22
|
-
// for UI pop-up asking for values for non-UUID keys
|
|
23
|
-
Object.keys(def.elements)
|
|
24
|
-
.filter(k => k !== 'IsActiveEntity' && def.elements[k].key && def.elements[k].type !== 'cds.UUID')
|
|
25
|
-
.forEach(k => (params[k] = { type: def.elements[k].type }))
|
|
26
|
-
def.actions[DRAFT_NEW] = { kind: 'action', params, returns: { type: each } }
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
}
|
|
12
|
+
if (cds.env.fiori.direct_crud) cds.compile.for.direct_crud(dsn)
|
|
30
13
|
|
|
31
14
|
Object.defineProperty (csn, '_4odata', {value:dsn})
|
|
32
15
|
Object.defineProperty (dsn, '_4odata', {value:dsn})
|
package/lib/compile/to/edm.js
CHANGED
|
@@ -42,6 +42,7 @@ function cds_compile_to_edmx (csn,_o) {
|
|
|
42
42
|
const next = () => {
|
|
43
43
|
if (!result) {
|
|
44
44
|
if (cds.env.features.annotate_for_flows) enhanceCSNwithFlowAnnotations4FE(csn)
|
|
45
|
+
if (cds.env.fiori.direct_crud) cds.compile.for.direct_crud(csn)
|
|
45
46
|
result = o.service === 'all' ? _many('.xml', cdsc.to.edmx.all(csn, o)) : cdsc.to.edmx(csn, o)
|
|
46
47
|
}
|
|
47
48
|
return result
|
package/lib/env/cds-env.js
CHANGED
|
@@ -295,7 +295,7 @@ class Config {
|
|
|
295
295
|
const any = this._add_vcap_services_to (vcaps)
|
|
296
296
|
if (any) this._sources.push ('process.env.VCAP_SERVICES')
|
|
297
297
|
} catch(e) {
|
|
298
|
-
throw new Error ('[cds.env] - failed to parse VCAP_SERVICES:\n '+ e.message)
|
|
298
|
+
throw new Error ('[cds.env] - failed to parse VCAP_SERVICES:\n '+ e.message, {cause: e})
|
|
299
299
|
}
|
|
300
300
|
}
|
|
301
301
|
|
package/lib/i18n/index.js
CHANGED
package/lib/ql/cds.ql-infer.js
CHANGED
package/lib/req/response.js
CHANGED
|
@@ -39,7 +39,7 @@ const _configured = (u,x) => {
|
|
|
39
39
|
u.attr = { ...u.attr, ...x }
|
|
40
40
|
}
|
|
41
41
|
if (u.jwt) {
|
|
42
|
-
if ((
|
|
42
|
+
if (( _deprecated (u.jwt.zid, 'jwt.zid','tenant'))) {
|
|
43
43
|
u.tenant = u.jwt.zid
|
|
44
44
|
}
|
|
45
45
|
if ((x = _deprecated (u.jwt.attributes, 'jwt.attributes','attr'))) {
|
package/lib/utils/cds-utils.js
CHANGED
|
@@ -189,7 +189,7 @@ exports.read = async function read (file, _encoding) {
|
|
|
189
189
|
if (_encoding === 'json' || !_encoding && f.endsWith('.json')) try {
|
|
190
190
|
return JSON.parse(src)
|
|
191
191
|
} catch(e) {
|
|
192
|
-
throw new Error (`Failed to parse JSON in ${f}: ${e.message}
|
|
192
|
+
throw new Error (`Failed to parse JSON in ${f}: ${e.message}`, { cause: e })
|
|
193
193
|
}
|
|
194
194
|
else return process.platform === 'win32' ? src?.replace(/\r\n/g, '\n') : src
|
|
195
195
|
}
|
|
@@ -36,7 +36,7 @@ class RedisMessaging extends cds.MessagingService {
|
|
|
36
36
|
try {
|
|
37
37
|
await this.client.connect()
|
|
38
38
|
} catch (e) {
|
|
39
|
-
throw new Error('Connection to Redis could not be established: ' + e)
|
|
39
|
+
throw new Error('Connection to Redis could not be established: ' + e, { cause: e })
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
this._ready = true
|
|
@@ -77,9 +77,11 @@ module.exports = class MessagingService extends cds.Service {
|
|
|
77
77
|
if (!cds.context) cds.context = {}
|
|
78
78
|
if (ctx.tenant) cds.context.tenant = ctx.tenant
|
|
79
79
|
if (!ctx.user) ctx.user = cds.User.privileged
|
|
80
|
+
// REVISIT: is cds.context.model really needed?
|
|
80
81
|
// this.tx expects cds.context.model
|
|
81
|
-
if (cds.model && (cds.env.requires.extensibility || cds.env.requires.toggles))
|
|
82
|
+
if (ctx.tenant && cds.model && (cds.env.requires.extensibility || cds.env.requires.toggles))
|
|
82
83
|
cds.context.model = await ExtendedModels.model4(ctx.tenant, ctx.features || {})
|
|
84
|
+
else if (cds.model) cds.context.model = cds.model
|
|
83
85
|
const me = this.options.inboxed || this.options.inbox ? cds.queued(this) : this
|
|
84
86
|
return await me.tx(ctx, tx => tx.emit(msg))
|
|
85
87
|
}
|
|
@@ -441,7 +441,7 @@ const _processBatch = async (srv, router, req, res, next, body, ct, boundary) =>
|
|
|
441
441
|
|
|
442
442
|
const queue = []
|
|
443
443
|
let _continue = true
|
|
444
|
-
const _queued_exec = (atomicityGroup, responses = []) => {
|
|
444
|
+
const _queued_exec = (atomicityGroup, agIndex, responses = []) => {
|
|
445
445
|
const groupId = atomicityGroup[0].atomicityGroup
|
|
446
446
|
|
|
447
447
|
return new Promise(resolve => queue.push(resolve)).then(() => {
|
|
@@ -487,16 +487,23 @@ const _processBatch = async (srv, router, req, res, next, body, ct, boundary) =>
|
|
|
487
487
|
|
|
488
488
|
// ensure all subrequests run in this tx
|
|
489
489
|
// (if first subrequest fails without really opening the tx, the rest are executed in a "dangling tx")
|
|
490
|
-
return Promise.allSettled(subrequests)
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
490
|
+
return Promise.allSettled(subrequests)
|
|
491
|
+
.then(ress => {
|
|
492
|
+
// wait for all previous atomicity groups (ignoring errors via allSettled) for odata v2
|
|
493
|
+
const prevs = []
|
|
494
|
+
for (let i = 0; i < agIndex; i++) prevs.push(promises[i])
|
|
495
|
+
return Promise.allSettled(prevs).then(() => ress)
|
|
496
|
+
})
|
|
497
|
+
.then(ress => {
|
|
498
|
+
const failed = ress.filter(({ status }) => status === 'rejected')
|
|
499
|
+
if (!failed.length) return
|
|
500
|
+
// throw first error and call srv.on('error') for the others
|
|
501
|
+
const first = failed.shift()
|
|
502
|
+
if (srv.handlers._error?.length)
|
|
503
|
+
for (const other of failed)
|
|
504
|
+
for (const each of srv.handlers._error) each.handler.call(srv, other.reason, cds.context)
|
|
505
|
+
throw first.reason
|
|
506
|
+
})
|
|
500
507
|
})
|
|
501
508
|
.catch(err => {
|
|
502
509
|
responses._has_failure = true
|
|
@@ -528,7 +535,7 @@ const _processBatch = async (srv, router, req, res, next, body, ct, boundary) =>
|
|
|
528
535
|
|
|
529
536
|
// queue execution of atomicity groups
|
|
530
537
|
const promises = []
|
|
531
|
-
for (
|
|
538
|
+
for (let i = 0; i < atomicityGroups.length; i++) promises.push(_queued_exec(atomicityGroups[i], i))
|
|
532
539
|
|
|
533
540
|
// trigger first max_parallel in queue
|
|
534
541
|
for (let i = 0; i < max_parallel; i++) if (queue.length) queue.shift()()
|
|
@@ -463,7 +463,6 @@ function _processSegments(from, model, namespace, cqn, protocol) {
|
|
|
463
463
|
|
|
464
464
|
// > navigation
|
|
465
465
|
one = !!(current.is2one || ref[i].where)
|
|
466
|
-
incompleteKeys = one || i === ref.length - 1 ? false : true
|
|
467
466
|
current = model.definitions[current.target]
|
|
468
467
|
target = current
|
|
469
468
|
|
package/libx/queue/index.js
CHANGED
|
@@ -259,7 +259,7 @@ const _processTasks = (target, tenant, _opts = {}) => {
|
|
|
259
259
|
LOG.error(`${service.name}: Programming error detected:`, e)
|
|
260
260
|
task.updateData = { attempts: opts.maxAttempts }
|
|
261
261
|
toBeUpdated.push(task)
|
|
262
|
-
throw new Error(`${service.name}: Programming error detected
|
|
262
|
+
throw new Error(`${service.name}: Programming error detected.`, { cause: e })
|
|
263
263
|
}
|
|
264
264
|
if (e.unrecoverable) {
|
|
265
265
|
LOG.error(`${service.name}: Unrecoverable error:`, e)
|