@sap/cds 8.8.3 → 8.9.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 (68) hide show
  1. package/CHANGELOG.md +40 -4
  2. package/_i18n/i18n_en_US_saptrc.properties +3 -0
  3. package/bin/colors.js +2 -0
  4. package/bin/test.js +103 -75
  5. package/eslint.config.mjs +16 -4
  6. package/lib/compile/for/lean_drafts.js +4 -0
  7. package/lib/compile/parse.js +26 -6
  8. package/lib/env/cds-env.js +3 -1
  9. package/lib/env/cds-requires.js +0 -3
  10. package/lib/env/schemas/cds-rc.js +11 -0
  11. package/lib/log/format/aspects/cls.js +2 -1
  12. package/lib/log/format/json.js +1 -1
  13. package/lib/plugins.js +2 -3
  14. package/lib/ql/SELECT.js +2 -1
  15. package/lib/ql/cds-ql.js +2 -0
  16. package/lib/ql/cds.ql-predicates.js +6 -4
  17. package/lib/ql/resolve.js +46 -0
  18. package/lib/req/validate.js +1 -0
  19. package/lib/srv/bindings.js +64 -43
  20. package/lib/srv/cds-connect.js +1 -1
  21. package/lib/srv/cds-serve.js +2 -2
  22. package/lib/srv/middlewares/auth/ias-auth.js +2 -0
  23. package/lib/srv/protocols/http.js +2 -2
  24. package/lib/srv/protocols/index.js +1 -1
  25. package/lib/srv/protocols/odata-v4.js +0 -1
  26. package/lib/srv/srv-tx.js +1 -1
  27. package/lib/test/cds-test.js +3 -4
  28. package/lib/utils/cds-utils.js +19 -19
  29. package/lib/utils/colors.js +46 -45
  30. package/lib/utils/csv-reader.js +5 -5
  31. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +1 -2
  32. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/index.js +1 -1
  33. package/libx/_runtime/common/Service.js +4 -2
  34. package/libx/_runtime/common/composition/data.js +1 -2
  35. package/libx/_runtime/common/composition/tree.js +6 -4
  36. package/libx/_runtime/common/generic/sorting.js +6 -2
  37. package/libx/_runtime/common/utils/cqn2cqn4sql.js +6 -7
  38. package/libx/_runtime/common/utils/differ.js +1 -1
  39. package/libx/_runtime/common/utils/draft.js +1 -1
  40. package/libx/_runtime/common/utils/foreignKeyPropagations.js +6 -2
  41. package/libx/_runtime/common/utils/keys.js +13 -84
  42. package/libx/_runtime/common/utils/propagateForeignKeys.js +4 -3
  43. package/libx/_runtime/common/utils/resolveView.js +96 -102
  44. package/libx/_runtime/common/utils/rewriteAsterisks.js +1 -1
  45. package/libx/_runtime/common/utils/stream.js +2 -3
  46. package/libx/_runtime/db/utils/columns.js +1 -1
  47. package/libx/_runtime/fiori/lean-draft.js +11 -7
  48. package/libx/_runtime/messaging/kafka.js +3 -4
  49. package/libx/_runtime/remote/Service.js +13 -5
  50. package/libx/_runtime/remote/utils/client.js +1 -0
  51. package/libx/_runtime/ucl/Service.js +135 -126
  52. package/libx/common/utils/path.js +34 -22
  53. package/libx/odata/middleware/create.js +2 -0
  54. package/libx/odata/middleware/operation.js +8 -2
  55. package/libx/odata/middleware/parse.js +1 -1
  56. package/libx/odata/middleware/stream.js +1 -2
  57. package/libx/odata/middleware/update.js +2 -0
  58. package/libx/odata/parse/afterburner.js +17 -9
  59. package/libx/odata/parse/cqn2odata.js +3 -1
  60. package/libx/odata/parse/grammar.peggy +21 -19
  61. package/libx/odata/parse/parser.js +1 -1
  62. package/libx/odata/utils/metadata.js +8 -2
  63. package/libx/odata/utils/odataBind.js +36 -0
  64. package/libx/outbox/index.js +1 -0
  65. package/libx/rest/middleware/operation.js +9 -8
  66. package/libx/rest/middleware/parse.js +1 -0
  67. package/package.json +3 -3
  68. package/lib/i18n/resources.js +0 -150
package/CHANGELOG.md CHANGED
@@ -4,6 +4,42 @@
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 8.9.0 - 2025-03-31
8
+
9
+ ### Added
10
+
11
+ - Support for parallel multi-instance processing of outbox entries
12
+ - Remote services: ensure request correlation by guaranteeing outgoing header `x-correlation-id`
13
+ - Support for `@odata.bind` to reference foreign keys
14
+ - Support for plugins in ESM format
15
+ - Dependency to `@eslint/js` so that `eslint` works w/o the application having to install it.
16
+ - IAS: In the `client_credentials` flow, the array of `ias_apis` (if present) is added to the technical user's roles
17
+ - Opt-in feature `cds.features.consistent_params` for `req.params` always being an array of objects
18
+ + That is, no more plain values for single-keyed entities with key `ID`
19
+ + Will become the default in `@sap/cds^9`
20
+
21
+ ### Changed
22
+
23
+ - Invalid draft requests now have status code 400
24
+ - Allow ESM loading of handler files (`.js`, `.ts`) in all situations, incl. test runs with Jest's `--experimental-vm-modules` option.
25
+ - Application and remote services now throw the error `Target <yourTarget> cannot be resolved for service <yourService>` when the query cannot be resolved to the service entity. Setting the feature flag `cds.env.features.restrict_service_scope` to false disables this.
26
+ - Accept 2xx status codes set in custom operation handlers
27
+ - Implicit orderby elements are marked as such and are no longer considered for requests to remote services
28
+
29
+ ### Fixed
30
+
31
+ - Lean draft: Proper navigation to the service entity of draft-administrative data
32
+ - Unprocessed foreign keys from expressions of semi join conditions in `UPDATE.data`
33
+ - Kafka: Each topic will have a dedicated consumer-group id (configurable with `consumerGroup`)
34
+ - Foreign-key calculation based on navigation path
35
+ - `cds.env` shortcuts like `cds.requires.db === 'hana'` are normalized to `cds.requires.db.kind === 'hana'` when combined from multiple sources
36
+ - Error handling for invalid access of an entity that does not have a key, by key, through REST
37
+ - `cds.validate` crashed with unknown target
38
+ - `cds.parse.expr` parsed SAP HANA native functions like `current_utctimestamp` erroneously as `ref`
39
+ - `null` values in logger if `custom_fields` are configured
40
+ - User-provided instances of SAP Cloud Logging should have either tag `cloud-logging` or `Cloud Logging`
41
+ - The `@odata.context` for entities and views with parameters should refer to the EntityType with `/Set` at the end e.g. `../$metadata#ViewWithParamType(1)/Set`
42
+
7
43
  ## Version 8.8.3 - 2025-03-20
8
44
 
9
45
  ### Fixed
@@ -31,7 +67,7 @@
31
67
  - Add missing 'and' between conditions in object notation of QL
32
68
  - Multiline payloads in `$batch` sub requests
33
69
  - Instance-based authorization for modeling like `$user.<property> is null`
34
- - Respect `cds.odata.contextAbsoluteUrl` in new OData adapter
70
+ - Respect `cds.odata.contextAbsoluteUrl` in new OData adapter
35
71
  - `cds.odata.context_with_columns` also applies to singletons
36
72
 
37
73
  ## Version 8.8.0 - 2025-03-03
@@ -46,9 +82,9 @@
46
82
  - Support implicit function parameters calls with @prefix
47
83
  - `cds.test` now uses package `@cap-js/cds-test` if installed, otherwise prints a hint to install it. With cds 9, this package will be required.
48
84
  - Operation response streaming
49
- + OData: Operations returning `cds.LargeBinary` annotated with `@Core.MediaType` may send stream responses.
50
- + REST: Operations may send stream responses.
51
- + Annotations `@Core.MediaType`, `@Core.ContentDisposition.Filename` and `@Core.ContentDisposition.Type` on operation return types will be considered.
85
+ + OData: Operations returning `cds.LargeBinary` annotated with `@Core.MediaType` may send stream responses.
86
+ + REST: Operations may send stream responses.
87
+ + Annotations `@Core.MediaType`, `@Core.ContentDisposition.Filename` and `@Core.ContentDisposition.Type` on operation return types will be considered.
52
88
 
53
89
  ### Changed
54
90
 
@@ -70,6 +70,9 @@ LanguageCode=NhI8Yd8pNFS7omWQsk5aJw_Language Code
70
70
  #XTIT: Language Code Description
71
71
  LanguageCode.Description=ajmXdjo3lK0nfaMLCpvPMw_Language code as specified by ISO 639-1
72
72
 
73
+ #XTIT Time zone code
74
+ TimeZoneCode=Y0KTpmsmzoysYLT6jDQEkQ_Time Zone Code
75
+
73
76
  #XTIT: User Identifier
74
77
  UserID=cjI0FCsEZ2aD8ERc6G/xZw_User ID
75
78
 
package/bin/colors.js ADDED
@@ -0,0 +1,2 @@
1
+ // to have same structure as in @sap/cds-test
2
+ module.exports = require('../lib/utils/colors')
package/bin/test.js CHANGED
@@ -1,97 +1,125 @@
1
1
  #!/usr/bin/env node
2
-
3
- module.exports = Object.assign ( test, {
4
- options: [
5
- '--files',
6
- '--include',
7
- '--exclude',
8
- '--pattern',
9
- '--skip',
10
- ],
11
- flags: [
12
- '--verbose',
13
- '--unmute',
14
- '--silent',
15
- '--quiet',
16
- '--list',
17
- '--recent',
18
- '--passed',
19
- '--failed',
20
- ],
21
- shortcuts: [ '-f', '-i', '-x', '-p', null, '-v', '-u', '-s', '-q', '-l' ],
22
- help: `
23
- `})
24
-
25
-
26
2
  /* eslint-disable no-console */
27
- const { DIMMED, RESET } = require('../lib/utils/colors')
28
3
 
29
- async function test (argv,o) {
30
- const _recent = require('os').userInfo().homedir + '/.cds-test-recent.json'
31
- const recent = require('fs').existsSync(_recent) ? require(_recent) : {}
32
- if (!o.recent) {
33
- o.files = await find (argv, o, recent)
34
- } else {
35
- Object.assign (o,recent.options)
36
- console.log ('\nchest',...o.argv)
37
- }
38
- if (o.list) return list (o.files)
39
- if (o.skip) process.env._chest_skip = o.skip
40
- if (o.files.length > 1) console.log(DIMMED,`\nRunning ${o.files.length} test suite${o.files.length > 1 ? 's' : ''}...`, RESET)
41
- const test = require('node:test').run({ ...o, concurrency:true })
42
- require('../lib/test/reporter')(test, test.options = o)
4
+ const options = {
5
+ 'timeout': { type:'string', short:'t' },
6
+ 'verbose': { type:'boolean', short:'v' },
7
+ 'unmute': { type:'boolean', short:'u' },
8
+ 'silent': { type:'boolean', short:'s' },
9
+ 'quiet': { type:'boolean', short:'q' },
10
+ 'list': { type:'boolean', short:'l' },
11
+ 'recent': { type:'boolean' },
12
+ 'passed': { type:'boolean' },
13
+ 'failed': { type:'boolean' },
14
+ 'match': { type:'string', default: '(.test.js|.spec.js)$' },
15
+ 'skip': { type:'string', default: '^(gen,*.tmp)$' },
16
+ 'only': { type:'string'},
17
+ 'help': { type:'boolean', short:'h' },
18
+ 'workers': { type:'string', short:'w' },
43
19
  }
44
20
 
21
+ const USAGE = `
22
+ Usage:
45
23
 
46
- async function find (argv,o,recent) {
24
+ cds test [ options ] [ patterns ]
47
25
 
48
- // pre-process options
49
- const { pattern = '.spec.js,.test.js', include, exclude = '_out' } = o,
50
- patterns = pattern?.split(',') || [],
51
- includes = include?.split(',') || [],
52
- excludes = exclude?.split(',') || []
26
+ Options:
53
27
 
54
- // return recent results if --passed or --failed is used
55
- if (o.passed) return recent.passed
56
- if (o.failed) return recent.failed
28
+ -l, --list List found test files
29
+ -q, --quiet No output at all
30
+ -s, --silent No output
31
+ -t, --timeout in milliseconds
32
+ -u, --unmute Unmute output
33
+ -v, --verbose Increase verbosity
34
+ -w, --workers Specify number of workers
35
+ -?, --help Displays this usage info
57
36
 
58
- // check if argv is a list of files or directories
59
- const {lstat} = require('fs').promises, files=[], roots=[]
60
- await Promise.all (argv.map (async x => {
61
- let ls = await lstat(x).catch(()=>{})
62
- if (!ls) return includes.push(x)
63
- if (ls.isDirectory()) return roots.push(x.replace(/\/$/,''))
64
- if (ls.isFile()) return files.push(x)
65
- else return includes.push(x)
66
- }))
67
- if (files.length && !roots.length && !includes.length) return files //> all files resolved
37
+ --match Pick matching files, default: ${options.match.default}
38
+ --skip Skip matching files, default: ${options.skip.default}
39
+ --only Run only the matching tests
40
+ --failed Repeat recently failed test suite(s)
41
+ --passed Repeat recently passed test suite(s)
42
+ --recent Repeat recently run test suite(s)
43
+ `
68
44
 
69
- // Prepare UNIX find command to fetch matching files
70
- let find = `find -L ${roots.join(' ')||'.'} -type f`
71
- if (patterns.length) find += ` \\( ${ patterns.map (p=>`-name "${p.replace(/^([^*])/,'*$1')}"`).join(' -o ') } \\)`
72
- if (includes.length) find += ` \\( ${ includes.map (p=>`-regex .*${p.replace(/\./g,'\\\\.')}.*`).join(' -o ') } \\)`
73
- if (excludes.length) find += ` \\( ${ excludes.map (x=>`! -regex .*${x.replace(/\./g,'\\\\.')}.*`).join(' ') } \\)`
45
+ const { DIMMED, YELLOW, GRAY, RESET } = require('./colors')
46
+ const regex4 = s => !s ? null : RegExp (s.replace(/[,.*]/g, s => ({ ',': '|', '.': '\\.', '*': '.*' })[s]))
47
+ const recent = () => {try { return require(home+'/.cds-test-recent.json') } catch {/* egal */}}
48
+ const os = require('os'), home = os.userInfo().homedir
74
49
 
75
- // Execute find command and return list of files
76
- const exec = require('node:util') .promisify (require('node:child_process').exec)
77
- const { stdout } = await exec (find)
78
- return files.concat (stdout.split('\n').slice(0, -1).sort())
50
+ async function test (argv,o) {
51
+ if (o.help || argv == '?') return console.log (USAGE)
52
+ if (o.recent) o = { ...o, ...recent().options }
53
+ if (o.passed) o.files = recent().passed
54
+ if (o.failed) o.files = recent().failed
55
+ if (!o.files) o.files = await fetch (argv,o)
56
+ if (o.list) return list (o.files)
57
+ if (o.skip) process.env._chest_skip = o.skip
58
+ if (o.files.length > 1) console.log (DIMMED,`\nRunning ${o.files.length} test suites...`, RESET)
59
+ const test = require('node:test').run({ ...o,
60
+ execArgv: [ '--require', require.resolve('../lib/fixtures/node-test.js') ],
61
+ timeout: +o.timeout || undefined,
62
+ concurrency: +o.workers || true,
63
+ testNamePatterns: regex4 (o.only), only: false,
64
+ })
65
+ require('./reporter')(test, test.options = o)
79
66
  }
80
67
 
68
+ async function fetch (argv,o) {
69
+ const patterns = regex4 (argv.join('|')) || { test: ()=> true }
70
+ const tests = regex4 (o.match || options.match.default) || { test: ()=> true }
71
+ const skip = regex4 (o.skip || options.skip.default) || { test: ()=> false }
72
+ const ignore = /^(\..*|node_modules|_out)$/
73
+ const files = []
74
+ const fs = require('node:fs'), path = require('node:path')
75
+ const _read = fs.promises.readdir
76
+ const _isdir = x => fs.statSync(x).isDirectory()
77
+ await async function _visit (dir) {
78
+ const entries = await _read (dir)
79
+ return Promise.all (entries.map (each => {
80
+ if (ignore.test(each) || skip.test(each = path.join (dir,each))) return
81
+ if (tests.test(each)) return patterns.test(each) && files.push(each)
82
+ if (_isdir(each)) return _visit (each)
83
+ }))
84
+ } (process.cwd())
85
+ if (!files.length) throw YELLOW+`\n No matching test files found. \n`+RESET
86
+ return files
87
+ }
81
88
 
82
89
  function list (files) {
83
90
  const { relative } = require('node:path'), cwd = process.cwd()
91
+ const time = (performance.now() / 1000).toFixed(3)
84
92
  console.log()
85
- console.log(`Found ${files.length} test file${files.length > 1 ? 's' : ''}:`, DIMMED, '\n')
93
+ console.log(`Found these matching test files:`, DIMMED, '\n')
86
94
  for (let f of files) console.log(' ', relative(cwd, f))
87
- console.log(RESET)
95
+ console.log(RESET+'\n', files.length, 'total')
96
+ console.log(GRAY, time+'s', RESET, '\n')
88
97
  }
89
98
 
90
99
 
91
- if (!module.parent) try {
92
- const [ argv, options ] = require('./args') (test, process.argv.slice(2))
93
- test (argv, options)
94
- } catch (err) {
95
- console.error(err) // eslint-disable-line no-console
96
- process.exitCode = 1
100
+ if (!module.parent) {
101
+ const { positionals, values } = require('node:util').parseArgs ({ options, allowPositionals: true })
102
+ test (positionals, values) .catch (e => console.error(e), process.exitCode = 1)
97
103
  }
104
+
105
+ else module.exports = Object.assign ( test, {
106
+ options: [
107
+ '--files',
108
+ '--match',
109
+ '--skip',
110
+ '--timeout',
111
+ '--workers',
112
+ ],
113
+ flags: [
114
+ '--verbose',
115
+ '--unmute',
116
+ '--silent',
117
+ '--quiet',
118
+ '--list',
119
+ '--recent',
120
+ '--passed',
121
+ '--failed',
122
+ ],
123
+ shortcuts: [ '-f', null, null, '-t', '-w', '-v', '-u', '-s', '-q', '-l' ],
124
+ help: USAGE
125
+ })
package/eslint.config.mjs CHANGED
@@ -1,3 +1,11 @@
1
+ import eslint_js from '@eslint/js'
2
+
3
+ /**
4
+ * Makes the eslint config object available for external use.
5
+ */
6
+ export const eslint = eslint_js.configs
7
+
8
+
1
9
  /**
2
10
  * Recommended ESLint config for @sap/cds projects.
3
11
  */
@@ -118,10 +126,14 @@ export const ignores = [
118
126
  * monorepo, other cap impl projects, as well as in cap-based projects.
119
127
  * Currently the same as internal, could differ in the future.
120
128
  */
121
- export const recommended = [ defaults, browser, tests, {ignores} ]
122
- await import('@eslint/js').then( // add recommended eslint rules if available
123
- ({default:eslint}) => recommended.unshift (eslint.configs.recommended)
124
- ).catch(()=>{})
129
+ export const recommended = [
130
+ eslint.recommended,
131
+ defaults,
132
+ browser,
133
+ tests,
134
+ { ignores },
135
+ { files: ['bin/*.js'], rules: { 'no-console': 'off' } },
136
+ ]
125
137
 
126
138
  /**
127
139
  * Default export is for internal use in @sap/cds and cap/dev projects.
@@ -142,6 +142,10 @@ module.exports = function cds_compile_for_lean_drafts(csn) {
142
142
  if (e._target['@odata.draft.enabled'] === false) continue // happens for texts if @fiori.draft.enabled is not set
143
143
  _redirect(newEl, addDraftEntity(e._target, model))
144
144
  }
145
+ if (e.name === 'DraftAdministrativeData') {
146
+ // redirect to DraftAdministrativeData service entity
147
+ _redirect(newEl, active._service.entities.DraftAdministrativeData)
148
+ }
145
149
  Object.defineProperty (newEl,'parent',{value:draft,enumerable:false, configurable: true, writable: true})
146
150
 
147
151
  for (const key in newEl) {
@@ -25,7 +25,7 @@ exports.cql = function cql (x,...etc) { try {
25
25
  exports.path = function path (x,...etc) {
26
26
  if (x.raw) return parse.ttl (path, x,...etc)
27
27
  if (cds.model?.definitions[x]) return {ref:[x]}
28
- if (/^([\w_.$]+)$/.test(x)) return {ref:[x]} // optimized parsing of simple paths of length 1
28
+ if (/^([\w_.$]+)$/.test(x)) return {ref:[x]} // optimized parsing of simple paths of length 1
29
29
  const [,head,tail] = /^([\w._]+)(?::(\w+))?$/.exec(x)||[]
30
30
  if (tail) return {ref:[head,...tail.split('.')]}
31
31
  if (head) return {ref:[head]}
@@ -36,11 +36,11 @@ exports.path = function path (x,...etc) {
36
36
  exports.expr = function expr (x,...etc) {
37
37
  if (x.raw) return parse.ttl (expr, x,...etc)
38
38
  if (typeof x !== 'string') throw cds.error.expected `${{x}} to be an expression string`
39
- if (x in globals) return globals[x] // optimized parsing of true, false, null
40
- if (/^([\d.]+)$/.test(x)) return {val:Number(x)} // optimized parsing of numeric vals
41
- if (/^([\w_$]+)$/.test(x)) return {ref:[x]} // optimized parsing of simple refs of length 1
42
- if (/^([\w_.$]+)$/.test(x)) return {ref:x.split('.')} // optimized parsing of simple refs of length > 1
43
- try { return cdsc.parse.expr(x,undefined,{ messages:[], ...etc[0] }) } catch(e) {
39
+ if (x in globals) return globals[x] // optimized parsing of true, false, null
40
+ if (/^([\d.]+)$/.test(x)) return {val:Number(x)} // optimized parsing of numeric vals
41
+ if (/^([\w_$]+)$/.test(x)) return native[x] || {ref:[x]} // optimized parsing of simple refs of length 1
42
+ if (/^([\w_.$]+)$/.test(x)) return {ref:x.split('.')} // optimized parsing of simple refs of length > 1
43
+ try { return cdsc.parse.expr (x,undefined,{ messages:[], ...etc[0] }) } catch(e) {
44
44
  // cds-compiler v5 does not put messages into `e.message` anymore; render them explicitly
45
45
  e.message = !e.messages ? e.message : e.toString();
46
46
  e.message = e.message.replace('<expr>.cds:1:',`In '${e.expr = x}' at `)
@@ -108,6 +108,26 @@ exports._select = (prefix, ttl, suffix) => {
108
108
  return cds.parse.cql (strings, ...values).SELECT
109
109
  }
110
110
 
111
+ const native = {
112
+ current_date : { func: 'current_date' },
113
+ current_time : { func: 'current_time' },
114
+ current_timestamp : { func: 'current_timestamp' },
115
+ current_user : { func: 'current_user' },
116
+ current_utcdate : { func: 'current_utcdate' },
117
+ current_utctime : { func: 'current_utctime' },
118
+ current_utctimestamp : { func: 'current_utctimestamp' },
119
+ session_user : { func: 'session_user' },
120
+ sysuuid : { func: 'sysuuid' },
121
+ CURRENT_DATE : { func: 'current_date' },
122
+ CURRENT_USER : { func: 'current_user' },
123
+ CURRENT_TIME : { func: 'current_time' },
124
+ CURRENT_TIMESTAMP : { func: 'current_timestamp' },
125
+ CURRENT_UTCDATE : { func: 'current_utcdate' },
126
+ CURRENT_UTCTIME : { func: 'current_utctime' },
127
+ CURRENT_UTCTIMESTAMP : { func: 'current_utctimestamp' },
128
+ SESSION_USER : { func: 'session_user' },
129
+ SYSUUID : { func: 'sysuuid' },
130
+ }
111
131
  const is_cqn = x => typeof x === 'object' && (
112
132
  'ref' in x ||
113
133
  'val' in x ||
@@ -470,7 +470,9 @@ function _merge (dst, src, _profiles) {
470
470
  else _merge (dst[p], v, _profiles)
471
471
  continue
472
472
  }
473
-
473
+ else if (typeof v === 'string' && typeof dst[p] === 'object' && dst[p].kind) {
474
+ dst[p].kind = v // requires.db = 'foo' -> requires.db.kind = 'foo'
475
+ }
474
476
  else if (v !== undefined) dst[p] = v
475
477
  }
476
478
  if (profiled.length > 0 && !_profiles.has('production')) {
@@ -1,4 +1,3 @@
1
- const { deprecated: cds_utils_deprecated } = require('../utils/cds-utils')
2
1
  const _runtime = '@sap/cds/libx/_runtime'
3
2
 
4
3
  exports = module.exports = {
@@ -81,12 +80,10 @@ const _authentication_strategies = {
81
80
 
82
81
  for (let each of Object.values(_authentication_strategies)) {
83
82
  Object.defineProperty (each, 'strategy', {get() {
84
- if (process.env.NODE_ENV !== 'production') cds_utils_deprecated({ old: 'auth.strategy', use: 'auth.kind' })
85
83
  return { jwt: 'JWT', mocked: 'mock' }[this.kind] || this.kind
86
84
  }})
87
85
  }
88
86
 
89
-
90
87
  const _services = {
91
88
 
92
89
  "app-service": {
@@ -820,6 +820,17 @@ module.exports = {
820
820
  description: `Specifies whether the extension linter includes existing extensions. Default will be 'true' with the next major release.`,
821
821
  default: false
822
822
  },
823
+ 'activate': {
824
+ type: 'object',
825
+ description: 'Activation settings',
826
+ additionalProperties: false,
827
+ properties: {
828
+ 'skip-db': {
829
+ type: 'boolean',
830
+ description: 'Skip database activation.'
831
+ }
832
+ }
833
+ },
823
834
  'element-prefix': {
824
835
  type: 'array',
825
836
  description: 'Field names must start with one of these strings.',
@@ -9,6 +9,7 @@ cls_aspect.cf = () => [...cds.env.log.cls_custom_fields]
9
9
  const VCAP_SERVICES = process.env.VCAP_SERVICES ? JSON.parse(process.env.VCAP_SERVICES) : {}
10
10
 
11
11
  module.exports =
12
- VCAP_SERVICES['cloud-logging'] || VCAP_SERVICES['user-provided']?.find(e => e.tags.includes('cloud-logging'))
12
+ VCAP_SERVICES['cloud-logging'] ||
13
+ VCAP_SERVICES['user-provided']?.find(e => e.tags.includes('cloud-logging') || e.tags.includes('Cloud Logging'))
13
14
  ? cls_aspect
14
15
  : () => {}
@@ -29,7 +29,7 @@ const _extract_custom_fields_and_categories = (args, toLog, custom_fields) => {
29
29
  let filter4removed = false
30
30
  for (let i = 0; i < args.length; i++) {
31
31
  const arg = args[i]
32
- if (typeof arg !== 'object') continue
32
+ if (typeof arg !== 'object' || arg === null) continue
33
33
  if ((custom_fields.size && _is_custom_fields(arg, custom_fields)) || _is_categories(arg)) {
34
34
  Object.assign(toLog, arg)
35
35
  args[i] = $remove
package/lib/plugins.js CHANGED
@@ -35,9 +35,8 @@ exports.activate = async function () {
35
35
  const { plugins } = cds.env, { local } = cds.utils
36
36
  const loadPlugin = async ([plugin, conf]) => {
37
37
  DEBUG?.(`loading plugin ${plugin}:`, { impl: local(conf.impl) })
38
- // TODO: support ESM plugins. But see cap/cds/pull/1838#issuecomment-1177200 !
39
- const p = require (conf.impl)
40
- if(p.activate) {
38
+ const p = cds.utils._import (conf.impl)
39
+ if (p.activate) {
41
40
  cds.log('plugins').warn(`WARNING: \n
42
41
  The @sap/cds plugin ${conf.impl} contains an 'activate' function, which is deprecated and won't be
43
42
  supported in future releases. Please rewrite the plugin to return a Promise within 'module.exports'.
package/lib/ql/SELECT.js CHANGED
@@ -149,10 +149,11 @@ class SELECT extends Whereable {
149
149
  return this
150
150
  }
151
151
 
152
- forUpdate ({ of, wait = cds.env.sql.lock_acquire_timeout || -1 } = {}) {
152
+ forUpdate ({ of, wait = cds.env.sql.lock_acquire_timeout || -1, ignoreLocked } = {}) {
153
153
  const sfu = this.SELECT.forUpdate = {}
154
154
  if (of) sfu.of = of.map (c => ({ref:c.split('.')}))
155
155
  if (wait >= 0) sfu.wait = wait
156
+ if (ignoreLocked) sfu.ignoreLocked = true
156
157
  return this
157
158
  }
158
159
 
package/lib/ql/cds-ql.js CHANGED
@@ -35,6 +35,8 @@ exports.DELETE = require('./DELETE')
35
35
  exports.CREATE = require('./CREATE')
36
36
  exports.DROP = require('./DROP')
37
37
 
38
+ exports.resolve = require('./resolve');
39
+
38
40
 
39
41
  exports.predicate = require('./cds.ql-predicates')
40
42
  exports.columns = require('./cds.ql-projections')
@@ -48,8 +48,6 @@ function _qbe (o, xpr=[]) {
48
48
  let count = 0
49
49
  for (let k in o) { const x = o[k]
50
50
 
51
- if (k !== 'and' && k !== 'or' && count++) xpr.push('and') //> add 'and' between conditions
52
-
53
51
  if (k.startsWith('not ')) { xpr.push('not'); k = k.slice(4) }
54
52
  switch (k) { // handle special cases like {and:{...}} or {or:{...}}
55
53
  case 'between':
@@ -61,7 +59,9 @@ function _qbe (o, xpr=[]) {
61
59
  case 'or': xpr.push(k)
62
60
  _qbe(x,xpr)
63
61
  continue
64
- case 'not': xpr.push(k)
62
+ case 'not':
63
+ if (count++) xpr.push('and') //> add 'and' between conditions
64
+ xpr.push(k)
65
65
  if (x && typeof x === 'object') x.in || x.like || x.exists || x.between ? _qbe(x,xpr) : xpr.push({xpr:_qbe(x)})
66
66
  else xpr.push(x === null ? 'null' : {val:x})
67
67
  continue
@@ -74,6 +74,7 @@ function _qbe (o, xpr=[]) {
74
74
  else xpr.push(x === null ? 'null' : {val:x})
75
75
  continue
76
76
  case 'exists':
77
+ if (count++) xpr.push('and') //> add 'and' between conditions
77
78
  xpr.push(k,_ref(x)||x)
78
79
  continue
79
80
  case 'in': case 'IN': // REVISIT: 'IN' is for compatibility only
@@ -84,7 +85,8 @@ function _qbe (o, xpr=[]) {
84
85
  continue
85
86
  }
86
87
 
87
- const a = cds.parse.ref(k) //> turn key into a ref for the left side of the expression
88
+ const a = cds.parse.ref(k) //> turn key into a ref for the left side of the expression
89
+ if (count++) xpr.push('and') //> add 'and' between conditions
88
90
  if (!x || typeof x !== 'object') xpr.push (a,'=',{val:x})
89
91
  else if (is_array(x)) xpr.push (a,'in',{list:x.map(_val)})
90
92
  else if (x.SELECT || x.list) xpr.push (a,'in',x)
@@ -0,0 +1,46 @@
1
+ const { resolveView, getTransition } = require('../../libx/_runtime/common/utils/resolveView')
2
+ const cds = require('../../lib')
3
+
4
+ const PERSISTENCE_TABLE = '@cds.persistence.table'
5
+ const _isPersistenceTable = target =>
6
+ Object.prototype.hasOwnProperty.call(target, PERSISTENCE_TABLE) && target[PERSISTENCE_TABLE]
7
+
8
+ // REVISIT revert after cds-dbs pr
9
+ // REVISIT: Remove once we get rid of old db
10
+ const _abortDB = resolve.abortDB = target => !!(_isPersistenceTable(target)|| !target.query?._target)
11
+ const _defaultAbort = tx => e => e._service?.name === tx.definition?.name
12
+
13
+ function resolve(query, tx, abortCondition) {
14
+ const ctx = cds.context
15
+ const abort = abortCondition ?? (typeof tx === 'function' ? tx : undefined)
16
+ const _tx = typeof tx === 'function' ? ctx?.tx : tx
17
+ const model = ctx?.model ?? _tx.model
18
+
19
+ return resolveView(query, model, _tx, abort || _defaultAbort(_tx))
20
+ }
21
+
22
+ resolve.resolve4db = (query, tx) => resolve(query, tx, _abortDB)
23
+
24
+ // REVISIT: Remove once we get rid of composition tree
25
+ resolve.table = target => {
26
+ if (target.query?._target && !_isPersistenceTable(target)) {
27
+ return resolve.table(target.query._target)
28
+ }
29
+ return target
30
+ }
31
+
32
+ // REVISIT: Remove argument `skipForbiddenViewCheck` once we get rid of composition tree
33
+ resolve.transitions = (query, tx, abortCondition, skipForbiddenViewCheck) => {
34
+ const target = query?.target || query?._target
35
+
36
+ const abort = abortCondition ?? (typeof tx === 'function' ? tx : undefined)
37
+ const _tx = typeof tx === 'function' ? cds.context?.tx : tx
38
+
39
+ return getTransition(target, _tx, skipForbiddenViewCheck, undefined, {
40
+ abort: abort ?? (tx.isDatabaseService ? _abortDB : _defaultAbort(tx))
41
+ })
42
+ }
43
+
44
+ resolve.transitions4db = (query, tx, skipForbiddenViewCheck) => resolve.transitions(query, tx, _abortDB, skipForbiddenViewCheck)
45
+
46
+ module.exports = resolve
@@ -5,6 +5,7 @@ const cds = require('..')
5
5
  * @returns {Error[]|undefined} an array of errors or undefined if no errors occurred
6
6
  */
7
7
  const conf = module.exports = exports = function validate (data, target, options={}) {
8
+ if (!target || target._unresolved) return
8
9
  const vc = new Validation (data, target, options)
9
10
  target.validate (data, null, vc)
10
11
  return vc.errors