@cap-js/db-service 1.3.1 → 1.3.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
@@ -1,10 +1,17 @@
1
- # Change Log
1
+ # Changelog
2
2
 
3
3
  - All notable changes to this project are documented in this file.
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 1.3.1 - 2023-10-10
7
+ ## [1.3.2](https://github.com/cap-js/cds-dbs/compare/db-service-v1.3.1...db-service-v1.3.2) (2023-10-13)
8
+
9
+
10
+ ### Fixed
11
+
12
+ - preserve $count for result of SELECT queries ([#280](https://github.com/cap-js/cds-dbs/issues/280)) ([23bef24](https://github.com/cap-js/cds-dbs/commit/23bef245e62952a57ed82afcfd238c0b294b2e9e))
13
+
14
+ ## [1.3.1](https://github.com/cap-js/cds-dbs/compare/db-service-v1.3.0...db-service-v1.3.1) (2023-10-10)
8
15
 
9
16
  ### Fixed
10
17
 
package/README.md CHANGED
@@ -12,6 +12,11 @@ This project is open to feature requests/suggestions, bug reports etc. via [GitH
12
12
 
13
13
  Contribution and feedback are encouraged and always welcome. For more information about how to contribute, the project structure, as well as additional contribution information, see our [Contribution Guidelines](CONTRIBUTING.md).
14
14
 
15
+ ## Versioning
16
+
17
+ This library follows [Semantic Versioning](https://semver.org/).
18
+ All notable changes are documented in [CHANGELOG.md](CHANGELOG.md).
19
+
15
20
  ## Code of Conduct
16
21
 
17
22
  We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone. By participating in this project, you agree to abide by its [Code of Conduct](CODE_OF_CONDUCT.md) at all times.
package/lib/SQLService.js CHANGED
@@ -18,7 +18,7 @@ class SQLService extends DatabaseService {
18
18
  init() {
19
19
  this.on(['SELECT'], this.transformStreamFromCQN)
20
20
  this.on(['UPDATE'], this.transformStreamIntoCQN)
21
- this.on(['INSERT', 'UPSERT', 'UPDATE'], require('./fill-in-keys')) // REVISIT should be replaced by correct input processing eventually
21
+ this.on(['INSERT', 'UPSERT', 'UPDATE'], require('./fill-in-keys')) // REVISIT: should be replaced by correct input processing eventually
22
22
  this.on(['INSERT', 'UPSERT', 'UPDATE'], require('./deep-queries').onDeep)
23
23
  this.on(['SELECT'], this.onSELECT)
24
24
  this.on(['INSERT'], this.onINSERT)
@@ -79,7 +79,10 @@ class SQLService extends DatabaseService {
79
79
  let rows = await ps.all(values)
80
80
  if (rows.length)
81
81
  if (cqn.SELECT.expand) rows = rows.map(r => (typeof r._json_ === 'string' ? JSON.parse(r._json_) : r._json_ || r))
82
- if (cqn.SELECT.count) rows.$count = await this.count(query, rows)
82
+ if (cqn.SELECT.count) {
83
+ // REVISIT: the runtime always expects that the count is preserved with .map, required for renaming in mocks
84
+ return SQLService._arrayWithCount(rows, await this.count(query, rows))
85
+ }
83
86
  return cqn.SELECT.one || query.SELECT.from?.ref?.[0].cardinality?.max === 1 ? rows[0] : rows
84
87
  }
85
88
 
@@ -252,6 +255,17 @@ class SQLService extends DatabaseService {
252
255
  */
253
256
  static CQN2SQL = require('./cqn2sql').class
254
257
 
258
+ // REVISIT: There must be a better way!
259
+ // preserves $count for .map calls on array
260
+ static _arrayWithCount = function (a, count) {
261
+ const _map = a.map
262
+ const map = function (..._) { return SQLService._arrayWithCount(_map.call(a, ..._), count) }
263
+ return Object.defineProperties(a, {
264
+ $count: { value: count, enumerable: false, configurable: true, writable: true },
265
+ map: { value: map, enumerable: false, configurable: true, writable: true }
266
+ })
267
+ }
268
+
255
269
  /** @param {unknown[]} args */
256
270
  constructor(...args) {
257
271
  super(...args)
@@ -387,6 +401,7 @@ const _unquirked = q => {
387
401
  return q
388
402
  }
389
403
 
404
+
390
405
  const sqls = new class extends SQLService { get factory() { return null } }
391
406
  cds.extend(cds.ql.Query).with(
392
407
  class {
package/lib/cqn4sql.js CHANGED
@@ -12,7 +12,7 @@ const infer = require('./infer')
12
12
  const eqOps = [['is'], ['='] /* ['=='] */]
13
13
  /**
14
14
  * For operators of <notEqOps>, do the same but use or instead of and.
15
- * This ensures that not struc == <value> is the same as struc != <value>.
15
+ * This ensures that not struct == <value> is the same as struct != <value>.
16
16
  */
17
17
  const notEqOps = [['is', 'not'], ['<>'], ['!=']]
18
18
  /**
@@ -668,12 +668,24 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
668
668
  // everything after the wildcard, is a potential replacement
669
669
  // in the wildcard expansion
670
670
  const replace = []
671
+
672
+ const baseRef = col.ref || []
673
+ const baseRefLinks = col.$refLinks || []
674
+
675
+ // column has no ref, then it is an anonymous expand:
676
+ // select from books { { * } as bar }
677
+ // only possible if there is exactly one query source
678
+ if (!baseRef.length) {
679
+ const [tableAlias, definition] = Object.entries(inferred.sources)[0]
680
+ baseRef.push(tableAlias)
681
+ baseRefLinks.push({ definition, source: definition })
682
+ }
671
683
  // we need to make the refs absolute
672
684
  col[prop].slice(wildcardIndex + 1).forEach(c => {
673
685
  const fakeColumn = { ...c }
674
686
  if (fakeColumn.ref) {
675
- fakeColumn.ref = [...col.ref, ...fakeColumn.ref]
676
- fakeColumn.$refLinks = [...col.$refLinks, ...c.$refLinks]
687
+ fakeColumn.ref = [...baseRef, ...fakeColumn.ref]
688
+ fakeColumn.$refLinks = [...baseRefLinks, ...c.$refLinks]
677
689
  }
678
690
  replace.push(fakeColumn)
679
691
  })
@@ -682,15 +694,15 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
682
694
  // fake the ref since excluding only has strings
683
695
  col.excluding.forEach(c => {
684
696
  const fakeColumn = {
685
- ref: [...col.ref, c],
697
+ ref: [...baseRef, c],
686
698
  }
687
699
  exclude.push(fakeColumn)
688
700
  })
689
701
  }
690
702
 
691
- if (col.$refLinks[col.$refLinks.length - 1].definition.kind === 'entity')
692
- res.push(...getColumnsForWildcard(exclude, replace))
693
- else
703
+ if (baseRefLinks.at(-1).definition.kind === 'entity') {
704
+ res.push(...getColumnsForWildcard(exclude, replace, col.as))
705
+ } else
694
706
  res.push(
695
707
  ...getFlatColumnsFor(col, { columnAlias: col.as, tableAlias: getQuerySourceName(col) }, [], {
696
708
  exclude,
@@ -907,12 +919,14 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
907
919
  *
908
920
  * Furthermore, foreign keys (FK) for OData CSN and blobs are excluded from the wildcard expansion.
909
921
  *
910
- * @param {Array} exclude - An optional list of columns to be excluded during the wildcard expansion.
911
- * @param {Array} replace - An optional list of columns to replace during the wildcard expansion.
922
+ * @param {array} exclude - An optional list of columns to be excluded during the wildcard expansion.
923
+ * @param {array} replace - An optional list of columns to replace during the wildcard expansion.
924
+ * @param {string} baseName - the explicit alias of the column.
925
+ * Only possible for anonymous expands on implicit table alias: `select from books { { * } as FOO }`
912
926
  *
913
927
  * @returns {Array} Returns an array of explicit columns derived from the wildcard.
914
928
  */
915
- function getColumnsForWildcard(exclude = [], replace = []) {
929
+ function getColumnsForWildcard(exclude = [], replace = [], baseName = null) {
916
930
  const wildcardColumns = []
917
931
  Object.keys(inferred.$combinedElements)
918
932
  .filter(k => !exclude.includes(k))
@@ -927,7 +941,13 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
927
941
  } else if (isCalculatedOnRead(element)) {
928
942
  wildcardColumns.push(resolveCalculatedElement(replace.find(r => r.as === k) || element))
929
943
  } else {
930
- const flatColumns = getFlatColumnsFor(element, { tableAlias: index }, [], { exclude, replace }, true)
944
+ const flatColumns = getFlatColumnsFor(
945
+ element,
946
+ { tableAlias: index, baseName },
947
+ [],
948
+ { exclude, replace },
949
+ true,
950
+ )
931
951
  wildcardColumns.push(...flatColumns)
932
952
  }
933
953
  })
@@ -969,7 +989,16 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
969
989
  * Columns excluded in a wildcard expansion or replaced by other columns are also handled accordingly.
970
990
  *
971
991
  * @param {object} column - The structured element which needs to be expanded.
972
- * @param {string} baseName - The prefixes of the column reference (joined with '_'). Optional.
992
+ * @param {{
993
+ * columnAlias: string
994
+ * tableAlias: string
995
+ * baseName: string
996
+ * }} names - configuration object for naming parameters:
997
+ * columnAlias - The explicit alias which the user has defined for the column.
998
+ * For instance `{ struct.foo as bar}` will be transformed into
999
+ * `{ struct_foo_leaf1 as bar_foo_leaf1, struct_foo_leaf2 as bar_foo_leaf2 }`.
1000
+ * tableAlias - The table alias to prepend to the column name. Optional.
1001
+ * baseName - The prefixes of the column reference (joined with '_'). Optional.
973
1002
  * @param {string} columnAlias - The explicit alias which the user has defined for the column.
974
1003
  * For instance `{ struct.foo as bar}` will be transformed into
975
1004
  * `{ struct_foo_leaf1 as bar_foo_leaf1, struct_foo_leaf2 as bar_foo_leaf2 }`.
@@ -1352,7 +1381,7 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
1352
1381
  if (flatRhs) {
1353
1382
  const flatLhs = flattenWithBaseName(token)
1354
1383
 
1355
- //Revisit: Early exit here? We kndow we cant compare the structs, however we do not know exactly why
1384
+ //REVISIT: Early exit here? We kndow we cant compare the structs, however we do not know exactly why
1356
1385
  // --> calculate error message or exit early? See test "proper error if structures cannot be compared / too many elements on lhs"
1357
1386
  if (flatRhs.length !== flatLhs.length)
1358
1387
  // make sure we can compare both structures
@@ -1419,10 +1448,10 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
1419
1448
 
1420
1449
  function assertNoStructInXpr(token, inInfixFilter = false) {
1421
1450
  if (!inInfixFilter && token.$refLinks?.[token.$refLinks.length - 1].definition.target)
1422
- // revisit: let this through if not requested otherwise
1451
+ // REVISIT: let this through if not requested otherwise
1423
1452
  rejectAssocInExpression()
1424
1453
  if (isStructured(token.$refLinks?.[token.$refLinks.length - 1].definition))
1425
- // revisit: let this through if not requested otherwise
1454
+ // REVISIT: let this through if not requested otherwise
1426
1455
  rejectStructInExpression()
1427
1456
 
1428
1457
  function rejectAssocInExpression() {
@@ -173,7 +173,9 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
173
173
  const nextStep = ref[1]?.id || ref[1]
174
174
  // no unmanaged assoc in infix filter path
175
175
  if (!expandOrExists && e.on)
176
- throw new Error(`"${e.name}" in path "${arg.ref.map(idOnly).join('.')}" must not be an unmanaged association`)
176
+ throw new Error(
177
+ `"${e.name}" in path "${arg.ref.map(idOnly).join('.')}" must not be an unmanaged association`,
178
+ )
177
179
  // no non-fk traversal in infix filter
178
180
  if (!expandOrExists && nextStep && !(nextStep in e.foreignKeys))
179
181
  throw new Error(`Only foreign keys of "${e.name}" can be accessed in infix filter`)
@@ -339,13 +341,11 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
339
341
  }
340
342
 
341
343
  // walk over all paths in other query properties
342
- if (where)
343
- walkTokenStream(where)
344
- if (groupBy)
345
- groupBy.forEach(token => inferQueryElement(token, false))
346
- if (having)
347
- walkTokenStream(having)
348
- if (_.with) // consider UPDATE.with
344
+ if (where) walkTokenStream(where)
345
+ if (groupBy) groupBy.forEach(token => inferQueryElement(token, false))
346
+ if (having) walkTokenStream(having)
347
+ if (_.with)
348
+ // consider UPDATE.with
349
349
  Object.values(_.with).forEach(val => inferQueryElement(val, false))
350
350
 
351
351
  return queryElements
@@ -355,8 +355,8 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
355
355
  * on the information whether the next token is resolved within an `exists` predicates.
356
356
  * If such a token has an infix filter, it is not join relevant, because the filter
357
357
  * condition is applied to the generated `exists <subquery>` condition.
358
- *
359
- * @param {array} tokenStream
358
+ *
359
+ * @param {array} tokenStream
360
360
  */
361
361
  function walkTokenStream(tokenStream) {
362
362
  let skipJoins
@@ -476,12 +476,10 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
476
476
  // if any path step points to an artifact with `@cds.persistence.skip`
477
477
  // we must ignore the element from the queries elements
478
478
  let isPersisted = true
479
- const firstStepIsTableAlias =
480
- (column.ref.length > 1 && column.ref[0] in sources) ||
481
- // nested projection on table alias
482
- (column.ref.length === 1 && column.ref[0] in sources && column.inline)
479
+ const firstStepIsTableAlias = column.ref.length > 1 && column.ref[0] in sources
483
480
  const firstStepIsSelf =
484
481
  !firstStepIsTableAlias && column.ref.length > 1 && ['$self', '$projection'].includes(column.ref[0])
482
+ const expandOnTableAlias = column.ref.length === 1 && column.ref[0] in sources && (column.expand || column.inline)
485
483
  const nameSegments = []
486
484
  // if a (segment) of a (structured) foreign key is renamed, we must not include
487
485
  // the aliased ref segments into the name of the final foreign key which is e.g. used in
@@ -535,6 +533,12 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
535
533
  const $refLink = { definition, target: $combinedElements[id][0].tableAlias }
536
534
  column.$refLinks.push($refLink)
537
535
  nameSegments.push(id)
536
+ } else if (expandOnTableAlias) {
537
+ // expand on table alias
538
+ column.$refLinks.push({
539
+ definition: sources[id],
540
+ target: sources[id],
541
+ })
538
542
  } else {
539
543
  stepNotFoundInCombinedElements(id) // REVISIT: fails with {__proto__:elements)
540
544
  }
@@ -545,9 +549,9 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
545
549
 
546
550
  if (firstStepIsSelf && element?.isAssociation) {
547
551
  throw new Error(
548
- `Paths starting with “$self” must not contain steps of type “cds.Association”: ref: [ ${column.ref.map(
549
- idOnly,
550
- ).join(', ')} ]`,
552
+ `Paths starting with “$self” must not contain steps of type “cds.Association”: ref: [ ${column.ref
553
+ .map(idOnly)
554
+ .join(', ')} ]`,
551
555
  )
552
556
  }
553
557
 
@@ -619,7 +623,12 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
619
623
  if (insertIntoQueryElements) queryElements[column.as || flatName] = getCopyWithAnnos(column, base)
620
624
  } else if (column.expand) {
621
625
  const elements = resolveExpand(column)
622
- if (insertIntoQueryElements) queryElements[column.as || flatName] = elements
626
+ let elementName
627
+ // expand on table alias
628
+ if (column.$refLinks.length === 1 && column.$refLinks[0].definition.kind === 'entity')
629
+ elementName = column.$refLinks[0].alias
630
+ else elementName = column.as || flatName
631
+ if (insertIntoQueryElements) queryElements[elementName] = elements
623
632
  } else if (column.inline && insertIntoQueryElements) {
624
633
  const elements = resolveInline(column)
625
634
  queryElements = { ...queryElements, ...elements }
@@ -752,8 +761,8 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
752
761
  */
753
762
  function resolveExpand(col) {
754
763
  const { expand, $refLinks } = col
755
- const $leafLink = $refLinks?.[$refLinks.length - 1]
756
- if ($leafLink?.definition._target) {
764
+ const $leafLink = $refLinks?.[$refLinks.length - 1] || inferred.SELECT.from.$refLinks.at(-1) // fallback to anonymous expand
765
+ if ($leafLink.definition._target) {
757
766
  const expandSubquery = {
758
767
  SELECT: {
759
768
  from: $leafLink.definition._target.name,
@@ -148,7 +148,6 @@ class JoinTree {
148
148
  *
149
149
  * It begins by inferring the source of the given column, which is the table alias where the column is resolvable.
150
150
  * Each step during this process represents a node in the join tree. If a node already exists in the tree, the current step is replaced by the already merged node.
151
- * For each step, it checks whether it has been seen before. If so, it resets the $refLink to point to the already merged $refLink.
152
151
  * If not, it creates a new Node and ensures proper aliasing and foreign key access.
153
152
  *
154
153
  * @param {object} col - The column object to be merged into the existing join tree. This object should have the properties $refLinks and ref.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cap-js/db-service",
3
- "version": "1.3.1",
3
+ "version": "1.3.2",
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": {