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