@sap/cds 6.4.1 → 6.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +79 -6
- package/README.md +5 -0
- package/apis/cqn.d.ts +14 -3
- package/apis/ql.d.ts +8 -8
- package/apis/services.d.ts +37 -65
- package/apis/test.d.ts +7 -0
- package/bin/build/buildTaskEngine.js +9 -14
- package/bin/build/buildTaskFactory.js +1 -1
- package/bin/build/buildTaskHandler.js +3 -14
- package/bin/build/index.js +8 -2
- package/bin/build/provider/buildTaskProviderInternal.js +18 -13
- package/bin/build/provider/fiori/index.js +5 -10
- package/bin/build/provider/hana/2migration.js +11 -2
- package/bin/build/provider/hana/index.js +17 -14
- package/bin/build/provider/hana/template/.hdiconfig-hanacloud +137 -0
- package/bin/build/provider/hana/template/package.json +3 -0
- package/bin/build/provider/mtx/resourcesTarBuilder.js +12 -3
- package/bin/build/provider/mtx-extension/index.js +57 -37
- package/bin/build/provider/mtx-sidecar/index.js +1 -1
- package/bin/build/util.js +18 -1
- package/bin/cds.js +1 -5
- package/bin/deploy/to-hana/hana.js +10 -3
- package/bin/serve.js +36 -20
- package/common.cds +7 -0
- package/lib/auth/jwt-auth.js +8 -7
- package/lib/compile/for/lean_drafts.js +55 -6
- package/lib/compile/minify.js +3 -3
- package/lib/dbs/cds-deploy.js +18 -17
- package/lib/env/cds-requires.js +1 -1
- package/lib/env/defaults.js +5 -1
- package/lib/env/schemas/cds-rc.json +74 -3
- package/lib/index.js +4 -2
- package/lib/lazy.js +6 -8
- package/lib/log/cds-error.js +2 -2
- package/lib/ql/Whereable.js +22 -11
- package/lib/ql/cds-ql.js +1 -1
- package/lib/req/cds-context.js +3 -3
- package/lib/req/response.js +8 -3
- package/lib/req/user.js +12 -2
- package/lib/srv/bindings.js +1 -2
- package/lib/srv/cds-serve.js +2 -1
- package/lib/srv/middlewares/trace.js +31 -15
- package/lib/srv/protocols/odata-v2-proxy.js +8 -8
- package/lib/srv/srv-handlers.js +26 -7
- package/lib/srv/srv-methods.js +2 -2
- package/lib/srv/srv-models.js +8 -3
- package/lib/utils/cds-test.js +7 -5
- package/lib/utils/cds-utils.js +3 -1
- package/lib/utils/tar.js +6 -3
- package/libx/_runtime/auth/strategies/JWT.js +1 -0
- package/libx/_runtime/auth/strategies/ias-auth.js +3 -2
- package/libx/_runtime/auth/strategies/mock.js +12 -1
- package/libx/_runtime/auth/strategies/xssecUtils.js +7 -8
- package/libx/_runtime/auth/strategies/xsuaa.js +1 -0
- package/libx/_runtime/cds-services/adapter/odata-v4/OData.js +6 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +1 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +26 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +8 -0
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/ExpressionToCQN.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/readToCQN.js +11 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/utils/PrimitiveValueDecoder.js +8 -8
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/utils/ValueConverter.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/validator/ValueValidator.js +14 -14
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/deserializer/DeserializerFactory.js +1 -0
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/serializer/ResourceJsonSerializer.js +3 -0
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/utils/UriHelper.js +2 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/metaInfo.js +3 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +7 -0
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/stream.js +0 -3
- package/libx/_runtime/cds-services/services/Service.js +11 -19
- package/libx/_runtime/cds-services/services/utils/columns.js +42 -40
- package/libx/_runtime/cds-services/util/assert.js +7 -1
- package/libx/_runtime/common/code-ext/WorkerReq.js +81 -0
- package/libx/_runtime/common/code-ext/config.js +13 -0
- package/libx/_runtime/common/code-ext/execute.js +113 -0
- package/libx/_runtime/common/code-ext/handlers.js +49 -0
- package/libx/_runtime/common/code-ext/worker.js +40 -0
- package/libx/_runtime/common/code-ext/workerQuery.js +45 -0
- package/libx/_runtime/common/code-ext/workerQueryExecutor.js +36 -0
- package/libx/_runtime/common/composition/data.js +5 -2
- package/libx/_runtime/common/composition/tree.js +2 -0
- package/libx/_runtime/common/generic/auth/restrict.js +1 -1
- package/libx/_runtime/common/generic/crud.js +4 -0
- package/libx/_runtime/common/generic/etag.js +3 -1
- package/libx/_runtime/common/generic/input.js +12 -14
- package/libx/_runtime/common/i18n/index.js +1 -1
- package/libx/_runtime/common/utils/cqn2cqn4sql.js +47 -22
- package/libx/_runtime/common/utils/path.js +5 -26
- package/libx/_runtime/common/utils/search2cqn4sql.js +16 -9
- package/libx/_runtime/common/utils/templateProcessorPathSerializer.js +19 -13
- package/libx/_runtime/db/data-conversion/post-processing.js +1 -1
- package/libx/_runtime/db/expand/expandCQNToJoin.js +7 -4
- package/libx/_runtime/db/expand/rawToExpanded.js +3 -2
- package/libx/_runtime/db/generic/input.js +2 -2
- package/libx/_runtime/db/generic/integrity.js +1 -0
- package/libx/_runtime/db/generic/virtual.js +1 -0
- package/libx/_runtime/db/query/read.js +3 -2
- package/libx/_runtime/db/utils/localized.js +1 -1
- package/libx/_runtime/fiori/generic/activate.js +7 -1
- package/libx/_runtime/fiori/generic/before.js +9 -1
- package/libx/_runtime/fiori/generic/edit.js +8 -1
- package/libx/_runtime/fiori/generic/new.js +2 -0
- package/libx/_runtime/fiori/generic/patch.js +2 -0
- package/libx/_runtime/fiori/generic/prepare.js +2 -0
- package/libx/_runtime/fiori/generic/read.js +16 -5
- package/libx/_runtime/fiori/generic/readOverDraft.js +2 -0
- package/libx/_runtime/fiori/lean-draft.js +505 -241
- package/libx/_runtime/fiori/utils/delete.js +2 -0
- package/libx/_runtime/hana/customBuilder/CustomSelectBuilder.js +5 -5
- package/libx/_runtime/hana/pool.js +1 -1
- package/libx/_runtime/hana/search2cqn4sql.js +51 -51
- package/libx/_runtime/messaging/Outbox.js +1 -1
- package/libx/_runtime/messaging/enterprise-messaging-utils/getTenantInfo.js +1 -0
- package/libx/_runtime/messaging/enterprise-messaging.js +2 -6
- package/libx/_runtime/messaging/file-based.js +1 -2
- package/libx/_runtime/messaging/outbox/OutboxRunner.js +1 -1
- package/libx/_runtime/messaging/outbox/utils.js +1 -1
- package/libx/_runtime/messaging/service.js +0 -1
- package/libx/_runtime/remote/Service.js +1 -0
- package/libx/_runtime/sqlite/convertDraftAdminPathExpression.js +19 -3
- package/libx/_runtime/sqlite/customBuilder/CustomExpressionBuilder.js +0 -18
- package/libx/_runtime/sqlite/customBuilder/CustomFunctionBuilder.js +0 -18
- package/libx/_runtime/sqlite/customBuilder/CustomSelectBuilder.js +0 -24
- package/libx/_runtime/sqlite/customBuilder/CustomUpsertBuilder.js +2 -1
- package/libx/_runtime/sqlite/customBuilder/index.js +47 -32
- package/libx/odata/afterburner.js +23 -8
- package/libx/odata/cqn2odata.js +1 -1
- package/libx/odata/grammar.pegjs +3 -4
- package/libx/odata/index.js +5 -1
- package/libx/odata/parseToCqn.js +3 -3
- package/libx/odata/parser.js +1 -1
- package/libx/odata/utils.js +58 -1
- package/libx/rest/middleware/parse.js +26 -4
- package/package.json +1 -1
- package/server.js +1 -1
- package/libx/_runtime/sqlite/customBuilder/CustomDeleteBuilder.js +0 -17
- package/libx/_runtime/sqlite/customBuilder/CustomReferenceBuilder.js +0 -11
- package/libx/_runtime/sqlite/customBuilder/CustomUpdateBuilder.js +0 -17
- /package/bin/build/provider/hana/template/{.hdiconfig → .hdiconfig-haas} +0 -0
|
@@ -938,7 +938,7 @@ function cov2ap(options = {}) {
|
|
|
938
938
|
}
|
|
939
939
|
}
|
|
940
940
|
|
|
941
|
-
Object.
|
|
941
|
+
Object.keys(headers).forEach(name => {
|
|
942
942
|
if (
|
|
943
943
|
name === "dataserviceversion" ||
|
|
944
944
|
name === "DataServiceVersion" ||
|
|
@@ -1171,7 +1171,7 @@ function cov2ap(options = {}) {
|
|
|
1171
1171
|
return req.context;
|
|
1172
1172
|
}
|
|
1173
1173
|
|
|
1174
|
-
function convertUrlLinks(url
|
|
1174
|
+
function convertUrlLinks(url) {
|
|
1175
1175
|
url.contextPath = url.contextPath.replace(/\/\$links\//gi, "/");
|
|
1176
1176
|
}
|
|
1177
1177
|
|
|
@@ -1630,7 +1630,7 @@ function cov2ap(options = {}) {
|
|
|
1630
1630
|
}
|
|
1631
1631
|
}
|
|
1632
1632
|
|
|
1633
|
-
function convertFilter(url
|
|
1633
|
+
function convertFilter(url) {
|
|
1634
1634
|
const _ = "§§";
|
|
1635
1635
|
|
|
1636
1636
|
let filter = url.query["$filter"];
|
|
@@ -1702,7 +1702,7 @@ function cov2ap(options = {}) {
|
|
|
1702
1702
|
}
|
|
1703
1703
|
}
|
|
1704
1704
|
|
|
1705
|
-
function convertSearch(url
|
|
1705
|
+
function convertSearch(url) {
|
|
1706
1706
|
if (url.query.search) {
|
|
1707
1707
|
let search = url.query.search;
|
|
1708
1708
|
if (quoteSearch) {
|
|
@@ -2789,7 +2789,7 @@ function cov2ap(options = {}) {
|
|
|
2789
2789
|
}
|
|
2790
2790
|
}
|
|
2791
2791
|
|
|
2792
|
-
function removeMetadata(data
|
|
2792
|
+
function removeMetadata(data) {
|
|
2793
2793
|
Object.keys(data).forEach((key) => {
|
|
2794
2794
|
if (key.startsWith("@") || key.startsWith("odata.") || key.includes("@odata.")) {
|
|
2795
2795
|
delete data[key];
|
|
@@ -2797,7 +2797,7 @@ function cov2ap(options = {}) {
|
|
|
2797
2797
|
});
|
|
2798
2798
|
}
|
|
2799
2799
|
|
|
2800
|
-
function convertMedia(data
|
|
2800
|
+
function convertMedia(data) {
|
|
2801
2801
|
Object.keys(data).forEach((key) => {
|
|
2802
2802
|
if (key.endsWith("@odata.mediaReadLink")) {
|
|
2803
2803
|
data[key.split("@odata.mediaReadLink")[0]] = data[key];
|
|
@@ -2807,7 +2807,7 @@ function cov2ap(options = {}) {
|
|
|
2807
2807
|
});
|
|
2808
2808
|
}
|
|
2809
2809
|
|
|
2810
|
-
function removeAnnotations(data
|
|
2810
|
+
function removeAnnotations(data) {
|
|
2811
2811
|
Object.keys(data).forEach((key) => {
|
|
2812
2812
|
if (key.startsWith("@")) {
|
|
2813
2813
|
delete data[key];
|
|
@@ -2999,7 +2999,7 @@ function cov2ap(options = {}) {
|
|
|
2999
2999
|
return `PT${timeParts[0] || "00"}H${timeParts[1] || "00"}M${timeParts[2] || "00"}S`;
|
|
3000
3000
|
}
|
|
3001
3001
|
|
|
3002
|
-
function addResultsNesting(data, headers, definition, elements
|
|
3002
|
+
function addResultsNesting(data, headers, definition, elements) {
|
|
3003
3003
|
if (!returnCollectionNested) {
|
|
3004
3004
|
return;
|
|
3005
3005
|
}
|
package/lib/srv/srv-handlers.js
CHANGED
|
@@ -16,13 +16,19 @@ class EventHandlers {
|
|
|
16
16
|
)}
|
|
17
17
|
|
|
18
18
|
async prepend (...impl_functions) {
|
|
19
|
-
// IMPORTANT: We might be called in parallel -> the ._handlers.
|
|
20
|
-
// game below avoids loosing registrations due to race conditions
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
19
|
+
// IMPORTANT: We might be called in parallel -> the ._handlers._real
|
|
20
|
+
// game below avoids loosing registrations due to race conditions.
|
|
21
|
+
// Note also that {__proto__:this, _handlers:_new} doesn't work as
|
|
22
|
+
// usages frequently look like that: srv.prepend(()=>srv.on(...)),
|
|
23
|
+
// which means the derived srv instance would be bypassed.
|
|
24
|
+
const _real = this._handlers._real || this._handlers
|
|
25
|
+
const _new = { on:[], before:[], after:[], _initial:[], _error:[], _real }
|
|
26
|
+
await Promise.all (impl_functions.map (fn => { if (is_impl(fn)) {
|
|
27
|
+
this._handlers = _new
|
|
28
|
+
return fn.call (this,this)
|
|
29
|
+
}}))
|
|
30
|
+
for (let handlers in _new) if (_new[handlers].length) _real[handlers] = [ ..._new[handlers], ..._real[handlers] ]
|
|
31
|
+
this._handlers = _real
|
|
26
32
|
return this
|
|
27
33
|
}
|
|
28
34
|
|
|
@@ -85,6 +91,19 @@ const _register = function (srv, phase, event, path, handler) { //NOSONAR
|
|
|
85
91
|
if (!path.startsWith(srv.name+'.')) path = `${srv.name}.${path}`
|
|
86
92
|
}
|
|
87
93
|
|
|
94
|
+
if (cds.env.features.lean_draft && cds.env.features.lean_draft_compatibility) {
|
|
95
|
+
const entity = path && srv.model?.definitions[path.name || path]
|
|
96
|
+
if (['PATCH', 'CANCEL', 'NEW'].includes(event)) {
|
|
97
|
+
// delegate to drafts
|
|
98
|
+
path = typeof path === 'string' && path !== '*' && !path.endsWith('.drafts') ? path + '.drafts' : typeof path === 'object' && path.drafts || path
|
|
99
|
+
if (event === 'PATCH') event = 'UPDATE'
|
|
100
|
+
}
|
|
101
|
+
else if (entity && (event === 'READ' || entity.actions?.[event]) && (entity.drafts && !entity.name.endsWith('.drafts'))) {
|
|
102
|
+
// additionally add drafts for READ and bound actions/functions
|
|
103
|
+
_register(srv, phase, event, entity.name + '.drafts', handler)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
88
107
|
// Finally register with a filter function to match requests to be handled
|
|
89
108
|
const _handlers = srv._handlers [event === 'error' ? '_error' : (handler._initial ? '_initial' : phase)] // REVISIT: remove _initial handlers
|
|
90
109
|
_handlers.push (new EventHandler (phase, event, path, handler))
|
package/lib/srv/srv-methods.js
CHANGED
|
@@ -4,8 +4,8 @@ const LOG = cds.log('cds.serve',{label:'cds'})
|
|
|
4
4
|
|
|
5
5
|
module.exports = (srv) => {
|
|
6
6
|
if (srv.model && ( //> we only support that for app services
|
|
7
|
-
srv
|
|
8
|
-
srv
|
|
7
|
+
srv.isAppService ||
|
|
8
|
+
srv.isExternal ||
|
|
9
9
|
srv._add_stub_methods
|
|
10
10
|
)) {
|
|
11
11
|
for (const each of srv.operations) {
|
package/lib/srv/srv-models.js
CHANGED
|
@@ -57,7 +57,13 @@ class ExtendedModels {
|
|
|
57
57
|
else return cache[key] = (async()=>{ // temporarily add promise to cache to avoid race conditions...
|
|
58
58
|
|
|
59
59
|
// If tenant doesn't have extensions check cache with tenant = undefined
|
|
60
|
-
|
|
60
|
+
let _has_extensions = false
|
|
61
|
+
try {
|
|
62
|
+
_has_extensions = tenant && extensibility && await _is_extended(tenant)
|
|
63
|
+
} catch (error) {
|
|
64
|
+
// Better error message for client
|
|
65
|
+
cds.error('`extensibility: true` is configured but table "cds.xt.Extensions" does not exist. Please redeploy.', error)
|
|
66
|
+
}
|
|
61
67
|
if (!_has_extensions) {
|
|
62
68
|
let k = cache.key4 (tenant = undefined, features)
|
|
63
69
|
let cached = cache.at(k); if (cached) return cached
|
|
@@ -146,8 +152,7 @@ class ExtendedModels {
|
|
|
146
152
|
if (Date.now() - m._cached.touched > ExtendedModels.sentinelInterval) {
|
|
147
153
|
delete this [key]
|
|
148
154
|
}
|
|
149
|
-
}}, ExtendedModels.sentinelInterval)
|
|
150
|
-
cds.on('shutdown', ()=> clearInterval(this.sentinel))
|
|
155
|
+
}}, ExtendedModels.sentinelInterval).unref()
|
|
151
156
|
}
|
|
152
157
|
|
|
153
158
|
|
package/lib/utils/cds-test.js
CHANGED
|
@@ -29,10 +29,8 @@ class Test extends require('./axios') {
|
|
|
29
29
|
catch (e) { if (is_mocha) console.error(e) } // eslint-disable-line no-console
|
|
30
30
|
})
|
|
31
31
|
|
|
32
|
-
// shutdown cds server...
|
|
33
|
-
after (
|
|
34
|
-
this.server ? this.server.close (done) : done && done()
|
|
35
|
-
})
|
|
32
|
+
// gracefully shutdown cds server...
|
|
33
|
+
after (() => this.server && cds.shutdown())
|
|
36
34
|
|
|
37
35
|
beforeEach (async () => {
|
|
38
36
|
if (this.data._autoReset) await this.data.reset()
|
|
@@ -80,7 +78,11 @@ class Test extends require('./axios') {
|
|
|
80
78
|
|
|
81
79
|
then(r) {
|
|
82
80
|
const {cds} = this
|
|
83
|
-
|
|
81
|
+
if (this.server) {
|
|
82
|
+
r({ server: this.server, url: this.url })
|
|
83
|
+
} else {
|
|
84
|
+
cds.once('listening', r)
|
|
85
|
+
}
|
|
84
86
|
}
|
|
85
87
|
}
|
|
86
88
|
|
package/lib/utils/cds-utils.js
CHANGED
|
@@ -63,6 +63,7 @@ exports.exists = function exists (x) {
|
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
// REVISIT naming: doesn't return boolean
|
|
66
67
|
exports.isdir = function isdir (x) {
|
|
67
68
|
if (x) try {
|
|
68
69
|
const y = resolve (cds.root,x)
|
|
@@ -72,6 +73,7 @@ exports.isdir = function isdir (x) {
|
|
|
72
73
|
} catch(e){/* ignore */}
|
|
73
74
|
}
|
|
74
75
|
|
|
76
|
+
// REVISIT naming: doesn't return boolean
|
|
75
77
|
exports.isfile = function isfile (x) {
|
|
76
78
|
if (x) try {
|
|
77
79
|
const y = resolve (cds.root,x)
|
|
@@ -100,7 +102,7 @@ exports.read = async function read (file, _encoding) {
|
|
|
100
102
|
exports.write = function write (file, data, o) {
|
|
101
103
|
if (arguments.length === 1) return {to:(...path) => write(join(...path),file)}
|
|
102
104
|
if (typeof data === 'object' && !Buffer.isBuffer(data))
|
|
103
|
-
data = JSON.stringify(data, null, ' '.repeat(o && o.spaces))
|
|
105
|
+
data = JSON.stringify(data, null, ' '.repeat(o && o.spaces)) + require('os').EOL
|
|
104
106
|
const f = resolve (cds.root,file)
|
|
105
107
|
return fs.mkdirp (dirname(f)).then (()=> fs.promises.writeFile (f,data,o))
|
|
106
108
|
}
|
package/lib/utils/tar.js
CHANGED
|
@@ -96,8 +96,11 @@ exports.create = async (dir='.', ...args) => {
|
|
|
96
96
|
c = spawnDir(dir, args)
|
|
97
97
|
}
|
|
98
98
|
} else {
|
|
99
|
-
if (Array.isArray(args[0]))
|
|
100
|
-
|
|
99
|
+
if (Array.isArray(args[0])) {
|
|
100
|
+
args.push (...args.shift().map (f => path.isAbsolute(f) ? path.relative(dir,f) : f))
|
|
101
|
+
} else {
|
|
102
|
+
args.push('.')
|
|
103
|
+
}
|
|
101
104
|
|
|
102
105
|
c = spawn ('tar', ['c', '-C', dir, ...args])
|
|
103
106
|
}
|
|
@@ -236,5 +239,5 @@ exports.t = tar.tf = tar.list
|
|
|
236
239
|
// ---------------------------------------------------------------------------------
|
|
237
240
|
// Compatibility...
|
|
238
241
|
|
|
239
|
-
exports.packTarArchive = (
|
|
242
|
+
exports.packTarArchive = (resources,d) => d ? tar.cz (d,resources) : tar.cz (resources)
|
|
240
243
|
exports.unpackTarArchive = (x,dir) => tar.xz(x).to(dir)
|
|
@@ -24,6 +24,7 @@ class JWTStrategy extends JS {
|
|
|
24
24
|
roles: xssecUtils.getRoles(['any', 'identified-user'], info, credentials),
|
|
25
25
|
attr: xssecUtils.getAttrForJWT(info)
|
|
26
26
|
})
|
|
27
|
+
xssecUtils.addRolesFromGrantType(user, info, credentials)
|
|
27
28
|
const tenant = xssecUtils.getTenant(info)
|
|
28
29
|
if (tenant) user.tenant = tenant
|
|
29
30
|
// call "super.success"
|
|
@@ -42,9 +42,10 @@ module.exports = function ias_auth(config) {
|
|
|
42
42
|
if (req.tokenInfo.getClientId() === req.tokenInfo.getSubject()) {
|
|
43
43
|
req.user = new cds.User({
|
|
44
44
|
id: 'system',
|
|
45
|
-
roles: ['
|
|
45
|
+
roles: ['authenticated-user'],
|
|
46
46
|
attr: {}
|
|
47
47
|
})
|
|
48
|
+
req.user._is_system = true
|
|
48
49
|
} else {
|
|
49
50
|
// add all unknown attributes to req.user.attr in order to keep public API small
|
|
50
51
|
const payload = req.tokenInfo.getPayload()
|
|
@@ -65,7 +66,7 @@ module.exports = function ias_auth(config) {
|
|
|
65
66
|
req.tenant = req.tokenInfo.getZoneId()
|
|
66
67
|
next()
|
|
67
68
|
})
|
|
68
|
-
.use((err, req, res,
|
|
69
|
+
.use((err, req, res, _next) => {
|
|
69
70
|
if (req.tokenInfo) {
|
|
70
71
|
LOG?.debug('error during token validation', req.tokenInfo.getErrorObject())
|
|
71
72
|
}
|
|
@@ -21,6 +21,17 @@ class MockStrategy {
|
|
|
21
21
|
if (user.password && user.password !== password) return this.fail(CHALLENGE)
|
|
22
22
|
|
|
23
23
|
const { features } = req.headers
|
|
24
|
+
// Only in the mock strategy the pseudo roles are kept in the role list.
|
|
25
|
+
// In all other cases pseudo roles are filtered out.
|
|
26
|
+
if (user.roles) {
|
|
27
|
+
if (Array.isArray(user.roles)) {
|
|
28
|
+
if (user.roles.includes('system-user')) user._is_system = true
|
|
29
|
+
if (user.roles.includes('internal-user')) user._is_internal = true
|
|
30
|
+
} else {
|
|
31
|
+
if ('system-user' in user.roles) user._is_system = true
|
|
32
|
+
if ('internal-user' in user.roles) user._is_internal = true
|
|
33
|
+
}
|
|
34
|
+
}
|
|
24
35
|
this.success(new cds.User(features ? { ...user, features } : user))
|
|
25
36
|
}
|
|
26
37
|
}
|
|
@@ -44,7 +55,7 @@ const _init_users = (users, tenants = {}) => {
|
|
|
44
55
|
Array.isArray(user.roles) ? user.roles.push(...scopes) : (user.roles = scopes)
|
|
45
56
|
}
|
|
46
57
|
if (user.jwt.grant_type === 'client_credentials' || user.jwt.grant_type === 'client_x509') {
|
|
47
|
-
user.
|
|
58
|
+
user._is_system = true
|
|
48
59
|
}
|
|
49
60
|
if (!user.tenant && user.jwt.zid) user.tenant = user.jwt.zid
|
|
50
61
|
}
|
|
@@ -5,21 +5,19 @@ const getUserId = (user, info) => {
|
|
|
5
5
|
return user.id || (info && info.getClientId && info.getClientId())
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
-
const
|
|
8
|
+
const addRolesFromGrantType = (user, info, credentials) => {
|
|
9
9
|
const grantType = info && (info.grantType || (info.getGrantType && info.getGrantType()))
|
|
10
10
|
if (grantType) {
|
|
11
11
|
// > not "weak"
|
|
12
|
-
roles
|
|
12
|
+
user.roles['authenticated-user'] = true
|
|
13
13
|
if (grantType in CLIENT) {
|
|
14
|
-
|
|
15
|
-
if (info.getClientId() === credentials.clientid)
|
|
14
|
+
user._is_system = true
|
|
15
|
+
if (info.getClientId() === credentials.clientid) user._is_internal = true
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
const getRoles = (roles, info
|
|
21
|
-
_addRolesFromGrantType(roles, info, credentials)
|
|
22
|
-
|
|
20
|
+
const getRoles = (roles, info) => {
|
|
23
21
|
// convert to object
|
|
24
22
|
roles = Object.assign(...roles.map(ele => ({ [ele]: true })))
|
|
25
23
|
|
|
@@ -90,5 +88,6 @@ module.exports = {
|
|
|
90
88
|
getRoles,
|
|
91
89
|
getAttrForJWT,
|
|
92
90
|
getAttrForXSSEC,
|
|
93
|
-
getTenant
|
|
91
|
+
getTenant,
|
|
92
|
+
addRolesFromGrantType
|
|
94
93
|
}
|
|
@@ -25,6 +25,7 @@ class XSUAAStrategy extends JS {
|
|
|
25
25
|
roles: xssecUtils.getRoles(['any', 'identified-user'], info, credentials),
|
|
26
26
|
attr: xssecUtils.getAttrForXSSEC(info)
|
|
27
27
|
})
|
|
28
|
+
xssecUtils.addRolesFromGrantType(user, info, credentials)
|
|
28
29
|
const tenant = xssecUtils.getTenant(info)
|
|
29
30
|
if (tenant) user.tenant = tenant
|
|
30
31
|
// call "super.success"
|
|
@@ -253,8 +253,12 @@ class OData {
|
|
|
253
253
|
this._odataService.process(req, res).catch(err => {
|
|
254
254
|
LOG.warn(err)
|
|
255
255
|
// REVISIT: use i18n
|
|
256
|
-
//do not reply with error, if response already processed (streaming)
|
|
257
|
-
|
|
256
|
+
// do not reply with error, if response already processed (streaming)
|
|
257
|
+
// destroy response socket instead
|
|
258
|
+
if (res.headersSent) {
|
|
259
|
+
// REVISIT: temp solution until streaming is switched to express middlewares
|
|
260
|
+
res.socket.destroy()
|
|
261
|
+
} else {
|
|
258
262
|
const { error, statusCode } = normalizeError(err, req)
|
|
259
263
|
res.status(statusCode).send({ error })
|
|
260
264
|
}
|
|
@@ -60,9 +60,8 @@ const action = service => {
|
|
|
60
60
|
await tx.commit(result)
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
if (isReturnMinimal(req) || result === null) odataRes.setStatusCode(204)
|
|
63
|
+
if (isReturnMinimal(req) || result === null) odataRes.setStatusCode(204, { overwrite: true })
|
|
64
64
|
else if (req.event === 'draftActivate' || req.event === 'EDIT') {
|
|
65
|
-
odataRes.setStatusCode(201)
|
|
66
65
|
const keys = Object.keys(req.target.keys).filter(k => {
|
|
67
66
|
return k !== 'IsActiveEntity' && !req.target.keys[k]._isAssociationStrict
|
|
68
67
|
})
|
|
@@ -251,6 +251,14 @@ const _readEntityOrProperty = async (tx, req, segments) => {
|
|
|
251
251
|
return odataResult
|
|
252
252
|
}
|
|
253
253
|
|
|
254
|
+
const _reliablePagingPossible = req => {
|
|
255
|
+
if (req.target._isDraftEnabled) return false
|
|
256
|
+
if (cds.context?.http.req.query.$apply) return false
|
|
257
|
+
if (req.query.SELECT.limit.offset?.val ?? req.query.SELECT.limit.offset > 0) return false
|
|
258
|
+
if (req.query.SELECT.orderBy.some(o => !o.ref)) return false
|
|
259
|
+
return req.query.SELECT.orderBy.every(o => req.query.SELECT.columns.some(c => o.ref[0] === c.ref[0]))
|
|
260
|
+
}
|
|
261
|
+
|
|
254
262
|
/**
|
|
255
263
|
* Read an entity collection without including the count of the total amount of entities.
|
|
256
264
|
*
|
|
@@ -260,6 +268,7 @@ const _readEntityOrProperty = async (tx, req, segments) => {
|
|
|
260
268
|
* @returns {Promise}
|
|
261
269
|
* @private
|
|
262
270
|
*/
|
|
271
|
+
// eslint-disable-next-line complexity
|
|
263
272
|
const _readCollection = async (tx, req, odataReq) => {
|
|
264
273
|
const result = (await tx.dispatch(req)) || []
|
|
265
274
|
if (Array.isArray(req.query)) {
|
|
@@ -284,7 +293,23 @@ const _readCollection = async (tx, req, odataReq) => {
|
|
|
284
293
|
const top = odataReq.getUriInfo().getQueryOption(QueryOptions.TOP)
|
|
285
294
|
if (limit && limit === result.length && limit !== top && !('$nextLink' in result)) {
|
|
286
295
|
const token = odataReq.getUriInfo().getQueryOption(QueryOptions.SKIPTOKEN)
|
|
287
|
-
|
|
296
|
+
if (cds.env.query.limit.reliablePaging && _reliablePagingPossible(req)) {
|
|
297
|
+
const decoded = token && JSON.parse(Buffer.from(token, 'base64').toString())
|
|
298
|
+
const skipToken = {
|
|
299
|
+
r: (decoded?.r || 0) + limit,
|
|
300
|
+
c: req.query.SELECT.orderBy.map(o => ({
|
|
301
|
+
a: o.sort ? o.sort === 'asc' : true,
|
|
302
|
+
k: o.ref[0],
|
|
303
|
+
v: result[result.length - 1][o.ref[0]]
|
|
304
|
+
}))
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (limit + (decoded?.r || 0) !== top) {
|
|
308
|
+
result.$nextLink = Buffer.from(JSON.stringify(skipToken)).toString('base64')
|
|
309
|
+
}
|
|
310
|
+
} else {
|
|
311
|
+
result.$nextLink = (token ? parseInt(token) : 0) + limit
|
|
312
|
+
}
|
|
288
313
|
}
|
|
289
314
|
|
|
290
315
|
const odataResult = toODataResult(result, req)
|
|
@@ -11,6 +11,7 @@ const { isReturnMinimal } = require('../utils/handlerUtils')
|
|
|
11
11
|
const { readAfterWrite } = require('../utils/readAfterWrite')
|
|
12
12
|
const { toODataResult, postProcess, postProcessMinimal } = require('../utils/result')
|
|
13
13
|
const { hasOmitValuesPreference } = require('../utils/omitValues')
|
|
14
|
+
const { isStreaming } = require('../utils/stream')
|
|
14
15
|
|
|
15
16
|
const { getSapMessages } = require('../../../../common/error/frontend')
|
|
16
17
|
|
|
@@ -146,6 +147,13 @@ const update = service => {
|
|
|
146
147
|
previousResult = await readAfterWrite(req, service, { isBefore: true })
|
|
147
148
|
}
|
|
148
149
|
|
|
150
|
+
// in case of express errors in streaming do rollback
|
|
151
|
+
const segments = odataReq.getUriInfo().getPathSegments()
|
|
152
|
+
if (isStreaming(segments)) {
|
|
153
|
+
odataReq.getIncomingRequest().on('error', async err => {
|
|
154
|
+
await tx.rollback(err).catch(() => {})
|
|
155
|
+
})
|
|
156
|
+
}
|
|
149
157
|
// try UPDATE and, on 404 error, try CREATE
|
|
150
158
|
;[result, req] = await _updateThenCreate(req, odataReq, odataRes, tx)
|
|
151
159
|
|
|
@@ -187,7 +187,7 @@ class ExpressionToCQN {
|
|
|
187
187
|
// ignore
|
|
188
188
|
}
|
|
189
189
|
}
|
|
190
|
-
return { func: `${
|
|
190
|
+
return operator ? [operator, { func: `${methodName}`, args }] : { func: `${methodName}`, args }
|
|
191
191
|
}
|
|
192
192
|
|
|
193
193
|
/* eslint-disable complexity */
|
|
@@ -41,6 +41,8 @@ const { isAsteriskColumn } = require('../../../../common/utils/rewriteAsterisks'
|
|
|
41
41
|
|
|
42
42
|
const { ensureUnlocalized } = require('../../../../fiori/utils/handler')
|
|
43
43
|
|
|
44
|
+
const { skipToken: handleSkipToken } = require('../../../../../odata/utils')
|
|
45
|
+
|
|
44
46
|
const _applyOnlyContainsFilter = apply => Object.keys(apply).length === 1 && apply.filter
|
|
45
47
|
|
|
46
48
|
/**
|
|
@@ -136,9 +138,9 @@ const _apply = (uriInfo, queryOptions, entity, model) => {
|
|
|
136
138
|
}
|
|
137
139
|
|
|
138
140
|
const _topSkip = (queryOptions, target, cqn) => {
|
|
139
|
-
if (queryOptions && (queryOptions.$top || queryOptions.$skip
|
|
141
|
+
if (queryOptions && (queryOptions.$top || queryOptions.$skip)) {
|
|
140
142
|
const top = queryOptions.$top ? parseInt(queryOptions.$top) : getDefaultPageSize(target)
|
|
141
|
-
const skip = parseInt(queryOptions.$skip || 0)
|
|
143
|
+
const skip = parseInt(queryOptions.$skip || 0)
|
|
142
144
|
cqn.limit(Math.min(top, getMaxPageSize(target)), skip)
|
|
143
145
|
}
|
|
144
146
|
}
|
|
@@ -324,6 +326,10 @@ const _handleApply = (apply, select) => {
|
|
|
324
326
|
select.push(...mergedArray)
|
|
325
327
|
}
|
|
326
328
|
|
|
329
|
+
const _skipToken = (token, cqn) => {
|
|
330
|
+
handleSkipToken(token, cqn)
|
|
331
|
+
}
|
|
332
|
+
|
|
327
333
|
/**
|
|
328
334
|
* Transform odata READ request into a CQN object.
|
|
329
335
|
*
|
|
@@ -393,6 +399,9 @@ const readToCQN = (service, target, odataReq) => {
|
|
|
393
399
|
if (isCollectionOrToMany) {
|
|
394
400
|
_topSkip(queryOptions, target, cqn)
|
|
395
401
|
_orderby(uriInfo, cqn)
|
|
402
|
+
|
|
403
|
+
const skipToken = queryOptions?.$skiptoken
|
|
404
|
+
if (skipToken) _skipToken(skipToken, cqn)
|
|
396
405
|
}
|
|
397
406
|
|
|
398
407
|
if (!isCollectionOrToMany || entity._isSingleton) cqn.SELECT.one = true
|
|
@@ -166,7 +166,7 @@ class PrimitiveValueDecoder {
|
|
|
166
166
|
throw new IllegalArgumentError(
|
|
167
167
|
'Invalid value ' +
|
|
168
168
|
value +
|
|
169
|
-
' (
|
|
169
|
+
' (' +
|
|
170
170
|
typeof value +
|
|
171
171
|
'). ' +
|
|
172
172
|
'A JSON string must be specified as value for type ' +
|
|
@@ -180,7 +180,7 @@ class PrimitiveValueDecoder {
|
|
|
180
180
|
throw new IllegalArgumentError(
|
|
181
181
|
'Invalid value ' +
|
|
182
182
|
value +
|
|
183
|
-
' (
|
|
183
|
+
' (' +
|
|
184
184
|
typeof value +
|
|
185
185
|
') ' +
|
|
186
186
|
'as value for type ' +
|
|
@@ -209,7 +209,7 @@ class PrimitiveValueDecoder {
|
|
|
209
209
|
throw new IllegalArgumentError(
|
|
210
210
|
'Invalid value ' +
|
|
211
211
|
value +
|
|
212
|
-
' (
|
|
212
|
+
' (' +
|
|
213
213
|
typeof value +
|
|
214
214
|
') ' +
|
|
215
215
|
'as value for type ' +
|
|
@@ -226,7 +226,7 @@ class PrimitiveValueDecoder {
|
|
|
226
226
|
throw new IllegalArgumentError(
|
|
227
227
|
'Invalid value ' +
|
|
228
228
|
value +
|
|
229
|
-
' (
|
|
229
|
+
' (' +
|
|
230
230
|
typeof value +
|
|
231
231
|
') ' +
|
|
232
232
|
'as value for type ' +
|
|
@@ -247,7 +247,7 @@ class PrimitiveValueDecoder {
|
|
|
247
247
|
throw new IllegalArgumentError(
|
|
248
248
|
'Invalid value ' +
|
|
249
249
|
value +
|
|
250
|
-
' (
|
|
250
|
+
' (' +
|
|
251
251
|
typeof value +
|
|
252
252
|
'). A JSON string must be specified ' +
|
|
253
253
|
'as value for type ' +
|
|
@@ -262,7 +262,7 @@ class PrimitiveValueDecoder {
|
|
|
262
262
|
throw new IllegalArgumentError(
|
|
263
263
|
'Invalid value ' +
|
|
264
264
|
value +
|
|
265
|
-
' (
|
|
265
|
+
' (' +
|
|
266
266
|
typeof value +
|
|
267
267
|
') ' +
|
|
268
268
|
'as value for type ' +
|
|
@@ -311,7 +311,7 @@ class PrimitiveValueDecoder {
|
|
|
311
311
|
throw new IllegalArgumentError(
|
|
312
312
|
'Invalid value ' +
|
|
313
313
|
value +
|
|
314
|
-
' (
|
|
314
|
+
' (' +
|
|
315
315
|
typeof value +
|
|
316
316
|
'). A JSON ' +
|
|
317
317
|
kind +
|
|
@@ -339,7 +339,7 @@ class PrimitiveValueDecoder {
|
|
|
339
339
|
throw new IllegalArgumentError(
|
|
340
340
|
'Invalid value ' +
|
|
341
341
|
value +
|
|
342
|
-
' (
|
|
342
|
+
' (' +
|
|
343
343
|
typeof value +
|
|
344
344
|
') for enumeration type ' +
|
|
345
345
|
type.getFullQualifiedName() +
|