@cap-js/db-service 1.0.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 +9 -0
- package/LICENSE +201 -0
- package/README.md +5 -0
- package/index.js +4 -0
- package/lib/InsertResults.js +95 -0
- package/lib/SQLService.js +288 -0
- package/lib/common/DatabaseService.js +144 -0
- package/lib/cql-functions.js +148 -0
- package/lib/cqn2sql.js +605 -0
- package/lib/cqn4sql.js +1846 -0
- package/lib/deep-queries.js +244 -0
- package/lib/fill-in-keys.js +65 -0
- package/lib/infer/index.js +922 -0
- package/lib/infer/join-tree.js +188 -0
- package/lib/infer/pseudos.js +23 -0
- package/package.json +30 -0
|
@@ -0,0 +1,922 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const cds = require('@sap/cds/lib')
|
|
4
|
+
|
|
5
|
+
const JoinTree = require('./join-tree')
|
|
6
|
+
const { pseudos } = require('./pseudos')
|
|
7
|
+
// REVISIT: we should always return cds.linked elements
|
|
8
|
+
const cdsTypes = cds.linked({
|
|
9
|
+
definitions: {
|
|
10
|
+
Timestamp: { type: 'cds.Timestamp' },
|
|
11
|
+
DateTime: { type: 'cds.DateTime' },
|
|
12
|
+
Date: { type: 'cds.Date' },
|
|
13
|
+
Time: { type: 'cds.Time' },
|
|
14
|
+
String: { type: 'cds.String' },
|
|
15
|
+
Decimal: { type: 'cds.Decimal' },
|
|
16
|
+
Integer: { type: 'cds.Integer' },
|
|
17
|
+
Boolean: { type: 'cds.Boolean' },
|
|
18
|
+
},
|
|
19
|
+
}).definitions
|
|
20
|
+
for (const each in cdsTypes) cdsTypes[`cds.${each}`] = cdsTypes[each]
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {CQN|CQL} originalQuery
|
|
24
|
+
* @param {CSN} [model]
|
|
25
|
+
* @returns {InferredCQN} = q with .target and .elements
|
|
26
|
+
*/
|
|
27
|
+
function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
28
|
+
if (!model) cds.error('Please specify a model')
|
|
29
|
+
const inferred = typeof originalQuery === 'string' ? cds.parse.cql(originalQuery) : cds.ql.clone(originalQuery)
|
|
30
|
+
|
|
31
|
+
// REVISIT: The more edge use cases we support, thes less optimized are we for the 90+% use cases
|
|
32
|
+
// e.g. there's a lot of overhead for infer( SELECT.from(Books) )
|
|
33
|
+
if (originalQuery.SET) cds.error('”UNION” based queries are not supported')
|
|
34
|
+
const _ =
|
|
35
|
+
inferred.SELECT ||
|
|
36
|
+
inferred.INSERT ||
|
|
37
|
+
inferred.UPSERT ||
|
|
38
|
+
inferred.UPDATE ||
|
|
39
|
+
inferred.DELETE ||
|
|
40
|
+
inferred.CREATE ||
|
|
41
|
+
inferred.DROP ||
|
|
42
|
+
inferred.STREAM
|
|
43
|
+
const sources = inferTarget(_.from || _.into || _.entity, {})
|
|
44
|
+
const joinTree = new JoinTree(sources)
|
|
45
|
+
const aliases = Object.keys(sources)
|
|
46
|
+
Object.defineProperties(inferred, {
|
|
47
|
+
// REVISIT: public, or for local reuse, or in cqn4sql only?
|
|
48
|
+
sources: { value: sources, writable: true },
|
|
49
|
+
target: { value: aliases.length === 1 ? sources[aliases[0]] : originalQuery, writable: true }, // REVISIT: legacy?
|
|
50
|
+
})
|
|
51
|
+
// also enrich original query -> writable because it may be inferred again
|
|
52
|
+
Object.defineProperties(originalQuery, {
|
|
53
|
+
sources: { value: sources, writable: true },
|
|
54
|
+
target: {
|
|
55
|
+
value: aliases.length === 1 ? sources[aliases[0]] : originalQuery,
|
|
56
|
+
writable: true,
|
|
57
|
+
},
|
|
58
|
+
})
|
|
59
|
+
if (originalQuery.SELECT || originalQuery.DELETE || originalQuery.UPDATE || originalQuery.STREAM) {
|
|
60
|
+
const $combinedElements = inferCombinedElements()
|
|
61
|
+
/**
|
|
62
|
+
* TODO: this function is currently only called on DELETE's
|
|
63
|
+
* because it correctly set's up the $refLink's in the
|
|
64
|
+
* where clause: This functionality should be pulled out
|
|
65
|
+
* of ´inferQueryElement()` as this is a subtle side effect
|
|
66
|
+
*/
|
|
67
|
+
const elements = inferQueryElements($combinedElements)
|
|
68
|
+
Object.defineProperties(inferred, {
|
|
69
|
+
$combinedElements: { value: $combinedElements, writable: true },
|
|
70
|
+
elements: { value: elements, writable: true },
|
|
71
|
+
joinTree: { value: joinTree, writable: true }, // REVISIT: eliminate
|
|
72
|
+
})
|
|
73
|
+
// also enrich original query -> writable because it may be inferred again
|
|
74
|
+
Object.defineProperty(originalQuery, 'elements', { value: elements, writable: true })
|
|
75
|
+
}
|
|
76
|
+
return inferred
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Infers all query sources from a given SQL-like query's `from` clause.
|
|
80
|
+
* It drills down into join arguments of the `from` clause.
|
|
81
|
+
*
|
|
82
|
+
* This function helps identify each source, target, and association within the `from` clause.
|
|
83
|
+
* It processes the `from` clause in the query and maps each source to a respective target and alias.
|
|
84
|
+
* In case of any errors like missing definitions or associations, this function will throw an error.
|
|
85
|
+
*
|
|
86
|
+
* @function inferTarget
|
|
87
|
+
* @param {object|string} from - The `from` clause of the query to infer the target from.
|
|
88
|
+
* It could be an object or a string.
|
|
89
|
+
* @param {object} querySources - An object to map the query sources.
|
|
90
|
+
* Each key is a query source alias, and its value is the corresponding CSN Definition.
|
|
91
|
+
* @returns {object} The updated `querySources` object with inferred sources from the `from` clause.
|
|
92
|
+
*/
|
|
93
|
+
function inferTarget(from, querySources) {
|
|
94
|
+
const { ref } = from
|
|
95
|
+
if (ref) {
|
|
96
|
+
const first = ref[0].id || ref[0]
|
|
97
|
+
let target = getDefinition(first, model)
|
|
98
|
+
if (!target) cds.error(`"${first}" not found in the definitions of your model`)
|
|
99
|
+
if (ref.length > 1) {
|
|
100
|
+
target = from.ref.slice(1).reduce((d, r) => {
|
|
101
|
+
const next = d.elements[r.id || r]?.elements ? d.elements[r.id || r] : d.elements[r.id || r]?._target
|
|
102
|
+
if (!next) cds.error(`No association "${r.id || r}" in ${d.kind} "${d.name}": ${d}`)
|
|
103
|
+
return next
|
|
104
|
+
}, target)
|
|
105
|
+
}
|
|
106
|
+
if (target.kind !== 'entity' && !target._isAssociation)
|
|
107
|
+
throw new Error(/Query source must be a an entity or an association/)
|
|
108
|
+
|
|
109
|
+
attachRefLinksToArg(from) // REVISIT: remove
|
|
110
|
+
const alias =
|
|
111
|
+
from.uniqueSubqueryAlias ||
|
|
112
|
+
from.as ||
|
|
113
|
+
(ref.length === 1 ? first.match(/[^.]+$/)[0] : ref[ref.length - 1].id || ref[ref.length - 1])
|
|
114
|
+
if (alias in querySources) throw new Error(`Duplicate alias "${alias}"`)
|
|
115
|
+
querySources[alias] = target
|
|
116
|
+
} else if (from.args) {
|
|
117
|
+
from.args.forEach(a => inferTarget(a, querySources))
|
|
118
|
+
} else if (from.SELECT) {
|
|
119
|
+
infer(from, model) // we need the .elements in the sources
|
|
120
|
+
querySources[from.as || ''] = from
|
|
121
|
+
} else if (typeof from === 'string') {
|
|
122
|
+
querySources[/([^.]*)$/.exec(from)[0]] = getDefinition(from, model)
|
|
123
|
+
} else if (from.SET) {
|
|
124
|
+
infer(from, model)
|
|
125
|
+
}
|
|
126
|
+
return querySources
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// REVISIT: this helper is doing by far too much, with too many side effects
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* This function recursively traverses through all 'ref' steps of the 'arg' object and enriches it by attaching
|
|
133
|
+
* additional information. For each 'ref' step, it adds the corresponding definition and the target in which the
|
|
134
|
+
* next 'ref' step should be looked up.
|
|
135
|
+
*
|
|
136
|
+
*
|
|
137
|
+
* @param {Object} arg - The argument object that will be augmented with additional properties.
|
|
138
|
+
* It must contain a 'ref' property, which is an array representing the steps to be processed.
|
|
139
|
+
* Optionally, it can also contain an 'xpr' property, which is also processed recursively.
|
|
140
|
+
*
|
|
141
|
+
* @param {Object} $baseLink - Optional parameter. It represents the environment in which the first 'ref' step should be
|
|
142
|
+
* resolved. It's needed for infix filter / expand columns. It must contain a 'definition'
|
|
143
|
+
* property, which is an object representing the base environment.
|
|
144
|
+
*
|
|
145
|
+
* @param {boolean} expandOrExists - Optional parameter, defaults to false. It indicates whether the 'arg' is part of a
|
|
146
|
+
* 'column.expand' or preceded by an 'exists'. When true, unmanaged association paths
|
|
147
|
+
* are allowed -> $baseLink is an `expand` or `assoc` preceded by `exists`.
|
|
148
|
+
*
|
|
149
|
+
* @throws Will throw an error if a 'ref' step cannot be found in the current environment or if a 'ref' step
|
|
150
|
+
* represents an unmanaged association in the case of infix filters and 'expandOrExists' is false.
|
|
151
|
+
*
|
|
152
|
+
* @returns {void} This function does not return a value; it mutates the 'arg' object directly.
|
|
153
|
+
*/
|
|
154
|
+
function attachRefLinksToArg(arg, $baseLink = null, expandOrExists = false) {
|
|
155
|
+
const { ref, xpr } = arg
|
|
156
|
+
if (xpr) xpr.forEach(t => attachRefLinksToArg(t, $baseLink, expandOrExists))
|
|
157
|
+
if (!ref) return
|
|
158
|
+
init$refLinks(arg)
|
|
159
|
+
ref.forEach((step, i) => {
|
|
160
|
+
const id = step.id || step
|
|
161
|
+
if (i === 0) {
|
|
162
|
+
// infix filter never have table alias
|
|
163
|
+
// we need to search for first step in ´model.definitions[infixAlias]`
|
|
164
|
+
if ($baseLink) {
|
|
165
|
+
const { definition } = $baseLink
|
|
166
|
+
const elements = definition.elements || definition._target?.elements
|
|
167
|
+
const e = elements?.[id] || cds.error`"${id}" not found in the elements of "${definition.name}"`
|
|
168
|
+
if (e.target) {
|
|
169
|
+
// only fk access in infix filter
|
|
170
|
+
const nextStep = ref[1]?.id || ref[1]
|
|
171
|
+
// no unmanaged assoc in infix filter path
|
|
172
|
+
if (!expandOrExists && e.on)
|
|
173
|
+
throw new Error(
|
|
174
|
+
`"${e.name}" in path "${arg.ref.map(idOnly).join('.')}" must not be an unmanaged association`,
|
|
175
|
+
)
|
|
176
|
+
// no non-fk traversal in infix filter
|
|
177
|
+
if (!expandOrExists && nextStep && !(nextStep in e.foreignKeys))
|
|
178
|
+
throw new Error(`Only foreign keys of "${e.name}" can be accessed in infix filter`)
|
|
179
|
+
}
|
|
180
|
+
arg.$refLinks.push({ definition: e, target: e._target || e })
|
|
181
|
+
// filter paths are flattened
|
|
182
|
+
// REVISIT: too much augmentation -> better remove flatName..
|
|
183
|
+
Object.defineProperty(arg, 'flatName', { value: ref.join('_'), writable: true })
|
|
184
|
+
} else {
|
|
185
|
+
// must be in model.definitions
|
|
186
|
+
const definition = getDefinition(id, model)
|
|
187
|
+
arg.$refLinks[0] = { definition, target: definition }
|
|
188
|
+
}
|
|
189
|
+
} else {
|
|
190
|
+
const recent = arg.$refLinks[i - 1]
|
|
191
|
+
const { elements } = recent.target
|
|
192
|
+
const e = elements[id]
|
|
193
|
+
if (!e) throw new Error(`"${id}" not found in the elements of "${arg.$refLinks[i - 1].definition.name}"`)
|
|
194
|
+
arg.$refLinks.push({ definition: e, target: e._target || e })
|
|
195
|
+
}
|
|
196
|
+
arg.$refLinks[i].alias = !ref[i + 1] && arg.as ? arg.as : id.split('.').pop()
|
|
197
|
+
|
|
198
|
+
// link refs in where
|
|
199
|
+
if (step.where) {
|
|
200
|
+
// REVISIT: why do we need to walk through these so early?
|
|
201
|
+
if (arg.$refLinks[i].definition.kind === 'entity' || arg.$refLinks[i].definition._target) {
|
|
202
|
+
let existsPredicate = false
|
|
203
|
+
const walkTokenStream = token => {
|
|
204
|
+
if (token === 'exists') {
|
|
205
|
+
// no joins for infix filters along `exists <path>`
|
|
206
|
+
existsPredicate = true
|
|
207
|
+
} else if (token.xpr) {
|
|
208
|
+
// don't miss an exists within an expression
|
|
209
|
+
token.xpr.forEach(walkTokenStream)
|
|
210
|
+
} else {
|
|
211
|
+
attachRefLinksToArg(token, arg.$refLinks[i], existsPredicate)
|
|
212
|
+
existsPredicate = false
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
step.where.forEach(walkTokenStream)
|
|
216
|
+
} else throw new Error('A filter can only be provided when navigating along associations')
|
|
217
|
+
}
|
|
218
|
+
})
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Calculates the `$combinedElements` based on the provided queries `sources`.
|
|
223
|
+
* The `$combinedElements` of a query consist of all accessible elements across all
|
|
224
|
+
* the table aliases found in the from clause.
|
|
225
|
+
*
|
|
226
|
+
* The `$combinedElements` are attached to the query as a non-enumerable property.
|
|
227
|
+
* Each entry in the `$combinedElements` dictionary maps from the element name
|
|
228
|
+
* to an array of objects containing the index and table alias where the element can be found.
|
|
229
|
+
*
|
|
230
|
+
* @returns {Object} The `$combinedElements` dictionary, which maps element names to an array of objects
|
|
231
|
+
* containing the index and table alias where the element can be found.
|
|
232
|
+
*/
|
|
233
|
+
function inferCombinedElements() {
|
|
234
|
+
const combinedElements = {}
|
|
235
|
+
for (const index in sources) {
|
|
236
|
+
const tableAlias = sources[index]
|
|
237
|
+
for (const key in tableAlias.elements) {
|
|
238
|
+
if (key in combinedElements) combinedElements[key].push({ index, tableAlias })
|
|
239
|
+
else combinedElements[key] = [{ index, tableAlias }]
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return combinedElements
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Assigns the given `element` as non-enumerable property 'element' onto `col`.
|
|
247
|
+
*
|
|
248
|
+
* @param {object} col
|
|
249
|
+
* @param {csn.Element} element
|
|
250
|
+
*/
|
|
251
|
+
function setElementOnColumns(col, element) {
|
|
252
|
+
Object.defineProperty(col, 'element', {
|
|
253
|
+
value: element,
|
|
254
|
+
writable: true,
|
|
255
|
+
})
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Walks over all columns of a query's `SELECT` and infers each `ref`, `xpr`, or `val` as a query element
|
|
260
|
+
* based on the query's `$combinedElements` and `sources`.
|
|
261
|
+
*
|
|
262
|
+
* The inferred `elements` are attached to the query as a non-enumerable property.
|
|
263
|
+
*
|
|
264
|
+
* Also walks over other `ref`s in the query, validates them, and attaches `$refLinks`.
|
|
265
|
+
* This includes handling `where`, infix filters within column `refs`, or other `csn` paths.
|
|
266
|
+
*
|
|
267
|
+
* @param {Object} $combinedElements The `$combinedElements` dictionary of the query, which maps element names
|
|
268
|
+
* to an array of objects containing the index and table alias where the element can be found.
|
|
269
|
+
* @returns {Object} The inferred `elements` dictionary of the query, which maps element names to their corresponding definitions.
|
|
270
|
+
*/
|
|
271
|
+
function inferQueryElements($combinedElements) {
|
|
272
|
+
let queryElements = {}
|
|
273
|
+
const { columns, where, groupBy, having, orderBy } = _
|
|
274
|
+
if (!columns) {
|
|
275
|
+
inferElementsFromWildCard(aliases)
|
|
276
|
+
} else {
|
|
277
|
+
let wildcardSelect = false
|
|
278
|
+
const refs = []
|
|
279
|
+
columns.forEach(col => {
|
|
280
|
+
if (col === '*') {
|
|
281
|
+
wildcardSelect = true
|
|
282
|
+
} else if (col.val !== undefined || col.xpr || col.SELECT || col.func || col.param) {
|
|
283
|
+
const as = col.as || col.func || col.val
|
|
284
|
+
if (as === undefined) throw cds.error`Expecting expression to have an alias name`
|
|
285
|
+
if (queryElements[as]) throw cds.error`Duplicate definition of element “${as}”`
|
|
286
|
+
if (col.xpr || col.SELECT) {
|
|
287
|
+
queryElements[as] = getElementForXprOrSubquery(col)
|
|
288
|
+
} else if (col.func) {
|
|
289
|
+
col.args?.forEach(arg => inferQueryElement(arg, false)) // {func}.args are optional
|
|
290
|
+
queryElements[as] = getElementForCast(col)
|
|
291
|
+
} else {
|
|
292
|
+
// either binding parameter (col.param) or value
|
|
293
|
+
queryElements[as] = col.cast ? getElementForCast(col) : getCdsTypeForVal(col.val)
|
|
294
|
+
}
|
|
295
|
+
setElementOnColumns(col, queryElements[as])
|
|
296
|
+
} else if (col.ref) {
|
|
297
|
+
refs.push(col)
|
|
298
|
+
} else if (col.expand) {
|
|
299
|
+
inferQueryElement(col)
|
|
300
|
+
} else {
|
|
301
|
+
throw cds.error`Not supported: ${JSON.stringify(col)}`
|
|
302
|
+
}
|
|
303
|
+
})
|
|
304
|
+
refs.forEach(col => {
|
|
305
|
+
inferQueryElement(col)
|
|
306
|
+
const { definition } = col.$refLinks[col.$refLinks.length - 1]
|
|
307
|
+
if (col.cast)
|
|
308
|
+
// final type overwritten -> element not visible anymore
|
|
309
|
+
setElementOnColumns(col, getElementForCast(col))
|
|
310
|
+
else if ((col.ref.length === 1) & (col.ref[0] === '$user'))
|
|
311
|
+
// shortcut to $user.id
|
|
312
|
+
setElementOnColumns(col, queryElements[col.as || '$user'])
|
|
313
|
+
else setElementOnColumns(col, definition)
|
|
314
|
+
})
|
|
315
|
+
if (wildcardSelect) inferElementsFromWildCard(aliases)
|
|
316
|
+
}
|
|
317
|
+
if (orderBy) {
|
|
318
|
+
// link $refLinks -> special name resolution rules for orderBy
|
|
319
|
+
orderBy.forEach(token => {
|
|
320
|
+
let $baseLink
|
|
321
|
+
// first check if token ref is resolvable in query elements
|
|
322
|
+
if (columns) {
|
|
323
|
+
const e = queryElements[token.ref?.[0]]
|
|
324
|
+
const isAssocExpand = e?.$assocExpand // expand on structure can be addressed
|
|
325
|
+
if (e && !isAssocExpand) $baseLink = { definition: { elements: queryElements }, target: inferred }
|
|
326
|
+
} else {
|
|
327
|
+
// fallback to elements of query source
|
|
328
|
+
$baseLink = null
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
inferQueryElement(token, false, $baseLink)
|
|
332
|
+
})
|
|
333
|
+
}
|
|
334
|
+
if (where) {
|
|
335
|
+
let skipJoins
|
|
336
|
+
const walkTokenStream = token => {
|
|
337
|
+
if (token === 'exists') {
|
|
338
|
+
// no joins for infix filters along `exists <path>`
|
|
339
|
+
skipJoins = true
|
|
340
|
+
} else if (token.xpr) {
|
|
341
|
+
// don't miss an exists within an expression
|
|
342
|
+
token.xpr.forEach(walkTokenStream)
|
|
343
|
+
} else {
|
|
344
|
+
inferQueryElement(token, false, null, { inExists: skipJoins, inExpr: true })
|
|
345
|
+
skipJoins = false
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
where.forEach(walkTokenStream)
|
|
349
|
+
}
|
|
350
|
+
if (groupBy)
|
|
351
|
+
// link $refLinks
|
|
352
|
+
groupBy.forEach(token => inferQueryElement(token, false))
|
|
353
|
+
if (having)
|
|
354
|
+
// link $refLinks
|
|
355
|
+
having.forEach(token => inferQueryElement(token, false))
|
|
356
|
+
if (_.with)
|
|
357
|
+
// consider UPDATE.with
|
|
358
|
+
Object.values(_.with).forEach(val => inferQueryElement(val, false))
|
|
359
|
+
|
|
360
|
+
return queryElements
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* This function is responsible for inferring a query element based on a provided column.
|
|
364
|
+
* It initializes and attaches a non-enumerable `$refLinks` property to the column,
|
|
365
|
+
* which stores an array of objects that represent the corresponding artifact of the ref step.
|
|
366
|
+
* Each object in the `$refLinks` array corresponds to the same index position in the `column.ref` array.
|
|
367
|
+
* Based on the leaf artifact (last object in the `$refLinks` array), the query element is inferred.
|
|
368
|
+
*
|
|
369
|
+
* @param {object} column - The column object that contains the properties to infer a query element.
|
|
370
|
+
* @param {boolean} [insertIntoQueryElements=true] - Determines whether the inferred element should be inserted into the queries elements.
|
|
371
|
+
* For instance, it's set to false when walking over the where clause.
|
|
372
|
+
* @param {object} [$baseLink=null] - A base reference link, usually it's an object with a definition and a target.
|
|
373
|
+
* Used for infix filters, exists <assoc> and nested projections.
|
|
374
|
+
* @param {object} [context={}] - Contextual information for element inference.
|
|
375
|
+
* @param {boolean} [context.inExists=false] - Flag to control the creation of joins for non-association path traversals.
|
|
376
|
+
* for `exists <assoc>` paths we do not need to create joins for path expressions as they are part of the semi-joined subquery.
|
|
377
|
+
* @param {boolean} [context.inExpr=false] - Flag to signal whether the element is part of an expression.
|
|
378
|
+
* Used to ignore non-persisted elements.
|
|
379
|
+
* @param {boolean} [context.inNestedProjection=false] - Flag to signal whether the element is part of a nested projection.
|
|
380
|
+
*
|
|
381
|
+
* Note:
|
|
382
|
+
* - `inExists` is used to specify cases where no joins should be created for non-association path traversals.
|
|
383
|
+
* It is primarily used for infix filters in `exists assoc[parent.foo='bar']`, where it becomes part of a semi-join.
|
|
384
|
+
* - Columns with a `param` property are parameter references resolved into values only at execution time.
|
|
385
|
+
* - Columns with an `args` property are function calls in expressions.
|
|
386
|
+
* - Columns with a `list` property represent a list of values (e.g., for the IN operator).
|
|
387
|
+
* - Columns with a `SELECT` property represent subqueries.
|
|
388
|
+
*
|
|
389
|
+
* @throws {Error} If an unmanaged association is found in an infix filter path, an error is thrown.
|
|
390
|
+
* @throws {Error} If a non-foreign key traversal is found in an infix filter, an error is thrown.
|
|
391
|
+
* @throws {Error} If a first step is not found in the combined elements, an error is thrown.
|
|
392
|
+
* @throws {Error} If a filter is provided while navigating along non-associations, an error is thrown.
|
|
393
|
+
* @throws {Error} If the same element name is inferred more than once, an error is thrown.
|
|
394
|
+
*
|
|
395
|
+
* @returns {void}
|
|
396
|
+
*/
|
|
397
|
+
|
|
398
|
+
function inferQueryElement(column, insertIntoQueryElements = true, $baseLink = null, context) {
|
|
399
|
+
const { inExists, inExpr, inNestedProjection } = context || {}
|
|
400
|
+
if (column.param) return // parameter references are only resolved into values on execution e.g. :val, :1 or ?
|
|
401
|
+
if (column.args) column.args.forEach(arg => inferQueryElement(arg, false, $baseLink, context)) // e.g. function in expression
|
|
402
|
+
if (column.list) column.list.forEach(arg => inferQueryElement(arg, false, $baseLink, context))
|
|
403
|
+
if (column.xpr)
|
|
404
|
+
column.xpr.forEach(token => inferQueryElement(token, false, $baseLink, { ...context, inExpr: true })) // e.g. function in expression
|
|
405
|
+
if (column.SELECT) return
|
|
406
|
+
|
|
407
|
+
if (!column.ref) {
|
|
408
|
+
if (column.expand) queryElements[column.as] = resolveExpand(column)
|
|
409
|
+
return
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
init$refLinks(column)
|
|
413
|
+
// if any path step points to an artifact with `@cds.persistence.skip`
|
|
414
|
+
// we must ignore the element from the queries elements
|
|
415
|
+
let isPersisted = true
|
|
416
|
+
const firstStepIsTableAlias =
|
|
417
|
+
(column.ref.length > 1 && column.ref[0] in sources) ||
|
|
418
|
+
// nested projection on table alias
|
|
419
|
+
(column.ref.length === 1 && column.ref[0] in sources && column.inline)
|
|
420
|
+
const firstStepIsSelf =
|
|
421
|
+
!firstStepIsTableAlias && column.ref.length > 1 && ['$self', '$projection'].includes(column.ref[0])
|
|
422
|
+
const nameSegments = []
|
|
423
|
+
// if a (segment) of a (structured) foreign key is renamed, we must not include
|
|
424
|
+
// the aliased ref segments into the name of the final foreign key which is e.g. used in
|
|
425
|
+
// on conditions of joins
|
|
426
|
+
const skipAliasedFkSegmentsOfNameStack = []
|
|
427
|
+
let pseudoPath = false
|
|
428
|
+
column.ref.forEach((step, i) => {
|
|
429
|
+
const id = step.id || step
|
|
430
|
+
if (i === 0) {
|
|
431
|
+
if (id in pseudos.elements) {
|
|
432
|
+
// pseudo path
|
|
433
|
+
column.$refLinks.push({ definition: pseudos.elements[id], target: pseudos })
|
|
434
|
+
pseudoPath = true // only first path step must be well defined
|
|
435
|
+
nameSegments.push(id)
|
|
436
|
+
} else if ($baseLink) {
|
|
437
|
+
const { definition, target } = $baseLink
|
|
438
|
+
const elements = definition.elements || definition._target?.elements
|
|
439
|
+
if (elements && id in elements) {
|
|
440
|
+
const element = elements[id]
|
|
441
|
+
if (!inExists && !inNestedProjection && element.target) {
|
|
442
|
+
// only fk access in infix filter
|
|
443
|
+
const nextStep = column.ref[1]?.id || column.ref[1]
|
|
444
|
+
// no unmanaged assoc in infix filter path
|
|
445
|
+
if (element.on)
|
|
446
|
+
throw new Error(
|
|
447
|
+
`"${element.name}" in path "${column.ref
|
|
448
|
+
.map(idOnly)
|
|
449
|
+
.join('.')}" must not be an unmanaged association`,
|
|
450
|
+
)
|
|
451
|
+
// no non-fk traversal in infix filter
|
|
452
|
+
if (nextStep && !(nextStep in element.foreignKeys))
|
|
453
|
+
throw new Error(`Only foreign keys of "${element.name}" can be accessed in infix filter`)
|
|
454
|
+
}
|
|
455
|
+
column.$refLinks.push({ definition: elements[id], target })
|
|
456
|
+
} else {
|
|
457
|
+
stepNotFoundInPredecessor(id, definition.name)
|
|
458
|
+
}
|
|
459
|
+
nameSegments.push(id)
|
|
460
|
+
} else if (firstStepIsTableAlias) {
|
|
461
|
+
column.$refLinks.push({ definition: sources[id], target: sources[id] })
|
|
462
|
+
} else if (firstStepIsSelf) {
|
|
463
|
+
column.$refLinks.push({ definition: { elements: queryElements }, target: { elements: queryElements } })
|
|
464
|
+
} else if (column.ref.length > 1 && inferred.outerQueries?.find(outer => id in outer.sources)) {
|
|
465
|
+
// outer query accessed via alias
|
|
466
|
+
const outerAlias = inferred.outerQueries.find(outer => id in outer.sources)
|
|
467
|
+
column.$refLinks.push({ definition: outerAlias.sources[id], target: outerAlias.sources[id] })
|
|
468
|
+
} else if (id in $combinedElements) {
|
|
469
|
+
if ($combinedElements[id].length > 1) stepIsAmbiguous(id) // exit
|
|
470
|
+
const definition = $combinedElements[id][0].tableAlias.elements[id]
|
|
471
|
+
const $refLink = { definition, target: $combinedElements[id][0].tableAlias }
|
|
472
|
+
column.$refLinks.push($refLink)
|
|
473
|
+
nameSegments.push(id)
|
|
474
|
+
} else {
|
|
475
|
+
stepNotFoundInCombinedElements(id) // REVISIT: fails with {__proto__:elements)
|
|
476
|
+
}
|
|
477
|
+
} else {
|
|
478
|
+
const { definition } = column.$refLinks[i - 1]
|
|
479
|
+
const elements = definition.elements || definition._target?.elements
|
|
480
|
+
if (elements && id in elements) {
|
|
481
|
+
const $refLink = { definition: elements[id], target: column.$refLinks[i - 1].target }
|
|
482
|
+
column.$refLinks.push($refLink)
|
|
483
|
+
} else if (firstStepIsSelf) {
|
|
484
|
+
stepNotFoundInColumnList(id)
|
|
485
|
+
} else if (column.ref[0] === '$user' && pseudoPath) {
|
|
486
|
+
// `$user.some.unknown.element` -> no error
|
|
487
|
+
column.$refLinks.push({ definition: {}, target: column.$refLinks[i - 1].target })
|
|
488
|
+
} else if (id === '$dummy') {
|
|
489
|
+
// `some.known.element.$dummy` -> no error; used by cds.ql to simulate joins
|
|
490
|
+
column.$refLinks.push({ definition: { name: '$dummy', parent: column.$refLinks[i - 1].target } })
|
|
491
|
+
Object.defineProperty(column, 'isJoinRelevant', { value: true })
|
|
492
|
+
} else {
|
|
493
|
+
const notFoundIn = pseudoPath ? column.ref[i - 1] : getFullPathForLinkedArg(column)
|
|
494
|
+
stepNotFoundInPredecessor(id, notFoundIn)
|
|
495
|
+
}
|
|
496
|
+
const foreignKeyAlias = Array.isArray(definition.keys)
|
|
497
|
+
? definition.keys.find(k => {
|
|
498
|
+
if (k.ref.every((step, j) => column.ref[i + j] === step)) {
|
|
499
|
+
skipAliasedFkSegmentsOfNameStack.push(...k.ref.slice(1))
|
|
500
|
+
return true
|
|
501
|
+
}
|
|
502
|
+
return false
|
|
503
|
+
})?.as
|
|
504
|
+
: null
|
|
505
|
+
if (foreignKeyAlias) nameSegments.push(foreignKeyAlias)
|
|
506
|
+
else if (skipAliasedFkSegmentsOfNameStack[0] === id) skipAliasedFkSegmentsOfNameStack.shift()
|
|
507
|
+
else nameSegments.push(id)
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
if (step.where) {
|
|
511
|
+
const danglingFilter = !(column.ref[i + 1] || column.expand || inExists)
|
|
512
|
+
if (!column.$refLinks[i].definition.target || danglingFilter)
|
|
513
|
+
throw new Error(/A filter can only be provided when navigating along associations/)
|
|
514
|
+
if (!column.expand) Object.defineProperty(column, 'isJoinRelevant', { value: true })
|
|
515
|
+
// books[exists genre[code='A']].title --> column is join relevant but inner exists filter is not
|
|
516
|
+
let skipJoinsForFilter = inExists
|
|
517
|
+
step.where.forEach(token => {
|
|
518
|
+
if (token === 'exists') {
|
|
519
|
+
// no joins for infix filters along `exists <path>`
|
|
520
|
+
skipJoinsForFilter = true
|
|
521
|
+
} else if (token.ref || token.xpr) {
|
|
522
|
+
inferQueryElement(token, false, column.$refLinks[i], {
|
|
523
|
+
inExists: skipJoinsForFilter,
|
|
524
|
+
inExpr: !!token.xpr,
|
|
525
|
+
})
|
|
526
|
+
} else if (token.func) {
|
|
527
|
+
token.args?.forEach(arg =>
|
|
528
|
+
inferQueryElement(arg, false, column.$refLinks[i], { inExists: skipJoinsForFilter, inExpr: true }),
|
|
529
|
+
)
|
|
530
|
+
}
|
|
531
|
+
})
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
column.$refLinks[i].alias = !column.ref[i + 1] && column.as ? column.as : id.split('.').pop()
|
|
535
|
+
if (column.$refLinks[i].definition._target?.['@cds.persistence.skip'] === true) isPersisted = false
|
|
536
|
+
if (!column.ref[i + 1]) {
|
|
537
|
+
const flatName = nameSegments.join('_')
|
|
538
|
+
Object.defineProperty(column, 'flatName', { value: flatName, writable: true })
|
|
539
|
+
// if column is casted, we overwrite it's origin with the new type
|
|
540
|
+
if (column.cast) {
|
|
541
|
+
const base = getElementForCast(column)
|
|
542
|
+
if (insertIntoQueryElements) queryElements[column.as || flatName] = getCopyWithAnnos(column, base)
|
|
543
|
+
} else if (column.expand) {
|
|
544
|
+
const elements = resolveExpand(column)
|
|
545
|
+
if (insertIntoQueryElements) queryElements[column.as || flatName] = elements
|
|
546
|
+
} else if (column.inline && insertIntoQueryElements) {
|
|
547
|
+
const elements = resolveInline(column)
|
|
548
|
+
queryElements = { ...queryElements, ...elements }
|
|
549
|
+
} else {
|
|
550
|
+
// shortcut for `ref: ['$user']` -> `ref: ['$user', 'id']`
|
|
551
|
+
const leafArt =
|
|
552
|
+
i === 0 && id === '$user' ? column.$refLinks[i].definition.elements.id : column.$refLinks[i].definition
|
|
553
|
+
// infer element based on leaf artifact of path
|
|
554
|
+
if (insertIntoQueryElements) {
|
|
555
|
+
let elementName
|
|
556
|
+
if (column.as) {
|
|
557
|
+
elementName = column.as
|
|
558
|
+
} else {
|
|
559
|
+
// if the navigation the user has written differs from the final flat ref - e.g. for renamed foreign keys -
|
|
560
|
+
// the inferred name of the element equals the flat version of the user-written ref.
|
|
561
|
+
const refNavigation = column.ref
|
|
562
|
+
.slice(firstStepIsSelf || firstStepIsTableAlias ? 1 : 0)
|
|
563
|
+
.map(idOnly)
|
|
564
|
+
.join('_')
|
|
565
|
+
if (refNavigation !== flatName) elementName = refNavigation
|
|
566
|
+
else elementName = flatName
|
|
567
|
+
}
|
|
568
|
+
if (queryElements[elementName] !== undefined)
|
|
569
|
+
throw new Error(`Duplicate definition of element “${elementName}”`)
|
|
570
|
+
queryElements[elementName] = getCopyWithAnnos(column, leafArt)
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
})
|
|
575
|
+
|
|
576
|
+
// ignore whole expand if target of assoc along path has ”@cds.persistence.skip”
|
|
577
|
+
if (column.expand) {
|
|
578
|
+
const { $refLinks } = column
|
|
579
|
+
const skip = $refLinks.some(
|
|
580
|
+
link => model.definitions[link.definition.target]?.['@cds.persistence.skip'] === true,
|
|
581
|
+
)
|
|
582
|
+
if (skip) {
|
|
583
|
+
$refLinks[$refLinks.length - 1].skipExpand = true
|
|
584
|
+
return
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
const virtual = (column.$refLinks[column.$refLinks.length - 1].definition.virtual || !isPersisted) && !inExpr
|
|
588
|
+
// check if we need to merge the column `ref` into the join tree of the query
|
|
589
|
+
if (!inExists && !virtual && isColumnJoinRelevant(column)) {
|
|
590
|
+
Object.defineProperty(column, 'isJoinRelevant', { value: true })
|
|
591
|
+
joinTree.mergeColumn(column)
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* Resolves and processes the inline attribute of a column in a database query.
|
|
596
|
+
*
|
|
597
|
+
* @param {Object} col - The column object with properties: `inline` and `$refLinks`.
|
|
598
|
+
* @param {string} [namePrefix=col.as || col.flatName] - Prefix for naming new columns. Defaults to `col.as` or `col.flatName`.
|
|
599
|
+
* @returns {Object} - An object with resolved and processed inline column definitions.
|
|
600
|
+
*
|
|
601
|
+
* Procedure:
|
|
602
|
+
* 1. Iterate through `inline` array. For each `inlineCol`:
|
|
603
|
+
* a. If `inlineCol` equals '*', wildcard elements are processed and added to the `elements` object.
|
|
604
|
+
* b. If `inlineCol` has inline or expand attributes, corresponding functions are called recursively and the resulting elements are added to the `elements` object.
|
|
605
|
+
* c. If `inlineCol` has val or func attributes, new elements are created and added to the `elements` object.
|
|
606
|
+
* d. Otherwise, the corresponding `$refLinks` definition is added to the `elements` object.
|
|
607
|
+
* 2. Returns the `elements` object.
|
|
608
|
+
*/
|
|
609
|
+
function resolveInline(col, namePrefix = col.as || col.flatName) {
|
|
610
|
+
const { inline, $refLinks } = col
|
|
611
|
+
const $leafLink = $refLinks[$refLinks.length - 1]
|
|
612
|
+
let elements = {}
|
|
613
|
+
inline.forEach(inlineCol => {
|
|
614
|
+
inferQueryElement(inlineCol, false, $leafLink, { inExpr: true, inNestedProjection: true })
|
|
615
|
+
if (inlineCol === '*') {
|
|
616
|
+
const wildCardElements = {}
|
|
617
|
+
// either the `.elements´ of the struct or the `.elements` of the assoc target
|
|
618
|
+
const leafLinkElements = $leafLink.definition.elements || $leafLink.definition._target.elements
|
|
619
|
+
Object.entries(leafLinkElements).forEach(([k, v]) => {
|
|
620
|
+
const name = namePrefix ? `${namePrefix}_${k}` : k
|
|
621
|
+
// if overwritten/excluded omit from wildcard elements
|
|
622
|
+
// in elements the names are already flat so consider the prefix
|
|
623
|
+
// in excluding, the elements are addressed without the prefix
|
|
624
|
+
if (!(name in elements || col.excluding?.some(e => e === k))) wildCardElements[name] = v
|
|
625
|
+
})
|
|
626
|
+
elements = { ...elements, ...wildCardElements }
|
|
627
|
+
} else {
|
|
628
|
+
const nameParts = namePrefix ? [namePrefix] : []
|
|
629
|
+
if (inlineCol.as) nameParts.push(inlineCol.as)
|
|
630
|
+
else nameParts.push(...inlineCol.ref.map(idOnly))
|
|
631
|
+
const name = nameParts.join('_')
|
|
632
|
+
if (inlineCol.inline) {
|
|
633
|
+
const inlineElements = resolveInline(inlineCol, name)
|
|
634
|
+
elements = { ...elements, ...inlineElements }
|
|
635
|
+
} else if (inlineCol.expand) {
|
|
636
|
+
const expandElements = resolveExpand(inlineCol)
|
|
637
|
+
elements = { ...elements, [name]: expandElements }
|
|
638
|
+
} else if (inlineCol.val) {
|
|
639
|
+
elements[name] = { ...getCdsTypeForVal(inlineCol.val) }
|
|
640
|
+
} else if (inlineCol.func) {
|
|
641
|
+
elements[name] = {}
|
|
642
|
+
} else {
|
|
643
|
+
elements[name] = inlineCol.$refLinks[inlineCol.$refLinks.length - 1].definition
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
})
|
|
647
|
+
return elements
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* Resolves a query column which has an `expand` property.
|
|
652
|
+
*
|
|
653
|
+
* @param {Object} col - The column object with properties: `expand` and `$refLinks`.
|
|
654
|
+
* @returns {Object} - A `cds.struct` object with expanded column definitions.
|
|
655
|
+
*
|
|
656
|
+
* Procedure:
|
|
657
|
+
* - if `$leafLink` is an association, constructs an `expandSubquery` and infers a new query structure.
|
|
658
|
+
* Returns a new `cds.struct` if the association has a target cardinality === 1 or a `cds.array` for to many relations.
|
|
659
|
+
* - else constructs an `elements` object based on the refs `expand` found in the expand and returns a new `cds.struct` with these `elements`.
|
|
660
|
+
*/
|
|
661
|
+
function resolveExpand(col) {
|
|
662
|
+
const { expand, $refLinks } = col
|
|
663
|
+
const $leafLink = $refLinks?.[$refLinks.length - 1]
|
|
664
|
+
if ($leafLink?.definition._target) {
|
|
665
|
+
const expandSubquery = {
|
|
666
|
+
SELECT: {
|
|
667
|
+
from: $leafLink.definition._target.name,
|
|
668
|
+
columns: expand.filter(c => !c.inline),
|
|
669
|
+
},
|
|
670
|
+
}
|
|
671
|
+
if (col.as) expandSubquery.SELECT.as = col.as
|
|
672
|
+
const inferredExpandSubquery = infer(expandSubquery, model)
|
|
673
|
+
const res = $leafLink.definition.is2one
|
|
674
|
+
? new cds.struct({ elements: inferredExpandSubquery.elements })
|
|
675
|
+
: new cds.array({ items: new cds.struct({ elements: inferredExpandSubquery.elements }) })
|
|
676
|
+
return Object.defineProperty(res, '$assocExpand', { value: true })
|
|
677
|
+
} // struct
|
|
678
|
+
let elements = {}
|
|
679
|
+
expand.forEach(e => {
|
|
680
|
+
if (e === '*') {
|
|
681
|
+
elements = { ...elements, ...$leafLink.definition.elements }
|
|
682
|
+
} else {
|
|
683
|
+
inferQueryElement(e, false, $leafLink, { inExpr: true, inNestedProjection: true })
|
|
684
|
+
if (e.expand) elements[e.as || e.flatName] = resolveExpand(e)
|
|
685
|
+
if (e.inline) elements = { ...elements, ...resolveInline(e) }
|
|
686
|
+
else elements[e.as || e.flatName] = e.$refLinks ? e.$refLinks[e.$refLinks.length - 1].definition : e
|
|
687
|
+
}
|
|
688
|
+
})
|
|
689
|
+
return new cds.struct({ elements })
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
function stepNotFoundInPredecessor(step, def) {
|
|
693
|
+
throw new Error(`"${step}" not found in "${def}"`)
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function stepIsAmbiguous(step) {
|
|
697
|
+
throw new Error(
|
|
698
|
+
`ambiguous reference to "${step}", write ${Object.values($combinedElements[step])
|
|
699
|
+
.map(ta => `"${ta.index}.${step}"`)
|
|
700
|
+
.join(', ')} instead`,
|
|
701
|
+
)
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
function stepNotFoundInCombinedElements(step) {
|
|
705
|
+
throw new Error(
|
|
706
|
+
`"${step}" not found in the elements of ${Object.values(sources)
|
|
707
|
+
.map(def => `"${def.name || /* subquery */ def.as}"`)
|
|
708
|
+
.join(', ')}`,
|
|
709
|
+
)
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
function stepNotFoundInColumnList(step) {
|
|
713
|
+
const err = [`"${step}" not found in the columns list of query`]
|
|
714
|
+
// if the `elt` from a `$self.elt` path is found in the `$combinedElements` -> hint to remove `$self`
|
|
715
|
+
if (step in $combinedElements)
|
|
716
|
+
err.push(` did you mean ${$combinedElements[step].map(ta => `"${ta.index || ta.as}.${step}"`).join(',')}?`)
|
|
717
|
+
throw new Error(err)
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
/**
|
|
722
|
+
* Checks whether or not the `ref` of the given column is join relevant.
|
|
723
|
+
* A `ref` is considered join relevant if it includes an association traversal and:
|
|
724
|
+
* - the association is unmanaged
|
|
725
|
+
* - a non-foreign key access is performed
|
|
726
|
+
* - an infix filter is applied at the association
|
|
727
|
+
*
|
|
728
|
+
* @param {object} column the column with the `ref` to check for join relevance
|
|
729
|
+
* @returns {boolean} true if the column ref needs to be merged into a join tree
|
|
730
|
+
*/
|
|
731
|
+
function isColumnJoinRelevant(column) {
|
|
732
|
+
let fkAccess = false
|
|
733
|
+
let assoc = null
|
|
734
|
+
for (let i = 0; i < column.ref.length; i++) {
|
|
735
|
+
const ref = column.ref[i]
|
|
736
|
+
const link = column.$refLinks[i]
|
|
737
|
+
if (link.definition.on && link.definition.isAssociation) {
|
|
738
|
+
if (!column.ref[i + 1]) {
|
|
739
|
+
if (column.expand && assoc) return true
|
|
740
|
+
// if unmanaged assoc is exposed, ignore it
|
|
741
|
+
return false
|
|
742
|
+
}
|
|
743
|
+
return true
|
|
744
|
+
}
|
|
745
|
+
if (assoc && assoc.keys?.some(key => key.ref.every((step, j) => column.ref[i + j] === step))) {
|
|
746
|
+
// foreign key access without filters never join relevant
|
|
747
|
+
return false
|
|
748
|
+
}
|
|
749
|
+
if (link.definition.target && link.definition.keys) {
|
|
750
|
+
if (column.ref[i + 1] || assoc) fkAccess = false
|
|
751
|
+
else fkAccess = true
|
|
752
|
+
assoc = link.definition
|
|
753
|
+
if (ref.where) {
|
|
754
|
+
// always join relevant except for expand assoc
|
|
755
|
+
if (column.expand && !column.ref[i + 1]) return false
|
|
756
|
+
return true
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
if (!assoc) return false
|
|
762
|
+
if (fkAccess) return false
|
|
763
|
+
else return true
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
/**
|
|
767
|
+
* Iterates over all `$combinedElements` of the `query` and puts them into the `query`s `elements`,
|
|
768
|
+
* if there is not already an element with the same name present.
|
|
769
|
+
*/
|
|
770
|
+
function inferElementsFromWildCard() {
|
|
771
|
+
if (Object.keys(queryElements).length === 0 && aliases.length === 1) {
|
|
772
|
+
// only one query source and no overwritten columns
|
|
773
|
+
Object.entries(sources[aliases[0]].elements).forEach(([name, element]) => {
|
|
774
|
+
if (element.type !== 'cds.LargeBinary') queryElements[name] = element
|
|
775
|
+
})
|
|
776
|
+
return
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
const exclude = _.excluding ? x => _.excluding.includes(x) : () => false
|
|
780
|
+
const ambiguousElements = {}
|
|
781
|
+
Object.entries($combinedElements).forEach(([name, tableAliases]) => {
|
|
782
|
+
if (Object.keys(tableAliases).length > 1) {
|
|
783
|
+
ambiguousElements[name] = tableAliases
|
|
784
|
+
return ambiguousElements[name]
|
|
785
|
+
}
|
|
786
|
+
if (exclude(name) || name in queryElements) return true
|
|
787
|
+
const element = tableAliases[0].tableAlias.elements[name]
|
|
788
|
+
if (element.type !== 'cds.LargeBinary') queryElements[name] = element
|
|
789
|
+
})
|
|
790
|
+
|
|
791
|
+
if (Object.keys(ambiguousElements).length > 0) throwAmbiguousWildcardError()
|
|
792
|
+
|
|
793
|
+
function throwAmbiguousWildcardError() {
|
|
794
|
+
const err = []
|
|
795
|
+
err.push('Ambiguous wildcard elements:')
|
|
796
|
+
Object.keys(ambiguousElements).forEach(name => {
|
|
797
|
+
const tableAliasNames = Object.values(ambiguousElements[name]).map(v => v.index)
|
|
798
|
+
err.push(
|
|
799
|
+
` select "${name}" explicitly with ${tableAliasNames
|
|
800
|
+
.map(taName => `"${taName}.${name}"`)
|
|
801
|
+
.join(', ')}`,
|
|
802
|
+
)
|
|
803
|
+
})
|
|
804
|
+
throw new Error(err.join('\n'))
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
/**
|
|
809
|
+
* Returns a new object which is the inferred element for the given `col`.
|
|
810
|
+
* A cast type (via cast function) on the column gets preserved.
|
|
811
|
+
*
|
|
812
|
+
* @param {object} col
|
|
813
|
+
* @returns object
|
|
814
|
+
*/
|
|
815
|
+
function getElementForXprOrSubquery(col) {
|
|
816
|
+
const { xpr } = col
|
|
817
|
+
let skipJoins = false
|
|
818
|
+
xpr?.forEach(token => {
|
|
819
|
+
if (token === 'exists') {
|
|
820
|
+
// no joins for infix filters along `exists <path>`
|
|
821
|
+
skipJoins = true
|
|
822
|
+
} else {
|
|
823
|
+
inferQueryElement(token, false, null, { inExists: skipJoins, inExpr: true })
|
|
824
|
+
skipJoins = false
|
|
825
|
+
}
|
|
826
|
+
})
|
|
827
|
+
const base = getElementForCast(col.cast ? col : xpr?.[0] || col)
|
|
828
|
+
if (col.key) base.key = col.key // > preserve key on column
|
|
829
|
+
return getCopyWithAnnos(col, base)
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
/**
|
|
833
|
+
* Returns an object with the cast-type defined in the cast of the `thing`.
|
|
834
|
+
* If no cast property is present, it just returns an empty object.
|
|
835
|
+
* The type of the cast is mapped to the `cds` type if possible.
|
|
836
|
+
*
|
|
837
|
+
* @param {object} thing with the cast property
|
|
838
|
+
* @returns {object}
|
|
839
|
+
*/
|
|
840
|
+
function getElementForCast(thing) {
|
|
841
|
+
const { cast, $refLinks } = thing
|
|
842
|
+
if (!cast) return {}
|
|
843
|
+
if ($refLinks?.[$refLinks.length - 1].definition.elements)
|
|
844
|
+
// no cast on structure
|
|
845
|
+
cds.error`Structured elements can't be cast to a different type`
|
|
846
|
+
thing.cast = cdsTypes[cast.type] || cast
|
|
847
|
+
return thing.cast
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
/**
|
|
852
|
+
* return a new object based on @param base
|
|
853
|
+
* with all annotations found in @param from
|
|
854
|
+
*
|
|
855
|
+
* @param {object} from
|
|
856
|
+
* @param {object} base
|
|
857
|
+
* @returns {object} a copy of @param base with all annotations of @param from
|
|
858
|
+
* @TODO prototype based
|
|
859
|
+
*/
|
|
860
|
+
// REVISIT: TODO: inferred.elements should be linked
|
|
861
|
+
function getCopyWithAnnos(from, base) {
|
|
862
|
+
const result = { ...base }
|
|
863
|
+
// REVISIT: we don't need to and hence should not handle annotations at runtime
|
|
864
|
+
for (const prop in from) {
|
|
865
|
+
if (prop.startsWith('@')) result[prop] = from[prop]
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
if (from.as && base.name !== from.as) Object.defineProperty(result, 'name', { value: from.as }) // TODO double check if this is needed
|
|
869
|
+
// in subqueries we need the linked element if an outer query accesses it
|
|
870
|
+
return Object.setPrototypeOf(result, base)
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
// REVISIT: functions without return are by nature side-effect functions -> bad
|
|
874
|
+
function init$refLinks(arg) {
|
|
875
|
+
Object.defineProperty(arg, '$refLinks', {
|
|
876
|
+
value: [],
|
|
877
|
+
writable: true,
|
|
878
|
+
})
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
function getCdsTypeForVal(val) {
|
|
882
|
+
// REVISIT: JS null should have a type for proper DB layer conversion logic
|
|
883
|
+
// if(val === null) return {type:'cds.String'}
|
|
884
|
+
switch (typeof val) {
|
|
885
|
+
case 'string':
|
|
886
|
+
return cdsTypes.String
|
|
887
|
+
case 'boolean':
|
|
888
|
+
return cdsTypes.Boolean
|
|
889
|
+
case 'number':
|
|
890
|
+
return Number.isSafeInteger(val) ? cdsTypes.Integer : cdsTypes.Decimal
|
|
891
|
+
default:
|
|
892
|
+
return {}
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
/** gets the CSN element for the given name from the model */
|
|
897
|
+
function getDefinition(name, model) {
|
|
898
|
+
return model.definitions[name] || cds.error`"${name}" not found in the definitions of your model`
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
/**
|
|
902
|
+
* Returns the csn path as string for a given column ref with sibling $refLinks
|
|
903
|
+
*
|
|
904
|
+
* @param {object} arg
|
|
905
|
+
* @returns {string}
|
|
906
|
+
*/
|
|
907
|
+
function getFullPathForLinkedArg(arg) {
|
|
908
|
+
let firstStepIsEntity = false
|
|
909
|
+
return arg.$refLinks.reduce((res, cur, i) => {
|
|
910
|
+
if (cur.definition.kind === 'entity') {
|
|
911
|
+
firstStepIsEntity = true
|
|
912
|
+
if (arg.$refLinks.length === 1) return `${cur.definition.name}`
|
|
913
|
+
return `${cur.definition.name}`
|
|
914
|
+
}
|
|
915
|
+
const dot = i === 1 && firstStepIsEntity ? ':' : '.' // divide with colon if first step is entity
|
|
916
|
+
return res !== '' ? res + dot + cur.definition.name : cur.definition.name
|
|
917
|
+
}, '')
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
const idOnly = ref => ref.id || ref
|
|
921
|
+
|
|
922
|
+
module.exports = infer
|