@sap/cds 5.8.0 → 5.8.3
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 +61 -4
- package/app/fiori/routes.js +3 -0
- package/bin/cds.js +7 -3
- package/bin/serve.js +2 -2
- package/lib/deploy.js +1 -1
- package/lib/log/format/kibana.js +3 -3
- package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +1 -3
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +13 -0
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/ResourcePathParser.js +23 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/UriHelper.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/deserializer/ResourceJsonDeserializer.js +5 -6
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/serializer/ContextURLFactory.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/serializer/ErrorJsonSerializer.js +2 -0
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/utils/UriHelper.js +4 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/stream.js +40 -5
- package/libx/_runtime/common/composition/index.js +1 -2
- package/libx/_runtime/common/composition/insert.js +3 -16
- package/libx/_runtime/common/composition/tree.js +1 -1
- package/libx/_runtime/common/error/frontend.js +2 -3
- package/libx/_runtime/common/i18n/index.js +2 -31
- package/libx/_runtime/common/utils/csn.js +15 -3
- package/libx/_runtime/common/utils/foreignKeyPropagations.js +9 -6
- package/libx/_runtime/common/utils/generateOnCond.js +5 -5
- package/libx/_runtime/common/utils/structured.js +10 -4
- package/libx/_runtime/db/expand/expandCQNToJoin.js +59 -20
- package/libx/_runtime/db/query/insert.js +1 -2
- package/libx/_runtime/db/sql-builder/SelectBuilder.js +7 -3
- package/libx/_runtime/db/utils/deep.js +10 -6
- package/libx/_runtime/fiori/generic/read.js +1 -3
- package/libx/_runtime/fiori/utils/handler.js +1 -11
- package/libx/_runtime/hana/conversion.js +2 -1
- package/libx/_runtime/hana/customBuilder/CustomSelectBuilder.js +1 -1
- package/libx/_runtime/hana/dynatrace.js +11 -5
- package/libx/_runtime/hana/execute.js +105 -8
- package/libx/_runtime/hana/search2cqn4sql.js +1 -4
- package/libx/_runtime/remote/utils/client.js +13 -4
- package/libx/_runtime/remote/utils/data.js +2 -1
- package/libx/gql/resolvers/crud/create.js +6 -1
- package/libx/gql/resolvers/crud/delete.js +6 -1
- package/libx/gql/resolvers/crud/read.js +6 -1
- package/libx/gql/resolvers/crud/update.js +11 -5
- package/package.json +1 -1
|
@@ -99,6 +99,7 @@ const _getVal = (data, name) => {
|
|
|
99
99
|
|
|
100
100
|
const _filterForStructProperty = (structElement, structData, op, prefix = '', nav = []) => {
|
|
101
101
|
const filterArray = []
|
|
102
|
+
const andOr = op === '!=' ? 'or' : 'and'
|
|
102
103
|
|
|
103
104
|
for (const elementName in structElement.elements) {
|
|
104
105
|
const element = structElement.elements[elementName]
|
|
@@ -123,8 +124,8 @@ const _filterForStructProperty = (structElement, structData, op, prefix = '', na
|
|
|
123
124
|
for (const key in assoc._target.keys) {
|
|
124
125
|
if (element.name === `${assocName}_${key}`) {
|
|
125
126
|
const ref = [`${prefix}_${assocName}_${key}`]
|
|
126
|
-
const val = _getVal(structData[assocName], key)
|
|
127
|
-
filterArray.push({ ref }, op, { val },
|
|
127
|
+
const val = _getVal(structData && structData[assocName], key)
|
|
128
|
+
filterArray.push({ ref }, op, { val }, andOr)
|
|
128
129
|
}
|
|
129
130
|
}
|
|
130
131
|
}
|
|
@@ -134,7 +135,7 @@ const _filterForStructProperty = (structElement, structData, op, prefix = '', na
|
|
|
134
135
|
{ ref: [...nav, `${prefix}_${element.name}`] },
|
|
135
136
|
op,
|
|
136
137
|
{ val: _getVal(structData, element.name) },
|
|
137
|
-
|
|
138
|
+
andOr
|
|
138
139
|
)
|
|
139
140
|
}
|
|
140
141
|
}
|
|
@@ -182,7 +183,12 @@ const _transformStructToFlatWhereHaving = ([first, op, second], resArray, struct
|
|
|
182
183
|
} else {
|
|
183
184
|
// transform complex structured to multiple single structured
|
|
184
185
|
const { nestedElement, prefix } = _nestedStructElement(structProperties, structElement)
|
|
185
|
-
|
|
186
|
+
const filterForStructProperty = _filterForStructProperty(nestedElement, structData, op, prefix, nav)
|
|
187
|
+
if (filterForStructProperty.length) {
|
|
188
|
+
filterForStructProperty.pop() // last and/or
|
|
189
|
+
if (op === '!=') resArray.push('(', ...filterForStructProperty, ')')
|
|
190
|
+
else resArray.push(...filterForStructProperty)
|
|
191
|
+
}
|
|
186
192
|
}
|
|
187
193
|
|
|
188
194
|
if (resArray[resArray.length - 1] === 'and') {
|
|
@@ -520,6 +520,17 @@ class JoinCQNFromExpanded {
|
|
|
520
520
|
|
|
521
521
|
// REVISIT required for other cqn properties as well?
|
|
522
522
|
this.adjustOrderBy(readToOneCQN.orderBy, mappings, column, tableAlias)
|
|
523
|
+
|
|
524
|
+
// In case active parent entity has orderBy with draft specific columns we need to add them to parent CQN
|
|
525
|
+
if (
|
|
526
|
+
readToOneCQN[IS_ACTIVE] &&
|
|
527
|
+
readToOneCQN.orderBy &&
|
|
528
|
+
column.as &&
|
|
529
|
+
(column.as === 'IsActiveEntity' || column.as === 'HasActiveEntity' || column.as === 'HasDraftEntity')
|
|
530
|
+
) {
|
|
531
|
+
readToOneCQNCopy.orderBy = readToOneCQN.orderBy
|
|
532
|
+
this._addColumnsInCaseOrderByHasDraft(readToOneCQNCopy, readToOneCQN.columns[readToOneCQN.columns.length - 1])
|
|
533
|
+
}
|
|
523
534
|
}
|
|
524
535
|
}
|
|
525
536
|
|
|
@@ -551,6 +562,14 @@ class JoinCQNFromExpanded {
|
|
|
551
562
|
}
|
|
552
563
|
}
|
|
553
564
|
|
|
565
|
+
_addColumnsInCaseOrderByHasDraft(readToOneCQNCopy, column) {
|
|
566
|
+
readToOneCQNCopy.orderBy.forEach(order => {
|
|
567
|
+
if (order.as === column.as) {
|
|
568
|
+
readToOneCQNCopy.columns.push(column)
|
|
569
|
+
}
|
|
570
|
+
})
|
|
571
|
+
}
|
|
572
|
+
|
|
554
573
|
/**
|
|
555
574
|
* Follow the tree to get to the relevant config object.
|
|
556
575
|
*
|
|
@@ -1132,28 +1151,37 @@ class JoinCQNFromExpanded {
|
|
|
1132
1151
|
}
|
|
1133
1152
|
}
|
|
1134
1153
|
}
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
each.
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1154
|
+
|
|
1155
|
+
if (!cqn[IS_ACTIVE]) {
|
|
1156
|
+
const ks = Object.keys(expandedEntity.keys).filter(
|
|
1157
|
+
c => !expandedEntity.keys[c].isAssociation && !DRAFT_COLUMNS.includes(c)
|
|
1158
|
+
)
|
|
1159
|
+
const user = (cds.context && cds.context.user && cds.context.user.id) || 'anonymous'
|
|
1160
|
+
const unionFrom = getCQNUnionFrom(cols, ref.replace(/_drafts$/, ''), ref, ks, user)
|
|
1161
|
+
for (const each of cqn.columns) {
|
|
1162
|
+
if (!each.as) continue
|
|
1163
|
+
// replace val with ref
|
|
1164
|
+
if (each.as === 'IsActiveEntity' || each.as === 'HasActiveEntity') {
|
|
1165
|
+
delete each.val
|
|
1166
|
+
each.ref = [tableAlias, each.as]
|
|
1167
|
+
each.as = tableAlias + '_' + each.as
|
|
1168
|
+
}
|
|
1169
|
+
// ensure the cast
|
|
1170
|
+
if (
|
|
1171
|
+
each.as.match(/IsActiveEntity$/) ||
|
|
1172
|
+
each.as.match(/HasActiveEntity$/) ||
|
|
1173
|
+
each.as.match(/HasDraftEntity$/)
|
|
1174
|
+
) {
|
|
1175
|
+
each.cast = { type: 'cds.Boolean' }
|
|
1176
|
+
}
|
|
1151
1177
|
}
|
|
1178
|
+
const cs = cqn.columns
|
|
1179
|
+
.filter(c => !c.expand && c.ref && c.ref[0] === tableAlias)
|
|
1180
|
+
.map(c => ({ ref: [c.ref[1]] }))
|
|
1181
|
+
const unionArgs = cqn.from.args
|
|
1182
|
+
unionArgs[0].SELECT = { columns: cs, from: unionFrom, distinct: true }
|
|
1183
|
+
delete unionArgs[0].ref
|
|
1152
1184
|
}
|
|
1153
|
-
const cs = cqn.columns.filter(c => !c.expand && c.ref && c.ref[0] === tableAlias).map(c => ({ ref: [c.ref[1]] }))
|
|
1154
|
-
const unionArgs = cqn.from.args
|
|
1155
|
-
unionArgs[0].SELECT = { columns: cs, from: unionFrom, distinct: true }
|
|
1156
|
-
delete unionArgs[0].ref
|
|
1157
1185
|
}
|
|
1158
1186
|
|
|
1159
1187
|
return cqn
|
|
@@ -1326,6 +1354,17 @@ class JoinCQNFromExpanded {
|
|
|
1326
1354
|
}
|
|
1327
1355
|
}
|
|
1328
1356
|
|
|
1357
|
+
if (readToOneCQN[IS_ACTIVE] && readToOneCQN.columns.length > 0) {
|
|
1358
|
+
readToOneCQN.columns.forEach(column => {
|
|
1359
|
+
if (
|
|
1360
|
+
column.as === `${parentAlias}_IsActiveEntity` ||
|
|
1361
|
+
column.as === `${parentAlias}_HasActiveEntity` ||
|
|
1362
|
+
column.as === `${parentAlias}_HasDraftEntity`
|
|
1363
|
+
)
|
|
1364
|
+
columns.push(column)
|
|
1365
|
+
})
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1329
1368
|
const subSelect = Object.assign({}, readToOneCQN, { columns })
|
|
1330
1369
|
|
|
1331
1370
|
const SELECT = { from: { SELECT: subSelect }, columns: outerColumns, distinct: true }
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const { hasDeepInsert, getDeepInsertCQNs
|
|
1
|
+
const { hasDeepInsert, getDeepInsertCQNs } = require('../../common/composition')
|
|
2
2
|
const { getFlatArray, processCQNs } = require('../utils/deep')
|
|
3
3
|
const { timestampToISO } = require('../data-conversion/timestamp')
|
|
4
4
|
|
|
@@ -15,7 +15,6 @@ const insert = executeInsertCQN => async (model, dbc, query, req) => {
|
|
|
15
15
|
return getFlatArray(results)
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
cleanEmptyCompositionsOfMany(model, query)
|
|
19
18
|
return executeInsertCQN(model, dbc, query, user, locale, isoTs)
|
|
20
19
|
}
|
|
21
20
|
|
|
@@ -96,7 +96,7 @@ class SelectBuilder extends BaseBuilder {
|
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
if (this._obj.SELECT.orderBy && this._obj.SELECT.orderBy.length) {
|
|
99
|
-
this._orderBy()
|
|
99
|
+
this._orderBy(noQuoting)
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
if (this._obj.SELECT.limit || this._obj.SELECT.one) {
|
|
@@ -373,7 +373,11 @@ class SelectBuilder extends BaseBuilder {
|
|
|
373
373
|
this._outputObj.values.push(...values)
|
|
374
374
|
}
|
|
375
375
|
|
|
376
|
-
|
|
376
|
+
_getOrderByElement(noQuoting, name, element) {
|
|
377
|
+
return (noQuoting ? name : this._quote(name)) + ' ' + (element.sort || 'asc').toUpperCase()
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
_orderBy(noQuoting) {
|
|
377
381
|
const sqls = []
|
|
378
382
|
this._outputObj.sql.push('ORDER BY')
|
|
379
383
|
for (const element of this._obj.SELECT.orderBy) {
|
|
@@ -385,7 +389,7 @@ class SelectBuilder extends BaseBuilder {
|
|
|
385
389
|
if (!columns.find(c => JSON.stringify(c.ref) === serialized)) {
|
|
386
390
|
const toMatch = element.as || (element.ref && element.ref.length === 1 && element.ref[0])
|
|
387
391
|
if (toMatch && columns.find(c => c.as === toMatch)) {
|
|
388
|
-
sqls.push(this.
|
|
392
|
+
sqls.push(this._getOrderByElement(noQuoting, toMatch, element))
|
|
389
393
|
continue
|
|
390
394
|
}
|
|
391
395
|
}
|
|
@@ -1,14 +1,18 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
const _flattenDeep = (arr, res) => {
|
|
2
|
+
if (!Array.isArray(arr)) {
|
|
3
|
+
res.push(arr)
|
|
4
|
+
return res
|
|
5
|
+
}
|
|
6
|
+
for (const a of arr) {
|
|
7
|
+
_flattenDeep(a, res)
|
|
8
|
+
}
|
|
9
|
+
return res
|
|
3
10
|
}
|
|
4
11
|
|
|
5
12
|
/*
|
|
6
13
|
* flatten with a dfs approach. this is important!!!
|
|
7
14
|
*/
|
|
8
|
-
|
|
9
|
-
if (!Array.isArray(arg)) return [arg]
|
|
10
|
-
return _flattenDeep(arg)
|
|
11
|
-
}
|
|
15
|
+
const getFlatArray = arg => _flattenDeep(arg, [])
|
|
12
16
|
|
|
13
17
|
async function _processChunk(processFn, model, dbc, cqns, user, locale, ts, indexes, results) {
|
|
14
18
|
const promises = []
|
|
@@ -3,10 +3,8 @@ const { SELECT } = cds.ql
|
|
|
3
3
|
|
|
4
4
|
const { cqn2cqn4sql, convertWhereExists } = require('../../common/utils/cqn2cqn4sql')
|
|
5
5
|
const { getElementDeep } = require('../../common/utils/csn')
|
|
6
|
-
|
|
7
6
|
const { DRAFT_COLUMNS, DRAFT_COLUMNS_MAP, SCENARIO } = require('../../common/constants/draft')
|
|
8
7
|
const {
|
|
9
|
-
adaptStreamCQN,
|
|
10
8
|
addColumnAlias,
|
|
11
9
|
draftIsLocked,
|
|
12
10
|
ensureDraftsSuffix,
|
|
@@ -18,8 +16,8 @@ const {
|
|
|
18
16
|
filterKeys
|
|
19
17
|
} = require('../utils/handler')
|
|
20
18
|
const { deleteCondition, readAndDeleteKeywords, removeIsActiveEntityRecursively } = require('../utils/where')
|
|
21
|
-
|
|
22
19
|
const { getColumns } = require('../../cds-services/services/utils/columns')
|
|
20
|
+
const { adaptStreamCQN } = require('../../cds-services/adapter/odata-v4/utils/stream')
|
|
23
21
|
|
|
24
22
|
const _findRootSubSelectFor = query => {
|
|
25
23
|
if (query.SELECT.where) {
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
const cds = require('../../cds')
|
|
2
2
|
const { UPDATE, SELECT } = cds.ql
|
|
3
|
-
const { removeIsActiveEntityRecursively, isActiveEntityRequested } = require('./where')
|
|
4
3
|
const { getColumns } = require('../../cds-services/services/utils/columns')
|
|
5
4
|
const { ensureNoDraftsSuffix, ensureDraftsSuffix, ensureUnlocalized } = require('../../common/utils/draft')
|
|
6
5
|
const getTemplate = require('../../common/utils/template')
|
|
@@ -126,7 +125,7 @@ const getEnrichedCQN = (cqn, select, draftWhere, scenarioAlias, addLimitOrder =
|
|
|
126
125
|
}
|
|
127
126
|
|
|
128
127
|
if (select.distinct) {
|
|
129
|
-
cqn.distinct
|
|
128
|
+
cqn.SELECT.distinct = true
|
|
130
129
|
}
|
|
131
130
|
|
|
132
131
|
const alias = (select.from && select.from.as) || scenarioAlias
|
|
@@ -249,14 +248,6 @@ const replaceRefWithDraft = ref => {
|
|
|
249
248
|
ref[0] = ensureDraftsSuffix(ref[0])
|
|
250
249
|
}
|
|
251
250
|
|
|
252
|
-
const adaptStreamCQN = cqn => {
|
|
253
|
-
if (isActiveEntityRequested(cqn.SELECT.where)) {
|
|
254
|
-
cqn.SELECT.where = removeIsActiveEntityRecursively(cqn.SELECT.where)
|
|
255
|
-
} else {
|
|
256
|
-
replaceRefWithDraft(cqn.SELECT.from.ref)
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
251
|
const draftIsLocked = lastChangedAt => {
|
|
261
252
|
// default timeout timer is 15 minutes
|
|
262
253
|
const DRAFT_CANCEL_TIMEOUT_IN_MS = ((cds.env.drafts && cds.env.drafts.cancellationTimeout) || 15) * 60 * 1000
|
|
@@ -289,7 +280,6 @@ module.exports = {
|
|
|
289
280
|
hasDraft,
|
|
290
281
|
proxifyToNoDraftsName,
|
|
291
282
|
addColumnAlias,
|
|
292
|
-
adaptStreamCQN,
|
|
293
283
|
replaceRefWithDraft,
|
|
294
284
|
getKeyProperty,
|
|
295
285
|
filterKeys,
|
|
@@ -46,7 +46,8 @@ const HANA_TYPE_CONVERSION_MAP = new Map([
|
|
|
46
46
|
['cds.Integer64', convertInt64ToString],
|
|
47
47
|
['cds.DateTime', convertToISONoMillis],
|
|
48
48
|
['cds.Timestamp', convertToISO],
|
|
49
|
-
['cds.LargeString', convertToString]
|
|
49
|
+
['cds.LargeString', convertToString],
|
|
50
|
+
['cds.hana.CLOB', convertToString]
|
|
50
51
|
])
|
|
51
52
|
|
|
52
53
|
if (cds.env.features.bigjs) {
|
|
@@ -48,7 +48,7 @@ class CustomSelectBuilder extends SelectBuilder {
|
|
|
48
48
|
select.from.ref &&
|
|
49
49
|
select.from.ref.length === 1 &&
|
|
50
50
|
// REVISIT this does not work with join and draft!
|
|
51
|
-
this._csn.definitions[select.from.ref[0]]
|
|
51
|
+
this._csn.definitions[select.from.ref[0].id || select.from.ref[0]]
|
|
52
52
|
// TODO FIXME
|
|
53
53
|
skip =
|
|
54
54
|
!select.orderBy ||
|
|
@@ -7,11 +7,12 @@ try {
|
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
const isDynatraceEnabled = () => {
|
|
10
|
-
return dynatrace.sdk !== undefined
|
|
10
|
+
return dynatrace.sdk !== undefined && !process.env.CDS_SKIP_DYNATRACE
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
const _dynatraceResultCallback = function (tracer, cb) {
|
|
14
|
-
return function (err,
|
|
14
|
+
return function (err, ...args) {
|
|
15
|
+
const results = args.shift()
|
|
15
16
|
if (err) {
|
|
16
17
|
tracer.error(err)
|
|
17
18
|
} else {
|
|
@@ -19,7 +20,7 @@ const _dynatraceResultCallback = function (tracer, cb) {
|
|
|
19
20
|
rowsReturned: (results && results.length) || results
|
|
20
21
|
})
|
|
21
22
|
}
|
|
22
|
-
tracer.end(cb, err, results,
|
|
23
|
+
tracer.end(cb, err, results, ...args)
|
|
23
24
|
}
|
|
24
25
|
}
|
|
25
26
|
|
|
@@ -73,9 +74,14 @@ const dynatraceClient = (client, credentials, tenant) => {
|
|
|
73
74
|
// hana-client does not like decorating.
|
|
74
75
|
// because of that, we need to override the fn and pass the original fn for execution
|
|
75
76
|
const originalExecFn = client.exec
|
|
76
|
-
const originalPrepareFn = client.prepare
|
|
77
77
|
client.exec = _execUsingDynatrace(client, originalExecFn, dbInfo)
|
|
78
|
-
|
|
78
|
+
const originalPrepareFn = client.prepare
|
|
79
|
+
if (client.name === '@sap/hana-client') {
|
|
80
|
+
// client.prepare = ... doesn't work for hana-client
|
|
81
|
+
Object.defineProperty(client, 'prepare', { value: _preparedStmtUsingDynatrace(client, originalPrepareFn, dbInfo) })
|
|
82
|
+
} else {
|
|
83
|
+
client.prepare = _preparedStmtUsingDynatrace(client, originalPrepareFn, dbInfo)
|
|
84
|
+
}
|
|
79
85
|
|
|
80
86
|
return client
|
|
81
87
|
}
|
|
@@ -51,9 +51,8 @@ function _getOutputParameters(stmt) {
|
|
|
51
51
|
const BINARY_TYPES = {
|
|
52
52
|
12: 'BINARY',
|
|
53
53
|
13: 'VARBINARY',
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
27: 'BLOB'
|
|
54
|
+
27: 'BLOB',
|
|
55
|
+
33: 'BSTRING'
|
|
57
56
|
}
|
|
58
57
|
|
|
59
58
|
function _getBinaries(stmt) {
|
|
@@ -68,15 +67,73 @@ function _getBinaries(stmt) {
|
|
|
68
67
|
|
|
69
68
|
const BASE64 = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{4}|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}={2})$/
|
|
70
69
|
|
|
70
|
+
function _isProcedureCall(sql) {
|
|
71
|
+
return sql.trim().match(/^call \s*"{0,1}\w*/i)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function _getProcedureName(sql) {
|
|
75
|
+
const match = sql.trim().match(/^call \s*"{0,1}(\w*)/i)
|
|
76
|
+
return match && match[1]
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function _hdbGetResultForProcedure(rows, args, outParameters) {
|
|
80
|
+
// on hdb, rows already contains results for scalar params
|
|
81
|
+
const result = rows || {}
|
|
82
|
+
// merge table output params into scalar params
|
|
83
|
+
if (args && args.length && outParameters) {
|
|
84
|
+
const params = outParameters.filter(md => !(md.PARAMETER_NAME in rows))
|
|
85
|
+
for (let i = 0; i < args.length; i++) {
|
|
86
|
+
result[params[i].PARAMETER_NAME] = args[i]
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return result
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function _hcGetResultForProcedure(stmt, resultSet, outParameters) {
|
|
93
|
+
const result = {}
|
|
94
|
+
// build result from scalar params
|
|
95
|
+
const paramInfo = stmt.getParameterInfo()
|
|
96
|
+
if (paramInfo.some(p => p.direction > 1)) {
|
|
97
|
+
for (let i = 0; i < paramInfo.length; i++) {
|
|
98
|
+
if (paramInfo[i].direction > 1) {
|
|
99
|
+
result[paramInfo[i].name] = stmt.getParameterValue(i)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// merge table output params into scalar params
|
|
104
|
+
if (outParameters && outParameters.length) {
|
|
105
|
+
const params = outParameters.filter(md => !(md.PARAMETER_NAME in result))
|
|
106
|
+
let i = 0
|
|
107
|
+
while (resultSet.next()) {
|
|
108
|
+
result[params[i].PARAMETER_NAME] = [resultSet.getValues()]
|
|
109
|
+
resultSet.nextResult()
|
|
110
|
+
i++
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return result
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function _getProcedureMetadata(procedureName, dbc) {
|
|
117
|
+
return new Promise((resolve, reject) => {
|
|
118
|
+
dbc.exec(
|
|
119
|
+
`SELECT PARAMETER_NAME FROM SYS.PROCEDURE_PARAMETERS WHERE SCHEMA_NAME = CURRENT_SCHEMA AND PROCEDURE_NAME = '${procedureName}' AND PARAMETER_TYPE IN ('OUT', 'INOUT') ORDER BY POSITION`,
|
|
120
|
+
(err, res) => {
|
|
121
|
+
if (err) reject(err)
|
|
122
|
+
else resolve(res)
|
|
123
|
+
}
|
|
124
|
+
)
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
|
|
71
128
|
function _executeAsPreparedStatement(dbc, sql, values, reject, resolve) {
|
|
72
|
-
dbc.prepare(sql, function (err, stmt) {
|
|
129
|
+
dbc.prepare(sql, async function (err, stmt) {
|
|
73
130
|
if (err) {
|
|
74
131
|
err.query = sql
|
|
75
132
|
if (values) err.values = SANITIZE_VALUES ? ['***'] : values
|
|
76
133
|
return reject(err)
|
|
77
134
|
}
|
|
78
135
|
|
|
79
|
-
// convert binary strings to buffers
|
|
136
|
+
// convert binary strings to buffers
|
|
80
137
|
if (cds.env.hana.base64_to_buffer !== false && _hasValues(values)) {
|
|
81
138
|
const binaries = _getBinaries(stmt)
|
|
82
139
|
if (binaries.length) {
|
|
@@ -91,6 +148,46 @@ function _executeAsPreparedStatement(dbc, sql, values, reject, resolve) {
|
|
|
91
148
|
}
|
|
92
149
|
}
|
|
93
150
|
|
|
151
|
+
if (cds.env.features.new_call_prodecure) {
|
|
152
|
+
// procedure call metadata
|
|
153
|
+
let outParameters
|
|
154
|
+
const isProcedureCall = _isProcedureCall(sql)
|
|
155
|
+
if (isProcedureCall) {
|
|
156
|
+
try {
|
|
157
|
+
const procedureName = _getProcedureName(sql)
|
|
158
|
+
outParameters = await _getProcedureMetadata(procedureName, dbc)
|
|
159
|
+
} catch (e) {
|
|
160
|
+
LOG._warn && LOG.warn('Unable to fetch procedure metadata due to error:', e)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// on @sap/hana-client, we need to use execQuery in case of calling procedures
|
|
165
|
+
stmt[isProcedureCall && dbc.name !== 'hdb' ? 'execQuery' : 'exec'](values, function (err, rows, ...args) {
|
|
166
|
+
if (err) {
|
|
167
|
+
stmt.drop(() => {})
|
|
168
|
+
err.query = sql
|
|
169
|
+
if (values) err.values = SANITIZE_VALUES ? ['***'] : values
|
|
170
|
+
return reject(err)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
let result
|
|
174
|
+
if (isProcedureCall) {
|
|
175
|
+
result =
|
|
176
|
+
dbc.name === 'hdb'
|
|
177
|
+
? _hdbGetResultForProcedure(rows, args, outParameters)
|
|
178
|
+
: _hcGetResultForProcedure(stmt, rows, outParameters)
|
|
179
|
+
} else {
|
|
180
|
+
result = rows
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
stmt.drop(() => {})
|
|
184
|
+
|
|
185
|
+
resolve(result)
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
return
|
|
189
|
+
}
|
|
190
|
+
|
|
94
191
|
stmt.exec(values, function (err, rows, procedureReturn) {
|
|
95
192
|
if (err) {
|
|
96
193
|
stmt.drop(() => {})
|
|
@@ -125,15 +222,15 @@ function _executeSimpleSQL(dbc, sql, values) {
|
|
|
125
222
|
values = Object.values(values)
|
|
126
223
|
}
|
|
127
224
|
// ensure that stored procedure with parameters is always executed as prepared
|
|
128
|
-
if (_hasValues(values) || sql
|
|
225
|
+
if (_hasValues(values) || _isProcedureCall(sql)) {
|
|
129
226
|
_executeAsPreparedStatement(dbc, sql, values, reject, resolve)
|
|
130
227
|
} else {
|
|
131
|
-
dbc.exec(sql, function (err, result
|
|
228
|
+
dbc.exec(sql, function (err, result) {
|
|
132
229
|
if (err) {
|
|
133
230
|
err.query = sql
|
|
134
231
|
return reject(err)
|
|
135
232
|
}
|
|
136
|
-
resolve(
|
|
233
|
+
resolve(result)
|
|
137
234
|
})
|
|
138
235
|
}
|
|
139
236
|
})
|
|
@@ -72,10 +72,7 @@ const search2cqn4sql = (query, entity, options) => {
|
|
|
72
72
|
return query
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
const _getLocalizedAssociation = entity =>
|
|
76
|
-
const associations = entity.associations
|
|
77
|
-
return associations && associations.localized
|
|
78
|
-
}
|
|
75
|
+
const _getLocalizedAssociation = entity => entity.associations && entity.associations.localized
|
|
79
76
|
|
|
80
77
|
// The inner join modifies the original SELECT ... FROM query and adds ambiguity,
|
|
81
78
|
// therefore add the table/entity name (as a preceding element) to the columns ref
|
|
@@ -67,6 +67,11 @@ const getDestination = (name, credentials) => {
|
|
|
67
67
|
throw new Error(`"url" or "destination" property must be configured in "credentials" of "${name}".`)
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
// Cloud SDK wants property "queryParameters" but we have documented "queries"
|
|
71
|
+
if (credentials.queries && !credentials.queryParameters) {
|
|
72
|
+
credentials.queryParameters = credentials.queries
|
|
73
|
+
}
|
|
74
|
+
|
|
70
75
|
return { name, ...credentials }
|
|
71
76
|
}
|
|
72
77
|
|
|
@@ -142,8 +147,8 @@ function _defineProperty(obj, property, value) {
|
|
|
142
147
|
}
|
|
143
148
|
|
|
144
149
|
function _normalizeMetadata(prefix, data, results) {
|
|
145
|
-
const target = results
|
|
146
|
-
if (typeof target !== 'object') return target
|
|
150
|
+
const target = results !== undefined ? results : data
|
|
151
|
+
if (typeof target !== 'object' || target === null) return target
|
|
147
152
|
const metadataKeys = Object.keys(data).filter(k => prefix.test(k))
|
|
148
153
|
for (const k of metadataKeys) {
|
|
149
154
|
const $ = k.replace(prefix, '$')
|
|
@@ -169,7 +174,7 @@ const _purgeODataV2 = (data, target, reqHeaders) => {
|
|
|
169
174
|
data = data.d
|
|
170
175
|
const ieee754Compatible = reqHeaders.accept && reqHeaders.accept.includes('IEEE754Compatible=true')
|
|
171
176
|
const exponentialDecimals = ieee754Compatible && reqHeaders.accept.includes('ExponentialDecimals=true')
|
|
172
|
-
const purgedResponse = data.results
|
|
177
|
+
const purgedResponse = 'results' in data ? data.results : data
|
|
173
178
|
const convertedResponse = convertV2ResponseData(purgedResponse, target, ieee754Compatible, exponentialDecimals)
|
|
174
179
|
return _normalizeMetadata(/^__/, data, convertedResponse)
|
|
175
180
|
}
|
|
@@ -177,7 +182,7 @@ const _purgeODataV2 = (data, target, reqHeaders) => {
|
|
|
177
182
|
const _purgeODataV4 = data => {
|
|
178
183
|
if (typeof data !== 'object') return data
|
|
179
184
|
|
|
180
|
-
const purgedResponse = data.value
|
|
185
|
+
const purgedResponse = 'value' in data ? data.value : data
|
|
181
186
|
return _normalizeMetadata(/^@odata\./, data, purgedResponse)
|
|
182
187
|
}
|
|
183
188
|
|
|
@@ -246,6 +251,7 @@ const run = async (
|
|
|
246
251
|
|
|
247
252
|
LOG._warn && LOG.warn(sanitizedError)
|
|
248
253
|
|
|
254
|
+
// REVISIT: switch from innererror to reason in cds^6
|
|
249
255
|
throw Object.assign(new Error(e.message), { statusCode: 502, innererror: sanitizedError })
|
|
250
256
|
}
|
|
251
257
|
|
|
@@ -267,6 +273,7 @@ const run = async (
|
|
|
267
273
|
|
|
268
274
|
LOG._warn && LOG.warn(sanitizedError)
|
|
269
275
|
|
|
276
|
+
// REVISIT: switch from innererror to reason in cds^6
|
|
270
277
|
throw Object.assign(new Error(`Error during request to remote service: ${e.message}`), {
|
|
271
278
|
statusCode: 502,
|
|
272
279
|
innererror: sanitizedError
|
|
@@ -294,6 +301,8 @@ const run = async (
|
|
|
294
301
|
: 'Request to remote service failed.'
|
|
295
302
|
const sanitizedError = _getSanitizedError(contentJSON, requestConfig)
|
|
296
303
|
LOG._warn && LOG.warn(sanitizedError)
|
|
304
|
+
|
|
305
|
+
// REVISIT: switch from innererror to reason in cds^6
|
|
297
306
|
throw Object.assign(new Error(contentJSON.message), { statusCode: 502, innererror: sanitizedError })
|
|
298
307
|
}
|
|
299
308
|
}
|
|
@@ -39,7 +39,8 @@ const _getConvertRecordFn = (target, convertValueFn) => record => {
|
|
|
39
39
|
if (!element) continue
|
|
40
40
|
|
|
41
41
|
const recordValue = record[key]
|
|
42
|
-
const value =
|
|
42
|
+
const value =
|
|
43
|
+
(recordValue && typeof recordValue === 'object' && 'results' in recordValue && recordValue.results) || recordValue
|
|
43
44
|
|
|
44
45
|
if (value && (element.isAssociation || Array.isArray(value))) {
|
|
45
46
|
record[key] = _convertData(value, element._target, convertValueFn)
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const cds = require('../../../../lib')
|
|
2
|
+
|
|
1
3
|
const { ARGUMENT } = require('../../constants/adapter')
|
|
2
4
|
const { getArgumentByName, astToEntries } = require('../parse/ast2cqn')
|
|
3
5
|
const { entriesStructureToEntityStructure } = require('./utils')
|
|
@@ -9,7 +11,10 @@ module.exports = async (service, entityFQN, selection) => {
|
|
|
9
11
|
const entries = entriesStructureToEntityStructure(service, entityFQN, astToEntries(input))
|
|
10
12
|
query.entries(entries)
|
|
11
13
|
|
|
12
|
-
const result = await service.tx(tx =>
|
|
14
|
+
const result = await service.tx(tx => {
|
|
15
|
+
cds.context = tx
|
|
16
|
+
return tx.run(query)
|
|
17
|
+
})
|
|
13
18
|
|
|
14
19
|
return Array.isArray(result) ? result : [result]
|
|
15
20
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const cds = require('../../../../lib')
|
|
2
|
+
|
|
1
3
|
const { ARGUMENT } = require('../../constants/adapter')
|
|
2
4
|
const { getArgumentByName, astToWhere } = require('../parse/ast2cqn')
|
|
3
5
|
|
|
@@ -11,7 +13,10 @@ module.exports = async (service, entityFQN, selection) => {
|
|
|
11
13
|
|
|
12
14
|
let result
|
|
13
15
|
try {
|
|
14
|
-
result = await service.tx(tx =>
|
|
16
|
+
result = await service.tx(tx => {
|
|
17
|
+
cds.context = tx
|
|
18
|
+
return tx.run(query)
|
|
19
|
+
})
|
|
15
20
|
} catch (e) {
|
|
16
21
|
if (e.code === 404) {
|
|
17
22
|
result = 0
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const cds = require('../../../../lib')
|
|
2
|
+
|
|
1
3
|
const { ARGUMENT } = require('../../constants/adapter')
|
|
2
4
|
const { getArgumentByName, astToColumns, astToWhere, astToOrderBy, astToLimit } = require('../parse/ast2cqn')
|
|
3
5
|
|
|
@@ -21,5 +23,8 @@ module.exports = async (service, entityFQN, selection) => {
|
|
|
21
23
|
query.limit(astToLimit(top, skip))
|
|
22
24
|
}
|
|
23
25
|
|
|
24
|
-
return await service.tx(tx =>
|
|
26
|
+
return await service.tx(tx => {
|
|
27
|
+
cds.context = tx
|
|
28
|
+
return tx.run(query)
|
|
29
|
+
})
|
|
25
30
|
}
|