@sap/cds 7.7.3 → 7.8.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 +24 -1
- package/lib/auth/ias-auth.js +5 -3
- package/lib/auth/jwt-auth.js +4 -2
- package/lib/compile/cdsc.js +0 -10
- package/lib/compile/for/java.js +9 -5
- package/lib/compile/for/lean_drafts.js +1 -1
- package/lib/compile/to/edm.js +2 -1
- package/lib/compile/to/sql.js +0 -21
- package/lib/compile/to/srvinfo.js +13 -4
- package/lib/dbs/cds-deploy.js +7 -7
- package/lib/env/cds-requires.js +6 -0
- package/lib/index.js +4 -3
- package/lib/linked/classes.js +151 -88
- package/lib/linked/entities.js +27 -23
- package/lib/linked/models.js +57 -36
- package/lib/linked/types.js +42 -104
- package/lib/ql/Whereable.js +3 -3
- package/lib/req/context.js +8 -4
- package/lib/srv/protocols/hcql.js +2 -1
- package/lib/srv/protocols/http.js +7 -7
- package/lib/srv/protocols/index.js +31 -13
- package/lib/srv/protocols/odata-v4.js +79 -58
- package/lib/srv/srv-api.js +7 -6
- package/lib/srv/srv-dispatch.js +1 -12
- package/lib/srv/srv-tx.js +9 -13
- package/lib/utils/cds-utils.js +6 -5
- package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +11 -8
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +21 -12
- package/libx/_runtime/cds-services/adapter/odata-v4/to.js +5 -3
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/handlerUtils.js +3 -7
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/metaInfo.js +5 -0
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +2 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/request.js +3 -7
- package/libx/_runtime/cds-services/services/utils/columns.js +6 -3
- package/libx/_runtime/cds.js +0 -13
- package/libx/_runtime/common/generic/input.js +3 -0
- package/libx/_runtime/common/generic/sorting.js +8 -6
- package/libx/_runtime/common/i18n/messages.properties +1 -0
- package/libx/_runtime/common/utils/cqn.js +5 -0
- package/libx/_runtime/common/utils/foreignKeyPropagations.js +7 -1
- package/libx/_runtime/common/utils/keys.js +2 -2
- package/libx/_runtime/common/utils/resolveView.js +2 -1
- package/libx/_runtime/common/utils/rewriteAsterisks.js +1 -1
- package/libx/_runtime/common/utils/stream.js +0 -10
- package/libx/_runtime/common/utils/template.js +20 -35
- package/libx/_runtime/db/Service.js +5 -1
- package/libx/_runtime/db/utils/columns.js +1 -1
- package/libx/_runtime/fiori/lean-draft.js +14 -2
- package/libx/_runtime/messaging/Outbox.js +7 -5
- package/libx/_runtime/messaging/kafka.js +266 -0
- package/libx/_runtime/messaging/service.js +7 -5
- package/libx/_runtime/sqlite/convertDraftAdminPathExpression.js +1 -0
- package/libx/common/assert/validation.js +1 -1
- package/libx/odata/index.js +8 -2
- package/libx/odata/middleware/batch.js +340 -0
- package/libx/odata/middleware/create.js +43 -46
- package/libx/odata/middleware/delete.js +27 -15
- package/libx/odata/middleware/error.js +6 -5
- package/libx/odata/middleware/metadata.js +16 -15
- package/libx/odata/middleware/operation.js +107 -59
- package/libx/odata/middleware/parse.js +15 -7
- package/libx/odata/middleware/read.js +150 -24
- package/libx/odata/middleware/service-document.js +17 -6
- package/libx/odata/middleware/stream.js +34 -17
- package/libx/odata/middleware/update.js +123 -87
- package/libx/odata/parse/afterburner.js +131 -28
- package/libx/odata/parse/cqn2odata.js +1 -1
- package/libx/odata/parse/grammar.peggy +4 -5
- package/libx/odata/parse/multipartToJson.js +163 -0
- package/libx/odata/parse/parser.js +1 -1
- package/libx/odata/utils/index.js +29 -47
- package/libx/odata/utils/path.js +72 -0
- package/libx/odata/utils/result.js +123 -20
- package/package.json +1 -1
- package/server.js +4 -0
|
@@ -1,78 +1,126 @@
|
|
|
1
1
|
const cds = require('../../../')
|
|
2
2
|
|
|
3
|
-
const { toODataResult } = require('../utils/result')
|
|
4
|
-
const { cds2edm } = require('../utils')
|
|
3
|
+
const { toODataResult, postProcess } = require('../utils/result')
|
|
4
|
+
const { cds2edm, calculateLocationHeader, getKeysAndParamsFromPath, handleSapMessages } = require('../utils')
|
|
5
5
|
|
|
6
6
|
const { deepCopy } = require('../../_runtime/common/utils/copy')
|
|
7
7
|
|
|
8
8
|
// REVISIT: move to or rewrite in libx/odata
|
|
9
|
+
const { readAfterWrite } = require('../../_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite')
|
|
9
10
|
const metaInfo = require('../../_runtime/cds-services/adapter/odata-v4/utils/metaInfo')
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
let { operation, args } = req._query.SELECT.from.ref.slice(-1)[0]
|
|
13
|
-
if (!operation) return next() //> create or read
|
|
14
|
-
|
|
15
|
-
// unbound vs. bound
|
|
16
|
-
let entity
|
|
17
|
-
if (srv.model.definitions[operation]) {
|
|
18
|
-
operation = srv.model.definitions[operation]
|
|
19
|
-
} else {
|
|
20
|
-
req._query.SELECT.from.ref.pop()
|
|
21
|
-
// TODO: this does not work when navigating to the entity
|
|
22
|
-
const lastRef = req._query.SELECT.from.ref.slice(-1)[0]
|
|
23
|
-
entity = lastRef.id || lastRef
|
|
24
|
-
entity = srv.model.definitions[entity]
|
|
25
|
-
operation = entity.actions[operation]
|
|
26
|
-
}
|
|
12
|
+
const DRAFT_EVENTS = { draftActivate: 1, EDIT: 1, draftPrepare: 1 }
|
|
27
13
|
|
|
28
|
-
|
|
14
|
+
module.exports = srv =>
|
|
15
|
+
function operation(req, res, next) {
|
|
16
|
+
let { operation, args } = req._query.SELECT?.from.ref?.slice(-1)[0] || {}
|
|
17
|
+
if (!operation) return next() //> create or read
|
|
29
18
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
19
|
+
// unbound vs. bound
|
|
20
|
+
let entity, /* keys, */ params
|
|
21
|
+
if (srv.model.definitions[operation]) {
|
|
22
|
+
operation = srv.model.definitions[operation]
|
|
23
|
+
} else {
|
|
24
|
+
req._query.SELECT.from.ref.pop()
|
|
25
|
+
let cur = { elements: srv.model.definitions }
|
|
26
|
+
for (const each of req._query.SELECT.from.ref) {
|
|
27
|
+
cur = cur.elements[each.id || each]
|
|
28
|
+
if (cur._target) cur = cur._target
|
|
29
|
+
}
|
|
30
|
+
operation = cur.actions[operation]
|
|
31
|
+
entity = cur
|
|
32
|
+
const keysAndParams = getKeysAndParamsFromPath(req._query.SELECT.from, srv)
|
|
33
|
+
params = keysAndParams.params
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// payload & params
|
|
37
|
+
const data = args || deepCopy(req.body)
|
|
37
38
|
|
|
38
|
-
|
|
39
|
-
|
|
39
|
+
// assert payload
|
|
40
|
+
const assertOptions = { filter: true, http: { req }, mandatories: true }
|
|
41
|
+
const errs = cds.assert(data, operation, assertOptions)
|
|
42
|
+
if (errs) {
|
|
43
|
+
if (errs.length === 1) throw errs[0]
|
|
44
|
+
throw Object.assign(new Error('MULTIPLE_ERRORS'), { statusCode: 400, details: errs })
|
|
45
|
+
}
|
|
40
46
|
|
|
41
|
-
|
|
42
|
-
|
|
47
|
+
// event
|
|
48
|
+
// REVISIT: when is operation.name actually prefixed with the service name?
|
|
49
|
+
let event = operation.name.replace(`${srv.name}.`, '')
|
|
50
|
+
// REVISIT: rewrite draft event -> do centrally in draft impl
|
|
51
|
+
if (event === 'draftEdit') event = 'EDIT'
|
|
43
52
|
|
|
44
|
-
|
|
45
|
-
.dispatch(cdsReq)
|
|
46
|
-
.then(result => {
|
|
47
|
-
// REVISIT: result === undefined valid for modelled return type?
|
|
48
|
-
if (!operation.returns || result === undefined) return res.sendStatus(204)
|
|
53
|
+
const query = entity ? req._query : undefined
|
|
49
54
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
+
// we need a cds.Request for multiple reasons, incl. params, headers, sap-messages, read after write, ...
|
|
56
|
+
const cdsReq = new cds.Request({ query, event, data, params, target: query?.target, req, res })
|
|
57
|
+
Object.defineProperty(cdsReq, 'protocol', { value: 'odata-v4' })
|
|
58
|
+
|
|
59
|
+
// REVISIT: only via srv.run in combination with srv.dispatch inside
|
|
60
|
+
// we automatically either use a single auto-managed tx for the req (i.e., insert and read after write in same tx)
|
|
61
|
+
// or the auto-managed tx opened for the respective atomicity group, if exists
|
|
62
|
+
return srv
|
|
63
|
+
.run(() => {
|
|
64
|
+
return srv.dispatch(cdsReq).then(result => {
|
|
65
|
+
handleSapMessages(cdsReq, req, res)
|
|
66
|
+
|
|
67
|
+
// FIXME: should be handled in draft impl
|
|
68
|
+
if (event in /* { draftActivate: 1, EDIT: 1 } */ DRAFT_EVENTS /* && cdsReq._.readAfterWrite */) {
|
|
69
|
+
let columns
|
|
70
|
+
const queryOptions = req.url.split('?')[1]
|
|
71
|
+
if (queryOptions) columns = cds.odata.parse(`/X?${queryOptions}`).SELECT.columns
|
|
72
|
+
return readAfterWrite(cdsReq, srv, { operation: { result, returnType: operation.returns }, columns })
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return result
|
|
55
76
|
})
|
|
56
|
-
}
|
|
77
|
+
})
|
|
78
|
+
.then(result => {
|
|
79
|
+
// we use an extra then block, after getting the result, so the transaction is commited, before sending the response
|
|
80
|
+
if (!operation.returns || result == null) return res.status(204).end()
|
|
57
81
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
82
|
+
if (operation.returns._type?.match?.(/^cds\./)) {
|
|
83
|
+
// TODO: check result type
|
|
84
|
+
return res.set('Content-Type', 'application/json;IEEE754Compatible=true').send({
|
|
85
|
+
'@odata.context': `${entity ? '../' : ''}$metadata#${cds2edm[operation.returns._type]}`,
|
|
86
|
+
value: result
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const info = metaInfo(req._query, event, srv, result, req)
|
|
91
|
+
|
|
92
|
+
// FIXME: info.metadata.isCollection and contextUrl are incorrect for draft events
|
|
93
|
+
if (event in /* { draftActivate: 1, EDIT: 1 } */ DRAFT_EVENTS) {
|
|
94
|
+
info.metadata.isCollection = false
|
|
95
|
+
info.metadata.contextUrl += '/$entity'
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// FIXME: info.metadata.isCollection is incorrect
|
|
99
|
+
if (!operation.returns.items) info.metadata.isCollection = false
|
|
65
100
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
101
|
+
if (info.metadata.returnType) {
|
|
102
|
+
postProcess(info.metadata.returnType, srv, result)
|
|
103
|
+
if (result['$etag']) res.set('etag', result['$etag'])
|
|
104
|
+
}
|
|
70
105
|
|
|
71
|
-
|
|
72
|
-
if (entity && !result['@odata.context'].match(/^\.\.\//))
|
|
73
|
-
result['@odata.context'] = '../' + result['@odata.context']
|
|
106
|
+
result = toODataResult(result, info)
|
|
74
107
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
108
|
+
// FIXME: draftActivate needs location header -> move to draft impl
|
|
109
|
+
// FIXME: draftActivate needs HasActiveEntity and HasDraftEntity -> move to draft impl
|
|
110
|
+
if (event in /* { draftActivate: 1, EDIT: 1 } */ DRAFT_EVENTS) {
|
|
111
|
+
res.set('location', '../' + calculateLocationHeader(cdsReq.target, srv, result))
|
|
112
|
+
result.HasDraftEntity = false
|
|
113
|
+
if (event === 'draftActivate' || event === 'draftPrepare') result.HasActiveEntity = false
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// // FIXME: draftEdit needs HasDraftEntity -> move to draft impl
|
|
117
|
+
// if (event === 'EDIT') result.HasDraftEntity = false
|
|
118
|
+
|
|
119
|
+
// FIXME: toODataResult() doesn't seem to handle this case
|
|
120
|
+
if (entity && !result['@odata.context'].match(/^\.\.\//))
|
|
121
|
+
result['@odata.context'] = '../' + result['@odata.context']
|
|
122
|
+
|
|
123
|
+
res.set('Content-Type', 'application/json;IEEE754Compatible=true').send(result)
|
|
124
|
+
})
|
|
125
|
+
.catch(next)
|
|
126
|
+
}
|
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
const cds = require('../../../')
|
|
2
2
|
|
|
3
|
-
module.exports = srv =>
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
module.exports = srv =>
|
|
4
|
+
function parse(req, _, next) {
|
|
5
|
+
// REVISIT: can't we register the batch handler before the parse handler to avoid this?
|
|
6
|
+
if (req.path.startsWith('/$batch')) return next()
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
// if not a GET, use req.path instead of req.url to ignore query parameters
|
|
9
|
+
req._query = cds.odata.parse(req.method === 'GET' ? req.url : req.path, {
|
|
10
|
+
service: srv,
|
|
11
|
+
baseUrl: req.baseUrl,
|
|
12
|
+
strict: true
|
|
13
|
+
})
|
|
9
14
|
|
|
10
|
-
|
|
11
|
-
|
|
15
|
+
const preferReturn = req.headers.prefer?.match(/\W?return=(\w+)/i)
|
|
16
|
+
if (preferReturn) req._preferReturn = preferReturn[1]
|
|
17
|
+
|
|
18
|
+
next()
|
|
19
|
+
}
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
const cds = require('../../../')
|
|
2
|
-
const { toODataResult } = require('../utils/result')
|
|
2
|
+
const { toODataResult, postProcess } = require('../utils/result')
|
|
3
3
|
const querystring = require('node:querystring')
|
|
4
|
-
const {
|
|
4
|
+
const { getKeysAndParamsFromPath, handleSapMessages } = require('../utils')
|
|
5
5
|
const { handleStreamProperties } = require('../../_runtime/common/utils/streamProp')
|
|
6
6
|
|
|
7
|
-
const { getKeysFromPath } = require('../utils')
|
|
8
|
-
|
|
9
7
|
// REVISIT: move to or rewrite in libx/odata
|
|
10
8
|
const metaInfo = require('../../_runtime/cds-services/adapter/odata-v4/utils/metaInfo')
|
|
9
|
+
const { getPageSize } = require('../../_runtime/common/generic/paging')
|
|
11
10
|
|
|
12
11
|
const _getCount = result =>
|
|
13
12
|
Array.isArray(result)
|
|
@@ -17,7 +16,7 @@ const _getCount = result =>
|
|
|
17
16
|
: result.$count || result._counted_ || 0
|
|
18
17
|
|
|
19
18
|
const _calculateNextLink = (req, result) => {
|
|
20
|
-
const $skiptoken = _calculateSkiptoken(req, result)
|
|
19
|
+
const $skiptoken = result.$nextLink ?? _calculateSkiptoken(req, result)
|
|
21
20
|
if ($skiptoken) {
|
|
22
21
|
const queryParamsWithSkipToken = { ...req.http.req.query, $skiptoken }
|
|
23
22
|
// REVISIT: slice replaces leading '/'. Always starts with '/'?
|
|
@@ -65,26 +64,140 @@ const _reliablePagingPossible = req => {
|
|
|
65
64
|
)
|
|
66
65
|
}
|
|
67
66
|
|
|
67
|
+
const _checkExpandDeep = (column, entity, namespace) => {
|
|
68
|
+
const { expand } = column
|
|
69
|
+
if (expand.length > 1 || expand[0] !== '*') {
|
|
70
|
+
for (const expandColumn of expand) {
|
|
71
|
+
if (expandColumn === '*') continue
|
|
72
|
+
if (expandColumn.expand) {
|
|
73
|
+
_checkExpandDeep(expandColumn, entity.elements[expandColumn.ref[0]]._target, namespace)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (!entity.name.startsWith(namespace) && !entity._service) {
|
|
78
|
+
// proxy, only add keys
|
|
79
|
+
const asteriskIndex = column.expand.findIndex(e => e === '*')
|
|
80
|
+
column.expand.splice(asteriskIndex)
|
|
81
|
+
for (const key in entity.keys) {
|
|
82
|
+
if (entity.elements[key].isAssociation) continue
|
|
83
|
+
column.expand.push({ ref: [key] })
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const resolveProxyExpands = ({ SELECT: { columns }, target: entity }, service) => {
|
|
89
|
+
if (!columns) return
|
|
90
|
+
|
|
91
|
+
for (const column of columns) {
|
|
92
|
+
if (column.expand) {
|
|
93
|
+
_checkExpandDeep(column, entity.elements[column.ref[0]]._target, service.namespace)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const _isNullableSingleton = query => query._target._isSingleton && query._target['@odata.singleton.nullable']
|
|
99
|
+
|
|
100
|
+
const _isToOneAssoc = query =>
|
|
101
|
+
query.SELECT.from.ref.length > 1 && typeof query.SELECT.from.ref.slice(-1)[0] === 'string'
|
|
102
|
+
|
|
103
|
+
// basically stolen from old read handler without understanding it ^^
|
|
104
|
+
const _handleArrayOfQueries = (srv, req, res, next) => {
|
|
105
|
+
const info = metaInfo(req._query, 'READ', srv, {}, req, false)
|
|
106
|
+
const cdsReq = new cds.Request({ query: req._query, req, res })
|
|
107
|
+
srv
|
|
108
|
+
.dispatch(cdsReq)
|
|
109
|
+
.then(result => {
|
|
110
|
+
handleSapMessages(cdsReq, req, res)
|
|
111
|
+
|
|
112
|
+
if (req.url.match(/\/\$count/)) {
|
|
113
|
+
const count = Array.isArray(result)
|
|
114
|
+
? result.reduce((acc, val) => {
|
|
115
|
+
return (
|
|
116
|
+
acc + ((val && (val.$count || val._counted_)) || (val[0] && (val[0].$count || val[0]._counted_))) || 0
|
|
117
|
+
)
|
|
118
|
+
}, 0)
|
|
119
|
+
: result.$count || result._counted_ || 0
|
|
120
|
+
return res.set('Content-Type', 'text/plain').send(count.toString())
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const adjustedResult = []
|
|
124
|
+
if (cdsReq.query[0].SELECT.count) adjustedResult.$count = 0
|
|
125
|
+
adjustedResult.push(...result[0])
|
|
126
|
+
adjustedResult.$count += result[0].$count ? result[0].$count : 0
|
|
127
|
+
for (let i = 1; i < result.length; i++) {
|
|
128
|
+
adjustedResult.push(...result[i])
|
|
129
|
+
adjustedResult.$count += result[i].$count ? result[i].$count : 0
|
|
130
|
+
// Add OData context, if it deviates from main context
|
|
131
|
+
if (info.metadata.contextUrl !== info.metadata.additionalContextUrl[i - 1])
|
|
132
|
+
result[i].forEach(entry => (entry['@odata.context'] = info.metadata.additionalContextUrl[i - 1]))
|
|
133
|
+
}
|
|
134
|
+
result.splice(0, result.length, ...adjustedResult)
|
|
135
|
+
if (cdsReq.query[0].SELECT.count) result.$count = adjustedResult.$count || 0
|
|
136
|
+
result = toODataResult(result, info)
|
|
137
|
+
res.set('Content-Type', 'application/json;IEEE754Compatible=true').send(result)
|
|
138
|
+
})
|
|
139
|
+
.catch(next)
|
|
140
|
+
}
|
|
141
|
+
|
|
68
142
|
module.exports = srv =>
|
|
69
143
|
function read(req, res, next) {
|
|
144
|
+
// disable express etag checks
|
|
145
|
+
req.headers['cache-control'] = 'no-cache'
|
|
146
|
+
|
|
70
147
|
if (req._preferReturn) {
|
|
71
|
-
|
|
72
|
-
|
|
148
|
+
throw Object.assign(new Error(`The 'return' preference is not allowed in ${req.method} requests`), {
|
|
149
|
+
statusCode: 400
|
|
150
|
+
})
|
|
73
151
|
}
|
|
74
152
|
|
|
153
|
+
// $apply with concat -> multiple queries with special handling
|
|
154
|
+
if (Array.isArray(req._query)) return _handleArrayOfQueries(srv, req, res, next)
|
|
155
|
+
|
|
156
|
+
// REVISIT: better solution for _propertyAccess
|
|
157
|
+
let {
|
|
158
|
+
SELECT: { from },
|
|
159
|
+
target,
|
|
160
|
+
_propertyAccess
|
|
161
|
+
} = req._query
|
|
75
162
|
const { _query: query } = req
|
|
76
163
|
|
|
77
|
-
//
|
|
164
|
+
// payload & params
|
|
165
|
+
const { keys, params } = getKeysAndParamsFromPath(from, srv)
|
|
166
|
+
const data = keys //> for read and delete, we provide keys in req.data
|
|
167
|
+
|
|
168
|
+
// we need a cds.Request for multiple reasons, incl. params, headers, sap-messages, read after write, ...
|
|
169
|
+
const cdsReq = new cds.Request({ query, data, params, req, res })
|
|
170
|
+
Object.defineProperty(cdsReq, 'protocol', { value: 'odata-v4' })
|
|
171
|
+
|
|
172
|
+
// REVISIT: what is this for? some tests fail without it... we should find a better solution!
|
|
173
|
+
Object.defineProperty(query.SELECT, '_4odata', { value: true })
|
|
174
|
+
|
|
175
|
+
// do now to get meta info before the query is rewritten + to know return type
|
|
78
176
|
const info = metaInfo(query, 'READ', srv, {}, req, false)
|
|
79
177
|
|
|
178
|
+
// FIXME: wrong contextUrl for SiblingEntity
|
|
179
|
+
if (info.metadata.contextUrl.match(/\/SiblingEntity\//)) {
|
|
180
|
+
const split = info.metadata.contextUrl.split('/')
|
|
181
|
+
const i = split.findIndex(s => s === 'SiblingEntity')
|
|
182
|
+
split.splice(i, 1)
|
|
183
|
+
if (split[i - 1].match(/IsActiveEntity=false/)) {
|
|
184
|
+
split[i - 1] = split[i - 1].replace('IsActiveEntity=false', 'IsActiveEntity=true')
|
|
185
|
+
info.metadata.contextUrl = split.join('/')
|
|
186
|
+
} else {
|
|
187
|
+
info.metadata.contextUrl = split.join('/').replace(/IsActiveEntity=true/g, 'IsActiveEntity=false')
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
80
191
|
const lastPathElement = req.path.split('/').slice(-1)[0]
|
|
81
192
|
|
|
82
|
-
|
|
193
|
+
query.SELECT.columns ??= ['*']
|
|
194
|
+
|
|
195
|
+
if (cds.env.effective.odata.proxies && cds.env.effective.odata.xrefs) {
|
|
196
|
+
// REVISIT check above is still not perfect solution
|
|
197
|
+
resolveProxyExpands(query, srv)
|
|
198
|
+
}
|
|
83
199
|
|
|
84
|
-
|
|
85
|
-
const cdsReq = new cds.Request({ query })
|
|
86
|
-
// for read and delete, we provide keys in req.data
|
|
87
|
-
cdsReq.data = getKeysFromPath(query.SELECT.from, srv)
|
|
200
|
+
handleStreamProperties(target, query.SELECT.columns, srv.model)
|
|
88
201
|
|
|
89
202
|
// REVISIT: what is this for? some tests fail without it... we should find a better solution!
|
|
90
203
|
Object.defineProperty(query.SELECT, '_4odata', { value: true })
|
|
@@ -92,25 +205,38 @@ module.exports = srv =>
|
|
|
92
205
|
return srv
|
|
93
206
|
.dispatch(cdsReq)
|
|
94
207
|
.then(result => {
|
|
95
|
-
|
|
96
|
-
return res.sendStatus(204)
|
|
208
|
+
handleSapMessages(cdsReq, req, res)
|
|
97
209
|
|
|
98
|
-
if (
|
|
99
|
-
|
|
100
|
-
return res.send(result.toString())
|
|
101
|
-
} else if (lastPathElement === '$value' && query._propertyAccess) {
|
|
102
|
-
return res.send(result[query._propertyAccess].toString())
|
|
210
|
+
if (cdsReq.target._etag && result == null && cdsReq.headers['if-none-match']) {
|
|
211
|
+
return res.status(304).end()
|
|
103
212
|
}
|
|
104
213
|
|
|
105
|
-
if (
|
|
214
|
+
if (result == null) {
|
|
215
|
+
if (_isNullableSingleton(query) || _isToOneAssoc(query)) return res.sendStatus(204)
|
|
216
|
+
throw Object.assign(new Error(`Not Found`), {
|
|
217
|
+
statusCode: 404
|
|
218
|
+
})
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (_propertyAccess && result[_propertyAccess] === null) return res.sendStatus(204)
|
|
106
222
|
|
|
107
|
-
if (
|
|
223
|
+
if (lastPathElement === '$count') {
|
|
224
|
+
result = _getCount(result)
|
|
225
|
+
return res.set('Content-Type', 'text/plain').send(result.toString())
|
|
226
|
+
} else if (lastPathElement === '$value' && _propertyAccess) {
|
|
227
|
+
return res.set('Content-Type', 'text/plain').send(result[_propertyAccess].toString())
|
|
228
|
+
}
|
|
108
229
|
|
|
109
|
-
if (info.metadata.isCollection
|
|
230
|
+
if (info.metadata.isCollection) _calculateNextLink(cdsReq, result)
|
|
231
|
+
postProcess(cdsReq.target, srv, result)
|
|
232
|
+
if (result['$etag']) res.set('etag', result['$etag'])
|
|
110
233
|
result = toODataResult(result, info)
|
|
111
234
|
|
|
112
235
|
// Express interprets numbers as HTTP status codes
|
|
113
|
-
|
|
236
|
+
const isNumber = typeof result === 'number'
|
|
237
|
+
res
|
|
238
|
+
.set('Content-Type', isNumber ? 'text/plain' : 'application/json;IEEE754Compatible=true')
|
|
239
|
+
.send(isNumber ? result.toString() : result)
|
|
114
240
|
})
|
|
115
241
|
.catch(next)
|
|
116
242
|
}
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
const cds = require('../../../')
|
|
2
2
|
|
|
3
3
|
const crypto = require('crypto')
|
|
4
|
+
const metaInfo = require('../../_runtime/cds-services/adapter/odata-v4/utils/metaInfo')
|
|
4
5
|
|
|
5
6
|
const normalize_header = value => {
|
|
6
7
|
return value.split(',').map(str => str.trim())
|
|
7
8
|
}
|
|
8
9
|
|
|
9
|
-
const validate_etag = (
|
|
10
|
-
const
|
|
11
|
-
return
|
|
10
|
+
const validate_etag = (header, etag) => {
|
|
11
|
+
const normalized = normalize_header(header)
|
|
12
|
+
return normalized.includes(etag) || normalized.includes('*') || normalized.includes('"*"')
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
const generateEtag = s => {
|
|
@@ -19,13 +20,20 @@ module.exports = srv =>
|
|
|
19
20
|
function service_document(req, res) {
|
|
20
21
|
if (req.method === 'HEAD') return res.end()
|
|
21
22
|
if (req.method !== 'GET')
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
throw Object.assign(new Error(`Method ${req.method} not allowed for service document.`), {
|
|
24
|
+
statusCode: 405
|
|
24
25
|
})
|
|
25
26
|
|
|
26
27
|
const m = cds.context.model || cds.model
|
|
27
28
|
const csnService = (cds.context.model || cds.model).definitions[srv.name]
|
|
28
29
|
|
|
30
|
+
if (req.headers['if-match']) {
|
|
31
|
+
if (csnService.srvDocEtag) {
|
|
32
|
+
const valid = validate_etag(req.headers['if-match'], csnService.srvDocEtag)
|
|
33
|
+
if (!valid) return res.status(412).end()
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
29
37
|
if (req.headers['if-none-match']) {
|
|
30
38
|
if (csnService.srvDocEtag) {
|
|
31
39
|
const unchanged = validate_etag(req.headers['if-none-match'], csnService.srvDocEtag)
|
|
@@ -44,8 +52,11 @@ module.exports = srv =>
|
|
|
44
52
|
|
|
45
53
|
csnService.srvDocEtag = generateEtag(JSON.stringify(exposedEntities))
|
|
46
54
|
res.set('Etag', csnService.srvDocEtag)
|
|
55
|
+
|
|
56
|
+
const info = metaInfo({ SELECT: { from: { ref: [srv.name] } } }, 'READ', srv, {}, req, false)
|
|
57
|
+
|
|
47
58
|
return res.json({
|
|
48
|
-
'@odata.context':
|
|
59
|
+
'@odata.context': info.metadata.contextUrl,
|
|
49
60
|
'@odata.metadataEtag': csnService.srvDocEtag,
|
|
50
61
|
value: exposedEntities.map(e => {
|
|
51
62
|
const e_ = e.replace(/\./g, '_')
|
|
@@ -3,7 +3,7 @@ const { Readable } = require('node:stream')
|
|
|
3
3
|
const getError = require('../../_runtime/common/error')
|
|
4
4
|
const { getTransition } = require('../../_runtime/common/utils/resolveView')
|
|
5
5
|
const LOG = cds.log('odata')
|
|
6
|
-
const {
|
|
6
|
+
const { getKeysAndParamsFromPath, handleSapMessages } = require('../utils')
|
|
7
7
|
|
|
8
8
|
const _resolveContentProperty = (target, annotName, resolvedProp) => {
|
|
9
9
|
if (target.elements[resolvedProp]) {
|
|
@@ -25,7 +25,7 @@ const isStream = query => {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
const isStreamByDollarValue = (query, previous, last) => {
|
|
28
|
-
return query.SELECT
|
|
28
|
+
return query.SELECT?.one && last === '$value' && !(previous in query.target.elements)
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
const _addMetadataProperty = (query, property, annotName, odataName) => {
|
|
@@ -82,11 +82,7 @@ const validateStream = (req, res, result) => {
|
|
|
82
82
|
|
|
83
83
|
// Reading one entity or a property of it should yield only a result length of one.
|
|
84
84
|
if (result.length === 0 || result[0] === undefined) {
|
|
85
|
-
if (req.headers['if-none-match'])
|
|
86
|
-
// TODO this should probably end the request?
|
|
87
|
-
res.status(304)
|
|
88
|
-
return
|
|
89
|
-
}
|
|
85
|
+
if (req.headers['if-none-match']) return
|
|
90
86
|
throw getError(404)
|
|
91
87
|
}
|
|
92
88
|
|
|
@@ -122,6 +118,8 @@ const _ensureStream = stream => {
|
|
|
122
118
|
}
|
|
123
119
|
|
|
124
120
|
const normalizeStream = (result, propertyName, lastPathElement, target) => {
|
|
121
|
+
if (!result) return null
|
|
122
|
+
|
|
125
123
|
let readable = result
|
|
126
124
|
if (typeof result === 'object') {
|
|
127
125
|
if (propertyName && result[propertyName] !== undefined) {
|
|
@@ -174,6 +172,9 @@ const stream = srv =>
|
|
|
174
172
|
function streamHandler(req, res, next) {
|
|
175
173
|
const { _query: query } = req
|
|
176
174
|
|
|
175
|
+
// $apply with concat -> multiple queries with special handling -> read only, no stream?
|
|
176
|
+
if (Array.isArray(query)) return next(null, req, res)
|
|
177
|
+
|
|
177
178
|
const [previous, lastPathElement] = req.path.split('/').slice(-2)
|
|
178
179
|
const _isStreamByDollarValue = isStreamByDollarValue(query, previous, lastPathElement)
|
|
179
180
|
|
|
@@ -198,37 +199,53 @@ const stream = srv =>
|
|
|
198
199
|
if (!query.target['@cds.persistence.skip']) addStreamMetadata(query)
|
|
199
200
|
|
|
200
201
|
// we need the cds request, so we can access the modified query, which is cloned due to lean-draft, so we need to use dispatch here and pass a cds req
|
|
201
|
-
const cdsReq = new cds.Request({ query })
|
|
202
|
+
const cdsReq = new cds.Request({ query, req, res })
|
|
203
|
+
Object.defineProperty(cdsReq, 'protocol', { value: 'odata-v4' })
|
|
204
|
+
|
|
202
205
|
// for read and delete, we provide keys in req.data
|
|
203
|
-
|
|
206
|
+
// payload & params
|
|
207
|
+
const { keys } = getKeysAndParamsFromPath(query.SELECT.from, srv)
|
|
208
|
+
cdsReq.data = keys
|
|
204
209
|
|
|
205
210
|
// REVISIT: what is this for? some tests fail without it... we should find a better solution!
|
|
206
211
|
Object.defineProperty(query.SELECT, '_4odata', { value: true })
|
|
207
212
|
|
|
208
|
-
return srv
|
|
209
|
-
|
|
210
|
-
.dispatch(cdsReq)
|
|
211
|
-
|
|
213
|
+
return srv
|
|
214
|
+
.tx(() => {
|
|
215
|
+
return srv.dispatch(cdsReq).then(async result => {
|
|
216
|
+
handleSapMessages(cdsReq, req, res)
|
|
212
217
|
validateStream(req, res, result)
|
|
213
218
|
|
|
214
219
|
const stream = normalizeStream(result, query._propertyAccess, lastPathElement, query.target)
|
|
215
220
|
if (stream === null) {
|
|
216
221
|
if (req.headers['if-none-match']) {
|
|
217
|
-
|
|
222
|
+
res.status(304)
|
|
223
|
+
return
|
|
218
224
|
}
|
|
219
|
-
|
|
225
|
+
res.status(204)
|
|
226
|
+
return
|
|
220
227
|
}
|
|
221
228
|
|
|
222
229
|
setStreamingHeaders(result, res)
|
|
223
230
|
|
|
224
231
|
return new Promise((resolve, reject) => {
|
|
232
|
+
if (res.destroyed) return reject(new Error('Response is closed while streaming'))
|
|
225
233
|
stream.pipe(res)
|
|
226
234
|
stream.on('end', () => resolve(result))
|
|
227
235
|
stream.once('error', reject)
|
|
236
|
+
let finished = false
|
|
237
|
+
res.on('finish', () => {
|
|
238
|
+
finished = true
|
|
239
|
+
})
|
|
240
|
+
res.on('close', () => !finished && reject(new Error('Response is closed while streaming')))
|
|
228
241
|
})
|
|
229
242
|
})
|
|
230
|
-
|
|
231
|
-
|
|
243
|
+
})
|
|
244
|
+
.then(() => {
|
|
245
|
+
// we use an extra then block, after getting the result, so the transaction is commited, before sending the response
|
|
246
|
+
res.end()
|
|
247
|
+
})
|
|
248
|
+
.catch(next) // catch outside of transaction, so tx is rolled back automatically in case of error
|
|
232
249
|
}
|
|
233
250
|
|
|
234
251
|
module.exports = {
|