@sap/cds 8.5.0 → 8.6.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 +54 -3
- package/_i18n/i18n.properties +4 -7
- package/eslint.config.mjs +1 -1
- package/lib/compile/etc/properties.js +2 -2
- package/lib/compile/for/java.js +15 -3
- package/lib/compile/for/lean_drafts.js +44 -34
- package/lib/compile/for/nodejs.js +19 -10
- package/lib/compile/minify.js +2 -4
- package/lib/compile/parse.js +106 -72
- package/lib/compile/to/edm.js +19 -9
- package/lib/compile/to/hana.js +25 -21
- package/lib/compile/to/sql.js +15 -8
- package/lib/core/linked-csn.js +10 -4
- package/lib/dbs/cds-deploy.js +2 -2
- package/lib/env/cds-env.js +76 -66
- package/lib/env/defaults.js +1 -0
- package/lib/i18n/bundles.js +2 -1
- package/lib/i18n/localize.js +2 -2
- package/lib/index.js +24 -18
- package/lib/ql/CREATE.js +11 -6
- package/lib/ql/DELETE.js +12 -9
- package/lib/ql/DROP.js +15 -8
- package/lib/ql/INSERT.js +19 -14
- package/lib/ql/SELECT.js +95 -168
- package/lib/ql/UPDATE.js +23 -14
- package/lib/ql/UPSERT.js +15 -2
- package/lib/ql/Whereable.js +44 -118
- package/lib/ql/cds-ql.js +222 -28
- package/lib/ql/{Query.js → cds.ql-Query.js} +52 -41
- package/lib/ql/cds.ql-predicates.js +133 -0
- package/lib/ql/cds.ql-projections.js +111 -0
- package/lib/ql/cqn.d.ts +146 -0
- package/lib/srv/cds-connect.js +3 -3
- package/lib/srv/cds-serve.js +2 -2
- package/lib/srv/cds.Service.js +132 -0
- package/lib/srv/{srv-api.js → cds.ServiceClient.js} +16 -71
- package/lib/srv/cds.ServiceProvider.js +20 -0
- package/lib/srv/factory.js +20 -8
- package/lib/srv/protocols/hcql.js +2 -3
- package/lib/srv/protocols/index.js +3 -3
- package/lib/srv/srv-dispatch.js +7 -6
- package/lib/srv/srv-handlers.js +103 -113
- package/lib/srv/srv-methods.js +14 -14
- package/lib/srv/srv-tx.js +5 -3
- package/lib/utils/cds-utils.js +2 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/error.js +3 -3
- package/libx/_runtime/cds.js +2 -1
- package/libx/_runtime/common/aspects/service.js +25 -0
- package/libx/_runtime/common/generic/auth/index.js +5 -0
- package/libx/_runtime/common/generic/auth/restrict.js +36 -14
- package/libx/_runtime/common/generic/auth/service.js +24 -0
- package/libx/_runtime/common/generic/auth/utils.js +14 -6
- package/libx/_runtime/common/generic/etag.js +1 -1
- package/libx/_runtime/common/utils/cqn.js +1 -2
- package/libx/_runtime/common/utils/cqn2cqn4sql.js +1 -1
- package/libx/_runtime/common/utils/generateOnCond.js +7 -3
- package/libx/_runtime/common/utils/postProcess.js +4 -1
- package/libx/_runtime/common/utils/restrictions.js +1 -0
- package/libx/_runtime/fiori/lean-draft.js +53 -42
- package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +1 -1
- package/libx/_runtime/remote/Service.js +2 -0
- package/libx/_runtime/remote/utils/client.js +12 -0
- package/libx/odata/ODataAdapter.js +2 -1
- package/libx/odata/index.js +5 -3
- package/libx/odata/middleware/batch.js +4 -0
- package/libx/odata/middleware/create.js +2 -2
- package/libx/odata/middleware/delete.js +2 -2
- package/libx/odata/middleware/operation.js +2 -2
- package/libx/odata/middleware/read.js +14 -12
- package/libx/odata/middleware/service-document.js +16 -8
- package/libx/odata/middleware/update.js +2 -2
- package/libx/odata/parse/afterburner.js +64 -30
- package/libx/odata/parse/grammar.peggy +95 -0
- package/libx/odata/parse/parser.js +1 -1
- package/libx/odata/utils/index.js +6 -1
- package/libx/odata/utils/metadata.js +69 -75
- package/libx/odata/utils/postProcess.js +24 -3
- package/package.json +1 -1
- package/server.js +1 -1
- package/lib/ql/parse.js +0 -36
- /package/lib/ql/{infer.js → cds.ql-infer.js} +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const cds = require('../../cds')
|
|
2
2
|
const { SELECT, INSERT, DELETE, UPDATE } = cds.ql
|
|
3
|
-
const Query = require('../../../../lib/ql/Query')
|
|
3
|
+
const Query = require('../../../../lib/ql/cds.ql-Query')
|
|
4
4
|
const { resolveView } = require('./resolveView')
|
|
5
5
|
const { ensureNoDraftsSuffix, getDraftColumnsCQNForDraft, ensureDraftsSuffix } = require('./draft')
|
|
6
6
|
const { flattenStructuredSelect, OPERATIONS_MAP } = require('./structured')
|
|
@@ -29,7 +29,7 @@ const _adaptRefs = (onCond, path, { select, join }) => {
|
|
|
29
29
|
return onCond.map(_adaptEl)
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
const replace$
|
|
32
|
+
const replace$selfAndAliasOnCond = (xpr, csnElement, aliases, path) => {
|
|
33
33
|
const selfIndex = xpr.findIndex(({ ref }) => ref?.[0] === '$self')
|
|
34
34
|
if (selfIndex != -1) {
|
|
35
35
|
let backLinkIndex
|
|
@@ -50,7 +50,7 @@ const replace$selfAndAliasOnCOnd = (xpr, csnElement, aliases, path) => {
|
|
|
50
50
|
for (let i = 0; i < xpr.length; i++) {
|
|
51
51
|
const element = xpr[i]
|
|
52
52
|
if (element.xpr) {
|
|
53
|
-
replace$
|
|
53
|
+
replace$selfAndAliasOnCond(element.xpr, csnElement, aliases, path)
|
|
54
54
|
continue
|
|
55
55
|
}
|
|
56
56
|
|
|
@@ -65,6 +65,10 @@ const replace$selfAndAliasOnCOnd = (xpr, csnElement, aliases, path) => {
|
|
|
65
65
|
element.ref = _toRef(undefined, element.ref.slice(0)).ref
|
|
66
66
|
continue
|
|
67
67
|
}
|
|
68
|
+
//no alias for special $now variable
|
|
69
|
+
if (element.ref[0] === '$now') {
|
|
70
|
+
continue
|
|
71
|
+
}
|
|
68
72
|
|
|
69
73
|
if (element.ref[0] === aliases.join || element.ref[0] === aliases.select) {
|
|
70
74
|
// nothing todo here, as already right alias
|
|
@@ -83,7 +87,7 @@ const _args = (csnElement, path, aliases) => {
|
|
|
83
87
|
if (!csnElement._isSelfManaged) return _adaptRefs(onCond, path, aliases)
|
|
84
88
|
|
|
85
89
|
const onCondCopy = JSON.parse(JSON.stringify(onCond))
|
|
86
|
-
replace$
|
|
90
|
+
replace$selfAndAliasOnCond(onCondCopy, csnElement, aliases, path)
|
|
87
91
|
|
|
88
92
|
return onCondCopy
|
|
89
93
|
}
|
|
@@ -73,7 +73,10 @@ const postProcess = (query, result, service, onlySelectAliases = false) => {
|
|
|
73
73
|
if (query.DELETE) return result
|
|
74
74
|
|
|
75
75
|
if (query.SELECT) {
|
|
76
|
-
if (query.SELECT.columns?.find(col => col.func === 'count' && col.as === '$count'))
|
|
76
|
+
if (query.SELECT.columns?.find(col => col.func === 'count' && col.as === '$count')) {
|
|
77
|
+
if (result[0] && '$count' in result[0]) return result
|
|
78
|
+
return [{ $count: result }]
|
|
79
|
+
}
|
|
77
80
|
|
|
78
81
|
handleAliasInResult(query.SELECT.columns, result)
|
|
79
82
|
|
|
@@ -3,6 +3,7 @@ const cds = require('../../cds')
|
|
|
3
3
|
|
|
4
4
|
const $cache = Symbol('service contains any restrictions cache')
|
|
5
5
|
|
|
6
|
+
/** @param {import('../../../../lib/srv/cds.Service')} srv */
|
|
6
7
|
const containsAnyRestrictions = srv => {
|
|
7
8
|
let { model } = srv
|
|
8
9
|
if (srv.isExtensible) model = cds.context?.model || srv.model // REVISIT: extensions are not allowed to add or change restrictions, are they?
|
|
@@ -197,13 +197,15 @@ const _lock = {
|
|
|
197
197
|
|
|
198
198
|
const _redirectRefToDrafts = (ref, model) => {
|
|
199
199
|
const [root, ...tail] = ref
|
|
200
|
-
const
|
|
200
|
+
const target = model.definitions[root.id || root]
|
|
201
|
+
const draft = target.drafts || target
|
|
201
202
|
return [root.id ? { ...root, id: draft.name } : draft.name, ...tail]
|
|
202
203
|
}
|
|
203
204
|
|
|
204
205
|
const _redirectRefToActives = (ref, model) => {
|
|
205
206
|
const [root, ...tail] = ref
|
|
206
|
-
const
|
|
207
|
+
const target = model.definitions[root.id || root]
|
|
208
|
+
const active = target.actives || target
|
|
207
209
|
return [root.id ? { ...root, id: active.name } : active.name, ...tail]
|
|
208
210
|
}
|
|
209
211
|
|
|
@@ -325,6 +327,11 @@ const h = cds.ApplicationService.prototype.handle
|
|
|
325
327
|
cds.ApplicationService.prototype.handle = async function (req) {
|
|
326
328
|
const handle = h.bind(this)
|
|
327
329
|
|
|
330
|
+
if (req.event === 'DISCARD') req.event = 'CANCEL'
|
|
331
|
+
if (req.event === 'SAVE') {
|
|
332
|
+
req.event = 'draftActivate'
|
|
333
|
+
req.query ??= SELECT.from(req.target, req.data) //> support simple srv.send('SAVE',entity,...)
|
|
334
|
+
}
|
|
328
335
|
if (
|
|
329
336
|
!req.query ||
|
|
330
337
|
// REVISIT: Currently all requests in an Object Page to nested composition targets, e.g. Incidents(ID)/conversation are also Draft Requests which seems wrong overkill -> is that required?
|
|
@@ -492,14 +499,14 @@ cds.ApplicationService.prototype.handle = async function (req) {
|
|
|
492
499
|
|
|
493
500
|
// It needs to be redirected to drafts
|
|
494
501
|
if (req.event === 'NEW' || req.event === 'CANCEL' || req.event === 'draftPrepare') {
|
|
495
|
-
req.target = req.target.drafts
|
|
502
|
+
if (!req.target.isDraft) req.target = req.target.drafts // COMPAT: also support these events for actives
|
|
496
503
|
|
|
497
|
-
if (query.INSERT
|
|
504
|
+
if (query.INSERT) {
|
|
498
505
|
if (typeof query.INSERT.into === 'string') query.INSERT.into = req.target.name
|
|
499
506
|
else if (query.INSERT.into.ref) query.INSERT.into.ref = _redirectRefToDrafts(query.INSERT.into.ref, this.model)
|
|
500
|
-
} else if (query.DELETE
|
|
507
|
+
} else if (query.DELETE) {
|
|
501
508
|
query.DELETE.from.ref = _redirectRefToDrafts(query.DELETE.from.ref, this.model)
|
|
502
|
-
} else if (query.SELECT
|
|
509
|
+
} else if (query.SELECT) {
|
|
503
510
|
query.SELECT.from.ref = _redirectRefToDrafts(query.SELECT.from.ref, this.model)
|
|
504
511
|
}
|
|
505
512
|
|
|
@@ -564,8 +571,7 @@ cds.ApplicationService.prototype.handle = async function (req) {
|
|
|
564
571
|
req.reject({ code: 428, statusCode: 428 })
|
|
565
572
|
}
|
|
566
573
|
|
|
567
|
-
const
|
|
568
|
-
const cols = expandStarStar(targetDraft, true)
|
|
574
|
+
const cols = expandStarStar(req.target.drafts, true)
|
|
569
575
|
|
|
570
576
|
// Use `run` (since also etags might need to be checked)
|
|
571
577
|
// REVISIT: Find a better approach (`etag` as part of CQN?)
|
|
@@ -1470,7 +1476,7 @@ function expandStarStar(target, draftActivate, recursion = new Map()) {
|
|
|
1470
1476
|
'NULL',
|
|
1471
1477
|
'then',
|
|
1472
1478
|
{ val: null },
|
|
1473
|
-
'else',
|
|
1479
|
+
'else',
|
|
1474
1480
|
{ val: '' },
|
|
1475
1481
|
'end'
|
|
1476
1482
|
],
|
|
@@ -1511,6 +1517,7 @@ async function onNew(req) {
|
|
|
1511
1517
|
|
|
1512
1518
|
if (req.target.actives['@Capabilities.InsertRestrictions.Insertable'] === false || req.target.actives['@readonly'])
|
|
1513
1519
|
req.reject({ code: 405, statusCode: 405 })
|
|
1520
|
+
req.query ??= INSERT.into(req.subject).entries(req.data || {}) //> support simple srv.send('NEW',entity,...)
|
|
1514
1521
|
const isDirectAccess = typeof req.query.INSERT.into === 'string' || req.query.INSERT.into.ref?.length === 1
|
|
1515
1522
|
// Only allowed for pseudo draft roots (entities with this action)
|
|
1516
1523
|
if (isDirectAccess && !req.target.actives['@Common.DraftRoot.ActivationAction'])
|
|
@@ -1592,8 +1599,10 @@ async function onNew(req) {
|
|
|
1592
1599
|
async function onEdit(req) {
|
|
1593
1600
|
LOG.debug('edit active')
|
|
1594
1601
|
|
|
1602
|
+
req.query ??= SELECT.from(req.target, req.data).where({ IsActiveEntity: true }) //> support simple srv.send('EDIT',entity,...)
|
|
1603
|
+
|
|
1595
1604
|
// use symbol for _draftParams
|
|
1596
|
-
const draftParams = req.query[DRAFT_PARAMS]
|
|
1605
|
+
const draftParams = req.query[DRAFT_PARAMS] || { IsActiveEntity: true } // REVISIT: can draftParams in the edit caser ever be undefined or other than IsActiveEntity=true ?
|
|
1597
1606
|
if (req.query.SELECT.from.ref.length > 1 || draftParams.IsActiveEntity !== true) {
|
|
1598
1607
|
req.reject({
|
|
1599
1608
|
code: 400,
|
|
@@ -1692,7 +1701,7 @@ async function onEdit(req) {
|
|
|
1692
1701
|
}
|
|
1693
1702
|
|
|
1694
1703
|
const cqns = [
|
|
1695
|
-
cds.env.fiori.read_actives_from_db ?
|
|
1704
|
+
cds.env.fiori.read_actives_from_db ? cds.db.run(selectActiveCQN) : this.run(selectActiveCQN),
|
|
1696
1705
|
existingDraft
|
|
1697
1706
|
]
|
|
1698
1707
|
|
|
@@ -1713,7 +1722,7 @@ async function onEdit(req) {
|
|
|
1713
1722
|
|
|
1714
1723
|
;[res, draft] = await _promiseAll([
|
|
1715
1724
|
// REVISIT: inofficial compat flag just in case it breaks something -> do not document
|
|
1716
|
-
cds.env.fiori.read_actives_from_db ?
|
|
1725
|
+
cds.env.fiori.read_actives_from_db ? cds.db.run(selectActiveCQN) : this.run(selectActiveCQN),
|
|
1717
1726
|
// no user check must be done here...
|
|
1718
1727
|
existingDraft
|
|
1719
1728
|
])
|
|
@@ -1747,13 +1756,12 @@ async function onEdit(req) {
|
|
|
1747
1756
|
InProcessByUser: req.user.id
|
|
1748
1757
|
})
|
|
1749
1758
|
|
|
1750
|
-
const targetDraft = req.target.drafts
|
|
1751
1759
|
// is set to `null` on srv layer
|
|
1752
1760
|
res.DraftAdministrativeData_DraftUUID = DraftUUID
|
|
1753
1761
|
res.HasActiveEntity = true
|
|
1754
1762
|
delete res.DraftAdministrativeData
|
|
1755
1763
|
// change to db run
|
|
1756
|
-
await INSERT.into(
|
|
1764
|
+
await INSERT.into(req.target.drafts).entries(res)
|
|
1757
1765
|
|
|
1758
1766
|
// REVISIT: we need to use okra API here because it must be set in the batched request
|
|
1759
1767
|
// status code must be set in handler to allow overriding for FE V2
|
|
@@ -1783,8 +1791,9 @@ async function onEdit(req) {
|
|
|
1783
1791
|
async function onCancel(req) {
|
|
1784
1792
|
LOG.debug('delete draft')
|
|
1785
1793
|
|
|
1794
|
+
req.query ??= DELETE(req.target, req.data) //> support simple srv.send('CANCEL',entity,...)
|
|
1786
1795
|
const activeRef = _redirectRefToActives(req.query.DELETE.from.ref, this.model)
|
|
1787
|
-
const draftParams = req.query[DRAFT_PARAMS]
|
|
1796
|
+
const draftParams = req.query[DRAFT_PARAMS] || { IsActiveEntity: false } // REVISIT: can draftParams in the cancel case ever be undefined or other than IsActiveEntity=false ?
|
|
1788
1797
|
|
|
1789
1798
|
const draftQuery = SELECT.one
|
|
1790
1799
|
.from({ ref: req.query.DELETE.from.ref })
|
|
@@ -1801,7 +1810,7 @@ async function onCancel(req) {
|
|
|
1801
1810
|
if (!cds.context.user._is_privileged && processByUser && processByUser !== cds.context.user.id)
|
|
1802
1811
|
req.reject({ code: 403, statusCode: 403, message: 'DRAFT_LOCKED_BY_ANOTHER_USER', args: [processByUser] })
|
|
1803
1812
|
}
|
|
1804
|
-
const draftDeleteQuery = DELETE.from({ ref: req.query.DELETE.from.ref })
|
|
1813
|
+
const draftDeleteQuery = DELETE.from({ ref: req.query.DELETE.from.ref }) // REVISIT: Isn't that == req.query ?
|
|
1805
1814
|
const queries = !draft
|
|
1806
1815
|
? []
|
|
1807
1816
|
: [draftParams.IsActiveEntity === false ? this.run(draftDeleteQuery, req.data) : draftDeleteQuery]
|
|
@@ -1900,38 +1909,40 @@ const _readAfterDraftAction = async function ({ req, payload, action }) {
|
|
|
1900
1909
|
}
|
|
1901
1910
|
}
|
|
1902
1911
|
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
return this.send({ event: 'CANCEL', query: DELETE(entity, data) })
|
|
1912
|
-
}
|
|
1913
|
-
if (!this.edit)
|
|
1914
|
-
this.edit = function (entity, data) {
|
|
1915
|
-
if ((typeof entity === 'string' && entity.endsWith('.drafts')) || entity.isDraft)
|
|
1916
|
-
throw new Error('Action `edit` must be called on the active entity')
|
|
1917
|
-
return this.send({ event: 'EDIT', query: SELECT.from(entity, data).where({ IsActiveEntity: true }) })
|
|
1918
|
-
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ makes sure draftParams are set
|
|
1919
|
-
}
|
|
1920
|
-
if (!this.save)
|
|
1921
|
-
this.save = function (entity, data) {
|
|
1922
|
-
// a bit fishy to demand registering it on drafts since `SAVE` is usually just a shortcut for `['CREATE', 'UPDATE']`
|
|
1923
|
-
// and is typically registered for active entities. Hence, we allow to register it on both and redirect to drafts
|
|
1924
|
-
const _entity =
|
|
1925
|
-
typeof entity === 'string' ? (entity.endsWith('.drafts') ? entity : entity + '.drafts') : entity?.drafts
|
|
1926
|
-
return this.send({ event: 'draftActivate', query: SELECT.from(_entity, data) })
|
|
1912
|
+
// REVISIT: Looking at the simplified impls, I wonder if there's really much added convenience.
|
|
1913
|
+
// Even more as these events are very rarely sent from programmatic clients, if at all, but rather from Fiori clients only.
|
|
1914
|
+
cds.extend(cds.ApplicationService).with(
|
|
1915
|
+
class {
|
|
1916
|
+
new(draft, key) {
|
|
1917
|
+
return {
|
|
1918
|
+
then: (r, e) => this.send('NEW', draft, key).then(r, e),
|
|
1919
|
+
for: key => this.send('EDIT', typeof draft === 'string' ? draft.replace(/\.drafts$/, '') : draft.actives, key)
|
|
1927
1920
|
}
|
|
1921
|
+
}
|
|
1922
|
+
edit(active, key) {
|
|
1923
|
+
return this.send('EDIT', active, key)
|
|
1924
|
+
}
|
|
1925
|
+
save(draft, key) {
|
|
1926
|
+
return this.send('SAVE', draft, key)
|
|
1927
|
+
}
|
|
1928
|
+
cancel(draft, key) {
|
|
1929
|
+
return this.send('CANCEL', draft, key)
|
|
1930
|
+
}
|
|
1931
|
+
discard(draft, key) {
|
|
1932
|
+
return this.send('DISCARD', draft, key)
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
)
|
|
1928
1936
|
|
|
1929
|
-
|
|
1937
|
+
module.exports = {
|
|
1938
|
+
impl() {
|
|
1939
|
+
// REVISIT: don't pollute services... -> do we really need this?
|
|
1940
|
+
Object.defineProperty(this, '_datasource', { value: cds.db })
|
|
1930
1941
|
|
|
1931
1942
|
function _wrapped(handler, isActiveEntity) {
|
|
1932
1943
|
const fn = function (req, next) {
|
|
1933
1944
|
if (!req.target?.drafts || (isActiveEntity && req.target.isDraft) || (!isActiveEntity && !req.target.isDraft))
|
|
1934
|
-
return next
|
|
1945
|
+
return next?.()
|
|
1935
1946
|
return handler.call(this, req, next)
|
|
1936
1947
|
}
|
|
1937
1948
|
if (handler._initial) fn._initial = true
|
|
@@ -19,7 +19,7 @@ class EndpointRegistry {
|
|
|
19
19
|
cds.app.use(basePath, cds.middlewares.context())
|
|
20
20
|
cds.app.use(basePath, jwt_auth(cds.requires.auth))
|
|
21
21
|
cds.app.use(basePath, (err, req, res, next) => {
|
|
22
|
-
if (err === 401) res.
|
|
22
|
+
if (err === 401) res.sendStatus(401)
|
|
23
23
|
else next(err)
|
|
24
24
|
})
|
|
25
25
|
}
|
|
@@ -275,6 +275,8 @@ class RemoteService extends cds.Service {
|
|
|
275
275
|
result = typeof query === 'object' && query.SELECT?.one && Array.isArray(result) ? result[0] : result
|
|
276
276
|
return result
|
|
277
277
|
})
|
|
278
|
+
|
|
279
|
+
return super.init()
|
|
278
280
|
}
|
|
279
281
|
|
|
280
282
|
// Overload .handle in order to resolve projections up to a definition that is known by the remote service instance.
|
|
@@ -33,9 +33,21 @@ const _executeHttpRequest = async ({ requestConfig, destination, destinationOpti
|
|
|
33
33
|
if (jwt) destination.headers.authorization = `Bearer ${jwt}`
|
|
34
34
|
else LOG._warn && LOG.warn('Missing JWT token for forwardAuthToken')
|
|
35
35
|
}
|
|
36
|
+
|
|
36
37
|
// Cloud SDK throws error if useCache is activated and jwt is undefined
|
|
37
38
|
if (destination.jwt === undefined) delete destination.useCache
|
|
38
39
|
|
|
40
|
+
// prevent token exchange if ias token for technical user (unofficial!!!)
|
|
41
|
+
if (cds.env.features.remote_prevent_token_exchange) {
|
|
42
|
+
try {
|
|
43
|
+
const jsonwebtoken = require('jsonwebtoken')
|
|
44
|
+
const decoded = jsonwebtoken.decode(jwt)
|
|
45
|
+
if (decoded.app_tid && decoded.azp && decoded.azp === decoded.sub) destination.iasToXsuaaTokenExchange = false
|
|
46
|
+
} catch (error) {
|
|
47
|
+
LOG._debug && LOG.debug('Preventing token exchange failed:', error)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
39
51
|
if (LOG._debug) {
|
|
40
52
|
const req2log = { headers: _sanitizeHeaders({ ...requestConfig.headers }) }
|
|
41
53
|
if (requestConfig.method !== 'GET' && requestConfig.method !== 'DELETE')
|
|
@@ -96,7 +96,8 @@ class ODataAdapter extends HttpAdapter {
|
|
|
96
96
|
return jsonBodyParser(req, res, next)
|
|
97
97
|
})
|
|
98
98
|
// batch
|
|
99
|
-
.
|
|
99
|
+
// .all is used deliberately instead of .use so that the matched path is not stripped from req properties
|
|
100
|
+
.all('/\\$batch', require('./middleware/batch')(this))
|
|
100
101
|
// handle
|
|
101
102
|
// REVISIT: with old adapter, we return 405 for HEAD requests -> check OData spec
|
|
102
103
|
.head('*', (_, res) => res.sendStatus(405))
|
package/libx/odata/index.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/** @typedef {import('../../lib/srv/cds.Service')} Service */
|
|
2
|
+
|
|
1
3
|
const cds = require('../../')
|
|
2
4
|
const { SELECT } = cds.ql
|
|
3
5
|
const { decodeURIComponent } = cds.utils
|
|
@@ -79,10 +81,10 @@ const _2query = cqn => {
|
|
|
79
81
|
|
|
80
82
|
const enhanceCqn = (cqn, options) => {
|
|
81
83
|
if (options.afterburner) {
|
|
82
|
-
|
|
83
|
-
let { model,
|
|
84
|
+
/** @type Service */ const service = options.service
|
|
85
|
+
let { model, namespace } = service // prettier-ignore
|
|
84
86
|
if (service.isExtensible) model = cds.context?.model || model
|
|
85
|
-
cqn = options.afterburner(cqn, model, namespace, protocol)
|
|
87
|
+
cqn = options.afterburner(cqn, model, namespace, options.protocol)
|
|
86
88
|
}
|
|
87
89
|
|
|
88
90
|
const query = _2query(cqn)
|
|
@@ -561,6 +561,10 @@ module.exports = adapter => {
|
|
|
561
561
|
})
|
|
562
562
|
|
|
563
563
|
return function odata_batch(req, res, next) {
|
|
564
|
+
if (req.method !== 'POST') {
|
|
565
|
+
throw cds.error(`Method ${req.method} is not allowed for calls to $batch endpoint`, { code: 405 })
|
|
566
|
+
}
|
|
567
|
+
|
|
564
568
|
if (req.headers['content-type'].includes('application/json')) {
|
|
565
569
|
return _processBatch(service, router, req, res, next)
|
|
566
570
|
}
|
|
@@ -90,8 +90,8 @@ module.exports = (adapter, isUpsert) => {
|
|
|
90
90
|
handleSapMessages(cdsReq, req, res)
|
|
91
91
|
|
|
92
92
|
// REVISIT: invoke service.on('error') for failed batch subrequests
|
|
93
|
-
if (cdsReq.http.req.path.startsWith('/$batch') && service.
|
|
94
|
-
for (const each of service.
|
|
93
|
+
if (cdsReq.http.req.path.startsWith('/$batch') && service.handlers._error.length) {
|
|
94
|
+
for (const each of service.handlers._error) each.handler.call(service, err, cdsReq)
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
next(err)
|
|
@@ -65,8 +65,8 @@ module.exports = adapter => {
|
|
|
65
65
|
handleSapMessages(cdsReq, req, res)
|
|
66
66
|
|
|
67
67
|
// REVISIT: invoke service.on('error') for failed batch subrequests
|
|
68
|
-
if (cdsReq.http.req.path.startsWith('/$batch') && service.
|
|
69
|
-
for (const each of service.
|
|
68
|
+
if (cdsReq.http.req.path.startsWith('/$batch') && service.handlers._error.length) {
|
|
69
|
+
for (const each of service.handlers._error) each.handler.call(service, err, cdsReq)
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
next(err)
|
|
@@ -146,8 +146,8 @@ module.exports = adapter => {
|
|
|
146
146
|
handleSapMessages(cdsReq, req, res)
|
|
147
147
|
|
|
148
148
|
// REVISIT: invoke service.on('error') for failed batch subrequests
|
|
149
|
-
if (cdsReq.http.req.path.startsWith('/$batch') && service.
|
|
150
|
-
for (const each of service.
|
|
149
|
+
if (cdsReq.http.req.path.startsWith('/$batch') && service.handlers._error.length) {
|
|
150
|
+
for (const each of service.handlers._error) each.handler.call(service, err, cdsReq)
|
|
151
151
|
}
|
|
152
152
|
|
|
153
153
|
next(err)
|
|
@@ -101,11 +101,11 @@ const _isToOneAssoc = query =>
|
|
|
101
101
|
query.SELECT.from.ref.length > 1 && typeof query.SELECT.from.ref.slice(-1)[0] === 'string'
|
|
102
102
|
|
|
103
103
|
const _count = result => {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
104
|
+
if (Array.isArray(result))
|
|
105
|
+
return result.reduce((acc, val) => {
|
|
106
|
+
return acc + (val?.$count ?? val?._counted_ ?? (Array.isArray(val) && _count(val))) || 0
|
|
107
|
+
}, 0)
|
|
108
|
+
else return result.$count ?? result._counted_ ?? 0
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
// REVISIT: integrate with default handler
|
|
@@ -136,14 +136,16 @@ const _handleArrayOfQueriesFactory = adapter => {
|
|
|
136
136
|
result: result[0],
|
|
137
137
|
isCollection: !req._query[0].SELECT.one
|
|
138
138
|
})
|
|
139
|
-
|
|
139
|
+
// Skip first query, as its context is represented in the main context
|
|
140
|
+
for (let i = 1; i < result.length; i++) {
|
|
140
141
|
const { context: subOdataContext } = getODataMetadata(req._query[i], {
|
|
141
142
|
result: result[i],
|
|
142
143
|
isCollection: !req._query[i].SELECT.one
|
|
143
144
|
})
|
|
144
145
|
// Add OData context, if it deviates from main context
|
|
145
|
-
if (
|
|
146
|
-
|
|
146
|
+
if (mainOdataContext !== subOdataContext) {
|
|
147
|
+
// OData spec: "If present, the context control information MUST be the first property in the JSON object."
|
|
148
|
+
result[i] = result[i].map(entry => ({ '@odata.context': subOdataContext, ...entry }))
|
|
147
149
|
}
|
|
148
150
|
}
|
|
149
151
|
|
|
@@ -161,8 +163,8 @@ const _handleArrayOfQueriesFactory = adapter => {
|
|
|
161
163
|
handleSapMessages(cdsReq, req, res)
|
|
162
164
|
|
|
163
165
|
// REVISIT: invoke service.on('error') for failed batch subrequests
|
|
164
|
-
if (cdsReq.http.req.path.startsWith('/$batch') && service.
|
|
165
|
-
for (const each of service.
|
|
166
|
+
if (cdsReq.http.req.path.startsWith('/$batch') && service.handlers._error.length) {
|
|
167
|
+
for (const each of service.handlers._error) each.handler.call(service, err, cdsReq)
|
|
166
168
|
}
|
|
167
169
|
|
|
168
170
|
next(err)
|
|
@@ -277,8 +279,8 @@ module.exports = adapter => {
|
|
|
277
279
|
handleSapMessages(cdsReq, req, res)
|
|
278
280
|
|
|
279
281
|
// REVISIT: invoke service.on('error') for failed batch subrequests
|
|
280
|
-
if (cdsReq.http.req.path.startsWith('/$batch') && service.
|
|
281
|
-
for (const each of service.
|
|
282
|
+
if (cdsReq.http.req.path.startsWith('/$batch') && service.handlers._error.length) {
|
|
283
|
+
for (const each of service.handlers._error) each.handler.call(service, err, cdsReq)
|
|
282
284
|
}
|
|
283
285
|
|
|
284
286
|
next(err)
|
|
@@ -44,10 +44,21 @@ module.exports = adapter => {
|
|
|
44
44
|
|
|
45
45
|
const srvEntities = model.entities(service.definition.name)
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
47
|
+
const exposedEntities = []
|
|
48
|
+
for (const e in srvEntities) {
|
|
49
|
+
if (e === 'DraftAdministrativeData') continue
|
|
50
|
+
|
|
51
|
+
const entity = srvEntities[e]
|
|
52
|
+
if (entity['@cds.api.ignore']) continue
|
|
53
|
+
if (cds.env.effective.odata.containment && csnService._containedEntities.has(entity.name)) continue
|
|
54
|
+
|
|
55
|
+
const odataName = e.replace(/\./g, '_')
|
|
56
|
+
const obj = { name: odataName, url: odataName }
|
|
57
|
+
|
|
58
|
+
if (entity['@odata.singleton'] || entity['@odata.singleton.nullable']) obj.kind = 'Singleton'
|
|
59
|
+
|
|
60
|
+
exposedEntities.push(obj)
|
|
61
|
+
}
|
|
51
62
|
|
|
52
63
|
csnService.srvDocEtag = generateEtag(JSON.stringify(exposedEntities))
|
|
53
64
|
res.set('ETag', csnService.srvDocEtag)
|
|
@@ -60,10 +71,7 @@ module.exports = adapter => {
|
|
|
60
71
|
return res.json({
|
|
61
72
|
'@odata.context': odataContext,
|
|
62
73
|
'@odata.metadataEtag': csnService.srvDocEtag,
|
|
63
|
-
value: exposedEntities
|
|
64
|
-
const e_ = e.replace(/\./g, '_')
|
|
65
|
-
return { name: e_, url: e_ }
|
|
66
|
-
})
|
|
74
|
+
value: exposedEntities
|
|
67
75
|
})
|
|
68
76
|
}
|
|
69
77
|
}
|
|
@@ -153,8 +153,8 @@ module.exports = adapter => {
|
|
|
153
153
|
}
|
|
154
154
|
|
|
155
155
|
// REVISIT: invoke service.on('error') for failed batch subrequests
|
|
156
|
-
if (cdsReq.http.req.path.startsWith('/$batch') && service.
|
|
157
|
-
for (const each of service.
|
|
156
|
+
if (cdsReq.http.req.path.startsWith('/$batch') && service.handlers._error.length) {
|
|
157
|
+
for (const each of service.handlers._error) each.handler.call(service, err, cdsReq)
|
|
158
158
|
}
|
|
159
159
|
|
|
160
160
|
// continue with caught error
|