@sap/cds 8.9.4 → 8.9.6
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 +15 -0
- package/lib/req/locale.js +2 -1
- package/lib/req/validate.js +1 -1
- package/libx/_runtime/common/generic/temporal.js +0 -1
- package/libx/_runtime/common/utils/normalizeTimestamp.js +10 -1
- package/libx/_runtime/common/utils/resolveView.js +1 -1
- package/libx/_runtime/fiori/lean-draft.js +4 -2
- package/libx/odata/parse/afterburner.js +1 -2
- package/libx/odata/utils/metadata.js +10 -7
- package/libx/odata/utils/normalizeTimeData.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,21 @@
|
|
|
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.6 - 2025-07-29
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Batch insert using `INSERT.entries()` on draft enabled entities
|
|
12
|
+
|
|
13
|
+
## Version 8.9.5 - 2025-07-25
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- `req.diff` in case of draft entities using associations to joins/unions
|
|
18
|
+
- Locale detection does not enforce `<http-req>.query` to be present. Some protocol adapters do not set it.
|
|
19
|
+
- View metadata for requests with $apply
|
|
20
|
+
- Handling of bad timestamps in URL ($filter and temporals)
|
|
21
|
+
|
|
7
22
|
## Version 8.9.4 - 2025-05-16
|
|
8
23
|
|
|
9
24
|
### Fixed
|
package/lib/req/locale.js
CHANGED
|
@@ -10,7 +10,8 @@ function normalized_locale_from (req) {
|
|
|
10
10
|
|
|
11
11
|
function original_locale_from (req) {
|
|
12
12
|
return !req ? undefined :
|
|
13
|
-
req.query
|
|
13
|
+
// req.query is guaranteed by express, but not by others like websockets or plain Node http
|
|
14
|
+
req.query?.['sap-locale'] || SAP_LANGUAGES[req.query?.['sap-language']] ||
|
|
14
15
|
req.headers['x-sap-request-language'] ||
|
|
15
16
|
req.headers['accept-language']
|
|
16
17
|
}
|
package/lib/req/validate.js
CHANGED
|
@@ -208,7 +208,7 @@ class struct extends $any {
|
|
|
208
208
|
}
|
|
209
209
|
// check values of given data
|
|
210
210
|
for (let each in data) { // will work for structured payloads as well as flattened ones with universal CSN
|
|
211
|
-
let /** @type {$any} */ d = elements[each]
|
|
211
|
+
let /** @type {$any} */ d = Object.hasOwn(elements, each) && elements[each]
|
|
212
212
|
if (!d || (d['@cds.api.ignore'] && ctx.rejectIgnore)) ctx.unknown (each, this, data)
|
|
213
213
|
else if (ctx.cleanse && d._is_readonly() && !d.key) delete data[each]
|
|
214
214
|
// @Core.Immutable processed only for root, children are handled when knowing db state
|
|
@@ -4,7 +4,6 @@ const normalizeTimestamp = require('../utils/normalizeTimestamp')
|
|
|
4
4
|
const _getDateFromQueryOptions = str => {
|
|
5
5
|
if (str) {
|
|
6
6
|
const match = str.match(/^date'(.+)'$/)
|
|
7
|
-
// REVISIT: What happens with invalid date values in query parameter? if match.length > 1
|
|
8
7
|
return normalizeTimestamp(match ? match[1] : str)
|
|
9
8
|
}
|
|
10
9
|
}
|
|
@@ -9,13 +9,22 @@ const _lengthIfNotFoundIndex = (index, length) => (index > -1 ? index : length)
|
|
|
9
9
|
module.exports = value => {
|
|
10
10
|
if (value instanceof Date) value = value.toISOString()
|
|
11
11
|
if (typeof value === 'number') value = new Date(value).toISOString()
|
|
12
|
+
if (typeof value !== 'string') {
|
|
13
|
+
const msg = `Value "${value}" is not a valid Timestamp`
|
|
14
|
+
throw Object.assign(new Error(msg), { statusCode: 400 })
|
|
15
|
+
}
|
|
12
16
|
|
|
13
17
|
const decimalPointIndex = _lengthIfNotFoundIndex(value.lastIndexOf('.'), value.length)
|
|
14
18
|
const tzRegexMatch = TZ_REGEX.exec(value)
|
|
15
19
|
const tz = tzRegexMatch?.[0] || ''
|
|
16
20
|
const tzIndex = _lengthIfNotFoundIndex(tzRegexMatch?.index, value.length)
|
|
17
21
|
const dateEndIndex = Math.min(decimalPointIndex, tzIndex)
|
|
18
|
-
|
|
22
|
+
let dt = new Date(value.slice(0, dateEndIndex) + tz)
|
|
23
|
+
if (isNaN(dt)) {
|
|
24
|
+
const msg = `Value "${value}" is not a valid Timestamp`
|
|
25
|
+
throw Object.assign(new Error(msg), { statusCode: 400 })
|
|
26
|
+
}
|
|
27
|
+
const dateNoMillisNoTZ = dt.toISOString().slice(0, 19)
|
|
19
28
|
const normalizedFractionalDigits = value
|
|
20
29
|
.slice(dateEndIndex + 1, tzIndex)
|
|
21
30
|
.replace(NON_DIGIT_REGEX, '')
|
|
@@ -551,7 +551,7 @@ const _mappedValue = (col, alias) => {
|
|
|
551
551
|
const getDBTable = target => cds.ql.resolve.table(target)
|
|
552
552
|
|
|
553
553
|
const _appendForeignKeys = (newColumns, target, columns, { as, ref = [] }) => {
|
|
554
|
-
const el = target.elements[as] || target.query._target
|
|
554
|
+
const el = target.elements[as] || target.query._target?.elements[ref.at(-1)]
|
|
555
555
|
|
|
556
556
|
if (el && el.isAssociation && el.keys) {
|
|
557
557
|
for (const key of el.keys) {
|
|
@@ -464,7 +464,7 @@ cds.ApplicationService.prototype.handle = async function (req) {
|
|
|
464
464
|
if (!containsDraftRoot) req.reject({ code: 403, statusCode: 403, message: 'DRAFT_MODIFICATION_ONLY_VIA_ROOT' })
|
|
465
465
|
|
|
466
466
|
const isDirectAccess = typeof req.query.INSERT.into === 'string' || req.query.INSERT.into.ref?.length === 1
|
|
467
|
-
const data = Object.assign({}, req.data) // IsActiveEntity is not enumerable
|
|
467
|
+
const data = Array.isArray(req.data) ? [...req.data] : Object.assign({}, req.data) // IsActiveEntity is not enumerable
|
|
468
468
|
const draftsRootRef =
|
|
469
469
|
typeof query.INSERT.into === 'string'
|
|
470
470
|
? [req.target.drafts.name]
|
|
@@ -489,7 +489,9 @@ cds.ApplicationService.prototype.handle = async function (req) {
|
|
|
489
489
|
|
|
490
490
|
const cqn = INSERT.into(query.INSERT.into).entries(data)
|
|
491
491
|
await run(cqn, { event: 'CREATE' })
|
|
492
|
-
const result =
|
|
492
|
+
const result = Array.isArray(data)
|
|
493
|
+
? data.map(d => ({ ...d, IsActiveEntity: true }))
|
|
494
|
+
: { ...data, IsActiveEntity: true }
|
|
493
495
|
req.data = result //> make keys available via req.data (as with normal crud)
|
|
494
496
|
return result
|
|
495
497
|
}
|
|
@@ -12,8 +12,7 @@ function _getDefinition(definition, name, namespace) {
|
|
|
12
12
|
return (
|
|
13
13
|
definition?.definitions?.[name] ||
|
|
14
14
|
definition?.elements?.[name] ||
|
|
15
|
-
(definition.actions && (definition.actions[name] || definition.actions[name.replace(namespace + '.', '')]))
|
|
16
|
-
definition[name]
|
|
15
|
+
(definition.actions && (definition.actions[name] || definition.actions[name.replace(namespace + '.', '')]))
|
|
17
16
|
)
|
|
18
17
|
}
|
|
19
18
|
|
|
@@ -32,6 +32,14 @@ const _lastValidRef = ref => {
|
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
const _getRef = query => {
|
|
36
|
+
if (query.SELECT?.from?.SELECT) return _getRef(query.SELECT.from)
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
query.SELECT?.from?.ref ?? query.UPDATE?.entity?.ref ?? query.INSERT?.into?.ref ?? query.DELETE?.from?.ref ?? []
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
|
|
35
43
|
const _toBinaryKeyValue = value => `binary'${value.toString('base64')}'`
|
|
36
44
|
|
|
37
45
|
const _odataContext = (query, options) => {
|
|
@@ -49,10 +57,7 @@ const _odataContext = (query, options) => {
|
|
|
49
57
|
|
|
50
58
|
path += '#'
|
|
51
59
|
|
|
52
|
-
|
|
53
|
-
const ref =
|
|
54
|
-
query.SELECT?.from?.ref ?? query.UPDATE?.entity?.ref ?? query.INSERT?.into?.ref ?? query.DELETE?.from?.ref ?? []
|
|
55
|
-
|
|
60
|
+
const ref = _getRef(query)
|
|
56
61
|
const isNavToDraftAdmin = _isNavToDraftAdmin(ref)
|
|
57
62
|
|
|
58
63
|
let edmName
|
|
@@ -73,9 +78,6 @@ const _odataContext = (query, options) => {
|
|
|
73
78
|
path += edmName
|
|
74
79
|
|
|
75
80
|
const isViewWithParams = query._target.kind === 'entity' && query._target.params
|
|
76
|
-
if (isViewWithParams) path += 'Type'
|
|
77
|
-
|
|
78
|
-
const lastRef = ref.at(-1)
|
|
79
81
|
|
|
80
82
|
if (propertyAccess || isNavToDraftAdmin || isViewWithParams) {
|
|
81
83
|
if (!contextAbsoluteUrl && (propertyAccess || isViewWithParams)) path = '../' + path
|
|
@@ -83,6 +85,7 @@ const _odataContext = (query, options) => {
|
|
|
83
85
|
const keyValuePairs = []
|
|
84
86
|
|
|
85
87
|
const lastValidRef = _lastValidRef(ref)
|
|
88
|
+
const lastRef = ref.at(-1)
|
|
86
89
|
const isSibling = lastRef === 'SiblingEntity'
|
|
87
90
|
let _keyValuePairs
|
|
88
91
|
if (lastValidRef.where) {
|
|
@@ -8,7 +8,7 @@ const _processorFn = elementInfo => {
|
|
|
8
8
|
const { row, key } = elementInfo
|
|
9
9
|
if (!(row[key] == null) && row[key] !== '$now') {
|
|
10
10
|
const dt = typeof row[key] === 'string' && new Date(row[key])
|
|
11
|
-
if (!isNaN(dt)) {
|
|
11
|
+
if (dt && !isNaN(dt)) {
|
|
12
12
|
switch (category) {
|
|
13
13
|
case 'cds.DateTime':
|
|
14
14
|
row[key] = new Date(row[key]).toISOString().replace(/\.\d\d\d/, '')
|