@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.
- package/CHANGELOG.md +48 -0
- package/bin/serve.js +38 -26
- package/lib/compile/for/flows.js +92 -19
- package/lib/compile/for/lean_drafts.js +0 -47
- package/lib/compile/for/nodejs.js +47 -14
- package/lib/compile/for/odata.js +20 -0
- package/lib/compile/load.js +22 -25
- package/lib/compile/minify.js +29 -11
- package/lib/compile/parse.js +1 -1
- package/lib/compile/resolve.js +133 -76
- package/lib/compile/to/csn.js +2 -2
- package/lib/dbs/cds-deploy.js +48 -43
- package/lib/env/cds-env.js +6 -0
- package/lib/env/cds-requires.js +9 -3
- package/lib/index.js +3 -1
- package/lib/plugins.js +1 -1
- package/lib/req/request.js +2 -2
- package/lib/srv/bindings.js +10 -5
- package/lib/srv/middlewares/auth/index.js +7 -5
- package/lib/srv/protocols/hcql.js +8 -3
- package/lib/srv/protocols/index.js +1 -0
- package/lib/utils/cds-utils.js +28 -1
- package/lib/utils/colors.js +1 -1
- package/libx/_runtime/common/generic/assert.js +1 -7
- package/libx/_runtime/common/generic/flows.js +14 -4
- package/libx/_runtime/common/utils/resolveView.js +4 -0
- package/libx/_runtime/fiori/lean-draft.js +8 -3
- package/libx/_runtime/messaging/common-utils/authorizedRequest.js +4 -0
- package/libx/_runtime/messaging/enterprise-messaging-utils/EMManagement.js +12 -12
- package/libx/_runtime/messaging/enterprise-messaging.js +1 -1
- package/libx/_runtime/messaging/http-utils/token.js +18 -3
- package/libx/_runtime/messaging/message-queuing.js +7 -7
- package/libx/_runtime/remote/Service.js +3 -1
- package/libx/_runtime/remote/utils/client.js +1 -0
- package/libx/_runtime/remote/utils/query.js +0 -1
- package/libx/odata/middleware/batch.js +128 -112
- package/libx/odata/middleware/error.js +7 -3
- package/libx/odata/parse/afterburner.js +10 -11
- package/libx/odata/parse/grammar.peggy +4 -2
- package/libx/odata/parse/parser.js +1 -1
- package/libx/odata/utils/odataBind.js +8 -2
- package/libx/queue/index.js +1 -0
- package/package.json +4 -7
- package/srv/outbox.cds +1 -1
- package/srv/ucl-service.cds +3 -5
- package/bin/colors.js +0 -2
- 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)
|
|
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
|
-
|
|
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
|
|
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${
|
|
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
|
-
|
|
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,
|
|
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 (
|
|
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
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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))
|
package/lib/compile/for/flows.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
133
|
-
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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 })
|
package/lib/compile/for/odata.js
CHANGED
|
@@ -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))
|
package/lib/compile/load.js
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
25
|
-
|
|
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
|
|
23
|
+
return get (files, options, 'inferred')
|
|
30
24
|
}
|
|
31
25
|
|
|
32
26
|
|
|
33
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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)
|