@cap-js/db-service 1.3.1 → 1.4.0
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 +27 -2
- package/README.md +5 -0
- package/lib/SQLService.js +17 -2
- package/lib/common/DatabaseService.js +22 -7
- package/lib/cql-functions.js +6 -6
- package/lib/cqn2sql.js +4 -2
- package/lib/cqn4sql.js +85 -17
- package/lib/infer/index.js +65 -42
- package/lib/infer/join-tree.js +0 -1
- package/lib/search.js +164 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,10 +1,35 @@
|
|
|
1
|
-
#
|
|
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
|
-
##
|
|
7
|
+
## [1.4.0](https://github.com/cap-js/cds-dbs/compare/db-service-v1.3.2...db-service-v1.4.0) (2023-11-20)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
* **`UPDATE`/`DELETE`:** Enable path expressions for improved data manipulation ([#325](https://github.com/cap-js/cds-dbs/issues/325)) ([94f0776](https://github.com/cap-js/cds-dbs/commit/94f077661cffad8f137dc692a2cb9b0ae5e4d75b))
|
|
13
|
+
* **temporal data:** add time slice key to conflict clause ([#249](https://github.com/cap-js/cds-dbs/issues/249)) ([67b8edf](https://github.com/cap-js/cds-dbs/commit/67b8edf9b7f6b0fbab0010d7c93ed03a01e103ed))
|
|
14
|
+
* use place holders for update and delete ([#323](https://github.com/cap-js/cds-dbs/issues/323)) ([81472b9](https://github.com/cap-js/cds-dbs/commit/81472b971183f701e401247611310be56745a87a))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
* align time function behavior ([#322](https://github.com/cap-js/cds-dbs/issues/322)) ([c3ab40a](https://github.com/cap-js/cds-dbs/commit/c3ab40a007c105465349dd2f612178367b8e713a))
|
|
20
|
+
* **calculated elements:** path expressions in `func.args` within `xpr` ([#321](https://github.com/cap-js/cds-dbs/issues/321)) ([cee25e3](https://github.com/cap-js/cds-dbs/commit/cee25e33cf289592a87779cfa34dddc53e467676))
|
|
21
|
+
* Disconnect db service on shutdown ([#327](https://github.com/cap-js/cds-dbs/issues/327)) ([8471bda](https://github.com/cap-js/cds-dbs/commit/8471bda44fc030205abec45b1581b2cf6ed7c800))
|
|
22
|
+
* non-fk access in filter conditions are properly rejected ([#336](https://github.com/cap-js/cds-dbs/issues/336)) ([4c948fe](https://github.com/cap-js/cds-dbs/commit/4c948fecead1de562e1583886516413e131a39aa))
|
|
23
|
+
* **search:** check calculated columns at any depth ([#310](https://github.com/cap-js/cds-dbs/issues/310)) ([8fd6153](https://github.com/cap-js/cds-dbs/commit/8fd6153dfcd472a6d95c33faa58c4b3f96f485df))
|
|
24
|
+
|
|
25
|
+
## [1.3.2](https://github.com/cap-js/cds-dbs/compare/db-service-v1.3.1...db-service-v1.3.2) (2023-10-13)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
- 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))
|
|
31
|
+
|
|
32
|
+
## [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
33
|
|
|
9
34
|
### Fixed
|
|
10
35
|
|
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)
|
|
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 {
|
|
@@ -6,10 +6,16 @@ const cds = require('@sap/cds/lib')
|
|
|
6
6
|
/** @typedef {unknown} DatabaseDriver */
|
|
7
7
|
|
|
8
8
|
class DatabaseService extends cds.Service {
|
|
9
|
+
|
|
10
|
+
init() {
|
|
11
|
+
cds.on('shutdown', () => this.disconnect())
|
|
12
|
+
return super.init()
|
|
13
|
+
}
|
|
14
|
+
|
|
9
15
|
/**
|
|
10
16
|
* Dictionary of connection pools per tenant
|
|
11
17
|
*/
|
|
12
|
-
pools = { _factory: this.factory }
|
|
18
|
+
pools = Object.setPrototypeOf({}, { _factory: this.factory })
|
|
13
19
|
|
|
14
20
|
/**
|
|
15
21
|
* Return a pool factory + options property as expected by
|
|
@@ -44,7 +50,9 @@ class DatabaseService extends cds.Service {
|
|
|
44
50
|
async begin() {
|
|
45
51
|
// We expect tx.begin() being called for an txed db service
|
|
46
52
|
const ctx = this.context
|
|
47
|
-
|
|
53
|
+
|
|
54
|
+
// If .begin is called explicitly it starts a new transaction and executes begin
|
|
55
|
+
if (!ctx) return this.tx().begin()
|
|
48
56
|
|
|
49
57
|
// REVISIT: tenant should be undefined if !this.isMultitenant
|
|
50
58
|
let isMultitenant = 'multiTenant' in this.options ? this.options.multiTenant : cds.env.requires.multitenancy
|
|
@@ -111,11 +119,18 @@ class DatabaseService extends cds.Service {
|
|
|
111
119
|
* @param {string} tenant
|
|
112
120
|
*/
|
|
113
121
|
async disconnect(tenant) {
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
122
|
+
const _disconnect = async tenant => {
|
|
123
|
+
const pool = this.pools[tenant]
|
|
124
|
+
if (!pool) {
|
|
125
|
+
return
|
|
126
|
+
}
|
|
127
|
+
await pool.drain()
|
|
128
|
+
await pool.clear()
|
|
129
|
+
delete this.pools[tenant]
|
|
130
|
+
}
|
|
131
|
+
if (tenant == null)
|
|
132
|
+
return Promise.all(Object.keys(this.pools).map(_disconnect))
|
|
133
|
+
return _disconnect(tenant)
|
|
119
134
|
}
|
|
120
135
|
|
|
121
136
|
/**
|
package/lib/cql-functions.js
CHANGED
|
@@ -149,37 +149,37 @@ const StandardFunctions = {
|
|
|
149
149
|
* Generates SQL statement that produces the year of a given timestamp
|
|
150
150
|
* @param {string} x
|
|
151
151
|
* @returns {string}
|
|
152
|
-
|
|
152
|
+
* /
|
|
153
153
|
year: x => `cast( strftime('%Y',${x}) as Integer )`,
|
|
154
154
|
/**
|
|
155
155
|
* Generates SQL statement that produces the month of a given timestamp
|
|
156
156
|
* @param {string} x
|
|
157
157
|
* @returns {string}
|
|
158
|
-
|
|
158
|
+
* /
|
|
159
159
|
month: x => `cast( strftime('%m',${x}) as Integer )`,
|
|
160
160
|
/**
|
|
161
161
|
* Generates SQL statement that produces the day of a given timestamp
|
|
162
162
|
* @param {string} x
|
|
163
163
|
* @returns {string}
|
|
164
|
-
|
|
164
|
+
* /
|
|
165
165
|
day: x => `cast( strftime('%d',${x}) as Integer )`,
|
|
166
166
|
/**
|
|
167
167
|
* Generates SQL statement that produces the hours of a given timestamp
|
|
168
168
|
* @param {string} x
|
|
169
169
|
* @returns {string}
|
|
170
|
-
|
|
170
|
+
* /
|
|
171
171
|
hour: x => `cast( strftime('%H',${x}) as Integer )`,
|
|
172
172
|
/**
|
|
173
173
|
* Generates SQL statement that produces the minutes of a given timestamp
|
|
174
174
|
* @param {string} x
|
|
175
175
|
* @returns {string}
|
|
176
|
-
|
|
176
|
+
* /
|
|
177
177
|
minute: x => `cast( strftime('%M',${x}) as Integer )`,
|
|
178
178
|
/**
|
|
179
179
|
* Generates SQL statement that produces the seconds of a given timestamp
|
|
180
180
|
* @param {string} x
|
|
181
181
|
* @returns {string}
|
|
182
|
-
|
|
182
|
+
* /
|
|
183
183
|
second: x => `cast( strftime('%S',${x}) as Integer )`,
|
|
184
184
|
|
|
185
185
|
/**
|
package/lib/cqn2sql.js
CHANGED
|
@@ -70,7 +70,7 @@ class CQN2SQLRenderer {
|
|
|
70
70
|
/** @type {unknown[]} */
|
|
71
71
|
this.values = [] // prepare values, filled in by subroutines
|
|
72
72
|
this[cmd]((this.cqn = q)) // actual sql rendering happens here
|
|
73
|
-
if (vars?.length && !this.values
|
|
73
|
+
if (vars?.length && !this.values?.length) this.values = vars
|
|
74
74
|
const sanitize_values = process.env.NODE_ENV === 'production' && cds.env.log.sanitize_values !== false
|
|
75
75
|
DEBUG?.(
|
|
76
76
|
this.sql,
|
|
@@ -527,6 +527,9 @@ class CQN2SQLRenderer {
|
|
|
527
527
|
.filter(c => !keys.includes(c))
|
|
528
528
|
.map(c => `${this.quote(c)} = excluded.${this.quote(c)}`)
|
|
529
529
|
|
|
530
|
+
// temporal data
|
|
531
|
+
keys.push(...Object.values(q.target.elements).filter(e => e['@cds.valid.from']).map(e => e.name))
|
|
532
|
+
|
|
530
533
|
keys = keys.map(k => this.quote(k))
|
|
531
534
|
const conflict = updateColumns.length
|
|
532
535
|
? `ON CONFLICT(${keys}) DO UPDATE SET ` + updateColumns
|
|
@@ -872,7 +875,6 @@ class CQN2SQLRenderer {
|
|
|
872
875
|
|
|
873
876
|
let val = _managed[element[annotation]?.['=']]
|
|
874
877
|
if (val) sql = `coalesce(${sql}, ${this.func({ func: 'session_context', args: [{ val }] })})`
|
|
875
|
-
|
|
876
878
|
else if (!isUpdate && element.default) {
|
|
877
879
|
const d = element.default
|
|
878
880
|
if (d.val !== undefined || d.ref?.[0] === '$now') {
|
package/lib/cqn4sql.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const cds = require('@sap/cds/lib')
|
|
4
|
-
const { computeColumnsToBeSearched } = require('
|
|
4
|
+
const { computeColumnsToBeSearched } = require('./search')
|
|
5
5
|
|
|
6
6
|
const infer = require('./infer')
|
|
7
7
|
|
|
@@ -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
|
|
15
|
+
* This ensures that not struct == <value> is the same as struct != <value>.
|
|
16
16
|
*/
|
|
17
17
|
const notEqOps = [['is', 'not'], ['<>'], ['!=']]
|
|
18
18
|
/**
|
|
@@ -96,7 +96,46 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
if (queryNeedsJoins) {
|
|
99
|
-
|
|
99
|
+
if (inferred.UPDATE || inferred.DELETE) {
|
|
100
|
+
const prop = inferred.UPDATE ? 'UPDATE' : 'DELETE'
|
|
101
|
+
const subquery = {
|
|
102
|
+
SELECT: {
|
|
103
|
+
from: { ...transformedFrom },
|
|
104
|
+
columns: [], // primary keys of the query target will be added later
|
|
105
|
+
where: [...transformedProp.where],
|
|
106
|
+
},
|
|
107
|
+
}
|
|
108
|
+
// The alias of the original query is now the alias for the subquery
|
|
109
|
+
// so that potential references in the where clause to the alias match.
|
|
110
|
+
// Hence, replace the alias of the original query with the next
|
|
111
|
+
// available alias, so that each alias is unique.
|
|
112
|
+
const uniqueSubqueryAlias = getNextAvailableTableAlias(transformedFrom.as)
|
|
113
|
+
transformedFrom.as = uniqueSubqueryAlias
|
|
114
|
+
|
|
115
|
+
// calculate the primary keys of the target entity, there is always exactly
|
|
116
|
+
// one query source for UPDATE / DELETE
|
|
117
|
+
const queryTarget = Object.values(originalQuery.sources)[0]
|
|
118
|
+
const keys = Object.values(queryTarget.elements).filter(e => e.key === true)
|
|
119
|
+
const primaryKey = { list: [] }
|
|
120
|
+
keys.forEach(k => {
|
|
121
|
+
// cqn4sql will add the table alias to the column later, no need to add it here
|
|
122
|
+
subquery.SELECT.columns.push({ ref: [k.name] })
|
|
123
|
+
|
|
124
|
+
// add the alias of the main query to the list of primary key references
|
|
125
|
+
primaryKey.list.push({ ref: [transformedFrom.as, k.name] })
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
const transformedSubquery = cqn4sql(subquery)
|
|
129
|
+
|
|
130
|
+
// replace where condition of original query with the transformed subquery
|
|
131
|
+
// correlate UPDATE / DELETE query with subquery by primary key matches
|
|
132
|
+
transformedQuery[prop].where = [primaryKey, 'in', transformedSubquery]
|
|
133
|
+
|
|
134
|
+
if (prop === 'UPDATE') transformedQuery.UPDATE.entity = transformedFrom
|
|
135
|
+
else transformedQuery.DELETE.from = transformedFrom
|
|
136
|
+
} else {
|
|
137
|
+
transformedQuery[kind].from = translateAssocsToJoins(transformedQuery[kind].from)
|
|
138
|
+
}
|
|
100
139
|
}
|
|
101
140
|
}
|
|
102
141
|
}
|
|
@@ -668,12 +707,24 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
668
707
|
// everything after the wildcard, is a potential replacement
|
|
669
708
|
// in the wildcard expansion
|
|
670
709
|
const replace = []
|
|
710
|
+
|
|
711
|
+
const baseRef = col.ref || []
|
|
712
|
+
const baseRefLinks = col.$refLinks || []
|
|
713
|
+
|
|
714
|
+
// column has no ref, then it is an anonymous expand:
|
|
715
|
+
// select from books { { * } as bar }
|
|
716
|
+
// only possible if there is exactly one query source
|
|
717
|
+
if (!baseRef.length) {
|
|
718
|
+
const [tableAlias, definition] = Object.entries(inferred.sources)[0]
|
|
719
|
+
baseRef.push(tableAlias)
|
|
720
|
+
baseRefLinks.push({ definition, source: definition })
|
|
721
|
+
}
|
|
671
722
|
// we need to make the refs absolute
|
|
672
723
|
col[prop].slice(wildcardIndex + 1).forEach(c => {
|
|
673
724
|
const fakeColumn = { ...c }
|
|
674
725
|
if (fakeColumn.ref) {
|
|
675
|
-
fakeColumn.ref = [...
|
|
676
|
-
fakeColumn.$refLinks = [...
|
|
726
|
+
fakeColumn.ref = [...baseRef, ...fakeColumn.ref]
|
|
727
|
+
fakeColumn.$refLinks = [...baseRefLinks, ...c.$refLinks]
|
|
677
728
|
}
|
|
678
729
|
replace.push(fakeColumn)
|
|
679
730
|
})
|
|
@@ -682,15 +733,15 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
682
733
|
// fake the ref since excluding only has strings
|
|
683
734
|
col.excluding.forEach(c => {
|
|
684
735
|
const fakeColumn = {
|
|
685
|
-
ref: [...
|
|
736
|
+
ref: [...baseRef, c],
|
|
686
737
|
}
|
|
687
738
|
exclude.push(fakeColumn)
|
|
688
739
|
})
|
|
689
740
|
}
|
|
690
741
|
|
|
691
|
-
if (
|
|
692
|
-
res.push(...getColumnsForWildcard(exclude, replace))
|
|
693
|
-
else
|
|
742
|
+
if (baseRefLinks.at(-1).definition.kind === 'entity') {
|
|
743
|
+
res.push(...getColumnsForWildcard(exclude, replace, col.as))
|
|
744
|
+
} else
|
|
694
745
|
res.push(
|
|
695
746
|
...getFlatColumnsFor(col, { columnAlias: col.as, tableAlias: getQuerySourceName(col) }, [], {
|
|
696
747
|
exclude,
|
|
@@ -907,12 +958,14 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
907
958
|
*
|
|
908
959
|
* Furthermore, foreign keys (FK) for OData CSN and blobs are excluded from the wildcard expansion.
|
|
909
960
|
*
|
|
910
|
-
* @param {
|
|
911
|
-
* @param {
|
|
961
|
+
* @param {array} exclude - An optional list of columns to be excluded during the wildcard expansion.
|
|
962
|
+
* @param {array} replace - An optional list of columns to replace during the wildcard expansion.
|
|
963
|
+
* @param {string} baseName - the explicit alias of the column.
|
|
964
|
+
* Only possible for anonymous expands on implicit table alias: `select from books { { * } as FOO }`
|
|
912
965
|
*
|
|
913
966
|
* @returns {Array} Returns an array of explicit columns derived from the wildcard.
|
|
914
967
|
*/
|
|
915
|
-
function getColumnsForWildcard(exclude = [], replace = []) {
|
|
968
|
+
function getColumnsForWildcard(exclude = [], replace = [], baseName = null) {
|
|
916
969
|
const wildcardColumns = []
|
|
917
970
|
Object.keys(inferred.$combinedElements)
|
|
918
971
|
.filter(k => !exclude.includes(k))
|
|
@@ -927,7 +980,13 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
927
980
|
} else if (isCalculatedOnRead(element)) {
|
|
928
981
|
wildcardColumns.push(resolveCalculatedElement(replace.find(r => r.as === k) || element))
|
|
929
982
|
} else {
|
|
930
|
-
const flatColumns = getFlatColumnsFor(
|
|
983
|
+
const flatColumns = getFlatColumnsFor(
|
|
984
|
+
element,
|
|
985
|
+
{ tableAlias: index, baseName },
|
|
986
|
+
[],
|
|
987
|
+
{ exclude, replace },
|
|
988
|
+
true,
|
|
989
|
+
)
|
|
931
990
|
wildcardColumns.push(...flatColumns)
|
|
932
991
|
}
|
|
933
992
|
})
|
|
@@ -969,7 +1028,16 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
969
1028
|
* Columns excluded in a wildcard expansion or replaced by other columns are also handled accordingly.
|
|
970
1029
|
*
|
|
971
1030
|
* @param {object} column - The structured element which needs to be expanded.
|
|
972
|
-
* @param {
|
|
1031
|
+
* @param {{
|
|
1032
|
+
* columnAlias: string
|
|
1033
|
+
* tableAlias: string
|
|
1034
|
+
* baseName: string
|
|
1035
|
+
* }} names - configuration object for naming parameters:
|
|
1036
|
+
* columnAlias - The explicit alias which the user has defined for the column.
|
|
1037
|
+
* For instance `{ struct.foo as bar}` will be transformed into
|
|
1038
|
+
* `{ struct_foo_leaf1 as bar_foo_leaf1, struct_foo_leaf2 as bar_foo_leaf2 }`.
|
|
1039
|
+
* tableAlias - The table alias to prepend to the column name. Optional.
|
|
1040
|
+
* baseName - The prefixes of the column reference (joined with '_'). Optional.
|
|
973
1041
|
* @param {string} columnAlias - The explicit alias which the user has defined for the column.
|
|
974
1042
|
* For instance `{ struct.foo as bar}` will be transformed into
|
|
975
1043
|
* `{ struct_foo_leaf1 as bar_foo_leaf1, struct_foo_leaf2 as bar_foo_leaf2 }`.
|
|
@@ -1352,7 +1420,7 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
1352
1420
|
if (flatRhs) {
|
|
1353
1421
|
const flatLhs = flattenWithBaseName(token)
|
|
1354
1422
|
|
|
1355
|
-
//
|
|
1423
|
+
//REVISIT: Early exit here? We kndow we cant compare the structs, however we do not know exactly why
|
|
1356
1424
|
// --> calculate error message or exit early? See test "proper error if structures cannot be compared / too many elements on lhs"
|
|
1357
1425
|
if (flatRhs.length !== flatLhs.length)
|
|
1358
1426
|
// make sure we can compare both structures
|
|
@@ -1419,10 +1487,10 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
1419
1487
|
|
|
1420
1488
|
function assertNoStructInXpr(token, inInfixFilter = false) {
|
|
1421
1489
|
if (!inInfixFilter && token.$refLinks?.[token.$refLinks.length - 1].definition.target)
|
|
1422
|
-
//
|
|
1490
|
+
// REVISIT: let this through if not requested otherwise
|
|
1423
1491
|
rejectAssocInExpression()
|
|
1424
1492
|
if (isStructured(token.$refLinks?.[token.$refLinks.length - 1].definition))
|
|
1425
|
-
//
|
|
1493
|
+
// REVISIT: let this through if not requested otherwise
|
|
1426
1494
|
rejectStructInExpression()
|
|
1427
1495
|
|
|
1428
1496
|
function rejectAssocInExpression() {
|
package/lib/infer/index.js
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
344
|
-
if (
|
|
345
|
-
|
|
346
|
-
|
|
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
|
|
@@ -501,20 +499,7 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
501
499
|
const elements = definition.elements || definition._target?.elements
|
|
502
500
|
if (elements && id in elements) {
|
|
503
501
|
const element = elements[id]
|
|
504
|
-
|
|
505
|
-
// only fk access in infix filter
|
|
506
|
-
const nextStep = column.ref[1]?.id || column.ref[1]
|
|
507
|
-
// no unmanaged assoc in infix filter path
|
|
508
|
-
if (!inExists && element.on)
|
|
509
|
-
throw new Error(
|
|
510
|
-
`"${element.name}" in path "${column.ref
|
|
511
|
-
.map(idOnly)
|
|
512
|
-
.join('.')}" must not be an unmanaged association`,
|
|
513
|
-
)
|
|
514
|
-
// no non-fk traversal in infix filter
|
|
515
|
-
if (nextStep && element.foreignKeys && !(nextStep in element.foreignKeys))
|
|
516
|
-
throw new Error(`Only foreign keys of "${element.name}" can be accessed in infix filter`)
|
|
517
|
-
}
|
|
502
|
+
rejectNonFkAccess(element)
|
|
518
503
|
const resolvableIn = definition.target ? definition._target : target
|
|
519
504
|
column.$refLinks.push({ definition: elements[id], target: resolvableIn })
|
|
520
505
|
} else {
|
|
@@ -535,6 +520,12 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
535
520
|
const $refLink = { definition, target: $combinedElements[id][0].tableAlias }
|
|
536
521
|
column.$refLinks.push($refLink)
|
|
537
522
|
nameSegments.push(id)
|
|
523
|
+
} else if (expandOnTableAlias) {
|
|
524
|
+
// expand on table alias
|
|
525
|
+
column.$refLinks.push({
|
|
526
|
+
definition: sources[id],
|
|
527
|
+
target: sources[id],
|
|
528
|
+
})
|
|
538
529
|
} else {
|
|
539
530
|
stepNotFoundInCombinedElements(id) // REVISIT: fails with {__proto__:elements)
|
|
540
531
|
}
|
|
@@ -545,14 +536,16 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
545
536
|
|
|
546
537
|
if (firstStepIsSelf && element?.isAssociation) {
|
|
547
538
|
throw new Error(
|
|
548
|
-
`Paths starting with “$self” must not contain steps of type “cds.Association”: ref: [ ${column.ref
|
|
549
|
-
idOnly
|
|
550
|
-
|
|
539
|
+
`Paths starting with “$self” must not contain steps of type “cds.Association”: ref: [ ${column.ref
|
|
540
|
+
.map(idOnly)
|
|
541
|
+
.join(', ')} ]`,
|
|
551
542
|
)
|
|
552
543
|
}
|
|
553
544
|
|
|
554
545
|
const target = definition._target || column.$refLinks[i - 1].target
|
|
555
546
|
if (element) {
|
|
547
|
+
if($baseLink)
|
|
548
|
+
rejectNonFkAccess(element)
|
|
556
549
|
const $refLink = { definition: elements[id], target }
|
|
557
550
|
column.$refLinks.push($refLink)
|
|
558
551
|
} else if (firstStepIsSelf) {
|
|
@@ -619,7 +612,12 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
619
612
|
if (insertIntoQueryElements) queryElements[column.as || flatName] = getCopyWithAnnos(column, base)
|
|
620
613
|
} else if (column.expand) {
|
|
621
614
|
const elements = resolveExpand(column)
|
|
622
|
-
|
|
615
|
+
let elementName
|
|
616
|
+
// expand on table alias
|
|
617
|
+
if (column.$refLinks.length === 1 && column.$refLinks[0].definition.kind === 'entity')
|
|
618
|
+
elementName = column.$refLinks[0].alias
|
|
619
|
+
else elementName = column.as || flatName
|
|
620
|
+
if (insertIntoQueryElements) queryElements[elementName] = elements
|
|
623
621
|
} else if (column.inline && insertIntoQueryElements) {
|
|
624
622
|
const elements = resolveInline(column)
|
|
625
623
|
queryElements = { ...queryElements, ...elements }
|
|
@@ -649,6 +647,29 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
649
647
|
}
|
|
650
648
|
}
|
|
651
649
|
}
|
|
650
|
+
|
|
651
|
+
/**
|
|
652
|
+
* Check if the next step in the ref is foreign key of `element`
|
|
653
|
+
* if not, an error is thrown.
|
|
654
|
+
*
|
|
655
|
+
* @param {CSN.Element} element if this is an association, the next step must be a foreign key of the element.
|
|
656
|
+
*/
|
|
657
|
+
function rejectNonFkAccess(element) {
|
|
658
|
+
if (!inNestedProjection && !inCalcElement && element.target) {
|
|
659
|
+
// only fk access in infix filter
|
|
660
|
+
const nextStep = column.ref[i + 1]?.id || column.ref[i + 1]
|
|
661
|
+
// no unmanaged assoc in infix filter path
|
|
662
|
+
if (!inExists && element.on)
|
|
663
|
+
throw new Error(
|
|
664
|
+
`"${element.name}" in path "${column.ref
|
|
665
|
+
.map(idOnly)
|
|
666
|
+
.join('.')}" must not be an unmanaged association`
|
|
667
|
+
)
|
|
668
|
+
// no non-fk traversal in infix filter
|
|
669
|
+
if (nextStep && element.foreignKeys && !(nextStep in element.foreignKeys))
|
|
670
|
+
throw new Error(`Only foreign keys of "${element.name}" can be accessed in infix filter`)
|
|
671
|
+
}
|
|
672
|
+
}
|
|
652
673
|
})
|
|
653
674
|
|
|
654
675
|
// ignore whole expand if target of assoc along path has ”@cds.persistence.skip”
|
|
@@ -671,10 +692,6 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
671
692
|
? { ref: [...baseColumn.ref, ...column.ref], $refLinks: [...baseColumn.$refLinks, ...column.$refLinks] }
|
|
672
693
|
: column
|
|
673
694
|
if (isColumnJoinRelevant(colWithBase)) {
|
|
674
|
-
if (originalQuery.UPDATE)
|
|
675
|
-
throw new Error(
|
|
676
|
-
'Path expressions for UPDATE statements are not supported. Use “where exists” with infix filters instead.',
|
|
677
|
-
)
|
|
678
695
|
Object.defineProperty(column, 'isJoinRelevant', { value: true })
|
|
679
696
|
joinTree.mergeColumn(colWithBase, originalQuery.outerQueries)
|
|
680
697
|
}
|
|
@@ -752,8 +769,8 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
752
769
|
*/
|
|
753
770
|
function resolveExpand(col) {
|
|
754
771
|
const { expand, $refLinks } = col
|
|
755
|
-
const $leafLink = $refLinks?.[$refLinks.length - 1]
|
|
756
|
-
if ($leafLink
|
|
772
|
+
const $leafLink = $refLinks?.[$refLinks.length - 1] || inferred.SELECT.from.$refLinks.at(-1) // fallback to anonymous expand
|
|
773
|
+
if ($leafLink.definition._target) {
|
|
757
774
|
const expandSubquery = {
|
|
758
775
|
SELECT: {
|
|
759
776
|
from: $leafLink.definition._target.name,
|
|
@@ -864,20 +881,26 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
864
881
|
if (leafOfCalculatedElementRef.value) mergePathsIntoJoinTree(leafOfCalculatedElementRef.value, basePath)
|
|
865
882
|
|
|
866
883
|
mergePathIfNecessary(basePath, arg)
|
|
867
|
-
} else if (arg.xpr) {
|
|
868
|
-
arg.xpr
|
|
884
|
+
} else if (arg.xpr || arg.args) {
|
|
885
|
+
const prop = arg.xpr ? 'xpr' : 'args'
|
|
886
|
+
arg[prop].forEach(step => {
|
|
887
|
+
const subPath = { $refLinks: [...basePath.$refLinks], ref: [...basePath.ref] }
|
|
869
888
|
if (step.ref) {
|
|
870
|
-
const subPath = { $refLinks: [...basePath.$refLinks], ref: [...basePath.ref] }
|
|
871
889
|
step.$refLinks.forEach((link, i) => {
|
|
872
890
|
const { definition } = link
|
|
873
891
|
if (definition.value) {
|
|
874
|
-
mergePathsIntoJoinTree(definition.value)
|
|
892
|
+
mergePathsIntoJoinTree(definition.value, subPath)
|
|
875
893
|
} else {
|
|
876
894
|
subPath.$refLinks.push(link)
|
|
877
895
|
subPath.ref.push(step.ref[i])
|
|
878
896
|
}
|
|
879
897
|
})
|
|
880
898
|
mergePathIfNecessary(subPath, step)
|
|
899
|
+
} else if (step.args || step.xpr) {
|
|
900
|
+
const nestedProp = step.xpr ? 'xpr' : 'args'
|
|
901
|
+
step[nestedProp].forEach(a => {
|
|
902
|
+
mergePathsIntoJoinTree(a, subPath)
|
|
903
|
+
})
|
|
881
904
|
}
|
|
882
905
|
})
|
|
883
906
|
}
|
package/lib/infer/join-tree.js
CHANGED
|
@@ -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/lib/search.js
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const DRAFT_COLUMNS_UNION = {
|
|
4
|
+
IsActiveEntity: 1,
|
|
5
|
+
HasActiveEntity: 1,
|
|
6
|
+
HasDraftEntity: 1,
|
|
7
|
+
DraftAdministrativeData_DraftUUID: 1,
|
|
8
|
+
SiblingEntity: 1,
|
|
9
|
+
DraftAdministrativeData: 1,
|
|
10
|
+
}
|
|
11
|
+
const DEFAULT_SEARCHABLE_TYPE = 'cds.String'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* This method gets all columns for an entity.
|
|
15
|
+
* It includes the generated foreign keys from managed associations, structured elements and complex and custom types.
|
|
16
|
+
* As well, it provides the annotations starting with '@' for each column.
|
|
17
|
+
*
|
|
18
|
+
* @param {object} entity - the csn entity
|
|
19
|
+
* @param {object} [options]
|
|
20
|
+
* @param [options.onlyNames=false] - decides if the column name or the csn representation of the column should be returned
|
|
21
|
+
* @param [options.filterDraft=false] - indicates whether the draft columns should be filtered if the entity is draft enabled
|
|
22
|
+
* @param [options.removeIgnore=false]
|
|
23
|
+
* @param [options.filterVirtual=false]
|
|
24
|
+
* @param [options.keysOnly=false]
|
|
25
|
+
* @returns {Array<object>} - array of columns
|
|
26
|
+
*/
|
|
27
|
+
const getColumns = (
|
|
28
|
+
entity,
|
|
29
|
+
{ onlyNames = false, removeIgnore = false, filterDraft = true, filterVirtual = false, keysOnly = false },
|
|
30
|
+
) => {
|
|
31
|
+
const skipDraft = filterDraft && entity._isDraftEnabled
|
|
32
|
+
const columns = []
|
|
33
|
+
const elements = entity.elements
|
|
34
|
+
|
|
35
|
+
for (const each in elements) {
|
|
36
|
+
const element = elements[each]
|
|
37
|
+
if (element.isAssociation) continue
|
|
38
|
+
if (filterVirtual && element.virtual) continue
|
|
39
|
+
if (removeIgnore && element['@cds.api.ignore']) continue
|
|
40
|
+
if (skipDraft && each in DRAFT_COLUMNS_UNION) continue
|
|
41
|
+
if (keysOnly && !element.key) continue
|
|
42
|
+
columns.push(onlyNames ? each : element)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return columns
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const _isColumnCalculated = (query, columnName) => {
|
|
49
|
+
if (!query) return false
|
|
50
|
+
if (query.SELECT?.columns?.find(col => col.xpr && col.as === columnName)) return true
|
|
51
|
+
return _isColumnCalculated(query._target?.query, columnName)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const _getSearchableColumns = entity => {
|
|
55
|
+
const columnsOptions = { removeIgnore: true, filterVirtual: true }
|
|
56
|
+
const columns = getColumns(entity, columnsOptions)
|
|
57
|
+
const cdsSearchTerm = '@cds.search'
|
|
58
|
+
const cdsSearchKeys = []
|
|
59
|
+
const cdsSearchColumnMap = new Map()
|
|
60
|
+
|
|
61
|
+
for (const key in entity) {
|
|
62
|
+
if (key.startsWith(cdsSearchTerm)) cdsSearchKeys.push(key)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let atLeastOneColumnIsSearchable = false
|
|
66
|
+
|
|
67
|
+
// build a map of columns annotated with the @cds.search annotation
|
|
68
|
+
for (const key of cdsSearchKeys) {
|
|
69
|
+
const columnName = key.split(cdsSearchTerm + '.').pop()
|
|
70
|
+
|
|
71
|
+
// REVISIT: for now, exclude search using path expression, as deep search is not currently
|
|
72
|
+
// supported
|
|
73
|
+
if (columnName.includes('.')) {
|
|
74
|
+
continue
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const annotationKey = `${cdsSearchTerm}.${columnName}`
|
|
78
|
+
const annotationValue = entity[annotationKey]
|
|
79
|
+
if (annotationValue) atLeastOneColumnIsSearchable = true
|
|
80
|
+
cdsSearchColumnMap.set(columnName, annotationValue)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const searchableColumns = columns.filter(column => {
|
|
84
|
+
const annotatedColumnValue = cdsSearchColumnMap.get(column.name)
|
|
85
|
+
|
|
86
|
+
// the element is searchable if it is annotated with the @cds.search, e.g.:
|
|
87
|
+
// `@cds.search { element1: true }` or `@cds.search { element1 }`
|
|
88
|
+
if (annotatedColumnValue) return true
|
|
89
|
+
|
|
90
|
+
// if at least one element is explicitly annotated as searchable, e.g.:
|
|
91
|
+
// `@cds.search { element1: true }` or `@cds.search { element1 }`
|
|
92
|
+
// and it is not the current column name, then it must be excluded from the search
|
|
93
|
+
if (atLeastOneColumnIsSearchable) return false
|
|
94
|
+
|
|
95
|
+
// the element is considered searchable if it is explicitly annotated as such or
|
|
96
|
+
// if it is not annotated and the column is typed as a string (excluding elements/elements expressions)
|
|
97
|
+
return (
|
|
98
|
+
annotatedColumnValue === undefined &&
|
|
99
|
+
column._type === DEFAULT_SEARCHABLE_TYPE &&
|
|
100
|
+
!_isColumnCalculated(entity?.query, column.name)
|
|
101
|
+
)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
// if the @cds.search annotation is provided -->
|
|
105
|
+
// Early return to ignore the interpretation of the @Search.defaultSearchElement
|
|
106
|
+
// annotation when an entity is annotated with the @cds.search annotation.
|
|
107
|
+
// The @cds.search annotation overrules the @Search.defaultSearchElement annotation.
|
|
108
|
+
if (cdsSearchKeys.length > 0) {
|
|
109
|
+
return searchableColumns.map(column => column.name)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return searchableColumns.map(column => column.name)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* @returns {Array<object>} - array of columns
|
|
117
|
+
*/
|
|
118
|
+
const computeColumnsToBeSearched = (cqn, entity = { __searchableColumns: [] }, alias) => {
|
|
119
|
+
let toBeSearched = []
|
|
120
|
+
|
|
121
|
+
// aggregations case
|
|
122
|
+
// in the new parser groupBy is moved to sub select.
|
|
123
|
+
if (cqn._aggregated || /* new parser */ cqn.SELECT.groupBy || cqn.SELECT?.from?.SELECT?.groupBy) {
|
|
124
|
+
cqn.SELECT.columns &&
|
|
125
|
+
cqn.SELECT.columns.forEach(column => {
|
|
126
|
+
if (column.func) {
|
|
127
|
+
// exclude $count by SELECT of number of Items in a Collection
|
|
128
|
+
if (
|
|
129
|
+
cqn.SELECT.columns.length === 1 &&
|
|
130
|
+
column.func === 'count' &&
|
|
131
|
+
(column.as === '_counted_' || column.as === '$count')
|
|
132
|
+
) {
|
|
133
|
+
return
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
toBeSearched.push(column)
|
|
137
|
+
return
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const columnRef = column.ref
|
|
141
|
+
if (columnRef) {
|
|
142
|
+
if (entity.elements[columnRef[columnRef.length - 1]]?._type !== DEFAULT_SEARCHABLE_TYPE) return
|
|
143
|
+
column = { ref: [...column.ref] }
|
|
144
|
+
if (alias) column.ref.unshift(alias)
|
|
145
|
+
toBeSearched.push(column)
|
|
146
|
+
}
|
|
147
|
+
})
|
|
148
|
+
} else {
|
|
149
|
+
toBeSearched = entity.own('__searchableColumns') || entity.set('__searchableColumns', _getSearchableColumns(entity))
|
|
150
|
+
if (cqn.SELECT.groupBy) toBeSearched = toBeSearched.filter(tbs => cqn.SELECT.groupBy.some(gb => gb.ref[0] === tbs))
|
|
151
|
+
toBeSearched = toBeSearched.map(c => {
|
|
152
|
+
const col = { ref: [c] }
|
|
153
|
+
if (alias) col.ref.unshift(alias)
|
|
154
|
+
return col
|
|
155
|
+
})
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return toBeSearched
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
module.exports = {
|
|
162
|
+
getColumns,
|
|
163
|
+
computeColumnsToBeSearched,
|
|
164
|
+
}
|
package/package.json
CHANGED