@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,9 +1,10 @@
|
|
|
1
1
|
// TODO: split into multiple files
|
|
2
2
|
|
|
3
3
|
const cds = require('../../../')
|
|
4
|
+
const _path = require('./path')
|
|
4
5
|
|
|
5
6
|
const { toBase64url } = require('../../_runtime/common/utils/binary')
|
|
6
|
-
const {
|
|
7
|
+
const { getSapMessages } = require('../../_runtime/common/error/frontend')
|
|
7
8
|
|
|
8
9
|
// copied from cds-compiler/lib/edm/edmUtils.js
|
|
9
10
|
const cds2edm = {
|
|
@@ -39,8 +40,6 @@ const cds2edm = {
|
|
|
39
40
|
// 'cds.hana.ST_GEOMETRY': 'Edm.Geometry',
|
|
40
41
|
}
|
|
41
42
|
|
|
42
|
-
const odataError = (code, message) => ({ error: { code, message } })
|
|
43
|
-
|
|
44
43
|
const getSafeNumber = inputString => {
|
|
45
44
|
if (typeof inputString !== 'string') return inputString
|
|
46
45
|
// Try to parse the input string as a floating-point number using parseFloat
|
|
@@ -211,6 +210,8 @@ const formatVal = (val, elementName, csnTarget, kind, func, literal) => {
|
|
|
211
210
|
}
|
|
212
211
|
const element = _getElement(csnTarget, elementName)
|
|
213
212
|
if (!element?.type) return typeof val === 'string' ? `'${val}'` : val
|
|
213
|
+
if ((element.type === 'cds.LargeString' || element.type === 'cds.String') && val.indexOf("'") >= 0)
|
|
214
|
+
val = val.replace(/'/g, "''")
|
|
214
215
|
return kind === 'odata-v2' ? _v2(val, element) : _v4(val, element)
|
|
215
216
|
}
|
|
216
217
|
|
|
@@ -269,49 +270,29 @@ const skipToken = (token, cqn) => {
|
|
|
269
270
|
}
|
|
270
271
|
}
|
|
271
272
|
|
|
272
|
-
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
entities.push(t)
|
|
287
|
-
cur = t.elements
|
|
288
|
-
}
|
|
289
|
-
for (let i = from.ref.length - 2; i >= 0; i--) {
|
|
290
|
-
const ref = from.ref[i]
|
|
291
|
-
if (ref.where) {
|
|
292
|
-
const relation = entities[i]._relations[from.ref[i + 1].id || from.ref[i + 1]].join('target', 'source')
|
|
293
|
-
const seg_keys = where2obj(ref.where)
|
|
294
|
-
if (relation?.[0].xpr) {
|
|
295
|
-
const join = [...relation[0].xpr]
|
|
296
|
-
while (join.length >= 3) {
|
|
297
|
-
const [left, _, right] = join.splice(0, 4)
|
|
298
|
-
if (left.ref?.[0] === 'target') {
|
|
299
|
-
if (left.ref[1] in keys) break // we already added the foreign key for the last segment
|
|
300
|
-
keys[left.ref[1]] = 'val' in right ? right.val : seg_keys[right.ref[1]]
|
|
301
|
-
} else if (right.ref?.[0] === 'target') {
|
|
302
|
-
if (right.ref[1] in keys) break // we already added the foreign key for the last segment
|
|
303
|
-
keys[right.ref[1]] = 'val' in left ? left.val : seg_keys[left.ref[1]]
|
|
304
|
-
} else {
|
|
305
|
-
// REVISIT: what to do here?
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
} else {
|
|
309
|
-
// REVISIT: what to do here?
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
}
|
|
273
|
+
const calculateLocationHeader = (target, srv, result) => {
|
|
274
|
+
const targetName = target.name.replace(`${srv.name}.`, '')
|
|
275
|
+
|
|
276
|
+
const keyValuePairs = Object.keys(target.keys).reduce((acc, key) => {
|
|
277
|
+
acc[key] = result[key]
|
|
278
|
+
return acc
|
|
279
|
+
}, {})
|
|
280
|
+
|
|
281
|
+
let keys
|
|
282
|
+
const entries = Object.entries(keyValuePairs)
|
|
283
|
+
if (entries.length === 1) {
|
|
284
|
+
keys = entries[0][1]
|
|
285
|
+
} else {
|
|
286
|
+
keys = entries.map(([key, value]) => `${key}=${value}`).join(',')
|
|
313
287
|
}
|
|
314
|
-
|
|
288
|
+
|
|
289
|
+
return `${targetName}(${keys})`
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const handleSapMessages = (cdsReq, req, res) => {
|
|
293
|
+
if (!cdsReq.messages || !cdsReq.messages.length) return
|
|
294
|
+
const msgs = getSapMessages(cdsReq.messages, req)
|
|
295
|
+
if (msgs) res.setHeader('sap-messages', msgs)
|
|
315
296
|
}
|
|
316
297
|
|
|
317
298
|
module.exports = {
|
|
@@ -319,6 +300,7 @@ module.exports = {
|
|
|
319
300
|
getSafeNumber,
|
|
320
301
|
formatVal,
|
|
321
302
|
skipToken,
|
|
322
|
-
|
|
323
|
-
|
|
303
|
+
calculateLocationHeader,
|
|
304
|
+
handleSapMessages,
|
|
305
|
+
getKeysAndParamsFromPath: _path.getKeysAndParamsFromPath
|
|
324
306
|
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
const { where2obj } = require('../../_runtime/common/utils/cqn')
|
|
2
|
+
|
|
3
|
+
const _handleXpr = (relation, keys, seg_keys) => {
|
|
4
|
+
const join = [...relation]
|
|
5
|
+
while (join.length >= 3) {
|
|
6
|
+
const [left, _, right] = join
|
|
7
|
+
|
|
8
|
+
if (left.xpr) {
|
|
9
|
+
// can be [ref = ref] or [xpr and ref = ref] and [xpr and xpr] so we will always catch xprs as left element, as it follows and/or or is first element
|
|
10
|
+
_handleXpr(left.xpr, keys, seg_keys)
|
|
11
|
+
join.splice(0, 2)
|
|
12
|
+
continue
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (left.ref?.[0] === 'target') {
|
|
16
|
+
if (left.ref[1] in keys) break // we already added the foreign key for the last segment
|
|
17
|
+
keys[left.ref[1]] = 'val' in right ? right.val : seg_keys[right.ref[1]]
|
|
18
|
+
join.splice(0, 4)
|
|
19
|
+
} else if (right.ref?.[0] === 'target') {
|
|
20
|
+
if (right.ref[1] in keys) break // we already added the foreign key for the last segment
|
|
21
|
+
keys[right.ref[1]] = 'val' in left ? left.val : seg_keys[left.ref[1]]
|
|
22
|
+
join.splice(0, 4)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// REVISIT: do we already have something like this _without using okra api_?
|
|
28
|
+
// REVISIT: should we still support process.env.CDS_FEATURES_PARAMS? probably nobody uses it...
|
|
29
|
+
const getKeysAndParamsFromPath = (from, srv) => {
|
|
30
|
+
if (!from.ref) return {}
|
|
31
|
+
|
|
32
|
+
const keys = {}
|
|
33
|
+
const params = []
|
|
34
|
+
|
|
35
|
+
// last path segment
|
|
36
|
+
if (from.ref[from.ref.length - 1].where) {
|
|
37
|
+
const seg_keys = where2obj(from.ref[from.ref.length - 1].where)
|
|
38
|
+
Object.assign(keys, seg_keys)
|
|
39
|
+
params.unshift(seg_keys.ID && Object.keys(seg_keys).length === 1 ? seg_keys.ID : seg_keys)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// previous path segments
|
|
43
|
+
if (from.ref.length > 1) {
|
|
44
|
+
const entities = []
|
|
45
|
+
let cur = srv.model.definitions
|
|
46
|
+
for (let i = 0; i < from.ref.length; i++) {
|
|
47
|
+
const id = from.ref[i].id || from.ref[i]
|
|
48
|
+
const t = cur[id]._target || cur[id]
|
|
49
|
+
entities.push(t)
|
|
50
|
+
cur = t.elements
|
|
51
|
+
}
|
|
52
|
+
for (let i = from.ref.length - 2; i >= 0; i--) {
|
|
53
|
+
const ref = from.ref[i]
|
|
54
|
+
if (ref.where) {
|
|
55
|
+
const relation = entities[i]._relations[from.ref[i + 1].id || from.ref[i + 1]].join('target', 'source')
|
|
56
|
+
const seg_keys = where2obj(ref.where)
|
|
57
|
+
if (relation?.[0].xpr) {
|
|
58
|
+
_handleXpr(relation[0].xpr, keys, seg_keys)
|
|
59
|
+
} else {
|
|
60
|
+
// REVISIT: what to do here?
|
|
61
|
+
}
|
|
62
|
+
params.unshift(seg_keys.ID && Object.keys(seg_keys).length === 1 ? seg_keys.ID : seg_keys)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return { keys, params }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
module.exports = {
|
|
71
|
+
getKeysAndParamsFromPath
|
|
72
|
+
}
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
const getTemplate = require('../../_runtime/common/utils/template')
|
|
2
|
+
const templateProcessor = require('../../_runtime/common/utils/templateProcessor')
|
|
3
|
+
|
|
1
4
|
const METADATA = {
|
|
2
5
|
$context: '@odata.context',
|
|
3
6
|
$count: '@odata.count',
|
|
@@ -22,25 +25,46 @@ const METADATA = {
|
|
|
22
25
|
$mediaEtag: '@odata.mediaEtag'
|
|
23
26
|
}
|
|
24
27
|
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
$mediaContentDispositionFilename: true,
|
|
33
|
-
$mediaContentDispositionType: true
|
|
34
|
-
}
|
|
28
|
+
const KEYSTOCLEANUP = {
|
|
29
|
+
// do not set "@odata.context" as it may be inherited of remote service
|
|
30
|
+
$context: true,
|
|
31
|
+
// REVISIT: okra doesn't support content disposition
|
|
32
|
+
$mediaContentDispositionFilename: true,
|
|
33
|
+
$mediaContentDispositionType: true
|
|
34
|
+
}
|
|
35
35
|
|
|
36
|
+
const _metadataRoot = (result, odataResult) => {
|
|
36
37
|
for (const key in METADATA) {
|
|
37
38
|
if (!(key in result)) continue
|
|
38
|
-
if (!
|
|
39
|
-
|
|
39
|
+
if (!KEYSTOCLEANUP[key]) odataResult[METADATA[key]] = result[key]
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const _metadata = (result, propertyName, odataResult) => {
|
|
44
|
+
for (const key in result) {
|
|
45
|
+
if (typeof result[key] === 'object') _metadata(result[key])
|
|
46
|
+
if (!(key in METADATA)) continue
|
|
47
|
+
if (!KEYSTOCLEANUP[key]) {
|
|
48
|
+
if (propertyName) odataResult[METADATA[key]] = result[key]
|
|
49
|
+
else result[METADATA[key]] = result[key]
|
|
40
50
|
}
|
|
41
|
-
delete result[key]
|
|
51
|
+
if (!propertyName) delete result[key]
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const _cleanupMetadata = (propertyName, result) => {
|
|
56
|
+
if (typeof result !== 'object') return odataResult
|
|
57
|
+
|
|
58
|
+
const odataResult = {}
|
|
59
|
+
if (propertyName) {
|
|
60
|
+
odataResult.value = result[propertyName]
|
|
61
|
+
} else {
|
|
62
|
+
odataResult.value = result
|
|
42
63
|
}
|
|
43
64
|
|
|
65
|
+
if (Array.isArray(result)) _metadataRoot(result, odataResult)
|
|
66
|
+
_metadata(result, propertyName, odataResult)
|
|
67
|
+
|
|
44
68
|
return odataResult
|
|
45
69
|
}
|
|
46
70
|
|
|
@@ -53,6 +77,90 @@ const _setContext = (odataResult, info, isCollection) => {
|
|
|
53
77
|
return odataResult
|
|
54
78
|
}
|
|
55
79
|
|
|
80
|
+
const _getParent = (model, name) => {
|
|
81
|
+
const target = model.definitions[name]
|
|
82
|
+
|
|
83
|
+
if (target && target.elements) {
|
|
84
|
+
for (const elementName in target.elements) {
|
|
85
|
+
const element = target.elements[elementName]
|
|
86
|
+
if (element._anchor && element._anchor._isContained) return element._anchor
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return null
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const addEtags = (row, key) => {
|
|
94
|
+
row['$etag'] = row[key].startsWith('W/') ? row[key] : `W/"${row[key]}"`
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const _processCategory = (category, elementInfo) => {
|
|
98
|
+
const { row, key } = elementInfo
|
|
99
|
+
|
|
100
|
+
switch (category) {
|
|
101
|
+
case '@odata.etag':
|
|
102
|
+
addEtags(row, key)
|
|
103
|
+
break
|
|
104
|
+
case 'stringify':
|
|
105
|
+
// REVISIT: remove once DB always returns strings
|
|
106
|
+
if (row[key] == null) return
|
|
107
|
+
row[key] = `${row[key]}`
|
|
108
|
+
break
|
|
109
|
+
// no default
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const _processorFn = () => elementInfo => {
|
|
114
|
+
const { row, key, plain } = elementInfo
|
|
115
|
+
if (typeof row !== 'object' || !Object.prototype.hasOwnProperty.call(row, key)) return
|
|
116
|
+
const categories = plain.categories
|
|
117
|
+
|
|
118
|
+
for (const category of categories) {
|
|
119
|
+
_processCategory(category, elementInfo)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const _pick = element => {
|
|
124
|
+
const categories = []
|
|
125
|
+
if (element['@odata.etag']) categories.push('@odata.etag')
|
|
126
|
+
if (element.type === 'cds.Decimal' || element.type === 'cds.Integer64') categories.push('stringify') // REVISIT: remove once DB always returns strings
|
|
127
|
+
if (categories.length) return { categories }
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const postProcess = (target, service, result, isMinimal) => {
|
|
131
|
+
const { model } = service
|
|
132
|
+
if (!target || !result || !model || !model.definitions[target.name]) return
|
|
133
|
+
|
|
134
|
+
const cacheKey = isMinimal ? 'postProcessMinimal' : 'postProcess'
|
|
135
|
+
const parent = _getParent(model, target.name)
|
|
136
|
+
const template = getTemplate(
|
|
137
|
+
cacheKey,
|
|
138
|
+
service,
|
|
139
|
+
target,
|
|
140
|
+
{ pick: _pick, ignore: isMinimal ? el => el.isAssociation : undefined },
|
|
141
|
+
parent
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
if (template.elements.size === 0) return
|
|
145
|
+
|
|
146
|
+
// normalize result to rows
|
|
147
|
+
result = result.value != null && Object.keys(result).filter(k => !k.match(/^\W/)).length === 1 ? result.value : result
|
|
148
|
+
|
|
149
|
+
if (typeof result === 'object' && result != null) {
|
|
150
|
+
const rows = Array.isArray(result) ? result : [result]
|
|
151
|
+
|
|
152
|
+
// process each row
|
|
153
|
+
const processFn = _processorFn()
|
|
154
|
+
for (const row of rows) {
|
|
155
|
+
templateProcessor({
|
|
156
|
+
processFn,
|
|
157
|
+
row,
|
|
158
|
+
template
|
|
159
|
+
})
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
56
164
|
/**
|
|
57
165
|
* Convert any result to the result object structure, which is expected of odata-v4.
|
|
58
166
|
*
|
|
@@ -72,12 +180,7 @@ const toODataResult = (result, info) => {
|
|
|
72
180
|
if (isCollection && !Array.isArray(result)) result = [result]
|
|
73
181
|
else if (!isCollection && Array.isArray(result)) result = result[0]
|
|
74
182
|
|
|
75
|
-
|
|
76
|
-
if (typeof result === 'object') {
|
|
77
|
-
if (propertyName) value = result[propertyName]
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const odataResult = _cleanupMetadata({ value }, result)
|
|
183
|
+
const odataResult = _cleanupMetadata(propertyName, result)
|
|
81
184
|
|
|
82
185
|
// REVISIT: Support exponential decimals header
|
|
83
186
|
// REVISIT: we always assume minimal metadata right now
|
|
@@ -88,4 +191,4 @@ const toODataResult = (result, info) => {
|
|
|
88
191
|
return odataResult
|
|
89
192
|
}
|
|
90
193
|
|
|
91
|
-
module.exports = { toODataResult }
|
|
194
|
+
module.exports = { toODataResult, postProcess }
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -22,6 +22,7 @@ module.exports = async function cds_server (options) {
|
|
|
22
22
|
|
|
23
23
|
// mount static resources and cors middleware
|
|
24
24
|
if (o.cors) app.use (o.cors) //> if not in prod
|
|
25
|
+
if (o.health) app.get ('/health', o.health)
|
|
25
26
|
if (o.static) app.use (express.static (o.static)) //> defaults to ./app
|
|
26
27
|
if (o.favicon) app.use ('/favicon.ico', o.favicon) //> if none in ./app
|
|
27
28
|
if (o.index) app.get ('/',o.index) //> if none in ./app
|
|
@@ -76,6 +77,9 @@ const defaults = {
|
|
|
76
77
|
}
|
|
77
78
|
next()
|
|
78
79
|
}
|
|
80
|
+
},
|
|
81
|
+
get health() {
|
|
82
|
+
return (_, res) => res.json({ status: 'UP' })
|
|
79
83
|
}
|
|
80
84
|
}
|
|
81
85
|
|