@sap/cds 8.1.1 → 8.2.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 +56 -0
- package/app/index.css +3 -0
- package/app/index.js +50 -4
- package/bin/serve.js +1 -1
- package/lib/compile/cdsc.js +2 -2
- package/lib/compile/etc/_localized.js +1 -1
- package/lib/compile/for/lean_drafts.js +1 -0
- package/lib/compile/to/sql.js +2 -2
- package/lib/env/cds-requires.js +6 -0
- package/lib/env/defaults.js +14 -3
- package/lib/env/plugins.js +6 -22
- package/lib/linked/classes.js +0 -14
- package/lib/linked/types.js +12 -0
- package/lib/linked/validate.js +13 -8
- package/lib/log/cds-log.js +3 -3
- package/lib/log/format/aspects/als.js +23 -29
- package/lib/log/format/aspects/cls.js +9 -0
- package/lib/log/format/json.js +42 -6
- package/lib/ql/Whereable.js +5 -1
- package/lib/srv/cds-connect.js +33 -32
- package/lib/srv/cds-serve.js +2 -1
- package/lib/srv/middlewares/cds-context.js +2 -1
- package/lib/utils/cds-utils.js +4 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/validator/ValueValidator.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/metaInfo.js +1 -5
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/result.js +2 -31
- package/libx/_runtime/common/generic/auth/utils.js +2 -0
- package/libx/_runtime/common/generic/input.js +2 -11
- package/libx/_runtime/common/generic/put.js +1 -10
- package/libx/_runtime/common/utils/binary.js +1 -7
- package/libx/_runtime/common/utils/resolveView.js +2 -2
- package/libx/_runtime/common/utils/search2cqn4sql.js +1 -1
- package/libx/_runtime/common/utils/streamProp.js +19 -6
- package/libx/_runtime/common/utils/template.js +26 -16
- package/libx/_runtime/common/utils/templateProcessor.js +8 -7
- package/libx/_runtime/common/utils/ucsn.js +2 -5
- package/libx/_runtime/db/expand/expandCQNToJoin.js +10 -0
- package/libx/_runtime/db/generic/input.js +1 -5
- package/libx/_runtime/fiori/lean-draft.js +272 -90
- package/libx/_runtime/messaging/event-broker.js +105 -40
- package/libx/_runtime/remote/utils/client.js +12 -4
- package/libx/_runtime/ucl/Service.js +16 -6
- package/libx/odata/middleware/batch.js +2 -2
- package/libx/odata/middleware/read.js +6 -10
- package/libx/odata/middleware/stream.js +4 -5
- package/libx/odata/parse/afterburner.js +3 -2
- package/libx/odata/parse/multipartToJson.js +3 -1
- package/libx/odata/utils/index.js +3 -3
- package/libx/odata/utils/postProcess.js +3 -25
- package/libx/rest/middleware/parse.js +1 -6
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,58 @@
|
|
|
4
4
|
- The format is based on [Keep a Changelog](http://keepachangelog.com/).
|
|
5
5
|
- This project adheres to [Semantic Versioning](http://semver.org/).
|
|
6
6
|
|
|
7
|
+
## Version 8.2.1 - 2024-09-04
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Date validation of legacy OData protocol adapter
|
|
12
|
+
- Content-Length headers in multipart batch request body
|
|
13
|
+
- Streaming requests with virtual properties
|
|
14
|
+
- Bring back support for `x-correlationid`
|
|
15
|
+
- Validation of inlined elements
|
|
16
|
+
- multipart `$batch` parsing with _--_ as part of payload
|
|
17
|
+
|
|
18
|
+
## Version 8.2.0 - 2024-08-30
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- Allow `cds.connect.to (SomeService)` where `SomeService` is a class
|
|
23
|
+
- Lean draft: support CDS orderBy in `list status: all`
|
|
24
|
+
- Support where not in as object in `cds.ql` expressions like: `where({ID:{not:{in:[...]}}})`
|
|
25
|
+
- Unbound CDS functions now show up in the server's index page along with an exemplary call signature
|
|
26
|
+
- `cds.log`'s JSON formatter:
|
|
27
|
+
+ Field `w3c_traceparent` is filled based on request header `traceparent` (cf. W3C Trace Context) for improved correlation
|
|
28
|
+
+ Custom fields `cds.env.log.cls_custom_fields` are filled if bound to an instance of SAP Cloud Logging
|
|
29
|
+
+ Default `cds.env.log.als_custom_fields` enhanced by `{ reason: 3 }` (project config takes precedence)
|
|
30
|
+
- Support for `cds.hana` types like `cds.hana.ST_POINT` in `cds.builtin`
|
|
31
|
+
- Internal `cds.debug()` API now always returns a logger instance, which allows switching on debugging subsequently, e.g. by the like of `cds.log('sql','debug')`
|
|
32
|
+
- New config flag `cds.server.shutdown_on_uncaught_errors` allows to control whether the server should shut down on uncaught errors. Default is `true`
|
|
33
|
+
|
|
34
|
+
### Changed
|
|
35
|
+
|
|
36
|
+
- Revert workaround from 8.1.0 for server startup message `WARNING: Package '@sap/cds' was loaded from different installations`. This is now addressed in `@sap/cds-mtxs` 2.0.5
|
|
37
|
+
|
|
38
|
+
### Fixed
|
|
39
|
+
|
|
40
|
+
- Resolving views with path expression renamings
|
|
41
|
+
- Set content-type-header in batch for actions with 204 No Content
|
|
42
|
+
- URI encoding of `@odata.nextLink` in OData response
|
|
43
|
+
- Requests reading media data streams did not provide `req.params`
|
|
44
|
+
- `cds.compile.to.hana` for legacy hana service with `@cap-js/sqlite` as dev dependency
|
|
45
|
+
- Better redaction of debug output
|
|
46
|
+
- Instance-based authorization using functions
|
|
47
|
+
- Fixed flaws in `cds.connect.to()` that lead to deadlocks in case of errors due to invalid service configurations or initializations.
|
|
48
|
+
- Navigation with backlink as key can now omit backlink keys for new OData adapter
|
|
49
|
+
|
|
50
|
+
### Removed
|
|
51
|
+
|
|
52
|
+
- Array methods `forEach`, `filter`, `find`, `map`, `some`, `every` from [`LinkedDefinitions`](https://cap.cloud.sap/docs/node.js/cds-reflect#iterable). Convert linked definitions into arrays before using these methods, for example:
|
|
53
|
+
|
|
54
|
+
```js
|
|
55
|
+
[...linked.definitions].map(d => d.name)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
|
|
7
59
|
## Version 8.1.1 - 2024-08-08
|
|
8
60
|
|
|
9
61
|
### Fixed
|
|
@@ -58,6 +110,9 @@
|
|
|
58
110
|
### Fixed
|
|
59
111
|
|
|
60
112
|
- Empty feature set by switched off feature toggles
|
|
113
|
+
- Allow programmatic operations on draft-enabled entities (`NEW`, `CREATE`, `UPDATE`, `DELETE`)
|
|
114
|
+
|
|
115
|
+
### Removed
|
|
61
116
|
- Allow deviating response types for `$batch`, e. g. input `multipart` and output `json`
|
|
62
117
|
|
|
63
118
|
## Version 8.0.2 - 2024-07-09
|
|
@@ -330,6 +385,7 @@
|
|
|
330
385
|
|
|
331
386
|
### Changed
|
|
332
387
|
|
|
388
|
+
- Optimized handling of large binaries (BLOBs) in case of drafts. Unchanged BLOBs are not copied into the draft entity. If those BLOBs from draft entities are requested, the unchanged BLOBs will be fetched from the corresponding active entity. Note that this change may require adjustment of custom logic, if large binaries from draft entities are requested (for example, using `ql.SELECT` statement). To restore previous behavior use `cds.features.binary_draft_compat`.
|
|
333
389
|
- The index page now lists all service endpoints, which is important for services that are exposed through multiple protocols.
|
|
334
390
|
- `cds.deploy` improves error diagnostics with deeper `Query` object inspection.
|
|
335
391
|
- Slightly changed the default export for ESM compatibility. This fixed failing ESM imports in Vitest tests.
|
package/app/index.css
CHANGED
package/app/index.js
CHANGED
|
@@ -14,7 +14,7 @@ module.exports = { get html(){
|
|
|
14
14
|
.replace (/{{app}}/g, cds.env.folders.app.replace(/*trailing slash*/ /\/$/, ''))
|
|
15
15
|
.replace ('{{style}}', css)
|
|
16
16
|
.replace ('{{apps}}', _app_links().map(
|
|
17
|
-
html =>
|
|
17
|
+
html => `<li><a href="${html}"><span>/${html.replace(/^\//,'').replace('/index.html','')}</span></a></li>`
|
|
18
18
|
).join('\n') || '— none —'
|
|
19
19
|
)
|
|
20
20
|
.replace ('{{services}}', cds.service.providers
|
|
@@ -25,14 +25,20 @@ module.exports = { get html(){
|
|
|
25
25
|
<h3 class="header">
|
|
26
26
|
<a href="${endpoint.path}"><span>${endpoint.path}</span></a>${metadata(endpoint)} ${_moreLinks(srv, endpoint, undefined, false)}
|
|
27
27
|
</h3>
|
|
28
|
-
<ul>${_entities_in(srv).map (e =>
|
|
29
|
-
return `
|
|
28
|
+
<ul>${_entities_in(srv).map (e => `
|
|
30
29
|
<li id="${asHtmlId(srv.name)}-${endpoint.kind}-${asHtmlId(e)}">
|
|
31
30
|
<div>
|
|
32
31
|
<a href="${endpoint.path}/${e.replace(/\./g, '_')}"><span>${e}</span></a>
|
|
33
32
|
</div>
|
|
34
33
|
${_moreLinks(srv, endpoint, e)}
|
|
35
|
-
</li>`
|
|
34
|
+
</li>`).join('')}
|
|
35
|
+
</ul>
|
|
36
|
+
<ul>${_operations_in(srv).map (e => `
|
|
37
|
+
<li id="${asHtmlId(srv.name)}-${endpoint.kind}-${asHtmlId(e.name)}" class="operation">
|
|
38
|
+
<div>
|
|
39
|
+
<a href="${endpoint.path}/${e.name}${e.params}" title="${endpoint.path}/${e.name}${e.params}"><span>${e.name}()</span></a>
|
|
40
|
+
</div>
|
|
41
|
+
</li>`).join('')}
|
|
36
42
|
</ul>
|
|
37
43
|
</div>
|
|
38
44
|
`).join(''))
|
|
@@ -63,6 +69,46 @@ function _entities_in (service) {
|
|
|
63
69
|
return exposed
|
|
64
70
|
}
|
|
65
71
|
|
|
72
|
+
function _operations_in (service) {
|
|
73
|
+
const exposed=[], {operations} = service
|
|
74
|
+
for (let name in operations) {
|
|
75
|
+
const op = cds.model.definitions[service.name + '.' + name]
|
|
76
|
+
if (op?.kind === 'function') {
|
|
77
|
+
const params = '('+ Object.values(op.params||[]).map(p => {
|
|
78
|
+
let val = _sampleValue(p)
|
|
79
|
+
if (typeof val === 'string') val = encodeURIComponent(`'${val}'`)
|
|
80
|
+
else if (typeof val === 'object') val = encodeURIComponent(`'${JSON.stringify(val)}'`)
|
|
81
|
+
return `${p.name}=${val}`
|
|
82
|
+
}).join(',') + ')'
|
|
83
|
+
exposed.push ({ name, params })
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return exposed
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function _sampleValue (param) {
|
|
90
|
+
if (param.items) // many
|
|
91
|
+
return [ _sampleValue(param.items) ]
|
|
92
|
+
if (param.elements) { // structured
|
|
93
|
+
return Object.entries(param.elements).reduce((all,[n,p]) => {
|
|
94
|
+
all[n] = _sampleValue(p)
|
|
95
|
+
return all
|
|
96
|
+
},{})
|
|
97
|
+
}
|
|
98
|
+
// scalar
|
|
99
|
+
const type = param._type || param.type
|
|
100
|
+
if (type === 'cds.String') return 'hello'
|
|
101
|
+
if (type === 'cds.Boolean') return true
|
|
102
|
+
if (type === 'cds.Decimal'||type === 'cds.Double') return '4.2'
|
|
103
|
+
if (type === 'cds.Date') return '2021-12-31'
|
|
104
|
+
if (type === 'cds.Time') return '23:42:42'
|
|
105
|
+
if (type === 'cds.DateTime') return '2021-12-31T23:42:42Z'
|
|
106
|
+
if (type === 'cds.Timestamp') return '2021-12-31T23:42:42.123Z'
|
|
107
|
+
if (type === 'cds.UUID') return cds.utils.uuid()
|
|
108
|
+
if (type?.match(/cds\..*Int.*/i)) return 42
|
|
109
|
+
return type // fallback
|
|
110
|
+
}
|
|
111
|
+
|
|
66
112
|
function _moreLinks (srv, endpoint, entity, div=true) {
|
|
67
113
|
return (srv.$linkProviders || [])
|
|
68
114
|
.map (linkProv => linkProv(entity, endpoint))
|
package/bin/serve.js
CHANGED
|
@@ -212,7 +212,7 @@ async function serve (all=[], o={}) {
|
|
|
212
212
|
|
|
213
213
|
const LOG = cds.log('cli|server')
|
|
214
214
|
cds.shutdown = _shutdown //> for programmatic invocation
|
|
215
|
-
if (!cds.repl) {
|
|
215
|
+
if (cds.env.server.shutdown_on_uncaught_errors && !cds.repl) {
|
|
216
216
|
process.on('unhandledRejection', e => _shutdown (e, cds.log().error('❗️Uncaught',e))) //> using std logger to have it labelled with [cds] - instead of [cli] -
|
|
217
217
|
process.on('uncaughtException', e => _shutdown (e, cds.log().error('❗️Uncaught',e))) //> using std logger to have it labelled with [cds] - instead of [cli] -
|
|
218
218
|
}
|
package/lib/compile/cdsc.js
CHANGED
|
@@ -77,8 +77,8 @@ const _options = {for: Object.assign (_options4, {
|
|
|
77
77
|
if (dialect) o.sqlDialect = dialect
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
const
|
|
81
|
-
if (_conf.impl
|
|
80
|
+
const legacy = '@sap/cds/libx/_runtime/' // includes legacy sqlite and hana
|
|
81
|
+
if (_conf.impl.includes(legacy)) {
|
|
82
82
|
o.betterSqliteSessionVariables = false
|
|
83
83
|
o.fewerLocalizedViews = false
|
|
84
84
|
}
|
|
@@ -16,7 +16,7 @@ const _been_here = Symbol('is _localized')
|
|
|
16
16
|
function unfold_ddl (ddl, csn, o={}) { // NOSONAR
|
|
17
17
|
const db = env.requires.db || env.requires.kinds.sql
|
|
18
18
|
const locales = db.impl === _legacy_sqlite && _locales_4sql[o.dialect]; if (!locales) return ddl
|
|
19
|
-
const localized_views = ddl.filter (each =>
|
|
19
|
+
const localized_views = ddl.filter (each => /^(?:-- .+\n)*(?:CREATE|DROP) VIEW localized_/g.test(each))
|
|
20
20
|
for (const localized_view of localized_views) {
|
|
21
21
|
for (const locale of locales) ddl.push (localized_view
|
|
22
22
|
.replace (/localized_/g, `localized_${locale}_`)
|
|
@@ -66,6 +66,7 @@ module.exports = function cds_compile_for_lean_drafts(csn) {
|
|
|
66
66
|
}
|
|
67
67
|
Object.defineProperty(model.definitions, _draftEntity, { value: draft })
|
|
68
68
|
Object.defineProperty(active, 'drafts', { value: draft })
|
|
69
|
+
Object.defineProperty(active, 'actives', { value: active })
|
|
69
70
|
Object.defineProperty(draft, 'actives', { value: active })
|
|
70
71
|
Object.defineProperty(draft, 'isDraft', { value: true })
|
|
71
72
|
|
package/lib/compile/to/sql.js
CHANGED
|
@@ -31,8 +31,8 @@ function cds_compile_to_deltaSql (csn, o, beforeCsn) {
|
|
|
31
31
|
const { afterImage, drops, createsAndAlters } = cdsc.to.deltaSql (csn, options, beforeCsn || {definitions: {}, $version: '2.0'} ); // FIXME: As default value in compiler API?
|
|
32
32
|
return {
|
|
33
33
|
afterImage,
|
|
34
|
-
drops: unfold_ddl(drops
|
|
35
|
-
createsAndAlters: unfold_ddl(createsAndAlters
|
|
34
|
+
drops: unfold_ddl(drops, csn, options),
|
|
35
|
+
createsAndAlters: unfold_ddl(createsAndAlters, csn, options)
|
|
36
36
|
};
|
|
37
37
|
}
|
|
38
38
|
|
package/lib/env/cds-requires.js
CHANGED
|
@@ -234,6 +234,12 @@ const _messaging = {
|
|
|
234
234
|
"event-broker": {
|
|
235
235
|
impl: `${_runtime}/messaging/event-broker.js`,
|
|
236
236
|
format: 'cloudevents',
|
|
237
|
+
vcap: {
|
|
238
|
+
label: "event-broker"
|
|
239
|
+
}
|
|
240
|
+
},
|
|
241
|
+
"event-broker-internal": {
|
|
242
|
+
kind: "event-broker",
|
|
237
243
|
vcap: {
|
|
238
244
|
label: "eventmesh-sap2sap-internal"
|
|
239
245
|
}
|
package/lib/env/defaults.js
CHANGED
|
@@ -39,6 +39,7 @@ const defaults = module.exports = {
|
|
|
39
39
|
requires: require('./cds-requires'),
|
|
40
40
|
|
|
41
41
|
server: {
|
|
42
|
+
shutdown_on_uncaught_errors: true,
|
|
42
43
|
force_exit_timeout: 1111,
|
|
43
44
|
body_parser: undefined, // Allows to configure all body parser options, e.g. limit
|
|
44
45
|
cors: !production, // CORS middleware is off in production
|
|
@@ -102,15 +103,25 @@ const defaults = module.exports = {
|
|
|
102
103
|
// the rest is only applicable for the json formatter
|
|
103
104
|
user: false,
|
|
104
105
|
mask_headers: ['/authorization/i', '/cookie/i', '/cert/i', '/ssl/i'],
|
|
105
|
-
aspects: ['./aspects/cf', './aspects/als'], //> EXPERIMENTAL!!!
|
|
106
|
+
aspects: ['./aspects/cf', './aspects/als', './aspects/cls'], //> EXPERIMENTAL!!!
|
|
106
107
|
// adds custom fields in kibana's error rendering (unknown fields are ignored); key: index
|
|
107
108
|
// note: custom fields are a feature of Application Logging Service (ALS) and not Kibana per se
|
|
108
109
|
als_custom_fields: {
|
|
109
110
|
// sql
|
|
110
111
|
query: 0,
|
|
111
112
|
// generic validations
|
|
112
|
-
target: 1, details: 2
|
|
113
|
-
|
|
113
|
+
target: 1, details: 2,
|
|
114
|
+
// errors
|
|
115
|
+
reason: 3
|
|
116
|
+
},
|
|
117
|
+
cls_custom_fields: [
|
|
118
|
+
// sql
|
|
119
|
+
'query',
|
|
120
|
+
// generic validations
|
|
121
|
+
'target', 'details',
|
|
122
|
+
// errors
|
|
123
|
+
'reason'
|
|
124
|
+
]
|
|
114
125
|
},
|
|
115
126
|
|
|
116
127
|
folders: { // IMPORTANT: order is significant for cds.load('*')
|
package/lib/env/plugins.js
CHANGED
|
@@ -1,34 +1,18 @@
|
|
|
1
1
|
// REVISIT: we should have a real modular plugin technique for cds.env
|
|
2
2
|
module.exports = function add_mtx_env (env) {
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const mtx_env = _require('@sap/cds-mtxs/env')
|
|
4
|
+
const mtx_env = require('@sap/cds-mtxs/env')
|
|
7
5
|
if (mtx_env) {
|
|
8
6
|
const {requires} = env, {kinds} = requires
|
|
9
7
|
Object.assign (env, mtx_env, {requires})
|
|
10
8
|
Object.assign (requires, mtx_env.requires, {kinds})
|
|
11
9
|
Object.assign (kinds, mtx_env.requires?.kinds)
|
|
12
10
|
}
|
|
13
|
-
return env
|
|
14
|
-
}
|
|
15
11
|
|
|
16
|
-
function
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
12
|
+
function require (id) {
|
|
13
|
+
try { return module.require(id) }
|
|
14
|
+
catch(e) { if (e.code !== 'MODULE_NOT_FOUND') throw e }
|
|
15
|
+
}
|
|
20
16
|
|
|
21
|
-
|
|
22
|
-
// May happen in PNPM setups that add all global installs to NODE_PATH. Seen in BAS.
|
|
23
|
-
function cds_load_mismatch() {
|
|
24
|
-
const cds = require('..')
|
|
25
|
-
try {
|
|
26
|
-
const mtxs = require.resolve('@sap/cds-mtxs')
|
|
27
|
-
const cds2 = require.resolve('@sap/cds/package.json', { paths:[mtxs] })
|
|
28
|
-
const csd2Home = cds.utils.path.resolve(cds2, '..')
|
|
29
|
-
if (csd2Home !== cds.home) {
|
|
30
|
-
return true
|
|
31
|
-
}
|
|
32
|
-
// console.log('home', cds.home, 'other', otherCdsHome)
|
|
33
|
-
} catch(e) { if (e.code !== 'MODULE_NOT_FOUND') throw e }
|
|
17
|
+
return env
|
|
34
18
|
}
|
package/lib/linked/classes.js
CHANGED
|
@@ -184,22 +184,8 @@ class event extends aspect {}
|
|
|
184
184
|
|
|
185
185
|
class LinkedDefinitions {
|
|
186
186
|
*[Symbol.iterator](){ for (let e in this) yield this[e] }
|
|
187
|
-
forEach(f){ let i=0; for (let k in this) f(this[k],i++,this) }
|
|
188
|
-
filter(f){ let i=0, r=[]; for (let k in this) f(this[k],i++,this) && r.push(this[k]); return r }
|
|
189
|
-
find(f){ for (let k in this) if (f(this[k])) return this[k] }
|
|
190
|
-
map(f){ let i=0, r=[]; for (let k in this) r.push(f(this[k],i++,this)); return r }
|
|
191
|
-
some(f){ for (let k in this) if (f(this[k])) return true }
|
|
192
|
-
every(f){ for (let k in this) if (!f(this[k])) return false }
|
|
193
187
|
}
|
|
194
188
|
|
|
195
|
-
// Protect LinkedDefinitions methods from erroneously being used as linked definitions/elements
|
|
196
|
-
Object.entries(Object.getOwnPropertyDescriptors(LinkedDefinitions.prototype)).forEach(([p,pd]) => p === 'constructor' || Object.defineProperties(pd.value, {
|
|
197
|
-
own: {get(){ throw new Error(`'${p}' is not a linked definition but a method inherited from LinkedDefinitions`) }},
|
|
198
|
-
name: {get(){ throw new Error(`'${p}' is not a linked definition but a method inherited from LinkedDefinitions`) }},
|
|
199
|
-
kind: {get(){ throw new Error(`'${p}' is not a linked definition but a method inherited from LinkedDefinitions`) }},
|
|
200
|
-
type: {get(){ throw new Error(`'${p}' is not a linked definition but a method inherited from LinkedDefinitions`) }},
|
|
201
|
-
_type: {get(){ throw new Error(`'${p}' is not a linked definition but a method inherited from LinkedDefinitions`) }},
|
|
202
|
-
}))
|
|
203
189
|
|
|
204
190
|
module.exports = {
|
|
205
191
|
|
package/lib/linked/types.js
CHANGED
|
@@ -20,6 +20,18 @@ Object.assign (protos, types.deprecated = {
|
|
|
20
20
|
'cds.Integer64': new classes.Int64,
|
|
21
21
|
})
|
|
22
22
|
|
|
23
|
+
Object.assign (protos, types.hana = {
|
|
24
|
+
'cds.hana.SMALLDECIMAL': new classes.Decimal,
|
|
25
|
+
'cds.hana.SMALLINT': new classes.Int16,
|
|
26
|
+
'cds.hana.TINYINT': new classes.UInt8,
|
|
27
|
+
'cds.hana.REAL': new classes.Double,
|
|
28
|
+
'cds.hana.CHAR': new classes.String,
|
|
29
|
+
'cds.hana.CLOB': new classes.LargeString,
|
|
30
|
+
'cds.hana.NCHAR': new classes.String,
|
|
31
|
+
'cds.hana.BINARY': new classes.String,
|
|
32
|
+
'cds.hana.ST_POINT': new classes.type,
|
|
33
|
+
'cds.hana.ST_GEOMETRY': new classes.type,
|
|
34
|
+
})
|
|
23
35
|
|
|
24
36
|
protos.service.set('is_service',true)
|
|
25
37
|
protos.struct.set('is_struct',true)
|
package/lib/linked/validate.js
CHANGED
|
@@ -56,13 +56,15 @@ class Validation {
|
|
|
56
56
|
class ValidationErrors extends Array {
|
|
57
57
|
add (error) {
|
|
58
58
|
const err = Object.create (ValidationErrors.proto)
|
|
59
|
-
err.message =
|
|
59
|
+
err.message = error
|
|
60
60
|
this.push (err)
|
|
61
61
|
return err
|
|
62
62
|
}
|
|
63
63
|
static proto = Object.create (Error.prototype, {
|
|
64
64
|
message: { writable:true, configurable:true },
|
|
65
|
-
stack: {
|
|
65
|
+
stack: { configurable:true, get() { return this.message },
|
|
66
|
+
set(v) { Object.defineProperty (this, 'stack', { value:v, writable:true, configurable:true }) },
|
|
67
|
+
},
|
|
66
68
|
code: { value: '400', writable:true }, // REVISIT: should be 'ASSERT_'... (i.e. msg) but we need to adjust all tests, and have a code catalogue
|
|
67
69
|
statusCode: { value: 400 }, // REVISIT: should go into mappings in adapter's error handlers -> requires a code catalogue // REVISIT: .statusCode vs .status?
|
|
68
70
|
numericSeverity: { value: 4, enumerable: true }, // REVISIT: that is OData-specific
|
|
@@ -118,12 +120,15 @@ const $any = class any {
|
|
|
118
120
|
}
|
|
119
121
|
|
|
120
122
|
_is_mandatory (d=this) {
|
|
121
|
-
return d.own('_mandatory', ()=>
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
123
|
+
return d.own('_mandatory', ()=> (
|
|
124
|
+
!d['@readonly'] // readonly -> not mandatory
|
|
125
|
+
&& (d['@mandatory'] || d['@Common.FieldControl']?.['#'] === 'Mandatory')
|
|
126
|
+
&& !d._is_flattened()
|
|
127
|
+
))
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
_is_flattened (d=this) {
|
|
131
|
+
return d.parent?.query?.SELECT.columns?.some (c => c.ref?.length > 1 && d.name === (c.as || c.ref.at(-1)))
|
|
127
132
|
}
|
|
128
133
|
|
|
129
134
|
_is_readonly (d=this) {
|
package/lib/log/cds-log.js
CHANGED
|
@@ -71,9 +71,9 @@ function cds_log (module, options) { // NOSONAR
|
|
|
71
71
|
*/
|
|
72
72
|
exports.debug = function cds_debug (id, options) {
|
|
73
73
|
const L = cds_log (id, options)
|
|
74
|
-
|
|
75
|
-
time: label => console.time (`[${id}] - ${label}`),
|
|
76
|
-
timeEnd: label => console.timeEnd (`[${id}] - ${label}`),
|
|
74
|
+
return Object.assign((..._) => L._debug && L.debug (..._), {
|
|
75
|
+
time: label => L._debug && console.time (`[${id}] - ${label}`),
|
|
76
|
+
timeEnd: label => L._debug && console.timeEnd (`[${id}] - ${label}`),
|
|
77
77
|
})
|
|
78
78
|
}
|
|
79
79
|
|
|
@@ -1,42 +1,36 @@
|
|
|
1
1
|
const cds = require('../../..')
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const _is_custom_fields = (arg, custom_fields) => {
|
|
6
|
-
if (!Object.keys(arg).length) return false
|
|
7
|
-
for (const k in arg) if (!(k in custom_fields)) return false
|
|
8
|
-
return true
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
const _is_categories = arg => arg.categories && Array.isArray(arg.categories) && Object.keys(arg).length === 1
|
|
12
|
-
|
|
13
|
-
function als_aspect(module, level, args, toLog) {
|
|
14
|
-
// REVISIT: kibana_custom_fields for backward compatibility. remove in cds^8.
|
|
3
|
+
let _kibana_custom_fields_deprecation_logged = false
|
|
4
|
+
const _get_als_custom_fields = () => {
|
|
15
5
|
const { als_custom_fields, kibana_custom_fields } = cds.env.log
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
if (args.length) {
|
|
21
|
-
let filter4removed = false
|
|
22
|
-
for (let i = 0; i < args.length; i++) {
|
|
23
|
-
const arg = args[i]
|
|
24
|
-
if (typeof arg !== 'object') continue
|
|
25
|
-
if ((this._HAS_CUSTOM_FIELDS && _is_custom_fields(arg, this._CUSTOM_FIELDS)) || _is_categories(arg)) {
|
|
26
|
-
Object.assign(toLog, arg)
|
|
27
|
-
args[i] = $remove
|
|
28
|
-
filter4removed = true
|
|
29
|
-
}
|
|
6
|
+
if (kibana_custom_fields) {
|
|
7
|
+
if (!_kibana_custom_fields_deprecation_logged) {
|
|
8
|
+
_kibana_custom_fields_deprecation_logged = true
|
|
9
|
+
cds.utils.deprecated({ old: 'cds.env.log.kibana_custom_fields', use: 'cds.env.log.als_custom_fields' })
|
|
30
10
|
}
|
|
31
|
-
|
|
11
|
+
return kibana_custom_fields
|
|
32
12
|
}
|
|
13
|
+
return als_custom_fields
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function als_aspect(module, level, args, toLog) {
|
|
17
|
+
this._ALS_CUSTOM_FIELDS ??= { ..._get_als_custom_fields() }
|
|
18
|
+
this._ALS_HAS_CUSTOM_FIELDS ??= Object.keys(this._ALS_CUSTOM_FIELDS).length > 0
|
|
33
19
|
|
|
34
20
|
// ALS custom fields
|
|
35
|
-
if (this.
|
|
21
|
+
if (this._ALS_HAS_CUSTOM_FIELDS) {
|
|
36
22
|
const cf = []
|
|
37
|
-
for (const k in this.
|
|
23
|
+
for (const k in this._ALS_CUSTOM_FIELDS) {
|
|
24
|
+
if (toLog[k]) {
|
|
25
|
+
const i = cf.findIndex(e => e.i === this._ALS_CUSTOM_FIELDS[k])
|
|
26
|
+
if (i > -1) cf[i] = { k, v: toLog[k], i: this._ALS_CUSTOM_FIELDS[k] }
|
|
27
|
+
else cf.push({ k, v: toLog[k], i: this._ALS_CUSTOM_FIELDS[k] })
|
|
28
|
+
}
|
|
29
|
+
}
|
|
38
30
|
if (cf.length) toLog['#cf'] = { string: cf }
|
|
39
31
|
}
|
|
40
32
|
}
|
|
41
33
|
|
|
34
|
+
als_aspect.cf = () => Object.keys({ ..._get_als_custom_fields() })
|
|
35
|
+
|
|
42
36
|
module.exports = process.env.VCAP_SERVICES?.match(/"label":\s*"application-logs"/) ? als_aspect : () => {}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
const cds = require('../../..')
|
|
2
|
+
|
|
3
|
+
function cls_aspect(/* module, level, args, toLog */) {
|
|
4
|
+
// actually nothing to do
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
cls_aspect.cf = () => [...cds.env.log.cls_custom_fields]
|
|
8
|
+
|
|
9
|
+
module.exports = process.env.VCAP_SERVICES?.match(/"label":\s*"cloud-logging"/) ? cls_aspect : () => {}
|
package/lib/log/format/json.js
CHANGED
|
@@ -5,7 +5,8 @@ const util = require('util')
|
|
|
5
5
|
const L2L = { 1: 'error', 2: 'warn', 3: 'info', 4: 'debug', 5: 'trace' }
|
|
6
6
|
const HEADER_MAPPINGS = {
|
|
7
7
|
x_vcap_request_id: 'request_id',
|
|
8
|
-
content_length: 'request_size_b'
|
|
8
|
+
content_length: 'request_size_b',
|
|
9
|
+
traceparent: 'w3c_traceparent'
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
const _is4xx = ele =>
|
|
@@ -13,6 +14,32 @@ const _is4xx = ele =>
|
|
|
13
14
|
(ele.status >= 400 && ele.status < 500) ||
|
|
14
15
|
(ele.statusCode >= 400 && ele.statusCode < 500)
|
|
15
16
|
|
|
17
|
+
const $remove = Symbol('remove')
|
|
18
|
+
|
|
19
|
+
const _is_custom_fields = (arg, custom_fields) => {
|
|
20
|
+
if (!Object.keys(arg).length) return false
|
|
21
|
+
for (const k in arg) if (!custom_fields.has(k)) return false
|
|
22
|
+
return true
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const _is_categories = arg => arg.categories && Array.isArray(arg.categories) && Object.keys(arg).length === 1
|
|
26
|
+
|
|
27
|
+
const _extract_custom_fields_and_categories = (args, toLog, custom_fields) => {
|
|
28
|
+
if (args.length) {
|
|
29
|
+
let filter4removed = false
|
|
30
|
+
for (let i = 0; i < args.length; i++) {
|
|
31
|
+
const arg = args[i]
|
|
32
|
+
if (typeof arg !== 'object') continue
|
|
33
|
+
if ((custom_fields.size && _is_custom_fields(arg, custom_fields)) || _is_categories(arg)) {
|
|
34
|
+
Object.assign(toLog, arg)
|
|
35
|
+
args[i] = $remove
|
|
36
|
+
filter4removed = true
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (filter4removed) args.sort((a, b) => (b === $remove) * -1).splice(args.lastIndexOf($remove))
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
16
43
|
const _getCircularReplacer = () => {
|
|
17
44
|
const seen = new WeakSet()
|
|
18
45
|
return (key, value) => {
|
|
@@ -51,13 +78,13 @@ module.exports = function format(module, level, ...args) {
|
|
|
51
78
|
// log user id, if configured (data privacy)
|
|
52
79
|
if (user && log_user) toLog.remote_user = user.id
|
|
53
80
|
// if available, add headers (normalized to lowercase and with _ instead of -) with masking as configured and mappings applied
|
|
54
|
-
const
|
|
55
|
-
if (
|
|
56
|
-
for (const k in
|
|
81
|
+
const headers = cds.context.http?.req?.headers
|
|
82
|
+
if (headers) {
|
|
83
|
+
for (const k in headers) {
|
|
57
84
|
const h = k.replace(/-/g, '_').toLowerCase()
|
|
58
85
|
toLog[h] = (() => {
|
|
59
86
|
if (this._MASK_HEADERS.some(m => k.match(m))) return '***'
|
|
60
|
-
return
|
|
87
|
+
return headers[k]
|
|
61
88
|
})()
|
|
62
89
|
if (h in HEADER_MAPPINGS) toLog[HEADER_MAPPINGS[h]] = toLog[h]
|
|
63
90
|
}
|
|
@@ -84,7 +111,16 @@ module.exports = function format(module, level, ...args) {
|
|
|
84
111
|
Object.assign(toLog, err, { level: toLog.level })
|
|
85
112
|
}
|
|
86
113
|
|
|
87
|
-
|
|
114
|
+
/*
|
|
115
|
+
* apply aspects:
|
|
116
|
+
* 1. extract custom fields (provided by the aspects) and categories from remaining args
|
|
117
|
+
* 2. actually apply the aspects
|
|
118
|
+
*/
|
|
119
|
+
if (!this._custom_fields) {
|
|
120
|
+
this._custom_fields = new Set()
|
|
121
|
+
for (const each of this._ASPECTS) if (each.cf) each.cf().forEach(v => this._custom_fields.add(v))
|
|
122
|
+
}
|
|
123
|
+
_extract_custom_fields_and_categories(args, toLog, this._custom_fields)
|
|
88
124
|
for (const each of this._ASPECTS) each.call(this, module, level, args, toLog)
|
|
89
125
|
|
|
90
126
|
// append remaining args via util.format()
|
package/lib/ql/Whereable.js
CHANGED
|
@@ -90,6 +90,10 @@ const _object_predicate = ([arg], _clause) => { // e.g. .where ({ID:4711, stock:
|
|
|
90
90
|
pred.push('and', 'not', 'exists', typeof x === 'object' ? x : { ref: x.split('.') })
|
|
91
91
|
continue
|
|
92
92
|
}
|
|
93
|
+
if (k === 'in') {
|
|
94
|
+
pred.push('in', val(x))
|
|
95
|
+
continue
|
|
96
|
+
}
|
|
93
97
|
else pred.push('and', parse.expr(k))
|
|
94
98
|
if (!x || x==='*') pred.push('=', {val:x})
|
|
95
99
|
else if (x.SELECT || x.list) pred.push('in', x)
|
|
@@ -98,7 +102,7 @@ const _object_predicate = ([arg], _clause) => { // e.g. .where ({ID:4711, stock:
|
|
|
98
102
|
else if (x instanceof Buffer) pred.push('=', {val:x})
|
|
99
103
|
else if (x instanceof RegExp) pred.push('like', {val:x})
|
|
100
104
|
else if (x instanceof Date) pred.push('=', {val:x})
|
|
101
|
-
else if (typeof x === 'object') for (let op in x) pred.push(op, val(x[op]))
|
|
105
|
+
else if (typeof x === 'object') for (let op in x) x[op]?.in ? pred.push(op, ...predicate4([x[op]],_clause)) : pred.push(op, val(x[op])) // REVIST: Should always be proper recursion
|
|
102
106
|
else if (_clause === 'on' && typeof x === 'string') pred.push('=', { ref: x.split('.') })
|
|
103
107
|
else pred.push('=', {val:x})
|
|
104
108
|
}
|