@cap-js/db-service
Advanced tools
+13
-0
@@ -7,2 +7,15 @@ # Changelog | ||
| ## [2.7.0](https://github.com/cap-js/cds-dbs/compare/db-service-v2.6.0...db-service-v2.7.0) (2025-11-26) | ||
| ### Added | ||
| * `error` standard function ([#1421](https://github.com/cap-js/cds-dbs/issues/1421)) ([b1b0fca](https://github.com/cap-js/cds-dbs/commit/b1b0fca00387c45ed91280b2df4282be90ea0a6e)) | ||
| ### Fixed | ||
| * LimitedRank with compositions ([#1391](https://github.com/cap-js/cds-dbs/issues/1391)) ([31766cd](https://github.com/cap-js/cds-dbs/commit/31766cd8f9b626d090129b174ac9a04b4d578c21)) | ||
| * reject nested projection if duplicated ([#1411](https://github.com/cap-js/cds-dbs/issues/1411)) ([6e924c9](https://github.com/cap-js/cds-dbs/commit/6e924c9942de6e6a4abf7b2c168d4378efcaefa9)) | ||
| ## [2.6.0](https://github.com/cap-js/cds-dbs/compare/db-service-v2.5.1...db-service-v2.6.0) (2025-10-23) | ||
@@ -9,0 +22,0 @@ |
+42
-0
@@ -8,2 +8,44 @@ 'use strict' | ||
| /** | ||
| * Generates SQL statement that produces a runtime compatible error object | ||
| * @param {string|object} message - The i18n key or message of the error object | ||
| * @param {Array<xpr>} args - The arguments to apply to the i18n string | ||
| * @param {Array<xpr>} targets - The name of the element that the error is related to | ||
| * @return {string} - SQL statement | ||
| */ | ||
| error: function (message, args, targets) { | ||
| targets = targets && (targets.list || (targets.val || targets.ref) && [targets]) | ||
| if (Array.isArray(targets)) targets = targets.map(e => e.ref && { val: e.ref.at(-1) } || e) | ||
| args = args && (args.list || (args.val || args.ref) && [args]) | ||
| return `(${this.SELECT({ | ||
| SELECT: { | ||
| expand: 'root', | ||
| columns: [ | ||
| { | ||
| __proto__: (message || { val: null }), | ||
| as: 'message', | ||
| }, | ||
| args ? { | ||
| func: 'json_array', | ||
| args: args, | ||
| as: 'args', | ||
| element: cds.builtin.types.Map, | ||
| } : { val: null, as: 'args' }, | ||
| targets ? { | ||
| func: 'json_array', | ||
| args: targets, | ||
| as: 'targets', | ||
| element: cds.builtin.types.Map, | ||
| } : { val: null, as: 'targets' }, | ||
| ] | ||
| }, | ||
| elements: { | ||
| message: cds.builtin.types.String, | ||
| args: cds.builtin.types.Map, | ||
| targets: cds.builtin.types.Map, | ||
| } | ||
| })})` | ||
| }, | ||
| /** | ||
| * Generates SQL statement that produces a boolean value indicating whether the search term is contained in the given columns | ||
@@ -10,0 +52,0 @@ * @param {string} ref - The reference object containing column information |
+2
-2
@@ -328,3 +328,3 @@ const cds = require('@sap/cds') | ||
| LimitedDescendantCount: { xpr: [{ ref: ['HIERARCHY_TREE_SIZE'] }, '-', { val: 1, param: false }], as: 'LimitedDescendantCount' }, | ||
| LimitedRank: { xpr: [{ func: 'row_number', args: [] }, 'OVER', { xpr: [] }, '-', { val: 1, param: false }], as: 'LimitedRank' } | ||
| LimitedRank: { xpr: [{ func: 'row_number', args: [] }, 'OVER', { xpr: ['ORDER', 'BY', { ref: ['HIERARCHY_RANK'] }, 'ASC'] }, '-', { val: 1, param: false }], as: 'LimitedRank' } | ||
| } | ||
@@ -1330,3 +1330,3 @@ | ||
| } | ||
| const fn = this.class.Functions[func]?.apply(this, args) || `${func}(${args})` | ||
| const fn = this.class.Functions[func]?.apply(this, Array.isArray(args) ? args: [args]) || `${func}(${args})` | ||
| if (xpr) return `${fn} ${this.xpr({ xpr })}` | ||
@@ -1333,0 +1333,0 @@ return fn |
+55
-14
@@ -50,3 +50,2 @@ 'use strict' | ||
| const sources = inferTarget(_.into || _.from || _.entity, {}) // IMPORTANT: _.into has to go before _.from for INSERT.into().from(SELECT) | ||
| const joinTree = new JoinTree(sources) | ||
| const aliases = Object.keys(sources) | ||
@@ -76,3 +75,2 @@ const target = aliases.length === 1 ? getDefinitionFromSources(sources, aliases[0]) : originalQuery | ||
| elements: { value: elements, writable: true, configurable: true }, | ||
| joinTree: { value: joinTree, writable: true, configurable: true }, // REVISIT: eliminate | ||
| }) | ||
@@ -101,2 +99,6 @@ // also enrich original query -> writable because it may be inferred again | ||
| const { ref } = from | ||
| // Given a from clause `Root:parent[$main.name = name].parent as Foo` | ||
| // we need to first resolve until to the last step of the from.ref | ||
| // before we can replace $main with `Foo` | ||
| const $mainLazyResolve = [] // TODO: remove and replace with real alias breakout | ||
| if (ref) { | ||
@@ -117,3 +119,3 @@ const { id, args } = ref[0] | ||
| inferArg(from, null, null, { inFrom: true }) | ||
| inferArg(from, null, null, { inFrom: true, $mainLazyResolve }) | ||
| const alias = | ||
@@ -144,2 +146,6 @@ from.uniqueSubqueryAlias || | ||
| } | ||
| const joinTree = new JoinTree(querySources) | ||
| Object.defineProperty( inferred, 'joinTree', { value: joinTree, writable: true, configurable: true } ) | ||
| for(const lazyRef of $mainLazyResolve) inferArg(lazyRef) | ||
| return querySources | ||
@@ -209,3 +215,3 @@ } | ||
| if (as === undefined) cds.error`Expecting expression to have an alias name` | ||
| if (queryElements[as]) cds.error`Duplicate definition of element β${as}β` | ||
| if (queryElements[as]) rejectDuplicatedElement(as) | ||
| if (col.xpr || col.SELECT) { | ||
@@ -431,3 +437,4 @@ queryElements[as] = getElementForXprOrSubquery(col, queryElements, dollarSelfRefs) | ||
| let firstStepIsTableAlias, firstStepIsSelf, expandOnTableAlias | ||
| if (!inFrom) { | ||
| const firstStepIsDollarMain = arg.ref.length > 1 && arg.ref[0] === '$main' | ||
| if (!inFrom && !firstStepIsDollarMain) { | ||
| firstStepIsTableAlias = arg.ref.length > 1 && arg.ref[0] in sources | ||
@@ -437,2 +444,3 @@ firstStepIsSelf = !firstStepIsTableAlias && arg.ref.length > 1 && ['$self', '$projection'].includes(arg.ref[0]) | ||
| } | ||
| if (dollarSelfRefs && firstStepIsSelf) { | ||
@@ -449,6 +457,16 @@ defineProperty(arg, 'inXpr', true) | ||
| let pseudoPath = false | ||
| arg.ref.forEach((step, i) => { | ||
| for(let i = 0; i < arg.ref.length; i++) { | ||
| const step = arg.ref[i] | ||
| const id = step.id || step | ||
| if (i === 0) { | ||
| if (id in pseudos.elements) { | ||
| if(firstStepIsDollarMain) { | ||
| if(inFrom) { // we need to resolve the full from clause first | ||
| context.$mainLazyResolve.push(arg) | ||
| return; // this will be done once the from clause is fully resolved | ||
| } else { | ||
| // replace $main with the alias of the outermost query | ||
| const mainAlias = getMainAlias(inferred) | ||
| arg.$refLinks.push(Object.assign(mainAlias, {$main: true})) | ||
| } | ||
| } else if (id in pseudos.elements) { | ||
| // pseudo path | ||
@@ -581,2 +599,3 @@ arg.$refLinks.push({ definition: pseudos.elements[id], target: pseudos }) | ||
| inferArg(token, false, arg.$refLinks[i], { | ||
| ...context, | ||
| inExists: skipJoinsForFilter || inExists, | ||
@@ -599,3 +618,4 @@ inXpr: !!token.xpr, | ||
| arg.$refLinks[i].alias = !arg.ref[i + 1] && arg.as ? arg.as : id.split('.').pop() | ||
| if(!arg.$refLinks[i].$main) | ||
| arg.$refLinks[i].alias = !arg.ref[i + 1] && arg.as ? arg.as : id.split('.').pop() | ||
| if (hasOwnSkip(getDefinition(arg.$refLinks[i].definition.target))) isPersisted = false | ||
@@ -616,5 +636,13 @@ if (!arg.ref[i + 1]) { | ||
| else elementName = arg.as || flatName | ||
| if (queryElements) queryElements[elementName] = elements | ||
| if (queryElements) { | ||
| if (queryElements[elementName] !== undefined) | ||
| rejectDuplicatedElement(elementName) | ||
| queryElements[elementName] = elements | ||
| } | ||
| } else if (arg.inline && queryElements) { | ||
| const elements = resolveInline(arg) | ||
| for (const elName in elements) { | ||
| if (queryElements[elName] !== undefined) rejectDuplicatedElement(elName) | ||
| } | ||
| Object.assign(queryElements, elements) | ||
@@ -641,3 +669,3 @@ } else { | ||
| if (queryElements[elementName] !== undefined) | ||
| throw new Error(`Duplicate definition of element β${elementName}β`) | ||
| rejectDuplicatedElement(elementName) | ||
| const element = getCopyWithAnnos(arg, leafArt) | ||
@@ -648,4 +676,3 @@ queryElements[elementName] = element | ||
| } | ||
| }) | ||
| } | ||
| // we need inner joins for the path expressions inside filter expressions after exists predicate | ||
@@ -673,3 +700,5 @@ if ($baseLink?.pathExpressionInsideFilter) defineProperty(arg, 'join', 'inner') | ||
| defineProperty(arg, 'isJoinRelevant', true) | ||
| joinTree.mergeColumn(colWithBase, originalQuery.outerQueries) | ||
| // join resolved in outer query | ||
| if(!(arg.$refLinks[0].$main && originalQuery.outerQueries)) | ||
| inferred.joinTree.mergeColumn(colWithBase, originalQuery.outerQueries) | ||
| } | ||
@@ -825,2 +854,6 @@ } | ||
| } | ||
| function rejectDuplicatedElement(elementName) { | ||
| throw new Error(`Duplicate definition of element β${elementName}β`) | ||
| } | ||
| function linkCalculatedElement(column, baseLink, baseColumn, context = {}) { | ||
@@ -920,3 +953,3 @@ const calcElement = column.$refLinks?.[column.$refLinks.length - 1].definition || column | ||
| defineProperty(step, 'isJoinRelevant',true) | ||
| joinTree.mergeColumn(p, originalQuery.outerQueries) | ||
| inferred.joinTree.mergeColumn(p, originalQuery.outerQueries) | ||
| } else { | ||
@@ -1180,2 +1213,10 @@ // we need to explicitly set the value to false in this case, | ||
| function getMainAlias (query) { | ||
| let mainAlias | ||
| if (query.outerQueries) mainAlias = query.outerQueries[0].SELECT?.from.$refLinks.at(-1) | ||
| else mainAlias = query.SELECT?.from.$refLinks.at(-1) | ||
| if(!mainAlias) throw new Error('Cannot determine main query source for $main, please report this') | ||
| return mainAlias | ||
| } | ||
| module.exports = infer |
+2
-2
| { | ||
| "name": "@cap-js/db-service", | ||
| "version": "2.6.0", | ||
| "version": "2.7.0", | ||
| "description": "CDS base database service", | ||
@@ -30,5 +30,5 @@ "homepage": "https://github.com/cap-js/cds-dbs/tree/main/db-service#cds-base-database-service", | ||
| "peerDependencies": { | ||
| "@sap/cds": ">=9" | ||
| "@sap/cds": ">=9.4.5" | ||
| }, | ||
| "license": "Apache-2.0" | ||
| } |
Sorry, the diff of this file is too big to display
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 2 instances in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 2 instances in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
365831
1.17%7244
1.17%71
1.43%