@cap-js/db-service 1.14.1 → 1.15.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 +21 -0
- package/lib/cqn2sql.js +1 -2
- package/lib/cqn4sql.js +28 -8
- package/lib/infer/index.js +68 -42
- package/lib/infer/join-tree.js +1 -0
- package/lib/search.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,27 @@
|
|
|
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.15.1](https://github.com/cap-js/cds-dbs/compare/db-service-v1.15.0...db-service-v1.15.1) (2024-11-18)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
* cross joins without on-condition ([#899](https://github.com/cap-js/cds-dbs/issues/899)) ([c61a04a](https://github.com/cap-js/cds-dbs/commit/c61a04aa4394511100f97cfebd362a2298221d96))
|
|
13
|
+
* pseudo paths in expands ([#896](https://github.com/cap-js/cds-dbs/issues/896)) ([014c50c](https://github.com/cap-js/cds-dbs/commit/014c50cec9c2de1ee3dfdf1861940ae0e2520c16))
|
|
14
|
+
|
|
15
|
+
## [1.15.0](https://github.com/cap-js/cds-dbs/compare/db-service-v1.14.1...db-service-v1.15.0) (2024-11-14)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
* enable path expressions in infix filter after `exists` predicate ([#875](https://github.com/cap-js/cds-dbs/issues/875)) ([7e50359](https://github.com/cap-js/cds-dbs/commit/7e5035932ac3bf39f052aa67e1565567e9d6b1ad))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
* **`search`:** ignore invalid path expressions inside `@cds.search` ([#849](https://github.com/cap-js/cds-dbs/issues/849)) ([250edd5](https://github.com/cap-js/cds-dbs/commit/250edd5ec9f7ba1d8e40e1330e4b4f9ad9e599b0))
|
|
26
|
+
* nested exists wrapped in xpr ([7e50359](https://github.com/cap-js/cds-dbs/commit/7e5035932ac3bf39f052aa67e1565567e9d6b1ad))
|
|
27
|
+
|
|
7
28
|
## [1.14.1](https://github.com/cap-js/cds-dbs/compare/db-service-v1.14.0...db-service-v1.14.1) (2024-10-28)
|
|
8
29
|
|
|
9
30
|
|
package/lib/cqn2sql.js
CHANGED
|
@@ -363,8 +363,7 @@ class CQN2SQLRenderer {
|
|
|
363
363
|
return _aliased(this.quote(this.name(z)))
|
|
364
364
|
}
|
|
365
365
|
if (from.SELECT) return _aliased(`(${this.SELECT(from)})`)
|
|
366
|
-
if (from.join)
|
|
367
|
-
return `${this.from(from.args[0])} ${from.join} JOIN ${this.from(from.args[1])} ON ${this.where(from.on)}`
|
|
366
|
+
if (from.join) return `${this.from(from.args[0])} ${from.join} JOIN ${this.from(from.args[1])}${from.on ? ` ON ${this.where(from.on)}` : ''}`
|
|
368
367
|
}
|
|
369
368
|
|
|
370
369
|
/**
|
package/lib/cqn4sql.js
CHANGED
|
@@ -266,10 +266,10 @@ function cqn4sql(originalQuery, model) {
|
|
|
266
266
|
const id = localized(r.queryArtifact)
|
|
267
267
|
args.push({ ref: [r.args ? { id, args: r.args } : id], as: r.alias })
|
|
268
268
|
}
|
|
269
|
-
from = { join: 'left', args, on: [] }
|
|
269
|
+
from = { join: r.join || 'left', args, on: [] }
|
|
270
270
|
r.children.forEach(c => {
|
|
271
271
|
from = joinForBranch(from, c)
|
|
272
|
-
from = { join: 'left', args: [from], on: [] }
|
|
272
|
+
from = { join: c.join || 'left', args: [from], on: [] }
|
|
273
273
|
})
|
|
274
274
|
})
|
|
275
275
|
return from.args.length > 1 ? from : from.args[0]
|
|
@@ -309,7 +309,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
309
309
|
}
|
|
310
310
|
if (node.children) {
|
|
311
311
|
node.children.forEach(c => {
|
|
312
|
-
lhs = { join: 'left', args: [lhs], on: [] }
|
|
312
|
+
lhs = { join: c.join || 'left', args: [lhs], on: [] }
|
|
313
313
|
lhs = joinForBranch(lhs, c)
|
|
314
314
|
})
|
|
315
315
|
}
|
|
@@ -1868,13 +1868,18 @@ function cqn4sql(originalQuery, model) {
|
|
|
1868
1868
|
value: [],
|
|
1869
1869
|
writable: true,
|
|
1870
1870
|
})
|
|
1871
|
+
let pseudoPath = false
|
|
1871
1872
|
ref.reduce((prev, res, i) => {
|
|
1872
1873
|
if (res === '$self')
|
|
1873
1874
|
// next is resolvable in entity
|
|
1874
1875
|
return prev
|
|
1875
1876
|
if (res in pseudos.elements) {
|
|
1877
|
+
pseudoPath = true
|
|
1876
1878
|
thing.$refLinks.push({ definition: pseudos.elements[res], target: pseudos })
|
|
1877
1879
|
return pseudos.elements[res]
|
|
1880
|
+
} else if (pseudoPath) {
|
|
1881
|
+
thing.$refLinks.push({ definition: {}, target: pseudos })
|
|
1882
|
+
return prev?.elements[res]
|
|
1878
1883
|
}
|
|
1879
1884
|
const definition =
|
|
1880
1885
|
prev?.elements?.[res] || getDefinition(prev?.target)?.elements[res] || pseudos.elements[res]
|
|
@@ -2093,11 +2098,6 @@ function cqn4sql(originalQuery, model) {
|
|
|
2093
2098
|
const unmanagedOn = onCondFor(inWhere ? next : current, inWhere ? current : next, inWhere)
|
|
2094
2099
|
on.push(...(customWhere && hasLogicalOr(unmanagedOn) ? [asXpr(unmanagedOn)] : unmanagedOn))
|
|
2095
2100
|
}
|
|
2096
|
-
// infix filter conditions are wrapped in `xpr` when added to the on-condition
|
|
2097
|
-
if (customWhere) {
|
|
2098
|
-
const filter = getTransformedTokenStream(customWhere, next)
|
|
2099
|
-
on.push(...['and', ...(hasLogicalOr(filter) ? [asXpr(filter)] : filter)])
|
|
2100
|
-
}
|
|
2101
2101
|
|
|
2102
2102
|
const subquerySource = assocTarget(nextDefinition) || nextDefinition
|
|
2103
2103
|
const id = localized(subquerySource)
|
|
@@ -2115,6 +2115,26 @@ function cqn4sql(originalQuery, model) {
|
|
|
2115
2115
|
],
|
|
2116
2116
|
where: on,
|
|
2117
2117
|
}
|
|
2118
|
+
if (next.pathExpressionInsideFilter) {
|
|
2119
|
+
SELECT.where = customWhere
|
|
2120
|
+
const transformedExists = transformSubquery({ SELECT })
|
|
2121
|
+
// infix filter conditions are wrapped in `xpr` when added to the on-condition
|
|
2122
|
+
if (transformedExists.SELECT.where) {
|
|
2123
|
+
on.push(
|
|
2124
|
+
...[
|
|
2125
|
+
'and',
|
|
2126
|
+
...(hasLogicalOr(transformedExists.SELECT.where)
|
|
2127
|
+
? [asXpr(transformedExists.SELECT.where)]
|
|
2128
|
+
: transformedExists.SELECT.where),
|
|
2129
|
+
],
|
|
2130
|
+
)
|
|
2131
|
+
}
|
|
2132
|
+
transformedExists.SELECT.where = on
|
|
2133
|
+
return transformedExists.SELECT
|
|
2134
|
+
} else if (customWhere) {
|
|
2135
|
+
const filter = getTransformedTokenStream(customWhere, next)
|
|
2136
|
+
on.push(...['and', ...(hasLogicalOr(filter) ? [asXpr(filter)] : filter)])
|
|
2137
|
+
}
|
|
2118
2138
|
return SELECT
|
|
2119
2139
|
}
|
|
2120
2140
|
|
package/lib/infer/index.js
CHANGED
|
@@ -172,28 +172,30 @@ function infer(originalQuery, model) {
|
|
|
172
172
|
if (!ref) return
|
|
173
173
|
init$refLinks(arg)
|
|
174
174
|
let i = 0
|
|
175
|
+
let pseudoPath = false
|
|
175
176
|
for (const step of ref) {
|
|
176
177
|
const id = step.id || step
|
|
177
178
|
if (i === 0) {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
179
|
+
if (id in pseudos.elements) {
|
|
180
|
+
// pseudo path
|
|
181
|
+
arg.$refLinks.push({ definition: pseudos.elements[id], target: pseudos })
|
|
182
|
+
pseudoPath = true // only first path step must be well defined
|
|
183
|
+
} else if ($baseLink) {
|
|
184
|
+
// infix filter never have table alias
|
|
185
|
+
// we need to search for first step in ´model.definitions[infixAlias]`
|
|
181
186
|
const { definition } = $baseLink
|
|
182
187
|
const elements = getDefinition(definition.target)?.elements || definition.elements
|
|
183
188
|
const e = elements?.[id] || cds.error`"${id}" not found in the elements of "${definition.name}"`
|
|
184
189
|
if (e.target) {
|
|
185
190
|
// only fk access in infix filter
|
|
186
191
|
const nextStep = ref[1]?.id || ref[1]
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
192
|
+
if (isNonForeignKeyNavigation(e, nextStep)) {
|
|
193
|
+
if (expandOrExists) {
|
|
194
|
+
Object.defineProperty($baseLink, 'pathExpressionInsideFilter', { value: true })
|
|
195
|
+
} else {
|
|
196
|
+
rejectNonFkNavigation(e, e.on ? $baseLink.definition.name : nextStep)
|
|
197
|
+
}
|
|
191
198
|
}
|
|
192
|
-
// no non-fk traversal in infix filter
|
|
193
|
-
if (!expandOrExists && nextStep && !isForeignKeyOf(nextStep, e))
|
|
194
|
-
throw new Error(
|
|
195
|
-
`Only foreign keys of “${e.name}” can be accessed in infix filter, but found “${nextStep}”`,
|
|
196
|
-
)
|
|
197
199
|
}
|
|
198
200
|
arg.$refLinks.push({ definition: e, target: definition })
|
|
199
201
|
// filter paths are flattened
|
|
@@ -204,11 +206,15 @@ function infer(originalQuery, model) {
|
|
|
204
206
|
const definition = getDefinition(id) || cds.error`"${id}" not found in the definitions of your model`
|
|
205
207
|
arg.$refLinks[0] = { definition, target: definition }
|
|
206
208
|
}
|
|
209
|
+
} else if (arg.ref[0] === '$user' && pseudoPath) {
|
|
210
|
+
// `$user.some.unknown.element` -> no error
|
|
211
|
+
arg.$refLinks.push({ definition: {}, target: pseudos })
|
|
207
212
|
} else {
|
|
208
213
|
const recent = arg.$refLinks[i - 1]
|
|
209
214
|
const { elements } = getDefinition(recent.definition.target) || recent.definition
|
|
210
215
|
const e = elements[id]
|
|
211
|
-
|
|
216
|
+
const notFoundIn = pseudoPath ? arg.ref[i - 1] : getFullPathForLinkedArg(arg)
|
|
217
|
+
if (!e) throw new Error(`"${id}" not found in the elements of "${notFoundIn}"`)
|
|
212
218
|
arg.$refLinks.push({ definition: e, target: getDefinition(e.target) || e })
|
|
213
219
|
}
|
|
214
220
|
arg.$refLinks[i].alias = !ref[i + 1] && arg.as ? arg.as : id.split('.').pop()
|
|
@@ -226,7 +232,7 @@ function infer(originalQuery, model) {
|
|
|
226
232
|
// don't miss an exists within an expression
|
|
227
233
|
token.xpr.forEach(walkTokenStream)
|
|
228
234
|
} else {
|
|
229
|
-
attachRefLinksToArg(token, arg.$refLinks[i], existsPredicate)
|
|
235
|
+
attachRefLinksToArg(token, arg.$refLinks[i], existsPredicate || expandOrExists)
|
|
230
236
|
existsPredicate = false
|
|
231
237
|
}
|
|
232
238
|
}
|
|
@@ -235,6 +241,7 @@ function infer(originalQuery, model) {
|
|
|
235
241
|
}
|
|
236
242
|
i += 1
|
|
237
243
|
}
|
|
244
|
+
if ($baseLink?.pathExpressionInsideFilter) Object.defineProperty(arg, 'join', { value: 'inner' })
|
|
238
245
|
const { definition, target } = arg.$refLinks[arg.$refLinks.length - 1]
|
|
239
246
|
if (definition.value) {
|
|
240
247
|
// nested calculated element
|
|
@@ -542,9 +549,19 @@ function infer(originalQuery, model) {
|
|
|
542
549
|
const elements = getDefinition(definition.target)?.elements || definition.elements
|
|
543
550
|
if (elements && id in elements) {
|
|
544
551
|
const element = elements[id]
|
|
545
|
-
|
|
552
|
+
if (inInfixFilter) {
|
|
553
|
+
const nextStep = column.ref[1]?.id || column.ref[1]
|
|
554
|
+
if (isNonForeignKeyNavigation(element, nextStep)) {
|
|
555
|
+
if (inExists) {
|
|
556
|
+
Object.defineProperty($baseLink, 'pathExpressionInsideFilter', { value: true })
|
|
557
|
+
} else {
|
|
558
|
+
rejectNonFkNavigation(element, element.on ? $baseLink.definition.name : nextStep)
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
546
562
|
const resolvableIn = getDefinition(definition.target) || target
|
|
547
|
-
|
|
563
|
+
const $refLink = { definition: elements[id], target: resolvableIn }
|
|
564
|
+
column.$refLinks.push($refLink)
|
|
548
565
|
} else {
|
|
549
566
|
stepNotFoundInPredecessor(id, definition.name)
|
|
550
567
|
}
|
|
@@ -593,7 +610,16 @@ function infer(originalQuery, model) {
|
|
|
593
610
|
|
|
594
611
|
const target = getDefinition(definition.target) || column.$refLinks[i - 1].target
|
|
595
612
|
if (element) {
|
|
596
|
-
if ($baseLink)
|
|
613
|
+
if ($baseLink && inInfixFilter) {
|
|
614
|
+
const nextStep = column.ref[i + 1]?.id || column.ref[i + 1]
|
|
615
|
+
if (isNonForeignKeyNavigation(element, nextStep)) {
|
|
616
|
+
if (inExists) {
|
|
617
|
+
Object.defineProperty($baseLink, 'pathExpressionInsideFilter', { value: true })
|
|
618
|
+
} else {
|
|
619
|
+
rejectNonFkNavigation(element, element.on ? $baseLink.definition.name : nextStep)
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
}
|
|
597
623
|
const $refLink = { definition: elements[id], target }
|
|
598
624
|
column.$refLinks.push($refLink)
|
|
599
625
|
} else if (firstStepIsSelf) {
|
|
@@ -637,7 +663,7 @@ function infer(originalQuery, model) {
|
|
|
637
663
|
skipJoinsForFilter = true
|
|
638
664
|
} else if (token.ref || token.xpr) {
|
|
639
665
|
inferQueryElement(token, false, column.$refLinks[i], {
|
|
640
|
-
inExists: skipJoinsForFilter,
|
|
666
|
+
inExists: skipJoinsForFilter || inExists,
|
|
641
667
|
inExpr: !!token.xpr,
|
|
642
668
|
inInfixFilter: true,
|
|
643
669
|
})
|
|
@@ -646,7 +672,7 @@ function infer(originalQuery, model) {
|
|
|
646
672
|
applyToFunctionArgs(token.args, inferQueryElement, [
|
|
647
673
|
false,
|
|
648
674
|
column.$refLinks[i],
|
|
649
|
-
{ inExists: skipJoinsForFilter, inExpr: true, inInfixFilter: true },
|
|
675
|
+
{ inExists: skipJoinsForFilter || inExists, inExpr: true, inInfixFilter: true },
|
|
650
676
|
])
|
|
651
677
|
}
|
|
652
678
|
}
|
|
@@ -700,31 +726,11 @@ function infer(originalQuery, model) {
|
|
|
700
726
|
}
|
|
701
727
|
}
|
|
702
728
|
}
|
|
703
|
-
|
|
704
|
-
/**
|
|
705
|
-
* Check if the next step in the ref is foreign key of `assoc`
|
|
706
|
-
* if not, an error is thrown.
|
|
707
|
-
*
|
|
708
|
-
* @param {CSN.Element} assoc if this is an association, the next step must be a foreign key of the element.
|
|
709
|
-
*/
|
|
710
|
-
function rejectNonFkAccess(assoc) {
|
|
711
|
-
if (inInfixFilter && assoc.target) {
|
|
712
|
-
// only fk access in infix filter
|
|
713
|
-
const nextStep = column.ref[i + 1]?.id || column.ref[i + 1]
|
|
714
|
-
// no unmanaged assoc in infix filter path
|
|
715
|
-
if (!inExists && assoc.on) {
|
|
716
|
-
const err = `Unexpected unmanaged association “${assoc.name}” in filter expression of “${$baseLink.definition.name}”`
|
|
717
|
-
throw new Error(err)
|
|
718
|
-
}
|
|
719
|
-
// no non-fk traversal in infix filter in non-exists path
|
|
720
|
-
if (nextStep && !assoc.on && !isForeignKeyOf(nextStep, assoc))
|
|
721
|
-
throw new Error(
|
|
722
|
-
`Only foreign keys of “${assoc.name}” can be accessed in infix filter, but found “${nextStep}”`,
|
|
723
|
-
)
|
|
724
|
-
}
|
|
725
|
-
}
|
|
726
729
|
})
|
|
727
730
|
|
|
731
|
+
// we need inner joins for the path expressions inside filter expressions after exists predicate
|
|
732
|
+
if ($baseLink?.pathExpressionInsideFilter) Object.defineProperty(column, 'join', { value: 'inner' })
|
|
733
|
+
|
|
728
734
|
// ignore whole expand if target of assoc along path has ”@cds.persistence.skip”
|
|
729
735
|
if (column.expand) {
|
|
730
736
|
const { $refLinks } = column
|
|
@@ -1214,6 +1220,26 @@ function infer(originalQuery, model) {
|
|
|
1214
1220
|
}
|
|
1215
1221
|
}
|
|
1216
1222
|
|
|
1223
|
+
/**
|
|
1224
|
+
* Determines if a given association is a non-foreign key navigation.
|
|
1225
|
+
*
|
|
1226
|
+
* @param {Object} assoc - The association.
|
|
1227
|
+
* @param {Object} nextStep - The next step in the navigation path.
|
|
1228
|
+
* @returns {boolean} - Returns true if the next step is a non-foreign key navigation, otherwise false.
|
|
1229
|
+
*/
|
|
1230
|
+
function isNonForeignKeyNavigation(assoc, nextStep) {
|
|
1231
|
+
if (!nextStep || !assoc.target) return false
|
|
1232
|
+
|
|
1233
|
+
return assoc.on || !isForeignKeyOf(nextStep, assoc)
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
function rejectNonFkNavigation(assoc, additionalInfo) {
|
|
1237
|
+
if (assoc.on) {
|
|
1238
|
+
throw new Error(`Unexpected unmanaged association “${assoc.name}” in filter expression of “${additionalInfo}”`)
|
|
1239
|
+
}
|
|
1240
|
+
throw new Error(`Only foreign keys of “${assoc.name}” can be accessed in infix filter, but found “${additionalInfo}”`)
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1217
1243
|
/**
|
|
1218
1244
|
* Returns true if e is a foreign key of assoc.
|
|
1219
1245
|
* this function is also compatible with unfolded csn (UCSN),
|
package/lib/infer/join-tree.js
CHANGED
|
@@ -181,6 +181,7 @@ class JoinTree {
|
|
|
181
181
|
// if no root node was found, the column is selected from a subquery
|
|
182
182
|
if (!node) return
|
|
183
183
|
while (i < col.ref.length) {
|
|
184
|
+
if(col.join === 'inner') node.join = 'inner'
|
|
184
185
|
const step = col.ref[i]
|
|
185
186
|
const { where, args } = step
|
|
186
187
|
const id = joinId(step, args, where)
|
package/lib/search.js
CHANGED
|
@@ -114,7 +114,7 @@ const _getSearchableColumns = entity => {
|
|
|
114
114
|
deepSearchCandidates.forEach(c => {
|
|
115
115
|
const element = c.ref.reduce((resolveIn, curr, i) => {
|
|
116
116
|
const next = resolveIn.elements?.[curr] || resolveIn._target.elements[curr]
|
|
117
|
-
if (next
|
|
117
|
+
if (next?.isAssociation && !c.ref[i + 1]) {
|
|
118
118
|
const searchInTarget = _getSearchableColumns(next._target)
|
|
119
119
|
searchInTarget.forEach(elementRefInTarget => {
|
|
120
120
|
searchableColumns.push({ ref: c.ref.concat(...elementRefInTarget.ref) })
|
package/package.json
CHANGED