@sap/cds-compiler 2.12.0 → 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.
Files changed (118) hide show
  1. package/CHANGELOG.md +110 -15
  2. package/bin/cdsc.js +13 -13
  3. package/bin/cdsse.js +2 -2
  4. package/doc/CHANGELOG_BETA.md +13 -6
  5. package/doc/CHANGELOG_DEPRECATED.md +22 -6
  6. package/doc/NameResolution.md +21 -16
  7. package/lib/api/main.js +28 -63
  8. package/lib/api/options.js +3 -3
  9. package/lib/api/validate.js +0 -5
  10. package/lib/backends.js +15 -23
  11. package/lib/base/dictionaries.js +0 -8
  12. package/lib/base/error.js +26 -0
  13. package/lib/base/keywords.js +7 -17
  14. package/lib/base/location.js +9 -4
  15. package/lib/base/message-registry.js +25 -4
  16. package/lib/base/messages.js +16 -26
  17. package/lib/base/model.js +2 -63
  18. package/lib/base/optionProcessorHelper.js +158 -123
  19. package/lib/checks/annotationsOData.js +1 -1
  20. package/lib/checks/cdsPersistence.js +2 -1
  21. package/lib/checks/enricher.js +17 -1
  22. package/lib/checks/invalidTarget.js +3 -1
  23. package/lib/checks/managedWithoutKeys.js +3 -1
  24. package/lib/checks/selectItems.js +4 -4
  25. package/lib/checks/sql-snippets.js +27 -26
  26. package/lib/checks/types.js +1 -1
  27. package/lib/checks/validator.js +4 -7
  28. package/lib/compiler/assert-consistency.js +5 -3
  29. package/lib/compiler/builtins.js +8 -6
  30. package/lib/compiler/checks.js +14 -3
  31. package/lib/compiler/cycle-detector.js +1 -1
  32. package/lib/compiler/define.js +1103 -0
  33. package/lib/compiler/extend.js +983 -0
  34. package/lib/compiler/finalize-parse-cdl.js +231 -0
  35. package/lib/compiler/index.js +32 -13
  36. package/lib/compiler/kick-start.js +190 -0
  37. package/lib/compiler/moduleLayers.js +4 -4
  38. package/lib/compiler/populate.js +1226 -0
  39. package/lib/compiler/propagator.js +111 -46
  40. package/lib/compiler/resolve.js +1433 -0
  41. package/lib/compiler/shared.js +64 -37
  42. package/lib/compiler/tweak-assocs.js +529 -0
  43. package/lib/compiler/utils.js +197 -33
  44. package/lib/edm/.eslintrc.json +5 -0
  45. package/lib/edm/annotations/genericTranslation.js +5 -9
  46. package/lib/edm/annotations/preprocessAnnotations.js +2 -2
  47. package/lib/edm/csn2edm.js +9 -8
  48. package/lib/edm/edm.js +11 -12
  49. package/lib/edm/edmPreprocessor.js +137 -73
  50. package/lib/edm/edmUtils.js +116 -22
  51. package/lib/gen/Dictionary.json +10 -3
  52. package/lib/gen/language.checksum +1 -1
  53. package/lib/gen/language.interp +9 -1
  54. package/lib/gen/language.tokens +86 -83
  55. package/lib/gen/languageLexer.interp +10 -1
  56. package/lib/gen/languageLexer.js +860 -833
  57. package/lib/gen/languageLexer.tokens +78 -75
  58. package/lib/gen/languageParser.js +5282 -4265
  59. package/lib/json/from-csn.js +12 -1
  60. package/lib/json/to-csn.js +126 -66
  61. package/lib/language/docCommentParser.js +2 -2
  62. package/lib/language/genericAntlrParser.js +76 -3
  63. package/lib/language/language.g4 +297 -130
  64. package/lib/language/multiLineStringParser.js +5 -5
  65. package/lib/main.d.ts +468 -59
  66. package/lib/main.js +35 -9
  67. package/lib/model/api.js +3 -1
  68. package/lib/model/csnRefs.js +225 -156
  69. package/lib/model/csnUtils.js +192 -223
  70. package/lib/model/enrichCsn.js +70 -29
  71. package/lib/model/revealInternalProperties.js +27 -6
  72. package/lib/model/sortViews.js +2 -1
  73. package/lib/modelCompare/compare.js +17 -12
  74. package/lib/optionProcessor.js +5 -4
  75. package/lib/render/manageConstraints.js +35 -32
  76. package/lib/render/toCdl.js +73 -288
  77. package/lib/render/toHdbcds.js +25 -23
  78. package/lib/render/toSql.js +98 -41
  79. package/lib/render/utils/common.js +5 -10
  80. package/lib/render/utils/sql.js +4 -3
  81. package/lib/render/utils/stringEscapes.js +111 -0
  82. package/lib/sql-identifier.js +1 -1
  83. package/lib/transform/.eslintrc.json +5 -0
  84. package/lib/transform/db/.eslintrc.json +2 -0
  85. package/lib/transform/db/applyTransformations.js +35 -12
  86. package/lib/transform/db/assertUnique.js +1 -1
  87. package/lib/transform/db/associations.js +103 -305
  88. package/lib/transform/db/cdsPersistence.js +2 -2
  89. package/lib/transform/db/constraints.js +55 -52
  90. package/lib/transform/db/expansion.js +46 -24
  91. package/lib/transform/db/flattening.js +553 -102
  92. package/lib/transform/db/groupByOrderBy.js +3 -1
  93. package/lib/transform/db/transformExists.js +59 -6
  94. package/lib/transform/db/views.js +5 -4
  95. package/lib/transform/draft/.eslintrc.json +38 -0
  96. package/lib/transform/{db/draft.js → draft/db.js} +6 -5
  97. package/lib/transform/draft/odata.js +227 -0
  98. package/lib/transform/forHanaNew.js +67 -183
  99. package/lib/transform/forOdataNew.js +17 -171
  100. package/lib/transform/localized.js +34 -19
  101. package/lib/transform/odata/generateForeignKeyElements.js +1 -1
  102. package/lib/transform/odata/referenceFlattener.js +95 -89
  103. package/lib/transform/odata/structureFlattener.js +1 -1
  104. package/lib/transform/odata/toFinalBaseType.js +86 -12
  105. package/lib/transform/odata/typesExposure.js +5 -5
  106. package/lib/transform/odata/utils.js +2 -2
  107. package/lib/transform/transformUtilsNew.js +36 -22
  108. package/lib/transform/translateAssocsToJoins.js +2 -19
  109. package/lib/transform/universalCsn/.eslintrc.json +36 -0
  110. package/lib/transform/universalCsn/coreComputed.js +170 -0
  111. package/lib/transform/universalCsn/universalCsnEnricher.js +715 -0
  112. package/lib/transform/universalCsn/utils.js +63 -0
  113. package/lib/utils/objectUtils.js +30 -0
  114. package/package.json +1 -1
  115. package/share/messages/README.md +26 -0
  116. package/lib/compiler/definer.js +0 -2361
  117. package/lib/compiler/resolver.js +0 -3079
  118. package/lib/transform/universalCsnEnricher.js +0 -237
@@ -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;