@sap/cds 8.6.1 → 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.
Files changed (53) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/_i18n/i18n_en_US_saptrc.properties +4 -7
  3. package/bin/serve.js +3 -1
  4. package/lib/compile/for/lean_drafts.js +1 -1
  5. package/lib/compile/for/nodejs.js +1 -0
  6. package/lib/compile/to/sql.js +12 -8
  7. package/lib/core/classes.js +3 -4
  8. package/lib/core/types.js +1 -0
  9. package/lib/env/cds-requires.js +2 -2
  10. package/lib/ql/cds-ql.js +8 -1
  11. package/lib/ql/cds.ql-Query.js +9 -2
  12. package/lib/req/validate.js +3 -2
  13. package/lib/srv/cds-connect.js +1 -1
  14. package/lib/srv/cds-serve.js +2 -9
  15. package/lib/srv/cds.Service.js +0 -1
  16. package/lib/srv/factory.js +56 -71
  17. package/lib/srv/middlewares/auth/ias-auth.js +44 -14
  18. package/lib/srv/middlewares/auth/jwt-auth.js +45 -16
  19. package/lib/srv/middlewares/auth/xssec.js +1 -1
  20. package/lib/srv/middlewares/errors.js +8 -10
  21. package/lib/utils/cds-utils.js +5 -1
  22. package/lib/utils/tar-lib.js +58 -0
  23. package/libx/_runtime/common/Service.js +0 -4
  24. package/libx/_runtime/common/generic/auth/utils.js +1 -1
  25. package/libx/_runtime/common/generic/input.js +3 -1
  26. package/libx/_runtime/common/utils/csn.js +5 -1
  27. package/libx/_runtime/common/utils/resolveView.js +1 -1
  28. package/libx/_runtime/fiori/lean-draft.js +1 -1
  29. package/libx/_runtime/messaging/enterprise-messaging-shared.js +7 -3
  30. package/libx/odata/middleware/batch.js +22 -23
  31. package/libx/odata/middleware/create.js +4 -0
  32. package/libx/odata/middleware/delete.js +2 -0
  33. package/libx/odata/middleware/operation.js +2 -0
  34. package/libx/odata/middleware/read.js +4 -0
  35. package/libx/odata/middleware/stream.js +4 -0
  36. package/libx/odata/middleware/update.js +4 -0
  37. package/libx/odata/parse/afterburner.js +9 -4
  38. package/libx/odata/parse/grammar.peggy +7 -8
  39. package/libx/odata/parse/multipartToJson.js +0 -1
  40. package/libx/odata/parse/parser.js +1 -1
  41. package/libx/odata/utils/normalizeTimeData.js +43 -0
  42. package/libx/odata/utils/readAfterWrite.js +1 -1
  43. package/libx/outbox/index.js +1 -1
  44. package/libx/rest/RestAdapter.js +20 -4
  45. package/package.json +6 -2
  46. package/lib/srv/protocols/odata-v2.js +0 -26
  47. package/libx/_runtime/common/code-ext/WorkerPool.js +0 -90
  48. package/libx/_runtime/common/code-ext/WorkerReq.js +0 -77
  49. package/libx/_runtime/common/code-ext/config.js +0 -13
  50. package/libx/_runtime/common/code-ext/execute.js +0 -123
  51. package/libx/_runtime/common/code-ext/handlers.js +0 -50
  52. package/libx/_runtime/common/code-ext/worker.js +0 -70
  53. package/libx/_runtime/common/code-ext/workerQueryExecutor.js +0 -37
package/CHANGELOG.md CHANGED
@@ -4,10 +4,46 @@
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
+
30
+ ## Version 8.6.2 - 2025-01-27
31
+
32
+ ### Fixed
33
+
34
+ - Crash during requests to actions with parameter `array of <type>`
35
+ - Instance based restriction using `is null`
36
+ - Filtering of grouped result on default aggregate
37
+ - Multipart batch response for failed changesets
38
+ - Handling of invalid parentheses in OData property access
39
+ - Resolve view: Mixins are not in elements of projection target
40
+ - Input provided via protocol adapter for elements annotated with `@cds.api.ignore` can be rejected with `cds.features.reject_ignored: true`.
41
+
7
42
  ## Version 8.6.1 - 2025-01-10
8
43
 
9
44
  ### Fixed
10
45
 
46
+ - find draft root in authorization checks when entity has recursive compositions
11
47
  - `default-env.json` was not loaded anymore when in production mode.
12
48
  - i18n texts like `1` or `true` were returned as numbers, or booleans instead of strings
13
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=N1K4iJe5n+tnvMRTgtP24w_A currency code as specified in ISO 4217
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=dkCbzc6I8aK8glGoJdGKfg_A country/region code as specified in ISO 3166-1
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=E/M7wAVsS7H/AhkTdqcvtg_A language code as specified in ISO 639-1
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=t9D7CV474t7qx3yqxKwwVw_A user's unique ID
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 = `[cds] - launched at ${new Date().toLocaleString()}, version: ${cds.version}, in`
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
 
@@ -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
- const options = cdsc._options.for.sql(o)
37
- if (typeof beforeCsn === 'string') beforeCsn = JSON.parse(beforeCsn)
38
- const { afterImage, drops, createsAndAlters } = cdsc.to.deltaSql (csn, options, beforeCsn || {definitions: {}, $version: '2.0'} ); // FIXME: As default value in compiler API?
39
- return {
40
- afterImage,
41
- drops: unfold_ddl(drops, csn, options),
42
- createsAndAlters: unfold_ddl(createsAndAlters, csn, options)
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) {
@@ -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
- class Float extends number {}
63
- class Double extends Float {}
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
- Float, Double, Decimal,
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,
@@ -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
- // eslint-disable-next-line no-console
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
@@ -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
- /** @private */ _target4 (t,...etc) {
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}, or a {SET}`
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) {
@@ -19,6 +19,7 @@ class Validation {
19
19
  this.target = target
20
20
  this.options = options
21
21
  this.insert = options.insert ?? options.mandatories
22
+ this.rejectIgnore = options.rejectIgnore
22
23
  this.cleanse = options.cleanse !== false
23
24
  }
24
25
 
@@ -157,7 +158,6 @@ const $any = class any {
157
158
  if (d['@cds.on.update']) return true
158
159
  if (d['@Core.Computed']) return true
159
160
  if (d['@Common.FieldControl']?.['#'] === 'ReadOnly') return true
160
- if (d['@cds.api.ignore']) return true
161
161
  else return false
162
162
  })
163
163
  }
@@ -192,6 +192,7 @@ const $any = class any {
192
192
  class struct extends $any {
193
193
  validate (data, path, /** @type {Validation} */ ctx, elements = this.elements, skip={}) {
194
194
  const path_ = !path ? [] : [...path, this.name]; if (path?.row) path_.push({...path})
195
+ if (data == null) return
195
196
  // check for required elements in case of inserts -- note: null values are handled in the payload loop below
196
197
  if (ctx.insert || data && path_.length && this._is_insert(data)) for (let each of this._required (elements)) {
197
198
  if (each.name in data) continue // got value for required element
@@ -204,7 +205,7 @@ class struct extends $any {
204
205
  // check values of given data
205
206
  for (let each in data) { // will work for structured payloads as well as flattened ones with universal CSN
206
207
  let /** @type {$any} */ d = elements[each]
207
- if (!d) ctx.unknown (each, this, data)
208
+ if (!d || (d['@cds.api.ignore'] && ctx.rejectIgnore)) ctx.unknown (each, this, data)
208
209
  else if (ctx.cleanse && d._is_readonly() && !d.key) delete data[each]
209
210
  // @Core.Immutable processed only for root, children are handled when knowing db state
210
211
  else if (ctx.cleanse && d['@Core.Immutable'] && !ctx.insert && !path) delete data[each]
@@ -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.is_factory ? options4 (datasource, options) : {}
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]
@@ -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 && o.service._is_service_class) {
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
- if (srv[_ready]) srv[_ready](srv)
82
+ srv[_ready]?.(srv)
90
83
  return srv
91
84
  })))
92
85
 
@@ -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
  //--------------------------------------------------------------------------
@@ -1,90 +1,75 @@
1
- const cds = require('..'), { path, isfile, redacted } = cds.utils
2
- const paths = Array.from (new Set ([ cds.root, ...require.resolve.paths('x') ]))
3
- const DEBUG = cds.debug('cds.service.factory'); DEBUG?.({ 'cds.root':cds.root, paths })
4
-
5
- /** @typedef {import('./cds.Service')} Service @type { (()=>Service) & (new()=>Service) } */
6
- const ServiceFactory = function (name, model, options) { //NOSONAR
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
- let it /* eslint-disable no-cond-assign */
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
- async function _use (it) {
23
- if (it._is_service_class) return new it (name,model,o)
24
- if (it._is_service_instance) return it
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
- function _required() {
32
- const kind = o.kind = serve && def['@kind'] || o.kind || 'app-service'
33
- if (_require[kind]) return _require[kind]
34
- const {impl} = cds.requires[kind] || cds.requires.kinds[kind] || cds.error `No configuration found for 'cds.requires.${kind}'`
35
- DEBUG?.('requires',{kind,impl})
36
- return _require[kind] = _require (impl || cds.error `No 'impl' configured for 'cds.requires.${kind}'`)
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
- const _require = (it,d) => {
41
- d && DEBUG?.('requires',{ service: d.name, source:_source(d), impl:it })
42
- if (it.startsWith('@sap/cds/')) it = cds.home + it.slice(8) //> for local tests in @sap/cds dev
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 sibling = (d) => {
61
- const { dir, name } = path.parse(_source(d)), TS = process.env.CDS_TYPESCRIPT
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 found = TS && _resolve(dir,subdir,name+'.ts') || _resolve(dir,subdir,name+'.js') || _resolve(dir,subdir,name+'.mjs')
64
- if (found) return found //> equiv to '.'+found.slice(home.length)
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
- // Note: @source has precedence over $location for csn.json cases
69
- const _source = (d) => d['@source'] || (d['@source'] = d.$location && d.$location.file.replace(/\\/g, '/') || '.')
70
- const _relative = (d,x,cwd=cds.root) => typeof x !== 'string' ? x : path.resolve (cwd, _source(d),'..',x)
71
- const _resolve = (...args) => {
72
- const f = path.join(...args)
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
- * Used internally by cds.connect() and cds.serve() to add handlers
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 init (srv) {
86
- const { impl } = srv.options
87
- if (typeof impl === 'function' && !/^class\b/.test(impl))
88
- await impl.call (srv,srv)
89
- return await srv.init?.() ?? srv
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
- const xssec = require('./xssec')
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
- return function ias_auth (req, _, next) {
82
- if (!req.headers.authorization) return next()
83
- const token = req.headers.authorization.slice(7) // skip /^bearer /
84
- xssec.createSecurityContext(token, credentials, 'IAS', function (err, securityContext, tokenInfo) {
85
-
86
- if (err) LOG.warn('User could not be authenticated due to error:', err)
87
- if (!securityContext) return next(401)
88
- else req.authInfo = securityContext //> compat req.authInfo
89
-
90
- const ctx = cds.context
91
- ctx.user = getUser(tokenInfo)
92
- ctx.tenant = tokenInfo.getZoneId()
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
- const xssec = require('./xssec')
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
- // NOTE: Use named function for better stack traces... for the actual middleware, of course, not that much for the factory!
47
- return function jwt_auth (req, _, next) {
48
- if (!req.headers.authorization) return next()
49
- const token = req.headers.authorization.slice(7) // skip /^bearer /
50
- xssec.createSecurityContext(token, credentials, function (err, securityContext, tokenInfo) {
51
-
52
- if (err) LOG.warn('User could not be authenticated due to error:', err)
53
- if (!securityContext) return next(401)
54
- else req.authInfo = securityContext //> compat req.authInfo
55
-
56
- const ctx = cds.context
57
- ctx.user = getUser(tokenInfo)
58
- ctx.tenant = tokenInfo.getZoneId()
59
- next()
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.v3 || xssec // use v3 compat api // REVISIT: why ???
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