@sap/cds 7.1.2 → 7.2.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 +68 -4
- package/apis/cds.d.ts +10 -6
- package/apis/connect.d.ts +1 -2
- package/apis/core.d.ts +54 -5
- package/apis/log.d.ts +19 -6
- package/apis/models.d.ts +0 -18
- package/apis/ql.d.ts +23 -23
- package/apis/serve.d.ts +18 -15
- package/apis/services.d.ts +67 -56
- package/apis/test.d.ts +1 -2
- package/bin/serve.js +4 -4
- package/common.cds +4 -4
- package/lib/auth/basic-auth.js +1 -1
- package/lib/auth/dummy-auth.js +2 -1
- package/lib/auth/ias-auth.js +68 -2
- package/lib/auth/index.js +5 -5
- package/lib/auth/jwt-auth.js +40 -24
- package/lib/auth/mocked-users.js +0 -13
- package/lib/auth/passport-basic.js +2 -0
- package/lib/auth/passport-digest.js +2 -0
- package/lib/compile/etc/_localized.js +0 -1
- package/lib/compile/extend.js +16 -0
- package/lib/compile/for/lean_drafts.js +38 -6
- package/lib/compile/resolve.js +7 -5
- package/lib/compile/to/json.js +6 -2
- package/lib/dbs/cds-deploy.js +3 -3
- package/lib/env/cds-env.js +3 -3
- package/lib/env/cds-requires.js +1 -0
- package/lib/env/defaults.js +8 -1
- package/lib/env/schemas/cds-rc.json +27 -3
- package/lib/i18n/localize.js +3 -3
- package/lib/index.js +4 -0
- package/lib/log/cds-log.js +10 -1
- package/lib/ql/Whereable.js +7 -3
- package/lib/req/user.js +18 -16
- package/lib/srv/middlewares/sap-statistics.js +3 -3
- package/lib/srv/middlewares/trace.js +5 -4
- package/lib/srv/srv-dispatch.js +10 -9
- package/lib/utils/axios.js +3 -0
- package/lib/utils/cds-test.js +3 -0
- package/lib/utils/cds-utils.js +2 -0
- package/libx/_runtime/auth/index.js +8 -32
- package/libx/_runtime/auth/strategies/ias-auth.js +1 -77
- package/libx/_runtime/auth/strategies/mock.js +1 -12
- package/libx/_runtime/auth/strategies/xssecUtils.js +2 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +11 -9
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/ExpressionToCQN.js +5 -0
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/applyToCQN.js +5 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/UriParser.js +4 -0
- package/libx/_runtime/common/composition/data.js +5 -3
- package/libx/_runtime/common/composition/insert.js +6 -3
- package/libx/_runtime/common/composition/update.js +12 -8
- package/libx/_runtime/common/error/constants.js +6 -1
- package/libx/_runtime/common/generic/auth/requires.js +11 -3
- package/libx/_runtime/common/generic/auth/restrict.js +21 -15
- package/libx/_runtime/common/generic/auth/restrictions.js +5 -2
- package/libx/_runtime/common/generic/crud.js +6 -0
- package/libx/_runtime/common/generic/paging.js +3 -1
- package/libx/_runtime/common/i18n/messages.properties +1 -0
- package/libx/_runtime/common/utils/cqn2cqn4sql.js +3 -5
- package/libx/_runtime/common/utils/resolveView.js +3 -1
- package/libx/_runtime/common/utils/restrictions.js +47 -0
- package/libx/_runtime/db/data-conversion/post-processing.js +3 -3
- package/libx/_runtime/db/generic/input.js +1 -1
- package/libx/_runtime/db/sql-builder/ExpressionBuilder.js +0 -17
- package/libx/_runtime/fiori/lean-draft.js +27 -24
- package/libx/_runtime/hana/driver.js +2 -4
- package/libx/_runtime/hana/pool.js +1 -1
- package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +2 -2
- package/libx/_runtime/messaging/outbox/utils.js +1 -2
- package/libx/_runtime/remote/Service.js +10 -9
- package/libx/_runtime/remote/utils/client.js +4 -3
- package/libx/_runtime/sqlite/Service.js +0 -4
- package/libx/_runtime/sqlite/customBuilder/CustomFunctionBuilder.js +2 -1
- package/libx/odata/afterburner.js +5 -3
- package/libx/odata/cqn2odata.js +7 -7
- package/libx/odata/utils.js +4 -1
- package/libx/rest/RestAdapter.js +15 -16
- package/package.json +1 -1
- package/lib/auth/xsuaa-auth.js +0 -2
- package/libx/_runtime/auth/utils.js +0 -32
- package/libx/audit-log/client.cds +0 -0
- package/libx/audit-log/client.js +0 -0
package/lib/auth/mocked-users.js
CHANGED
|
@@ -10,19 +10,6 @@ class MockedUsers {
|
|
|
10
10
|
if (typeof v === 'boolean') continue
|
|
11
11
|
if (typeof v === 'string') v = { password:v }
|
|
12
12
|
let id = _configured(v).id || k
|
|
13
|
-
|
|
14
|
-
// Only for mock users the pseudo roles are kept in the role list.
|
|
15
|
-
// In all other cases pseudo roles are filtered out.
|
|
16
|
-
if (v.roles) {
|
|
17
|
-
if (Array.isArray(v.roles)) {
|
|
18
|
-
if (v.roles.includes('system-user')) v._is_system = true
|
|
19
|
-
if (v.roles.includes('internal-user')) v._is_internal = true
|
|
20
|
-
} else {
|
|
21
|
-
if ('system-user' in v.roles) v._is_system = true
|
|
22
|
-
if ('internal-user' in v.roles) v._is_internal = true
|
|
23
|
-
}
|
|
24
|
-
} else v.roles = []
|
|
25
|
-
|
|
26
13
|
let u = users[id] = new User ({ id, ...v })
|
|
27
14
|
let fts = tenants[u.tenant]?.features
|
|
28
15
|
if (fts && !u.features) u.features = fts
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// REVISIT: either document passport basic auth or remove it
|
|
2
|
+
|
|
1
3
|
/* eslint-disable cds/no-missing-dependencies */
|
|
2
4
|
module.exports = function passport_basic_auth (options) {
|
|
3
5
|
// const session = require('express-session')({ secret:'secret', resave:false, saveUninitialized:true, })
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// REVISIT: either document passport digest auth or remove it
|
|
2
|
+
|
|
1
3
|
/* eslint-disable cds/no-missing-dependencies */
|
|
2
4
|
module.exports = function passport_digest_auth (options) {
|
|
3
5
|
// const session = require('express-session')({ secret:'secret', resave:false, saveUninitialized:true, })
|
package/lib/compile/extend.js
CHANGED
|
@@ -3,15 +3,31 @@ const { extend } = require ('../lazy')
|
|
|
3
3
|
|
|
4
4
|
module.exports = o => o.definitions ? { with(...csns) {
|
|
5
5
|
|
|
6
|
+
// merge all extension csns
|
|
6
7
|
const csn=o, merged = { definitions: {}, extensions: [] }
|
|
7
8
|
for (const { definitions, extensions } of csns) {
|
|
8
9
|
if (definitions) Object.assign(merged.definitions, definitions)
|
|
9
10
|
if (extensions) merged.extensions.push(...extensions)
|
|
10
11
|
}
|
|
12
|
+
|
|
13
|
+
// extend given base csn with merged extensions
|
|
11
14
|
const extended = compile({
|
|
12
15
|
'base.csn': compile.to.json(csn),
|
|
13
16
|
'ext.csn': compile.to.json(merged)
|
|
14
17
|
})
|
|
18
|
+
|
|
19
|
+
// handle localized extension elements
|
|
20
|
+
for (let ext of merged.extensions) {
|
|
21
|
+
for (let name in ext.elements) {
|
|
22
|
+
const e = ext.elements[name]
|
|
23
|
+
if (e.localized) {
|
|
24
|
+
// add localized element also to respective .texts entity
|
|
25
|
+
const texts = extended.definitions[ext.extend+'.texts']
|
|
26
|
+
texts.elements[name] ??= { ...e, localized:null }
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
15
31
|
extended.$sources = csn.$sources // required to load resources like i18n later on
|
|
16
32
|
return extended
|
|
17
33
|
|
|
@@ -72,17 +72,49 @@ module.exports = function cds_compile_for_lean_drafts(csn) {
|
|
|
72
72
|
// Positive list would be bigger (search, requires, fiori, ...)
|
|
73
73
|
if (draft['@readonly']) draft['@readonly'] = undefined
|
|
74
74
|
if (draft['@insertonly']) draft['@insertonly'] = undefined
|
|
75
|
-
if (draft['@restrict'])
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
75
|
+
if (draft['@restrict']) {
|
|
76
|
+
const restrictions = ['CREATE', 'WRITE', '*']
|
|
77
|
+
draft['@restrict'] = draft['@restrict']
|
|
78
|
+
.map(d => ({
|
|
79
|
+
...d,
|
|
80
|
+
grant:
|
|
81
|
+
d.grant && Array.isArray(d.grant)
|
|
82
|
+
? d.grant.filter(g => restrictions.includes(g))
|
|
83
|
+
: typeof d.grant === 'string' && restrictions.includes(d.grant)
|
|
84
|
+
? [d.grant]
|
|
85
|
+
: []
|
|
86
|
+
}))
|
|
87
|
+
.filter(r => r.grant.length > 0)
|
|
88
|
+
if (draft['@restrict'].length > 0) {
|
|
89
|
+
// Change WRITE & CREATE to NEW
|
|
90
|
+
draft['@restrict'] = draft['@restrict'].map(d => {
|
|
91
|
+
if (d.grant.includes('WRITE') || d.grant.includes('CREATE')) {
|
|
92
|
+
return { ...d, grant: 'NEW' }
|
|
93
|
+
}
|
|
94
|
+
return d
|
|
95
|
+
})
|
|
96
|
+
} else {
|
|
97
|
+
draft['@restrict'] = undefined
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if ('@Capabilities.DeleteRestrictions.Deletable' in draft)
|
|
101
|
+
draft['@Capabilities.DeleteRestrictions.Deletable'] = undefined
|
|
102
|
+
if ('@Capabilities.InsertRestrictions.Insertable' in draft)
|
|
103
|
+
draft['@Capabilities.InsertRestrictions.Insertable'] = undefined
|
|
104
|
+
if ('@Capabilities.UpdateRestrictions.Updatable' in draft)
|
|
105
|
+
draft['@Capabilities.UpdateRestrictions.Updatable'] = undefined
|
|
106
|
+
if ('@Capabilities.NavigationRestrictions.RestrictedProperties' in draft)
|
|
107
|
+
draft['@Capabilities.NavigationRestrictions.RestrictedProperties'] = undefined
|
|
80
108
|
|
|
81
109
|
// Recursively add drafts for compositions
|
|
82
110
|
for (const each in draft.elements) {
|
|
83
111
|
const e = draft.elements[each]
|
|
84
112
|
const newEl = Object.create(e)
|
|
85
|
-
if (
|
|
113
|
+
if (
|
|
114
|
+
e.isComposition ||
|
|
115
|
+
(e.isAssociation && e['@odata.draft.enclosed']) ||
|
|
116
|
+
((!active['@Common.DraftRoot.ActivationAction'] || e._target === active) && _isCompositionBacklink(e) && _isDraft(e._target))
|
|
117
|
+
) {
|
|
86
118
|
if (e._target['@odata.draft.enabled'] === false) continue // happens for texts if @fiori.draft.enabled is not set
|
|
87
119
|
_redirect(newEl, addDraftEntity(e._target, model))
|
|
88
120
|
}
|
package/lib/compile/resolve.js
CHANGED
|
@@ -14,7 +14,6 @@ const suffixes = [ '.csn', '.cds', sep+'index.csn', sep+'index.cds', sep+'csn.js
|
|
|
14
14
|
* @returns and array of absolute filenames
|
|
15
15
|
*/
|
|
16
16
|
module.exports = exports = function cds_resolve (model, o={}) { // NOSONAR
|
|
17
|
-
|
|
18
17
|
if (!model || model === '--') return
|
|
19
18
|
if (model._resolved) return model
|
|
20
19
|
if (model === '*') return _resolve_all(o,this)
|
|
@@ -25,7 +24,7 @@ module.exports = exports = function cds_resolve (model, o={}) { // NOSONAR
|
|
|
25
24
|
if (model.endsWith('/*')) return _resolve_subdirs_in(model,o,this)
|
|
26
25
|
|
|
27
26
|
const cwd = o.root || this.root, local = resolve (cwd,model)
|
|
28
|
-
const context = _paths(cwd,o), {cached} = context
|
|
27
|
+
const context = _paths(cwd,o,this), {cached} = context
|
|
29
28
|
let id = model.startsWith('.') ? local : model
|
|
30
29
|
if (id in cached) return cached[id]
|
|
31
30
|
|
|
@@ -93,11 +92,14 @@ function _resolve_subdirs_in (pattern='fts/*',o,cds) {
|
|
|
93
92
|
}
|
|
94
93
|
}
|
|
95
94
|
|
|
96
|
-
function _paths (dir,o) {
|
|
95
|
+
function _paths (dir,o,cds) {
|
|
97
96
|
const cache = o.cache || exports.cache
|
|
98
97
|
const cached = cache[dir]; if (cached) return cached
|
|
99
|
-
const a = dir.split(sep), n = a.length,
|
|
100
|
-
const
|
|
98
|
+
const a = dir.split(sep), n = a.length, paths = [ dir ]
|
|
99
|
+
const { cdsc: { moduleLookupDirectories }} = o.env ?? cds.env
|
|
100
|
+
for (const mld of moduleLookupDirectories) { // node_modules/ usually, more for Java
|
|
101
|
+
paths.push(...a.map ((_,i,a)=> a.slice(0,n-i).join(sep)+sep+mld))
|
|
102
|
+
}
|
|
101
103
|
return cache[dir] = { paths, cached:{} }
|
|
102
104
|
}
|
|
103
105
|
|
package/lib/compile/to/json.js
CHANGED
|
@@ -4,6 +4,7 @@ const path = require('path')
|
|
|
4
4
|
module.exports = (csn,o={}) => {
|
|
5
5
|
const relative = filename => (o.src !== o.cwd) ? path.relative(o.src, path.join(o.cwd, filename)) : filename
|
|
6
6
|
const relative_cds_home = RegExp ('^' + path.relative (o.src || o.cwd || cds.root, cds.home) + '/')
|
|
7
|
+
const { moduleLookupDirectories } = cds.env.cdsc
|
|
7
8
|
|
|
8
9
|
const resolver = (_,v) => {
|
|
9
10
|
|
|
@@ -20,9 +21,12 @@ module.exports = (csn,o={}) => {
|
|
|
20
21
|
// Preserve original sources for services so we can use them for finding
|
|
21
22
|
// sibling implementation files when reloaded from csn.json.
|
|
22
23
|
let file = relative(v.$location.file)
|
|
23
|
-
.replace(relative_cds_home,'@sap/cds/')
|
|
24
|
-
.replace('node_modules/','')
|
|
25
24
|
.replace(/\\/g,'/')
|
|
25
|
+
.replace(relative_cds_home,'@sap/cds/')
|
|
26
|
+
for (const mld of moduleLookupDirectories) { // node_modules/ usually, more for Java
|
|
27
|
+
file = file.replace(mld, '')
|
|
28
|
+
}
|
|
29
|
+
|
|
26
30
|
// If there is still a relative path pointing outside of cwd, convert it to a module path
|
|
27
31
|
// e.g. ../bookshop/srv/cat-service.cds -> @capire/bookshop/srv/cat-service.cds
|
|
28
32
|
if (file.startsWith('../')) {
|
package/lib/dbs/cds-deploy.js
CHANGED
|
@@ -80,7 +80,7 @@ exports.create = async function cds_deploy_create (db, csn=db.model, o) {
|
|
|
80
80
|
let schevo = o.schema_evolution === 'auto' || o['with-auto-schema-evolution'] || o['model-only'] || o['delta-from'] || (o.kind === 'postgres' && o.schema_evolution !== false);
|
|
81
81
|
if (schevo) {
|
|
82
82
|
const { prior, table_exists } = await get_prior_model()
|
|
83
|
-
const { afterImage, drops: d, createsAndAlters } = cds.compile.to.sql.delta(csn, o, prior
|
|
83
|
+
const { afterImage, drops: d, createsAndAlters } = cds.compile.to.sql.delta(csn, o, prior);
|
|
84
84
|
const after = JSON.stringify(afterImage)
|
|
85
85
|
if (!o.dry && after != prior) {
|
|
86
86
|
if (!table_exists) {
|
|
@@ -135,7 +135,7 @@ exports.create = async function cds_deploy_create (db, csn=db.model, o) {
|
|
|
135
135
|
let file = o['delta-from']
|
|
136
136
|
if (file) {
|
|
137
137
|
let prior = await cds.utils.read(file)
|
|
138
|
-
return { prior }
|
|
138
|
+
return { prior: typeof prior === 'string' ? JSON.parse(prior) : prior }
|
|
139
139
|
}
|
|
140
140
|
if (o.dry) return {}
|
|
141
141
|
|
|
@@ -153,7 +153,7 @@ exports.create = async function cds_deploy_create (db, csn=db.model, o) {
|
|
|
153
153
|
|
|
154
154
|
if (table_exists) {
|
|
155
155
|
let [{ csn }] = await db.run('SELECT csn from cds_model')
|
|
156
|
-
return { prior: csn, table_exists }
|
|
156
|
+
return { prior: csn && JSON.parse(csn), table_exists }
|
|
157
157
|
}
|
|
158
158
|
return { table_exists } // no prior csn
|
|
159
159
|
}
|
package/lib/env/cds-env.js
CHANGED
|
@@ -33,6 +33,7 @@ class Config {
|
|
|
33
33
|
if (NODE_ENV) profiles.push (NODE_ENV)
|
|
34
34
|
if (CDS_ENV) profiles.push (...CDS_ENV.split(/\s*,\s*/))
|
|
35
35
|
if (_home) _add_static_profiles (_home, profiles);
|
|
36
|
+
if (_home && this['project-nature'] === 'java') profiles.push('java')
|
|
36
37
|
if (!profiles.includes('production')) profiles.push('development')
|
|
37
38
|
this._profiles = new Set (profiles)
|
|
38
39
|
this._profiles._defined = new Set
|
|
@@ -173,8 +174,8 @@ class Config {
|
|
|
173
174
|
* For BAS only: to find out whether this is a Java or Node.js project
|
|
174
175
|
*/
|
|
175
176
|
get "project-nature" () {
|
|
176
|
-
const has_pom_xml = [this.folders
|
|
177
|
-
f => isfile (path.join (this._home, f, 'pom.xml'))
|
|
177
|
+
const has_pom_xml = [this.folders?.srv,'.'] .some (
|
|
178
|
+
f => f && isfile (path.join (this._home, f, 'pom.xml'))
|
|
178
179
|
)
|
|
179
180
|
return has_pom_xml ? 'java' : 'nodejs'
|
|
180
181
|
}
|
|
@@ -193,7 +194,6 @@ class Config {
|
|
|
193
194
|
// The following are internal APIs which can always change!
|
|
194
195
|
//
|
|
195
196
|
|
|
196
|
-
|
|
197
197
|
_add_to_process_env (cwd, filename) {
|
|
198
198
|
const file = path.resolve (cwd,filename)
|
|
199
199
|
try {
|
package/lib/env/cds-requires.js
CHANGED
|
@@ -46,6 +46,7 @@ const _authentication_strategies = {
|
|
|
46
46
|
tenants: {}
|
|
47
47
|
},
|
|
48
48
|
"mocked-auth": {
|
|
49
|
+
_kind: 'mocked', // REVISIT: workaround to distinguish from 'basic-auth' (cf. restrict_all_services)
|
|
49
50
|
kind: 'basic-auth',
|
|
50
51
|
users: {
|
|
51
52
|
alice: { tenant: 't1', roles: [ ...admin ] },
|
package/lib/env/defaults.js
CHANGED
|
@@ -149,7 +149,10 @@ const defaults = module.exports = {
|
|
|
149
149
|
},
|
|
150
150
|
|
|
151
151
|
build: {
|
|
152
|
-
target: 'gen'
|
|
152
|
+
target: 'gen',
|
|
153
|
+
'[java]': {
|
|
154
|
+
target: '.'
|
|
155
|
+
}
|
|
153
156
|
},
|
|
154
157
|
|
|
155
158
|
mtx: {
|
|
@@ -163,6 +166,10 @@ const defaults = module.exports = {
|
|
|
163
166
|
},
|
|
164
167
|
|
|
165
168
|
cdsc: {
|
|
169
|
+
moduleLookupDirectories: ['node_modules/'],
|
|
170
|
+
'[java]': {
|
|
171
|
+
moduleLookupDirectories: ['node_modules/', 'target/cds/'],
|
|
172
|
+
}
|
|
166
173
|
// cv2: {
|
|
167
174
|
// _localized_entries: true,
|
|
168
175
|
// _texts_entries: true,
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"profile": {
|
|
12
12
|
"description": "A single static profile",
|
|
13
13
|
"anyOf": [
|
|
14
|
-
{ "enum": [ "mtx-sidecar", "with-mtx-sidecar" ] },
|
|
14
|
+
{ "enum": [ "mtx-sidecar", "with-mtx-sidecar", "java" ] },
|
|
15
15
|
{ "type": "string" }
|
|
16
16
|
]
|
|
17
17
|
},
|
|
@@ -257,8 +257,32 @@
|
|
|
257
257
|
"description": "Enables new middlewares support (experimental)."
|
|
258
258
|
},
|
|
259
259
|
"multitenancy": {
|
|
260
|
-
"
|
|
261
|
-
|
|
260
|
+
"oneOf": [
|
|
261
|
+
{
|
|
262
|
+
"type": "boolean",
|
|
263
|
+
"description": "Shortcut to enable multitenancy."
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
"type": "object",
|
|
267
|
+
"description": "Multitenancy configuration options.",
|
|
268
|
+
"properties": {
|
|
269
|
+
"jobs": {
|
|
270
|
+
"type": "object",
|
|
271
|
+
"description": "Configuration options for the built-in async job executor.",
|
|
272
|
+
"properties": {
|
|
273
|
+
"workerSize": {
|
|
274
|
+
"type": "number",
|
|
275
|
+
"description": "Number of workers running in parallel per database."
|
|
276
|
+
},
|
|
277
|
+
"clusterSize": {
|
|
278
|
+
"type": "number",
|
|
279
|
+
"description": "Number of databases executing parallel tasks."
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
]
|
|
262
286
|
},
|
|
263
287
|
"extensibility": {
|
|
264
288
|
"oneOf": [
|
package/lib/i18n/localize.js
CHANGED
|
@@ -3,7 +3,7 @@ const {existsSync, readdirSync} = require ('fs')
|
|
|
3
3
|
const {join,dirname,resolve,parse,sep} = require ('path')
|
|
4
4
|
|
|
5
5
|
const DEBUG = cds.debug('i18n')
|
|
6
|
-
const _node_modules = sep
|
|
6
|
+
const _node_modules = cds.env.cdsc.moduleLookupDirectories.map(d => sep+d.slice(0, -1))
|
|
7
7
|
|
|
8
8
|
module.exports = Object.assign (localize, {
|
|
9
9
|
bundles4, folders4, folder4, bundle4
|
|
@@ -182,8 +182,8 @@ function folder4 (loc) {
|
|
|
182
182
|
}
|
|
183
183
|
//> no --> search up the folder hierarchy up to cds.root, cds.home, or some .../node_modules/<package>
|
|
184
184
|
let next = dirname(loc)
|
|
185
|
-
if (next.includes(
|
|
186
|
-
if (next.endsWith(
|
|
185
|
+
if (_node_modules.some(m => next.includes(m))) {
|
|
186
|
+
if (_node_modules.some(m => next.endsWith(m))) return folder4[loc] = null
|
|
187
187
|
} else {
|
|
188
188
|
if (!(
|
|
189
189
|
next.startsWith(cds.root) ||
|
package/lib/index.js
CHANGED
|
@@ -139,3 +139,7 @@ global.cds = cds // REVISIT: using global.cds seems wrong
|
|
|
139
139
|
|
|
140
140
|
// install jest util if jest is defined
|
|
141
141
|
if (process.env.CDS_JEST_MEM_FIX && typeof jest !== 'undefined') require('./utils/jest.js')
|
|
142
|
+
|
|
143
|
+
// Allow for import cds from '@sap/cds' without esModuleInterop
|
|
144
|
+
Object.defineProperty(module.exports, "__esModule", { value: true });
|
|
145
|
+
module.exports.default = module.exports
|
package/lib/log/cds-log.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const cds = require ('../index'), conf = cds.env.log
|
|
2
2
|
const log = module.exports = exports = cds_log
|
|
3
|
+
const path = require('path')
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Cache used for all constructed loggers.
|
|
@@ -167,7 +168,15 @@ const { ERROR, WARN, INFO, DEBUG, TRACE } = exports.levels = {
|
|
|
167
168
|
|
|
168
169
|
;(function _init() {
|
|
169
170
|
const conf = cds.env.log
|
|
170
|
-
if (conf.Logger)
|
|
171
|
+
if (conf.Logger) {
|
|
172
|
+
let resolvedPath
|
|
173
|
+
try { resolvedPath = require.resolve(conf.Logger) } catch {
|
|
174
|
+
try { resolvedPath = require.resolve(path.join(cds.root, conf.Logger)) } catch {
|
|
175
|
+
throw new Error(`Cannot find logger at "${conf.Logger}"`)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
exports.Logger = require (resolvedPath) // Use configured logger in case of cds serve
|
|
179
|
+
}
|
|
171
180
|
if (conf.service) {
|
|
172
181
|
const {app} = cds, serveIn = app => require('./service').serveIn(app)
|
|
173
182
|
app ? setImmediate(() => serveIn(app)) : cds.on('bootstrap', app => serveIn(app))
|
package/lib/ql/Whereable.js
CHANGED
|
@@ -78,12 +78,16 @@ const _object_predicate = ([arg], _clause) => { // e.g. .where ({ID:4711, stock:
|
|
|
78
78
|
pred.push('or', ...predicate4([x],_clause))
|
|
79
79
|
continue
|
|
80
80
|
}
|
|
81
|
+
if (k === 'not') {
|
|
82
|
+
pred.push('not', {xpr:predicate4([x],_clause)})
|
|
83
|
+
continue
|
|
84
|
+
}
|
|
81
85
|
if (k === 'exists') {
|
|
82
|
-
pred.push(
|
|
86
|
+
pred.push('and', 'exists', typeof x === 'object' ? x : { ref: x.split('.') })
|
|
83
87
|
continue
|
|
84
88
|
}
|
|
85
89
|
if (k === 'not exists') {
|
|
86
|
-
pred.push(
|
|
90
|
+
pred.push('and', 'not', 'exists', typeof x === 'object' ? x : { ref: x.split('.') })
|
|
87
91
|
continue
|
|
88
92
|
}
|
|
89
93
|
else pred.push('and', parse.expr(k))
|
|
@@ -97,7 +101,7 @@ const _object_predicate = ([arg], _clause) => { // e.g. .where ({ID:4711, stock:
|
|
|
97
101
|
else if (_clause === 'on' && typeof x === 'string') pred.push('=', { ref: x.split('.') })
|
|
98
102
|
else pred.push('=', {val:x})
|
|
99
103
|
}
|
|
100
|
-
return pred.slice(1)
|
|
104
|
+
return pred[0] === 'and' ? pred.slice(1) : pred
|
|
101
105
|
}
|
|
102
106
|
|
|
103
107
|
const _fluid_predicate = (args) => { // e.g. .where ('ID=',4711, 'and stock >=',1)
|
package/lib/req/user.js
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
const PSEUDO_ROLES = ['system-user', 'internal-user']
|
|
2
|
-
|
|
3
1
|
class User {
|
|
4
2
|
|
|
5
3
|
constructor (_) {
|
|
@@ -8,27 +6,31 @@ class User {
|
|
|
8
6
|
if (new.target === Anonymous) return
|
|
9
7
|
else return new User.default
|
|
10
8
|
}
|
|
11
|
-
if (typeof _ === 'string')
|
|
12
|
-
|
|
13
|
-
const roles = this.hasOwnProperty('roles') && this.roles // eslint-disable-line no-prototype-builtins
|
|
14
|
-
if (Array.isArray(roles)) this.roles = roles.filter(r => !PSEUDO_ROLES.includes(r)).reduce ((p,n)=>{p[n]=1; return p},{})
|
|
15
|
-
else PSEUDO_ROLES.forEach(r => delete this.roles[r])
|
|
9
|
+
else if (typeof _ === 'string') this.id = _
|
|
10
|
+
else Object.assign(this,_)
|
|
16
11
|
}
|
|
17
12
|
|
|
18
13
|
get attr() { return super.attr = {} }
|
|
14
|
+
set attr(a) { super.attr = a }
|
|
15
|
+
|
|
19
16
|
get roles(){ return super.roles = {} }
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
role === '
|
|
27
|
-
role === '
|
|
28
|
-
|
|
17
|
+
set roles(r) {
|
|
18
|
+
super.roles = !Array.isArray(r) ? r : r.reduce((p,n)=>{ p[n]=1; return p },{})
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
is (role) {
|
|
22
|
+
return (
|
|
23
|
+
role === 'authenticated-user' ||
|
|
24
|
+
role === 'identified-user' ||
|
|
25
|
+
role === 'any' ||
|
|
26
|
+
!!this.roles[role]
|
|
27
|
+
)
|
|
29
28
|
}
|
|
30
29
|
valueOf() { return this.id }
|
|
31
30
|
|
|
31
|
+
// compatibility
|
|
32
|
+
get _roles(){ return this.roles }
|
|
33
|
+
set _roles(r){ this.roles = r }
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
/**
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
const { performance
|
|
1
|
+
const { performance } = require ('perf_hooks')
|
|
2
2
|
|
|
3
3
|
module.exports = (prec = 1000) => function sap_statistics (req, res, next) {
|
|
4
4
|
if (req.query['sap-statistics'] || req.headers['sap-statistics']) {
|
|
5
|
-
const { writeHead } = res, t0 = now()
|
|
5
|
+
const { writeHead } = res, t0 = performance.now()
|
|
6
6
|
res.writeHead = function (...args) {
|
|
7
|
-
const total = ((now() - t0) / prec).toFixed(2)
|
|
7
|
+
const total = ((performance.now() - t0) / prec).toFixed(2)
|
|
8
8
|
if (res.statusCode < 400) res.setHeader('sap-statistics', `total=${total}`)
|
|
9
9
|
writeHead.call(this, ...args)
|
|
10
10
|
}
|
|
@@ -24,19 +24,20 @@ if (!LOG._debug) module.exports = ()=>[]; else {
|
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
const { performance
|
|
27
|
+
const { performance } = require ('perf_hooks')
|
|
28
28
|
const { format } = require ('util')
|
|
29
29
|
|
|
30
30
|
class PerfTrace extends Array {
|
|
31
31
|
log (...details) {
|
|
32
|
-
const e = { details, start:now() }
|
|
32
|
+
const e = { details, start:performance.now() }
|
|
33
|
+
console.log(e)
|
|
33
34
|
return this.push(e), e
|
|
34
35
|
}
|
|
35
36
|
done (e) {
|
|
36
|
-
return e.stop = now()
|
|
37
|
+
return e.stop = performance.now()
|
|
37
38
|
}
|
|
38
39
|
toString ({truncate}) {
|
|
39
|
-
const t0 = this[0].start; if (!this[0].stop) this[0].stop = now()
|
|
40
|
+
const t0 = this[0].start; if (!this[0].stop) this[0].stop = performance.now()
|
|
40
41
|
return '\n'+ this.map (e => truncate (format (
|
|
41
42
|
(e.start - t0).toFixed(2).padStart(6), '→',
|
|
42
43
|
(e.stop - t0).toFixed(2).padEnd(6), '= ',
|
package/lib/srv/srv-dispatch.js
CHANGED
|
@@ -114,16 +114,17 @@ const _ensure_target = (srv,req) => {
|
|
|
114
114
|
req.target = typeof q === 'object' ? cds.infer(q,defs) : defs[req.path]
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
-
const _ensure_fqn = (x,p,srv,
|
|
118
|
-
if (
|
|
117
|
+
const _ensure_fqn = (x,p,srv, from = x[p]) => {
|
|
118
|
+
if (!from) return
|
|
119
|
+
if (typeof from === 'string') {
|
|
119
120
|
if (srv.isDatabaseService) return
|
|
120
|
-
if (srv.model &&
|
|
121
|
-
if (
|
|
122
|
-
if (
|
|
123
|
-
else x[p] = `${srv.namespace}.${
|
|
124
|
-
} else if (
|
|
125
|
-
const [head] =
|
|
126
|
-
head.id ? _ensure_fqn(head,'id',srv) : _ensure_fqn(
|
|
121
|
+
if (srv.model && from in srv.model.definitions) return
|
|
122
|
+
if (from.startsWith(srv.namespace)) return
|
|
123
|
+
if (from.endsWith('_drafts')) return // REVISIT: rather fix test/fiori/localized-draft.test.js ?
|
|
124
|
+
else x[p] = `${srv.namespace}.${from}`
|
|
125
|
+
} else if (from.ref) {
|
|
126
|
+
const [head] = from.ref
|
|
127
|
+
head.id ? _ensure_fqn(head,'id',srv) : _ensure_fqn(from.ref,0,srv)
|
|
127
128
|
if (x.where) for (let y of x.where) if (y.SELECT) _ensure_fqn(y.SELECT,'from',srv)
|
|
128
129
|
}
|
|
129
130
|
}
|
package/lib/utils/axios.js
CHANGED
|
@@ -44,6 +44,9 @@ const _error = (e) => {
|
|
|
44
44
|
})
|
|
45
45
|
const { code, message } = e.response && e.response.data && e.response.data.error || {}
|
|
46
46
|
if (message) e.message = code && code !== 'null' ? `${code} - ${message}` : message
|
|
47
|
+
// Promote toJSON from prototype to own property to make it iterable
|
|
48
|
+
// eslint-disable-next-line no-self-assign
|
|
49
|
+
if (typeof jest !== 'undefined') e.toJSON = e.toJSON
|
|
47
50
|
throw e
|
|
48
51
|
}
|
|
49
52
|
|
package/lib/utils/cds-test.js
CHANGED
|
@@ -23,6 +23,9 @@ class Test extends require('./axios') {
|
|
|
23
23
|
|
|
24
24
|
// launch cds server...
|
|
25
25
|
before (`launching ${cmd} ${args.join(' ')}...`, async () => {
|
|
26
|
+
// cds.plugins is filling cds.env before cds serve -> profile must be parsed here
|
|
27
|
+
const profile = args.indexOf('--profile')
|
|
28
|
+
if (profile !== -1) process.env.CDS_ENV = [...process.env.CDS_ENV?.split(',') ?? [], ...args[profile+1].split(',')]
|
|
26
29
|
await cds.plugins
|
|
27
30
|
cds.once ('listening', ({server,url}) => {
|
|
28
31
|
const axp = Reflect.getOwnPropertyDescriptor(this,'axios')
|
package/lib/utils/cds-utils.js
CHANGED
|
@@ -5,6 +5,8 @@ const ux = module.exports = exports = new class {
|
|
|
5
5
|
get inflect() { return super.inflect = require('./inflect') }
|
|
6
6
|
get inspect() { return super.inspect = require('util').inspect }
|
|
7
7
|
get uuid() { return super.uuid = require('@sap/cds-foss').uuid }
|
|
8
|
+
get yaml() { return super.yaml = require('@sap/cds-foss').yaml }
|
|
9
|
+
get pool() { return super.pool = require('@sap/cds-foss').pool }
|
|
8
10
|
get tar() { return super.tar = require('./tar') }
|
|
9
11
|
}
|
|
10
12
|
|
|
@@ -2,7 +2,8 @@ const cds = require('../cds')
|
|
|
2
2
|
const LOG = cds.log()
|
|
3
3
|
|
|
4
4
|
const _require = require('../common/utils/require')
|
|
5
|
-
const {
|
|
5
|
+
const { containsAnyRestrictions } = require('../common/utils/restrictions')
|
|
6
|
+
const { ODATA_UNAUTHORIZED } = require('../common/error/constants')
|
|
6
7
|
|
|
7
8
|
let passport, logged
|
|
8
9
|
|
|
@@ -60,7 +61,7 @@ const _log = (req, challenges) => {
|
|
|
60
61
|
const cap_auth_callback = (req, res, next, internalError, user, arg) => {
|
|
61
62
|
// An internal error occurs during the authentication process
|
|
62
63
|
if (internalError) {
|
|
63
|
-
return res.status(401).json({ error:
|
|
64
|
+
return res.status(401).json({ error: ODATA_UNAUTHORIZED }) // no details to client
|
|
64
65
|
}
|
|
65
66
|
|
|
66
67
|
let challenges
|
|
@@ -106,10 +107,9 @@ const _mountMockAuth = (srv, app, strategy, config) => {
|
|
|
106
107
|
|
|
107
108
|
const _mountPassportAuth = (srv, app, strategy, config) => {
|
|
108
109
|
if (strategy in { jwt: 1, xsuaa: 1 } && !config.credentials) {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
return
|
|
110
|
+
let msg = `Authentication kind "${config.kind}" configured, but no XSUAA instance bound to application.`
|
|
111
|
+
msg += ' Either bind an XSUAA instance, or switch to an authentication kind that does not require a binding.'
|
|
112
|
+
throw new Error(msg)
|
|
113
113
|
}
|
|
114
114
|
|
|
115
115
|
if (!passport) passport = _require('passport')
|
|
@@ -140,11 +140,12 @@ module.exports = (srv, options = srv.options) => {
|
|
|
140
140
|
// NOTE: options.auth is not an official API
|
|
141
141
|
let config = 'auth' in options ? options.auth : cds.env.requires.auth
|
|
142
142
|
if (!config) {
|
|
143
|
+
// REVISIT: can config be falsy? req.user would be undefined!
|
|
143
144
|
if (cds.requires.db && cds.requires.multitenancy) {
|
|
144
145
|
process.exitCode = 1 // REVISIT: why exitCode needed?
|
|
145
146
|
throw new Error('Authentication required for multitenancy')
|
|
146
147
|
}
|
|
147
|
-
if (
|
|
148
|
+
if (containsAnyRestrictions(srv)) {
|
|
148
149
|
process.exitCode = 1 // REVISIT: why exitCode needed?
|
|
149
150
|
throw new Error('Authentication required for authorization checks')
|
|
150
151
|
}
|
|
@@ -161,12 +162,6 @@ module.exports = (srv, options = srv.options) => {
|
|
|
161
162
|
// mount authentication middleware or strategy
|
|
162
163
|
if (!logged) LOG._debug && LOG.debug(`Using authentication`, { kind: config.kind })
|
|
163
164
|
|
|
164
|
-
// Security by default: set restrict_all_services if not disabled
|
|
165
|
-
// this is done dynamically to also cover custom auth impl
|
|
166
|
-
if (process.env.NODE_ENV === 'production' && config.restrict_all_services !== false) {
|
|
167
|
-
config.restrict_all_services = true
|
|
168
|
-
}
|
|
169
|
-
|
|
170
165
|
if (config.impl) {
|
|
171
166
|
// mount custom authentication middleware
|
|
172
167
|
_mountCustomAuth(srv, app, config)
|
|
@@ -184,15 +179,6 @@ module.exports = (srv, options = srv.options) => {
|
|
|
184
179
|
}
|
|
185
180
|
}
|
|
186
181
|
|
|
187
|
-
// Security by default: enforce authenticated users in production if auth service bound
|
|
188
|
-
if (
|
|
189
|
-
cds.requires.multitenancy ||
|
|
190
|
-
(process.env.NODE_ENV === 'production' && config.credentials && config.restrict_all_services)
|
|
191
|
-
) {
|
|
192
|
-
if (!logged) LOG._debug && LOG.debug(`Enforcing authenticated users for all services`)
|
|
193
|
-
app.use(cap_enforce_login)
|
|
194
|
-
}
|
|
195
|
-
|
|
196
182
|
// so we don't log the same stuff multiple times
|
|
197
183
|
logged = true
|
|
198
184
|
|
|
@@ -206,13 +192,3 @@ const _strategy4 = config => {
|
|
|
206
192
|
process.exitCode = 1 // REVISIT: why exitCode needed?
|
|
207
193
|
throw new Error(`Authentication kind "${config.kind}" is not supported`)
|
|
208
194
|
}
|
|
209
|
-
|
|
210
|
-
const cap_enforce_login = (req, res, next) => {
|
|
211
|
-
if (req.user && req.user.is('authenticated-user')) return next() // pass if user is authenticated
|
|
212
|
-
if (!req.user || req.user._is_anonymous) {
|
|
213
|
-
if (req.user && req.user._challenges) res.set('WWW-Authenticate', req.user._challenges.join(';'))
|
|
214
|
-
return res.status(401).json({ error: UNAUTHORIZED }) // no details to client
|
|
215
|
-
} else {
|
|
216
|
-
return res.status(403).json({ error: FORBIDDEN }) // no details to client
|
|
217
|
-
}
|
|
218
|
-
}
|