@sap/cds 7.4.2 → 7.5.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 +100 -0
- package/apis/cds.d.ts +1 -38
- package/apis/core.d.ts +21 -101
- package/apis/cqn.d.ts +18 -76
- package/apis/csn.d.ts +18 -114
- package/apis/events.d.ts +16 -123
- package/apis/internal/inference.d.ts +18 -32
- package/apis/linked.d.ts +18 -97
- package/apis/log.d.ts +19 -164
- package/apis/models.d.ts +18 -180
- package/apis/ql.d.ts +16 -323
- package/apis/reflect.d.ts +32 -0
- package/apis/server.d.ts +18 -135
- package/apis/services.d.ts +18 -380
- package/bin/cds-serve.js +5 -2
- package/bin/serve.js +7 -16
- package/lib/auth/basic-auth.js +3 -1
- package/lib/auth/ias-auth.js +62 -48
- package/lib/auth/ias-claims.js +34 -0
- package/lib/auth/index.js +55 -33
- package/lib/auth/jwt-auth.js +55 -52
- package/lib/compile/cdsc.js +2 -2
- package/lib/compile/to/edm.js +4 -4
- package/lib/compile/to/hdbtabledata.js +5 -8
- package/lib/compile/to/srvinfo.js +2 -2
- package/lib/env/cds-env.js +3 -9
- package/lib/env/cds-requires.js +16 -17
- package/lib/env/compat.js +0 -9
- package/lib/env/defaults.js +17 -6
- package/lib/i18n/localize.js +46 -42
- package/lib/index.js +6 -8
- package/lib/linked/classes.js +7 -118
- package/lib/linked/entities.js +1 -1
- package/lib/log/cds-log.js +15 -10
- package/lib/log/format/aspects/als.js +41 -0
- package/lib/log/format/aspects/cf.js +36 -0
- package/lib/log/format/json.js +96 -0
- package/lib/plugins.js +7 -3
- package/lib/req/context.js +4 -2
- package/lib/srv/cds-connect.js +3 -5
- package/lib/srv/cds-serve.js +13 -26
- package/lib/srv/factory.js +3 -3
- package/lib/srv/middlewares/index.js +0 -2
- package/lib/srv/middlewares/trace.js +2 -3
- package/lib/srv/protocols/_legacy.js +27 -30
- package/lib/srv/protocols/index.js +173 -58
- package/lib/srv/protocols/odata-v4.js +29 -16
- package/lib/srv/srv-api.js +8 -13
- package/lib/srv/srv-handlers.js +14 -14
- package/lib/utils/cds-utils.js +15 -0
- package/libx/_runtime/auth/index.js +4 -5
- package/libx/_runtime/auth/strategies/basic.js +2 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/OData.js +23 -13
- package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +6 -15
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +10 -3
- package/libx/_runtime/cds-services/adapter/odata-v4/to.js +5 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/metaInfo.js +2 -1
- package/libx/_runtime/cds-services/services/utils/columns.js +3 -9
- package/libx/_runtime/cds.js +13 -0
- package/libx/_runtime/common/composition/data.js +3 -0
- package/libx/_runtime/common/composition/delete.js +1 -1
- package/libx/_runtime/common/error/frontend.js +2 -2
- package/libx/_runtime/common/generic/auth/readOnly.js +1 -1
- package/libx/_runtime/common/generic/auth/restrictions.js +1 -1
- package/libx/_runtime/common/generic/sorting.js +4 -5
- package/libx/_runtime/common/utils/csn.js +23 -18
- package/libx/_runtime/common/utils/restrictions.js +6 -15
- package/libx/_runtime/db/generic/input.js +3 -2
- package/libx/_runtime/fiori/generic/readOverDraft.js +2 -5
- package/libx/_runtime/fiori/lean-draft.js +69 -5
- package/libx/_runtime/hana/Service.js +1 -1
- package/libx/_runtime/messaging/AMQPWebhookMessaging.js +1 -1
- package/libx/_runtime/messaging/Outbox.js +3 -8
- package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +1 -0
- package/libx/_runtime/messaging/enterprise-messaging.js +1 -1
- package/libx/_runtime/messaging/file-based.js +1 -1
- package/libx/_runtime/messaging/service.js +7 -10
- package/libx/_runtime/remote/Service.js +15 -45
- package/libx/_runtime/remote/utils/client.js +20 -33
- package/libx/_runtime/remote/utils/cloudSdkProvider.js +30 -0
- package/libx/_runtime/sqlite/Service.js +2 -2
- package/libx/odata/afterburner.js +29 -21
- package/libx/odata/cqn2odata.js +1 -1
- package/libx/odata/error.js +7 -0
- package/libx/odata/grammar.peggy +16 -20
- package/libx/odata/metadata.js +73 -78
- package/libx/odata/parser.js +1 -1
- package/libx/odata/read.js +94 -0
- package/libx/odata/result.js +91 -0
- package/libx/odata/service-document.js +31 -37
- package/libx/odata/utils.js +2 -1
- package/libx/outbox/index.js +9 -4
- package/libx/rest/RestAdapter.js +68 -67
- package/libx/rest/middleware/create.js +20 -26
- package/libx/rest/middleware/delete.js +5 -3
- package/libx/rest/middleware/error.js +2 -3
- package/libx/rest/middleware/input.js +5 -5
- package/libx/rest/middleware/operation.js +96 -41
- package/libx/rest/middleware/parse.js +4 -6
- package/libx/rest/middleware/payload.js +5 -5
- package/libx/rest/middleware/read.js +11 -17
- package/libx/rest/middleware/update.js +20 -25
- package/package.json +2 -1
- package/server.js +7 -4
- package/srv/outbox.cds +9 -10
- package/apis/env.d.ts +0 -25
- package/apis/test.d.ts +0 -81
- package/apis/utils.d.ts +0 -15
- package/lib/auth/passport-basic.js +0 -14
- package/lib/auth/passport-digest.js +0 -16
- package/lib/env/presets.js +0 -35
- package/lib/log/format/cf.js +0 -16
- package/lib/log/format/kibana.js +0 -92
- package/lib/srv/middlewares/ctx-auth.js +0 -11
- package/libx/_runtime/cds-services/adapter/rest/utils/validation-checks.js +0 -119
|
@@ -10,9 +10,9 @@ let passport, logged
|
|
|
10
10
|
// strategy initializers for lazy loading of dependencies
|
|
11
11
|
const _initializers = {
|
|
12
12
|
// REVISIT: support basic authentication?
|
|
13
|
-
basic: ({ credentials }) => {
|
|
13
|
+
basic: ({ credentials, users }) => {
|
|
14
14
|
const BasicStrategy = require('./strategies/basic')
|
|
15
|
-
passport.use(new BasicStrategy(credentials))
|
|
15
|
+
passport.use(new BasicStrategy(credentials || users))
|
|
16
16
|
},
|
|
17
17
|
dummy: () => {
|
|
18
18
|
const DummyStrategy = require('./strategies/dummy')
|
|
@@ -165,7 +165,7 @@ module.exports = (srv, options = srv.options) => {
|
|
|
165
165
|
if (config.impl) {
|
|
166
166
|
// mount custom authentication middleware
|
|
167
167
|
_mountCustomAuth(srv, app, config)
|
|
168
|
-
} else if (config.kind === 'ias-auth') {
|
|
168
|
+
} else if (config.kind === 'ias' || config.kind === 'ias-auth') {
|
|
169
169
|
// ias-auth follows the new implementation pattern for auth middlewares
|
|
170
170
|
const iasAuth = require('./strategies/ias-auth')(config)
|
|
171
171
|
if (iasAuth) app.use(iasAuth)
|
|
@@ -186,8 +186,7 @@ module.exports = (srv, options = srv.options) => {
|
|
|
186
186
|
}
|
|
187
187
|
|
|
188
188
|
const _strategy4 = config => {
|
|
189
|
-
const strategy = config.
|
|
190
|
-
if (strategy === 'mocked') return 'mock'
|
|
189
|
+
const strategy = config.kind.replace('mocked', 'mock').replace(/-auth$/, '')
|
|
191
190
|
if (strategy in _initializers) return strategy
|
|
192
191
|
process.exitCode = 1 // REVISIT: why exitCode needed?
|
|
193
192
|
throw new Error(`Authentication kind "${config.kind}" is not supported`)
|
|
@@ -8,8 +8,8 @@ const { BasicStrategy: BS } = _require('passport-http')
|
|
|
8
8
|
class BasicStrategy extends BS {
|
|
9
9
|
constructor(credentials) {
|
|
10
10
|
super(function (user, password, done) {
|
|
11
|
-
if (credentials[user] === password) {
|
|
12
|
-
done(null, new cds.User({ id: user }))
|
|
11
|
+
if ((credentials[user]?.password || credentials[user]) === password) {
|
|
12
|
+
done(null, new cds.User({ id: user, roles: credentials[user].roles || [] }))
|
|
13
13
|
} else {
|
|
14
14
|
this.fail()
|
|
15
15
|
}
|
|
@@ -29,6 +29,7 @@ const _action = require('./handlers/action')
|
|
|
29
29
|
const { normalizeError, isClientError } = require('../../../common/error/frontend')
|
|
30
30
|
const { getErrorMessage } = require('../../../common/error/utils')
|
|
31
31
|
|
|
32
|
+
// eslint-disable-next-line complexity
|
|
32
33
|
function _log(level, arg) {
|
|
33
34
|
const { params } = arg
|
|
34
35
|
|
|
@@ -46,34 +47,43 @@ function _log(level, arg) {
|
|
|
46
47
|
if (!obj.level) obj.level = arg.level
|
|
47
48
|
if (!obj.timestamp) obj.timestamp = arg.timestamp
|
|
48
49
|
|
|
49
|
-
if (level === 'error') {
|
|
50
|
-
// reduce 4xx to warning
|
|
51
|
-
if (isClientError(obj)) {
|
|
52
|
-
if (!LOG._warn) {
|
|
53
|
-
return
|
|
54
|
-
}
|
|
55
|
-
level = 'warn'
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
50
|
// replace messages in toLog with developer texts (i.e., undefined locale) (cf. req.reject() etc.)
|
|
60
51
|
const _message = obj.message
|
|
52
|
+
const _code = obj.code
|
|
61
53
|
const _details = obj.details
|
|
62
54
|
|
|
55
|
+
// REVISIT: the (ugly!) stuff re code is somewhat of a duplicate to what we do in normalizeError -> refactor with new adapters!
|
|
63
56
|
obj.message = getErrorMessage(obj)
|
|
64
|
-
if (obj.
|
|
57
|
+
if (_code) obj.code = obj.code.match?.(/^ASSERT_/) ? 400 : obj.code
|
|
58
|
+
if (_details) {
|
|
65
59
|
const details = []
|
|
60
|
+
const codes = new Set()
|
|
66
61
|
for (const d of obj.details) {
|
|
67
|
-
|
|
62
|
+
const entry = Object.assign({}, d, { message: getErrorMessage(d) })
|
|
63
|
+
if (!_code) {
|
|
64
|
+
const k = d.statusCode ? 'statusCode' : d.status ? 'status' : d.code ? 'code' : undefined
|
|
65
|
+
const v = d[k]?.match?.(/^ASSERT_/) ? 400 : d[k]
|
|
66
|
+
codes.add(v)
|
|
67
|
+
entry[k] = v
|
|
68
|
+
}
|
|
69
|
+
details.push(entry)
|
|
68
70
|
}
|
|
69
71
|
obj.details = details
|
|
72
|
+
if (!_code && codes.size === 1) {
|
|
73
|
+
const n = Number(codes.values().next().value)
|
|
74
|
+
if (!isNaN(n)) obj.code = n
|
|
75
|
+
}
|
|
70
76
|
}
|
|
71
77
|
|
|
78
|
+
// reduce 4xx to warning
|
|
79
|
+
if (level === 'error') if (isClientError(obj)) level = 'warn'
|
|
80
|
+
|
|
72
81
|
// log it
|
|
73
|
-
LOG[level](obj)
|
|
82
|
+
LOG[`_${level}`] && LOG[level](obj)
|
|
74
83
|
|
|
75
84
|
// restore
|
|
76
85
|
obj.message = _message
|
|
86
|
+
if (_code) obj.code = _code
|
|
77
87
|
if (_details) obj.details = _details
|
|
78
88
|
}
|
|
79
89
|
|
|
@@ -199,16 +199,11 @@ class ODataRequest extends cds.Request {
|
|
|
199
199
|
const that = this
|
|
200
200
|
Object.defineProperty(this._, 'shared', {
|
|
201
201
|
get() {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
if (that.context) {
|
|
208
|
-
that._shared = that.context._shared = that.context._shared || { req, res }
|
|
209
|
-
} else {
|
|
210
|
-
that._shared = that._shared || { req, res }
|
|
211
|
-
}
|
|
202
|
+
cds._logDeprecation('req._.shared is deprecated and will be removed.')
|
|
203
|
+
|
|
204
|
+
if (that.context) that._shared = that.context._shared = that.context._shared || { req, res }
|
|
205
|
+
else that._shared = that._shared || { req, res }
|
|
206
|
+
|
|
212
207
|
return that._shared
|
|
213
208
|
}
|
|
214
209
|
})
|
|
@@ -217,11 +212,7 @@ class ODataRequest extends cds.Request {
|
|
|
217
212
|
const attr = { identityZone: this.tenant }
|
|
218
213
|
Object.defineProperty(this, 'attr', {
|
|
219
214
|
get() {
|
|
220
|
-
|
|
221
|
-
LOG._warn && LOG.warn('req.attr is deprecated and will be removed.')
|
|
222
|
-
cds._deprecationWarningForAttr = true
|
|
223
|
-
}
|
|
224
|
-
|
|
215
|
+
cds._logDeprecation('req.attr is deprecated and will be removed.')
|
|
225
216
|
return attr
|
|
226
217
|
}
|
|
227
218
|
})
|
|
@@ -14,8 +14,15 @@ const { hasOmitValuesPreference } = require('../utils/omitValues')
|
|
|
14
14
|
|
|
15
15
|
const { getSapMessages } = require('../../../../common/error/frontend')
|
|
16
16
|
|
|
17
|
-
const _isUpsertAllowed =
|
|
18
|
-
return
|
|
17
|
+
const _isUpsertAllowed = req => {
|
|
18
|
+
return (
|
|
19
|
+
!(cds.env.runtime && cds.env.runtime.allow_upsert === false) &&
|
|
20
|
+
!(
|
|
21
|
+
req.target &&
|
|
22
|
+
req.target._isDraftEnabled &&
|
|
23
|
+
(!cds.env.fiori.lean_draft || (!req.data.IsActiveEntity && req.event === 'PATCH'))
|
|
24
|
+
)
|
|
25
|
+
)
|
|
19
26
|
}
|
|
20
27
|
|
|
21
28
|
const _infoForeignKeyInParent = (req, tx) => {
|
|
@@ -94,7 +101,7 @@ const _updateThenCreate = async (req, odataReq, odataRes, tx) => {
|
|
|
94
101
|
result = await tx.dispatch(req)
|
|
95
102
|
} catch (e) {
|
|
96
103
|
const is404 = e.code === 404 || e.status === 404 || e.statusCode === 404
|
|
97
|
-
if (is404 && _isUpsertAllowed(req
|
|
104
|
+
if (is404 && _isUpsertAllowed(req)) {
|
|
98
105
|
// PUT/ PATCH with if-match header means "only if already exists", i.e., no insert if not
|
|
99
106
|
if (req.headers['if-match'])
|
|
100
107
|
throw Object.assign(new Error('412'), { statusCode: 412 })
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
const { alias2ref } = require('../../../common/utils/csn') // REVISIT: eliminate that
|
|
2
1
|
const cds = require('../../../cds')
|
|
3
2
|
const OData = require('./OData')
|
|
4
3
|
|
|
4
|
+
const { alias2ref } = require('../../../common/utils/csn')
|
|
5
|
+
|
|
5
6
|
/**
|
|
6
7
|
* This is the express handler for a specific OData endpoint.
|
|
7
8
|
* Note: the same service can be served at different endpoints.
|
|
@@ -13,7 +14,9 @@ module.exports = srv => {
|
|
|
13
14
|
|
|
14
15
|
function OkraAdapter(srv, model = srv.model) {
|
|
15
16
|
const edm = cds.compile.to.edm(model, { service: srv.definition?.name || srv.name })
|
|
16
|
-
|
|
17
|
+
|
|
18
|
+
alias2ref(srv, edm)
|
|
19
|
+
|
|
17
20
|
return new OData(edm, model, srv.options).addCDSServiceToChannel(srv)
|
|
18
21
|
}
|
|
19
22
|
|
|
@@ -123,7 +123,8 @@ const _stringifyColumnsFromData = columns =>
|
|
|
123
123
|
.map(key => `${key}(${_stringifyColumnsFromData(columns[key])})`)
|
|
124
124
|
.join(',')
|
|
125
125
|
|
|
126
|
-
const _listColumns = ({ columns, data, isUpsert, returnType, event, /* express */ _req, service }) => {
|
|
126
|
+
const _listColumns = ({ columns, data, isUpsert, returnType, event, /* express */ _req, service, propertyName }) => {
|
|
127
|
+
if (columns.length === 1 && propertyName) return `/${propertyName}`
|
|
127
128
|
// query options ($select, $expand, etc) as strings
|
|
128
129
|
const queryOptions = _req.query
|
|
129
130
|
let columnsStr
|
|
@@ -114,15 +114,9 @@ const _getSearchableColumns = entity => {
|
|
|
114
114
|
const defaultSearchFilteredColumns = searchableColumns.filter(column => column[defaultSearchElementTerm])
|
|
115
115
|
|
|
116
116
|
if (defaultSearchFilteredColumns.length > 0) {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
'Annotation "@Search.defaultSearchElement" is deprecated and will be removed in an upcoming release. ' +
|
|
121
|
-
'Use "@cds.search" instead.'
|
|
122
|
-
)
|
|
123
|
-
cds._deprecationWarningForDefaultSearchElement = true
|
|
124
|
-
}
|
|
125
|
-
|
|
117
|
+
cds._logDeprecation(
|
|
118
|
+
'Annotation "@Search.defaultSearchElement" is deprecated and will be removed. Use "@cds.search" instead.'
|
|
119
|
+
)
|
|
126
120
|
return defaultSearchFilteredColumns.map(column => column.name)
|
|
127
121
|
}
|
|
128
122
|
|
package/libx/_runtime/cds.js
CHANGED
|
@@ -9,3 +9,16 @@ const { any, entity, Association } = cds.builtin.classes
|
|
|
9
9
|
cds.extend(any).with(require('./common/aspects/any'))
|
|
10
10
|
cds.extend(Association).with(require('./common/aspects/Association'))
|
|
11
11
|
cds.extend(entity).with(require('./common/aspects/entity'))
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Logs a deprecation warning once per process
|
|
15
|
+
*
|
|
16
|
+
* @param {string} msg the deprecation warning to be logged
|
|
17
|
+
*/
|
|
18
|
+
cds._logDeprecation = function (msg) {
|
|
19
|
+
if (!cds._deprecations.has(msg)) {
|
|
20
|
+
cds._deprecations.add(msg)
|
|
21
|
+
cds.log().warn(msg)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
Object.defineProperty(cds, '_deprecations', { value: new Set() })
|
|
@@ -118,6 +118,9 @@ const _parentKey = (element, key) => {
|
|
|
118
118
|
const _findWhere = (data, where) => {
|
|
119
119
|
return data.filter(entry => {
|
|
120
120
|
return Object.keys(where).every(key => {
|
|
121
|
+
if (Buffer.isBuffer(entry[key]) && Buffer.isBuffer(where[key])) {
|
|
122
|
+
return Buffer.compare(entry[key], where[key]) === 0
|
|
123
|
+
}
|
|
121
124
|
return where[key] === entry[key]
|
|
122
125
|
})
|
|
123
126
|
})
|
|
@@ -137,7 +137,7 @@ function _getStaticWhere(allBackLinks, entity1) {
|
|
|
137
137
|
|
|
138
138
|
const _addToCQNs = (cqns, subCQN, element, model, level) => {
|
|
139
139
|
// REVISIT:
|
|
140
|
-
// The compiler generates foreign-key constraints (if features.
|
|
140
|
+
// The compiler generates foreign-key constraints (if features.assert_integrity === 'db')
|
|
141
141
|
// and enables DELETE CASCADE. For these cases, the runtime doesn't need to delete compositions
|
|
142
142
|
// manually, it's done by the database itself.
|
|
143
143
|
// However, there are cases (unmanaged compositions), where this doesn't happen.
|
|
@@ -38,7 +38,7 @@ const _getFiltered = err => {
|
|
|
38
38
|
return error
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
const
|
|
41
|
+
const _rewriteError = error => {
|
|
42
42
|
let { code, message } = error
|
|
43
43
|
code = String(code || 'null')
|
|
44
44
|
|
|
@@ -66,7 +66,7 @@ const _rewriteDBError = error => {
|
|
|
66
66
|
|
|
67
67
|
const _normalize = (err, locale, formatterFn = _getFiltered) => {
|
|
68
68
|
// REVISIT: code and message rewriting
|
|
69
|
-
|
|
69
|
+
_rewriteError(err)
|
|
70
70
|
|
|
71
71
|
// message (i18n)
|
|
72
72
|
err.message = getErrorMessage(err, locale)
|
|
@@ -16,7 +16,7 @@ function handler(req) {
|
|
|
16
16
|
|
|
17
17
|
// @read-only
|
|
18
18
|
let entity = getAuthRelevantEntity(req, this.model, ['@readonly'])
|
|
19
|
-
if (cds.env.fiori.lean_draft
|
|
19
|
+
if (cds.env.fiori.lean_draft) entity = entity?.actives || entity
|
|
20
20
|
|
|
21
21
|
if (!entity || !entity['@readonly']) return
|
|
22
22
|
if (entity['@readonly'] && req.event in WRITE_EVENTS) req.reject(405, 'ENTITY_IS_READ_ONLY', [entity.name])
|
|
@@ -69,7 +69,7 @@ const _addNormalizedRestrictPerGrant = (grant, where, restrict, restricts, defin
|
|
|
69
69
|
|
|
70
70
|
const _addNormalizedRestrict = (restrict, restricts, definition) => {
|
|
71
71
|
const where = restrict.where
|
|
72
|
-
? restrict.where.replace(/\$user/g, '$user.id').replace(/\$user\.id\./g, '$user.')
|
|
72
|
+
? (restrict.where['='] || restrict.where).replace(/\$user/g, '$user.id').replace(/\$user\.id\./g, '$user.')
|
|
73
73
|
: undefined
|
|
74
74
|
|
|
75
75
|
restrict.grant = Array.isArray(restrict.grant) ? restrict.grant : [restrict.grant || '*']
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
const cds = require('../../cds')
|
|
2
|
-
const LOG = cds.log('app')
|
|
3
2
|
const { getAllKeys } = require('../../cds-services/adapter/odata-v4/odata-to-cqn/utils')
|
|
4
3
|
const { DRAFT_COLUMNS_MAP } = require('../../common/constants/draft')
|
|
5
4
|
|
|
@@ -7,10 +6,10 @@ const _getStaticOrders = req => {
|
|
|
7
6
|
const { target: entity, query } = req
|
|
8
7
|
const defaultOrders = entity['@cds.default.order'] || entity['@odata.default.order'] || []
|
|
9
8
|
|
|
10
|
-
if (
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
if (defaultOrders.length > 0) {
|
|
10
|
+
cds._logDeprecation(
|
|
11
|
+
'Annotations "@cds.default.order" and "@odata.default.order" are deprecated and will be removed.'
|
|
12
|
+
)
|
|
14
13
|
}
|
|
15
14
|
|
|
16
15
|
const ordersFromKeys = []
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const cds = require('../../cds')
|
|
2
|
+
|
|
2
3
|
const resolveStructured = require('../../common/utils/resolveStructured')
|
|
3
4
|
|
|
4
5
|
const getEtagElement = entity => {
|
|
@@ -91,7 +92,8 @@ const _initializeCache = (model, namespace) => {
|
|
|
91
92
|
}
|
|
92
93
|
|
|
93
94
|
const findCsnTargetFor = (edmName, model, namespace) => {
|
|
94
|
-
const cache =
|
|
95
|
+
const cache =
|
|
96
|
+
model._edmToCSNNameMap || Object.defineProperty(model, '_edmToCSNNameMap', { value: {} })._edmToCSNNameMap
|
|
95
97
|
const edm2csnMap = cache[namespace] || (cache[namespace] = _initializeCache(model, namespace))
|
|
96
98
|
|
|
97
99
|
if (edm2csnMap[edmName]) return edm2csnMap[edmName]
|
|
@@ -133,12 +135,6 @@ const _setAlias2ref = entity => {
|
|
|
133
135
|
return entity
|
|
134
136
|
}
|
|
135
137
|
|
|
136
|
-
function _alias2RefRest(service) {
|
|
137
|
-
for (const each of service.entities) {
|
|
138
|
-
_setAlias2ref(each)
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
138
|
const prefixForStruct = element => {
|
|
143
139
|
const prefixes = []
|
|
144
140
|
let parent = element.parent
|
|
@@ -149,19 +145,28 @@ const prefixForStruct = element => {
|
|
|
149
145
|
return prefixes.length ? prefixes.reverse().join('_') + '_' : ''
|
|
150
146
|
}
|
|
151
147
|
|
|
148
|
+
/*
|
|
149
|
+
* REVISIT:
|
|
150
|
+
* - which scenarios require this?
|
|
151
|
+
* - does it still work when serving multiple protocols?
|
|
152
|
+
* - can we cache it?
|
|
153
|
+
*/
|
|
152
154
|
function alias2ref(service, edm) {
|
|
153
155
|
if (!edm) {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
156
|
+
// REST
|
|
157
|
+
for (const each of service.entities) _setAlias2ref(each)
|
|
158
|
+
} else {
|
|
159
|
+
// OData
|
|
160
|
+
const defs = edm[service.definition.name]
|
|
161
|
+
for (const each of service.entities) {
|
|
162
|
+
const def = defs[each.name.replace(service.definition.name + '.', '').replace(/\./g, '_')]
|
|
163
|
+
if (!def || !def.$Key || def.$Key.every(ele => typeof ele === 'string')) continue
|
|
164
|
+
each._alias2ref = Object.defineProperty({}, '__2alias', { value: {}, configurable: true })
|
|
165
|
+
for (const mapping of def.$Key.filter(ele => typeof ele !== 'string')) {
|
|
166
|
+
for (const [key, value] of Object.entries(mapping)) {
|
|
167
|
+
each._alias2ref[key] = value.split('/')
|
|
168
|
+
each._alias2ref.__2alias[value] = key
|
|
169
|
+
}
|
|
165
170
|
}
|
|
166
171
|
}
|
|
167
172
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const cds = require('../../cds')
|
|
2
2
|
|
|
3
|
+
// REVISIT: This is a perfect example of the expensive-check-before-doing anti pattern
|
|
3
4
|
const containsAnyRestrictions = srv => {
|
|
4
5
|
const accessRestrictions = getAccessRestrictions(srv)
|
|
5
6
|
if (accessRestrictions.length > 1 || accessRestrictions[0] !== 'any') return true
|
|
@@ -22,23 +23,13 @@ const containsAnyRestrictions = srv => {
|
|
|
22
23
|
const getAccessRestrictions = srv => {
|
|
23
24
|
let restrictions = srv.definition['@restrict'] || srv.definition['@requires']
|
|
24
25
|
if (restrictions) {
|
|
25
|
-
if (typeof restrictions === 'string')
|
|
26
|
-
|
|
27
|
-
restrictions = restrictions
|
|
28
|
-
.map(r => (typeof r === 'string' ? r : r.to))
|
|
29
|
-
.reduce((acc, cur) => {
|
|
30
|
-
Array.isArray(cur) ? acc.push(...cur) : acc.push(cur)
|
|
31
|
-
return acc
|
|
32
|
-
}, [])
|
|
26
|
+
if (typeof restrictions === 'string') return [restrictions]
|
|
27
|
+
return restrictions.map(r => r.to || r).flat()
|
|
33
28
|
} else {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const is_mocked_auth = cds.env.requires.auth._kind === 'mocked'
|
|
38
|
-
if (restrict_all_services === false || !in_prod || is_mocked_auth) restrictions = ['any']
|
|
39
|
-
else restrictions = ['authenticated-user']
|
|
29
|
+
if (cds.env.requires.auth.restrict_all_services === false) return ['any']
|
|
30
|
+
if (process.env.NODE_ENV !== 'production') return ['any']
|
|
31
|
+
return ['authenticated-user']
|
|
40
32
|
}
|
|
41
|
-
return restrictions
|
|
42
33
|
}
|
|
43
34
|
|
|
44
35
|
module.exports = {
|
|
@@ -31,7 +31,8 @@ const _processComplexCategory = ({ row, key, val, category, req, element }) => {
|
|
|
31
31
|
|
|
32
32
|
// propagate keys
|
|
33
33
|
if (category === 'propagateForeignKeys') {
|
|
34
|
-
|
|
34
|
+
if (req.event !== 'UPSERT')
|
|
35
|
+
propagateForeignKeys(key, row, element._foreignKeys, element.isComposition, { deleteAssocs: true })
|
|
35
36
|
return
|
|
36
37
|
}
|
|
37
38
|
|
|
@@ -175,7 +176,7 @@ const _pickDraft = element => {
|
|
|
175
176
|
return { categories } // > no need to continue
|
|
176
177
|
}
|
|
177
178
|
|
|
178
|
-
if (element.default && !DRAFT_COLUMNS_MAP[element.name]) {
|
|
179
|
+
if (element.default && !DRAFT_COLUMNS_MAP[element.name] && !element.isAssociation) {
|
|
179
180
|
categories.push({ category: 'default', args: element })
|
|
180
181
|
}
|
|
181
182
|
|
|
@@ -73,7 +73,7 @@ const _shouldReadOverDraft = (req, definitions) => {
|
|
|
73
73
|
|
|
74
74
|
// for $expand requests, read over the draft if the navigation target is non-draft-enabled
|
|
75
75
|
// REVISIT: this is an interim workaround to be removed
|
|
76
|
-
if (!req.target._isDraftEnabled && SELECT.columns
|
|
76
|
+
if (!req.target._isDraftEnabled && SELECT.columns?.some(column => column.expand)) return true
|
|
77
77
|
|
|
78
78
|
// read over the draft only for navigation scenarios
|
|
79
79
|
if (fromRef.length === 1) return false
|
|
@@ -90,10 +90,7 @@ const _shouldReadOverDraft = (req, definitions) => {
|
|
|
90
90
|
if (isActiveEntityRequested(firstFromRef.where)) return false
|
|
91
91
|
|
|
92
92
|
const pathSegments = fromRef.map(path => (typeof path === 'string' ? path : path.id))
|
|
93
|
-
const excludeAssoc = assoc =>
|
|
94
|
-
if (assoc.name === 'DraftAdministrativeData' || assoc.name === 'SiblingEntity') return true
|
|
95
|
-
return false
|
|
96
|
-
}
|
|
93
|
+
const excludeAssoc = assoc => assoc.name === 'DraftAdministrativeData' || assoc.name === 'SiblingEntity'
|
|
97
94
|
|
|
98
95
|
// Read over the draft only if:
|
|
99
96
|
// - the navigation target is an association and
|
|
@@ -203,7 +203,43 @@ cds.ApplicationService.prototype.handle = async function (req) {
|
|
|
203
203
|
}
|
|
204
204
|
|
|
205
205
|
if (req.event === 'NEW' || req.event === 'CANCEL' || req.event === 'draftPrepare') {
|
|
206
|
-
if (draftParams.IsActiveEntity) req.reject(501)
|
|
206
|
+
if (draftParams.IsActiveEntity && !cds.env.fiori.bypass_draft) req.reject(501)
|
|
207
|
+
if (req.data.IsActiveEntity === true && cds.env.fiori.bypass_draft) {
|
|
208
|
+
const containsDraftRoot =
|
|
209
|
+
this.model.entities[query.INSERT.into?.ref?.[0]?.id || query.INSERT.into?.ref?.[0] || query.INSERT.into][
|
|
210
|
+
'@Common.DraftRoot.ActivationAction'
|
|
211
|
+
]
|
|
212
|
+
|
|
213
|
+
if (!containsDraftRoot) req.reject(403, 'DRAFT_MODIFICATION_ONLY_VIA_ROOT')
|
|
214
|
+
|
|
215
|
+
const isDirectAccess = typeof req.query.INSERT.into === 'string' || req.query.INSERT.into.ref?.length === 1
|
|
216
|
+
const data = Object.assign({}, req.data) // IsActiveEntity is not enumerable
|
|
217
|
+
const draftsRootRef =
|
|
218
|
+
typeof query.INSERT.into === 'string'
|
|
219
|
+
? [req.target.drafts.name]
|
|
220
|
+
: _redirectRefToDrafts([query.INSERT.into.ref[0]], this.model)
|
|
221
|
+
let rootHasDraft
|
|
222
|
+
|
|
223
|
+
// children: check root entity has no draft
|
|
224
|
+
if (!isDirectAccess) {
|
|
225
|
+
rootHasDraft = await SELECT.one([1]).from({ ref: draftsRootRef })
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// direct access and req.data contains keys: check if root has no draft with that keys
|
|
229
|
+
if (isDirectAccess && entity_keys(query._target).every(k => k in data)) {
|
|
230
|
+
const keyData = entity_keys(query._target).reduce((res, k) => {
|
|
231
|
+
res[k] = req.data[k]
|
|
232
|
+
return res
|
|
233
|
+
}, {})
|
|
234
|
+
rootHasDraft = await SELECT.one([1]).from({ ref: draftsRootRef }).where(keyData)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (rootHasDraft) req.reject(409, 'DRAFT_ALREADY_EXISTS')
|
|
238
|
+
|
|
239
|
+
const cqn = INSERT.into(query.INSERT.into).entries(data)
|
|
240
|
+
await run(cqn, { event: 'CREATE' })
|
|
241
|
+
return { ...data, IsActiveEntity: true }
|
|
242
|
+
}
|
|
207
243
|
req.target = req.target.drafts
|
|
208
244
|
|
|
209
245
|
if (query.INSERT?.into) {
|
|
@@ -244,13 +280,16 @@ cds.ApplicationService.prototype.handle = async function (req) {
|
|
|
244
280
|
|
|
245
281
|
if (req.event === 'draftActivate') {
|
|
246
282
|
LOG.debug('activate draft')
|
|
283
|
+
|
|
247
284
|
// It would be great if we'd have a SELECT ** to deeply expand the entity (along compositions), that should
|
|
248
285
|
// be implemented in expand implementation.
|
|
249
286
|
if (req.query.SELECT.from.ref.length > 1 || draftParams.IsActiveEntity !== false) {
|
|
250
287
|
req.reject(400, 'Action "draftActivate" can only be called on the root draft entity')
|
|
251
288
|
}
|
|
289
|
+
|
|
252
290
|
const targetDraft = req.target.drafts
|
|
253
291
|
const cols = expandStarStar(targetDraft)
|
|
292
|
+
|
|
254
293
|
// Use `run` (since also etags might need to be checked)
|
|
255
294
|
// REVISIT: Find a better approach (`etag` as part of CQN?)
|
|
256
295
|
const draftRef = _redirectRefToDrafts(query.SELECT.from.ref, this.model)
|
|
@@ -265,13 +304,15 @@ cds.ApplicationService.prototype.handle = async function (req) {
|
|
|
265
304
|
])
|
|
266
305
|
)
|
|
267
306
|
if (!res) req.reject(_etagValidationType ? 412 : { code: 'DRAFT_NOT_EXISTING', status: 404 })
|
|
268
|
-
if (res.DraftAdministrativeData?.InProcessByUser !== cds.context.user.id)
|
|
307
|
+
if (res.DraftAdministrativeData?.InProcessByUser !== cds.context.user.id) {
|
|
269
308
|
req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER', [res.DraftAdministrativeData?.InProcessByUser])
|
|
309
|
+
}
|
|
270
310
|
const DraftAdministrativeData_DraftUUID = res.DraftAdministrativeData_DraftUUID
|
|
271
311
|
delete res.DraftAdministrativeData_DraftUUID
|
|
272
312
|
delete res.DraftAdministrativeData
|
|
273
313
|
const HasActiveEntity = res.HasActiveEntity
|
|
274
314
|
delete res.HasActiveEntity
|
|
315
|
+
|
|
275
316
|
// First run the handlers as they might need access to DraftAdministrativeData or the draft entities
|
|
276
317
|
const result = await run(
|
|
277
318
|
HasActiveEntity
|
|
@@ -299,6 +340,7 @@ cds.ApplicationService.prototype.handle = async function (req) {
|
|
|
299
340
|
columns.push({ ref: ['DraftAdministrativeData'], expand: [{ ref: ['InProcessByUser'] }] })
|
|
300
341
|
rootQuery.SELECT.columns = columns
|
|
301
342
|
rootQuery.SELECT.one = true
|
|
343
|
+
rootQuery.SELECT.from = { ref: [query.SELECT.from.ref[0]] }
|
|
302
344
|
const root = await cds.run(rootQuery)
|
|
303
345
|
if (!root) req.reject(404)
|
|
304
346
|
if (root.DraftAdministrativeData?.InProcessByUser !== cds.context.user.id) req.reject(403)
|
|
@@ -308,7 +350,8 @@ cds.ApplicationService.prototype.handle = async function (req) {
|
|
|
308
350
|
}
|
|
309
351
|
|
|
310
352
|
if (req.event === 'PATCH') {
|
|
311
|
-
if (
|
|
353
|
+
if (!('IsActiveEntity' in draftParams)) req.reject(501)
|
|
354
|
+
|
|
312
355
|
if (draftParams.IsActiveEntity === false) {
|
|
313
356
|
LOG.debug('patch draft')
|
|
314
357
|
if (req.target?.name.endsWith('DraftAdministrativeData')) req.reject(405)
|
|
@@ -334,6 +377,28 @@ cds.ApplicationService.prototype.handle = async function (req) {
|
|
|
334
377
|
req.data.IsActiveEntity = false
|
|
335
378
|
return req.data
|
|
336
379
|
}
|
|
380
|
+
|
|
381
|
+
LOG.debug('patch active')
|
|
382
|
+
|
|
383
|
+
if (!cds.env.fiori.bypass_draft) {
|
|
384
|
+
const msg =
|
|
385
|
+
!cds.profiles?.includes('production') &&
|
|
386
|
+
'`cds.env.fiori.bypass_draft` must be enabled to support updating active instances.'
|
|
387
|
+
return req.reject(403, msg)
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const entityRef = query.UPDATE.entity.ref
|
|
391
|
+
|
|
392
|
+
if (!this.model.entities[entityRef[0].id]['@Common.DraftRoot.ActivationAction']) {
|
|
393
|
+
req.reject(403, 'DRAFT_MODIFICATION_ONLY_VIA_ROOT')
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const draftsRef = _redirectRefToDrafts(entityRef, this.model)
|
|
397
|
+
const hasDraft = !!(await SELECT.one([1]).from({ ref: [draftsRef[0]] }))
|
|
398
|
+
if (hasDraft) req.reject(409, 'DRAFT_ALREADY_EXISTS')
|
|
399
|
+
|
|
400
|
+
await run(query)
|
|
401
|
+
return req.data
|
|
337
402
|
}
|
|
338
403
|
|
|
339
404
|
if (req.event === 'STREAM' && draftParams.IsActiveEntity === false) {
|
|
@@ -345,7 +410,6 @@ cds.ApplicationService.prototype.handle = async function (req) {
|
|
|
345
410
|
}
|
|
346
411
|
|
|
347
412
|
req.query = query
|
|
348
|
-
|
|
349
413
|
return handle(req)
|
|
350
414
|
}
|
|
351
415
|
|
|
@@ -1132,7 +1196,7 @@ async function onCancel(req) {
|
|
|
1132
1196
|
if (processByUser && processByUser !== cds.context.user.id)
|
|
1133
1197
|
req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER', [processByUser])
|
|
1134
1198
|
}
|
|
1135
|
-
const queries = !draft ? [] : [this.run(DELETE.from({ ref: req.query.DELETE.from.ref }))]
|
|
1199
|
+
const queries = !draft ? [] : [this.run(DELETE.from({ ref: req.query.DELETE.from.ref }), req.data)]
|
|
1136
1200
|
if (draft && req.target['@Common.DraftRoot.ActivationAction'])
|
|
1137
1201
|
// only for draft root
|
|
1138
1202
|
queries.push(
|
|
@@ -94,7 +94,7 @@ class HanaDatabase extends DatabaseService {
|
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
_registerBeforeHandlers() {
|
|
97
|
-
this.before(['CREATE', 'UPDATE'], '*', this._input) // > has to run before rewrite
|
|
97
|
+
this.before(['CREATE', 'UPDATE', 'UPSERT'], '*', this._input) // > has to run before rewrite
|
|
98
98
|
this.before('READ', '*', search) // > has to run before rewrite
|
|
99
99
|
this.before(['CREATE', 'READ', 'UPDATE', 'DELETE', 'UPSERT'], '*', this._rewrite)
|
|
100
100
|
|
|
@@ -39,7 +39,7 @@ class AMQPWebhookMessaging extends MessagingService {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
startListening(opt = {}) {
|
|
42
|
-
if (!this._listenToAll && !this.subscribedTopics.size) return
|
|
42
|
+
if (!this._listenToAll.value && !this.subscribedTopics.size) return
|
|
43
43
|
if (!opt.doNotDeploy) {
|
|
44
44
|
const management = this.getManagement()
|
|
45
45
|
this.queued(management.createQueueAndSubscriptions.bind(management))()
|
|
@@ -1,15 +1,10 @@
|
|
|
1
1
|
const cds = require('../cds')
|
|
2
|
-
const LOG = cds.log()
|
|
3
|
-
|
|
4
|
-
let logged
|
|
5
2
|
|
|
6
3
|
module.exports = class Outbox extends cds.Service {
|
|
7
4
|
constructor(...args) {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
logged = true
|
|
12
|
-
}
|
|
5
|
+
cds._logDeprecation(
|
|
6
|
+
'Internal class `OutboxService` is deprecated and will be removed. Services are outboxable via config or `cds.outboxed()`.'
|
|
7
|
+
)
|
|
13
8
|
|
|
14
9
|
super(...args)
|
|
15
10
|
|