@sap/cds 7.8.2 → 7.9.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 +45 -0
- package/_i18n/i18n_ar.properties +3 -0
- package/_i18n/i18n_cs.properties +3 -0
- package/_i18n/i18n_da.properties +3 -0
- package/_i18n/i18n_es_MX.properties +3 -0
- package/_i18n/i18n_fi.properties +3 -0
- package/_i18n/i18n_hu.properties +6 -0
- package/_i18n/i18n_ko.properties +3 -0
- package/_i18n/i18n_ms.properties +3 -0
- package/_i18n/i18n_nl.properties +3 -0
- package/_i18n/i18n_no.properties +3 -0
- package/_i18n/i18n_ro.properties +3 -0
- package/_i18n/i18n_sv.properties +3 -0
- package/_i18n/i18n_th.properties +3 -0
- package/_i18n/i18n_tr.properties +6 -0
- package/_i18n/i18n_zh_TW.properties +3 -0
- package/bin/serve.js +5 -5
- package/lib/auth/basic-auth.js +1 -1
- package/lib/compile/cdsc.js +33 -6
- package/lib/compile/etc/_localized.js +14 -7
- package/lib/compile/for/lean_drafts.js +9 -0
- package/lib/compile/to/edm-files.js +116 -0
- package/lib/compile/to/edm.js +8 -1
- package/lib/compile/to/hdbtabledata.js +3 -3
- package/lib/compile/to/sql.js +4 -2
- package/lib/compile/to/srvinfo.js +6 -5
- package/lib/compile/to/yaml.js +22 -21
- package/lib/dbs/cds-deploy.js +5 -6
- package/lib/env/cds-env.js +7 -0
- package/lib/env/cds-requires.js +20 -1
- package/lib/env/defaults.js +21 -5
- package/lib/env/schemas/cds-package.js +1 -1
- package/lib/env/schemas/cds-rc.js +85 -4
- package/lib/index.js +1 -1
- package/lib/linked/entities.js +10 -0
- package/lib/linked/models.js +1 -1
- package/lib/plugins.js +1 -1
- package/lib/ql/INSERT.js +17 -3
- package/lib/ql/Query.js +4 -0
- package/lib/ql/infer.js +1 -1
- package/lib/req/request.js +1 -1
- package/lib/srv/cds-serve.js +1 -0
- package/lib/srv/middlewares/cds-context.js +1 -1
- package/lib/srv/protocols/odata-v4.js +5 -6
- package/lib/srv/srv-models.js +9 -2
- package/lib/utils/cds-test.js +2 -0
- package/lib/utils/cds-utils.js +9 -4
- package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/create.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/delete.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +3 -6
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +22 -10
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +4 -4
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +4 -3
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/ExpressionToCQN.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/applyToCQN.js +4 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/expandToCQN.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/index.js +38 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/readToCQN.js +2 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/to.js +32 -21
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/data.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/metaInfo.js +1 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +0 -10
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/request.js +3 -1
- package/libx/_runtime/cds-services/services/utils/compareJson.js +2 -274
- package/libx/_runtime/{cds-services/services → common}/Service.js +39 -29
- package/libx/_runtime/common/generic/auth/autoexpose.js +41 -0
- package/libx/_runtime/common/generic/auth/index.js +2 -0
- package/libx/_runtime/common/generic/auth/readOnly.js +0 -11
- package/libx/_runtime/common/generic/auth/restrict.js +6 -5
- package/libx/_runtime/common/generic/auth/utils.js +1 -1
- package/libx/_runtime/common/generic/crud.js +5 -8
- package/libx/_runtime/common/generic/etag.js +8 -6
- package/libx/_runtime/common/generic/sorting.js +2 -2
- package/libx/_runtime/common/i18n/messages.properties +1 -0
- package/libx/_runtime/{cds-services/services → common}/utils/columns.js +4 -4
- package/libx/_runtime/common/utils/compareJson.js +274 -0
- package/libx/_runtime/common/utils/cqn2cqn4sql.js +1 -1
- package/libx/_runtime/{cds-services/services → common}/utils/differ.js +8 -8
- package/libx/_runtime/common/utils/ensureIEEE754.js +29 -0
- package/libx/_runtime/common/utils/{postProcessing.js → postProcess.js} +1 -3
- package/libx/_runtime/common/utils/resolveView.js +0 -16
- package/libx/_runtime/common/utils/rewriteAsterisks.js +1 -1
- package/libx/_runtime/common/utils/search2cqn4sql.js +1 -1
- package/libx/_runtime/common/utils/streamProp.js +9 -2
- package/libx/_runtime/common/utils/ucsn.js +1 -1
- package/libx/_runtime/db/expand/expandCQNToJoin.js +5 -3
- package/libx/_runtime/db/generic/rewrite.js +7 -13
- package/libx/_runtime/fiori/generic/activate.js +1 -1
- package/libx/_runtime/fiori/generic/edit.js +1 -1
- package/libx/_runtime/fiori/generic/prepare.js +1 -1
- package/libx/_runtime/fiori/lean-draft.js +151 -46
- package/libx/_runtime/fiori/utils/handler.js +1 -1
- package/libx/_runtime/hana/execute.js +6 -2
- package/libx/_runtime/hana/pool.js +3 -0
- package/libx/_runtime/hana/search2cqn4sql.js +1 -1
- package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +1 -2
- package/libx/_runtime/messaging/event-broker.js +212 -0
- package/libx/_runtime/remote/Service.js +9 -32
- package/libx/_runtime/remote/utils/client.js +13 -21
- package/libx/_runtime/sqlite/convertAssocToOneManaged.js +7 -1
- package/libx/_runtime/sqlite/execute.js +8 -3
- package/libx/_runtime/ucl/Service.js +259 -0
- package/libx/common/assert/index.js +5 -11
- package/libx/common/assert/validation.js +6 -1
- package/libx/odata/index.js +47 -25
- package/libx/odata/middleware/batch.js +8 -7
- package/libx/odata/middleware/create.js +42 -16
- package/libx/odata/middleware/delete.js +18 -11
- package/libx/odata/middleware/metadata.js +15 -14
- package/libx/odata/middleware/operation.js +30 -40
- package/libx/odata/middleware/parse.js +2 -3
- package/libx/odata/middleware/read.js +59 -52
- package/libx/odata/middleware/service-document.js +7 -7
- package/libx/odata/middleware/stream.js +26 -24
- package/libx/odata/middleware/update.js +53 -92
- package/libx/odata/parse/afterburner.js +45 -47
- package/libx/odata/parse/grammar.peggy +3 -3
- package/libx/odata/parse/multipartToJson.js +10 -22
- package/libx/odata/parse/parser.js +1 -1
- package/libx/odata/utils/etag.js +13 -0
- package/libx/odata/utils/handler.js +120 -0
- package/libx/odata/utils/index.js +15 -2
- package/libx/odata/utils/metaInfo.js +410 -0
- package/libx/odata/utils/path.js +5 -2
- package/libx/odata/utils/readAfterWrite.js +23 -0
- package/libx/odata/utils/result.js +4 -5
- package/libx/rest/RestAdapter.js +4 -13
- package/libx/rest/middleware/parse.js +40 -7
- package/package.json +1 -1
- package/server.js +1 -0
- package/libx/_runtime/cds-services/util/dataProcessUtils.js +0 -93
- package/libx/_runtime/common/utils/thenable.js +0 -51
- package/libx/_runtime/rest/service.js +0 -2
- package/libx/odata/parse/parseToCqn.js +0 -39
- package/libx/rest/middleware/input.js +0 -54
- package/libx/rest/middleware/payload.js +0 -13
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
const cds = require('../../_runtime/cds')
|
|
2
|
+
const LOG = cds.log('odata')
|
|
3
|
+
|
|
4
|
+
const { appURL } = require('../../_runtime/common/utils/vcap')
|
|
5
|
+
const { resolveFromSelect, targetFromPath } = require('../../_runtime/common/utils/cqn')
|
|
6
|
+
const { setEntityContained } = require('../../_runtime/common/utils/csn')
|
|
7
|
+
const { getNavigationIfStruct } = require('../../_runtime/common/utils/structured')
|
|
8
|
+
const getTemplate = require('../../_runtime/common/utils/template')
|
|
9
|
+
const templateProcessor = require('../../_runtime/common/utils/templateProcessor')
|
|
10
|
+
|
|
11
|
+
const _ignoreColumns = columns => {
|
|
12
|
+
if (!(Array.isArray(columns) && columns.some(c => c === '*' || c.as || c.ref))) return true
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const _getNestedQueryOptions = (ref, expand, expandString) => {
|
|
16
|
+
const isNested = expandString.match(new RegExp(`${ref}(?=\\()`))
|
|
17
|
+
// if no "ref(" i.e. "ref" without open bracket => no nested options => shift and return
|
|
18
|
+
if (!(isNested && Array.isArray(expand) && expand.length)) return { _expand: expandString.replace(ref, '') }
|
|
19
|
+
// if "ref(" found, shift to the first position after "("
|
|
20
|
+
expandString = expandString.slice(isNested.index + ref.length + 1)
|
|
21
|
+
// i.e. we found 1 open bracket already
|
|
22
|
+
let openBrackets = 1
|
|
23
|
+
let head = ''
|
|
24
|
+
// if expandString is '$top=10;$expand=foo($select=*),bar($select=buz));$select=a,b)',
|
|
25
|
+
// then outterQueryOptions is '$top=10;$expand=foo,bar;$select=a,b'
|
|
26
|
+
let outterQueryOptions = ''
|
|
27
|
+
let nestedExpand = ''
|
|
28
|
+
|
|
29
|
+
// parse until the last even closing bracket
|
|
30
|
+
while (openBrackets) {
|
|
31
|
+
const bracketFound = expandString.match(/\(|\)/)
|
|
32
|
+
head = expandString.substring(0, bracketFound.index)
|
|
33
|
+
expandString = expandString.slice(bracketFound.index + 1)
|
|
34
|
+
nestedExpand = `${nestedExpand}${head}${bracketFound[0]}`
|
|
35
|
+
// every time we have only 1 opened bracket and find another one,
|
|
36
|
+
// everything to the left is related to outter query options
|
|
37
|
+
if (openBrackets === 1 && bracketFound[0] === '(') {
|
|
38
|
+
outterQueryOptions = `${outterQueryOptions}${head}`
|
|
39
|
+
}
|
|
40
|
+
openBrackets = bracketFound[0] === '(' ? openBrackets + 1 : openBrackets - 1
|
|
41
|
+
}
|
|
42
|
+
outterQueryOptions = `${outterQueryOptions}${head}`
|
|
43
|
+
|
|
44
|
+
// outterQueryOptions also contain $expand, but without nested options i.e. can safely be split by ";"
|
|
45
|
+
const $select = outterQueryOptions.split(';').find(s => s.startsWith('$select'))
|
|
46
|
+
|
|
47
|
+
const expandIndex = nestedExpand.indexOf('$expand=')
|
|
48
|
+
// last symbol is a pair to open bracket in "ref(" => slice(..., -1)
|
|
49
|
+
const $expand = expandIndex === -1 ? '' : nestedExpand.slice(expandIndex + '$expand='.length, -1)
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
$select: $select && $select.replace('$select=', ''),
|
|
53
|
+
$expand,
|
|
54
|
+
_expand: expandString
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const _columnsFromQuery = (columns, target, options) => {
|
|
59
|
+
// must use query.columns as it includes columns from $apply except of $apply=expand()
|
|
60
|
+
// must use query options to get nested $selects inside $expand() as they are mixed into query columns
|
|
61
|
+
// example: GET /Foo?$select=bar&$expand=bar => @odata.context: $metadata#Foo(bar,bar())
|
|
62
|
+
// REVISIT tbd if having expand column in $select could be integrated into query in grammar.peggy
|
|
63
|
+
// REVISIT support $apply=expand()
|
|
64
|
+
if (_ignoreColumns(columns, options)) return ''
|
|
65
|
+
const context = []
|
|
66
|
+
const _select = options.$select ? options.$select.split(',') : []
|
|
67
|
+
let _expand = options.$expand || ''
|
|
68
|
+
|
|
69
|
+
const hasAsterisk = _select.indexOf('*') > -1
|
|
70
|
+
if (hasAsterisk) context.push('*')
|
|
71
|
+
|
|
72
|
+
for (const c of columns) {
|
|
73
|
+
if (!c) continue
|
|
74
|
+
const ref = c.ref && c.ref.join('/')
|
|
75
|
+
if (!hasAsterisk && !c.expand) {
|
|
76
|
+
if (c.as) context.push(c.as)
|
|
77
|
+
else if (ref) context.push(ref)
|
|
78
|
+
} else if (c.expand) {
|
|
79
|
+
if (!hasAsterisk && _select.indexOf(ref) > -1) context.push(ref)
|
|
80
|
+
|
|
81
|
+
const nextTarget = getNavigationIfStruct(target, c.ref)
|
|
82
|
+
if (nextTarget && nextTarget._target && nextTarget._target.elements) {
|
|
83
|
+
const nestedOptions = _getNestedQueryOptions(ref, c.expand, _expand)
|
|
84
|
+
_expand = nestedOptions._expand
|
|
85
|
+
context.push(`${ref}(${_columnsFromQuery(c.expand, nextTarget._target, nestedOptions)})`)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (context.length) return context.join(',')
|
|
90
|
+
else if (hasAsterisk) return '*'
|
|
91
|
+
return ''
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const _processFn = columns => {
|
|
95
|
+
return ({ row, key, element, pathSegmentsInfo }) => {
|
|
96
|
+
if (!(key in row) || row[key] === null) return
|
|
97
|
+
let cur = columns
|
|
98
|
+
if (element.parent._isStructured) {
|
|
99
|
+
const prefix = pathSegmentsInfo.join('/')
|
|
100
|
+
key = `${prefix}/${key}`
|
|
101
|
+
} else {
|
|
102
|
+
for (let p of pathSegmentsInfo) {
|
|
103
|
+
if (!cur[p]) cur[p] = {}
|
|
104
|
+
cur = cur[p]
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (!cur[key]) cur[key] = {}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const _columnsFromData = (data, definition, service) => {
|
|
112
|
+
const columns = {}
|
|
113
|
+
const template = getTemplate('odata-context', service, definition, { pick: element => element.isAssociation })
|
|
114
|
+
if (!template || !template.elements.size) return ''
|
|
115
|
+
const arrayData = Array.isArray(data) ? data : data ? [data] : []
|
|
116
|
+
for (const row of arrayData) {
|
|
117
|
+
templateProcessor({ processFn: _processFn(columns), row, template, pathOptions: { pathSegmentsInfo: [] } })
|
|
118
|
+
}
|
|
119
|
+
return _stringifyColumnsFromData(columns)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const _stringifyColumnsFromData = columns =>
|
|
123
|
+
Object.keys(columns)
|
|
124
|
+
.map(key => `${key}(${_stringifyColumnsFromData(columns[key])})`)
|
|
125
|
+
.join(',')
|
|
126
|
+
|
|
127
|
+
const _listColumns = ({ columns, data, isUpsert, returnType, event, /* express */ _req, service, propertyName }) => {
|
|
128
|
+
if (columns.length === 1 && propertyName) return `/${propertyName}`
|
|
129
|
+
// query options ($select, $expand, etc) as strings
|
|
130
|
+
const queryOptions = _req.query
|
|
131
|
+
let columnsStr
|
|
132
|
+
if (!isUpsert && event in { CREATE: 1 }) {
|
|
133
|
+
columnsStr = _columnsFromData(data, returnType, service)
|
|
134
|
+
} else {
|
|
135
|
+
columnsStr = _columnsFromQuery(columns, returnType, queryOptions)
|
|
136
|
+
}
|
|
137
|
+
return columnsStr && `(${columnsStr})`
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const _getContextUrlPrefix = ({ _req, path, target }) => {
|
|
141
|
+
if (cds.env.odata.contextAbsoluteUrl) {
|
|
142
|
+
try {
|
|
143
|
+
if (typeof cds.env.odata.contextAbsoluteUrl === 'string') {
|
|
144
|
+
const userDefinedURL = new URL(cds.env.odata.contextAbsoluteUrl, cds.env.odata.contextAbsoluteUrl).toString()
|
|
145
|
+
return (!userDefinedURL.endsWith('/') && `${userDefinedURL}/`) || userDefinedURL
|
|
146
|
+
}
|
|
147
|
+
} catch (e) {
|
|
148
|
+
e.message = `cds.odata.contextAbsoluteUrl could not be parsed as URL: ${cds.env.odata.contextAbsoluteUrl}`
|
|
149
|
+
LOG._warn && LOG.warn(e)
|
|
150
|
+
}
|
|
151
|
+
const reqURL = _req && _req.get && _req.get('host') && `${_req.protocol || 'https'}://${_req.get('host')}`
|
|
152
|
+
const baseAppURL = appURL || reqURL || ''
|
|
153
|
+
const serviceUrl = `${(_req && _req.baseUrl) || ''}/`
|
|
154
|
+
return baseAppURL && new URL(serviceUrl, baseAppURL).toString()
|
|
155
|
+
}
|
|
156
|
+
return target && target.params ? '../'.repeat(path.length) : '../'.repeat(path.length - 1)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const _findEdmNameFor = (definition, namespace, fullyQualified = false) => {
|
|
160
|
+
let name
|
|
161
|
+
if (!definition) return ''
|
|
162
|
+
if (definition._isStructured) {
|
|
163
|
+
const structured = [definition.name]
|
|
164
|
+
while (definition.parent) {
|
|
165
|
+
definition = definition.parent
|
|
166
|
+
structured.unshift(definition.name)
|
|
167
|
+
}
|
|
168
|
+
name = structured.join('_')
|
|
169
|
+
} else {
|
|
170
|
+
name = definition.name
|
|
171
|
+
}
|
|
172
|
+
if (!name.startsWith(`${namespace}.`)) return name
|
|
173
|
+
return fullyQualified ? name : name.replace(new RegExp(`^${namespace}\\.`), '')
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const _opResultName = ({ service, returnType, operation, isServiceEntity }) => {
|
|
177
|
+
const { namespace } = service
|
|
178
|
+
if (returnType.name) {
|
|
179
|
+
const resultName = _findEdmNameFor(returnType, namespace)
|
|
180
|
+
if (returnType.name.startsWith(`${namespace}.`)) {
|
|
181
|
+
if (isServiceEntity) return resultName.replace(/\./g, '_')
|
|
182
|
+
return `${namespace}.${resultName.replace(/\./g, '_')}`
|
|
183
|
+
}
|
|
184
|
+
return resultName
|
|
185
|
+
}
|
|
186
|
+
// bound action / function returns inline structure
|
|
187
|
+
if (operation.parent) {
|
|
188
|
+
const boundEntityName = _findEdmNameFor(operation.parent, namespace, true).replace(/\./g, '_')
|
|
189
|
+
// REVISIT exactly this return type name is generated in edm by compiler
|
|
190
|
+
return `${namespace}.return_${boundEntityName}_${_findEdmNameFor(operation, namespace)}`
|
|
191
|
+
}
|
|
192
|
+
// unbound action / function returns inline structure
|
|
193
|
+
// REVISIT exactly this return type name is generated in edm by compiler
|
|
194
|
+
return `${namespace}.return_${_findEdmNameFor(operation, namespace, true).replace(/\./g, '_')}`
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const _isNavToDraftAdmin = path => path.length > 1 && path[path.length - 1] === 'DraftAdministrativeData'
|
|
198
|
+
|
|
199
|
+
const _getCanonicalUrl = (path, target, model) => {
|
|
200
|
+
const toManySegment =
|
|
201
|
+
path.length > 1 && Array.isArray(path[path.length - 1].where) && path[path.length - 1].where.length && path.pop()
|
|
202
|
+
if (target.params) path.push('Set')
|
|
203
|
+
// construct path with only innermost refs for @odata.context
|
|
204
|
+
const _path = []
|
|
205
|
+
for (const seg of path) {
|
|
206
|
+
if (typeof seg === 'string') _path.push(seg)
|
|
207
|
+
else {
|
|
208
|
+
const _seg = { ...seg }
|
|
209
|
+
if (_seg.where) {
|
|
210
|
+
_seg.where = []
|
|
211
|
+
for (const ele of seg.where) {
|
|
212
|
+
if (ele.ref && ele.ref.length > 1) _seg.where.push({ ref: [ele.ref[ele.ref.length - 1]] })
|
|
213
|
+
else _seg.where.push(ele)
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
_path.push(_seg)
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
const odataUrl = cds.odata.urlify({ SELECT: { from: { ref: _path } } }, { model, kind: 'odata' })
|
|
220
|
+
let contextPath = odataUrl.path && odataUrl.path.match(/^([^?]*)\??/)[1]
|
|
221
|
+
if (toManySegment) {
|
|
222
|
+
contextPath += `/${toManySegment.id}`
|
|
223
|
+
path.push(toManySegment)
|
|
224
|
+
}
|
|
225
|
+
return contextPath
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const _getReturnTypeUrl = options => {
|
|
229
|
+
const {
|
|
230
|
+
service,
|
|
231
|
+
isCollection,
|
|
232
|
+
returnType,
|
|
233
|
+
operation,
|
|
234
|
+
path,
|
|
235
|
+
target,
|
|
236
|
+
propertyName,
|
|
237
|
+
isServiceEntity,
|
|
238
|
+
isTargetComposition
|
|
239
|
+
} = options
|
|
240
|
+
const { namespace } = service
|
|
241
|
+
if (operation) {
|
|
242
|
+
const resultName = _opResultName(options)
|
|
243
|
+
if (isServiceEntity) return resultName
|
|
244
|
+
return isCollection ? `Collection(${resultName})` : resultName
|
|
245
|
+
}
|
|
246
|
+
if (isTargetComposition || propertyName || target.params || _isNavToDraftAdmin(path)) {
|
|
247
|
+
return _getCanonicalUrl(path, target, service.model)
|
|
248
|
+
}
|
|
249
|
+
if (isServiceEntity) return _findEdmNameFor(returnType, namespace).replace(/\./g, '_')
|
|
250
|
+
return isCollection ? `Collection(${returnType.name})` : returnType.name
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const _isSingleEntity = options => {
|
|
254
|
+
const { isCollection, propertyName, returnType, isServiceEntity, isTargetComposition } = options
|
|
255
|
+
if (isCollection || (returnType && returnType._isSingleton) || propertyName) return false
|
|
256
|
+
return isServiceEntity || isTargetComposition
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const _isStructuredProperty = ({ returnType }) => {
|
|
260
|
+
return returnType.elements && returnType.kind === 'element'
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const _getContextUrl = options => {
|
|
264
|
+
if (!options.returnType) return ''
|
|
265
|
+
const contextUrlPrefix = _getContextUrlPrefix(options)
|
|
266
|
+
|
|
267
|
+
if (options.returnType.kind === 'service') {
|
|
268
|
+
return `${contextUrlPrefix}$metadata`
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const returnTypeUrl = _getReturnTypeUrl(options)
|
|
272
|
+
const columnsStringified = options.propertyName || _isStructuredProperty(options) ? '' : _listColumns(options)
|
|
273
|
+
const $entity = _isSingleEntity(options) ? '/$entity' : ''
|
|
274
|
+
return `${contextUrlPrefix}$metadata#${returnTypeUrl}${columnsStringified}${$entity}`
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const _getAdditionalContextUrl = (query, service, data, eventType, _req, isUpsert) => {
|
|
278
|
+
if (Array.isArray(query)) {
|
|
279
|
+
const additionalContextUrls = []
|
|
280
|
+
for (let i = 1; i < query.length; i++) {
|
|
281
|
+
additionalContextUrls.push(
|
|
282
|
+
_getContextUrl(
|
|
283
|
+
Object.assign(_getQueryInfo(query[i], service, data, eventType), { _req, service, data, isUpsert })
|
|
284
|
+
)
|
|
285
|
+
)
|
|
286
|
+
}
|
|
287
|
+
return additionalContextUrls
|
|
288
|
+
}
|
|
289
|
+
return []
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const _partialCopyColumn = c => {
|
|
293
|
+
if (c.expand) {
|
|
294
|
+
const copy = { expand: Array.isArray(c.expand) ? c.expand.map(_partialCopyColumn) : c.expand }
|
|
295
|
+
if (c.ref) copy.ref = [...c.ref]
|
|
296
|
+
return copy
|
|
297
|
+
}
|
|
298
|
+
if (c.ref) return { ref: [...c.ref] }
|
|
299
|
+
if (c.as) return { as: c.as }
|
|
300
|
+
return c
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const _partialCopyColumns = query => {
|
|
304
|
+
if (query.SELECT) {
|
|
305
|
+
// stop digging into subSelects as soon as columns found, as deeper columns will be shadowed by these
|
|
306
|
+
if (!query.SELECT.columns && query.SELECT.from.SELECT) return _partialCopyColumns(query.SELECT.from)
|
|
307
|
+
if (query.SELECT.columns) return query.SELECT.columns.map(_partialCopyColumn)
|
|
308
|
+
}
|
|
309
|
+
return []
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// eslint-disable-next-line complexity
|
|
313
|
+
const _getPathInfo = (query, model) => {
|
|
314
|
+
const queryFrom =
|
|
315
|
+
(query.SELECT && resolveFromSelect(query)) ||
|
|
316
|
+
(query.INSERT && query.INSERT.into) ||
|
|
317
|
+
(query.UPDATE && query.UPDATE.entity) ||
|
|
318
|
+
(query.DELETE && query.DELETE.from)
|
|
319
|
+
const { last, target, path, isTargetComposition } = targetFromPath(queryFrom, model)
|
|
320
|
+
const operation = (last.kind === 'action' || last.kind === 'function') && last
|
|
321
|
+
let returnType, isCollection, propertyName, unbound
|
|
322
|
+
if (operation) {
|
|
323
|
+
// last segment is bound action/function => must not be in ref
|
|
324
|
+
queryFrom && queryFrom.ref && queryFrom.ref.pop()
|
|
325
|
+
unbound = !operation.parent
|
|
326
|
+
if (operation.returns) {
|
|
327
|
+
returnType = setEntityContained(operation.returns.items || operation.returns, model)
|
|
328
|
+
isCollection = !!operation.returns.items
|
|
329
|
+
}
|
|
330
|
+
// no propertyName as operations do not (yet?) support navigation
|
|
331
|
+
} else {
|
|
332
|
+
returnType = target
|
|
333
|
+
isCollection = Array.isArray(query) || (query.SELECT && !query.SELECT.one)
|
|
334
|
+
propertyName = query._propertyAccess && query.SELECT.columns[0].ref[query.SELECT.columns[0].ref.length - 1]
|
|
335
|
+
if (propertyName && path.slice(-1)[0] !== propertyName) {
|
|
336
|
+
path.push(propertyName)
|
|
337
|
+
returnType = query.SELECT.columns[0].ref.reduce((r, c) => r.elements[c], target)
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
const isStream = propertyName && target.elements[propertyName]?.['@Core.MediaType']
|
|
341
|
+
return {
|
|
342
|
+
path,
|
|
343
|
+
target,
|
|
344
|
+
last,
|
|
345
|
+
operation,
|
|
346
|
+
returnType,
|
|
347
|
+
isCollection,
|
|
348
|
+
propertyName,
|
|
349
|
+
isStream,
|
|
350
|
+
unbound,
|
|
351
|
+
isTargetComposition
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const _getEvent = (eventType, namespace, data, { last, target }) => {
|
|
356
|
+
if (last && (last.kind === 'action' || last.kind === 'function')) {
|
|
357
|
+
// BOUND
|
|
358
|
+
if (last.parent) eventType = last.name
|
|
359
|
+
// UNBOUND
|
|
360
|
+
eventType = last.name.replace(`${namespace}.`, '')
|
|
361
|
+
}
|
|
362
|
+
// draft
|
|
363
|
+
if (target && target._isDraftEnabled) {
|
|
364
|
+
if (eventType === 'CREATE') return 'NEW'
|
|
365
|
+
else if (eventType === 'draftEdit') return 'EDIT'
|
|
366
|
+
else if (eventType === 'UPDATE') return 'PATCH'
|
|
367
|
+
else if (eventType === 'DELETE' && data.IsActiveEntity !== true) return 'CANCEL'
|
|
368
|
+
}
|
|
369
|
+
return eventType
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const _getQueryInfo = (query, service, data, eventType) => {
|
|
373
|
+
const { namespace, model } = service
|
|
374
|
+
const _pathInfo = _getPathInfo(Array.isArray(query) ? query[0] : query, model)
|
|
375
|
+
const { returnType } = _pathInfo
|
|
376
|
+
|
|
377
|
+
// store original columns before they are polluted by drafts, db and so on
|
|
378
|
+
const columns = _partialCopyColumns(Array.isArray(query) ? query[0] : query)
|
|
379
|
+
const isServiceEntity = _findEdmNameFor(returnType, namespace) in service.entities && !returnType._isContained
|
|
380
|
+
const event = _getEvent(eventType, namespace, data, _pathInfo)
|
|
381
|
+
return Object.assign(_pathInfo, {
|
|
382
|
+
columns,
|
|
383
|
+
isServiceEntity,
|
|
384
|
+
event
|
|
385
|
+
})
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
module.exports = (query, eventType, service, data, /* express req */ _req, isUpsert) => {
|
|
389
|
+
const queryInfo = _getQueryInfo(query, service, data, eventType)
|
|
390
|
+
|
|
391
|
+
const { isCollection, isStream, propertyName, unbound, event, returnType, isServiceEntity } = queryInfo
|
|
392
|
+
|
|
393
|
+
const contextUrl = _getContextUrl(Object.assign(queryInfo, { _req, service, data, isUpsert }))
|
|
394
|
+
|
|
395
|
+
const additionalContextUrl = _getAdditionalContextUrl(query, service, data, eventType, _req, isUpsert)
|
|
396
|
+
|
|
397
|
+
return {
|
|
398
|
+
event,
|
|
399
|
+
unbound,
|
|
400
|
+
metadata: {
|
|
401
|
+
isCollection,
|
|
402
|
+
isStream,
|
|
403
|
+
propertyName,
|
|
404
|
+
contextUrl,
|
|
405
|
+
returnType,
|
|
406
|
+
isServiceEntity,
|
|
407
|
+
additionalContextUrl
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
package/libx/odata/utils/path.js
CHANGED
|
@@ -3,6 +3,7 @@ const { where2obj } = require('../../_runtime/common/utils/cqn')
|
|
|
3
3
|
const _handleXpr = (relation, keys, seg_keys) => {
|
|
4
4
|
const join = [...relation]
|
|
5
5
|
while (join.length >= 3) {
|
|
6
|
+
// eslint-disable-next-line no-unused-vars
|
|
6
7
|
const [left, _, right] = join
|
|
7
8
|
|
|
8
9
|
if (left.xpr) {
|
|
@@ -14,11 +15,13 @@ const _handleXpr = (relation, keys, seg_keys) => {
|
|
|
14
15
|
|
|
15
16
|
if (left.ref?.[0] === 'target') {
|
|
16
17
|
if (left.ref[1] in keys) break // we already added the foreign key for the last segment
|
|
17
|
-
|
|
18
|
+
const keyValue = 'val' in right ? right.val : seg_keys[right.ref[1]]
|
|
19
|
+
if (keyValue !== undefined) keys[left.ref[1]] = keyValue
|
|
18
20
|
join.splice(0, 4)
|
|
19
21
|
} else if (right.ref?.[0] === 'target') {
|
|
20
22
|
if (right.ref[1] in keys) break // we already added the foreign key for the last segment
|
|
21
|
-
|
|
23
|
+
const keyValue = 'val' in left ? left.val : seg_keys[left.ref[1]]
|
|
24
|
+
if (keyValue !== undefined) keys[right.ref[1]] = keyValue
|
|
22
25
|
join.splice(0, 4)
|
|
23
26
|
}
|
|
24
27
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const cds = require('../../_runtime/cds')
|
|
2
|
+
const { Request } = cds
|
|
3
|
+
|
|
4
|
+
const readAfterWrite = async (req, srv, query) => {
|
|
5
|
+
// gracefully set location and no body if no read auth or not readable capability
|
|
6
|
+
let result
|
|
7
|
+
try {
|
|
8
|
+
const _req = new Request({ query, event: 'READ', _: req._, params: req.params })
|
|
9
|
+
result = await srv.dispatch(_req)
|
|
10
|
+
// NEW/PATCH must not include DraftAdministrativeData_DraftUUID for plain v4 usage, however required for odata-v2
|
|
11
|
+
if (result && req.target._isDraftEnabled && req.headers?.['x-cds-odata-version'] !== 'v2') {
|
|
12
|
+
delete result.DraftAdministrativeData_DraftUUID
|
|
13
|
+
}
|
|
14
|
+
} catch (e) {
|
|
15
|
+
// read was not possible because of access restrictions => ignore
|
|
16
|
+
if (!(Number(e.code) in { 401: 1, 403: 1, 404: 1, 405: 1 })) throw e
|
|
17
|
+
result = null
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return result
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
module.exports = readAfterWrite
|
|
@@ -91,6 +91,7 @@ const _getParent = (model, name) => {
|
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
const addEtags = (row, key) => {
|
|
94
|
+
if (!row[key]) return
|
|
94
95
|
row['$etag'] = row[key].startsWith('W/') ? row[key] : `W/"${row[key]}"`
|
|
95
96
|
}
|
|
96
97
|
|
|
@@ -101,10 +102,8 @@ const _processCategory = (category, elementInfo) => {
|
|
|
101
102
|
case '@odata.etag':
|
|
102
103
|
addEtags(row, key)
|
|
103
104
|
break
|
|
104
|
-
case '
|
|
105
|
-
|
|
106
|
-
if (row[key] == null) return
|
|
107
|
-
row[key] = `${row[key]}`
|
|
105
|
+
case '@cds.api.ignore':
|
|
106
|
+
delete row[key]
|
|
108
107
|
break
|
|
109
108
|
// no default
|
|
110
109
|
}
|
|
@@ -123,7 +122,7 @@ const _processorFn = () => elementInfo => {
|
|
|
123
122
|
const _pick = element => {
|
|
124
123
|
const categories = []
|
|
125
124
|
if (element['@odata.etag']) categories.push('@odata.etag')
|
|
126
|
-
if (element
|
|
125
|
+
if (element['@cds.api.ignore']) categories.push('@cds.api.ignore')
|
|
127
126
|
if (categories.length) return { categories }
|
|
128
127
|
}
|
|
129
128
|
|
package/libx/rest/RestAdapter.js
CHANGED
|
@@ -4,14 +4,12 @@ const cds = require('../_runtime/cds')
|
|
|
4
4
|
const express = require('express')
|
|
5
5
|
|
|
6
6
|
const parse_factory = require('./middleware/parse')
|
|
7
|
-
const input_factory = require('./middleware/input')
|
|
8
7
|
|
|
9
8
|
const create_factory = require('./middleware/create')
|
|
10
9
|
const read_factory = require('./middleware/read')
|
|
11
10
|
const update_factory = require('./middleware/update')
|
|
12
11
|
const delete_factory = require('./middleware/delete')
|
|
13
12
|
const operation_factory = require('./middleware/operation')
|
|
14
|
-
const payload_factory = require('./middleware/payload')
|
|
15
13
|
|
|
16
14
|
const error_factory = require('./middleware/error')
|
|
17
15
|
|
|
@@ -19,10 +17,6 @@ const { bufferToBase64 } = require('../_runtime/common/utils/binary')
|
|
|
19
17
|
const { getAccessRestrictions } = require('../_runtime/common/utils/restrictions')
|
|
20
18
|
|
|
21
19
|
const RestAdapter = function (srv) {
|
|
22
|
-
const parse = parse_factory(srv)
|
|
23
|
-
const input = input_factory(srv)
|
|
24
|
-
const payload = payload_factory(srv)
|
|
25
|
-
|
|
26
20
|
const router = express.Router()
|
|
27
21
|
|
|
28
22
|
// -----------------------------------------------------------------------------------------
|
|
@@ -38,8 +32,8 @@ const RestAdapter = function (srv) {
|
|
|
38
32
|
// > unauthorized or forbidden?
|
|
39
33
|
if (req.user._is_anonymous) {
|
|
40
34
|
// NOTE: "return req._login()" would not invoke custom error handlers
|
|
41
|
-
if (req._login) res.set('
|
|
42
|
-
else if (req.user._challenges) res.set('
|
|
35
|
+
if (req._login) res.set('www-authenticate', `Basic realm="Users"`)
|
|
36
|
+
else if (req.user._challenges) res.set('www-authenticate', req.user._challenges.join(';'))
|
|
43
37
|
throw cds.error('Unauthorized', { statusCode: 401, code: '401' })
|
|
44
38
|
}
|
|
45
39
|
throw cds.error('Forbidden', { statusCode: 403, code: '403' })
|
|
@@ -89,11 +83,8 @@ const RestAdapter = function (srv) {
|
|
|
89
83
|
}
|
|
90
84
|
next()
|
|
91
85
|
})
|
|
92
|
-
router.use(express.json())
|
|
93
|
-
router.use(
|
|
94
|
-
router.use(payload) // REVISIT: -> move?
|
|
95
|
-
// payload validation
|
|
96
|
-
router.use(input) // REVISIT: This is protocol-independent, isn't it? -> move to service layer
|
|
86
|
+
router.use(express.json())
|
|
87
|
+
router.use(parse_factory(srv))
|
|
97
88
|
|
|
98
89
|
// -----------------------------------------------------------------------------------------
|
|
99
90
|
// begin tx
|
|
@@ -1,10 +1,34 @@
|
|
|
1
1
|
const cds = require('../../_runtime/cds')
|
|
2
2
|
const { INSERT, SELECT, UPDATE, DELETE } = cds.ql
|
|
3
3
|
|
|
4
|
+
const { base64ToBuffer } = require('../../_runtime/common/utils/binary')
|
|
5
|
+
const { deepCopy } = require('../../_runtime/common/utils/copy')
|
|
4
6
|
const { where2obj } = require('../../_runtime/common/utils/cqn')
|
|
5
|
-
|
|
6
7
|
const { convertStructured } = require('../../_runtime/common/utils/ucsn')
|
|
7
|
-
|
|
8
|
+
|
|
9
|
+
const getTemplate = require('../../_runtime/common/utils/template')
|
|
10
|
+
const templateProcessor = require('../../_runtime/common/utils/templateProcessor')
|
|
11
|
+
const { checkStaticElementByKey } = require('../../_runtime/cds-services/util/assert')
|
|
12
|
+
|
|
13
|
+
const _processorFn = errors => {
|
|
14
|
+
return ({ row, key, plain: categories, target }) => {
|
|
15
|
+
// REVISIT move validation to generic asserter => see PR 717
|
|
16
|
+
if (categories['static_validation'] && row[key] != null) {
|
|
17
|
+
const validations = checkStaticElementByKey(target, key, row[key])
|
|
18
|
+
errors.push(...validations)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const _picker = element => {
|
|
24
|
+
const categories = {}
|
|
25
|
+
if (Array.isArray(element)) return
|
|
26
|
+
if (element._isStructured || element.isAssociation || element.items) return
|
|
27
|
+
categories['static_validation'] = true
|
|
28
|
+
return categories
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const _cache = req => `rest-input;skip-key-validation:${req.method !== 'POST'}`
|
|
8
32
|
|
|
9
33
|
module.exports = srv => (req, res, next) => {
|
|
10
34
|
// REVISIT: Once we don't display the error message location in terms of an offset, but instead a copy of the
|
|
@@ -107,20 +131,29 @@ module.exports = srv => (req, res, next) => {
|
|
|
107
131
|
} else {
|
|
108
132
|
// TODO: add keys from url into payload (overwriting if already present) -> document this behavior, also for OData
|
|
109
133
|
const payload = deepCopy(args || req.body)
|
|
134
|
+
let errs
|
|
110
135
|
if (cds.env.features.cds_assert) {
|
|
111
136
|
const assertOptions = {
|
|
112
137
|
filter: true,
|
|
113
138
|
http: { req },
|
|
114
139
|
mandatories: req.method === 'POST' || req.method === 'PUT' || undefined
|
|
115
140
|
}
|
|
116
|
-
|
|
117
|
-
if (errs) {
|
|
118
|
-
if (errs.length === 1) throw errs[0]
|
|
119
|
-
throw Object.assign(new Error('MULTIPLE_ERRORS'), { statusCode: 400, details: errs })
|
|
120
|
-
}
|
|
141
|
+
errs = cds.assert(payload, definition, assertOptions)
|
|
121
142
|
} else {
|
|
122
143
|
convertStructured(srv, operation || definition, payload, { cleanupStruct: cds.env.features.rest_struct_data })
|
|
144
|
+
const template = getTemplate(_cache(req), srv, definition, { pick: _picker })
|
|
145
|
+
if (template && template.elements.size) {
|
|
146
|
+
errs = []
|
|
147
|
+
for (const row of Array.isArray(payload) ? payload : [payload]) {
|
|
148
|
+
templateProcessor({ processFn: _processorFn(errs), row, template })
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (errs?.length) {
|
|
153
|
+
if (errs.length === 1) throw errs[0]
|
|
154
|
+
throw Object.assign(new Error('MULTIPLE_ERRORS'), { statusCode: 400, details: errs })
|
|
123
155
|
}
|
|
156
|
+
base64ToBuffer(payload, srv, definition)
|
|
124
157
|
req._data = payload
|
|
125
158
|
}
|
|
126
159
|
}
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -29,6 +29,7 @@ module.exports = async function cds_server (options) {
|
|
|
29
29
|
|
|
30
30
|
// load and prepare models
|
|
31
31
|
const csn = await cds.load(o.from||'*',o) .then (cds.minify)
|
|
32
|
+
cds.edmxs = cds.compile.to.edmx.files (csn)
|
|
32
33
|
cds.model = cds.compile.for.nodejs (csn)
|
|
33
34
|
|
|
34
35
|
// connect to essential framework services
|