@sap/cds 8.9.3 → 8.9.5

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,24 @@
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.5 - 2025-07-25
8
+
9
+ ### Fixed
10
+
11
+ - `req.diff` in case of draft entities using associations to joins/unions
12
+ - Locale detection does not enforce `<http-req>.query` to be present. Some protocol adapters do not set it.
13
+ - View metadata for requests with $apply
14
+ - Handling of bad timestamps in URL ($filter and temporals)
15
+
16
+ ## Version 8.9.4 - 2025-05-16
17
+
18
+ ### Fixed
19
+
20
+ - No longer require `@sap/cds-compiler` versions 6.x as these are not supported with CAP Java 3.
21
+ - Regression in view resolving with mixins
22
+ - View resolving for external service entities aborted too early
23
+ - `cds.Map` validation in action/function parameters
24
+
7
25
  ## Version 8.9.3 - 2025-05-06
8
26
 
9
27
  ### Fixed
@@ -147,6 +147,7 @@ class type extends any { is(kind) { return kind === 'type' || super.is(kind) }
147
147
  }
148
148
 
149
149
  class Map extends struct {
150
+ get '@open' () { return this.set('@open', true) }
150
151
  get elements() { return this.set('elements', new LinkedDefinitions) }
151
152
  }
152
153
 
@@ -47,6 +47,7 @@ class LinkedCSN {
47
47
  if (p.actions && !d.actions) _set (d,'actions',undefined) //> don't propagate .actions
48
48
  if (p.params && !d.params) _set (d,'params',undefined) //> don't propagate .params
49
49
  if (d.elements?.localized) _set (d,'texts', defs[d.elements.localized.target])
50
+ if (!Object.hasOwn(d,'_service')) _set (d,'_service',undefined) //> don't propagate ._service
50
51
  } else if (d.kind === 'element') {
51
52
  if (p.key && !d.key) _set (d,'key',undefined) //> don't propagate .key
52
53
  }
package/lib/ql/resolve.js CHANGED
@@ -8,6 +8,7 @@ const _isPersistenceTable = target =>
8
8
  // REVISIT revert after cds-dbs pr
9
9
  // REVISIT: Remove once we get rid of old db
10
10
  const _abortDB = resolve.abortDB = target => !!(_isPersistenceTable(target)|| !target.query?._target)
11
+ // _service seems to be inherited in projections, so do not consider prototype chain
11
12
  const _defaultAbort = tx => e => e._service?.name === tx.definition?.name
12
13
 
13
14
  function resolve(query, tx, abortCondition) {
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, '')
@@ -32,7 +32,7 @@ const _inverseTransition = transition => {
32
32
  const ref0 = value.ref[0]
33
33
  if (value.ref.length > 1) {
34
34
  // ignore flattened columns like author.name
35
- if (transition.target.elements[ref0].isAssociation) continue
35
+ if (transition.target.elements[ref0]?.isAssociation) continue
36
36
 
37
37
  const nested = inverseTransition.mapping.get(ref0) || {}
38
38
  if (!nested.transition) nested.transition = { mapping: new Map() }
@@ -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) {
@@ -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.3",
3
+ "version": "8.9.5",
4
4
  "description": "SAP Cloud Application Programming Model - CDS for Node.js",
5
5
  "homepage": "https://cap.cloud.sap/",
6
6
  "keywords": [
@@ -33,7 +33,7 @@
33
33
  "node": ">=18"
34
34
  },
35
35
  "dependencies": {
36
- "@sap/cds-compiler": ">=5.1",
36
+ "@sap/cds-compiler": "^5",
37
37
  "@sap/cds-fiori": "^1",
38
38
  "@sap/cds-foss": "^5.0.0"
39
39
  },