@sap/cds 7.5.2 → 7.6.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 +79 -22
- package/app/index.js +1 -1
- package/lib/auth/index.js +3 -0
- package/lib/compile/extend.js +9 -4
- package/lib/compile/for/lean_drafts.js +3 -4
- package/lib/compile/load.js +11 -15
- package/lib/compile/minify.js +2 -4
- package/lib/compile/to/sql.js +6 -4
- package/lib/compile/to/srvinfo.js +25 -3
- package/lib/compile/to/yaml.js +1 -1
- package/lib/dbs/cds-deploy.js +7 -13
- package/lib/env/defaults.js +1 -10
- package/lib/env/schemas/cds-package.js +27 -0
- package/lib/env/schemas/cds-rc.js +693 -0
- package/lib/env/schemas/index.js +6 -4
- package/lib/i18n/localize.js +15 -1
- package/lib/index.js +40 -47
- package/lib/log/cds-error.js +6 -0
- package/lib/ql/Query.js +2 -1
- package/lib/ql/cds-ql.js +1 -2
- package/lib/ql/infer.js +0 -2
- package/lib/req/request.js +3 -6
- package/lib/srv/middlewares/trace.js +2 -2
- package/lib/srv/protocols/hcql.js +44 -30
- package/lib/srv/protocols/http.js +60 -0
- package/lib/srv/protocols/index.js +0 -7
- package/lib/srv/protocols/odata-v4.js +8 -2
- package/lib/srv/srv-api.js +129 -62
- package/lib/srv/srv-handlers.js +0 -1
- package/lib/srv/srv-models.js +1 -0
- package/lib/utils/cds-test.js +1 -1
- package/lib/utils/cds-utils.js +26 -0
- package/lib/utils/check-version.js +10 -13
- package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +22 -6
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +3 -4
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +89 -21
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/boundToCQN.js +4 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/readToCQN.js +1 -24
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/updateToCQN.js +1 -7
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/ApplyParser.js +3 -3
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/serializer/TrustedResourceJsonSerializer.js +7 -0
- package/libx/_runtime/cds-services/adapter/odata-v4/to.js +0 -5
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/handlerUtils.js +2 -0
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/metaInfo.js +17 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/result.js +22 -2
- package/libx/_runtime/cds-services/services/utils/columns.js +1 -2
- package/libx/_runtime/common/aspects/Association.js +17 -9
- package/libx/_runtime/common/generic/crud.js +13 -22
- package/libx/_runtime/common/generic/etag.js +1 -1
- package/libx/_runtime/common/generic/input.js +9 -1
- package/libx/_runtime/common/generic/paging.js +3 -3
- package/libx/_runtime/common/generic/sorting.js +25 -15
- package/libx/_runtime/common/generic/stream.js +2 -16
- package/libx/_runtime/common/utils/copy.js +5 -0
- package/libx/_runtime/common/utils/cqn.js +1 -1
- package/libx/_runtime/common/utils/cqn2cqn4sql.js +4 -3
- package/libx/_runtime/common/utils/csn.js +0 -49
- package/libx/_runtime/common/utils/foreignKeyPropagations.js +5 -5
- package/libx/_runtime/common/utils/generateOnCond.js +50 -25
- package/libx/_runtime/common/utils/resolveView.js +5 -44
- package/libx/_runtime/common/utils/rewriteAsterisks.js +17 -4
- package/libx/_runtime/common/utils/stream.js +16 -15
- package/libx/_runtime/common/utils/streamProp.js +25 -22
- package/libx/_runtime/db/Service.js +27 -8
- package/libx/_runtime/db/generic/input.js +6 -1
- package/libx/_runtime/db/generic/rewrite.js +3 -2
- package/libx/_runtime/db/query/read.js +15 -5
- package/libx/_runtime/db/sql-builder/ExpressionBuilder.js +0 -11
- package/libx/_runtime/db/utils/columns.js +1 -0
- package/libx/_runtime/db/utils/stream.js +41 -0
- package/libx/_runtime/fiori/generic/read.js +2 -1
- package/libx/_runtime/fiori/generic/readOverDraft.js +1 -1
- package/libx/_runtime/fiori/lean-draft.js +216 -59
- package/libx/_runtime/hana/Service.js +1 -1
- package/libx/_runtime/hana/execute.js +53 -14
- package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +34 -15
- package/libx/_runtime/remote/Service.js +2 -1
- package/libx/_runtime/remote/utils/client.js +1 -1
- package/libx/_runtime/sqlite/Service.js +1 -1
- package/libx/_runtime/sqlite/execute.js +17 -5
- package/libx/odata/afterburner.js +58 -19
- package/libx/odata/cqn2odata.js +6 -8
- package/libx/odata/create.js +44 -0
- package/libx/odata/delete.js +25 -0
- package/libx/odata/error.js +8 -3
- package/libx/odata/metadata.js +6 -8
- package/libx/odata/service-document.js +1 -1
- package/libx/odata/update.js +110 -0
- package/libx/odata/utils.js +9 -6
- package/libx/outbox/index.js +48 -78
- package/libx/rest/RestAdapter.js +0 -3
- package/package.json +1 -1
- package/lib/env/schemas/cds-package.json +0 -17
- package/lib/env/schemas/cds-rc.json +0 -740
- package/lib/ql/STREAM.js +0 -90
|
@@ -9,6 +9,7 @@ const cds = require('../cds')
|
|
|
9
9
|
const LOG = cds.log('sqlite|db|sql')
|
|
10
10
|
// && {_debug:true, debug(sql){ cds._debug && console.log(sql+';\n') }} //> please keep that for debugging stakeholder tests
|
|
11
11
|
const coloredTxCommands = require('../db/utils/coloredTxCommands')
|
|
12
|
+
const { convertStream } = require('../db/utils/stream')
|
|
12
13
|
const { inspect } = require('util')
|
|
13
14
|
|
|
14
15
|
const SANITIZE_VALUES = process.env.NODE_ENV === 'production' && cds.env.log.sanitize_values !== false
|
|
@@ -234,10 +235,12 @@ function executeInsertSQL(dbc, sql, values, query) {
|
|
|
234
235
|
})
|
|
235
236
|
}
|
|
236
237
|
|
|
237
|
-
|
|
238
|
+
// REVISIT: optimize - now sub-arrays are called sequentially
|
|
239
|
+
async function _convertStreamValues(values) {
|
|
238
240
|
let any
|
|
239
|
-
values.
|
|
240
|
-
|
|
241
|
+
for (let i = 0; i < values.length; i++) {
|
|
242
|
+
const v = values[i]
|
|
243
|
+
if (v instanceof Readable) {
|
|
241
244
|
any = values[i] = new Promise(resolve => {
|
|
242
245
|
const chunks = []
|
|
243
246
|
v.on('data', chunk => chunks.push(chunk))
|
|
@@ -247,8 +250,10 @@ function _convertStreamValues(values) {
|
|
|
247
250
|
v.push(null)
|
|
248
251
|
})
|
|
249
252
|
})
|
|
253
|
+
} else if (Array.isArray(v)) {
|
|
254
|
+
values[i] = await _convertStreamValues(v)
|
|
250
255
|
}
|
|
251
|
-
}
|
|
256
|
+
}
|
|
252
257
|
return any ? Promise.all(values) : values
|
|
253
258
|
}
|
|
254
259
|
|
|
@@ -295,13 +300,19 @@ function executeGenericCQN(model, dbc, cqn, user, locale, txTimestamp) {
|
|
|
295
300
|
return executePlainSQL(dbc, sql, values)
|
|
296
301
|
}
|
|
297
302
|
|
|
298
|
-
|
|
303
|
+
// REVISIT: consider deleting this function after removing stream_compat
|
|
304
|
+
async function executeSelectStreamCQN({ model, dbc, query, user, locale, txTimestamp }) {
|
|
299
305
|
const result = await executeSelectCQN(model, dbc, query, user, locale, txTimestamp)
|
|
300
306
|
|
|
301
307
|
if (result == null || result.length === 0) {
|
|
302
308
|
return
|
|
303
309
|
}
|
|
304
310
|
|
|
311
|
+
if (!cds.env.features.stream_compat) {
|
|
312
|
+
convertStream(query.SELECT.columns, query.target, result, query.SELECT.one)
|
|
313
|
+
return result
|
|
314
|
+
}
|
|
315
|
+
|
|
305
316
|
let val = Array.isArray(result) ? Object.values(result[0])[0] : Object.values(result)[0]
|
|
306
317
|
if (val === null) {
|
|
307
318
|
return null
|
|
@@ -323,6 +334,7 @@ module.exports = {
|
|
|
323
334
|
update: executeUpdateCQN,
|
|
324
335
|
select: executeSelectCQN,
|
|
325
336
|
stream: executeSelectStreamCQN,
|
|
337
|
+
convert: convertStream,
|
|
326
338
|
cqn: executeGenericCQN,
|
|
327
339
|
sql: executePlainSQL
|
|
328
340
|
}
|
|
@@ -4,6 +4,7 @@ const { where2obj, resolveFromSelect } = require('../_runtime/common/utils/cqn')
|
|
|
4
4
|
const { findCsnTargetFor } = require('../_runtime/common/utils/csn')
|
|
5
5
|
const normalizeTimestamp = require('../_runtime/common/utils/normalizeTimestamp')
|
|
6
6
|
const { rewriteExpandAsterisk } = require('../_runtime/common/utils/rewriteAsterisks')
|
|
7
|
+
const resolveStructured = require('../_runtime/common/utils/resolveStructured')
|
|
7
8
|
|
|
8
9
|
const _addKeysDeep = (keys, keysCollector, ignoreManagedBacklinks) => {
|
|
9
10
|
for (const keyName in keys) {
|
|
@@ -44,21 +45,20 @@ function _getDefinition(definition, name, namespace) {
|
|
|
44
45
|
)
|
|
45
46
|
}
|
|
46
47
|
|
|
47
|
-
function _resolveAliasInParams(params, entity) {
|
|
48
|
-
if (!entity._alias2ref) return
|
|
49
|
-
const paramKeys = Object.keys(params)
|
|
50
|
-
for (const paramKey of paramKeys) {
|
|
51
|
-
if (entity._alias2ref[paramKey]) {
|
|
52
|
-
params[entity._alias2ref[paramKey].join('_')] = params[paramKey]
|
|
53
|
-
params[paramKey] = undefined
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
48
|
function _resolveAliasesInRef(ref, target) {
|
|
59
49
|
if (ref.length === 1) {
|
|
60
50
|
if (target.keys[ref[0]]) return ref
|
|
61
|
-
|
|
51
|
+
// resolve multi-part refs for innermost ref in url
|
|
52
|
+
if (target._flattenedKeys === undefined) {
|
|
53
|
+
const flattenedKeys = []
|
|
54
|
+
for (const key in target.keys) {
|
|
55
|
+
if (!target.keys[key].elements) continue
|
|
56
|
+
flattenedKeys.push(...resolveStructured({ element: target.keys[key], structProperties: [] }, false, true))
|
|
57
|
+
}
|
|
58
|
+
target._flattenedKeys = flattenedKeys.length ? flattenedKeys : null
|
|
59
|
+
}
|
|
60
|
+
const fk = target._flattenedKeys?.find(fk => fk.key === ref[0])
|
|
61
|
+
if (fk) return [...fk.resolved]
|
|
62
62
|
}
|
|
63
63
|
for (const seg of ref) {
|
|
64
64
|
target = target.elements[seg.id || seg]
|
|
@@ -143,7 +143,7 @@ function _processWhere(where, entity) {
|
|
|
143
143
|
|
|
144
144
|
if (operator in forbidden) {
|
|
145
145
|
// xpr check needs to be done first, else it could happen, that we ignore xpr OR xpr
|
|
146
|
-
continue
|
|
146
|
+
continue
|
|
147
147
|
}
|
|
148
148
|
|
|
149
149
|
let valIndex = -1
|
|
@@ -216,7 +216,7 @@ function _processSegments(from, model, namespace, cqn) {
|
|
|
216
216
|
let incompleteKeys = false
|
|
217
217
|
let one
|
|
218
218
|
let target
|
|
219
|
-
|
|
219
|
+
|
|
220
220
|
function _handleCollectionBoundActions(i) {
|
|
221
221
|
let action
|
|
222
222
|
if (current.actions) {
|
|
@@ -224,9 +224,9 @@ function _processSegments(from, model, namespace, cqn) {
|
|
|
224
224
|
const shortName = nextRef && nextRef.replace(namespace + '.', '')
|
|
225
225
|
action = shortName && current.actions[shortName]
|
|
226
226
|
}
|
|
227
|
-
|
|
227
|
+
|
|
228
228
|
incompleteKeys = ref[i].where ? false : i === ref.length - 1 || one ? false : true
|
|
229
|
-
|
|
229
|
+
|
|
230
230
|
if (incompleteKeys && action) {
|
|
231
231
|
if (
|
|
232
232
|
action['@cds.odata.bindingparameter.collection'] ||
|
|
@@ -239,7 +239,7 @@ function _processSegments(from, model, namespace, cqn) {
|
|
|
239
239
|
}
|
|
240
240
|
}
|
|
241
241
|
}
|
|
242
|
-
|
|
242
|
+
|
|
243
243
|
for (let i = 0; i < ref.length; i++) {
|
|
244
244
|
const seg = ref[i].id || ref[i]
|
|
245
245
|
const whereRef = ref[i].where
|
|
@@ -286,7 +286,6 @@ function _processSegments(from, model, namespace, cqn) {
|
|
|
286
286
|
if (whereRef) {
|
|
287
287
|
keyCount += addRefToWhereIfNecessary(ref[i].where, current)
|
|
288
288
|
_resolveAliasesInXpr(ref[i].where, current)
|
|
289
|
-
_resolveAliasInParams(params, current)
|
|
290
289
|
_processWhere(ref[i].where, current)
|
|
291
290
|
}
|
|
292
291
|
|
|
@@ -320,7 +319,6 @@ function _processSegments(from, model, namespace, cqn) {
|
|
|
320
319
|
if (whereRef) {
|
|
321
320
|
keyCount += addRefToWhereIfNecessary(whereRef, current)
|
|
322
321
|
_resolveAliasesInXpr(whereRef, current)
|
|
323
|
-
_resolveAliasInParams(params, current)
|
|
324
322
|
// in case of Foo(1), params will be {} (before addRefToWhereIfNecessary was called)
|
|
325
323
|
if (!Object.keys(params).length) params = where2obj(ref[i].where)
|
|
326
324
|
_processWhere(ref[i].where, current)
|
|
@@ -513,6 +511,41 @@ const _checkAllKeysProvided = (params, entity) => {
|
|
|
513
511
|
}
|
|
514
512
|
}
|
|
515
513
|
|
|
514
|
+
function _cleanupIgnoredXpr(xpr, ignoredColumns, target, isOne) {
|
|
515
|
+
if (!xpr) return
|
|
516
|
+
|
|
517
|
+
for (const x of xpr) {
|
|
518
|
+
if (x.xpr) {
|
|
519
|
+
_cleanupIgnoredXpr(x.xpr, ignoredColumns, target, isOne)
|
|
520
|
+
continue
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
if (x.ref && ignoredColumns.includes(x.ref[0])) {
|
|
524
|
+
cds.error(
|
|
525
|
+
`Property '${x.ref}' does not exist in type '${
|
|
526
|
+
isOne ? target.name.replace(`${target._service.name}.`, '') : target.name
|
|
527
|
+
}'`,
|
|
528
|
+
{
|
|
529
|
+
code: 400
|
|
530
|
+
}
|
|
531
|
+
)
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
if (x.func) {
|
|
535
|
+
_cleanupIgnoredXpr(x.args, ignoredColumns, target, isOne)
|
|
536
|
+
continue
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function _cleanupIgnored(SELECT, ignoredColumns, target, isOne) {
|
|
542
|
+
_cleanupIgnoredXpr(SELECT.columns, ignoredColumns, target, isOne)
|
|
543
|
+
_cleanupIgnoredXpr(SELECT.orderBy, ignoredColumns, target, isOne)
|
|
544
|
+
_cleanupIgnoredXpr(SELECT.where, ignoredColumns, target, isOne)
|
|
545
|
+
_cleanupIgnoredXpr(SELECT.groupBy, ignoredColumns, target, isOne)
|
|
546
|
+
_cleanupIgnoredXpr(SELECT.having, ignoredColumns, target, isOne)
|
|
547
|
+
}
|
|
548
|
+
|
|
516
549
|
function _4service(service) {
|
|
517
550
|
const { namespace, model } = service
|
|
518
551
|
if (!model) return cqn => cqn
|
|
@@ -563,6 +596,12 @@ function _4service(service) {
|
|
|
563
596
|
*/
|
|
564
597
|
_processColumns(cqn, current)
|
|
565
598
|
|
|
599
|
+
const ignoredColumns = Object.values(root.elements ?? {})
|
|
600
|
+
.filter(element => element['@cds.api.ignore'])
|
|
601
|
+
.map(element => element.name)
|
|
602
|
+
|
|
603
|
+
_cleanupIgnored(cqn.SELECT, ignoredColumns, root, one)
|
|
604
|
+
|
|
566
605
|
return cqn
|
|
567
606
|
}
|
|
568
607
|
}
|
package/libx/odata/cqn2odata.js
CHANGED
|
@@ -177,7 +177,12 @@ function _xpr(expr, target, kind, isLambda) {
|
|
|
177
177
|
} else if (_isLambda(cur, expr[i + 1])) {
|
|
178
178
|
const { where, id } = expr[i + 1].ref.slice(-1)[0]
|
|
179
179
|
const nav = [...expr[i + 1].ref.slice(0, -1), id].join('/')
|
|
180
|
-
|
|
180
|
+
// odata-v2 does not support lambda expressions but successfactors allows filter like for to-one assocs
|
|
181
|
+
if (kind === 'odata-v2') {
|
|
182
|
+
cds.log('remote').info(`OData V2 does not support lambda expressions. Using path expression as best effort.`)
|
|
183
|
+
isLambda = false
|
|
184
|
+
res.push(`${id}%2F${_xpr(where, target, kind)}`)
|
|
185
|
+
} else if (!where) res.push(`${nav}/any()`)
|
|
181
186
|
else res.push(`${nav}/any(${LAMBDA_VARIABLE}:${_xpr(where, target, kind, true)})`)
|
|
182
187
|
i++
|
|
183
188
|
} else {
|
|
@@ -216,13 +221,6 @@ const _keysOfWhere = (where, kind, target) => {
|
|
|
216
221
|
const res = []
|
|
217
222
|
for (const cur of where) {
|
|
218
223
|
if (hasValidProps(cur, 'ref')) {
|
|
219
|
-
if (target && target._alias2ref) {
|
|
220
|
-
const alias = target._alias2ref.__2alias[cur.ref.join('/')]
|
|
221
|
-
if (alias) {
|
|
222
|
-
res.push(_format({ ref: [alias] }))
|
|
223
|
-
continue
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
224
|
res.push(_format(cur))
|
|
227
225
|
} else if (hasValidProps(cur, 'val')) {
|
|
228
226
|
// find previous ref
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
const cds = require('../../')
|
|
2
|
+
const { odataError } = require('./utils')
|
|
3
|
+
const { INSERT } = require('../../lib/ql/cds-ql')
|
|
4
|
+
const { readAfterWrite } = require('../_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite')
|
|
5
|
+
const metaInfo = require('../_runtime/cds-services/adapter/odata-v4/utils/metaInfo')
|
|
6
|
+
const { toODataResult } = require('./result')
|
|
7
|
+
|
|
8
|
+
module.exports = srv =>
|
|
9
|
+
function create(req, res, next) {
|
|
10
|
+
const query = cds.odata.parse(req.url, { service: srv, baseUrl: req.baseUrl })
|
|
11
|
+
|
|
12
|
+
const {
|
|
13
|
+
SELECT: { one }
|
|
14
|
+
} = query
|
|
15
|
+
|
|
16
|
+
if (one) {
|
|
17
|
+
const singleton = query.target._isSingleton
|
|
18
|
+
const error = odataError('405', `Method ${req.method} not allowed for ${singleton ? 'SINGLETON' : 'ENTITY'}`)
|
|
19
|
+
return res.status(405).json(error)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const queryPathXpr = query.SELECT?.from
|
|
23
|
+
|
|
24
|
+
const insertQuery = INSERT.into(queryPathXpr).entries(req.body)
|
|
25
|
+
|
|
26
|
+
const cdsReq = new cds.Request({ query: insertQuery })
|
|
27
|
+
return srv
|
|
28
|
+
.dispatch(cdsReq)
|
|
29
|
+
.then(async result => {
|
|
30
|
+
if (cdsReq._.readAfterWrite) {
|
|
31
|
+
// TODO see if in old odata impl for other checks that should happen
|
|
32
|
+
result = await readAfterWrite(cdsReq, srv, { operation: { result } })
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (result == null) {
|
|
36
|
+
res.status(204)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const info = metaInfo(insertQuery, 'CREATE', srv, req.body, req, false)
|
|
40
|
+
|
|
41
|
+
return res.status(201).send(toODataResult(result, info))
|
|
42
|
+
})
|
|
43
|
+
.catch(next)
|
|
44
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const cds = require('../../')
|
|
2
|
+
const { odataError } = require('./utils')
|
|
3
|
+
|
|
4
|
+
module.exports = srv =>
|
|
5
|
+
function deleete(req, res, next) {
|
|
6
|
+
const query = cds.odata.parse(req.url, { service: srv, baseUrl: req.baseUrl })
|
|
7
|
+
|
|
8
|
+
let {
|
|
9
|
+
SELECT: { one }
|
|
10
|
+
} = query
|
|
11
|
+
|
|
12
|
+
if (!one) {
|
|
13
|
+
return res.status(405).json(odataError('405', `Method DELETE not allowed for ENTITY.COLLECTION`))
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const _target = query.SELECT && query.SELECT.from
|
|
17
|
+
const deleteQuery = DELETE.from(_target)
|
|
18
|
+
|
|
19
|
+
return srv
|
|
20
|
+
.run(deleteQuery)
|
|
21
|
+
.then(() => {
|
|
22
|
+
return res.send(204)
|
|
23
|
+
})
|
|
24
|
+
.catch(next)
|
|
25
|
+
}
|
package/libx/odata/error.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
const { normalizeError } = require('../_runtime/common/error/frontend')
|
|
2
|
+
const { odataError } = require('./utils')
|
|
3
|
+
|
|
4
|
+
module.exports = _srv => (err, req, res, _next) => {
|
|
5
|
+
const { error, statusCode } = normalizeError(err, req)
|
|
6
|
+
|
|
7
|
+
if (statusCode >= 400 && statusCode < 500) {
|
|
8
|
+
return res.status(statusCode).json(odataError(`${err.code}`, error.message))
|
|
4
9
|
}
|
|
5
10
|
|
|
6
11
|
return res.status(500).send('Internal Server Error')
|
package/libx/odata/metadata.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const cds = require('../../lib')
|
|
2
2
|
const LOG = cds.log('odata')
|
|
3
3
|
const crypto = require('crypto')
|
|
4
|
-
const {
|
|
4
|
+
const { odataError } = require('./utils')
|
|
5
5
|
|
|
6
6
|
const _requestedFormat = (queryOption, header) => {
|
|
7
7
|
if (queryOption) return queryOption.match(/json/i) ? 'json' : 'xml'
|
|
@@ -43,18 +43,16 @@ const generateEtag = s => {
|
|
|
43
43
|
return `W/"${crypto.createHash('sha256').update(s).digest('base64')}"`
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
const odata_error = (code, message) => ({ error: { code, message } })
|
|
47
|
-
|
|
48
46
|
const mpSupportsEmptyLocale = () => {
|
|
49
47
|
const pkg = require(require.resolve('@sap/cds-mtxs/package.json'))
|
|
50
48
|
const [major, minor] = pkg.version.split('.').map(Number)
|
|
51
|
-
return major > 1 || major === 1 && minor >= 12
|
|
49
|
+
return major > 1 || (major === 1 && minor >= 12)
|
|
52
50
|
}
|
|
53
51
|
|
|
54
52
|
module.exports = srv =>
|
|
55
|
-
async function metadata(req, res,
|
|
53
|
+
async function metadata(req, res, _next) {
|
|
56
54
|
if (req.method !== 'GET')
|
|
57
|
-
return res.status(405).json(
|
|
55
|
+
return res.status(405).json(odataError('METHOD_NOT_ALLOWED', `Method ${req.method} not allowed for $metadata.`))
|
|
58
56
|
|
|
59
57
|
const tenant = cds.context.tenant
|
|
60
58
|
const locale = cds.context.locale
|
|
@@ -82,7 +80,7 @@ module.exports = srv =>
|
|
|
82
80
|
res
|
|
83
81
|
.status(400)
|
|
84
82
|
.json(
|
|
85
|
-
|
|
83
|
+
odataError(
|
|
86
84
|
'UNSUPPORTED_METADATA_TYPE',
|
|
87
85
|
'JSON metadata is not supported if cds.requires.extensibilty: true.'
|
|
88
86
|
)
|
|
@@ -113,7 +111,7 @@ module.exports = srv =>
|
|
|
113
111
|
LOG.error(e)
|
|
114
112
|
}
|
|
115
113
|
|
|
116
|
-
return res.status(503).json(
|
|
114
|
+
return res.status(503).json(odataError('SERVICE_UNAVAILABLE', 'Service unavailable'))
|
|
117
115
|
}
|
|
118
116
|
}
|
|
119
117
|
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
const cds = require('../../')
|
|
2
|
+
const metaInfo = require('../_runtime/cds-services/adapter/odata-v4/utils/metaInfo')
|
|
3
|
+
const { readAfterWrite } = require('../_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite')
|
|
4
|
+
const { where2obj } = require('../_runtime/common/utils/cqn')
|
|
5
|
+
const { toODataResult } = require('./result')
|
|
6
|
+
const { odataError } = require('./utils')
|
|
7
|
+
|
|
8
|
+
const _isUpsertAllowed = ({ target, data, event }) => {
|
|
9
|
+
return (
|
|
10
|
+
!(cds.env.runtime && cds.env.runtime.allow_upsert === false) &&
|
|
11
|
+
!(target && target._isDraftEnabled && (!cds.env.fiori.lean_draft || (!data.IsActiveEntity && event === 'PATCH')))
|
|
12
|
+
)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const _isNavigationWithKeyInParent = (keys, data, pathExpression, model) => {
|
|
16
|
+
// keys not in data
|
|
17
|
+
if (keys && Object.keys(keys).some(key => key in data)) {
|
|
18
|
+
return false
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const nav = pathExpression.ref && pathExpression.ref.length !== 0 && pathExpression.ref[1]
|
|
22
|
+
const parent = pathExpression.ref && pathExpression.ref[0].id
|
|
23
|
+
|
|
24
|
+
// not a navigation
|
|
25
|
+
if (!parent || !nav) {
|
|
26
|
+
return false
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const navID = typeof nav === 'string' ? nav : nav.id
|
|
30
|
+
const navElement = model.definitions[parent].elements[navID]
|
|
31
|
+
|
|
32
|
+
// not a containment
|
|
33
|
+
if (!navElement._isContained) {
|
|
34
|
+
return false
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const where = pathExpression.ref[0].where
|
|
38
|
+
return parent && navElement && where
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
module.exports = srv =>
|
|
42
|
+
function update(req, res, next) {
|
|
43
|
+
const query = cds.odata.parse(req.url, { service: srv, baseUrl: req.baseUrl })
|
|
44
|
+
|
|
45
|
+
const {
|
|
46
|
+
SELECT: { one }
|
|
47
|
+
} = query
|
|
48
|
+
|
|
49
|
+
if (!one) {
|
|
50
|
+
return res.status(405).json(odataError('405', `Method ${req.method} not allowed for ENTITY.COLLECTION`))
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const queryPathXpr = query.SELECT && query.SELECT.from
|
|
54
|
+
|
|
55
|
+
const isPrimitive = query._propertyAccess
|
|
56
|
+
const data = isPrimitive ? { [query._propertyAccess]: req.body.value } : req.body
|
|
57
|
+
|
|
58
|
+
const updateQuery = UPDATE.entity(queryPathXpr).with(data)
|
|
59
|
+
|
|
60
|
+
// we need the cds request, so we can access req._.readAfterWrite
|
|
61
|
+
const cdsReq = new cds.Request({ query: updateQuery })
|
|
62
|
+
|
|
63
|
+
const info = metaInfo(query, 'UPDATE', srv, data, req, false)
|
|
64
|
+
|
|
65
|
+
if (!isPrimitive && queryPathXpr.ref?.[queryPathXpr.ref.length - 1].where) {
|
|
66
|
+
Object.assign(data, where2obj(queryPathXpr.ref?.[queryPathXpr.ref.length - 1].where))
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return srv
|
|
70
|
+
.dispatch(cdsReq)
|
|
71
|
+
.then(async result => {
|
|
72
|
+
if (!isPrimitive && cdsReq._.readAfterWrite) {
|
|
73
|
+
// TODO see if in old odata impl for other checks that should happen
|
|
74
|
+
result = await readAfterWrite(cdsReq, srv, { operation: { result } })
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (result == null) {
|
|
78
|
+
res.status(204)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return res.send(toODataResult(result, info))
|
|
82
|
+
})
|
|
83
|
+
.catch(async e => {
|
|
84
|
+
// UPSERT
|
|
85
|
+
const is404 = e.code === 404 || e.status === 404 || e.statusCode === 404
|
|
86
|
+
if (is404 && !isPrimitive && _isUpsertAllowed({ target: query.target, data: req.body, event: req.method })) {
|
|
87
|
+
// PUT / PATCH with if-match header means "only if already exists", i.e., no insert if not
|
|
88
|
+
if (req.headers['if-match']) throw Object.assign(new Error('412'), { statusCode: 412 })
|
|
89
|
+
|
|
90
|
+
if (_isNavigationWithKeyInParent(query.target.keys, data, queryPathXpr, srv.model)) {
|
|
91
|
+
// REVISIT better error message
|
|
92
|
+
return res.status(422).json(odataError('422', `Unprocessable Entity`))
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// REVISIT: up_XX needs to be looked up -> composition of aspect
|
|
96
|
+
const insertQuery = INSERT.into(queryPathXpr).entries(data)
|
|
97
|
+
const cdsReq = new cds.Request({ query: insertQuery })
|
|
98
|
+
let result = await srv.dispatch(cdsReq)
|
|
99
|
+
|
|
100
|
+
if (cdsReq._.readAfterWrite) {
|
|
101
|
+
// TODO see if in old odata impl for other checks that should happen
|
|
102
|
+
result = await readAfterWrite(cdsReq, srv, { operation: { result } })
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return res.status(201).send(toODataResult(result, info))
|
|
106
|
+
}
|
|
107
|
+
throw e
|
|
108
|
+
})
|
|
109
|
+
.catch(next)
|
|
110
|
+
}
|
package/libx/odata/utils.js
CHANGED
|
@@ -3,18 +3,20 @@ const cds = require('../_runtime/cds')
|
|
|
3
3
|
|
|
4
4
|
const MATH_FUNC = { round: 1, floor: 1, ceiling: 1 }
|
|
5
5
|
|
|
6
|
+
const odataError = (code, message) => ({ error: { code, message } })
|
|
7
|
+
|
|
6
8
|
const getSafeNumber = inputString => {
|
|
7
9
|
if (typeof inputString !== 'string') return inputString
|
|
8
10
|
// Try to parse the input string as a floating-point number using parseFloat
|
|
9
11
|
const parsedFloat = parseFloat(inputString)
|
|
10
|
-
|
|
12
|
+
|
|
11
13
|
// Check if the parsed value is not NaN and is equal to the original input string
|
|
12
14
|
if (!isNaN(parsedFloat) && String(parsedFloat) === inputString) {
|
|
13
15
|
return parsedFloat
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
+
}
|
|
17
|
+
|
|
16
18
|
// Try to parse the input string as an integer using parseInt
|
|
17
|
-
const parsedInt = parseInt(inputString)
|
|
19
|
+
const parsedInt = parseInt(inputString)
|
|
18
20
|
// special case like '3.00000000000001', the precision is not lost and string is returned
|
|
19
21
|
if (!isNaN(parsedInt) && String(parsedInt) === inputString.replace(/^-?\d+\.0+$/, inputString.split('.')[0])) {
|
|
20
22
|
return parsedInt
|
|
@@ -156,7 +158,7 @@ const _v4 = (val, element) => {
|
|
|
156
158
|
const formatVal = (val, elementName, csnTarget, kind, func, literal) => {
|
|
157
159
|
if (val === null || val === 'null') return 'null'
|
|
158
160
|
if (typeof val === 'boolean') return val
|
|
159
|
-
if (typeof val === 'string' && literal === 'number'
|
|
161
|
+
if (typeof val === 'string' && literal === 'number') return `${val}`
|
|
160
162
|
if (typeof val === 'string') {
|
|
161
163
|
if (!csnTarget && UUID.test(val)) return kind === 'odata-v2' ? `guid'${val}'` : val
|
|
162
164
|
if (func in MATH_FUNC) return val
|
|
@@ -231,5 +233,6 @@ const skipToken = (token, cqn) => {
|
|
|
231
233
|
module.exports = {
|
|
232
234
|
getSafeNumber,
|
|
233
235
|
formatVal,
|
|
234
|
-
skipToken
|
|
236
|
+
skipToken,
|
|
237
|
+
odataError
|
|
235
238
|
}
|