@sap/cds 9.1.0 → 9.2.0

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.
Files changed (63) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/bin/deploy.js +29 -0
  3. package/bin/serve.js +1 -5
  4. package/lib/compile/etc/csv.js +11 -6
  5. package/lib/compile/load.js +8 -5
  6. package/lib/compile/to/hdbtabledata.js +1 -1
  7. package/lib/dbs/cds-deploy.js +0 -31
  8. package/lib/env/cds-env.js +2 -1
  9. package/lib/env/cds-requires.js +3 -0
  10. package/lib/env/schemas/cds-rc.js +4 -0
  11. package/lib/index.js +38 -38
  12. package/lib/log/cds-error.js +12 -11
  13. package/lib/log/format/json.js +1 -1
  14. package/lib/ql/SELECT.js +31 -0
  15. package/lib/ql/resolve.js +1 -1
  16. package/lib/req/context.js +1 -1
  17. package/lib/req/validate.js +16 -17
  18. package/lib/srv/cds.Service.js +18 -28
  19. package/lib/srv/middlewares/auth/ias-auth.js +29 -2
  20. package/lib/srv/middlewares/auth/jwt-auth.js +11 -1
  21. package/lib/srv/srv-models.js +1 -1
  22. package/lib/srv/srv-tx.js +2 -2
  23. package/lib/utils/cds-utils.js +35 -2
  24. package/lib/utils/csv-reader.js +1 -1
  25. package/lib/utils/version.js +18 -0
  26. package/libx/_runtime/cds.js +1 -1
  27. package/libx/_runtime/common/aspects/any.js +1 -23
  28. package/libx/_runtime/common/generic/input.js +111 -50
  29. package/libx/_runtime/common/generic/sorting.js +1 -1
  30. package/libx/_runtime/common/utils/draft.js +1 -1
  31. package/libx/_runtime/common/utils/entityFromCqn.js +1 -1
  32. package/libx/_runtime/common/utils/propagateForeignKeys.js +1 -1
  33. package/libx/_runtime/common/utils/resolveView.js +2 -2
  34. package/libx/_runtime/common/utils/structured.js +2 -2
  35. package/libx/_runtime/common/utils/templateProcessor.js +0 -5
  36. package/libx/_runtime/common/utils/vcap.js +1 -1
  37. package/libx/_runtime/fiori/lean-draft.js +63 -23
  38. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +3 -2
  39. package/libx/_runtime/messaging/service.js +1 -1
  40. package/libx/_runtime/remote/utils/client.js +1 -1
  41. package/libx/common/assert/utils.js +2 -12
  42. package/libx/common/utils/streaming.js +4 -9
  43. package/libx/http/location.js +1 -0
  44. package/libx/odata/index.js +1 -1
  45. package/libx/odata/middleware/batch.js +6 -1
  46. package/libx/odata/middleware/create.js +1 -1
  47. package/libx/odata/middleware/error.js +22 -19
  48. package/libx/odata/middleware/stream.js +1 -1
  49. package/libx/odata/parse/cqn2odata.js +16 -10
  50. package/libx/odata/parse/grammar.peggy +4 -2
  51. package/libx/odata/parse/parser.js +1 -1
  52. package/libx/odata/utils/index.js +1 -1
  53. package/libx/queue/index.js +2 -2
  54. package/libx/rest/RestAdapter.js +1 -2
  55. package/libx/rest/middleware/create.js +5 -2
  56. package/package.json +2 -2
  57. package/server.js +1 -1
  58. package/bin/deploy/to-hana.js +0 -1
  59. package/lib/utils/check-version.js +0 -9
  60. package/lib/utils/unit.js +0 -19
  61. package/libx/_runtime/cds-services/util/assert.js +0 -181
  62. package/libx/_runtime/types/api.js +0 -129
  63. package/libx/common/assert/validation.js +0 -109
package/CHANGELOG.md CHANGED
@@ -4,6 +4,39 @@
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.2.0 - 2025-07-29
8
+
9
+ ### Added
10
+
11
+ - `srv.schedule` allows to specify the time in a more readable way, e.g. `srv.schedule(...).after('1min')`
12
+ - Support for `jwt`/`xsuaa`-auth on XSA
13
+ - Enable `@sap/xssec`'s caching mechanisms (requires `@sap/xssec^4.8`)
14
+ + The signature cache can be configured via `cds.requires.auth.config`, which is passed to `@sap/xssec`'s authentication services
15
+ + The token decode cache can be configured programmatically via `require('@sap/xssec').Token.enableDecodeCache(config?)` and deactivated via `require('@sap/xssec').Token.decodeCache = false`
16
+ - `cds.requires` correctly resolve service credentials on Kyma when its merged env configuration is only `true` and the service is found via its property name.
17
+ - `ias`-auth: Support for fallback XSUAA-based authentication meant to ease migration to IAS
18
+ + The fallback is automatically enabled if XSUAA credentials are available. To enable the credentials look-up, simply add `cds.requires.xsuaa = true` to your env.
19
+ + In case you need a custom config for the fallback (passed through to `@sap/xssec` as is!), configure it via `cds.requires.xsuaa = { config: { ... } }`
20
+ - Better error message if `cds.xt.Extensions` table is missing in extensibility scenarios.
21
+
22
+ ### Changed
23
+
24
+ - Upgrade to Peggy 5 version
25
+ - Enabled conversion of `not exists where not` to OData `all`, integrating the inverse of the policy applied by the OData parser.
26
+ - Numeric values in `.csv` files are now returned as numbers instead of strings, e.g. `1` instead of `'1'`;
27
+ when pre-padded with zeros, e.g., `0123`, they are returned as strings, e.g. `'0123'` instead of `123`.
28
+
29
+ ### Fixed
30
+
31
+ - Runtime error in transaction handling in messaging services when used with outbox
32
+ - Always use `cds.context` middleware for `enterprise-messaging` endpoints
33
+ - Crash during Location header generation caused by custom response not matching the entity definition.
34
+ - Support for logging of correct error locations with `cds watch` and `cds run`.
35
+ - Double-unescaping of values in double quotes during OData URL parsing
36
+ - Throw explicit error if the result of a media data query is not an instance of `Readable`, rather than responding with `No Content`
37
+ - When loading `.csv` files quoted strings containing the separator (comma or semicolon) where erroneously
38
+ parsed as two separate values instead of one.
39
+
7
40
  ## Version 9.1.0 - 2025-06-30
8
41
 
9
42
  ### Added
@@ -63,6 +96,7 @@
63
96
  ### Changed
64
97
 
65
98
  - Lean draft handler is registered in a service only if a draft-enabled service entity exists
99
+ - Add back in server version on CAP server launch info log record
66
100
 
67
101
  ### Fixed
68
102
 
@@ -160,6 +194,15 @@
160
194
  - Deprecated stripping of unnecessary topic prefix `topic:` in messaging
161
195
  - Deprecated messaging `Outbox` class. Please use config or `cds.outboxed(srv)` to outbox your service.
162
196
 
197
+ ## Version 8.9.5 - 2025-07-25
198
+
199
+ ### Fixed
200
+
201
+ - `req.diff` in case of draft entities using associations to joins/unions
202
+ - Locale detection does not enforce `<http-req>.query` to be present. Some protocol adapters do not set it.
203
+ - View metadata for requests with $apply
204
+ - Handling of bad timestamps in URL ($filter and temporals)
205
+
163
206
  ## Version 8.9.4 - 2025-05-16
164
207
 
165
208
  ### Fixed
package/bin/deploy.js ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env node
2
+ /* eslint-disable no-console */
3
+
4
+ const cds = require('../lib');
5
+ const argv = process.argv.slice(2);
6
+ const o = {};
7
+ cds.cli = { command: 'deploy', argv, options: o }
8
+
9
+ Promise.resolve(cds.plugins).then(async () => {
10
+ let db = cds.requires.db || {}
11
+ for (let i = 0; i < argv.length; ++i) {
12
+ let k = argv[i], v = argv[++i]
13
+ if (!k.startsWith('--')) return error `Invalid argument: ${k}. Expected format --key value`
14
+ if (v?.startsWith('--')) { --i; v = true } // allow --key without value
15
+ o[k = k.slice(2)] = v ?? true // allow last --key without value
16
+ if (k === 'to') db = { kind: v, dialect: v }
17
+ else (db.credentials ??= {})[k] = v
18
+ }
19
+ console.debug('Deploying with options:', db, o)
20
+ cds.db = await cds.connect.to(db)
21
+ return await cds.deploy('*',o).to(db)
22
+ })
23
+ .catch (e => { console.error(e); process.exitCode = 1 })
24
+ .finally (() => cds.db?.disconnect?.())
25
+
26
+ const error = (...args) => {
27
+ console.error (String.raw(...args))
28
+ process.exitCode = 1
29
+ }
package/bin/serve.js CHANGED
@@ -275,10 +275,6 @@ async function _local_server_js() {
275
275
  function _prepare_logging () { // NOSONAR
276
276
 
277
277
  const LOG = cds.log('cds.serve|server',{label:'cds'}); if (!LOG._info) return; else log = LOG.info
278
- const _timer = process.env.NODE_ENV === 'production'
279
- ? `[cds] - server launched at ${new Date().toLocaleString()}, version: ${cds.version}, in`
280
- : '[cds] - server launched in'
281
- console.time (_timer)
282
278
 
283
279
  // print information when model is loaded
284
280
  cds.on ('loaded', ({$sources:srcs})=>{
@@ -313,7 +309,7 @@ function _prepare_logging () { // NOSONAR
313
309
  cds.once ('listening', ({url})=>{
314
310
  console.log()
315
311
  LOG.info ('server listening on',{url})
316
- _timer && console.timeEnd (_timer)
312
+ LOG.info ('server', 'v'+cds.version, 'launched in', performance.now().toFixed(0),'ms')
317
313
  if (process.stdin.isTTY) LOG.info (`[ terminate with ^C ]\n`)
318
314
  })
319
315
  }
@@ -34,6 +34,10 @@ function parse (csv) {
34
34
  if (headers.includes(currCol)) values.push (_value4(val, quoted)) // skip value if column was skipped
35
35
  val = undefined, quoted = false //> start new val
36
36
  }
37
+ else if (c === ' ' && val === undefined) {
38
+ // ignore leading spaces
39
+ continue
40
+ }
37
41
  else if (c === '"' && val === undefined) { // start quoted string
38
42
  val = ''
39
43
  inString = true
@@ -43,7 +47,7 @@ function parse (csv) {
43
47
  else inString = false, quoted = true // stop string
44
48
  }
45
49
  else { // normal char
46
- if (val === undefined) val = ''
50
+ val ??= ''
47
51
  val += c
48
52
  }
49
53
  }
@@ -59,12 +63,13 @@ function parse (csv) {
59
63
  return rows
60
64
  }
61
65
 
62
- function _value4 (val, quoted = false) {
66
+ const globals = { null:null, true:true, false:false }
67
+ function _value4 (val, quoted) {
63
68
  if (quoted) return val
64
- if (val) val = val.trim()
65
- if (val === 'true') return true
66
- else if (val === 'false') return false
67
- else return val
69
+ if (val) val = val.trim(); else return undefined
70
+ if (val in globals) return globals[val] //> null, true, false
71
+ let n = Number(val)
72
+ return n.toString() == val ? n : val
68
73
  }
69
74
 
70
75
  function _ignoreLine(line) {
@@ -9,13 +9,16 @@ if (TRACE) {
9
9
  module.exports = exports = function load (files, options) {
10
10
  let any = cds.resolve(files,options)
11
11
 
12
+ // REVISIT: we need to find a better way to handle this -> doing that in cds.load is by far too central
12
13
  // REVISIT: bandaid for grow as you go scenario with task queues enabled by default
13
14
  let locations
14
- const _is_outbox = p => cds.utils.path.posix.normalize(p).match(/\/cds\/srv\/outbox(\.cds)?$/)
15
- const _outbox_only = any?.length === 1 && _is_outbox(any[0]) && (!Array.isArray(files) || !files.some(_is_outbox))
16
- if (_outbox_only) {
17
- any = undefined
18
- locations = cds.resolve(files, false).filter(f => !_is_outbox(f))
15
+ if (cds.watched) {
16
+ const _is_outbox = p => cds.utils.path.posix.normalize(p).match(/\/cds\/srv\/outbox(\.cds)?$/)
17
+ const _outbox_only = any?.length === 1 && _is_outbox(any[0]) && (!Array.isArray(files) || !files.some(_is_outbox))
18
+ if (_outbox_only) {
19
+ any = undefined
20
+ locations = cds.resolve(files, false).filter(f => !_is_outbox(f))
21
+ }
19
22
  }
20
23
 
21
24
  if (!any) return Promise.reject (new cds.error ({
@@ -1,4 +1,4 @@
1
- const cds = require('../../../lib')
1
+ const cds = require('../..')
2
2
  const { getElementCdsPersistenceName, getArtifactCdsPersistenceName } = require('@sap/cds-compiler')
3
3
  const { fs, path, isdir, csv } = cds.utils
4
4
  const { readdir } = fs.promises
@@ -1,4 +1,3 @@
1
- #!/usr/bin/env node
2
1
  const cds = require('../index'), { local, path } = cds.utils
3
2
  const DEBUG = cds.debug('deploy')
4
3
  const TRACE = cds.debug('trace')
@@ -387,33 +386,3 @@ const _entity4 = (file, csn) => {
387
386
  }
388
387
  return entity.name ? entity : { name, __proto__:entity }
389
388
  }
390
-
391
- /** CLI used as via cds-deploy as deployer for PostgreSQL */
392
- if (!module.parent) (async function CLI () {
393
- cds.cli = { command: 'deploy', argv: process.argv.slice(2), options: {} }
394
- await cds.plugins // IMPORTANT: that has to go before any call to cds.env, like through cds.deploy or cds.requires below
395
- let db = cds.requires.db
396
- try {
397
- let o={}, recent
398
- for (let each of process.argv.slice(2)) {
399
- if (each.startsWith('--')) o[(recent = each.slice(2))] = true
400
- else o[recent] = each
401
- }
402
- if (o.to) {
403
- db = { kind: o.to, dialect: o.to }
404
- if (o.url) (db.credentials ??= {}).url = o.url
405
- if (o.host) (db.credentials ??= {}).host = o.host
406
- if (o.port) (db.credentials ??= {}).port = o.port
407
- if (o.username) (db.credentials ??= {}).username = o.username
408
- if (o.password) (db.credentials ??= {}).password = o.password
409
- }
410
- cds.cli.options = o
411
- db = await cds.connect.to(db);
412
- db = await cds.deploy('*',o).to(db)
413
- } finally {
414
- await db?.disconnect?.()
415
- }
416
- })().catch((e) => {
417
- console.error(e)
418
- process.exitCode = 1
419
- })
@@ -408,7 +408,8 @@ class Config {
408
408
  const { credentials } = this._find_credentials_for_required_service(service, conf, vcaps) || {}
409
409
  if (credentials) {
410
410
  // Merge `credentials`. Needed because some app-defined things like `credentials.destination` must survive.
411
- any = conf.credentials = Object.assign ({}, conf.credentials, credentials)
411
+ if (conf === true) any = this.requires[service] = { credentials }
412
+ else any = conf.credentials = { ...conf.credentials, ...credentials }
412
413
  }
413
414
  }
414
415
  return !!any
@@ -85,6 +85,9 @@ for (let each of Object.values(_authentication_strategies)) {
85
85
  }})
86
86
  }
87
87
 
88
+ // enable credentials lookup for xsuaa fallback
89
+ _authentication_strategies.xsuaa = { vcap: { label: 'xsuaa' } }
90
+
88
91
  const _services = {
89
92
 
90
93
  "app-service": {
@@ -219,6 +219,10 @@ module.exports = {
219
219
  },
220
220
  db: {
221
221
  oneOf: [
222
+ {
223
+ type: 'boolean',
224
+ description: 'Shortcut to enable primary database.'
225
+ },
222
226
  {
223
227
  type: 'string',
224
228
  description: 'Settings for the primary database (shortcut).',
package/lib/index.js CHANGED
@@ -1,23 +1,23 @@
1
- if (process.env.CDS_STRICT_NODE_VERSION !== 'false') require ('./utils/check-version.js')
1
+ if (process.env.CDS_STRICT_NODE_VERSION !== 'false') require('./utils/version').check()
2
2
  ! (global.__cds_loaded_from ??= new Set).add(__filename.toLowerCase()) // track from where we loaded cds, lowercase to avoid path duplicates on Windows
3
3
 
4
4
  const { AsyncLocalStorage } = require ('async_hooks')
5
5
  const context = new AsyncLocalStorage
6
6
 
7
7
  const { EventEmitter } = require('node:events')
8
- const cds = module.exports = global.cds = new class cds_facade extends EventEmitter {
8
+ const cds = exports = module.exports = global.cds = new class cds extends EventEmitter {
9
9
 
10
- /** Contexts and Transactions @type {import('./req/context')} */
10
+ /** Contexts and Transactions @type {import('./req/context.js')} */
11
11
  get context() { return context.getStore() }
12
12
  set context(x) { this._with(x) }
13
13
  _with (x,fn,...args) {
14
- const ctx = typeof x !== 'object' || x instanceof cds.EventContext ? x : x.context || cds.EventContext.for(x)
14
+ const ctx = typeof x !== 'object' || x instanceof exports.EventContext ? x : x.context || exports.EventContext.for(x)
15
15
  return fn ? context.run (ctx,fn,...args) : context.enterWith (ctx)
16
16
  }
17
- get spawn() { return super.spawn = require('./req/spawn') }
17
+ get spawn() { return super.spawn = require('./req/spawn.js') }
18
18
 
19
- /** @import {LinkedCSN} from './core/linked-csn' */
20
- /** @import {Service} from './srv/cds.Service' */
19
+ /** @import {LinkedCSN} from './core/linked-csn.js' */
20
+ /** @import {Service} from './srv/cds.Service.js' */
21
21
  /** @type LinkedCSN */ model = undefined
22
22
  /** @type Service */ db = undefined
23
23
  /** CLI args */ cli = { command:'', options:{}, argv:[] }
@@ -33,24 +33,24 @@ const cds = module.exports = global.cds = new class cds_facade extends EventEmit
33
33
 
34
34
  // Configuration & Information
35
35
  get requires() { return super.requires = this.env.requires._resolved() }
36
- get plugins() { return super.plugins = require('./plugins').activate() }
36
+ get plugins() { return super.plugins = require('./plugins.js').activate() }
37
37
  get version() { return super.version = require('../package.json').version }
38
- get env() { return super.env = require('./env/cds-env').for(this) }
38
+ get env() { return super.env = require('./env/cds-env.js').for(this) }
39
39
  get home() { return super.home = __dirname.slice(0,-4) }
40
- get schema() { return super.schema = require('./env/schemas') } // REVISIT: Better move that to cds-dk?
40
+ get schema() { return super.schema = require('./env/schemas/index.js') } // REVISIT: Better move that to cds-dk?
41
41
 
42
42
  // Loading and Compiling Models
43
- get compiler() { return super.compiler = require('./compile/cdsc') }
44
- get compile() { return super.compile = require('./compile/cds-compile') }
45
- get resolve() { return super.resolve = require('./compile/resolve') }
46
- get load() { return super.load = require('./compile/load') }
43
+ get compiler() { return super.compiler = require('./compile/cdsc.js') }
44
+ get compile() { return super.compile = require('./compile/cds-compile.js') }
45
+ get resolve() { return super.resolve = require('./compile/resolve.js') }
46
+ get load() { return super.load = require('./compile/load.js') }
47
47
  get get() { return super.get = this.load.parsed }
48
- get parse() { return super.parse = require('./compile/parse') }
49
- get minify() { return super.minify = require('./compile/minify') }
50
- get extend() { return super.extend = require('./compile/extend') }
51
- get deploy() { return super.deploy = require('./dbs/cds-deploy') }
52
- get localize() { return super.localize = require('./i18n/localize') }
53
- get i18n() { return super.i18n = require('./i18n') }
48
+ get parse() { return super.parse = require('./compile/parse.js') }
49
+ get minify() { return super.minify = require('./compile/minify.js') }
50
+ get extend() { return super.extend = require('./compile/extend.js') }
51
+ get deploy() { return super.deploy = require('./dbs/cds-deploy.js') }
52
+ get localize() { return super.localize = require('./i18n/localize.js') }
53
+ get i18n() { return super.i18n = require('./i18n/index.js') }
54
54
 
55
55
  // Model Reflection, Builtin types and classes
56
56
  get entities() { return this.db?.entities || this.model?.entities }
@@ -75,23 +75,23 @@ const cds = module.exports = global.cds = new class cds_facade extends EventEmit
75
75
  get _pending(){ return this.#pending ??= {} } #pending
76
76
  }
77
77
  get server() { return super.server = require('../server.js') }
78
- get serve() { return super.serve = require('./srv/cds-serve') }
79
- get connect() { return super.connect = require('./srv/cds-connect') }
78
+ get serve() { return super.serve = require('./srv/cds-serve.js') }
79
+ get connect() { return super.connect = require('./srv/cds-connect.js') }
80
80
  get outboxed() { return this.queued }
81
81
  get unboxed() { return this.unqueued }
82
- get queued() { return super.queued = require('../libx/queue').queued }
83
- get unqueued() { return super.unqueued = require('../libx/queue').unqueued }
84
- get middlewares() { return super.middlewares = require('./srv/middlewares') }
85
- get odata() { return super.odata = require('../libx/odata') }
86
- get auth() { return super.auth = require('./auth') }
82
+ get queued() { return super.queued = require('../libx/queue/index.js').queued }
83
+ get unqueued() { return super.unqueued = require('../libx/queue/index.js').unqueued }
84
+ get middlewares() { return super.middlewares = require('./srv/middlewares/index.js') }
85
+ get odata() { return super.odata = require('../libx/odata/index.js') }
86
+ get auth() { return super.auth = require('./auth.js') }
87
87
  shutdown() { this.app?.server && process.exit() } // is overridden in bin/serve.js
88
88
 
89
89
  // Core Services API
90
- get Service() { return super.Service = require('./srv/cds.Service') }
91
- get EventContext() { return super.EventContext = require('./req/context') }
92
- get Request() { return super.Request = require('./req/request') }
93
- get Event() { return super.Event = require('./req/event') }
94
- get User() { return super.User = require('./req/user') }
90
+ get Service() { return super.Service = require('./srv/cds.Service.js') }
91
+ get EventContext() { return super.EventContext = require('./req/context.js') }
92
+ get Request() { return super.Request = require('./req/request.js') }
93
+ get Event() { return super.Event = require('./req/event.js') }
94
+ get User() { return super.User = require('./req/user.js') }
95
95
  get validate() { return super.validate = require('./req/validate.js') }
96
96
 
97
97
  // Services, Protocols and Periphery
@@ -101,18 +101,18 @@ const cds = module.exports = global.cds = new class cds_facade extends EventEmit
101
101
  get RemoteService() { return super.RemoteService = require('../libx/_runtime/remote/Service.js') }
102
102
 
103
103
  // Helpers
104
- get utils() { return super.utils = require('./utils/cds-utils') }
105
- get error() { return super.error = require('./log/cds-error') }
106
- get exec() { return super.exec = require('../bin/serve').exec }
107
- get test() { return super.test = require('./test/cds-test') }
108
- get log() { return super.log = require('./log/cds-log') }
104
+ get utils() { return super.utils = require('./utils/cds-utils.js') }
105
+ get error() { return super.error = require('./log/cds-error.js') }
106
+ get exec() { return super.exec = require('../bin/serve.js').exec }
107
+ get test() { return super.test = require('./test/cds-test.js') }
108
+ get log() { return super.log = require('./log/cds-log.js') }
109
109
  get debug() { return super.debug = this.log.debug }
110
110
  clone(x) { return structuredClone(x) }
111
111
 
112
112
  // Querying and Databases
113
113
  get infer(){ return super.infer = require('./ql/cds.ql-infer.js') }
114
114
  get txs() { return super.txs = new this.Service('cds.tx') }
115
- get ql() { return super.ql = require('./ql/cds-ql') }
115
+ get ql() { return super.ql = require('./ql/cds-ql.js') }
116
116
  // get tx() { return super.tx = this.Service.prototype.tx.bind (new this.Service) } // Note: this would break too much of existing code/tests
117
117
  tx (..._) { return (this.db || this.txs).tx(..._) }
118
118
  run (..._) { return (this.db || typeof _[0] === 'function' && this.txs || this.error._no_primary_db).run(..._) }
@@ -21,7 +21,7 @@ const { format, inspect } = require('../utils/cds-utils')
21
21
  * @param {object} [details] - Additional error details
22
22
  * @param {Function} [caller] - The function calling this
23
23
  */
24
- const error = exports = module.exports = function cds_error ( status, message, details, caller ) {
24
+ const error = exports = module.exports = function error ( status, message, details, caller ) {
25
25
  if (typeof status !== 'number') [ status, message, details, caller ] = status.raw ? [ undefined, error.message(...arguments) ] : [ undefined, status, message, details ]
26
26
  if (typeof message === 'object') [ message, details, caller ] = [ undefined, message, details ]
27
27
  let err = details && 'stack' in details ? details : Object.assign (new Error (message, details), details)
@@ -61,16 +61,17 @@ exports.expected = ([,type], arg) => {
61
61
  return error (`Expected argument '${name}'${type}, but got: ${inspect(value)}`, undefined, error.expected)
62
62
  }
63
63
 
64
-
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)
73
-
64
+ exports.isSystemError = err => {
65
+ // all errors thrown by the peggy parser should not crash the app
66
+ if (err.name === 'SyntaxError' && err.constructor?.name === 'peg$SyntaxError') return false
67
+ return err.name in {
68
+ TypeError:1,
69
+ ReferenceError:1,
70
+ SyntaxError:1,
71
+ RangeError:1,
72
+ URIError:1,
73
+ }
74
+ }
74
75
 
75
76
  //
76
77
  // Private helpers ...
@@ -1,4 +1,4 @@
1
- const cds = require('../../')
1
+ const cds = require('../..')
2
2
 
3
3
  const util = require('util')
4
4
 
package/lib/ql/SELECT.js CHANGED
@@ -1,4 +1,6 @@
1
+ const { pipeline } = require('stream/promises')
1
2
  const Whereable = require('./Whereable')
3
+ const Query = require('./cds.ql-Query')
2
4
  const is_number = x => !isNaN(x)
3
5
  const cds = require('../index')
4
6
  const $ = Object.assign
@@ -163,6 +165,35 @@ class SELECT extends Whereable {
163
165
  return this
164
166
  }
165
167
 
168
+ async foreach(callback) {
169
+ for await (const row of this) {
170
+ callback(row)
171
+ }
172
+ }
173
+
174
+ [Symbol.asyncIterator]() {
175
+ return (async function* (self) {
176
+ const srv = self._srv || cds.db || cds.error`Can't execute query as no primary database is connected.`
177
+ const stream = await srv.send({
178
+ iterator: true,
179
+ objectMode: true,
180
+ query: self,
181
+ })
182
+ for await (const row of stream) yield row
183
+ })(this)
184
+ }
185
+
186
+ async pipeline(...args) {
187
+ const srv = this._srv || cds.db || cds.error`Can't execute query as no primary database is connected.`
188
+ const res = await srv.send({
189
+ iterator: true,
190
+ objectMode: false,
191
+ query: this,
192
+ })
193
+ if (args.length) return pipeline(res, ...args.map(a => a instanceof Query ? stream => a.entries(stream) : a))
194
+ return res
195
+ }
196
+
166
197
  hints (...args) {
167
198
  if (args.length) this.SELECT.hints = args.flat()
168
199
  return this
package/lib/ql/resolve.js CHANGED
@@ -1,5 +1,5 @@
1
1
  const { resolveView, getTransition } = require('../../libx/_runtime/common/utils/resolveView')
2
- const cds = require('../../lib')
2
+ const cds = require('..')
3
3
 
4
4
  const PERSISTENCE_TABLE = '@cds.persistence.table'
5
5
  const _isPersistenceTable = target =>
@@ -1,6 +1,6 @@
1
1
  const cds = require ('../index'), { uuid } = cds.utils
2
2
  const async_events = { succeeded:1, failed:1, done:1, commit:1 }
3
- const locale = require('./../i18n/locale')
3
+ const locale = require('../i18n/locale')
4
4
  const { EventEmitter } = require('events')
5
5
 
6
6
  /**
@@ -24,15 +24,16 @@ class Validation {
24
24
  this.cleanse = options.cleanse !== false
25
25
  }
26
26
 
27
- error (code, path, leaf, val, ...args) {
27
+ error (code, path, leaf, i18n, ...args) {
28
28
  const err = (this.errors ??= new ValidationErrors).add (code)
29
- if (this.options.path) path = [ this.options.path, ...path ] // e.g. used to prefic 'in/' for actions
29
+ if (this.options.path) path = [ this.options.path, ...path ] // e.g. used to prefix 'in/' for actions
30
30
  if (path) err.target = (!leaf ? path : path.concat(leaf)).reduce?.((p,n)=> (
31
31
  n?.row ? p + this.filter4(n) : //> some/entity(ID=1)...
32
32
  typeof n === 'number' ? p + `[${n}]` : //> some/array[1]...
33
33
  p && n ? p+'/'+n : n //> some/element...
34
34
  ),'')
35
- if (val !== undefined) err.args = [ val, ...args ]
35
+ if (typeof i18n === 'string') err.i18n = i18n
36
+ if (args.length) err.args = args
36
37
  return err
37
38
  }
38
39
 
@@ -64,10 +65,8 @@ class ValidationErrors extends Array {
64
65
  return err
65
66
  }
66
67
  static proto = Object.create (Error.prototype, {
68
+ stack: { configurable:true, get() { return this.message } },
67
69
  message: { writable:true, configurable:true },
68
- stack: { configurable:true, get() { return this.message },
69
- set(v) { Object.defineProperty (this, 'stack', { value:v, writable:true, configurable:true }) },
70
- },
71
70
  status: { value: 400 },
72
71
  })
73
72
  }
@@ -100,30 +99,30 @@ const $any = class any {
100
99
  const asserts = []
101
100
  const type_check = conf.strict && this.strict_check || this.type_check
102
101
  if (type_check) {
103
- asserts.push ((v,p,ctx) => v == null || type_check(v) || ctx.error ('ASSERT_DATA_TYPE', p, this.name, v, this ))
102
+ asserts.push ((v,p,ctx) => v == null || type_check(v) || ctx.error ('ASSERT_DATA_TYPE', p, this.name, null, v, this ))
104
103
  }
105
104
  if (this._is_mandatory()) {
106
- 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
105
+ asserts.push ((v,p,ctx) => v != null && v.trim?.() !== '' || ctx.error ('ASSERT_NOT_NULL', p, this.name, this['@mandatory.message'] || this['@mandatory'], v)) // ASSERT_NOT_NULL is misleading -> should be ASSERT_REQUIRED
107
106
  }
108
107
  if (this['@assert.format']) {
109
108
  const format = new RegExp(this['@assert.format'],'u')
110
- asserts.push ((v,p,ctx) => v == null || format.test(v) || ctx.error ('ASSERT_FORMAT', p, this.name, v, format))
109
+ asserts.push ((v,p,ctx) => v == null || format.test(v) || ctx.error ('ASSERT_FORMAT', p, this.name, this['@assert.format.message'], v, format))
111
110
  }
112
111
  if (this['@assert.range'] && !this.enum) {
113
112
  const [ min, max ] = this['@assert.range']
114
113
  if (min['='] === '_') min.val = -Infinity
115
114
  if (max['='] === '_') max.val = +Infinity
116
115
  asserts.push (
117
- min.val !== undefined && max.val !== undefined ? (v,p,ctx) => v == null || min.val < v && v < max.val || ctx.error ('ASSERT_RANGE', p, this.name, v, '>'+min.val, '<'+max.val) :
118
- min.val !== undefined ? (v,p,ctx) => v == null || min.val < v && v <= max || ctx.error ('ASSERT_RANGE', p, this.name, v, '>'+min.val, max) :
119
- max.val !== undefined ? (v,p,ctx) => v == null || min <= v && v < max.val || ctx.error ('ASSERT_RANGE', p, this.name, v, min, '<'+max.val) :
120
- (v,p,ctx) => v == null || min <= v && v <= max || ctx.error ('ASSERT_RANGE', p, this.name, v, min, max)
116
+ min.val !== undefined && max.val !== undefined ? (v,p,ctx) => v == null || min.val < v && v < max.val || ctx.error ('ASSERT_RANGE', p, this.name, this['@assert.range.message'], v, '>'+min.val, '<'+max.val) :
117
+ min.val !== undefined ? (v,p,ctx) => v == null || min.val < v && v <= max || ctx.error ('ASSERT_RANGE', p, this.name, this['@assert.range.message'], v, '>'+min.val, max) :
118
+ max.val !== undefined ? (v,p,ctx) => v == null || min <= v && v < max.val || ctx.error ('ASSERT_RANGE', p, this.name, this['@assert.range.message'], v, min, '<'+max.val) :
119
+ (v,p,ctx) => v == null || min <= v && v <= max || ctx.error ('ASSERT_RANGE', p, this.name, null, v, min, max)
121
120
  )
122
121
  }
123
122
  if (this['@assert.enum'] || this['@assert.range'] && this.enum) {
124
123
  const vals = Object.entries(this.enum).map(([k,v]) => 'val' in v ? v.val : k)
125
124
  const enums = vals.reduce((a,v) => (a[v]=true, a),{})
126
- 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(', ')))
125
+ asserts.push ((v,p,ctx) => v == null || v in enums || vals.some(x => x == v) || ctx.error ('ASSERT_ENUM', p, this.name, this['@assert.enum.message'], typeof v === 'string' ? `"${v}"` : v, vals.join(', ')))
127
126
  }
128
127
  if (!asserts.length) return ()=>{} // nothing to do
129
128
  return (v,p,ctx) => asserts.forEach (a => a(v,p,ctx))
@@ -194,19 +193,19 @@ class struct extends $any {
194
193
  validate (data, path, /** @type {Validation} */ ctx, elements = this.elements, skip={}) {
195
194
  if (data == null) return
196
195
  const path_ = !path ? [] : [...path, this.name]; if (path?.row) path_.push({...path})
197
- if (typeof data !== 'object') return ctx.error ('ASSERT_DATA_TYPE', path_, this.name, data, this.target)
196
+ if (typeof data !== 'object') return ctx.error ('ASSERT_DATA_TYPE', path_, this.name, null, data, this.target)
198
197
  // check for required elements in case of inserts -- note: null values are handled in the payload loop below
199
198
  if (ctx.insert || data && path_.length && this._is_insert(data)) for (let each of this._required (elements)) {
200
199
  if (each.name in data) continue // got value for required element
201
200
  if (each.name in skip) continue // skip uplinks in deep inserts -> see Composition.validate()
202
201
  if (each.$struct in data) continue // got struct for flattened element/fk, e.g. {author:{ID:1}}
203
- if (each.elements || each.foreignKeys) continue // skip struct-likes as we check flat payloads above, and deep payloads via struct.validate()
202
+ if ((each.elements && each.kind !== 'param' ) || each.foreignKeys) continue // skip struct-likes as we check flat payloads above, and deep payloads via struct.validate(), parameters don't have flat elements
204
203
  if (each.isAssociation) continue // unmanaged associations are always ignored (no value like)
205
204
  else ctx.error ('ASSERT_NOT_NULL', path_, each.name) // ASSERT_NOT_NULL should be ASSERT_REQUIRED
206
205
  }
207
206
  // check values of given data
208
207
  for (let each in data) { // will work for structured payloads as well as flattened ones with universal CSN
209
- let /** @type {$any} */ d = elements[each]
208
+ let /** @type {$any} */ d = Object.hasOwn(elements, each) && elements[each]
210
209
  if (!d || (d['@cds.api.ignore'] && ctx.rejectIgnore)) ctx.unknown (each, this, data)
211
210
  else if (ctx.cleanse && d._is_readonly() && !d.key) delete data[each]
212
211
  // @Core.Immutable processed only for root, children are handled when knowing db state