@cap-js/sqlite 0.1.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,786 +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} expand whether the `arg` is part of a `column.expand`
133
- */
134
- function attachRefLinksToArg(arg, $baseLink = null, expand = false) {
135
- const { ref, xpr } = arg;
136
- if(xpr)
137
- xpr.forEach(t => attachRefLinksToArg(t, $baseLink, expand))
138
- if (!ref)
139
- return;
140
- init$refLinks(arg);
141
- ref.forEach((step, i) => {
142
- const id = step.id || step;
143
- if (i === 0) {
144
- // infix filter never have table alias
145
- // we need to search for first step in ´model.definitions[infixAlias]`
146
- if ($baseLink) {
147
- const { definition } = $baseLink;
148
- const elements = definition.elements || definition._target?.elements;
149
- const e = elements?.[id] || cds.error`"${ id }" not found in the elements of "${ definition.name }"`;
150
- if (e.target) { // only fk access in infix filter
151
- const nextStep = ref[1]?.id || ref[1];
152
- // no unmanaged assoc in infix filter path
153
- if (!expand && e.on)
154
- throw new Error(`"${ e.name }" in path "${ arg.ref.join('.') }" must not be an unmanaged association`);
155
- // no non-fk traversal in infix filter
156
- if (!expand && nextStep && !(nextStep in e.foreignKeys))
157
- throw new Error(`Only foreign keys of "${ e.name }" can be accessed in infix filter`);
158
- }
159
- arg.$refLinks.push({ definition: e, target: e._target || e });
160
- // filter paths are flattened
161
- // REVISIT: too much augmentation -> better remove flatName..
162
- Object.defineProperty(arg, 'flatName', { value: ref.join('_'), writable: true });
163
- }
164
- else {
165
- // must be in model.definitions
166
- const definition = getDefinition(id, model);
167
- arg.$refLinks[0] = { definition, target: definition };
168
- }
169
- }
170
- else {
171
- const recent = arg.$refLinks[i - 1];
172
- const { elements } = recent.target;
173
- const e = elements[id];
174
- if (!e)
175
- throw new Error(`"${ id }" not found in the elements of "${ arg.$refLinks[i - 1].definition.name }"`);
176
- arg.$refLinks.push({ definition: e, target: e._target || e });
177
- }
178
- arg.$refLinks[i].alias = !ref[i + 1] && arg.as ? arg.as : id.split('.').pop();
179
-
180
- // link refs in where
181
- if (step.where) { // REVISIT: why do we need to walk through these so early?
182
- if (arg.$refLinks[i].definition.kind === 'entity' || arg.$refLinks[i].definition._target)
183
- step.where.forEach(each => (each.ref || each.xpr) && attachRefLinksToArg(each, arg.$refLinks[i]));
184
- else
185
- throw new Error('A filter can only be provided when navigating along associations');
186
- }
187
- });
188
- }
189
-
190
- /**
191
- * Based on the queries `sources`, the `$combinedElements` are calculated.
192
- * The `$combinedElements` of a query consist of all accessible elements
193
- * across all the table aliases found in the from clause.
194
- *
195
- * The `$combinedElements` are attached to the query as non-enumerable property.
196
- * Each entry in the `$combinedElements` dictionary maps from the element name
197
- * to all table aliases where an element with this name can be found.
198
- */
199
- function inferCombinedElements() {
200
- const combinedElements = {};
201
- for (const index in sources) {
202
- const tableAlias = sources[index];
203
- for (const key in tableAlias.elements) {
204
- if (key in combinedElements)
205
- combinedElements[key].push({ index, tableAlias });
206
- else
207
- combinedElements[key] = [ { index, tableAlias } ];
208
- }
209
- }
210
- return combinedElements;
211
- }
212
-
213
- /**
214
- * Assigns the given `element` as non-enumerable property 'element' onto `col`.
215
- *
216
- * @param {object} col
217
- * @param {csn.Element} element
218
- */
219
- function setElementOnColumns(col, element) {
220
- Object.defineProperty(col, 'element', {
221
- value: element,
222
- writable: true
223
- });
224
- }
225
-
226
- /**
227
- * Walks over all columns of a queries `SELECT` and infers each `ref`, `xpr`
228
- * or `val` as query element based on the queries `$combinedElements` and
229
- * `sources`.
230
- *
231
- * The `elements` are attached to the query as non-enumerable property.
232
- *
233
- * Also walks over other `ref`s in the query, validates them and attaches `$refLinks`.
234
- * --> `where`, infix filters within column refs or other csn paths...
235
- *
236
- */
237
- function inferQueryElements($combinedElements) {
238
- let queryElements = {};
239
- const {
240
- columns, where, groupBy, having, orderBy,
241
- } = _;
242
- if (!columns) {
243
- inferElementsFromWildCard(aliases);
244
- }
245
- else {
246
- let wildcardSelect = false;
247
- const refs = [];
248
- columns.forEach((col) => {
249
- if (col === '*') {
250
- wildcardSelect = true;
251
- } else if (col.val !== undefined || col.xpr || col.SELECT || col.func || col.param) {
252
- const as = col.as || col.func || col.val;
253
- if (as === undefined)
254
- throw cds.error`Expecting expression to have an alias name`;
255
- if (queryElements[as])
256
- throw cds.error`Duplicate definition of element “${ as }”`;
257
- if (col.xpr || col.SELECT) {
258
- queryElements[as] = getElementForXprOrSubquery(col);
259
- } else if (col.func) {
260
- col.args?.forEach(arg => inferQueryElement(arg, false)); // {func}.args are optional
261
- queryElements[as] = getElementForCast(col);
262
- } else { // either binding parameter (col.param) or value
263
- queryElements[as] = col.cast ? getElementForCast(col) : getCdsTypeForVal(col.val);
264
- }
265
- setElementOnColumns(col, queryElements[as])
266
- } else if (col.ref) {
267
- refs.push(col);
268
- } else {
269
- throw cds.error`Not supported: ${ JSON.stringify(col) }`;
270
- }
271
- });
272
- refs.forEach(col => {
273
- inferQueryElement(col)
274
- const {definition} = col.$refLinks[col.$refLinks.length-1]
275
- if(col.cast) // final type overwritten -> element not visible anymore
276
- setElementOnColumns(col, getElementForCast(col))
277
- else if(col.ref.length === 1 & col.ref[0] === '$user') // shortcut to $user.id
278
- setElementOnColumns(col, queryElements[col.as || '$user'])
279
- else
280
- setElementOnColumns(col, definition)
281
- });
282
- if (wildcardSelect)
283
- inferElementsFromWildCard(aliases);
284
- }
285
- if (orderBy) { // link $refLinks -> special name resolution rules for orderBy
286
- orderBy.forEach((token) => {
287
- let $baseLink;
288
- // first check if token ref is resolvable in query elements
289
- if (columns) {
290
- const e = queryElements[token.ref?.[0]];
291
- const isAssocExpand = e?.$assocExpand; // expand on structure can be addressed
292
- if (e && !isAssocExpand)
293
- $baseLink = { definition: { elements: queryElements }, target: inferred };
294
- }
295
- else { // fallback to elements of query source
296
- $baseLink = null;
297
- }
298
-
299
- inferQueryElement(token, false, $baseLink);
300
- });
301
- }
302
- if (where) {
303
- let skipJoins;
304
- const walkTokenStream = (token) => {
305
- if (token === 'exists') { // no joins for infix filters along `exists <path>`
306
- skipJoins = true;
307
- } else if(token.xpr) {
308
- // don't miss an exists within an expression
309
- token.xpr.forEach(walkTokenStream)
310
- }
311
- else {
312
- inferQueryElement(token, false, null, skipJoins);
313
- skipJoins = false;
314
- }
315
- }
316
- where.forEach(walkTokenStream);
317
-
318
- }
319
- if (groupBy) // link $refLinks
320
- groupBy.forEach(token => inferQueryElement(token, false));
321
- if (having) // link $refLinks
322
- having.forEach(token => inferQueryElement(token, false));
323
- if(_.with) // consider UPDATE.with
324
- Object.values(_.with).forEach(val => inferQueryElement(val, false))
325
-
326
- return queryElements;
327
-
328
- /**
329
- * Infers an element of the query based on the given `column`
330
- *
331
- * attaches non-enumerable property `$refLinks` to the `column`
332
- * which holds the corresponding artifact represented by the ref step
333
- * at the same index. Based on the leaf artifact of the `ref` path, the queryElement
334
- * is inferred.
335
- *
336
- * @param {object} column
337
- * @param {object} [insertIntoQueryElements=true]
338
- * whether the inferred element shall be inserted into the queries elements.
339
- * E.g. we do not want to do that when we walk over the where clause.
340
- * @param {boolean} [inExists=false]
341
- * In some cases, no joins must be created for non-assoc path traversals:
342
- * - for infix filters in `exists assoc[parent.foo='bar']` -> part of semi join
343
- */
344
- function inferQueryElement(column, insertIntoQueryElements = true, $baseLink = null, inExists = false, inInline = false) {
345
- if (column.param)
346
- return; // parameter references are only resolved into values on execution e.g. :val, :1 or ?
347
- if (column.args)
348
- column.args.forEach( arg => inferQueryElement(arg, false, $baseLink, inExists) ); // e.g. function in expression
349
- if (column.list)
350
- column.list.forEach(arg => inferQueryElement(arg, false, $baseLink, inExists));
351
- if (column.xpr)
352
- column.xpr.forEach(token => inferQueryElement(token, false, $baseLink, inExists)); // e.g. function in expression
353
- if (column.SELECT)
354
- return
355
-
356
- if (!column.ref)
357
- return;
358
-
359
- init$refLinks(column);
360
-
361
- const firstStepIsTableAlias = column.ref.length > 1 && column.ref[0] in sources ||
362
- // nested projection on table alias
363
- column.ref.length === 1 && column.ref[0] in sources && (column.inline);
364
- const firstStepIsSelf
365
- = !firstStepIsTableAlias && column.ref.length > 1 && [ '$self', '$projection' ].includes(column.ref[0]);
366
- const nameSegments = [];
367
- let pseudoPath = false;
368
- column.ref.forEach((step, i) => {
369
- const id = step.id || step;
370
- if (i === 0) {
371
- if (id in pseudos.elements) { // pseudo path
372
- column.$refLinks.push({ definition: pseudos.elements[id], target: pseudos });
373
- pseudoPath = true; // only first path step must be well defined
374
- nameSegments.push(id);
375
- }
376
- else if ($baseLink) {
377
- const { definition, target } = $baseLink;
378
- const elements = definition.elements || definition._target?.elements;
379
- if (elements && id in elements) {
380
- if (!inExists && isStepJoinRelevant(definition, id, inInline ? null : $baseLink))
381
- Object.defineProperty(column, 'isJoinRelevant', { value: true });
382
- column.$refLinks.push({ definition: elements[id], target });
383
- }
384
- else {
385
- stepNotFoundInPredecessor(id, definition.name);
386
- }
387
- nameSegments.push(id);
388
- }
389
- else if (firstStepIsTableAlias) {
390
- column.$refLinks.push({ definition: sources[id], target: sources[id] });
391
- }
392
- else if (firstStepIsSelf) {
393
- column.$refLinks.push({ definition: { elements: queryElements }, target: { elements: queryElements } });
394
- }
395
- else if (id in $combinedElements) {
396
- if ($combinedElements[id].length > 1)
397
- stepIsAmbiguous(id); // exit
398
- const definition = $combinedElements[id][0].tableAlias.elements[id];
399
- const $refLink = { definition, target: $combinedElements[id][0].tableAlias };
400
- column.$refLinks.push($refLink);
401
- nameSegments.push(id);
402
- }
403
- else if (inferred.outerQueries) { // outer query accessed via alias
404
- const outerAlias = inferred.outerQueries.find(outer => id in outer.sources);
405
- if (!outerAlias)
406
- stepNotFoundInCombinedElements(id);
407
- column.$refLinks.push({ definition: outerAlias.sources[id], target: outerAlias.sources[id] });
408
- }
409
- else {
410
- stepNotFoundInCombinedElements(id); // REVISIT: fails with {__proto__:elements)
411
- }
412
- }
413
- else {
414
- const { definition } = column.$refLinks[i - 1];
415
- const elements = definition.elements || definition._target?.elements;
416
- if (elements && id in elements) {
417
- const $refLink = { definition: elements[id], target: column.$refLinks[i - 1].target };
418
- if (!inExists && isStepJoinRelevant(definition, id))
419
- Object.defineProperty(column, 'isJoinRelevant', { value: true });
420
- column.$refLinks.push($refLink);
421
- }
422
- else if (firstStepIsSelf) {
423
- stepNotFoundInColumnList(id);
424
- }
425
- else if (column.ref[0] === '$user' && pseudoPath) {
426
- // `$user.some.unknown.element` -> no error
427
- column.$refLinks.push({ definition: {}, target: column.$refLinks[i - 1].target });
428
- }
429
- else if (id === '$dummy') {
430
- // `some.known.element.$dummy` -> no error; used by cds.ql to simulate joins
431
- column.$refLinks.push({ definition: { name: '$dummy', parent: column.$refLinks[i - 1].target } });
432
- Object.defineProperty(column, 'isJoinRelevant', { value: true });
433
- }
434
- else {
435
- const notFoundIn = pseudoPath ? column.ref[i - 1] : getFullPathForLinkedArg(column);
436
- stepNotFoundInPredecessor(id, notFoundIn);
437
- }
438
- const foreignKeyAlias = Array.isArray(definition.keys)
439
- ? definition.keys.find(k => k.ref[0] === id )?.as : null;
440
- nameSegments.push(foreignKeyAlias || id);
441
- }
442
-
443
- if (step.where) {
444
- const danglingFilter = !(column.ref[i + 1] || column.expand || inExists);
445
- if (!column.$refLinks[i].definition.target || danglingFilter)
446
- throw new Error(/A filter can only be provided when navigating along associations/);
447
- if (!column.expand)
448
- Object.defineProperty(column, 'isJoinRelevant', { value: true });
449
- // books[exists genre[code='A']].title --> column is join relevant but inner exists filter is not
450
- let skipJoinsForFilter = inExists;
451
- step.where.forEach( (token) => {
452
- if (token === 'exists') { // no joins for infix filters along `exists <path>`
453
- skipJoinsForFilter = true;
454
- }
455
- else if (token.ref || token.xpr) {
456
- inferQueryElement(token, false, column.$refLinks[i], skipJoinsForFilter);
457
- }
458
- });
459
- }
460
-
461
- column.$refLinks[i].alias = !column.ref[i + 1] && column.as ? column.as : id.split('.').pop();
462
-
463
- if (!column.ref[i + 1]) {
464
- const flatName = nameSegments.join('_');
465
- Object.defineProperty(column, 'flatName', { value: flatName, writable: true });
466
- // if column is casted, we overwrite it's origin with the new type
467
- if (column.cast) {
468
- const base = getElementForCast(column);
469
- if (insertIntoQueryElements)
470
- queryElements[column.as || flatName] = getCopyWithAnnos(column, base);
471
- }
472
- else if (column.expand) {
473
- const elements = resolveExpand(column);
474
- if (insertIntoQueryElements)
475
- queryElements[column.as || flatName] = elements;
476
- }
477
- else if (column.inline && insertIntoQueryElements) {
478
- const elements = resolveInline(column);
479
- queryElements = { ...queryElements, ...elements };
480
- }
481
- else {
482
- // shortcut for `ref: ['$user']` -> `ref: ['$user', 'id']`
483
- const leafArt
484
- = i === 0 && id === '$user' ? column.$refLinks[i].definition.elements.id : column.$refLinks[i].definition;
485
- // infer element based on leaf artifact of path
486
- if (insertIntoQueryElements) {
487
- const name = column.as || flatName;
488
- if (queryElements[name] !== undefined)
489
- throw new Error(`Duplicate definition of element “${ name }”`);
490
- queryElements[name] = getCopyWithAnnos(column, leafArt);
491
- }
492
- }
493
- }
494
- });
495
-
496
- // ignore whole expand if target of assoc along path has ”@cds.persistence.skip”
497
- if (column.expand) {
498
- const { $refLinks } = column;
499
- const skip = $refLinks
500
- .some(link => model.definitions[link.definition.target]?.['@cds.persistence.skip'] === true);
501
- if (skip) {
502
- $refLinks[$refLinks.length - 1].skipExpand = true;
503
- return;
504
- }
505
- }
506
-
507
- if (!inExists && column.isJoinRelevant)
508
- joinTree.mergeColumn(column);
509
-
510
-
511
- function resolveInline(col, namePrefix = col.as || col.flatName) {
512
- const { inline, $refLinks } = col;
513
- const $leafLink = $refLinks[$refLinks.length - 1];
514
- let elements = {};
515
- inline.forEach((inlineCol) => {
516
- inferQueryElement(inlineCol, false, $leafLink, false, true);
517
- if (inlineCol === '*') {
518
- const wildCardElements = {};
519
- // either the `.elements´ of the struct or the `.elements` of the assoc target
520
- const leafLinkElements = $leafLink.definition.elements || $leafLink.definition._target.elements
521
- Object.entries(leafLinkElements).forEach(([ k, v ]) => {
522
- const name = namePrefix ? `${ namePrefix }_${ k }` : k;
523
- // if overwritten/excluded omit from wildcard elements
524
- // in elements the names are already flat so consider the prefix
525
- // in excluding, the elements are addressed without the prefix
526
- if (!(name in elements || col.excluding?.some(e => e === k)))
527
- wildCardElements[name] = v;
528
- });
529
- elements = { ...elements, ...wildCardElements };
530
- }
531
- else {
532
- const nameParts = namePrefix ? [namePrefix] : []
533
- if(inlineCol.as)
534
- nameParts.push(inlineCol.as)
535
- else
536
- nameParts.push(...inlineCol.ref.map(idOnly))
537
- const name = nameParts.join('_')
538
- if (inlineCol.inline) {
539
- const inlineElements = resolveInline(inlineCol, name);
540
- elements = { ...elements, ...inlineElements };
541
- }
542
- else if (inlineCol.expand) {
543
- const expandElements = resolveExpand(inlineCol);
544
- elements = { ...elements, [name]: expandElements };
545
- }
546
- else if (inlineCol.val) {
547
- elements[name] = {...getCdsTypeForVal(inlineCol.val)}
548
- }
549
- else if (inlineCol.func) {
550
- elements[name] = {}
551
- }
552
- else {
553
- elements[name] = inlineCol.$refLinks[inlineCol.$refLinks.length - 1].definition;
554
- }
555
- }
556
- });
557
- return elements;
558
- }
559
- function resolveExpand(col) {
560
- const { expand, $refLinks } = col;
561
- const $leafLink = $refLinks[$refLinks.length - 1];
562
- if ($leafLink.definition._target) {
563
- const expandSubquery = {
564
- SELECT: {
565
- from: $leafLink.definition._target.name,
566
- columns: expand.filter(c => !c.inline),
567
- },
568
- };
569
- if (col.as)
570
- expandSubquery.SELECT.as = col.as;
571
- const inferredExpandSubquery = infer(expandSubquery, model);
572
- const res = $leafLink.definition._isStructured || $leafLink.definition.is2one
573
- // IMPORTANT: all definitions / elements in a cds.linked model have to be linked
574
- ? new cds.struct ({ elements: inferredExpandSubquery.elements })
575
- : new cds.array ({ items: new cds.struct({ elements: inferredExpandSubquery.elements }) });
576
- return Object.defineProperty(res, '$assocExpand', { value: true });
577
- } // struct
578
- let elements = {};
579
- expand.forEach((e) => {
580
- if (e === '*') {
581
- elements = { ...elements, ...$leafLink.definition.elements };
582
- }
583
- else {
584
- inferQueryElement(e, false, $leafLink);
585
- if (e.expand)
586
- elements[e.as || e.flatName] = resolveExpand(e);
587
- if (e.inline)
588
- elements = { ...elements, ...resolveInline(e) };
589
-
590
- else
591
- elements[e.as || e.flatName] = e.$refLinks ? e.$refLinks[e.$refLinks.length - 1].definition : e;
592
- }
593
- });
594
- return new cds.struct({ elements });
595
- }
596
-
597
- function stepNotFoundInPredecessor(step, def) {
598
- throw new Error(`"${ step }" not found in "${ def }"`);
599
- }
600
-
601
- function stepIsAmbiguous(step) {
602
- throw new Error(
603
- `ambiguous reference to "${ step }", write ${ Object.values($combinedElements[step])
604
- .map(ta => `"${ ta.index }.${ step }"`)
605
- .join(', ') } instead`
606
- );
607
- }
608
-
609
- function stepNotFoundInCombinedElements(step) {
610
- throw new Error(
611
- `"${ step }" not found in the elements of ${ Object.values(sources)
612
- .map(def => `"${ def.name || /* subquery */ def.as }"`)
613
- .join(', ') }`
614
- );
615
- }
616
-
617
- function stepNotFoundInColumnList(step) {
618
- const err = [ `"${ step }" not found in the columns list of query` ];
619
- // if the `elt` from a `$self.elt` path is found in the `$combinedElements` -> hint to remove `$self`
620
- if (step in $combinedElements)
621
- err.push(` did you mean ${ $combinedElements[step].map(ta => `"${ ta.index || ta.as }.${ step }"`).join(',') }?`);
622
- throw new Error(err);
623
- }
624
- }
625
-
626
- function isStepJoinRelevant(definition, id, inInfixFilter = null) {
627
- return (definition.on || Array.isArray(definition.keys) &&
628
- /* infix filter in column always join relevant, even if fk access */
629
- (inInfixFilter || !definition.keys.some(fk => fk.as === id || fk.ref[fk.ref.length - 1] === id)));
630
- }
631
-
632
- /**
633
- * Iterates over all `$combinedElements` of the `query` and puts them into the `query`s `elements`,
634
- * if there is not already an element with the same name present.
635
- */
636
- function inferElementsFromWildCard() {
637
- if (Object.keys(queryElements).length === 0 && aliases.length === 1) {
638
- // only one query source and no overwritten columns
639
- queryElements = sources[aliases[0]].elements;
640
- return;
641
- }
642
-
643
- const exclude = _.excluding ? x => _.excluding.includes(x) : () => false;
644
- const ambiguousElements = {};
645
- Object.entries($combinedElements).forEach(([ name, tableAliases ]) => {
646
- if (Object.keys(tableAliases).length > 1) {
647
- ambiguousElements[name] = tableAliases;
648
- return ambiguousElements[name];
649
- }
650
- if (exclude(name) || name in queryElements)
651
- return true;
652
- queryElements[name] = tableAliases[0].tableAlias.elements[name];
653
- return queryElements[name];
654
- });
655
-
656
- if (Object.keys(ambiguousElements).length > 0)
657
- throwAmbiguousWildcardError();
658
-
659
- function throwAmbiguousWildcardError() {
660
- const err = [];
661
- err.push('Ambiguous wildcard elements:');
662
- Object.keys(ambiguousElements).forEach((name) => {
663
- const tableAliasNames = Object.values(ambiguousElements[name]).map(v => v.index);
664
- err.push(
665
- ` select "${ name }" explicitly with ${ tableAliasNames.map(taName => `"${ taName }.${ name }"`).join(', ') }`
666
- );
667
- });
668
- throw new Error(err.join('\n'));
669
- }
670
- }
671
-
672
- /**
673
- * Returns a new object which is the inferred element for the given `col`.
674
- * A cast type (via cast function) on the column gets preserved.
675
- *
676
- * @param {object} col
677
- * @returns object
678
- */
679
- function getElementForXprOrSubquery(col) {
680
- const { xpr } = col;
681
- let skipJoins = false;
682
- xpr?.forEach( (token) => {
683
- if (token === 'exists') { // no joins for infix filters along `exists <path>`
684
- skipJoins = true;
685
- }
686
- else {
687
- inferQueryElement(token, false, null, skipJoins);
688
- skipJoins = false;
689
- }
690
- });
691
- const base = getElementForCast(col.cast ? col : xpr?.[0] || col);
692
- if (col.key)
693
- base.key = col.key; // > preserve key on column
694
- return getCopyWithAnnos(col, base);
695
- }
696
-
697
- /**
698
- * Returns an object with the cast-type defined in the cast of the `thing`.
699
- * If no cast property is present, it just returns an empty object.
700
- * The type of the cast is mapped to the `cds` type if possible.
701
- *
702
- * @param {object} thing with the cast property
703
- * @returns {object}
704
- */
705
- function getElementForCast(thing) {
706
- const { cast, $refLinks } = thing;
707
- if (!cast)
708
- return {};
709
- if($refLinks?.[$refLinks.length - 1].definition.elements) // no cast on structure
710
- cds.error`Structured elements can't be cast to a different type`;
711
- thing.cast = cdsTypes[cast.type] || cast;
712
- return thing.cast;
713
- }
714
- }
715
-
716
- /**
717
- * return a new object based on @param base
718
- * with all annotations found in @param from
719
- *
720
- * @param {object} from
721
- * @param {object} base
722
- * @returns {object} a copy of @param base with all annotations of @param from
723
- * @TODO prototype based
724
- */
725
- // REVISIT: TODO: inferred.elements should be linked
726
- function getCopyWithAnnos(from, base) {
727
- const result = { ...base };
728
- // REVISIT: we don't need to and hence should not handle annotations at runtime
729
- for (const prop in from) {
730
- if (prop.startsWith('@'))
731
- result[prop] = from[prop];
732
- }
733
-
734
- if (from.as && base.name !== from.as)
735
- Object.defineProperty(result, 'name', { value: from.as }); // TODO double check if this is needed
736
- // in subqueries we need the linked element if an outer query accesses it
737
- return Object.setPrototypeOf(result, base);
738
- }
739
-
740
- // REVISIT: functions without return are by nature side-effect functions -> bad
741
- function init$refLinks(arg) {
742
- Object.defineProperty(arg, '$refLinks', {
743
- value: [],
744
- writable: true,
745
- });
746
- }
747
-
748
- function getCdsTypeForVal(val) {
749
- // REVISIT: JS null should have a type for proper DB layer conversion logic
750
- // if(val === null) return {type:'cds.String'}
751
- switch (typeof val) {
752
- case 'string': return cdsTypes.String;
753
- case 'boolean': return cdsTypes.Boolean;
754
- case 'number': return Number.isSafeInteger(val) ? cdsTypes.Integer : cdsTypes.Decimal;
755
- default: return {};
756
- }
757
- }
758
-
759
- /** gets the CSN element for the given name from the model */
760
- function getDefinition(name, model) {
761
- return model.definitions[name] || cds.error`"${ name }" not found in the definitions of your model`;
762
- }
763
-
764
- /**
765
- * Returns the csn path as string for a given column ref with sibling $refLinks
766
- *
767
- * @param {object} arg
768
- * @returns {string}
769
- */
770
- function getFullPathForLinkedArg(arg) {
771
- let firstStepIsEntity = false;
772
- return arg.$refLinks.reduce((res, cur, i) => {
773
- if (cur.definition.kind === 'entity') {
774
- firstStepIsEntity = true;
775
- if (arg.$refLinks.length === 1)
776
- return `${ cur.definition.name }`;
777
- return `${ cur.definition.name }`;
778
- }
779
- const dot = i === 1 && firstStepIsEntity ? ':' : '.'; // divide with colon if first step is entity
780
- return res !== '' ? res + dot + cur.definition.name : cur.definition.name;
781
- }, '');
782
- }
783
- }
784
- const idOnly = (ref) => ref.id || ref
785
-
786
- module.exports = infer;