@sap/cds-compiler 6.3.4 → 6.4.2
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 +54 -0
- package/LICENSE +32 -0
- package/README.md +14 -2
- package/bin/cdsse.js +0 -3
- package/doc/CHANGELOG_BETA.md +1 -1
- package/doc/CHANGELOG_DEPRECATED.md +1 -1
- package/lib/base/message-registry.js +7 -0
- package/lib/base/messages.js +1 -1
- package/lib/base/model.js +2 -0
- package/lib/compiler/assert-consistency.js +1 -0
- package/lib/compiler/checks.js +37 -26
- package/lib/compiler/define.js +1 -1
- package/lib/compiler/extend.js +39 -50
- package/lib/compiler/finalize-parse-cdl.js +1 -1
- package/lib/compiler/lsp-api.js +1 -1
- package/lib/compiler/populate.js +2 -2
- package/lib/compiler/propagator.js +29 -6
- package/lib/compiler/resolve.js +13 -3
- package/lib/compiler/shared.js +31 -25
- package/lib/compiler/tweak-assocs.js +86 -28
- package/lib/compiler/xpr-rewrite.js +70 -38
- package/lib/edm/annotations/edmJson.js +206 -37
- package/lib/edm/csn2edm.js +13 -0
- package/lib/edm/edmUtils.js +2 -2
- package/lib/gen/BaseParser.js +106 -72
- package/lib/gen/CdlGrammar.checksum +1 -1
- package/lib/gen/CdlParser.js +1500 -1509
- package/lib/json/to-csn.js +8 -5
- package/lib/language/genericAntlrParser.js +0 -0
- package/lib/main.js +19 -16
- package/lib/model/csnRefs.js +589 -521
- package/lib/model/csnUtils.js +26 -7
- package/lib/model/enrichCsn.js +1 -0
- package/lib/parsers/AstBuildingParser.js +72 -27
- package/lib/render/toCdl.js +2 -1
- package/lib/render/toHdbcds.js +6 -3
- package/lib/render/toSql.js +5 -0
- package/lib/transform/db/applyTransformations.js +1 -1
- package/lib/transform/db/assertUnique.js +4 -1
- package/lib/transform/db/cdsPersistence.js +17 -18
- package/lib/transform/db/expansion.js +179 -3
- package/lib/transform/db/flattening.js +16 -5
- package/lib/transform/db/rewriteCalculatedElements.js +79 -283
- package/lib/transform/effective/main.js +8 -1
- package/lib/transform/forOdata.js +1 -1
- package/lib/transform/forRelationalDB.js +21 -80
- package/lib/transform/localized.js +65 -110
- package/lib/transform/odata/foreignKeyRefsInXprAnnos.js +89 -63
- package/lib/transform/transformUtils.js +23 -21
- package/lib/transform/translateAssocsToJoins.js +7 -5
- package/lib/transform/tupleExpansion.js +16 -3
- package/package.json +1 -1
- package/doc/DeprecatedOptions_v2.md +0 -150
- package/doc/NameResolution.md +0 -837
- package/lib/transform/parseExpr.js +0 -415
|
@@ -156,14 +156,14 @@ function _addLocalizationViews(csn, options, config) {
|
|
|
156
156
|
|
|
157
157
|
/**
|
|
158
158
|
* Add a localized convenience view for the given artifact.
|
|
159
|
-
* Can either be an entity or view. `
|
|
159
|
+
* Can either be an entity or view. `localizedElements` are the elements which
|
|
160
160
|
* are needed for creating a horizontal convenience view, i.e. only required
|
|
161
161
|
* for entities.
|
|
162
162
|
*
|
|
163
163
|
* @param {string} artName
|
|
164
|
-
* @param {string[]} [
|
|
164
|
+
* @param {string[]} [localizedElements=[]]
|
|
165
165
|
*/
|
|
166
|
-
function addLocalizedView( artName,
|
|
166
|
+
function addLocalizedView( artName, localizedElements = [] ) {
|
|
167
167
|
const art = csn.definitions[artName];
|
|
168
168
|
const artPath = [ 'definitions', artName ];
|
|
169
169
|
const viewName = `localized.${ artName }`;
|
|
@@ -183,32 +183,32 @@ function _addLocalizationViews(csn, options, config) {
|
|
|
183
183
|
if (art.query || art.projection)
|
|
184
184
|
view = createLocalizedViewForView(art, viewName);
|
|
185
185
|
else
|
|
186
|
-
view = createLocalizedViewForEntity(art, artName, viewName,
|
|
186
|
+
view = createLocalizedViewForEntity(art, artName, viewName, localizedElements);
|
|
187
187
|
|
|
188
188
|
copyPersistenceAnnotations(view, art);
|
|
189
189
|
csn.definitions[viewName] = view;
|
|
190
190
|
}
|
|
191
191
|
|
|
192
192
|
/**
|
|
193
|
-
* Create a localized data view for the given entity `art` with `
|
|
193
|
+
* Create a localized data view for the given entity `art` with `localizedElements`.
|
|
194
194
|
* In JOIN mode the FROM query is rewritten to remove associations and the
|
|
195
195
|
* columns are expanded.
|
|
196
196
|
*
|
|
197
197
|
* @param {CSN.Definition} entity
|
|
198
198
|
* @param {string} entityName
|
|
199
199
|
* @param {string} viewName Name of the localized view.
|
|
200
|
-
* @param {string[]} [
|
|
200
|
+
* @param {string[]} [localizedElements]
|
|
201
201
|
* @returns {CSN.View}
|
|
202
202
|
*/
|
|
203
|
-
function createLocalizedViewForEntity( entity, entityName, viewName,
|
|
203
|
+
function createLocalizedViewForEntity( entity, entityName, viewName, localizedElements = [] ) {
|
|
204
204
|
// Only use joins if requested and text elements are provided.
|
|
205
|
-
const shouldUseJoin = useJoins && !!
|
|
205
|
+
const shouldUseJoin = useJoins && !!localizedElements.length;
|
|
206
206
|
const columns = [ ];
|
|
207
207
|
|
|
208
208
|
const convenienceView = {
|
|
209
209
|
'@odata.draft.enabled': false,
|
|
210
210
|
kind: 'entity',
|
|
211
|
-
query: {
|
|
211
|
+
query: {
|
|
212
212
|
SELECT: {
|
|
213
213
|
from: createFromClauseForEntity(),
|
|
214
214
|
columns,
|
|
@@ -222,20 +222,12 @@ function _addLocalizationViews(csn, options, config) {
|
|
|
222
222
|
|
|
223
223
|
if (shouldUseJoin)
|
|
224
224
|
// Expand elements; (variant 1)
|
|
225
|
-
columns.push( ...columnsForEntityWithExcludeList( entity, 'L_0',
|
|
225
|
+
columns.push( ...columnsForEntityWithExcludeList( entity, 'L_0', localizedElements ) );
|
|
226
226
|
else
|
|
227
227
|
columns.push( '*' ); // (variant 2)
|
|
228
228
|
|
|
229
|
-
for (const originalElement of
|
|
230
|
-
|
|
231
|
-
// Note: $key is used by forRelationalDB.js to indicate that this element was a key in the original,
|
|
232
|
-
// user's entity. Keys may have been changed by the backends (e.g. by `@cds.valid.key`)
|
|
233
|
-
if (!elem.key && !elem.$key)
|
|
234
|
-
columns.push( createColumnLocalizedElement( originalElement, shouldUseJoin ) );
|
|
235
|
-
else if (shouldUseJoin)
|
|
236
|
-
// In JOIN mode we also want to add keys.
|
|
237
|
-
columns.push( createColumnRef( [ 'L_0', originalElement ] ));
|
|
238
|
-
|
|
229
|
+
for (const originalElement of localizedElements) {
|
|
230
|
+
columns.push( createColumnLocalizedElement( originalElement, shouldUseJoin ) );
|
|
239
231
|
addCoreComputedIfNecessary(convenienceView.elements, originalElement);
|
|
240
232
|
}
|
|
241
233
|
|
|
@@ -254,22 +246,43 @@ function _addLocalizationViews(csn, options, config) {
|
|
|
254
246
|
on: [],
|
|
255
247
|
};
|
|
256
248
|
|
|
257
|
-
|
|
258
|
-
const elem = entity.elements[originalElement];
|
|
259
|
-
if (elem.key || elem.$key) {
|
|
260
|
-
from.on.push( createColumnRef( [ 'localized_1', originalElement ] ));
|
|
261
|
-
from.on.push( '=' );
|
|
262
|
-
from.on.push( createColumnRef( [ 'L_0', originalElement ] ));
|
|
263
|
-
from.on.push( 'and' );
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
from.on.push( createColumnRef( [ 'localized_1', 'locale' ] ) );
|
|
268
|
-
from.on.push( '=' );
|
|
269
|
-
from.on.push( createColumnRef( [ '$user', 'locale' ] ) );
|
|
249
|
+
from.on.push(...createJoinConditionFromLocaleElement());
|
|
270
250
|
|
|
271
251
|
return from;
|
|
272
252
|
}
|
|
253
|
+
|
|
254
|
+
function createJoinConditionFromLocaleElement() {
|
|
255
|
+
return adaptExpr(entity.elements.localized.on);
|
|
256
|
+
|
|
257
|
+
function adaptExpr(expr) {
|
|
258
|
+
// We only support a few specific ON-conditions, not generic expressions.
|
|
259
|
+
// In case of unsupported ON-conditions, we emit an error.
|
|
260
|
+
return expr.map((x) => {
|
|
261
|
+
if (!x || typeof x === 'string')
|
|
262
|
+
return x;
|
|
263
|
+
if (x.xpr)
|
|
264
|
+
return { xpr: adaptExpr(x.xpr) };
|
|
265
|
+
if (x.ref && !x.ref.some(ref => ref.args || ref.where))
|
|
266
|
+
return adaptRef(x);
|
|
267
|
+
|
|
268
|
+
messageFunctions.error(
|
|
269
|
+
'def-invalid-localized',
|
|
270
|
+
[ 'definitions', entityName, 'elements', 'localized', 'on' ],
|
|
271
|
+
{ name: 'localized', alias: entityName },
|
|
272
|
+
'Element $(NAME) of entity $(ALIAS) does not have a supported ON-condition'
|
|
273
|
+
);
|
|
274
|
+
return x;
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function adaptRef(expr) {
|
|
279
|
+
if (expr.ref[0].charAt(0) === '$') // variable
|
|
280
|
+
return { ref: [ ...expr.ref ] };
|
|
281
|
+
if (expr.ref[0] === 'localized') // target side
|
|
282
|
+
return { ref: [ 'localized_1', ...expr.ref.slice(1) ] };
|
|
283
|
+
return { ref: [ 'L_0', ...expr.ref ] }; // source side
|
|
284
|
+
}
|
|
285
|
+
}
|
|
273
286
|
}
|
|
274
287
|
|
|
275
288
|
/**
|
|
@@ -316,7 +329,6 @@ function _addLocalizationViews(csn, options, config) {
|
|
|
316
329
|
if (noCoalesce)
|
|
317
330
|
return createColumnRef( [ ...localizedNames, elementName ], elementName );
|
|
318
331
|
|
|
319
|
-
|
|
320
332
|
return {
|
|
321
333
|
func: 'coalesce',
|
|
322
334
|
args: [
|
|
@@ -357,8 +369,8 @@ function _addLocalizationViews(csn, options, config) {
|
|
|
357
369
|
|
|
358
370
|
/**
|
|
359
371
|
* Returns all text element names for a definition `<artName>` if its texts entity
|
|
360
|
-
* exists and `<artName>` has localized fields. Otherwise `null` is returned.
|
|
361
|
-
* Text elements are localized elements
|
|
372
|
+
* exists and `<artName>` has localized fields. Otherwise, `null` is returned.
|
|
373
|
+
* Text elements are non-key localized elements.
|
|
362
374
|
*
|
|
363
375
|
* @param {string} artName Artifact name
|
|
364
376
|
* @return {string[] | null}
|
|
@@ -367,49 +379,31 @@ function _addLocalizationViews(csn, options, config) {
|
|
|
367
379
|
const art = csn.definitions[artName];
|
|
368
380
|
const artPath = [ 'definitions', artName ];
|
|
369
381
|
|
|
370
|
-
|
|
371
|
-
let textElements = [];
|
|
382
|
+
const localizedElements = [];
|
|
372
383
|
|
|
373
384
|
forEachGeneric(art, 'elements', (elem, elemName, _prop) => {
|
|
374
385
|
if (elem.$ignore) // from SAP HANA backend
|
|
375
386
|
return;
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
keyCount += 1;
|
|
379
|
-
|
|
380
|
-
if (elem.key || elem.$key || elem.localized)
|
|
381
|
-
textElements.push( elemName );
|
|
387
|
+
if (elem.localized && !elem.key && !elem.$key)
|
|
388
|
+
localizedElements.push( elemName );
|
|
382
389
|
}, artPath);
|
|
383
390
|
|
|
384
|
-
if (
|
|
391
|
+
if (!localizedElements.length) {
|
|
385
392
|
// Nothing to do: no localized fields or all localized fields are keys
|
|
386
393
|
return null;
|
|
387
|
-
|
|
388
|
-
if (!
|
|
389
|
-
messageFunctions.info(
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
394
|
+
}
|
|
395
|
+
if (!art.elements.localized) {
|
|
396
|
+
messageFunctions.info('def-expected-localized', artPath, { '#': 'missing', name: artName, alias: 'localized' });
|
|
397
|
+
return null;
|
|
398
|
+
}
|
|
399
|
+
if (!art.elements.localized.target) {
|
|
400
|
+
messageFunctions.info('def-expected-localized', artPath, { '#': 'non-assoc', name: artName, alias: 'localized' });
|
|
393
401
|
return null;
|
|
394
402
|
}
|
|
395
403
|
|
|
396
404
|
const textsName = textsEntityName( artName );
|
|
397
405
|
const textsEntity = csn.definitions[textsName];
|
|
398
406
|
|
|
399
|
-
if (!textsEntity) {
|
|
400
|
-
messageFunctions.info(
|
|
401
|
-
null, artPath, { name: artName },
|
|
402
|
-
'Skipped creation of convenience view for $(NAME) because its texts entity could not be found'
|
|
403
|
-
);
|
|
404
|
-
return null;
|
|
405
|
-
}
|
|
406
|
-
if (!isValidTextsEntity( textsEntity )) {
|
|
407
|
-
messageFunctions.info(
|
|
408
|
-
null, [ 'definitions', textsName ], { name: artName },
|
|
409
|
-
'Skipped creation of convenience view for $(NAME) because its texts entity does not appear to be valid'
|
|
410
|
-
);
|
|
411
|
-
return null;
|
|
412
|
-
}
|
|
413
407
|
if (!art[annoPersistenceSkip] && textsEntity[annoPersistenceSkip]) {
|
|
414
408
|
messageFunctions.message(
|
|
415
409
|
'anno-unexpected-localized-skip', artPath,
|
|
@@ -418,22 +412,10 @@ function _addLocalizationViews(csn, options, config) {
|
|
|
418
412
|
return null;
|
|
419
413
|
}
|
|
420
414
|
|
|
421
|
-
//
|
|
422
|
-
//
|
|
423
|
-
//
|
|
424
|
-
|
|
425
|
-
textElements = textElements.filter((elemName) => {
|
|
426
|
-
const hasElement = !!textsEntity.elements[elemName];
|
|
427
|
-
if (!hasElement && (art.elements[elemName].key || art.elements[elemName].$key))
|
|
428
|
-
keyCount--;
|
|
429
|
-
return hasElement;
|
|
430
|
-
});
|
|
431
|
-
|
|
432
|
-
if (textElements.length <= keyCount || keyCount <= 0)
|
|
433
|
-
// Repeat the check already used above as the number of keys may have changed.
|
|
434
|
-
return null;
|
|
435
|
-
|
|
436
|
-
return textElements;
|
|
415
|
+
// Due to recompilation / flattening, properties may have been propagated from "type-of".
|
|
416
|
+
// That means we have localized elements with no corresponding element in the texts-entity.
|
|
417
|
+
// Hence, we simply filter here.
|
|
418
|
+
return localizedElements.filter(elemName => textsEntity.elements[elemName]);
|
|
437
419
|
}
|
|
438
420
|
|
|
439
421
|
/**
|
|
@@ -646,8 +628,8 @@ function _addLocalizationViews(csn, options, config) {
|
|
|
646
628
|
* @param {string} artName
|
|
647
629
|
*/
|
|
648
630
|
function textsEntityName(artName) {
|
|
649
|
-
// We can assume that the element exists.
|
|
650
|
-
return csn.definitions[artName].elements.
|
|
631
|
+
// We can assume that the element exists.
|
|
632
|
+
return csn.definitions[artName].elements.localized.target;
|
|
651
633
|
}
|
|
652
634
|
|
|
653
635
|
/**
|
|
@@ -808,33 +790,6 @@ function checkExistingLocalizationViews(csn, options, messageFunctions) {
|
|
|
808
790
|
return hasExistingViews || hasNonViews;
|
|
809
791
|
}
|
|
810
792
|
|
|
811
|
-
/**
|
|
812
|
-
* Returns true if the given entity appears to be a valid texts entity.
|
|
813
|
-
*
|
|
814
|
-
* @param {CSN.Artifact} entity
|
|
815
|
-
*/
|
|
816
|
-
function isValidTextsEntity(entity) {
|
|
817
|
-
if (!entity)
|
|
818
|
-
return false;
|
|
819
|
-
const requiredTextsProps = [ 'locale' ];
|
|
820
|
-
return requiredTextsProps.some( prop => !!entity.elements[prop]);
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
/**
|
|
824
|
-
* Returns true if the localized entity has elements that are generated by
|
|
825
|
-
* the core-compiler. If elements are missing but the entity is localized
|
|
826
|
-
* then the pre-processing by the core-compiler was not done.
|
|
827
|
-
*
|
|
828
|
-
* @param {CSN.Artifact} entity
|
|
829
|
-
*/
|
|
830
|
-
function isEntityPreprocessed(entity) {
|
|
831
|
-
if (!entity)
|
|
832
|
-
return false;
|
|
833
|
-
if (!entity.elements.localized)
|
|
834
|
-
return false;
|
|
835
|
-
return entity.elements.texts && entity.elements.texts.target;
|
|
836
|
-
}
|
|
837
|
-
|
|
838
793
|
/**
|
|
839
794
|
* @param {string} name
|
|
840
795
|
* @returns {boolean}
|
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const { applyTransformations, transformAnnotationExpression } = require('../../model/csnUtils');
|
|
4
|
-
const { isBuiltinType } = require('../../base/builtins');
|
|
3
|
+
const { applyTransformations, transformAnnotationExpression, implicitAs } = require('../../model/csnUtils');
|
|
5
4
|
|
|
6
5
|
|
|
7
|
-
|
|
6
|
+
/**
|
|
7
|
+
* If a path in an annotation expression can be interpreted as accessing a local foreign key, then
|
|
8
|
+
* the foreign key reference is replaced with the foreign key itself.
|
|
9
|
+
* Exception is when the association path step has a filter.
|
|
10
|
+
*
|
|
11
|
+
* @param {CSN} csn
|
|
12
|
+
* @param {Object} csnUtils
|
|
13
|
+
* @param {Object} iterateOptions
|
|
14
|
+
*/
|
|
15
|
+
function replaceForeignKeyRefsInExpressionAnnotations(csn, csnUtils, iterateOptions = {}) {
|
|
8
16
|
const transformers = {
|
|
9
|
-
|
|
10
|
-
params: processRef,
|
|
11
|
-
actions: processRef,
|
|
12
|
-
// '@': processRef
|
|
17
|
+
'@': processRef,
|
|
13
18
|
};
|
|
14
19
|
applyTransformations(csn, transformers, [ processRef ], iterateOptions);
|
|
15
20
|
|
|
@@ -17,29 +22,27 @@ function replaceForeignKeyRefsInExpressionAnnotations(csn, options, messageFunct
|
|
|
17
22
|
transformAnnotationExpression(parent, prop, {
|
|
18
23
|
ref: (parent, _prop, ref, path, _p, _ppn, ctx) => {
|
|
19
24
|
const { art, links }
|
|
20
|
-
=
|
|
25
|
+
= parent._art && parent._links
|
|
21
26
|
? { art: parent._art, links: parent._links }
|
|
22
27
|
: csnUtils.inspectRef(path);
|
|
28
|
+
|
|
23
29
|
// if a reference points to a structure(managed assoc or structured element), then we do not process
|
|
24
30
|
// as we can't guess which specific foreign key is targeted
|
|
25
|
-
if (
|
|
31
|
+
if (
|
|
32
|
+
!art ||
|
|
33
|
+
csnUtils.isManagedAssociation(art) ||
|
|
34
|
+
csnUtils.isStructured(art)
|
|
35
|
+
)
|
|
26
36
|
return;
|
|
27
37
|
|
|
28
|
-
const
|
|
29
|
-
if (!allMngAssocsInRef.length)
|
|
30
|
-
return;
|
|
31
|
-
let firstAssocToProcess = allMngAssocsInRef[0];
|
|
38
|
+
const modifiedRef = replaceRefsWithFKs(ref, links, art);
|
|
32
39
|
|
|
33
|
-
|
|
34
|
-
if (
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const match = findMatchingForeignKeyForAssoc(firstAssocToProcess, art, ref, links);
|
|
40
|
-
if (match) {
|
|
41
|
-
const refHead = ref.slice(0, match.idx);
|
|
42
|
-
parent.ref = [ ...refHead, match.fkName ];
|
|
40
|
+
// update the ref and string token to true if there was FK replacement
|
|
41
|
+
if (
|
|
42
|
+
modifiedRef.length !== ref.length ||
|
|
43
|
+
!modifiedRef.every((val, index) => val === ref[index])
|
|
44
|
+
) {
|
|
45
|
+
parent.ref = modifiedRef;
|
|
43
46
|
if (ctx?.annoExpr?.['='])
|
|
44
47
|
ctx.annoExpr['='] = true;
|
|
45
48
|
}
|
|
@@ -48,50 +51,73 @@ function replaceForeignKeyRefsInExpressionAnnotations(csn, options, messageFunct
|
|
|
48
51
|
path);
|
|
49
52
|
}
|
|
50
53
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
return acc;
|
|
74
|
-
}, {});
|
|
75
|
-
let bufferRef = [];
|
|
76
|
-
for (let i = assoc.idx + 1; i < links.length; i++) {
|
|
77
|
-
const link = links[i];
|
|
78
|
-
bufferRef.push(ref[i]);
|
|
79
|
-
if (csnUtils.isManagedAssociation(link.art)) {
|
|
80
|
-
const subFkName = findExpectedFkName(link, ref, links);
|
|
81
|
-
if (!subFkName)
|
|
82
|
-
return undefined;
|
|
83
|
-
expectedFkName += bufferRef.length > 1
|
|
84
|
-
? `_${ bufferRef.slice(0, -1).join('_') }_${ subFkName }`
|
|
85
|
-
: `_${ subFkName }`;
|
|
86
|
-
break;
|
|
54
|
+
// Replace references to foreign keys
|
|
55
|
+
function replaceRefsWithFKs(originalRef, links, expectedFkArt) {
|
|
56
|
+
let result = [ ...originalRef ];
|
|
57
|
+
// stringify the tail of the ref for finding the potential foreign key
|
|
58
|
+
const refTail = [ originalRef[originalRef.length - 1] ];
|
|
59
|
+
|
|
60
|
+
for (let i = originalRef.length - 2; i >= 0; i--) {
|
|
61
|
+
const currentRef = originalRef[i];
|
|
62
|
+
const currentLink = links[i].art;
|
|
63
|
+
|
|
64
|
+
// skip processing if the current reference is a filter
|
|
65
|
+
if (typeof currentRef !== 'string')
|
|
66
|
+
return result;
|
|
67
|
+
|
|
68
|
+
// check if the current link is a managed association
|
|
69
|
+
if (csnUtils.isManagedAssociation(currentLink)) {
|
|
70
|
+
const matchedForeignKey = findMatchingForeignKeyForAssoc(currentLink, currentRef, refTail, expectedFkArt);
|
|
71
|
+
|
|
72
|
+
if (matchedForeignKey) {
|
|
73
|
+
// update the result and refTailAsStr with the matched foreign key
|
|
74
|
+
result = [ ...result.slice(0, i), matchedForeignKey.name ];
|
|
75
|
+
refTail.unshift(currentRef);
|
|
87
76
|
}
|
|
88
|
-
else
|
|
89
|
-
|
|
90
|
-
bufferRef = [];
|
|
77
|
+
else {
|
|
78
|
+
return result; // return if no matching foreign key is found
|
|
91
79
|
}
|
|
92
80
|
}
|
|
93
|
-
|
|
81
|
+
else {
|
|
82
|
+
// update refTail for non-association links
|
|
83
|
+
refTail.unshift(currentRef);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return result;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Lookup the foreign key in the association's generated foreign keys
|
|
90
|
+
function findMatchingForeignKeyForAssoc(assoc, assocName, refTail, expectedFkArt) {
|
|
91
|
+
const expectedFkName = getExpectedForeignKeyName(assoc, assocName, refTail);
|
|
92
|
+
const matchedFk = assoc.$generatedForeignKeys?.find(fk => fk.source === expectedFkArt && fk.name === expectedFkName);
|
|
93
|
+
return matchedFk;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Generate the expected foreign key name, considering aliases, tuple expansion name changes, etc.
|
|
97
|
+
function getExpectedForeignKeyName(assoc, assocName, refTail) {
|
|
98
|
+
const refAliasMapping = assoc.keys.reduce( (acc, key) => {
|
|
99
|
+
acc[key.ref.join('_')] = key.as || implicitAs(key.ref);
|
|
100
|
+
return acc;
|
|
101
|
+
}, {});
|
|
102
|
+
// generate the string representation of the reference tail
|
|
103
|
+
const refTailAsStr = replaceRefsIfAliased(refTail, refAliasMapping) || refTail.join('_');
|
|
104
|
+
return `${ assocName }_${ refTailAsStr }`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Check if any prefix of refTail matches an alias in refAliasMapping and replace it
|
|
108
|
+
function replaceRefsIfAliased(refTail, refAliasMapping) {
|
|
109
|
+
// loop through refTail and try to find a match in the refAliasMapping
|
|
110
|
+
// no need to look for the longest match as it is not allowed to declare
|
|
111
|
+
// duplicate key references in one FKs scope
|
|
112
|
+
let candidate = '';
|
|
113
|
+
for (let idx = 0; idx < refTail.length; idx++) {
|
|
114
|
+
candidate = candidate ? `${ candidate }_${ refTail[idx] }` : refTail[idx];
|
|
115
|
+
if (refAliasMapping[candidate]) {
|
|
116
|
+
refTail.splice(0, idx + 1, refAliasMapping[candidate]);
|
|
117
|
+
return refTail.join('_');
|
|
118
|
+
}
|
|
94
119
|
}
|
|
120
|
+
return undefined;
|
|
95
121
|
}
|
|
96
122
|
}
|
|
97
123
|
|
|
@@ -54,7 +54,7 @@ function getTransformers(model, options, msgFunctions, pathDelimiter = '_') {
|
|
|
54
54
|
extractValidFromToKeyElement,
|
|
55
55
|
checkMultipleAssignments,
|
|
56
56
|
checkAssignment,
|
|
57
|
-
recurseElements,
|
|
57
|
+
recurseElements, // standalone-function
|
|
58
58
|
renameAnnotation,
|
|
59
59
|
setAnnotation,
|
|
60
60
|
resetAnnotation,
|
|
@@ -808,26 +808,6 @@ function getTransformers(model, options, msgFunctions, pathDelimiter = '_') {
|
|
|
808
808
|
}
|
|
809
809
|
}
|
|
810
810
|
|
|
811
|
-
/**
|
|
812
|
-
* Calls `callback` for each element in `elements` property of `artifact` recursively.
|
|
813
|
-
*
|
|
814
|
-
* @param {CSN.Artifact} artifact the artifact
|
|
815
|
-
* @param {CSN.Path} path path to get to `artifact` (mainly used for error messages)
|
|
816
|
-
* @param {(art: CSN.Artifact, path: CSN.Path) => any} callback Function called for each element recursively.
|
|
817
|
-
*/
|
|
818
|
-
function recurseElements(artifact, path, callback) {
|
|
819
|
-
callback(artifact, path);
|
|
820
|
-
const { elements } = artifact;
|
|
821
|
-
if (elements) {
|
|
822
|
-
path.push('elements', null);
|
|
823
|
-
forEach(elements, (name, obj) => {
|
|
824
|
-
path[path.length - 1] = name;
|
|
825
|
-
recurseElements(obj, path, callback);
|
|
826
|
-
});
|
|
827
|
-
// reset path for subsequent usages
|
|
828
|
-
path.length -= 2; // equivalent to 2x pop()
|
|
829
|
-
}
|
|
830
|
-
}
|
|
831
811
|
|
|
832
812
|
// Rename annotation 'fromName' in 'node' to 'toName' (both names including '@')
|
|
833
813
|
function renameAnnotation(node, fromName, toName) {
|
|
@@ -920,8 +900,30 @@ function rewriteBuiltinTypeRef(csn) {
|
|
|
920
900
|
});
|
|
921
901
|
}
|
|
922
902
|
|
|
903
|
+
/**
|
|
904
|
+
* Calls `callback` for each element in `elements` property of `artifact` recursively.
|
|
905
|
+
*
|
|
906
|
+
* @param {CSN.Artifact} artifact the artifact
|
|
907
|
+
* @param {CSN.Path} path path to get to `artifact` (mainly used for error messages)
|
|
908
|
+
* @param {(art: CSN.Artifact, path: CSN.Path) => any} callback Function called for each element recursively.
|
|
909
|
+
*/
|
|
910
|
+
function recurseElements(artifact, path, callback) {
|
|
911
|
+
callback(artifact, path);
|
|
912
|
+
const { elements } = artifact;
|
|
913
|
+
if (elements) {
|
|
914
|
+
path.push('elements', null);
|
|
915
|
+
forEach(elements, (name, obj) => {
|
|
916
|
+
path[path.length - 1] = name;
|
|
917
|
+
recurseElements(obj, path, callback);
|
|
918
|
+
});
|
|
919
|
+
// reset path for subsequent usages
|
|
920
|
+
path.length -= 2; // equivalent to 2x pop()
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
923
924
|
module.exports = {
|
|
924
925
|
// This function retrieves the actual exports
|
|
925
926
|
getTransformers,
|
|
926
927
|
rewriteBuiltinTypeRef,
|
|
928
|
+
recurseElements,
|
|
927
929
|
};
|
|
@@ -487,10 +487,12 @@ function translateAssocsToJoins(model, inputOptions = {}) {
|
|
|
487
487
|
// create a new toplevel AND op otherwise
|
|
488
488
|
const onCond = (Array.isArray(node.on) ? node.on[0] : node.on);
|
|
489
489
|
|
|
490
|
-
if (
|
|
491
|
-
onCond.
|
|
492
|
-
|
|
493
|
-
|
|
490
|
+
if (filter.args?.length !== 0) {
|
|
491
|
+
if (onCond.op.val === 'and')
|
|
492
|
+
onCond.args.push(parenthesise(filter));
|
|
493
|
+
else
|
|
494
|
+
node.on = parenthesise({ op: { val: 'and' }, args: [ parenthesise(onCond), parenthesise(filter) ] });
|
|
495
|
+
}
|
|
494
496
|
}
|
|
495
497
|
return node;
|
|
496
498
|
|
|
@@ -1496,7 +1498,7 @@ function translateAssocsToJoins(model, inputOptions = {}) {
|
|
|
1496
1498
|
*/
|
|
1497
1499
|
let qatName = pathStep.id;
|
|
1498
1500
|
|
|
1499
|
-
if (pathStep.where)
|
|
1501
|
+
if (pathStep.where && pathStep.where?.args?.length !== 0)
|
|
1500
1502
|
qatName += JSON.stringify(compactExpr(pathStep.where));
|
|
1501
1503
|
|
|
1502
1504
|
if (pathStep.args) {
|
|
@@ -14,7 +14,7 @@ const { cloneCsnNonDict } = require('../model/cloneCsn');
|
|
|
14
14
|
*
|
|
15
15
|
* @type {string[]}
|
|
16
16
|
*/
|
|
17
|
-
const RelationalOperators = [ '=', '<>', '==', '!=', 'is', 'is not'
|
|
17
|
+
const RelationalOperators = [ '=', '<>', '==', '!=', 'is', 'is not' ];
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* Operators that to be used to combine expanded expressions by keeping logical relations.
|
|
@@ -64,6 +64,11 @@ function tupleExpansion(csn, csnUtils, msgFunctions) {
|
|
|
64
64
|
having: expandExpr,
|
|
65
65
|
where: expandExpr,
|
|
66
66
|
xpr: expandExpr,
|
|
67
|
+
list: (parent, name, args, path) => {
|
|
68
|
+
// Don't iterate `group by (foo, bar)`
|
|
69
|
+
if (path.at(-2) !== 'groupBy' && path.at(-2) !== 'orderBy')
|
|
70
|
+
expandExpr(parent, name, args, path);
|
|
71
|
+
},
|
|
67
72
|
args: (parent, name, args, path) => {
|
|
68
73
|
if (!parent.id && !parent.func)
|
|
69
74
|
return; // ensure we're not in JOIN
|
|
@@ -262,6 +267,10 @@ function tupleExpansion(csn, csnUtils, msgFunctions) {
|
|
|
262
267
|
}
|
|
263
268
|
}
|
|
264
269
|
|
|
270
|
+
/**
|
|
271
|
+
* @param expr
|
|
272
|
+
* @param {CSN.Path} location
|
|
273
|
+
*/
|
|
265
274
|
function rejectAnyDirectStructureReference(expr, location) {
|
|
266
275
|
if (expr[0] === 'exists') {
|
|
267
276
|
// we ignore WHERE EXISTS clauses; they are not relevant for OData,
|
|
@@ -318,11 +327,15 @@ function tupleExpansion(csn, csnUtils, msgFunctions) {
|
|
|
318
327
|
* `{ _art: <leaf_artifact>, ref: [...] }`
|
|
319
328
|
* with `_art` identifying `ref[ref.length-1]`
|
|
320
329
|
*
|
|
321
|
-
* A produced path has the form `{ _art: <ref>, ref: [ <id> (, <id>)* ] }`
|
|
330
|
+
* A produced path has the form `{ _art: <ref>, ref: [ <id> (, <id>)* ], comparisonRef: [ <id> (, <id>)* ] }`
|
|
322
331
|
*
|
|
323
332
|
* Flattening stops on all non-structured elements, if followMgdAssoc=false.
|
|
324
333
|
*
|
|
325
|
-
* If fullRef is true, a path step is produced as `{ id: <id>, _art: <link> }
|
|
334
|
+
* If fullRef is true, a path step is produced as `{ id: <id>, _art: <link> }`.
|
|
335
|
+
*
|
|
336
|
+
* The returned paths will have a property 'comparisonRef', that may differ from 'ref'
|
|
337
|
+
* for managed associations (as it uses the foreign key name).
|
|
338
|
+
* The caller may need to delete that property.
|
|
326
339
|
*/
|
|
327
340
|
function flattenPath(path, fullRef = false, followMgdAssoc = false) {
|
|
328
341
|
let art = path._art;
|