@sap/cds 6.4.0 → 6.5.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 (88) hide show
  1. package/CHANGELOG.md +59 -3
  2. package/apis/cds.d.ts +2 -0
  3. package/apis/cqn.d.ts +14 -3
  4. package/apis/ql.d.ts +12 -8
  5. package/apis/services.d.ts +39 -64
  6. package/apis/test.d.ts +7 -0
  7. package/bin/build/buildTaskEngine.js +9 -12
  8. package/bin/build/buildTaskHandler.js +3 -14
  9. package/bin/build/index.js +8 -2
  10. package/bin/build/provider/buildTaskProviderInternal.js +8 -7
  11. package/bin/build/provider/hana/template/package.json +3 -0
  12. package/bin/build/provider/mtx/resourcesTarBuilder.js +13 -4
  13. package/bin/build/provider/mtx-extension/index.js +41 -38
  14. package/bin/build/util.js +17 -0
  15. package/bin/deploy/to-hana/hdiDeployUtil.js +11 -5
  16. package/bin/serve.js +6 -2
  17. package/common.cds +7 -0
  18. package/lib/auth/index.js +17 -15
  19. package/lib/auth/jwt-auth.js +4 -3
  20. package/lib/compile/for/lean_drafts.js +1 -1
  21. package/lib/compile/minify.js +3 -3
  22. package/lib/core/index.js +1 -0
  23. package/lib/dbs/cds-deploy.js +13 -10
  24. package/lib/env/cds-requires.js +1 -1
  25. package/lib/env/defaults.js +5 -1
  26. package/lib/env/schemas/cds-rc.json +74 -3
  27. package/lib/lazy.js +6 -8
  28. package/lib/log/cds-error.js +2 -2
  29. package/lib/ql/Whereable.js +22 -11
  30. package/lib/ql/cds-ql.js +1 -1
  31. package/lib/req/response.js +8 -3
  32. package/lib/req/user.js +12 -2
  33. package/lib/srv/middlewares/cds-context.js +0 -2
  34. package/lib/srv/middlewares/ctx-auth.js +11 -0
  35. package/lib/srv/middlewares/ctx-model.js +22 -20
  36. package/lib/srv/middlewares/index.js +7 -9
  37. package/lib/srv/protocols/_legacy.js +4 -0
  38. package/lib/srv/protocols/graphql.js +2 -2
  39. package/lib/srv/protocols/index.js +7 -3
  40. package/lib/srv/srv-api.js +1 -0
  41. package/lib/srv/srv-models.js +6 -1
  42. package/lib/utils/cds-utils.js +3 -1
  43. package/lib/utils/data.js +2 -2
  44. package/lib/utils/tar.js +37 -12
  45. package/libx/_runtime/auth/strategies/JWT.js +1 -0
  46. package/libx/_runtime/auth/strategies/ias-auth.js +2 -1
  47. package/libx/_runtime/auth/strategies/mock.js +12 -1
  48. package/libx/_runtime/auth/strategies/xssecUtils.js +7 -8
  49. package/libx/_runtime/auth/strategies/xsuaa.js +1 -0
  50. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +1 -2
  51. package/libx/_runtime/cds-services/services/Service.js +3 -0
  52. package/libx/_runtime/cds-services/services/utils/columns.js +35 -36
  53. package/libx/_runtime/common/code-ext/WorkerReq.js +79 -0
  54. package/libx/_runtime/common/code-ext/config.js +13 -0
  55. package/libx/_runtime/common/code-ext/execute.js +106 -0
  56. package/libx/_runtime/common/code-ext/handlers.js +49 -0
  57. package/libx/_runtime/common/code-ext/worker.js +36 -0
  58. package/libx/_runtime/common/code-ext/workerQuery.js +45 -0
  59. package/libx/_runtime/common/code-ext/workerQueryExecutor.js +33 -0
  60. package/libx/_runtime/common/generic/crud.js +5 -1
  61. package/libx/_runtime/common/generic/paging.js +8 -7
  62. package/libx/_runtime/common/i18n/index.js +1 -1
  63. package/libx/_runtime/common/utils/cqn2cqn4sql.js +47 -11
  64. package/libx/_runtime/common/utils/path.js +5 -25
  65. package/libx/_runtime/common/utils/resolveView.js +2 -0
  66. package/libx/_runtime/common/utils/search2cqn4sql.js +13 -9
  67. package/libx/_runtime/db/expand/expandCQNToJoin.js +2 -1
  68. package/libx/_runtime/db/sql-builder/InsertBuilder.js +5 -1
  69. package/libx/_runtime/db/sql-builder/UpsertBuilder.js +9 -32
  70. package/libx/_runtime/db/sql-builder/annotations.js +6 -3
  71. package/libx/_runtime/db/utils/localized.js +1 -1
  72. package/libx/_runtime/fiori/generic/activate.js +4 -0
  73. package/libx/_runtime/fiori/generic/before.js +8 -1
  74. package/libx/_runtime/fiori/generic/edit.js +5 -0
  75. package/libx/_runtime/fiori/generic/read.js +8 -3
  76. package/libx/_runtime/fiori/lean-draft.js +12 -1
  77. package/libx/_runtime/hana/Service.js +1 -1
  78. package/libx/_runtime/hana/customBuilder/CustomSelectBuilder.js +5 -5
  79. package/libx/_runtime/hana/execute.js +5 -5
  80. package/libx/_runtime/hana/pool.js +1 -1
  81. package/libx/_runtime/hana/search2cqn4sql.js +51 -51
  82. package/libx/_runtime/sqlite/Service.js +1 -1
  83. package/libx/_runtime/sqlite/customBuilder/CustomUpsertBuilder.js +20 -38
  84. package/libx/odata/afterburner.js +6 -3
  85. package/libx/odata/cqn2odata.js +1 -1
  86. package/libx/rest/middleware/parse.js +26 -4
  87. package/package.json +1 -1
  88. package/server.js +2 -20
@@ -1,5 +1,7 @@
1
1
  const path = require('path')
2
+ const fs = require('fs')
2
3
  const cds = require('../../cds')
4
+
3
5
  const BuildTaskHandlerInternal = require('../buildTaskHandlerInternal')
4
6
  const { FOLDER_GEN } = require('../../constants')
5
7
  const ResourcesTarBuilder = require('../mtx/resourcesTarBuilder')
@@ -13,47 +15,48 @@ class MtxExtensionModuleBuilder extends BuildTaskHandlerInternal {
13
15
  }
14
16
 
15
17
  async build() {
18
+ const { src, dest } = this.task
19
+ const destExt = path.join(dest, 'ext')
20
+
21
+ await this.copy(path.join(src, 'package.json')).to(path.join(destExt, 'package.json'))
22
+
23
+ // copy handlers
24
+ const folders = [path.join(src, cds.env.folders.srv, 'handlers')]
25
+ await this.copyNativeContent(src, destExt, res => {
26
+ if (fs.statSync(res).isDirectory()) {
27
+ return folders.some(folder => folder.startsWith(res))
28
+ }
29
+ if (folders.includes(path.dirname(res)) && /\.js$/.test(res)) {
30
+ return true
31
+ }
32
+ })
33
+
16
34
  const model = await this.model()
17
- if (!model) {
18
- return
19
- }
20
- const allFiles = []
21
- const destExt = path.join(this.task.dest, 'ext')
22
-
23
- const packageJson = path.join(destExt, 'package.json')
24
- await this.copy(path.join(this.task.src, 'package.json')).to(packageJson)
25
- allFiles.push(packageJson)
26
-
27
- // extension CSN using parsed format
28
- const options = { ...this.options(), flavor: 'parsed' }
29
- const extCsn = await cds.load(this.resolveModel(), options)
30
- if (extCsn.requires) {
31
- extCsn.requires.length = 0
32
- }
33
- const csnFile = path.join(destExt, 'extension.csn')
34
- await this.compileToJson(extCsn, csnFile)
35
- allFiles.push(csnFile)
36
-
37
- // static i18n folder name as runtime does not use the CDS config of the extension project
38
- const i18n = await this.collectLanguageBundles(extCsn, path.join(destExt, 'i18n'))
39
- if (i18n) {
40
- allFiles.push(i18n.file)
41
- }
35
+ if (model) {
36
+ // extension CSN using parsed format
37
+ const options = { ...this.options(), flavor: 'parsed' }
38
+ const extCsn = await cds.load(this.resolveModel(), options)
39
+ if (extCsn.requires) {
40
+ extCsn.requires.length = 0
41
+ }
42
+ await this.compileToJson(extCsn, path.join(destExt, 'extension.csn'))
43
+
44
+ await this.collectLanguageBundles(extCsn, path.join(destExt, 'i18n'))
42
45
 
43
- const files = Object.keys(await cds.deploy.resources(model))
44
- if (files.length > 0) {
45
- const dataDest = path.join(destExt, 'data')
46
- await Promise.all(
47
- files
48
- .filter(file => /\.csv$/.test(file))
49
- .map(csv => {
50
- const csvFile = path.join(dataDest, path.basename(csv))
51
- allFiles.push(csvFile)
52
- return this.copy(csv).to(csvFile)
53
- })
54
- )
46
+ const files = Object.keys(await cds.deploy.resources(model))
47
+ if (files.length > 0) {
48
+ const dataDest = path.join(destExt, 'data')
49
+ await Promise.all(
50
+ files
51
+ .filter(file => /\.csv$/.test(file))
52
+ .map(csv => {
53
+ return this.copy(csv).to(path.join(dataDest, path.basename(csv)))
54
+ })
55
+ )
56
+ }
55
57
  }
56
- await new ResourcesTarBuilder(this).writeTarFile(allFiles, destExt, path.join(this.task.dest, 'extension.tgz'))
58
+ // add all resources contained in the 'ext' folder
59
+ await new ResourcesTarBuilder(this).writeTarFile(path.join(this.task.dest, 'extension.tgz'), destExt)
57
60
  }
58
61
  }
59
62
  module.exports = MtxExtensionModuleBuilder
package/bin/build/util.js CHANGED
@@ -187,6 +187,22 @@ function flatten(modelPaths) {
187
187
  }, [])
188
188
  }
189
189
 
190
+ /**
191
+ * Copy a file or directory. The directory can have contents.
192
+ * REVISIT: 'fs.promises.cp' replacement for nodejs 14
193
+ * @param src <String> Note that if src is a directory it will copy everything inside of this directory, not the entire directory itself.
194
+ * @param dest <String> Note that if src is a file, dest cannot be a directory.
195
+ */
196
+ async function copy(src, dest) {
197
+ if ((await fs.promises.stat(src)).isDirectory()) {
198
+ const entries = await fs.promises.readdir(src)
199
+ return Promise.all(entries.map(async each => copy(path.join(src, each), path.join(dest, each))))
200
+ } else {
201
+ await fs.promises.mkdir(path.dirname(dest), { recursive: true })
202
+ return fs.promises.copyFile(src, dest)
203
+ }
204
+ }
205
+
190
206
  class BuildMessage extends Error {
191
207
  constructor(message, severity = SEVERITY_ERROR) {
192
208
  super(message)
@@ -228,6 +244,7 @@ module.exports = {
228
244
  resolveRequiredSapModels,
229
245
  getDefaultModelOptions,
230
246
  flatten,
247
+ copy,
231
248
  BuildMessage,
232
249
  BuildError
233
250
  }
@@ -97,9 +97,15 @@ Add it either as a devDependency using 'npm install -D ${this.deployerName}' or
97
97
 
98
98
 
99
99
  async _npmSearchPaths(cwd) {
100
- const npmRootCall = await execAsync('npm root -g');
101
- const globalNodeModules = npmRootCall.stdout.toString().trim();
102
- return [cwd, globalNodeModules, '@sap/hdi-deploy']
100
+ // REVISIT: we shouldn't have to rely on `npm` on the server
101
+ try {
102
+ const npmRootCall = await execAsync('npm root -g');
103
+ const globalNodeModules = npmRootCall.stdout.toString().trim();
104
+ return [cwd, globalNodeModules, '@sap/hdi-deploy']
105
+ } catch (error) {
106
+ if (/Command failed: npm.*not found/s.test(error.message)) return [cwd, '@sap/hdi-deploy']
107
+ else throw error
108
+ }
103
109
  }
104
110
 
105
111
 
@@ -107,10 +113,10 @@ Add it either as a devDependency using 'npm install -D ${this.deployerName}' or
107
113
  const hdiDeployLib = await this._getHdiDeployLib(dbDir);
108
114
  return new Promise((resolve, reject) => {
109
115
  const callbacks = {
110
- stderrCB: error => console.error(error.toString())
116
+ stderrCB: error => LOG.error(error.toString())
111
117
  }
112
118
  if (LOG.level !== SILENT) {
113
- callbacks.stdoutCB = (data) => console.log(data.toString());
119
+ callbacks.stdoutCB = (data) => LOG.log(data.toString());
114
120
  }
115
121
 
116
122
  hdiDeployLib.deploy(dbDir, env, (error, response) => {
package/bin/serve.js CHANGED
@@ -6,7 +6,7 @@ module.exports = Object.assign ( serve, {
6
6
  flags: [
7
7
  '--project', '--projects',
8
8
  '--in-memory', '--in-memory?',
9
- '--mocked', '--with-mocks', '--with-bindings',
9
+ '--mocked', '--with-mocks', '--with-bindings', '--resolve-bindings',
10
10
  '--watch',
11
11
  ],
12
12
  shortcuts: [ '-s', undefined, '-2', '-a', '-w', undefined, '-p' ],
@@ -98,6 +98,10 @@ module.exports = Object.assign ( serve, {
98
98
  All required services are bound automatically upon bootstrapping.
99
99
  Option *--with-mocks* subsumes this option.
100
100
 
101
+ *--resolve-bindings* (beta)
102
+
103
+ Resolve remote service bindings configured via *cds bind*.
104
+
101
105
  *--in-memory[?]*
102
106
 
103
107
  Automatically adds a transient in-memory database bootstrapped on
@@ -265,7 +269,7 @@ function _prepare_logging () { // NOSONAR
265
269
 
266
270
  // print info when we are finally on air
267
271
  cds.once ('listening', ({url})=>{
268
- console.log()
272
+ LOG.info ()
269
273
  LOG.info ('server listening on',{url})
270
274
  _timer && console.timeEnd (_timer)
271
275
  if (process.stdin.isTTY) LOG.info (`[ terminate with ^C ]\n`)
package/common.cds CHANGED
@@ -72,6 +72,13 @@ context sap.common {
72
72
  name : localized String(255) @title : '{i18n>Name}';
73
73
  descr : localized String(1000) @title : '{i18n>Description}';
74
74
  }
75
+
76
+ /*
77
+ * Aspect that is included by generated `.texts` entities for localized entities.
78
+ */
79
+ aspect TextsAspect {
80
+ key locale: Locale;
81
+ }
75
82
  }
76
83
 
77
84
 
package/lib/auth/index.js CHANGED
@@ -1,6 +1,22 @@
1
1
 
2
+ const cds = require ('../index'), { path, local } = cds.utils
3
+
4
+ const _require = require; require = cds.lazified (module) // eslint-disable-line no-global-assign
5
+ module.exports = Object.assign (auth_factory, {
6
+ mocked: require('./basic-auth'),
7
+ basic: require('./basic-auth'),
8
+ dummy: require('./dummy-auth'),
9
+ ias: require('./ias-auth'),
10
+ jwt: require('./jwt-auth'),
11
+ xsuaa: require('./jwt-auth'),
12
+ })
13
+ require = _require // eslint-disable-line no-global-assign
14
+
15
+
16
+ /**
17
+ * Constructs one of the above middlewares as configured
18
+ */
2
19
  function auth_factory (options) {
3
- const cds = require ('../index'), { path, local } = cds.utils
4
20
  const o = { ...options, ...cds.requires.auth }
5
21
  let kind = o.kind || o.strategy
6
22
  let middleware = cds.auth[kind]
@@ -16,17 +32,3 @@ function auth_factory (options) {
16
32
  }
17
33
  return middleware(o)
18
34
  }
19
-
20
- const { lazified } = require('../lazy')
21
- const _require = require; require = lazified (module) // eslint-disable-line no-global-assign
22
-
23
- module.exports = lazified (Object.assign (auth_factory, {
24
- mocked: require('./basic-auth'),
25
- basic: require('./basic-auth'),
26
- dummy: require('./dummy-auth'),
27
- ias: require('./ias-auth'),
28
- jwt: require('./jwt-auth'),
29
- xsuaa: require('./jwt-auth'),
30
- }))
31
-
32
- require = _require // eslint-disable-line no-global-assign
@@ -26,6 +26,7 @@ module.exports = function jwt_auth(config) {
26
26
  const payload = req.tokenInfo.getPayload()
27
27
 
28
28
  let id = req.user.id
29
+ let _is_system, _is_internal
29
30
 
30
31
  let roles = payload.scope.map(s => s.replace(new RegExp(`^(${config.credentials.xsappname + '.'})`), ''))
31
32
  roles.push('identified-user')
@@ -36,8 +37,8 @@ module.exports = function jwt_auth(config) {
36
37
  const CLIENT = { client_credentials: 1, client_x509: 1 }
37
38
  if (payload.grant_type in CLIENT) {
38
39
  id = 'system'
39
- roles.push('system-user')
40
- if (req.tokenInfo.getClientId() === config.credentials.clientid) roles.push('internal-user')
40
+ _is_system = true
41
+ if (req.tokenInfo.getClientId() === config.credentials.clientid) _is_internal = true
41
42
  }
42
43
  }
43
44
 
@@ -49,7 +50,7 @@ module.exports = function jwt_auth(config) {
49
50
  attr.email = req.authInfo.getEmail()
50
51
  }
51
52
 
52
- req.user = new cds.User({ id, roles, attr })
53
+ req.user = new cds.User({ id, roles, attr, _is_system, _is_internal })
53
54
  req.tenant = req.tokenInfo.getZoneId?.()
54
55
  next()
55
56
  })
@@ -54,7 +54,7 @@ module.exports = function cds_compile_for_lean_drafts(csn, o) {
54
54
  for (const each in draft.elements) {
55
55
  const e = draft.elements[each]
56
56
  const newEl = Object.create(e)
57
- if (e.isComposition || (e.isAssociation && e['@odata.draft.enclosed']) || e._isBacklink) {
57
+ if (e.isComposition || (e.isAssociation && e['@odata.draft.enclosed']) || e._isCompositionBacklink) {
58
58
  _redirect(newEl, draftEntity(e._target, model))
59
59
  }
60
60
  newEl.parent = draft
@@ -35,10 +35,10 @@ module.exports = function cds_minify (csn, _roots) { // IMPORTANT: don't add cds
35
35
  }
36
36
  function _visit (d) {
37
37
  if (typeof d === 'string') {
38
- if (cds.compiler.model.isInReservedNamespace(d)) return
39
- else d = all[d]
38
+ d = all[d]
39
+ if (!d) return // builtins like cds.String
40
40
  } else if (d.ref) return d.ref.reduce((p,n) => {
41
- let d = (p.elements || csn.definitions[p.target].elements)[n.id || n] // > n.id -> view with parameters
41
+ let d = (p.elements || all[p.target || p.type].elements)[n.id || n] // > n.id -> view with parameters
42
42
  if (d) _visit(d)
43
43
  return d
44
44
  },{elements:all})
package/lib/core/index.js CHANGED
@@ -26,6 +26,7 @@ const roots = _roots ({
26
26
  Association: {type:'type'},
27
27
  Composition: {type:'Association'},
28
28
  service: {type:'context'},
29
+ $self: {}, //> to support polymorphic self links like in: action foo( self: [many] $self, ...)
29
30
  })
30
31
 
31
32
  /**
@@ -107,7 +107,7 @@ exports.exclude_external_entities_in = function (csn) { // NOSONAR
107
107
 
108
108
  function getSqls(db, csn, o, beforeCsn) {
109
109
  const schemaEvo = (db.options?.schema_evolution === 'auto' || o.schema_evolution === 'auto')
110
- const creds = db.options?.credentials
110
+ const creds = db.options?.credentials
111
111
  const in_memory = (creds?.url || creds?.database) === ':memory:';
112
112
  if (!in_memory && schemaEvo) {
113
113
  const { afterImage: afterCsn, drops, createsAndAlters: creas } = cds.compile.to.sql.delta (csn, o, beforeCsn);
@@ -134,7 +134,7 @@ exports.create = async function (db, csn=db.model, o) {
134
134
  const schemaEvo = (db.options?.schema_evolution === 'auto' || o.schema_evolution === 'auto')
135
135
  if(db.deploy && !schemaEvo) {
136
136
  // reset CSN state saved in db - if there is any
137
- await db.run('DROP table if exists cds_Model;');
137
+ if(!o.dry) await db.run('DROP table if exists cds_Model;');
138
138
  return db.deploy(csn, o);
139
139
  }
140
140
 
@@ -176,20 +176,23 @@ exports.init = (db, csn=db.model, o, csvs, log=()=>{}) => db.run (async tx => {
176
176
 
177
177
  if (csvs) {
178
178
  const ccsn = cds.compile.for['nodejs'](csn) // compile to calculate keys for newly added entities
179
- for(let [e,src] of Object.entries(csvs)) {
180
- const q = INSERT_from_csv (e,src,schemaEvo); if (!q) continue
181
- if (db.kind === 'better-sqlite') _add_missing_pks2(q)
182
- q._target = ccsn.definitions[e]
183
- inits.push (tx.run(q) .catch (e => {
184
- throw Object.assign (e, { message: 'in cds.deploy(): ' + e.message +'\n'+ inspect(q) })
185
- }))
179
+ for(let [file,src] of Object.entries(csvs)) {
180
+ const entity = _entity4(path.basename(file, '.csv'), csn)
181
+ if (entity?.name) {
182
+ const q = INSERT_from_csv (entity.name,src,schemaEvo); if (!q) continue
183
+ if (db.kind === 'better-sqlite') _add_missing_pks2(q)
184
+ q._target = ccsn.definitions[entity.name]
185
+ inits.push (tx.run(q) .catch (e => {
186
+ throw Object.assign (e, { message: 'in cds.deploy(): ' + e.message +'\n'+ inspect(q) })
187
+ }))
188
+ }
186
189
  }
187
190
  } else {
188
191
  for (let [file,e] of Object.entries(resources)) {
189
192
  if (e === '*') { // init.js/ts
190
193
  let x = await cds.utils._import(file); if (!x) continue
191
194
  if (x.default) x = x.default // default ESM export
192
- inits.push (!x.then && typeof x === 'function' ? x(db,csn) : x)
195
+ inits.push (!x.then && typeof x === 'function' ? x(tx,csn) : x)
193
196
  log (file)
194
197
  } else { // from .csv or .json
195
198
  const INSERT_into = _from_csv_or_json [path.extname(file)]
@@ -131,7 +131,7 @@ const _databases = {
131
131
  },
132
132
  "better-sqlite": _compat_to_use({
133
133
  credentials: { url: ":memory:" },
134
- impl: "@sap/cds-sqlite",
134
+ impl: "@cap-js/sqlite",
135
135
  }),
136
136
  "sql-mt": {
137
137
  '[development]': { kind: 'sqlite' },
@@ -29,7 +29,11 @@ module.exports = {
29
29
  localized: true,
30
30
  assert_integrity: false,
31
31
  cds_tx_protection: true,
32
- cds_tx_inheritance: true
32
+ cds_tx_inheritance: true,
33
+ lean_draft: false,
34
+ '[lean-draft]': {
35
+ lean_draft: true,
36
+ }
33
37
  },
34
38
 
35
39
  log: {
@@ -246,8 +246,26 @@
246
246
  "description": "Shortcut to enable multitenancy."
247
247
  },
248
248
  "extensibility": {
249
- "type": "boolean",
250
- "description": "Shortcut to enable extensibility."
249
+ "oneOf": [
250
+ {
251
+ "type": "boolean",
252
+ "description": "Shortcut to enable extensibility."
253
+ },
254
+ {
255
+ "type": "object",
256
+ "description": "Extensibility configuration options.",
257
+ "properties": {
258
+ "tenantCheckInterval": {
259
+ "type": "number",
260
+ "description": "Time interval in ms to check for new extensions and refreshed models."
261
+ },
262
+ "evictionInterval": {
263
+ "type": "number",
264
+ "description": "Time interval in ms after which to evict models for inactive tenants."
265
+ }
266
+ }
267
+ }
268
+ ]
251
269
  },
252
270
  "toggles": {
253
271
  "type": "boolean",
@@ -264,7 +282,7 @@
264
282
  },
265
283
  {
266
284
  "type": "object",
267
- "description": "Configuration options",
285
+ "description": "ModelProviderService configuration options.",
268
286
  "additionalProperties": true,
269
287
  "properties": {
270
288
  "root": {
@@ -295,6 +313,37 @@
295
313
  },
296
314
  {
297
315
  "$ref": "#/$defs/servicePresetSidecar"
316
+ },
317
+ {
318
+ "type": "object",
319
+ "description": "DeploymentService configuration options.",
320
+ "properties": {
321
+ "hdi": {
322
+ "type": "object",
323
+ "description": "Bundles HDI-specific settings.",
324
+ "properties": {
325
+ "create": {
326
+ "type": "object",
327
+ "description": "HDI container provisioning parameters.",
328
+ "properties": {
329
+ "database_id": {
330
+ "type": "string",
331
+ "description": "HANA Cloud instance ID."
332
+ },
333
+ "additionalProperties": true
334
+ }
335
+ },
336
+ "bind": {
337
+ "type": "object",
338
+ "description": "HDI container binding parameters."
339
+ },
340
+ "deploy": {
341
+ "type": "object",
342
+ "description": "HDI deployment parameters as defined on https://www.npmjs.com/package/@sap/hdi-deploy#supported-features"
343
+ }
344
+ }
345
+ }
346
+ }
298
347
  }
299
348
  ]
300
349
  },
@@ -303,6 +352,28 @@
303
352
  "oneOf": [
304
353
  {
305
354
  "$ref": "#/$defs/serviceActivation"
355
+ },
356
+ {
357
+ "type": "object",
358
+ "description": "SaasProvisioningService configuration options.",
359
+ "additionalProperties": true,
360
+ "properties": {
361
+ "jobs": {
362
+ "type": "object",
363
+ "description": "Configuration options for the built-in async job executor.",
364
+ "properties": {
365
+ "workerSize": {
366
+ "type": "number",
367
+ "description": "Number of workers running in parallel per database."
368
+ },
369
+ "clusterSize": {
370
+ "type": "number",
371
+ "description": "Number of databases executing parallel tasks."
372
+ }
373
+ },
374
+ "additionalProperties": true
375
+ }
376
+ }
306
377
  }
307
378
  ]
308
379
  }
package/lib/lazy.js CHANGED
@@ -9,7 +9,7 @@ const extend = (target) => ({
9
9
  for (let each of aspects) {
10
10
  for (let p of Reflect.ownKeys(each)) {
11
11
  if (p in excludes) continue
12
- define (target,p, describe(each,p))
12
+ Reflect.defineProperty (target,p, Reflect.getOwnPropertyDescriptor(each,p))
13
13
  }
14
14
  if (is_class(target) && is_class(each)) {
15
15
  extend(target.prototype).with(each.prototype)
@@ -28,10 +28,10 @@ const _excludes = {
28
28
  const lazify = (o) => {
29
29
  if (o.constructor === module.constructor) return lazify_module(o)
30
30
  for (let p of Reflect.ownKeys(o)) {
31
- const d = describe(o,p)
32
- if (is_lazy(d.value)) define (o,p,{
33
- set(v) { define (this,p,{value:v,__proto__:d}) },
34
- get() { return this[p] = d.value(p,this) },
31
+ const d = Reflect.getOwnPropertyDescriptor(o,p)
32
+ if (is_lazy(d.value)) Reflect.defineProperty (o,p,{
33
+ set(v) { Reflect.defineProperty (this,p,{value:v,__proto__:d}) },
34
+ get() { return this[p] = d.value.call(this,p,this) },
35
35
  configurable: true,
36
36
  })
37
37
  }
@@ -45,9 +45,7 @@ const lazify_module = (module) => {
45
45
  return (id) => (lazy) => module.require(id)
46
46
  }
47
47
 
48
- const is_lazy = (x) => typeof x === 'function' && /^\(?lazy[,)\t =]/.test(x)
48
+ const is_lazy = (x) => typeof x === 'function' && /^(function\s?)?\(?lazy[,)\t =]/.test(x)
49
49
  const is_class = (x) => typeof x === 'function' && x.prototype && /^class\b/.test(x)
50
- const describe = Reflect.getOwnPropertyDescriptor
51
- const define = Reflect.defineProperty
52
50
 
53
51
  module.exports = { extend, lazify, lazified:lazify }
@@ -47,9 +47,9 @@ exports.message = (strings,...values) => String.raw(strings,...values.map(_forma
47
47
  * typeof x === 'string' || cds.error.expected `${{x}} to be a string`
48
48
  * //> Error: Expected argument 'x' to be a string, but got: { foo: 'bar' }
49
49
  */
50
- exports.expected = ([,type], arg) => {
50
+ const expected = exports.expected = ([,type], arg) => {
51
51
  const [ name, value ] = Object.entries(arg)[0]
52
- return error `Expected argument '${name}'${type}, but got: ${require('util').inspect(value,{depth:11})}`
52
+ return error (`Expected argument '${name}'${type}, but got: ${require('util').inspect(value,{depth:11})}`, undefined, expected)
53
53
  }
54
54
 
55
55
 
@@ -7,25 +7,33 @@ class Query extends require('./Query') {
7
7
  where(...x) { return this._where (x,'and','where') }
8
8
  and(...x) { return this._where (x,'and') }
9
9
  or(...x) { return this._where (x,'or') }
10
- _where (args, and_or, _clause) {
11
- let pred = predicate4(args, _clause)
10
+ _where (args, and_or, _where) {
11
+ let pred = predicate4(args, _where)
12
12
  if (pred && pred.length > 0) {
13
13
  let _ = this[this.cmd]
14
- if (!_clause) _clause = (
14
+ const clause = _where ?? (
15
15
  _.having ? 'having' :
16
16
  _.where ? 'where' :
17
17
  _.from?.on ? 'on' :
18
18
  error (`Invalid attempt to call '${this.cmd}.${and_or}()' before a prior call to '${this.cmd}.where()'`)
19
19
  )
20
- if (_clause === 'on') _ = _.from
21
- let left = Reflect.getOwnPropertyDescriptor(_,_clause)?.value
22
- if (!left) {
20
+ if (clause === 'on') _ = _.from
21
+ let left = Reflect.getOwnPropertyDescriptor(_,clause)?.value
22
+ if (!left) { //> .where() called first time
23
+ // SELECT.from `X` .where `x or y` .and `z` -> SELECT from X where (x or y) and z
23
24
  if (pred.includes('or')) this._left_has_or = true
24
- _[_clause] = pred
25
- } else {
26
- if (this._left_has_or && and_or === 'and') { left = [{xpr:left}]; delete this._left_has_or }
25
+ _[clause] = pred
26
+ } else { //> .where(), .and(), .or() called successively
27
+ if (_where) {
28
+ // SELECT.from `X` .where `x` .or `y` .where `z` -> SELECT from X where (x or y) and z
29
+ if (left.includes('or')) left = [{xpr:left}]
30
+ } else if (and_or === 'and') {
31
+ // SELECT.from `X` .where `x` .or `y` .and `z` -> SELECT from X where x or y and z
32
+ if (this._left_has_or) { left = [{xpr:left}]; delete this._left_has_or }
33
+ }
34
+ // SELECT.from `X` .where `x` .and `y or z` -> SELECT from X where x and (y or z)
27
35
  if (pred.includes('or')) pred = [{xpr:pred}]
28
- _[_clause] = [ ...left, and_or, ...pred ]
36
+ _[clause] = [ ...left, and_or, ...pred ]
29
37
  }
30
38
  }
31
39
  return this
@@ -44,7 +52,10 @@ class Query extends require('./Query') {
44
52
 
45
53
  const predicate4 = (args, _clause) => {
46
54
  if (args.length === 0) return; /* else */ const x = args[0]
47
- if (x.raw) return parse.CXL(...args).xpr
55
+ if (x.raw) {
56
+ let cxn = parse.CXL(...args)
57
+ return cxn.xpr ?? [cxn] //> the fallback is for single-item exprs like `1` or `ref`
58
+ }
48
59
  if (args.length === 1 && typeof x === 'object') {
49
60
  if (is_array(x)) return x
50
61
  if (is_cqn(x)) return args
package/lib/ql/cds-ql.js CHANGED
@@ -1,4 +1,3 @@
1
- const cds = require('../index')
2
1
  const Query = require('./Query')
3
2
  require = path => { // eslint-disable-line no-global-assign
4
3
  const clazz = module.require (path); if (!clazz._api) return clazz
@@ -30,6 +29,7 @@ function _deprecated_srv_ql() { // eslint-disable-next-line no-console
30
29
  }
31
30
 
32
31
  module.exports._reset = ()=>{ // for strange tests only
32
+ const cds = require('../index')
33
33
  const _name = cds.env.sql.names === 'quoted' ? n =>`"${n}"` : n => n.replace(/[.:]/g,'_')
34
34
  Object.defineProperty (Query.prototype,'valueOf',{ configurable:1, value: function(cmd=this.cmd) {
35
35
  return `${cmd} ${_name(this._target.name)} `
@@ -4,8 +4,8 @@ const cds = require ('../index')
4
4
  * Messages Collector, used for `req.errors` and `req.messages`
5
5
  */
6
6
  class Responses extends Array {
7
- add (severity, code, message, target, args) { // NOSONAR
8
- let e // be filled in below...
7
+ static get (severity, code, message, target, args) {
8
+ let e // be filled in below...
9
9
  if (typeof code === 'object') e = code; else {
10
10
  if (typeof code === 'number') e = { code }; else [ code, message, target, args, e ] = [ undefined, code, message, target, {} ]
11
11
  if (typeof message === 'object') e = Object.assign(message,e); else {
@@ -16,9 +16,14 @@ class Responses extends Array {
16
16
  }
17
17
  }
18
18
  if (!e.numericSeverity) e.numericSeverity = severity
19
- this.push(e)
20
19
  return e
21
20
  }
21
+
22
+ add (...args) {
23
+ const response = Responses.get(...args)
24
+ this.push(response)
25
+ return response
26
+ }
22
27
  }
23
28
 
24
29
  class Errors extends Responses {