@sap/cds 9.5.2 → 9.6.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.
Files changed (36) hide show
  1. package/CHANGELOG.md +46 -1
  2. package/bin/args.js +3 -3
  3. package/bin/serve.js +18 -14
  4. package/lib/compile/for/flows.js +11 -5
  5. package/lib/compile/for/lean_drafts.js +1 -1
  6. package/lib/compile/for/nodejs.js +2 -0
  7. package/lib/compile/parse.js +1 -1
  8. package/lib/core/linked-csn.js +23 -13
  9. package/lib/env/cds-env.js +6 -0
  10. package/lib/env/defaults.js +3 -1
  11. package/lib/index.js +2 -1
  12. package/lib/log/format/aspects/als.js +5 -1
  13. package/lib/log/format/aspects/cls.js +7 -3
  14. package/lib/log/service/index.js +5 -1
  15. package/lib/req/validate.js +1 -1
  16. package/lib/srv/cds.Service.js +37 -5
  17. package/libx/_runtime/common/generic/assert.js +1 -0
  18. package/libx/_runtime/common/generic/flows.js +8 -12
  19. package/libx/_runtime/common/generic/input.js +8 -2
  20. package/libx/_runtime/fiori/lean-draft.js +7 -3
  21. package/libx/_runtime/messaging/kafka.js +1 -0
  22. package/libx/odata/ODataAdapter.js +2 -1
  23. package/libx/odata/middleware/service-document.js +1 -1
  24. package/libx/odata/parse/afterburner.js +6 -2
  25. package/libx/odata/parse/cqn2odata.js +9 -9
  26. package/libx/odata/parse/grammar.peggy +6 -8
  27. package/libx/odata/parse/parser.js +1 -1
  28. package/libx/odata/utils/index.js +6 -2
  29. package/libx/queue/index.js +127 -84
  30. package/package.json +2 -2
  31. package/srv/outbox.cds +2 -0
  32. package/tasks/enterprise-messaging-deploy.js +7 -5
  33. package/lib/srv/middlewares/sap-statistics.js +0 -13
  34. package/lib/utils/index.js +0 -2
  35. package/libx/_runtime/common/generic/stream.js +0 -21
  36. package/libx/_runtime/common/i18n/index.js +0 -79
package/CHANGELOG.md CHANGED
@@ -4,6 +4,51 @@
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.6.1 - 2025-12-18
8
+
9
+ ### Fixed
10
+
11
+ - Status check in case of non-existing subject
12
+ - Gracefully handle bad value when calculating `@Core.OperationAvailable` from `@from`
13
+
14
+ ## Version 9.6.0 - 2025-12-16
15
+
16
+ ### Added
17
+
18
+ - New config entry `cds.folders.apps = 'app/*'` to fetch and load all .cds files from subfolders of `./app` automatically. This eliminates the need for an `./app/index.cds` file with respective `using` directives. Can be disabled by setting `cds.folders.apps` to `false`.
19
+ - Support for direct CRUD on draft-enabled entities (beta)
20
+ + Enable via `cds.fiori.direct_crud` (replacing `cds.features.new_draft_via_action`)
21
+ + In such requests, `IsActiveEntity` is defaulted to `true`
22
+ - Columns `task` and `appid` to `cds.outbox.Messages`
23
+ - `cds.env` getter for `appid`
24
+
25
+ ### Changed
26
+
27
+ - Improved Event Queue Processing
28
+ + For db-only message processors, the guarantee is now "exactly once" instead of "at least once".
29
+ + Use default target `queue` unless specified (formerly it was the name of the service). In effect, all services with the default queue config are processed together.
30
+ - Cleaned-up Model Reflection APIs
31
+ + `cds.entities` and `model.entities` provide access to all entities, not just the ones matching the first sources' namespace
32
+ + Function usage `cds.entities()` without passing a namespace no longer offers unqualified access to the entities of the first sources' namespace
33
+ + `srv.entities` returns `LinkedDefinitions` for that service
34
+ + Undocumented function usages `srv.entities()`, `srv.types()`, `srv.events()`, and `srv.actions()` are officially deprecated and will be removed with `cds^10`.
35
+ + For `srv.entities()`, use `cds.entities()` instead. The others have no replacement. Simply use `srv.types`, `srv.events`, and `srv.actions`.
36
+ + All `.entities` variants only provide `.texts` entities if `cds.features.compat_texts_entities = true` (default until `cds^10`). Use `.texts` property of the respective entity instead (i.e., `CatalogService.Books.texts` instead of `CatalogService['Books.texts']`).
37
+
38
+ ### Fixed
39
+
40
+ - Actions rejected with `415` for requests with empty body
41
+ - `cds.parse.expr` with template literals using "param" as parameter name
42
+ - Kafka messaging won't listen for messages without registered on handlers
43
+ - Single quote escaping in remote OData requests
44
+ - Lambda prefix within function calls in remote OData requests
45
+ - Hierarchy requests with `$top` in combination with `$filter`
46
+ - Response format of `ASSERT_DATA_TYPE` errors with draft messages
47
+ - Deletion of `@assert` draft messages on SAVE
48
+ - Check for `process.env.VCAP_SERVICES_FILE_PATH` in logging aspects enterprise messaging tasks
49
+ - Remove control data from deserialized task
50
+ - Hierarchy draft requests with $filter
51
+
7
52
  ## Version 9.5.2 - 2025-12-09
8
53
 
9
54
  ### Fixed
@@ -44,7 +89,6 @@
44
89
  - Support for pseudo protocols
45
90
  - Support for Async UCL tenant mapping notification flow
46
91
  - `flush` on a queued service returns a Promise that resolves when immediate work (i.e., not scheduled for future) is processed
47
- - For draft-enabled entities, IsActiveEntity=true can be omitted from url
48
92
  - Support for `@Common.DraftRoot.NewAction` annotation with feature flag `cds.features.new_draft_via_action`
49
93
  + Generic collection bound action `draftNew` will be added to draft enabled entities
50
94
  + The action specified in the annotation will be rewritten into a draft `NEW` event
@@ -74,6 +118,7 @@
74
118
  - Read-after-write for create events during draft choreography will no longer include messages targeting siblings
75
119
  - `before` and `after` handlers now really run in parallel. If that causes trouble, you can restore the previous behavior with `cds.features.async_handler_compat=true` until `@sap/cds@10`.
76
120
  - Escaping of JSON escape sequences during localization
121
+ - Persisted draft messages in case of on-commit errors
77
122
 
78
123
  ## Version 9.4.5 - 2025-11-07
79
124
 
package/bin/args.js CHANGED
@@ -17,12 +17,12 @@ module.exports = function _args4 (task, argv) {
17
17
  }
18
18
  // consistent production setting for NODE_ENV and CDS_ENV
19
19
  if (process.env.NODE_ENV !== 'production') { if (process.env.CDS_ENV?.split(',').includes('production')) process.env.NODE_ENV = 'production' }
20
- else process.env.CDS_ENV = Array.from(new Set([...process.env.CDS_ENV?.split(',') ?? [], 'production']))
20
+ else process.env.CDS_ENV = Array.from(new Set([...process.env.CDS_ENV?.split(',') ?? [], 'production'])).join(',')
21
21
 
22
22
  function add (k,v) { options[k.slice(2)] = v || true }
23
23
  function add_global (k,v='') {
24
- if (k === '--production') return process.env.CDS_ENV = Array.from(new Set([...process.env.CDS_ENV?.split(',') ?? [], 'production']))
25
- if (k === '--profile') return process.env.CDS_ENV = Array.from(new Set([...process.env.CDS_ENV?.split(',') ?? [], ...v.split(',')]))
24
+ if (k === '--production') return process.env.CDS_ENV = Array.from(new Set([...process.env.CDS_ENV?.split(',') ?? [], 'production'])).join(',')
25
+ if (k === '--profile') return process.env.CDS_ENV = Array.from(new Set([...process.env.CDS_ENV?.split(',') ?? [], ...v.split(',')])).join(',')
26
26
  if (k === '--odata') v = { flavor:v }
27
27
  let e=env || (env={}), path = k.slice(2).split('-')
28
28
  while (path.length > 1) { let p = path.shift(); e = e[p]||(e[p]={}) }
package/bin/serve.js CHANGED
@@ -1,17 +1,21 @@
1
1
  #!/usr/bin/env node
2
- module.exports = exports = Object.assign ( serve, {
3
- options: [
4
- '--service', '--from', '--to', '--at', '--with',
5
- '--port', '--workers',
6
- ],
7
- flags: [
8
- '--project', '--projects',
9
- '--in-memory', '--in-memory?',
10
- '--mocked', '--with-mocks', '--with-bindings', '--resolve-bindings',
11
- '--watch', '--with-mtx',
12
- ],
13
- shortcuts: [ '-s', undefined, '-2', '-a', '-w', undefined, undefined, '-p' ],
14
- help: `
2
+ module.exports = exports = serve
3
+
4
+ exports.options = [
5
+ '--service', '--from', '--to', '--at', '--with',
6
+ '--port', '--workers',
7
+ ]
8
+
9
+ exports.flags = [
10
+ '--project', '--projects',
11
+ '--in-memory', '--in-memory?',
12
+ '--mocked', '--with-mocks', '--with-bindings', '--resolve-bindings',
13
+ '--watch', '--with-mtx',
14
+ ]
15
+
16
+ exports.shortcuts = [ '-s', undefined, '-2', '-a', '-w', undefined, undefined, '-p' ]
17
+
18
+ exports.help = `
15
19
  # SYNOPSIS
16
20
 
17
21
  *cds serve* [ <filenames> ] [ <options> ]
@@ -136,7 +140,7 @@ module.exports = exports = Object.assign ( serve, {
136
140
  *cds watch* some/project
137
141
  *cds watch*
138
142
 
139
- `})
143
+ `
140
144
 
141
145
 
142
146
  const cds = require('../lib'), { exists, isfile, local, redacted, path } = cds.utils
@@ -13,12 +13,18 @@ const getFrom = action => {
13
13
  }
14
14
 
15
15
  function addOperationAvailableToActions(actions, statusEnum, statusElementName) {
16
- for (const action of Object.values(actions)) {
16
+ action: for (const action of Object.values(actions)) {
17
17
  const fromList = getFrom(action)
18
- const conditions = fromList.map(from => {
18
+ const conditions = []
19
+ for (const from of fromList) {
19
20
  const value = from['#'] ? statusEnum[from['#']]?.val ?? from['#'] : from
20
- return `$self.${statusElementName} = ${typeof value === 'string' ? `'${value}'` : value}`
21
- })
21
+ if (typeof value !== 'string') {
22
+ const msg = `Error while constructing @Core.OperationAvailable for action "${action.name}" of "${action.parent.name}". Value of @from must either be an enum symbol or a raw string.`
23
+ cds.log('cds|edmx').warn(msg)
24
+ continue action
25
+ }
26
+ conditions.push(`$self.${statusElementName} = '${value}'`)
27
+ }
22
28
  const condition = `(${conditions.join(' OR ')})`
23
29
  const parsedXpr = cds.parse.expr(condition)
24
30
  action['@Core.OperationAvailable'] ??= {
@@ -74,7 +80,7 @@ function addActionsToTarget(targetAnnotation, entity, actions) {
74
80
  identification.push({
75
81
  $Type: 'UI.DataFieldForAction',
76
82
  Action: `${entity._service.name}.${actionName}`,
77
- Label: action["@Common.Label"] ?? action["@title"] ?? `{i18n>${actionName}}`,
83
+ Label: action['@Common.Label'] ?? action['@title'] ?? `{i18n>${actionName}}`,
78
84
  ...(entity['@odata.draft.enabled'] && {
79
85
  '@UI.Hidden': {
80
86
  '=': true,
@@ -264,6 +264,6 @@ module.exports = function cds_compile_for_lean_drafts(csn) {
264
264
  // will insert drafts entities, so that others can use `.drafts` even without incoming draft requests
265
265
  addDraftEntity(def, csn)
266
266
 
267
- if (cds.env.fiori.draft_new_action) addNewActionAnnotation(def)
267
+ if (cds.env.fiori.direct_crud) addNewActionAnnotation(def)
268
268
  }
269
269
  }
@@ -46,6 +46,8 @@ function _compile_for_nodejs (csn, o) {
46
46
 
47
47
  module.exports = function cds_compile_for_nodejs (csn,o) {
48
48
  if ('_4nodejs' in csn) return csn._4nodejs
49
+ // REVISIT: remove _compat_texts_entities with cds^10
50
+ if (cds.env.features.compat_texts_entities) Object.defineProperty(csn, '_compat_texts_entities', { value: true, enumerable: true })
49
51
  TRACE?.time('cds.compile 4nodejs'.padEnd(22)); try {
50
52
  let result, next = ()=> result ??= _compile_for_nodejs (csn,o)
51
53
  cds.emit ('compile.for.runtime', csn, o, next)
@@ -87,7 +87,7 @@ exports.ttl = (parse, strings, ...values) => {
87
87
  for (let k in o) {
88
88
  const x = o[k]
89
89
  if (!x) continue
90
- if (x.param) {
90
+ if (x.param && x.ref) {
91
91
  let val = values[x.ref[0]]; if (val === undefined) continue
92
92
  let y = o[k] = cxn4(val) //; y.$ = x.ref[0]
93
93
  if (x.cast) y.cast = x.cast
@@ -107,22 +107,32 @@ class LinkedCSN {
107
107
  return this
108
108
  }
109
109
 
110
- childrenOf (x, filter = ()=>true, defs = this.definitions) {
111
- const children = namespace => !namespace ? children : this.childrenOf (namespace,filter)
112
- const prefix = !x ? '' : typeof x === 'string' ? x+'.' : ((x = x.namespace || x.name)) ? x+'.' : ''
113
- for (let fqn in defs) if (fqn.startsWith(prefix)) {
114
- const d = defs[fqn]; if (!filter(d)) continue
115
- else children[fqn.slice(prefix.length)] = d
116
- }
117
- return _iterable (children)
110
+ childrenOf (x, include = ()=>true, defs = this.definitions, children = new LinkedDefinitions) {
111
+ const prefix = !x ? '' : typeof x === 'string' ? x+'.' : x.namespace ? x.namespace+'.' : x.name ? x.name+'.' : ''
112
+ for (let name in defs)
113
+ if (name.startsWith(prefix) && include (defs[name]))
114
+ children[name.slice(prefix.length)] = defs[name]
115
+ return children
116
+ }
117
+
118
+ get exports() {
119
+ const exports = this.childrenOf (this)
120
+ return this.set ('exports', exports)
121
+ }
122
+
123
+ get entities() {
124
+ // REVISIT: remove _compat_texts_entities with cds^10
125
+ const filter = this._compat_texts_entities ? d => d.is_entity : d => d.is_entity && !d.name.endsWith('.texts')
126
+ const entities = ns => this.childrenOf (ns, filter)
127
+ for (let d of this.definitions) if (d.is_entity) entities[d.name] = d // adds all entities
128
+ Object.setPrototypeOf (entities, entities (this.namespace)) // adds exported entities
129
+ return this.set ('entities', entities)
118
130
  }
119
131
 
120
- get exports() { return this.set ('exports', this.childrenOf (this)) }
121
- get entities() { return this.set ('entities', this.childrenOf (this, d => d.is_entity)) }
122
132
  get services() {
123
- let srvs = this.all (d => d.is_service)
124
- for (let s of srvs) Object.defineProperty (srvs, s.name, {value:s})
125
- return this.set ('services', srvs)
133
+ const services = this.all (d => d.is_service)
134
+ for (let s of services) Object.defineProperty (services, s.name, {value:s})
135
+ return this.set ('services', services)
126
136
  }
127
137
 
128
138
  /** A common cache for all kinds of things -> keeps the models clean */
@@ -92,6 +92,12 @@ class Config {
92
92
  // Complete service configurations from cloud service bindings
93
93
  this._add_cloud_service_bindings(process.env)
94
94
 
95
+ this.appid ??= null
96
+ if (typeof this.appid === 'boolean') {
97
+ if (this.appid) this.#import(_home, 'package.json', { get: p => ({ appid: p.name }) })
98
+ else this.appid = null
99
+ }
100
+
95
101
  // Only if feature is enabled
96
102
  if (this.features && this.features.emulate_vcap_services) {
97
103
  this._emulate_vcap_services()
@@ -47,6 +47,7 @@ module.exports = {
47
47
  precise_timestamps: false,
48
48
  ieee754compatible: undefined,
49
49
  consistent_params: true, //> remove with cds^10
50
+ compat_texts_entities: true, //> remove with cds^10
50
51
  annotate_for_flows: true,
51
52
  history_for_flows: true,
52
53
  compile_for_assert: undefined,
@@ -62,7 +63,7 @@ module.exports = {
62
63
  draft_lock_timeout: true,
63
64
  draft_deletion_timeout: true,
64
65
  draft_messages: true,
65
- draft_new_action: false
66
+ direct_crud: false
66
67
  },
67
68
 
68
69
  ql: {
@@ -105,6 +106,7 @@ module.exports = {
105
106
  db: 'db/',
106
107
  srv: 'srv/',
107
108
  app: 'app/',
109
+ apps: 'app/*',
108
110
  },
109
111
 
110
112
  i18n: {
package/lib/index.js CHANGED
@@ -53,7 +53,7 @@ const cds = exports = module.exports = global.cds = new class cds extends EventE
53
53
  get i18n() { return super.i18n = require('./i18n/index.js') }
54
54
 
55
55
  // Model Reflection, Builtin types and classes
56
- get entities() { return this.db?.entities || this.model?.entities }
56
+ get entities() { return (this.model||this.db?.model)?.entities }
57
57
  get reflect() { return super.reflect = this.linked }
58
58
  get linked() { return super.linked = require('./core/linked-csn.js') }
59
59
  get builtin() { return super.builtin = require('./core/types.js') }
@@ -80,6 +80,7 @@ const cds = exports = module.exports = global.cds = new class cds extends EventE
80
80
  get unboxed() { return this.unqueued }
81
81
  get queued() { return super.queued = require('../libx/queue/index.js').queued }
82
82
  get unqueued() { return super.unqueued = require('../libx/queue/index.js').unqueued }
83
+ get flush() { return super.flush = require('../libx/queue/index.js').cdsFlush }
83
84
  get middlewares() { return super.middlewares = require('./srv/middlewares/index.js') }
84
85
  get odata() { return super.odata = require('../libx/odata/index.js') }
85
86
  get auth() { return super.auth = require('./srv/middlewares/auth/index.js') }
@@ -20,4 +20,8 @@ function als_aspect(module, level, args, toLog) {
20
20
 
21
21
  als_aspect.cf = () => Object.keys({ ...cds.env.log.als_custom_fields })
22
22
 
23
- module.exports = process.env.VCAP_SERVICES?.match(/"label":\s*"application-logs"/) ? als_aspect : () => {}
23
+ const vcap_services = process.env.VCAP_SERVICES_FILE_PATH
24
+ ? cds.utils.fs.readFileSync(process.env.VCAP_SERVICES_FILE_PATH, 'utf-8')
25
+ : process.env.VCAP_SERVICES
26
+
27
+ module.exports = vcap_services?.match(/"label":\s*"application-logs"/) ? als_aspect : () => {}
@@ -6,10 +6,14 @@ function cls_aspect(/* module, level, args, toLog */) {
6
6
 
7
7
  cls_aspect.cf = () => [...cds.env.log.cls_custom_fields]
8
8
 
9
- const VCAP_SERVICES = process.env.VCAP_SERVICES ? JSON.parse(process.env.VCAP_SERVICES) : {}
9
+ const vcap_services = JSON.parse(
10
+ process.env.VCAP_SERVICES_FILE_PATH
11
+ ? cds.utils.fs.readFileSync(process.env.VCAP_SERVICES_FILE_PATH, 'utf-8')
12
+ : process.env.VCAP_SERVICES || '{}'
13
+ )
10
14
 
11
15
  module.exports =
12
- VCAP_SERVICES['cloud-logging'] ||
13
- VCAP_SERVICES['user-provided']?.find(e => e.tags.includes('cloud-logging') || e.tags.includes('Cloud Logging'))
16
+ vcap_services['cloud-logging'] ||
17
+ vcap_services['user-provided']?.find(e => e.tags.includes('cloud-logging') || e.tags.includes('Cloud Logging'))
14
18
  ? cls_aspect
15
19
  : () => {}
@@ -7,7 +7,11 @@ module.exports = class LogService extends cds.Service {
7
7
 
8
8
  // Secure by basic auth from xsuaa in production
9
9
  if (process.env.NODE_ENV === 'production') {
10
- const { xsuaa } = (process.env.VCAP_SERVICES && JSON.parse(process.env.VCAP_SERVICES)) || {}
10
+ const { xsuaa } = JSON.parse(
11
+ process.env.VCAP_SERVICES_FILE_PATH
12
+ ? cds.utils.fs.readFileSync(process.env.VCAP_SERVICES_FILE_PATH, 'utf-8')
13
+ : process.env.VCAP_SERVICES || '{}'
14
+ )
11
15
  if (xsuaa) {
12
16
  const { clientid, clientsecret } = xsuaa[0].credentials
13
17
  const secret = 'Basic ' + Buffer.from (clientid + ':' + clientsecret).toString('base64')
@@ -201,7 +201,7 @@ class struct extends $any {
201
201
  validate (data, path, /** @type {Validation} */ ctx, elements = this.elements, skip={}) {
202
202
  if (data == null) return
203
203
  const path_ = !path ? [] : [...path, this.name]; if (path?.row) path_.push({...path})
204
- if (typeof data !== 'object') return ctx.error ('ASSERT_DATA_TYPE', path_, this.name, null, data, this.target)
204
+ if (typeof data !== 'object') return ctx.error ('ASSERT_DATA_TYPE', path_, null, null, data, this.target || this.type?.replace(/^cds\./,''))
205
205
  // check for required elements in case of inserts -- note: null values are handled in the payload loop below
206
206
  if (ctx.insert || data && path_.length && this._is_insert(data)) for (let each of this._required (elements)) {
207
207
  if (each.name in data) continue // got value for required element
@@ -97,13 +97,41 @@ class ReflectionAPI extends ConsumptionAPI {
97
97
  || !this.isDatabaseService && !/\W/.test(this.name) && this.name
98
98
  || undefined
99
99
  }
100
- get entities() { return super.entities = this.reflect (d => d.kind === 'entity') }
101
- get events() { return super.events = this.reflect (d => d.kind === 'event') }
102
- get types() { return super.types = this.reflect (d => !d.kind || d.kind === 'type') }
103
- get actions() { return super.actions = this.reflect (d => d.kind === 'action' || d.kind === 'function') }
100
+ get entities() {
101
+ if (!this.model) return super.entities = []
102
+ // REVISIT: remove _compat_texts_entities with cds^10
103
+ const filter = this.model._compat_texts_entities ? d => d.kind === 'entity' : d => d.kind === 'entity' && !d.name.endsWith('.texts')
104
+ const entities = this.reflect(filter)
105
+ return super.entities = deconstructable_and_iterable(entities, 'entities', this)
106
+ }
107
+ get events() {
108
+ if (!this.model) return super.events = []
109
+ const events = this.reflect(d => d.kind === 'event')
110
+ return super.events = deconstructable_and_iterable(events, 'events', this)
111
+ }
112
+ get types() {
113
+ if (!this.model) return super.types = []
114
+ const types = this.reflect(d => !d.kind || d.kind === 'type')
115
+ return super.types = deconstructable_and_iterable(types, 'types', this)
116
+ }
117
+ get actions() {
118
+ if (!this.model) return super.actions = []
119
+ const actions = this.reflect(d => d.kind === 'action' || d.kind === 'function')
120
+ return super.actions = deconstructable_and_iterable(actions, 'actions', this)
121
+ }
104
122
  reflect (filter) { return this.model?.childrenOf (this.namespace, filter) || [] }
105
123
  }
106
124
 
125
+ const deconstructable_and_iterable = (it, what, srv) => {
126
+ // REVISIT: remove deprecated function API with cds^10
127
+ const compat_function_api = Object.assign(compat_function_factory(what, srv, it), it)
128
+ // srv.* is both deconstructable and iterable
129
+ return Object.setPrototypeOf(compat_function_api, it)
130
+ }
131
+ const compat_function_factory = (api, srv, it) => cds.utils.deprecated (ns => {
132
+ return !ns || ns === srv.namespace ? it : srv.model[api]?.(ns) || {}
133
+ }, { kind: 'API', old: `srv.${api}()`, use: api === 'entities' ? `cds.${api}()` : undefined })
134
+
107
135
 
108
136
  /**
109
137
  * This class provides the API used by service providers to add event handlers.
@@ -111,6 +139,10 @@ class ReflectionAPI extends ConsumptionAPI {
111
139
  */
112
140
  class Service extends ReflectionAPI {
113
141
 
142
+ /**
143
+ * @param {string} name
144
+ * @param {import('../core/linked-csn').LinkedCSN} model
145
+ */
114
146
  constructor (name, model, options) { super()
115
147
  if (typeof name === 'object') [ model, options, name = _service_in(model) ] = [ name, model ]
116
148
  this.name = name || new.target.name // i.e. when called without any arguments
@@ -124,7 +156,7 @@ class Service extends ReflectionAPI {
124
156
 
125
157
  // Handler registration API
126
158
  prepend (fn) { return this.handlers.prepend.call (this,fn) }
127
- /** @typedef {( entity?, path?, handler:(req:import('../req/request'))=>{})=> Service} boa */
159
+ /** @typedef {( event, entity?, handler:(req:import('../req/request'))=>{})=> Service} boa */
128
160
  /** @type boa */ before (...args) { return this.handlers.register (this, 'before', ...args) }
129
161
  /** @type boa */ on (...args) { return this.handlers.register (this, 'on', ...args) }
130
162
  /** @type boa */ after (...args) { return this.handlers.register (this, 'after', ...args) }
@@ -128,6 +128,7 @@ module.exports = cds.service.impl(async function () {
128
128
 
129
129
  const failedAsserts = failedColumns.map(([element, message]) => {
130
130
  const error = {
131
+ status: 400,
131
132
  code: 'ASSERT',
132
133
  target: element,
133
134
  numericSeverity: 4,
@@ -9,24 +9,20 @@ const FLOW_PREVIOUS = '$flow.previous'
9
9
 
10
10
  const $transitions_ = Symbol.for('transitions_')
11
11
 
12
- function buildAllowedCondition(action, statusElementName, statusEnum) {
12
+ function isCurrentStatusInFrom(result, action, statusElementName, statusEnum) {
13
13
  const fromList = getFrom(action)
14
- const conditions = fromList.map(from => {
14
+ const allowed = fromList.filter(from => {
15
15
  const value = from['#'] ? (statusEnum[from['#']]?.val ?? statusEnum[from['#']]['$path'].at(-1)) : from
16
- return `${statusElementName} = ${typeof value === 'string' ? `'${value}'` : value}`
16
+ return result[statusElementName] === value
17
17
  })
18
- return `(${conditions.join(' OR ')})`
19
- }
20
-
21
- async function isCurrentStatusInFrom(subject, action, statusElementName, statusEnum) {
22
- const cond = buildAllowedCondition(action, statusElementName, statusEnum)
23
- const parsedXpr = cds.parse.expr(cond)
24
- const dbEntity = await SELECT.one.from(subject).where(parsedXpr)
25
- return dbEntity !== undefined
18
+ return allowed.length
26
19
  }
27
20
 
28
21
  async function checkStatus(subject, action, statusElementName, statusEnum) {
29
- const allowed = await isCurrentStatusInFrom(subject, action, statusElementName, statusEnum)
22
+ const result = await SELECT.one.from(subject)
23
+ if (!result) cds.error(404)
24
+
25
+ const allowed = isCurrentStatusInFrom(result, action, statusElementName, statusEnum)
30
26
  if (!allowed) {
31
27
  const from = getFrom(action)
32
28
  const fromValues = JSON.stringify(from.flatMap(el => Object.values(el)))
@@ -360,9 +360,15 @@ async function validate_input(req) {
360
360
 
361
361
  const errs = cds.validate(req.data, req.target, assertOptions)
362
362
  if (errs) {
363
+ if (errs.some(e => e.message in EARLY_REJECT_CODES)) {
364
+ // ensure to use the orginal req.error, not the one monkey patched for draft messages
365
+ // REVISIT: this is an ugly workaround -> fix in lean draft please!
366
+ const errorFn = cds.Request.prototype.error.bind(req)
367
+ errs.forEach(err => errorFn(err))
368
+ req.reject()
369
+ }
363
370
  errs.forEach(err => req.error(err))
364
- if (errs.some(e => e.message in EARLY_REJECT_CODES)) req.reject()
365
- else return
371
+ return
366
372
  }
367
373
 
368
374
  // -------------------------------------------------
@@ -705,7 +705,7 @@ const draftHandle = async function (req) {
705
705
  if (typeof rootEntityName === 'object') rootEntityName = rootEntityName.id
706
706
  const rootEntity = this.model.definitions[rootEntityName]
707
707
 
708
- const isNewDraftViaActionEnabled = cds.env.fiori.draft_new_action ?? false
708
+ const isNewDraftViaActionEnabled = cds.env.fiori.direct_crud ?? false
709
709
  let newDraftAction = rootEntity['@Common.DraftRoot.NewAction']
710
710
  if (typeof newDraftAction != 'string' || !newDraftAction.length) newDraftAction = false
711
711
  else newDraftAction = newDraftAction.split('.').pop()
@@ -979,7 +979,7 @@ const draftHandle = async function (req) {
979
979
  if (error.code) errors.push({ ...error })
980
980
  if (error.details) errors.push(...error.details.map(e => ({ ...e })))
981
981
  }
982
- const nextDraftMessages = compileUpdatedDraftMessages(errors, persistedDraftMessages, {}, draftRef)
982
+ const nextDraftMessages = compileUpdatedDraftMessages(errors, persistedDraftMessages, _req.data || {}, draftRef)
983
983
  await cds.tx(async () => {
984
984
  await UPDATE('DRAFT.DraftAdministrativeData')
985
985
  .set({ DraftMessages: nextDraftMessages })
@@ -1762,12 +1762,16 @@ function _cleansed(query, model) {
1762
1762
 
1763
1763
  // set the target to null to ensure cds.infer(...) correctly infer the
1764
1764
  // target after query modifications
1765
- draftsQuery._target = null
1765
+ Object.defineProperty(draftsQuery, '_target', { value: null, configurable: true, writable: true })
1766
1766
  let draftSelect = draftsQuery.SELECT
1767
1767
  let querySelect = query.SELECT
1768
1768
 
1769
1769
  // in the $apply scenario, only the most inner nested SELECT data structure must be cleansed
1770
1770
  while (draftSelect.from.SELECT) {
1771
+ // set the target to null to ensure cds.infer(...) correctly infer the
1772
+ // target after query modifications
1773
+ if (draftSelect.from._target)
1774
+ Object.defineProperty(draftSelect.from, '_target', { value: null, configurable: true, writable: true })
1771
1775
  draftSelect = draftSelect.from.SELECT
1772
1776
  querySelect = querySelect.from.SELECT
1773
1777
  }
@@ -94,6 +94,7 @@ class KafkaService extends cds.MessagingService {
94
94
  }
95
95
 
96
96
  async startListening() {
97
+ if (!this._listenToAll.value && !this.subscribedTopics.size) return
97
98
  const consumer = this.client.consumer({ groupId: this.consumerGroup })
98
99
  await consumer.connect()
99
100
 
@@ -50,7 +50,8 @@ module.exports = class ODataAdapter extends HttpAdapter {
50
50
  const isJson = type.match(/^application\/json$/) && suffix !== ''
51
51
 
52
52
  // POST with empty body is allowed if no content-type header is set
53
- if (req.method === 'POST' && (!contentLength || isJson)) return jsonBodyParser(req, res, next)
53
+ if (req.method === 'POST' && (!contentLength || contentLength === '0' || isJson))
54
+ return jsonBodyParser(req, res, next)
54
55
 
55
56
  if (req.method in { POST: 1, PUT: 1, PATCH: 1 }) {
56
57
  if (!isJson) {
@@ -42,7 +42,7 @@ module.exports = adapter => {
42
42
  }
43
43
  }
44
44
 
45
- const srvEntities = model.entities(service.definition.name)
45
+ const srvEntities = service.entities
46
46
 
47
47
  const exposedEntities = []
48
48
  for (const e in srvEntities) {
@@ -11,6 +11,8 @@ const resolveStructured = require('../../_runtime/common/utils/resolveStructured
11
11
  // Same regex as peggy parser
12
12
  const RELAXED_UUID_REGEX = /^[0-9a-z]{8}-?[0-9a-z]{4}-?[0-9a-z]{4}-?[0-9a-z]{4}-?[0-9a-z]{12}$/i
13
13
 
14
+ let _isRelevantKey
15
+
14
16
  function _getDefinition(definition, name, namespace) {
15
17
  return (
16
18
  definition?.definitions?.[name] ||
@@ -336,7 +338,7 @@ function _processSegments(from, model, namespace, cqn, protocol) {
336
338
 
337
339
  ref[i] = null
338
340
  ref[i - keyCount] = base
339
- incompleteKeys = keyCount < keys.filter(k => k !== 'IsActiveEntity').length
341
+ incompleteKeys = keyCount < keys.filter(_isRelevantKey).length
340
342
  } else {
341
343
  // > entity or property (incl. nested) or navigation or action or function
342
344
  keys = null
@@ -746,7 +748,7 @@ const _checkAllKeysProvided = (params, entity) => {
746
748
 
747
749
  if (!keysOfEntity) return
748
750
  for (const keyOfEntity of keysOfEntity) {
749
- if (keyOfEntity !== 'IsActiveEntity' && !(keyOfEntity in params)) {
751
+ if (_isRelevantKey(keyOfEntity) && !(keyOfEntity in params)) {
750
752
  if (isView && entity.params[keyOfEntity].default) {
751
753
  // will be added later?
752
754
  continue
@@ -876,6 +878,8 @@ function _validateQuery(SELECT, target, isOne, model) {
876
878
  module.exports = (cqn, model, namespace, protocol) => {
877
879
  if (!model) return cqn
878
880
 
881
+ _isRelevantKey ??= cds.env.fiori.direct_crud ? k => k !== 'IsActiveEntity' : () => true
882
+
879
883
  const from = resolveFromSelect(cqn)
880
884
  const { ref } = from
881
885
 
@@ -71,7 +71,7 @@ function hasValidProps(obj, ...names) {
71
71
  return true
72
72
  }
73
73
 
74
- function _args(args, func, navPrefix) {
74
+ function _args(args, func = null, navPrefix = [], isLambda = false) {
75
75
  const res = []
76
76
 
77
77
  for (const cur of args) {
@@ -81,11 +81,11 @@ function _args(args, func, navPrefix) {
81
81
  }
82
82
 
83
83
  if (hasValidProps(cur, 'func', 'args')) {
84
- res.push(`${cur.func}(${_args(cur.args, cur.func, navPrefix)})`)
84
+ res.push(`${cur.func}(${_args(cur.args, cur.func, navPrefix, isLambda)})`)
85
85
  } else if (hasValidProps(cur, 'ref')) {
86
- res.push(_format(cur, null, null, null, null, null, navPrefix))
86
+ res.push(_format(cur, null, null, null, isLambda, func, navPrefix))
87
87
  } else if (hasValidProps(cur, 'val')) {
88
- res.push(_format(cur, null, null, null, null, func))
88
+ res.push(_format(cur, null, null, null, isLambda, func))
89
89
  }
90
90
  }
91
91
 
@@ -105,13 +105,13 @@ const _in = (column, /* in */ collection, target, kind, isLambda, navPrefix) =>
105
105
  }
106
106
  }
107
107
 
108
- const _odataV2Func = (func, args, navPrefix) => {
108
+ const _odataV2Func = (func, args, navPrefix, isLambda) => {
109
109
  switch (func) {
110
110
  case 'contains':
111
111
  // this doesn't support the contains signature with two collections as args, introduced in odata v4.01
112
- return `substringof(${_args([args[1], args[0]], null, navPrefix)})`
112
+ return `substringof(${_args([args[1], args[0]], null, navPrefix, isLambda)})`
113
113
  default:
114
- return `${func}(${_args(args, func, navPrefix)})`
114
+ return `${func}(${_args(args, func, navPrefix, isLambda)})`
115
115
  }
116
116
  }
117
117
 
@@ -128,8 +128,8 @@ const _format = (cur, elementName, target, kind, isLambda, func, navPrefix = [])
128
128
  if (hasValidProps(cur, 'func')) {
129
129
  if (cur.args?.length) {
130
130
  return kind === 'odata-v2'
131
- ? _odataV2Func(cur.func, cur.args, navPrefix)
132
- : `${cur.func}(${_args(cur.args, cur.func)})`
131
+ ? _odataV2Func(cur.func, cur.args, navPrefix, isLambda)
132
+ : `${cur.func}(${_args(cur.args, cur.func, navPrefix, isLambda)})`
133
133
  }
134
134
  return `${cur.func}()`
135
135
  }