@sap/cds 8.2.0 → 8.2.2
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 +18 -0
- package/lib/linked/validate.js +58 -38
- package/lib/srv/middlewares/cds-context.js +2 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/validator/ValueValidator.js +1 -1
- package/libx/_runtime/common/utils/streamProp.js +7 -5
- package/libx/odata/parse/afterburner.js +1 -1
- package/libx/odata/parse/multipartToJson.js +3 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,24 @@
|
|
|
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.2 - 2024-09-13
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Erroneous caching in `cds.validate`
|
|
12
|
+
- Properly check `$filter` element types across navigations
|
|
13
|
+
|
|
14
|
+
## Version 8.2.1 - 2024-09-04
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- Date validation of legacy OData protocol adapter
|
|
19
|
+
- Content-Length headers in multipart batch request body
|
|
20
|
+
- Streaming requests with virtual properties
|
|
21
|
+
- Bring back support for `x-correlationid`
|
|
22
|
+
- Validation of inlined elements
|
|
23
|
+
- multipart `$batch` parsing with _--_ as part of payload
|
|
24
|
+
|
|
7
25
|
## Version 8.2.0 - 2024-08-30
|
|
8
26
|
|
|
9
27
|
### Added
|
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
|
|
@@ -90,38 +92,52 @@ const $any = class any {
|
|
|
90
92
|
* this method for subsequent usages, with statically determined checks.
|
|
91
93
|
*/
|
|
92
94
|
check_asserts (val, path, /** @type {Validation} */ ctx) {
|
|
93
|
-
//
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
asserts
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
95
|
+
// IMPORTANT: We need to use this.own() here as elements derived from reuse
|
|
96
|
+
// definitions or from elements of base entities might have different asserts
|
|
97
|
+
// than inherited ones.
|
|
98
|
+
const check_asserts = this.own('_check_asserts', () => {
|
|
99
|
+
const asserts = []
|
|
100
|
+
const type_check = conf.strict && this.strict_check || this.type_check
|
|
101
|
+
if (type_check) {
|
|
102
|
+
asserts.push ((v,p,ctx) => v == null || type_check(v) || ctx.error ('ASSERT_DATA_TYPE', p, this.name, v, this ))
|
|
103
|
+
}
|
|
104
|
+
if (this._is_mandatory()) {
|
|
105
|
+
asserts.push ((v,p,ctx) => v != null && v.trim?.() !== '' || ctx.error ('ASSERT_NOT_NULL', p, this.name, v)) // ASSERT_NOT_NULL is misleading -> should be ASSERT_REQUIRED
|
|
106
|
+
}
|
|
107
|
+
if (this['@assert.format']) {
|
|
108
|
+
const format = new RegExp(this['@assert.format'],'u')
|
|
109
|
+
asserts.push ((v,p,ctx) => v == null || format.test(v) || ctx.error ('ASSERT_FORMAT', p, this.name, v, format))
|
|
110
|
+
}
|
|
111
|
+
if (this['@assert.range'] && !this.enum) {
|
|
112
|
+
const [ min, max ] = this['@assert.range']
|
|
113
|
+
asserts.push ((v,p,ctx) => v == null || min <= v && v <= max || ctx.error ('ASSERT_RANGE', p, this.name, v, min, max))
|
|
114
|
+
}
|
|
115
|
+
if (this['@assert.enum'] || this['@assert.range'] && this.enum) {
|
|
116
|
+
const vals = Object.entries(this.enum).map(([k,v]) => 'val' in v ? v.val : k)
|
|
117
|
+
const enums = vals.reduce((a,v) => (a[v]=true, a),{})
|
|
118
|
+
asserts.push ((v,p,ctx) => v == null || v in enums || vals.some(x => x == v) || ctx.error ('ASSERT_ENUM', p, this.name, typeof v === 'string' ? `"${v}"` : v, vals.join(', ')))
|
|
119
|
+
}
|
|
120
|
+
if (!asserts.length) return ()=>{} // nothing to do
|
|
121
|
+
return (v,p,ctx) => asserts.forEach (a => a(v,p,ctx))
|
|
122
|
+
})
|
|
123
|
+
return check_asserts (val, path, ctx)
|
|
118
124
|
}
|
|
119
125
|
|
|
120
126
|
_is_mandatory (d=this) {
|
|
121
127
|
return d.own('_mandatory', ()=> {
|
|
122
|
-
if (d
|
|
123
|
-
if (d['@mandatory'])
|
|
124
|
-
|
|
128
|
+
if (d._is_readonly()) return false // readonly annotations have precedence over mandatory ones
|
|
129
|
+
if (d['@mandatory'] || d['@Common.FieldControl']?.['#'] === 'Mandatory') {
|
|
130
|
+
const q = d.parent?.query?.SELECT
|
|
131
|
+
if (!q) return true // it's a regular entity's element marked as mandatory
|
|
132
|
+
if (!q.from?.ref) return false // join or union -> elements can't be mandatory
|
|
133
|
+
const c = q.columns?.find (c => alias4(c) === d.name)
|
|
134
|
+
if (!c) return true // * or foo.* -> can't tell whether d is joined
|
|
135
|
+
if (!c.ref) return false // calculated fields aren't mandatory
|
|
136
|
+
if (c.ref.length === 1) return true // SELECT from Foo { foo }
|
|
137
|
+
if (c.ref.length === 2 && c.ref[0] === alias4(q.from)) return true // SELECT from Foo as f { f.foo }
|
|
138
|
+
else return false // joined field which can't be mandatory, e.g. SELECT from Books { author.name as author }
|
|
139
|
+
function alias4 (x) { return x.as || x.ref?.at(-1) }
|
|
140
|
+
}
|
|
125
141
|
else return false
|
|
126
142
|
})
|
|
127
143
|
}
|
|
@@ -143,17 +159,21 @@ const $any = class any {
|
|
|
143
159
|
* This is the case if the row date does not contain all primary key elements of the target entity.
|
|
144
160
|
*/
|
|
145
161
|
_is_insert (row) {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
162
|
+
// IMPORTANT: We need to use this.own() here as derived entities might have
|
|
163
|
+
// different keys and thus different insert checks.
|
|
164
|
+
const _is_insert = this.own('__is_insert', () => {
|
|
165
|
+
const entity = this._target || this
|
|
166
|
+
const keys = Object.keys (entity.keys||{})
|
|
167
|
+
if (!keys.length) return ()=> true
|
|
168
|
+
else return data => typeof data === 'object' && !keys.every(k => k in data)
|
|
169
|
+
})
|
|
170
|
+
return _is_insert(row)
|
|
151
171
|
}
|
|
152
172
|
|
|
153
173
|
_required (elements) {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
return
|
|
174
|
+
// IMPORTANT: We need to use this.own() here as derived entities might have
|
|
175
|
+
// different elements or elements with different annotations than base entitites.
|
|
176
|
+
return this.own('__required', ()=> Object.values(elements).filter(this._is_mandatory))
|
|
157
177
|
}
|
|
158
178
|
|
|
159
179
|
/** Forward declaration for universal CSN */
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const cds = require ('../../index')
|
|
2
2
|
const corr_id = 'x-correlation-id'
|
|
3
|
+
const crippled_corr_id = 'x-correlationid'
|
|
3
4
|
const req_id = 'x-request-id'
|
|
4
5
|
const vr_id = 'x-vcap-request-id'
|
|
5
6
|
const { uuid } = cds.utils
|
|
@@ -8,7 +9,7 @@ const { EventContext } = cds
|
|
|
8
9
|
module.exports = () => {
|
|
9
10
|
/** @type { import('express').Handler } */
|
|
10
11
|
return function cds_context (req, res, next) {
|
|
11
|
-
const id = req.headers[corr_id] ??= req.headers[req_id] || req.headers[vr_id] || uuid()
|
|
12
|
+
const id = req.headers[corr_id] ??= req.headers[req_id] || req.headers[vr_id] || req.headers[crippled_corr_id] || uuid()
|
|
12
13
|
const ctx = EventContext.for ({ id, http: { req, res } })
|
|
13
14
|
res.set ('X-Correlation-ID', id) // Note: we use capitalized style here as that's common standard in HTTP world
|
|
14
15
|
cds._context.run (ctx, next)
|
package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/validator/ValueValidator.js
CHANGED
|
@@ -4,7 +4,7 @@ const { big } = require('@sap/cds-foss')
|
|
|
4
4
|
const { isInvalidBase64string } = require('../../../../../../common/utils/binary')
|
|
5
5
|
const IllegalArgumentError = require('../errors/IllegalArgumentError')
|
|
6
6
|
|
|
7
|
-
const YEAR_RE = '(?:-?(?:(?:(?:0\\d{3})|(?:[1-9]\\d{3
|
|
7
|
+
const YEAR_RE = '(?:-?(?:(?:(?:0\\d{3})|(?:[1-9]\\d{3}))))'
|
|
8
8
|
const MONTH_RE = '(?:(?:0[1-9])|(?:1[012]))'
|
|
9
9
|
const DAY_RE = '(?:(?:0[1-9])|(?:[12]\\d)|(?:3[01]))'
|
|
10
10
|
const HOURS_RE = '(?:(?:[01]\\d)|(?:2[0-3]))'
|
|
@@ -2,8 +2,10 @@ const cds = require('../../cds')
|
|
|
2
2
|
const { ensureNoDraftsSuffix, ensureUnlocalized } = require('./draft')
|
|
3
3
|
const { isDuplicate } = require('./rewriteAsterisks')
|
|
4
4
|
|
|
5
|
-
const _addColumn = (name, type, columns, url) => {
|
|
6
|
-
|
|
5
|
+
const _addColumn = (name, type, columns, url, target) => {
|
|
6
|
+
let mediaType = typeof type === 'object' && type['=']
|
|
7
|
+
if (mediaType && target.elements[mediaType]?.virtual) return
|
|
8
|
+
mediaType = mediaType ? { ref: [mediaType.replaceAll(/\./g, '_')] } : { val: type }
|
|
7
9
|
const col = {
|
|
8
10
|
xpr: [
|
|
9
11
|
'case',
|
|
@@ -34,8 +36,8 @@ const _addColumn = (name, type, columns, url) => {
|
|
|
34
36
|
const _addColumns = (target, columns) => {
|
|
35
37
|
for (const k in target.elements) {
|
|
36
38
|
const el = target.elements[k]
|
|
37
|
-
if (el['@Core.MediaType']) {
|
|
38
|
-
_addColumn(el.name, el['@Core.MediaType'], columns, el['@Core.IsURL'] && el.type === 'cds.String')
|
|
39
|
+
if (el['@Core.MediaType'] && !el.virtual) {
|
|
40
|
+
_addColumn(el.name, el['@Core.MediaType'], columns, el['@Core.IsURL'] && el.type === 'cds.String', target)
|
|
39
41
|
}
|
|
40
42
|
}
|
|
41
43
|
}
|
|
@@ -58,7 +60,7 @@ const handleStreamProperties = (target, columns, model) => {
|
|
|
58
60
|
_addColumns(target, columns)
|
|
59
61
|
} else if (col.ref && (type === 'cds.LargeBinary' || (mediaType && !ignoreMediaType))) {
|
|
60
62
|
if (mediaType) {
|
|
61
|
-
_addColumn(name, mediaType, columns, element['@Core.IsURL'])
|
|
63
|
+
_addColumn(name, mediaType, columns, element['@Core.IsURL'], target)
|
|
62
64
|
columns.splice(index, 1)
|
|
63
65
|
} else if (!cds.env.features.stream_compat) {
|
|
64
66
|
columns.splice(index, 1)
|
|
@@ -69,7 +69,9 @@ const parseStream = async function* (body, boundary) {
|
|
|
69
69
|
const process = chunk => {
|
|
70
70
|
let changed = chunk
|
|
71
71
|
.toString()
|
|
72
|
-
.replace(
|
|
72
|
+
.replace(/^--(.*)$/gm, (_, g) => `HEAD /${g} HTTP/1.1${g.slice(-2) === '--' ? CRLF : ''}`)
|
|
73
|
+
// correct content-length for non-HEAD requests is inserted below
|
|
74
|
+
.replace(/content-length: \d+\r\n/gim, '')
|
|
73
75
|
.replace(/ \$/g, ' /$')
|
|
74
76
|
|
|
75
77
|
// HACKS!!!
|