@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.
- package/CHANGELOG.md +31 -0
- package/apis/log.d.ts +3 -5
- package/bin/build/provider/buildTaskHandlerFeatureToggles.js +2 -3
- package/bin/build/provider/hana/index.js +4 -2
- package/bin/build/provider/mtx/resourcesTarBuilder.js +4 -8
- package/bin/deploy/to-hana/hana.js +20 -25
- package/bin/deploy/to-hana/hdiDeployUtil.js +13 -2
- package/lib/dbs/cds-deploy.js +2 -2
- package/lib/env/schemas/cds-rc.json +10 -1
- package/lib/ql/Query.js +9 -3
- package/lib/ql/SELECT.js +1 -1
- package/lib/ql/cds-ql.js +0 -9
- package/lib/req/context.js +15 -11
- package/lib/srv/srv-api.js +8 -0
- package/lib/srv/srv-dispatch.js +11 -7
- package/lib/srv/srv-models.js +4 -3
- package/lib/srv/srv-tx.js +52 -40
- package/lib/utils/cds-utils.js +3 -3
- package/lib/utils/resources/index.js +5 -5
- package/lib/utils/resources/tar.js +1 -1
- package/libx/_runtime/auth/index.js +2 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/utils/PrimitiveValueDecoder.js +0 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/deserializer/DeserializerFactory.js +3 -1
- package/libx/_runtime/common/utils/binary.js +3 -4
- package/libx/_runtime/common/utils/resolveView.js +1 -1
- package/libx/_runtime/db/expand/rawToExpanded.js +7 -6
- package/libx/_runtime/db/generic/input.js +4 -1
- package/libx/_runtime/extensibility/add.js +3 -0
- package/libx/_runtime/extensibility/handler/transformREAD.js +20 -18
- package/libx/_runtime/extensibility/push.js +11 -11
- package/libx/_runtime/extensibility/utils.js +8 -6
- package/libx/_runtime/sqlite/Service.js +7 -5
- package/libx/_runtime/sqlite/execute.js +41 -28
- package/libx/rest/RestAdapter.js +3 -6
- 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
|
|
45
|
-
SILENT
|
|
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(
|
|
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
|
-
|
|
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
|
|
56
|
-
const
|
|
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 ===
|
|
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
|
|
52
|
+
let vcapFileEnv;
|
|
53
53
|
if (vcapFile) {
|
|
54
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
207
|
+
async _loadDefaultEnv(defaultEnvFile) {
|
|
208
208
|
try {
|
|
209
|
-
const content =
|
|
210
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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,
|
|
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
|
-
|
|
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.`);
|
package/lib/dbs/cds-deploy.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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":
|
|
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
|
-
|
|
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
|
|
20
|
-
|
|
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) {
|
package/lib/req/context.js
CHANGED
|
@@ -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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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 ||
|
|
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
|
-
|
|
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
|
|
160
|
-
if (
|
|
161
|
-
if (!this.hasOwnProperty('context')) this.context =
|
|
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 =
|
|
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
|
}
|
package/lib/srv/srv-api.js
CHANGED
|
@@ -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
|
}
|
package/lib/srv/srv-dispatch.js
CHANGED
|
@@ -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
|
|
17
|
-
if (
|
|
18
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
package/lib/srv/srv-models.js
CHANGED
|
@@ -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.
|
|
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
|
|
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
|
-
|
|
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 =
|
|
19
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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,
|
|
49
|
-
|
|
50
|
-
if (
|
|
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,
|
|
57
|
+
if (!tx) txs.set (srv, tx = new this (srv,ctx))
|
|
54
58
|
return tx
|
|
55
59
|
}
|
|
56
60
|
|
|
57
|
-
constructor (srv,
|
|
58
|
-
const tx = { __proto__:srv, context:
|
|
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
|
|
111
|
-
* @param {EventContext}
|
|
114
|
+
* Register the new transaction with the given context.
|
|
115
|
+
* @param {EventContext} ctx
|
|
112
116
|
*/
|
|
113
|
-
static for (srv,
|
|
114
|
-
|
|
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
|
|
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 {
|
|
169
|
+
* @param {EventContext} ctx
|
|
160
170
|
*/
|
|
161
|
-
constructor (srv,
|
|
162
|
-
super (srv,
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
if ('end' in srv)
|
|
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
|
package/lib/utils/cds-utils.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
41
|
+
if (await exists(temp)) await fs.promises.rm(temp, { recursive: true, force: true })
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
|
|
@@ -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(
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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()
|
|
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
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
111
|
-
|
|
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
|
|
71
|
-
|
|
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
|
-
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
|
23
|
-
? () => {
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
44
|
-
|
|
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
|
-
|
|
57
|
-
|
|
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
|
|
144
|
-
|
|
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(
|
|
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
|
-
|
|
216
|
-
|
|
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) {
|
package/libx/rest/RestAdapter.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
})
|