@sap/cds 9.3.1 → 9.4.2
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 +54 -3
- package/_i18n/i18n_vi.properties +113 -0
- package/_i18n/messages.properties +106 -17
- package/_i18n/messages_ar.properties +194 -0
- package/_i18n/messages_bg.properties +194 -0
- package/_i18n/messages_cs.properties +194 -0
- package/_i18n/messages_da.properties +194 -0
- package/_i18n/messages_de.properties +194 -0
- package/_i18n/messages_el.properties +194 -0
- package/_i18n/messages_en.properties +194 -0
- package/_i18n/messages_en_US_saptrc.properties +194 -0
- package/_i18n/messages_es.properties +194 -0
- package/_i18n/messages_es_MX.properties +194 -0
- package/_i18n/messages_fi.properties +194 -0
- package/_i18n/messages_fr.properties +194 -0
- package/_i18n/messages_he.properties +194 -0
- package/_i18n/messages_hr.properties +194 -0
- package/_i18n/messages_hu.properties +194 -0
- package/_i18n/messages_it.properties +194 -0
- package/_i18n/messages_ja.properties +194 -0
- package/_i18n/messages_kk.properties +194 -0
- package/_i18n/messages_ko.properties +194 -0
- package/_i18n/messages_ms.properties +194 -0
- package/_i18n/messages_nl.properties +194 -0
- package/_i18n/messages_no.properties +194 -0
- package/_i18n/messages_pl.properties +194 -0
- package/_i18n/messages_pt.properties +194 -0
- package/_i18n/messages_ro.properties +194 -0
- package/_i18n/messages_ru.properties +194 -0
- package/_i18n/messages_sh.properties +194 -0
- package/_i18n/messages_sk.properties +194 -0
- package/_i18n/messages_sl.properties +194 -0
- package/_i18n/messages_sv.properties +194 -0
- package/_i18n/messages_th.properties +194 -0
- package/_i18n/messages_tr.properties +194 -0
- package/_i18n/messages_uk.properties +194 -0
- package/_i18n/messages_vi.properties +194 -0
- package/_i18n/messages_zh_CN.properties +194 -0
- package/_i18n/messages_zh_TW.properties +194 -0
- package/bin/serve.js +9 -1
- package/common.cds +9 -1
- package/lib/compile/cds-compile.js +1 -0
- package/lib/compile/etc/properties.js +1 -0
- package/lib/compile/for/flows.js +70 -4
- package/lib/compile/for/nodejs.js +1 -1
- package/lib/compile/minify.js +84 -56
- package/lib/compile/to/csn.js +2 -0
- package/lib/compile/to/yaml.js +1 -1
- package/lib/env/cds-requires.js +3 -0
- package/lib/i18n/bundles.js +8 -1
- package/lib/i18n/files.js +5 -1
- package/lib/i18n/index.js +1 -5
- package/lib/i18n/localize.js +4 -2
- package/lib/index.js +1 -1
- package/lib/ql/SELECT.js +16 -19
- package/lib/req/validate.js +10 -5
- package/lib/srv/bindings.js +1 -1
- package/lib/srv/cds-serve.js +1 -1
- package/lib/srv/middlewares/auth/ias-auth.js +3 -2
- package/lib/srv/middlewares/auth/jwt-auth.js +3 -2
- package/lib/srv/protocols/hcql.js +8 -6
- package/lib/srv/srv-dispatch.js +4 -8
- package/lib/srv/srv-handlers.js +28 -1
- package/lib/utils/colors.js +54 -49
- package/libx/_runtime/common/generic/flows.js +79 -12
- package/libx/_runtime/fiori/lean-draft.js +10 -2
- package/libx/_runtime/messaging/common-utils/connections.js +31 -18
- package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +2 -2
- package/libx/_runtime/messaging/redis-messaging.js +1 -1
- package/libx/_runtime/ucl/Service.js +5 -5
- package/libx/http/body-parser.js +10 -1
- package/libx/odata/ODataAdapter.js +10 -7
- package/libx/odata/middleware/error.js +3 -0
- package/libx/odata/parse/afterburner.js +13 -16
- package/libx/odata/parse/multipartToJson.js +3 -1
- package/libx/rest/middleware/parse.js +1 -1
- package/package.json +1 -1
- package/server.js +1 -1
|
@@ -9,9 +9,14 @@ const TO = '@to'
|
|
|
9
9
|
const FLOW_FROM = '@flow.from'
|
|
10
10
|
const FLOW_TO = '@flow.to'
|
|
11
11
|
|
|
12
|
+
const FLOW_PREVIOUS = '$flow.previous'
|
|
13
|
+
|
|
12
14
|
function buildAllowedCondition(action, statusElementName, statusEnum) {
|
|
13
15
|
const fromList = getFrom(action)
|
|
14
|
-
const conditions = fromList.map(from =>
|
|
16
|
+
const conditions = fromList.map(from => {
|
|
17
|
+
const value = from['#'] ? (statusEnum[from['#']]?.val ?? statusEnum[from['#']]['$path'].at(-1)) : from
|
|
18
|
+
return `${statusElementName} = ${typeof value === 'string' ? `'${value}'` : value}`
|
|
19
|
+
})
|
|
15
20
|
return `(${conditions.join(' OR ')})`
|
|
16
21
|
}
|
|
17
22
|
|
|
@@ -26,14 +31,58 @@ async function checkStatus(req, action, statusElementName, statusEnum) {
|
|
|
26
31
|
const allowed = await isCurrentStatusInFrom(req, action, statusElementName, statusEnum)
|
|
27
32
|
if (!allowed) {
|
|
28
33
|
const from = getFrom(action)
|
|
29
|
-
|
|
34
|
+
const fromValues = JSON.stringify(from.flatMap(el => Object.values(el)))
|
|
35
|
+
cds.error({
|
|
30
36
|
code: 409,
|
|
31
37
|
message: from.length > 1 ? 'INVALID_FLOW_TRANSITION_MULTI' : 'INVALID_FLOW_TRANSITION_SINGLE',
|
|
32
|
-
args: [action.name, statusElementName,
|
|
38
|
+
args: [action.name, statusElementName, fromValues]
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const buildUpKeys = parentKeys => {
|
|
44
|
+
const upKeys = {}
|
|
45
|
+
for (const key in parentKeys) {
|
|
46
|
+
upKeys[`up__${key}`] = parentKeys[key]
|
|
47
|
+
}
|
|
48
|
+
return upKeys
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const updateFlowHistory = async (req, toValue, upKeys, changes, isPrevious) => {
|
|
52
|
+
if (cds.env.features.flows_history_stack && isPrevious) {
|
|
53
|
+
await DELETE.from(req.target.compositions['transitions_'].target).where({
|
|
54
|
+
timestamp: changes[changes.length - 1].timestamp
|
|
55
|
+
})
|
|
56
|
+
} else {
|
|
57
|
+
await INSERT.into(req.target.compositions['transitions_'].target).entries({
|
|
58
|
+
...upKeys,
|
|
59
|
+
status: toValue
|
|
33
60
|
})
|
|
34
61
|
}
|
|
35
62
|
}
|
|
36
63
|
|
|
64
|
+
const buildToKey = (action, statusEnum) => {
|
|
65
|
+
const to = action[TO] ?? action[FLOW_TO]
|
|
66
|
+
const toKey = to['#'] ? (statusEnum[to['#']].val ?? statusEnum[to['#']]['$path'].at(-1)) : to
|
|
67
|
+
return toKey
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const handleStatusTransitionWithHistory = async (req, statusElementName, toKey, service) => {
|
|
71
|
+
let upKeys, changes
|
|
72
|
+
upKeys = buildUpKeys(req.params[0])
|
|
73
|
+
changes = await SELECT.from(req.target.compositions['transitions_'].target)
|
|
74
|
+
.where({ ...upKeys })
|
|
75
|
+
.orderBy('timestamp asc')
|
|
76
|
+
const isPrevious = toKey['='] === FLOW_PREVIOUS
|
|
77
|
+
if (isPrevious) {
|
|
78
|
+
if (changes.length <= 1)
|
|
79
|
+
return cds.error({ code: 409, message: 'No change has been made yet, cannot transition to previous status.' })
|
|
80
|
+
toKey = changes[changes.length - 2].status
|
|
81
|
+
}
|
|
82
|
+
await service.run(UPDATE(req.subject).with({ [statusElementName]: toKey }))
|
|
83
|
+
await updateFlowHistory(req, toKey, upKeys, changes, isPrevious)
|
|
84
|
+
}
|
|
85
|
+
|
|
37
86
|
/**
|
|
38
87
|
* handler registration
|
|
39
88
|
*/
|
|
@@ -54,9 +103,10 @@ module.exports = cds.service.impl(function () {
|
|
|
54
103
|
|
|
55
104
|
let statusElement = Object.values(entity.elements).find(el => el[FLOW_STATUS])
|
|
56
105
|
if (!statusElement) {
|
|
57
|
-
cds.error(
|
|
58
|
-
|
|
59
|
-
|
|
106
|
+
cds.error({
|
|
107
|
+
code: 409,
|
|
108
|
+
message: `Entity ${entity.name} does not have a status element, but its actions have registered @flow annotations.`
|
|
109
|
+
})
|
|
60
110
|
}
|
|
61
111
|
|
|
62
112
|
let statusEnum, statusElementName
|
|
@@ -67,9 +117,10 @@ module.exports = cds.service.impl(function () {
|
|
|
67
117
|
statusEnum = statusElement._target.elements['code'].enum
|
|
68
118
|
statusElementName = statusElement.name + '_code'
|
|
69
119
|
} else {
|
|
70
|
-
cds.error(
|
|
71
|
-
|
|
72
|
-
|
|
120
|
+
cds.error({
|
|
121
|
+
code: 409,
|
|
122
|
+
message: `Status element in entity ${entity.name} is not an enum and does not have a valid target with code enum.`
|
|
123
|
+
})
|
|
73
124
|
}
|
|
74
125
|
|
|
75
126
|
entry.push({ events: fromActions, entity, statusElementName, statusEnum })
|
|
@@ -92,12 +143,28 @@ module.exports = cds.service.impl(function () {
|
|
|
92
143
|
}
|
|
93
144
|
|
|
94
145
|
for (const each of exit) {
|
|
146
|
+
async function handle_after_create(res, req) {
|
|
147
|
+
const parentKeys = Object.keys(req.target.keys)
|
|
148
|
+
const entry = {}
|
|
149
|
+
for (let i = 0; i < parentKeys.length; i++) {
|
|
150
|
+
entry[`up__${parentKeys[i]}`] = req.data[parentKeys[i]]
|
|
151
|
+
}
|
|
152
|
+
await INSERT.into(req.target.compositions['transitions_'].target).entries({
|
|
153
|
+
...entry,
|
|
154
|
+
status: res[each.statusElementName]
|
|
155
|
+
})
|
|
156
|
+
}
|
|
157
|
+
if ('transitions_' in (each.entity.compositions ?? {})) this.after('CREATE', each.entity, handle_after_create)
|
|
158
|
+
|
|
95
159
|
async function handle_exit_state(req, next) {
|
|
96
160
|
const res = await next()
|
|
97
161
|
const action = req.target.actions[req.event]
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
162
|
+
let toKey = buildToKey(action, each.statusEnum)
|
|
163
|
+
if ('transitions_' in (req.target.compositions ?? {})) {
|
|
164
|
+
await handleStatusTransitionWithHistory(req, each.statusElementName, toKey, this)
|
|
165
|
+
} else {
|
|
166
|
+
await this.run(UPDATE(req.subject).with({ [each.statusElementName]: toKey }))
|
|
167
|
+
}
|
|
101
168
|
return res
|
|
102
169
|
}
|
|
103
170
|
this.on(each.events, each.entity, handle_exit_state)
|
|
@@ -438,6 +438,8 @@ const _compileUpdatedDraftMessages = (newMessages, persistedMessages, requestDat
|
|
|
438
438
|
message.additionalTargets = message.additionalTargets?.map(t => (t.startsWith('in/') ? t.slice(3) : t))
|
|
439
439
|
delete message['@Common.additionalTargets']
|
|
440
440
|
|
|
441
|
+
if (!message.target) return acc //> silently ignore messages without target
|
|
442
|
+
|
|
441
443
|
// Handle validation messages produced during draftActivate, that went through error normalization already
|
|
442
444
|
// > We must not store pre-localized data in DraftAdministrativeData.DraftMessages
|
|
443
445
|
const messageTarget = message.target.startsWith('in/') ? message.target.slice(3) : message.target
|
|
@@ -580,7 +582,12 @@ const handle = async function (req) {
|
|
|
580
582
|
if (cds.model.definitions['DRAFT.DraftAdministrativeData'].elements.DraftMessages) {
|
|
581
583
|
if (_req.target.isDraft && (_req.event === 'UPDATE' || _req.event === 'NEW')) {
|
|
582
584
|
// Degrade all errors to messages & prevent !!req.errors into req.reject() in dispatch
|
|
583
|
-
|
|
585
|
+
const originalError = _req.error.bind(_req)
|
|
586
|
+
_req.error = (...args) => {
|
|
587
|
+
// REVISIT: re-consider target variants
|
|
588
|
+
if (args[0]?.target || args[2]) return _req._messages.add(4, ...args)
|
|
589
|
+
return originalError(...args)
|
|
590
|
+
}
|
|
584
591
|
}
|
|
585
592
|
}
|
|
586
593
|
|
|
@@ -910,10 +917,11 @@ const handle = async function (req) {
|
|
|
910
917
|
if (cds.model.definitions['DRAFT.DraftAdministrativeData'].elements.DraftMessages && _req.errors)
|
|
911
918
|
_req.on('failed', async () => {
|
|
912
919
|
const nextDraftMessages = _compileUpdatedDraftMessages(
|
|
920
|
+
// REVISIT: e._message hack for draft validation messages
|
|
913
921
|
// Errors procesed during 'failed' will have undergone error._normalize at this point
|
|
914
922
|
// > We need to revert the code - message swap _normalize includes
|
|
915
923
|
// > This is required to ensure, no localized messages are persisted and redundant localization is avoided
|
|
916
|
-
_req.errors.map(e => ({ message: e.
|
|
924
|
+
_req.errors.map(e => ({ message: e._message ?? e.message, target: e.target, args: e.args, i18n: e.i18n })),
|
|
917
925
|
persistedDraftMessages,
|
|
918
926
|
{},
|
|
919
927
|
draftRef
|
|
@@ -1,17 +1,31 @@
|
|
|
1
1
|
const { expBkfRnd: waitingTime } = require('../../common/utils/waitingTime')
|
|
2
2
|
|
|
3
|
+
const _rmHandlers = client => {
|
|
4
|
+
client.removeAllListeners('connected')
|
|
5
|
+
client.removeAllListeners('error')
|
|
6
|
+
client.removeAllListeners('disconnected')
|
|
7
|
+
}
|
|
8
|
+
|
|
3
9
|
const _connectUntilConnected = (client, LOG, x) => {
|
|
10
|
+
if (client._reconnecting) return
|
|
11
|
+
client._reconnecting = true
|
|
12
|
+
|
|
4
13
|
const _waitingTime = waitingTime(x)
|
|
5
14
|
setTimeout(() => {
|
|
6
15
|
connect(client, LOG, true)
|
|
7
16
|
.then(() => {
|
|
17
|
+
client._reconnecting = false
|
|
8
18
|
LOG._warn && LOG.warn('Reconnected to Enterprise Messaging Client')
|
|
9
19
|
})
|
|
10
|
-
.catch(
|
|
20
|
+
.catch(e => {
|
|
21
|
+
_rmHandlers(client)
|
|
22
|
+
LOG.error(e)
|
|
23
|
+
|
|
11
24
|
LOG._warn &&
|
|
12
25
|
LOG.warn(
|
|
13
26
|
`Connection to Enterprise Messaging Client lost: Reconnecting in ${Math.round(_waitingTime / 1000)} s`
|
|
14
27
|
)
|
|
28
|
+
client._reconnecting = false
|
|
15
29
|
_connectUntilConnected(client, LOG, x + 1)
|
|
16
30
|
})
|
|
17
31
|
}, _waitingTime)
|
|
@@ -19,35 +33,36 @@ const _connectUntilConnected = (client, LOG, x) => {
|
|
|
19
33
|
|
|
20
34
|
const connect = (client, LOG, keepAlive) => {
|
|
21
35
|
return new Promise((resolve, reject) => {
|
|
36
|
+
_rmHandlers(client)
|
|
37
|
+
|
|
22
38
|
client
|
|
23
39
|
.once('connected', function () {
|
|
24
|
-
|
|
40
|
+
const handleReconnection = err => {
|
|
41
|
+
if (client._reconnecting) return
|
|
25
42
|
|
|
26
|
-
|
|
27
|
-
if (LOG._error) {
|
|
43
|
+
if (err && LOG._error) {
|
|
28
44
|
err.message = 'Client error: ' + err.message
|
|
29
45
|
LOG.error(err)
|
|
30
46
|
}
|
|
31
47
|
if (keepAlive) {
|
|
32
|
-
client
|
|
33
|
-
client.removeAllListeners('connected')
|
|
48
|
+
_rmHandlers(client)
|
|
34
49
|
_connectUntilConnected(client, LOG, 0)
|
|
35
50
|
}
|
|
36
|
-
}
|
|
51
|
+
}
|
|
37
52
|
|
|
53
|
+
_rmHandlers(client)
|
|
54
|
+
client.once('error', handleReconnection)
|
|
38
55
|
if (keepAlive) {
|
|
39
|
-
client.once('disconnected',
|
|
40
|
-
client.removeAllListeners('error')
|
|
41
|
-
client.removeAllListeners('connected')
|
|
42
|
-
_connectUntilConnected(client, LOG, 0)
|
|
43
|
-
})
|
|
56
|
+
client.once('disconnected', handleReconnection)
|
|
44
57
|
}
|
|
45
58
|
|
|
46
|
-
resolve(
|
|
59
|
+
resolve(client)
|
|
47
60
|
})
|
|
48
61
|
.once('error', err => {
|
|
49
|
-
client
|
|
50
|
-
|
|
62
|
+
_rmHandlers(client)
|
|
63
|
+
const e = new Error('Connection error')
|
|
64
|
+
e.cause = err
|
|
65
|
+
reject(e)
|
|
51
66
|
})
|
|
52
67
|
|
|
53
68
|
client.connect()
|
|
@@ -56,9 +71,7 @@ const connect = (client, LOG, keepAlive) => {
|
|
|
56
71
|
|
|
57
72
|
const disconnect = client => {
|
|
58
73
|
return new Promise((resolve, reject) => {
|
|
59
|
-
client
|
|
60
|
-
client.removeAllListeners('connected')
|
|
61
|
-
client.removeAllListeners('error')
|
|
74
|
+
_rmHandlers(client)
|
|
62
75
|
|
|
63
76
|
client.once('disconnected', () => {
|
|
64
77
|
client.removeAllListeners('error')
|
|
@@ -102,8 +102,8 @@ class EndpointRegistry {
|
|
|
102
102
|
const queues = req.body && !_isAll(req.body.queues) && req.body.queues
|
|
103
103
|
const options = { wipeData: req.body && req.body.wipeData }
|
|
104
104
|
|
|
105
|
-
if (tenants && !Array.isArray(tenants)) res.
|
|
106
|
-
if (queues && !Array.isArray(queues)) res.
|
|
105
|
+
if (tenants && !Array.isArray(tenants)) res.status(400).send('Request parameter `tenants` must be an array.')
|
|
106
|
+
if (queues && !Array.isArray(queues)) res.status(400).send('Request parameter `queues` must be an array.')
|
|
107
107
|
|
|
108
108
|
const tenantInfo = tenants ? await Promise.all(tenants.map(t => getTenantInfo(t))) : await getTenantInfo()
|
|
109
109
|
|
|
@@ -82,8 +82,8 @@ module.exports = class UCLService extends cds.Service {
|
|
|
82
82
|
.map(token => token.trim())
|
|
83
83
|
.every(token => reqClientCertSubjectTokens.includes(token))
|
|
84
84
|
if (!matchesUclInfoSubject) {
|
|
85
|
-
LOG.debug('Received Request Subject Info:
|
|
86
|
-
LOG.debug('Expected UCL Subject Info:
|
|
85
|
+
LOG.debug('Received Request Subject Info:', reqClientCertSubject)
|
|
86
|
+
LOG.debug('Expected UCL Subject Info:', trustedCertSubject)
|
|
87
87
|
throw new cds.error(401, 'Received .cert subject does not match trusted UCL info subject')
|
|
88
88
|
}
|
|
89
89
|
const matchesUclInfoIssuer = trustedCertIssuer
|
|
@@ -91,8 +91,8 @@ module.exports = class UCLService extends cds.Service {
|
|
|
91
91
|
.map(token => token.trim())
|
|
92
92
|
.every(token => reqClientCertIssuerTokens.includes(token))
|
|
93
93
|
if (!matchesUclInfoIssuer) {
|
|
94
|
-
LOG.debug('Received Request Issuer Info:
|
|
95
|
-
LOG.debug('Expected UCL Issuer Info:
|
|
94
|
+
LOG.debug('Received Request Issuer Info:', reqClientCertIssuer)
|
|
95
|
+
LOG.debug('Expected UCL Issuer Info:', trustedCertIssuer)
|
|
96
96
|
throw new cds.error(401, 'Received .cert issuer does not match trusted UCL info issuer')
|
|
97
97
|
}
|
|
98
98
|
}
|
|
@@ -104,7 +104,7 @@ module.exports = class UCLService extends cds.Service {
|
|
|
104
104
|
if (req.headers.location)
|
|
105
105
|
throw new cds.error(400, 'Location header found in tenant mapping notification: Async flow not supported!')
|
|
106
106
|
|
|
107
|
-
LOG.debug('Tenant mapping notification:
|
|
107
|
+
LOG.debug('Tenant mapping notification:', req.data)
|
|
108
108
|
|
|
109
109
|
const { operation } = req.data?.context ?? {}
|
|
110
110
|
if (operation !== 'assign' && operation !== 'unassign')
|
package/libx/http/body-parser.js
CHANGED
|
@@ -3,6 +3,8 @@ const express = require('express')
|
|
|
3
3
|
// basically express.json() with string representation of body stored in req._raw for recovery
|
|
4
4
|
// REVISIT: why do we need our own body parser? Only because of req._raw?
|
|
5
5
|
module.exports = function bodyParser4(adapter, options = {}) {
|
|
6
|
+
const express5 = !express.application.del
|
|
7
|
+
|
|
6
8
|
Object.assign(options, adapter.body_parser_options)
|
|
7
9
|
options.type ??= 'json' // REVISIT: why do we need to override type here?
|
|
8
10
|
const textParser = express.text(options)
|
|
@@ -13,8 +15,15 @@ module.exports = function bodyParser4(adapter, options = {}) {
|
|
|
13
15
|
return next()
|
|
14
16
|
}
|
|
15
17
|
textParser(req, res, function http_body_parser_next(err) {
|
|
18
|
+
// REVISIT: content-length > 0 but empty body is not an error with express^5
|
|
19
|
+
if (!err && express5 && !req.body && req.headers['content-length'] > 0) {
|
|
20
|
+
err = new Error('request aborted')
|
|
21
|
+
err.code = 'ECONNABORTED'
|
|
22
|
+
err.status = err.statusCode = 400
|
|
23
|
+
}
|
|
24
|
+
|
|
16
25
|
if (err) return next(Object.assign(err, { statusCode: 400 }))
|
|
17
|
-
if (typeof req.body !== 'string') return next()
|
|
26
|
+
if (typeof req.body !== 'string' && req.body !== undefined) return next()
|
|
18
27
|
|
|
19
28
|
req._raw = req.body || '{}'
|
|
20
29
|
try {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
const cds = require('../../lib')
|
|
2
2
|
const LOG = cds.log('odata')
|
|
3
3
|
|
|
4
|
+
const express = require('express')
|
|
5
|
+
|
|
4
6
|
const HttpAdapter = require('../../lib/srv/protocols/http')
|
|
5
7
|
const HttpRequest = require('../http/HttpRequest')
|
|
6
8
|
const bodyParser4 = require('../http/body-parser')
|
|
@@ -32,6 +34,7 @@ module.exports = class ODataAdapter extends HttpAdapter {
|
|
|
32
34
|
next()
|
|
33
35
|
}
|
|
34
36
|
|
|
37
|
+
const jsonBodyParser = bodyParser4(this)
|
|
35
38
|
function validate_representation_headers(req, res, next) {
|
|
36
39
|
if (req.method === 'PUT' && isStream(req._query)) {
|
|
37
40
|
req.body = { value: req }
|
|
@@ -69,7 +72,7 @@ module.exports = class ODataAdapter extends HttpAdapter {
|
|
|
69
72
|
next(operation ? { code: 405 } : undefined)
|
|
70
73
|
}
|
|
71
74
|
|
|
72
|
-
const
|
|
75
|
+
const wildcard = express.application.del ? '*' : '{*splat}'
|
|
73
76
|
return (
|
|
74
77
|
super.router
|
|
75
78
|
.use(set_odata_version)
|
|
@@ -83,13 +86,13 @@ module.exports = class ODataAdapter extends HttpAdapter {
|
|
|
83
86
|
// .all is used deliberately instead of .use so that the matched path is not stripped from req properties
|
|
84
87
|
.all('/\\$batch', require('./middleware/batch')(this))
|
|
85
88
|
// handle
|
|
86
|
-
.head(
|
|
87
|
-
.post(
|
|
88
|
-
.get(
|
|
89
|
+
.head(wildcard, (_, res) => res.sendStatus(405))
|
|
90
|
+
.post(wildcard, operation4(this), create4(this))
|
|
91
|
+
.get(wildcard, operation4(this), stream4(this), read4(this))
|
|
89
92
|
.use(validate_operation_http_method)
|
|
90
|
-
.put(
|
|
91
|
-
.patch(
|
|
92
|
-
.delete(
|
|
93
|
+
.put(wildcard, update4(this), create4(this, 'upsert'))
|
|
94
|
+
.patch(wildcard, update4(this), create4(this, 'upsert'))
|
|
95
|
+
.delete(wildcard, delete4(this))
|
|
93
96
|
// error
|
|
94
97
|
.use(error4(this))
|
|
95
98
|
)
|
|
@@ -60,6 +60,8 @@ const _normalize = (err, req, keep,
|
|
|
60
60
|
const msg = i18n.messages.at (key, '', err.args) // lookup messages for log output from factory default texts
|
|
61
61
|
if (msg && msg !== key) {
|
|
62
62
|
if (typeof err.code !== 'string') err.code = key
|
|
63
|
+
// REVISIT: e._message hack for draft validation messages
|
|
64
|
+
Object.defineProperty(err, '_message', { value: err.message })
|
|
63
65
|
err.message = msg
|
|
64
66
|
}
|
|
65
67
|
if (typeof err.code !== 'string') err.code = String(err.code ?? status ?? '')
|
|
@@ -77,6 +79,7 @@ const _normalize = (err, req, keep,
|
|
|
77
79
|
if (!that.message) that.message = this.message
|
|
78
80
|
return that
|
|
79
81
|
}})
|
|
82
|
+
|
|
80
83
|
return status
|
|
81
84
|
}
|
|
82
85
|
|
|
@@ -650,11 +650,12 @@ function _processColumns(cqn, target, protocol) {
|
|
|
650
650
|
}
|
|
651
651
|
const prefixRef = processedColumnRef.slice(0, processedColumnRef.length - 1)
|
|
652
652
|
const aggregatedElementRef = [...prefixRef, aggregatedPropertyName]
|
|
653
|
-
const isCurrencyCodeOrUnitOfMeasure = !!(aggregatedElement[SMTCS_CC] || aggregatedElement[SMTCS_UOM])
|
|
654
653
|
processedColumn.as = processedColumn.as || aggregatedPropertyName
|
|
655
654
|
|
|
656
|
-
|
|
655
|
+
const isCurrencyCodeOrUnitOfMeasure =
|
|
656
|
+
aggregatedElement && !!(aggregatedElement[SMTCS_CC] || aggregatedElement[SMTCS_UOM])
|
|
657
657
|
if (isCurrencyCodeOrUnitOfMeasure) {
|
|
658
|
+
// Specifically handle aggregating semantic amounts
|
|
658
659
|
columns[i] = {
|
|
659
660
|
xpr: [
|
|
660
661
|
'case',
|
|
@@ -772,13 +773,13 @@ function _validateXpr(xpr, target, isOne, model, aliases = []) {
|
|
|
772
773
|
const ignoredColumns = Object.values(target?.elements ?? {})
|
|
773
774
|
.filter(element => element['@cds.api.ignore'] && !element.isAssociation)
|
|
774
775
|
.map(element => element.name)
|
|
775
|
-
const
|
|
776
|
+
const newFoundAliases = []
|
|
776
777
|
|
|
777
778
|
for (const x of xpr) {
|
|
778
|
-
if (x.as)
|
|
779
|
+
if (x.as) newFoundAliases.push(x.as)
|
|
779
780
|
|
|
780
781
|
if (x.xpr) {
|
|
781
|
-
_validateXpr(x.xpr, target, isOne, model)
|
|
782
|
+
_validateXpr(x.xpr, target, isOne, model, aliases)
|
|
782
783
|
continue
|
|
783
784
|
}
|
|
784
785
|
|
|
@@ -793,13 +794,11 @@ function _validateXpr(xpr, target, isOne, model, aliases = []) {
|
|
|
793
794
|
_validateXpr(x.ref[0].where, element._target ?? element.items, isOne, model)
|
|
794
795
|
}
|
|
795
796
|
|
|
796
|
-
if (!target?.elements) {
|
|
797
|
-
_doesNotExistError(
|
|
797
|
+
if (ignoredColumns.includes(refName) || (!target?.elements?.[refName] && !aliases.includes(refName))) {
|
|
798
|
+
_doesNotExistError(x.expand, refName, target.name, target.kind)
|
|
798
799
|
}
|
|
799
800
|
|
|
800
|
-
if (
|
|
801
|
-
_doesNotExistError(x.expand, refName, target.name)
|
|
802
|
-
} else if (x.ref.length > 1) {
|
|
801
|
+
if (x.ref.length > 1) {
|
|
803
802
|
const element = target.elements[refName]
|
|
804
803
|
if (element.isAssociation) {
|
|
805
804
|
// navigation
|
|
@@ -833,7 +832,7 @@ function _validateXpr(xpr, target, isOne, model, aliases = []) {
|
|
|
833
832
|
}
|
|
834
833
|
|
|
835
834
|
if (x.func) {
|
|
836
|
-
_validateXpr(x.args, target, isOne, model)
|
|
835
|
+
_validateXpr(x.args, target, isOne, model, aliases)
|
|
837
836
|
continue
|
|
838
837
|
}
|
|
839
838
|
|
|
@@ -843,7 +842,7 @@ function _validateXpr(xpr, target, isOne, model, aliases = []) {
|
|
|
843
842
|
}
|
|
844
843
|
}
|
|
845
844
|
|
|
846
|
-
return
|
|
845
|
+
return newFoundAliases
|
|
847
846
|
}
|
|
848
847
|
|
|
849
848
|
function _validateQuery(SELECT, target, isOne, model) {
|
|
@@ -851,12 +850,10 @@ function _validateQuery(SELECT, target, isOne, model) {
|
|
|
851
850
|
|
|
852
851
|
if (SELECT.from.SELECT) {
|
|
853
852
|
const { target } = targetFromPath(SELECT.from.SELECT.from, model)
|
|
854
|
-
|
|
855
|
-
aliases.push(...subselectAliases)
|
|
853
|
+
aliases.push(..._validateQuery(SELECT.from.SELECT, target, SELECT.from.SELECT.one, model))
|
|
856
854
|
}
|
|
857
855
|
|
|
858
|
-
|
|
859
|
-
aliases.push(...columnAliases)
|
|
856
|
+
aliases.push(..._validateXpr(SELECT.columns, target, isOne, model, aliases))
|
|
860
857
|
|
|
861
858
|
_validateXpr(SELECT.orderBy, target, isOne, model, aliases)
|
|
862
859
|
_validateXpr(SELECT.where, target, isOne, model, aliases)
|
|
@@ -117,7 +117,9 @@ const _parseStream = async function* (body, boundary) {
|
|
|
117
117
|
.replace(/^--(.*)$/gm, (_, g) => `HEAD /${g} HTTP/1.1${g.slice(-2) === '--' ? CRLF : ''}`)
|
|
118
118
|
// correct content-length for non-HEAD requests is inserted below
|
|
119
119
|
.replace(/content-length: \d+\r\n/gim, '') // if content-length is given it should be taken
|
|
120
|
-
.
|
|
120
|
+
.replaceAll(/^(?:(GET|PUT|POST|PATCH|DELETE)).*(?:\r?\n(?!\r?\n).*)*/gim, block => {
|
|
121
|
+
return block.replaceAll(/ \$/g, ' /$')
|
|
122
|
+
})
|
|
121
123
|
|
|
122
124
|
// HACKS!!!
|
|
123
125
|
// ensure URLs start with slashes
|
|
@@ -120,7 +120,7 @@ exports.parse = function (req, res, next) {
|
|
|
120
120
|
if (operation && (operation.kind === 'action' || operation.kind === 'function') && !operation.params) {
|
|
121
121
|
req._data = {}
|
|
122
122
|
} else {
|
|
123
|
-
const payload = args || req.body
|
|
123
|
+
const payload = args || req.body || {}
|
|
124
124
|
if (!operation) Object.assign(payload, keys)
|
|
125
125
|
preProcessData(payload, service, definition)
|
|
126
126
|
req._data = payload
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -48,7 +48,7 @@ module.exports = async function cds_server (options) {
|
|
|
48
48
|
if (o.index) app.get ('/',o.index) //> if none in ./app
|
|
49
49
|
|
|
50
50
|
// load and prepare models
|
|
51
|
-
const csn = await cds.load(o.from||'*',o)
|
|
51
|
+
const csn = await cds.load(o.from||'*',o); o.from = false
|
|
52
52
|
cds.edmxs = cds.compile.to.edmx.files (csn)
|
|
53
53
|
cds.model = cds.compile.for.nodejs (csn)
|
|
54
54
|
|