@sap/cds 8.6.2 → 8.7.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 +24 -0
- package/_i18n/i18n_en_US_saptrc.properties +4 -7
- package/bin/serve.js +3 -1
- package/lib/compile/for/lean_drafts.js +1 -1
- package/lib/compile/for/nodejs.js +1 -0
- package/lib/compile/to/sql.js +12 -8
- package/lib/core/classes.js +3 -4
- package/lib/core/types.js +1 -0
- package/lib/env/cds-requires.js +2 -2
- package/lib/ql/cds-ql.js +8 -1
- package/lib/ql/cds.ql-Query.js +9 -2
- package/lib/req/validate.js +1 -2
- package/lib/srv/cds-connect.js +1 -1
- package/lib/srv/cds-serve.js +2 -9
- package/lib/srv/cds.Service.js +0 -1
- package/lib/srv/factory.js +56 -71
- package/lib/srv/middlewares/auth/ias-auth.js +44 -14
- package/lib/srv/middlewares/auth/jwt-auth.js +45 -16
- package/lib/srv/middlewares/auth/xssec.js +1 -1
- package/lib/srv/middlewares/errors.js +8 -10
- package/lib/utils/cds-utils.js +5 -1
- package/lib/utils/tar-lib.js +58 -0
- package/libx/_runtime/common/Service.js +0 -4
- package/libx/_runtime/common/generic/input.js +1 -1
- package/libx/_runtime/common/utils/csn.js +5 -1
- package/libx/_runtime/fiori/lean-draft.js +1 -1
- package/libx/_runtime/messaging/enterprise-messaging-shared.js +7 -3
- package/libx/odata/middleware/create.js +4 -0
- package/libx/odata/middleware/delete.js +2 -0
- package/libx/odata/middleware/operation.js +2 -0
- package/libx/odata/middleware/read.js +4 -0
- package/libx/odata/middleware/stream.js +4 -0
- package/libx/odata/middleware/update.js +4 -0
- package/libx/odata/parse/afterburner.js +2 -2
- package/libx/odata/parse/multipartToJson.js +0 -1
- package/libx/odata/utils/normalizeTimeData.js +43 -0
- package/libx/odata/utils/readAfterWrite.js +1 -1
- package/libx/outbox/index.js +1 -1
- package/libx/rest/RestAdapter.js +2 -2
- package/package.json +6 -2
- package/lib/srv/protocols/odata-v2.js +0 -26
- package/libx/_runtime/common/code-ext/WorkerPool.js +0 -90
- package/libx/_runtime/common/code-ext/WorkerReq.js +0 -77
- package/libx/_runtime/common/code-ext/config.js +0 -13
- package/libx/_runtime/common/code-ext/execute.js +0 -123
- package/libx/_runtime/common/code-ext/handlers.js +0 -50
- package/libx/_runtime/common/code-ext/worker.js +0 -70
- package/libx/_runtime/common/code-ext/workerQueryExecutor.js +0 -37
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,29 @@
|
|
|
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 8.7.0 - 2025-01-28
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Allow usage of tar library (https://www.npmjs.com/package/tar) as a workaround to solve remaining issues by extension build on Windows. The tar library should be installed by app developers.
|
|
12
|
+
- `cds.ql` supports limit with an optional offset, e.g. `limit(10, 5)`
|
|
13
|
+
- Basic support for new built-in type `cds.Map`
|
|
14
|
+
- Normalization of DateTime and Timestamp payloads in new OData adapter
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- Cleanse immutable values in draft modifications
|
|
19
|
+
- Do not use compatibility mode of @sap/xssec 4, can be reverted with `cds.env.features.xssec_compat = true`
|
|
20
|
+
- `cds.Float` is now correctly deprecated in `cds.builtin.types`.
|
|
21
|
+
- Input provided via protocol adapter for elements annotated with `@cds.api.ignore` are rejected. Previously, they were ignored.
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- Narrowed down peer dependency version of `express` to `^4`
|
|
26
|
+
- OData, REST: Responses are only written in case that the response object is not already closed, which allows responding to requests directly in custom handlers.
|
|
27
|
+
+ Note: Responses sent directly are not transactionally safe! Further, subsequent errors can no longer be communicated to the client!
|
|
28
|
+
+ Note: Only respond directly in non-`$batch` requests!
|
|
29
|
+
|
|
7
30
|
## Version 8.6.2 - 2025-01-27
|
|
8
31
|
|
|
9
32
|
### Fixed
|
|
@@ -20,6 +43,7 @@
|
|
|
20
43
|
|
|
21
44
|
### Fixed
|
|
22
45
|
|
|
46
|
+
- find draft root in authorization checks when entity has recursive compositions
|
|
23
47
|
- `default-env.json` was not loaded anymore when in production mode.
|
|
24
48
|
- i18n texts like `1` or `true` were returned as numbers, or booleans instead of strings
|
|
25
49
|
- CSN files produced by `cds build` now again contain information to resolve handler files. That was broken in case of reflected/linked models set by e.g. plugins.
|
|
@@ -44,7 +44,7 @@ Currency=bNSEwGmQtXNxy/Qh310iSQ_Currency
|
|
|
44
44
|
CurrencyCode=3cKgP4qz+IsDHtETO3InVQ_Currency Code
|
|
45
45
|
|
|
46
46
|
#XTIT: Currency Code Description
|
|
47
|
-
CurrencyCode.Description=
|
|
47
|
+
CurrencyCode.Description=pPQIrs2UIayPnWseWvdbuA_Currency code as specified by ISO 4217
|
|
48
48
|
|
|
49
49
|
#XTIT: Currency Symbol
|
|
50
50
|
CurrencySymbol=ICns/nlRq2+/URxMXV+T8g_Currency Symbol
|
|
@@ -59,7 +59,7 @@ Country=AbsSu8Y0n1nvBQMk+UaLfw_Country/Region
|
|
|
59
59
|
CountryCode=IgAotY/RI8UnAUTEtFNGkw_Country/Region Code
|
|
60
60
|
|
|
61
61
|
#XTIT: Country/Region Code Description
|
|
62
|
-
CountryCode.Description=
|
|
62
|
+
CountryCode.Description=uSGs3P7TwJlYCpDq83TJNg_Country/region code as specified by ISO 3166-1
|
|
63
63
|
|
|
64
64
|
#XTIT: Language
|
|
65
65
|
Language=gFWTYTeLWksYL6uD/TAgFA_Language
|
|
@@ -68,10 +68,7 @@ Language=gFWTYTeLWksYL6uD/TAgFA_Language
|
|
|
68
68
|
LanguageCode=NhI8Yd8pNFS7omWQsk5aJw_Language Code
|
|
69
69
|
|
|
70
70
|
#XTIT: Language Code Description
|
|
71
|
-
LanguageCode.Description=
|
|
72
|
-
|
|
73
|
-
#XTIT Time zone code
|
|
74
|
-
TimeZoneCode=Y0KTpmsmzoysYLT6jDQEkQ_Time Zone Code
|
|
71
|
+
LanguageCode.Description=ajmXdjo3lK0nfaMLCpvPMw_Language code as specified by ISO 639-1
|
|
75
72
|
|
|
76
73
|
#XTIT: User Identifier
|
|
77
74
|
UserID=cjI0FCsEZ2aD8ERc6G/xZw_User ID
|
|
@@ -83,7 +80,7 @@ Name=xOqOj8rcOinN3ZBO0WdWjA_Name
|
|
|
83
80
|
Description=VGpOoz5siT25o1W0WDiHGw_Description
|
|
84
81
|
|
|
85
82
|
#XTOL: A user's unique Indentifier
|
|
86
|
-
UserID.Description=
|
|
83
|
+
UserID.Description=yOjW1w1qaIvgZeYb0qAsig_User's unique ID
|
|
87
84
|
|
|
88
85
|
#XTIT: Admin data for a draft document
|
|
89
86
|
Draft_DraftAdministrativeData=LsxY+0o1fWbFeMrxNNIRvg_Draft Administrative Data
|
package/bin/serve.js
CHANGED
|
@@ -270,7 +270,9 @@ async function _local_server_js() {
|
|
|
270
270
|
function _prepare_logging () { // NOSONAR
|
|
271
271
|
|
|
272
272
|
const LOG = cds.log('cds.serve|server',{label:'cds'}); if (!LOG._info) return; else log = LOG.info
|
|
273
|
-
const _timer =
|
|
273
|
+
const _timer = process.env.NODE_ENV === 'production'
|
|
274
|
+
? `[cds] - server launched at ${new Date().toLocaleString()}, version: ${cds.version}, in`
|
|
275
|
+
: '[cds] - server launched in'
|
|
274
276
|
console.time (_timer)
|
|
275
277
|
|
|
276
278
|
// print information when model is loaded
|
|
@@ -148,7 +148,7 @@ module.exports = function cds_compile_for_lean_drafts(csn) {
|
|
|
148
148
|
if (
|
|
149
149
|
key === '@mandatory' ||
|
|
150
150
|
key === '@Common.FieldControl' && newEl[key]?.['#'] === 'Mandatory' ||
|
|
151
|
-
key === '@Core.Immutable'
|
|
151
|
+
// key === '@Core.Immutable': Not allowed via UI anyway -> okay to cleanse them in PATCH
|
|
152
152
|
key.startsWith('@assert') ||
|
|
153
153
|
key.startsWith('@PersonalData')
|
|
154
154
|
)
|
|
@@ -11,6 +11,7 @@ function _compile_for_nodejs (csn, o) {
|
|
|
11
11
|
cds.compile.for.lean_drafts(dsn, o)
|
|
12
12
|
Object.defineProperty(csn, '_4nodejs', { value: dsn })
|
|
13
13
|
Object.defineProperty(dsn, '_4nodejs', { value: dsn })
|
|
14
|
+
Object.assign (dsn.meta, csn.meta, dsn.meta) // merge meta data, as it may have been enhanced
|
|
14
15
|
return dsn
|
|
15
16
|
}
|
|
16
17
|
|
package/lib/compile/to/sql.js
CHANGED
|
@@ -33,14 +33,18 @@ function cds_compile_to_hana(csn, o, beforeCsn) {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
function cds_compile_to_deltaSql (csn, o, beforeCsn) {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
36
|
+
let result, next = ()=> result ??= function (){
|
|
37
|
+
const options = cdsc._options.for.sql(o)
|
|
38
|
+
if (typeof beforeCsn === 'string') beforeCsn = JSON.parse(beforeCsn)
|
|
39
|
+
const { afterImage, drops, createsAndAlters } = cdsc.to.deltaSql (csn, options, beforeCsn || {definitions: {}, $version: '2.0'} ); // FIXME: As default value in compiler API?
|
|
40
|
+
return {
|
|
41
|
+
afterImage,
|
|
42
|
+
drops: unfold_ddl(drops, csn, options),
|
|
43
|
+
createsAndAlters: unfold_ddl(createsAndAlters, csn, options)
|
|
44
|
+
};
|
|
45
|
+
}()
|
|
46
|
+
cds.emit ('compile.to.dbx', csn, o, next)
|
|
47
|
+
return result ??= next() //> in case no handler called next
|
|
44
48
|
}
|
|
45
49
|
|
|
46
50
|
function cds_compile_to_hdbcds (csn,o) {
|
package/lib/core/classes.js
CHANGED
|
@@ -59,9 +59,8 @@ class type extends any { is(kind) { return kind === 'type' || super.is(kind) }
|
|
|
59
59
|
class Int16 extends Integer {}
|
|
60
60
|
class Int32 extends Integer {}
|
|
61
61
|
class Int64 extends Integer {}
|
|
62
|
-
|
|
63
|
-
class
|
|
64
|
-
class Decimal extends Float {
|
|
62
|
+
class Double extends number {}
|
|
63
|
+
class Decimal extends number {
|
|
65
64
|
toString(){
|
|
66
65
|
return this.precision ? this.scale ? `Decimal(${this.precision},${this.scale})` : `Decimal(${this.precision})` : 'Decimal'
|
|
67
66
|
}
|
|
@@ -202,7 +201,7 @@ module.exports = {
|
|
|
202
201
|
UUID, Boolean, String,
|
|
203
202
|
Integer, UInt8, Int16, Int32, Int64,
|
|
204
203
|
|
|
205
|
-
|
|
204
|
+
Double, Decimal,
|
|
206
205
|
Date, Time, DateTime, Timestamp,
|
|
207
206
|
Binary, Vector, LargeBinary, LargeString,
|
|
208
207
|
|
package/lib/core/types.js
CHANGED
|
@@ -15,6 +15,7 @@ for (let k in classes) if (k !== 'LinkedDefinitions' && k !== 'mixins') {
|
|
|
15
15
|
|
|
16
16
|
Object.assign (protos, types.deprecated = {
|
|
17
17
|
'cds.DecimalFloat': Object.defineProperty (new classes.Decimal, '_type', { value:'cds.DecimalFloat' }),
|
|
18
|
+
'cds.Float': Object.defineProperty (new classes.number, '_type', { value:'cds.Float' }),
|
|
18
19
|
'cds.Integer16': new classes.Int16,
|
|
19
20
|
'cds.Integer32': new classes.Int32,
|
|
20
21
|
'cds.Integer64': new classes.Int64,
|
package/lib/env/cds-requires.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
const { deprecated: cds_utils_deprecated } = require('../utils/cds-utils')
|
|
1
2
|
const _runtime = '@sap/cds/libx/_runtime'
|
|
2
3
|
|
|
3
4
|
exports = module.exports = {
|
|
@@ -80,8 +81,7 @@ const _authentication_strategies = {
|
|
|
80
81
|
|
|
81
82
|
for (let each of Object.values(_authentication_strategies)) {
|
|
82
83
|
Object.defineProperty (each, 'strategy', {get() {
|
|
83
|
-
|
|
84
|
-
if (process.env.NODE_ENV !== 'production') console.trace('WARNING: auth.strategy is deprecated, use auth.kind instead')
|
|
84
|
+
if (process.env.NODE_ENV !== 'production') cds_utils_deprecated({ old: 'auth.strategy', use: 'auth.kind' })
|
|
85
85
|
return { jwt: 'JWT', mocked: 'mock' }[this.kind] || this.kind
|
|
86
86
|
}})
|
|
87
87
|
}
|
package/lib/ql/cds-ql.js
CHANGED
|
@@ -169,7 +169,7 @@ exports.nested = (ref, ...args) => {
|
|
|
169
169
|
if (ref.raw) return ql.nested (ql.ref(ref,...args))
|
|
170
170
|
else if (!ref.ref) ref = ql.ref(ref)
|
|
171
171
|
for (let each of args) {
|
|
172
|
-
if (each.as || each.where || each.orderBy) ref = {...ref, ...each}
|
|
172
|
+
if (each.as || each.where || each.orderBy || each.limit) ref = {...ref, ...each}
|
|
173
173
|
else ref.columns = ql.columns(each)
|
|
174
174
|
}
|
|
175
175
|
ref.columns ??= ['*']
|
|
@@ -208,6 +208,13 @@ exports.orders = (...args) => {
|
|
|
208
208
|
}
|
|
209
209
|
}
|
|
210
210
|
|
|
211
|
+
/** @returns {{ limit: { rows: { val: any; }; offset?: { val: any; }; }; }} */
|
|
212
|
+
exports.limit = (...args) => {
|
|
213
|
+
const [ limit, offset ] = args; if (limit?.raw) return {limit: cds.parse._select('from X limit',args).limit }
|
|
214
|
+
if (!offset) return { limit: { rows: { val: limit } } }
|
|
215
|
+
else return { limit: { rows: { val: +limit }, offset: { val: +offset } } }
|
|
216
|
+
};
|
|
217
|
+
|
|
211
218
|
const _cqn_or_val = x => typeof x === 'object' ? x : {val:x}
|
|
212
219
|
const is_object = x => typeof x === 'object'
|
|
213
220
|
const is_array = Array.isArray
|
package/lib/ql/cds.ql-Query.js
CHANGED
|
@@ -71,20 +71,27 @@ class Query {
|
|
|
71
71
|
/** @private */ get _target_ref() { return this._.from } // overridden in subclasses
|
|
72
72
|
/** @private */ set _target(t) { this._set('_target', t.ref ? {name:t.ref[0]} : t) }
|
|
73
73
|
|
|
74
|
-
/**
|
|
74
|
+
/**
|
|
75
|
+
* @param {string | Function | object} t - the target
|
|
76
|
+
* @param {unknown[]} etc - additional arguments
|
|
77
|
+
* @returns {typeof this._target | import('@sap/cds').ref}
|
|
78
|
+
* @private
|
|
79
|
+
**/
|
|
80
|
+
_target4 (t,...etc) {
|
|
75
81
|
switch (typeof t) {
|
|
76
82
|
case 'string': {
|
|
77
83
|
// NOTE: even though this._target will be {name:ref[0]} this returns the parsed {ref}
|
|
78
84
|
// NOTE: we need to clone cached refs, as they might get modified afterwards
|
|
79
85
|
return this._target = cloned (cached[t] ??= cds.parse.path(t))
|
|
80
86
|
}
|
|
87
|
+
case 'function': // fallthrough for classes generated by cds-typer. Will end up in t.name subcase
|
|
81
88
|
case 'object': {
|
|
82
89
|
if (t.raw) return this._target = !etc.length ? this._target4(t[0]) : cds.parse.path(t,...etc)
|
|
83
90
|
if (t.name) return {ref:[ (this._target = t).name ]}
|
|
84
91
|
if (t.ref || t.SELECT || t.SET) return this._target = t
|
|
85
92
|
}
|
|
86
93
|
}
|
|
87
|
-
throw this._expected `${{target:t}} to be an entity path string, a CSN definition, a {ref}, a {SELECT},
|
|
94
|
+
throw this._expected `${{target:t}} to be an entity path string, a CSN definition, a {ref}, a {SELECT}, a {SET}, or a class representing an entity definition`
|
|
88
95
|
}
|
|
89
96
|
|
|
90
97
|
/** @private */ _expected (...args) {
|
package/lib/req/validate.js
CHANGED
|
@@ -158,7 +158,6 @@ const $any = class any {
|
|
|
158
158
|
if (d['@cds.on.update']) return true
|
|
159
159
|
if (d['@Core.Computed']) return true
|
|
160
160
|
if (d['@Common.FieldControl']?.['#'] === 'ReadOnly') return true
|
|
161
|
-
if (d['@cds.api.ignore'] && !cds.env.features.reject_ignored) return true
|
|
162
161
|
else return false
|
|
163
162
|
})
|
|
164
163
|
}
|
|
@@ -206,7 +205,7 @@ class struct extends $any {
|
|
|
206
205
|
// check values of given data
|
|
207
206
|
for (let each in data) { // will work for structured payloads as well as flattened ones with universal CSN
|
|
208
207
|
let /** @type {$any} */ d = elements[each]
|
|
209
|
-
if (!d || (d['@cds.api.ignore'] && ctx.rejectIgnore
|
|
208
|
+
if (!d || (d['@cds.api.ignore'] && ctx.rejectIgnore)) ctx.unknown (each, this, data)
|
|
210
209
|
else if (ctx.cleanse && d._is_readonly() && !d.key) delete data[each]
|
|
211
210
|
// @Core.Immutable processed only for root, children are handled when knowing db state
|
|
212
211
|
else if (ctx.cleanse && d['@Core.Immutable'] && !ctx.insert && !path) delete data[each]
|
package/lib/srv/cds-connect.js
CHANGED
|
@@ -34,7 +34,7 @@ connect.to = (datasource, options) => {
|
|
|
34
34
|
}
|
|
35
35
|
const promise = (async()=>{
|
|
36
36
|
TRACE?.time(`cds.connect.to ${datasource}`.padEnd(22).slice(0,22))
|
|
37
|
-
const o = Service.
|
|
37
|
+
const o = Service._is_service_class ? {} : options4 (datasource, options)
|
|
38
38
|
const m = await model4 (o)
|
|
39
39
|
// check if required service definition exists
|
|
40
40
|
const required = cds.requires[datasource]
|
package/lib/srv/cds-serve.js
CHANGED
|
@@ -13,7 +13,6 @@ module.exports = function cds_serve (som, _options) { // NOSONAR
|
|
|
13
13
|
|
|
14
14
|
if (som && typeof som === 'object' && !is_csn(som) && !is_files(som)) {
|
|
15
15
|
[som,_options] = [undefined,
|
|
16
|
-
som._is_service_instance ? { service:som, from:'*' } :
|
|
17
16
|
som._is_service_class ? { service:som, from:'*' } :
|
|
18
17
|
som
|
|
19
18
|
]
|
|
@@ -43,13 +42,8 @@ module.exports = function cds_serve (som, _options) { // NOSONAR
|
|
|
43
42
|
// Pass 1: Construct service provider instances...
|
|
44
43
|
const all=[], provided = loaded.then (async csn => { // NOSONAR
|
|
45
44
|
|
|
46
|
-
// Shortcut for directly passed service instances
|
|
47
|
-
if (o.service && o.service._is_service_instance) {
|
|
48
|
-
return all.push (o.service)
|
|
49
|
-
}
|
|
50
|
-
|
|
51
45
|
// Shortcut for directly passed service classes
|
|
52
|
-
if (o.service
|
|
46
|
+
if (o.service?._is_service_class) {
|
|
53
47
|
const Service = o.service, d = { name: o.service.name }
|
|
54
48
|
const srv = await _new (Service, d,csn,o)
|
|
55
49
|
return all.push (srv)
|
|
@@ -83,10 +77,9 @@ module.exports = function cds_serve (som, _options) { // NOSONAR
|
|
|
83
77
|
// Note: doing that in a second pass guarantees all own services are in
|
|
84
78
|
// cds.services, so they'll be found when they cds.connect to each others.
|
|
85
79
|
let ready = provided.then (()=> Promise.all (all.map (async srv => {
|
|
86
|
-
if (o.service && o.service._is_service_instance) return srv
|
|
87
80
|
cds.services[srv.name] = await Service.init (srv)
|
|
88
81
|
cds.service.providers.push (srv)
|
|
89
|
-
|
|
82
|
+
srv[_ready]?.(srv)
|
|
90
83
|
return srv
|
|
91
84
|
})))
|
|
92
85
|
|
package/lib/srv/cds.Service.js
CHANGED
|
@@ -116,7 +116,6 @@ class Service extends ModeledService {
|
|
|
116
116
|
/** @deprecated */ Service.prototype.transaction = function(...args) { return this.tx(...args) }
|
|
117
117
|
/** @deprecated */ Service.prototype._implicit_next = cds.env.features.implicit_next
|
|
118
118
|
|
|
119
|
-
Service.prototype._is_service_instance = true //> for factory
|
|
120
119
|
Service._is_service_class = true //> for factory
|
|
121
120
|
|
|
122
121
|
//--------------------------------------------------------------------------
|
package/lib/srv/factory.js
CHANGED
|
@@ -1,90 +1,75 @@
|
|
|
1
|
-
const
|
|
2
|
-
const
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
const exts = process.env.CDS_TYPESCRIPT ? ['ts','js','mjs'] : ['js','mjs']
|
|
2
|
+
const cds = require('..'), { path, isfile } = cds.utils
|
|
3
|
+
/**
|
|
4
|
+
* NOTE: Need this typed helper variable to be able to use IntelliSense for calls with new keyword.
|
|
5
|
+
* @import Service from './cds.Service'
|
|
6
|
+
* @type new() => Service
|
|
7
|
+
*/
|
|
8
|
+
const factory = ServiceFactory
|
|
9
|
+
module.exports = exports = factory
|
|
7
10
|
|
|
8
|
-
const o = { ...options } // avoid changing shared options
|
|
9
|
-
const conf = cds.requires[name]
|
|
10
|
-
const serve = !conf?.external || o.mocked && !conf.credentials
|
|
11
|
-
const defs = !model ? {[name]:{}} : model.definitions || cds.error `Invalid argument for 'model': ${model}`
|
|
12
|
-
const def = !name || name === 'db' ? {} : defs[name] || {}
|
|
13
|
-
DEBUG?.({ name, definition:def, options:redacted(o) })
|
|
14
11
|
|
|
15
|
-
|
|
16
|
-
if (it = o.with) return _use (it) // from cds.serve (<options>)
|
|
17
|
-
if (it = serve && def['@impl']) return _use (it) // from service definition
|
|
18
|
-
if (it = serve && sibling(def)) return _use (it) // next to <service>.cds
|
|
19
|
-
if (it = o.impl) return _use (it) // from cds.connect (<options>)
|
|
20
|
-
return _use (_required())
|
|
12
|
+
function ServiceFactory (name, model, options) {
|
|
21
13
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
if (typeof it === 'function') return _use (await _required(), /*with:*/ o.impl = _function(it)) // NOSONAR
|
|
26
|
-
if (typeof it === 'object') return _use (it[name] || it.default || await _required())
|
|
27
|
-
if (typeof it === 'string') return Object.assign (await _use (await _require(it,def)), {_source:it})
|
|
28
|
-
throw cds.error `Invalid service implementation for ${name}: ${it}`
|
|
29
|
-
}
|
|
14
|
+
const o = { ...options } // avoid changing shared options
|
|
15
|
+
const def = model?.definitions[name] || {}
|
|
16
|
+
const remote = o.external && (o.credentials || !o.mocked)
|
|
30
17
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
18
|
+
return _use (remote ? o.impl : o.with || def['@impl'] || _sibling(def) || o.impl || _kind())
|
|
19
|
+
async function _use (impl) { switch (typeof impl) {
|
|
20
|
+
case 'function':
|
|
21
|
+
if (impl._is_service_class) return new impl (name, model, o)
|
|
22
|
+
return _use (_kind(), /*with:*/ o.impl = _legacy(impl) || impl)
|
|
23
|
+
case 'object':
|
|
24
|
+
return _use (impl[name] || impl.default || _kind())
|
|
25
|
+
case 'string':
|
|
26
|
+
if (impl.startsWith('@sap/cds/')) impl = cds.home + impl.slice(8) //> for local tests in @sap/cds dev
|
|
27
|
+
if (impl.startsWith('./')) impl = path.resolve (cds.root, _source4(def) || '.', '..', impl.slice(2))
|
|
28
|
+
try { var resolved = require.resolve(impl, {paths:[ cds.root, cds.home ]}) } catch {
|
|
29
|
+
try { resolved = require.resolve(path.join(cds.root, impl)) } catch (e) { // compatibility
|
|
30
|
+
throw cds.error(`Failed loading service implementation from ` + impl, { cause: e })
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
impl = await cds.utils._import (resolved)
|
|
34
|
+
impl = await _use (impl)
|
|
35
|
+
impl._source = resolved
|
|
36
|
+
return impl
|
|
37
|
+
default: throw cds.error`Invalid service implementation for ${name}: ${impl}`
|
|
38
|
+
}}
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
if (it.startsWith('./')) it = _relative (d,it.slice(2)) //> relative to <service>.cds
|
|
44
|
-
try { var resolved = require.resolve(it,{paths}) } catch {
|
|
45
|
-
try { resolved = require.resolve(path.join(cds.root,it)) } catch(e) { // compatibility
|
|
46
|
-
throw cds.error `Failed loading service implementation from '${it}' ${{ Reason:e, paths, 'cds.root':cds.root }}`
|
|
47
|
-
}
|
|
40
|
+
function _kind (kind = o.kind ??= def['@kind'] || 'app-service') {
|
|
41
|
+
const {impl} = cds.env.requires.kinds[kind] || cds.error `No configuration found for 'cds.requires.kinds.${kind}'`
|
|
42
|
+
return impl || cds.error `No 'impl' configured for 'cds.requires.kinds.${kind}'`
|
|
48
43
|
}
|
|
49
|
-
DEBUG?.({resolved})
|
|
50
|
-
return cds.utils._import(resolved)
|
|
51
44
|
}
|
|
52
45
|
|
|
53
|
-
const _function = (impl) => !_is_class(impl) ? impl : (srv) => {
|
|
54
|
-
const instance = new impl, skip = {constructor:1,prototype:1}
|
|
55
|
-
for (let each of Reflect.ownKeys (impl.prototype)) {
|
|
56
|
-
each in skip || srv.on (each, (...args) => instance[each](...args))
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
46
|
|
|
60
|
-
const
|
|
61
|
-
|
|
47
|
+
const _source4 = d => d['@source'] || d.$location?.file
|
|
48
|
+
const _sibling = d => {
|
|
49
|
+
let file = _source4(d); if (!file) return
|
|
50
|
+
let { dir, name } = path.parse (file)
|
|
62
51
|
for (let subdir of ['', './lib', './handlers']) {
|
|
63
|
-
let
|
|
64
|
-
|
|
52
|
+
for (let ext of exts) {
|
|
53
|
+
if (file = isfile (dir, subdir, name + '.' + ext)) return file // eslint-disable-line no-cond-assign
|
|
54
|
+
}
|
|
65
55
|
}
|
|
66
56
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
try { return require.resolve(f) }
|
|
74
|
-
catch(e) { if (e.code === 'MODULE_NOT_FOUND') return isfile(path.resolve(cds.root,f)); else throw e }
|
|
57
|
+
const _legacy = impl => { // legacy hello-world style class
|
|
58
|
+
if (impl.prototype && /^class\b/.test(impl)) return function() {
|
|
59
|
+
const legacy = new impl
|
|
60
|
+
for (let k of Reflect.ownKeys(impl.prototype))
|
|
61
|
+
k === 'constructor' || k === 'prototype' || this.on(k, legacy[k].bind(legacy))
|
|
62
|
+
}
|
|
75
63
|
}
|
|
76
|
-
const _is_class = (impl) => typeof impl === 'function' && impl.prototype && /^class\b/.test(impl)
|
|
77
64
|
|
|
78
|
-
module.exports = Object.assign (ServiceFactory, { init, is_factory:true })
|
|
79
65
|
|
|
80
66
|
/**
|
|
81
|
-
*
|
|
82
|
-
* from cds.service.impl-style implementations.
|
|
67
|
+
* Called by cds.connect() and cds.serve() for cds.service.impl-style implementations.
|
|
83
68
|
* @protected
|
|
84
69
|
*/
|
|
85
|
-
async function
|
|
86
|
-
const {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
return
|
|
70
|
+
exports.init = async function (srv) {
|
|
71
|
+
const {impl} = srv.options; if (typeof impl === 'function' && !impl._is_service_class)
|
|
72
|
+
await impl.call(srv, srv)
|
|
73
|
+
await srv.init()
|
|
74
|
+
return srv
|
|
90
75
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
const cds = require('../../../index.js')
|
|
2
2
|
const LOG = cds.log('auth')
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
let xssec = require('./xssec')
|
|
5
|
+
|
|
5
6
|
|
|
6
7
|
// REVISIT: Why do we need to know and do that?
|
|
7
8
|
const KNOWN_CLAIMS = Object.values({
|
|
@@ -78,19 +79,48 @@ module.exports = function ias_auth(config) {
|
|
|
78
79
|
}
|
|
79
80
|
|
|
80
81
|
// NOTE: Use named function for better stack traces... for the actual middleware, of course, not that much for the factory!
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
if (!
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
82
|
+
|
|
83
|
+
if (xssec.v3 && cds.env.features.xssec_compat !== true) { // no official flag!
|
|
84
|
+
let { createSecurityContext, IdentityService, errors: { ValidationError } } = xssec
|
|
85
|
+
const authService = new IdentityService(credentials)
|
|
86
|
+
|
|
87
|
+
return async function ias_auth (req, _, next) {
|
|
88
|
+
if (!req.headers.authorization) return next()
|
|
89
|
+
try {
|
|
90
|
+
const secContext = await createSecurityContext(authService, { req })
|
|
91
|
+
const tokenInfo = secContext.token
|
|
92
|
+
const ctx = cds.context
|
|
93
|
+
ctx.user = getUser(tokenInfo)
|
|
94
|
+
ctx.tenant = tokenInfo.getZoneId()
|
|
95
|
+
req.authInfo = secContext //> compat req.authInfo
|
|
96
|
+
} catch(e) {
|
|
97
|
+
if (e instanceof ValidationError) {
|
|
98
|
+
LOG.warn("Unauthenticated request: ", e);
|
|
99
|
+
return next(401)
|
|
100
|
+
}
|
|
101
|
+
LOG.error("Error while authenticating user: ", e);
|
|
102
|
+
return next(500)
|
|
103
|
+
}
|
|
93
104
|
next()
|
|
94
|
-
}
|
|
105
|
+
}
|
|
106
|
+
} else { // TODO: Remove with cds 9
|
|
107
|
+
xssec = xssec.v3 || xssec
|
|
108
|
+
return function ias_auth (req, _, next) {
|
|
109
|
+
if (!req.headers.authorization) return next()
|
|
110
|
+
const token = req.headers.authorization.slice(7) // skip /^bearer /
|
|
111
|
+
xssec.createSecurityContext(token, credentials, 'IAS', function (err, securityContext, tokenInfo) {
|
|
112
|
+
|
|
113
|
+
if (err) LOG.warn('User could not be authenticated due to error:', err)
|
|
114
|
+
if (!securityContext) {
|
|
115
|
+
return next(401)
|
|
116
|
+
}
|
|
117
|
+
else req.authInfo = securityContext //> compat req.authInfo
|
|
118
|
+
|
|
119
|
+
const ctx = cds.context
|
|
120
|
+
ctx.user = getUser(tokenInfo)
|
|
121
|
+
ctx.tenant = tokenInfo.getZoneId()
|
|
122
|
+
next()
|
|
123
|
+
})
|
|
124
|
+
}
|
|
95
125
|
}
|
|
96
126
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const cds = require('../../../index.js')
|
|
2
2
|
const LOG = cds.log('auth')
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
let xssec = require('./xssec')
|
|
5
5
|
|
|
6
6
|
module.exports = function jwt_auth(config) {
|
|
7
7
|
const { kind, credentials } = config
|
|
@@ -42,21 +42,50 @@ module.exports = function jwt_auth(config) {
|
|
|
42
42
|
|
|
43
43
|
return new cds.User({ id, roles, attr, tokenInfo })
|
|
44
44
|
}
|
|
45
|
+
if (xssec.v3 && cds.env.features.xssec_compat !== true) { // no official flag!
|
|
46
|
+
const { createSecurityContext, XsuaaService, errors: { ValidationError } } = xssec
|
|
47
|
+
const authService = new XsuaaService(credentials)
|
|
45
48
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
49
|
+
return async function jwt_auth(req, _, next) {
|
|
50
|
+
if (!req.headers.authorization) return next()
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const secContext = await createSecurityContext(authService, { req })
|
|
54
|
+
const tokenInfo = secContext.token
|
|
55
|
+
const ctx = cds.context
|
|
56
|
+
ctx.user = getUser(tokenInfo)
|
|
57
|
+
ctx.tenant = tokenInfo.getZoneId()
|
|
58
|
+
req.authInfo = secContext //> compat req.authInfo
|
|
59
|
+
} catch(e) {
|
|
60
|
+
if(e instanceof ValidationError) {
|
|
61
|
+
LOG.warn("Unauthenticated request: ", e);
|
|
62
|
+
return next(401)
|
|
63
|
+
}
|
|
64
|
+
LOG.error("Error while authenticating user: ", e);
|
|
65
|
+
return next(500)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
next()
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
} else {
|
|
72
|
+
xssec = xssec.v3 || xssec
|
|
73
|
+
|
|
74
|
+
// NOTE: Use named function for better stack traces... for the actual middleware, of course, not that much for the factory!
|
|
75
|
+
return function jwt_auth (req, _, next) {
|
|
76
|
+
if (!req.headers.authorization) return next()
|
|
77
|
+
const token = req.headers.authorization.slice(7) // skip /^bearer /
|
|
78
|
+
xssec.createSecurityContext(token, credentials, function (err, securityContext, tokenInfo) {
|
|
79
|
+
|
|
80
|
+
if (err) LOG.warn('User could not be authenticated due to error:', err)
|
|
81
|
+
if (!securityContext) return next(401)
|
|
82
|
+
else req.authInfo = securityContext //> compat req.authInfo
|
|
83
|
+
|
|
84
|
+
const ctx = cds.context
|
|
85
|
+
ctx.user = getUser(tokenInfo)
|
|
86
|
+
ctx.tenant = tokenInfo.getZoneId()
|
|
87
|
+
next()
|
|
88
|
+
})
|
|
89
|
+
}
|
|
61
90
|
}
|
|
62
91
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
try {
|
|
2
2
|
const xssec = require('@sap/xssec')
|
|
3
|
-
module.exports = xssec
|
|
3
|
+
module.exports = xssec // use v3 compat api // REVISIT: why ???
|
|
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
|