@sap/cds 9.1.0 → 9.2.1
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 +53 -0
- package/bin/deploy.js +29 -0
- package/bin/serve.js +1 -5
- package/lib/compile/etc/csv.js +11 -6
- package/lib/compile/load.js +8 -5
- package/lib/compile/to/hdbtabledata.js +1 -1
- package/lib/dbs/cds-deploy.js +0 -31
- package/lib/env/cds-env.js +2 -1
- package/lib/env/cds-requires.js +3 -0
- package/lib/env/schemas/cds-rc.js +4 -0
- package/lib/index.js +38 -38
- package/lib/log/cds-error.js +12 -11
- package/lib/log/format/json.js +1 -1
- package/lib/ql/SELECT.js +31 -0
- package/lib/ql/UPDATE.js +3 -1
- package/lib/ql/resolve.js +1 -1
- package/lib/req/context.js +1 -1
- package/lib/req/validate.js +16 -17
- package/lib/srv/cds.Service.js +18 -28
- package/lib/srv/middlewares/auth/ias-auth.js +31 -4
- package/lib/srv/middlewares/auth/jwt-auth.js +11 -1
- package/lib/srv/srv-models.js +1 -1
- package/lib/srv/srv-tx.js +2 -2
- package/lib/utils/cds-utils.js +35 -2
- package/lib/utils/csv-reader.js +1 -1
- package/lib/utils/version.js +18 -0
- package/libx/_runtime/cds.js +1 -1
- package/libx/_runtime/common/aspects/any.js +1 -23
- package/libx/_runtime/common/generic/input.js +111 -50
- package/libx/_runtime/common/generic/sorting.js +1 -1
- package/libx/_runtime/common/utils/draft.js +1 -1
- package/libx/_runtime/common/utils/entityFromCqn.js +1 -1
- package/libx/_runtime/common/utils/propagateForeignKeys.js +1 -1
- package/libx/_runtime/common/utils/resolveView.js +2 -2
- package/libx/_runtime/common/utils/rewriteAsterisks.js +2 -2
- package/libx/_runtime/common/utils/structured.js +2 -2
- package/libx/_runtime/common/utils/templateProcessor.js +0 -5
- package/libx/_runtime/common/utils/vcap.js +1 -1
- package/libx/_runtime/fiori/lean-draft.js +63 -23
- package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +3 -2
- package/libx/_runtime/messaging/file-based.js +2 -1
- package/libx/_runtime/messaging/service.js +1 -1
- package/libx/_runtime/remote/utils/client.js +1 -1
- package/libx/common/assert/utils.js +2 -12
- package/libx/common/utils/streaming.js +4 -9
- package/libx/http/location.js +1 -0
- package/libx/odata/index.js +1 -1
- package/libx/odata/middleware/batch.js +6 -1
- package/libx/odata/middleware/create.js +1 -1
- package/libx/odata/middleware/error.js +22 -19
- package/libx/odata/middleware/stream.js +1 -1
- package/libx/odata/parse/cqn2odata.js +16 -10
- package/libx/odata/parse/grammar.peggy +8 -4
- package/libx/odata/parse/parser.js +1 -1
- package/libx/odata/utils/index.js +1 -1
- package/libx/queue/index.js +3 -3
- package/libx/rest/RestAdapter.js +1 -2
- package/libx/rest/middleware/create.js +5 -2
- package/package.json +2 -2
- package/server.js +1 -1
- package/bin/deploy/to-hana.js +0 -1
- package/lib/utils/check-version.js +0 -9
- package/lib/utils/unit.js +0 -19
- package/libx/_runtime/cds-services/util/assert.js +0 -181
- package/libx/_runtime/types/api.js +0 -129
- package/libx/common/assert/validation.js +0 -109
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,49 @@
|
|
|
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.1 - 2025-08-15
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Check whether token validation was configured
|
|
12
|
+
- `UPDATE(Foo).with`foo=${'bar'}` erroneously constructed the equivalent of `UPDATE(Foo).with`foo=bar` instead of `UPDATE(Foo).with`foo='bar'`
|
|
13
|
+
- Errors in emits for file-based messaging are thrown
|
|
14
|
+
- Queue: Ensure `method`, `path`, `entity` and `params` are correctly taken over when creating tasks
|
|
15
|
+
- Reject navigations in `$expand` without parsing the navigation path
|
|
16
|
+
|
|
17
|
+
## Version 9.2.0 - 2025-07-29
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- `srv.schedule` allows to specify the time in a more readable way, e.g. `srv.schedule(...).after('1min')`
|
|
22
|
+
- Support for `jwt`/`xsuaa`-auth on XSA
|
|
23
|
+
- Enable `@sap/xssec`'s caching mechanisms (requires `@sap/xssec^4.8`)
|
|
24
|
+
+ The signature cache can be configured via `cds.requires.auth.config`, which is passed to `@sap/xssec`'s authentication services
|
|
25
|
+
+ The token decode cache can be configured programmatically via `require('@sap/xssec').Token.enableDecodeCache(config?)` and deactivated via `require('@sap/xssec').Token.decodeCache = false`
|
|
26
|
+
- `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.
|
|
27
|
+
- `ias`-auth: Support for fallback XSUAA-based authentication meant to ease migration to IAS
|
|
28
|
+
+ 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.
|
|
29
|
+
+ In case you need a custom config for the fallback (passed through to `@sap/xssec` as is!), configure it via `cds.requires.xsuaa = { config: { ... } }`
|
|
30
|
+
- Better error message if `cds.xt.Extensions` table is missing in extensibility scenarios.
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
|
|
34
|
+
- Upgrade to Peggy 5 version
|
|
35
|
+
- Enabled conversion of `not exists where not` to OData `all`, integrating the inverse of the policy applied by the OData parser.
|
|
36
|
+
- Numeric values in `.csv` files are now returned as numbers instead of strings, e.g. `1` instead of `'1'`;
|
|
37
|
+
when pre-padded with zeros, e.g., `0123`, they are returned as strings, e.g. `'0123'` instead of `123`.
|
|
38
|
+
|
|
39
|
+
### Fixed
|
|
40
|
+
|
|
41
|
+
- Runtime error in transaction handling in messaging services when used with outbox
|
|
42
|
+
- Always use `cds.context` middleware for `enterprise-messaging` endpoints
|
|
43
|
+
- Crash during Location header generation caused by custom response not matching the entity definition.
|
|
44
|
+
- Support for logging of correct error locations with `cds watch` and `cds run`.
|
|
45
|
+
- Double-unescaping of values in double quotes during OData URL parsing
|
|
46
|
+
- Throw explicit error if the result of a media data query is not an instance of `Readable`, rather than responding with `No Content`
|
|
47
|
+
- When loading `.csv` files quoted strings containing the separator (comma or semicolon) where erroneously
|
|
48
|
+
parsed as two separate values instead of one.
|
|
49
|
+
|
|
7
50
|
## Version 9.1.0 - 2025-06-30
|
|
8
51
|
|
|
9
52
|
### Added
|
|
@@ -63,6 +106,7 @@
|
|
|
63
106
|
### Changed
|
|
64
107
|
|
|
65
108
|
- Lean draft handler is registered in a service only if a draft-enabled service entity exists
|
|
109
|
+
- Add back in server version on CAP server launch info log record
|
|
66
110
|
|
|
67
111
|
### Fixed
|
|
68
112
|
|
|
@@ -160,6 +204,15 @@
|
|
|
160
204
|
- Deprecated stripping of unnecessary topic prefix `topic:` in messaging
|
|
161
205
|
- Deprecated messaging `Outbox` class. Please use config or `cds.outboxed(srv)` to outbox your service.
|
|
162
206
|
|
|
207
|
+
## Version 8.9.5 - 2025-07-25
|
|
208
|
+
|
|
209
|
+
### Fixed
|
|
210
|
+
|
|
211
|
+
- `req.diff` in case of draft entities using associations to joins/unions
|
|
212
|
+
- Locale detection does not enforce `<http-req>.query` to be present. Some protocol adapters do not set it.
|
|
213
|
+
- View metadata for requests with $apply
|
|
214
|
+
- Handling of bad timestamps in URL ($filter and temporals)
|
|
215
|
+
|
|
163
216
|
## Version 8.9.4 - 2025-05-16
|
|
164
217
|
|
|
165
218
|
### 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
|
-
|
|
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
|
}
|
package/lib/compile/etc/csv.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
66
|
+
const globals = { null:null, true:true, false:false }
|
|
67
|
+
function _value4 (val, quoted) {
|
|
63
68
|
if (quoted) return val
|
|
64
|
-
if (val)
|
|
65
|
-
if (val
|
|
66
|
-
|
|
67
|
-
|
|
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) {
|
package/lib/compile/load.js
CHANGED
|
@@ -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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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 ({
|
package/lib/dbs/cds-deploy.js
CHANGED
|
@@ -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
|
-
})
|
package/lib/env/cds-env.js
CHANGED
|
@@ -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 =
|
|
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
|
package/lib/env/cds-requires.js
CHANGED
package/lib/index.js
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
|
-
if (process.env.CDS_STRICT_NODE_VERSION !== 'false') require
|
|
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
|
|
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
|
|
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(..._) }
|
package/lib/log/cds-error.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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 ...
|
package/lib/log/format/json.js
CHANGED
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/UPDATE.js
CHANGED
|
@@ -31,7 +31,9 @@ class UPDATE extends Whereable {
|
|
|
31
31
|
|
|
32
32
|
// A tagged template string with a single expression, e.g. .with `my.stock -= 1`
|
|
33
33
|
if (args[0].raw) {
|
|
34
|
-
|
|
34
|
+
const [ strings, ...values ] = args
|
|
35
|
+
const vals = values.map (v => typeof v === 'string' ? `'${v}'` : v)
|
|
36
|
+
_add (this, ..._parse_set_expr (this, String.raw(strings, ...vals)))
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
// Alternating expr fragment / values args, e.g. .with ('my.stock -=',1, 'lastOrder =', '$now')
|
package/lib/ql/resolve.js
CHANGED
package/lib/req/context.js
CHANGED
package/lib/req/validate.js
CHANGED
|
@@ -24,15 +24,16 @@ class Validation {
|
|
|
24
24
|
this.cleanse = options.cleanse !== false
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
error (code, path, leaf,
|
|
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
|
|
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 (
|
|
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
|