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