@cap-js/db-service 1.19.1 → 1.20.1

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,30 @@
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
+ ## [1.20.1](https://github.com/cap-js/cds-dbs/compare/db-service-v1.20.0...db-service-v1.20.1) (2025-05-27)
8
+
9
+
10
+ ### Fixed
11
+
12
+ * **`search`:** do not search on non-projected elements ([#1198](https://github.com/cap-js/cds-dbs/issues/1198)) ([1461673](https://github.com/cap-js/cds-dbs/commit/14616730ba8c27e8ffa30c5962b881badfab991c))
13
+ * current_utctimestamp as default ([#1161](https://github.com/cap-js/cds-dbs/issues/1161)) ([c0cccad](https://github.com/cap-js/cds-dbs/commit/c0cccad921c45db96e14f0e2afeced6af69da4a2))
14
+ * exists within expression is properly detected ([#1156](https://github.com/cap-js/cds-dbs/issues/1156)) ([febe175](https://github.com/cap-js/cds-dbs/commit/febe1755186d291b92edbdf69cebbab68a53d0af))
15
+ * resilience for query re-use scenarios ([#1175](https://github.com/cap-js/cds-dbs/issues/1175)) ([fe9abd5](https://github.com/cap-js/cds-dbs/commit/fe9abd5e4c5e9153c0afad9164f240ae2eadf581))
16
+
17
+ ## [1.20.0](https://github.com/cap-js/cds-dbs/compare/db-service-v1.19.1...db-service-v1.20.0) (2025-04-17)
18
+
19
+
20
+ ### Added
21
+
22
+ * Result set streaming ([#702](https://github.com/cap-js/cds-dbs/issues/702)) ([2fe02ea](https://github.com/cap-js/cds-dbs/commit/2fe02eafd02993e5697efbdab90ad997fb2c9e00))
23
+
24
+
25
+ ### Fixed
26
+
27
+ * **`expand`:** proper subquery `from` construction ([#1126](https://github.com/cap-js/cds-dbs/issues/1126)) ([e343e79](https://github.com/cap-js/cds-dbs/commit/e343e7978acc0c181a012f61b6181b7c558aa178)), closes [#1114](https://github.com/cap-js/cds-dbs/issues/1114) [#1112](https://github.com/cap-js/cds-dbs/issues/1112)
28
+ * Improved support for special characters in column names ([#1141](https://github.com/cap-js/cds-dbs/issues/1141)) ([ba04697](https://github.com/cap-js/cds-dbs/commit/ba046971921d645e8571a80c27ef07988c8c01ad))
29
+ * **infer:** for localized queries, use `localized.<entity>` as `_target` ([#1140](https://github.com/cap-js/cds-dbs/issues/1140)) ([b08707b](https://github.com/cap-js/cds-dbs/commit/b08707b76a53c74f1c4388a8be4d0818506388c5))
30
+
7
31
  ## [1.19.1](https://github.com/cap-js/cds-dbs/compare/db-service-v1.19.0...db-service-v1.19.1) (2025-04-01)
8
32
 
9
33
 
package/lib/SQLService.js CHANGED
@@ -1,6 +1,7 @@
1
1
  const cds = require('@sap/cds'),
2
2
  DEBUG = cds.debug('sql|db')
3
- const { Readable } = require('stream')
3
+ const { Readable, Transform } = require('stream')
4
+ const { pipeline } = require('stream/promises')
4
5
  const { resolveView, getDBTable, getTransition } = require('@sap/cds/libx/_runtime/common/utils/resolveView')
5
6
  const DatabaseService = require('./common/DatabaseService')
6
7
  const cqn4sql = require('./cqn4sql')
@@ -117,7 +118,7 @@ class SQLService extends DatabaseService {
117
118
  * Handler for SELECT
118
119
  * @type {Handler}
119
120
  */
120
- async onSELECT({ query, data }) {
121
+ async onSELECT({ query, data, iterator, objectMode }) {
121
122
  // REVISIT: for custom joins, infer is called twice, which is bad
122
123
  // --> make cds.infer properly work with custom joins and remove this
123
124
  if (!(query._target instanceof cds.entity)) {
@@ -131,35 +132,56 @@ class SQLService extends DatabaseService {
131
132
  const { sql, values, cqn } = this.cqn2sql(query, data)
132
133
  const expand = query.SELECT.expand
133
134
  delete query.SELECT.expand
135
+ const isOne = cqn.SELECT.one || query.SELECT.from?.ref?.[0].cardinality?.max === 1
134
136
 
135
137
  let ps = await this.prepare(sql)
136
- let rows = await ps.all(values)
137
- if (rows.length)
138
- if (expand) rows = rows.map(r => (typeof r._json_ === 'string' ? JSON.parse(r._json_) : r._json_ || r))
139
-
140
- if (cds.env.features.stream_compat) {
141
- if (query._streaming) {
142
- this._changeToStreams(cqn.SELECT.columns, rows, true, true)
143
- if (!rows.length) return
144
-
145
- const result = rows[0]
146
- // stream is always on position 0. Further properties like etag are inserted later.
147
- let [key, val] = Object.entries(result)[0]
148
- result.value = val
149
- delete result[key]
150
-
151
- return result
138
+ let rows = iterator ? await ps.stream(values, isOne, objectMode) : await ps.all(values)
139
+ try {
140
+ if (rows.length)
141
+ if (expand) rows = rows.map(r => (typeof r._json_ === 'string' ? JSON.parse(r._json_) : r._json_ || r))
142
+
143
+ if (!iterator) {
144
+ // REVISIT: remove after removing stream_compat feature flag
145
+ if (cds.env.features.stream_compat) {
146
+ if (query._streaming) {
147
+ if (!rows.length) return
148
+ this._changeToStreams(cqn.SELECT.columns, rows, true, true)
149
+ const result = rows[0]
150
+
151
+ // stream is always on position 0. Further properties like etag are inserted later.
152
+ let [key, val] = Object.entries(result)[0]
153
+ result.value = val
154
+ delete result[key]
155
+
156
+ return result
157
+ }
158
+ } else {
159
+ this._changeToStreams(cqn.SELECT.columns, rows, query.SELECT.one, false)
160
+ }
161
+ } else if (objectMode) {
162
+ const converter = (row) => this._changeToStreams(cqn.SELECT.columns, row, true)
163
+ const changeToStreams = new Transform({
164
+ objectMode: true,
165
+ transform(row, enc, cb) {
166
+ converter(row)
167
+ cb(null, row)
168
+ }
169
+ })
170
+ pipeline(rows, changeToStreams)
171
+ rows = changeToStreams
152
172
  }
153
- } else {
154
- this._changeToStreams(cqn.SELECT.columns, rows, query.SELECT.one, false)
155
- }
156
173
 
157
- if (cqn.SELECT.count) {
158
- // REVISIT: the runtime always expects that the count is preserved with .map, required for renaming in mocks
159
- return SQLService._arrayWithCount(rows, await this.count(query, rows))
160
- }
174
+ if (cqn.SELECT.count) {
175
+ // REVISIT: the runtime always expects that the count is preserved with .map, required for renaming in mocks
176
+ return SQLService._arrayWithCount(rows, await this.count(query, rows))
177
+ }
161
178
 
162
- return cqn.SELECT.one || query.SELECT.from?.ref?.[0].cardinality?.max === 1 ? rows[0] : rows
179
+ return iterator !== false && isOne ? rows[0] : rows
180
+ } catch (err) {
181
+ // Ensure that iterators receive pre stream errors
182
+ if (iterator) rows.emit('error', err)
183
+ throw err
184
+ }
163
185
  }
164
186
 
165
187
  /**
@@ -314,7 +336,7 @@ class SQLService extends DatabaseService {
314
336
  * @returns {Promise<number>}
315
337
  */
316
338
  async count(query, ret) {
317
- if (ret) {
339
+ if (ret?.length) {
318
340
  const { one, limit: _ } = query.SELECT,
319
341
  n = ret.length
320
342
  const [max, offset = 0] = one ? [1] : _ ? [_.rows?.val, _.offset?.val] : []
@@ -355,6 +377,7 @@ class SQLService extends DatabaseService {
355
377
  // preserves $count for .map calls on array
356
378
  static _arrayWithCount = function (a, count) {
357
379
  const _map = a.map
380
+
358
381
  const map = function (..._) {
359
382
  return SQLService._arrayWithCount(_map.call(a, ..._), count)
360
383
  }
@@ -475,7 +498,6 @@ SQLService.prototype.PreparedStatement = PreparedStatement
475
498
  const _target_name4 = q => {
476
499
  const target =
477
500
  q._target_ref ||
478
- q.from_into_ntt ||
479
501
  q.SELECT?.from ||
480
502
  q.INSERT?.into ||
481
503
  q.UPSERT?.into ||
package/lib/cqn2sql.js CHANGED
@@ -119,7 +119,7 @@ class CQN2SQLRenderer {
119
119
  * @param {import('./infer/cqn').CREATE} q
120
120
  */
121
121
  CREATE(q) {
122
- let { target } = q
122
+ let { _target: target } = q
123
123
  let query = target?.query || q.CREATE.as
124
124
  if (!target || target._unresolved) {
125
125
  const entity = q.CREATE.entity
@@ -213,7 +213,7 @@ class CQN2SQLRenderer {
213
213
  * @param {import('./infer/cqn').DROP} q
214
214
  */
215
215
  DROP(q) {
216
- const { target } = q
216
+ const { _target: target } = q
217
217
  const isView = target?.query || target?.projection || q.DROP.view
218
218
  const name = target?.name || q.DROP.table?.ref?.[0] || q.DROP.view?.ref?.[0]
219
219
  return (this.sql = `DROP ${isView ? 'VIEW' : 'TABLE'} IF EXISTS ${this.quote(this.name(name, q))}`)
@@ -307,8 +307,7 @@ class CQN2SQLRenderer {
307
307
  }
308
308
  : x => {
309
309
  const name = this.column_name(x)
310
- const escaped = `${name.replace(/"/g, '""')}`
311
- return `'$."${escaped}"',${this.output_converter4(x.element, this.quote(name))}`
310
+ return `${this.string(`$.${JSON.stringify(name)}`)},${this.output_converter4(x.element, this.quote(name))}`
312
311
  }).flat()
313
312
 
314
313
  if (isSimple) return `SELECT ${cols} FROM (${sql})`
@@ -429,8 +428,8 @@ class CQN2SQLRenderer {
429
428
  return orderBy.map(c => {
430
429
  const o = localized
431
430
  ? this.expr(c) +
432
- (c.element?.[this.class._localized] ? ' COLLATE NOCASE' : '') +
433
- (c.sort?.toLowerCase() === 'desc' || c.sort === -1 ? ' DESC' : ' ASC')
431
+ (c.element?.[this.class._localized] ? ' COLLATE NOCASE' : '') +
432
+ (c.sort?.toLowerCase() === 'desc' || c.sort === -1 ? ' DESC' : ' ASC')
434
433
  : this.expr(c) + (c.sort?.toLowerCase() === 'desc' || c.sort === -1 ? ' DESC' : ' ASC')
435
434
  if (c.nulls) return o + ' NULLS ' + (c.nulls.toLowerCase() === 'first' ? 'FIRST' : 'LAST')
436
435
  return o
@@ -524,13 +523,14 @@ class CQN2SQLRenderer {
524
523
  // Include this.values for placeholders
525
524
  /** @type {unknown[][]} */
526
525
  this.entries = []
527
- if (INSERT.entries[0] instanceof Readable) {
526
+ if (INSERT.entries[0] instanceof Readable && !INSERT.entries[0].readableObjectMode) {
528
527
  INSERT.entries[0].type = 'json'
529
528
  this.entries = [[...this.values, INSERT.entries[0]]]
530
529
  } else {
531
- const stream = Readable.from(this.INSERT_entries_stream(INSERT.entries), { objectMode: false })
530
+ const entries = INSERT.entries[0]?.[Symbol.iterator] || INSERT.entries[0]?.[Symbol.asyncIterator] || INSERT.entries[0] instanceof Readable ? INSERT.entries[0] : INSERT.entries
531
+ const stream = Readable.from(this.INSERT_entries_stream(entries), { objectMode: false })
532
532
  stream.type = 'json'
533
- stream._raw = INSERT.entries
533
+ stream._raw = entries
534
534
  this.entries = [[...this.values, stream]]
535
535
  }
536
536
 
@@ -545,7 +545,7 @@ class CQN2SQLRenderer {
545
545
  let buffer = '['
546
546
 
547
547
  let sep = ''
548
- for (const row of entries) {
548
+ for await (const row of entries) {
549
549
  buffer += `${sep}{`
550
550
  if (!sep) sep = ','
551
551
 
@@ -619,7 +619,7 @@ class CQN2SQLRenderer {
619
619
  if (val != null && elements[this.columns[key]]?.type in this.BINARY_TYPES) {
620
620
  val = Buffer.from(val, 'base64').toString(binaryEncoding)
621
621
  }
622
- buffer += `${sepsub}${val === undefined ? 'null' : JSON.stringify(val)}`
622
+ buffer += `${sepsub}${val == null ? 'null' : JSON.stringify(val)}`
623
623
  }
624
624
 
625
625
  if (!sepsub) sepsub = ','
@@ -1102,7 +1102,7 @@ class CQN2SQLRenderer {
1102
1102
 
1103
1103
  let onInsert = this.managed_session_context(element[cdsOnInsert]?.['='])
1104
1104
  || this.managed_session_context(element.default?.ref?.[0])
1105
- || (element.default?.val !== undefined && { val: element.default.val, param: false })
1105
+ || (element.default && { __proto__: element.default, param: false })
1106
1106
  let onUpdate = this.managed_session_context(element[cdsOnUpdate]?.['='])
1107
1107
 
1108
1108
  if (onInsert) onInsert = this.expr(onInsert)
@@ -1143,8 +1143,8 @@ class CQN2SQLRenderer {
1143
1143
  managed_extract(name, element, converter) {
1144
1144
  const { UPSERT, INSERT } = this.cqn
1145
1145
  const extract = !(INSERT?.entries || UPSERT?.entries) && (INSERT?.rows || UPSERT?.rows)
1146
- ? `value->>'$[${this.columns.indexOf(name)}]'`
1147
- : `value->>'$."${name.replace(/"/g, '""')}"'`
1146
+ ? `value->>${this.string(`$[${this.columns.indexOf(name)}]`)}`
1147
+ : `value->>${this.string(`$.${JSON.stringify(name)}`)}`
1148
1148
  const sql = converter?.(extract) || extract
1149
1149
  return { extract, sql }
1150
1150
  }
package/lib/cqn4sql.js CHANGED
@@ -5,7 +5,7 @@ cds.infer.target ??= q => q._target || q.target // instanceof cds.entity ? q._ta
5
5
 
6
6
  const infer = require('./infer')
7
7
  const { computeColumnsToBeSearched } = require('./search')
8
- const { prettyPrintRef, isCalculatedOnRead, isCalculatedElement, getImplicitAlias } = require('./utils')
8
+ const { prettyPrintRef, isCalculatedOnRead, isCalculatedElement, getImplicitAlias, defineProperty, getModelUtils } = require('./utils')
9
9
 
10
10
  /**
11
11
  * For operators of <eqOps>, this is replaced by comparing all leaf elements with null, combined with and.
@@ -78,6 +78,7 @@ function cqn4sql(originalQuery, model) {
78
78
  }
79
79
  }
80
80
  inferred = infer(inferred, model)
81
+ const { getLocalizedName, isLocalized, getDefinition } = getModelUtils(model, originalQuery) // TODO: pass model to getModelUtils
81
82
  // if the query has custom joins we don't want to transform it
82
83
  // TODO: move all the way to the top of this function once cds.infer supports joins as well
83
84
  // we need to infer the query even if no transformation will happen because cds.infer can't calculate the target
@@ -225,7 +226,7 @@ function cqn4sql(originalQuery, model) {
225
226
  */
226
227
  function transformQueryForInsertUpsert(kind) {
227
228
  const { as } = transformedQuery[kind].into
228
- const target = cds.infer.target (inferred) // REVISIT: we should reliably use inferred._target instead
229
+ const target = cds.infer.target(inferred) // REVISIT: we should reliably use inferred._target instead
229
230
  transformedQuery[kind].into = { ref: [target.name] }
230
231
  if (as) transformedQuery[kind].into.as = as
231
232
  return transformedQuery
@@ -281,7 +282,7 @@ function cqn4sql(originalQuery, model) {
281
282
  const args = []
282
283
  if (r.queryArtifact.SELECT) args.push({ SELECT: transformSubquery(r.queryArtifact).SELECT, as: r.alias })
283
284
  else {
284
- const id = localized(r.queryArtifact)
285
+ const id = getLocalizedName(r.queryArtifact)
285
286
  args.push({ ref: [r.args ? { id, args: r.args } : id], as: r.alias })
286
287
  }
287
288
  from = { join: r.join || 'left', args, on: [] }
@@ -308,7 +309,7 @@ function cqn4sql(originalQuery, model) {
308
309
  ),
309
310
  )
310
311
 
311
- const id = localized(getDefinition(nextAssoc.$refLink.definition.target))
312
+ const id = getDefinition(nextAssoc.$refLink.definition.target).name
312
313
  const { args } = nextAssoc
313
314
  const arg = {
314
315
  ref: [args ? { id, args } : id],
@@ -801,8 +802,9 @@ function cqn4sql(originalQuery, model) {
801
802
  })
802
803
  } else {
803
804
  outerAlias = transformedQuery.SELECT.from.as
805
+ const getInnermostTarget = q => (q._target ? getInnermostTarget(q._target) : q)
804
806
  subqueryFromRef = [
805
- ...(transformedQuery.SELECT.from.ref || /* subq in from */ transformedQuery.SELECT.from.SELECT.from.ref),
807
+ ...(transformedQuery.SELECT.from.ref || /* subq in from */ [getInnermostTarget(transformedQuery).name]),
806
808
  ...ref,
807
809
  ]
808
810
  }
@@ -843,10 +845,7 @@ function cqn4sql(originalQuery, model) {
843
845
  }
844
846
  const expanded = transformSubquery(subquery)
845
847
  const correlated = _correlate({ ...expanded, as: columnAlias }, outerAlias)
846
- Object.defineProperty(correlated, 'elements', {
847
- value: expanded.elements,
848
- writable: true,
849
- })
848
+ defineProperty(correlated, 'elements', expanded.elements)
850
849
  return correlated
851
850
 
852
851
  function _correlate(subq, outer) {
@@ -1068,9 +1067,9 @@ function cqn4sql(originalQuery, model) {
1068
1067
  else {
1069
1068
  const outerQueries = inferred.outerQueries || []
1070
1069
  outerQueries.push(inferred)
1071
- Object.defineProperty(q, 'outerQueries', { value: outerQueries })
1070
+ defineProperty(q, 'outerQueries', outerQueries)
1072
1071
  }
1073
- const target = cds.infer.target (inferred) // REVISIT: we should reliably use inferred._target instead
1072
+ const target = cds.infer.target(inferred) // REVISIT: we should reliably use inferred._target instead
1074
1073
  if (isLocalized(target)) q.SELECT.localized = true
1075
1074
  if (q.SELECT.from.ref && !q.SELECT.from.as) assignUniqueSubqueryAlias()
1076
1075
  return cqn4sql(q, model)
@@ -1082,7 +1081,7 @@ function cqn4sql(originalQuery, model) {
1082
1081
  getImplicitAlias(last.id || last),
1083
1082
  inferred.outerQueries,
1084
1083
  )
1085
- Object.defineProperty(q.SELECT.from, 'uniqueSubqueryAlias', { value: uniqueSubqueryAlias })
1084
+ defineProperty(q.SELECT.from, 'uniqueSubqueryAlias', uniqueSubqueryAlias)
1086
1085
  }
1087
1086
  }
1088
1087
 
@@ -1309,7 +1308,7 @@ function cqn4sql(originalQuery, model) {
1309
1308
  const flatForeignKey = getDefinition(element.parent.name)?.elements[fkBaseName]
1310
1309
 
1311
1310
  setElementOnColumns(flatColumn, flatForeignKey || fkElement)
1312
- Object.defineProperty(flatColumn, '_csnPath', { value: csnPath, writable: true })
1311
+ defineProperty(flatColumn, '_csnPath', csnPath)
1313
1312
  flatColumns.push(flatColumn)
1314
1313
  }
1315
1314
  }
@@ -1341,7 +1340,7 @@ function cqn4sql(originalQuery, model) {
1341
1340
  if (column.sort) flatRef.sort = column.sort
1342
1341
  if (columnAlias) flatRef.as = columnAlias
1343
1342
  setElementOnColumns(flatRef, element)
1344
- Object.defineProperty(flatRef, '_csnPath', { value: csnPath, writable: true })
1343
+ defineProperty(flatRef, '_csnPath', csnPath)
1345
1344
  return [flatRef]
1346
1345
 
1347
1346
  function getReplacement(from) {
@@ -1674,7 +1673,7 @@ function cqn4sql(originalQuery, model) {
1674
1673
  const transformedWhere = []
1675
1674
  let transformedFrom = copy(from) // REVISIT: too expensive!
1676
1675
  if (from.$refLinks)
1677
- Object.defineProperty(transformedFrom, '$refLinks', { value: [...from.$refLinks], writable: true })
1676
+ defineProperty(transformedFrom, '$refLinks', [...from.$refLinks])
1678
1677
  if (from.args) {
1679
1678
  transformedFrom.args = []
1680
1679
  from.args.forEach(arg => {
@@ -1739,7 +1738,7 @@ function cqn4sql(originalQuery, model) {
1739
1738
  * with the main query alias. see @function expandColumn()
1740
1739
  * There is one exception:
1741
1740
  * - if current and next have the same alias, we need to assign a new alias to the next
1742
- *
1741
+ *
1743
1742
  */
1744
1743
  if (!(inferred.SELECT?.expand === true && current.alias.toLowerCase() !== as.toLowerCase())) {
1745
1744
  as = getNextAvailableTableAlias(as)
@@ -1786,7 +1785,7 @@ function cqn4sql(originalQuery, model) {
1786
1785
  const subquerySource =
1787
1786
  getDefinition(transformedFrom.$refLinks[0].definition.target) || transformedFrom.$refLinks[0].target
1788
1787
  if (subquerySource.params && !args) args = {}
1789
- const id = localized(subquerySource)
1788
+ const id = getLocalizedName(subquerySource)
1790
1789
  transformedFrom.ref = [args ? { id, args } : id]
1791
1790
 
1792
1791
  return { transformedWhere, transformedFrom }
@@ -1922,10 +1921,7 @@ function cqn4sql(originalQuery, model) {
1922
1921
  const refLinkFaker = thing => {
1923
1922
  const { ref } = thing
1924
1923
  const assocHost = getParentEntity(assocRefLink.definition)
1925
- Object.defineProperty(thing, '$refLinks', {
1926
- value: [],
1927
- writable: true,
1928
- })
1924
+ defineProperty(thing, '$refLinks', [])
1929
1925
  let pseudoPath = false
1930
1926
  ref.reduce((prev, res, i) => {
1931
1927
  if (res === '$self') {
@@ -1994,9 +1990,7 @@ function cqn4sql(originalQuery, model) {
1994
1990
  }
1995
1991
  // assumption: if first step is the association itself, all following ref steps must be resolvable
1996
1992
  // within target `assoc.assoc.fk` -> `assoc.assoc_fk`
1997
- else if (
1998
- lhsFirstDef === getParentEntity(assocRefLink.definition).elements[assocRefLink.definition.name]
1999
- )
1993
+ else if (lhsFirstDef === getParentEntity(assocRefLink.definition).elements[assocRefLink.definition.name])
2000
1994
  result[i].ref = [assocRefLink.alias, lhs.ref.slice(lhs.ref[0] === '$self' ? 2 : 1).join('_')]
2001
1995
  // naive assumption: if the path starts with an association which is not the association from
2002
1996
  // which the on-condition originates, it must be a foreign key and hence resolvable in the source
@@ -2077,7 +2071,7 @@ function cqn4sql(originalQuery, model) {
2077
2071
  // pseudo element
2078
2072
  return element
2079
2073
  if (element.kind === 'entity') return element
2080
- else return getDefinition(localized(getParentEntity(element.parent)))
2074
+ else return getDefinition(getParentEntity(element.parent).name)
2081
2075
  }
2082
2076
  }
2083
2077
 
@@ -2164,8 +2158,8 @@ function cqn4sql(originalQuery, model) {
2164
2158
  on.push(...(customWhere && hasLogicalOr(unmanagedOn) ? [asXpr(unmanagedOn)] : unmanagedOn))
2165
2159
  }
2166
2160
 
2167
- const subquerySource = assocTarget(nextDefinition) || nextDefinition
2168
- const id = localized(subquerySource)
2161
+ const subquerySource = getDefinition(nextDefinition.target) || nextDefinition
2162
+ const id = getLocalizedName(subquerySource)
2169
2163
  if (subquerySource.params && !customArgs) customArgs = {}
2170
2164
  const SELECT = {
2171
2165
  from: {
@@ -2203,52 +2197,6 @@ function cqn4sql(originalQuery, model) {
2203
2197
  return SELECT
2204
2198
  }
2205
2199
 
2206
- /**
2207
- * If the query is `localized`, return the name of the `localized` entity for the `definition`.
2208
- * If there is no `localized` entity for the `definition`, return the name of the `definition`
2209
- *
2210
- * @param {CSN.definition} definition
2211
- * @returns the name of the localized entity for the given `definition` or `definition.name`
2212
- */
2213
- function localized(definition) {
2214
- if (!isLocalized(definition)) return definition.name
2215
- const view = getDefinition(`localized.${definition.name}`)
2216
- return view?.name || definition.name
2217
- }
2218
-
2219
- /**
2220
- * If a given query is required to be translated, the query has
2221
- * the `.localized` property set to `true`. If that is the case,
2222
- * and the definition has not set the `@cds.localized` annotation
2223
- * to `false`, the given definition must be translated.
2224
- *
2225
- * @returns true if the given definition shall be localized
2226
- */
2227
- function isLocalized(definition) {
2228
- return (
2229
- inferred.SELECT?.localized &&
2230
- definition['@cds.localized'] !== false &&
2231
- !inferred.SELECT.forUpdate &&
2232
- !inferred.SELECT.forShareLock
2233
- )
2234
- }
2235
-
2236
- /** returns the CSN definition for the given name from the model */
2237
- function getDefinition(name) {
2238
- if (!name) return null
2239
- return model.definitions[name]
2240
- }
2241
-
2242
- /**
2243
- * Get the csn definition of the target of a given association
2244
- *
2245
- * @param assoc
2246
- * @returns the csn definition of the association target or null if it is not an association
2247
- */
2248
- function assocTarget(assoc) {
2249
- return getDefinition(assoc.target) || null
2250
- }
2251
-
2252
2200
  /**
2253
2201
  * For a given search expression return a function "search" which holds the search expression
2254
2202
  * as well as the searchable columns as arguments.
@@ -2416,10 +2364,7 @@ function getParentEntity(element) {
2416
2364
  * @param {csn.Element} element
2417
2365
  */
2418
2366
  function setElementOnColumns(col, element) {
2419
- Object.defineProperty(col, 'element', {
2420
- value: element,
2421
- writable: true,
2422
- })
2367
+ defineProperty(col, 'element', element)
2423
2368
  }
2424
2369
 
2425
2370
  const getName = col => col.as || col.ref?.at(-1)
@@ -4,7 +4,7 @@ const cds = require('@sap/cds')
4
4
 
5
5
  const JoinTree = require('./join-tree')
6
6
  const { pseudos } = require('./pseudos')
7
- const { isCalculatedOnRead, getImplicitAlias } = require('../utils')
7
+ const { isCalculatedOnRead, getImplicitAlias, getModelUtils, defineProperty } = require('../utils')
8
8
  const cdsTypes = cds.linked({
9
9
  definitions: {
10
10
  Timestamp: { type: 'cds.Timestamp' },
@@ -27,6 +27,8 @@ function infer(originalQuery, model) {
27
27
  if (!model) throw new Error('Please specify a model')
28
28
  const inferred = originalQuery
29
29
 
30
+ const { getDefinition } = getModelUtils(model, originalQuery)
31
+
30
32
  // REVISIT: The more edge use cases we support, thes less optimized are we for the 90+% use cases
31
33
  // e.g. there's a lot of overhead for infer( SELECT.from(Books) )
32
34
  if (originalQuery.SET) throw new Error('”UNION” based queries are not supported')
@@ -73,7 +75,7 @@ function infer(originalQuery, model) {
73
75
  joinTree: { value: joinTree, writable: true, configurable: true }, // REVISIT: eliminate
74
76
  })
75
77
  // also enrich original query -> writable because it may be inferred again
76
- Object.defineProperty(originalQuery, 'elements', { value: elements, writable: true, configurable: true })
78
+ defineProperty(originalQuery, 'elements', elements)
77
79
  }
78
80
  return inferred
79
81
 
@@ -111,13 +113,13 @@ function infer(originalQuery, model) {
111
113
 
112
114
  inferArg(from, null, null, { inFrom: true })
113
115
  const alias =
114
- from.uniqueSubqueryAlias ||
115
- from.as ||
116
- (ref.length === 1
117
- ? getImplicitAlias(first, useTechnicalAlias)
118
- : getImplicitAlias(ref.at(-1).id || ref.at(-1), useTechnicalAlias));
116
+ from.uniqueSubqueryAlias ||
117
+ from.as ||
118
+ (ref.length === 1
119
+ ? getImplicitAlias(first, useTechnicalAlias)
120
+ : getImplicitAlias(ref.at(-1).id || ref.at(-1), useTechnicalAlias))
119
121
  if (alias in querySources) throw new Error(`Duplicate alias "${alias}"`)
120
- querySources[alias] = { definition: target, args }
122
+ querySources[alias] = { definition: getDefinition(target.name), args }
121
123
  const last = from.$refLinks.at(-1)
122
124
  last.alias = alias
123
125
  } else if (from.args) {
@@ -169,10 +171,7 @@ function infer(originalQuery, model) {
169
171
  * @param {csn.Element} element
170
172
  */
171
173
  function setElementOnColumns(col, element) {
172
- Object.defineProperty(col, 'element', {
173
- value: element,
174
- writable: true,
175
- })
174
+ defineProperty(col, 'element', element)
176
175
  }
177
176
 
178
177
  /**
@@ -209,7 +208,7 @@ function infer(originalQuery, model) {
209
208
  if (col.func) {
210
209
  if (col.args) {
211
210
  // {func}.args are optional
212
- applyToFunctionArgs(col.args, inferArg, [false, null, {dollarSelfRefs}])
211
+ applyToFunctionArgs(col.args, inferArg, [false, null, { dollarSelfRefs }])
213
212
  }
214
213
  queryElements[as] = getElementForCast(col)
215
214
  }
@@ -243,7 +242,7 @@ function infer(originalQuery, model) {
243
242
  // link $refLinks -> special name resolution rules for orderBy
244
243
  orderBy.forEach(token => {
245
244
  let $baseLink
246
- let rejectJoinRelevantPath
245
+ let needsElementsOfQueryAsBase
247
246
  // first check if token ref is resolvable in query elements
248
247
  if (columns) {
249
248
  const firstStep = token.ref?.[0].id || token.ref?.[0]
@@ -251,14 +250,11 @@ function infer(originalQuery, model) {
251
250
  const columnName = c.as || c.flatName || c.ref?.at(-1).id || c.ref?.at(-1) || c.func
252
251
  return columnName === firstStep
253
252
  })
254
- const needsElementsOfQueryAsBase =
253
+ needsElementsOfQueryAsBase =
255
254
  tokenPointsToQueryElements &&
256
- queryElements[token.ref?.[0]] &&
257
- /* expand on structure can be addressed */ !queryElements[token.ref?.[0]].$assocExpand
255
+ queryElements[firstStep] &&
256
+ /* expand on structure can be addressed */ !queryElements[firstStep].$assocExpand
258
257
 
259
- // if the ref points into the query itself and follows an exposed association
260
- // to a non-fk column, we must reject the ref, as we can't join with the queries own results
261
- rejectJoinRelevantPath = needsElementsOfQueryAsBase
262
258
  if (needsElementsOfQueryAsBase) $baseLink = { definition: { elements: queryElements }, target: inferred }
263
259
  } else {
264
260
  // fallback to elements of query source
@@ -266,7 +262,9 @@ function infer(originalQuery, model) {
266
262
  }
267
263
 
268
264
  inferArg(token, queryElements, $baseLink, { inQueryModifier: true })
269
- if (token.isJoinRelevant && rejectJoinRelevantPath) {
265
+ // if the ref points into the query itself and follows an exposed association
266
+ // to a non-fk column, we must reject the ref, as we can't join with the queries own results
267
+ if (token.isJoinRelevant && needsElementsOfQueryAsBase) {
270
268
  // reverse the array, find the last association and calculate the index of the association in non-reversed order
271
269
  const assocIndex =
272
270
  token.$refLinks.length - 1 - token.$refLinks.reverse().findIndex(link => link.definition.isAssociation)
@@ -350,7 +348,7 @@ function infer(originalQuery, model) {
350
348
  }
351
349
 
352
350
  function handleRef(col, inXpr) {
353
- inferArg(col, queryElements, null, { inXpr })
351
+ inferArg(col, queryElements, null, { inXpr })
354
352
  const { definition } = col.$refLinks[col.$refLinks.length - 1]
355
353
  if (col.cast)
356
354
  // final type overwritten -> element not visible anymore
@@ -399,22 +397,27 @@ function infer(originalQuery, model) {
399
397
  */
400
398
 
401
399
  function inferArg(arg, queryElements = null, $baseLink = null, context = {}) {
402
- const { inExists, inXpr, inCalcElement, baseColumn, inInfixFilter, inQueryModifier, inFrom, dollarSelfRefs } = context
400
+ const { inExists, inXpr, inCalcElement, baseColumn, inInfixFilter, inQueryModifier, inFrom, dollarSelfRefs } =
401
+ context
403
402
  if (arg.param || arg.SELECT) return // parameter references are only resolved into values on execution e.g. :val, :1 or ?
404
403
  if (arg.args) applyToFunctionArgs(arg.args, inferArg, [null, $baseLink, context])
405
404
  if (arg.list) arg.list.forEach(arg => inferArg(arg, null, $baseLink, context))
406
- if (arg.xpr) arg.xpr.forEach(token => inferArg(token, queryElements, $baseLink, { ...context, inXpr: true })) // e.g. function in expression
405
+ if (arg.xpr)
406
+ arg.xpr.forEach((token, i) =>
407
+ inferArg(token, queryElements, $baseLink, { ...context, inXpr: true, inExists: arg.xpr[i - 1] === 'exists' }),
408
+ ) // e.g. function in expression
407
409
 
408
410
  if (!arg.ref) {
409
411
  if (arg.expand && queryElements) queryElements[arg.as] = resolveExpand(arg)
410
412
  return
411
413
  }
412
414
 
413
- // initialize $refLinks
414
- Object.defineProperty(arg, '$refLinks', {
415
- value: [],
416
- writable: true,
417
- })
415
+ // Before the arg is linked, it's meta information should be cleaned up.
416
+ // This may be important if one manipulates the arg object
417
+ // __after__ a query has been fired and re-uses the manipulated query
418
+ defineProperty(arg, '$refLinks', [])
419
+ defineProperty(arg, 'isJoinRelevant', false)
420
+
418
421
  // if any path step points to an artifact with `@cds.persistence.skip`
419
422
  // we must ignore the element from the queries elements
420
423
  let isPersisted = true
@@ -424,8 +427,8 @@ function infer(originalQuery, model) {
424
427
  firstStepIsSelf = !firstStepIsTableAlias && arg.ref.length > 1 && ['$self', '$projection'].includes(arg.ref[0])
425
428
  expandOnTableAlias = arg.ref.length === 1 && arg.ref[0] in sources && (arg.expand || arg.inline)
426
429
  }
427
- if(dollarSelfRefs && firstStepIsSelf) {
428
- Object.defineProperty(arg, 'inXpr', { value: true, writable: true })
430
+ if (dollarSelfRefs && firstStepIsSelf) {
431
+ defineProperty(arg, 'inXpr', true)
429
432
  dollarSelfRefs.push(arg)
430
433
  return
431
434
  }
@@ -452,7 +455,7 @@ function infer(originalQuery, model) {
452
455
  const nextStep = arg.ref[1]?.id || arg.ref[1]
453
456
  if (isNonForeignKeyNavigation(element, nextStep)) {
454
457
  if (inExists) {
455
- Object.defineProperty($baseLink, 'pathExpressionInsideFilter', { value: true })
458
+ defineProperty($baseLink, 'pathExpressionInsideFilter', true)
456
459
  } else {
457
460
  rejectNonFkNavigation(element, element.on ? $baseLink.definition.name : nextStep)
458
461
  }
@@ -516,7 +519,7 @@ function infer(originalQuery, model) {
516
519
  const nextStep = arg.ref[i + 1]?.id || arg.ref[i + 1]
517
520
  if (isNonForeignKeyNavigation(element, nextStep)) {
518
521
  if (inExists) {
519
- Object.defineProperty($baseLink, 'pathExpressionInsideFilter', { value: true })
522
+ defineProperty($baseLink, 'pathExpressionInsideFilter', true)
520
523
  } else {
521
524
  rejectNonFkNavigation(element, element.on ? $baseLink.definition.name : nextStep)
522
525
  }
@@ -532,7 +535,7 @@ function infer(originalQuery, model) {
532
535
  } else if (id === '$dummy') {
533
536
  // `some.known.element.$dummy` -> no error; used by cds.ql to simulate joins
534
537
  arg.$refLinks.push({ definition: { name: '$dummy', parent: arg.$refLinks[i - 1].target } })
535
- Object.defineProperty(arg, 'isJoinRelevant', { value: true })
538
+ defineProperty(arg, 'isJoinRelevant', true)
536
539
  } else {
537
540
  const notFoundIn = pseudoPath ? arg.ref[i - 1] : getFullPathForLinkedArg(arg)
538
541
  stepNotFoundInPredecessor(id, notFoundIn)
@@ -558,7 +561,7 @@ function infer(originalQuery, model) {
558
561
  const definition = arg.$refLinks[i].definition
559
562
  if ((!definition.target && definition.kind !== 'entity') || (!inFrom && danglingFilter))
560
563
  throw new Error('A filter can only be provided when navigating along associations')
561
- if (!inFrom && !arg.expand) Object.defineProperty(arg, 'isJoinRelevant', { value: true })
564
+ if (!inFrom && !arg.expand)defineProperty(arg, 'isJoinRelevant', true)
562
565
  let skipJoinsForFilter = false
563
566
  step.where.forEach(token => {
564
567
  if (token === 'exists') {
@@ -587,7 +590,7 @@ function infer(originalQuery, model) {
587
590
  if (getDefinition(arg.$refLinks[i].definition.target)?.['@cds.persistence.skip'] === true) isPersisted = false
588
591
  if (!arg.ref[i + 1]) {
589
592
  const flatName = nameSegments.join('_')
590
- Object.defineProperty(arg, 'flatName', { value: flatName, writable: true })
593
+ defineProperty(arg, 'flatName', flatName)
591
594
  // if column is casted, we overwrite it's origin with the new type
592
595
  if (arg.cast) {
593
596
  const base = getElementForCast(arg)
@@ -632,7 +635,7 @@ function infer(originalQuery, model) {
632
635
  })
633
636
 
634
637
  // we need inner joins for the path expressions inside filter expressions after exists predicate
635
- if ($baseLink?.pathExpressionInsideFilter) Object.defineProperty(arg, 'join', { value: 'inner' })
638
+ if ($baseLink?.pathExpressionInsideFilter) defineProperty(arg, 'join', 'inner')
636
639
 
637
640
  // ignore whole expand if target of assoc along path has ”@cds.persistence.skip”
638
641
  if (arg.expand) {
@@ -652,7 +655,7 @@ function infer(originalQuery, model) {
652
655
  ? { ref: [...baseColumn.ref, ...arg.ref], $refLinks: [...baseColumn.$refLinks, ...arg.$refLinks] }
653
656
  : arg
654
657
  if (isColumnJoinRelevant(colWithBase)) {
655
- Object.defineProperty(arg, 'isJoinRelevant', { value: true })
658
+ defineProperty(arg, 'isJoinRelevant', true)
656
659
  joinTree.mergeColumn(colWithBase, originalQuery.outerQueries)
657
660
  }
658
661
  }
@@ -758,7 +761,7 @@ function infer(originalQuery, model) {
758
761
  const res = $leafLink.definition.is2one
759
762
  ? new cds.struct({ elements: inferredExpandSubquery.elements })
760
763
  : new cds.array({ items: new cds.struct({ elements: inferredExpandSubquery.elements }) })
761
- return Object.defineProperty(res, '$assocExpand', { value: true })
764
+ return defineProperty(res, '$assocExpand', true)
762
765
  } else if ($leafLink.definition.elements) {
763
766
  let elements = {}
764
767
  expand.forEach(e => {
@@ -810,7 +813,8 @@ function infer(originalQuery, model) {
810
813
  else alreadySeenCalcElements.add(calcElement)
811
814
  const { ref, xpr } = calcElement.value
812
815
  if (ref || xpr) {
813
- baseLink = { definition: calcElement.parent, target: calcElement.parent }
816
+ const parentElementDefinition = getDefinition(calcElement.parent.name)
817
+ baseLink = { definition: parentElementDefinition, target: parentElementDefinition }
814
818
  inferArg(calcElement.value, null, baseLink, { inCalcElement: true, ...context })
815
819
  const basePath =
816
820
  column.$refLinks?.length > 1
@@ -825,7 +829,13 @@ function infer(originalQuery, model) {
825
829
 
826
830
  if (calcElement.value.args) {
827
831
  const processArgument = (arg, calcElement, column) => {
828
- inferArg(arg, null, { definition: calcElement.parent, target: calcElement.parent }, { inCalcElement: true })
832
+ const parentElementDefinition = getDefinition(calcElement.parent.name)
833
+ inferArg(
834
+ arg,
835
+ null,
836
+ { definition: parentElementDefinition, target: parentElementDefinition },
837
+ { inCalcElement: true },
838
+ )
829
839
  const basePath =
830
840
  column.$refLinks?.length > 1
831
841
  ? { $refLinks: column.$refLinks.slice(0, -1), ref: column.ref.slice(0, -1) }
@@ -879,8 +889,7 @@ function infer(originalQuery, model) {
879
889
  step[nestedProp].forEach(a => {
880
890
  // reset sub path for each nested argument
881
891
  // e.g. case when <path> then <otherPath> else <anotherPath> end
882
- if(!a.ref)
883
- subPath = { $refLinks: [...basePath.$refLinks], ref: [...basePath.ref] }
892
+ if (!a.ref) subPath = { $refLinks: [...basePath.$refLinks], ref: [...basePath.ref] }
884
893
  mergePathsIntoJoinTree(a, subPath)
885
894
  })
886
895
  }
@@ -891,13 +900,13 @@ function infer(originalQuery, model) {
891
900
  const calcElementIsJoinRelevant = isColumnJoinRelevant(p)
892
901
  if (calcElementIsJoinRelevant) {
893
902
  if (!calcElement.value.isJoinRelevant)
894
- Object.defineProperty(step, 'isJoinRelevant', { value: true, writable: true, })
903
+ defineProperty(step, 'isJoinRelevant',true)
895
904
  joinTree.mergeColumn(p, originalQuery.outerQueries)
896
905
  } else {
897
906
  // we need to explicitly set the value to false in this case,
898
907
  // e.g. `SELECT from booksCalc.Books { ID, author.{name }, author {name } }`
899
908
  // --> for the inline column, the name is join relevant, while for the expand, it is not
900
- Object.defineProperty(step, 'isJoinRelevant', { value: false, writable: true })
909
+ defineProperty(step, 'isJoinRelevant', false)
901
910
  }
902
911
  }
903
912
  }
@@ -960,7 +969,7 @@ function infer(originalQuery, model) {
960
969
  if (Object.keys(queryElements).length === 0 && aliases.length === 1) {
961
970
  const { elements } = getDefinitionFromSources(sources, aliases[0])
962
971
  // only one query source and no overwritten columns
963
- for (const k of Object.keys(elements)) {
972
+ for (const k in elements) {
964
973
  if (!exclude(k)) {
965
974
  const element = elements[k]
966
975
  if (element.type !== 'cds.LargeBinary') {
@@ -1055,7 +1064,6 @@ function infer(originalQuery, model) {
1055
1064
  * @returns {object} a copy of @param base with all annotations of @param from
1056
1065
  * @TODO prototype based
1057
1066
  */
1058
- // REVISIT: TODO: inferred.elements should be linked
1059
1067
  function getCopyWithAnnos(from, base) {
1060
1068
  const result = { ...base }
1061
1069
  // REVISIT: we don't need to and hence should not handle annotations at runtime
@@ -1063,7 +1071,7 @@ function infer(originalQuery, model) {
1063
1071
  if (prop.startsWith('@')) result[prop] = from[prop]
1064
1072
  }
1065
1073
 
1066
- if (from.as && base.name !== from.as) Object.defineProperty(result, 'name', { value: from.as }) // TODO double check if this is needed
1074
+ if (from.as && base.name !== from.as) defineProperty(result, 'name', from.as) // TODO double check if this is needed
1067
1075
  // in subqueries we need the linked element if an outer query accesses it
1068
1076
  return Object.setPrototypeOf(result, base)
1069
1077
  }
@@ -1083,12 +1091,6 @@ function infer(originalQuery, model) {
1083
1091
  }
1084
1092
  }
1085
1093
 
1086
- /** returns the CSN definition for the given name from the model */
1087
- function getDefinition(name) {
1088
- if (!name) return null
1089
- return model.definitions[name]
1090
- }
1091
-
1092
1094
  function getDefinitionFromSources(sources, id) {
1093
1095
  return sources[id].definition
1094
1096
  }
@@ -170,7 +170,7 @@ class JoinTree {
170
170
  // find the correct query source
171
171
  if (
172
172
  r.queryArtifact === head.target ||
173
- r.queryArtifact === head.target.target /** might as well be a query for order by */
173
+ r.queryArtifact === head.target._target /** might as well be a query for order by */
174
174
  )
175
175
  node = r
176
176
  })
package/lib/search.js CHANGED
@@ -79,7 +79,7 @@ const _getSearchableColumns = entity => {
79
79
  const column = entity.elements[columnName]
80
80
  if (column?.isAssociation || columnName.includes('.')) {
81
81
  deepSearchCandidates.push({ ref: columnName.split('.') })
82
- continue;
82
+ continue
83
83
  }
84
84
  cdsSearchColumnMap.set(columnName, annotationValue)
85
85
  }
@@ -93,8 +93,8 @@ const _getSearchableColumns = entity => {
93
93
  // `@cds.search { element1: true }` or `@cds.search { element1 }`
94
94
  if (annotatedColumnValue) return true
95
95
 
96
- // calculated elements are only searchable if requested through `@cds.search`
97
- if(column.value) return false
96
+ // calculated elements are only searchable if requested through `@cds.search`
97
+ if (column.value) return false
98
98
 
99
99
  // if at least one element is explicitly annotated as searchable, e.g.:
100
100
  // `@cds.search { element1: true }` or `@cds.search { element1 }`
@@ -112,16 +112,23 @@ const _getSearchableColumns = entity => {
112
112
 
113
113
  if (deepSearchCandidates.length) {
114
114
  deepSearchCandidates.forEach(c => {
115
- const element = c.ref.reduce((resolveIn, curr, i) => {
116
- const next = resolveIn.elements?.[curr] || resolveIn._target.elements[curr]
117
- if (next?.isAssociation && !c.ref[i + 1]) {
118
- const searchInTarget = _getSearchableColumns(next._target)
119
- searchInTarget.forEach(elementRefInTarget => {
120
- searchableColumns.push({ ref: c.ref.concat(...elementRefInTarget.ref) })
121
- })
115
+ let element = entity
116
+ for (let i = 0; i < c.ref.length; ++i) {
117
+ const curr = c.ref[i]
118
+ const next = element.elements?.[curr] ?? element._target?.elements?.[curr]
119
+
120
+ if (!next) { // e.g. if a search element is not part of a projection
121
+ element = undefined
122
+ break
122
123
  }
123
- return next
124
- }, entity)
124
+
125
+ if (next.isAssociation && i === c.ref.length - 1) {
126
+ _getSearchableColumns(next._target).forEach(r => searchableColumns.push({ ref: c.ref.concat(...r.ref) }))
127
+ }
128
+
129
+ element = next
130
+ }
131
+
125
132
  if (element?.type === DEFAULT_SEARCHABLE_TYPE) {
126
133
  searchableColumns.push({ ref: c.ref })
127
134
  }
@@ -129,9 +136,8 @@ const _getSearchableColumns = entity => {
129
136
  }
130
137
 
131
138
  return searchableColumns.map(column => {
132
- if(column.ref)
133
- return column
134
- return { ref: [ column.name ] }
139
+ if (column.ref) return column
140
+ return { ref: [column.name] }
135
141
  })
136
142
  }
137
143
 
@@ -183,15 +189,16 @@ const computeColumnsToBeSearched = (cqn, entity = { __searchableColumns: [] }) =
183
189
  }
184
190
  })
185
191
  } else {
186
- if(entity.kind === 'entity') {
192
+ if (entity.kind === 'entity') {
187
193
  // first check cache
188
- toBeSearched = entity.own('__searchableColumns') || entity.set('__searchableColumns', _getSearchableColumns(entity))
194
+ toBeSearched =
195
+ entity.own('__searchableColumns') || entity.set('__searchableColumns', _getSearchableColumns(entity))
189
196
  } else {
190
197
  // if we search on a subquery, we don't have a cache
191
198
  toBeSearched = _getSearchableColumns(entity)
192
199
  }
193
200
  toBeSearched = toBeSearched.map(c => {
194
- const column = {ref: [...c.ref]}
201
+ const column = { ref: [...c.ref] }
195
202
  return column
196
203
  })
197
204
  }
package/lib/utils.js CHANGED
@@ -40,32 +40,103 @@ function isCalculatedElement(def) {
40
40
 
41
41
  /**
42
42
  * Calculates the implicit table alias for a given string.
43
- *
43
+ *
44
44
  * Based on the last part of the string, the implicit alias is calculated
45
45
  * by taking the first character and prepending it with '$'.
46
46
  * A leading '$' is removed if the last part already starts with '$'.
47
- *
47
+ *
48
48
  * @example
49
49
  * getImplicitAlias('Books') => '$B'
50
50
  * getImplicitAlias('bookshop.Books') => '$B'
51
51
  * getImplicitAlias('bookshop.$B') => '$B'
52
- *
52
+ *
53
53
  * @param {string} str - The input string.
54
- * @returns {string}
54
+ * @returns {string}
55
55
  */
56
56
  function getImplicitAlias(str, useTechnicalAlias = true) {
57
57
  const index = str.lastIndexOf('.')
58
- if(useTechnicalAlias) {
58
+ if (useTechnicalAlias) {
59
59
  const postfix = (index != -1 ? str.substring(index + 1) : str).replace(/^\$/, '')[0] || /* str === '$' */ '$'
60
60
  return '$' + postfix
61
61
  }
62
62
  return index != -1 ? str.substring(index + 1) : str
63
63
  }
64
64
 
65
+ function defineProperty(obj, prop, value) {
66
+ return Object.defineProperty(obj, prop, {
67
+ value,
68
+ writable: true,
69
+ configurable: true,
70
+ })
71
+ }
72
+
73
+ /**
74
+ * Shared utility functions which operate dynamically on the model / query.
75
+ *
76
+ * @param {CSN.model} model
77
+ * @param {CQL} query
78
+ */
79
+ function getModelUtils(model, query) {
80
+ /**
81
+ * Returns the name of the localized entity for the given `definition`.
82
+ *
83
+ * If the query is `localized`, returns the name of the `localized` version of the `definition`.
84
+ * If there is no `localized` version of the `definition`, return the name of the `definition`
85
+ *
86
+ * @param {CSN.definition} definition
87
+ * @returns the name of the localized entity for the given `definition` or `definition.name`
88
+ */
89
+ function getLocalizedName(definition) {
90
+ if (!isLocalized(definition)) return definition.name
91
+ const view = getDefinition(`localized.${definition.name}`)
92
+ return view?.name || definition.name
93
+ }
94
+
95
+ /**
96
+ * Returns true if the definition shall be localized, in the context of the given query.
97
+ *
98
+ * If a given query is required to be translated, the query has
99
+ * the `.localized` property set to `true`. If that is the case,
100
+ * and the definition has not set the `@cds.localized` annotation
101
+ * to `false`, the given definition must be translated.
102
+ *
103
+ * @returns true if the given definition shall be localized
104
+ */
105
+ function isLocalized(definition) {
106
+ return (
107
+ query.SELECT?.localized &&
108
+ definition?.['@cds.localized'] !== false &&
109
+ !query.SELECT.forUpdate &&
110
+ !query.SELECT.forShareLock
111
+ )
112
+ }
113
+
114
+ /**
115
+ * Returns the (potentially localized) CSN definition for the given name from the model.
116
+ *
117
+ * @param {string} name - The name of the definition to retrieve.
118
+ * @returns {Object|null} The CSN definition or null if not found. The definition may be localized.
119
+ */
120
+ function getDefinition(name) {
121
+ if (!name) return null
122
+ const def = model.definitions[name]
123
+ if (!def || !isLocalized(def)) return def
124
+ return model.definitions[`localized.${def.name}`] || def
125
+ }
126
+
127
+ return {
128
+ getLocalizedName,
129
+ isLocalized,
130
+ getDefinition,
131
+ }
132
+ }
133
+
65
134
  // export the function to be used in other modules
66
135
  module.exports = {
67
136
  prettyPrintRef,
68
137
  isCalculatedOnRead,
69
138
  isCalculatedElement,
70
139
  getImplicitAlias,
140
+ defineProperty,
141
+ getModelUtils,
71
142
  }
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@cap-js/db-service",
3
- "version": "1.19.1",
3
+ "version": "1.20.1",
4
4
  "description": "CDS base database service",
5
5
  "homepage": "https://github.com/cap-js/cds-dbs/tree/main/db-service#cds-base-database-service",
6
6
  "repository": {
7
7
  "type": "git",
8
- "url": "https://github.com/cap-js/cds-dbs"
8
+ "url": "git+https://github.com/cap-js/cds-dbs.git"
9
9
  },
10
10
  "bugs": {
11
11
  "url": "https://github.com/cap-js/cds-dbs/issues"