@sap/cds 8.8.3 → 8.9.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.
- package/CHANGELOG.md +48 -4
- package/_i18n/i18n_en_US_saptrc.properties +3 -0
- package/bin/colors.js +2 -0
- package/bin/test.js +103 -75
- package/eslint.config.mjs +16 -4
- package/lib/compile/for/lean_drafts.js +4 -0
- package/lib/compile/parse.js +26 -6
- package/lib/env/cds-env.js +3 -1
- package/lib/env/cds-requires.js +0 -3
- package/lib/env/schemas/cds-rc.js +11 -0
- package/lib/log/format/aspects/cls.js +2 -1
- package/lib/log/format/json.js +1 -1
- package/lib/plugins.js +2 -3
- package/lib/ql/SELECT.js +2 -1
- package/lib/ql/cds-ql.js +2 -0
- package/lib/ql/cds.ql-predicates.js +6 -4
- package/lib/ql/resolve.js +46 -0
- package/lib/req/validate.js +1 -0
- package/lib/srv/bindings.js +64 -43
- package/lib/srv/cds-connect.js +1 -1
- package/lib/srv/cds-serve.js +2 -2
- package/lib/srv/middlewares/auth/ias-auth.js +2 -0
- package/lib/srv/protocols/http.js +2 -2
- package/lib/srv/protocols/index.js +1 -1
- package/lib/srv/protocols/odata-v4.js +0 -1
- package/lib/srv/srv-tx.js +1 -1
- package/lib/test/cds-test.js +3 -4
- package/lib/utils/cds-utils.js +19 -19
- package/lib/utils/colors.js +46 -45
- package/lib/utils/csv-reader.js +5 -5
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +1 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/index.js +1 -1
- package/libx/_runtime/common/Service.js +4 -2
- package/libx/_runtime/common/composition/data.js +1 -2
- package/libx/_runtime/common/composition/tree.js +6 -4
- package/libx/_runtime/common/generic/sorting.js +6 -2
- package/libx/_runtime/common/utils/cqn2cqn4sql.js +6 -7
- package/libx/_runtime/common/utils/differ.js +1 -1
- package/libx/_runtime/common/utils/draft.js +1 -1
- package/libx/_runtime/common/utils/foreignKeyPropagations.js +6 -2
- package/libx/_runtime/common/utils/keys.js +13 -84
- package/libx/_runtime/common/utils/propagateForeignKeys.js +4 -3
- package/libx/_runtime/common/utils/resolveView.js +96 -102
- package/libx/_runtime/common/utils/rewriteAsterisks.js +1 -1
- package/libx/_runtime/common/utils/stream.js +2 -3
- package/libx/_runtime/db/utils/columns.js +1 -1
- package/libx/_runtime/fiori/lean-draft.js +11 -7
- package/libx/_runtime/messaging/AMQPWebhookMessaging.js +1 -1
- package/libx/_runtime/messaging/common-utils/normalizeIncomingMessage.js +1 -2
- package/libx/_runtime/messaging/file-based.js +6 -6
- package/libx/_runtime/messaging/kafka.js +5 -7
- package/libx/_runtime/messaging/redis-messaging.js +1 -1
- package/libx/_runtime/messaging/service.js +11 -4
- package/libx/_runtime/remote/Service.js +13 -5
- package/libx/_runtime/remote/utils/client.js +1 -0
- package/libx/_runtime/ucl/Service.js +135 -126
- package/libx/common/utils/path.js +34 -22
- package/libx/odata/middleware/create.js +2 -0
- package/libx/odata/middleware/operation.js +8 -2
- package/libx/odata/middleware/parse.js +1 -1
- package/libx/odata/middleware/stream.js +1 -2
- package/libx/odata/middleware/update.js +2 -0
- package/libx/odata/parse/afterburner.js +17 -9
- package/libx/odata/parse/cqn2odata.js +43 -22
- package/libx/odata/parse/grammar.peggy +21 -19
- package/libx/odata/parse/parser.js +1 -1
- package/libx/odata/utils/metadata.js +8 -2
- package/libx/odata/utils/odataBind.js +36 -0
- package/libx/outbox/index.js +1 -0
- package/libx/rest/middleware/operation.js +9 -8
- package/libx/rest/middleware/parse.js +1 -0
- package/package.json +3 -3
- package/lib/i18n/resources.js +0 -150
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,50 @@
|
|
|
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.1 - 2025-04-03
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- `cds.env` merging for `null` values
|
|
12
|
+
- Best-effort mechanisms for lambda support on OData V2 remote services (usage of functions in lambda expressions)
|
|
13
|
+
- Use extended model in `enterprise-messaging` inbound handlers
|
|
14
|
+
|
|
15
|
+
## Version 8.9.0 - 2025-03-31
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- Support for parallel multi-instance processing of outbox entries
|
|
20
|
+
- Remote services: ensure request correlation by guaranteeing outgoing header `x-correlation-id`
|
|
21
|
+
- Support for `@odata.bind` to reference foreign keys
|
|
22
|
+
- Support for plugins in ESM format
|
|
23
|
+
- Dependency to `@eslint/js` so that `eslint` works w/o the application having to install it.
|
|
24
|
+
- IAS: In the `client_credentials` flow, the array of `ias_apis` (if present) is added to the technical user's roles
|
|
25
|
+
- Opt-in feature `cds.features.consistent_params` for `req.params` always being an array of objects
|
|
26
|
+
+ That is, no more plain values for single-keyed entities with key `ID`
|
|
27
|
+
+ Will become the default in `@sap/cds^9`
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
|
|
31
|
+
- Invalid draft requests now have status code 400
|
|
32
|
+
- Allow ESM loading of handler files (`.js`, `.ts`) in all situations, incl. test runs with Jest's `--experimental-vm-modules` option.
|
|
33
|
+
- 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.
|
|
34
|
+
- Accept 2xx status codes set in custom operation handlers
|
|
35
|
+
- Implicit orderby elements are marked as such and are no longer considered for requests to remote services
|
|
36
|
+
|
|
37
|
+
### Fixed
|
|
38
|
+
|
|
39
|
+
- Lean draft: Proper navigation to the service entity of draft-administrative data
|
|
40
|
+
- Unprocessed foreign keys from expressions of semi join conditions in `UPDATE.data`
|
|
41
|
+
- Kafka: Each topic will have a dedicated consumer-group id (configurable with `consumerGroup`)
|
|
42
|
+
- Foreign-key calculation based on navigation path
|
|
43
|
+
- `cds.env` shortcuts like `cds.requires.db === 'hana'` are normalized to `cds.requires.db.kind === 'hana'` when combined from multiple sources
|
|
44
|
+
- Error handling for invalid access of an entity that does not have a key, by key, through REST
|
|
45
|
+
- `cds.validate` crashed with unknown target
|
|
46
|
+
- `cds.parse.expr` parsed SAP HANA native functions like `current_utctimestamp` erroneously as `ref`
|
|
47
|
+
- `null` values in logger if `custom_fields` are configured
|
|
48
|
+
- User-provided instances of SAP Cloud Logging should have either tag `cloud-logging` or `Cloud Logging`
|
|
49
|
+
- 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`
|
|
50
|
+
|
|
7
51
|
## Version 8.8.3 - 2025-03-20
|
|
8
52
|
|
|
9
53
|
### Fixed
|
|
@@ -31,7 +75,7 @@
|
|
|
31
75
|
- Add missing 'and' between conditions in object notation of QL
|
|
32
76
|
- Multiline payloads in `$batch` sub requests
|
|
33
77
|
- Instance-based authorization for modeling like `$user.<property> is null`
|
|
34
|
-
- Respect `cds.odata.contextAbsoluteUrl` in new OData adapter
|
|
78
|
+
- Respect `cds.odata.contextAbsoluteUrl` in new OData adapter
|
|
35
79
|
- `cds.odata.context_with_columns` also applies to singletons
|
|
36
80
|
|
|
37
81
|
## Version 8.8.0 - 2025-03-03
|
|
@@ -46,9 +90,9 @@
|
|
|
46
90
|
- Support implicit function parameters calls with @prefix
|
|
47
91
|
- `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
92
|
- 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.
|
|
93
|
+
+ OData: Operations returning `cds.LargeBinary` annotated with `@Core.MediaType` may send stream responses.
|
|
94
|
+
+ REST: Operations may send stream responses.
|
|
95
|
+
+ Annotations `@Core.MediaType`, `@Core.ContentDisposition.Filename` and `@Core.ContentDisposition.Type` on operation return types will be considered.
|
|
52
96
|
|
|
53
97
|
### Changed
|
|
54
98
|
|
|
@@ -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
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
24
|
+
cds test [ options ] [ patterns ]
|
|
47
25
|
|
|
48
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
|
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)
|
|
92
|
-
const
|
|
93
|
-
test (
|
|
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 = [
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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) {
|
package/lib/compile/parse.js
CHANGED
|
@@ -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]}
|
|
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]
|
|
40
|
-
if (/^([\d.]+)$/.test(x)) return {val:Number(x)}
|
|
41
|
-
if (/^([\w_$]+)$/.test(x)) return {ref:[x]}
|
|
42
|
-
if (/^([\w_.$]+)$/.test(x)) return {ref:x.split('.')}
|
|
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 ||
|
package/lib/env/cds-env.js
CHANGED
|
@@ -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')) {
|
package/lib/env/cds-requires.js
CHANGED
|
@@ -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'] ||
|
|
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
|
: () => {}
|
package/lib/log/format/json.js
CHANGED
|
@@ -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
|
-
|
|
39
|
-
|
|
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':
|
|
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)
|
|
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
|
package/lib/req/validate.js
CHANGED
|
@@ -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
|