@sap/cds 7.1.0 → 7.1.2
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 +21 -0
- package/bin/serve.js +2 -2
- package/lib/dbs/cds-deploy.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +3 -1
- package/libx/_runtime/cds-services/services/utils/compareJson.js +0 -9
- package/libx/_runtime/cds-services/services/utils/differ.js +8 -10
- package/libx/_runtime/common/composition/data.js +5 -4
- package/libx/_runtime/common/composition/insert.js +3 -2
- package/libx/_runtime/common/composition/update.js +6 -4
- package/libx/_runtime/common/generic/auth/restrict.js +1 -1
- package/libx/_runtime/common/generic/crud.js +1 -1
- package/libx/_runtime/common/utils/cqn2cqn4sql.js +5 -1
- package/libx/_runtime/common/utils/resolveView.js +1 -1
- package/libx/_runtime/db/utils/coloredTxCommands.js +5 -3
- package/libx/_runtime/db/utils/localized.js +1 -1
- package/libx/_runtime/fiori/generic/edit.js +1 -1
- package/libx/_runtime/fiori/lean-draft.js +21 -18
- package/libx/_runtime/hana/localized.js +1 -1
- package/libx/_runtime/hana/pool.js +52 -56
- package/libx/_runtime/hana/search2cqn4sql.js +3 -1
- package/libx/_runtime/sqlite/localized.js +1 -1
- package/libx/odata/afterburner.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,26 @@
|
|
|
4
4
|
- The format is based on [Keep a Changelog](http://keepachangelog.com/).
|
|
5
5
|
- This project adheres to [Semantic Versioning](http://semver.org/).
|
|
6
6
|
|
|
7
|
+
## Version 7.1.2 - 2023-08-11
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- `req.tenant` is undefined when using new OData parser
|
|
12
|
+
- Draft: replace some occurrences of the `arr.push(...largeArray)` pattern when copying large arrays to prevent maximum call stack size exceeded errors due to large deep update processing when saving the draft
|
|
13
|
+
- Do not add keys to diff as they could be renamed (which leads to SQL error)
|
|
14
|
+
- Custom bound actions for draft-enabled entities don't trigger a READ request on application service anymore
|
|
15
|
+
- `cds.connect.to('db', options)`: add-hoc options for SAP HANA Database Service
|
|
16
|
+
- Reading key-less singleton with `$select` clause while using the new OData parser `cds.env.features.odata_new_parser`
|
|
17
|
+
- Don't use colored logs if `process.env.NO_COLOR` is set or not logging to a terminal (i.e., `!process.stdout.isTTY || !process.stderr.isTTY`)
|
|
18
|
+
|
|
19
|
+
## Version 7.1.1 - 2023-08-01
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- Lean draft: read actives via service on draft edit
|
|
24
|
+
- Resolve column name for `STREAM` CQN queries that point to views
|
|
25
|
+
- Only log the error in case of an unhandled rejection
|
|
26
|
+
|
|
7
27
|
## Version 7.1.0 - 2023-07-28
|
|
8
28
|
|
|
9
29
|
### Added
|
|
@@ -304,6 +324,7 @@ cds env requires/cds.xt.ModelProviderService
|
|
|
304
324
|
## Version 6.6.0 - 2023-02-27
|
|
305
325
|
|
|
306
326
|
### Added
|
|
327
|
+
|
|
307
328
|
- Improved error handling for `cds build` if the SaaS base model is missing in an extension project.
|
|
308
329
|
- Support for reliable paging using `$skiptoken`. Can be activated via `cds.query.limit.reliablePaging = true`
|
|
309
330
|
- Built-in models are now added to existing model options of custom build tasks.
|
package/bin/serve.js
CHANGED
|
@@ -206,8 +206,8 @@ async function serve (all=[], o={}) {
|
|
|
206
206
|
|
|
207
207
|
const LOG = cds.log('cli|server')
|
|
208
208
|
cds.shutdown = _shutdown //> for programmatic invocation
|
|
209
|
-
process.on('unhandledRejection',
|
|
210
|
-
process.on('uncaughtException',
|
|
209
|
+
process.on('unhandledRejection', e => _shutdown (e, cds.log('cds').error('❗️Uncaught',e)))
|
|
210
|
+
process.on('uncaughtException', e => _shutdown (e, cds.log('cds').error('❗️Uncaught',e)))
|
|
211
211
|
process.on('SIGINT', cds.watched ? _shutdown : (s,n)=>_shutdown(s,n,console.log())) //> newline after ^C
|
|
212
212
|
process.on('SIGHUP', _shutdown)
|
|
213
213
|
process.on('SIGHUP2', _shutdown)
|
package/lib/dbs/cds-deploy.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
const cds = require('../index'), { local } = cds.utils
|
|
3
|
-
const COLORS = !!process.stdout.isTTY && !!process.stderr.isTTY
|
|
3
|
+
const COLORS = !!process.stdout.isTTY && !!process.stderr.isTTY && !process.env.NO_COLOR
|
|
4
4
|
const GREY = COLORS ? '\x1b[2m' : ''
|
|
5
5
|
const RESET = COLORS ? '\x1b[0m' : ''
|
|
6
6
|
let DEBUG // IMPORTANT: initialized later after await cds.plugins
|
|
@@ -90,6 +90,7 @@ class ODataRequest extends cds.Request {
|
|
|
90
90
|
// REVISIT needed in case of $batch, replace after removing okra
|
|
91
91
|
const method = odataReq.getIncomingRequest().method
|
|
92
92
|
const { user } = req
|
|
93
|
+
const tenant = req.tenant || user?.tenant
|
|
93
94
|
const info = metaInfo(query, type, service, data, req, upsert)
|
|
94
95
|
const { event, unbound } = info
|
|
95
96
|
if (event === 'READ') {
|
|
@@ -98,7 +99,8 @@ class ODataRequest extends cds.Request {
|
|
|
98
99
|
if (!isStreaming(segments)) handleStreamProperties(target, query, service.model, true)
|
|
99
100
|
}
|
|
100
101
|
const _queryOptions = odataReq.getQueryOptions()
|
|
101
|
-
|
|
102
|
+
// prettier-ignore
|
|
103
|
+
super({ event, target, data, query: unbound ? {} : query, user, tenant, method, headers, req, res, _queryOptions })
|
|
102
104
|
this._metaInfo = info.metadata
|
|
103
105
|
} else {
|
|
104
106
|
/*
|
|
@@ -133,11 +133,6 @@ const _addToBeDeletedEntriesToResult = (results, entity, keys, newValues, oldVal
|
|
|
133
133
|
|
|
134
134
|
const _normalizeToArray = value => (Array.isArray(value) ? value : value === null ? [] : [value])
|
|
135
135
|
|
|
136
|
-
const _addKeysToEntryIfNotExists = (keys, newEntry) => {
|
|
137
|
-
if (!newEntry) return
|
|
138
|
-
for (const key of keys) if (!(key in newEntry)) newEntry[key] = undefined
|
|
139
|
-
}
|
|
140
|
-
|
|
141
136
|
const _isUnManaged = element => {
|
|
142
137
|
return element.on && !element._isSelfManaged
|
|
143
138
|
}
|
|
@@ -207,11 +202,7 @@ const compareJsonDeep = (entity, newValue = [], oldValue = [], opts) => {
|
|
|
207
202
|
for (const newEntry of newValues) {
|
|
208
203
|
const result = {}
|
|
209
204
|
const oldEntry = _getCorrespondingEntryWithSameKeys(oldValues, newEntry, keys)
|
|
210
|
-
|
|
211
|
-
_addKeysToEntryIfNotExists(keys, newEntry)
|
|
212
|
-
|
|
213
205
|
_iteratePropsInNewEntry(newEntry, keys, result, oldEntry, entity, opts)
|
|
214
|
-
|
|
215
206
|
resultsArray.push(result)
|
|
216
207
|
}
|
|
217
208
|
|
|
@@ -37,16 +37,14 @@ module.exports = class Differ {
|
|
|
37
37
|
return columns
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
_diffDelete(req) {
|
|
40
|
+
async _diffDelete(req) {
|
|
41
41
|
const { DELETE } = cds.env.fiori.lean_draft ? req.query : (req._ && req._.query) || req.query
|
|
42
42
|
const target = DELETE._transitions?.[DELETE._transitions.length - 1]?.target || req.target
|
|
43
43
|
const query = SELECT.from(DELETE.from).columns(this._createSelectColumnsForDelete(target))
|
|
44
44
|
if (DELETE.where) query.where(DELETE.where)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
.run(query)
|
|
49
|
-
.then(dbState => compareJson(undefined, dbState, req.target, { ignoreDraftColumns: true }))
|
|
45
|
+
const dbState = await cds.tx(req).run(query)
|
|
46
|
+
const diff = compareJson(undefined, dbState, req.target, { ignoreDraftColumns: true })
|
|
47
|
+
return diff
|
|
50
48
|
}
|
|
51
49
|
|
|
52
50
|
async _addPartialPersistentState(req) {
|
|
@@ -62,7 +60,8 @@ module.exports = class Differ {
|
|
|
62
60
|
enrichDataWithKeysFromWhere(combinedData, req, this._srv)
|
|
63
61
|
const lastTransition = newQuery.UPDATE._transitions[newQuery.UPDATE._transitions.length - 1]
|
|
64
62
|
const revertedPersistent = revertData(req._.partialPersistentState, lastTransition, this._srv)
|
|
65
|
-
|
|
63
|
+
const diff = compareJson(combinedData, revertedPersistent, req.target, { ignoreDraftColumns: true })
|
|
64
|
+
return diff
|
|
66
65
|
}
|
|
67
66
|
|
|
68
67
|
async _diffPatch(req, providedData) {
|
|
@@ -87,10 +86,9 @@ module.exports = class Differ {
|
|
|
87
86
|
providedData || (req.query.INSERT.entries && req.query.INSERT.entries.length === 1)
|
|
88
87
|
? req.query.INSERT.entries[0]
|
|
89
88
|
: req.query.INSERT.entries
|
|
90
|
-
|
|
91
89
|
enrichDataWithKeysFromWhere(originalData, req, this._srv)
|
|
92
|
-
|
|
93
|
-
return
|
|
90
|
+
const diff = compareJson(originalData, undefined, req.target, { ignoreDraftColumns: true })
|
|
91
|
+
return diff
|
|
94
92
|
}
|
|
95
93
|
|
|
96
94
|
async calculate(req, providedData) {
|
|
@@ -133,7 +133,7 @@ const _subData = (data, prop) =>
|
|
|
133
133
|
data.reduce((result, entry) => {
|
|
134
134
|
if (prop in entry) {
|
|
135
135
|
const elementValue = ctUtils.val(entry[prop])
|
|
136
|
-
|
|
136
|
+
for (const val of ctUtils.array(elementValue)) result.push(val)
|
|
137
137
|
}
|
|
138
138
|
return result
|
|
139
139
|
}, [])
|
|
@@ -196,7 +196,8 @@ const _mergeResults = (result, selectData, root, model, compositionTree, entityN
|
|
|
196
196
|
if (newData[0]) selectEntry[compositionTree.name] = Object.assign(selectEntry[compositionTree.name], newData[0])
|
|
197
197
|
else selectEntry[compositionTree.name] = null
|
|
198
198
|
} else if (assoc.is2many) {
|
|
199
|
-
selectEntry[compositionTree.name]
|
|
199
|
+
const entry = selectEntry[compositionTree.name]
|
|
200
|
+
for (const val of newData) entry.push(val)
|
|
200
201
|
}
|
|
201
202
|
|
|
202
203
|
return selectEntry
|
|
@@ -263,13 +264,13 @@ const _selectDeepUpdateData = async args => {
|
|
|
263
264
|
}
|
|
264
265
|
const _args = { ...args, where: [keys, 'in', values], parentKeys: undefined }
|
|
265
266
|
const selectCQN = _select(_args)
|
|
266
|
-
result.
|
|
267
|
+
result = result.concat(await tx.run(selectCQN))
|
|
267
268
|
}
|
|
268
269
|
} else if (where && !Array.isArray(where)) {
|
|
269
270
|
for (let w of Object.values(where)) {
|
|
270
271
|
const _args = { ...args, where: w }
|
|
271
272
|
const selectCQN = _select(_args)
|
|
272
|
-
result.
|
|
273
|
+
result = result.concat(await tx.run(selectCQN))
|
|
273
274
|
}
|
|
274
275
|
} else {
|
|
275
276
|
const selectCQN = _select(args)
|
|
@@ -32,8 +32,9 @@ const _addSubDeepInsertCQN = (model, compositionTree, data, cqns, draft) => {
|
|
|
32
32
|
const subData = ctUtils.array(elementValue).filter(ele => Object.keys(ele).length > 0)
|
|
33
33
|
if (subData.length > 0) {
|
|
34
34
|
// REVISIT: this can make problems
|
|
35
|
-
insertCQN.INSERT.entries
|
|
36
|
-
|
|
35
|
+
const entries = insertCQN.INSERT.entries
|
|
36
|
+
for (const data of ctUtils.cleanDeepData(subEntity, subData)) entries.push(data)
|
|
37
|
+
for (const data of subData) result.push(data)
|
|
37
38
|
}
|
|
38
39
|
}
|
|
39
40
|
}
|
|
@@ -144,7 +144,8 @@ function _addSubDeepUpdateCQNForUpdateInsert({ entity, entityName, data, selectD
|
|
|
144
144
|
async function _addSubDeepUpdateCQNCollect(model, cqns, updateCQNs, insertCQN, deleteCQNs, req) {
|
|
145
145
|
if (updateCQNs.length > 0) {
|
|
146
146
|
cqns[0] = cqns[0] || []
|
|
147
|
-
cqns[0]
|
|
147
|
+
const cqn = cqns[0]
|
|
148
|
+
for (const updateCQN of updateCQNs) cqn.push(updateCQN)
|
|
148
149
|
}
|
|
149
150
|
|
|
150
151
|
if (insertCQN.INSERT.entries.length > 0) {
|
|
@@ -157,7 +158,8 @@ async function _addSubDeepUpdateCQNCollect(model, cqns, updateCQNs, insertCQN, d
|
|
|
157
158
|
if (!intoCQN) {
|
|
158
159
|
cqns[0].push(insertCQN)
|
|
159
160
|
} else {
|
|
160
|
-
intoCQN.INSERT.entries
|
|
161
|
+
const intoCQNEntries = intoCQN.INSERT.entries
|
|
162
|
+
for (const entry of insertCQN.INSERT.entries) intoCQNEntries.push(entry)
|
|
161
163
|
}
|
|
162
164
|
})
|
|
163
165
|
}
|
|
@@ -182,7 +184,7 @@ const _addToData = (subData, entity, element, entry) => {
|
|
|
182
184
|
const value = ctUtils.val(entry[element.name])
|
|
183
185
|
const subDataEntries = ctUtils.array(value)
|
|
184
186
|
const unwrappedSubData = subDataEntries.map(entry => _unwrapIfNotArray(entry))
|
|
185
|
-
subData.push(
|
|
187
|
+
for (const val of unwrappedSubData) subData.push(val)
|
|
186
188
|
}
|
|
187
189
|
|
|
188
190
|
async function _addSubDeepUpdateCQNRecursion({ model, compositionTree, entity, data, selectData, cqns, draft, req }) {
|
|
@@ -310,7 +312,7 @@ const getDeepUpdateCQNs = async (model, req, selectData) => {
|
|
|
310
312
|
})
|
|
311
313
|
subCQNs.forEach((subCQNs, index) => {
|
|
312
314
|
cqns[index] = cqns[index] || []
|
|
313
|
-
cqns[index].push(
|
|
315
|
+
for (const cqn of subCQNs) cqns[index].push(cqn)
|
|
314
316
|
})
|
|
315
317
|
|
|
316
318
|
// remove empty updates and inserts
|
|
@@ -121,7 +121,7 @@ const _addWheresToRef = (ref, model, resolvedApplicables) => {
|
|
|
121
121
|
newIdentifier.where = [{ xpr: newIdentifier.where }, 'and']
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
-
newIdentifier.where.push(
|
|
124
|
+
for (const val of _getMergedWhere(applicablesForEntity)) newIdentifier.where.push(val)
|
|
125
125
|
}
|
|
126
126
|
|
|
127
127
|
newRef.push(newIdentifier)
|
|
@@ -346,6 +346,8 @@ const _getWhereExistsSubSelect = (queryTarget, outerAlias, innerAlias, ref, mode
|
|
|
346
346
|
if (condition.length > 1 || (condition.length === 1 && !('val' in condition[0]))) subSelect.where(condition)
|
|
347
347
|
}
|
|
348
348
|
|
|
349
|
+
if (options.localized != null) subSelect.SELECT.localized = options.localized
|
|
350
|
+
|
|
349
351
|
subSelect.where(queryTarget._relations[navName].join(innerAlias, outerAlias))
|
|
350
352
|
if (cds.env.effective.odata.structs || cds.env.features.ucsn_struct_conversion) {
|
|
351
353
|
flattenStructuredSelect(subSelect, model)
|
|
@@ -354,6 +356,7 @@ const _getWhereExistsSubSelect = (queryTarget, outerAlias, innerAlias, ref, mode
|
|
|
354
356
|
|
|
355
357
|
// nested where exists needs recursive conversion
|
|
356
358
|
options.lambdaIteration++
|
|
359
|
+
|
|
357
360
|
return _convertSelect(subSelect, model, options)
|
|
358
361
|
}
|
|
359
362
|
|
|
@@ -711,10 +714,11 @@ const _convertToOneEqNullInFilter = (query, target) => {
|
|
|
711
714
|
}
|
|
712
715
|
}
|
|
713
716
|
}
|
|
717
|
+
|
|
714
718
|
// eslint-disable-next-line complexity
|
|
715
719
|
const _convertSelect = (query, model, _options) => {
|
|
716
720
|
const _4db = _options.service?.isDatabaseService
|
|
717
|
-
const options = Object.assign({ _4db, isStreaming: query._streaming }, _options)
|
|
721
|
+
const options = Object.assign({ _4db, isStreaming: query._streaming, localized: query.SELECT.localized }, _options)
|
|
718
722
|
|
|
719
723
|
// ensure query is ql enabled
|
|
720
724
|
if (!(query instanceof Query)) Object.setPrototypeOf(query, Object.getPrototypeOf(SELECT()))
|
|
@@ -419,7 +419,7 @@ const _newStream = (query, transitions) => {
|
|
|
419
419
|
} else {
|
|
420
420
|
newStream.into = targetName
|
|
421
421
|
}
|
|
422
|
-
if (newStream.column) newStream.column = _resolveColumn(
|
|
422
|
+
if (newStream.column) newStream.column = _resolveColumn(newStream.column, targetTransition)
|
|
423
423
|
Object.defineProperty(newStream, '_transitions', {
|
|
424
424
|
enumerable: false,
|
|
425
425
|
value: transitions
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
const COLORS = !!process.stdout.isTTY && !!process.stderr.isTTY && !process.env.NO_COLOR
|
|
2
|
+
|
|
1
3
|
module.exports = {
|
|
2
|
-
BEGIN: '\x1b[1m\x1b[33mBEGIN\x1b[0m',
|
|
3
|
-
COMMIT: '\x1b[1m\x1b[32mCOMMIT\x1b[0m',
|
|
4
|
-
ROLLBACK: '\x1b[1m\x1b[91mROLLBACK\x1b[0m'
|
|
4
|
+
BEGIN: COLORS ? '\x1b[1m\x1b[33mBEGIN\x1b[0m' : 'BEGIN',
|
|
5
|
+
COMMIT: COLORS ? '\x1b[1m\x1b[32mCOMMIT\x1b[0m' : 'COMMIT',
|
|
6
|
+
ROLLBACK: COLORS ? '\x1b[1m\x1b[91mROLLBACK\x1b[0m' : 'ROLLBACK'
|
|
5
7
|
}
|
|
@@ -115,7 +115,7 @@ const fioriGenericEdit = async function (req, next) {
|
|
|
115
115
|
for (const q of selectCQNs) {
|
|
116
116
|
const entity = definitions[q.SELECT.from.ref[0]]
|
|
117
117
|
if (entity && !entity.name.match(/\.texts$/)) {
|
|
118
|
-
|
|
118
|
+
q.SELECT.localized = false
|
|
119
119
|
}
|
|
120
120
|
}
|
|
121
121
|
|
|
@@ -286,9 +286,13 @@ cds.ApplicationService.prototype.handle = async function (req) {
|
|
|
286
286
|
if (req.target.actions?.[req.event] && draftParams.IsActiveEntity === false) {
|
|
287
287
|
if (query.SELECT?.from?.ref) query.SELECT.from.ref = _redirectRefToDrafts(query.SELECT.from.ref, this.model)
|
|
288
288
|
const rootQuery = query.clone()
|
|
289
|
-
|
|
289
|
+
const columns = Object_keys(query._target.keys)
|
|
290
|
+
.filter(k => k !== 'IsActiveEntity')
|
|
291
|
+
.map(k => ({ ref: [k] }))
|
|
292
|
+
columns.push({ ref: ['DraftAdministrativeData'], expand: [{ ref: ['InProcessByUser'] }] })
|
|
293
|
+
rootQuery.SELECT.columns = columns
|
|
290
294
|
rootQuery.SELECT.one = true
|
|
291
|
-
const root = await rootQuery
|
|
295
|
+
const root = await cds.run(rootQuery)
|
|
292
296
|
if (!root) req.reject(404)
|
|
293
297
|
if (root.DraftAdministrativeData?.InProcessByUser !== cds.context.user.id) req.reject(403)
|
|
294
298
|
const _req = _newReq(req, query, draftParams, { event: req.event })
|
|
@@ -450,6 +454,7 @@ const Read = {
|
|
|
450
454
|
},
|
|
451
455
|
all: async function (run, query) {
|
|
452
456
|
LOG.debug('List Editing Status: All')
|
|
457
|
+
if (!query._drafts) return []
|
|
453
458
|
query._drafts.SELECT.count = false
|
|
454
459
|
query._drafts.SELECT.limit = undefined // We need all entries for the keys to properly select actives (count)
|
|
455
460
|
const isCount = _isCount(query._drafts)
|
|
@@ -567,21 +572,17 @@ const Read = {
|
|
|
567
572
|
if (!actives.length) return []
|
|
568
573
|
const drafts = cds.ql.clone(query._drafts)
|
|
569
574
|
drafts.SELECT.where = Read.whereIn(query._target, actives)
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
const relevantColumns = ['DraftAdministrativeData', 'DraftAdministrativeData_DraftUUID']
|
|
577
|
-
drafts.SELECT.columns = (
|
|
578
|
-
drafts.SELECT.columns?.filter(c => c.ref && relevantColumns.includes(c.ref[0])) ||
|
|
579
|
-
relevantColumns.map(k => ({ ref: [k] }))
|
|
580
|
-
).concat(
|
|
581
|
-
Object_keys(query._target.keys)
|
|
582
|
-
.filter(k => k !== 'IsActiveEntity')
|
|
583
|
-
.map(k => ({ ref: [k] }))
|
|
575
|
+
const newColumns = Object_keys(query._target.keys)
|
|
576
|
+
.filter(k => k !== 'IsActiveEntity')
|
|
577
|
+
.map(k => ({ ref: [k] }))
|
|
578
|
+
if (
|
|
579
|
+
!drafts.SELECT.columns ||
|
|
580
|
+
drafts.SELECT.columns.some(c => c === '*' || c.ref?.[0] === 'DraftAdministrativeData_DraftUUID')
|
|
584
581
|
)
|
|
582
|
+
newColumns.push({ ref: ['DraftAdministrativeData_DraftUUID'] })
|
|
583
|
+
const draftAdmin = drafts.SELECT.columns?.find(c => c.ref?.[0] === 'DraftAdministrativeData')
|
|
584
|
+
if (draftAdmin) newColumns.push(draftAdmin)
|
|
585
|
+
drafts.SELECT.columns = newColumns
|
|
585
586
|
drafts.SELECT.count = undefined
|
|
586
587
|
drafts.SELECT.search = undefined
|
|
587
588
|
drafts.SELECT.one = undefined
|
|
@@ -662,6 +663,7 @@ function _cleansed(query, model) {
|
|
|
662
663
|
draftsQuery._target = undefined
|
|
663
664
|
const [root, ...tail] = draftsQuery.SELECT.from.ref
|
|
664
665
|
const draft = model.definitions[root.id || root].drafts
|
|
666
|
+
if (!draft) return
|
|
665
667
|
draftsQuery.SELECT.from = {
|
|
666
668
|
ref: [root.id ? { ...root, id: draft.name } : draft.name, ...tail]
|
|
667
669
|
}
|
|
@@ -967,7 +969,7 @@ async function onEdit(req) {
|
|
|
967
969
|
existingDraft[DRAFT_PARAMS] = draftParams
|
|
968
970
|
|
|
969
971
|
const activeCQN = SELECT.one.from(req.target).columns(cols).where(targetWhere)
|
|
970
|
-
activeCQN.
|
|
972
|
+
activeCQN.SELECT.localized = false
|
|
971
973
|
|
|
972
974
|
const activeCheck = SELECT.one(req.target).columns([1]).where(targetWhere).forUpdate()
|
|
973
975
|
activeCheck[DRAFT_PARAMS] = draftParams
|
|
@@ -979,7 +981,8 @@ async function onEdit(req) {
|
|
|
979
981
|
} catch {} // eslint-disable-line no-empty
|
|
980
982
|
|
|
981
983
|
const [res, draft] = await _promiseAll([
|
|
982
|
-
|
|
984
|
+
// REVISIT: inofficial compat flag just in case it breaks something -> do not document
|
|
985
|
+
cds.env.fiori.read_actives_from_db ? this._datasource.run(activeCQN) : this.run(activeCQN),
|
|
983
986
|
// no user check must be done here...
|
|
984
987
|
existingDraft
|
|
985
988
|
])
|
|
@@ -21,7 +21,7 @@ const localizedHandler = function (req) {
|
|
|
21
21
|
if (!req.locale) return
|
|
22
22
|
|
|
23
23
|
// suppress localization by instruction
|
|
24
|
-
if (query.
|
|
24
|
+
if (query.SELECT.localized === false) return
|
|
25
25
|
|
|
26
26
|
// suppress localization for pure counts
|
|
27
27
|
const columns = query.SELECT.columns
|
|
@@ -15,18 +15,20 @@ const _getMassagedCreds = function (creds) {
|
|
|
15
15
|
return creds
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
//
|
|
19
|
-
async function credentials4(tenant,
|
|
20
|
-
const {
|
|
18
|
+
// `disableCache: true` means to force fetch credentials from service manager
|
|
19
|
+
async function credentials4(tenant, db) {
|
|
20
|
+
const { disableCache = false, options } = db
|
|
21
|
+
const credentials = options?.credentials ?? cds.env.requires.db?.credentials
|
|
21
22
|
if (!credentials) throw new Error('No database credentials provided')
|
|
23
|
+
|
|
22
24
|
if (cds.requires.multitenancy) {
|
|
23
25
|
// eslint-disable-next-line cds/no-missing-dependencies
|
|
24
26
|
const res = await require('@sap/cds-mtxs/lib').xt.serviceManager.get(tenant, { disableCache })
|
|
25
27
|
return _getMassagedCreds(res.credentials)
|
|
26
|
-
} else {
|
|
27
|
-
if (typeof credentials !== 'object' || !credentials.host) throw new Error('Malformed database credentials provided')
|
|
28
|
-
return _getMassagedCreds(credentials)
|
|
29
28
|
}
|
|
29
|
+
|
|
30
|
+
if (typeof credentials !== 'object' || !credentials.host) throw new Error('Malformed database credentials provided')
|
|
31
|
+
return _getMassagedCreds(credentials)
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
function factory4(creds, tenant) {
|
|
@@ -89,55 +91,48 @@ const pools = new Map()
|
|
|
89
91
|
|
|
90
92
|
async function pool4(tenant, db) {
|
|
91
93
|
if (!pools.get(tenant)) {
|
|
92
|
-
|
|
93
|
-
tenant,
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
.
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const INVALID_CREDENTIALS_ERROR = new Error(
|
|
103
|
-
`Create is blocked for tenant "${tenant}" due to invalid credentials.`
|
|
104
|
-
)
|
|
105
|
-
|
|
106
|
-
/*
|
|
107
|
-
* The error listener for "factoryCreateError" is registered to find out failed connection attempts.
|
|
108
|
-
* If it fails due to invalid credentials, we delete the current pool from the pools map and overwrite the
|
|
109
|
-
* pool factory create function.
|
|
110
|
-
* Background is that generic-pool will continue to try to open a connection by calling the factory create
|
|
111
|
-
* function until the "acquireTimeoutMillis" is reached.
|
|
112
|
-
* This ends up in many connection attempts for one request even though the credentials are invalid.
|
|
113
|
-
* Because of the deletion in the map, subsequent requests will fetch the credentials again.
|
|
114
|
-
*/
|
|
115
|
-
p.on('factoryCreateError', async function (err) {
|
|
116
|
-
if (err._connectError) {
|
|
117
|
-
LOG._warn && LOG.warn(INVALID_CREDENTIALS_WARNING)
|
|
118
|
-
pools.delete(tenant)
|
|
119
|
-
if (p._factory && p._factory.create) {
|
|
120
|
-
// reject after 100 ms to not block CPU completely
|
|
121
|
-
p._factory.create = () =>
|
|
122
|
-
new Promise((resolve, reject) => setTimeout(() => reject(INVALID_CREDENTIALS_ERROR), 100))
|
|
123
|
-
}
|
|
124
|
-
await p.drain()
|
|
125
|
-
await p.clear()
|
|
126
|
-
}
|
|
127
|
-
})
|
|
94
|
+
const poolPromise = new Promise((resolve, reject) => {
|
|
95
|
+
credentials4(tenant, db)
|
|
96
|
+
.then(creds => {
|
|
97
|
+
const config = _getPoolConfig()
|
|
98
|
+
LOG._info && LOG.info('effective pool configuration:', config)
|
|
99
|
+
const p = pool.createPool(factory4(creds, tenant), config)
|
|
100
|
+
const INVALID_CREDENTIALS_WARNING = `Could not establish connection for tenant "${tenant}". Existing pool will be drained.`
|
|
101
|
+
const INVALID_CREDENTIALS_ERROR = new Error(
|
|
102
|
+
`Create is blocked for tenant "${tenant}" due to invalid credentials.`
|
|
103
|
+
)
|
|
128
104
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
105
|
+
// The error listener for `factoryCreateError` is registered to detect failed connection attempts.
|
|
106
|
+
// If it fails due to invalid credentials, we delete the current pool from the pools map and overwrite the
|
|
107
|
+
// pool factory create function.
|
|
108
|
+
// The background is that the generic pool will keep trying to establish a connection by invoking the factory
|
|
109
|
+
// create function until the `acquireTimeoutMillis` is reached.
|
|
110
|
+
// This leads to numerous connection attempts for a single request, even when the credentials are invalid.
|
|
111
|
+
// Due to the deletion in the map, subsequent requests will retrieve the credentials again.
|
|
112
|
+
p.on('factoryCreateError', async function (err) {
|
|
113
|
+
if (err._connectError) {
|
|
114
|
+
LOG._warn && LOG.warn(INVALID_CREDENTIALS_WARNING)
|
|
115
|
+
pools.delete(tenant)
|
|
116
|
+
if (p._factory && p._factory.create) {
|
|
117
|
+
// reject after 100 ms to not block CPU completely
|
|
118
|
+
p._factory.create = () =>
|
|
119
|
+
new Promise((resolve, reject) => setTimeout(() => reject(INVALID_CREDENTIALS_ERROR), 100))
|
|
120
|
+
}
|
|
121
|
+
await p.drain()
|
|
122
|
+
await p.clear()
|
|
123
|
+
}
|
|
135
124
|
})
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
125
|
+
|
|
126
|
+
resolve(p)
|
|
127
|
+
})
|
|
128
|
+
.catch(e => {
|
|
129
|
+
// delete pools entry if fetching credentials failed
|
|
130
|
+
pools.delete(tenant)
|
|
131
|
+
reject(e)
|
|
132
|
+
})
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
pools.set(tenant, poolPromise)
|
|
141
136
|
}
|
|
142
137
|
|
|
143
138
|
if ('then' in pools.get(tenant)) {
|
|
@@ -150,9 +145,10 @@ async function pool4(tenant, db) {
|
|
|
150
145
|
async function resilientAcquire(pool, attempts = 1) {
|
|
151
146
|
// max 3 attempts
|
|
152
147
|
attempts = Math.min(attempts, 3)
|
|
153
|
-
let client
|
|
154
|
-
|
|
155
|
-
|
|
148
|
+
let client,
|
|
149
|
+
err,
|
|
150
|
+
attempt = 0
|
|
151
|
+
|
|
156
152
|
while (!client && attempt < attempts) {
|
|
157
153
|
try {
|
|
158
154
|
client = await pool.acquire()
|
|
@@ -60,8 +60,10 @@ const search2cqn4sql = (query, entity, options) => {
|
|
|
60
60
|
subQuery.where(expression)
|
|
61
61
|
|
|
62
62
|
// suppress the localize handler from redirecting the subQuery's target to the localized view
|
|
63
|
-
|
|
63
|
+
subQuery.SELECT.localized = false
|
|
64
|
+
|
|
64
65
|
query.where('exists', subQuery)
|
|
66
|
+
|
|
65
67
|
return query
|
|
66
68
|
}
|
|
67
69
|
|
|
@@ -33,7 +33,7 @@ const sqliteLocalized = function (req) {
|
|
|
33
33
|
if (!req.locale) return
|
|
34
34
|
|
|
35
35
|
// suppress localization by instruction
|
|
36
|
-
if (query.
|
|
36
|
+
if (query.SELECT.localized === false) return
|
|
37
37
|
|
|
38
38
|
// suppress localization for pure counts
|
|
39
39
|
const columns = query.SELECT.columns
|
|
@@ -28,8 +28,8 @@ const _addKeysDeep = (keys, keysCollector, ignoreManagedBacklinks) => {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
function _keysOf(entity, ignoreManagedBacklinks) {
|
|
31
|
-
if (!entity || !entity.keys) return
|
|
32
31
|
const keysCollector = []
|
|
32
|
+
if (!entity || !entity.keys) return keysCollector
|
|
33
33
|
_addKeysDeep(entity.keys, keysCollector, ignoreManagedBacklinks)
|
|
34
34
|
return keysCollector
|
|
35
35
|
}
|