@sap/cds 9.6.4 → 9.7.0

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 (47) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/bin/serve.js +38 -26
  3. package/lib/compile/for/flows.js +92 -19
  4. package/lib/compile/for/lean_drafts.js +0 -47
  5. package/lib/compile/for/nodejs.js +47 -14
  6. package/lib/compile/for/odata.js +20 -0
  7. package/lib/compile/load.js +22 -25
  8. package/lib/compile/minify.js +29 -11
  9. package/lib/compile/parse.js +1 -1
  10. package/lib/compile/resolve.js +133 -76
  11. package/lib/compile/to/csn.js +2 -2
  12. package/lib/dbs/cds-deploy.js +48 -43
  13. package/lib/env/cds-env.js +6 -0
  14. package/lib/env/cds-requires.js +9 -3
  15. package/lib/index.js +3 -1
  16. package/lib/plugins.js +1 -1
  17. package/lib/req/request.js +2 -2
  18. package/lib/srv/bindings.js +10 -5
  19. package/lib/srv/middlewares/auth/index.js +7 -5
  20. package/lib/srv/protocols/hcql.js +8 -3
  21. package/lib/srv/protocols/index.js +1 -0
  22. package/lib/utils/cds-utils.js +28 -1
  23. package/lib/utils/colors.js +1 -1
  24. package/libx/_runtime/common/generic/assert.js +1 -7
  25. package/libx/_runtime/common/generic/flows.js +14 -4
  26. package/libx/_runtime/common/utils/resolveView.js +4 -0
  27. package/libx/_runtime/fiori/lean-draft.js +8 -3
  28. package/libx/_runtime/messaging/common-utils/authorizedRequest.js +4 -0
  29. package/libx/_runtime/messaging/enterprise-messaging-utils/EMManagement.js +12 -12
  30. package/libx/_runtime/messaging/enterprise-messaging.js +1 -1
  31. package/libx/_runtime/messaging/http-utils/token.js +18 -3
  32. package/libx/_runtime/messaging/message-queuing.js +7 -7
  33. package/libx/_runtime/remote/Service.js +3 -1
  34. package/libx/_runtime/remote/utils/client.js +1 -0
  35. package/libx/_runtime/remote/utils/query.js +0 -1
  36. package/libx/odata/middleware/batch.js +128 -112
  37. package/libx/odata/middleware/error.js +7 -3
  38. package/libx/odata/parse/afterburner.js +10 -11
  39. package/libx/odata/parse/grammar.peggy +4 -2
  40. package/libx/odata/parse/parser.js +1 -1
  41. package/libx/odata/utils/odataBind.js +8 -2
  42. package/libx/queue/index.js +1 -0
  43. package/package.json +4 -7
  44. package/srv/outbox.cds +1 -1
  45. package/srv/ucl-service.cds +3 -5
  46. package/bin/colors.js +0 -2
  47. package/libx/_runtime/.eslintrc +0 -14
package/CHANGELOG.md CHANGED
@@ -4,6 +4,48 @@
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.7.0 - 2026-02-02
8
+
9
+ ### Added
10
+
11
+ - Support for express 5 (in addition to express 4)
12
+ - New config option `cds.requires.db.data` to configure source folders for initial data and test data CSV files
13
+ - Enterprise Messaging now caches access tokens to support high-throughput message processing from Event Mesh
14
+ - Automatically add `@Common.DraftRoot.NewAction` for each draft-enabled entity during `compile.for.odata` via `cds.fiori.direct_crud=true`
15
+ - Support for `null` value in `@odata.bind`
16
+ - Validation of flow annotations at compile step
17
+
18
+ ### Changed
19
+
20
+ - Colors are enabled by default in GitHub Actions workflows
21
+ - `queue`: Manually update `lastAttemptTimestamp` of outbox messages (instead of relying on `@cds.on.update: $now`)
22
+ - `express` is no longer a peer dependency of `@sap/cds` but a regular one. Applications that want to pin it or require it in their custom code, should declare the dependency on their own.
23
+ - `hcql` response format: `{ data: [], errors: [] }`
24
+
25
+ ### Fixed
26
+
27
+ - Correctly respond with status `404` when `@cds.api.ignore` annotated action is requested
28
+ - Ensure plugin debug emitted with `DEBUG=all`
29
+ - Prevent app crash when `JSON.parse` of operation parameters fails
30
+ - Generate correct UI annotations for Status Transition Flows when building and compiling
31
+ - Remote services: Prefer `cds.context.user?.authInfo?.token?.jwt` over JWT in HTTP header of incoming request
32
+ - References to child elements in `@Common.Text` annotations will now be checked. The reference will not be included in `@cds.search`, in case ...
33
+ * ... the reference can not be found in the annotated entity's associations
34
+ * ... the referenced entity is annotated with `@cds.persistence.skip`
35
+ * ... the referenced field does not exist in the referenced entity
36
+ * References to children of children will be ignored.
37
+ - OData parser: Ignore superfluous brackets
38
+ - Prevent app crash in case of `req.reject()` during draft activate triggered via OData batch
39
+ - `cds minify` no longer removes services if their actions are kept
40
+ - Better error when subquery can't be resolved for the current service
41
+ - Flows: Record transition to default value on `INSERT`/ `UPSERT`
42
+ - Error response properties of OData batch subrequests are now formatted identically to properties in single OData error responses
43
+ - Prevent `@Common.numericSeverity` from appearing in persistent draft messages (in addition to the correct property `numericSeverity`)
44
+
45
+ ### Removed
46
+
47
+ - `@cds.on.update: $now` from `cds.outbox.Messages.lastAttemptTimestamp`
48
+
7
49
  ## Version 9.6.4 - 2026-01-20
8
50
 
9
51
  ### Fixed
@@ -499,6 +541,12 @@
499
541
  - Deprecated stripping of unnecessary topic prefix `topic:` in messaging
500
542
  - Deprecated messaging `Outbox` class. Please use config or `cds.outboxed(srv)` to outbox your service.
501
543
 
544
+ ## Version 8.9.8 - 2025-12-17
545
+
546
+ ### Fixed
547
+
548
+ - `enterprise-messaging-shared`: preserve error listener during reconnect
549
+
502
550
  ## Version 8.9.7 - 2025-11-07
503
551
 
504
552
  ### Fixed
package/bin/serve.js CHANGED
@@ -144,7 +144,6 @@ exports.help = `
144
144
 
145
145
 
146
146
  const cds = require('../lib'), { exists, isfile, local, redacted, path } = cds.utils
147
- const COLORS = process.stdout.isTTY && !process.env.NO_COLOR || process.env.FORCE_COLOR
148
147
 
149
148
  /* eslint-disable no-console */
150
149
 
@@ -185,7 +184,7 @@ async function serve (all=[], o={}) {
185
184
 
186
185
  // Load local server.js early in order to allow setting custom cds.log.Loggers
187
186
  const cds_server = await _local_server_js() || cds.server
188
- if (!o.silent) _prepare_logging ()
187
+ if (!o.silent) _init_logging ()
189
188
 
190
189
  // The following things are meant for dev mode, which can be overruled by feature flags...
191
190
  const {features} = cds.env
@@ -218,7 +217,7 @@ async function serve (all=[], o={}) {
218
217
  // server.on ('close', _shutdown) // IMPORTANT: Don't do that as that would be a very strange loop
219
218
  // process.on ('exit', _shutdown) // IMPORTANT: Don't do that as that would be a very strange loop
220
219
  async function _started() {
221
- _assert_no_multi_installs()
220
+ _check_setup()
222
221
  const url = cds.server.url = `http://localhost:${server.address().port}`
223
222
  cds.emit ('listening', {server,url}) //> inform local listeners
224
223
  _resolve ({ server, url })
@@ -275,20 +274,22 @@ async function _local_server_js() {
275
274
  }
276
275
 
277
276
 
278
- function _prepare_logging () { // NOSONAR
277
+ function _init_logging () {
279
278
 
280
279
  const LOG = cds.log('cds.serve|server',{label:'cds'}); if (!LOG._info) return; else log = LOG.info
280
+ const { DIMMED, RESET, enabled:colors } = cds.utils.colors
281
+ const { inspect } = require('util')
281
282
 
282
283
  // print information when model is loaded
283
284
  cds.on ('loaded', ({$sources:srcs})=>{
284
- LOG.info (`loaded model from ${srcs.length} file(s):\n${COLORS ? '\x1b[2m' : ''}`)
285
+ LOG.info (`loaded model from ${srcs.length} file(s):\n ${DIMMED}`)
285
286
  const limit = 30, shown = srcs.length === limit + 1 ? limit + 1 : limit // REVISIT: configurable limit?
286
287
  for (let each of srcs.slice(0, shown)) console.log (' ', local(each))
287
288
  if (srcs.length > shown) {
288
289
  if (LOG._debug) for (let each of srcs.slice(shown)) console.log (' ', local(each))
289
290
  else console.log (` ...${srcs.length-shown} more. Run with DEBUG=serve to show all files.`)
290
291
  }
291
- COLORS && console.log ('\x1b[0m')
292
+ console.log (RESET)
292
293
  })
293
294
 
294
295
  // print information about each connected service
@@ -297,12 +298,14 @@ function _prepare_logging () { // NOSONAR
297
298
  })
298
299
 
299
300
  // print information about each provided service
301
+ const builtin = RegExp(`^(@sap/cds|${path.join(cds.home,'lib')}|${path.join(cds.home,'srv')})`)
302
+ const is_custom_impl = impl => !impl?.match(builtin)
300
303
  cds.on ('serving', (srv) => {
301
- const details = {}, loc = srv.definition?.$location, src = srv._source
304
+ const details = {}, loc = srv.definition?.$location, impl = srv._source
302
305
  if (srv.endpoints.length) details.at = srv.endpoints.map(ep => ep.path)
303
306
  if (loc?.file) details.decl = local(loc.file)+':'+loc.line
304
- if (src && !src.startsWith('@sap')) details.impl = local(src)
305
- LOG.info (srv.mocked ? 'mocking' : 'serving', srv.name, details)
307
+ if (is_custom_impl(impl)) details.impl = local (impl)
308
+ LOG.info (srv.mocked ? 'mocking' : 'serving', srv.name, inspect (details, {colors,compact:1}))
306
309
  })
307
310
 
308
311
  // print info when we are finally on air
@@ -313,13 +316,6 @@ function _prepare_logging () { // NOSONAR
313
316
  })
314
317
  }
315
318
 
316
- /** handles --watch option */
317
- function _watch (project,o) {
318
- o.args = process.argv.slice(2) .filter (a => a !== '--watch' && a !== '-w')
319
- return this.load('watch')([project],o)
320
- }
321
-
322
-
323
319
  /** handles --project option */
324
320
  function _chdir_to (project) {
325
321
  // try using the given project as dirname, e.g. './bookshop'
@@ -331,6 +327,11 @@ function _chdir_to (project) {
331
327
  catch { cds.error `No such folder or package: '${process.cwd()}' -> '${project}'` }
332
328
  }
333
329
 
330
+ /** handles --watch option */
331
+ function _watch (project,o) {
332
+ o.args = process.argv.slice(2) .filter (a => a !== '--watch' && a !== '-w')
333
+ return this.load('watch')([project],o)
334
+ }
334
335
 
335
336
  /** handles --in-memory[?] option */
336
337
  function _in_memory (o) {
@@ -349,7 +350,6 @@ function _in_memory (o) {
349
350
  }
350
351
  }
351
352
 
352
-
353
353
  /** handles --with-mocks option */
354
354
  function _with_mocks (o) {
355
355
  if (o.mocked || (o.mocked = o['with-mocks'])) {
@@ -363,17 +363,27 @@ function _with_mocks (o) {
363
363
  }
364
364
  }
365
365
 
366
- const _assert_no_multi_installs = ()=> { if (global.__cds_loaded_from?.size > 1) {
367
- console.error(`
368
- -----------------------------------------------------------------------
369
- ERROR: Package '@sap/cds' was loaded from different installations:`,
370
- [ ...global.__cds_loaded_from ],
371
- `\nEnsure a single install to avoid hard-to-resolve errors.
372
- -----------------------------------------------------------------------
373
- `);
366
+ function _check_setup() {
367
+ if (global.__cds_loaded_from?.size <= 1) return // all good
368
+ const home = require('os').homedir().toLowerCase()
369
+ const { BRIGHT, RED, DIMMED, RESET } = cds.utils.colors
370
+ console.error (`
371
+ -----------------------------------------------------------------------
372
+ ${BRIGHT+RED}ERROR:${RESET} @sap/cds was loaded from different locations:
373
+ ${DIMMED} ${[...global.__cds_loaded_from].map(
374
+ path => '\n ' + path.replace(home,'~') ).join('')}
375
+ ${RESET}
376
+ Ensure a single install to avoid hard-to-resolve errors.
377
+ -----------------------------------------------------------------------
378
+ `.replace(/^ {4}/gm,''))
374
379
  if (cds.env.server.exit_on_multi_install) process.exit(1)
375
- }}
380
+ }
376
381
 
382
+
383
+ /**
384
+ * Allows programmatic usage of 'cds serve' command.
385
+ * @param {...string} argv command line arguments
386
+ */
377
387
  exports.exec = function cds_serve (...argv) {
378
388
  try {
379
389
  const [ args, options ] = require('./args') (serve, argv)
@@ -383,4 +393,6 @@ exports.exec = function cds_serve (...argv) {
383
393
  process.exitCode = 1
384
394
  }
385
395
  }
396
+
397
+ // If called directly, e.g. 'node bin/serve.js', we execute the command
386
398
  if (!module.parent) exports.exec (...process.argv.slice(2))
@@ -17,7 +17,7 @@ function addOperationAvailableToActions(actions, statusEnum, statusElementName)
17
17
  const fromList = getFrom(action)
18
18
  const conditions = []
19
19
  for (const from of fromList) {
20
- const value = from['#'] ? statusEnum[from['#']]?.val ?? from['#'] : from
20
+ const value = from['#'] ? (statusEnum[from['#']]?.val ?? from['#']) : from
21
21
  if (typeof value !== 'string') {
22
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
23
  cds.log('cds|edmx').warn(msg)
@@ -37,10 +37,8 @@ function addOperationAvailableToActions(actions, statusEnum, statusElementName)
37
37
  function addSideEffectToActions(actions, statusElementName) {
38
38
  for (const action of Object.values(actions)) {
39
39
  const properties = []
40
- if (statusElementName.endsWith('.code')) {
41
- const baseName = statusElementName.slice(0, -5)
42
- properties.push(`in/${statusElementName}`)
43
- properties.push(`in/${baseName}/*`)
40
+ if (statusElementName.endsWith('.code') || statusElementName.endsWith('_code')) {
41
+ const baseName = statusElementName.substring(0, statusElementName.length - 5)
44
42
  properties.push(`in/${baseName}_code`)
45
43
  } else {
46
44
  properties.push(`in/${statusElementName}`)
@@ -129,8 +127,9 @@ function enhanceCSNwithFlowAnnotations4FE(csn) {
129
127
  const codeElem = targetDef.elements.code
130
128
  const statusEnum = resolveStatusEnum(csn, codeElem)
131
129
  if (statusEnum) {
132
- // REVISIT: is there no way to know from the CSN?
133
- const statusElementName = csn._4java ? elemName + '.code' : elemName + '_code'
130
+ const hasElemNameCode =
131
+ entity.elements && Object.prototype.hasOwnProperty.call(entity.elements, `${elemName}_code`)
132
+ const statusElementName = hasElemNameCode ? `${elemName}_code` : `${elemName}.code`
134
133
  addSideEffectToActions(toActions, statusElementName)
135
134
  addOperationAvailableToActions(fromActions, statusEnum, statusElementName)
136
135
  }
@@ -153,21 +152,21 @@ module.exports = function cds_compile_for_flows(csn) {
153
152
  const _requires_history = !history_for_flows
154
153
  ? () => false
155
154
  : history_for_flows === 'all'
156
- ? def => {
157
- for (const each in def.elements) {
158
- if (def.elements[each]['@flow.status']) {
159
- return true
155
+ ? def => {
156
+ for (const each in def.elements) {
157
+ if (def.elements[each]['@flow.status']) {
158
+ return true
159
+ }
160
160
  }
161
161
  }
162
- }
163
- : def => {
164
- for (const each in def.actions) {
165
- const action = def.actions[each]
166
- if (action && action[TO]?.['='] === FLOW_PREVIOUS) {
167
- return true
162
+ : def => {
163
+ for (const each in def.actions) {
164
+ const action = def.actions[each]
165
+ if (action && action[TO]?.['='] === FLOW_PREVIOUS) {
166
+ return true
167
+ }
168
168
  }
169
169
  }
170
- }
171
170
 
172
171
  /*
173
172
  * 1. propagate flows for well-known actions from extensions to definitions
@@ -255,6 +254,81 @@ module.exports = function cds_compile_for_flows(csn) {
255
254
  csn = dsn
256
255
  }
257
256
 
257
+ // validate flow in CSN
258
+ const messages = []
259
+ const validate = (status, enumVals, action, fromTo) => {
260
+ if (status === null) return true
261
+ if (enumVals) {
262
+ let val = status['#']
263
+ if (
264
+ !(typeof val === 'string' && Object.entries(enumVals).some(([key]) => key === val)) &&
265
+ !(fromTo === TO && typeof status === 'object' && status['='] === FLOW_PREVIOUS)
266
+ ) {
267
+ messages.push(`Invalid ${fromTo} value(s) in action ${action}`);
268
+ return false;
269
+ }
270
+ } else {
271
+ if (typeof status !== 'string' && !(fromTo === TO && typeof status === 'object' && status['='] === FLOW_PREVIOUS)) {
272
+ messages.push(`Invalid ${fromTo} value(s) in action ${action}`)
273
+ return false
274
+ }
275
+ }
276
+
277
+ return true
278
+ }
279
+
280
+ for (const name in csn.definitions) {
281
+ const def = csn.definitions[name]
282
+ if (!def.kind || def.kind !== 'entity' || !def.actions || !def.elements) continue
283
+
284
+ const statusElements = Object.values(def.elements).filter(e => e['@flow.status'])
285
+ if (statusElements.length === 0) continue
286
+ if (statusElements.length > 1) {
287
+ messages.push(`Entity ${name} has multiple status elements. Only one @flow.status element is allowed per entity`)
288
+ continue
289
+ }
290
+ const status = statusElements[0]
291
+
292
+ let enumVals
293
+ if (status.type === 'cds.Association') {
294
+ const target = csn.definitions[status.target]
295
+ if (!status.keys || status.keys.length !== 1) {
296
+ messages.push(`Status element in entity ${name} must have exactly one key when used as association`)
297
+ continue
298
+ }
299
+ const code = target.elements[status.keys[0].ref[0]]
300
+ enumVals = code.enum || csn.definitions[code.type]?.enum
301
+ } else if (status.enum) {
302
+ enumVals = status.enum
303
+ } else if (csn.definitions[status.type]?.enum) {
304
+ enumVals = csn.definitions[status.type].enum
305
+ }
306
+
307
+ for (const each in def.actions) {
308
+ const action = def.actions[each]
309
+ let froms = action[FROM]
310
+ if (froms !== undefined) {
311
+ froms = Array.isArray(froms) ? froms : [froms]
312
+ for (let from of froms) if (!validate(from, enumVals, each, FROM)) break
313
+ }
314
+ let tos = action[TO]
315
+ if (tos !== undefined) {
316
+ if (Array.isArray(tos)) {
317
+ messages.push(`${TO} must not be an array in action ${each}`)
318
+ continue
319
+ }
320
+ validate(tos, enumVals, each, TO)
321
+ }
322
+ }
323
+ }
324
+ if (messages.length) {
325
+ if (messages.length === 1) cds.error(messages[0])
326
+ else {
327
+ const errors = messages.map(message => ({ message }))
328
+ cds.error ('MULTIPLE_ERRORS', { details: errors })
329
+ }
330
+ }
331
+
258
332
  // REVISIT: annotate all X.transitions_ with @odata.draft.enabled: false
259
333
  for (const name in csn.definitions)
260
334
  if (name.endsWith('.transitions_')) csn.definitions[name]['@odata.draft.enabled'] = false
@@ -313,4 +387,3 @@ const FlowHistory = `{
313
387
  }`
314
388
 
315
389
  module.exports.enhanceCSNwithFlowAnnotations4FE = enhanceCSNwithFlowAnnotations4FE
316
- module.exports.getFrom = getFrom
@@ -68,51 +68,6 @@ function DraftEntity4(active, name = active.name + '.drafts') {
68
68
  return draft
69
69
  }
70
70
 
71
- function addNewActionAnnotation(def) {
72
- // Skip if a new action was defined manually
73
- if (def.own('@Common.DraftRoot.NewAction')) return
74
-
75
- // Skip for non draft roots
76
- if (!def.own('@Common.DraftRoot.ActivationAction')) return
77
-
78
- // TODO: This is perhaps THE ugliest way to automatically add a 'draftNew' action:
79
- // TODO: > Instead, this should happen in cds-compiler/lib/transfrom/draft/odata.js
80
- // TODO: > Within generateDrafts -> generateDraftForOData
81
- // TODO: > Unfortunately, the 'createAction' utility does not currently allow creating collection bound actions
82
-
83
- def['@Common.DraftRoot.NewAction'] = `${def._service.name}.draftNew`
84
-
85
- // TODO: Find a better way than this:
86
- // TODO: > By rewriting `draftNew` into a `NEW` req in draftHandle, action input validation is skipped
87
- // TODO: > This causes issues if the action has parameters derived from key fields that should be mandatory
88
- // TODO: > This will bubble up a NOT NULL CONSTRAINT error instead of raising a proper client error
89
- // TODO: > This behavior also occurs for regular custom actions
90
-
91
- // Format a list of cds action parameters, based on the entities key fields
92
- // > E.g.: [ 'dayKey: Integer', 'nameKey: String', ...]
93
- // > UUID keys are skipped as they are generated
94
- const idParameters = Object.values(def.keys)
95
- .filter(el => el.key && !el.virtual && el._type !== 'cds.UUID') // TODO: Ignore @UI.Hidden keys?
96
- .map(el => `${el.name}: ${el._type}`)
97
-
98
- // Use cds.linked to create a valid action definition
99
- const { draftNew } = cds.linked(`
100
- service Service {
101
- entity ActiveEntity { } actions {
102
- action draftNew(in: many $self, ${idParameters.join(', ')}) returns ActiveEntity;
103
- }
104
- }
105
- `).definitions['Service.ActiveEntity'].actions
106
-
107
- draftNew.name = 'draftNew'
108
- draftNew.returns = Object.create(def)
109
- draftNew.returns.type = def.name
110
- draftNew.parent = { name: def.name}
111
- delete draftNew['$location']
112
-
113
- def.actions['draftNew'] = draftNew
114
- }
115
-
116
71
  module.exports = function cds_compile_for_lean_drafts(csn) {
117
72
  function _redirect(assoc, target) {
118
73
  assoc.target = target.name
@@ -263,7 +218,5 @@ module.exports = function cds_compile_for_lean_drafts(csn) {
263
218
 
264
219
  // will insert drafts entities, so that others can use `.drafts` even without incoming draft requests
265
220
  addDraftEntity(def, csn)
266
-
267
- if (cds.env.fiori.direct_crud) addNewActionAnnotation(def)
268
221
  }
269
222
  }
@@ -18,23 +18,56 @@ function _compile_for_nodejs (csn, o) {
18
18
  // if any @cds.search.* === true, @Common.Text should be ignored
19
19
  for (const key in def) if (key.startsWith('@cds.search') && def[key]) continue text_to_search
20
20
 
21
- let searched = false
21
+ let isSearchAddedProgrammatically = false
22
+
23
+ // Add elements referenced in common.text annotations to search-relevant elements
22
24
  for (const name in def.associations) {
23
25
  const assoc = def.associations[name]
24
- if (assoc['@Common.Text']?.['='] && def[`@cds.search.${assoc['@Common.Text']['=']}`] !== false) {
25
- def[`@cds.search.${assoc['@Common.Text']['=']}`] = true
26
- if (!searched) {
27
- // add all string elements to @cds.search, if not annotated with @cds.search = false
28
- for (const el in def.elements) {
29
- const search_el = def.elements[el]
30
- if (search_el._type === 'cds.String' && def[`@cds.search.${search_el.name}`] !== false) {
31
- def[`@cds.search.${search_el.name}`] = true
32
- }
33
- }
34
- searched = true
35
- }
36
- }
26
+
27
+ // Extract the reference value from the @Common.Text annotation
28
+ let annotationValue = assoc['@Common.Text']
29
+ if (!annotationValue) continue
30
+
31
+ // Extract the relevant label reference
32
+ let commonTextRef = annotationValue.ref
33
+ if (!commonTextRef) commonTextRef = annotationValue['='].split('.')
34
+
35
+ // Ignore empty references & those with more than two segments
36
+ if (commonTextRef.length < 1 || commonTextRef.length > 2) continue
37
+
38
+ // Make sure the reference is not explicitly excluded from search
39
+ const commonTextValue = commonTextRef.join('.')
40
+ if (def[`@cds.search.${commonTextValue}`] === false) continue
41
+
42
+ if (commonTextRef.length === 1) {
43
+ const element = def.elements[commonTextRef[0]]
44
+ if (!element || element.target || element.items) continue
45
+ }
46
+
47
+ if (commonTextRef.length === 2) {
48
+ const association = def.associations[commonTextRef[0]]
49
+ if (!association) continue
50
+
51
+ const associationTarget = dsn.definitions[association.target]
52
+ if (!associationTarget) continue
53
+ if (associationTarget['@cds.persistence.skip']) continue
54
+
55
+ const element = associationTarget.elements[commonTextRef[1]]
56
+ if (!element || element.target || element.items) continue
57
+ }
58
+
59
+ def[`@cds.search.${commonTextValue}`] = true
60
+ isSearchAddedProgrammatically = true
37
61
  }
62
+
63
+ // add all string elements to @cds.search, if not annotated with @cds.search = false
64
+ if (isSearchAddedProgrammatically)
65
+ for (const el in def.elements) {
66
+ const search_el = def.elements[el]
67
+ if (search_el._type === 'cds.String' && def[`@cds.search.${search_el.name}`] !== false) {
68
+ def[`@cds.search.${search_el.name}`] = true
69
+ }
70
+ }
38
71
  }
39
72
 
40
73
  Object.defineProperty(csn, '_4nodejs', { value: dsn })
@@ -8,6 +8,26 @@ module.exports = function cds_compile_for_odata (csn,_o) {
8
8
  let o = compile._options.for.odata(_o) //> required to inspect .sql_mapping below
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
+
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
+ }
30
+
11
31
  Object.defineProperty (csn, '_4odata', {value:dsn})
12
32
  Object.defineProperty (dsn, '_4odata', {value:dsn})
13
33
  TRACE?.timeEnd('cds.compile 4odata'.padEnd(22))
@@ -1,4 +1,10 @@
1
- const cds = require('..')
1
+ module.exports = exports = load
2
+ exports.parsed = get
3
+
4
+ const cds = require ('../index.js')
5
+ const outbox_cds = cds.env.requires.queue?.model
6
+ const [ outbox ] = cds.resolve (outbox_cds) || []
7
+
2
8
  const TRACE = cds.debug('trace')
3
9
  if (TRACE) {
4
10
  TRACE?.time('require cds.compiler'.padEnd(22))
@@ -6,31 +12,19 @@ if (TRACE) {
6
12
  TRACE?.timeEnd('require cds.compiler'.padEnd(22))
7
13
  }
8
14
 
9
- module.exports = exports = function load (files, options) {
10
- let any = cds.resolve(files,options)
11
-
12
- // REVISIT: we need to find a better way to handle this -> doing that in cds.load is by far too central
13
- // REVISIT: bandaid for grow as you go scenario with task queues enabled by default
14
- let locations
15
- if (cds.watched) {
16
- const _is_outbox = p => cds.utils.path.posix.normalize(p).match(/((\/cds\/srv\/outbox)|(\\cds\\srv\\outbox))(\.cds)?$/)
17
- const _outbox_only = any?.length === 1 && _is_outbox(any[0]) && (!Array.isArray(files) || !files.some(_is_outbox))
18
- if (_outbox_only) {
19
- any = undefined
20
- locations = cds.resolve(files, false).filter(f => !_is_outbox(f))
21
- }
22
- }
23
15
 
24
- if (!any) return Promise.reject (new cds.error ({
25
- message: `Couldn't find a CDS model for '${files}' in ${cds.root}`,
16
+ function load (models, options) {
17
+ const files = cds.resolve (models, options)
18
+ if (!files || cds.watched && files == outbox) return Promise.reject (new cds.error ({
19
+ message: `Couldn't find a CDS model for '${models}' in ${cds.root}`,
20
+ files: cds.resolve(models,false)?.filter (f => f !== outbox_cds),
26
21
  code: 'MODEL_NOT_FOUND',
27
- files, locations
28
22
  }))
29
- return this.get (any,options,'inferred')
23
+ return get (files, options, 'inferred')
30
24
  }
31
25
 
32
26
 
33
- exports.parsed = function cds_get (files, options, _flavor) {
27
+ function get (files, options, _flavor) {
34
28
  const o = typeof options === 'string' ? { flavor:options } : options || {}
35
29
  if (!files) files = ['*']; else if (!Array.isArray(files)) files = [files]
36
30
  if (o.files || o.flavor === 'files') return cds.resolve(files,o)
@@ -44,19 +38,22 @@ exports.parsed = function cds_get (files, options, _flavor) {
44
38
  o.flavor || _flavor || 'parsed'
45
39
  )
46
40
  return csn.then?.(_finalize) || _finalize(csn)
41
+
47
42
  function _finalize (csn) {
43
+ if (outbox) csn.$sources = csn.$sources.filter (f => f !== outbox)
48
44
  if (!o.silent) cds.emit ('loaded', csn)
49
45
  if (!o.silent) TRACE?.timeEnd('cds.compile *.cds'.padEnd(22))
50
46
  return csn
51
47
  }
52
- }
53
48
 
54
- const _sources4 = async (files) => {
55
- const {path:{relative},fs:{promises:{readFile}}} = cds.utils, cwd = cds.root
56
- const sources = await Promise.all (files.map (f => readFile(f,'utf-8')))
57
- return files.reduce ((all,f,i) => { all[relative(cwd,f)] = sources[i]; return all },{})
49
+ async function _sources4 (files) {
50
+ const {relative} = cds.utils.path, {readFile} = cds.utils.fs.promises
51
+ const all = await Promise.all (files.map (f => readFile(f,'utf-8')))
52
+ return files.reduce ((d,f,i) => (d[relative(cds.root,f)] = all[i], d),{})
53
+ }
58
54
  }
59
55
 
56
+
60
57
  exports.properties = (...args) => (exports.properties = require('./etc/properties').read) (...args)
61
58
  exports.yaml = (file) => (exports.yaml = require('./etc/yaml').read) (file)
62
59
  exports.csv = (file) => (exports.csv = require('./etc/csv').read) (file)