@sap/cds-compiler 2.11.2 → 2.13.6
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 +175 -2
- package/bin/.eslintrc.json +1 -2
- package/bin/cds_update_identifiers.js +10 -8
- package/bin/cdsc.js +23 -17
- package/bin/cdsse.js +2 -2
- package/bin/cdsv2m.js +3 -2
- package/doc/CHANGELOG_ARCHIVE.md +1 -1
- package/doc/CHANGELOG_BETA.md +25 -6
- package/doc/CHANGELOG_DEPRECATED.md +22 -6
- package/doc/NameResolution.md +21 -16
- package/lib/api/main.js +32 -79
- package/lib/api/options.js +3 -2
- package/lib/api/validate.js +2 -1
- package/lib/backends.js +16 -26
- package/lib/base/dictionaries.js +0 -8
- package/lib/base/error.js +26 -0
- package/lib/base/keywords.js +10 -19
- package/lib/base/location.js +9 -4
- package/lib/base/message-registry.js +75 -9
- package/lib/base/messages.js +31 -35
- package/lib/base/model.js +2 -62
- package/lib/base/optionProcessorHelper.js +246 -183
- package/lib/checks/.eslintrc.json +2 -0
- package/lib/checks/actionsFunctions.js +2 -1
- package/lib/checks/annotationsOData.js +1 -1
- package/lib/checks/cdsPersistence.js +2 -1
- package/lib/checks/emptyOrOnlyVirtual.js +2 -2
- package/lib/checks/enricher.js +17 -1
- package/lib/checks/foreignKeys.js +4 -4
- package/lib/checks/invalidTarget.js +3 -1
- package/lib/checks/managedInType.js +4 -4
- package/lib/checks/managedWithoutKeys.js +3 -1
- package/lib/checks/queryNoDbArtifacts.js +1 -3
- package/lib/checks/selectItems.js +4 -4
- package/lib/checks/sql-snippets.js +94 -0
- package/lib/checks/types.js +1 -1
- package/lib/checks/unknownMagic.js +1 -1
- package/lib/checks/validator.js +12 -7
- package/lib/compiler/assert-consistency.js +12 -8
- package/lib/compiler/base.js +0 -1
- package/lib/compiler/builtins.js +42 -21
- package/lib/compiler/checks.js +46 -12
- package/lib/compiler/cycle-detector.js +1 -1
- package/lib/compiler/define.js +1103 -0
- package/lib/compiler/extend.js +983 -0
- package/lib/compiler/finalize-parse-cdl.js +231 -0
- package/lib/compiler/index.js +46 -39
- package/lib/compiler/kick-start.js +190 -0
- package/lib/compiler/moduleLayers.js +4 -4
- package/lib/compiler/populate.js +1226 -0
- package/lib/compiler/propagator.js +113 -47
- package/lib/compiler/resolve.js +1433 -0
- package/lib/compiler/shared.js +100 -65
- package/lib/compiler/tweak-assocs.js +529 -0
- package/lib/compiler/utils.js +215 -33
- package/lib/edm/.eslintrc.json +5 -0
- package/lib/edm/annotations/genericTranslation.js +38 -25
- package/lib/edm/annotations/preprocessAnnotations.js +3 -3
- package/lib/edm/csn2edm.js +10 -9
- package/lib/edm/edm.js +19 -20
- package/lib/edm/edmPreprocessor.js +166 -95
- package/lib/edm/edmUtils.js +127 -34
- package/lib/gen/Dictionary.json +92 -43
- package/lib/gen/language.checksum +1 -1
- package/lib/gen/language.interp +11 -1
- package/lib/gen/language.tokens +86 -82
- package/lib/gen/languageLexer.interp +18 -1
- package/lib/gen/languageLexer.js +925 -847
- package/lib/gen/languageLexer.tokens +78 -74
- package/lib/gen/languageParser.js +5434 -4298
- package/lib/json/from-csn.js +59 -17
- package/lib/json/to-csn.js +189 -71
- package/lib/language/antlrParser.js +3 -3
- package/lib/language/docCommentParser.js +3 -3
- package/lib/language/errorStrategy.js +26 -8
- package/lib/language/genericAntlrParser.js +144 -53
- package/lib/language/language.g4 +424 -200
- package/lib/language/multiLineStringParser.js +536 -0
- package/lib/main.d.ts +550 -61
- package/lib/main.js +38 -11
- package/lib/model/api.js +3 -1
- package/lib/model/csnRefs.js +322 -198
- package/lib/model/csnUtils.js +226 -370
- package/lib/model/enrichCsn.js +124 -69
- package/lib/model/revealInternalProperties.js +29 -7
- package/lib/model/sortViews.js +10 -2
- package/lib/modelCompare/compare.js +17 -12
- package/lib/optionProcessor.js +8 -3
- package/lib/render/.eslintrc.json +1 -2
- package/lib/render/DuplicateChecker.js +1 -1
- package/lib/render/manageConstraints.js +36 -33
- package/lib/render/toCdl.js +174 -275
- package/lib/render/toHdbcds.js +203 -122
- package/lib/render/toRename.js +7 -10
- package/lib/render/toSql.js +161 -82
- package/lib/render/utils/common.js +22 -8
- package/lib/render/utils/sql.js +10 -7
- package/lib/render/utils/stringEscapes.js +111 -0
- package/lib/sql-identifier.js +1 -1
- package/lib/transform/.eslintrc.json +5 -0
- package/lib/transform/braceExpression.js +4 -2
- package/lib/transform/db/.eslintrc.json +2 -0
- package/lib/transform/db/applyTransformations.js +212 -0
- package/lib/transform/db/assertUnique.js +1 -1
- package/lib/transform/db/associations.js +187 -0
- package/lib/transform/db/cdsPersistence.js +150 -0
- package/lib/transform/db/constraints.js +61 -56
- package/lib/transform/db/expansion.js +50 -29
- package/lib/transform/db/flattening.js +556 -106
- package/lib/transform/db/groupByOrderBy.js +3 -1
- package/lib/transform/db/temporal.js +236 -0
- package/lib/transform/db/transformExists.js +103 -28
- package/lib/transform/db/views.js +92 -44
- package/lib/transform/draft/.eslintrc.json +38 -0
- package/lib/transform/{db/draft.js → draft/db.js} +9 -7
- package/lib/transform/draft/odata.js +227 -0
- package/lib/transform/forHanaNew.js +98 -783
- package/lib/transform/forOdataNew.js +22 -175
- package/lib/transform/localized.js +36 -32
- package/lib/transform/odata/generateForeignKeyElements.js +3 -3
- package/lib/transform/odata/referenceFlattener.js +95 -89
- package/lib/transform/odata/structureFlattener.js +1 -1
- package/lib/transform/odata/toFinalBaseType.js +86 -12
- package/lib/transform/odata/typesExposure.js +5 -5
- package/lib/transform/odata/utils.js +2 -2
- package/lib/transform/transformUtilsNew.js +47 -33
- package/lib/transform/translateAssocsToJoins.js +13 -30
- package/lib/transform/universalCsn/.eslintrc.json +36 -0
- package/lib/transform/universalCsn/coreComputed.js +170 -0
- package/lib/transform/universalCsn/universalCsnEnricher.js +715 -0
- package/lib/transform/universalCsn/utils.js +63 -0
- package/lib/utils/file.js +8 -3
- package/lib/utils/objectUtils.js +30 -0
- package/lib/utils/timetrace.js +8 -2
- package/package.json +1 -1
- package/share/messages/README.md +26 -0
- package/lib/compiler/definer.js +0 -2349
- package/lib/compiler/resolver.js +0 -2922
- package/lib/transform/db/helpers.js +0 -58
- package/lib/transform/universalCsnEnricher.js +0 -67
|
@@ -0,0 +1,983 @@
|
|
|
1
|
+
// Extend, include, localized data and managed compositions
|
|
2
|
+
|
|
3
|
+
// Is handled together in this file because people want to extend the generated
|
|
4
|
+
// definitions in the future.
|
|
5
|
+
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
const { searchName, weakLocation } = require('../base/messages');
|
|
9
|
+
const {
|
|
10
|
+
isDeprecatedEnabled, isBetaEnabled,
|
|
11
|
+
forEachGeneric, forEachInOrder,
|
|
12
|
+
} = require('../base/model');
|
|
13
|
+
const { dictAdd } = require('../base/dictionaries');
|
|
14
|
+
const { kindProperties, dictKinds } = require('./base');
|
|
15
|
+
const {
|
|
16
|
+
setLink,
|
|
17
|
+
setArtifactLink,
|
|
18
|
+
annotateWith,
|
|
19
|
+
linkToOrigin,
|
|
20
|
+
setMemberParent,
|
|
21
|
+
dependsOnSilent,
|
|
22
|
+
augmentPath,
|
|
23
|
+
splitIntoPath,
|
|
24
|
+
} = require('./utils');
|
|
25
|
+
const { compareLayer, layer } = require('./moduleLayers');
|
|
26
|
+
|
|
27
|
+
function extend( model ) {
|
|
28
|
+
const { options } = model;
|
|
29
|
+
// Get simplified "resolve" functionality and the message function:
|
|
30
|
+
const {
|
|
31
|
+
message, error, warning, info,
|
|
32
|
+
} = model.$messageFunctions;
|
|
33
|
+
const {
|
|
34
|
+
resolvePath,
|
|
35
|
+
resolveUncheckedPath,
|
|
36
|
+
defineAnnotations,
|
|
37
|
+
attachAndEmitValidNames,
|
|
38
|
+
checkDefinitions,
|
|
39
|
+
initArtifact,
|
|
40
|
+
initMembers,
|
|
41
|
+
extensionsDict, // not a function - TODO
|
|
42
|
+
} = model.$functions;
|
|
43
|
+
|
|
44
|
+
Object.assign( model.$functions, {
|
|
45
|
+
lateExtensions,
|
|
46
|
+
} );
|
|
47
|
+
|
|
48
|
+
applyExtensions();
|
|
49
|
+
|
|
50
|
+
const commonLanguagesEntity // TODO: remove beta after a grace period
|
|
51
|
+
= (options.addTextsLanguageAssoc || isBetaEnabled( options, 'addTextsLanguageAssoc' )) &&
|
|
52
|
+
model.definitions['sap.common.Languages'];
|
|
53
|
+
const addTextsLanguageAssoc = !!(commonLanguagesEntity && commonLanguagesEntity.elements &&
|
|
54
|
+
commonLanguagesEntity.elements.code);
|
|
55
|
+
Object.keys( model.definitions ).forEach( processArtifact );
|
|
56
|
+
|
|
57
|
+
lateExtensions( false );
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Process "composition of" artifacts.
|
|
61
|
+
*
|
|
62
|
+
* @param {string} name
|
|
63
|
+
*/
|
|
64
|
+
function processArtifact( name ) {
|
|
65
|
+
const art = model.definitions[name];
|
|
66
|
+
if (!(art.$duplicates)) {
|
|
67
|
+
processAspectComposition( art );
|
|
68
|
+
if (art.kind === 'entity' && !art.query && art.elements)
|
|
69
|
+
// check potential entity parse error
|
|
70
|
+
processLocalizedData( art );
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* @param {XSN.Definition} art
|
|
76
|
+
* @param {string} prop
|
|
77
|
+
*/
|
|
78
|
+
function propagateEarly( art, prop ) {
|
|
79
|
+
if (art[prop])
|
|
80
|
+
return;
|
|
81
|
+
for (const ref of art.includes) {
|
|
82
|
+
const aspect = ref._artifact;
|
|
83
|
+
if (aspect) {
|
|
84
|
+
const anno = aspect[prop];
|
|
85
|
+
if (anno && (anno.val !== null || !art[prop]))
|
|
86
|
+
art[prop] = Object.assign( { $inferred: 'include' }, anno );
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// extend ------------------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Apply the extensions inside the extensionsDict on the model.
|
|
95
|
+
*
|
|
96
|
+
* Phase 1: context extends, 2: extends with structure includes, 3: extends
|
|
97
|
+
* without structure includes (in the case of cyclic includes)
|
|
98
|
+
*
|
|
99
|
+
* Before phase 1: all artifact extensions have been collected (even those
|
|
100
|
+
* inside extend context), only "empty" ones from structure includes are still unknown.
|
|
101
|
+
* After phase 1, all main artifacts are known, also "empty" extensions are known.
|
|
102
|
+
*/
|
|
103
|
+
function applyExtensions() {
|
|
104
|
+
let phase = 1; // TODO: basically remove phase 1
|
|
105
|
+
let extNames = Object.keys( extensionsDict ).sort();
|
|
106
|
+
// Remark: The sort() makes sure that an extend for artifact C.E is applied
|
|
107
|
+
// after the extend for C has been applied (which could have defined C.E).
|
|
108
|
+
// Looping over model.definitions in Phase 1 would miss the `extend
|
|
109
|
+
// context` for a context C.C defined in an `extend context C`.
|
|
110
|
+
//
|
|
111
|
+
// TODO: no need to sort anymore
|
|
112
|
+
while (extNames.length) {
|
|
113
|
+
const { length } = extNames;
|
|
114
|
+
for (const name of extNames) {
|
|
115
|
+
const art = model.definitions[name];
|
|
116
|
+
if (!art || art.kind === 'namespace') {
|
|
117
|
+
model.$lateExtensions[name] = extensionsDict[name];
|
|
118
|
+
delete extensionsDict[name];
|
|
119
|
+
}
|
|
120
|
+
else if (art.$duplicates) { // cannot extend redefinitions
|
|
121
|
+
delete extensionsDict[name];
|
|
122
|
+
}
|
|
123
|
+
else if (phase === 1
|
|
124
|
+
? extendContext( name, art )
|
|
125
|
+
: extendArtifact( extensionsDict[name], art, phase > 2 )) { // >2: no self-include
|
|
126
|
+
delete extensionsDict[name];
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
extNames = Object.keys( extensionsDict ); // no sort() required anymore
|
|
130
|
+
if (phase === 1)
|
|
131
|
+
phase = 2;
|
|
132
|
+
else if (extNames.length >= length)
|
|
133
|
+
phase = 3;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function extendContext( name, art ) {
|
|
138
|
+
// (ext.expectedKind == art.kind) already checked by parser except for context/service
|
|
139
|
+
if (!kindProperties[art.kind].artifacts) {
|
|
140
|
+
// no context or service => warn about context extensions
|
|
141
|
+
for (const ext of extensionsDict[name]) {
|
|
142
|
+
if (ext.expectedKind === 'context' || ext.expectedKind === 'service') {
|
|
143
|
+
const loc = ext.name.location;
|
|
144
|
+
// TODO: warning is enough
|
|
145
|
+
error( 'extend-with-artifacts', [ loc, ext ], { name, '#': ext.expectedKind }, {
|
|
146
|
+
std: 'Cannot extend non-context / non-service $(NAME) with artifacts',
|
|
147
|
+
service: 'Cannot extend non-service $(NAME) with artifacts',
|
|
148
|
+
context: 'Cannot extend non-context $(NAME) with artifacts',
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
for (const ext of extensionsDict[name]) {
|
|
156
|
+
setArtifactLink( ext.name, art );
|
|
157
|
+
checkDefinitions( ext, art, 'elements'); // error for elements etc
|
|
158
|
+
checkDefinitions( ext, art, 'enum');
|
|
159
|
+
checkDefinitions( ext, art, 'actions');
|
|
160
|
+
checkDefinitions( ext, art, 'params');
|
|
161
|
+
defineAnnotations( ext, art, ext._block, ext.kind );
|
|
162
|
+
}
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Extend artifact `art` by `extensions`. `noIncludes` can have values:
|
|
168
|
+
* - false: includes are applied, extend and annotate is performed
|
|
169
|
+
* - true: includes are not applied, extend and annotate is performed
|
|
170
|
+
* - 'gen': no includes and no extensions allowed, annotate is performed
|
|
171
|
+
*
|
|
172
|
+
* @param {XSN.Extension[]} extensions
|
|
173
|
+
* @param {XSN.Definition} art
|
|
174
|
+
* @param {boolean|'gen'} [noIncludes=false]
|
|
175
|
+
*/
|
|
176
|
+
function extendArtifact( extensions, art, noIncludes = false) {
|
|
177
|
+
if (!noIncludes && !(canApplyIncludes( art ) && extensions.every( canApplyIncludes )))
|
|
178
|
+
return false;
|
|
179
|
+
if (!art.query) {
|
|
180
|
+
model._entities.push( art ); // add structure with includes in dep order
|
|
181
|
+
art.$entity = ++model.$entity;
|
|
182
|
+
}
|
|
183
|
+
if (!noIncludes && art.includes)
|
|
184
|
+
applyIncludes( art, art );
|
|
185
|
+
extendMembers( extensions, art, noIncludes === 'gen' );
|
|
186
|
+
if (!noIncludes && art.includes) {
|
|
187
|
+
// early propagation of specific annotation assignments
|
|
188
|
+
propagateEarly( art, '@cds.autoexpose' );
|
|
189
|
+
propagateEarly( art, '@fiori.draft.enabled' );
|
|
190
|
+
}
|
|
191
|
+
// TODO: complain about element extensions inside projection
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function extendMembers( extensions, art, noExtend ) {
|
|
196
|
+
// TODO: do the whole extension stuff lazily if the elements are requested
|
|
197
|
+
const elemExtensions = [];
|
|
198
|
+
extensions.sort( compareLayer );
|
|
199
|
+
for (const ext of extensions) {
|
|
200
|
+
// console.log(message( 'id', [ext.location, ext], { art: ext.name._artifact },
|
|
201
|
+
// 'Info', 'EXT').toString())
|
|
202
|
+
if (!('_artifact' in ext.name)) { // not already applied
|
|
203
|
+
setArtifactLink( ext.name, art );
|
|
204
|
+
if (noExtend && ext.kind === 'extend') {
|
|
205
|
+
error( 'extend-for-generated', [ ext.name.location, ext ], { art },
|
|
206
|
+
'You can\'t use EXTEND on the generated $(ART)' );
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
if (ext.includes) {
|
|
210
|
+
// TODO: currently, re-compiling from gensrc does not give the exact
|
|
211
|
+
// element sequence - we need something like
|
|
212
|
+
// includes = ['Base1',3,'Base2']
|
|
213
|
+
// where 3 means adding the next 3 elements before applying include 'Base2'
|
|
214
|
+
if (art.includes)
|
|
215
|
+
art.includes.push(...ext.includes);
|
|
216
|
+
else
|
|
217
|
+
art.includes = [ ...ext.includes ];
|
|
218
|
+
applyIncludes( ext, art );
|
|
219
|
+
}
|
|
220
|
+
defineAnnotations( ext, art, ext._block, ext.kind );
|
|
221
|
+
// TODO: do we allow to add elements with array of {...}? If yes, adapt
|
|
222
|
+
initMembers( ext, art, ext._block ); // might set _extend, _annotate
|
|
223
|
+
dependsOnSilent(art, ext); // art depends silently on ext (inverse to normal dep!)
|
|
224
|
+
}
|
|
225
|
+
for (const name in ext.elements) {
|
|
226
|
+
const elem = ext.elements[name];
|
|
227
|
+
if (elem.kind === 'element') { // i.e. not extend or annotate
|
|
228
|
+
elemExtensions.push( elem );
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (ext.columns) // extend projection
|
|
234
|
+
extendColumns( ext, art );
|
|
235
|
+
}
|
|
236
|
+
if (elemExtensions.length > 1)
|
|
237
|
+
reportUnstableExtensions( elemExtensions );
|
|
238
|
+
|
|
239
|
+
[ 'elements', 'actions' ].forEach( (prop) => {
|
|
240
|
+
const dict = art._extend && art._extend[prop];
|
|
241
|
+
for (const name in dict) {
|
|
242
|
+
let obj = art;
|
|
243
|
+
if (obj.targetAspect)
|
|
244
|
+
obj = obj.targetAspect;
|
|
245
|
+
while (obj.items)
|
|
246
|
+
obj = obj.items;
|
|
247
|
+
const validDict = obj[prop] || prop === 'elements' && obj.enum;
|
|
248
|
+
const member = validDict[name];
|
|
249
|
+
if (!member)
|
|
250
|
+
extendNothing( dict[name], prop, name, art, validDict );
|
|
251
|
+
else if (!(member.$duplicates))
|
|
252
|
+
extendMembers( dict[name], member );
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Copy columns for EXTEND PROJECTION
|
|
259
|
+
*
|
|
260
|
+
* @param {XSN.Extension} ext
|
|
261
|
+
* @param {XSN.Artifact} art
|
|
262
|
+
*/
|
|
263
|
+
function extendColumns( ext, art ) {
|
|
264
|
+
// TODO: consider reportUnstableExtensions
|
|
265
|
+
const { location } = ext.name;
|
|
266
|
+
const { query } = art;
|
|
267
|
+
if (!query) {
|
|
268
|
+
if (art.kind !== 'annotate')
|
|
269
|
+
error( 'extend-columns', [ location, ext ], { art } );
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
if (!query.from || !query.from.path) {
|
|
273
|
+
error( 'extend-columns', [ location, ext ], { art } );
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
if (!query.columns)
|
|
277
|
+
query.columns = [ { location, val: '*' } ];
|
|
278
|
+
|
|
279
|
+
for (const column of ext.columns) {
|
|
280
|
+
setLink( column, '_block', ext._block );
|
|
281
|
+
query.columns.push(column);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function reportUnstableExtensions( extensions ) {
|
|
287
|
+
// Report 'Warning: Unstable element order due to repeated extensions'.
|
|
288
|
+
// Similar to chooseAssignment(), TODO there: also extra intralayer message
|
|
289
|
+
// as this is a modeling error
|
|
290
|
+
let lastExt = null;
|
|
291
|
+
let open = []; // the "highest" layers
|
|
292
|
+
for (const ext of extensions) {
|
|
293
|
+
const extLayer = layer( ext ) || { realname: '', _layerExtends: Object.create(null) };
|
|
294
|
+
if (!open.length) {
|
|
295
|
+
lastExt = ext;
|
|
296
|
+
open = [ extLayer.realname ];
|
|
297
|
+
}
|
|
298
|
+
else if (extLayer.realname === open[open.length - 1]) { // in same layer
|
|
299
|
+
if (lastExt) {
|
|
300
|
+
message( 'extend-repeated-intralayer', [ lastExt.location, lastExt ] );
|
|
301
|
+
lastExt = null;
|
|
302
|
+
}
|
|
303
|
+
message( 'extend-repeated-intralayer', [ ext.location, ext ] );
|
|
304
|
+
}
|
|
305
|
+
else {
|
|
306
|
+
if (lastExt && (open.length > 1 || !extLayer._layerExtends[open[0]])) {
|
|
307
|
+
// report for lastExt if that is unrelated to other open exts or current ext
|
|
308
|
+
message( 'extend-unrelated-layer', [ lastExt.location, lastExt ], {},
|
|
309
|
+
'Unstable element order due to other extension in unrelated layer' );
|
|
310
|
+
}
|
|
311
|
+
lastExt = ext;
|
|
312
|
+
open = open.filter( name => !extLayer._layerExtends[name] );
|
|
313
|
+
open.push( extLayer.realname );
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* @param {XSN.Extension[]} extensions
|
|
319
|
+
* @param {string} prop
|
|
320
|
+
* @param {string} name
|
|
321
|
+
* @param {XSN.Artifact} art
|
|
322
|
+
* @param {object} validDict
|
|
323
|
+
*/
|
|
324
|
+
function extendNothing( extensions, prop, name, art, validDict ) {
|
|
325
|
+
for (const ext of extensions) {
|
|
326
|
+
// TODO: use shared functionality with notFound in resolver.js
|
|
327
|
+
const { location } = ext.name;
|
|
328
|
+
const msg
|
|
329
|
+
= error( 'extend-undefined', [ location, ext ],
|
|
330
|
+
{ art: searchName( art, name, dictKinds[prop] ) },
|
|
331
|
+
{
|
|
332
|
+
std: 'Unknown $(ART) - nothing to extend',
|
|
333
|
+
// eslint-disable-next-line max-len
|
|
334
|
+
element: 'Artifact $(ART) has no element or enum $(MEMBER) - nothing to extend',
|
|
335
|
+
action: 'Artifact $(ART) has no action $(MEMBER) - nothing to extend',
|
|
336
|
+
} );
|
|
337
|
+
attachAndEmitValidNames(msg, validDict);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* @param {Function|false} [veryLate]
|
|
343
|
+
*/
|
|
344
|
+
function lateExtensions( veryLate ) {
|
|
345
|
+
for (const name in model.$lateExtensions) {
|
|
346
|
+
const art = model.definitions[name];
|
|
347
|
+
const exts = model.$lateExtensions[name];
|
|
348
|
+
if (art && art.kind !== 'namespace') {
|
|
349
|
+
if (art.builtin) {
|
|
350
|
+
for (const ext of exts)
|
|
351
|
+
info( 'anno-builtin', [ ext.name.location, ext ] );
|
|
352
|
+
}
|
|
353
|
+
// created texts entity, autoexposed entity
|
|
354
|
+
if (exts) {
|
|
355
|
+
extendArtifact( exts, art, 'gen' );
|
|
356
|
+
if (veryLate)
|
|
357
|
+
veryLate( art );
|
|
358
|
+
model.$lateExtensions[name] = null; // done
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
else if (veryLate) {
|
|
362
|
+
// Complain about unused extensions, i.e. those
|
|
363
|
+
// which do not point to a valid artifact
|
|
364
|
+
for (const ext of exts) {
|
|
365
|
+
delete ext.name.path[0]._artifact; // get message for root
|
|
366
|
+
// TODO: make resolvePath('extend'/'annotate') ignore namespaces
|
|
367
|
+
if (resolvePath( ext.name, ext.kind, ext )) { // should issue error/info
|
|
368
|
+
// should issue error for cds extensions (annotate ok)
|
|
369
|
+
if (art.kind === 'namespace') {
|
|
370
|
+
info( 'anno-namespace', [ ext.name.location, ext ], {},
|
|
371
|
+
'Namespaces can\'t be annotated' );
|
|
372
|
+
}
|
|
373
|
+
// Builtin annotations would be represented as annotations in to-csn.js
|
|
374
|
+
else if (art.builtin) {
|
|
375
|
+
info( 'anno-builtin', [ ext.name.location, ext ] );
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
// TODO: warning for context/service extension on non-correct
|
|
379
|
+
if (ext.kind === 'annotate')
|
|
380
|
+
delete ext.name._artifact; // make it be considered by extendArtifact()
|
|
381
|
+
}
|
|
382
|
+
// create "super" ANNOTATE containing all non-applied ones
|
|
383
|
+
const first = exts[0];
|
|
384
|
+
const { location } = first.name;
|
|
385
|
+
|
|
386
|
+
/** @type {XSN.Definition} */
|
|
387
|
+
const annotationArtifact = {
|
|
388
|
+
kind: 'annotate',
|
|
389
|
+
name: { path: [ { id: name, location } ], absolute: name, location },
|
|
390
|
+
location: first.location,
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
if (!model.extensions)
|
|
394
|
+
model.extensions = [];
|
|
395
|
+
|
|
396
|
+
model.extensions.push(annotationArtifact);
|
|
397
|
+
extendArtifact( exts, annotationArtifact ); // also sets _artifact link in extensions
|
|
398
|
+
// if one of the annotate statement mentions 'returns', assume it
|
|
399
|
+
// TODO: with warning/info?
|
|
400
|
+
for (const ext of exts) {
|
|
401
|
+
if (ext.$syntax === 'returns')
|
|
402
|
+
annotationArtifact.$syntax = 'returns';
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// includes ----------------------------------------------------------------
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* @param {XSN.Definition} art
|
|
412
|
+
*/
|
|
413
|
+
function canApplyIncludes( art ) {
|
|
414
|
+
if (art.includes) {
|
|
415
|
+
for (const ref of art.includes) {
|
|
416
|
+
const template = resolvePath( ref, 'include', art );
|
|
417
|
+
if (template && template.name.absolute in extensionsDict)
|
|
418
|
+
return false;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
return true;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* @param {XSN.Extension} ext
|
|
426
|
+
* @param {XSN.Artifact} art
|
|
427
|
+
*/
|
|
428
|
+
function applyIncludes( ext, art ) {
|
|
429
|
+
if (!art._ancestors)
|
|
430
|
+
setLink( art, '_ancestors', [] ); // recursive array of includes
|
|
431
|
+
for (const ref of ext.includes) {
|
|
432
|
+
const template = ref._artifact; // already resolved
|
|
433
|
+
if (template) {
|
|
434
|
+
if (template._ancestors)
|
|
435
|
+
art._ancestors.push( ...template._ancestors );
|
|
436
|
+
art._ancestors.push( template );
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
includeMembers( ext, 'elements', forEachInOrder, ext === art && art );
|
|
440
|
+
includeMembers( ext, 'actions', forEachGeneric, ext === art && art );
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* @param {XSN.Extension} ext
|
|
445
|
+
* @param {string} prop
|
|
446
|
+
* @param {function} forEach
|
|
447
|
+
* @param {XSN.Artifact} parent
|
|
448
|
+
*/
|
|
449
|
+
function includeMembers( ext, prop, forEach, parent ) {
|
|
450
|
+
// TODO two kind of messages:
|
|
451
|
+
// Error 'More than one include defines element "A"' (at include ref)
|
|
452
|
+
// Warning 'Overwrites definition from include "I" (at elem def)
|
|
453
|
+
const members = ext[prop];
|
|
454
|
+
ext[prop] = Object.create(null); // TODO: do not set actions property if there are none
|
|
455
|
+
for (const ref of ext.includes) {
|
|
456
|
+
const template = ref._artifact; // already resolved
|
|
457
|
+
if (template) { // be robust
|
|
458
|
+
forEach( template, prop, ( origin, name ) => {
|
|
459
|
+
if (members && name in members)
|
|
460
|
+
return; // TODO: warning for overwritten element
|
|
461
|
+
const elem = linkToOrigin( origin, name, parent, prop, weakLocation( ref.location ) );
|
|
462
|
+
if (!parent) // not yet set for EXTEND foo WITH bar
|
|
463
|
+
dictAdd( ext[prop], name, elem );
|
|
464
|
+
elem.$inferred = 'include';
|
|
465
|
+
if (origin.masked)
|
|
466
|
+
elem.masked = Object.assign( { $inferred: 'include' }, origin.masked );
|
|
467
|
+
if (origin.key)
|
|
468
|
+
elem.key = Object.assign( { $inferred: 'include' }, origin.key );
|
|
469
|
+
// TODO: also complain if elem is just defined in art
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
// TODO: expand elements having direct elements (if needed)
|
|
474
|
+
if (members) {
|
|
475
|
+
forEach( { [prop]: members }, prop, ( elem, name ) => {
|
|
476
|
+
dictAdd( ext[prop], name, elem );
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// localized texts entities
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* @param {XSN.Artifact} art
|
|
485
|
+
*/
|
|
486
|
+
function processLocalizedData( art ) {
|
|
487
|
+
const fioriAnno = art['@fiori.draft.enabled'];
|
|
488
|
+
const fioriEnabled = fioriAnno && (fioriAnno.val === undefined || fioriAnno.val);
|
|
489
|
+
|
|
490
|
+
const textsName = (isDeprecatedEnabled( options, 'generatedEntityNameWithUnderscore' ))
|
|
491
|
+
? `${ art.name.absolute }_texts`
|
|
492
|
+
: `${ art.name.absolute }.texts`;
|
|
493
|
+
const textsEntity = model.definitions[textsName];
|
|
494
|
+
const localized = localizedData( art, textsEntity, fioriEnabled );
|
|
495
|
+
if (!localized)
|
|
496
|
+
return;
|
|
497
|
+
if (textsEntity) // expanded localized data in source
|
|
498
|
+
return; // -> make it idempotent
|
|
499
|
+
createTextsEntity( art, textsName, localized, fioriEnabled );
|
|
500
|
+
addTextsAssociations( art, textsName, localized );
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* @param {XSN.Artifact} art
|
|
505
|
+
* @param {XSN.Artifact|undefined} textsEntity
|
|
506
|
+
* @param {boolean} fioriEnabled
|
|
507
|
+
*/
|
|
508
|
+
function localizedData( art, textsEntity, fioriEnabled ) {
|
|
509
|
+
let keys = 0;
|
|
510
|
+
const textElems = [];
|
|
511
|
+
const conflictingElements = [];
|
|
512
|
+
const protectedElements = [ 'locale', 'texts', 'localized' ];
|
|
513
|
+
if (fioriEnabled)
|
|
514
|
+
protectedElements.push('ID_texts');
|
|
515
|
+
if (addTextsLanguageAssoc)
|
|
516
|
+
protectedElements.push('language');
|
|
517
|
+
|
|
518
|
+
for (const name in art.elements) {
|
|
519
|
+
const elem = art.elements[name];
|
|
520
|
+
if (elem.$duplicates)
|
|
521
|
+
return false; // no localized-data unfold with redefined elems
|
|
522
|
+
if (protectedElements.includes( name ))
|
|
523
|
+
conflictingElements.push( elem );
|
|
524
|
+
|
|
525
|
+
const isKey = elem.key && elem.key.val;
|
|
526
|
+
const isLocalized = hasTruthyProp( elem, 'localized' );
|
|
527
|
+
|
|
528
|
+
if (isKey) {
|
|
529
|
+
keys += 1;
|
|
530
|
+
textElems.push( elem );
|
|
531
|
+
}
|
|
532
|
+
else if (isLocalized) {
|
|
533
|
+
textElems.push( elem );
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
if (isKey && isLocalized) { // key with localized is wrong - ignore localized
|
|
537
|
+
const errpos = elem.localized || elem.type || elem.name;
|
|
538
|
+
warning( 'localized-key', [ errpos.location, elem ], { keyword: 'localized' },
|
|
539
|
+
'Keyword $(KEYWORD) is ignored for primary keys' );
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
if (textElems.length <= keys)
|
|
543
|
+
return false;
|
|
544
|
+
|
|
545
|
+
if (!keys) {
|
|
546
|
+
warning( null, [ art.name.location, art ], {},
|
|
547
|
+
'No texts entity can be created when no key element exists' );
|
|
548
|
+
return false;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
if (textsEntity) {
|
|
552
|
+
if (textsEntity.$duplicates)
|
|
553
|
+
return false;
|
|
554
|
+
if (textsEntity.kind !== 'entity' || textsEntity.query ||
|
|
555
|
+
// already have elements "texts" and "localized" (and optionally ID_texts)
|
|
556
|
+
conflictingElements.length !== 2 || art.elements.locale ||
|
|
557
|
+
(fioriEnabled && art.elements.ID_texts)) {
|
|
558
|
+
// TODO if we have too much time: check all elements of texts entity for safety
|
|
559
|
+
warning( null, [ art.name.location, art ], { art: textsEntity },
|
|
560
|
+
// eslint-disable-next-line max-len
|
|
561
|
+
'Texts entity $(ART) can\'t be created as there is another definition with that name' );
|
|
562
|
+
info( null, [ textsEntity.name.location, textsEntity ], { art },
|
|
563
|
+
'Texts entity for $(ART) can\'t be created with this definition' );
|
|
564
|
+
}
|
|
565
|
+
else if (!art._block || art._block.$frontend !== 'json') {
|
|
566
|
+
info( null, [ art.name.location, art ], {},
|
|
567
|
+
'Localized data expansions has already been done' );
|
|
568
|
+
return textElems; // make double-compilation even with after toHana
|
|
569
|
+
}
|
|
570
|
+
else if (!art._block.$withLocalized && !options.$recompile) {
|
|
571
|
+
art._block.$withLocalized = true;
|
|
572
|
+
info( 'recalculated-text-entities', [ art.name.location, null ], {},
|
|
573
|
+
'Input CSN contains expansions for localized data' );
|
|
574
|
+
return textElems; // make compilation idempotent
|
|
575
|
+
}
|
|
576
|
+
else {
|
|
577
|
+
return textElems;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
for (const elem of conflictingElements) {
|
|
581
|
+
warning( null, [ elem.name.location, art ], { name: elem.name.id },
|
|
582
|
+
'No texts entity can be created when element $(NAME) exists' );
|
|
583
|
+
}
|
|
584
|
+
return !textsEntity && !conflictingElements.length && textElems;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* TODO: set _parent also for main artifacts!
|
|
589
|
+
*
|
|
590
|
+
* @param {XSN.Artifact} base
|
|
591
|
+
* @param {string} absolute
|
|
592
|
+
* @param {XSN.Element[]} textElems
|
|
593
|
+
* @param {boolean} fioriEnabled
|
|
594
|
+
*/
|
|
595
|
+
function createTextsEntity( base, absolute, textElems, fioriEnabled ) {
|
|
596
|
+
const elements = Object.create(null);
|
|
597
|
+
const { location } = base.name;
|
|
598
|
+
const art = {
|
|
599
|
+
kind: 'entity',
|
|
600
|
+
name: { path: splitIntoPath( location, absolute ), absolute, location },
|
|
601
|
+
location: base.location,
|
|
602
|
+
elements,
|
|
603
|
+
$inferred: 'localized-entity',
|
|
604
|
+
};
|
|
605
|
+
const locale = {
|
|
606
|
+
name: { location, id: 'locale' },
|
|
607
|
+
kind: 'element',
|
|
608
|
+
type: augmentPath( location, 'cds.String' ),
|
|
609
|
+
length: { literal: 'number', val: 14, location },
|
|
610
|
+
location,
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
if (!fioriEnabled) {
|
|
614
|
+
locale.key = { val: true, location };
|
|
615
|
+
// To be compatible, we switch off draft without @fiori.draft.enabled
|
|
616
|
+
// TODO (next major version): remove?
|
|
617
|
+
annotateWith( art, '@odata.draft.enabled', art.location, false );
|
|
618
|
+
}
|
|
619
|
+
else {
|
|
620
|
+
const textId = {
|
|
621
|
+
name: { location, id: 'ID_texts' },
|
|
622
|
+
kind: 'element',
|
|
623
|
+
key: { val: true, location },
|
|
624
|
+
type: augmentPath( location, 'cds.UUID' ),
|
|
625
|
+
location,
|
|
626
|
+
};
|
|
627
|
+
dictAdd( art.elements, 'ID_texts', textId );
|
|
628
|
+
}
|
|
629
|
+
if (isDeprecatedEnabled( options, 'generatedEntityNameWithUnderscore' ))
|
|
630
|
+
setLink( art, '_base', base );
|
|
631
|
+
|
|
632
|
+
dictAdd( art.elements, 'locale', locale );
|
|
633
|
+
if (addTextsLanguageAssoc) {
|
|
634
|
+
const language = {
|
|
635
|
+
name: { location, id: 'language' },
|
|
636
|
+
kind: 'element',
|
|
637
|
+
location,
|
|
638
|
+
type: augmentPath( location, 'cds.Association' ),
|
|
639
|
+
target: augmentPath( location, 'sap.common.Languages' ),
|
|
640
|
+
on: {
|
|
641
|
+
op: { val: '=', location },
|
|
642
|
+
args: [
|
|
643
|
+
{ path: [ { id: 'language', location }, { id: 'code', location } ], location },
|
|
644
|
+
{ path: [ { id: 'locale', location } ], location },
|
|
645
|
+
],
|
|
646
|
+
location,
|
|
647
|
+
},
|
|
648
|
+
};
|
|
649
|
+
setLink( language, '_block', model.$internal );
|
|
650
|
+
dictAdd( art.elements, 'language', language );
|
|
651
|
+
}
|
|
652
|
+
setLink( art, '_block', model.$internal );
|
|
653
|
+
model.definitions[absolute] = art;
|
|
654
|
+
initArtifact( art );
|
|
655
|
+
|
|
656
|
+
// assertUnique array value, first entry is 'locale'
|
|
657
|
+
const assertUniqueValue = [ {
|
|
658
|
+
path: [ { id: locale.name.id, location: locale.location } ],
|
|
659
|
+
location: locale.location,
|
|
660
|
+
} ];
|
|
661
|
+
|
|
662
|
+
for (const orig of textElems) {
|
|
663
|
+
const elem = linkToOrigin( orig, orig.name.id, art, 'elements' );
|
|
664
|
+
if (orig.key && orig.key.val) {
|
|
665
|
+
// elem.key = { val: fioriEnabled ? null : true, $inferred: 'localized', location };
|
|
666
|
+
// TODO: the previous would be better, but currently not supported in toCDL
|
|
667
|
+
if (!fioriEnabled) {
|
|
668
|
+
elem.key = { val: true, $inferred: 'localized', location };
|
|
669
|
+
// If the propagated elements remain key (that is not fiori.draft.enabled)
|
|
670
|
+
// they should be omitted from OData containment EDM
|
|
671
|
+
annotateWith( elem, '@odata.containment.ignore', location );
|
|
672
|
+
}
|
|
673
|
+
else {
|
|
674
|
+
// add the former key paths to the unique constraint
|
|
675
|
+
assertUniqueValue.push({
|
|
676
|
+
path: [ { id: orig.name.id, location: orig.location } ],
|
|
677
|
+
location: orig.location,
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
if (hasTruthyProp( orig, 'localized' )) { // use location of LOCALIZED keyword
|
|
682
|
+
const localized = orig.localized || orig.type || orig.name;
|
|
683
|
+
elem.localized = { val: null, $inferred: 'localized', location: localized.location };
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
if (fioriEnabled)
|
|
687
|
+
annotateWith( art, '@assert.unique.locale', art.location, assertUniqueValue, 'array' );
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* @param {XSN.Artifact} art
|
|
692
|
+
* @param {string} textsName
|
|
693
|
+
* @param {XSN.Element[]} textElems
|
|
694
|
+
*/
|
|
695
|
+
function addTextsAssociations( art, textsName, textElems ) {
|
|
696
|
+
// texts : Composition of many Books.texts on texts.ID=ID;
|
|
697
|
+
/** @type {array} */
|
|
698
|
+
const keys = textElems.filter( e => e.key && e.key.val );
|
|
699
|
+
const { location } = art.name;
|
|
700
|
+
const texts = {
|
|
701
|
+
name: { location, id: 'texts' },
|
|
702
|
+
kind: 'element',
|
|
703
|
+
location,
|
|
704
|
+
$inferred: 'localized',
|
|
705
|
+
type: augmentPath( location, 'cds.Composition' ),
|
|
706
|
+
cardinality: { targetMax: { literal: 'string', val: '*', location }, location },
|
|
707
|
+
target: augmentPath( location, textsName ),
|
|
708
|
+
on: augmentEqual( location, 'texts', keys ),
|
|
709
|
+
};
|
|
710
|
+
setMemberParent( texts, 'texts', art, 'elements' );
|
|
711
|
+
setLink( texts, '_block', model.$internal );
|
|
712
|
+
// localized : Association to Books.texts on
|
|
713
|
+
// localized.ID=ID and localized.locale = $user.locale;
|
|
714
|
+
keys.push( [ 'localized.locale', '$user.locale' ] );
|
|
715
|
+
const localized = {
|
|
716
|
+
name: { location, id: 'localized' },
|
|
717
|
+
kind: 'element',
|
|
718
|
+
location,
|
|
719
|
+
$inferred: 'localized',
|
|
720
|
+
type: augmentPath( location, 'cds.Association' ),
|
|
721
|
+
target: augmentPath( location, textsName ),
|
|
722
|
+
on: augmentEqual( location, 'localized', keys ),
|
|
723
|
+
};
|
|
724
|
+
setMemberParent( localized, 'localized', art, 'elements' );
|
|
725
|
+
setLink( localized, '_block', model.$internal );
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
/**
|
|
729
|
+
* @param {XSN.Artifact} art
|
|
730
|
+
* @param {string} prop
|
|
731
|
+
*/
|
|
732
|
+
function hasTruthyProp( art, prop ) {
|
|
733
|
+
// Returns whether art directly or indirectly has the property 'prop',
|
|
734
|
+
// following the 'origin' and the 'type' (not involving elements).
|
|
735
|
+
//
|
|
736
|
+
// TODO: we should issue a warning if we get localized via TYPE OF
|
|
737
|
+
// TODO XSN: for anno short form, use { val: true, location, <no literal prop> }
|
|
738
|
+
// ...then this function also works with annotations
|
|
739
|
+
const processed = Object.create(null); // avoid infloops with circular refs
|
|
740
|
+
let name = art.name.absolute; // is ok, since no recursive type possible
|
|
741
|
+
while (art && !processed[name]) {
|
|
742
|
+
if (art[prop])
|
|
743
|
+
return art[prop].val;
|
|
744
|
+
processed[name] = art;
|
|
745
|
+
if (art._origin) {
|
|
746
|
+
art = art._origin;
|
|
747
|
+
if (!art.name) // anonymous aspect
|
|
748
|
+
return false;
|
|
749
|
+
name = art && art.name.absolute;
|
|
750
|
+
}
|
|
751
|
+
else if (art.type && art._block && art.type.scope !== 'typeOf') {
|
|
752
|
+
// TODO: also do something special for TYPE OF inside `art`s own elements
|
|
753
|
+
name = resolveUncheckedPath( art.type, 'type', art );
|
|
754
|
+
art = name && model.definitions[name];
|
|
755
|
+
}
|
|
756
|
+
else {
|
|
757
|
+
return false;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
return false;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// managed composition of aspects ------------------------------------------
|
|
764
|
+
|
|
765
|
+
function processAspectComposition( base ) {
|
|
766
|
+
// TODO: we need to forbid COMPOSITION of entity w/o keys and ON anyway
|
|
767
|
+
// TODO: consider entity includes
|
|
768
|
+
// TODO: nested containment
|
|
769
|
+
// TODO: better do circular checks in the aspect!
|
|
770
|
+
if (base.kind !== 'entity' || base.query)
|
|
771
|
+
return;
|
|
772
|
+
const keys = baseKeys();
|
|
773
|
+
if (keys)
|
|
774
|
+
forEachGeneric( base, 'elements', expand ); // TODO: recursively here?
|
|
775
|
+
return;
|
|
776
|
+
|
|
777
|
+
function baseKeys() {
|
|
778
|
+
const k = Object.create(null);
|
|
779
|
+
for (const name in base.elements) {
|
|
780
|
+
const elem = base.elements[name];
|
|
781
|
+
if (elem.$duplicates)
|
|
782
|
+
return false; // no composition-of-type unfold with redefined elems
|
|
783
|
+
if (elem.key && elem.key.val)
|
|
784
|
+
k[name] = elem;
|
|
785
|
+
}
|
|
786
|
+
return k;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
function expand( elem ) {
|
|
790
|
+
if (elem.target)
|
|
791
|
+
return;
|
|
792
|
+
let origin = elem;
|
|
793
|
+
// included element do not have target aspect directly
|
|
794
|
+
while (origin && !origin.targetAspect && origin._origin)
|
|
795
|
+
origin = origin._origin;
|
|
796
|
+
let target = origin.targetAspect;
|
|
797
|
+
if (target && target.path)
|
|
798
|
+
target = resolvePath( origin.targetAspect, 'compositionTarget', origin );
|
|
799
|
+
if (!target || !target.elements)
|
|
800
|
+
return;
|
|
801
|
+
const entityName = (isDeprecatedEnabled( options, 'generatedEntityNameWithUnderscore' ))
|
|
802
|
+
? `${ base.name.absolute }_${ elem.name.id }`
|
|
803
|
+
: `${ base.name.absolute }.${ elem.name.id }`;
|
|
804
|
+
const entity = allowAspectComposition( target, elem, keys, entityName ) &&
|
|
805
|
+
createTargetEntity( target, elem, keys, entityName, base );
|
|
806
|
+
elem.target = {
|
|
807
|
+
location: (elem.targetAspect || elem).location,
|
|
808
|
+
$inferred: 'aspect-composition',
|
|
809
|
+
};
|
|
810
|
+
setArtifactLink( elem.target, entity );
|
|
811
|
+
if (entity) {
|
|
812
|
+
model.$compositionTargets[entity.name.absolute] = true;
|
|
813
|
+
processAspectComposition( entity );
|
|
814
|
+
processLocalizedData( entity );
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
/**
|
|
820
|
+
* @returns {boolean|0} `true`, if allowed, `false` if forbidden, `0` if circular containment.
|
|
821
|
+
*/
|
|
822
|
+
function allowAspectComposition( target, elem, keys, entityName ) {
|
|
823
|
+
if (!target.elements || Object.values( target.elements ).some( e => e.$duplicates ))
|
|
824
|
+
return false; // no elements or with redefinitions
|
|
825
|
+
const location = elem.target && elem.target.location || elem.location;
|
|
826
|
+
if ((elem._main._upperAspects || []).includes( target ))
|
|
827
|
+
return 0; // circular containment of the same aspect
|
|
828
|
+
|
|
829
|
+
const keyNames = Object.keys( keys );
|
|
830
|
+
if (!keyNames.length) {
|
|
831
|
+
// TODO: for "inner aspect-compositions", signal already in type
|
|
832
|
+
error( null, [ location, elem ], { target },
|
|
833
|
+
'An aspect $(TARGET) can\'t be used as target in an entity without keys' );
|
|
834
|
+
return false;
|
|
835
|
+
}
|
|
836
|
+
// if (keys.up_) { // only to be tested if we allow to provide a prefix, which could be ''
|
|
837
|
+
// // Cannot be in an "inner aspect-compositions" as it would already be wrong before
|
|
838
|
+
// // TODO: if anonymous type, use location of "up_" element
|
|
839
|
+
// // FUTURE: add sub info with location of "up_" element
|
|
840
|
+
// message( 'id', [location, elem], { target, name: 'up_' }, 'Error',
|
|
841
|
+
// 'An aspect $(TARGET) can't be used as target in an entity with a key named $(NAME)' );
|
|
842
|
+
// return false;
|
|
843
|
+
// }
|
|
844
|
+
if (target.elements.up_) {
|
|
845
|
+
// TODO: for "inner aspect-compositions", signal already in type
|
|
846
|
+
// TODO: if anonymous type, use location of "up_" element
|
|
847
|
+
// FUTURE: if named type, add sub info with location of "up_" element
|
|
848
|
+
error( null, [ location, elem ], { target, name: 'up_' },
|
|
849
|
+
'An aspect $(TARGET) with an element named $(NAME) can\'t be used as target' );
|
|
850
|
+
return false;
|
|
851
|
+
}
|
|
852
|
+
if (model.definitions[entityName]) {
|
|
853
|
+
error( null, [ location, elem ], { art: entityName },
|
|
854
|
+
// eslint-disable-next-line max-len
|
|
855
|
+
'Target entity $(ART) can\'t be created as there is another definition with this name' );
|
|
856
|
+
return false;
|
|
857
|
+
}
|
|
858
|
+
const names = Object.keys( target.elements )
|
|
859
|
+
.filter( n => n.startsWith('up__') && keyNames.includes( n.substring(4) ) );
|
|
860
|
+
if (names.length) {
|
|
861
|
+
// FUTURE: if named type, add sub info with location of "up_" element
|
|
862
|
+
error( null, [ location, elem ], { target: entityName, names }, {
|
|
863
|
+
std: 'Key elements $(NAMES) can\'t be added to $(TARGET) as these already exist',
|
|
864
|
+
one: 'Key element $(NAMES) can\'t be added to $(TARGET) as it already exist',
|
|
865
|
+
});
|
|
866
|
+
return false;
|
|
867
|
+
}
|
|
868
|
+
return true;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
function createTargetEntity( target, elem, keys, entityName, base ) {
|
|
872
|
+
const { location } = elem.targetAspect || elem.target || elem;
|
|
873
|
+
elem.on = {
|
|
874
|
+
location,
|
|
875
|
+
op: { val: '=', location },
|
|
876
|
+
args: [
|
|
877
|
+
augmentPath( location, elem.name.id, 'up_' ),
|
|
878
|
+
augmentPath( location, '$self' ),
|
|
879
|
+
],
|
|
880
|
+
$inferred: 'aspect-composition',
|
|
881
|
+
};
|
|
882
|
+
|
|
883
|
+
const elements = Object.create(null);
|
|
884
|
+
const art = {
|
|
885
|
+
kind: 'entity',
|
|
886
|
+
name: { path: splitIntoPath( location, entityName ), absolute: entityName, location },
|
|
887
|
+
location,
|
|
888
|
+
elements,
|
|
889
|
+
$inferred: 'composition-entity',
|
|
890
|
+
};
|
|
891
|
+
if (target.name) { // named target aspect
|
|
892
|
+
setLink( art, '_origin', target );
|
|
893
|
+
setLink( art, '_upperAspects', [ target, ...(elem._main._upperAspects || []) ] );
|
|
894
|
+
}
|
|
895
|
+
else {
|
|
896
|
+
setLink( art, '_origin', target );
|
|
897
|
+
// TODO: do we need to give the anonymous target aspect a kind and name?
|
|
898
|
+
setLink( art, '_upperAspects', elem._main._upperAspects || [] );
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
const up = { // elements.up_ = ...
|
|
902
|
+
name: { location, id: 'up_' },
|
|
903
|
+
kind: 'element',
|
|
904
|
+
location,
|
|
905
|
+
$inferred: 'aspect-composition',
|
|
906
|
+
type: augmentPath( location, 'cds.Association' ),
|
|
907
|
+
target: augmentPath( location, base.name.absolute ),
|
|
908
|
+
cardinality: {
|
|
909
|
+
targetMin: { val: 1, literal: 'number', location },
|
|
910
|
+
targetMax: { val: 1, literal: 'number', location },
|
|
911
|
+
location,
|
|
912
|
+
},
|
|
913
|
+
};
|
|
914
|
+
// By default, 'up_' is a managed primary key association.
|
|
915
|
+
// If 'up_' shall be rendered unmanaged, infer the parent
|
|
916
|
+
// primary keys and add the ON condition
|
|
917
|
+
if (isDeprecatedEnabled( options, 'unmanagedUpInComponent' )) {
|
|
918
|
+
addProxyElements( art, keys, 'aspect-composition', target.name && location,
|
|
919
|
+
'up__', '@odata.containment.ignore' );
|
|
920
|
+
up.on = augmentEqual( location, 'up_', Object.values( keys ), 'up__' );
|
|
921
|
+
}
|
|
922
|
+
else {
|
|
923
|
+
up.key = { location, val: true };
|
|
924
|
+
// managed associations must be explicitly set to not null
|
|
925
|
+
// even if target cardinality is 1..1
|
|
926
|
+
up.notNull = { location, val: true };
|
|
927
|
+
}
|
|
928
|
+
if (isDeprecatedEnabled( options, 'generatedEntityNameWithUnderscore' ))
|
|
929
|
+
setLink( art, '_base', base._base || base );
|
|
930
|
+
|
|
931
|
+
dictAdd( art.elements, 'up_', up);
|
|
932
|
+
addProxyElements( art, target.elements, 'aspect-composition', target.name && location );
|
|
933
|
+
|
|
934
|
+
setLink( art, '_block', model.$internal );
|
|
935
|
+
model.definitions[entityName] = art;
|
|
936
|
+
initArtifact( art );
|
|
937
|
+
return art;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
function addProxyElements( proxyDict, elements, inferred, location, prefix = '', anno = '' ) {
|
|
941
|
+
// TODO: also use for includeMembers()?
|
|
942
|
+
for (const name in elements) {
|
|
943
|
+
const pname = `${ prefix }${ name }`;
|
|
944
|
+
const origin = elements[name];
|
|
945
|
+
const proxy = linkToOrigin( origin, pname, null, null, location || origin.location );
|
|
946
|
+
proxy.$inferred = inferred;
|
|
947
|
+
if (origin.masked)
|
|
948
|
+
proxy.masked = Object.assign( { $inferred: 'include' }, origin.masked );
|
|
949
|
+
if (origin.key)
|
|
950
|
+
proxy.key = Object.assign( { $inferred: 'include' }, origin.key );
|
|
951
|
+
if (anno)
|
|
952
|
+
annotateWith( proxy, anno );
|
|
953
|
+
dictAdd( proxyDict.elements, pname, proxy );
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
function augmentEqual( location, assocname, relations, prefix = '' ) {
|
|
959
|
+
const args = relations.map( eq );
|
|
960
|
+
return (args.length === 1)
|
|
961
|
+
? args[0]
|
|
962
|
+
: { op: { val: 'and', location }, args, location };
|
|
963
|
+
|
|
964
|
+
function eq( refs ) {
|
|
965
|
+
if (Array.isArray(refs))
|
|
966
|
+
return { op: { val: '=', location }, args: refs.map( ref ), location };
|
|
967
|
+
|
|
968
|
+
const { id } = refs.name;
|
|
969
|
+
return {
|
|
970
|
+
op: { val: '=', location },
|
|
971
|
+
args: [
|
|
972
|
+
{ path: [ { id: assocname, location }, { id, location } ], location },
|
|
973
|
+
{ path: [ { id: `${ prefix }${ id }`, location } ], location },
|
|
974
|
+
],
|
|
975
|
+
location,
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
function ref( path ) {
|
|
979
|
+
return { path: path.split('.').map( id => ({ id, location }) ), location };
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
module.exports = extend;
|