@sap/cds-compiler 3.8.2 → 3.9.4

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.
Files changed (82) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/bin/cdsc.js +2 -2
  3. package/doc/CHANGELOG_BETA.md +26 -5
  4. package/lib/api/.eslintrc.json +3 -2
  5. package/lib/api/options.js +3 -1
  6. package/lib/api/validate.js +1 -1
  7. package/lib/base/message-registry.js +28 -19
  8. package/lib/base/messages.js +6 -1
  9. package/lib/base/model.js +2 -2
  10. package/lib/checks/.eslintrc.json +1 -0
  11. package/lib/checks/actionsFunctions.js +6 -6
  12. package/lib/checks/annotationsOData.js +1 -1
  13. package/lib/checks/elements.js +28 -17
  14. package/lib/checks/foreignKeys.js +1 -1
  15. package/lib/checks/invalidTarget.js +1 -1
  16. package/lib/checks/onConditions.js +11 -6
  17. package/lib/checks/queryNoDbArtifacts.js +1 -1
  18. package/lib/checks/types.js +1 -1
  19. package/lib/checks/utils.js +1 -1
  20. package/lib/checks/validator.js +3 -2
  21. package/lib/compiler/assert-consistency.js +7 -2
  22. package/lib/compiler/base.js +8 -4
  23. package/lib/compiler/builtins.js +7 -0
  24. package/lib/compiler/checks.js +73 -6
  25. package/lib/compiler/define.js +10 -5
  26. package/lib/compiler/extend.js +910 -1711
  27. package/lib/compiler/finalize-parse-cdl.js +1 -1
  28. package/lib/compiler/generate.js +838 -0
  29. package/lib/compiler/index.js +2 -0
  30. package/lib/compiler/populate.js +2 -2
  31. package/lib/compiler/propagator.js +20 -8
  32. package/lib/compiler/resolve.js +3 -3
  33. package/lib/compiler/shared.js +3 -1
  34. package/lib/edm/annotations/genericTranslation.js +18 -8
  35. package/lib/edm/csn2edm.js +14 -14
  36. package/lib/edm/edm.js +25 -11
  37. package/lib/edm/edmPreprocessor.js +47 -23
  38. package/lib/edm/edmUtils.js +37 -9
  39. package/lib/gen/Dictionary.json +5 -7
  40. package/lib/gen/language.checksum +1 -1
  41. package/lib/gen/language.interp +3 -1
  42. package/lib/gen/language.tokens +24 -23
  43. package/lib/gen/languageLexer.interp +4 -1
  44. package/lib/gen/languageLexer.js +792 -784
  45. package/lib/gen/languageLexer.tokens +12 -11
  46. package/lib/gen/languageParser.js +3564 -3493
  47. package/lib/json/from-csn.js +28 -6
  48. package/lib/json/to-csn.js +10 -6
  49. package/lib/language/antlrParser.js +11 -3
  50. package/lib/language/genericAntlrParser.js +2 -1
  51. package/lib/language/language.g4 +14 -3
  52. package/lib/model/csnRefs.js +10 -5
  53. package/lib/model/csnUtils.js +41 -76
  54. package/lib/modelCompare/utils/.eslintrc.json +1 -1
  55. package/lib/optionProcessor.js +7 -4
  56. package/lib/render/.eslintrc.json +1 -1
  57. package/lib/render/toCdl.js +244 -168
  58. package/lib/render/toHdbcds.js +18 -10
  59. package/lib/render/toSql.js +24 -2
  60. package/lib/transform/db/.eslintrc.json +4 -3
  61. package/lib/transform/db/cdsPersistence.js +1 -1
  62. package/lib/transform/db/expansion.js +11 -6
  63. package/lib/transform/db/flattening.js +22 -15
  64. package/lib/transform/db/rewriteCalculatedElements.js +50 -29
  65. package/lib/transform/db/temporal.js +1 -1
  66. package/lib/transform/db/views.js +1 -1
  67. package/lib/transform/draft/db.js +1 -1
  68. package/lib/transform/draft/odata.js +3 -4
  69. package/lib/transform/forOdataNew.js +5 -6
  70. package/lib/transform/forRelationalDB.js +7 -7
  71. package/lib/transform/localized.js +1 -1
  72. package/lib/transform/odata/toFinalBaseType.js +6 -6
  73. package/lib/transform/odata/typesExposure.js +12 -3
  74. package/lib/transform/odata/utils.js +3 -0
  75. package/lib/transform/transformUtilsNew.js +11 -26
  76. package/lib/transform/translateAssocsToJoins.js +9 -9
  77. package/lib/transform/universalCsn/.eslintrc.json +3 -2
  78. package/lib/transform/universalCsn/coreComputed.js +1 -1
  79. package/lib/transform/universalCsn/universalCsnEnricher.js +6 -4
  80. package/lib/utils/file.js +3 -3
  81. package/lib/utils/moduleResolve.js +1 -1
  82. package/package.json +1 -1
@@ -0,0 +1,838 @@
1
+ // Generate: localized data and managed compositions
2
+
3
+ 'use strict';
4
+
5
+ const {
6
+ isDeprecatedEnabled,
7
+ forEachGeneric, forEachDefinition,
8
+ } = require('../base/model');
9
+ const { dictAdd } = require('../base/dictionaries');
10
+ const {
11
+ setLink,
12
+ setArtifactLink,
13
+ setAnnotation,
14
+ linkToOrigin,
15
+ setMemberParent,
16
+ augmentPath,
17
+ splitIntoPath,
18
+ isDirectComposition,
19
+ } = require('./utils');
20
+
21
+ function generate( model ) {
22
+ const { options } = model;
23
+ // Get simplified "resolve" functionality and the message function:
24
+ const {
25
+ error, warning, info,
26
+ } = model.$messageFunctions;
27
+ const {
28
+ resolvePath,
29
+ resolveUncheckedPath,
30
+ initArtifact,
31
+ extendArtifactBefore,
32
+ applyIncludes,
33
+ } = model.$functions;
34
+
35
+ const addTextsLanguageAssoc = checkTextsLanguageAssocOption(model, options);
36
+ const useTextsAspect = checkTextsAspect();
37
+
38
+ Object.keys( model.definitions ).forEach( processArtifact );
39
+
40
+ compositionChildPersistence();
41
+ return;
42
+
43
+ /**
44
+ * Process "composition of" artifacts.
45
+ *
46
+ * @param {string} name
47
+ */
48
+ function processArtifact( name ) {
49
+ const art = model.definitions[name];
50
+ if (!(art.$duplicates)) {
51
+ processAspectComposition( art );
52
+ if (art.kind === 'entity' && !art.query && art.elements)
53
+ // check potential entity parse error
54
+ processLocalizedData( art );
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Copy `@cds.persistence.skip` and `@cds.persistence.skip` from parent to child
60
+ * for managed compositions. This needs to be done after extensions, i.e. annotations,
61
+ * have been applied or `annotate E.comp` would not have an effect on `E.comp.subComp`.
62
+ */
63
+ function compositionChildPersistence() {
64
+ const processed = new WeakSet();
65
+ forEachDefinition(model, processCompositionPersistence);
66
+
67
+ function processCompositionPersistence( def ) {
68
+ if (def.$inferred === 'composition-entity' && !processed.has(def)) {
69
+ if (def._parent)
70
+ processCompositionPersistence(def._parent);
71
+ copyPersistenceAnnotations( def, def._parent );
72
+ processed.add(def);
73
+ }
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Check that special `sap.common.*` aspects for `.texts` entities are
79
+ * consistent with compiler expectations. Emits messages and returns
80
+ * false if the aspects are not valid.
81
+ *
82
+ * @return {boolean}
83
+ */
84
+ function checkTextsAspect() {
85
+ const textsAspect = model.definitions['sap.common.TextsAspect'];
86
+ if (!textsAspect)
87
+ return false;
88
+
89
+ const specialElements = { locale: { key: true } };
90
+
91
+ if (textsAspect.kind !== 'aspect' || !textsAspect.elements) {
92
+ error('def-invalid-texts-aspect', [ textsAspect.name.location, textsAspect ],
93
+ { '#': 'no-aspect', art: textsAspect });
94
+ return false;
95
+ }
96
+
97
+ let hasError = false;
98
+ if (addTextsLanguageAssoc && textsAspect.elements.language) {
99
+ const lang = textsAspect.elements.language;
100
+ error('def-unexpected-element', [ lang.name.location, lang ],
101
+ { option: 'addTextsLanguageAssoc', art: textsAspect, name: 'language' },
102
+ // eslint-disable-next-line max-len
103
+ '$(ART) is not used because option $(OPTION) conflicts with existing element $(NAME); remove either option or element' );
104
+ hasError = true;
105
+ }
106
+
107
+ for (const name in specialElements) {
108
+ const expected = specialElements[name];
109
+ const elem = textsAspect.elements[name];
110
+ if (!elem) {
111
+ error('def-invalid-texts-aspect', [ textsAspect.name.location, textsAspect ],
112
+ { '#': 'missing', art: textsAspect, name });
113
+ hasError = true;
114
+ }
115
+ else if (expected.key !== undefined && !!elem.key?.val !== expected.key) {
116
+ const loc = elem.key?.location || elem.name?.location || textsAspect.name.location;
117
+ error('def-invalid-texts-aspect', [ loc, elem ],
118
+ { '#': expected.key ? 'key' : 'no-key', art: elem });
119
+ hasError = true;
120
+ }
121
+ }
122
+
123
+ if (hasError) // avoid subsequent errors, if the special elements are already wrong
124
+ return false;
125
+
126
+ for (const name in textsAspect.elements) {
127
+ const elem = textsAspect.elements[name];
128
+ const include = elem.$inferred === 'include';
129
+ if (!specialElements[name] && elem.key) {
130
+ const loc = include ? elem.location : elem.key.location;
131
+ error( 'def-unexpected-key', [ loc, elem ],
132
+ { '#': !include ? 'std' : 'include', art: textsAspect } );
133
+ hasError = true;
134
+ }
135
+ else if (hasTruthyProp( elem, 'localized' )) {
136
+ // TODO: T:loc, i.e. "localized" from other type (needs resolver?)
137
+ // Not supported anyway, but important for recompilation (which fails correctly).
138
+ const loc = elem.localized?.location || elem.location;
139
+ error( 'def-unexpected-localized', [ loc, elem ],
140
+ { '#': !include ? 'std' : 'include', art: textsAspect } );
141
+ hasError = true;
142
+ }
143
+ else if (elem.targetAspect) {
144
+ error( 'def-unexpected-composition', [ elem.targetAspect.location, elem ],
145
+ { art: textsAspect },
146
+ '$(ART) can\'t have composition of aspects' );
147
+ hasError = true;
148
+ }
149
+ }
150
+
151
+ return !hasError;
152
+ }
153
+
154
+ // localized texts entities ---------------------------------------------------
155
+
156
+ /**
157
+ * Process localized data for `art`. This includes creating `.texts` entities
158
+ * and `locale` associations.
159
+ *
160
+ * @param {XSN.Artifact} art
161
+ */
162
+ function processLocalizedData( art ) {
163
+ const fioriAnno = art['@fiori.draft.enabled'];
164
+ const fioriEnabled = fioriAnno && (fioriAnno.val === undefined || fioriAnno.val);
165
+
166
+ const textsName = `${ art.name.absolute }.texts`;
167
+ const textsEntity = model.definitions[textsName];
168
+ const localized = localizedData( art, textsEntity, fioriEnabled );
169
+ if (!localized)
170
+ return;
171
+ if (textsEntity) // expanded localized data in source
172
+ return; // -> make it idempotent
173
+ createTextsEntity( art, textsName, localized, fioriEnabled );
174
+ addTextsAssociations( art, textsName, localized );
175
+ }
176
+
177
+ /**
178
+ * Returns `false`, if there is no localized data or an array of elements
179
+ * that are required for `.texts` entities such as keys and localized elements.
180
+ *
181
+ * @param {XSN.Artifact} art
182
+ * @param {XSN.Artifact|undefined} textsEntity
183
+ * @param {boolean} fioriEnabled
184
+ * @returns {false|XSN.Element[]}
185
+ */
186
+ function localizedData( art, textsEntity, fioriEnabled ) {
187
+ let keys = 0;
188
+ const textElems = [];
189
+ const conflictingElements = [];
190
+ // These elements are required or the localized-mechanism does not work.
191
+ // Other elements from sap.common.TextsAspect may be "overridden" as per
192
+ // usual include-mechanism.
193
+ const protectedElements = [ 'locale', 'texts', 'localized' ];
194
+ if (fioriEnabled)
195
+ protectedElements.push('ID_texts');
196
+ if (addTextsLanguageAssoc)
197
+ protectedElements.push('language');
198
+
199
+ for (const name in art.elements) {
200
+ const elem = art.elements[name];
201
+ if (elem.$duplicates)
202
+ return false; // no localized-data unfold with redefined elems
203
+ if (protectedElements.includes( name ))
204
+ conflictingElements.push( elem );
205
+
206
+ const isKey = elem.key && elem.key.val;
207
+ const isLocalized = hasTruthyProp( elem, 'localized' );
208
+
209
+ if (isKey) {
210
+ keys += 1;
211
+ textElems.push( elem );
212
+ }
213
+ else if (isLocalized) {
214
+ textElems.push( elem );
215
+ }
216
+
217
+ if (isKey && isLocalized) { // key with localized is wrong - ignore localized
218
+ const errpos = elem.localized || elem.type || elem.name;
219
+ warning( 'def-ignoring-localized-key', [ errpos.location, elem ], { keyword: 'localized' },
220
+ 'Keyword $(KEYWORD) is ignored for primary keys' );
221
+ }
222
+ }
223
+ if (textElems.length <= keys)
224
+ return false;
225
+
226
+ if (!keys) {
227
+ warning( 'def-expecting-key', [ art.name.location, art ], {},
228
+ 'No texts entity can be created when no key element exists' );
229
+ return false;
230
+ }
231
+
232
+ if (textsEntity) {
233
+ if (textsEntity.$duplicates)
234
+ return false;
235
+ if (textsEntity.kind !== 'entity' || textsEntity.query ||
236
+ // already have elements "texts" and "localized" (and optionally ID_texts)
237
+ conflictingElements.length !== 2 || art.elements.locale ||
238
+ (fioriEnabled && art.elements.ID_texts)) {
239
+ // TODO if we have too much time: check all elements of texts entity for safety
240
+ warning( null, [ art.name.location, art ], { art: textsEntity },
241
+ // eslint-disable-next-line max-len
242
+ 'Texts entity $(ART) can\'t be created as there is another definition with that name' );
243
+ info( null, [ textsEntity.name.location, textsEntity ], { art },
244
+ 'Texts entity for $(ART) can\'t be created with this definition' );
245
+ }
246
+ else if (!art._block || art._block.$frontend !== 'json') {
247
+ info( null, [ art.name.location, art ], {},
248
+ 'Localized data expansions has already been done' );
249
+ return textElems; // make double-compilation even with after toHana
250
+ }
251
+ else if (!art._block.$withLocalized && !options.$recompile) {
252
+ art._block.$withLocalized = true;
253
+ info( 'recalculated-text-entities', [ art.name.location, null ], {},
254
+ 'Input CSN contains expansions for localized data' );
255
+ return textElems; // make compilation idempotent
256
+ }
257
+ else {
258
+ return textElems;
259
+ }
260
+ }
261
+ for (const elem of conflictingElements) {
262
+ warning( null, [ elem.name.location, art ], { name: elem.name.id },
263
+ 'No texts entity can be created when element $(NAME) exists' );
264
+ }
265
+ return !textsEntity && !conflictingElements.length && textElems;
266
+ }
267
+
268
+ /**
269
+ * Create the `.texts` entity for the given base artifact.
270
+ *
271
+ * @param {XSN.Artifact} base
272
+ * @param {string} absolute
273
+ * @param {XSN.Element[]} textElems
274
+ * @param {boolean} fioriEnabled
275
+ */
276
+ function createTextsEntity( base, absolute, textElems, fioriEnabled ) {
277
+ const art = useTextsAspect
278
+ ? createTextsEntityWithInclude( base, absolute, fioriEnabled )
279
+ : createTextsEntityWithDefaultElements( base, absolute, fioriEnabled );
280
+ // both functions are rather similar...
281
+
282
+ const { location } = base.name;
283
+
284
+ if (addTextsLanguageAssoc) {
285
+ const language = {
286
+ name: { location, id: 'language' },
287
+ kind: 'element',
288
+ location,
289
+ type: augmentPath( location, 'cds.Association' ),
290
+ target: augmentPath( location, 'sap.common.Languages' ),
291
+ on: {
292
+ op: { val: '=', location },
293
+ args: [
294
+ { path: [ { id: 'language', location }, { id: 'code', location } ], location },
295
+ { path: [ { id: 'locale', location } ], location },
296
+ ],
297
+ location,
298
+ },
299
+ };
300
+ setLink( language, '_block', model.$internal );
301
+ dictAdd( art.elements, 'language', language );
302
+ }
303
+
304
+ // assertUnique array value, first entry is 'locale'
305
+ const assertUniqueValue = [];
306
+
307
+ for (const orig of textElems) {
308
+ const elem = linkToOrigin( orig, orig.name.id, art, 'elements' );
309
+ if (orig.key && orig.key.val) {
310
+ // elem.key = { val: fioriEnabled ? null : true, $inferred: 'localized', location };
311
+ // TODO: the previous would be better, but currently not supported in toCDL
312
+ if (!fioriEnabled) {
313
+ elem.key = { val: true, $inferred: 'localized', location };
314
+ // If the propagated elements remain key (that is not fiori.draft.enabled)
315
+ // they should be omitted from OData containment EDM
316
+ setAnnotation( elem, '@odata.containment.ignore', location );
317
+ }
318
+ else {
319
+ // add the former key paths to the unique constraint
320
+ assertUniqueValue.push({
321
+ path: [ { id: orig.name.id, location: orig.location } ],
322
+ location: orig.location,
323
+ });
324
+ }
325
+ }
326
+ if (hasTruthyProp( orig, 'localized' )) { // use location of LOCALIZED keyword
327
+ const localized = orig.localized || orig.type || orig.name;
328
+ elem.localized = { val: null, $inferred: 'localized', location: localized.location };
329
+ }
330
+ }
331
+
332
+ initArtifact( art );
333
+ if (art.includes) {
334
+ // add elements `locale`, etc. which are required below.
335
+ applyIncludes( art, art ); // TODO: rethink - can we avoid this if only new extend?
336
+ }
337
+
338
+ if (fioriEnabled) {
339
+ // The includes mechanism puts TextsAspect's elements before .texts' elements.
340
+ // Because ID_texts is not copied from TextsAspect, the order is messed
341
+ // up. Fix it.
342
+ const { elements } = art;
343
+ art.elements = Object.create(null);
344
+ const names = [ 'ID_texts', 'locale', ...Object.keys(elements) ];
345
+ for (const name of names)
346
+ art.elements[name] = elements[name];
347
+
348
+ const { locale } = art.elements;
349
+ assertUniqueValue.unshift({
350
+ path: [ { id: locale.name.id, location: locale.location } ],
351
+ location: locale.location,
352
+ });
353
+ setAnnotation( art, '@assert.unique.locale', art.location, assertUniqueValue, 'array' );
354
+ }
355
+
356
+ copyPersistenceAnnotations( art, base );
357
+ return art;
358
+ }
359
+
360
+ /**
361
+ * Create the `.texts` entity for the given base artifact.
362
+ * In contrast to createTextsEntityWithDefaultElements(), this one creates
363
+ * an include for `sap.common.TextsAspect`.
364
+ *
365
+ * Does NOT apply the include!
366
+ *
367
+ * @param {XSN.Artifact} base
368
+ * @param {string} absolute
369
+ * @param {boolean} fioriEnabled
370
+ */
371
+ function createTextsEntityWithInclude( base, absolute, fioriEnabled ) {
372
+ const textsAspectName = 'sap.common.TextsAspect';
373
+ const textsAspect = model.definitions['sap.common.TextsAspect'];
374
+ const elements = Object.create(null);
375
+ const { location } = base.name;
376
+ const art = {
377
+ kind: 'entity',
378
+ name: { path: splitIntoPath( location, absolute ), absolute, location },
379
+ includes: [ createInclude( textsAspectName, base.location ) ],
380
+ location: base.location,
381
+ elements,
382
+ $inferred: 'localized-entity',
383
+ };
384
+
385
+ if (!fioriEnabled) {
386
+ // To be compatible, we switch off draft without @fiori.draft.enabled
387
+ // TODO (next major version): remove?
388
+ setAnnotation( art, '@odata.draft.enabled', art.location, false );
389
+ }
390
+ else {
391
+ // @fiori.draft.enabled artifacts need default elements ID_texts and locale.
392
+ // `locale` is copied from `sap.common.TextsAspect`, but without "key".
393
+ const textId = {
394
+ name: { location, id: 'ID_texts' },
395
+ kind: 'element',
396
+ key: { val: true, location },
397
+ type: augmentPath( location, 'cds.UUID' ),
398
+ location,
399
+ };
400
+ dictAdd( art.elements, 'ID_texts', textId );
401
+
402
+ // "Early" include; only for element `locale`, which has its `key` property
403
+ // removed (or rather: it is not copied).
404
+ linkToOrigin( textsAspect.elements.locale, 'locale', art, 'elements', location );
405
+ }
406
+
407
+ if (addTextsLanguageAssoc && art.elements.language)
408
+ art.elements.language = undefined; // TODO: Message? Ignore?
409
+ // TODO: what is this necessary? We do not create a text entity in this case
410
+
411
+ setLink( art, '_block', model.$internal );
412
+ model.definitions[absolute] = art;
413
+ extendArtifactBefore( art ); // having extensions here would be wrong
414
+ return art;
415
+ }
416
+
417
+ /**
418
+ * @param {XSN.Artifact} base
419
+ * @param {string} absolute
420
+ * @param {boolean} fioriEnabled
421
+ */
422
+ function createTextsEntityWithDefaultElements( base, absolute, fioriEnabled ) {
423
+ const elements = Object.create(null);
424
+ const { location } = base.name;
425
+ const art = {
426
+ kind: 'entity',
427
+ name: { path: splitIntoPath( location, absolute ), absolute, location },
428
+ location: base.location,
429
+ elements,
430
+ $inferred: 'localized-entity',
431
+ };
432
+ // If there is a type `sap.common.Locale`, then use it as the type for the element `locale`.
433
+ // If not, use the default `cds.String` with a length of 14.
434
+ const hasLocaleType = model.definitions['sap.common.Locale']?.kind === 'type';
435
+ const locale = {
436
+ name: { location, id: 'locale' },
437
+ kind: 'element',
438
+ type: augmentPath( location, hasLocaleType ? 'sap.common.Locale' : 'cds.String' ),
439
+ location,
440
+ };
441
+ if (!hasLocaleType)
442
+ locale.length = { literal: 'number', val: 14, location };
443
+
444
+ if (!fioriEnabled) {
445
+ locale.key = { val: true, location };
446
+ // To be compatible, we switch off draft without @fiori.draft.enabled
447
+ // TODO (next major version): remove?
448
+ setAnnotation( art, '@odata.draft.enabled', art.location, false );
449
+ }
450
+ else {
451
+ const textId = {
452
+ name: { location, id: 'ID_texts' },
453
+ kind: 'element',
454
+ key: { val: true, location },
455
+ type: augmentPath( location, 'cds.UUID' ),
456
+ location,
457
+ };
458
+ dictAdd( art.elements, 'ID_texts', textId );
459
+ }
460
+ dictAdd( art.elements, 'locale', locale );
461
+
462
+ setLink( art, '_block', model.$internal );
463
+ model.definitions[absolute] = art;
464
+ extendArtifactBefore( art ); // having extensions here would be wrong
465
+ return art;
466
+ }
467
+
468
+ /**
469
+ * @param {XSN.Artifact} art
470
+ * @param {string} textsName
471
+ * @param {XSN.Element[]} textElems
472
+ */
473
+ function addTextsAssociations( art, textsName, textElems ) {
474
+ // texts : Composition of many Books.texts on texts.ID=ID;
475
+ /** @type {array} */
476
+ const keys = textElems.filter( e => e.key && e.key.val );
477
+ const { location } = art.name;
478
+ const texts = {
479
+ name: { location, id: 'texts' },
480
+ kind: 'element',
481
+ location,
482
+ $inferred: 'localized',
483
+ type: augmentPath( location, 'cds.Composition' ),
484
+ cardinality: { targetMax: { literal: 'string', val: '*', location }, location },
485
+ target: augmentPath( location, textsName ),
486
+ on: augmentEqual( location, 'texts', keys ),
487
+ };
488
+ setMemberParent( texts, 'texts', art, 'elements' );
489
+ setLink( texts, '_block', model.$internal );
490
+ // localized : Association to Books.texts on
491
+ // localized.ID=ID and localized.locale = $user.locale;
492
+ keys.push( [ 'localized.locale', '$user.locale' ] );
493
+ const localized = {
494
+ name: { location, id: 'localized' },
495
+ kind: 'element',
496
+ location,
497
+ $inferred: 'localized',
498
+ type: augmentPath( location, 'cds.Association' ),
499
+ target: augmentPath( location, textsName ),
500
+ on: augmentEqual( location, 'localized', keys ),
501
+ };
502
+ setMemberParent( localized, 'localized', art, 'elements' );
503
+ setLink( localized, '_block', model.$internal );
504
+ }
505
+
506
+ /**
507
+ * Create a structure that can be used as an item in `includes`.
508
+ *
509
+ * @param {string} name
510
+ * @param {XSN.Location} location
511
+ */
512
+ function createInclude( name, location ) {
513
+ const include = {
514
+ path: [ { id: name, location } ],
515
+ location,
516
+ };
517
+ setArtifactLink( include.path[0], model.definitions[name] );
518
+ setArtifactLink( include, model.definitions[name] );
519
+ return include;
520
+ }
521
+
522
+ /**
523
+ * Returns whether `art` directly or indirectly has the property 'prop',
524
+ * following the 'origin' and the 'type' (not involving elements).
525
+ *
526
+ * DON'T USE FOR ANNOTATIONS (see TODO below)
527
+ *
528
+ * TODO: we should issue a warning if we get localized via TYPE OF
529
+ * TODO: XSN: for anno short form, use { val: true, location, <no literal prop> }
530
+ * ...then this function also works with annotations
531
+ *
532
+ * @param {XSN.Artifact} art
533
+ * @param {string} prop
534
+ * @returns {boolean}
535
+ */
536
+ function hasTruthyProp( art, prop ) {
537
+ const processed = Object.create(null); // avoid infloops with circular refs
538
+ let name = art.name.absolute; // is ok, since no recursive type possible
539
+ while (art && !processed[name]) {
540
+ if (art[prop])
541
+ return art[prop].val;
542
+ processed[name] = art;
543
+ if (art._origin) {
544
+ art = art._origin;
545
+ if (!art.name) // anonymous aspect
546
+ return false;
547
+ name = art && art.name.absolute;
548
+ }
549
+ else if (art.type && art._block && art.type.scope !== 'typeOf') {
550
+ // TODO: also do something special for TYPE OF inside `art`s own elements
551
+ name = resolveUncheckedPath( art.type, 'type', art );
552
+ art = name && model.definitions[name];
553
+ }
554
+ else {
555
+ return false;
556
+ }
557
+ }
558
+ return false;
559
+ }
560
+
561
+ // managed composition of aspects ------------------------------------------
562
+
563
+ function processAspectComposition( base ) {
564
+ // TODO: we need to forbid COMPOSITION of entity w/o keys and ON anyway
565
+ // TODO: consider entity includes
566
+ // TODO: nested containment
567
+ // TODO: better do circular checks in the aspect!
568
+ if (base.kind !== 'entity' || base.query)
569
+ return;
570
+ const keys = baseKeys();
571
+ if (keys)
572
+ forEachGeneric( base, 'elements', expand ); // TODO: recursively here?
573
+ return;
574
+
575
+ function baseKeys() {
576
+ const k = Object.create(null);
577
+ for (const name in base.elements) {
578
+ const elem = base.elements[name];
579
+ if (elem.$duplicates)
580
+ return false; // no composition-of-type unfold with redefined elems
581
+ if (elem.key && elem.key.val)
582
+ k[name] = elem;
583
+ }
584
+ return k;
585
+ }
586
+
587
+ function expand( elem ) {
588
+ if (elem.target)
589
+ return;
590
+ let origin = elem;
591
+ // included element do not have target aspect directly
592
+ while (origin && !origin.targetAspect && origin._origin)
593
+ origin = origin._origin;
594
+ let target = origin.targetAspect;
595
+ if (target && target.path)
596
+ target = resolvePath( origin.targetAspect, 'compositionTarget', origin );
597
+ if (!target || !target.elements)
598
+ return;
599
+ const entityName = `${ base.name.absolute }.${ elem.name.id }`;
600
+ const entity = allowAspectComposition( target, elem, keys, entityName ) &&
601
+ createTargetEntity( target, elem, keys, entityName, base );
602
+ elem.target = {
603
+ location: (elem.targetAspect || elem).location,
604
+ $inferred: 'aspect-composition',
605
+ };
606
+ setArtifactLink( elem.target, entity );
607
+ if (entity) {
608
+ // Support using the up_ element in the generated entity to be used
609
+ // inside the anonymous aspect:
610
+ const { up_ } = target.$tableAliases;
611
+ // TODO: invalidate "up_" alias (at least further navigation) if it
612
+ // already has an _origin (when the managed composition is included)
613
+ if (up_)
614
+ setLink( up_, '_origin', entity.elements.up_ );
615
+ model.$compositionTargets[entity.name.absolute] = true;
616
+ processAspectComposition( entity );
617
+ processLocalizedData( entity );
618
+ }
619
+ }
620
+ }
621
+
622
+ /**
623
+ * @returns {boolean|0} `true`, if allowed, `false` if forbidden, `0` if circular containment.
624
+ */
625
+ function allowAspectComposition( target, elem, keys, entityName ) {
626
+ if (!target.elements || Object.values( target.elements ).some( e => e.$duplicates ))
627
+ return false; // no elements or with redefinitions
628
+ const location = elem.target && elem.target.location || elem.location;
629
+ if ((elem._main._upperAspects || []).includes( target ))
630
+ return 0; // circular containment of the same aspect
631
+
632
+ const keyNames = Object.keys( keys );
633
+ if (!keyNames.length) {
634
+ // TODO: for "inner aspect-compositions", signal already in type
635
+ error( null, [ location, elem ], { target },
636
+ 'An aspect $(TARGET) can\'t be used as target in an entity without keys' );
637
+ return false;
638
+ }
639
+ // if (keys.up_) { // only to be tested if we allow to provide a prefix, which could be ''
640
+ // // Cannot be in an "inner aspect-compositions" as it would already be wrong before
641
+ // // TODO: if anonymous type, use location of "up_" element
642
+ // // FUTURE: add sub info with location of "up_" element
643
+ // message( 'id', [location, elem], { target, name: 'up_' }, 'Error',
644
+ // 'An aspect $(TARGET) can't be used as target in an entity with a key named $(NAME)' );
645
+ // return false;
646
+ // }
647
+ if (target.elements.up_) {
648
+ // TODO: for "inner aspect-compositions", signal already in type
649
+ // TODO: if anonymous type, use location of "up_" element
650
+ // FUTURE: if named type, add sub info with location of "up_" element
651
+ error( null, [ location, elem ], { target, name: 'up_' },
652
+ 'An aspect $(TARGET) with an element named $(NAME) can\'t be used as target' );
653
+ return false;
654
+ }
655
+ if (model.definitions[entityName]) {
656
+ error( null, [ location, elem ], { art: entityName },
657
+ // eslint-disable-next-line max-len
658
+ 'Target entity $(ART) can\'t be created as there is another definition with this name' );
659
+ return false;
660
+ }
661
+ const names = Object.keys( target.elements )
662
+ .filter( n => n.startsWith('up__') && keyNames.includes( n.substring(4) ) );
663
+ if (names.length) {
664
+ // FUTURE: if named type, add sub info with location of "up_" element
665
+ error( null, [ location, elem ], { target: entityName, names }, {
666
+ std: 'Key elements $(NAMES) can\'t be added to $(TARGET) as these already exist',
667
+ one: 'Key element $(NAMES) can\'t be added to $(TARGET) as it already exist',
668
+ });
669
+ return false;
670
+ }
671
+
672
+ if (elem.type && !isDirectComposition(elem)) {
673
+ // Only issue warning for direct usages, not for projections, includes, etc.
674
+ // TODO: Make it configurable error; v4: error
675
+ warning( 'def-expected-comp-aspect', [ elem.type.location, elem ],
676
+ { prop: 'Composition of', otherprop: 'Association to' },
677
+ 'Expected $(PROP), but found $(OTHERPROP) for composition of aspect');
678
+ }
679
+
680
+ return true;
681
+ }
682
+
683
+ function createTargetEntity( target, elem, keys, entityName, base ) {
684
+ const { location } = elem.targetAspect || elem.target || elem;
685
+ elem.on = {
686
+ location,
687
+ op: { val: '=', location },
688
+ args: [
689
+ augmentPath( location, elem.name.id, 'up_' ),
690
+ augmentPath( location, '$self' ),
691
+ ],
692
+ $inferred: 'aspect-composition',
693
+ };
694
+
695
+ const elements = Object.create(null);
696
+ const art = {
697
+ kind: 'entity',
698
+ name: { path: splitIntoPath( location, entityName ), absolute: entityName, location },
699
+ location,
700
+ elements,
701
+ $inferred: 'composition-entity',
702
+ };
703
+ if (target.name) { // named target aspect
704
+ setLink( art, '_origin', target );
705
+ setLink( art, '_upperAspects', [ target, ...(elem._main._upperAspects || []) ] );
706
+ }
707
+ else {
708
+ setLink( art, '_origin', target );
709
+ // TODO: do we need to give the anonymous target aspect a kind and name?
710
+ setLink( art, '_upperAspects', elem._main._upperAspects || [] );
711
+ }
712
+
713
+ const up = { // elements.up_ = ...
714
+ name: { location, id: 'up_' },
715
+ kind: 'element',
716
+ location,
717
+ $inferred: 'aspect-composition',
718
+ type: augmentPath( location, 'cds.Association' ),
719
+ target: augmentPath( location, base.name.absolute ),
720
+ cardinality: {
721
+ targetMin: { val: 1, literal: 'number', location },
722
+ targetMax: { val: 1, literal: 'number', location },
723
+ location,
724
+ },
725
+ };
726
+ // By default, 'up_' is a managed primary key association.
727
+ // If 'up_' shall be rendered unmanaged, infer the parent
728
+ // primary keys and add the ON condition
729
+ if (isDeprecatedEnabled( options, '_unmanagedUpInComponent' )) {
730
+ addProxyElements( art, keys, 'aspect-composition', target.name && location,
731
+ 'up__', '@odata.containment.ignore' );
732
+ up.on = augmentEqual( location, 'up_', Object.values( keys ), 'up__' );
733
+ }
734
+ else {
735
+ up.key = { location, val: true };
736
+ // managed associations must be explicitly set to not null
737
+ // even if target cardinality is 1..1
738
+ up.notNull = { location, val: true };
739
+ }
740
+
741
+ dictAdd( art.elements, 'up_', up);
742
+ addProxyElements( art, target.elements, 'aspect-composition', target.name && location );
743
+
744
+ setLink( art, '_block', model.$internal );
745
+ model.definitions[entityName] = art;
746
+ initArtifact( art );
747
+
748
+ extendArtifactBefore( art ); // having extensions here would be wrong
749
+ // Copy persistence annotations from aspect.
750
+ copyPersistenceAnnotations( art, target ); // after chooseAnnotation()
751
+ return art;
752
+ }
753
+
754
+ function addProxyElements( proxyDict, elements, inferred, location, prefix = '', anno = '' ) {
755
+ // TODO: also use for includeMembers()?
756
+ for (const name in elements) {
757
+ const pname = `${ prefix }${ name }`;
758
+ const origin = elements[name];
759
+ const proxy = linkToOrigin( origin, pname, null, null, location || origin.location );
760
+ proxy.$inferred = inferred;
761
+ if (origin.masked)
762
+ proxy.masked = Object.assign( { $inferred: 'include' }, origin.masked );
763
+ if (origin.key)
764
+ proxy.key = Object.assign( { $inferred: 'include' }, origin.key );
765
+ if (anno)
766
+ setAnnotation( proxy, anno );
767
+ dictAdd( proxyDict.elements, pname, proxy );
768
+ }
769
+ }
770
+
771
+ /**
772
+ * Copy the annotations `@cds.persistence.skip`/`@cds.persistence.exists` from
773
+ * source to target if present on source but not target.
774
+ *
775
+ * @param {object} target
776
+ * @param {object} source
777
+ */
778
+ function copyPersistenceAnnotations( target, source ) {
779
+ if (!source)
780
+ return;
781
+
782
+ const copyExists = !isDeprecatedEnabled( options, 'eagerPersistenceForGeneratedEntities' );
783
+ if (copyExists)
784
+ copy( '@cds.persistence.exists' );
785
+ copy( '@cds.persistence.skip' );
786
+
787
+ function copy( anno ) {
788
+ if ( source[anno] && !target[anno] )
789
+ target[anno] = { ...source[anno], $inferred: 'parent-origin' };
790
+ }
791
+ }
792
+ }
793
+
794
+ function augmentEqual( location, assocname, relations, prefix = '' ) {
795
+ const args = relations.map( eq );
796
+ return (args.length === 1)
797
+ ? args[0]
798
+ : { op: { val: 'and', location }, args, location };
799
+
800
+ function eq( refs ) {
801
+ if (Array.isArray(refs))
802
+ return { op: { val: '=', location }, args: refs.map( ref ), location };
803
+
804
+ const { id } = refs.name;
805
+ return {
806
+ op: { val: '=', location },
807
+ args: [
808
+ { path: [ { id: assocname, location }, { id, location } ], location },
809
+ { path: [ { id: `${ prefix }${ id }`, location } ], location },
810
+ ],
811
+ location,
812
+ };
813
+ }
814
+ function ref( path ) {
815
+ return { path: path.split('.').map( id => ({ id, location }) ), location };
816
+ }
817
+ }
818
+
819
+ function checkTextsLanguageAssocOption( model, options ) {
820
+ const languages = model.definitions['sap.common.Languages'];
821
+ const commonLanguagesEntity = options.addTextsLanguageAssoc && languages?.elements?.code;
822
+
823
+ if (options.addTextsLanguageAssoc && !commonLanguagesEntity) {
824
+ const variant = !languages ? 'std' : 'code';
825
+ const loc = model.definitions['sap.common.Languages']?.name?.location || null;
826
+ model.$messageFunctions.info('api-ignoring-language-assoc', loc, {
827
+ '#': variant, option: 'addTextsLanguageAssoc', art: 'sap.common.Languages', name: 'code',
828
+ }, {
829
+ std: 'Ignoring option $(OPTION) because entity $(ART) is missing',
830
+ code: 'Ignoring option $(OPTION) because entity $(ART) is missing element $(NAME)',
831
+ });
832
+ }
833
+
834
+ return !!commonLanguagesEntity;
835
+ }
836
+
837
+
838
+ module.exports = generate;