@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 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', (e,p) => _shutdown (e, LOG.error('❗️Uncaught',p)))
210
- process.on('uncaughtException', (e) => _shutdown (e, LOG.error('❗️Uncaught',e)))
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)
@@ -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
- super({ event, target, data, query: unbound ? {} : query, user, method, headers, req, res, _queryOptions })
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
- return cds
47
- .tx(req)
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
- return compareJson(combinedData, revertedPersistent, req.target, { ignoreDraftColumns: true })
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 compareJson(originalData, undefined, req.target, { ignoreDraftColumns: true })
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
- result.push(...ctUtils.array(elementValue))
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].push(...newData)
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.push(...(await tx.run(selectCQN)))
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.push(...(await tx.run(selectCQN)))
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.push(...ctUtils.cleanDeepData(subEntity, subData))
36
- result.push(...subData)
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].push(...updateCQNs)
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.push(...insertCQN.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(...unwrappedSubData)
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(...subCQNs)
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(..._getMergedWhere(applicablesForEntity))
124
+ for (const val of _getMergedWhere(applicablesForEntity)) newIdentifier.where.push(val)
125
125
  }
126
126
 
127
127
  newRef.push(newIdentifier)
@@ -60,7 +60,7 @@ exports.impl = cds.service.impl(function () {
60
60
  }
61
61
 
62
62
  if (req.event === 'READ' && req.query?.SELECT) {
63
- req.query.SELECT.localized = true
63
+ req.query.SELECT.localized ??= true
64
64
  }
65
65
 
66
66
  if (!result) {
@@ -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([newStream.column], targetTransition)
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
  }
@@ -11,7 +11,7 @@ const _redirectXpr = (xpr, localize) => {
11
11
  }
12
12
 
13
13
  if (ele.SELECT) {
14
- if (!ele._suppressLocalization) redirect(ele.SELECT, localize)
14
+ if (ele.SELECT.localized) redirect(ele.SELECT, localize)
15
15
  }
16
16
  })
17
17
  }
@@ -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
- Object.defineProperty(q, '_suppressLocalization', { value: true })
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
- rootQuery.SELECT.columns = [{ ref: ['DraftAdministrativeData'], expand: [{ ref: ['InProcessByUser'] }] }]
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
- if (drafts.SELECT.columns?.some(c => c === '*')) {
571
- drafts.SELECT.columns = drafts.SELECT.columns.filter(c => c !== '*')
572
- if (!drafts.SELECT.columns.some(c => c.ref?.[0] === 'DraftAdministrativeData_DraftUUID')) {
573
- drafts.SELECT.columns.push({ ref: ['DraftAdministrativeData_DraftUUID'] })
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._suppressLocalization = true // in the future we should be able to just set activeCQN.SELECT.localized = false
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
- this._datasource.run(activeCQN),
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._suppressLocalization) return
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
- // NOTE: disableCache: true means "force fetch credentials from service manager"
19
- async function credentials4(tenant, { disableCache = false }) {
20
- const { credentials } = cds.env.requires.db
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
- pools.set(
93
- tenant,
94
- 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
-
101
- const INVALID_CREDENTIALS_WARNING = `Could not establish connection for tenant "${tenant}". Existing pool will be drained.`
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
- resolve(p)
130
- })
131
- .catch(e => {
132
- // delete pools entry if fetching credentials failed
133
- pools.delete(tenant)
134
- reject(e)
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
- }).then(p => {
137
- pools.set(tenant, p)
138
- return p
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
- let err
155
- let attempt = 0
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
- Object.defineProperty(subQuery, '_suppressLocalization', { value: true })
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._suppressLocalization) return
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sap/cds",
3
- "version": "7.1.0",
3
+ "version": "7.1.2",
4
4
  "description": "SAP Cloud Application Programming Model - CDS for Node.js",
5
5
  "homepage": "https://cap.cloud.sap/",
6
6
  "keywords": [