@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/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
|
|
|
@@ -35,25 +44,35 @@ module.exports = function ias_auth(config) {
|
|
|
35
44
|
const should_validate =
|
|
36
45
|
process.env.VCAP_APPLICATION &&
|
|
37
46
|
JSON.parse(process.env.VCAP_APPLICATION).application_uris?.some(uri => uri.match(/\.cert\./))
|
|
38
|
-
const
|
|
47
|
+
const validation_configured = serviceConfig.validation?.x5t?.enabled != null || serviceConfig.validation?.proofToken?.enabled != null
|
|
39
48
|
|
|
40
49
|
let validating_auth_service
|
|
41
|
-
if (should_validate && !
|
|
50
|
+
if (should_validate && !validation_configured) {
|
|
42
51
|
const _serviceConfig = { ...serviceConfig }
|
|
43
52
|
_serviceConfig.validation = { x5t: { enabled: true }, proofToken: { enabled: true } }
|
|
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
|
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
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const semver = exports = module.exports = (x,y,z) => {
|
|
2
|
+
if (typeof x === 'string') [ x,y,z ] = String(x).split('.')
|
|
3
|
+
return 1e6 * (x||0) + 1e3 * (y||0) + +(z||0)
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
exports.checkNodeVersion = (cds = require ('../../package.json')) => {
|
|
7
|
+
let required = cds.engines?.node?.slice(2) //> e.g. >=22
|
|
8
|
+
let current = process.version.slice(1) //> e.g. v24.4.1
|
|
9
|
+
if (semver(current) >= semver(required)) return; else process.stderr.write (`
|
|
10
|
+
Node.js version ${required} or higher is required for @sap/cds v${cds.version}.
|
|
11
|
+
Current version ${current} does not satisfy this. \n\n`)
|
|
12
|
+
return process.exit(1)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
exports.check = (x, min, max) => {
|
|
16
|
+
let v = semver(x)
|
|
17
|
+
return (!min || semver(min) <= v) && (!max || v <= semver(max))
|
|
18
|
+
}
|
package/libx/_runtime/cds.js
CHANGED
|
@@ -16,7 +16,7 @@ cds.extend(service).with(require('./common/aspects/service'))
|
|
|
16
16
|
*/
|
|
17
17
|
cds.Service.prototype._requires_resolving = function (req) {
|
|
18
18
|
if (req._resolved) return false
|
|
19
|
-
if (!this.
|
|
19
|
+
if (!this.definition) return false
|
|
20
20
|
if (!req.query || typeof req.query !== 'object') return false
|
|
21
21
|
if (Array.isArray(req.query)) return false
|
|
22
22
|
if (Object.keys(req.query).length === 0) return false
|
|
@@ -1,20 +1,10 @@
|
|
|
1
|
-
const { foreignKey4 } = require('
|
|
1
|
+
const { foreignKey4 } = require('../utils/foreignKeyPropagations')
|
|
2
2
|
|
|
3
3
|
const _getCommonFieldControl = e => {
|
|
4
4
|
const cfr = e['@Common.FieldControl']
|
|
5
5
|
return cfr && cfr['#']
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
-
const _isMandatory = e => {
|
|
9
|
-
return (
|
|
10
|
-
e['@assert.mandatory'] !== false &&
|
|
11
|
-
(e['@mandatory'] ||
|
|
12
|
-
e['@Common.FieldControl.Mandatory'] ||
|
|
13
|
-
e['@FieldControl.Mandatory'] ||
|
|
14
|
-
_getCommonFieldControl(e) === 'Mandatory')
|
|
15
|
-
)
|
|
16
|
-
}
|
|
17
|
-
|
|
18
8
|
const _isReadOnly = e => {
|
|
19
9
|
return (
|
|
20
10
|
e['@readonly'] ||
|
|
@@ -34,22 +24,10 @@ module.exports = class {
|
|
|
34
24
|
return this.own('__isStructured', () => !!this.elements && this.kind !== 'entity')
|
|
35
25
|
}
|
|
36
26
|
|
|
37
|
-
get _isMandatory() {
|
|
38
|
-
return this.own('__isMandatory', () => !this.isAssociation && _isMandatory(this))
|
|
39
|
-
}
|
|
40
|
-
|
|
41
27
|
get _isReadOnly() {
|
|
42
28
|
return this.own('__isReadOnly', () => !this.key && _isReadOnly(this))
|
|
43
29
|
}
|
|
44
30
|
|
|
45
|
-
get _mandatories() {
|
|
46
|
-
return this.own(
|
|
47
|
-
'__mandatories',
|
|
48
|
-
// eslint-disable-next-line no-unused-vars
|
|
49
|
-
() => this.elements && Object.entries(this.elements).filter(([_, v]) => v._isMandatory)
|
|
50
|
-
)
|
|
51
|
-
}
|
|
52
|
-
|
|
53
31
|
get _foreignKey4() {
|
|
54
32
|
return this.own('__foreignKey4', () => foreignKey4(this))
|
|
55
33
|
}
|
|
@@ -13,11 +13,11 @@ const LOG = cds.log('app')
|
|
|
13
13
|
const { Readable } = require('node:stream')
|
|
14
14
|
|
|
15
15
|
const { enrichDataWithKeysFromWhere } = require('../utils/keys')
|
|
16
|
-
const { DRAFT_COLUMNS_MAP } = require('
|
|
16
|
+
const { DRAFT_COLUMNS_MAP } = require('../constants/draft')
|
|
17
17
|
const propagateForeignKeys = require('../utils/propagateForeignKeys')
|
|
18
|
-
const { checkInputConstraints, assertTargets } = require('../../cds-services/util/assert')
|
|
19
18
|
const getTemplate = require('../utils/template')
|
|
20
19
|
const getRowUUIDGeneratorFn = require('../utils/rowUUIDGenerator')
|
|
20
|
+
const templatePathSerializer = require('../utils/templateProcessorPathSerializer')
|
|
21
21
|
|
|
22
22
|
const _shouldSuppressErrorPropagation = (event, value) => {
|
|
23
23
|
return (
|
|
@@ -96,6 +96,113 @@ const _preProcessAssertTarget = (assocInfo, assertMap) => {
|
|
|
96
96
|
})
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
const _enumValues = element => {
|
|
100
|
+
return Object.keys(element).map(enumKey => {
|
|
101
|
+
const enum_ = element[enumKey]
|
|
102
|
+
const enumValue = enum_ && enum_.val
|
|
103
|
+
|
|
104
|
+
if (enumValue !== undefined) {
|
|
105
|
+
if (enumValue['=']) return enumValue['=']
|
|
106
|
+
if (enum_ && enum_.literal && enum_.literal === 'number') return Number(enumValue)
|
|
107
|
+
return enumValue
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return enumKey
|
|
111
|
+
})
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// REVISIT: this needs a cleanup!
|
|
115
|
+
const _assertError = (code, element, value, key, path) => {
|
|
116
|
+
let args
|
|
117
|
+
|
|
118
|
+
if (typeof code === 'object') {
|
|
119
|
+
args = code.args
|
|
120
|
+
code = code.code
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const { name, type, precision, scale } = element
|
|
124
|
+
const error = new Error()
|
|
125
|
+
const errorEntry = {
|
|
126
|
+
code,
|
|
127
|
+
message: code,
|
|
128
|
+
target: path ?? element.name ?? key,
|
|
129
|
+
args: args ?? [name ?? key]
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const assertError = Object.assign(error, errorEntry)
|
|
133
|
+
Object.assign(assertError, {
|
|
134
|
+
entity: element.parent && element.parent.name,
|
|
135
|
+
element: name, // > REVISIT: when is error.element needed?
|
|
136
|
+
type: element.items ? element.items._type : type,
|
|
137
|
+
status: 400,
|
|
138
|
+
value
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
if (element.enum) assertError.enum = _enumValues(element)
|
|
142
|
+
if (precision) assertError.precision = precision
|
|
143
|
+
if (scale) assertError.scale = scale
|
|
144
|
+
|
|
145
|
+
if (element.target) {
|
|
146
|
+
// REVISIT: when does this case apply?
|
|
147
|
+
assertError.target = element.target
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return assertError
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Check whether the target entity referenced by the association (the reference's target) exists and assert an error if
|
|
155
|
+
* the the reference's target doesn't exist.
|
|
156
|
+
*
|
|
157
|
+
* In other words, use this annotation to check whether a non-null foreign key input in a table has a corresponding
|
|
158
|
+
* primary key (also known as a parent key) in the associated/referenced target table (also known as a parent table).
|
|
159
|
+
*
|
|
160
|
+
* @param {object} assertMap - Map containing the targets to assert.
|
|
161
|
+
* @param {array} errors - Array to collect errors.
|
|
162
|
+
* @see {@link https://cap.cloud.sap/docs/guides/providing-services#assert-target @assert.target} for further information.
|
|
163
|
+
*/
|
|
164
|
+
const _assertTargets = async (assertMap, errors) => {
|
|
165
|
+
const { targets: targetsMap, allTargets } = assertMap
|
|
166
|
+
if (targetsMap.size === 0) return
|
|
167
|
+
|
|
168
|
+
const targets = Array.from(targetsMap.values())
|
|
169
|
+
const transactions = targets.map(({ keys, entity }) => {
|
|
170
|
+
const where = Object.assign({}, ...keys)
|
|
171
|
+
return cds.db.exists(entity, where).forShareLock()
|
|
172
|
+
})
|
|
173
|
+
const targetsExistsResults = await Promise.allSettled(transactions)
|
|
174
|
+
|
|
175
|
+
targetsExistsResults.forEach((txPromise, index) => {
|
|
176
|
+
const isPromiseRejected = txPromise.status === 'rejected'
|
|
177
|
+
const shouldAssertError = (txPromise.status === 'fulfilled' && txPromise.value == null) || isPromiseRejected
|
|
178
|
+
if (!shouldAssertError) return
|
|
179
|
+
|
|
180
|
+
const target = targets[index]
|
|
181
|
+
const { element } = target.assocInfo
|
|
182
|
+
|
|
183
|
+
if (isPromiseRejected) {
|
|
184
|
+
LOG._debug &&
|
|
185
|
+
LOG.debug(
|
|
186
|
+
`The transaction to check the @assert.target constraint for foreign key "${element.name}" failed`,
|
|
187
|
+
txPromise.reason
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
throw new Error(txPromise.reason.message)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
allTargets
|
|
194
|
+
.filter(t => t.key === target.key)
|
|
195
|
+
.forEach(target => {
|
|
196
|
+
const { row, pathSegmentsInfo } = target.assocInfo
|
|
197
|
+
const key = target.foreignKey.name
|
|
198
|
+
let path
|
|
199
|
+
if (pathSegmentsInfo?.length) path = templatePathSerializer(key, pathSegmentsInfo)
|
|
200
|
+
const error = _assertError('ASSERT_TARGET', target.foreignKey, row[key], key, path)
|
|
201
|
+
errors.push(error)
|
|
202
|
+
})
|
|
203
|
+
})
|
|
204
|
+
}
|
|
205
|
+
|
|
99
206
|
const _processCategory = (req, category, value, elementInfo, assertMap) => {
|
|
100
207
|
const { row, key, element, isRoot } = elementInfo
|
|
101
208
|
category = _getSimpleCategory(category)
|
|
@@ -166,7 +273,7 @@ const _getProcessorFn = (req, errors, assertMap) => {
|
|
|
166
273
|
const event = req.event
|
|
167
274
|
|
|
168
275
|
return elementInfo => {
|
|
169
|
-
const { row, key,
|
|
276
|
+
const { row, key, plain } = elementInfo
|
|
170
277
|
// ugly pointer passing for sonar
|
|
171
278
|
const value = { mandatory: false, val: row && row[key] }
|
|
172
279
|
|
|
@@ -175,9 +282,6 @@ const _getProcessorFn = (req, errors, assertMap) => {
|
|
|
175
282
|
}
|
|
176
283
|
|
|
177
284
|
if (_shouldSuppressErrorPropagation(event, value)) return
|
|
178
|
-
|
|
179
|
-
// REVISIT: Convert checkInputConstraints to template mechanism
|
|
180
|
-
checkInputConstraints({ element, value: value.val, errors, pathSegmentsInfo, event })
|
|
181
285
|
}
|
|
182
286
|
}
|
|
183
287
|
|
|
@@ -276,49 +380,12 @@ async function validate_input(req) {
|
|
|
276
380
|
pathSegmentsInfo: []
|
|
277
381
|
})
|
|
278
382
|
if (assertMap.targets.size > 0) {
|
|
279
|
-
await
|
|
383
|
+
await _assertTargets(assertMap, errors)
|
|
280
384
|
}
|
|
281
385
|
|
|
282
386
|
if (errors.length) for (const error of errors) req.error(error)
|
|
283
387
|
}
|
|
284
388
|
|
|
285
|
-
const _getProcessorFnForActionsFunctions =
|
|
286
|
-
(errors, opName) =>
|
|
287
|
-
({ row, key, element }) => {
|
|
288
|
-
const value = row && row[key]
|
|
289
|
-
|
|
290
|
-
// REVISIT: Convert checkInputConstraints to template mechanism
|
|
291
|
-
checkInputConstraints({ element, value, errors, key: opName })
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
const _processActionFunctionRow = (row, param, key, errors, event, service) => {
|
|
295
|
-
const values = Array.isArray(row[key]) ? row[key] : [row[key]]
|
|
296
|
-
|
|
297
|
-
// unstructured
|
|
298
|
-
for (const value of values) {
|
|
299
|
-
checkInputConstraints({ element: param, value, errors, key })
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
// structured
|
|
303
|
-
const template = getTemplate('app-input-operation', service, param, {
|
|
304
|
-
pick: _pick,
|
|
305
|
-
ignore: element => element._isAssociationStrict
|
|
306
|
-
})
|
|
307
|
-
|
|
308
|
-
template.process(values, _getProcessorFnForActionsFunctions(errors, key))
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
const _processActionFunction = (row, eventParams, errors, event, service) => {
|
|
312
|
-
for (const key in eventParams) {
|
|
313
|
-
let param = eventParams[key]
|
|
314
|
-
|
|
315
|
-
// .type of action/function behaves different to .type of other csn elements
|
|
316
|
-
const _type = param.type
|
|
317
|
-
if (!_type && param.items) param = param.items
|
|
318
|
-
_processActionFunctionRow(row, param, key, errors, event, service)
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
|
|
322
389
|
function validate_action(req) {
|
|
323
390
|
const operation = this.actions?.[req.event] || req.target?.actions?.[req.event]
|
|
324
391
|
if (!operation) return
|
|
@@ -334,12 +401,6 @@ function validate_action(req) {
|
|
|
334
401
|
let errs = cds.validate(data, operation, assertOptions)
|
|
335
402
|
if (errs) return errs.forEach(err => req.error(err))
|
|
336
403
|
|
|
337
|
-
// REVISIT: we still need the following because cds.validate doesn't check for @mandatory params (both flat and nested)
|
|
338
|
-
const errors = []
|
|
339
|
-
const arrayData = Array.isArray(data) ? data : [data]
|
|
340
|
-
for (const row of arrayData) _processActionFunction(row, operation.params, errors, req.event, this)
|
|
341
|
-
if (errors.length) for (const error of errors) req.error(error)
|
|
342
|
-
|
|
343
404
|
// convert binaries
|
|
344
405
|
operation.params &&
|
|
345
406
|
!cds.env.features.base64_binaries &&
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const cds = require('../../../../lib')
|
|
2
|
-
const { DRAFT_COLUMNS_MAP } = require('
|
|
2
|
+
const { DRAFT_COLUMNS_MAP } = require('../constants/draft')
|
|
3
3
|
|
|
4
4
|
const _4sqlite = cds.env.i18n && Array.isArray(cds.env.i18n.for_sqlite) ? cds.env.i18n.for_sqlite : []
|
|
5
5
|
// compiler reserves 'localized' and raises a corresponding exception if used in models
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const cds = require('../../../../lib')
|
|
2
2
|
let LOG = cds.log('app')
|
|
3
3
|
|
|
4
|
-
const { rewriteAsterisks } = require('
|
|
4
|
+
const { rewriteAsterisks } = require('./rewriteAsterisks')
|
|
5
5
|
|
|
6
6
|
const _setInverseTransition = (mapping, ref, mapped) => {
|
|
7
7
|
const existing = mapping.get(ref)
|
|
@@ -531,7 +531,7 @@ const _mappedValue = (col, alias) => {
|
|
|
531
531
|
const getDBTable = target => cds.ql.resolve.table(target)
|
|
532
532
|
|
|
533
533
|
const _appendForeignKeys = (newColumns, target, columns, { as, ref = [] }) => {
|
|
534
|
-
const el = target.elements[as] || target.query._target
|
|
534
|
+
const el = target.elements[as] || target.query._target?.elements[ref.at(-1)]
|
|
535
535
|
|
|
536
536
|
if (el && el.isAssociation && el.keys) {
|
|
537
537
|
for (const key of el.keys) {
|
|
@@ -59,7 +59,7 @@ const _resolveTarget = (ref, target) => {
|
|
|
59
59
|
if (ref.length > 1) {
|
|
60
60
|
const element = target.elements[ref[0]]
|
|
61
61
|
if (element) {
|
|
62
|
-
if (element.isAssociation) throw cds.error(`Navigation "${ref.join('/')}" in expand is not supported`)
|
|
62
|
+
if (element.isAssociation) throw cds.error(400, `Navigation "${ref.join('/')}" in expand is not supported`)
|
|
63
63
|
// structured
|
|
64
64
|
return _resolveTarget(ref.slice(1), element)
|
|
65
65
|
} else {
|
|
@@ -72,7 +72,7 @@ const _resolveTarget = (ref, target) => {
|
|
|
72
72
|
const element = target.elements[_ref]
|
|
73
73
|
if (element) return element._target
|
|
74
74
|
|
|
75
|
-
throw cds.error(`Navigation property "${_ref}" is not defined in ${target.name}
|
|
75
|
+
throw cds.error(400, `Navigation property "${_ref}" is not defined in ${target.name}`)
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
const rewriteExpandAsterisk = (columns, target) => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const resolveStructured = require('./resolveStructured')
|
|
2
|
-
const { ensureNoDraftsSuffix } = require('
|
|
3
|
-
const { traverseFroms } = require('
|
|
2
|
+
const { ensureNoDraftsSuffix } = require('./draft')
|
|
3
|
+
const { traverseFroms } = require('./entityFromCqn')
|
|
4
4
|
|
|
5
5
|
const OPERATIONS_MAP = ['=', '>', '<', '!=', '<>', '>=', '<=', 'like', 'between', 'in', 'not in'].reduce((acc, cur) => {
|
|
6
6
|
acc[cur] = 1
|
|
@@ -6,7 +6,6 @@ const _processElement = (processFn, row, key, target, picked = {}, isRoot, pathS
|
|
|
6
6
|
|
|
7
7
|
if (!plain) return
|
|
8
8
|
|
|
9
|
-
/** @type import('../../types/api').templateElementInfo */
|
|
10
9
|
const elementInfo = { row, key, element, target, plain, isRoot, pathSegmentsInfo }
|
|
11
10
|
|
|
12
11
|
if (!element && target._flat2struct?.[key] && elementInfo.pathSegmentsInfo) {
|
|
@@ -53,7 +52,6 @@ const _processComplex = (processFn, row, template, key, pathOptions) => {
|
|
|
53
52
|
let pathSegmentInfo
|
|
54
53
|
if (pathOptions.includeKeyValues) {
|
|
55
54
|
pathOptions.rowUUIDGenerator?.(keyNames, row, template)
|
|
56
|
-
/** @type import('../../types/api').pathSegmentInfo */
|
|
57
55
|
pathSegmentInfo = { key, keyNames, row, elements: template.target.elements, draftKeys: pathOptions.draftKeys }
|
|
58
56
|
}
|
|
59
57
|
|
|
@@ -63,9 +61,6 @@ const _processComplex = (processFn, row, template, key, pathOptions) => {
|
|
|
63
61
|
}
|
|
64
62
|
}
|
|
65
63
|
|
|
66
|
-
/**
|
|
67
|
-
* @param {import("../../types/api").TemplateProcessor} args
|
|
68
|
-
*/
|
|
69
64
|
const templateProcessor = ({ processFn, data, template, isRoot = true, pathOptions = {} }) => {
|
|
70
65
|
if (!template || !template.elements.size || !data || typeof data !== 'object') return
|
|
71
66
|
const dataArr = Array.isArray(data) ? data : [data]
|