@sap/cds 6.2.3 → 6.3.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 (67) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/apis/connect.d.ts +1 -1
  3. package/apis/cqn.d.ts +1 -1
  4. package/apis/internal/inference.d.ts +14 -0
  5. package/apis/ql.d.ts +40 -36
  6. package/apis/services.d.ts +23 -6
  7. package/bin/build/buildTaskHandler.js +3 -3
  8. package/bin/build/provider/buildTaskHandlerEdmx.js +1 -1
  9. package/bin/build/provider/buildTaskHandlerFeatureToggles.js +4 -3
  10. package/bin/build/provider/buildTaskHandlerInternal.js +2 -2
  11. package/bin/build/provider/java/index.js +2 -1
  12. package/bin/build/provider/mtx/index.js +2 -1
  13. package/bin/build/provider/mtx/resourcesTarBuilder.js +3 -2
  14. package/bin/build/provider/mtx-extension/index.js +2 -1
  15. package/bin/build/provider/mtx-sidecar/index.js +3 -1
  16. package/lib/auth/index.js +2 -1
  17. package/lib/auth/jwt-auth.js +64 -3
  18. package/lib/auth/xsuaa-auth.js +2 -3
  19. package/lib/compile/cdsc.js +1 -0
  20. package/lib/compile/etc/_localized.js +1 -0
  21. package/lib/dbs/cds-deploy.js +2 -1
  22. package/lib/env/cds-env.js +14 -49
  23. package/lib/env/cds-requires.js +13 -7
  24. package/lib/env/defaults.js +4 -0
  25. package/lib/i18n/localize.js +11 -8
  26. package/lib/index.js +1 -1
  27. package/lib/log/cds-log.js +2 -2
  28. package/lib/log/format/cf.js +16 -0
  29. package/lib/log/format/kibana.js +15 -2
  30. package/lib/ql/INSERT.js +12 -11
  31. package/lib/ql/Query.js +14 -7
  32. package/lib/ql/UPSERT.js +1 -0
  33. package/lib/ql/Whereable.js +6 -2
  34. package/lib/ql/cds-ql.js +2 -4
  35. package/lib/req/request.js +2 -0
  36. package/lib/srv/middlewares/cds-context.js +1 -1
  37. package/lib/srv/srv-dispatch.js +1 -0
  38. package/lib/srv/srv-tx.js +3 -3
  39. package/lib/utils/cds-utils.js +75 -30
  40. package/lib/utils/inflect.js +24 -0
  41. package/libx/_runtime/auth/strategies/ias-auth.js +1 -1
  42. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/error.js +9 -1
  43. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +23 -6
  44. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +1 -0
  45. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/validator/ValueValidator.js +27 -15
  46. package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +1 -1
  47. package/libx/_runtime/cds-services/services/utils/compareJson.js +11 -10
  48. package/libx/_runtime/cds-services/services/utils/differ.js +6 -4
  49. package/libx/_runtime/common/composition/data.js +29 -40
  50. package/libx/_runtime/common/composition/update.js +6 -19
  51. package/libx/_runtime/common/generic/paging.js +1 -1
  52. package/libx/_runtime/common/utils/resolveView.js +7 -13
  53. package/libx/_runtime/db/utils/generateAliases.js +1 -0
  54. package/libx/_runtime/fiori/generic/read.js +11 -4
  55. package/libx/_runtime/hana/execute.js +2 -2
  56. package/libx/_runtime/hana/search2cqn4sql.js +1 -0
  57. package/libx/_runtime/messaging/AMQPWebhookMessaging.js +1 -1
  58. package/libx/_runtime/messaging/enterprise-messaging-utils/EMManagement.js +5 -2
  59. package/libx/_runtime/messaging/enterprise-messaging.js +7 -1
  60. package/libx/_runtime/messaging/file-based.js +1 -1
  61. package/libx/_runtime/messaging/message-queuing.js +5 -2
  62. package/libx/_runtime/messaging/outbox/utils.js +1 -1
  63. package/libx/_runtime/messaging/service.js +5 -3
  64. package/libx/odata/cqn2odata.js +4 -1
  65. package/libx/odata/utils.js +8 -7
  66. package/libx/rest/RestAdapter.js +1 -4
  67. package/package.json +1 -1
@@ -1,4 +1,4 @@
1
- const { isfile, fs, path } = require('../utils/cds-utils')
1
+ const { isdir, isfile, fs, path } = require('../utils/cds-utils')
2
2
  const DEFAULTS = require('./defaults'), defaults = require.resolve ('./defaults')
3
3
  const compat = require('./compat')
4
4
  const presets = require('./presets')
@@ -6,7 +6,7 @@ const serviceBindings = require('./serviceBindings');
6
6
 
7
7
 
8
8
  /**
9
- * Both a config inctance as well as factory for.
9
+ * Both a config instance as well as factory for.
10
10
  */
11
11
  class Config {
12
12
 
@@ -28,7 +28,7 @@ class Config {
28
28
  constructor (_context, _home, _defaults=true) {
29
29
  Object.assign (this, { _context, _home, _sources:[] })
30
30
 
31
- // 0. determine profiles from NODE_ENV+ CDS_ENV
31
+ // 0. determine profiles from NODE_ENV + CDS_ENV
32
32
  const { NODE_ENV, CDS_ENV } = process.env, profiles = []
33
33
  if (NODE_ENV) profiles.push (NODE_ENV)
34
34
  if (CDS_ENV) profiles.push (...CDS_ENV.split(/\s*,\s*/))
@@ -52,29 +52,29 @@ class Config {
52
52
  const sources = Config.sources(_home, _context)
53
53
 
54
54
  // 2. read config sources in defined order
55
- for (const source of sources) {
56
- this._load(source.path, source.file, source.mapper, this._profiles, false)
55
+ for (const { path, file, mapper } of sources) {
56
+ this._load(path, file, mapper, this._profiles, false)
57
57
  }
58
58
 
59
59
  // 3. read important (!) profiles from config sources in defined order
60
60
  const important = new Set(this.profiles.map( profile => `${profile}!` ).filter( profile => this._profiles._defined.has( profile ) ));
61
61
  if (important.size > 0) {
62
- for (const source of sources) {
63
- this._load(source.path, source.file, source.mapper, important, true)
62
+ for (const { path, file, mapper } of sources) {
63
+ this._load(path, file, mapper, important, true)
64
64
  }
65
65
  }
66
66
 
67
- // 4. link dependant services (through kind/use)
67
+ // 4. link dependent services (through kind/use)
68
68
  this._link_required_services()
69
69
 
70
70
  // 5. add process env
71
71
  this._add_process_env(_context, _home)
72
72
 
73
- // 6. link dependant services again -> e.g. to allow things like CDS_requires_db=sql
73
+ // 6. link dependent services again -> e.g. to allow things like CDS_requires_db=sql
74
74
  this._link_required_services()
75
75
 
76
76
  // 7. complete service configurations from cloud service bindings
77
- this._add_cloud_service_bindings({ VCAP_SERVICES: process.env.VCAP_SERVICES, SERVICE_BINDING_ROOT: process.env.SERVICE_BINDING_ROOT })
77
+ this._add_cloud_service_bindings(process.env)
78
78
 
79
79
  // 8. Add compatibility for mtx
80
80
  if (this.requires && this.requires.db) {
@@ -480,48 +480,13 @@ function _readJson (file) {
480
480
 
481
481
 
482
482
 
483
- function _readFromDir (p, isDir) {
484
- if (typeof isDir === "undefined") {
485
- try {
486
- const entry = fs.statSync(p)
487
- if (entry.isDirectory()) {
488
- isDir = true
489
- } else if (isFile(p, entry)) {
490
- isDir = false
491
- } else {
492
- return undefined
493
- }
494
- } catch (e) {
495
- return undefined
496
- }
497
- }
498
- if (isDir) {
483
+ function _readFromDir (p) {
484
+ if (isdir(p)) {
499
485
  const result = {}
500
- const entries = fs.readdirSync(p, {withFileTypes: true})
501
- for (let entry of entries) {
502
- const entryPath = path.join(p, entry.name)
503
- if (entry.isDirectory()) {
504
- result[entry.name] = _readFromDir(entryPath, true)
505
- } else if (isFile(entryPath, entry)) {
506
- result[entry.name] = _readFromDir(entryPath, false)
507
- }
508
- }
486
+ for (const dirent of fs.readdirSync(p)) result[dirent] = _readFromDir(path.join(p, dirent))
509
487
  return result
510
- } else {
511
- return _value4(fs.readFileSync(p, "utf-8"))
512
- }
513
-
514
- function isFile(p, entry) {
515
- if (entry.isFile()) return true
516
- if (entry.isSymbolicLink()) {
517
- // Kubernetes credentials use symlinks
518
- const target = fs.realpathSync(p)
519
- const targetStat = fs.statSync(target)
520
-
521
- if (targetStat.isFile()) return true
522
- }
523
- return false
524
488
  }
489
+ return _value4(fs.readFileSync(p, "utf-8"))
525
490
  }
526
491
 
527
492
 
@@ -38,6 +38,9 @@ exports = module.exports = {
38
38
  }
39
39
 
40
40
 
41
+ const admin = [ 'cds.Subscriber', 'admin' ]
42
+ const builder = [ 'cds.ExtensionDeveloper', 'cds.UIFlexDeveloper' ]
43
+
41
44
  const _authentication_strategies = {
42
45
 
43
46
  "basic-auth": {
@@ -49,12 +52,13 @@ const _authentication_strategies = {
49
52
  "mocked-auth": {
50
53
  kind: 'basic-auth',
51
54
  users: {
52
- alice: { roles: ['admin', 'cds.Subscriber'] },
53
- bob: { roles: ['builder'] },
54
- carol: { tenant: 't1', roles: ['admin', 'cds.Subscriber', 'cds.ExtensionDeveloper', 'cds.UIFlexDeveloper'] },
55
- dave: { tenant: 't1', roles: ['cds.Subscriber'], features: [] }, // user-specific features
56
- erin: { tenant: 't2', roles: ['admin', 'cds.Subscriber', 'cds.ExtensionDeveloper'] },
57
- fred: { tenant: 't2', roles: [], features: ['isbn'] },
55
+ alice: { tenant: 't1', roles: [ ...admin ] },
56
+ bob: { tenant: 't1', roles: [ ...builder ] },
57
+ carol: { tenant: 't1', roles: [ ...admin, ...builder ] },
58
+ dave: { tenant: 't1', roles: [ ...admin ], features: [] },
59
+ erin: { tenant: 't2', roles: [ ...admin, ...builder ] },
60
+ fred: { tenant: 't2', features: ['isbn'] },
61
+ me: { tenant: 't1', features: ['*'] },
58
62
  yves: { roles: ['internal-user'] },
59
63
  '*': true
60
64
  },
@@ -65,14 +69,16 @@ const _authentication_strategies = {
65
69
  },
66
70
  "jwt-auth": {
67
71
  strategy: 'JWT', // REVISIT: Can be removed when we switch to new auth middlewars
72
+ kind: 'jwt-auth',
68
73
  vcap: { label: 'xsuaa' }
69
74
  },
70
75
  "ias-auth": {
71
76
  kind: 'ias-auth',
72
77
  vcap: { label: 'identity' }
73
78
  },
74
- "xsuaa": {
79
+ "xsuaa": { // CLARIFY why is this not -auth postfixed?
75
80
  strategy: 'xsuaa', // REVISIT: Can be removed when we switch to new auth middlewars
81
+ kind: 'xsuaa',
76
82
  vcap: { label: 'xsuaa' }
77
83
  },
78
84
  "dummy-auth": {
@@ -12,6 +12,10 @@ module.exports = {
12
12
  },
13
13
 
14
14
  features: {
15
+ "[schevo]": {
16
+ schema_evolution: true,
17
+ },
18
+ schema_evolution: false,
15
19
  folders: 'fts/*', // where to find feature toggles -> switch on by default when released
16
20
  cls: major > 12 || major == 12 && minor >= 18,
17
21
  live_reload: !production,
@@ -10,8 +10,8 @@ module.exports = Object.assign (localize, {
10
10
  })
11
11
 
12
12
 
13
- function localize (model, /*with:*/ locale, aString) {
14
- const _localize = bundle => localizeString (aString, bundle)
13
+ function localize (model, /*with:*/ locale, aString, extBundle) {
14
+ const _localize = bundle => localizeString (aString, bundle, extBundle)
15
15
 
16
16
  const bundle = bundles4 (model, locale)
17
17
  if (Array.isArray(locale)) { // array of multiple locales
@@ -29,14 +29,14 @@ function localize (model, /*with:*/ locale, aString) {
29
29
 
30
30
  const TEXT_KEY_MARKER = 'i18n>'
31
31
  const TEXT_KEYS = /{b?i18n>([^"}]+)}/g
32
- function localizeString (aString, bundle) {
32
+ function localizeString (aString, bundle, extBundle) {
33
33
  if (!bundle || !aString) return aString
34
34
  if (typeof aString === 'object') aString = JSON.stringify(aString, null, 2)
35
35
  // quick check for presence of any text key, to avoid expensive operation below
36
36
  if (!aString.includes(TEXT_KEY_MARKER)) return aString
37
37
  const escape = aString.startsWith('<?xml') ? escapeXmlAttr : /^[{[]/.test(aString) ? escapeJson : v=>v
38
38
  return aString.replace (TEXT_KEYS, (_, key) => {
39
- const val = bundle[key]
39
+ const val = (extBundle && extBundle[key]) || bundle[key]
40
40
  return val ? escape(val) : key
41
41
  })
42
42
  }
@@ -155,11 +155,14 @@ function folders4 (model) {
155
155
  // foo/node_modules/reuse-level-1/model.cds
156
156
  // foo/node_modules/reuse-level-2/model.cds
157
157
  if (!model.$sources) return []
158
- const folders=[]; for (let src of model.$sources) {
158
+ const srcFolders = new Set (model.$sources.map (dirname))
159
+ const folders = []
160
+ srcFolders.forEach(src => {
159
161
  let folder = folder4 (src)
160
- if (!folder || folders.includes(folder)) continue
161
- folders.push(folder) // use an array here to not screw up the folder order
162
- }
162
+ if (folder && !folders.includes(folder)) {
163
+ folders.push(folder) // use an array here to not screw up the folder order
164
+ }
165
+ })
163
166
 
164
167
  Object.defineProperty (model, '_i18nfolders', {value:folders})
165
168
  return folders.reverse()
package/lib/index.js CHANGED
@@ -132,7 +132,7 @@ extend (cds.__proto__) .with (lazified ({
132
132
  const odp = Object.defineProperty, _global = (_,...pp) => pp.forEach (p => odp(global,p,{
133
133
  configurable:true, get:()=>{ let v=cds[_][p]; odp(this,p,{value:v}); return v }
134
134
  }))
135
- _global ('ql','SELECT','INSERT','UPDATE','DELETE','CREATE','DROP')
135
+ _global ('ql','SELECT','INSERT','UPSERT','UPDATE','DELETE','CREATE','DROP')
136
136
  _global ('parse','CDL','CQL','CXL')
137
137
 
138
138
  // Check Node.js version
@@ -136,7 +136,7 @@ exports.winstonLogger = (options) => (label, level) => {
136
136
  return simple (label, level, ...args)
137
137
  },
138
138
  get json() {
139
- return this._json || (this._json = require('./format/kibana'))
139
+ return this._json || (this._json = require('./format/cf'))
140
140
  }
141
141
  }
142
142
 
@@ -149,7 +149,7 @@ exports.winstonLogger = (options) => (label, level) => {
149
149
  *
150
150
  * @param {string} label the label to prefix to log output
151
151
  * @param {number} level the log level to enable -> 0=off, 1=error, 2=warn, 3=info, 4=debug, 5=trace
152
- * @param {any[]} args the arguments passed to Logger.debug|log|info|wanr|error()
152
+ * @param {any[]} args the arguments passed to Logger.debug|log|info|warn|error()
153
153
  */
154
154
  exports.format = (
155
155
  process.env.NODE_ENV === 'production' && cds.env.features.kibana_formatter ? log.formatters.json :
@@ -0,0 +1,16 @@
1
+ const cds = require ('../../')
2
+ const kibana = require('./kibana')
3
+
4
+ /*
5
+ * extension of log formatter for kibana that additionally logs Cloud Foundry specific data
6
+ */
7
+ module.exports = (module, level, ...args) => {
8
+ const toLog = kibana.addFields(module, level, ...args)
9
+
10
+ toLog.layer = 'cds'
11
+
12
+ // cds.context._ instead of cds.context.http because of messaging
13
+ toLog.tenant_subdomain = cds.context?._?.req?.authInfo?.getSubdomain()
14
+
15
+ return kibana.format(toLog)
16
+ }
@@ -6,7 +6,12 @@ const _l2l = { 1: 'error', 2: 'warn', 3: 'info', 4: 'debug', 5: 'trace' }
6
6
  /*
7
7
  * log formatter for kibana
8
8
  */
9
- module.exports = (module, level, ...args) => {
9
+ module.exports = exports = (module, level, ...args) => {
10
+ const toLog = exports.addFields(module, level, ...args)
11
+ return exports.format(toLog)
12
+ }
13
+
14
+ exports.addFields = (module, level, ...args) => {
10
15
  // config
11
16
  const { user: log_user , kibana_custom_fields } = cds.env.log
12
17
 
@@ -28,7 +33,11 @@ module.exports = (module, level, ...args) => {
28
33
  const req = cds.context._ && cds.context._.req
29
34
  if (req && req.headers)
30
35
  for (const k in req.headers)
31
- toLog[k.replace(/-/g, '_')] = k.match(/authorization/i) ? `${req.headers[k].split(' ')[0]} ***` : req.headers[k]
36
+ toLog[k.replace(/-/g, '_')] = (() => {
37
+ if (k.match(/authorization/i)) return `${req.headers[k].split(' ')[0]} ***`
38
+ if (k.match(/cookie/i)) return '***'
39
+ return req.headers[k]
40
+ })()
32
41
  }
33
42
  toLog.timestamp = new Date()
34
43
 
@@ -56,6 +65,10 @@ module.exports = (module, level, ...args) => {
56
65
  if (cf.length) toLog['#cf'] = { string: cf }
57
66
  }
58
67
 
68
+ return toLog
69
+ }
70
+
71
+ exports.format = toLog => {
59
72
  const getCircularReplacer = () => {
60
73
  const seen = new WeakSet()
61
74
  return (key, value) => {
package/lib/ql/INSERT.js CHANGED
@@ -7,54 +7,55 @@ module.exports = class Query extends require('./Query') {
7
7
  }
8
8
 
9
9
  into (entity, ...data) {
10
- this.INSERT.into = this._target_name4 (...arguments) // supporting tts
10
+ this[this.cmd].into = this._target_name4 (...arguments) // supporting tts
11
11
  if (data.length) this.entries(...data)
12
12
  return this
13
13
  }
14
14
 
15
15
  entries (...x) {
16
- this.INSERT.entries = is_array(x[0]) ? x[0] : x
16
+ this[this.cmd].entries = is_array(x[0]) ? x[0] : x
17
17
  return this
18
18
  }
19
19
  columns (...x) {
20
- this.INSERT.columns = is_array(x[0]) ? x[0] : x
20
+ this[this.cmd].columns = is_array(x[0]) ? x[0] : x
21
21
  return this
22
22
  }
23
23
  values (...x) {
24
- this.INSERT.values = is_array(x[0]) ? x[0] : x
24
+ this[this.cmd].values = is_array(x[0]) ? x[0] : x
25
25
  return this
26
26
  }
27
27
  rows (...rows) {
28
28
  if (is_array(rows[0]) && is_array(rows[0][0])) rows = rows[0]
29
29
  if (!is_array(rows[0])) this._expected `Arguments ${{rows}} to be an array of arrays`
30
- this.INSERT.rows = rows
30
+ this[this.cmd].rows = rows
31
31
  return this
32
32
  }
33
33
  _rows(rows, ...args) {
34
34
 
35
+ const INSERT = this.cmd
35
36
  if (Array.isArray(rows)) {
36
37
  // check if all the entries in the array are arrays
37
38
  if (rows.every(e => Array.isArray(e))) {
38
- this.INSERT.rows = rows
39
+ this[INSERT].rows = rows
39
40
  // check if array contains one or multiple objects
40
41
  } else if (rows.every(e => typeof e === 'object')) {
41
- this.INSERT.entries = rows
42
+ this[INSERT].entries = rows
42
43
  // the rows have been added as arguments
43
44
  } else if (args.length !== 0) {
44
45
  args.unshift(rows)
45
- this.INSERT.rows = args
46
+ this[INSERT].rows = args
46
47
  } else {
47
- this.INSERT.values = rows
48
+ this[INSERT].values = rows
48
49
  }
49
50
  } else if (typeof rows === 'object') {
50
- this.INSERT.entries = rows
51
+ this[INSERT].entries = rows
51
52
  }
52
53
 
53
54
  return this
54
55
  }
55
56
  as (query) {
56
57
  if (!query || !query.SELECT) this._expected `${{query}} to be a CQN {SELECT} query object`
57
- this.INSERT.as = query
58
+ this[this.cmd].as = query
58
59
  return this
59
60
  }
60
61
  valueOf() {
package/lib/ql/Query.js CHANGED
@@ -5,16 +5,23 @@ class Query {
5
5
 
6
6
  constructor(_={}) { this[this.cmd] = _ }
7
7
 
8
+ alias (a) {
9
+ let _ = this[this.cmd] ;(_.from || _.into || _.entity).as = a
10
+ return this
11
+ }
12
+
8
13
  /** Creates a derived instance that initially inherits all properties. */
9
- clone(){
10
- const inherited = Object.create (this[this.cmd])
11
- return {__proto__:this, [this.cmd]: inherited }
14
+ clone (_) {
15
+ const cmd = this.cmd || Object.keys(this)[0]
16
+ return {__proto__:this, [cmd]: {__proto__:this[cmd],..._} }
12
17
  }
13
18
 
14
- flat(){
15
- const flat = this[this.cmd]
16
- for (let x=flat; x.__proto__;) Object.assign(flat, x = x.__proto__)
17
- return new this.constructor (flat)
19
+ flat (q=this) {
20
+ let x = q.cmd || Object.keys(q)[0], y = q[x]
21
+ let protos = [y]; for (let o=y; o.__proto__;) protos.push (o = o.__proto__)
22
+ q[x] = Object.assign ({}, ...protos.reverse())
23
+ if (y.columns) for (let c of y.columns) if (c.SELECT) (this||Query.prototype).flat(c)
24
+ return q
18
25
  }
19
26
 
20
27
  /** Binds this query to be executed with the given service */
@@ -0,0 +1 @@
1
+ module.exports = class Query extends require('./INSERT') {}
@@ -43,7 +43,7 @@ class Query extends require('./Query') {
43
43
  }
44
44
 
45
45
  const predicate4 = (args, _clause) => {
46
- if (args.length === 0) return; const x = args[0]
46
+ if (args.length === 0) return; /* else */ const x = args[0]
47
47
  if (x.raw) return parse.CXL(...args).xpr
48
48
  if (args.length === 1 && typeof x === 'object') {
49
49
  if (is_array(x)) return x
@@ -67,7 +67,11 @@ const _object_predicate = ([arg], _clause) => { // e.g. .where ({ID:4711, stock:
67
67
  continue
68
68
  }
69
69
  if (k === 'exists') {
70
- pred.push(null, 'exists', ...predicate4([x],_clause))
70
+ pred.push(pred.length && 'and', 'exists', typeof x === 'object' ? x : { ref: x.split('.') })
71
+ continue
72
+ }
73
+ if (k === 'not exists') {
74
+ pred.push(pred.length && 'and', 'not', 'exists', typeof x === 'object' ? x : { ref: x.split('.') })
71
75
  continue
72
76
  }
73
77
  else pred.push('and', parse.expr(k))
package/lib/ql/cds-ql.js CHANGED
@@ -11,12 +11,10 @@ require = path => { // eslint-disable-line no-global-assign
11
11
  }
12
12
 
13
13
  module.exports = Object.assign (_deprecated_srv_ql, { cdr: true,
14
- Query, clone(q) {
15
- const cmd = q.cmd || Object.keys(q)[0]
16
- return {__proto__:q, [cmd]: {__proto__:q[cmd] }}
17
- },
14
+ Query, clone: (q,_) => Query.prototype.clone.call(q,_),
18
15
  SELECT: require('./SELECT'),
19
16
  INSERT: require('./INSERT'),
17
+ UPSERT: require('./UPSERT'),
20
18
  UPDATE: require('./UPDATE'),
21
19
  DELETE: require('./DELETE'),
22
20
  CREATE: require('./CREATE'),
@@ -38,6 +38,7 @@ class Request extends require('./event') {
38
38
  const q = this.query; if (this.query) { // IMPORTANT: Bulk queries don't have a _.query
39
39
  if (q.SELECT) return this._set ('path', _path4 (q.SELECT,'from'))
40
40
  if (q.INSERT) return this._set ('path', _path4 (q.INSERT,'into'))
41
+ if (q.UPSERT) return this._set ('path', _path4 (q.UPSERT,'into'))
41
42
  if (q.UPDATE) return this._set ('path', _path4 (q.UPDATE,'entity'))
42
43
  if (q.DELETE) return this._set ('path', _path4 (q.DELETE,'from'))
43
44
  }
@@ -104,6 +105,7 @@ const Http2Crud = {
104
105
  const SQL2Crud = {
105
106
  SELECT: 'READ',
106
107
  INSERT: 'CREATE',
108
+ UPSERT: 'UPSERT',
107
109
  UPDATE: 'UPDATE',
108
110
  DELETE: 'DELETE',
109
111
  BEGIN: 'BEGIN',
@@ -4,7 +4,7 @@ module.exports = ()=> {
4
4
 
5
5
  /** @type { import('express').Handler } */
6
6
  async function cds_context_provider (req, res, next) {
7
- const ctx = new cds.EventContext
7
+ const ctx = {}
8
8
  ctx.http = { req, res }
9
9
  ctx.id = _id4(req)
10
10
  ctx.user = req.user
@@ -102,6 +102,7 @@ const _ensure_target = (srv,req) => {
102
102
  if (p) _ensure_fqn (req,'path',srv, p.startsWith('/') ? p.slice(1) : p)
103
103
  else if (q.SELECT) _ensure_fqn (q.SELECT,'from',srv)
104
104
  else if (q.INSERT) _ensure_fqn (q.INSERT,'into',srv)
105
+ else if (q.UPSERT) _ensure_fqn (q.UPSERT,'into',srv)
105
106
  else if (q.UPDATE) _ensure_fqn (q.UPDATE,'entity',srv)
106
107
  else if (q.DELETE) _ensure_fqn (q.DELETE,'from',srv)
107
108
  }
package/lib/srv/srv-tx.js CHANGED
@@ -17,7 +17,7 @@ class NestedContext extends EventContext {
17
17
  /**
18
18
  * This is the implementation of the `srv.tx(req)` method. It constructs
19
19
  * a new Transaction as a derivate of the `srv` (i.e. {__proto__:srv})
20
- * @returns { Transaction & import('./srv-api') }
20
+ * @returns { Promise<Transaction & import('./srv-api')> }
21
21
  * @param { EventContext } ctx
22
22
  */
23
23
  function srv_tx (ctx,fn) { const srv = this
@@ -92,7 +92,7 @@ class Transaction {
92
92
 
93
93
  /*
94
94
  * srv.on('error', function (err, req) { ... })
95
- * synchroneous modification of passed error only
95
+ * synchronous modification of passed error only
96
96
  * err is undefined if nested tx (cf. "root.before ('failed', ()=> this.rollback())")
97
97
  */
98
98
  if (err) for (const each of this._handlers._error) each.handler.call(this, err, this.context)
@@ -192,7 +192,7 @@ const _begin = async function (req) {
192
192
  return this.ready = this.__proto__.dispatch.call (this,req)
193
193
  // Protection against unintended tx.run() after root tx.commit/rollback()
194
194
  if (typeof this.ready === 'string' || !this.ready && this.context.tx._done) {
195
- if (!cds_tx_protection) this.ready = this.begin() // compatibiliy to former behavior, which allowed tx.run() after commit/rollback
195
+ if (!cds_tx_protection) this.ready = this.begin() // compatibility to former behavior, which allowed tx.run() after commit/rollback
196
196
  else throw cds.error (
197
197
  `Transaction is ${this.ready || this.context.tx._done}, no subsequent .run allowed, without prior .begin`,
198
198
  { code: 'TRANSACTION_CLOSED' }
@@ -2,6 +2,7 @@ const cwd = process.env._original_cwd || process.cwd()
2
2
  const cds = require('../index')
3
3
 
4
4
  const ux = module.exports = exports = new class {
5
+ get inflect() { return super.inflect = require('./inflect') }
5
6
  get inspect() { return super.inspect = require('util').inspect }
6
7
  get uuid() { return super.uuid = require('@sap/cds-foss').uuid }
7
8
  get tar() { return super.tar = require('./tar') }
@@ -10,20 +11,50 @@ const ux = module.exports = exports = new class {
10
11
  const path = exports.path = require('path'), { dirname, extname, join, resolve, relative } = path
11
12
  const fs = exports.fs = Object.assign (ux,require('fs')) //> for compatibility
12
13
 
13
- exports.decodeURIComponent = s => { try { return decodeURIComponent(s) } catch { return s } }
14
- exports.decodeURI = s => { try { return decodeURI(s) } catch { return s } }
15
-
16
- exports.local = (file) => relative(cwd,file)
17
14
 
18
- exports.readdir = async function (x) {
19
- const d = resolve (cds.root,x)
20
- return fs.promises.readdir(d)
15
+ /**
16
+ * Variant of `Object.keys()` which includes all keys inherited from the
17
+ * given object's prototypes.
18
+ */
19
+ exports.Object_keys = o => ({
20
+ [Symbol.iterator]: function*(){ for (let k in o) yield k },
21
+ forEach(f){ let i=0; for (let k in o) f(k,i++,o) },
22
+ filter(f){ let i=0, r=[]; for (let k in o) f(k,i++,o) && r.push(k); return r },
23
+ map(f){ let i=0, r=[]; for (let k in o) r.push(f(k,i++,o)); return r },
24
+ some(f){ for (let k in o) if (f(k)) return true },
25
+ find(f){ for (let k in o) if (f(k)) return k },
26
+ })
27
+
28
+
29
+ /**
30
+ * Simple helper to always access results as arrays.
31
+ */
32
+ exports.results = oa => {
33
+ return Array.isArray(oa) ? oa : oa != null ? [oa] : []
21
34
  }
22
35
 
23
- exports.stat = async function (x) {
24
- const d = resolve (cds.root,x)
25
- return fs.promises.stat(d)
36
+
37
+ /**
38
+ * Should be used in data providers, i.e., db services to return single
39
+ * rows in response to SELECT.one queries.
40
+ */
41
+ exports.chimera = oa => {
42
+ return Array.isArray(oa) ? oa : Object.defineProperties(oa,chimera)
26
43
  }
44
+ const chimera = Object.getOwnPropertyDescriptors (class Chimera {
45
+ *[Symbol.iterator] (){ yield this }
46
+ forEach(f){ f(this,0,this) }
47
+ filter(f){ return f(this,0,this) ? [this] : [] }
48
+ map(f){ return [f(this,0,this)] }
49
+ some(f){ return f(this,0,this) }
50
+ find(f){ if (f(this,0,this)) return this }
51
+ }.prototype)
52
+
53
+
54
+ exports.decodeURIComponent = s => { try { return decodeURIComponent(s) } catch { return s } }
55
+ exports.decodeURI = s => { try { return decodeURI(s) } catch { return s } }
56
+
57
+ exports.local = (file) => relative(cwd,file)
27
58
 
28
59
  exports.exists = function exists (x) {
29
60
  if (x) {
@@ -50,6 +81,16 @@ exports.isfile = function isfile (x) {
50
81
  } catch(e){/* ignore */}
51
82
  }
52
83
 
84
+ exports.stat = async function (x) {
85
+ const d = resolve (cds.root,x)
86
+ return fs.promises.stat(d)
87
+ }
88
+
89
+ exports.readdir = async function (x) {
90
+ const d = resolve (cds.root,x)
91
+ return fs.promises.readdir(d)
92
+ }
93
+
53
94
  exports.read = async function read (file, _encoding) {
54
95
  const f = resolve (cds.root,file)
55
96
  const src = await fs.promises.readFile (f, _encoding !== 'json' && _encoding || 'utf8')
@@ -64,18 +105,37 @@ exports.write = function write (file, data, o) {
64
105
  return fs.mkdirp (dirname(f)).then (()=> fs.promises.writeFile (f,data,o))
65
106
  }
66
107
 
108
+ exports.copy = function copy (x,y) {
109
+ if (arguments.length === 1) return {to:(...path) => copy(x,join(...path))}
110
+ const src = resolve (cds.root,x)
111
+ const dst = resolve (cds.root,y)
112
+ if (fs.promises.cp) return fs.promises.cp (src,dst,{recursive:true})
113
+ return fs.mkdirp (dirname(dst)) .then (async ()=>{
114
+ if (fs.isdir(src)) {
115
+ const entries = await fs.promises.readdir(src)
116
+ return Promise.all (entries.map (async each => {
117
+ const e = join (src,each)
118
+ const f = join (dst,each)
119
+ return copy (e,f)
120
+ }))
121
+ } else {
122
+ return fs.promises.copyFile (src,dst)
123
+ }
124
+ })
125
+ }
126
+
67
127
  exports.mkdirp = async function (...path) {
68
128
  const d = resolve (cds.root,...path)
69
129
  await fs.promises.mkdir (d,{recursive:true})
70
130
  return d
71
131
  }
72
132
 
73
- exports.rmdir = (...path) => {
133
+ exports.rmdir = async function (...path) {
74
134
  const d = resolve (cds.root,...path)
75
135
  return fs.promises.rm (d, {recursive:true})
76
136
  }
77
137
 
78
- exports.rimraf = (...path) => {
138
+ exports.rimraf = async function (...path) {
79
139
  const d = resolve (cds.root,...path)
80
140
  return fs.promises.rm (d, {recursive:true,force:true})
81
141
  }
@@ -85,23 +145,6 @@ exports.rm = async function rm (x) {
85
145
  return fs.promises.rm(y)
86
146
  }
87
147
 
88
- exports.copy = async function copy (x,y) {
89
- const src = resolve (cds.root,x)
90
- const dst = resolve (cds.root,y)
91
- if (fs.promises.cp) return fs.promises.cp (src,dst,{recursive:true})
92
- await fs.mkdirp (dirname(dst))
93
- if (fs.isdir(src)) {
94
- const entries = await fs.promises.readdir(src)
95
- return Promise.all (entries.map (async each => {
96
- const e = join (src,each)
97
- const f = join (dst,each)
98
- return copy (e,f)
99
- }))
100
- } else {
101
- return fs.promises.copyFile (src,dst)
102
- }
103
- }
104
-
105
148
  exports.find = function find (base, patterns='*', filter=()=>true) {
106
149
  const files=[]; base = resolve (cds.root,base)
107
150
  if (typeof patterns === 'string') patterns = patterns.split(',')
@@ -134,7 +177,9 @@ exports.find = function find (base, patterns='*', filter=()=>true) {
134
177
  }
135
178
 
136
179
 
137
- // internal utility to load a file through ESM or CommonJs. TODO find a better place.
180
+ /**
181
+ * Internal utility to load a file through ESM or CommonJs. TODO find a better place.
182
+ */
138
183
  exports._import = id => require(id)
139
184
  if (typeof jest === 'undefined') { // jest's ESM support is experimental: https://jestjs.io/docs/ecmascript-modules
140
185
  const { pathToFileURL } = require('url')