@sap/cds 9.8.0 → 9.8.1

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 CHANGED
@@ -4,6 +4,14 @@
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.1 - 2026-03-09
8
+
9
+ ### Fixed
10
+
11
+ - OData batch parallel processing: Preserve request sequence for OData v2
12
+ - In inbound messaging, only load the extended model if there is a tenant
13
+ - Ensure `cds.fiori.direct_crud` is considered during `cds build`
14
+
7
15
  ## Version 9.8.0 - 2026-03-06
8
16
 
9
17
  ### Added
package/eslint.config.mjs CHANGED
@@ -14,6 +14,9 @@ export const defaults = {
14
14
  rules: {
15
15
  'no-unused-vars': 'warn',
16
16
  'no-console': 'warn',
17
+ 'preserve-caught-error': 'off',
18
+ 'no-useless-assignment': 'off',
19
+ 'no-unassigned-vars': 'off'
17
20
  },
18
21
 
19
22
  languageOptions: {
@@ -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') }
@@ -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
+ }
@@ -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})
@@ -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
@@ -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).then(ress => {
491
- const failed = ress.filter(({ status }) => status === 'rejected')
492
- if (!failed.length) return
493
- // throw first error and call srv.on('error') for the others
494
- const first = failed.shift()
495
- if (srv.handlers._error?.length)
496
- for (const other of failed)
497
- for (const each of srv.handlers._error) each.handler.call(srv, other.reason, cds.context)
498
- throw first.reason
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 (const atomicityGroup of atomicityGroups) promises.push(_queued_exec(atomicityGroup))
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()()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sap/cds",
3
- "version": "9.8.0",
3
+ "version": "9.8.1",
4
4
  "description": "SAP Cloud Application Programming Model - CDS for Node.js",
5
5
  "homepage": "https://cap.cloud.sap/",
6
6
  "keywords": [