@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 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['sap-locale'] || SAP_LANGUAGES[req.query['sap-language']] ||
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
  }
@@ -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
- const dateNoMillisNoTZ = new Date(value.slice(0, dateEndIndex) + tz).toISOString().slice(0, 19)
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.elements[ref.at(-1)]
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 = { ...data, IsActiveEntity: true }
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
- // REVISIT: subselect is treated as empty array
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/, '')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sap/cds",
3
- "version": "8.9.4",
3
+ "version": "8.9.6",
4
4
  "description": "SAP Cloud Application Programming Model - CDS for Node.js",
5
5
  "homepage": "https://cap.cloud.sap/",
6
6
  "keywords": [