@sap/cds-compiler 4.3.2 → 4.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 +39 -0
- package/lib/api/main.js +14 -24
- package/lib/api/options.js +1 -0
- package/lib/api/trace.js +38 -0
- package/lib/base/location.js +46 -1
- package/lib/base/message-registry.js +68 -16
- package/lib/base/messages.js +8 -3
- package/lib/checks/.eslintrc.json +1 -0
- package/lib/checks/actionsFunctions.js +1 -1
- package/lib/checks/annotationsOData.js +2 -2
- package/lib/checks/selectItems.js +4 -1
- package/lib/compiler/assert-consistency.js +3 -2
- package/lib/compiler/base.js +1 -1
- package/lib/compiler/builtins.js +25 -1
- package/lib/compiler/checks.js +6 -5
- package/lib/compiler/define.js +12 -10
- package/lib/compiler/extend.js +44 -23
- package/lib/compiler/finalize-parse-cdl.js +1 -1
- package/lib/compiler/generate.js +70 -53
- package/lib/compiler/kick-start.js +7 -5
- package/lib/compiler/populate.js +31 -22
- package/lib/compiler/propagator.js +6 -2
- package/lib/compiler/resolve.js +52 -17
- package/lib/compiler/shared.js +85 -39
- package/lib/compiler/tweak-assocs.js +64 -23
- package/lib/compiler/utils.js +40 -23
- package/lib/edm/.eslintrc.json +2 -0
- package/lib/edm/EdmPrimitiveTypeDefinitions.js +260 -0
- package/lib/edm/annotations/edmJson.js +994 -0
- package/lib/edm/annotations/genericTranslation.js +82 -423
- package/lib/edm/annotations/vocabularyDefinitions.js +160 -0
- package/lib/edm/csn2edm.js +12 -5
- package/lib/edm/edm.js +14 -73
- package/lib/edm/edmPreprocessor.js +6 -0
- package/lib/gen/Dictionary.json +187 -16
- package/lib/gen/language.checksum +1 -1
- package/lib/gen/language.interp +1 -1
- package/lib/gen/languageLexer.interp +1 -1
- package/lib/gen/languageLexer.js +1129 -671
- package/lib/gen/languageParser.js +4285 -4283
- package/lib/json/from-csn.js +13 -18
- package/lib/json/to-csn.js +11 -6
- package/lib/language/antlrParser.js +0 -1
- package/lib/language/docCommentParser.js +1 -1
- package/lib/language/errorStrategy.js +95 -30
- package/lib/language/genericAntlrParser.js +21 -1
- package/lib/main.js +13 -3
- package/lib/model/csnRefs.js +42 -8
- package/lib/model/csnUtils.js +14 -2
- package/lib/model/enrichCsn.js +33 -5
- package/lib/model/revealInternalProperties.js +5 -0
- package/lib/modelCompare/compare.js +76 -14
- package/lib/modelCompare/utils/filter.js +19 -12
- package/lib/optionProcessor.js +2 -0
- package/lib/render/.eslintrc.json +1 -1
- package/lib/render/manageConstraints.js +1 -0
- package/lib/render/toHdbcds.js +3 -0
- package/lib/render/toRename.js +3 -1
- package/lib/render/toSql.js +46 -92
- package/lib/render/utils/common.js +76 -0
- package/lib/render/utils/delta.js +17 -3
- package/lib/sql-identifier.js +1 -1
- package/lib/transform/db/.eslintrc.json +1 -0
- package/lib/transform/db/applyTransformations.js +30 -4
- package/lib/transform/db/associations.js +22 -10
- package/lib/transform/db/backlinks.js +6 -2
- package/lib/transform/db/expansion.js +2 -2
- package/lib/transform/db/transformExists.js +13 -39
- package/lib/transform/draft/db.js +14 -3
- package/lib/transform/draft/odata.js +5 -18
- package/lib/transform/effective/associations.js +46 -15
- package/lib/transform/effective/main.js +7 -2
- package/lib/transform/effective/misc.js +43 -24
- package/lib/transform/effective/queries.js +20 -22
- package/lib/transform/effective/types.js +6 -2
- package/lib/transform/forOdata.js +10 -3
- package/lib/transform/localized.js +1 -1
- package/lib/transform/parseExpr.js +73 -21
- package/lib/transform/translateAssocsToJoins.js +22 -15
- package/lib/utils/term.js +2 -2
- package/package.json +2 -1
package/lib/compiler/checks.js
CHANGED
|
@@ -214,6 +214,7 @@ function check( model ) {
|
|
|
214
214
|
|
|
215
215
|
// TODO: This check should be moved to localized.js
|
|
216
216
|
// "key" keyword at localized element in SELECT list.
|
|
217
|
+
// TODO: not in inferred elements, but also inside aspects
|
|
217
218
|
if (elem.key?.val && elem._main?.query) {
|
|
218
219
|
// original element is localized but not key, as that would have
|
|
219
220
|
// already resulted in a warning by localized.js
|
|
@@ -459,7 +460,7 @@ function check( model ) {
|
|
|
459
460
|
error( 'ref-unexpected-calculated', [ key.location, elem ], { '#': 'fkey' } );
|
|
460
461
|
}
|
|
461
462
|
}
|
|
462
|
-
if (elem.default) {
|
|
463
|
+
if (elem.default?.val !== undefined) {
|
|
463
464
|
if (elem.targetAspect || elem.on || fkCount !== 1) {
|
|
464
465
|
const variant = (elem.targetAspect && 'targetAspect') || (elem.on && 'onCond') || 'multi';
|
|
465
466
|
error( 'type-unexpected-default', [ elem.default.location, elem ], {
|
|
@@ -852,7 +853,7 @@ function check( model ) {
|
|
|
852
853
|
|
|
853
854
|
// Element must exist in annotation
|
|
854
855
|
if (!elementDecl) {
|
|
855
|
-
warning( null, [ anno.location || anno.name.location, art ],
|
|
856
|
+
warning( null, [ anno.location || anno.name.location, art, anno ],
|
|
856
857
|
{ name: anno.name.id, anno: annoDecl.name.id },
|
|
857
858
|
'Element $(NAME) not found for annotation $(ANNO)' );
|
|
858
859
|
return;
|
|
@@ -866,11 +867,11 @@ function check( model ) {
|
|
|
866
867
|
// Must have literal or path unless it is a boolean
|
|
867
868
|
if (!anno.literal && !anno.path && elementDecl._effectiveType?.category !== 'boolean') {
|
|
868
869
|
if (elementDecl.type?._artifact) {
|
|
869
|
-
warning( 'anno-expecting-value', [ anno.location || anno.name.location, art ],
|
|
870
|
+
warning( 'anno-expecting-value', [ anno.location || anno.name.location, art, anno ],
|
|
870
871
|
{ '#': 'type', type: elementDecl.type._artifact } );
|
|
871
872
|
}
|
|
872
873
|
else {
|
|
873
|
-
warning( 'anno-expecting-value', [ anno.location || anno.name.location, art ],
|
|
874
|
+
warning( 'anno-expecting-value', [ anno.location || anno.name.location, art, anno ],
|
|
874
875
|
{ '#': 'std', anno: anno.name.id } );
|
|
875
876
|
}
|
|
876
877
|
|
|
@@ -891,7 +892,7 @@ function check( model ) {
|
|
|
891
892
|
return;
|
|
892
893
|
|
|
893
894
|
const anno = annoDef.name.id;
|
|
894
|
-
const loc = [ value.location || value.name.location, art ];
|
|
895
|
+
const loc = [ value.location || value.name.location, art, annoDef ];
|
|
895
896
|
|
|
896
897
|
// Array expected?
|
|
897
898
|
if (elementDecl._effectiveType.items) {
|
package/lib/compiler/define.js
CHANGED
|
@@ -136,7 +136,7 @@ const {
|
|
|
136
136
|
storeExtension,
|
|
137
137
|
dependsOnSilent,
|
|
138
138
|
pathName,
|
|
139
|
-
|
|
139
|
+
targetCantBeAspect,
|
|
140
140
|
} = require('./utils');
|
|
141
141
|
const { compareLayer } = require('./moduleLayers');
|
|
142
142
|
const { initBuiltins, isInReservedNamespace } = require('./builtins');
|
|
@@ -313,7 +313,6 @@ function define( model ) {
|
|
|
313
313
|
artifacts[using] = {
|
|
314
314
|
kind: 'using', // !, not namespace - we do not know artifact yet
|
|
315
315
|
name: { id: using, location, $inferred: 'as' },
|
|
316
|
-
// TODO: use global ref (in general - all uses of splitIntoPath)
|
|
317
316
|
extern: { location, id: absolute },
|
|
318
317
|
location,
|
|
319
318
|
$inferred: 'path-prefix',
|
|
@@ -564,7 +563,7 @@ function define( model ) {
|
|
|
564
563
|
initMembers( art, art, block );
|
|
565
564
|
initDollarSelf( art ); // $self
|
|
566
565
|
if (art.params)
|
|
567
|
-
initDollarParameters( art );
|
|
566
|
+
initDollarParameters( art ); // $parameters
|
|
568
567
|
|
|
569
568
|
if (!art.query)
|
|
570
569
|
return;
|
|
@@ -1199,7 +1198,7 @@ function define( model ) {
|
|
|
1199
1198
|
|
|
1200
1199
|
// To be reworked -------------------------------------------------------------
|
|
1201
1200
|
|
|
1202
|
-
// TODO: make special for extend/annotate
|
|
1201
|
+
// TODO: is only necessary for extensions - make special for extend/annotate
|
|
1203
1202
|
function checkDefinitions( construct, parent, prop, dict = construct[prop] ) {
|
|
1204
1203
|
// TODO: do differently, see also annotateMembers() in resolver
|
|
1205
1204
|
// To have been checked by parsers:
|
|
@@ -1227,7 +1226,7 @@ function define( model ) {
|
|
|
1227
1226
|
//
|
|
1228
1227
|
else if (parent.kind === 'action' || parent.kind === 'function') {
|
|
1229
1228
|
error( 'ext-unexpected-action', [ construct.location, construct ], { '#': parent.kind }, {
|
|
1230
|
-
std: 'Actions and functions can\'t be extended, only annotated',
|
|
1229
|
+
std: 'Actions and functions can\'t be extended, only annotated', // TODO: → ext-unsupported
|
|
1231
1230
|
action: 'Actions can\'t be extended, only annotated',
|
|
1232
1231
|
function: 'Functions can\'t be extended, only annotated',
|
|
1233
1232
|
} );
|
|
@@ -1277,17 +1276,20 @@ function define( model ) {
|
|
|
1277
1276
|
|
|
1278
1277
|
/**
|
|
1279
1278
|
* Return whether the `target` is actually a `targetAspect`
|
|
1280
|
-
* TODO: really do that here and not in kick-start.js?
|
|
1281
1279
|
*/
|
|
1282
1280
|
function targetIsTargetAspect( elem ) {
|
|
1283
1281
|
const { target } = elem;
|
|
1284
|
-
if (target.elements)
|
|
1285
|
-
// TODO: error if CSN has both target.elements and targetAspect.elements
|
|
1286
|
-
// -> delete target
|
|
1282
|
+
if (target.elements) // CSN parser ensures: has no targetAspect then
|
|
1287
1283
|
return true;
|
|
1284
|
+
|
|
1285
|
+
if (elem.targetAspect) {
|
|
1286
|
+
// Ensure that a compiled CSN is parseable - not inside query, only on element
|
|
1287
|
+
return false;
|
|
1288
1288
|
}
|
|
1289
|
-
if (elem
|
|
1289
|
+
if (targetCantBeAspect( elem ) || options.parseCdl)
|
|
1290
1290
|
return false;
|
|
1291
|
+
// Compare this check with check in acceptEntity() called by resolvePath()
|
|
1292
|
+
// Remark: do not check `on` and `foreignKeys` here, we want error for those, not the aspect
|
|
1291
1293
|
const name = resolveUncheckedPath( target, 'target', elem );
|
|
1292
1294
|
const aspect = name && model.definitions[name];
|
|
1293
1295
|
return (aspect?.kind === 'aspect' || aspect?.kind === 'type') && // type is sloppy
|
package/lib/compiler/extend.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
'use strict';
|
|
4
4
|
|
|
5
|
-
const {
|
|
5
|
+
const { weakRefLocation } = require('../base/location');
|
|
6
6
|
const { searchName } = require('../base/messages');
|
|
7
7
|
const {
|
|
8
8
|
forEachInOrder,
|
|
@@ -31,7 +31,22 @@ const $location = Symbol.for( 'cds.$location' );
|
|
|
31
31
|
// attach stupid location - TODO: remove in v5
|
|
32
32
|
const genLocation = new CsnLocation( '' );
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
const draftElements = [
|
|
35
|
+
'IsActiveEntity',
|
|
36
|
+
'HasActiveEntity',
|
|
37
|
+
'HasDraftEntity',
|
|
38
|
+
'DraftAdministrativeData',
|
|
39
|
+
'SiblingEntity',
|
|
40
|
+
];
|
|
41
|
+
const draftBoundActions = [
|
|
42
|
+
'draftPrepare',
|
|
43
|
+
'draftActivate',
|
|
44
|
+
'draftEdit',
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
function canBeDraftMember( name, parent, draftMembers ) {
|
|
48
|
+
return parent?.kind === 'entity' && parent._service && draftMembers.includes( name );
|
|
49
|
+
}
|
|
35
50
|
|
|
36
51
|
function extend( model ) {
|
|
37
52
|
// Get simplified "resolve" functionality and the message function:
|
|
@@ -686,21 +701,15 @@ function extend( model ) {
|
|
|
686
701
|
}
|
|
687
702
|
}
|
|
688
703
|
|
|
689
|
-
// const unexpected_props = {
|
|
690
|
-
// elements: 'anno-unexpected-elements',
|
|
691
|
-
// enum: 'anno-unexpected-elements', // TODO
|
|
692
|
-
// params: 'anno-unexpected-params',
|
|
693
|
-
// actions: 'anno-unexpected-actions',
|
|
694
|
-
// };
|
|
695
|
-
// const undefined_props = {
|
|
696
|
-
// elements: 'anno-undefined-element',
|
|
697
|
-
// enum: 'anno-undefined-element', // TODO
|
|
698
|
-
// params: 'anno-undefined-param',
|
|
699
|
-
// actions: 'anno-undefined-action',
|
|
700
|
-
// };
|
|
701
|
-
|
|
702
704
|
function checkRemainingMemberExtensions( parent, ext, prop, name ) {
|
|
703
705
|
// console.log('CRME:',prop,name,parent,ext)
|
|
706
|
+
|
|
707
|
+
// TODO: just use `ext-undefined-element` etc also when no elements are there
|
|
708
|
+
// at all (but use an extra text variant and the `{…}` location). Reason: we
|
|
709
|
+
// might allow to add new actions, and an `annotate` on an undefined action
|
|
710
|
+
// should not lead to another message id. We would use and extra message id
|
|
711
|
+
// if we consider this an error or such sub annotates are then ignored
|
|
712
|
+
// (i.e. not put into the "super annotate").
|
|
704
713
|
const dict = parent[prop];
|
|
705
714
|
if (!dict) {
|
|
706
715
|
// TODO: check - for each name? - better locations
|
|
@@ -714,6 +723,9 @@ function extend( model ) {
|
|
|
714
723
|
{ '#': (parent._effectiveType?.kind === 'entity') ? 'entity' : 'std' }, {
|
|
715
724
|
std: 'Elements only exist in entities, types or typed constructs',
|
|
716
725
|
entity: 'Elements of entity types can\'t be annotated',
|
|
726
|
+
// TODO: extra msg for 'entity'? → this is some other
|
|
727
|
+
// situation, somehow similar when trying to annotate elements
|
|
728
|
+
// of target entity
|
|
717
729
|
} );
|
|
718
730
|
break;
|
|
719
731
|
case 'params':
|
|
@@ -721,8 +733,10 @@ function extend( model ) {
|
|
|
721
733
|
'Parameters only exist for actions or functions' );
|
|
722
734
|
break;
|
|
723
735
|
case 'actions':
|
|
724
|
-
|
|
725
|
-
|
|
736
|
+
if (canBeDraftMember( name, parent, draftBoundActions ))
|
|
737
|
+
return true;
|
|
738
|
+
// TODO: use extra text variant and location of dictionary - no
|
|
739
|
+
notFound( 'ext-undefined-action', ext.name.location, ext,
|
|
726
740
|
{ '#': 'action', art: parent, name } );
|
|
727
741
|
break;
|
|
728
742
|
default:
|
|
@@ -737,22 +751,26 @@ function extend( model ) {
|
|
|
737
751
|
const art = inReturns || parent;
|
|
738
752
|
switch (prop) {
|
|
739
753
|
case 'elements':
|
|
740
|
-
|
|
754
|
+
if (canBeDraftMember( name, parent, draftElements ))
|
|
755
|
+
break;
|
|
756
|
+
notFound( 'ext-undefined-element', ext.name.location, ext,
|
|
741
757
|
{ '#': (inReturns ? 'returns' : 'element'), art, name },
|
|
742
758
|
parent.elements );
|
|
743
759
|
break;
|
|
744
760
|
case 'enum': // TODO: extra msg id?
|
|
745
|
-
notFound( '
|
|
761
|
+
notFound( 'ext-undefined-element', ext.name.location, ext,
|
|
746
762
|
{ '#': (inReturns ? 'enum-returns' : 'enum'), art, name },
|
|
747
763
|
parent.enum );
|
|
748
764
|
break;
|
|
749
765
|
case 'params':
|
|
750
|
-
notFound( '
|
|
766
|
+
notFound( 'ext-undefined-param', ext.name.location, ext,
|
|
751
767
|
{ '#': 'param', art: parent, name },
|
|
752
768
|
parent.params );
|
|
753
769
|
break;
|
|
754
770
|
case 'actions':
|
|
755
|
-
|
|
771
|
+
if (canBeDraftMember( name, parent, draftBoundActions ))
|
|
772
|
+
break;
|
|
773
|
+
notFound( 'ext-undefined-action', ext.name.location, ext,
|
|
756
774
|
{ '#': 'action', art: parent, name },
|
|
757
775
|
parent.actions );
|
|
758
776
|
break;
|
|
@@ -1153,8 +1171,10 @@ function extend( model ) {
|
|
|
1153
1171
|
// Warning 'Overwrites definition from include "I" (at elem def)
|
|
1154
1172
|
const parent = ext === art && art;
|
|
1155
1173
|
const members = ext[prop];
|
|
1156
|
-
if (members)
|
|
1174
|
+
if (members) {
|
|
1157
1175
|
ext[prop] = Object.create( null );
|
|
1176
|
+
ext[prop][$location] = members[$location];
|
|
1177
|
+
}
|
|
1158
1178
|
let hasNewElement = false;
|
|
1159
1179
|
|
|
1160
1180
|
for (const ref of ext.includes) {
|
|
@@ -1162,6 +1182,7 @@ function extend( model ) {
|
|
|
1162
1182
|
if (template) { // be robust
|
|
1163
1183
|
if (template[prop] && !ext[prop])
|
|
1164
1184
|
ext[prop] = Object.create( null );
|
|
1185
|
+
const location = weakRefLocation( ref );
|
|
1165
1186
|
// eslint-disable-next-line no-loop-func
|
|
1166
1187
|
forEachInOrder( template, prop, ( origin, name ) => {
|
|
1167
1188
|
if (members && members[name]) {
|
|
@@ -1170,7 +1191,7 @@ function extend( model ) {
|
|
|
1170
1191
|
return;
|
|
1171
1192
|
}
|
|
1172
1193
|
hasNewElement = true;
|
|
1173
|
-
const elem = linkToOrigin( origin, name, parent, prop,
|
|
1194
|
+
const elem = linkToOrigin( origin, name, parent, prop, location );
|
|
1174
1195
|
setLink( elem, '_block', origin._block );
|
|
1175
1196
|
if (!parent) // not yet set for EXTEND foo WITH bar => linkToOrigin() did not add it
|
|
1176
1197
|
dictAdd( ext[prop], name, elem );
|
|
@@ -86,7 +86,7 @@ function finalizeParseCdl( model ) {
|
|
|
86
86
|
resolveUncheckedPath( include, 'include', main );
|
|
87
87
|
|
|
88
88
|
// define.js takes care that `target` is a ref and
|
|
89
|
-
// `targetAspect` is a structure.
|
|
89
|
+
// `targetAspect` is a structure (anonymous aspect) or ref to aspect.
|
|
90
90
|
if (artifact.target)
|
|
91
91
|
resolveUncheckedPath( artifact.target, 'target', main );
|
|
92
92
|
if (artifact.targetAspect)
|
package/lib/compiler/generate.js
CHANGED
|
@@ -17,7 +17,9 @@ const {
|
|
|
17
17
|
isDirectComposition,
|
|
18
18
|
copyExpr,
|
|
19
19
|
} = require('./utils');
|
|
20
|
-
const { weakLocation } = require('../base/location');
|
|
20
|
+
const { weakLocation, weakRefLocation, weakEndLocation } = require('../base/location');
|
|
21
|
+
|
|
22
|
+
const $location = Symbol.for( 'cds.$location' );
|
|
21
23
|
|
|
22
24
|
function generate( model ) {
|
|
23
25
|
const { options } = model;
|
|
@@ -220,6 +222,7 @@ function generate( model ) {
|
|
|
220
222
|
warning( 'def-ignoring-localized', [ errpos.location, elem ],
|
|
221
223
|
{ keyword: 'localized' },
|
|
222
224
|
'Keyword $(KEYWORD) is ignored for primary keys' );
|
|
225
|
+
// continuation semantics as stated: counts as key field in texts entity
|
|
223
226
|
}
|
|
224
227
|
}
|
|
225
228
|
if (textElems.length <= keys)
|
|
@@ -277,11 +280,11 @@ function generate( model ) {
|
|
|
277
280
|
* @param {boolean} fioriEnabled
|
|
278
281
|
*/
|
|
279
282
|
function createTextsEntity( base, absolute, textElems, fioriEnabled ) {
|
|
280
|
-
const
|
|
283
|
+
const location = weakLocation( base.elements[$location] || base.location );
|
|
281
284
|
const art = {
|
|
282
285
|
kind: 'entity',
|
|
283
286
|
name: { id: absolute, location },
|
|
284
|
-
location
|
|
287
|
+
location,
|
|
285
288
|
elements: Object.create( null ),
|
|
286
289
|
$inferred: 'localized-entity',
|
|
287
290
|
};
|
|
@@ -332,30 +335,8 @@ function generate( model ) {
|
|
|
332
335
|
// assertUnique array value, first entry is 'locale'
|
|
333
336
|
const assertUniqueValue = [];
|
|
334
337
|
|
|
335
|
-
for (const orig of textElems)
|
|
336
|
-
|
|
337
|
-
if (orig.key && orig.key.val) {
|
|
338
|
-
// elem.key = { val: fioriEnabled ? null : true, $inferred: 'localized', location };
|
|
339
|
-
// TODO: the previous would be better, but currently not supported in toCDL
|
|
340
|
-
if (!fioriEnabled) {
|
|
341
|
-
elem.key = { val: true, $inferred: 'localized', location };
|
|
342
|
-
// If the propagated elements remain key (that is not fiori.draft.enabled)
|
|
343
|
-
// they should be omitted from OData containment EDM
|
|
344
|
-
setAnnotation( elem, '@odata.containment.ignore', location );
|
|
345
|
-
}
|
|
346
|
-
else {
|
|
347
|
-
// add the former key paths to the unique constraint
|
|
348
|
-
assertUniqueValue.push({
|
|
349
|
-
path: [ { id: orig.name.id, location: orig.location } ],
|
|
350
|
-
location: orig.location,
|
|
351
|
-
});
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
if (hasTruthyProp( orig, 'localized' )) { // use location of LOCALIZED keyword
|
|
355
|
-
const localized = orig.localized || orig.type || orig.name;
|
|
356
|
-
elem.localized = { val: null, $inferred: 'localized', location: localized.location };
|
|
357
|
-
}
|
|
358
|
-
}
|
|
338
|
+
for (const orig of textElems)
|
|
339
|
+
addElementToTextsEntity( orig, art, fioriEnabled, assertUniqueValue );
|
|
359
340
|
|
|
360
341
|
initArtifact( art );
|
|
361
342
|
if (art.includes) {
|
|
@@ -366,7 +347,7 @@ function generate( model ) {
|
|
|
366
347
|
if (fioriEnabled) {
|
|
367
348
|
// The includes mechanism puts TextsAspect's elements before .texts' elements.
|
|
368
349
|
// Because ID_texts is not copied from TextsAspect, the order is messed
|
|
369
|
-
// up. Fix it.
|
|
350
|
+
// up. Fix it. TODO: introduce $includeAfter from Extensions.md
|
|
370
351
|
const { elements } = art;
|
|
371
352
|
art.elements = Object.create( null );
|
|
372
353
|
const names = [ 'ID_texts', 'locale', ...Object.keys( elements ) ];
|
|
@@ -385,6 +366,31 @@ function generate( model ) {
|
|
|
385
366
|
return art;
|
|
386
367
|
}
|
|
387
368
|
|
|
369
|
+
function addElementToTextsEntity( orig, art, fioriEnabled, assertUniqueValue ) {
|
|
370
|
+
const elem = linkToOrigin( orig, orig.name.id, art, 'elements' );
|
|
371
|
+
// To keep the locations of non-inferred original elements, do not set $inferred:
|
|
372
|
+
if (orig.$inferred)
|
|
373
|
+
elem.$inferred = 'localized-origin';
|
|
374
|
+
const { location } = elem;
|
|
375
|
+
if (orig.key && orig.key.val) {
|
|
376
|
+
// elem.key = { val: fioriEnabled ? null : true, $inferred: 'localized', location };
|
|
377
|
+
// TODO: the previous would be better, but currently not supported in toCDL
|
|
378
|
+
if (!fioriEnabled) {
|
|
379
|
+
elem.key = { val: true, $inferred: 'localized', location };
|
|
380
|
+
// If the propagated elements remain key (that is not fiori.draft.enabled)
|
|
381
|
+
// they should be omitted from OData containment EDM
|
|
382
|
+
setAnnotation( elem, '@odata.containment.ignore', location );
|
|
383
|
+
}
|
|
384
|
+
else {
|
|
385
|
+
// add the former key paths to the unique constraint
|
|
386
|
+
assertUniqueValue.push( { path: [ { id: orig.name.id, location } ], location } );
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
if (hasTruthyProp( orig, 'localized' )) { // use location of LOCALIZED keyword
|
|
390
|
+
elem.localized = { val: null, $inferred: 'localized', location };
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
388
394
|
/**
|
|
389
395
|
* Enrich the `.texts` entity for the given base artifact.
|
|
390
396
|
* In contrast to createTextsEntityWithDefaultElements(), this one creates
|
|
@@ -400,14 +406,15 @@ function generate( model ) {
|
|
|
400
406
|
function enrichTextsEntityWithInclude( art, base, absolute, fioriEnabled ) {
|
|
401
407
|
const textsAspectName = 'sap.common.TextsAspect';
|
|
402
408
|
const textsAspect = model.definitions['sap.common.TextsAspect'];
|
|
403
|
-
const { location } = art
|
|
409
|
+
const { location } = art;
|
|
404
410
|
|
|
405
|
-
art.includes = [ createInclude( textsAspectName,
|
|
411
|
+
art.includes = [ createInclude( textsAspectName, location ) ];
|
|
406
412
|
|
|
407
413
|
if (fioriEnabled) {
|
|
408
414
|
// "Early" include; only for element `locale`, which has its `key` property
|
|
409
415
|
// removed (or rather: it is not copied).
|
|
410
416
|
linkToOrigin( textsAspect.elements.locale, 'locale', art, 'elements', location );
|
|
417
|
+
art.elements.locale.$inferred = 'localized';
|
|
411
418
|
}
|
|
412
419
|
|
|
413
420
|
if (addTextsLanguageAssoc && art.elements.language)
|
|
@@ -425,12 +432,13 @@ function generate( model ) {
|
|
|
425
432
|
// If there is a type `sap.common.Locale`, then use it as the type for the element `locale`.
|
|
426
433
|
// If not, use the default `cds.String` with a length of 14.
|
|
427
434
|
const hasLocaleType = model.definitions['sap.common.Locale']?.kind === 'type';
|
|
428
|
-
const { location } = art
|
|
435
|
+
const { location } = art; // is already a weak location
|
|
429
436
|
const locale = {
|
|
430
437
|
name: { location, id: 'locale' },
|
|
431
438
|
kind: 'element',
|
|
432
439
|
type: linkMainArtifact( location, hasLocaleType ? 'sap.common.Locale' : 'cds.String' ),
|
|
433
440
|
location,
|
|
441
|
+
$inferred: 'localized', // $generated in Universal CSN, no $location
|
|
434
442
|
};
|
|
435
443
|
if (!hasLocaleType)
|
|
436
444
|
locale.length = { literal: 'number', val: 14, location };
|
|
@@ -449,7 +457,7 @@ function generate( model ) {
|
|
|
449
457
|
// texts : Composition of many Books.texts on texts.ID=ID;
|
|
450
458
|
/** @type {array} */
|
|
451
459
|
const keys = textElems.filter( e => e.key && e.key.val );
|
|
452
|
-
const
|
|
460
|
+
const location = weakEndLocation( art.elements[$location] ) || weakLocation( art.location );
|
|
453
461
|
const texts = {
|
|
454
462
|
name: { location, id: 'texts' },
|
|
455
463
|
kind: 'element',
|
|
@@ -568,7 +576,7 @@ function generate( model ) {
|
|
|
568
576
|
while (origin && !origin.targetAspect && origin._origin)
|
|
569
577
|
origin = origin._origin;
|
|
570
578
|
let target = origin.targetAspect;
|
|
571
|
-
if (target
|
|
579
|
+
if (target?.path)
|
|
572
580
|
target = resolvePath( origin.targetAspect, 'targetAspect', origin );
|
|
573
581
|
if (!target || !target.elements)
|
|
574
582
|
return;
|
|
@@ -601,7 +609,7 @@ function generate( model ) {
|
|
|
601
609
|
function allowAspectComposition( target, elem, keys, entityName ) {
|
|
602
610
|
if (!target.elements || Object.values( target.elements ).some( e => e.$duplicates ))
|
|
603
611
|
return false; // no elements or with redefinitions
|
|
604
|
-
const location = elem.
|
|
612
|
+
const location = elem.targetAspect?.location || elem.location;
|
|
605
613
|
if ((elem._main._upperAspects || []).includes( target ))
|
|
606
614
|
return 0; // circular containment of the same aspect
|
|
607
615
|
|
|
@@ -648,16 +656,22 @@ function generate( model ) {
|
|
|
648
656
|
if (elem.type && !isDirectComposition( elem )) {
|
|
649
657
|
// Only issue warning for direct usages, not for projections, includes, etc.
|
|
650
658
|
// TODO: Make it configurable error; v5: error
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
'
|
|
659
|
+
// TODO: move to resolve.js where we test the targetAspect,
|
|
660
|
+
warning( 'type-expecting-composition', [ elem.type.location, elem ],
|
|
661
|
+
{ newcode: 'Composition of', code: 'Association to' },
|
|
662
|
+
'Expecting $(NEWCODE), not $(CODE) for the anonymous target aspect' );
|
|
663
|
+
// auto-correct to avoid additional error 'type-unexpected-target-aspect' if
|
|
664
|
+
// cds.Association:
|
|
665
|
+
const { path, $inferred } = elem.type;
|
|
666
|
+
if (!$inferred && path?.length === 1 && path[0].id === 'cds.Association')
|
|
667
|
+
path[0].id = 'cds.Composition';
|
|
654
668
|
}
|
|
655
669
|
|
|
656
670
|
return true;
|
|
657
671
|
}
|
|
658
672
|
|
|
659
673
|
function createTargetEntity( target, elem, keys, entityName, base ) {
|
|
660
|
-
const
|
|
674
|
+
const location = weakRefLocation( elem.targetAspect || elem.target || elem );
|
|
661
675
|
elem.on = {
|
|
662
676
|
location,
|
|
663
677
|
op: { val: '=', location },
|
|
@@ -673,7 +687,7 @@ function generate( model ) {
|
|
|
673
687
|
name: {
|
|
674
688
|
id: entityName,
|
|
675
689
|
// for code navigation (e.g. via `extend`s): point to the element's name
|
|
676
|
-
location: elem.name.location,
|
|
690
|
+
location: weakLocation( elem.name.location ),
|
|
677
691
|
},
|
|
678
692
|
location,
|
|
679
693
|
elements: Object.create( null ),
|
|
@@ -690,18 +704,17 @@ function generate( model ) {
|
|
|
690
704
|
}
|
|
691
705
|
|
|
692
706
|
// Since there is no user-written up_ element, use a weak location to the beginning of {…}.
|
|
693
|
-
const upLocation = weakLocation( location );
|
|
694
707
|
const up = { // elements.up_ = ...
|
|
695
|
-
name: { location
|
|
708
|
+
name: { location, id: 'up_' },
|
|
696
709
|
kind: 'element',
|
|
697
|
-
location
|
|
710
|
+
location,
|
|
698
711
|
$inferred: 'aspect-composition',
|
|
699
|
-
type: linkMainArtifact(
|
|
700
|
-
target: linkMainArtifact(
|
|
712
|
+
type: linkMainArtifact( location, 'cds.Association' ),
|
|
713
|
+
target: linkMainArtifact( location, base.name.id ),
|
|
701
714
|
cardinality: {
|
|
702
|
-
targetMin: { val: 1, literal: 'number', location
|
|
703
|
-
targetMax: { val: 1, literal: 'number', location
|
|
704
|
-
location
|
|
715
|
+
targetMin: { val: 1, literal: 'number', location },
|
|
716
|
+
targetMax: { val: 1, literal: 'number', location },
|
|
717
|
+
location,
|
|
705
718
|
},
|
|
706
719
|
};
|
|
707
720
|
// By default, 'up_' is a managed primary key association.
|
|
@@ -710,18 +723,21 @@ function generate( model ) {
|
|
|
710
723
|
if (isDeprecatedEnabled( options, '_unmanagedUpInComponent' )) {
|
|
711
724
|
addProxyElements( art, keys, 'aspect-composition', target.name && location,
|
|
712
725
|
'up__', '@odata.containment.ignore' );
|
|
713
|
-
up.on = augmentEqual(
|
|
726
|
+
up.on = augmentEqual( location, 'up_', Object.values( keys ), 'up__' );
|
|
714
727
|
}
|
|
715
728
|
else {
|
|
716
|
-
up.key = { location
|
|
729
|
+
up.key = { location, val: true };
|
|
717
730
|
// managed associations must be explicitly set to not null
|
|
718
731
|
// even if target cardinality is 1..1
|
|
719
|
-
up.notNull = { location
|
|
732
|
+
up.notNull = { location, val: true };
|
|
720
733
|
}
|
|
721
734
|
|
|
722
735
|
dictAdd( art.elements, 'up_', up );
|
|
723
736
|
// Only for named aspects, use a new location; otherwise use the origin's one.
|
|
724
|
-
|
|
737
|
+
|
|
738
|
+
// To keep the locations of non-inferred original elements, do not set $inferred:
|
|
739
|
+
const enforceLocation = target.name || elem.$inferred;
|
|
740
|
+
addProxyElements( art, target.elements, 'aspect-composition', enforceLocation && location );
|
|
725
741
|
|
|
726
742
|
setLink( art, '_block', model.$internal );
|
|
727
743
|
model.definitions[entityName] = art;
|
|
@@ -742,7 +758,8 @@ function generate( model ) {
|
|
|
742
758
|
const origin = elements[name];
|
|
743
759
|
const proxy = linkToOrigin( origin, pname, null, null, location, true );
|
|
744
760
|
setLink( proxy, '_block', origin._block );
|
|
745
|
-
|
|
761
|
+
if (location)
|
|
762
|
+
proxy.$inferred = inferred;
|
|
746
763
|
if (origin.masked)
|
|
747
764
|
proxy.masked = Object.assign( { $inferred: 'include' }, origin.masked );
|
|
748
765
|
if (origin.key)
|
|
@@ -784,7 +801,7 @@ function generate( model ) {
|
|
|
784
801
|
}
|
|
785
802
|
|
|
786
803
|
function linkMainArtifact( location, absolute ) {
|
|
787
|
-
const r = { location };
|
|
804
|
+
const r = { location, path: [ { id: absolute, location } ] };
|
|
788
805
|
setArtifactLink( r, model.definitions[absolute] );
|
|
789
806
|
return r;
|
|
790
807
|
}
|
|
@@ -3,7 +3,12 @@
|
|
|
3
3
|
'use strict';
|
|
4
4
|
|
|
5
5
|
const { isBetaEnabled, forEachGeneric } = require('../base/model');
|
|
6
|
-
const {
|
|
6
|
+
const {
|
|
7
|
+
setLink,
|
|
8
|
+
annotationVal,
|
|
9
|
+
annotationIsFalse,
|
|
10
|
+
isDirectComposition,
|
|
11
|
+
} = require('./utils');
|
|
7
12
|
|
|
8
13
|
function kickStart( model ) {
|
|
9
14
|
const { options } = model;
|
|
@@ -122,10 +127,7 @@ function kickStart( model ) {
|
|
|
122
127
|
}
|
|
123
128
|
|
|
124
129
|
function tagCompositionTargets( elem ) {
|
|
125
|
-
|
|
126
|
-
if (elem.target && type &&
|
|
127
|
-
(type._artifact === model.definitions['cds.Composition'] ||
|
|
128
|
-
type.path?.[0].id === 'cds.Composition')) {
|
|
130
|
+
if (elem.target && isDirectComposition( elem )) {
|
|
129
131
|
// A target aspect would have already moved to property `targetAspect` in
|
|
130
132
|
// define.js (hm... more something for kick-start.js...)
|
|
131
133
|
// TODO: for safety, just use resolveUncheckedPath()
|