@sap/cds 9.6.4 → 9.7.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 (49) hide show
  1. package/CHANGELOG.md +57 -0
  2. package/bin/serve.js +38 -26
  3. package/lib/compile/for/flows.js +100 -20
  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/http.js +1 -1
  22. package/lib/srv/protocols/index.js +1 -0
  23. package/lib/utils/cds-utils.js +28 -1
  24. package/lib/utils/colors.js +1 -1
  25. package/libx/_runtime/common/generic/assert.js +1 -7
  26. package/libx/_runtime/common/generic/flows.js +14 -4
  27. package/libx/_runtime/common/utils/resolveView.js +4 -0
  28. package/libx/_runtime/fiori/lean-draft.js +8 -3
  29. package/libx/_runtime/messaging/common-utils/authorizedRequest.js +4 -0
  30. package/libx/_runtime/messaging/enterprise-messaging-utils/EMManagement.js +12 -12
  31. package/libx/_runtime/messaging/enterprise-messaging.js +1 -1
  32. package/libx/_runtime/messaging/http-utils/token.js +18 -3
  33. package/libx/_runtime/messaging/message-queuing.js +7 -7
  34. package/libx/_runtime/remote/Service.js +14 -3
  35. package/libx/_runtime/remote/utils/client.js +1 -0
  36. package/libx/_runtime/remote/utils/query.js +0 -1
  37. package/libx/odata/middleware/batch.js +128 -112
  38. package/libx/odata/middleware/delete.js +2 -1
  39. package/libx/odata/middleware/error.js +7 -3
  40. package/libx/odata/parse/afterburner.js +10 -11
  41. package/libx/odata/parse/grammar.peggy +4 -2
  42. package/libx/odata/parse/parser.js +1 -1
  43. package/libx/odata/utils/odataBind.js +8 -2
  44. package/libx/queue/index.js +1 -0
  45. package/package.json +4 -7
  46. package/srv/outbox.cds +1 -1
  47. package/srv/ucl-service.cds +3 -5
  48. package/bin/colors.js +0 -2
  49. package/libx/_runtime/.eslintrc +0 -14
package/CHANGELOG.md CHANGED
@@ -4,6 +4,57 @@
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.1 - 2026-02-06
8
+
9
+ ### Fixed
10
+
11
+ - `DELETE` requests nulling a `@mandatory` property
12
+ - Correctly call remote collection bound action for `odata-v4` services
13
+ - Flow annotation validation at compile time strictly follows the documentation: only enum status values are allowed
14
+ + Status value validation can be disabled via `cds.features.skip_flows_validation=true`
15
+
16
+ ## Version 9.7.0 - 2026-02-02
17
+
18
+ ### Added
19
+
20
+ - Support for express 5 (in addition to express 4)
21
+ - New config option `cds.requires.db.data` to configure source folders for initial data and test data CSV files
22
+ - Enterprise Messaging now caches access tokens to support high-throughput message processing from Event Mesh
23
+ - Automatically add `@Common.DraftRoot.NewAction` for each draft-enabled entity during `compile.for.odata` via `cds.fiori.direct_crud=true`
24
+ - Support for `null` value in `@odata.bind`
25
+ - Validation of flow annotations at compile step
26
+
27
+ ### Changed
28
+
29
+ - Colors are enabled by default in GitHub Actions workflows
30
+ - `queue`: Manually update `lastAttemptTimestamp` of outbox messages (instead of relying on `@cds.on.update: $now`)
31
+ - `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.
32
+ - `hcql` response format: `{ data: [], errors: [] }`
33
+
34
+ ### Fixed
35
+
36
+ - Correctly respond with status `404` when `@cds.api.ignore` annotated action is requested
37
+ - Ensure plugin debug emitted with `DEBUG=all`
38
+ - Prevent app crash when `JSON.parse` of operation parameters fails
39
+ - Generate correct UI annotations for Status Transition Flows when building and compiling
40
+ - Remote services: Prefer `cds.context.user?.authInfo?.token?.jwt` over JWT in HTTP header of incoming request
41
+ - References to child elements in `@Common.Text` annotations will now be checked. The reference will not be included in `@cds.search`, in case ...
42
+ * ... the reference can not be found in the annotated entity's associations
43
+ * ... the referenced entity is annotated with `@cds.persistence.skip`
44
+ * ... the referenced field does not exist in the referenced entity
45
+ * References to children of children will be ignored.
46
+ - OData parser: Ignore superfluous brackets
47
+ - Prevent app crash in case of `req.reject()` during draft activate triggered via OData batch
48
+ - `cds minify` no longer removes services if their actions are kept
49
+ - Better error when subquery can't be resolved for the current service
50
+ - Flows: Record transition to default value on `INSERT`/ `UPSERT`
51
+ - Error response properties of OData batch subrequests are now formatted identically to properties in single OData error responses
52
+ - Prevent `@Common.numericSeverity` from appearing in persistent draft messages (in addition to the correct property `numericSeverity`)
53
+
54
+ ### Removed
55
+
56
+ - `@cds.on.update: $now` from `cds.outbox.Messages.lastAttemptTimestamp`
57
+
7
58
  ## Version 9.6.4 - 2026-01-20
8
59
 
9
60
  ### Fixed
@@ -499,6 +550,12 @@
499
550
  - Deprecated stripping of unnecessary topic prefix `topic:` in messaging
500
551
  - Deprecated messaging `Outbox` class. Please use config or `cds.outboxed(srv)` to outbox your service.
501
552
 
553
+ ## Version 8.9.8 - 2025-12-17
554
+
555
+ ### Fixed
556
+
557
+ - `enterprise-messaging-shared`: preserve error listener during reconnect
558
+
502
559
  ## Version 8.9.7 - 2025-11-07
503
560
 
504
561
  ### 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
  }
@@ -148,23 +147,78 @@ function enhanceCSNwithFlowAnnotations4FE(csn) {
148
147
  }
149
148
 
150
149
  module.exports = function cds_compile_for_flows(csn) {
151
- const { history_for_flows } = cds.env.features
150
+ const { history_for_flows, skip_flows_validation } = cds.env.features
152
151
 
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 => {
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
+ }
169
+ }
170
+
171
+ const _validate_status_types = skip_flows_validation
172
+ ? () => {}
173
+ : (name, def, status, csn, errors) => {
174
+ // enum
175
+ let enumVals
176
+ if (status.type === 'cds.Association') {
177
+ const target = csn.definitions[status.target]
178
+ if (!status.keys || status.keys.length !== 1) {
179
+ errors.push(`Status element in entity ${name} must have exactly one key when used as association`)
180
+ return
181
+ }
182
+ const code = target.elements[status.keys[0].ref[0]]
183
+ enumVals = code.enum || csn.definitions[code.type]?.enum
184
+ } else {
185
+ enumVals = status.enum ?? csn.definitions[status.type]?.enum
186
+ }
187
+ if (!enumVals) {
188
+ errors.push(`Status element in entity ${name} must be an enum or target an entity with an enum named "code"`)
189
+ return
190
+ }
191
+ // actions
164
192
  for (const each in def.actions) {
165
193
  const action = def.actions[each]
166
- if (action && action[TO]?.['='] === FLOW_PREVIOUS) {
167
- return true
194
+ const from = action[FROM]
195
+ if (from !== undefined) {
196
+ if (Array.isArray(from)) {
197
+ for (let i = 0; i < from.length; i++) {
198
+ if (from[i] !== null) {
199
+ let val = from[i]['#']
200
+ if (!(typeof val === 'string' && Object.entries(enumVals).some(([key]) => key === val))) {
201
+ errors.push(`Invalid ${FROM} value at position ${i} in action ${each}`)
202
+ }
203
+ }
204
+ }
205
+ } else if (from !== null) {
206
+ let val = from['#']
207
+ if (!(typeof val === 'string' && Object.entries(enumVals).some(([key]) => key === val))) {
208
+ errors.push(`Invalid ${FROM} value in action ${each}`)
209
+ }
210
+ }
211
+ }
212
+ const to = action[TO]
213
+ if (to !== undefined) {
214
+ if (Array.isArray(to)) {
215
+ errors.push(`${TO} must not be an array in action ${each}`)
216
+ } else if (to !== null && to['='] !== FLOW_PREVIOUS) {
217
+ let val = to['#']
218
+ if (!(typeof val === 'string' && Object.entries(enumVals).some(([key]) => key === val))) {
219
+ errors.push(`Invalid ${TO} value in action ${each}`)
220
+ }
221
+ }
168
222
  }
169
223
  }
170
224
  }
@@ -186,11 +240,14 @@ module.exports = function cds_compile_for_flows(csn) {
186
240
  }
187
241
  }
188
242
 
243
+ const errors = []
189
244
  const to_be_extended = new Set()
190
245
 
191
246
  for (const name in csn.definitions) {
192
247
  const def = csn.definitions[name]
193
248
 
249
+ if (!def.kind || def.kind !== 'entity' || !def.elements || !def.actions) continue
250
+
194
251
  /*
195
252
  * 2. propagate @flow.status to respective element and make it @readonly
196
253
  */
@@ -202,8 +259,6 @@ module.exports = function cds_compile_for_flows(csn) {
202
259
  }
203
260
  }
204
261
 
205
- if (!def.kind || def.kind !== 'entity' || !def.actions) continue
206
-
207
262
  /*
208
263
  * 3. normalize @from and @to annotations
209
264
  */
@@ -214,7 +269,19 @@ module.exports = function cds_compile_for_flows(csn) {
214
269
  }
215
270
 
216
271
  /*
217
- * 4. automatically apply aspect FlowHistory if needed and not present yet
272
+ * 4. validate annotations
273
+ */
274
+ let status = Object.values(def.elements).filter(e => e['@flow.status'])
275
+ if (status.length === 0) continue
276
+ if (status.length > 1) {
277
+ errors.push(`Entity ${name} has multiple status elements. Only one @flow.status element is allowed per entity`)
278
+ continue
279
+ }
280
+ status = status[0]
281
+ _validate_status_types(name, def, status, csn, errors)
282
+
283
+ /*
284
+ * 5. automatically apply aspect FlowHistory if needed and not present yet
218
285
  */
219
286
  if (!_requires_history(def)) continue
220
287
 
@@ -227,6 +294,17 @@ module.exports = function cds_compile_for_flows(csn) {
227
294
  to_be_extended.add(base_name)
228
295
  }
229
296
 
297
+ /*
298
+ * 6. throw validation errors, if any
299
+ */
300
+ if (errors.length) {
301
+ if (errors.length === 1) cds.error(errors[0])
302
+ else cds.error('MULTIPLE_ERRORS', { details: errors.map(message => ({ message })) })
303
+ }
304
+
305
+ /*
306
+ * 7. apply the extensions
307
+ */
230
308
  if (to_be_extended.size) {
231
309
  // REVISIT: ensure sap.common.FlowHistory is there
232
310
  csn.definitions['sap.common.FlowHistory'] ??= JSON.parse(FlowHistory)
@@ -255,6 +333,9 @@ module.exports = function cds_compile_for_flows(csn) {
255
333
  csn = dsn
256
334
  }
257
335
 
336
+ /*
337
+ * 8. exclude transitions_ from draft
338
+ */
258
339
  // REVISIT: annotate all X.transitions_ with @odata.draft.enabled: false
259
340
  for (const name in csn.definitions)
260
341
  if (name.endsWith('.transitions_')) csn.definitions[name]['@odata.draft.enabled'] = false
@@ -313,4 +394,3 @@ const FlowHistory = `{
313
394
  }`
314
395
 
315
396
  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)