@sap/cds-dk 10.0.3 → 10.0.5

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.
Files changed (82) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/bin/debug.js +6 -2
  3. package/bin/up.js +1 -1
  4. package/bin/watch.js +1 -0
  5. package/lib/build/plugins/helm/files/templates/_deployment_helpers.tpl +2 -0
  6. package/lib/build/plugins/nodejs/index.js +6 -3
  7. package/lib/build/util.js +1 -1
  8. package/lib/init/template/ams/files/containerize.yaml.hbs +1 -0
  9. package/lib/init/template/approuter/files/values.yaml.hbs +9 -1
  10. package/lib/init/template/data/as-csv.js +4 -2
  11. package/lib/init/template/data/as-json.js +4 -1
  12. package/lib/init/template/github-actions/files/workflows/cf.yaml.hbs +6 -6
  13. package/lib/init/template/ias/files/values.yaml.hbs +4 -4
  14. package/lib/init/template/portal/files/values.yaml.hbs +12 -0
  15. package/lib/init/template/sample/files/app/browse/fiori-service.cds +2 -0
  16. package/lib/init/template/typer/files/xsconfig.json +1 -1
  17. package/lib/init/template/typescript/files/package.json +1 -1
  18. package/lib/lint/eslintHelp.json +1 -1
  19. package/lib/watch/esm-resolve-registration.js +27 -0
  20. package/lib/watch/vite-watch.js +1 -1
  21. package/node_modules/@cap-js/db-service/CHANGELOG.md +768 -0
  22. package/node_modules/@cap-js/db-service/LICENSE +201 -0
  23. package/node_modules/@cap-js/db-service/README.md +26 -0
  24. package/node_modules/@cap-js/db-service/index.js +19 -0
  25. package/node_modules/@cap-js/db-service/lib/InsertResults.js +122 -0
  26. package/node_modules/@cap-js/db-service/lib/SQLService.js +581 -0
  27. package/node_modules/@cap-js/db-service/lib/common/DatabaseService.js +179 -0
  28. package/node_modules/@cap-js/db-service/lib/common/factory.d.ts +5 -0
  29. package/node_modules/@cap-js/db-service/lib/common/generic-pool.js +310 -0
  30. package/node_modules/@cap-js/db-service/lib/common/session-context.js +46 -0
  31. package/node_modules/@cap-js/db-service/lib/converters.d.ts +24 -0
  32. package/node_modules/@cap-js/db-service/lib/cql-functions.js +464 -0
  33. package/node_modules/@cap-js/db-service/lib/cqn2pql.js +116 -0
  34. package/node_modules/@cap-js/db-service/lib/cqn2sql.js +1612 -0
  35. package/node_modules/@cap-js/db-service/lib/cqn4sql.js +2895 -0
  36. package/node_modules/@cap-js/db-service/lib/deep-queries.js +290 -0
  37. package/node_modules/@cap-js/db-service/lib/fill-in-keys.js +75 -0
  38. package/node_modules/@cap-js/db-service/lib/infer/cqn.d.ts +42 -0
  39. package/node_modules/@cap-js/db-service/lib/infer/index.js +1290 -0
  40. package/node_modules/@cap-js/db-service/lib/infer/join-tree.js +278 -0
  41. package/node_modules/@cap-js/db-service/lib/infer/pseudos.js +24 -0
  42. package/node_modules/@cap-js/db-service/lib/search.js +220 -0
  43. package/node_modules/@cap-js/db-service/lib/utils.js +177 -0
  44. package/node_modules/@cap-js/db-service/package.json +36 -0
  45. package/node_modules/@cap-js/sqlite/CHANGELOG.md +404 -0
  46. package/node_modules/@cap-js/sqlite/LICENSE +201 -0
  47. package/node_modules/@cap-js/sqlite/README.md +34 -0
  48. package/node_modules/@cap-js/sqlite/cds-plugin.js +0 -0
  49. package/node_modules/@cap-js/sqlite/index.js +2 -0
  50. package/node_modules/@cap-js/sqlite/lib/SQLiteService.js +324 -0
  51. package/node_modules/@cap-js/sqlite/lib/cql-functions.js +160 -0
  52. package/node_modules/@cap-js/sqlite/lib/node-sqlite.js +36 -0
  53. package/node_modules/@cap-js/sqlite/lib/session.json +3 -0
  54. package/node_modules/@cap-js/sqlite/lib/sql.js.js +99 -0
  55. package/node_modules/@cap-js/sqlite/package.json +68 -0
  56. package/node_modules/@sap/cds-compiler/CHANGELOG.md +14 -0
  57. package/node_modules/@sap/cds-compiler/bin/cdsse.js +1 -1
  58. package/node_modules/@sap/cds-compiler/bin/cdsv2m.js +1 -1
  59. package/node_modules/@sap/cds-compiler/doc/IncompatibleChanges_v7.md +20 -13
  60. package/node_modules/@sap/cds-compiler/lib/base/builtins.js +4 -1
  61. package/node_modules/@sap/cds-compiler/lib/base/message-registry.js +15 -2
  62. package/node_modules/@sap/cds-compiler/lib/checks/defaultValues.js +0 -1
  63. package/node_modules/@sap/cds-compiler/lib/compiler/resolve.js +6 -5
  64. package/node_modules/@sap/cds-compiler/lib/compiler/tweak-assocs.js +27 -6
  65. package/node_modules/@sap/cds-compiler/lib/edm/annotations/edmJson.js +5 -0
  66. package/node_modules/@sap/cds-compiler/lib/json/from-csn.js +3 -3
  67. package/node_modules/@sap/cds-compiler/lib/json/to-csn.js +5 -4
  68. package/node_modules/@sap/cds-compiler/lib/parsers/AstBuildingParser.js +14 -3
  69. package/node_modules/@sap/cds-compiler/lib/tool-lib/enrichCsn.js +1 -1
  70. package/node_modules/@sap/cds-compiler/lib/transform/db/cdsPersistence.js +3 -2
  71. package/node_modules/@sap/cds-compiler/lib/transform/db/views.js +2 -2
  72. package/node_modules/@sap/cds-compiler/lib/transform/transformUtils.js +1 -2
  73. package/node_modules/@sap/cds-compiler/package.json +1 -1
  74. package/node_modules/iconv-lite/encodings/sbcs-data.js +2 -0
  75. package/node_modules/iconv-lite/encodings/utf32.js +10 -3
  76. package/node_modules/iconv-lite/package.json +2 -2
  77. package/node_modules/iconv-lite/types/encodings.d.ts +2 -0
  78. package/node_modules/ws/lib/receiver.js +15 -32
  79. package/node_modules/ws/lib/websocket-server.js +4 -4
  80. package/node_modules/ws/lib/websocket.js +4 -4
  81. package/node_modules/ws/package.json +5 -1
  82. 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
+ }