@sap/cds-dk 10.1.1 → 10.1.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.
Files changed (44) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/lib/build/plugins/internal.js +0 -26
  3. package/lib/build/plugins/nodejs/index.js +8 -3
  4. package/lib/cds.js +8 -2
  5. package/lib/init/merge.js +21 -17
  6. package/lib/init/template/cloud-logging/index.js +3 -3
  7. package/lib/init/template/destination/index.js +3 -3
  8. package/lib/init/template/enterprise-messaging/index.js +4 -1
  9. package/lib/init/template/event-mesh/index.js +3 -0
  10. package/lib/init/template/multitenancy/index.js +2 -2
  11. package/lib/init/template/workzone-standard/index.js +1 -1
  12. package/node_modules/@cap-js/db-service/CHANGELOG.md +13 -0
  13. package/node_modules/@cap-js/db-service/lib/cqn4sql.js +9 -5
  14. package/node_modules/@cap-js/db-service/package.json +1 -1
  15. package/node_modules/@cap-js/sqlite/CHANGELOG.md +9 -0
  16. package/node_modules/@cap-js/sqlite/package.json +2 -2
  17. package/node_modules/@sap/cds/CHANGELOG.md +19 -0
  18. package/node_modules/@sap/cds/common.cds +3 -3
  19. package/node_modules/@sap/cds/lib/compile/minify.js +3 -1
  20. package/node_modules/@sap/cds/lib/env/cds-env.js +1 -1
  21. package/node_modules/@sap/cds/lib/env/cds-requires.js +1 -0
  22. package/node_modules/@sap/cds/libx/_runtime/common/generic/crud.js +7 -1
  23. package/node_modules/@sap/cds/libx/_runtime/common/utils/cqn.js +1 -1
  24. package/node_modules/@sap/cds/libx/_runtime/fiori/lean-draft.js +1 -1
  25. package/node_modules/@sap/cds/libx/_runtime/messaging/common-utils/normalizeIncomingMessage.js +1 -1
  26. package/node_modules/@sap/cds/libx/_runtime/messaging/kafka.js +1 -1
  27. package/node_modules/@sap/cds/libx/http/location.js +10 -11
  28. package/node_modules/@sap/cds/libx/odata/middleware/read.js +1 -1
  29. package/node_modules/@sap/cds/libx/queue/SchedulingService.js +4 -0
  30. package/node_modules/@sap/cds/libx/queue/index.js +44 -10
  31. package/node_modules/@sap/cds/libx/queue/processing.js +47 -16
  32. package/node_modules/@sap/cds/libx/queue/utils.js +3 -0
  33. package/node_modules/@sap/cds/package.json +1 -1
  34. package/node_modules/@sap/cds/tasks/enterprise-messaging-deploy.js +13 -5
  35. package/node_modules/proxy-addr/HISTORY.md +60 -54
  36. package/node_modules/proxy-addr/README.md +1 -1
  37. package/node_modules/proxy-addr/index.js +32 -0
  38. package/node_modules/proxy-addr/package.json +8 -4
  39. package/node_modules/yaml/browser/dist/compose/resolve-flow-scalar.js +19 -18
  40. package/node_modules/yaml/browser/dist/nodes/Alias.js +25 -23
  41. package/node_modules/yaml/dist/compose/resolve-flow-scalar.js +19 -18
  42. package/node_modules/yaml/dist/nodes/Alias.js +25 -23
  43. package/node_modules/yaml/package.json +1 -1
  44. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -6,6 +6,18 @@ This project adheres to [Semantic Versioning](https://semver.org/).
6
6
 
7
7
  The format is based on [Keep a Changelog](https://keepachangelog.com/).
8
8
 
9
+ ## Version 10.1.2 - 2026-09-17
10
+
11
+ ### Fixed
12
+
13
+ - `cds` no longer crashes with an `ENOENT` (`uv_cwd`) stack trace when the shell's current directory has been deleted, e.g. during tab-completion.
14
+ - `cds build --for java` no longer writes Node.js runtime messages.
15
+ - `cds build` preserves line breaks in merged `messages.properties` bundles.
16
+ - `cds add xsuaa` no longer generates an invalid `mta.yaml` when added after `workzone`.
17
+ - `cds add cloud-logging` now binds existing approuter and multitenancy modules when `mta` is added later.
18
+ - `cds add enterprise-messaging` and `cds add event-mesh` now contribute to an MTA descriptor added later.
19
+ - `cds add destination` now binds existing approuter and multitenancy modules when `mta` is added later.
20
+
9
21
  ## Version 10.1.1 - 2026-09-09
10
22
 
11
23
  ### Added
@@ -132,36 +132,10 @@ module.exports = class InternalBuildPlugin extends require('./plugin') {
132
132
  // bundleDest might be null
133
133
  if (bundleDest && Object.keys(bundles).length > 0) {
134
134
  await this.write(bundles).to(file)
135
- // also collect and write messages from plugins
136
- await this.collectMessageBundles(bundleDest, model)
137
135
  return { file, bundles }
138
136
  }
139
137
  return null
140
138
  }
141
-
142
- /**
143
- * Collect and write message bundles from CAP plugins into messages.properties files.
144
- * @param {string} bundleDest
145
- * @param {Object} model - passed for signature consistency with subclass overrides; not used here
146
- * as Java does not require neighborhood resolution of plugin i18n files.
147
- */
148
- // eslint-disable-next-line no-unused-vars
149
- async collectMessageBundles(bundleDest, model) {
150
- const messageBundle = cds.i18n.bundle4('messages')
151
- const messagesData = messageBundle.translations4?.(cds.env.i18n.languages) ?? {}
152
-
153
- // Write messages.properties files for each locale
154
- for (const [locale, messages] of Object.entries(messagesData)) {
155
- if (Object.keys(messages).length > 0) {
156
- const fileName = locale ? `messages_${locale}.properties` : 'messages.properties'
157
- const filePath = path.join(bundleDest, fileName)
158
- const content = Object.entries(messages)
159
- .map(([key, value]) => `${key}=${value}`)
160
- .join('\n') + '\n'
161
- await this.write(content).to(filePath)
162
- }
163
- }
164
- }
165
139
  }
166
140
 
167
141
  // Emits the 'compile.for.runtime' event, as the produced CSN is for runtime usage.
@@ -10,6 +10,7 @@ const DEBUG = cds.debug('cli|build')
10
10
  const NODE_ENGINE_VERSION = '^24'
11
11
  const { exec } = require('node:child_process')
12
12
  const pexec = require('node:util').promisify(exec)
13
+ const escapeLineBreaks = value => value.replace(/\r/g, '\\u000D').replace(/\n/g, '\\u000A')
13
14
 
14
15
  module.exports = class NodejsBuildPlugin extends require('../edmx') {
15
16
 
@@ -353,15 +354,19 @@ module.exports = class NodejsBuildPlugin extends require('../edmx') {
353
354
  return Array.isArray(parsed) ? parsed : Object.values(parsed)
354
355
  }
355
356
 
356
- // Override to pass model for neighborhood resolution of plugin i18n files.
357
- // Cannot be done in InternalBuildPlugin because Java relies on the model-less behaviour.
357
+ async collectLanguageBundles(model, bundleDest) {
358
+ const result = await super.collectLanguageBundles(model, bundleDest)
359
+ if (result) await this.collectMessageBundles(bundleDest, model)
360
+ return result
361
+ }
362
+
358
363
  async collectMessageBundles(bundleDest, model) {
359
364
  const messageBundle = cds.i18n.bundle4('messages', { model })
360
365
  const messagesData = messageBundle.translations4?.(cds.env.i18n.languages) ?? {}
361
366
  for (const [locale, messages] of Object.entries(messagesData)) {
362
367
  if (Object.keys(messages).length > 0) {
363
368
  const fileName = locale ? `messages_${locale}.properties` : 'messages.properties'
364
- const content = Object.entries(messages).map(([k, v]) => `${k}=${v}`).join('\n') + '\n'
369
+ const content = Object.entries(messages).map(([k, v]) => `${k}=${escapeLineBreaks(v)}`).join('\n') + '\n'
365
370
  await this.write(content).to(path.join(bundleDest, fileName))
366
371
  }
367
372
  }
package/lib/cds.js CHANGED
@@ -11,10 +11,16 @@ module.exports = Object.defineProperties (cds, {
11
11
 
12
12
  function _local (id, ...morePaths) {
13
13
  return require(require.resolve (id, {paths:[
14
- process.cwd(), // project-local module is preferred
14
+ _cwd(), // project-local module is preferred
15
15
  __dirname, // otherwise from our own dependencies
16
16
  ...morePaths
17
- ]}))
17
+ ].filter(Boolean)})) // drop empty paths (e.g. _cwd()/_npmRoot() fallbacks); require.resolve expects real paths
18
+ }
19
+
20
+ function _cwd() {
21
+ // process.cwd() throws ENOENT (uv_cwd) when the cwd was deleted
22
+ try { return process.cwd() }
23
+ catch (e) { if (e.code === 'ENOENT') return '' }
18
24
  }
19
25
 
20
26
  function _npmRoot() {
package/lib/init/merge.js CHANGED
@@ -165,8 +165,28 @@ async function _mergeYAML(into, from, project, semantics = {}) {
165
165
  t.value = s.value
166
166
  }
167
167
  }
168
+ const deleteEntries = entries => {
169
+ for (const { item, key, relationships: refs } of entries) {
170
+ const seq = locate(target, item.in)
171
+ const node = locate(target, item)
172
+ if (!YAML.isSeq(seq) || !node) continue
173
+ const removed = node.toJSON()
174
+ key ? node.deleteIn(path(key)) : seq.delete(seq.items.indexOf(node))
175
+ for (const { removeProperty, allWithin } of refs ?? []) {
176
+ const [parentPath, childPath, prop] = allWithin
177
+ for (const parent of locate(target, parentPath)?.items ?? []) {
178
+ const list = parent.getIn(path(childPath))
179
+ const i = list?.items?.findIndex(n => n.get?.(prop) === removed[removeProperty]) ?? -1
180
+ if (i > -1) list.delete(i)
181
+ }
182
+ }
183
+ }
184
+ }
168
185
  mergeNode(target.contents, source.contents)
169
186
 
187
+ // Deletions: remove the matched node (or a sub-key), then cascade-remove references to it.
188
+ deleteEntries(deletions.filter(({ key }) => !key))
189
+
170
190
  // Placement: add matched (and relationship-referenced) template entries not already present.
171
191
  const isEntry = e => e && typeof e === 'object' && !Array.isArray(e) && e.in !== undefined
172
192
  const insertNode = ins => Array.isArray(ins) ? (ins.length === 1 ? ins[0] : undefined) : ins
@@ -208,22 +228,7 @@ async function _mergeYAML(into, from, project, semantics = {}) {
208
228
  }
209
229
  }
210
230
 
211
- // Deletions: remove the matched node (or a sub-key), then cascade-remove references to it.
212
- for (const { item, key, relationships: refs } of deletions) {
213
- const seq = locate(target, item.in)
214
- const node = locate(target, item)
215
- if (!YAML.isSeq(seq) || !node) continue
216
- const removed = node.toJSON()
217
- key ? node.deleteIn(path(key)) : seq.delete(seq.items.indexOf(node))
218
- for (const { removeProperty, allWithin } of refs ?? []) {
219
- const [parentPath, childPath, prop] = allWithin
220
- for (const parent of locate(target, parentPath)?.items ?? []) {
221
- const list = parent.getIn(path(childPath))
222
- const i = list?.items?.findIndex(n => n.get?.(prop) === removed[removeProperty]) ?? -1
223
- if (i > -1) list.delete(i)
224
- }
225
- }
226
- }
231
+ deleteEntries(deletions.filter(({ key }) => key))
227
232
 
228
233
  typeof into === 'string' && await writeYAML(into, target)
229
234
  return target
@@ -349,4 +354,3 @@ module.exports.merge = (...src) => {
349
354
  * @property {Deletion[]} [deletions] - matched nodes (or sub-keys) to delete
350
355
  * @property {Relationship[]} [relationships] - relationships to wire into target sequences
351
356
  */
352
-
@@ -40,7 +40,7 @@ module.exports = class extends require('../../plugin') {
40
40
 
41
41
  async combine(facet) {
42
42
  const project = readProject(facet)
43
- const { hasMta, addMta, addKyma, addApprouter, isJava, addMultitenancy, srv } = project
43
+ const { hasMta, addMta, addKyma, addApprouter, hasApprouter, isJava, addMultitenancy, hasMultitenancy, srv } = project
44
44
 
45
45
  if (addMta || (addMultitenancy || addApprouter) && hasMta) {
46
46
  const cloudLogging = {
@@ -58,14 +58,14 @@ module.exports = class extends require('../../plugin') {
58
58
  into: [srvModule, 'requires', 'name']
59
59
  })
60
60
  }
61
- if (addMultitenancy) {
61
+ if (hasMultitenancy) {
62
62
  const mtxSidecar = mtxSidecar4(isJava ? 'mtx/sidecar' : 'gen/mtx/sidecar')
63
63
  relationships.push({
64
64
  insert: [cloudLogging, 'name'],
65
65
  into: [mtxSidecar, 'requires', 'name']
66
66
  })
67
67
  }
68
- if (addApprouter) {
68
+ if (hasApprouter) {
69
69
  relationships.push({
70
70
  insert: [cloudLogging, 'name'],
71
71
  into: [approuter, 'requires', 'name']
@@ -25,7 +25,7 @@ module.exports = class DestinationTemplate extends require('../../plugin') {
25
25
 
26
26
  async combine(facet) {
27
27
  const project = readProject(facet)
28
- const { addApprouter, addMultitenancy, addKyma, addMta, hasMta, srv, isJava } = project
28
+ const { addApprouter, hasApprouter, addMultitenancy, hasMultitenancy, addKyma, addMta, hasMta, srv, isJava } = project
29
29
 
30
30
  if (addMta || (addApprouter || addMultitenancy) && hasMta) {
31
31
  const srvModule = srv4(srv.path)
@@ -34,13 +34,13 @@ module.exports = class DestinationTemplate extends require('../../plugin') {
34
34
  insert: [destination, 'name'],
35
35
  into: [srvModule, 'requires', 'name']
36
36
  }]
37
- if (addApprouter) {
37
+ if (hasApprouter) {
38
38
  relationships.push({
39
39
  insert: [destination, 'name'],
40
40
  into: [approuter, 'requires', 'name']
41
41
  })
42
42
  }
43
- if (addMultitenancy) {
43
+ if (hasMultitenancy) {
44
44
  relationships.push({
45
45
  insert: [destination, 'name'],
46
46
  into: [mtxSidecar4(isJava ? 'mtx/sidecar' : 'gen/mtx/sidecar'), 'requires', 'name']
@@ -6,6 +6,9 @@ const { srv4, enterpriseMessaging, xsuaa } = require('../../registries/mta')
6
6
  const KymaTemplate = require('../kyma')
7
7
 
8
8
  module.exports = class EnterpriseMessagingTemplate extends require('../../plugin') {
9
+ static get kind() { return 'enterprise-messaging-http' }
10
+ static get vcap() { return 'enterprise-messaging' }
11
+
9
12
  constructor(...args) {
10
13
  super(...args)
11
14
  this._name = 'enterprise-messaging'
@@ -37,7 +40,7 @@ module.exports = class EnterpriseMessagingTemplate extends require('../../plugin
37
40
 
38
41
  static hasInProduction(env) {
39
42
  const { kind = 'dummy', vcap } = env.requires?.messaging ?? {}
40
- return kind === this._kind && vcap?.label === this._vcap
43
+ return kind === this.kind && vcap?.label === this.vcap
41
44
  }
42
45
 
43
46
  async run() {
@@ -1,6 +1,9 @@
1
1
  const { eventMesh } = require('../../registries/mta')
2
2
 
3
3
  module.exports = class EventMeshTemplate extends require('../enterprise-messaging') {
4
+ static get kind() { return 'enterprise-messaging-http' }
5
+ static get vcap() { return 'event-mesh-message-client' }
6
+
4
7
  constructor(...args) {
5
8
  super(...args)
6
9
  this._name = 'event-mesh'
@@ -44,11 +44,11 @@ module.exports = class MultitenancyTemplate extends require('../../plugin') {
44
44
  const project = readProject(facet)
45
45
  const {
46
46
  isNodejs, isJava, addMta, hasMta,
47
- addXsuaa, hasXsuaa, addIas, hasIas, hasHana, hasHanaServerless, addKyma, hasApprouter,
47
+ addXsuaa, hasXsuaa, addIas, hasIas, addMultitenancy, hasHana, hasHanaServerless, addKyma, hasApprouter,
48
48
  srv, appName
49
49
  } = project
50
50
 
51
- if (addMta || (addIas || addXsuaa) && hasMta) {
51
+ if (addMta || (addMultitenancy || addIas || addXsuaa) && hasMta) {
52
52
  const srvModule = srv4(srv.path)
53
53
  const mtxSidecar = mtxSidecar4(isJava ? 'mtx/sidecar' : 'gen/mtx/sidecar')
54
54
  const modules = [srvModule, mtxSidecar]
@@ -93,7 +93,7 @@ module.exports = class extends require('../../plugin') {
93
93
  }
94
94
  if (addXsuaa) {
95
95
  relationships.push({
96
- insert: [xsuaa],
96
+ insert: [xsuaa, 'name'],
97
97
  into: [destinations, 'requires']
98
98
  })
99
99
  additions.push({
@@ -4,6 +4,19 @@
4
4
  - The format is based on [Keep a Changelog](http://keepachangelog.com/).
5
5
  - This project adheres to [Semantic Versioning](http://semver.org/).
6
6
 
7
+ ## [3.1.1](https://github.com/cap-js/cds-dbs/compare/db-service-v3.1.0...db-service-v3.1.1) (2026-09-15)
8
+
9
+
10
+ ### Fixed
11
+
12
+ * **cqn4sql:** correctly expand `not exists <assoc>` alongside a null-check branch ([#1736](https://github.com/cap-js/cds-dbs/issues/1736)) ([bfb0a01](https://github.com/cap-js/cds-dbs/commit/bfb0a010cce2cea089f4fabf92a3ecea43486a09))
13
+
14
+
15
+ ### Performance Improvements
16
+
17
+ * do not use ranked search for static values ([#1727](https://github.com/cap-js/cds-dbs/issues/1727)) ([33fb36b](https://github.com/cap-js/cds-dbs/commit/33fb36bbf0f86291e5fc0b9e2ff5b7244b49d19e))
18
+ * if explicit order by is specified, do not add ranked search as a secondary clause ([33fb36b](https://github.com/cap-js/cds-dbs/commit/33fb36bbf0f86291e5fc0b9e2ff5b7244b49d19e))
19
+
7
20
  ## [3.1.0](https://github.com/cap-js/cds-dbs/compare/db-service-v3.0.1...db-service-v3.1.0) (2026-09-07)
8
21
 
9
22
 
@@ -338,9 +338,11 @@ function _cqn4sql(originalQuery, model, useTechnicalAlias = true) {
338
338
  // it would sort by a constant boolean.
339
339
  const ranksSearch =
340
340
  cds.db?.kind === 'hana' && cds.env.hana?.fuzzy !== false && cds.env.hana?.fuzzy?.ranked_search !== false
341
- // count queries do not need ranked search
342
- const isCountQuery = columns?.length === 1 && columns[0].func === 'count'
343
- const searchRank = ranksSearch && !isCountQuery && inferred.$searchRank && buildSearchRankOrderBy(inferred.$searchRank)
341
+ // count queries or static values do not need ranked search
342
+ const isCountQuery = columns?.length === 1 && typeof columns[0] === 'object' && (columns[0].func === 'count' || 'val' in columns[0])
343
+ // a user-provided ORDER BY takes precedence over the search rank — don't inject the ranking then
344
+ const hasExplicitOrderBy = (orderBy || []).some(o => !o.implicit)
345
+ const searchRank = ranksSearch && !isCountQuery && !hasExplicitOrderBy && inferred.$searchRank && buildSearchRankOrderBy(inferred.$searchRank)
344
346
  if (searchRank) {
345
347
  // precedence: user ordering, then rank, then the runtime's implicit key ordering
346
348
  const implicitAt = (orderBy || []).findIndex(o => o.implicit)
@@ -1776,8 +1778,10 @@ function _cqn4sql(originalQuery, model, useTechnicalAlias = true) {
1776
1778
  }
1777
1779
 
1778
1780
  const whereExists = { SELECT: whereExistsSubqueries(whereExistsSubSelects) }
1779
- transformedTokenStream[i + 1] = whereExists
1780
- // skip newly created subquery from being iterated
1781
+ // append, don't index by `i`: a preceding branch (e.g. flattening `<assoc> is null`)
1782
+ // may have changed the transformed stream's length, so `[i + 1]` would leave holes
1783
+ transformedTokenStream.push(whereExists)
1784
+ // skip the association ref which we just turned into the subquery
1781
1785
  i += 1
1782
1786
  } else if (token !== null && typeof token === 'object' && '#' in token) {
1783
1787
  // Enum token: resolve to its value
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cap-js/db-service",
3
- "version": "3.1.0",
3
+ "version": "3.1.1",
4
4
  "description": "CDS base database service",
5
5
  "homepage": "https://github.com/cap-js/cds-dbs/tree/main/db-service#cds-base-database-service",
6
6
  "repository": {
@@ -4,6 +4,15 @@
4
4
  - The format is based on [Keep a Changelog](http://keepachangelog.com/).
5
5
  - This project adheres to [Semantic Versioning](http://semver.org/).
6
6
 
7
+ ## [3.1.1](https://github.com/cap-js/cds-dbs/compare/sqlite-v3.1.0...sqlite-v3.1.1) (2026-09-15)
8
+
9
+
10
+ ### Dependencies
11
+
12
+ * The following workspace dependencies were updated
13
+ * dependencies
14
+ * @cap-js/db-service bumped from ^3.1.0 to ^3.1.1
15
+
7
16
  ## [3.1.0](https://github.com/cap-js/cds-dbs/compare/sqlite-v3.0.2...sqlite-v3.1.0) (2026-09-07)
8
17
 
9
18
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cap-js/sqlite",
3
- "version": "3.1.0",
3
+ "version": "3.1.1",
4
4
  "description": "CDS database service for SQLite",
5
5
  "homepage": "https://github.com/cap-js/cds-dbs/tree/main/sqlite#cds-database-service-for-sqlite",
6
6
  "repository": {
@@ -26,7 +26,7 @@
26
26
  "test": "cds-test"
27
27
  },
28
28
  "dependencies": {
29
- "@cap-js/db-service": "^3.1.0"
29
+ "@cap-js/db-service": "^3.1.1"
30
30
  },
31
31
  "peerDependencies": {
32
32
  "@sap/cds": "^10",
@@ -4,6 +4,25 @@
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 10.1.1 - 2026-09-17
8
+
9
+ ### Fixed
10
+
11
+ - Messaging consumers (Event Mesh AMQP, Kafka, Redis) no longer crash with `TypeError: Cannot use 'in' operator to search for 'data' in null` when a message with a `null` payload is received
12
+ - `cds.env` now correctly replaces an array default with an object project override instead of assigning object properties onto the array
13
+ - SQLite now uses its default pool config (single, non-evicting connection) even when `@cap-js/sqlite` is installed globally rather than as a local dependency
14
+ - Stripping of `IsActiveEntity` in programmatic deep updates to draft instances
15
+ - Server crash when string keys contain control characters in OData `Location` response header
16
+ - Binary keys in OData `Location` response header are now formatted as binary literals (`binary'...'`)
17
+ - Skip periodic message processing when no provider tenant is configured
18
+ - The parent-readable check during CREATE/navigation no longer swallows system errors (e.g. database failures); such errors are now propagated instead of being masked as a `404`
19
+ - Support X.509 in `enterprise-messaging`'s deploy task
20
+ - Incompatibility of global config `cds.sql.lock_acquire_timeout` and event-queue implementation
21
+ - Outbox messages written before `appid` was introduced are now processed after an upgrade instead of being skipped
22
+ - A queued outbox message whose target service cannot be resolved is now dead-lettered instead of crashing the application
23
+ - Recurring scheduled tasks (`.every()`) are no longer resurrected when unscheduled while running, and re-submitting one while it runs no longer discards the updated schedule and payload
24
+ - Convenience handling of `/$count` requests for custom handlers simply returning arrays
25
+
7
26
  ## Version 10.1.0 - 2026-09-02
8
27
 
9
28
  ### Added
@@ -166,9 +166,9 @@ annotate managed with {
166
166
  createdBy @Core.Immutable;
167
167
  }
168
168
 
169
- annotate sap.common.Countries with { code @Common.Text:name; }
170
- annotate sap.common.Currencies with { code @Common.Text:name; }
171
- annotate sap.common.Languages with { code @Common.Text:name; }
169
+ annotate sap.common.Currencies :code with @Common.Text:name;
170
+ annotate sap.common.Countries :code with @Common.Text:name;
171
+ annotate sap.common.Languages :code with @Common.Text:name;
172
172
 
173
173
 
174
174
  //---------------------------------------------------------------------------
@@ -93,7 +93,8 @@ class Minifier {
93
93
  let r = q.from.ref[0]; if (r.id) r = r.id; if (r in this.kept) return
94
94
  let e = this.defs[r]; if (!e) return
95
95
  let all = e.elements, kept = {}
96
- if (q.columns) _keep (q.columns)
96
+ if (e['@cds.minify'] === 'unused-elements') return this.keep (r,e) // avoids propagation
97
+ if (q.columns) _keep (q.columns); else return this.keep (r,e)
97
98
  if (q.where) _keep (q.where)
98
99
  e.elements = kept
99
100
  return this.keep (r,e)
@@ -101,6 +102,7 @@ class Minifier {
101
102
  if (x.func) { _keep (x.args); continue }
102
103
  if (x.xpr) { _keep (x.xpr); continue }
103
104
  if (x.ref) { let [r] = x.ref; if (r in all) kept[r] = all[r] }
105
+ if (x === '*') return kept = all
104
106
  }}
105
107
  }
106
108
 
@@ -515,7 +515,7 @@ function _merge (dst, src, _profiles) {
515
515
  const v = pd.value
516
516
  if (typeof v === 'object' && !Array.isArray(v) && v != null) {
517
517
  if (!dst[p]) dst[p] = {}
518
- if (typeof dst[p] !== 'object') dst[p] = v
518
+ if (typeof dst[p] !== 'object' || Array.isArray(dst[p])) dst[p] = v
519
519
  else _merge (dst[p], v, _profiles)
520
520
  continue
521
521
  }
@@ -138,6 +138,7 @@ const _databases = {
138
138
 
139
139
  "sqlite": {
140
140
  impl: '@cap-js/sqlite', credentials: { url: 'db.sqlite' },
141
+ pool: { evictionRunIntervalMillis: 0, min: 1, max: 1 },
141
142
  ..._db_data,
142
143
  },
143
144
  "sqlite:memory": {
@@ -1,4 +1,5 @@
1
1
  const cds = require('../../cds')
2
+ const LOG = cds.log('app')
2
3
  const { SELECT } = cds.ql
3
4
 
4
5
  module.exports = exports = cds.service.impl(function () {
@@ -18,7 +19,7 @@ module.exports = exports = cds.service.impl(function () {
18
19
  ? SELECT.one`1 as $parent`.from({ ref: ref.slice(0, -1) })
19
20
  : null
20
21
  const parent_readable_check = parent_exists_query
21
- ? async () => (parent_skip_auth ? cds.db : this).run(parent_exists_query).catch(() => null)
22
+ ? async () => (parent_skip_auth ? cds.db : this).run(parent_exists_query).catch(parent_unreadable_error)
22
23
  : null
23
24
 
24
25
  if (req.event === 'CREATE' && parent_readable_check != null) (await parent_readable_check()) || req.reject(404)
@@ -84,6 +85,11 @@ async function legacy_behavior(req, result, parent) {
84
85
  }
85
86
  }
86
87
 
88
+ function parent_unreadable_error(e) {
89
+ if (cds.error.isSystemError(e)) throw e
90
+ LOG._debug && LOG.debug('Parent readable check failed:', e)
91
+ }
92
+
87
93
  exports._handle_result = legacy_behavior
88
94
  exports._always_return_arrays = on_off => {
89
95
  if (on_off === undefined) return exports._handle_result === new_behavior
@@ -4,7 +4,7 @@ const { setEntityContained } = require('./csn')
4
4
 
5
5
  // scope: simple wheres à la "[{ ref: ['foo'] }, '=', { val: 'bar' }, 'and', ... ]"
6
6
  function where2obj(where, target = null, data = {}) {
7
- for (let i = 0; i < where.length;) {
7
+ for (let i = 0; i < where.length; ) {
8
8
  const a = where[i++]
9
9
  if (a.xpr) {
10
10
  where2obj(a.xpr, target, data)
@@ -1090,7 +1090,7 @@ const draftHandle = async function (req) {
1090
1090
  for (const assoc in target.associations) {
1091
1091
  const val = data[assoc]
1092
1092
  if (val && typeof val === 'object') {
1093
- const _target = req.target.associations[assoc]._target
1093
+ const _target = target.associations[assoc]._target
1094
1094
  if (Array.isArray(val)) {
1095
1095
  val.forEach(v => _rmIsActiveEntity(v, _target))
1096
1096
  } else {
@@ -11,7 +11,7 @@ const _JSONorString = string => {
11
11
  const normalizeIncomingMessage = message => {
12
12
  const _payload = typeof message === 'object' ? message : _JSONorString(message)
13
13
  let data, headers
14
- if (typeof _payload === 'object' && 'data' in _payload) {
14
+ if (_payload !== null && typeof _payload === 'object' && 'data' in _payload) {
15
15
  data = _payload.data
16
16
  headers = { ..._payload }
17
17
  delete headers.data
@@ -161,7 +161,7 @@ function _JSONorString(string) {
161
161
  function _normalizeIncomingMessage(message) {
162
162
  const _payload = _JSONorString(message)
163
163
  let data, headers
164
- if (typeof _payload === 'object' && 'data' in _payload) {
164
+ if (_payload !== null && typeof _payload === 'object' && 'data' in _payload) {
165
165
  data = _payload.data
166
166
  headers = { ..._payload }
167
167
  delete headers.data
@@ -8,14 +8,16 @@ module.exports = (target, srv, result, keys_as_segments) => {
8
8
 
9
9
  const keyValuePairs = filteredKeys.reduce((acc, key) => {
10
10
  const value = result[key]
11
- if (result[key] === undefined) return acc
12
11
 
13
12
  if (Buffer.isBuffer(value)) {
14
- acc[key] = value.toString('base64')
13
+ const base64 = value.toString('base64')
14
+ acc[key] = keys_as_segments ? base64 : `binary'${base64}'`
15
+ } else if (keys_as_segments) {
16
+ acc[key] = value
15
17
  } else {
16
- const _type = target.elements[key]._type
17
- if (!keys_as_segments && typeof value === 'string' && _type !== 'cds.UUID') acc[key] = `'${value}'`
18
- else acc[key] = value
18
+ const el = target.elements[key]
19
+ const needsQuotes = typeof value === 'string' && (el._type !== 'cds.UUID' || el['@odata.Type'] === 'Edm.String')
20
+ acc[key] = needsQuotes ? `'${value}'` : value
19
21
  }
20
22
 
21
23
  return acc
@@ -24,19 +26,16 @@ module.exports = (target, srv, result, keys_as_segments) => {
24
26
  if (keys_as_segments) {
25
27
  let location = targetName
26
28
  for (const k in keyValuePairs) location += `/${keyValuePairs[k]}`
27
- return location
29
+ return encodeURI(location)
28
30
  }
29
31
 
30
32
  let keys
31
33
  const entries = Object.entries(keyValuePairs)
32
34
  if (entries.length === 1) {
33
35
  keys = entries[0][1]
34
- if (target.elements[entries[0][0]]['@odata.Type'] === 'Edm.String') keys = `'${keys}'`
35
36
  } else {
36
- keys = entries
37
- .map(([key, value]) => `${key}=${target.elements[key]['@odata.Type'] === 'Edm.String' ? `'${value}'` : value}`)
38
- .join(',')
37
+ keys = entries.map(([key, value]) => `${key}=${value}`).join(',')
39
38
  }
40
39
 
41
- return `${targetName}(${keys})`
40
+ return encodeURI(`${targetName}(${keys})`)
42
41
  }
@@ -126,7 +126,7 @@ const _isNullableSingleton = query => query._target._isSingleton && query._targe
126
126
  const _isToOneAssoc = query =>
127
127
  query.SELECT.from.ref.length > 1 && typeof query.SELECT.from.ref.slice(-1)[0] === 'string'
128
128
 
129
- const _getCountFromResult = result => (Array.isArray(result) ? (result[0].$count ?? 0) : (result.$count ?? 0))
129
+ const _getCountFromResult = result => (Array.isArray(result) ? (result[0]?.$count ?? 0) : (result?.$count ?? 0))
130
130
 
131
131
  const _getCountFromResultForArrayOfQueries = result => {
132
132
  if (!Array.isArray(result)) return result.$count ?? 0
@@ -24,6 +24,8 @@ const _setLongTimeout = (cb, delay) => {
24
24
  }
25
25
 
26
26
  let T0
27
+ const MISSING_T0 =
28
+ 'Skipping periodic message processing: no provider tenant is configured via `cds.requires.multitenancy.t0`. This is unexpected outside the `mtx-sidecar` profile.'
27
29
 
28
30
  module.exports = class SchedulingService extends cds.Service {
29
31
  constructor(...args) {
@@ -101,6 +103,8 @@ module.exports = class SchedulingService extends cds.Service {
101
103
 
102
104
  // NOTE: in the future, we also need to trigger task processing in sidecar for mtx
103
105
  if (cds.env.profile !== 'mtx-sidecar') {
106
+ if (!T0) return LOG.warn(MISSING_T0)
107
+
104
108
  const periodic_flush = setInterval(() => _flush(T0), flush_interval_ms).unref()
105
109
  cds.on('shutdown', () => clearInterval(periodic_flush))
106
110
  _flush(T0)