@sap/cds 9.0.4 → 9.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/bin/deploy.js +29 -0
  3. package/bin/serve.js +1 -5
  4. package/lib/compile/etc/csv.js +11 -6
  5. package/lib/compile/for/lean_drafts.js +29 -7
  6. package/lib/compile/load.js +8 -5
  7. package/lib/compile/to/hdbtabledata.js +1 -1
  8. package/lib/dbs/cds-deploy.js +5 -34
  9. package/lib/env/cds-env.js +2 -1
  10. package/lib/env/cds-requires.js +4 -1
  11. package/lib/env/defaults.js +0 -11
  12. package/lib/env/schemas/cds-rc.js +218 -6
  13. package/lib/index.js +38 -38
  14. package/lib/log/cds-error.js +12 -11
  15. package/lib/log/format/json.js +1 -1
  16. package/lib/ql/SELECT.js +31 -0
  17. package/lib/ql/resolve.js +1 -1
  18. package/lib/req/context.js +1 -1
  19. package/lib/req/request.js +1 -1
  20. package/lib/req/validate.js +17 -19
  21. package/lib/srv/cds.Service.js +18 -28
  22. package/lib/srv/middlewares/auth/ias-auth.js +29 -2
  23. package/lib/srv/middlewares/auth/jwt-auth.js +11 -1
  24. package/lib/srv/middlewares/auth/xssec.js +1 -1
  25. package/lib/srv/srv-models.js +1 -1
  26. package/lib/srv/srv-tx.js +2 -2
  27. package/lib/utils/cds-utils.js +35 -2
  28. package/lib/utils/csv-reader.js +1 -1
  29. package/lib/utils/inflect.js +2 -2
  30. package/lib/utils/tar.js +60 -23
  31. package/lib/utils/version.js +18 -0
  32. package/libx/_runtime/cds.js +1 -1
  33. package/libx/_runtime/common/aspects/any.js +1 -23
  34. package/libx/_runtime/common/generic/crud.js +1 -3
  35. package/libx/_runtime/common/generic/input.js +113 -52
  36. package/libx/_runtime/common/generic/sorting.js +1 -1
  37. package/libx/_runtime/common/generic/temporal.js +0 -6
  38. package/libx/_runtime/common/utils/draft.js +1 -1
  39. package/libx/_runtime/common/utils/entityFromCqn.js +1 -1
  40. package/libx/_runtime/common/utils/propagateForeignKeys.js +1 -1
  41. package/libx/_runtime/common/utils/resolveView.js +2 -2
  42. package/libx/_runtime/common/utils/structured.js +2 -2
  43. package/libx/_runtime/common/utils/templateProcessor.js +0 -5
  44. package/libx/_runtime/common/utils/vcap.js +1 -1
  45. package/libx/_runtime/fiori/lean-draft.js +529 -143
  46. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +3 -2
  47. package/libx/_runtime/messaging/service.js +1 -1
  48. package/libx/_runtime/remote/utils/client.js +2 -1
  49. package/libx/common/assert/utils.js +2 -12
  50. package/libx/common/utils/streaming.js +4 -9
  51. package/libx/http/location.js +1 -0
  52. package/libx/odata/ODataAdapter.js +47 -43
  53. package/libx/odata/index.js +1 -1
  54. package/libx/odata/middleware/batch.js +6 -2
  55. package/libx/odata/middleware/create.js +1 -1
  56. package/libx/odata/middleware/error.js +27 -17
  57. package/libx/odata/middleware/operation.js +15 -21
  58. package/libx/odata/middleware/stream.js +1 -1
  59. package/libx/odata/parse/afterburner.js +22 -8
  60. package/libx/odata/parse/cqn2odata.js +16 -10
  61. package/libx/odata/parse/grammar.peggy +185 -134
  62. package/libx/odata/parse/parser.js +1 -1
  63. package/libx/odata/utils/index.js +1 -36
  64. package/libx/odata/utils/metadata.js +34 -1
  65. package/libx/odata/utils/odataBind.js +2 -1
  66. package/libx/odata/utils/result.js +22 -20
  67. package/libx/queue/index.js +7 -4
  68. package/libx/rest/RestAdapter.js +1 -2
  69. package/libx/rest/middleware/create.js +5 -2
  70. package/package.json +2 -2
  71. package/server.js +1 -1
  72. package/bin/deploy/to-hana.js +0 -1
  73. package/lib/utils/check-version.js +0 -9
  74. package/lib/utils/unit.js +0 -19
  75. package/libx/_runtime/cds-services/util/assert.js +0 -181
  76. package/libx/_runtime/types/api.js +0 -129
  77. package/libx/common/assert/validation.js +0 -109
package/lib/index.js CHANGED
@@ -1,23 +1,23 @@
1
- if (process.env.CDS_STRICT_NODE_VERSION !== 'false') require ('./utils/check-version.js')
1
+ if (process.env.CDS_STRICT_NODE_VERSION !== 'false') require('./utils/version').check()
2
2
  ! (global.__cds_loaded_from ??= new Set).add(__filename.toLowerCase()) // track from where we loaded cds, lowercase to avoid path duplicates on Windows
3
3
 
4
4
  const { AsyncLocalStorage } = require ('async_hooks')
5
5
  const context = new AsyncLocalStorage
6
6
 
7
7
  const { EventEmitter } = require('node:events')
8
- const cds = module.exports = global.cds = new class cds_facade extends EventEmitter {
8
+ const cds = exports = module.exports = global.cds = new class cds extends EventEmitter {
9
9
 
10
- /** Contexts and Transactions @type {import('./req/context')} */
10
+ /** Contexts and Transactions @type {import('./req/context.js')} */
11
11
  get context() { return context.getStore() }
12
12
  set context(x) { this._with(x) }
13
13
  _with (x,fn,...args) {
14
- const ctx = typeof x !== 'object' || x instanceof cds.EventContext ? x : x.context || cds.EventContext.for(x)
14
+ const ctx = typeof x !== 'object' || x instanceof exports.EventContext ? x : x.context || exports.EventContext.for(x)
15
15
  return fn ? context.run (ctx,fn,...args) : context.enterWith (ctx)
16
16
  }
17
- get spawn() { return super.spawn = require('./req/spawn') }
17
+ get spawn() { return super.spawn = require('./req/spawn.js') }
18
18
 
19
- /** @import {LinkedCSN} from './core/linked-csn' */
20
- /** @import {Service} from './srv/cds.Service' */
19
+ /** @import {LinkedCSN} from './core/linked-csn.js' */
20
+ /** @import {Service} from './srv/cds.Service.js' */
21
21
  /** @type LinkedCSN */ model = undefined
22
22
  /** @type Service */ db = undefined
23
23
  /** CLI args */ cli = { command:'', options:{}, argv:[] }
@@ -33,24 +33,24 @@ const cds = module.exports = global.cds = new class cds_facade extends EventEmit
33
33
 
34
34
  // Configuration & Information
35
35
  get requires() { return super.requires = this.env.requires._resolved() }
36
- get plugins() { return super.plugins = require('./plugins').activate() }
36
+ get plugins() { return super.plugins = require('./plugins.js').activate() }
37
37
  get version() { return super.version = require('../package.json').version }
38
- get env() { return super.env = require('./env/cds-env').for(this) }
38
+ get env() { return super.env = require('./env/cds-env.js').for(this) }
39
39
  get home() { return super.home = __dirname.slice(0,-4) }
40
- get schema() { return super.schema = require('./env/schemas') } // REVISIT: Better move that to cds-dk?
40
+ get schema() { return super.schema = require('./env/schemas/index.js') } // REVISIT: Better move that to cds-dk?
41
41
 
42
42
  // Loading and Compiling Models
43
- get compiler() { return super.compiler = require('./compile/cdsc') }
44
- get compile() { return super.compile = require('./compile/cds-compile') }
45
- get resolve() { return super.resolve = require('./compile/resolve') }
46
- get load() { return super.load = require('./compile/load') }
43
+ get compiler() { return super.compiler = require('./compile/cdsc.js') }
44
+ get compile() { return super.compile = require('./compile/cds-compile.js') }
45
+ get resolve() { return super.resolve = require('./compile/resolve.js') }
46
+ get load() { return super.load = require('./compile/load.js') }
47
47
  get get() { return super.get = this.load.parsed }
48
- get parse() { return super.parse = require('./compile/parse') }
49
- get minify() { return super.minify = require('./compile/minify') }
50
- get extend() { return super.extend = require('./compile/extend') }
51
- get deploy() { return super.deploy = require('./dbs/cds-deploy') }
52
- get localize() { return super.localize = require('./i18n/localize') }
53
- get i18n() { return super.i18n = require('./i18n') }
48
+ get parse() { return super.parse = require('./compile/parse.js') }
49
+ get minify() { return super.minify = require('./compile/minify.js') }
50
+ get extend() { return super.extend = require('./compile/extend.js') }
51
+ get deploy() { return super.deploy = require('./dbs/cds-deploy.js') }
52
+ get localize() { return super.localize = require('./i18n/localize.js') }
53
+ get i18n() { return super.i18n = require('./i18n/index.js') }
54
54
 
55
55
  // Model Reflection, Builtin types and classes
56
56
  get entities() { return this.db?.entities || this.model?.entities }
@@ -75,23 +75,23 @@ const cds = module.exports = global.cds = new class cds_facade extends EventEmit
75
75
  get _pending(){ return this.#pending ??= {} } #pending
76
76
  }
77
77
  get server() { return super.server = require('../server.js') }
78
- get serve() { return super.serve = require('./srv/cds-serve') }
79
- get connect() { return super.connect = require('./srv/cds-connect') }
78
+ get serve() { return super.serve = require('./srv/cds-serve.js') }
79
+ get connect() { return super.connect = require('./srv/cds-connect.js') }
80
80
  get outboxed() { return this.queued }
81
81
  get unboxed() { return this.unqueued }
82
- get queued() { return super.queued = require('../libx/queue').queued }
83
- get unqueued() { return super.unqueued = require('../libx/queue').unqueued }
84
- get middlewares() { return super.middlewares = require('./srv/middlewares') }
85
- get odata() { return super.odata = require('../libx/odata') }
86
- get auth() { return super.auth = require('./auth') }
82
+ get queued() { return super.queued = require('../libx/queue/index.js').queued }
83
+ get unqueued() { return super.unqueued = require('../libx/queue/index.js').unqueued }
84
+ get middlewares() { return super.middlewares = require('./srv/middlewares/index.js') }
85
+ get odata() { return super.odata = require('../libx/odata/index.js') }
86
+ get auth() { return super.auth = require('./auth.js') }
87
87
  shutdown() { this.app?.server && process.exit() } // is overridden in bin/serve.js
88
88
 
89
89
  // Core Services API
90
- get Service() { return super.Service = require('./srv/cds.Service') }
91
- get EventContext() { return super.EventContext = require('./req/context') }
92
- get Request() { return super.Request = require('./req/request') }
93
- get Event() { return super.Event = require('./req/event') }
94
- get User() { return super.User = require('./req/user') }
90
+ get Service() { return super.Service = require('./srv/cds.Service.js') }
91
+ get EventContext() { return super.EventContext = require('./req/context.js') }
92
+ get Request() { return super.Request = require('./req/request.js') }
93
+ get Event() { return super.Event = require('./req/event.js') }
94
+ get User() { return super.User = require('./req/user.js') }
95
95
  get validate() { return super.validate = require('./req/validate.js') }
96
96
 
97
97
  // Services, Protocols and Periphery
@@ -101,18 +101,18 @@ const cds = module.exports = global.cds = new class cds_facade extends EventEmit
101
101
  get RemoteService() { return super.RemoteService = require('../libx/_runtime/remote/Service.js') }
102
102
 
103
103
  // Helpers
104
- get utils() { return super.utils = require('./utils/cds-utils') }
105
- get error() { return super.error = require('./log/cds-error') }
106
- get exec() { return super.exec = require('../bin/serve').exec }
107
- get test() { return super.test = require('./test/cds-test') }
108
- get log() { return super.log = require('./log/cds-log') }
104
+ get utils() { return super.utils = require('./utils/cds-utils.js') }
105
+ get error() { return super.error = require('./log/cds-error.js') }
106
+ get exec() { return super.exec = require('../bin/serve.js').exec }
107
+ get test() { return super.test = require('./test/cds-test.js') }
108
+ get log() { return super.log = require('./log/cds-log.js') }
109
109
  get debug() { return super.debug = this.log.debug }
110
110
  clone(x) { return structuredClone(x) }
111
111
 
112
112
  // Querying and Databases
113
113
  get infer(){ return super.infer = require('./ql/cds.ql-infer.js') }
114
114
  get txs() { return super.txs = new this.Service('cds.tx') }
115
- get ql() { return super.ql = require('./ql/cds-ql') }
115
+ get ql() { return super.ql = require('./ql/cds-ql.js') }
116
116
  // get tx() { return super.tx = this.Service.prototype.tx.bind (new this.Service) } // Note: this would break too much of existing code/tests
117
117
  tx (..._) { return (this.db || this.txs).tx(..._) }
118
118
  run (..._) { return (this.db || typeof _[0] === 'function' && this.txs || this.error._no_primary_db).run(..._) }
@@ -21,7 +21,7 @@ const { format, inspect } = require('../utils/cds-utils')
21
21
  * @param {object} [details] - Additional error details
22
22
  * @param {Function} [caller] - The function calling this
23
23
  */
24
- const error = exports = module.exports = function cds_error ( status, message, details, caller ) {
24
+ const error = exports = module.exports = function error ( status, message, details, caller ) {
25
25
  if (typeof status !== 'number') [ status, message, details, caller ] = status.raw ? [ undefined, error.message(...arguments) ] : [ undefined, status, message, details ]
26
26
  if (typeof message === 'object') [ message, details, caller ] = [ undefined, message, details ]
27
27
  let err = details && 'stack' in details ? details : Object.assign (new Error (message, details), details)
@@ -61,16 +61,17 @@ exports.expected = ([,type], arg) => {
61
61
  return error (`Expected argument '${name}'${type}, but got: ${inspect(value)}`, undefined, error.expected)
62
62
  }
63
63
 
64
-
65
- const system_errors = [
66
- TypeError,
67
- ReferenceError,
68
- SyntaxError,
69
- RangeError,
70
- URIError
71
- ]
72
- exports.isSystemError = err => system_errors.some(e => err instanceof e)
73
-
64
+ exports.isSystemError = err => {
65
+ // all errors thrown by the peggy parser should not crash the app
66
+ if (err.name === 'SyntaxError' && err.constructor?.name === 'peg$SyntaxError') return false
67
+ return err.name in {
68
+ TypeError:1,
69
+ ReferenceError:1,
70
+ SyntaxError:1,
71
+ RangeError:1,
72
+ URIError:1,
73
+ }
74
+ }
74
75
 
75
76
  //
76
77
  // Private helpers ...
@@ -1,4 +1,4 @@
1
- const cds = require('../../')
1
+ const cds = require('../..')
2
2
 
3
3
  const util = require('util')
4
4
 
package/lib/ql/SELECT.js CHANGED
@@ -1,4 +1,6 @@
1
+ const { pipeline } = require('stream/promises')
1
2
  const Whereable = require('./Whereable')
3
+ const Query = require('./cds.ql-Query')
2
4
  const is_number = x => !isNaN(x)
3
5
  const cds = require('../index')
4
6
  const $ = Object.assign
@@ -163,6 +165,35 @@ class SELECT extends Whereable {
163
165
  return this
164
166
  }
165
167
 
168
+ async foreach(callback) {
169
+ for await (const row of this) {
170
+ callback(row)
171
+ }
172
+ }
173
+
174
+ [Symbol.asyncIterator]() {
175
+ return (async function* (self) {
176
+ const srv = self._srv || cds.db || cds.error`Can't execute query as no primary database is connected.`
177
+ const stream = await srv.send({
178
+ iterator: true,
179
+ objectMode: true,
180
+ query: self,
181
+ })
182
+ for await (const row of stream) yield row
183
+ })(this)
184
+ }
185
+
186
+ async pipeline(...args) {
187
+ const srv = this._srv || cds.db || cds.error`Can't execute query as no primary database is connected.`
188
+ const res = await srv.send({
189
+ iterator: true,
190
+ objectMode: false,
191
+ query: this,
192
+ })
193
+ if (args.length) return pipeline(res, ...args.map(a => a instanceof Query ? stream => a.entries(stream) : a))
194
+ return res
195
+ }
196
+
166
197
  hints (...args) {
167
198
  if (args.length) this.SELECT.hints = args.flat()
168
199
  return this
package/lib/ql/resolve.js CHANGED
@@ -1,5 +1,5 @@
1
1
  const { resolveView, getTransition } = require('../../libx/_runtime/common/utils/resolveView')
2
- const cds = require('../../lib')
2
+ const cds = require('..')
3
3
 
4
4
  const PERSISTENCE_TABLE = '@cds.persistence.table'
5
5
  const _isPersistenceTable = target =>
@@ -1,6 +1,6 @@
1
1
  const cds = require ('../index'), { uuid } = cds.utils
2
2
  const async_events = { succeeded:1, failed:1, done:1, commit:1 }
3
- const locale = require('./../i18n/locale')
3
+ const locale = require('../i18n/locale')
4
4
  const { EventEmitter } = require('events')
5
5
 
6
6
  /**
@@ -110,7 +110,7 @@ class Request extends require('./event') {
110
110
  delete err.stack
111
111
  throw err
112
112
  }
113
- let e = this.error(...args)
113
+ let e = this._errors.add(4, ...args)
114
114
  if (!('stack' in e)) Error.captureStackTrace (e = Object.assign(new Error,e), this.reject)
115
115
  if (!('message' in e)) e.message = String (e.code || e.status)
116
116
  throw e
@@ -24,15 +24,16 @@ class Validation {
24
24
  this.cleanse = options.cleanse !== false
25
25
  }
26
26
 
27
- error (code, path, leaf, val, ...args) {
27
+ error (code, path, leaf, i18n, ...args) {
28
28
  const err = (this.errors ??= new ValidationErrors).add (code)
29
- if (this.options.path) path = [ this.options.path, ...path ] // e.g. used to prefic 'in/' for actions
29
+ if (this.options.path) path = [ this.options.path, ...path ] // e.g. used to prefix 'in/' for actions
30
30
  if (path) err.target = (!leaf ? path : path.concat(leaf)).reduce?.((p,n)=> (
31
31
  n?.row ? p + this.filter4(n) : //> some/entity(ID=1)...
32
32
  typeof n === 'number' ? p + `[${n}]` : //> some/array[1]...
33
33
  p && n ? p+'/'+n : n //> some/element...
34
34
  ),'')
35
- if (val !== undefined) err.args = [ val, ...args ]
35
+ if (typeof i18n === 'string') err.i18n = i18n
36
+ if (args.length) err.args = args
36
37
  return err
37
38
  }
38
39
 
@@ -64,10 +65,8 @@ class ValidationErrors extends Array {
64
65
  return err
65
66
  }
66
67
  static proto = Object.create (Error.prototype, {
68
+ stack: { configurable:true, get() { return this.message } },
67
69
  message: { writable:true, configurable:true },
68
- stack: { configurable:true, get() { return this.message },
69
- set(v) { Object.defineProperty (this, 'stack', { value:v, writable:true, configurable:true }) },
70
- },
71
70
  status: { value: 400 },
72
71
  })
73
72
  }
@@ -100,30 +99,30 @@ const $any = class any {
100
99
  const asserts = []
101
100
  const type_check = conf.strict && this.strict_check || this.type_check
102
101
  if (type_check) {
103
- asserts.push ((v,p,ctx) => v == null || type_check(v) || ctx.error ('ASSERT_DATA_TYPE', p, this.name, v, this ))
102
+ asserts.push ((v,p,ctx) => v == null || type_check(v) || ctx.error ('ASSERT_DATA_TYPE', p, this.name, null, v, this ))
104
103
  }
105
104
  if (this._is_mandatory()) {
106
- asserts.push ((v,p,ctx) => v != null && v.trim?.() !== '' || ctx.error ('ASSERT_NOT_NULL', p, this.name, v)) // ASSERT_NOT_NULL is misleading -> should be ASSERT_REQUIRED
105
+ asserts.push ((v,p,ctx) => v != null && v.trim?.() !== '' || ctx.error ('ASSERT_NOT_NULL', p, this.name, this['@mandatory.message'] || this['@mandatory'], v)) // ASSERT_NOT_NULL is misleading -> should be ASSERT_REQUIRED
107
106
  }
108
107
  if (this['@assert.format']) {
109
108
  const format = new RegExp(this['@assert.format'],'u')
110
- asserts.push ((v,p,ctx) => v == null || format.test(v) || ctx.error ('ASSERT_FORMAT', p, this.name, v, format))
109
+ asserts.push ((v,p,ctx) => v == null || format.test(v) || ctx.error ('ASSERT_FORMAT', p, this.name, this['@assert.format.message'], v, format))
111
110
  }
112
111
  if (this['@assert.range'] && !this.enum) {
113
112
  const [ min, max ] = this['@assert.range']
114
113
  if (min['='] === '_') min.val = -Infinity
115
114
  if (max['='] === '_') max.val = +Infinity
116
115
  asserts.push (
117
- min.val !== undefined && max.val !== undefined ? (v,p,ctx) => v == null || min.val < v && v < max.val || ctx.error ('ASSERT_RANGE', p, this.name, v, '>'+min.val, '<'+max.val) :
118
- min.val !== undefined ? (v,p,ctx) => v == null || min.val < v && v <= max || ctx.error ('ASSERT_RANGE', p, this.name, v, '>'+min.val, max) :
119
- max.val !== undefined ? (v,p,ctx) => v == null || min <= v && v < max.val || ctx.error ('ASSERT_RANGE', p, this.name, v, min, '<'+max.val) :
120
- (v,p,ctx) => v == null || min <= v && v <= max || ctx.error ('ASSERT_RANGE', p, this.name, v, min, max)
116
+ min.val !== undefined && max.val !== undefined ? (v,p,ctx) => v == null || min.val < v && v < max.val || ctx.error ('ASSERT_RANGE', p, this.name, this['@assert.range.message'], v, '>'+min.val, '<'+max.val) :
117
+ min.val !== undefined ? (v,p,ctx) => v == null || min.val < v && v <= max || ctx.error ('ASSERT_RANGE', p, this.name, this['@assert.range.message'], v, '>'+min.val, max) :
118
+ max.val !== undefined ? (v,p,ctx) => v == null || min <= v && v < max.val || ctx.error ('ASSERT_RANGE', p, this.name, this['@assert.range.message'], v, min, '<'+max.val) :
119
+ (v,p,ctx) => v == null || min <= v && v <= max || ctx.error ('ASSERT_RANGE', p, this.name, null, v, min, max)
121
120
  )
122
121
  }
123
122
  if (this['@assert.enum'] || this['@assert.range'] && this.enum) {
124
123
  const vals = Object.entries(this.enum).map(([k,v]) => 'val' in v ? v.val : k)
125
124
  const enums = vals.reduce((a,v) => (a[v]=true, a),{})
126
- asserts.push ((v,p,ctx) => v == null || v in enums || vals.some(x => x == v) || ctx.error ('ASSERT_ENUM', p, this.name, typeof v === 'string' ? `"${v}"` : v, vals.join(', ')))
125
+ asserts.push ((v,p,ctx) => v == null || v in enums || vals.some(x => x == v) || ctx.error ('ASSERT_ENUM', p, this.name, this['@assert.enum.message'], typeof v === 'string' ? `"${v}"` : v, vals.join(', ')))
127
126
  }
128
127
  if (!asserts.length) return ()=>{} // nothing to do
129
128
  return (v,p,ctx) => asserts.forEach (a => a(v,p,ctx))
@@ -194,19 +193,19 @@ class struct extends $any {
194
193
  validate (data, path, /** @type {Validation} */ ctx, elements = this.elements, skip={}) {
195
194
  if (data == null) return
196
195
  const path_ = !path ? [] : [...path, this.name]; if (path?.row) path_.push({...path})
197
- if (typeof data !== 'object') return ctx.error ('ASSERT_DATA_TYPE', path_, this.name, data, this.target)
196
+ if (typeof data !== 'object') return ctx.error ('ASSERT_DATA_TYPE', path_, this.name, null, data, this.target)
198
197
  // check for required elements in case of inserts -- note: null values are handled in the payload loop below
199
198
  if (ctx.insert || data && path_.length && this._is_insert(data)) for (let each of this._required (elements)) {
200
199
  if (each.name in data) continue // got value for required element
201
200
  if (each.name in skip) continue // skip uplinks in deep inserts -> see Composition.validate()
202
201
  if (each.$struct in data) continue // got struct for flattened element/fk, e.g. {author:{ID:1}}
203
- if (each.elements || each.foreignKeys) continue // skip struct-likes as we check flat payloads above, and deep payloads via struct.validate()
202
+ if ((each.elements && each.kind !== 'param' ) || each.foreignKeys) continue // skip struct-likes as we check flat payloads above, and deep payloads via struct.validate(), parameters don't have flat elements
204
203
  if (each.isAssociation) continue // unmanaged associations are always ignored (no value like)
205
204
  else ctx.error ('ASSERT_NOT_NULL', path_, each.name) // ASSERT_NOT_NULL should be ASSERT_REQUIRED
206
205
  }
207
206
  // check values of given data
208
207
  for (let each in data) { // will work for structured payloads as well as flattened ones with universal CSN
209
- let /** @type {$any} */ d = elements[each]
208
+ let /** @type {$any} */ d = Object.hasOwn(elements, each) && elements[each]
210
209
  if (!d || (d['@cds.api.ignore'] && ctx.rejectIgnore)) ctx.unknown (each, this, data)
211
210
  else if (ctx.cleanse && d._is_readonly() && !d.key) delete data[each]
212
211
  // @Core.Immutable processed only for root, children are handled when knowing db state
@@ -242,8 +241,7 @@ class action extends struct {
242
241
  super.validate (data, path, ctx, this.params || {})
243
242
  }
244
243
 
245
- // REVISIT: why is e['@mandatory'] not checked here?
246
- _is_mandatory(e) { return e.notNull && !e.default } // params
244
+ _is_mandatory(e) { return e.notNull && !e.default || e._is_mandatory() } // params
247
245
  }
248
246
 
249
247
  /** Managed associations are struct-like, with foreign keys as elements to validate. */
@@ -24,32 +24,20 @@ class ConsumptionAPI {
24
24
  else return this.dispatch (new Event ({ event, data, headers }))
25
25
  }
26
26
 
27
- send (req, path, data, headers) {
28
- if (is_object(req)) return this.dispatch (req instanceof Request ? req : new Request(req))
29
- if (is_object(path)) return this.dispatch (new Request (path.is_linked //...
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
- schedule (method, path, data, headers) {
35
- // not great to normalize args... better way? need to 'merge' with after/every
36
- const req = method instanceof cds.Request ? method : new cds.Request(_nrm4skd(method, path, data, headers))
31
+
32
+ schedule (...args) {
33
+ const req = _req4 (...args), {ms4} = cds.utils
37
34
  return {
38
- after (ms) {
39
- req.queue ??= {}
40
- req.queue.after = ms
41
- return this
42
- },
43
- every (ms) {
44
- req.queue ??= {}
45
- req.queue.every = ms
46
- return this
47
- },
48
- then: (r, e) => {
49
- return cds.queued(this).send(req).then(r, e)
50
- }
35
+ after (t,u) { (req.queue ??= {}).after = ms4(t,u); return this },
36
+ every (t,u) { (req.queue ??= {}).every = ms4(t,u); return this },
37
+ then: (r,e) => cds.queued(this).send(req).then(r,e)
51
38
  }
52
39
  }
40
+
53
41
  get (...args) { return is_rest(args[0]) ? this.send('GET', ...args) : this.read (...args) }
54
42
  put (...args) { return is_rest(args[0]) ? this.send('PUT', ...args) : this.update (...args) }
55
43
  post (...args) { return is_rest(args[0]) ? this.send('POST', ...args) : this.create (...args) }
@@ -179,13 +167,15 @@ const is_query = x => x?.bind || Array.isArray(x) && !x.raw
179
167
  const is_rest = x => typeof x === 'string' && x[0] === '/'
180
168
  const _service_in = m => cds.linked(m).services?.[0]?.name
181
169
  || cds.error.expected `${{model:m}} to be a CSN with a single service definition`
182
- const _nrm4skd = (method, path, data, headers) => {
183
- if (typeof method === 'object') return method
184
- if (typeof path !== 'object') return { method, path, data, headers }
185
- if (path.is_linked) return { method, entity: path, data, headers }
186
- return { method, data: path, headers: data }
187
- }
188
170
 
171
+ const _req4 = (event, path, data, headers) => {
172
+ if (is_query(event)) return new Request({ query: event, data: path, headers })
173
+ if (is_object(event)) return event instanceof Request ? event : new Request(event)
174
+ if (is_object(path)) return new Request (path.is_linked //...
175
+ ? { method:event, entity:path, data, headers }
176
+ : { method:event, data:path, headers:data })
177
+ else return new Request({ method:event, path, data, headers })
178
+ }
189
179
 
190
180
  exports = module.exports = Service
191
181
  exports.Service = Service
@@ -3,7 +3,10 @@ const LOG = cds.log('auth')
3
3
 
4
4
  const {
5
5
  createSecurityContext,
6
+ Token,
6
7
  IdentityService,
8
+ XsuaaService,
9
+ XsuaaToken,
7
10
  errors: { ValidationError }
8
11
  } = require('./xssec')
9
12
 
@@ -18,6 +21,12 @@ module.exports = function ias_auth(config) {
18
21
  'Either bind an IAS instance, or switch to an authentication kind that does not require a binding.'
19
22
  )
20
23
 
24
+ // enable signature cache by default
25
+ serviceConfig.validation ??= {}
26
+ if (!('signatureCache' in serviceConfig.validation)) serviceConfig.validation.signatureCache = { enabled: true }
27
+ // activate decode cache if not already done or explicitely disabled by setting Token.decodeCache to false or undefined
28
+ if (Token.decodeCache === null) Token.enableDecodeCache()
29
+
21
30
  const auth_service = new IdentityService(credentials, serviceConfig)
22
31
  const user_factory = get_user_factory(credentials, skipped_attrs)
23
32
 
@@ -44,16 +53,26 @@ module.exports = function ias_auth(config) {
44
53
  validating_auth_service = new IdentityService(credentials, _serviceConfig)
45
54
  }
46
55
 
56
+ // xsuaa fallback allows to also accept XSUAA tokens during migration to IAS
57
+ // automatically enabled if xsuaa credentials are available
58
+ let xsuaa_service, xsuaa_user_factory
59
+ if (cds.env.requires.xsuaa?.credentials) {
60
+ const { credentials: xsuaa_credentials, config: xsuaa_serviceConfig = {} } = cds.env.requires.xsuaa
61
+ xsuaa_service = new XsuaaService(xsuaa_credentials, xsuaa_serviceConfig)
62
+ const get_xsuaa_user_factory = require('./jwt-auth')._get_user_factory
63
+ xsuaa_user_factory = get_xsuaa_user_factory(xsuaa_credentials, xsuaa_credentials.xsappname, 'xsuaa')
64
+ }
65
+
47
66
  return async function ias_auth(req, _, next) {
48
67
  if (!req.headers.authorization) return next()
49
68
 
50
69
  try {
51
70
  const _auth_service =
52
71
  validating_auth_service && req.host.match(/\.cert\./) ? validating_auth_service : auth_service
53
- const securityContext = await createSecurityContext(_auth_service, { req })
72
+ const securityContext = await createSecurityContext(xsuaa_service ? [_auth_service, xsuaa_service] : _auth_service, { req })
54
73
  const tokenInfo = securityContext.token
55
74
  const ctx = cds.context
56
- ctx.user = user_factory(tokenInfo)
75
+ ctx.user = tokenInfo instanceof XsuaaToken ? xsuaa_user_factory(tokenInfo) : user_factory(tokenInfo)
57
76
  ctx.tenant = tokenInfo.getZoneId()
58
77
  req.authInfo = securityContext //> compat req.authInfo
59
78
  } catch (e) {
@@ -73,6 +92,14 @@ function get_user_factory(credentials, skipped_attrs) {
73
92
  return function user_factory(tokenInfo) {
74
93
  const payload = tokenInfo.getPayload()
75
94
 
95
+ /*
96
+ * NOTE:
97
+ * for easier migration, xssec will offer IAS without policies via so-called XsuaaFallback.
98
+ * in that case, we would need to add the roles here based on the tokenInfo (similar to xsuaa-auth).
99
+ * however, it is not yet clear where the roles will be stored in IAS' tokenInfo object.
100
+ * further, stakeholders would need to configure the "extension" programmatically (e.g., in a custom server.js).
101
+ */
102
+
76
103
  const clientid = tokenInfo.getClientId()
77
104
  if (clientid === payload.sub) {
78
105
  //> grant_type === client_credentials or x509
@@ -3,7 +3,9 @@ const LOG = cds.log('auth')
3
3
 
4
4
  const {
5
5
  createSecurityContext,
6
+ Token,
6
7
  XsuaaService,
8
+ XsaService,
7
9
  errors: { ValidationError }
8
10
  } = require('./xssec')
9
11
 
@@ -16,7 +18,13 @@ module.exports = function jwt_auth(config) {
16
18
  'Either bind an XSUAA instance, or switch to an authentication kind that does not require a binding.'
17
19
  )
18
20
 
19
- const auth_service = new XsuaaService(credentials, serviceConfig)
21
+ // enable signature cache by default
22
+ serviceConfig.validation ??= {}
23
+ if (!('signatureCache' in serviceConfig.validation)) serviceConfig.validation.signatureCache = { enabled: true }
24
+ // activate decode cache if not already done or explicitely disabled by setting Token.decodeCache to false or undefined
25
+ if (Token.decodeCache === null) Token.enableDecodeCache()
26
+
27
+ const auth_service = !credentials.uaadomain ? new XsaService(credentials, serviceConfig) : new XsuaaService(credentials, serviceConfig)
20
28
  const user_factory = get_user_factory(credentials, credentials.xsappname, kind)
21
29
 
22
30
  return async function jwt_auth(req, _, next) {
@@ -76,3 +84,5 @@ function get_user_factory(credentials, xsappname, kind) {
76
84
  return new cds.User({ id, roles, attr, tokenInfo })
77
85
  }
78
86
  }
87
+
88
+ module.exports._get_user_factory = get_user_factory
@@ -1,6 +1,6 @@
1
1
  try {
2
2
  const xssec = require('@sap/xssec')
3
- module.exports = xssec // use v3 compat api // REVISIT: why ???
3
+ module.exports = xssec
4
4
  } catch (e) {
5
5
  if (e.code === 'MODULE_NOT_FOUND') e.message = `Cannot find '@sap/xssec'. Make sure to install it with 'npm i @sap/xssec'\n` + e.message
6
6
  throw e
@@ -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('`extensibility: true` is configured but table "cds.xt.Extensions" does not exist. Please redeploy.', 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
 
@@ -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 _unit() { return super._unit = require('./unit') }
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
- exports.local = (file) => file && relative(cwd,file)
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
@@ -1,6 +1,6 @@
1
1
  const { createReadStream, createWriteStream, promises: fsp } = require('fs')
2
2
  const { Readable } = require('stream')
3
- const cds = require('../../lib')
3
+ const cds = require('..')
4
4
 
5
5
  const SEPARATOR = /[,;\t]/
6
6
 
@@ -2,7 +2,7 @@
2
2
  this.singular4 = (dn,stripped) => {
3
3
  let n = dn.name || dn; if (stripped) n = n.match(last)[0]
4
4
  return dn['@singular'] || (
5
- /species|news$/i.test(n) ? n :
5
+ /(species|news)$/i.test(n) ? n :
6
6
  /ess$/.test(n) ? n : // Address
7
7
  /ees$/.test(n) ? n.slice(0, -1) : // Employees --> Employee
8
8
  /[sz]es$/.test(n) ? n.slice(0, -2) :
@@ -15,7 +15,7 @@ this.singular4 = (dn,stripped) => {
15
15
  this.plural4 = (dn,stripped) => {
16
16
  let n = dn.name || dn; if (stripped) n = n.match(last)[0]
17
17
  return dn['@plural'] || (
18
- /analysis|status|species|sheep|news$/i.test(n) ? n :
18
+ /(analysis|status|species|sheep|news)$/i.test(n) ? n :
19
19
  /[^aeiou]y$/.test(n) ? n.slice(0,-1) + 'ies' :
20
20
  /(s|x|z|ch|sh)$/.test(n) ? n + 'es' :
21
21
  n + 's'