@sap/cds 6.1.0 → 6.1.1

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 (35) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/apis/log.d.ts +3 -5
  3. package/bin/build/provider/buildTaskHandlerFeatureToggles.js +2 -3
  4. package/bin/build/provider/hana/index.js +4 -2
  5. package/bin/build/provider/mtx/resourcesTarBuilder.js +4 -8
  6. package/bin/deploy/to-hana/hana.js +20 -25
  7. package/bin/deploy/to-hana/hdiDeployUtil.js +13 -2
  8. package/lib/dbs/cds-deploy.js +2 -2
  9. package/lib/env/schemas/cds-rc.json +10 -1
  10. package/lib/ql/Query.js +9 -3
  11. package/lib/ql/SELECT.js +1 -1
  12. package/lib/ql/cds-ql.js +0 -9
  13. package/lib/req/context.js +15 -11
  14. package/lib/srv/srv-api.js +8 -0
  15. package/lib/srv/srv-dispatch.js +11 -7
  16. package/lib/srv/srv-models.js +4 -3
  17. package/lib/srv/srv-tx.js +52 -40
  18. package/lib/utils/cds-utils.js +3 -3
  19. package/lib/utils/resources/index.js +5 -5
  20. package/lib/utils/resources/tar.js +1 -1
  21. package/libx/_runtime/auth/index.js +2 -2
  22. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/utils/PrimitiveValueDecoder.js +0 -2
  23. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/deserializer/DeserializerFactory.js +3 -1
  24. package/libx/_runtime/common/utils/binary.js +3 -4
  25. package/libx/_runtime/common/utils/resolveView.js +1 -1
  26. package/libx/_runtime/db/expand/rawToExpanded.js +7 -6
  27. package/libx/_runtime/db/generic/input.js +4 -1
  28. package/libx/_runtime/extensibility/add.js +3 -0
  29. package/libx/_runtime/extensibility/handler/transformREAD.js +20 -18
  30. package/libx/_runtime/extensibility/push.js +11 -11
  31. package/libx/_runtime/extensibility/utils.js +8 -6
  32. package/libx/_runtime/sqlite/Service.js +7 -5
  33. package/libx/_runtime/sqlite/execute.js +41 -28
  34. package/libx/rest/RestAdapter.js +3 -6
  35. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,37 @@
4
4
  - The format is based on [Keep a Changelog](http://keepachangelog.com/).
5
5
  - This project adheres to [Semantic Versioning](http://semver.org/).
6
6
 
7
+ ## Version 6.1.1 - 2022-08-24
8
+
9
+ ### Added
10
+ - The configuration schema now includes `cds.extends` and `new-fields` (in `cds.xt.ExtensibilityService`)
11
+
12
+ - `srv.run(fn)` now accepts a function as first argument, which will be run in an active outer transaction, if any, or in a newly created one. This is in contrast to `srv.tx(fn)` which always creates a new tx.
13
+ ```js
14
+ cds.run (tx => { // nested operations are guaranteed to run in a tx
15
+ await INSERT.into (Foo, ...)
16
+ await INSERT.into (Bar, ...)
17
+ })
18
+ ```
19
+
20
+ ### Fixed
21
+
22
+ - Erroneous checks for `join` in `SELECT.from(SELECT.from('xxx'))`
23
+ - Virtual fields with default values in draft context
24
+ - View resolving without model information doesn't crash
25
+ - Unable to upload large attachments. Uploading a large attachment (base64 encoded) caused a runtime exception.
26
+ - `cds.Query.then()` is using `AsyncResource.runInAsyncScope` from now on. → this avoids callstacks being cut off, e.g. in debuggers.
27
+ - `cds.tx()` and `cds.context` have been fixed to avoid accidential fallbacks to auto-commit mode.
28
+ - HDI configuration data (e.g. `./cfg`, `.hdiignore`) is now included in the `resources.tgz` file which is required for Streamlined MTX.
29
+ - `cds deploy` accepts in addition to `VCAP_SERVICES` also `TARGET_CONTAINER` and `SERVICE_REPLACEMENTS` from vcap file when using `--vcap-file` parameter.
30
+ - `cds build` doesn't duplicate CSV files that are contained in `db/src/**`.
31
+ - Typescript issues in `apis/log.d.ts`
32
+ - `cds build` adds OS agnostic base model path to generated feature CSN.
33
+ - Unhandled promise rejection in `expand` handling
34
+ - `cds.context.model` middleware is not mounted for not extensible services
35
+ - `cds.context` continuation was sometimes not reset in REST adapter
36
+ - Requests don't fail with `RangeError: Unable to get service from service map due to error: Invalid time value` anymore
37
+
7
38
  ## Version 6.1.0 - 2022-08-10
8
39
 
9
40
  ### Added
package/apis/log.d.ts CHANGED
@@ -24,7 +24,7 @@ declare class cds {
24
24
  log(name: string, options?: string | number | { level: number, prefix: string }): Logger
25
25
  }
26
26
 
27
- class Logger {
27
+ declare class Logger {
28
28
  /**
29
29
  * Formats a log outputs by returning an array of arguments which are passed to
30
30
  * console.log() et al.
@@ -41,8 +41,6 @@ class Logger {
41
41
  error(message?: any, ...optionalParams: any[]): void
42
42
  }
43
43
 
44
- declare const levels = {
45
- SILENT: 0, ERROR: 1, WARN: 2, INFO: 3, DEBUG: 4, TRACE: 5, SILLY: 5, VERBOSE: 5
44
+ declare enum levels {
45
+ SILENT = 0, ERROR = 1, WARN = 2, INFO = 3, DEBUG = 4, TRACE = 5, SILLY = 5, VERBOSE = 5
46
46
  }
47
-
48
- declare const { ERROR, WARN, INFO, DEBUG, TRACE } = levels
@@ -100,17 +100,16 @@ class FeatureToggleBuilder extends BuildTaskHandlerInternal {
100
100
  }
101
101
  const features = {}
102
102
  const options = { ...this.options(), flavor: 'parsed' }
103
- const srvFolder = path.relative(destFts, destBase)
104
103
 
105
104
  // create feature models
106
105
  for (const ftName in ftsPaths) {
107
106
  const ftCsn = await this.cds.load(ftsPaths[ftName], options)
107
+ const ftPath = path.join(destFts, this.ftsName, ftName)
108
108
 
109
109
  // replace require paths by base model path to ensure precedence of feature annos
110
110
  // see https://pages.github.tools.sap/cap/docs/cds/compiler-messages#anno-duplicate-unrelated-layer
111
- ftCsn.requires = [path.join('../..', srvFolder, DEFAULT_CSN_FILE_NAME)]
111
+ ftCsn.requires = [path.join(path.relative(ftPath, destBase), DEFAULT_CSN_FILE_NAME).replace(/\\/g,'/')]
112
112
 
113
- const ftPath = path.join(destFts, this.ftsName, ftName)
114
113
  await this.compileToJson(ftCsn, path.join(ftPath, DEFAULT_CSN_FILE_NAME))
115
114
  await this._validateFeature(ftPath)
116
115
  features[ftName] = ftCsn
@@ -106,8 +106,11 @@ class HanaModuleBuilder extends BuildTaskHandlerInternal {
106
106
 
107
107
  // 1. copy csv files into 'src/gen/data' or 'src/gen/csv' subfolder
108
108
  const promises = []
109
+ const dbSrc = path.join(src, 'src')
110
+
109
111
  resources.forEach(res => {
110
- if (res && /\.csv$/.test(res)) {
112
+ // do not duplicate resources that are already contained in db/src/**
113
+ if (res && /\.csv$/.test(res) && !res.startsWith(dbSrc)) {
111
114
  promises.push(this.copy(res).to(path.join(this.task.options.compileDest, csvFolder, path.basename(res))))
112
115
  }
113
116
  })
@@ -118,7 +121,6 @@ class HanaModuleBuilder extends BuildTaskHandlerInternal {
118
121
  blockList += this.hasBuildOption(CONTENT_ENV, false) ? "|\\.env($|\\..*$)" : ""
119
122
  blockList += this.hasBuildOption(CONTENT_DEFAULT_ENV_JSON, false) ? "|default-env\\.json$" : ""
120
123
  blockList = new RegExp(blockList)
121
- const dbSrc = path.join(src, 'src')
122
124
 
123
125
  await this.copyNativeContent(src, dest, (entry) => {
124
126
  if (entry.startsWith(dbSrc)) {
@@ -47,20 +47,16 @@ class ResourcesTarBuilder {
47
47
  * Reads all resources for HANA deployment - 'db/src/**' and 'db/undeploy.json'.
48
48
  * - CSV files, native HANA artefacts, generated HANA artefacts, undeploy.json
49
49
  * See '../../../../lib.deploy'
50
- *
50
+ * Note: the hana build task has already been executed
51
51
  * @param {string} root
52
52
  * @returns an array containing all resources
53
53
  */
54
54
  _getHanaResources(root) {
55
- const hanaSrcFolder = path.join(root, 'src')
56
- const undeployJson = path.join(root, 'undeploy.json')
55
+ const hanaNativeFolders = [path.join(root, 'src'), path.join(root, 'cfg')]
56
+ const hanaNativeFiles = [path.join(root, 'undeploy.json'), path.join(root, '.hdiignore')]
57
57
 
58
- // hana build task has already been executed
59
58
  return BuildTaskHandlerInternal._find(root, (res) => {
60
- if (res === undeployJson) {
61
- return true
62
- }
63
- if (res.startsWith(hanaSrcFolder)) {
59
+ if (hanaNativeFolders.some(folder => res.startsWith(folder)) || hanaNativeFiles.some(file => res === file)) {
64
60
  return true
65
61
  }
66
62
  return false
@@ -35,7 +35,7 @@ class HanaDeployer {
35
35
  this.logger = logger;
36
36
 
37
37
  this.logger.log(`${bold('Starting deploy to SAP HANA ...')}`);
38
- if (vcapFile) {
38
+ if (vcapFile) {
39
39
  this.logger.log();
40
40
  this.logger.log(`Using VCAP_SERVICES from file ${vcapFile} (beta feature).`);
41
41
  bindCallback = null // credentials are given - no cds bind then
@@ -49,9 +49,9 @@ class HanaDeployer {
49
49
 
50
50
  const { buildResults } = await this._build(buildTaskOptions, model);
51
51
 
52
- let vcapServices;
52
+ let vcapFileEnv;
53
53
  if (vcapFile) {
54
- vcapServices = await this._loadVCAPServices(vcapFile);
54
+ vcapFileEnv = await this._loadDefaultEnv(vcapFile);
55
55
  }
56
56
 
57
57
  for (const buildResult of buildResults) {
@@ -71,7 +71,7 @@ class HanaDeployer {
71
71
  serviceKeyName = cfServiceInstanceKeyName;
72
72
  serviceName = serviceName || cfServiceInstanceName;
73
73
 
74
- vcapServices = this._getVCAPServicesEntry(cfServiceInstanceName, serviceKey);
74
+ vcapFileEnv = this._getVCAPServicesEntry(cfServiceInstanceName, serviceKey);
75
75
 
76
76
  if (!noSave && !bindCallback) {
77
77
  await this._addInstanceToDefaultEnvJson([currentModelFolder, projectPath], cfServiceInstanceName, serviceKey);
@@ -95,7 +95,7 @@ class HanaDeployer {
95
95
  });
96
96
  }
97
97
 
98
- await hdiDeployUtil.deploy(currentModelFolder, vcapServices, hdiOptions);
98
+ await hdiDeployUtil.deploy(currentModelFolder, vcapFileEnv, hdiOptions);
99
99
 
100
100
  if (bindCallback) {
101
101
  const args = [path.relative(projectPath, buildResult.task.src)];
@@ -204,16 +204,12 @@ class HanaDeployer {
204
204
  }
205
205
 
206
206
 
207
- async _loadVCAPServices(vcapFile) {
207
+ async _loadDefaultEnv(defaultEnvFile) {
208
208
  try {
209
- const content = JSON.parse(await fs.readFile(vcapFile));
210
- if (!content.VCAP_SERVICES) {
211
- throw new Error(`The vcap file ${vcapFile} does not contain a VCAP_SERVICES entry.`);
212
- }
213
-
214
- return content.VCAP_SERVICES;
209
+ const content = await fs.readFile(defaultEnvFile);
210
+ return JSON.parse(content);
215
211
  } catch (err) {
216
- throw new Error(`Error reading vcap file: ${err.message}`);
212
+ throw new Error(`Error reading default env file: ${err.message}`);
217
213
  }
218
214
  }
219
215
 
@@ -237,13 +233,10 @@ class HanaDeployer {
237
233
  }
238
234
 
239
235
  const hanaEntry = this._getVCAPServicesEntry(serviceInstanceName, serviceKey)
240
- defaultEnvJson.VCAP_SERVICES = {
241
- ...defaultEnvJson.VCAP_SERVICES,
242
- ...hanaEntry
243
- }
236
+ Object.assign(defaultEnvJson, hanaEntry);
244
237
 
245
238
  this.logger.log(`Writing ${defaultEnvJsonPath}`);
246
- await fs.mkdir(path.dirname(defaultEnvJsonPath), {recursive: true})
239
+ await fs.mkdir(path.dirname(defaultEnvJsonPath), { recursive: true })
247
240
  await fs.writeFile(defaultEnvJsonPath, JSON.stringify(defaultEnvJson, null, 2));
248
241
  }
249
242
  }
@@ -251,13 +244,15 @@ class HanaDeployer {
251
244
 
252
245
  _getVCAPServicesEntry(serviceInstanceName, serviceKey) {
253
246
  return {
254
- hana: [
255
- {
256
- name: serviceInstanceName,
257
- tags: ['hana'],
258
- credentials: serviceKey
259
- }
260
- ]
247
+ VCAP_SERVICES: {
248
+ hana: [
249
+ {
250
+ name: serviceInstanceName,
251
+ tags: ['hana'],
252
+ credentials: serviceKey
253
+ }
254
+ ]
255
+ }
261
256
  };
262
257
  }
263
258
 
@@ -20,7 +20,10 @@ class HdiDeployUtil {
20
20
  await this._executeDeploy(dbDir, env, logger);
21
21
  }
22
22
 
23
- async deploy(dbDir, vcapServices, options = {}) {
23
+ async deploy(dbDir, vcapEnv, options) {
24
+ vcapEnv = vcapEnv || {}; // handles null and undefined
25
+ options = options || {};
26
+
24
27
  LOG.log();
25
28
  LOG.log(`Deploying to HANA from ${dbDir}`);
26
29
 
@@ -34,7 +37,15 @@ class HdiDeployUtil {
34
37
  deployerEnv = hdiDeployLib.clean_env(deployerEnv);
35
38
  }
36
39
 
37
- deployerEnv.VCAP_SERVICES = JSON.stringify(vcapServices);
40
+ if (vcapEnv.VCAP_SERVICES) {
41
+ deployerEnv.VCAP_SERVICES = JSON.stringify(vcapEnv.VCAP_SERVICES);
42
+ }
43
+ if (vcapEnv.SERVICE_REPLACEMENTS) {
44
+ deployerEnv.SERVICE_REPLACEMENTS = JSON.stringify(vcapEnv.SERVICE_REPLACEMENTS);
45
+ }
46
+ if (vcapEnv.TARGET_CONTAINER) {
47
+ deployerEnv.TARGET_CONTAINER = vcapEnv.TARGET_CONTAINER;
48
+ }
38
49
 
39
50
  if (options.autoUndeploy) {
40
51
  LOG.log(`Hdi deployer automatically undeploys deleted resources using --auto-undeploy.`);
@@ -111,7 +111,7 @@ exports.create = async function (db, csn=db.model, o) {
111
111
  console.log(); for (let each of drops) console.log(each)
112
112
  console.log(); for (let each of creates) console.log(each,'\n')
113
113
  return
114
- } else return db.tx (async tx => {
114
+ } else return db.run (async tx => {
115
115
  await tx.run(drops)
116
116
  await tx.run(creates)
117
117
  return true
@@ -120,7 +120,7 @@ exports.create = async function (db, csn=db.model, o) {
120
120
  }
121
121
 
122
122
 
123
- exports.init = (db, csn=db.model, log=()=>{}) => db.tx (async tx => {
123
+ exports.init = (db, csn=db.model, log=()=>{}) => db.run (async tx => {
124
124
  const resources = await exports.resources(csn, {testdata: cds.env.features.test_data})
125
125
  const inits=[]
126
126
  for (let [file,e] of Object.entries(resources)) {
@@ -126,6 +126,10 @@
126
126
  }
127
127
  }
128
128
  },
129
+ "extends": {
130
+ "description": "Name of the application that shall be extended",
131
+ "type": "string"
132
+ },
129
133
  "requires": {
130
134
  "type": "object",
131
135
  "description": "CDS Dependencies to databases and services can be configured by listing them within a requires section.",
@@ -567,7 +571,7 @@
567
571
  "uniqueItems": true,
568
572
  "items": {
569
573
  "type": "object",
570
- "additionalProperties": true,
574
+ "additionalProperties": false,
571
575
  "properties": {
572
576
  "for": {
573
577
  "type": "array",
@@ -595,6 +599,11 @@
595
599
  "type": "integer",
596
600
  "description": "Number of fields to be added at most",
597
601
  "minimum": 1
602
+ },
603
+ "new-entities": {
604
+ "type": "integer",
605
+ "description": "Number of entities to be added at most",
606
+ "minimum": 1
598
607
  }
599
608
  }
600
609
  }
package/lib/ql/Query.js CHANGED
@@ -1,7 +1,8 @@
1
+ const { AsyncResource } = require('async_hooks')
1
2
  const { inspect } = require('util')
2
3
  const cds = require('../index')
3
4
 
4
- module.exports = class Query {
5
+ class Query {
5
6
 
6
7
  constructor(_={}) { this[this.cmd] = _ }
7
8
 
@@ -16,8 +17,10 @@ module.exports = class Query {
16
17
  }
17
18
 
18
19
  /** Turns all queries into Thenables which execute with primary db by default */
19
- then (r,e) {
20
- return (this._srv || cds.db) .run (this) .then (r,e)
20
+ get then() {
21
+ const srv = this._srv || cds.db || cds.error `Can't execute query as no primary database is connected.`
22
+ const q = new AsyncResource('await cds.query')
23
+ return (r,e) => q.runInAsyncScope (srv.run, srv, this) .then (r,e)
21
24
  }
22
25
 
23
26
  /** Beautifies output in REPL */
@@ -87,3 +90,6 @@ const _target4 = (target, arg2) => target && (
87
90
  )
88
91
 
89
92
  const _name = cds.env.sql.names === 'quoted' ? n =>`"${n}"` : n => n.replace(/[.:]/g,'_')
93
+
94
+
95
+ module.exports = Query
package/lib/ql/SELECT.js CHANGED
@@ -94,7 +94,7 @@ module.exports = class SELECT extends Whereable {
94
94
  return Object.defineProperty(this, '_where_or_having', { value: 'on', configurable: true })
95
95
  }
96
96
  on (...args) {
97
- if (!this.SELECT.from || !this.SELECT.from.join)
97
+ if (!this.SELECT.from || !this.SELECT.from.join || !this.SELECT.from.args) // `join` can also be a function, e.g. in SELECT.from(SELECT.from('foo'))
98
98
  throw new Error(`Invalid call of "SELECT.on()" without prior call of "SELECT.join()"`)
99
99
  return this._where (args,'and','on')
100
100
  }
package/lib/ql/cds-ql.js CHANGED
@@ -22,15 +22,6 @@ function _deprecated_srv_ql() { // eslint-disable-next-line no-console
22
22
  return module.exports
23
23
  }
24
24
 
25
- if (cds.env.features.cls && cds.env.features.debug_queries) {
26
- const Query = require('./Query'), { then } = Query.prototype
27
- const { AsyncResource } = require('async_hooks')
28
- Object.defineProperty (Query.prototype,'then',{ get(){
29
- const q = new AsyncResource('cds.Query')
30
- return (r,e) => q.runInAsyncScope (then,this,r,e)
31
- }})
32
- }
33
-
34
25
  module.exports._reset = ()=>{ // for strange tests only
35
26
  const _name = cds.env.sql.names === 'quoted' ? n =>`"${n}"` : n => n.replace(/[.:]/g,'_')
36
27
  Object.defineProperty (require('./Query').prototype,'valueOf',{ configurable:1, value: function(cmd=this.cmd) {
@@ -12,11 +12,15 @@ const { EventEmitter } = require('events')
12
12
  class EventContext {
13
13
 
14
14
  /** Creates a new instance that inherits from cds.context */
15
- static for (_) {
15
+ static for (_,_as_root) {
16
16
  const ctx = new this (_)
17
- if (features.cds_tx_inheritance) {
18
- const base = cds.context
19
- if (base) ctx._set('_propagated', base)
17
+ const base = cds.context
18
+ if (base) {
19
+ ctx._set('_propagated', base) // we inherit from former cds.currents
20
+ if (!_as_root) {
21
+ if (!ctx.context) ctx._set('context', base.context) // all transaction handling works with root contexts
22
+ if (!ctx.tx && base.tx) ctx.tx = base.tx
23
+ }
20
24
  }
21
25
  return ctx
22
26
  }
@@ -38,7 +42,7 @@ class EventContext {
38
42
  //
39
43
 
40
44
  get emitter() {
41
- return this.context._emitter || (this.context._emitter = new EventEmitter)
45
+ return this.context._emitter || this.context._set('_emitter', new EventEmitter)
42
46
  }
43
47
 
44
48
  async emit (event,...args) {
@@ -115,7 +119,8 @@ class EventContext {
115
119
  }
116
120
 
117
121
  get model() {
118
- return super.model = this._propagated.model || this.http?.req.__model // IMPORTANT: Never use that anywhere else
122
+ const m = this._propagated.model || this.http?.req.__model // IMPORTANT: Never use that anywhere else
123
+ return this._set('model',m)
119
124
  }
120
125
  set model(m) {
121
126
  super.model = m
@@ -156,12 +161,11 @@ class EventContext {
156
161
  */
157
162
  set tx (tx) {
158
163
  Object.defineProperty (this,'tx',{value:tx}) //> allowed only once!
159
- const ctx = tx.context
160
- if (ctx && ctx !== this) {
161
- if (!this.hasOwnProperty('context')) this.context = ctx // eslint-disable-line no-prototype-builtins
162
-
164
+ const root = tx.context?.context
165
+ if (root && root !== this) {
166
+ if (!this.hasOwnProperty('context')) this.context = root // eslint-disable-line no-prototype-builtins
163
167
  if (features.assert_integrity && features.assert_integrity_type == 'RT') {
164
- const reqs = ctx._children || ctx._set('_children', {})
168
+ const reqs = root._children || root._set('_children', {})
165
169
  const all = reqs[tx.name] || (reqs[tx.name] = [])
166
170
  all.push(this)
167
171
  }
@@ -56,6 +56,14 @@ class Service extends require('./srv-handlers') {
56
56
  * Querying API to send synchronous requests...
57
57
  */
58
58
  run (query, data) {
59
+ if (typeof query === 'function') {
60
+ const ctx = cds.context, fn = query
61
+ if (ctx?.tx && !ctx.tx._done) {
62
+ return fn (this.tx(ctx)) // with nested tx
63
+ } else {
64
+ return this.tx (fn) // with root tx
65
+ }
66
+ }
59
67
  const req = new Request ({ query, data })
60
68
  return this.dispatch (req)
61
69
  }
@@ -13,18 +13,23 @@ exports.dispatch = async function dispatch (req) { //NOSONAR
13
13
 
14
14
  // Ensure we are in a proper transaction
15
15
  if (!this.context) {
16
- const gc = cds.context
17
- if (gc && gc.tx && !gc.tx._done) return this.tx(gc).dispatch(req) // with nested tx
18
- else return this.tx(tx => tx.dispatch(req)) // as root tx
16
+ const ctx = cds.context
17
+ if (ctx?.tx && !ctx.tx._done) {
18
+ return this.tx (ctx) .dispatch(req) // with nested tx
19
+ } else {
20
+ return this.tx (tx => tx.dispatch(req)) // with root tx
21
+ }
19
22
  }
20
23
  if (!req.tx) req.tx = this // `this` is a tx from now on...
21
24
 
22
25
  // Inform potential listeners // REVISIT: -> this should move into protocol adapters
23
- if (_is_root(req)) req._.req.emit ('dispatch',req)
26
+ let _is_root = req.constructor.name in { ODataRequest:1, RestRequest:2 }
27
+ if (_is_root) req._.req.emit ('dispatch',req)
24
28
 
25
29
  // Handle batches of queries
26
- if (_is_array(req.query))
27
- return Promise.all (req.query.map (q => this.dispatch ({query:q,__proto__:req,context:req.context})))
30
+ if (_is_array(req.query)) return Promise.all (req.query.map (
31
+ q => this.dispatch ({ query:q, context: req.context, __proto__:req })
32
+ ))
28
33
 
29
34
  // Ensure target and fqns
30
35
  if (!req.target) _ensure_target (this,req)
@@ -88,7 +93,6 @@ exports.handle = async function handle (req) {
88
93
  }
89
94
 
90
95
 
91
- const _is_root = (req) => /OData|REST/i.test(req.constructor.name)
92
96
  const _is_array = Array.isArray
93
97
  const _dummy = ()=>{} // REVISIT: required for some messaging tests which obviously still expect and call next()
94
98
 
@@ -22,13 +22,13 @@ class ExtendedModels {
22
22
  */
23
23
  static middleware4 (srv) {
24
24
  if (!(srv instanceof cds.ApplicationService)) return [] //> no middleware to add // REVISIT: move to `srv.isExtensible`
25
- if (srv.name?.startsWith('cds.xt.')) return [] //> no middleware to add // REVISIT: move to `srv.isExtensible`
26
- else return async (req,res,next) => {
25
+ if (!srv.isExtensible) return [] //> no middleware to add
26
+ else return async function cds_context_model (req,res,next) {
27
27
  if (!req.user?.tenant) return next()
28
28
  const ctx = cds.context = cds.EventContext.for({ http: { req, res } })
29
29
  ctx.user = req.user // REVISIT: should move to auth middleware?
30
30
  try {
31
- ctx.model = req.__model = await this.model4 (ctx.tenant, ctx.features)
31
+ ctx.model = req.__model = await ExtendedModels.model4 (ctx.tenant, ctx.features)
32
32
  } catch (e) {
33
33
  LOG.error (Object.assign(e, { message: 'Unable to get service from service map due to error: ' + e.message }))
34
34
 
@@ -99,6 +99,7 @@ class ExtendedModels {
99
99
  if (model.then) return model //> promised model to avoid race conditions
100
100
 
101
101
  const {_cached} = model, interval = ExtendedModels.checkInterval
102
+ if (!_cached.touched) return model
102
103
  if (Date.now() - _cached.touched < interval) return model //> checked recently
103
104
 
104
105
  else return this[key] = (async()=>{ // temporarily replace cache entry by promise to avoid race conditions...
package/lib/srv/srv-tx.js CHANGED
@@ -1,6 +1,18 @@
1
1
  const cds = require('../index'), { cds_tx_protection } = cds.env.features
2
2
  const EventContext = require('../req/context')
3
- class RootContext extends EventContext {}
3
+ class RootContext extends EventContext {
4
+ static for(_) {
5
+ if (_ instanceof EventContext) return _
6
+ else return super.for(_,'as root')
7
+ }
8
+ }
9
+ class NestedContext extends EventContext {
10
+ static for(_) {
11
+ if (_ instanceof EventContext) return _
12
+ else return super.for(_)
13
+ }
14
+ }
15
+
4
16
 
5
17
  /**
6
18
  * This is the implementation of the `srv.tx(req)` method. It constructs
@@ -8,35 +20,28 @@ class RootContext extends EventContext {}
8
20
  * @returns { Transaction & import('./srv-api') }
9
21
  * @param { EventContext } ctx
10
22
  */
11
- module.exports = function tx (ctx,fn) { const srv = this
23
+ function srv_tx (ctx,fn) { const srv = this
12
24
 
13
25
  if (srv.context) return srv // srv.tx().tx() -> idempotent
26
+ if (!ctx) return RootTransaction.for (srv)
27
+
28
+ // Creating root or nested txes for existing contexts
29
+ if (ctx instanceof EventContext) {
30
+ if (ctx.tx) return NestedTransaction.for (srv, ctx)
31
+ else return RootTransaction.for (srv, ctx)
32
+ }
14
33
 
15
34
  // Last arg may be a function -> srv.tx (tx => { ... })
16
35
  if (typeof ctx === 'function') [ ctx, fn ] = [ undefined, ctx ]
17
36
  if (typeof fn === 'function') {
18
- const tx = srv.tx(ctx), fx = ()=> Promise.resolve(fn(tx)).then(tx.commit,tx.rollback)
19
- const gc = cds.context, _has_tx = gc && gc.tx && !gc.tx._done
20
- return _has_tx ? fx() : cds._context.run(tx,fx)
37
+ const tx = RootTransaction.for (srv, ctx)
38
+ return cds._context.run (tx, ()=> Promise.resolve(fn(tx)) .then (tx.commit, tx.rollback))
21
39
  }
22
40
 
23
- if (ctx) {
24
- if (ctx.context) ctx = ctx.context
25
-
26
- // REVISIT: This is for compatibility with former srv.tx(req) usages
27
- if (ctx instanceof EventContext) {
28
- if (ctx.tx) return NestedTransaction.for (srv, ctx)
29
- else return RootTransaction.for (srv, ctx)
30
- }
31
-
32
- // REVISIT: This is for compatibility with AFC only
33
- if (ctx._txed_before) return NestedTransaction.for (srv, ctx._txed_before)
34
- else Object.defineProperty(ctx, '_txed_before', { value: ctx = RootContext.for(ctx) })
35
- return RootTransaction.for (srv, ctx)
36
- }
37
-
38
- // `ctx` is a plain context object or undefined
39
- return RootTransaction.for (srv, RootContext.for(ctx))
41
+ // REVISIT: following is for compatibility with AFC only -> we should get rid of that
42
+ if (ctx._txed_before) return NestedTransaction.for (srv, ctx._txed_before)
43
+ else Object.defineProperty (ctx, '_txed_before', { value: ctx = RootContext.for(ctx) })
44
+ return RootTransaction.for (srv, ctx)
40
45
  }
41
46
 
42
47
 
@@ -45,17 +50,16 @@ class Transaction {
45
50
  /**
46
51
  * Returns an already started tx for given srv, or creates a new instance
47
52
  */
48
- static for (srv,root) {
49
- let txs = root.transactions
50
- if (!txs) Object.defineProperty(root, 'transactions', {value: txs = new Map})
51
- if (srv._real_srv) srv = srv._real_srv // REVISIT: srv._real_srv is a qirty hack for current Okra Adapters
53
+ static for (srv, ctx) {
54
+ const txs = ctx.context.transactions || ctx.context._set ('transactions', new Map)
55
+ if (srv._real_srv) srv = srv._real_srv // REVISIT: srv._real_srv is a dirty hack for current Okra Adapters
52
56
  let tx = txs.get (srv)
53
- if (!tx) txs.set (srv, tx = new this (srv,root))
57
+ if (!tx) txs.set (srv, tx = new this (srv,ctx))
54
58
  return tx
55
59
  }
56
60
 
57
- constructor (srv,root) {
58
- const tx = { __proto__:srv, context:root }
61
+ constructor (srv, ctx) {
62
+ const tx = { __proto__:srv, _kind: new.target.name, context: ctx }
59
63
  const proto = new.target.prototype
60
64
  tx.commit = proto.commit.bind(tx)
61
65
  tx.rollback = proto.rollback.bind(tx)
@@ -107,11 +111,12 @@ class Transaction {
107
111
  class RootTransaction extends Transaction {
108
112
 
109
113
  /**
110
- * Register the new transaction with the root context.
111
- * @param {EventContext} root
114
+ * Register the new transaction with the given context.
115
+ * @param {EventContext} ctx
112
116
  */
113
- static for (srv,root) {
114
- return root.tx = super.for (srv,root)
117
+ static for (srv, ctx) {
118
+ ctx = RootContext.for (ctx?.tx?._done ? {} : ctx)
119
+ return ctx.tx = super.for (srv, ctx)
115
120
  }
116
121
 
117
122
  /**
@@ -153,16 +158,21 @@ class RootTransaction extends Transaction {
153
158
 
154
159
  class NestedTransaction extends Transaction {
155
160
 
161
+ static for (srv,ctx) {
162
+ ctx = NestedContext.for (ctx)
163
+ return super.for (srv, ctx)
164
+ }
165
+
156
166
  /**
157
- * Registers event listeners with the root context, to commit or rollback
167
+ * Registers event listeners with the given context, to commit or rollback
158
168
  * when the root tx is about to commit or rollback.
159
- * @param {import ('../req/context')} root
169
+ * @param {EventContext} ctx
160
170
  */
161
- constructor (srv,root) {
162
- super (srv,root)
163
- root.before ('succeeded', ()=> this.commit())
164
- root.before ('failed', ()=> this.rollback())
165
- if ('end' in srv) root.once ('done', ()=> srv.end())
171
+ constructor (srv,ctx) {
172
+ super (srv,ctx)
173
+ ctx.before ('succeeded', ()=> this.commit())
174
+ ctx.before ('failed', ()=> this.rollback())
175
+ if ('end' in srv) ctx.once ('done', ()=> srv.end())
166
176
  }
167
177
 
168
178
  }
@@ -193,3 +203,5 @@ const _begin = async function (req) {
193
203
  delete this.dispatch
194
204
  return this.dispatch (req)
195
205
  }
206
+
207
+ module.exports = srv_tx
@@ -70,17 +70,17 @@ exports.mkdirp = async function (...path) {
70
70
 
71
71
  exports.rmdir = (...path) => {
72
72
  const d = resolve (cds.root,...path)
73
- return (fs.promises.rm || fs.promises.rmdir) (d, {recursive:true})
73
+ return fs.promises.rm (d, {recursive:true})
74
74
  }
75
75
 
76
76
  exports.rimraf = (...path) => {
77
77
  const d = resolve (cds.root,...path)
78
- return (fs.promises.rm || fs.promises.rmdir) (d, {recursive:true,force:true})
78
+ return fs.promises.rm (d, {recursive:true,force:true})
79
79
  }
80
80
 
81
81
  exports.rm = async function rm (x) {
82
82
  const y = resolve (cds.root,x)
83
- return (fs.promises.rm || fs.promises.unlink)(y)
83
+ return fs.promises.rm(y)
84
84
  }
85
85
 
86
86
  exports.copy = async function copy (x,y) {
@@ -8,22 +8,22 @@ const TEMP_DIR = fs.realpathSync(require('os').tmpdir())
8
8
 
9
9
  const packTarArchive = async (files, root, flat = false) => {
10
10
  if (typeof files === 'string') return await packArchiveCLI(files)
11
-
11
+
12
12
  let tgzBuffer, temp
13
13
  try {
14
- temp = await fs.promises.mkdtemp(`${TEMP_DIR}${path.sep}tar-`)
14
+ temp = await fs.promises.mkdtemp(`${TEMP_DIR}${path.sep}tar-`)
15
15
  for (const file of files) {
16
16
  const fname = flat ? path.basename(file) : path.relative(root, file)
17
17
  const destination = path.join(temp, fname)
18
18
  const dirname = path.dirname(destination)
19
19
  if (!await exists(dirname)) await fs.promises.mkdir(dirname, { recursive: true })
20
- await fs.promises.copyFile(file, destination)
20
+ await fs.promises.copyFile(file, destination)
21
21
  }
22
22
 
23
23
  tgzBuffer = await packArchiveCLI(temp)
24
24
  } finally {
25
25
  if (await exists(temp)) {
26
- await (fs.promises.rm || fs.promises.rmdir)(temp, { recursive: true, force: true })
26
+ await fs.promises.rm(temp, { recursive: true, force: true })
27
27
  }
28
28
  }
29
29
 
@@ -38,7 +38,7 @@ const unpackTarArchive = async (buffer, folder) => {
38
38
  try {
39
39
  await unpackArchiveCLI(tgz, folder)
40
40
  } finally {
41
- if (await exists(temp)) await (fs.promises.rm || fs.promises.rmdir)(temp, { recursive: true, force: true })
41
+ if (await exists(temp)) await fs.promises.rm(temp, { recursive: true, force: true })
42
42
  }
43
43
  }
44
44
 
@@ -27,7 +27,7 @@ const packArchiveCLI = async (root) => {
27
27
  }
28
28
  finally {
29
29
  if (await exists(temp)) {
30
- await (fs.promises.rm || fs.promises.rmdir)(temp, { recursive: true, force: true })
30
+ await fs.promises.rm(temp, { recursive: true, force: true })
31
31
  }
32
32
  }
33
33
  }
@@ -183,7 +183,7 @@ module.exports = (srv, options = srv.options) => {
183
183
  (process.env.NODE_ENV === 'production' && config.credentials && config.restrict_all_services)
184
184
  ) {
185
185
  if (!logged) LOG._debug && LOG.debug(`Enforcing authenticated users for all services`)
186
- app.use(_enforce_authenticated_user)
186
+ app.use(cap_enforce_login)
187
187
  }
188
188
 
189
189
  // so we don't log the same stuff multiple times
@@ -199,7 +199,7 @@ const _strategy4 = config => {
199
199
  throw new Error(`Authentication kind "${config.kind}" is not supported`)
200
200
  }
201
201
 
202
- const _enforce_authenticated_user = (req, res, next) => {
202
+ const cap_enforce_login = (req, res, next) => {
203
203
  if (req.user && req.user.is('authenticated-user')) return next() // pass if user is authenticated
204
204
  if (!req.user || req.user._is_anonymous) {
205
205
  if (req.user && req.user._challenges) res.set('WWW-Authenticate', req.user._challenges.join(';'))
@@ -68,8 +68,6 @@ const MULTI_LINE_STRING_VALIDATION = new RegExp('^' + SRID + 'MultiLineString\\(
68
68
  const MULTI_POLYGON_VALIDATION = new RegExp('^' + SRID + 'MultiPolygon\\((' + MULTI_POLYGON + ')\\)$', 'i')
69
69
  const COLLECTION_VALIDATION = new RegExp('^' + SRID + 'Collection\\((' + MULTI_GEO_LITERAL + ')\\)$', 'i')
70
70
 
71
- const BASE64 = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{4}|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}={2})$/
72
-
73
71
  function _getBase64(val) {
74
72
  if (isInvalidBase64string(val)) return
75
73
  // convert url-safe to standard base64
@@ -40,8 +40,10 @@ class DeserializerFactory {
40
40
  let additionalInformation = { hasDelta: false }
41
41
  const deserializer = new ResourceJsonDeserializer(edm, jsonContentTypeInfo)
42
42
  return (edmObject, value) => {
43
+ const body = deserializer[name](edmObject, value, expand, additionalInformation)
44
+
43
45
  return {
44
- body: deserializer[name](edmObject, value, expand, additionalInformation),
46
+ body,
45
47
  expand,
46
48
  additionalInformation
47
49
  }
@@ -1,8 +1,6 @@
1
1
  const getTemplate = require('./template')
2
2
  const templateProcessor = require('./templateProcessor')
3
3
 
4
- const BASE64 = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{4}|[A-Za-z0-9+/]{3}={0,1}|[A-Za-z0-9+/]{2}={0,2})$/
5
-
6
4
  // convert the standard base64 encoding to the URL-safe variant
7
5
  const toBase64url = value =>
8
6
  (Buffer.isBuffer(value) ? value.toString('base64') : value).replace(/\//g, '_').replace(/\+/g, '-')
@@ -17,12 +15,13 @@ const isInvalidBase64string = value => {
17
15
  if (Buffer.isBuffer(value)) return // ok
18
16
 
19
17
  // convert to standard base64 string; let it crash if typeof value !== 'string'
20
- const base64 = value.replace(/_/g, '/').replace(/-/g, '+')
18
+ const base64value = value.replace(/_/g, '/').replace(/-/g, '+')
21
19
  const normalized = normalizeBase64string(value)
22
20
 
23
21
  // example of invalid base64 string --> 'WTGTdDsD/k21LnFRb+uNcAi=' <-- '...i=' must be '...g='
24
22
  // see https://datatracker.ietf.org/doc/html/rfc4648#section-4
25
- return !base64.match(BASE64) || base64.replace(/=/g, '') !== normalized.replace(/=/g, '')
23
+ if (base64value.replace(/=/g, '') !== normalized.replace(/=/g, '')) return true
24
+ return base64value.length > normalized.length
26
25
  }
27
26
 
28
27
  const _picker = element => {
@@ -612,7 +612,7 @@ const _newQuery = (query, event, model, service) => {
612
612
  }[event]
613
613
  const newQuery = Object.create(query)
614
614
  const transitions = _entityTransitionsForTarget(query[event][_prop], model, service)
615
- newQuery[event] = (transitions[0] && _func(newQuery, transitions, service)) || { ...query[event] }
615
+ newQuery[event] = (transitions?.[0] && _func(newQuery, transitions, service)) || { ...query[event] }
616
616
  return newQuery
617
617
  }
618
618
 
@@ -30,9 +30,13 @@ class RawToExpanded {
30
30
  if (each._conversionMapper) for (const [k, v] of [...each._conversionMapper]) conversionMapper.set(k, v)
31
31
  }
32
32
 
33
- for (let i = 0, length = this._queries.length; i < length; i++) {
33
+ const queryResults = await Promise.all(this._queries)
34
+ // NOTE: this doesn't work:
35
+ // for (let each of this._queries) await each
36
+
37
+ for (let i = 0, length = queryResults.length; i < length; i++) {
34
38
  const { _toManyTree: toManyTree = [] } = queries[i]
35
- const result = await this._queries[i]
39
+ const result = queryResults[i]
36
40
  if (toManyTree.length === 0) {
37
41
  this._parseMainResult(result, mappings, conversionMapper, toManyTree)
38
42
  } else {
@@ -249,10 +253,7 @@ class RawToExpanded {
249
253
  * @returns {Promise<Array>} The complete expanded result set.
250
254
  */
251
255
  const rawToExpanded = (configs, queries, one, rootEntity) => {
252
- return new RawToExpanded(configs, queries, one, rootEntity).toExpanded().catch(err => {
253
- Promise.all(queries).catch(() => {})
254
- throw err
255
- })
256
+ return new RawToExpanded(configs, queries, one, rootEntity).toExpanded()
256
257
  }
257
258
 
258
259
  module.exports = rawToExpanded
@@ -176,7 +176,10 @@ const _pickDraft = element => {
176
176
  // collect actions to apply
177
177
  const categories = []
178
178
 
179
- if (element.virtual) categories.push('virtual')
179
+ if (_isVirtualOrCalculated(element)) {
180
+ categories.push('virtual')
181
+ return { categories } // > no need to continue
182
+ }
180
183
 
181
184
  if (element.default && !DRAFT_COLUMNS_MAP[element.name]) {
182
185
  categories.push({ category: 'default', args: element })
@@ -1,4 +1,5 @@
1
1
  const cds = require('../cds')
2
+ const LOG = cds.log('mtx')
2
3
 
3
4
  const { validateExtension } = require('./validation')
4
5
  const handleDefaults = require('./defaults')
@@ -16,6 +17,7 @@ const add = async function (req) {
16
17
  const extCsn = _isCSN(extension) ? JSON.parse(extension) : cds.parse.cdl(extension)
17
18
  if (extCsn.requires) delete extCsn.requires
18
19
 
20
+ LOG.info(`validating extension '${tag}' ...`)
19
21
  const { 'cds.xt.ModelProviderService': mps } = cds.services
20
22
  const csn = await mps.getCsn(tenant, ['*'])
21
23
  validateExtension(extCsn, csn, req)
@@ -26,6 +28,7 @@ const add = async function (req) {
26
28
  INSERT.into('cds.xt.Extensions').entries([{ ID, tag, csn: JSON.stringify(extCsn), activated: activate }])
27
29
  )
28
30
  const njCsn = cds.compile.for.nodejs(csn)
31
+ LOG.info(`activating extension to '${activate}' ...`)
29
32
  if (activate === 'propertyBag' && extCsn.extensions)
30
33
  extCsn.extensions.forEach(async ext => await handleDefaults(ext, njCsn))
31
34
  if (activate === 'database') await activateExt(ID, tag, tenant, njCsn)
@@ -42,23 +42,25 @@ const _removeExtendedFields = (columns, extFields, alias) => {
42
42
 
43
43
  const _transformUnion = (req, model) => {
44
44
  // second element is active entity
45
- const name = req.target.name.SET ? req.target.name.SET.args[1]._target.name : req.target.name
46
- const extFields = getExtendedFields(name, model)
47
- _addBackPack(req.query.SELECT.columns, extFields)
48
- _removeExtendedFields(req.query.SELECT.columns, extFields)
49
-
50
- _addBackPack(
51
- req.query.SELECT.from.SET.args[0].SELECT.columns,
52
- extFields,
53
- req.query.SELECT.from.SET.args[0].SELECT.from.args[0].as
54
- )
55
- _addBackPack(req.query.SELECT.from.SET.args[1].SELECT.columns, extFields)
56
- _removeExtendedFields(
57
- req.query.SELECT.from.SET.args[0].SELECT.columns,
58
- extFields,
59
- req.query.SELECT.from.SET.args[0].SELECT.from.args[0].as
60
- )
61
- _removeExtendedFields(req.query.SELECT.from.SET.args[1].SELECT.columns, extFields)
45
+ if (req.target) {
46
+ const name = req.target.name.SET ? req.target.name.SET.args[1]._target.name : req.target.name
47
+ const extFields = getExtendedFields(name, model)
48
+ _addBackPack(req.query.SELECT.columns, extFields)
49
+ _removeExtendedFields(req.query.SELECT.columns, extFields)
50
+
51
+ _addBackPack(
52
+ req.query.SELECT.from.SET.args[0].SELECT.columns,
53
+ extFields,
54
+ req.query.SELECT.from.SET.args[0].SELECT.from.args[0].as
55
+ )
56
+ _addBackPack(req.query.SELECT.from.SET.args[1].SELECT.columns, extFields)
57
+ _removeExtendedFields(
58
+ req.query.SELECT.from.SET.args[0].SELECT.columns,
59
+ extFields,
60
+ req.query.SELECT.from.SET.args[0].SELECT.from.args[0].as
61
+ )
62
+ _removeExtendedFields(req.query.SELECT.from.SET.args[1].SELECT.columns, extFields)
63
+ }
62
64
  }
63
65
 
64
66
  const _getAliasedEntitiesForJoin = (args, model) => {
@@ -111,7 +113,7 @@ function transformExtendedFieldsREAD(req) {
111
113
  _transformColumns(req.query.SELECT.columns, target.name, this.model)
112
114
 
113
115
  if (req.query.SELECT.from.SET) return _transformUnion(req, this.model) // union
114
- if (req.query.SELECT.from.join) return _transformJoin(req, this.model) // join
116
+ if (req.query.SELECT.from.join && req.query.SELECT.from.args) return _transformJoin(req, this.model) // join
115
117
  }
116
118
 
117
119
  module.exports = {
@@ -22,7 +22,7 @@ const _compileProject = async function (extension, req) {
22
22
  if (err.messages) req.reject(400, getCompilerError(err.messages))
23
23
  else throw err
24
24
  } finally {
25
- ;(fs.promises.rm || fs.promises.rmdir)(root, { recursive: true, force: true }).catch(() => {})
25
+ fs.promises.rm(root, { recursive: true, force: true }).catch(() => {})
26
26
  }
27
27
 
28
28
  return { csn, files }
@@ -68,7 +68,7 @@ const pull = async function (req) {
68
68
  // for (const file of i18nFiles) await _copyFile(file, temp)
69
69
  tgz = await packTarArchive(temp)
70
70
  } finally {
71
- ;(fs.promises.rm || fs.promises.rmdir)(temp, { recursive: true, force: true }).catch(() => {})
71
+ fs.promises.rm(temp, { recursive: true, force: true }).catch(() => {})
72
72
  }
73
73
 
74
74
  return tgz
@@ -85,31 +85,31 @@ const push = async function (req) {
85
85
  if (tenant) cds.context = { tenant }
86
86
 
87
87
  // remove current extension with tag
88
- let currentExt
89
88
  if (tag) {
90
- currentExt = await cds.db.run(SELECT.from('cds.xt.Extensions').where({ tag }))
91
- if (currentExt.length) await cds.db.run(DELETE.from('cds.xt.Extensions').where({ tag }))
89
+ await DELETE.from('cds.xt.Extensions').where({ tag })
92
90
  }
93
91
 
94
92
  LOG.info(`validating extension '${tag}' ...`)
95
93
  // validation
96
94
  const { 'cds.xt.ModelProviderService': mps } = cds.services
95
+ // REVISIT: Isn't that also done during activate?
97
96
  const csn = await mps.getCsn(tenant, Object.keys(cds.context.features || {}))
98
97
  try {
99
98
  cds.extend(csn).with(extCsn)
100
99
  } catch (err) {
101
- if (currentExt && currentExt.length) {
102
- await cds.db.run(INSERT.into('cds.xt.Extensions').entries(currentExt)) // REVISIT: why did we eagerly delete that at all above?
103
- }
104
100
  return req.reject(400, getCompilerError(err.messages))
105
101
  }
106
102
  await linter(extCsn, csn, files, req)
107
103
 
108
104
  // insert and activate extension
109
105
  const ID = cds.utils.uuid()
110
- await cds.db.run(
111
- INSERT.into('cds.xt.Extensions').entries([{ ID, csn: JSON.stringify(extCsn), sources, activated: 'database', tag }])
112
- )
106
+ await INSERT.into('cds.xt.Extensions').entries({
107
+ ID,
108
+ csn: JSON.stringify(extCsn),
109
+ sources,
110
+ activated: 'database',
111
+ tag
112
+ })
113
113
 
114
114
  LOG.info(`activating extension '${tag}' ...`)
115
115
  await activate(ID, null, tenant)
@@ -8,7 +8,7 @@ const EXT_BACK_PACK = 'extensions__'
8
8
 
9
9
  const getTargetRead = req => {
10
10
  let name = ''
11
- if (req.query.SELECT.from.join) {
11
+ if (req.query.SELECT.from.join && req.query.SELECT.from.args) {
12
12
  // join
13
13
  name = req.query.SELECT.from.args.find(arg => arg.ref && arg.ref[0] !== 'DRAFT.DraftAdministativeData').ref[0]
14
14
  } else if (req.target.name.SET) {
@@ -63,15 +63,17 @@ const hasExtendedEntity = (req, model) => {
63
63
  return true
64
64
  }
65
65
 
66
- if (req.query.SELECT.from.join) {
66
+ if (req.query.SELECT.from.join && req.query.SELECT.from.args) {
67
67
  return _hasExtendedEntityArgs(req.query.SELECT.from.args, model)
68
68
  }
69
69
 
70
- if (req.target.name.SET) {
71
- return isExtendedEntity(req.target.name.SET.args[0]._target.name, model)
72
- }
70
+ if (req.target) {
71
+ if (req.target.name.SET) {
72
+ return isExtendedEntity(req.target.name.SET.args[0]._target.name, model)
73
+ }
73
74
 
74
- return isExtendedEntity(req.target.name, model)
75
+ return isExtendedEntity(req.target.name, model)
76
+ }
75
77
  }
76
78
 
77
79
  const getExtendedFields = (entityName, model) => {
@@ -187,11 +187,13 @@ module.exports = class SQLiteDatabase extends DatabaseService {
187
187
  return
188
188
  }
189
189
 
190
- const tx = this.transaction()
191
- await tx.run(dropViews)
192
- await tx.run(dropTables)
193
- await tx.run(createEntities)
194
- await tx.commit()
190
+ await this.run(async tx => {
191
+ // This starts a new transaction if called from CLI, while joining
192
+ // existing root tx, e.g. when called from DeploymenrService
193
+ await tx.run(dropViews)
194
+ await tx.run(dropTables)
195
+ await tx.run(createEntities)
196
+ })
195
197
 
196
198
  return true
197
199
  }
@@ -19,31 +19,47 @@ const SANITIZE_VALUES = process.env.NODE_ENV === 'production' && cds.env.log.san
19
19
  * -> only if DEBUG (which should not be used in production)
20
20
  */
21
21
  const DEBUG = cds.debug('sqlite')
22
- const _captureStack = DEBUG
23
- ? () => {
24
- const o = {}
25
- Error.captureStackTrace(o, _captureStack)
26
- return o
22
+ const _exec = DEBUG
23
+ ? (dbc, op, ...args) => {
24
+ const callback = args[args.length - 1]
25
+ const captured = {}
26
+ Error.captureStackTrace(captured, _exec)
27
+ args[args.length - 1] = function (err) {
28
+ if (err) {
29
+ err.message += ' in: \n' + args[0]
30
+ err.query = args[0]
31
+ if (args.length > 2) err.values = SANITIZE_VALUES ? ['***'] : args[1]
32
+ err.stack =
33
+ err.message +
34
+ captured.stack
35
+ .slice(5)
36
+ .replace(/at( _exec)? /, 'at SQLite.' + op + ' ')
37
+ .replace(/\s+at new Promise .*\n.*/, '')
38
+ }
39
+ callback.apply(this, arguments)
40
+ }
41
+ return dbc[op](...args)
42
+ }
43
+ : (dbc, op, ...args) => {
44
+ const callback = args[args.length - 1]
45
+ args[args.length - 1] = function (err) {
46
+ if (err) {
47
+ err.message += ' in: \n' + args[0]
48
+ err.query = args[0]
49
+ if (args.length > 2) err.values = SANITIZE_VALUES ? ['***'] : args[1]
50
+ }
51
+ callback.apply(this, arguments)
52
+ }
53
+ return dbc[op](...args)
27
54
  }
28
- : () => undefined
29
-
30
- const _augmented = (err, sql, values, o) => {
31
- err.query = sql
32
- if (values) err.values = SANITIZE_VALUES ? ['***'] : values
33
- err.message += ' in: \n' + sql
34
- if (o) err.stack = err.message + o.stack.slice(5)
35
- return err
36
- }
37
55
 
38
56
  function _executeSimpleSQL(dbc, sql, values) {
39
57
  LOG._debug &&
40
58
  LOG.debug(coloredTxCommands[sql] || sql, Array.isArray(values) ? (SANITIZE_VALUES ? ['***'] : values) : '')
41
59
 
42
60
  return new Promise((resolve, reject) => {
43
- const o = _captureStack()
44
- dbc.run(sql, values, function (err) {
45
- if (err) return reject(_augmented(err, sql, values, o))
46
-
61
+ _exec(dbc, 'run', sql, values, function (err) {
62
+ if (err) return reject(err)
47
63
  resolve(this.changes)
48
64
  })
49
65
  })
@@ -53,9 +69,8 @@ function executeSelectSQL(dbc, sql, values, isOne, postMapper) {
53
69
  LOG._debug && LOG.debug(sql, SANITIZE_VALUES ? ['***'] : values)
54
70
 
55
71
  return new Promise((resolve, reject) => {
56
- const o = _captureStack()
57
- dbc[isOne ? 'get' : 'all'](sql, values, (err, result) => {
58
- if (err) return reject(_augmented(err, sql, values, o))
72
+ _exec(dbc, isOne ? 'get' : 'all', sql, values, (err, result) => {
73
+ if (err) return reject(err)
59
74
 
60
75
  // REVISIT
61
76
  // .get returns undefined if nothing in db
@@ -140,9 +155,8 @@ const _executeBulkInsertSQL = (dbc, sql, values) =>
140
155
  }
141
156
 
142
157
  LOG._debug && LOG.debug(sql, SANITIZE_VALUES ? ['***'] : values)
143
- const o = _captureStack()
144
- const stmt = dbc.prepare(sql, err => {
145
- if (err) return reject(_augmented(err, sql, values, o))
158
+ const stmt = _exec(dbc, 'prepare', sql, err => {
159
+ if (err) return reject(err)
146
160
 
147
161
  if (!Array.isArray(values[0])) values = [values]
148
162
 
@@ -159,7 +173,7 @@ const _executeBulkInsertSQL = (dbc, sql, values) =>
159
173
  if (!isFinalized) {
160
174
  isFinalized = true
161
175
  stmt.finalize()
162
- return reject(_augmented(err, sql, each, o))
176
+ return reject(err)
163
177
  }
164
178
  }
165
179
 
@@ -212,9 +226,8 @@ function executeInsertSQL(dbc, sql, values, query) {
212
226
  LOG._debug && LOG.debug(sql, SANITIZE_VALUES ? ['***'] : values)
213
227
 
214
228
  return new Promise((resolve, reject) => {
215
- const o = _captureStack()
216
- dbc.run(sql, values, function (err) {
217
- if (err) return reject(_augmented(err, sql, values, o))
229
+ _exec(dbc, 'run', sql, values, function (err) {
230
+ if (err) return reject(err)
218
231
 
219
232
  // InsertResult needs an object per row with its values
220
233
  if (query && values.length > 0) {
@@ -117,8 +117,7 @@ const RestAdapter = function(srv) {
117
117
  // begin tx
118
118
  router.use((req, res, next) => { // REVISIT: -> move to actual handler(s)
119
119
  // create tx and set as cds.context
120
- // REVISIT: req._tx should not be used like that!
121
- req.tx = cds.context = srv.tx({ user: req.user, req, res })
120
+ cds.context = srv.tx(new cds.EventContext({ user: req.user, req, res }))
122
121
  next()
123
122
  })
124
123
 
@@ -148,8 +147,7 @@ const RestAdapter = function(srv) {
148
147
 
149
148
  // unfortunately, express doesn't catch async errors -> try catch needed
150
149
  try {
151
- // REVISIT: req._tx should not be used like that!
152
- await req.tx.commit(result)
150
+ await cds.context?.tx?.commit(result)
153
151
  } catch (e) {
154
152
  return next(e)
155
153
  }
@@ -179,8 +177,7 @@ const RestAdapter = function(srv) {
179
177
  // request may fail during processing or during commit -> both caught here
180
178
 
181
179
  // REVISIT: rollback needed if error occured before commit attempted -> how to distinguish?
182
- // REVISIT: req._tx should not be used like that!
183
- if (req.tx) req.tx.rollback(err).catch(() => {}) // REVISIT: silently ?!?
180
+ cds.context?.tx?.rollback(err).catch(() => {}) // REVISIT: silently ?!?
184
181
 
185
182
  next(err)
186
183
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sap/cds",
3
- "version": "6.1.0",
3
+ "version": "6.1.1",
4
4
  "description": "SAP Cloud Application Programming Model - CDS for Node.js",
5
5
  "homepage": "https://cap.cloud.sap/",
6
6
  "keywords": [