@sap/cds-compiler 2.12.0 → 2.15.2

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