@sap/cds-dk 10.0.3 → 10.0.4
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 +6 -0
- package/node_modules/@cap-js/db-service/CHANGELOG.md +768 -0
- package/node_modules/@cap-js/db-service/LICENSE +201 -0
- package/node_modules/@cap-js/db-service/README.md +26 -0
- package/node_modules/@cap-js/db-service/index.js +19 -0
- package/node_modules/@cap-js/db-service/lib/InsertResults.js +122 -0
- package/node_modules/@cap-js/db-service/lib/SQLService.js +581 -0
- package/node_modules/@cap-js/db-service/lib/common/DatabaseService.js +179 -0
- package/node_modules/@cap-js/db-service/lib/common/factory.d.ts +5 -0
- package/node_modules/@cap-js/db-service/lib/common/generic-pool.js +310 -0
- package/node_modules/@cap-js/db-service/lib/common/session-context.js +46 -0
- package/node_modules/@cap-js/db-service/lib/converters.d.ts +24 -0
- package/node_modules/@cap-js/db-service/lib/cql-functions.js +464 -0
- package/node_modules/@cap-js/db-service/lib/cqn2pql.js +116 -0
- package/node_modules/@cap-js/db-service/lib/cqn2sql.js +1612 -0
- package/node_modules/@cap-js/db-service/lib/cqn4sql.js +2895 -0
- package/node_modules/@cap-js/db-service/lib/deep-queries.js +290 -0
- package/node_modules/@cap-js/db-service/lib/fill-in-keys.js +75 -0
- package/node_modules/@cap-js/db-service/lib/infer/cqn.d.ts +42 -0
- package/node_modules/@cap-js/db-service/lib/infer/index.js +1290 -0
- package/node_modules/@cap-js/db-service/lib/infer/join-tree.js +278 -0
- package/node_modules/@cap-js/db-service/lib/infer/pseudos.js +24 -0
- package/node_modules/@cap-js/db-service/lib/search.js +220 -0
- package/node_modules/@cap-js/db-service/lib/utils.js +177 -0
- package/node_modules/@cap-js/db-service/package.json +36 -0
- package/node_modules/@cap-js/sqlite/CHANGELOG.md +404 -0
- package/node_modules/@cap-js/sqlite/LICENSE +201 -0
- package/node_modules/@cap-js/sqlite/README.md +34 -0
- package/node_modules/@cap-js/sqlite/cds-plugin.js +0 -0
- package/node_modules/@cap-js/sqlite/index.js +2 -0
- package/node_modules/@cap-js/sqlite/lib/SQLiteService.js +324 -0
- package/node_modules/@cap-js/sqlite/lib/cql-functions.js +160 -0
- package/node_modules/@cap-js/sqlite/lib/node-sqlite.js +36 -0
- package/node_modules/@cap-js/sqlite/lib/session.json +3 -0
- package/node_modules/@cap-js/sqlite/lib/sql.js.js +99 -0
- package/node_modules/@cap-js/sqlite/package.json +68 -0
- package/node_modules/iconv-lite/encodings/sbcs-data.js +2 -0
- package/node_modules/iconv-lite/encodings/utf32.js +10 -3
- package/node_modules/iconv-lite/package.json +2 -2
- package/node_modules/iconv-lite/types/encodings.d.ts +2 -0
- package/package.json +5 -3
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { prettyPrintRef } = require('../utils')
|
|
4
|
+
|
|
5
|
+
// REVISIT: define following unknown types
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {unknown} $refLink
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {unknown} parent
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @typedef {unknown} where
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @typedef {unknown} children
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @typedef {unknown} queryArtifact
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @typedef {string} alias
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @typedef {Map<alias,Root>} _roots
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @typedef {Object.<string, unknown>} sources
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* A class representing a Node in the join tree.
|
|
41
|
+
*/
|
|
42
|
+
class Node {
|
|
43
|
+
/**
|
|
44
|
+
* @param {$refLink} $refLink
|
|
45
|
+
* @param {parent} parent
|
|
46
|
+
* @param {where} where
|
|
47
|
+
*/
|
|
48
|
+
constructor($refLink, parent, where = null, args = null) {
|
|
49
|
+
/** @type {$refLink} - A reference link to this node. */
|
|
50
|
+
this.$refLink = $refLink
|
|
51
|
+
/** @type {parent} - The parent Node of this node. */
|
|
52
|
+
this.parent = parent
|
|
53
|
+
/** @type {where} - An optional condition to be applied to this node. */
|
|
54
|
+
this.where = where
|
|
55
|
+
/** @type {args} - optional parameter object to be applied to this node. */
|
|
56
|
+
const targetHasParams = $refLink.definition._target?.params || $refLink.definition._target?.['@cds.persistence.udf']
|
|
57
|
+
if (!args && targetHasParams) args = {} // if no args are provided, provide empty argument list
|
|
58
|
+
this.args = args
|
|
59
|
+
/** @type {children} - A Map of children nodes belonging to this node. */
|
|
60
|
+
this.children = new Map()
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* A class representing the root of the join tree.
|
|
66
|
+
*/
|
|
67
|
+
class Root {
|
|
68
|
+
/**
|
|
69
|
+
* @param {[alias, queryArtifact]} querySource
|
|
70
|
+
*/
|
|
71
|
+
constructor(querySource) {
|
|
72
|
+
let [alias, { definition, args }] = querySource
|
|
73
|
+
/** @type {queryArtifact} - The artifact used to make the query. */
|
|
74
|
+
this.queryArtifact = definition
|
|
75
|
+
/** @type {args} - optional parameter object to be applied to this node. */
|
|
76
|
+
const definitionHasParams = definition.params || definition['@cds.persistence.udf']
|
|
77
|
+
if (!args && definitionHasParams) args = {} // if no args are provided, provide empty argument list
|
|
78
|
+
this.args = args
|
|
79
|
+
/** @type {alias} - The alias of the artifact. */
|
|
80
|
+
this.alias = alias
|
|
81
|
+
/** @type {parent} - The parent Node of this root, null for the root Node. */
|
|
82
|
+
this.parent = null
|
|
83
|
+
/** @type {children} - A Map of children nodes belonging to this root. */
|
|
84
|
+
this.children = new Map()
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* A class representing a Join Tree.
|
|
90
|
+
*/
|
|
91
|
+
class JoinTree {
|
|
92
|
+
/**
|
|
93
|
+
*
|
|
94
|
+
* @param {sources} sources
|
|
95
|
+
*/
|
|
96
|
+
constructor(sources) {
|
|
97
|
+
/** @type {_roots} - A Map of root nodes. */
|
|
98
|
+
this._roots = new Map()
|
|
99
|
+
/** @type {boolean} - A boolean indicating if the join tree is in its initial state. */
|
|
100
|
+
this.isInitial = true
|
|
101
|
+
/**
|
|
102
|
+
* A map that holds query aliases which are used during the
|
|
103
|
+
* association to join translation. It is also considered during the
|
|
104
|
+
* where exists expansion.
|
|
105
|
+
*
|
|
106
|
+
* The table aliases are treated case insensitive. The index of each
|
|
107
|
+
* table alias entry, is the capitalized version of the alias.
|
|
108
|
+
* @type {Map<string, string>}
|
|
109
|
+
*/
|
|
110
|
+
this._queryAliases = new Map()
|
|
111
|
+
Object.entries(sources).forEach(entry => {
|
|
112
|
+
const alias = this.addNextAvailableTableAlias(entry[0])
|
|
113
|
+
this._roots.set(alias, new Root(entry))
|
|
114
|
+
})
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Recursively adds aliases of subqueries from a given query source to the alias map.
|
|
119
|
+
*
|
|
120
|
+
* @param {object} sources - The sources of the inferred subquery in a FROM clause.
|
|
121
|
+
*/
|
|
122
|
+
addAliasesOfSubqueryInFrom(sources) {
|
|
123
|
+
Object.entries(sources).forEach(e => {
|
|
124
|
+
this.addNextAvailableTableAlias(e[0])
|
|
125
|
+
if (e[1].sources)
|
|
126
|
+
// recurse
|
|
127
|
+
this.addAliasesOfSubqueryInFrom(e[1].sources)
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Calculates and adds the next available table alias to the alias map.
|
|
133
|
+
*
|
|
134
|
+
* @param {string} alias - The original alias name.
|
|
135
|
+
* @param {unknown[]} outerQueries - An array of outer queries.
|
|
136
|
+
* @param {string} key - The key to be used for storing the alias in the map. If not provided, the upper-case version of the alias will be used as the key.
|
|
137
|
+
* @returns {string} - The next unambiguous table alias.
|
|
138
|
+
*/
|
|
139
|
+
addNextAvailableTableAlias(alias, outerQueries, key) {
|
|
140
|
+
const upperAlias = alias.toUpperCase()
|
|
141
|
+
if (this._queryAliases.get(upperAlias) || outerQueries?.some(outer => outerHasAlias(outer, key))) {
|
|
142
|
+
let j = 2
|
|
143
|
+
while (this._queryAliases.get(upperAlias + j) || outerQueries?.some(outer => outerHasAlias(outer, key, j))) j += 1
|
|
144
|
+
alias += j
|
|
145
|
+
}
|
|
146
|
+
this._queryAliases.set(key || alias.toUpperCase(), alias)
|
|
147
|
+
return alias
|
|
148
|
+
|
|
149
|
+
function outerHasAlias(outer, searchInValues = false, number) {
|
|
150
|
+
const currAlias = number ? upperAlias + number : upperAlias
|
|
151
|
+
return searchInValues ? Array.from(outer.joinTree._queryAliases.values()).includes(currAlias) : outer.joinTree._queryAliases.get(currAlias)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Merges a column into the join tree.
|
|
157
|
+
*
|
|
158
|
+
* It begins by inferring the source of the given column, which is the table alias where the column is resolvable.
|
|
159
|
+
* 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.
|
|
160
|
+
* If not, it creates a new Node and ensures proper aliasing and foreign key access.
|
|
161
|
+
*
|
|
162
|
+
* @param {object} col - The column object to be merged into the existing join tree. This object should have the properties $refLinks and ref.
|
|
163
|
+
* @returns {boolean} - Always returns true, indicating the column has been successfully merged into the join tree.
|
|
164
|
+
*/
|
|
165
|
+
mergeColumn(col, outerQueries = null) {
|
|
166
|
+
if (this.isInitial) this.isInitial = false
|
|
167
|
+
const head = col.$refLinks[0]
|
|
168
|
+
let node = this._roots.get(head.alias)
|
|
169
|
+
let i = 0
|
|
170
|
+
if (!node) {
|
|
171
|
+
this._roots.forEach(r => {
|
|
172
|
+
// find the correct query source
|
|
173
|
+
if (
|
|
174
|
+
r.queryArtifact === head.target ||
|
|
175
|
+
r.queryArtifact === head.target._target /** might as well be a query for order by */
|
|
176
|
+
)
|
|
177
|
+
node = r
|
|
178
|
+
})
|
|
179
|
+
} else {
|
|
180
|
+
i += 1 // skip first step which is table alias
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// if no root node was found, the column is selected from a subquery
|
|
184
|
+
if (!node) return
|
|
185
|
+
while (i < col.ref.length) {
|
|
186
|
+
if(col.join === 'inner') node.join = 'inner'
|
|
187
|
+
const step = col.ref[i]
|
|
188
|
+
const { where, args } = step
|
|
189
|
+
const id = joinId(step, args, where)
|
|
190
|
+
const next = node.children.get(id)
|
|
191
|
+
const $refLink = col.$refLinks[i]
|
|
192
|
+
// sanity check: error out if we can't produce a join
|
|
193
|
+
if ($refLink.definition.keys && $refLink.definition.keys.length === 0) {
|
|
194
|
+
const path = prettyPrintRef(col.ref)
|
|
195
|
+
throw new Error(`Path step “${$refLink.alias}” of “${path}” has no foreign keys`)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (next) {
|
|
199
|
+
// step already seen before
|
|
200
|
+
node = next
|
|
201
|
+
// re-set $refLink to equal the one which got already merged
|
|
202
|
+
col.$refLinks[i].alias = node.$refLink.alias
|
|
203
|
+
col.$refLinks[i].definition = node.$refLink.definition
|
|
204
|
+
col.$refLinks[i].target = node.$refLink.target
|
|
205
|
+
col.$refLinks[i].onlyForeignKeyAccess = node.$refLink.onlyForeignKeyAccess
|
|
206
|
+
} else {
|
|
207
|
+
if (col.expand && !col.ref[i + 1]) {
|
|
208
|
+
node.$refLink.onlyForeignKeyAccess = false
|
|
209
|
+
return true
|
|
210
|
+
}
|
|
211
|
+
const child = new Node($refLink, node, where, args)
|
|
212
|
+
if (child.$refLink.definition.isAssociation) {
|
|
213
|
+
if (child.where || child.$refLink.definition.on || col.inline) {
|
|
214
|
+
// filter is always join relevant
|
|
215
|
+
// if the column ends up in an `inline` -> each assoc step is join relevant
|
|
216
|
+
child.$refLink.onlyForeignKeyAccess = false
|
|
217
|
+
// all parents are now also join relevant
|
|
218
|
+
markParentAsJoinRelevant(child.parent)
|
|
219
|
+
} else {
|
|
220
|
+
child.$refLink.onlyForeignKeyAccess = true
|
|
221
|
+
}
|
|
222
|
+
child.$refLink.alias = this.addNextAvailableTableAlias($refLink.alias, outerQueries)
|
|
223
|
+
}
|
|
224
|
+
const elements =
|
|
225
|
+
node.$refLink?.definition.isAssociation &&
|
|
226
|
+
(node.$refLink.definition.elements || node.$refLink.definition.foreignKeys)
|
|
227
|
+
if (node.$refLink && (!elements || !(child.$refLink.definition.name in elements))) {
|
|
228
|
+
// no foreign key access
|
|
229
|
+
node.$refLink.onlyForeignKeyAccess = false
|
|
230
|
+
markParentAsJoinRelevant(node.parent)
|
|
231
|
+
|
|
232
|
+
col.$refLinks[i - 1] = node.$refLink
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
node.children.set(id, child)
|
|
236
|
+
node = child
|
|
237
|
+
}
|
|
238
|
+
i += 1
|
|
239
|
+
}
|
|
240
|
+
return true
|
|
241
|
+
|
|
242
|
+
function markParentAsJoinRelevant(parent) {
|
|
243
|
+
while (parent) {
|
|
244
|
+
if (parent.$refLink?.definition.isAssociation) {
|
|
245
|
+
parent.$refLink.onlyForeignKeyAccess = false
|
|
246
|
+
}
|
|
247
|
+
parent = parent.parent
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function joinId(step, args, where) {
|
|
252
|
+
let appendix
|
|
253
|
+
if (where && args) appendix = JSON.stringify(where) + JSON.stringify(args)
|
|
254
|
+
else if (where) appendix = JSON.stringify(where)
|
|
255
|
+
else if (args) appendix = JSON.stringify(args)
|
|
256
|
+
return appendix ? step.id + appendix : step
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Performs a depth-first search for the next association in the children of the given node which does not only access foreign keys.
|
|
262
|
+
*
|
|
263
|
+
* @param {Node} node - The node from which to search for the next association.
|
|
264
|
+
* @returns {Node|null} - Returns the node which represents an association or null if none was found.
|
|
265
|
+
*/
|
|
266
|
+
findNextAssoc(node) {
|
|
267
|
+
if (node.$refLink.definition.isAssociation && !node.$refLink.onlyForeignKeyAccess) return node
|
|
268
|
+
// recurse on each child node
|
|
269
|
+
for (const child of node.children.values()) {
|
|
270
|
+
const grandChild = this.findNextAssoc(child)
|
|
271
|
+
if (grandChild) return grandChild
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return null
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
module.exports = JoinTree
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const cds = require('@sap/cds')
|
|
4
|
+
const { String, Timestamp } = cds.builtin.types
|
|
5
|
+
|
|
6
|
+
const pseudos = {
|
|
7
|
+
elements: {
|
|
8
|
+
$user: {
|
|
9
|
+
elements: {
|
|
10
|
+
id: String,
|
|
11
|
+
locale: String, // deprecated
|
|
12
|
+
tenant: String, // deprecated
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
$now: Timestamp,
|
|
16
|
+
$at: Timestamp,
|
|
17
|
+
$from: Timestamp,
|
|
18
|
+
$to: Timestamp,
|
|
19
|
+
$locale: String,
|
|
20
|
+
$tenant: String,
|
|
21
|
+
},
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
module.exports = { pseudos }
|
|
@@ -0,0 +1,220 @@
|
|
|
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
|
+
// only those which return strings are relevant for search
|
|
14
|
+
const aggregateFunctions = {
|
|
15
|
+
MAX: true,
|
|
16
|
+
MIN: true,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* This method gets all columns for an entity.
|
|
21
|
+
* It includes the generated foreign keys from managed associations, structured elements and complex and custom types.
|
|
22
|
+
* Moreover, it provides the annotations starting with '@' for each column.
|
|
23
|
+
*
|
|
24
|
+
* @param {object} entity - the csn entity
|
|
25
|
+
* @param {object} [options]
|
|
26
|
+
* @param [options.onlyNames=false] - decides if the column name or the csn representation of the column should be returned
|
|
27
|
+
* @param [options.filterDraft=false] - indicates whether the draft columns should be filtered if the entity is draft enabled
|
|
28
|
+
* @param [options.removeIgnore=false]
|
|
29
|
+
* @param [options.filterVirtual=false]
|
|
30
|
+
* @param [options.keysOnly=false]
|
|
31
|
+
* @returns {Array<object>} - array of columns
|
|
32
|
+
*/
|
|
33
|
+
const getColumns = (
|
|
34
|
+
entity,
|
|
35
|
+
{ onlyNames = false, removeIgnore = false, filterDraft = true, filterVirtual = false, keysOnly = false },
|
|
36
|
+
) => {
|
|
37
|
+
const columns = []
|
|
38
|
+
const elements = entity.elements
|
|
39
|
+
|
|
40
|
+
for (const each in elements) {
|
|
41
|
+
const element = elements[each]
|
|
42
|
+
if (filterVirtual && element.virtual) continue
|
|
43
|
+
if (removeIgnore && element['@cds.api.ignore']) continue
|
|
44
|
+
if (filterDraft && each in DRAFT_COLUMNS_UNION) continue
|
|
45
|
+
if (keysOnly && !element.key) continue
|
|
46
|
+
columns.push(onlyNames ? each : element)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return columns
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const _isColumnCalculated = (query, columnName) => {
|
|
53
|
+
if (!query) return false
|
|
54
|
+
if (query.SELECT?.columns?.find(col => col.xpr && col.as === columnName)) return true
|
|
55
|
+
return _isColumnCalculated(query._target?.query, columnName)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const _getSearchableColumns = entity => {
|
|
59
|
+
const columnsOptions = { removeIgnore: true, filterVirtual: true }
|
|
60
|
+
const columns = entity.SELECT?.columns || getColumns(entity, columnsOptions)
|
|
61
|
+
const cdsSearchTerm = '@cds.search'
|
|
62
|
+
const cdsSearchKeys = []
|
|
63
|
+
const cdsSearchColumnMap = new Map()
|
|
64
|
+
|
|
65
|
+
for (const key in entity) {
|
|
66
|
+
if (key.startsWith(cdsSearchTerm)) cdsSearchKeys.push(key)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
let skipDefaultSearchableElements = false
|
|
70
|
+
const deepSearchCandidates = []
|
|
71
|
+
|
|
72
|
+
// build a map of columns annotated with the @cds.search annotation
|
|
73
|
+
for (const key of cdsSearchKeys) {
|
|
74
|
+
const columnName = key.split(cdsSearchTerm + '.').pop()
|
|
75
|
+
const annotationKey = `${cdsSearchTerm}.${columnName}`
|
|
76
|
+
const annotationValue = entity[annotationKey]
|
|
77
|
+
|
|
78
|
+
const column = entity.elements[columnName]
|
|
79
|
+
// always ignore virtual elements from search
|
|
80
|
+
if(column?.virtual) continue
|
|
81
|
+
if (column?.isAssociation || columnName.includes('.')) {
|
|
82
|
+
if(!annotationValue)
|
|
83
|
+
continue
|
|
84
|
+
|
|
85
|
+
const ref = columnName.split('.')
|
|
86
|
+
if(ref.length > 1) skipDefaultSearchableElements = true
|
|
87
|
+
deepSearchCandidates.push({ ref })
|
|
88
|
+
continue
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if(annotationValue) skipDefaultSearchableElements = true
|
|
92
|
+
cdsSearchColumnMap.set(columnName, annotationValue)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const searchableColumns = columns.filter(column => {
|
|
96
|
+
const annotatedColumnValue = cdsSearchColumnMap.get(column.name)
|
|
97
|
+
const elementName = column.as || column.ref?.at(-1) || column.name
|
|
98
|
+
const element = entity.elements[elementName]
|
|
99
|
+
|
|
100
|
+
// the element is searchable if it is annotated with the @cds.search, e.g.:
|
|
101
|
+
// `@cds.search { element1: true }` or `@cds.search { element1 }`
|
|
102
|
+
if (annotatedColumnValue) return true
|
|
103
|
+
|
|
104
|
+
// calculated elements are only searchable if requested through `@cds.search`
|
|
105
|
+
if (column.value) return false
|
|
106
|
+
|
|
107
|
+
// if at least one element is explicitly annotated as searchable, e.g.:
|
|
108
|
+
// `@cds.search { element1: true }` or `@cds.search { element1 }`
|
|
109
|
+
// and it is not the current column name, then it must be excluded from the search
|
|
110
|
+
if (skipDefaultSearchableElements) return false
|
|
111
|
+
|
|
112
|
+
// the element is considered searchable if it is explicitly annotated as such or
|
|
113
|
+
// if it is not annotated and the column is typed as a string (excluding elements/elements expressions)
|
|
114
|
+
return (
|
|
115
|
+
annotatedColumnValue === undefined &&
|
|
116
|
+
element?.type === DEFAULT_SEARCHABLE_TYPE &&
|
|
117
|
+
!_isColumnCalculated(entity?.query, column.name)
|
|
118
|
+
)
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
if (deepSearchCandidates.length) {
|
|
122
|
+
deepSearchCandidates.forEach(c => {
|
|
123
|
+
let element = entity
|
|
124
|
+
for (let i = 0; i < c.ref.length; ++i) {
|
|
125
|
+
const curr = c.ref[i]
|
|
126
|
+
const next = element.elements?.[curr] ?? element._target?.elements?.[curr]
|
|
127
|
+
|
|
128
|
+
if (!next) { // e.g. if a search element is not part of a projection
|
|
129
|
+
element = undefined
|
|
130
|
+
break
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (next.isAssociation && i === c.ref.length - 1) {
|
|
134
|
+
_getSearchableColumns(next._target).forEach(r => searchableColumns.push({ ref: c.ref.concat(...r.ref) }))
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
element = next
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (element?.type === DEFAULT_SEARCHABLE_TYPE) {
|
|
141
|
+
searchableColumns.push({ ref: c.ref })
|
|
142
|
+
}
|
|
143
|
+
})
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return searchableColumns.map(column => {
|
|
147
|
+
if (column.ref) return column
|
|
148
|
+
return { ref: [column.name] }
|
|
149
|
+
})
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* @returns {Array<object>} - array of columns
|
|
154
|
+
*/
|
|
155
|
+
const computeColumnsToBeSearched = (cqn, entity = { __searchableColumns: [] }) => {
|
|
156
|
+
let toBeSearched = []
|
|
157
|
+
|
|
158
|
+
// aggregations case
|
|
159
|
+
// in the new parser groupBy is moved to sub select.
|
|
160
|
+
if (cqn._aggregated || /* new parser */ cqn.SELECT.groupBy || cqn.SELECT?.from?.SELECT?.groupBy) {
|
|
161
|
+
cqn.SELECT.columns?.forEach(column => {
|
|
162
|
+
const elementName = column.as || column.ref?.at(-1) || column.name
|
|
163
|
+
const element = cqn.elements[elementName]
|
|
164
|
+
if (column.func || column.xpr) {
|
|
165
|
+
// exclude $count by SELECT of number of Items in a Collection
|
|
166
|
+
if (
|
|
167
|
+
cqn.SELECT.columns.length === 1 &&
|
|
168
|
+
column.func === 'count' &&
|
|
169
|
+
(column.as === '_counted_' || column.as === '$count')
|
|
170
|
+
) {
|
|
171
|
+
return
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// only strings can be searched
|
|
175
|
+
if (element?.type !== DEFAULT_SEARCHABLE_TYPE) {
|
|
176
|
+
if (column.xpr) return
|
|
177
|
+
if (column.func && !(column.func.toUpperCase() in aggregateFunctions)) return
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const searchTerm = {}
|
|
181
|
+
if (column.func) {
|
|
182
|
+
searchTerm.func = column.func
|
|
183
|
+
searchTerm.args = column.args
|
|
184
|
+
} else if (column.xpr) {
|
|
185
|
+
searchTerm.xpr = column.xpr
|
|
186
|
+
}
|
|
187
|
+
toBeSearched.push(searchTerm)
|
|
188
|
+
return
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// no need to set ref[0] to alias, because columns were already properly transformed
|
|
192
|
+
if (column.ref) {
|
|
193
|
+
if (element?.type !== DEFAULT_SEARCHABLE_TYPE) return
|
|
194
|
+
column = { ref: [...column.ref] }
|
|
195
|
+
toBeSearched.push(column)
|
|
196
|
+
return
|
|
197
|
+
}
|
|
198
|
+
})
|
|
199
|
+
} else {
|
|
200
|
+
if (entity.kind === 'entity') {
|
|
201
|
+
// first check cache
|
|
202
|
+
toBeSearched =
|
|
203
|
+
entity.own('__searchableColumns') || entity.set('__searchableColumns', _getSearchableColumns(entity))
|
|
204
|
+
} else {
|
|
205
|
+
// if we search on a subquery, we don't have a cache
|
|
206
|
+
toBeSearched = _getSearchableColumns(entity)
|
|
207
|
+
}
|
|
208
|
+
toBeSearched = toBeSearched.map(c => {
|
|
209
|
+
const column = { ref: [...c.ref] }
|
|
210
|
+
return column
|
|
211
|
+
})
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return toBeSearched
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
module.exports = {
|
|
218
|
+
getColumns,
|
|
219
|
+
computeColumnsToBeSearched,
|
|
220
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const cds = require('@sap/cds')
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Formats a ref array into a string representation.
|
|
7
|
+
* If the first step is an entity, the separator is a colon, otherwise a dot.
|
|
8
|
+
*
|
|
9
|
+
* @param {Array} ref - The reference array to be formatted.
|
|
10
|
+
* @param {Object} model - The model object containing definitions.
|
|
11
|
+
* @returns {string} The formatted string representation of the reference.
|
|
12
|
+
*/
|
|
13
|
+
function prettyPrintRef(ref, model = null) {
|
|
14
|
+
return ref.reduce((acc, curr, j) => {
|
|
15
|
+
if (j > 0) {
|
|
16
|
+
if (j === 1 && model?.definitions[ref[0]]?.kind === 'entity') {
|
|
17
|
+
acc += ':'
|
|
18
|
+
} else {
|
|
19
|
+
acc += '.'
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return acc + `${curr.id ? curr.id + '[…]' : curr}`
|
|
23
|
+
}, '')
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function hasOwnSkip(definition) {
|
|
27
|
+
return (
|
|
28
|
+
definition && Object.hasOwn(definition, '@cds.persistence.skip') && definition['@cds.persistence.skip'] === true
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function isRuntimeView(definition) {
|
|
33
|
+
if (!definition || !cds.env.features.runtime_views) return false
|
|
34
|
+
if (definition['_isRuntimeView']) return true
|
|
35
|
+
if (!definition['@cds.persistence.skip']) {
|
|
36
|
+
Object.defineProperty(definition, '_isRuntimeView', {
|
|
37
|
+
value: true,
|
|
38
|
+
writable: false,
|
|
39
|
+
configurable: true,
|
|
40
|
+
enumerable: false
|
|
41
|
+
})
|
|
42
|
+
return true
|
|
43
|
+
}
|
|
44
|
+
// views with "as select from" variant are also runtime views, even if they are annotated with persistence skip
|
|
45
|
+
if (definition.query && !definition.query._target) return true
|
|
46
|
+
if (definition.query) return isRuntimeView(definition.query._target)
|
|
47
|
+
|
|
48
|
+
return false
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Determines if a definition is calculated on read.
|
|
53
|
+
* - Stored calculated elements are not unfolded
|
|
54
|
+
* - Association like calculated elements have been re-written by the compiler
|
|
55
|
+
* they essentially behave like unmanaged associations as their calculations
|
|
56
|
+
* have been incorporated into an on-condition which is handled elsewhere
|
|
57
|
+
*
|
|
58
|
+
* @param {Object} def - The definition to check.
|
|
59
|
+
* @returns {boolean} - Returns true if the definition is calculated on read, otherwise false.
|
|
60
|
+
*/
|
|
61
|
+
function isCalculatedOnRead(def) {
|
|
62
|
+
return isCalculatedElement(def) && !def.value.stored && !def.on
|
|
63
|
+
}
|
|
64
|
+
function isCalculatedElement(def) {
|
|
65
|
+
return def?.value
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Calculates the implicit table alias for a given string.
|
|
70
|
+
*
|
|
71
|
+
* Based on the last part of the string, the implicit alias is calculated
|
|
72
|
+
* by taking the first character and prepending it with '$'.
|
|
73
|
+
* A leading '$' is removed if the last part already starts with '$'.
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* getImplicitAlias('Books') => '$B'
|
|
77
|
+
* getImplicitAlias('bookshop.Books') => '$B'
|
|
78
|
+
* getImplicitAlias('bookshop.$B') => '$B'
|
|
79
|
+
*
|
|
80
|
+
* @param {string} str - The input string.
|
|
81
|
+
* @returns {string}
|
|
82
|
+
*/
|
|
83
|
+
function getImplicitAlias(str, useTechnicalAlias = true) {
|
|
84
|
+
const index = str.lastIndexOf('.')
|
|
85
|
+
if (useTechnicalAlias) {
|
|
86
|
+
const postfix = (index != -1 ? str.substring(index + 1) : str).replace(/^\$/, '')[0] || /* str === '$' */ '$'
|
|
87
|
+
return '$' + postfix
|
|
88
|
+
}
|
|
89
|
+
return index != -1 ? str.substring(index + 1) : str
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function defineProperty(obj, prop, value) {
|
|
93
|
+
return Object.defineProperty(obj, prop, {
|
|
94
|
+
value,
|
|
95
|
+
writable: true,
|
|
96
|
+
configurable: true,
|
|
97
|
+
})
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Shared utility functions which operate dynamically on the model / query.
|
|
102
|
+
*
|
|
103
|
+
* @param {CSN.model} model
|
|
104
|
+
* @param {CQL} query
|
|
105
|
+
*/
|
|
106
|
+
function getModelUtils(model, query) {
|
|
107
|
+
/**
|
|
108
|
+
* Returns the name of the localized entity for the given `definition`.
|
|
109
|
+
*
|
|
110
|
+
* If the query is `localized`, returns the name of the `localized` version of the `definition`.
|
|
111
|
+
* If there is no `localized` version of the `definition`, return the name of the `definition`
|
|
112
|
+
*
|
|
113
|
+
* @param {CSN.definition} definition
|
|
114
|
+
* @returns the name of the localized entity for the given `definition` or `definition.name`
|
|
115
|
+
*/
|
|
116
|
+
function getLocalizedName(definition) {
|
|
117
|
+
if (!isLocalized(definition)) return definition.name
|
|
118
|
+
const view = getDefinition(`localized.${definition.name}`)
|
|
119
|
+
return view?.name || definition.name
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Returns true if the definition shall be localized, in the context of the given query.
|
|
124
|
+
*
|
|
125
|
+
* If a given query is required to be translated, the query has
|
|
126
|
+
* the `.localized` property set to `true`. If that is the case,
|
|
127
|
+
* and the definition has not set the `@cds.localized` annotation
|
|
128
|
+
* to `false`, the given definition must be translated.
|
|
129
|
+
*
|
|
130
|
+
* @returns true if the given definition shall be localized
|
|
131
|
+
*/
|
|
132
|
+
function isLocalized(definition) {
|
|
133
|
+
return (
|
|
134
|
+
query.SELECT?.localized &&
|
|
135
|
+
definition?.['@cds.localized'] !== false &&
|
|
136
|
+
!query.SELECT.forUpdate &&
|
|
137
|
+
!query.SELECT.forShareLock
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Returns the (potentially localized) CSN definition for the given name from the model.
|
|
143
|
+
*
|
|
144
|
+
* @param {string} name - The name of the definition to retrieve.
|
|
145
|
+
* @returns {Object|null} The CSN definition or null if not found. The definition may be localized.
|
|
146
|
+
*/
|
|
147
|
+
function getDefinition(name) {
|
|
148
|
+
if (!name) return null
|
|
149
|
+
const def = model.definitions[name]
|
|
150
|
+
if (!def || !isLocalized(def)) return def
|
|
151
|
+
return model.definitions[`localized.${def.name}`] || def
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
getLocalizedName,
|
|
155
|
+
isLocalized,
|
|
156
|
+
getDefinition,
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function resolveTable(target) {
|
|
161
|
+
if (target.query?._target && !Object.prototype.hasOwnProperty.call(target, '@cds.persistence.table'))
|
|
162
|
+
return resolveTable(target.query._target)
|
|
163
|
+
return target
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// export the function to be used in other modules
|
|
167
|
+
module.exports = {
|
|
168
|
+
prettyPrintRef,
|
|
169
|
+
isCalculatedOnRead,
|
|
170
|
+
isCalculatedElement,
|
|
171
|
+
getImplicitAlias,
|
|
172
|
+
defineProperty,
|
|
173
|
+
getModelUtils,
|
|
174
|
+
hasOwnSkip,
|
|
175
|
+
isRuntimeView,
|
|
176
|
+
resolveTable
|
|
177
|
+
}
|