@sap/cds 6.4.1 → 6.6.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 (139) hide show
  1. package/CHANGELOG.md +79 -6
  2. package/README.md +5 -0
  3. package/apis/cqn.d.ts +14 -3
  4. package/apis/ql.d.ts +8 -8
  5. package/apis/services.d.ts +37 -65
  6. package/apis/test.d.ts +7 -0
  7. package/bin/build/buildTaskEngine.js +9 -14
  8. package/bin/build/buildTaskFactory.js +1 -1
  9. package/bin/build/buildTaskHandler.js +3 -14
  10. package/bin/build/index.js +8 -2
  11. package/bin/build/provider/buildTaskProviderInternal.js +18 -13
  12. package/bin/build/provider/fiori/index.js +5 -10
  13. package/bin/build/provider/hana/2migration.js +11 -2
  14. package/bin/build/provider/hana/index.js +17 -14
  15. package/bin/build/provider/hana/template/.hdiconfig-hanacloud +137 -0
  16. package/bin/build/provider/hana/template/package.json +3 -0
  17. package/bin/build/provider/mtx/resourcesTarBuilder.js +12 -3
  18. package/bin/build/provider/mtx-extension/index.js +57 -37
  19. package/bin/build/provider/mtx-sidecar/index.js +1 -1
  20. package/bin/build/util.js +18 -1
  21. package/bin/cds.js +1 -5
  22. package/bin/deploy/to-hana/hana.js +10 -3
  23. package/bin/serve.js +36 -20
  24. package/common.cds +7 -0
  25. package/lib/auth/jwt-auth.js +8 -7
  26. package/lib/compile/for/lean_drafts.js +55 -6
  27. package/lib/compile/minify.js +3 -3
  28. package/lib/dbs/cds-deploy.js +18 -17
  29. package/lib/env/cds-requires.js +1 -1
  30. package/lib/env/defaults.js +5 -1
  31. package/lib/env/schemas/cds-rc.json +74 -3
  32. package/lib/index.js +4 -2
  33. package/lib/lazy.js +6 -8
  34. package/lib/log/cds-error.js +2 -2
  35. package/lib/ql/Whereable.js +22 -11
  36. package/lib/ql/cds-ql.js +1 -1
  37. package/lib/req/cds-context.js +3 -3
  38. package/lib/req/response.js +8 -3
  39. package/lib/req/user.js +12 -2
  40. package/lib/srv/bindings.js +1 -2
  41. package/lib/srv/cds-serve.js +2 -1
  42. package/lib/srv/middlewares/trace.js +31 -15
  43. package/lib/srv/protocols/odata-v2-proxy.js +8 -8
  44. package/lib/srv/srv-handlers.js +26 -7
  45. package/lib/srv/srv-methods.js +2 -2
  46. package/lib/srv/srv-models.js +8 -3
  47. package/lib/utils/cds-test.js +7 -5
  48. package/lib/utils/cds-utils.js +3 -1
  49. package/lib/utils/tar.js +6 -3
  50. package/libx/_runtime/auth/strategies/JWT.js +1 -0
  51. package/libx/_runtime/auth/strategies/ias-auth.js +3 -2
  52. package/libx/_runtime/auth/strategies/mock.js +12 -1
  53. package/libx/_runtime/auth/strategies/xssecUtils.js +7 -8
  54. package/libx/_runtime/auth/strategies/xsuaa.js +1 -0
  55. package/libx/_runtime/cds-services/adapter/odata-v4/OData.js +6 -2
  56. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +1 -2
  57. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +26 -1
  58. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +8 -0
  59. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/ExpressionToCQN.js +1 -1
  60. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/readToCQN.js +11 -2
  61. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/utils/PrimitiveValueDecoder.js +8 -8
  62. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/utils/ValueConverter.js +1 -1
  63. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/validator/ValueValidator.js +14 -14
  64. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/deserializer/DeserializerFactory.js +1 -0
  65. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/serializer/ResourceJsonSerializer.js +3 -0
  66. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/utils/UriHelper.js +2 -1
  67. package/libx/_runtime/cds-services/adapter/odata-v4/utils/metaInfo.js +3 -2
  68. package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +7 -0
  69. package/libx/_runtime/cds-services/adapter/odata-v4/utils/stream.js +0 -3
  70. package/libx/_runtime/cds-services/services/Service.js +11 -19
  71. package/libx/_runtime/cds-services/services/utils/columns.js +42 -40
  72. package/libx/_runtime/cds-services/util/assert.js +7 -1
  73. package/libx/_runtime/common/code-ext/WorkerReq.js +81 -0
  74. package/libx/_runtime/common/code-ext/config.js +13 -0
  75. package/libx/_runtime/common/code-ext/execute.js +113 -0
  76. package/libx/_runtime/common/code-ext/handlers.js +49 -0
  77. package/libx/_runtime/common/code-ext/worker.js +40 -0
  78. package/libx/_runtime/common/code-ext/workerQuery.js +45 -0
  79. package/libx/_runtime/common/code-ext/workerQueryExecutor.js +36 -0
  80. package/libx/_runtime/common/composition/data.js +5 -2
  81. package/libx/_runtime/common/composition/tree.js +2 -0
  82. package/libx/_runtime/common/generic/auth/restrict.js +1 -1
  83. package/libx/_runtime/common/generic/crud.js +4 -0
  84. package/libx/_runtime/common/generic/etag.js +3 -1
  85. package/libx/_runtime/common/generic/input.js +12 -14
  86. package/libx/_runtime/common/i18n/index.js +1 -1
  87. package/libx/_runtime/common/utils/cqn2cqn4sql.js +47 -22
  88. package/libx/_runtime/common/utils/path.js +5 -26
  89. package/libx/_runtime/common/utils/search2cqn4sql.js +16 -9
  90. package/libx/_runtime/common/utils/templateProcessorPathSerializer.js +19 -13
  91. package/libx/_runtime/db/data-conversion/post-processing.js +1 -1
  92. package/libx/_runtime/db/expand/expandCQNToJoin.js +7 -4
  93. package/libx/_runtime/db/expand/rawToExpanded.js +3 -2
  94. package/libx/_runtime/db/generic/input.js +2 -2
  95. package/libx/_runtime/db/generic/integrity.js +1 -0
  96. package/libx/_runtime/db/generic/virtual.js +1 -0
  97. package/libx/_runtime/db/query/read.js +3 -2
  98. package/libx/_runtime/db/utils/localized.js +1 -1
  99. package/libx/_runtime/fiori/generic/activate.js +7 -1
  100. package/libx/_runtime/fiori/generic/before.js +9 -1
  101. package/libx/_runtime/fiori/generic/edit.js +8 -1
  102. package/libx/_runtime/fiori/generic/new.js +2 -0
  103. package/libx/_runtime/fiori/generic/patch.js +2 -0
  104. package/libx/_runtime/fiori/generic/prepare.js +2 -0
  105. package/libx/_runtime/fiori/generic/read.js +16 -5
  106. package/libx/_runtime/fiori/generic/readOverDraft.js +2 -0
  107. package/libx/_runtime/fiori/lean-draft.js +505 -241
  108. package/libx/_runtime/fiori/utils/delete.js +2 -0
  109. package/libx/_runtime/hana/customBuilder/CustomSelectBuilder.js +5 -5
  110. package/libx/_runtime/hana/pool.js +1 -1
  111. package/libx/_runtime/hana/search2cqn4sql.js +51 -51
  112. package/libx/_runtime/messaging/Outbox.js +1 -1
  113. package/libx/_runtime/messaging/enterprise-messaging-utils/getTenantInfo.js +1 -0
  114. package/libx/_runtime/messaging/enterprise-messaging.js +2 -6
  115. package/libx/_runtime/messaging/file-based.js +1 -2
  116. package/libx/_runtime/messaging/outbox/OutboxRunner.js +1 -1
  117. package/libx/_runtime/messaging/outbox/utils.js +1 -1
  118. package/libx/_runtime/messaging/service.js +0 -1
  119. package/libx/_runtime/remote/Service.js +1 -0
  120. package/libx/_runtime/sqlite/convertDraftAdminPathExpression.js +19 -3
  121. package/libx/_runtime/sqlite/customBuilder/CustomExpressionBuilder.js +0 -18
  122. package/libx/_runtime/sqlite/customBuilder/CustomFunctionBuilder.js +0 -18
  123. package/libx/_runtime/sqlite/customBuilder/CustomSelectBuilder.js +0 -24
  124. package/libx/_runtime/sqlite/customBuilder/CustomUpsertBuilder.js +2 -1
  125. package/libx/_runtime/sqlite/customBuilder/index.js +47 -32
  126. package/libx/odata/afterburner.js +23 -8
  127. package/libx/odata/cqn2odata.js +1 -1
  128. package/libx/odata/grammar.pegjs +3 -4
  129. package/libx/odata/index.js +5 -1
  130. package/libx/odata/parseToCqn.js +3 -3
  131. package/libx/odata/parser.js +1 -1
  132. package/libx/odata/utils.js +58 -1
  133. package/libx/rest/middleware/parse.js +26 -4
  134. package/package.json +1 -1
  135. package/server.js +1 -1
  136. package/libx/_runtime/sqlite/customBuilder/CustomDeleteBuilder.js +0 -17
  137. package/libx/_runtime/sqlite/customBuilder/CustomReferenceBuilder.js +0 -11
  138. package/libx/_runtime/sqlite/customBuilder/CustomUpdateBuilder.js +0 -17
  139. /package/bin/build/provider/hana/template/{.hdiconfig → .hdiconfig-haas} +0 -0
@@ -24,20 +24,21 @@ module.exports = function jwt_auth(config) {
24
24
  .use(passport.authenticate(config.kind, { session: false }))
25
25
  .use((req, res, next) => {
26
26
  const payload = req.tokenInfo.getPayload()
27
-
27
+
28
28
  let id = req.user.id
29
-
29
+ let _is_system, _is_internal
30
+
30
31
  let roles = payload.scope.map(s => s.replace(new RegExp(`^(${config.credentials.xsappname + '.'})`), ''))
31
32
  roles.push('identified-user')
32
33
  if (payload.grant_type) {
33
34
  // > not "weak"
34
35
  roles.push('authenticated-user')
35
-
36
+
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,11 +50,11 @@ 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
  })
56
- .use((err, req, res, next) => {
57
+ .use((err, req, res, _next) => {
57
58
  if (req.tokenInfo) {
58
59
  LOG?.debug('error during token validation', req.tokenInfo.getErrorObject())
59
60
  }
@@ -1,5 +1,43 @@
1
- const cds = require ('../../index')
2
- module.exports = function cds_compile_for_lean_drafts(csn, o) {
1
+ const cds = require('../../index')
2
+
3
+ function _getBacklinkName(on) {
4
+ const i = on.findIndex(e => e.ref && e.ref[0] === '$self')
5
+ if (i === -1) return
6
+ let ref
7
+ if (on[i + 1] && on[i + 1] === '=') ref = on[i + 2].ref
8
+ if (on[i - 1] && on[i - 1] === '=') ref = on[i - 2].ref
9
+ return ref && ref[ref.length - 1]
10
+ }
11
+
12
+ function _isCompositionBacklink(e) {
13
+ if (!e.isAssociation) return
14
+ if (!e._target?.associations) return
15
+ if (!(!e.isComposition && (e.keys || e.on))) return
16
+ for (const anchor of Object.values(e._target.associations)) {
17
+ if (!(anchor.isComposition && anchor.on?.length > 2)) continue
18
+ if (_getBacklinkName(anchor.on) === e.name && anchor.target === e.parent.name) return anchor
19
+ }
20
+ }
21
+
22
+ const IGNORED_ANNOTATIONS = [
23
+ '@assert.range',
24
+ '@assert.enum',
25
+ '@assert.format',
26
+ '@assert.target',
27
+ '@mandatory',
28
+ '@Core.Immutable',
29
+ '@readonly',
30
+ '@cds.on.update',
31
+ '@cds.on.insert',
32
+ '@Core.Computed',
33
+ '@Common.FieldControl.Readonly',
34
+ '@Common.FieldControl.Mandatory',
35
+ '@FieldControl.Mandatory',
36
+ '@FieldControl.ReadOnly',
37
+ '@Common.FieldControl'
38
+ ]
39
+
40
+ module.exports = function cds_compile_for_lean_drafts(csn) {
3
41
  const DRAFT_ELEMENTS = new Set([
4
42
  'IsActiveEntity',
5
43
  'HasDraftEntity',
@@ -22,6 +60,13 @@ module.exports = function cds_compile_for_lean_drafts(csn, o) {
22
60
  return on
23
61
  }
24
62
 
63
+ function _isDraft(def) {
64
+ return (
65
+ def.associations?.DraftAdministrativeData ||
66
+ (def.own('@odata.draft.enabled') && def.own('@Common.DraftRoot.ActivationAction'))
67
+ )
68
+ }
69
+
25
70
  const { Draft } = cds.linked(`
26
71
  entity ActiveEntity { key ID: UUID; }
27
72
  entity Draft {
@@ -49,15 +94,21 @@ module.exports = function cds_compile_for_lean_drafts(csn, o) {
49
94
  const draft = { __proto__: active, name: _draftEntity, elements: { ...active.elements, ...Draft.elements } }
50
95
  Object.defineProperty(model.definitions, _draftEntity, { value: draft })
51
96
  Object.defineProperty(active, 'drafts', { value: draft })
97
+ Object.defineProperty(draft, 'actives', { value: active })
52
98
  draft['@cds.persistence.table'] = _draftEntity
99
+ if (draft['@restrict']) draft['@restrict'] = undefined
53
100
  // Recursively add drafts for compositions
54
101
  for (const each in draft.elements) {
55
102
  const e = draft.elements[each]
56
103
  const newEl = Object.create(e)
57
- if (e.isComposition || (e.isAssociation && e['@odata.draft.enclosed']) || e._isBacklink) {
104
+ if (e.isComposition || (e.isAssociation && e['@odata.draft.enclosed']) || _isCompositionBacklink(e)) {
105
+ if (e._target['@odata.draft.enabled'] === false) continue // happens for texts if @fiori.draft.enabled is not set
58
106
  _redirect(newEl, draftEntity(e._target, model))
59
107
  }
60
108
  newEl.parent = draft
109
+ for (const ignoredAnno of IGNORED_ANNOTATIONS) {
110
+ if (newEl[ignoredAnno]) newEl[ignoredAnno] = undefined
111
+ }
61
112
  draft.elements[each] = newEl
62
113
  }
63
114
  // TODO: Redirect associations to localized
@@ -65,9 +116,7 @@ module.exports = function cds_compile_for_lean_drafts(csn, o) {
65
116
  }
66
117
  for (const name in csn.definitions) {
67
118
  const def = csn.definitions[name]
68
- if (!def._isDraftEnabled || def.name.endsWith('.DraftAdministrativeData'))
69
- continue
70
- // so that database ignores them
119
+ if (!_isDraft(def)) continue
71
120
  ;[
72
121
  'IsActiveEntity',
73
122
  'HasDraftEntity',
@@ -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})
@@ -107,9 +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
111
- const in_memory = (creds?.url || creds?.database) === ':memory:';
112
- if (!in_memory && schemaEvo) {
110
+ if (schemaEvo) {
113
111
  const { afterImage: afterCsn, drops, createsAndAlters: creas } = cds.compile.to.sql.delta (csn, o, beforeCsn);
114
112
  if(beforeCsn === undefined) {
115
113
  // If this is the first deployment done with automatic schema evolution, generate everything as if it was a drop create
@@ -134,13 +132,12 @@ exports.create = async function (db, csn=db.model, o) {
134
132
  const schemaEvo = (db.options?.schema_evolution === 'auto' || o.schema_evolution === 'auto')
135
133
  if(db.deploy && !schemaEvo) {
136
134
  // reset CSN state saved in db - if there is any
137
- await db.run('DROP table if exists cds_Model;');
135
+ if(!o.dry) await db.run('DROP table if exists cds_Model;');
138
136
  return db.deploy(csn, o);
139
137
  }
140
138
 
141
- const in_memory = db.options?.credentials?.url === ':memory:';
142
139
  let beforeCsn
143
- if (!in_memory && schemaEvo) try {
140
+ if (schemaEvo) try {
144
141
  const [{ csn }] = await db.read('cds.Model')
145
142
  beforeCsn = JSON.parse(csn);
146
143
  } catch(e) {
@@ -160,7 +157,7 @@ exports.create = async function (db, csn=db.model, o) {
160
157
  } else return db.run (async tx => {
161
158
  await tx.run(drops)
162
159
  await tx.run(creas)
163
- if (!in_memory && schemaEvo) {
160
+ if (schemaEvo) {
164
161
  await tx.update('cds.Model').with({ csn: JSON.stringify(afterCsn) })
165
162
  }
166
163
  return true
@@ -169,27 +166,31 @@ exports.create = async function (db, csn=db.model, o) {
169
166
 
170
167
 
171
168
  exports.init = (db, csn=db.model, o, csvs, log=()=>{}) => db.run (async tx => {
169
+
170
+ const {tenant} = cds.context; if (tenant && tenant === cds.requires.multitenancy?.t0) return
171
+ const schemaEvo = db.options?.schema_evolution === 'auto' || o?.schema_evolution === 'auto'
172
172
  const resources = await exports.resources(csn, {testdata: cds.env.features.test_data})
173
173
  const inits=[]
174
- const schemaEvo = (db.options?.schema_evolution === 'auto' || o?.schema_evolution === 'auto')
175
-
176
174
 
177
175
  if (csvs) {
178
176
  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
- }))
177
+ for(let [file,src] of Object.entries(csvs)) {
178
+ const entity = _entity4(path.basename(file, '.csv'), csn)
179
+ if (entity?.name) {
180
+ const q = INSERT_from_csv (entity.name,src,schemaEvo); if (!q) continue
181
+ if (db.kind === 'better-sqlite') _add_missing_pks2(q)
182
+ q._target = ccsn.definitions[entity.name]
183
+ inits.push (tx.run(q) .catch (e => {
184
+ throw Object.assign (e, { message: 'in cds.deploy(): ' + e.message +'\n'+ inspect(q) })
185
+ }))
186
+ }
186
187
  }
187
188
  } else {
188
189
  for (let [file,e] of Object.entries(resources)) {
189
190
  if (e === '*') { // init.js/ts
190
191
  let x = await cds.utils._import(file); if (!x) continue
191
192
  if (x.default) x = x.default // default ESM export
192
- inits.push (!x.then && typeof x === 'function' ? x(db,csn) : x)
193
+ inits.push (!x.then && typeof x === 'function' ? x(tx,csn) : x)
193
194
  log (file)
194
195
  } else { // from .csv or .json
195
196
  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/index.js CHANGED
@@ -12,8 +12,9 @@ const facade = class cds extends require('events') {
12
12
  set context(_){ this._context._for(this,_) }
13
13
  get spawn() { return super.spawn = this._context.spawn }
14
14
 
15
- emit (eve, ...args) {
16
- if (eve === 'served') return Promise.all (this.listeners(eve).map (l => l.call(this,...args)))
15
+ async emit (eve, ...args) {
16
+ // if (eve === 'served') return Promise.all (this.listeners(eve).map (l => l.call(this,...args)))
17
+ if (eve === 'served') for (let l of this.listeners(eve)) await l.call(this,...args)
17
18
  else return super.emit (eve, ...args)
18
19
  }
19
20
  }
@@ -93,6 +94,7 @@ const cds = module.exports = extend (new facade) .with ({
93
94
  test: require ('./utils/cds-test'),
94
95
  log: require ('./log/cds-log'), debug: lazy => cds.log.debug,
95
96
  exec: require ('../bin/cds'),
97
+ exit: (code) => cds.shutdown ? cds.shutdown() : process.exit(code),
96
98
  clone: m => JSON.parse (JSON.stringify(m)),
97
99
  lazified, lazify,
98
100
 
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)} `
@@ -47,9 +47,9 @@ module.exports = new class extends AsyncLocalStorage {
47
47
  })
48
48
  }
49
49
  const em = new EventEmitter; em.timer = (
50
- (o && o.after) ? setTimeout(fx, o.after) :
51
- (o && o.every) ? setInterval(fx, o.every) :
52
- setImmediate(fx)
50
+ (o && o.after) ? setTimeout(fx, o.after).unref() :
51
+ (o && o.every) ? setInterval(fx, o.every).unref() :
52
+ setImmediate(fx).unref()
53
53
  )
54
54
  return em
55
55
  }
@@ -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 {
package/lib/req/user.js CHANGED
@@ -1,3 +1,5 @@
1
+ const PSEUDO_ROLES = ['system-user', 'internal-user']
2
+
1
3
  class User {
2
4
 
3
5
  constructor (_) {
@@ -9,14 +11,22 @@ class User {
9
11
  if (typeof _ === 'string') { this.id = _; return }
10
12
  for (let each in _) super[each === '_roles' ? 'roles' : each] = _[each] // overrides getters
11
13
  const roles = this.hasOwnProperty('roles') && this.roles // eslint-disable-line no-prototype-builtins
12
- if (Array.isArray(roles)) this.roles = roles.reduce ((p,n)=>{p[n]=1; return p},{})
14
+ if (Array.isArray(roles)) this.roles = roles.filter(r => !PSEUDO_ROLES.includes(r)).reduce ((p,n)=>{p[n]=1; return p},{})
15
+ else PSEUDO_ROLES.forEach(r => delete this.roles[r])
13
16
  }
14
17
 
15
18
  get attr() { return super.attr = {} }
16
19
  get roles(){ return super.roles = {} }
17
20
  get _roles(){ return this.roles } // compatibility
18
21
 
19
- is (role) { return role === 'any' || role === 'identified-user' || role === 'authenticated-user' && this.authLevel !== 'weak' || !!this.roles[role] }
22
+ is (role) {
23
+ return role === 'any' ||
24
+ role === 'identified-user' ||
25
+ role === 'system-user' && this._is_system ||
26
+ role === 'internal-user' && this._is_internal ||
27
+ role === 'authenticated-user' && this.authLevel !== 'weak' ||
28
+ !!this.roles[role]
29
+ }
20
30
  valueOf() { return this.id }
21
31
 
22
32
  }
@@ -68,8 +68,7 @@ module.exports = class Bindings {
68
68
  }
69
69
  }
70
70
  }
71
- cds.on ('shutdown', ()=>this.purge())
72
- process.on ('exit', ()=>this.purge()) // last resort e.g. in case of errors
71
+ process.on ('exit', ()=>this.purge())
73
72
  return this.store()
74
73
  }
75
74
 
@@ -56,7 +56,8 @@ module.exports = function cds_serve (som, _options) { // NOSONAR
56
56
  if (o.service && o.service !== 'all') {
57
57
  // skip services not chosen by o.service, if specified
58
58
  const specified = o.service.split(/\s*,\s*/).map (s => required[s] && required[s].service || s )
59
- services = services.filter (s => specified.some (n => s.name.endsWith(n)))
59
+ // matching exact or unqualified name
60
+ services = services.filter (s => specified.some (n => s.name === n || s.name.endsWith('.'+n)))
60
61
  if (!services.length) throw cds.error (`No such service: '${o.service}'`)
61
62
  }
62
63
  services = services.filter (d => !(
@@ -66,15 +66,22 @@ function _instrument_sqlite (_get_perf) {
66
66
  try { require.resolve('sqlite3') } catch { return }
67
67
  // eslint-disable-next-line cds/no-missing-dependencies
68
68
  const sqlite = require('sqlite3').Database.prototype
69
- for (let each of ['all', 'get', 'run', 'prepare']) {
70
- const _super = sqlite[each]
71
- sqlite[each] = function (q, ..._) {
72
- const perf = _get_perf(q)
69
+ for (let each of ['all', 'get', 'run', 'prepare']) _wrap(each,sqlite)
70
+ function _wrap (op,sqlite) {
71
+ const _super = sqlite[op]
72
+ sqlite[op] = function (q, ..._) {
73
+ const perf = _get_perf(q) //> q is a SQL command like BEGIN, COMMIT, ROLLBACK, SELECT ...
73
74
  if (perf) {
74
- const pe = perf.log ('sqlite -', q)
75
- const callback = _[_.length-1]; _[_.length-1] = function(){
76
- perf.done(pe)
77
- callback.apply (this, arguments)
75
+ const pe = perf.log ('sqlite3', '-', q)
76
+ const callback = _[_.length-1]; _[_.length-1] = function(ps){
77
+ if (op === 'prepare') callback.apply (this, {
78
+ all: _wrap('all',ps),
79
+ get: _wrap('get',ps),
80
+ run: _wrap('run',ps),
81
+ }); else {
82
+ perf.done(pe)
83
+ callback.apply (this, arguments)
84
+ }
78
85
  }
79
86
  }
80
87
  return _super.call (this, q, ..._)
@@ -87,14 +94,23 @@ function _instrument_better_sqlite (_get_perf) {
87
94
  try { require.resolve('better-sqlite3') } catch { return }
88
95
  // eslint-disable-next-line cds/no-missing-dependencies
89
96
  const sqlite = require('better-sqlite3').prototype
90
- for (let each of ['exec', 'prepare']) {
91
- const _super = sqlite[each]
92
- sqlite[each] = function (q, ..._) {
93
- const perf = _get_perf(q)
97
+ for (let each of ['exec', 'prepare']) _wrap(each,sqlite)
98
+ function _wrap (op,sqlite) {
99
+ const _super = sqlite[op]
100
+ sqlite[op] = function (q, ..._) {
101
+ const perf = _get_perf(q) //> q is a SQL command like BEGIN, COMMIT, ROLLBACK, SELECT ...
94
102
  if (perf) {
95
- const pe = perf.log ('sqlite -', q)
96
- try { return _super.call (this, q, ..._) }
97
- finally { perf.done(pe) }
103
+ const pe = perf.log ('better-sqlite3', '-', q)
104
+ try {
105
+ const x = _super.call (this, q, ..._)
106
+ if (op === 'prepare') return {
107
+ all(..._){ try { return x.all(..._) } finally { perf.done(pe) }},
108
+ get(..._){ try { return x.get(..._) } finally { perf.done(pe) }},
109
+ run(..._){ try { return x.run(..._) } finally { perf.done(pe) }},
110
+ }
111
+ else return x
112
+ }
113
+ catch(e) { perf.done(pe); throw e }
98
114
  }
99
115
  else return _super.call (this, q, ..._)
100
116
  }