@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 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
+ }
@@ -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
@@ -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
@@ -1,3 +1,5 @@
1
+ /* eslint-disable no-useless-assignment */
2
+
1
3
  const I18nBundle = require ('./bundles')
2
4
  const I18nFiles = require ('./files')
3
5
  const cds = require('../index')
@@ -1,3 +1,5 @@
1
+ /* eslint-disable no-useless-assignment */
2
+
1
3
  const cds = require('../index')
2
4
  const infer = exports = module.exports = (..._) => infer.target(..._)
3
5
 
@@ -1,3 +1,5 @@
1
+ /* eslint-disable no-useless-assignment */
2
+
1
3
  const cds = require ('../index')
2
4
  const placeholders = [...'x'.repeat(9)].map((x,i) => `{${i+1}}`)
3
5
 
@@ -39,7 +39,7 @@ const _configured = (u,x) => {
39
39
  u.attr = { ...u.attr, ...x }
40
40
  }
41
41
  if (u.jwt) {
42
- if ((x = _deprecated (u.jwt.zid, 'jwt.zid','tenant'))) {
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'))) {
@@ -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).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()()
@@ -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
 
@@ -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)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sap/cds",
3
- "version": "9.8.0",
3
+ "version": "9.8.2",
4
4
  "description": "SAP Cloud Application Programming Model - CDS for Node.js",
5
5
  "homepage": "https://cap.cloud.sap/",
6
6
  "keywords": [