@cap-js/db-service 1.0.0 → 1.0.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,15 @@
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.0.1 - 2023-07-03
8
+
9
+ ### Fixed
10
+
11
+ - Paths addressing a column of the query via `$self.<column>` in `group by` / `order by`, `having` or `where`
12
+ are now correctly substituted.
13
+ - Mapping for OData `average` function to ANSI SQL compliant `avg` function.
14
+
15
+
7
16
  ## Version 1.0.0 - 2023-06-23
8
17
 
9
18
  - Initial Release
@@ -3,6 +3,7 @@ const StandardFunctions = {
3
3
 
4
4
  // String and Collection Functions
5
5
  // length : (x) => `length(${x})`,
6
+ average: x => `avg(${x})`,
6
7
  search: function (ref, arg) {
7
8
  if (!('val' in arg)) throw `SQLite only supports single value arguments for $search`
8
9
  const refs = ref.list || [ref],
package/lib/cqn4sql.js CHANGED
@@ -301,15 +301,24 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
301
301
  */
302
302
  function getTransformedColumns(columns) {
303
303
  const transformedColumns = []
304
-
305
304
  for (let i = 0; i < columns.length; i++) {
306
305
  const col = columns[i]
307
306
 
308
307
  if (col.expand) {
308
+ if (col.ref?.length > 1 && col.ref[0] === '$self' && !col.$refLinks[0].definition.kind) {
309
+ const dollarSelfReplacement = calculateDollarSelfColumn(col)
310
+ transformedColumns.push(...getTransformedColumns([dollarSelfReplacement]))
311
+ continue
312
+ }
309
313
  handleExpand(col)
310
314
  } else if (col.inline) {
311
315
  handleInline(col)
312
316
  } else if (col.ref) {
317
+ if (col.ref.length > 1 && col.ref[0] === '$self' && !col.$refLinks[0].definition.kind) {
318
+ const dollarSelfReplacement = calculateDollarSelfColumn(col)
319
+ transformedColumns.push(...getTransformedColumns([dollarSelfReplacement]))
320
+ continue
321
+ }
313
322
  handleRef(col)
314
323
  } else if (col === '*') {
315
324
  handleWildcard(columns)
@@ -391,7 +400,7 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
391
400
  if (replaceWith === -1) transformedColumns.push(transformedColumn)
392
401
  else transformedColumns.splice(replaceWith, 1, transformedColumn)
393
402
 
394
- Object.defineProperty(transformedColumn, 'element', { value: originalQuery.elements[col.as] })
403
+ setElementOnColumns(transformedColumn, originalQuery.elements[col.as])
395
404
  }
396
405
 
397
406
  function getTransformedColumn(col) {
@@ -424,6 +433,64 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
424
433
  }
425
434
  }
426
435
 
436
+ /**
437
+ * This function resolves a `ref` starting with a `$self`.
438
+ * Such a path targets another element of the query by it's implicit, or explicit alias.
439
+ *
440
+ * A `$self` reference may also target another `$self` path. In this case, this function
441
+ * recursively resolves the tail of the `$self` references (`$selfPath.ref.slice(2)`) onto it's
442
+ * new base.
443
+ *
444
+ * @param {object} col with a ref like `[ '$self', <target column>, <optional further path navigation> ]`
445
+ * @param {boolean} omitAlias if we replace a $self reference in an aggregation or a token stream, we must not add an "as" to the result
446
+ */
447
+ function calculateDollarSelfColumn(col, omitAlias = false) {
448
+ const dummyColumn = buildDummyColumnForDollarSelf({ ...col }, col.$refLinks)
449
+
450
+ return dummyColumn
451
+
452
+ function buildDummyColumnForDollarSelf(dollarSelfColumn, $refLinks) {
453
+ const { ref, as } = dollarSelfColumn
454
+ const stepToFind = ref[1]
455
+ let referencedColumn = inferred.SELECT.columns.find(
456
+ otherColumn =>
457
+ otherColumn !== dollarSelfColumn &&
458
+ (otherColumn.as
459
+ ? stepToFind === otherColumn.as
460
+ : stepToFind === otherColumn.ref?.[otherColumn.ref.length - 1]),
461
+ )
462
+ if (referencedColumn.ref?.[0] === '$self') {
463
+ referencedColumn = buildDummyColumnForDollarSelf({ ...referencedColumn }, referencedColumn.$refLinks)
464
+ }
465
+
466
+ if (referencedColumn.ref) {
467
+ dollarSelfColumn.ref = [...referencedColumn.ref, ...dollarSelfColumn.ref.slice(2)]
468
+ Object.defineProperties(dollarSelfColumn, {
469
+ flatName: {
470
+ value: dollarSelfColumn.ref.join('_'),
471
+ },
472
+ isJoinRelevant: {
473
+ value: referencedColumn.isJoinRelevant,
474
+ },
475
+ $refLinks: {
476
+ value: [...referencedColumn.$refLinks, ...$refLinks.slice(2)],
477
+ },
478
+ })
479
+ } else {
480
+ // target column is `val` or `xpr`, destructure and throw away the ref with the $self
481
+ // eslint-disable-next-line no-unused-vars
482
+ const { xpr, val, ref, as: _as, ...rest } = referencedColumn
483
+ if (xpr) rest.xpr = xpr
484
+ else rest.val = val
485
+ dollarSelfColumn = { ...rest } // reassign dummyColumn without 'ref'
486
+ if (!omitAlias) dollarSelfColumn.as = as
487
+ }
488
+ return dollarSelfColumn.ref?.[0] === '$self'
489
+ ? buildDummyColumnForDollarSelf({ ...dollarSelfColumn }, $refLinks)
490
+ : dollarSelfColumn
491
+ }
492
+ }
493
+
427
494
  /**
428
495
  * Calculates the columns for a nested projection on a structure.
429
496
  *
@@ -622,7 +689,7 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
622
689
  if (isLocalized(inferred.target)) subquery.SELECT.localized = true
623
690
  const expanded = transformSubquery(subquery)
624
691
  const correlated = _correlate({ ...expanded, as: columnAlias }, outerAlias)
625
- Object.defineProperty(correlated, 'elements', { value: subquery.elements })
692
+ Object.defineProperty(correlated, 'elements', { value: subquery.elements, writable: true })
626
693
  return correlated
627
694
 
628
695
  function _correlate(subq, outer) {
@@ -671,6 +738,11 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
671
738
  res.push({ ...col })
672
739
  } else if (col.ref) {
673
740
  if (col.$refLinks.some(link => link.definition._target?.['@cds.persistence.skip'] === true)) continue
741
+ if (col.ref.length > 1 && col.ref[0] === '$self' && !col.$refLinks[0].definition.kind) {
742
+ const dollarSelfReplacement = calculateDollarSelfColumn(col)
743
+ res.push(...getTransformedOrderByGroupBy([dollarSelfReplacement], inOrderBy))
744
+ continue
745
+ }
674
746
  const { target } = col.$refLinks[0]
675
747
  const tableAlias = target.SELECT ? null : getQuerySourceName(col) // do not prepend TA if orderBy column addresses element of query
676
748
  const leaf = col.$refLinks[col.$refLinks.length - 1].definition
@@ -903,8 +975,9 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
903
975
  if (columnAlias) flatColumn = { ref: [fkBaseName], as: `${columnAlias}_${fk.ref.join('_')}` }
904
976
  else flatColumn = { ref: [fkBaseName] }
905
977
  if (tableAlias) flatColumn.ref.unshift(tableAlias)
906
- Object.defineProperty(flatColumn, 'element', { value: fkElement })
907
- Object.defineProperty(flatColumn, '_csnPath', { value: csnPath })
978
+
979
+ setElementOnColumns(flatColumn, fkElement)
980
+ Object.defineProperty(flatColumn, '_csnPath', { value: csnPath, writable: true })
908
981
  flatColumns.push(flatColumn)
909
982
  }
910
983
  })
@@ -934,8 +1007,8 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
934
1007
  }
935
1008
  if (column.sort) flatRef.sort = column.sort
936
1009
  if (columnAlias) flatRef.as = columnAlias
937
- Object.defineProperty(flatRef, 'element', { value: element })
938
- Object.defineProperty(flatRef, '_csnPath', { value: csnPath })
1010
+ setElementOnColumns(flatRef, element)
1011
+ Object.defineProperty(flatRef, '_csnPath', { value: csnPath, writable: true })
939
1012
  return [flatRef]
940
1013
 
941
1014
  function getReplacement(from) {
@@ -962,11 +1035,11 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
962
1035
  * @returns {object[]} - The transformed token stream.
963
1036
  */
964
1037
  function getTransformedTokenStream(tokenStream, $baseLink = null) {
965
- const transformedWhere = []
1038
+ const transformedTokenStream = []
966
1039
  for (let i = 0; i < tokenStream.length; i++) {
967
1040
  const token = tokenStream[i]
968
1041
  if (token === 'exists') {
969
- transformedWhere.push(token)
1042
+ transformedTokenStream.push(token)
970
1043
  const whereExistsSubSelects = []
971
1044
  const { ref, $refLinks } = tokenStream[i + 1]
972
1045
  if (!ref) continue
@@ -1008,7 +1081,7 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
1008
1081
  }
1009
1082
 
1010
1083
  const whereExists = { SELECT: whereExistsSubqueries(whereExistsSubSelects) }
1011
- transformedWhere[i + 1] = whereExists
1084
+ transformedTokenStream[i + 1] = whereExists
1012
1085
  // skip newly created subquery from being iterated
1013
1086
  i += 1
1014
1087
  } else if (token.list) {
@@ -1021,14 +1094,14 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
1021
1094
  typeof precedingTwoTokens[1] === 'string' ? precedingTwoTokens[1].toLowerCase() : ''
1022
1095
 
1023
1096
  if (firstPrecedingToken === 'not') {
1024
- transformedWhere.splice(i - 2, 2, 'is', 'not', 'null')
1097
+ transformedTokenStream.splice(i - 2, 2, 'is', 'not', 'null')
1025
1098
  } else if (secondPrecedingToken === 'in') {
1026
- transformedWhere.splice(i - 1, 1, '=', { val: null })
1099
+ transformedTokenStream.splice(i - 1, 1, '=', { val: null })
1027
1100
  } else {
1028
- transformedWhere.push({ list: [] })
1101
+ transformedTokenStream.push({ list: [] })
1029
1102
  }
1030
1103
  } else {
1031
- transformedWhere.push({ list: getTransformedTokenStream(token.list) })
1104
+ transformedTokenStream.push({ list: getTransformedTokenStream(token.list) })
1032
1105
  }
1033
1106
  } else if (tokenStream.length === 1 && token.val && $baseLink) {
1034
1107
  // infix filter - OData variant w/o mentioning key --> flatten out and compare each leaf to token.val
@@ -1046,12 +1119,12 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
1046
1119
  throw new Error('Filters can only be applied to managed associations which result in a single foreign key')
1047
1120
  flatKeys.forEach(c => keyValComparisons.push([...[c, '=', token]]))
1048
1121
  keyValComparisons.forEach((kv, j) =>
1049
- transformedWhere.push(...kv) && keyValComparisons[j + 1] ? transformedWhere.push('and') : null,
1122
+ transformedTokenStream.push(...kv) && keyValComparisons[j + 1] ? transformedTokenStream.push('and') : null,
1050
1123
  )
1051
1124
  } else if (token.ref && token.param) {
1052
- transformedWhere.push({ ...token })
1125
+ transformedTokenStream.push({ ...token })
1053
1126
  } else if (pseudos.elements[token.ref?.[0]]) {
1054
- transformedWhere.push({ ...token })
1127
+ transformedTokenStream.push({ ...token })
1055
1128
  } else {
1056
1129
  // expand `struct = null | struct2`
1057
1130
  const { definition } = token.$refLinks?.[token.$refLinks.length - 1] || {}
@@ -1075,7 +1148,7 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
1075
1148
  cds.error(`The operator "${next}" is not supported for structure comparison`)
1076
1149
  const newTokens = expandComparison(token, ops, rhs)
1077
1150
  const needXpr = Boolean(tokenStream[i - 1] || tokenStream[indexRhs + 1])
1078
- transformedWhere.push(...(needXpr ? [asXpr(newTokens)] : newTokens))
1151
+ transformedTokenStream.push(...(needXpr ? [asXpr(newTokens)] : newTokens))
1079
1152
  i = indexRhs // jump to next relevant index
1080
1153
  }
1081
1154
  } else {
@@ -1084,6 +1157,11 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
1084
1157
 
1085
1158
  let result = is_regexp(token?.val) ? token : copy(token) // REVISIT: too expensive! //
1086
1159
  if (token.ref) {
1160
+ if (token.ref.length > 1 && token.ref[0] === '$self' && !token.$refLinks[0].definition.kind) {
1161
+ const dollarSelfReplacement = [calculateDollarSelfColumn(token, true)]
1162
+ transformedTokenStream.push(...getTransformedTokenStream(dollarSelfReplacement))
1163
+ continue
1164
+ }
1087
1165
  const tableAlias = getQuerySourceName(token, $baseLink)
1088
1166
  if (!$baseLink && token.isJoinRelevant) {
1089
1167
  result.ref = [tableAlias, getFullName(token.$refLinks[token.$refLinks.length - 1].definition)]
@@ -1106,11 +1184,11 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
1106
1184
  })
1107
1185
  }
1108
1186
 
1109
- transformedWhere.push(result)
1187
+ transformedTokenStream.push(result)
1110
1188
  }
1111
1189
  }
1112
1190
  }
1113
- return transformedWhere
1191
+ return transformedTokenStream
1114
1192
  }
1115
1193
 
1116
1194
  /**
@@ -1797,7 +1875,7 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
1797
1875
  }
1798
1876
 
1799
1877
  function getCombinedElementAlias(node) {
1800
- return getLastStringSegment(inferred.$combinedElements[node.ref[0].id || node.ref[0]][0].index)
1878
+ return getLastStringSegment(inferred.$combinedElements[node.ref[0].id || node.ref[0]]?.[0].index)
1801
1879
  }
1802
1880
  }
1803
1881
  }
@@ -1842,5 +1920,18 @@ function getLastStringSegment(str) {
1842
1920
  return index != -1 ? str.substring(index + 1) : str
1843
1921
  }
1844
1922
 
1923
+ /**
1924
+ * Assigns the given `element` as non-enumerable property 'element' onto `col`.
1925
+ *
1926
+ * @param {object} col
1927
+ * @param {csn.Element} element
1928
+ */
1929
+ function setElementOnColumns(col, element) {
1930
+ Object.defineProperty(col, 'element', {
1931
+ value: element,
1932
+ writable: true,
1933
+ })
1934
+ }
1935
+
1845
1936
  const idOnly = ref => ref.id || ref
1846
1937
  const is_regexp = x => x?.constructor?.name === 'RegExp' // NOTE: x instanceof RegExp doesn't work in repl
@@ -275,7 +275,7 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
275
275
  inferElementsFromWildCard(aliases)
276
276
  } else {
277
277
  let wildcardSelect = false
278
- const refs = []
278
+ const dollarSelfRefs = []
279
279
  columns.forEach(col => {
280
280
  if (col === '*') {
281
281
  wildcardSelect = true
@@ -294,24 +294,24 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
294
294
  }
295
295
  setElementOnColumns(col, queryElements[as])
296
296
  } else if (col.ref) {
297
- refs.push(col)
297
+ const firstStepIsTableAlias =
298
+ (col.ref.length > 1 && col.ref[0] in sources) ||
299
+ // nested projection on table alias
300
+ (col.ref.length === 1 && col.ref[0] in sources && col.inline)
301
+ const firstStepIsSelf =
302
+ !firstStepIsTableAlias && col.ref.length > 1 && ['$self', '$projection'].includes(col.ref[0])
303
+ // we must handle $self references after the query elements have been calculated
304
+ if (firstStepIsSelf) dollarSelfRefs.push(col)
305
+ else handleRef(col)
298
306
  } else if (col.expand) {
299
307
  inferQueryElement(col)
300
308
  } else {
301
309
  throw cds.error`Not supported: ${JSON.stringify(col)}`
302
310
  }
303
311
  })
304
- refs.forEach(col => {
305
- inferQueryElement(col)
306
- const { definition } = col.$refLinks[col.$refLinks.length - 1]
307
- if (col.cast)
308
- // final type overwritten -> element not visible anymore
309
- setElementOnColumns(col, getElementForCast(col))
310
- else if ((col.ref.length === 1) & (col.ref[0] === '$user'))
311
- // shortcut to $user.id
312
- setElementOnColumns(col, queryElements[col.as || '$user'])
313
- else setElementOnColumns(col, definition)
314
- })
312
+
313
+ if (dollarSelfRefs.length) inferDollarSelfRefs(dollarSelfRefs)
314
+
315
315
  if (wildcardSelect) inferElementsFromWildCard(aliases)
316
316
  }
317
317
  if (orderBy) {
@@ -359,6 +359,54 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
359
359
 
360
360
  return queryElements
361
361
 
362
+ /**
363
+ * Processes references starting with `$self`, which are intended to target other query elements.
364
+ * These `$self` paths must be handled after processing the "regular" columns since they are dependent on other query elements.
365
+ *
366
+ * This function checks for `$self` references that may target other `$self` columns, and delays their processing.
367
+ * `$self` references not targeting other `$self` references are handled by the generic `handleRef` function immediately.
368
+ *
369
+ * @param {array} dollarSelfColumns - An array of column objects containing `$self` references.
370
+ */
371
+ function inferDollarSelfRefs(dollarSelfColumns) {
372
+ do {
373
+ const unprocessedColumns = []
374
+
375
+ for (const currentDollarSelfColumn of dollarSelfColumns) {
376
+ const { ref } = currentDollarSelfColumn
377
+ const stepToFind = ref[1]
378
+
379
+ const referencesOtherDollarSelfColumn = dollarSelfColumns.find(
380
+ otherDollarSelfCol =>
381
+ otherDollarSelfCol !== currentDollarSelfColumn &&
382
+ (otherDollarSelfCol.as
383
+ ? stepToFind === otherDollarSelfCol.as
384
+ : stepToFind === otherDollarSelfCol.ref?.[otherDollarSelfCol.ref.length - 1]),
385
+ )
386
+
387
+ if (referencesOtherDollarSelfColumn) {
388
+ unprocessedColumns.push(currentDollarSelfColumn)
389
+ } else {
390
+ handleRef(currentDollarSelfColumn)
391
+ }
392
+ }
393
+
394
+ dollarSelfColumns = unprocessedColumns
395
+ } while (dollarSelfColumns.length > 0)
396
+ }
397
+
398
+ function handleRef(col) {
399
+ inferQueryElement(col)
400
+ const { definition } = col.$refLinks[col.$refLinks.length - 1]
401
+ if (col.cast)
402
+ // final type overwritten -> element not visible anymore
403
+ setElementOnColumns(col, getElementForCast(col))
404
+ else if ((col.ref.length === 1) & (col.ref[0] === '$user'))
405
+ // shortcut to $user.id
406
+ setElementOnColumns(col, queryElements[col.as || '$user'])
407
+ else setElementOnColumns(col, definition)
408
+ }
409
+
362
410
  /**
363
411
  * This function is responsible for inferring a query element based on a provided column.
364
412
  * It initializes and attaches a non-enumerable `$refLinks` property to the column,
@@ -477,7 +525,17 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
477
525
  } else {
478
526
  const { definition } = column.$refLinks[i - 1]
479
527
  const elements = definition.elements || definition._target?.elements
480
- if (elements && id in elements) {
528
+ const element = elements?.[id]
529
+
530
+ if (firstStepIsSelf && element?.isAssociation) {
531
+ throw cds.error(
532
+ `Paths starting with “$self” must not contain steps of type “cds.Association”: ref: [ ${column.ref.map(
533
+ idOnly,
534
+ )} ]`,
535
+ )
536
+ }
537
+
538
+ if (element) {
481
539
  const $refLink = { definition: elements[id], target: column.$refLinks[i - 1].target }
482
540
  column.$refLinks.push($refLink)
483
541
  } else if (firstStepIsSelf) {
@@ -504,7 +562,9 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
504
562
  : null
505
563
  if (foreignKeyAlias) nameSegments.push(foreignKeyAlias)
506
564
  else if (skipAliasedFkSegmentsOfNameStack[0] === id) skipAliasedFkSegmentsOfNameStack.shift()
507
- else nameSegments.push(id)
565
+ else {
566
+ nameSegments.push(firstStepIsSelf && i === 1 ? element.__proto__.name : id)
567
+ }
508
568
  }
509
569
 
510
570
  if (step.where) {
@@ -586,7 +646,7 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
586
646
  }
587
647
  const virtual = (column.$refLinks[column.$refLinks.length - 1].definition.virtual || !isPersisted) && !inExpr
588
648
  // check if we need to merge the column `ref` into the join tree of the query
589
- if (!inExists && !virtual && isColumnJoinRelevant(column)) {
649
+ if (!inExists && !virtual && isColumnJoinRelevant(column, firstStepIsSelf)) {
590
650
  Object.defineProperty(column, 'isJoinRelevant', { value: true })
591
651
  joinTree.mergeColumn(column)
592
652
  }
@@ -760,7 +820,7 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
760
820
 
761
821
  if (!assoc) return false
762
822
  if (fkAccess) return false
763
- else return true
823
+ return true
764
824
  }
765
825
 
766
826
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cap-js/db-service",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "CDS base database service",
5
5
  "homepage": "https://cap.cloud.sap/",
6
6
  "keywords": [