@sap/cds 7.1.1 → 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 +13 -0
- 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/db/utils/coloredTxCommands.js +5 -3
- package/libx/_runtime/fiori/lean-draft.js +18 -16
- package/libx/_runtime/hana/pool.js +52 -56
- package/libx/odata/afterburner.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,18 @@
|
|
|
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
|
+
|
|
7
19
|
## Version 7.1.1 - 2023-08-01
|
|
8
20
|
|
|
9
21
|
### Fixed
|
|
@@ -312,6 +324,7 @@ cds env requires/cds.xt.ModelProviderService
|
|
|
312
324
|
## Version 6.6.0 - 2023-02-27
|
|
313
325
|
|
|
314
326
|
### Added
|
|
327
|
+
|
|
315
328
|
- Improved error handling for `cds build` if the SaaS base model is missing in an extension project.
|
|
316
329
|
- Support for reliable paging using `$skiptoken`. Can be activated via `cds.query.limit.reliablePaging = true`
|
|
317
330
|
- Built-in models are now added to existing model options of custom build tasks.
|
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)
|
|
@@ -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
|
}
|
|
@@ -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
|
}
|
|
@@ -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()
|
|
@@ -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
|
}
|