@sap/cds 9.0.2 → 9.0.4

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,25 @@
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 9.0.4 - 2025-06-18
8
+
9
+ ### Fixed
10
+
11
+ - In some cases, the app crashed if an element was named like a reserved CSN key.
12
+ - Locale detection does not enforce `<http-req>.query` to be present. Some protocol adapters do not set it.
13
+ - `between` operator for remote OData requests
14
+ - `cds serve/watch` and `cds.test()` no longer try to connect to an SQLite database if none is configured.
15
+ - `cds.connect.to` for required queueable services if no persistence is configured
16
+ - Persistent queue is not enabled if no persistence is configured
17
+
18
+ ## Version 9.0.3 - 2025-06-04
19
+
20
+ ### Fixed
21
+
22
+ - Handling of bad timestamps in URL ($filter and temporals)
23
+ - View metadata for requests with $apply
24
+ - Server crash for some URLs
25
+
7
26
  ## Version 9.0.2 - 2025-05-28
8
27
 
9
28
  ### Changed
package/bin/serve.js CHANGED
@@ -337,16 +337,18 @@ function _chdir_to (project) {
337
337
  }
338
338
 
339
339
 
340
- /** handles --in-memory option */
340
+ /** handles --in-memory[?] option */
341
341
  function _in_memory (o) {
342
- const {env} = cds, db = env.requires.db
343
- if (o['in-memory'] || o['in-memory?'] && !db) {
344
- env.add ({ requires: { db: {
345
- kind:'sqlite', ...env.requires.kinds.sqlite,
346
- credentials: db?.credentials?.url ? {url:':memory:'} : {database:':memory:'}
347
- }}})
348
- return true
349
- }
342
+ const {env} = cds, db = env.requires.db, sqlite = env.requires.kinds.sqlite
343
+ try {
344
+ if (o['in-memory'] || (o['in-memory?'] && !db && require.resolve(sqlite.impl, {paths: [cds.root, cds.home]}))) {
345
+ env.add ({ requires: { db: {
346
+ kind:'sqlite', ...sqlite,
347
+ credentials: db?.credentials?.url ? {url:':memory:'} : {database:':memory:'}
348
+ }}})
349
+ return true
350
+ }
351
+ } catch (e) { if (e.code !== 'MODULE_NOT_FOUND') throw e }
350
352
  if (db && db.credentials && (db.credentials.database || db.credentials.url) === ':memory:') {
351
353
  return true
352
354
  }
@@ -18,7 +18,8 @@ function normalized_locale_from (req) {
18
18
 
19
19
  function raw_locale_from (req) {
20
20
  return !req ? undefined :
21
- req.query['sap-locale'] || SAP_LANGUAGES[req.query['sap-language']] ||
21
+ // req.query is guaranteed by express, but not by others like websockets or plain Node http
22
+ req.query?.['sap-locale'] || SAP_LANGUAGES[req.query?.['sap-language']] ||
22
23
  req.headers['x-sap-request-language'] ||
23
24
  req.headers['accept-language']
24
25
  }
@@ -62,13 +62,14 @@ exports.expected = ([,type], arg) => {
62
62
  }
63
63
 
64
64
 
65
- exports.isSystemError = err => err.name in {
66
- TypeError:1,
67
- ReferenceError:1,
68
- SyntaxError:1,
69
- RangeError:1,
70
- URIError:1,
71
- }
65
+ const system_errors = [
66
+ TypeError,
67
+ ReferenceError,
68
+ SyntaxError,
69
+ RangeError,
70
+ URIError
71
+ ]
72
+ exports.isSystemError = err => system_errors.some(e => err instanceof e)
72
73
 
73
74
 
74
75
  //
@@ -44,7 +44,7 @@ connect.to = (datasource, options) => {
44
44
  }
45
45
  // construct new service instance
46
46
  let srv = await new Service (datasource,m,o); await (Service._is_service_class ? srv.init?.() : Service.init?.(srv))
47
- if (!(srv instanceof cds.DatabaseService) && _is_queued(o)) srv = cds.queued(srv)
47
+ if (!srv.isDatabaseService && _is_queued(o)) srv = cds.queued(srv)
48
48
  if (datasource && !options) {
49
49
  if (datasource === 'db') cds.db = srv
50
50
  cds.services[datasource] = srv
@@ -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, '')
@@ -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
 
@@ -167,8 +167,8 @@ function _xpr(expr, target, kind, isLambda, navPrefix = []) {
167
167
  const operator = isOrIsNotValue[1] /* 'is not' */ ? 'ne' : 'eq'
168
168
  res.push(...[operator, _format({ val: isOrIsNotValue[2] })])
169
169
  } else if (cur === 'between') {
170
- // ref gt low.val and ref lt high.val
171
- const between = [expr[i - 1], 'gt', expr[i + 1], 'and', expr[i - 1], 'lt', expr[i + 3]]
170
+ // between is inclusive, so we need to use ge and le
171
+ const between = [expr[i - 1], 'ge', expr[i + 1], 'and', expr[i - 1], 'le', expr[i + 3]]
172
172
  // cleanup previous ref
173
173
  res.pop()
174
174
  res.push(`(${_xpr(between, target, kind, isLambda, navPrefix)})`)
@@ -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
@@ -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/, '')
@@ -36,6 +36,7 @@ const _isProviderTenant = tenant =>
36
36
  cds.requires.multitenancy.t0 === tenant
37
37
 
38
38
  const _hasPersistentQueue = tenant => {
39
+ if (!cds.db) return false // no persistence configured
39
40
  if (cds.requires.multitenancy && tenant && _isProviderTenant(tenant)) return false // no persistence for provider account
40
41
  return true
41
42
  }
@@ -423,6 +424,7 @@ const _createTask = (name, msg, context, taskOpts) => {
423
424
  delete _msg.query._target
424
425
  delete _msg.query.__target
425
426
  delete _msg.query.target
427
+ delete _msg.data // `req.data` should be a getter to whatever is in `req.query`
426
428
  }
427
429
  const taskMsg = {
428
430
  ID: cds.utils.uuid(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sap/cds",
3
- "version": "9.0.2",
3
+ "version": "9.0.4",
4
4
  "description": "SAP Cloud Application Programming Model - CDS for Node.js",
5
5
  "homepage": "https://cap.cloud.sap/",
6
6
  "keywords": [