@sap/cds 9.0.4 → 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.
- package/CHANGELOG.md +68 -0
- package/bin/deploy.js +29 -0
- package/bin/serve.js +1 -5
- package/lib/compile/etc/csv.js +11 -6
- package/lib/compile/for/lean_drafts.js +29 -7
- package/lib/compile/load.js +8 -5
- package/lib/compile/to/hdbtabledata.js +1 -1
- package/lib/dbs/cds-deploy.js +5 -34
- package/lib/env/cds-env.js +2 -1
- package/lib/env/cds-requires.js +4 -1
- package/lib/env/defaults.js +0 -11
- package/lib/env/schemas/cds-rc.js +218 -6
- 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/resolve.js +1 -1
- package/lib/req/context.js +1 -1
- package/lib/req/request.js +1 -1
- package/lib/req/validate.js +17 -19
- package/lib/srv/cds.Service.js +18 -28
- package/lib/srv/middlewares/auth/ias-auth.js +29 -2
- package/lib/srv/middlewares/auth/jwt-auth.js +11 -1
- package/lib/srv/middlewares/auth/xssec.js +1 -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/inflect.js +2 -2
- package/lib/utils/tar.js +60 -23
- 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/crud.js +1 -3
- package/libx/_runtime/common/generic/input.js +113 -52
- package/libx/_runtime/common/generic/sorting.js +1 -1
- package/libx/_runtime/common/generic/temporal.js +0 -6
- 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/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 +529 -143
- package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +3 -2
- package/libx/_runtime/messaging/service.js +1 -1
- package/libx/_runtime/remote/utils/client.js +2 -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/ODataAdapter.js +47 -43
- package/libx/odata/index.js +1 -1
- package/libx/odata/middleware/batch.js +6 -2
- package/libx/odata/middleware/create.js +1 -1
- package/libx/odata/middleware/error.js +27 -17
- package/libx/odata/middleware/operation.js +15 -21
- package/libx/odata/middleware/stream.js +1 -1
- package/libx/odata/parse/afterburner.js +22 -8
- package/libx/odata/parse/cqn2odata.js +16 -10
- package/libx/odata/parse/grammar.peggy +185 -134
- package/libx/odata/parse/parser.js +1 -1
- package/libx/odata/utils/index.js +1 -36
- package/libx/odata/utils/metadata.js +34 -1
- package/libx/odata/utils/odataBind.js +2 -1
- package/libx/odata/utils/result.js +22 -20
- package/libx/queue/index.js +7 -4
- 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/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/resolve.js
CHANGED
package/lib/req/context.js
CHANGED
package/lib/req/request.js
CHANGED
|
@@ -110,7 +110,7 @@ class Request extends require('./event') {
|
|
|
110
110
|
delete err.stack
|
|
111
111
|
throw err
|
|
112
112
|
}
|
|
113
|
-
let e = this.
|
|
113
|
+
let e = this._errors.add(4, ...args)
|
|
114
114
|
if (!('stack' in e)) Error.captureStackTrace (e = Object.assign(new Error,e), this.reject)
|
|
115
115
|
if (!('message' in e)) e.message = String (e.code || e.status)
|
|
116
116
|
throw e
|
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
|
|
@@ -242,8 +241,7 @@ class action extends struct {
|
|
|
242
241
|
super.validate (data, path, ctx, this.params || {})
|
|
243
242
|
}
|
|
244
243
|
|
|
245
|
-
|
|
246
|
-
_is_mandatory(e) { return e.notNull && !e.default } // params
|
|
244
|
+
_is_mandatory(e) { return e.notNull && !e.default || e._is_mandatory() } // params
|
|
247
245
|
}
|
|
248
246
|
|
|
249
247
|
/** Managed associations are struct-like, with foreign keys as elements to validate. */
|
package/lib/srv/cds.Service.js
CHANGED
|
@@ -24,32 +24,20 @@ class ConsumptionAPI {
|
|
|
24
24
|
else return this.dispatch (new Event ({ event, data, headers }))
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
send (
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
? { method:req, entity:path, data, headers }
|
|
31
|
-
: { method:req, data:path, headers:data }))
|
|
32
|
-
else return this.dispatch (new Request({ method:req, path, data, headers }))
|
|
27
|
+
send (...args) {
|
|
28
|
+
const req = _req4 (...args)
|
|
29
|
+
return this.dispatch (req)
|
|
33
30
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const req =
|
|
31
|
+
|
|
32
|
+
schedule (...args) {
|
|
33
|
+
const req = _req4 (...args), {ms4} = cds.utils
|
|
37
34
|
return {
|
|
38
|
-
after (
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
return this
|
|
42
|
-
},
|
|
43
|
-
every (ms) {
|
|
44
|
-
req.queue ??= {}
|
|
45
|
-
req.queue.every = ms
|
|
46
|
-
return this
|
|
47
|
-
},
|
|
48
|
-
then: (r, e) => {
|
|
49
|
-
return cds.queued(this).send(req).then(r, e)
|
|
50
|
-
}
|
|
35
|
+
after (t,u) { (req.queue ??= {}).after = ms4(t,u); return this },
|
|
36
|
+
every (t,u) { (req.queue ??= {}).every = ms4(t,u); return this },
|
|
37
|
+
then: (r,e) => cds.queued(this).send(req).then(r,e)
|
|
51
38
|
}
|
|
52
39
|
}
|
|
40
|
+
|
|
53
41
|
get (...args) { return is_rest(args[0]) ? this.send('GET', ...args) : this.read (...args) }
|
|
54
42
|
put (...args) { return is_rest(args[0]) ? this.send('PUT', ...args) : this.update (...args) }
|
|
55
43
|
post (...args) { return is_rest(args[0]) ? this.send('POST', ...args) : this.create (...args) }
|
|
@@ -179,13 +167,15 @@ const is_query = x => x?.bind || Array.isArray(x) && !x.raw
|
|
|
179
167
|
const is_rest = x => typeof x === 'string' && x[0] === '/'
|
|
180
168
|
const _service_in = m => cds.linked(m).services?.[0]?.name
|
|
181
169
|
|| cds.error.expected `${{model:m}} to be a CSN with a single service definition`
|
|
182
|
-
const _nrm4skd = (method, path, data, headers) => {
|
|
183
|
-
if (typeof method === 'object') return method
|
|
184
|
-
if (typeof path !== 'object') return { method, path, data, headers }
|
|
185
|
-
if (path.is_linked) return { method, entity: path, data, headers }
|
|
186
|
-
return { method, data: path, headers: data }
|
|
187
|
-
}
|
|
188
170
|
|
|
171
|
+
const _req4 = (event, path, data, headers) => {
|
|
172
|
+
if (is_query(event)) return new Request({ query: event, data: path, headers })
|
|
173
|
+
if (is_object(event)) return event instanceof Request ? event : new Request(event)
|
|
174
|
+
if (is_object(path)) return new Request (path.is_linked //...
|
|
175
|
+
? { method:event, entity:path, data, headers }
|
|
176
|
+
: { method:event, data:path, headers:data })
|
|
177
|
+
else return new Request({ method:event, path, data, headers })
|
|
178
|
+
}
|
|
189
179
|
|
|
190
180
|
exports = module.exports = Service
|
|
191
181
|
exports.Service = Service
|
|
@@ -3,7 +3,10 @@ const LOG = cds.log('auth')
|
|
|
3
3
|
|
|
4
4
|
const {
|
|
5
5
|
createSecurityContext,
|
|
6
|
+
Token,
|
|
6
7
|
IdentityService,
|
|
8
|
+
XsuaaService,
|
|
9
|
+
XsuaaToken,
|
|
7
10
|
errors: { ValidationError }
|
|
8
11
|
} = require('./xssec')
|
|
9
12
|
|
|
@@ -18,6 +21,12 @@ module.exports = function ias_auth(config) {
|
|
|
18
21
|
'Either bind an IAS instance, or switch to an authentication kind that does not require a binding.'
|
|
19
22
|
)
|
|
20
23
|
|
|
24
|
+
// enable signature cache by default
|
|
25
|
+
serviceConfig.validation ??= {}
|
|
26
|
+
if (!('signatureCache' in serviceConfig.validation)) serviceConfig.validation.signatureCache = { enabled: true }
|
|
27
|
+
// activate decode cache if not already done or explicitely disabled by setting Token.decodeCache to false or undefined
|
|
28
|
+
if (Token.decodeCache === null) Token.enableDecodeCache()
|
|
29
|
+
|
|
21
30
|
const auth_service = new IdentityService(credentials, serviceConfig)
|
|
22
31
|
const user_factory = get_user_factory(credentials, skipped_attrs)
|
|
23
32
|
|
|
@@ -44,16 +53,26 @@ module.exports = function ias_auth(config) {
|
|
|
44
53
|
validating_auth_service = new IdentityService(credentials, _serviceConfig)
|
|
45
54
|
}
|
|
46
55
|
|
|
56
|
+
// xsuaa fallback allows to also accept XSUAA tokens during migration to IAS
|
|
57
|
+
// automatically enabled if xsuaa credentials are available
|
|
58
|
+
let xsuaa_service, xsuaa_user_factory
|
|
59
|
+
if (cds.env.requires.xsuaa?.credentials) {
|
|
60
|
+
const { credentials: xsuaa_credentials, config: xsuaa_serviceConfig = {} } = cds.env.requires.xsuaa
|
|
61
|
+
xsuaa_service = new XsuaaService(xsuaa_credentials, xsuaa_serviceConfig)
|
|
62
|
+
const get_xsuaa_user_factory = require('./jwt-auth')._get_user_factory
|
|
63
|
+
xsuaa_user_factory = get_xsuaa_user_factory(xsuaa_credentials, xsuaa_credentials.xsappname, 'xsuaa')
|
|
64
|
+
}
|
|
65
|
+
|
|
47
66
|
return async function ias_auth(req, _, next) {
|
|
48
67
|
if (!req.headers.authorization) return next()
|
|
49
68
|
|
|
50
69
|
try {
|
|
51
70
|
const _auth_service =
|
|
52
71
|
validating_auth_service && req.host.match(/\.cert\./) ? validating_auth_service : auth_service
|
|
53
|
-
const securityContext = await createSecurityContext(_auth_service, { req })
|
|
72
|
+
const securityContext = await createSecurityContext(xsuaa_service ? [_auth_service, xsuaa_service] : _auth_service, { req })
|
|
54
73
|
const tokenInfo = securityContext.token
|
|
55
74
|
const ctx = cds.context
|
|
56
|
-
ctx.user = user_factory(tokenInfo)
|
|
75
|
+
ctx.user = tokenInfo instanceof XsuaaToken ? xsuaa_user_factory(tokenInfo) : user_factory(tokenInfo)
|
|
57
76
|
ctx.tenant = tokenInfo.getZoneId()
|
|
58
77
|
req.authInfo = securityContext //> compat req.authInfo
|
|
59
78
|
} catch (e) {
|
|
@@ -73,6 +92,14 @@ function get_user_factory(credentials, skipped_attrs) {
|
|
|
73
92
|
return function user_factory(tokenInfo) {
|
|
74
93
|
const payload = tokenInfo.getPayload()
|
|
75
94
|
|
|
95
|
+
/*
|
|
96
|
+
* NOTE:
|
|
97
|
+
* for easier migration, xssec will offer IAS without policies via so-called XsuaaFallback.
|
|
98
|
+
* in that case, we would need to add the roles here based on the tokenInfo (similar to xsuaa-auth).
|
|
99
|
+
* however, it is not yet clear where the roles will be stored in IAS' tokenInfo object.
|
|
100
|
+
* further, stakeholders would need to configure the "extension" programmatically (e.g., in a custom server.js).
|
|
101
|
+
*/
|
|
102
|
+
|
|
76
103
|
const clientid = tokenInfo.getClientId()
|
|
77
104
|
if (clientid === payload.sub) {
|
|
78
105
|
//> grant_type === client_credentials or x509
|
|
@@ -3,7 +3,9 @@ const LOG = cds.log('auth')
|
|
|
3
3
|
|
|
4
4
|
const {
|
|
5
5
|
createSecurityContext,
|
|
6
|
+
Token,
|
|
6
7
|
XsuaaService,
|
|
8
|
+
XsaService,
|
|
7
9
|
errors: { ValidationError }
|
|
8
10
|
} = require('./xssec')
|
|
9
11
|
|
|
@@ -16,7 +18,13 @@ module.exports = function jwt_auth(config) {
|
|
|
16
18
|
'Either bind an XSUAA instance, or switch to an authentication kind that does not require a binding.'
|
|
17
19
|
)
|
|
18
20
|
|
|
19
|
-
|
|
21
|
+
// enable signature cache by default
|
|
22
|
+
serviceConfig.validation ??= {}
|
|
23
|
+
if (!('signatureCache' in serviceConfig.validation)) serviceConfig.validation.signatureCache = { enabled: true }
|
|
24
|
+
// activate decode cache if not already done or explicitely disabled by setting Token.decodeCache to false or undefined
|
|
25
|
+
if (Token.decodeCache === null) Token.enableDecodeCache()
|
|
26
|
+
|
|
27
|
+
const auth_service = !credentials.uaadomain ? new XsaService(credentials, serviceConfig) : new XsuaaService(credentials, serviceConfig)
|
|
20
28
|
const user_factory = get_user_factory(credentials, credentials.xsappname, kind)
|
|
21
29
|
|
|
22
30
|
return async function jwt_auth(req, _, next) {
|
|
@@ -76,3 +84,5 @@ function get_user_factory(credentials, xsappname, kind) {
|
|
|
76
84
|
return new cds.User({ id, roles, attr, tokenInfo })
|
|
77
85
|
}
|
|
78
86
|
}
|
|
87
|
+
|
|
88
|
+
module.exports._get_user_factory = get_user_factory
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
try {
|
|
2
2
|
const xssec = require('@sap/xssec')
|
|
3
|
-
module.exports = xssec
|
|
3
|
+
module.exports = xssec
|
|
4
4
|
} catch (e) {
|
|
5
5
|
if (e.code === 'MODULE_NOT_FOUND') e.message = `Cannot find '@sap/xssec'. Make sure to install it with 'npm i @sap/xssec'\n` + e.message
|
|
6
6
|
throw e
|
package/lib/srv/srv-models.js
CHANGED
|
@@ -31,7 +31,7 @@ class ExtendedModels {
|
|
|
31
31
|
} catch (error) {
|
|
32
32
|
// Better error message for client
|
|
33
33
|
if (error.status === 404) throw error
|
|
34
|
-
cds.error(
|
|
34
|
+
cds.error(`\`extensibility: true\` is configured but table "cds.xt.Extensions" does not exist - please upgrade tenant '${tenant}'`, error)
|
|
35
35
|
}
|
|
36
36
|
if (!_has_extensions) {
|
|
37
37
|
let k = cache.key4 (tenant = undefined, features)
|
package/lib/srv/srv-tx.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
/** @typedef {import('./cds.Service')} Service } */
|
|
2
1
|
|
|
3
2
|
const cds = require('../index'), { srv_tx_compat_for_afc = true } = cds.env.features
|
|
4
3
|
const EventContext = require('../req/context')
|
|
@@ -9,8 +8,9 @@ class NestedContext extends EventContext { static for(_) { return _ instanceof E
|
|
|
9
8
|
/**
|
|
10
9
|
* This is the implementation of the `srv.tx(req)` method. It constructs
|
|
11
10
|
* a new Transaction as a derivate of the `srv` (i.e. {__proto__:srv})
|
|
11
|
+
* @typedef {import('./cds.Service')} Service
|
|
12
|
+
* @this {Service} @param { EventContext } ctx
|
|
12
13
|
* @returns { Promise<Transaction & Service> }
|
|
13
|
-
* @param { EventContext } ctx
|
|
14
14
|
*/
|
|
15
15
|
module.exports = exports = function srv_tx (ctx,fn) { const srv = this
|
|
16
16
|
|
package/lib/utils/cds-utils.js
CHANGED
|
@@ -20,7 +20,7 @@ exports = module.exports = new class {
|
|
|
20
20
|
get uuid() { return super.uuid = require('crypto').randomUUID }
|
|
21
21
|
get yaml() { const yaml = require('js-yaml'); return super.yaml = Object.assign(yaml,{parse:yaml.load}) }
|
|
22
22
|
get tar() { return super.tar = process.platform === 'win32' && _tarLib() ? require('./tar-lib') : require('./tar') }
|
|
23
|
-
get
|
|
23
|
+
get semver() { return super.semver = require('./version') }
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
/** @type {import('node:path')} */
|
|
@@ -88,7 +88,14 @@ const chimera = Object.getOwnPropertyDescriptors (class Chimera {
|
|
|
88
88
|
exports.decodeURIComponent = s => { try { return decodeURIComponent(s) } catch { return s } }
|
|
89
89
|
exports.decodeURI = s => { try { return decodeURI(s) } catch { return s } }
|
|
90
90
|
|
|
91
|
-
|
|
91
|
+
/**
|
|
92
|
+
* Computes a relative path from the a working directory to the given relative file,
|
|
93
|
+
* considering a divergent 'outer' root path that is not `cds.root`.
|
|
94
|
+
* Needed for `cds watch/run <dir>` calls.
|
|
95
|
+
* @param {string} file - the relative file path to compute
|
|
96
|
+
* @returns {string} - the relative path
|
|
97
|
+
*/
|
|
98
|
+
exports.local = (file) => file && relative(cwd, resolve(cds.root,file))
|
|
92
99
|
|
|
93
100
|
const { prepareStackTrace, stackTraceLimit } = Error
|
|
94
101
|
|
|
@@ -334,3 +341,29 @@ exports.redacted = function _redacted(cred) {
|
|
|
334
341
|
}
|
|
335
342
|
return cred
|
|
336
343
|
}
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Converts a time span with a unit into milliseconds. @example
|
|
348
|
+
* ms4(5,'s') //> 5000
|
|
349
|
+
* ms4('5s') //> 5000
|
|
350
|
+
* @param {number|string} ts - time span as number, or string with unit suffix, with or without spaces
|
|
351
|
+
* @param {string} [unit] - time span unit
|
|
352
|
+
* @returns {number} - time span in milliseconds
|
|
353
|
+
*/
|
|
354
|
+
const ms4 = exports.ms4 = (ts, unit, u=unit) => {
|
|
355
|
+
if (typeof ts === 'string') [,ts,u] = /(\d+) ?(\w*)/.exec(ts) || cds.error `Invalid time span format: ${ts}`
|
|
356
|
+
return ts * ms4[u||unit||'ms'] || cds.error `Invalid time span unit: ${unit} in ${ts}`
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Constants for time spans factors to milliseconds. @example
|
|
361
|
+
* const { days, hours, minutes, second } = cds.utils.ms4
|
|
362
|
+
* 4 * days + 3 * hours + 2 * minutes + 1 * second //> 356521000
|
|
363
|
+
*/
|
|
364
|
+
const ms = ms4.ms = 1
|
|
365
|
+
ms4.seconds = ms4.second = ms4.s = ms4.sec = 1000 *ms
|
|
366
|
+
ms4.minutes = ms4.minute = ms4.m = ms4.min = 1000 *ms * 60
|
|
367
|
+
ms4.hours = ms4.hour = ms4.h = ms4.hrs = 1000 *ms * 60 * 60
|
|
368
|
+
ms4.days = ms4.day = ms4.d = 1000 *ms * 60 * 60 * 24
|
|
369
|
+
ms4.weeks = ms4.week = ms4.w = 1000 *ms * 60 * 60 * 24 * 7
|
package/lib/utils/csv-reader.js
CHANGED
package/lib/utils/inflect.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
this.singular4 = (dn,stripped) => {
|
|
3
3
|
let n = dn.name || dn; if (stripped) n = n.match(last)[0]
|
|
4
4
|
return dn['@singular'] || (
|
|
5
|
-
/species|news$/i.test(n) ? n :
|
|
5
|
+
/(species|news)$/i.test(n) ? n :
|
|
6
6
|
/ess$/.test(n) ? n : // Address
|
|
7
7
|
/ees$/.test(n) ? n.slice(0, -1) : // Employees --> Employee
|
|
8
8
|
/[sz]es$/.test(n) ? n.slice(0, -2) :
|
|
@@ -15,7 +15,7 @@ this.singular4 = (dn,stripped) => {
|
|
|
15
15
|
this.plural4 = (dn,stripped) => {
|
|
16
16
|
let n = dn.name || dn; if (stripped) n = n.match(last)[0]
|
|
17
17
|
return dn['@plural'] || (
|
|
18
|
-
/analysis|status|species|sheep|news$/i.test(n) ? n :
|
|
18
|
+
/(analysis|status|species|sheep|news)$/i.test(n) ? n :
|
|
19
19
|
/[^aeiou]y$/.test(n) ? n.slice(0,-1) + 'ies' :
|
|
20
20
|
/(s|x|z|ch|sh)$/.test(n) ? n + 'es' :
|
|
21
21
|
n + 's'
|