@sap/cds-compiler 2.11.2 → 2.13.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. package/CHANGELOG.md +175 -2
  2. package/bin/.eslintrc.json +1 -2
  3. package/bin/cds_update_identifiers.js +10 -8
  4. package/bin/cdsc.js +23 -17
  5. package/bin/cdsse.js +2 -2
  6. package/bin/cdsv2m.js +3 -2
  7. package/doc/CHANGELOG_ARCHIVE.md +1 -1
  8. package/doc/CHANGELOG_BETA.md +25 -6
  9. package/doc/CHANGELOG_DEPRECATED.md +22 -6
  10. package/doc/NameResolution.md +21 -16
  11. package/lib/api/main.js +32 -79
  12. package/lib/api/options.js +3 -2
  13. package/lib/api/validate.js +2 -1
  14. package/lib/backends.js +16 -26
  15. package/lib/base/dictionaries.js +0 -8
  16. package/lib/base/error.js +26 -0
  17. package/lib/base/keywords.js +10 -19
  18. package/lib/base/location.js +9 -4
  19. package/lib/base/message-registry.js +75 -9
  20. package/lib/base/messages.js +31 -35
  21. package/lib/base/model.js +2 -62
  22. package/lib/base/optionProcessorHelper.js +246 -183
  23. package/lib/checks/.eslintrc.json +2 -0
  24. package/lib/checks/actionsFunctions.js +2 -1
  25. package/lib/checks/annotationsOData.js +1 -1
  26. package/lib/checks/cdsPersistence.js +2 -1
  27. package/lib/checks/emptyOrOnlyVirtual.js +2 -2
  28. package/lib/checks/enricher.js +17 -1
  29. package/lib/checks/foreignKeys.js +4 -4
  30. package/lib/checks/invalidTarget.js +3 -1
  31. package/lib/checks/managedInType.js +4 -4
  32. package/lib/checks/managedWithoutKeys.js +3 -1
  33. package/lib/checks/queryNoDbArtifacts.js +1 -3
  34. package/lib/checks/selectItems.js +4 -4
  35. package/lib/checks/sql-snippets.js +94 -0
  36. package/lib/checks/types.js +1 -1
  37. package/lib/checks/unknownMagic.js +1 -1
  38. package/lib/checks/validator.js +12 -7
  39. package/lib/compiler/assert-consistency.js +12 -8
  40. package/lib/compiler/base.js +0 -1
  41. package/lib/compiler/builtins.js +42 -21
  42. package/lib/compiler/checks.js +46 -12
  43. package/lib/compiler/cycle-detector.js +1 -1
  44. package/lib/compiler/define.js +1103 -0
  45. package/lib/compiler/extend.js +983 -0
  46. package/lib/compiler/finalize-parse-cdl.js +231 -0
  47. package/lib/compiler/index.js +46 -39
  48. package/lib/compiler/kick-start.js +190 -0
  49. package/lib/compiler/moduleLayers.js +4 -4
  50. package/lib/compiler/populate.js +1226 -0
  51. package/lib/compiler/propagator.js +113 -47
  52. package/lib/compiler/resolve.js +1433 -0
  53. package/lib/compiler/shared.js +100 -65
  54. package/lib/compiler/tweak-assocs.js +529 -0
  55. package/lib/compiler/utils.js +215 -33
  56. package/lib/edm/.eslintrc.json +5 -0
  57. package/lib/edm/annotations/genericTranslation.js +38 -25
  58. package/lib/edm/annotations/preprocessAnnotations.js +3 -3
  59. package/lib/edm/csn2edm.js +10 -9
  60. package/lib/edm/edm.js +19 -20
  61. package/lib/edm/edmPreprocessor.js +166 -95
  62. package/lib/edm/edmUtils.js +127 -34
  63. package/lib/gen/Dictionary.json +92 -43
  64. package/lib/gen/language.checksum +1 -1
  65. package/lib/gen/language.interp +11 -1
  66. package/lib/gen/language.tokens +86 -82
  67. package/lib/gen/languageLexer.interp +18 -1
  68. package/lib/gen/languageLexer.js +925 -847
  69. package/lib/gen/languageLexer.tokens +78 -74
  70. package/lib/gen/languageParser.js +5434 -4298
  71. package/lib/json/from-csn.js +59 -17
  72. package/lib/json/to-csn.js +189 -71
  73. package/lib/language/antlrParser.js +3 -3
  74. package/lib/language/docCommentParser.js +3 -3
  75. package/lib/language/errorStrategy.js +26 -8
  76. package/lib/language/genericAntlrParser.js +144 -53
  77. package/lib/language/language.g4 +424 -200
  78. package/lib/language/multiLineStringParser.js +536 -0
  79. package/lib/main.d.ts +550 -61
  80. package/lib/main.js +38 -11
  81. package/lib/model/api.js +3 -1
  82. package/lib/model/csnRefs.js +322 -198
  83. package/lib/model/csnUtils.js +226 -370
  84. package/lib/model/enrichCsn.js +124 -69
  85. package/lib/model/revealInternalProperties.js +29 -7
  86. package/lib/model/sortViews.js +10 -2
  87. package/lib/modelCompare/compare.js +17 -12
  88. package/lib/optionProcessor.js +8 -3
  89. package/lib/render/.eslintrc.json +1 -2
  90. package/lib/render/DuplicateChecker.js +1 -1
  91. package/lib/render/manageConstraints.js +36 -33
  92. package/lib/render/toCdl.js +174 -275
  93. package/lib/render/toHdbcds.js +203 -122
  94. package/lib/render/toRename.js +7 -10
  95. package/lib/render/toSql.js +161 -82
  96. package/lib/render/utils/common.js +22 -8
  97. package/lib/render/utils/sql.js +10 -7
  98. package/lib/render/utils/stringEscapes.js +111 -0
  99. package/lib/sql-identifier.js +1 -1
  100. package/lib/transform/.eslintrc.json +5 -0
  101. package/lib/transform/braceExpression.js +4 -2
  102. package/lib/transform/db/.eslintrc.json +2 -0
  103. package/lib/transform/db/applyTransformations.js +212 -0
  104. package/lib/transform/db/assertUnique.js +1 -1
  105. package/lib/transform/db/associations.js +187 -0
  106. package/lib/transform/db/cdsPersistence.js +150 -0
  107. package/lib/transform/db/constraints.js +61 -56
  108. package/lib/transform/db/expansion.js +50 -29
  109. package/lib/transform/db/flattening.js +556 -106
  110. package/lib/transform/db/groupByOrderBy.js +3 -1
  111. package/lib/transform/db/temporal.js +236 -0
  112. package/lib/transform/db/transformExists.js +103 -28
  113. package/lib/transform/db/views.js +92 -44
  114. package/lib/transform/draft/.eslintrc.json +38 -0
  115. package/lib/transform/{db/draft.js → draft/db.js} +9 -7
  116. package/lib/transform/draft/odata.js +227 -0
  117. package/lib/transform/forHanaNew.js +98 -783
  118. package/lib/transform/forOdataNew.js +22 -175
  119. package/lib/transform/localized.js +36 -32
  120. package/lib/transform/odata/generateForeignKeyElements.js +3 -3
  121. package/lib/transform/odata/referenceFlattener.js +95 -89
  122. package/lib/transform/odata/structureFlattener.js +1 -1
  123. package/lib/transform/odata/toFinalBaseType.js +86 -12
  124. package/lib/transform/odata/typesExposure.js +5 -5
  125. package/lib/transform/odata/utils.js +2 -2
  126. package/lib/transform/transformUtilsNew.js +47 -33
  127. package/lib/transform/translateAssocsToJoins.js +13 -30
  128. package/lib/transform/universalCsn/.eslintrc.json +36 -0
  129. package/lib/transform/universalCsn/coreComputed.js +170 -0
  130. package/lib/transform/universalCsn/universalCsnEnricher.js +715 -0
  131. package/lib/transform/universalCsn/utils.js +63 -0
  132. package/lib/utils/file.js +8 -3
  133. package/lib/utils/objectUtils.js +30 -0
  134. package/lib/utils/timetrace.js +8 -2
  135. package/package.json +1 -1
  136. package/share/messages/README.md +26 -0
  137. package/lib/compiler/definer.js +0 -2349
  138. package/lib/compiler/resolver.js +0 -2922
  139. package/lib/transform/db/helpers.js +0 -58
  140. package/lib/transform/universalCsnEnricher.js +0 -67
@@ -0,0 +1,1226 @@
1
+ // Populate views with elements, elements with association targets, ...
2
+
3
+ // The functionality in this file is the heart of the Core Compiler and the
4
+ // most complex part. It essentially implements the function `environment`
5
+ // used when resolving element references: when starting a references at a
6
+ // certain definition or element, which names are allowed next?
7
+ //
8
+ // To calculate that info, the compiler might needs the same info for other
9
+ // definitions. In other words: it calls itself recursively (using an iterative
10
+ // algorithm where appropriate). The be able to calculate that info on demand,
11
+ // the definitions need to have enough information, which must have been set in
12
+ // an earlier compiler phase. It is essential to do things in the right order.
13
+
14
+ // TODO: It might be that we need to call propagateKeyProps() and
15
+ // addImplicitForeignKeys() in populate.js, as we might need to know the
16
+ // foreign keys in populate.js (foreign key access w/o JOINs).
17
+
18
+ 'use strict';
19
+
20
+ const {
21
+ isDeprecatedEnabled,
22
+ isBetaEnabled,
23
+ forEachDefinition,
24
+ forEachMember,
25
+ forEachGeneric,
26
+ } = require('../base/model');
27
+ const {
28
+ dictAdd, dictAddArray,
29
+ } = require('../base/dictionaries');
30
+ const { dictLocation } = require('../base/location');
31
+ const { weakLocation } = require('../base/messages');
32
+
33
+ const { kindProperties } = require('./base');
34
+ const {
35
+ pushLink,
36
+ setLink,
37
+ setArtifactLink,
38
+ annotationVal,
39
+ augmentPath,
40
+ splitIntoPath,
41
+ linkToOrigin,
42
+ setMemberParent,
43
+ dependsOn,
44
+ traverseQueryPost,
45
+ setExpandStatus,
46
+ } = require('./utils');
47
+
48
+ const $inferred = Symbol.for('cds.$inferred');
49
+
50
+
51
+ // Export function of this file.
52
+ function populate( model ) {
53
+ const { options } = model;
54
+ // Get shared functionality and the message function:
55
+ const {
56
+ info, warning, error, message,
57
+ } = model.$messageFunctions;
58
+ const {
59
+ resolvePath,
60
+ attachAndEmitValidNames,
61
+ initArtifact,
62
+ projectionAncestor,
63
+ } = model.$functions;
64
+ model.$volatileFunctions.environment = environment;
65
+ Object.assign( model.$functions, {
66
+ effectiveType,
67
+ directType,
68
+ resolveType,
69
+ populateQuery,
70
+ } );
71
+
72
+
73
+ /** @type {any} may also be a boolean */
74
+ let newAutoExposed = [];
75
+
76
+ // behavior depending on option `deprecated`:
77
+ const enableExpandElements = !isDeprecatedEnabled( options, 'noElementsExpansion' );
78
+ // TODO: we should get rid of noElementsExpansion soon; both
79
+ // beta.nestedProjections and beta.universalCsn do not work with it.
80
+ const scopedRedirections
81
+ = enableExpandElements &&
82
+ !isDeprecatedEnabled( options, 'generatedEntityNameWithUnderscore' ) &&
83
+ !isDeprecatedEnabled( options, 'shortAutoexposed' ) &&
84
+ !isDeprecatedEnabled( options, 'longAutoexposed' ) &&
85
+ !isDeprecatedEnabled( options, 'noInheritedAutoexposeViaComposition' ) &&
86
+ !isDeprecatedEnabled( options, 'noScopedRedirections' );
87
+ const autoexposeViaComposition
88
+ = (isDeprecatedEnabled( options, 'noInheritedAutoexposeViaComposition' ))
89
+ ? 'Composition'
90
+ : true;
91
+ const redirectInSubQueries = isDeprecatedEnabled( options, 'redirectInSubQueries' );
92
+
93
+ forEachDefinition( model, traverseElementEnvironments );
94
+ while (newAutoExposed.length) {
95
+ // console.log( newAutoExposed.map( a => a.name.absolute ) )
96
+ const all = newAutoExposed;
97
+ newAutoExposed = [];
98
+ all.forEach( traverseElementEnvironments );
99
+ }
100
+ newAutoExposed = true; // internal error if auto-expose after here
101
+ return;
102
+
103
+
104
+ //--------------------------------------------------------------------------
105
+ // The central functions for path resolution - must work on-demand
106
+ //--------------------------------------------------------------------------
107
+ // Phase 2: call populateView(), which also works on-demand
108
+
109
+ function traverseElementEnvironments( art ) {
110
+ populateView( art );
111
+ environment( art );
112
+ forEachMember( art, traverseElementEnvironments );
113
+ }
114
+
115
+ // Return effective search environment provided by artifact `art`, i.e. the
116
+ // `artifacts` or `elements` dictionary. For the latter, follow the `type`
117
+ // chain and resolve the association `target`. View elements are calculated
118
+ // on demand.
119
+ function environment( art, location, user, assocSpec ) {
120
+ if (!art)
121
+ return Object.create(null);
122
+ const env = navigationEnv( art, location, user, assocSpec );
123
+ return env && env.elements || Object.create(null);
124
+ }
125
+
126
+ function navigationEnv( art, location, user, assocSpec ) {
127
+ let type = effectiveType(art) || art;
128
+ // console.log( info(null, [ art.location, art ], { art, type }, 'ENV')
129
+ // .toString(), art.elements && Object.keys(art.elements))
130
+ if (type.target) {
131
+ type = resolvePath( type.target, 'target', type );
132
+ if (!type) {
133
+ if (type === 0 && location)
134
+ dependsOn( art, art, (art.target || art.type).location );
135
+ return type;
136
+ }
137
+ // TODO: combine this with setTargetReferenceKey&Co in getPathItem?
138
+ else if (assocSpec === false) { // TODO: else warning for assoc usage
139
+ error( null, [ location, user ], {},
140
+ 'Following an association is not allowed in an association key definition' );
141
+ }
142
+ else if (assocSpec && user) {
143
+ dependsOn( user, type, location );
144
+ }
145
+ }
146
+ populateView( type );
147
+ return type;
148
+ }
149
+
150
+ // Follow the `type` chain, i.e. derived types and TYPE OF, stop just before
151
+ // built-in types (otherwise, we would loose type parameters). Return that
152
+ // type and set it as property `_effectiveType` on all artifacts on the chain.
153
+ // TODO: clarify for (query) elements without type: self, not undefined - also for entities!
154
+ // TODO: directly "propagate" (with implicit redirection the targets), also
155
+ // "proxy-copy" elements
156
+
157
+ // In v2, the name-resolution relevant properties (elements,items,target) are
158
+ // proxy-copied, i.e. the effectiveType of an artifact is always the artifact
159
+ // itself. Except for the situation with recursive element expansions; then
160
+ // we have element: 0, and use original final type.
161
+ function effectiveType( art ) {
162
+ if ('_effectiveType' in art)
163
+ return art._effectiveType;
164
+
165
+ // console.log(message( null, art.location, art, {}, 'Info','FT').toString())
166
+ const chain = [];
167
+ while (art && !('_effectiveType' in art) &&
168
+ (art.type || art._origin || art.value && art.value.path) &&
169
+ // TODO: really stop at art.enum?
170
+ !art.target && !art.enum && !art.elements && !art.items) {
171
+ chain.push( art );
172
+ setLink( art, '_effectiveType', 0 ); // initial setting in case of cycles
173
+ art = directType( art );
174
+ }
175
+ if (art) {
176
+ if ('_effectiveType' in art) { // is the case for builtins
177
+ art = art._effectiveType;
178
+ }
179
+ else {
180
+ setLink( art, '_effectiveType', art );
181
+ if (art.expand && !art.value && !art.elements)
182
+ initFromColumns( art, art.expand );
183
+ // When not blocked (by future origin = false) and not REDIRECTED TO and not MIXIN
184
+ // try to implicitly redirect explicitly provided target:
185
+ else if (art.target && art._origin == null && !art.value && art.kind !== 'mixin')
186
+ redirectImplicitly( art, art );
187
+ }
188
+ }
189
+ chain.reverse();
190
+ if (!art) {
191
+ for (const a of chain)
192
+ setLink( a, '_effectiveType', art );
193
+ }
194
+ else {
195
+ let eType = art;
196
+ if (eType._outer)
197
+ eType = effectiveType( eType._outer );
198
+ // collect the "latest" cardinality (calculate lazyly if necessary)
199
+ let cardinality = art.cardinality ||
200
+ art._effectiveType && (() => getCardinality( art._effectiveType ));
201
+ let prev = art;
202
+ for (const a of chain) {
203
+ if (a.cardinality)
204
+ cardinality = a.cardinality;
205
+ if (a.expand && expandFromColumns( a, art, cardinality ) ||
206
+ art.target && redirectImplicitly( a, art ) ||
207
+ art.elements && expandElements( a, art, eType ) ||
208
+ art.items && expandItems( a, art, eType ))
209
+ art = a;
210
+ else if (art.enum && expandEnum( a, prev ))
211
+ prev = a; // do not set art - effective type is base
212
+ setLink( a, '_effectiveType', art );
213
+ }
214
+ }
215
+ return art;
216
+ }
217
+
218
+ // TODO: test it in combination with top-level CAST function
219
+ function directType( art ) {
220
+ // Be careful when using it with art.target or art.enum or art.elements
221
+ if (art._origin || art.builtin)
222
+ return art._origin;
223
+ if (art.type)
224
+ return resolveType( art.type, art );
225
+ // console.log( 'EXPR-IN', art.kind, refString(art.name) )
226
+ if (!art._main || !art.value || !art.value.path)
227
+ return undefined;
228
+ if (art._pathHead && art.value) {
229
+ setLink( art, '_origin', resolvePath( art.value, 'expr', art, null ) );
230
+ return art._origin;
231
+ }
232
+ const query = userQuery( art ) || art._parent;
233
+ if (query.kind !== 'select')
234
+ return undefined;
235
+ // Reached an element in a query which is a simple ref -> return referred artifact
236
+ // TODO: remember that we still have to resolve path arguments and filters
237
+ return setLink( art, '_origin',
238
+ resolvePath( art.value, 'expr', art, query._combined ) );
239
+ // console.log( 'EXPR-OUT', art.value._artifact.kind, refString(art.val ue._artifact.name) );
240
+ }
241
+
242
+ function resolveType( ref, user ) {
243
+ if ('_artifact' in ref)
244
+ return ref._artifact;
245
+ if (ref.scope === 'typeOf') {
246
+ let struct = user;
247
+ while (struct.kind === 'element')
248
+ struct = struct._parent;
249
+ if (struct.kind === 'select') {
250
+ message( 'type-unexpected-typeof', [ ref.location, user ],
251
+ { keyword: 'type of', '#': struct.kind } );
252
+ // we actually refer to an element in _combined; TODO: return null if
253
+ // not configurable; would produce illegal CSN with sub queries in FROM
254
+ }
255
+ else if (struct !== user._main) {
256
+ message( 'type-unexpected-typeof', [ ref.location, user ],
257
+ { keyword: 'type of', '#': struct.kind } );
258
+ return setArtifactLink( ref, null );
259
+ }
260
+ return resolvePath( ref, 'typeOf', user );
261
+ }
262
+ while (user._outer) // in items
263
+ user = user._outer;
264
+ if (user.kind === 'event')
265
+ return resolvePath( ref, 'eventType', user );
266
+ if (user.kind === 'param' && user._parent &&
267
+ (user._parent.kind === 'action' || user._parent.kind === 'function'))
268
+ return resolvePath( ref, 'actionParamType', user );
269
+ return resolvePath( ref, 'type', user );
270
+ }
271
+
272
+ function getCardinality( type ) {
273
+ // only to be called without cycles
274
+ while (type) {
275
+ if (type.cardinality)
276
+ return type.cardinality;
277
+ type = directType( type );
278
+ }
279
+ return {};
280
+ }
281
+
282
+ function userQuery( user ) {
283
+ // TODO: we need _query links set by the definer
284
+ while (user._main) {
285
+ if (user.kind === 'select' || user.kind === '$join')
286
+ return user;
287
+ user = user._parent;
288
+ }
289
+ return null;
290
+ }
291
+
292
+ // Expansiosn --------------------------------------------------------------
293
+
294
+
295
+ function expandItems( art, origin, eType ) {
296
+ if (!enableExpandElements || art.items)
297
+ return false;
298
+ if (isInParents( art, eType )) {
299
+ art.items = 0; // circular
300
+ return true;
301
+ }
302
+ const ref = art.type || art.value || art.name;
303
+ const location = ref && ref.location || art.location;
304
+ art.items = { $inferred: 'expand-element', location };
305
+ setLink( art.items, '_outer', art );
306
+ setLink( art.items, '_origin', origin.items );
307
+ if (!art.$expand)
308
+ art.$expand = 'origin'; // if value stays, elements won't appear in CSN
309
+ return true;
310
+ }
311
+
312
+ function expandElements( art, struct, eType ) {
313
+ if (!enableExpandElements)
314
+ return false;
315
+ if (art.elements || art.kind === '$tableAlias' ||
316
+ // no element expansions for "non-proper" types like
317
+ // entities (as parameter types) etc:
318
+ struct.kind !== 'type' && struct.kind !== 'element' && struct.kind !== 'param' &&
319
+ !struct._outer)
320
+ return false;
321
+ if (struct.elements === 0 || isInParents( art, eType )) {
322
+ art.elements = 0; // circular
323
+ return true;
324
+ }
325
+ const ref = art.type || art.value || art.name;
326
+ const location = ref && ref.location || art.location;
327
+ // console.log( message( null, location, art, {target:struct,art}, 'Info','EXPAND-ELEM')
328
+ // .toString(), Object.keys(struct.elements))
329
+ for (const name in struct.elements) {
330
+ const orig = struct.elements[name];
331
+ // const orig = elem.kind === '$navElement'
332
+ if (Array.isArray( orig )) // redefinitions
333
+ continue;
334
+ linkToOrigin( orig, name, art, 'elements', weakLocation( location ), true )
335
+ // or should we use orig.location? - TODO: try to find test to see message
336
+ .$inferred = 'expand-element';
337
+ }
338
+ // Set elements expansion status (the if condition is always true, as no
339
+ // elements expansion will take place on artifact with existing other
340
+ // member property):
341
+ if (!art.$expand)
342
+ art.$expand = 'origin'; // if value stays, elements won't appear in CSN
343
+ // TODO: have some art.elements[SYM.$inferred] = 'expand-element';
344
+ return true;
345
+ }
346
+
347
+ function expandEnum( art, origin ) {
348
+ if (!enableExpandElements || art.enum)
349
+ return false;
350
+ const ref = art.type || art.value || art.name;
351
+ const location = weakLocation( ref && ref.location || art.location );
352
+ art.enum = Object.create(null);
353
+ for (const name in origin.enum) {
354
+ const orig = origin.enum[name];
355
+ linkToOrigin( orig, name, art, 'enum', location, true )
356
+ // or should we use orig.location? - TODO: try to find test to see message
357
+ .$inferred = 'expand-element';
358
+ }
359
+ // Set elements expansion status (the if condition is always true, as no
360
+ // elements expansion will take place on artifact with existing other
361
+ // member property):
362
+ if (!art.$expand)
363
+ art.$expand = 'origin'; // if value stays, elements won't appear in CSN
364
+ art.enum[$inferred] = 'expand-element';
365
+ return true;
366
+ }
367
+
368
+ /**
369
+ * Return true iff `struct` is `art` or a direct or indirect parent of `art`,
370
+ * we also check for the outer objects of `items`. (There is no need to
371
+ * check the parents of main artifacts, as these are contexts, services or
372
+ * namespaces, and do not serve as type.)
373
+ */
374
+ function isInParents( art, struct ) {
375
+ if (art === struct)
376
+ return true;
377
+ while (art._outer) { // for items
378
+ art = art._outer;
379
+ if (art === struct)
380
+ return true;
381
+ }
382
+ while (art._main) {
383
+ art = art._parent;
384
+ if (art === struct)
385
+ return true;
386
+ }
387
+ return false;
388
+ }
389
+
390
+ //--------------------------------------------------------------------------
391
+ // Views
392
+ //--------------------------------------------------------------------------
393
+
394
+ // Make a view to have elements (remember: wildcard), and prepare that their
395
+ // final type can be resolved, i.e. we know how to resolve select item refs.
396
+ // We do so by first populate views in the FROM clause, then the view query.
397
+ function populateView( art ) {
398
+ if (!art._from || art._status === '_query')
399
+ return;
400
+ const resolveChain = [];
401
+ const fromChain = [ art ];
402
+ while (fromChain.length) {
403
+ const view = fromChain.pop();
404
+ if (view._status === '_query') // already fully resolved (status at def)
405
+ continue;
406
+ resolveChain.push( view );
407
+ for (const from of view._from) {
408
+ if (from._status) // status at the ref -> illegal recursion -> stop
409
+ continue;
410
+ setLink( from, '_status', '_query' );
411
+ // setLink before resolvePath - Cycle: view V as select from V.toV
412
+ let source = resolvePath( from, 'from', view ); // filter and args in resolveQuery
413
+ // console.log('ST:',msgName(source),from._status)
414
+ if (source && source._main) { // element -> should be assoc
415
+ const type = effectiveType( source );
416
+ source = type && type.target;
417
+ }
418
+ if (source && source._from && source._status !== '_query')
419
+ fromChain.push( source );
420
+ }
421
+ }
422
+ // console.log( resolveChain.map( v => msgName(v)+v._status ) );
423
+ for (const view of resolveChain.reverse()) {
424
+ if (view._status !== '_query' ) { // not already resolved
425
+ setLink( view, '_status', '_query' );
426
+ // must be run in order “sub query in FROM first”:
427
+ traverseQueryPost( view.query, null, populateQuery );
428
+ if (view.elements$) // specified elements
429
+ mergeSpecifiedElements( view );
430
+ if (!view.$entity) {
431
+ model._entities.push( view );
432
+ view.$entity = ++model.$entity;
433
+ }
434
+ }
435
+ }
436
+ }
437
+
438
+ function mergeSpecifiedElements( view ) {
439
+ // Later we use specified elements as proxies to inferred of leading query
440
+ // (No, we probably do not.)
441
+ for (const id in view.elements) {
442
+ const ielem = view.elements[id]; // inferred element
443
+ const selem = view.elements$[id]; // specified element
444
+ if (!selem) {
445
+ info( 'query-missing-element', [ ielem.name.location, view ], { id },
446
+ 'Element $(ID) is missing in specified elements' );
447
+ }
448
+ else {
449
+ for (const prop in selem) {
450
+ // just annotation assignments and doc comments for the moment
451
+ if (prop.charAt(0) === '@' || prop === 'doc')
452
+ ielem[prop] = selem[prop];
453
+ }
454
+ selem.$replacement = true;
455
+ }
456
+ }
457
+ for (const id in view.elements$) {
458
+ const selem = view.elements$[id]; // specified element
459
+ if (!selem.$replacement) {
460
+ error( 'query-unspecified-element', [ selem.name.location, selem ], { id },
461
+ 'Element $(ID) does not result from the query' );
462
+ }
463
+ }
464
+ }
465
+
466
+ function populateQuery( query ) {
467
+ if (query._combined || !query.from || !query.$tableAliases)
468
+ // already done or $join query or parse error
469
+ return;
470
+ setLink( query, '_combined', Object.create(null) );
471
+ query.$inlines = [];
472
+ forEachGeneric( query, '$tableAliases', resolveTabRef );
473
+
474
+ initFromColumns( query, query.columns );
475
+ // TODO: already in definer: complain about EXCLUDING with no wildcard
476
+ // (would have been automatically with a good CDL syntax: `* without (...)`)
477
+ if (query.excludingDict) {
478
+ for (const name in query.excludingDict)
479
+ resolveExcluding( name, query._combined, query.excludingDict, query );
480
+ }
481
+ return;
482
+
483
+ function resolveTabRef( alias ) {
484
+ if (alias.kind === 'mixin' || alias.kind === '$self')
485
+ return;
486
+ if (!alias.elements) { // could be false in hierarchical JOIN
487
+ // if (main._block.$frontend!=='json') console.log('TABREF:',alias.name,main,main._block)
488
+ // const tab = alias.path && resolvePath( alias, 'from', query );
489
+ // // if (tab) setLink( alias, '_effectiveType', alias );
490
+ // const elements = alias.query ? alias.query.elements : environment( tab );
491
+ if (!('_origin' in alias) && alias.path) {
492
+ const tab = resolvePath( alias, 'from', query );
493
+ setLink( alias, '_origin', tab && navigationEnv( tab ) );
494
+ }
495
+ alias.elements = Object.create(null); // Set explicitly, as...
496
+ // ...with circular dep to source, no elements can be found.
497
+ const location = alias.path && alias.path.location;
498
+ const qtab = alias._origin;
499
+ if (!qtab || !qtab.elements)
500
+ return;
501
+ forEachGeneric( qtab, 'elements', ( origin, name ) => {
502
+ const elem = linkToOrigin( origin, name, alias, 'elements',
503
+ location || origin.name.location );
504
+ elem.kind = '$navElement';
505
+ // elem.name.select = query.name.select;
506
+ if (origin.masked)
507
+ elem.masked = Object.assign( { $inferred: 'nav' }, origin.masked );
508
+ });
509
+ }
510
+ forEachGeneric( { elements: alias.elements }, 'elements', ( elem, name ) => {
511
+ if (elem.$duplicates !== true)
512
+ dictAddArray( query._combined, name, elem, null ); // not dictAdd()
513
+ });
514
+ }
515
+ }
516
+
517
+ function resolveExcluding( name, env, excludingDict, user ) {
518
+ if (env[name])
519
+ return;
520
+ /** @type {object} */
521
+ // console.log(name,Object.keys(env),Object.keys(excludingDict))
522
+ const compileMessageRef = info(
523
+ 'ref-undefined-excluding', [ excludingDict[name].location, user ], { name },
524
+ 'Element $(NAME) has not been found'
525
+ );
526
+ attachAndEmitValidNames( compileMessageRef, env );
527
+ }
528
+
529
+ // query columns -----------------------------------------------------------
530
+
531
+ function expandFromColumns( elem, assoc, cardinality ) {
532
+ const path = elem.value && elem.value.path;
533
+ if (!path || path.broken)
534
+ return null;
535
+ if (!assoc.target)
536
+ return initFromColumns( elem, elem.expand );
537
+ const { targetMax } = path[path.length - 1].cardinality ||
538
+ (cardinality instanceof Function ? cardinality() : cardinality);
539
+ if (targetMax && (targetMax.val === '*' || targetMax.val > 1))
540
+ elem.items = { location: dictLocation( elem.expand ) }; // TODO: array location
541
+ return initFromColumns( elem, elem.expand );
542
+ }
543
+
544
+ // TODO: make this function shorter - make part of this (e.g. setting
545
+ // parent/name) also be part of definer.js
546
+ // TODO: query is actually the elemParent, where the new elements are added to
547
+ // top-level: just query, columns
548
+ // inline: + elements (TODO: remove), colParent
549
+ // expand: just query (which is a column/element), columns=array of expand
550
+ function initFromColumns( query, columns, inlineHead = undefined ) {
551
+ const elemsParent = query.items || query;
552
+ if (!inlineHead) {
553
+ elemsParent.elements = Object.create(null);
554
+ if (query._main._leadingQuery === query) // never the case for 'expand'
555
+ query._main.elements = elemsParent.elements;
556
+ }
557
+
558
+ for (const col of columns || [ { val: '*' } ]) {
559
+ if (col.val === '*') {
560
+ const siblings = wildcardSiblings( columns, query );
561
+ expandWildcard( col, siblings, inlineHead, query );
562
+ }
563
+ if ((col.expand || col.inline) && !isBetaEnabled( options, 'nestedProjections' )) {
564
+ error( null, [ col.location, query ], { prop: (col.expand ? 'expand' : 'inline') },
565
+ 'Unsupported nested $(PROP)' );
566
+ }
567
+ if (!col.value && !col.expand)
568
+ continue; // error should have been reported by parser
569
+ if (col.inline) {
570
+ col.kind = '$inline';
571
+ col.name = {};
572
+ // a name for this internal symtab entry (e.g. '.2' to avoid clashes
573
+ // with real elements) is only relevant for for `cdsc -R`/debugging
574
+ const q = userQuery( query );
575
+ q.$inlines.push( col );
576
+ // or use userQuery( query ) in the following, too?
577
+ setMemberParent( col, `.${ q.$inlines.length }`, query );
578
+ initFromColumns( query, col.inline, col );
579
+ }
580
+ else if (!col.$replacement) {
581
+ const id = ensureColumnName( col, query );
582
+ col.kind = 'element';
583
+ dictAdd( elemsParent.elements, id, col, ( name, location ) => {
584
+ error( 'duplicate-definition', [ location, query ], { name, '#': 'element' } );
585
+ });
586
+ setMemberParent( col, id, query );
587
+ }
588
+ }
589
+ forEachGeneric( query, 'elements', e => initElem( e, query ) );
590
+ return true;
591
+ }
592
+
593
+ // TODO: probably do this already in definer.js
594
+ function ensureColumnName( col, query ) {
595
+ if (col.name)
596
+ return col.name.id;
597
+ if (col.inline || col.val === '*')
598
+ return '';
599
+ const path = col.value &&
600
+ (col.value.path || !col.value.args && col.value.func && col.value.func.path);
601
+ if (path) {
602
+ const last = !path.broken && path.length && path[path.length - 1];
603
+ if (last) {
604
+ col.name = { id: last.id, location: last.location, $inferred: 'as' };
605
+ return col.name.id;
606
+ }
607
+ }
608
+ else if (col.value || col.expand) {
609
+ error( 'query-req-name', [ col.value && col.value.location || col.location, query ], {},
610
+ 'Alias name is required for this select item' );
611
+ }
612
+ // invent a name for code completion in expression
613
+ col.name = {
614
+ id: '',
615
+ location: col.value && col.value.location || col.location,
616
+ $inferred: 'none',
617
+ };
618
+ return '';
619
+ }
620
+
621
+ function initElem( elem, query ) {
622
+ if (elem.type && !elem.type.$inferred)
623
+ return; // explicit type -> enough or directType()
624
+ if (elem.$inferred) {
625
+ // redirectImplicitly( elem, elem._origin );
626
+ return;
627
+ }
628
+ if (!elem.value || !elem.value.path) // TODO: test $inferred
629
+ return; // no value ref or $inferred
630
+ // TODO: what about SELECT from E { $projection.a as a1, a } !!!!!!
631
+
632
+ const env = columnEnv( elem._pathHead, query );
633
+ const origin = setLink( elem, '_origin',
634
+ resolvePath( elem.value, 'expr', elem, env ) );
635
+ // console.log( message( null, elem.location, elem, {art:query}, 'Info','RED').toString(),
636
+ // elem.value)
637
+ // TODO: make this resolvePath() also part of directType() ?!
638
+ if (!origin)
639
+ return;
640
+ if (elem.foreignKeys) { // REDIRECTED with explicit foreign keys
641
+ forEachGeneric( elem, 'foreignKeys', (key, name) => initKey( key, name, elem ) );
642
+ }
643
+
644
+ // now set things which are necessary for later sub phases:
645
+ const nav = pathNavigation( elem.value );
646
+ if (nav.navigation && nav.item === elem.value.path[elem.value.path.length - 1]) {
647
+ // redirectImplicitly( elem, origin );
648
+ pushLink( nav.navigation, '_projections', elem );
649
+ }
650
+ }
651
+
652
+ function initKey( key, name, elem ) {
653
+ setLink( key, '_block', elem._block );
654
+ setMemberParent( key, name, elem ); // TODO: set _block here if not present?
655
+ }
656
+
657
+ // col ($replacement set before *)
658
+ // false if two cols have same name
659
+ function wildcardSiblings( columns, query ) {
660
+ const siblings = Object.create(null);
661
+ if (!columns)
662
+ return siblings;
663
+
664
+ let seenWildcard = null;
665
+ for (const col of columns) {
666
+ const id = ensureColumnName( col, query );
667
+ if (id) {
668
+ col.$replacement = !seenWildcard;
669
+ siblings[id] = !(id in siblings) && col;
670
+ }
671
+ else if (col.val === '*') {
672
+ seenWildcard = true;
673
+ }
674
+ }
675
+ return siblings;
676
+ }
677
+
678
+ // TODO: disallow $self.elem.* and $self.*, toSelf.* (circular dependency)
679
+ function expandWildcard( wildcard, siblingElements, colParent, query ) {
680
+ const { elements } = query.items || query;
681
+ let location = wildcard.location || query.from && query.from.location || query.location;
682
+ const inferred = query._main.$inferred;
683
+ const excludingDict = (colParent || query).excludingDict || Object.create(null);
684
+
685
+ const envParent = wildcard._pathHead; // TODO: rename _pathHead to _pathEnv
686
+ // console.log('S1:',location.line,location.col,
687
+ // envParent&&!!envParent._origin&&envParent._origin.name)
688
+ const env = columnEnv( envParent, query );
689
+ // console.log('S2:',location.line,location.col,
690
+ // envParent&&!!envParent._origin&&envParent._origin.name,
691
+ // Object.keys(env),Object.keys(elements))
692
+ for (const name in env) {
693
+ const navElem = env[name];
694
+ // TODO: if it is an array, filter out those with masked
695
+ if (excludingDict[name] || navElem.masked && navElem.masked.val)
696
+ continue;
697
+ const sibling = siblingElements[name];
698
+ if (sibling) { // is explicitly provided (without duplicate)
699
+ if (!inferred && !envParent) // not yet for expand/inline
700
+ reportReplacement( sibling, navElem, query );
701
+ if (!sibling.$replacement) {
702
+ sibling.$replacement = true;
703
+ sibling.kind = 'element';
704
+ dictAdd( elements, name, sibling, ( _name, loc ) => {
705
+ // there can be a definition from a previous inline with the same name:
706
+ error( 'duplicate-definition', [ loc, query ], { name, '#': 'element' } );
707
+ });
708
+ setMemberParent( sibling, name, query );
709
+ }
710
+ // else {
711
+ // sibling.$inferred = 'query';
712
+ // }
713
+ }
714
+ else if (Array.isArray(navElem)) {
715
+ const names = navElem.filter( e => !e.$duplicates)
716
+ .map( e => `${ e.name.alias }.${ e.name.element }` );
717
+ if (names.length) {
718
+ error( 'wildcard-ambiguous', [ location, query ], { id: name, names },
719
+ 'Ambiguous wildcard, select $(ID) explicitly with $(NAMES)' );
720
+ }
721
+ }
722
+ else {
723
+ location = weakLocation( location );
724
+ const origin = envParent ? navElem : navElem._origin;
725
+ const elem = linkToOrigin( origin, name, query, null, location );
726
+ // TODO: check assocToMany { * }
727
+ dictAdd( elements, name, elem, ( _name, loc ) => {
728
+ // there can be a definition from a previous inline with the same name:
729
+ error( 'duplicate-definition', [ loc, query ], { name, '#': 'element' } );
730
+ });
731
+ elem.$inferred = '*';
732
+ elem.name.$inferred = '*';
733
+ if (envParent)
734
+ setWildcardExpandInline( elem, envParent, origin, name, location );
735
+ else
736
+ setElementOrigin( elem, navElem, name, location );
737
+ }
738
+ }
739
+ if (envParent || query.kind !== 'select') {
740
+ // already done in populateQuery (TODO: change that and check whether
741
+ // `*` is allowed at all in definer)
742
+ const user = colParent || query;
743
+ for (const name in user.excludingDict)
744
+ resolveExcluding( name, env, excludingDict, query );
745
+ }
746
+ }
747
+
748
+ function columnEnv( envParent, query ) { // etc. wildcard._pathHead;
749
+ return (envParent)
750
+ ? environment( directType( envParent ) || envParent )
751
+ : userQuery( query )._combined;
752
+ }
753
+
754
+ function reportReplacement( sibling, navElem, query ) {
755
+ // TODO: bring this much less often = only if shadowed elem does not appear
756
+ // in expr and if not projected as other name.
757
+ // Probably needs to be reported at a later phase
758
+ const path = sibling.value && sibling.value.path;
759
+ if (!sibling.target || sibling.target.$inferred || // not explicit REDIRECTED TO
760
+ path && path[path.length - 1].id !== sibling.name.id) { // or renamed
761
+ const { id } = sibling.name;
762
+ if (Array.isArray(navElem)) {
763
+ info( 'wildcard-excluding-many', [ sibling.name.location, query ], { id },
764
+ 'This select item replaces $(ID) from two or more sources' );
765
+ }
766
+ else {
767
+ info( 'wildcard-excluding-one', [ sibling.name.location, query ],
768
+ { id, alias: navElem._parent.name.id },
769
+ 'This select item replaces $(ID) from table alias $(ALIAS)' );
770
+ }
771
+ }
772
+ }
773
+
774
+ function setWildcardExpandInline( queryElem, pathHead, origin, name, location ) {
775
+ setLink( queryElem, '_pathHead', pathHead );
776
+ const path = [ { id: name, location } ];
777
+ queryElem.value = { path, location }; // TODO: can we omit that? We have _origin
778
+ setArtifactLink( path[0], origin );
779
+ setLink( queryElem, '_origin', origin );
780
+ // TODO: set _projections when top-level?
781
+ }
782
+
783
+ function setElementOrigin( queryElem, navElem, name, location ) {
784
+ const sourceElem = navElem._origin;
785
+ const alias = navElem._parent;
786
+ // always expand * to path with table alias (reason: columns current_date etc)
787
+ const path = [ { id: alias.name.id, location }, { id: name, location } ];
788
+ queryElem.value = { path, location };
789
+ setLink( path[0], '_navigation', alias );
790
+ setArtifactLink( path[0], alias._origin );
791
+ setArtifactLink( path[1], sourceElem );
792
+ // TODO: or should we set the _artifact/_effectiveType directly to the target?
793
+ setArtifactLink( queryElem.value, sourceElem );
794
+ pushLink( navElem, '_projections', queryElem );
795
+ // TODO: _effectiveType?
796
+ }
797
+
798
+ //--------------------------------------------------------------------------
799
+ // Auto-Redirections
800
+ //--------------------------------------------------------------------------
801
+
802
+ // Conditions for redirecting target of assoc in elem
803
+ // - we (the elem) are in a service
804
+ // - target provided in assoc is not defined in current service
805
+ // - elem is to be auto-redirected (included elem, elem from main query, ...)
806
+ // - assoc is not defined in current service (or was not to be auto-redirected)
807
+ function redirectImplicitly( elem, assoc ) {
808
+ // PRE: elem has no target, assoc has target prop
809
+ if (elem.kind === '$tableAlias')
810
+ return false;
811
+ const assocTarget = resolvePath( assoc.target, 'target', assoc );
812
+ let target = assocTarget;
813
+ // console.log( info( null, [ elem.location, elem ], {target,art:assoc,name:''+assoc.target},
814
+ // 'RED').toString())
815
+ if (!target)
816
+ return false; // error in target ref
817
+ const { location } = elem.value || elem.type || elem.name;
818
+ const service = (elem._main || elem)._service;
819
+ if (service && service !== target._service && assocIsToBeRedirected( elem )) {
820
+ if (service !== (assoc._main || assoc)._service ||
821
+ !assocIsToBeRedirected( assoc ) ||
822
+ elem === assoc)
823
+ target = redirectImplicitlyDo( elem, assoc, target, service );
824
+ }
825
+ if (elem === assoc) { // redirection of user-provided target
826
+ if (assocTarget === target) // no change (due to no implicit redirection)
827
+ return true;
828
+ const type = resolvePath( elem.type, 'type', elem ); // cds.Association or cds.Composition
829
+ const origin = {
830
+ kind: elem.kind, // necessary for rewrite, '$user-provided' would be best
831
+ name: elem.name,
832
+ type: { // TODO: necessary?
833
+ path: [ { id: type.name.absolute, location: elem.type.location } ],
834
+ scope: 'global',
835
+ location: elem.type.location,
836
+ $inferred: 'REDIRECTED',
837
+ },
838
+ target: elem.target,
839
+ $inferred: 'REDIRECTED',
840
+ location: elem.target.location,
841
+ };
842
+ setLink( elem, '_origin', origin );
843
+ setArtifactLink( elem.type, type );
844
+ setLink( origin, '_outer', elem );
845
+ setLink( origin, '_parent', elem._parent );
846
+ if (elem._main) // remark: the param `elem` can also be a type
847
+ setLink( origin, '_main', elem._main );
848
+ setLink( origin, '_effectiveType', origin );
849
+ setLink( origin, '_block', elem._block );
850
+ if (elem.foreignKeys) {
851
+ origin.foreignKeys = elem.foreignKeys;
852
+ delete elem.foreignKeys; // will be rewritten
853
+ }
854
+ if (elem.on) {
855
+ origin.on = elem.on;
856
+ delete elem.on; // will be rewritten
857
+ }
858
+ }
859
+ if (target !== assocTarget)
860
+ setExpandStatus( elem, 'target' ); // (might) also set in rewriteCondition
861
+ elem.target = {
862
+ path: [ { id: target.name.absolute, location } ],
863
+ scope: 'global',
864
+ location,
865
+ $inferred: (target !== assocTarget ? 'IMPLICIT' : 'rewrite' ),
866
+ };
867
+ setArtifactLink( elem.target, target );
868
+ setArtifactLink( elem.target.path[0], target );
869
+ return true;
870
+ }
871
+
872
+ function assocIsToBeRedirected( assoc ) {
873
+ if (assoc.kind === 'mixin')
874
+ return false;
875
+ const query = userQuery( assoc );
876
+ return redirectInSubQueries || !query || query._main._leadingQuery === query;
877
+ }
878
+
879
+ function redirectImplicitlyDo( elem, assoc, target, service ) {
880
+ // console.log('ES:',elem.name.absolute,elem.name.element);
881
+ const elemScope = scopedRedirections && // null if no scoped redirections
882
+ preferredElemScope( target, service, elem, assoc._main || assoc );
883
+ const exposed = minimalExposure( target, service, elemScope );
884
+
885
+ if (!exposed.length && elemScope !== true) {
886
+ const origTarget = target;
887
+ if (isAutoExposed( target ))
888
+ target = createAutoExposed( origTarget, service, elemScope );
889
+ const desc = origTarget._descendants ||
890
+ setLink( origTarget, '_descendants', Object.create(null) );
891
+ if (!desc[service.name.absolute]) // could be the target itself (no repeated msgs)!
892
+ desc[service.name.absolute] = [ target ];
893
+ else
894
+ desc[service.name.absolute].push( target );
895
+ }
896
+ else if (exposed.length === 1) {
897
+ return exposed[0];
898
+ }
899
+ else if (elem === assoc) {
900
+ // `assoc: Association to ModelEntity`: user-provided target is to be auto-redirected
901
+ warning( 'type-ambiguous-target',
902
+ [ elem.target.location, elem ],
903
+ {
904
+ target,
905
+ // art: definitionScope( target ), - TODO extra debug info in message
906
+ sorted_arts: exposed,
907
+ }, {
908
+ // eslint-disable-next-line max-len
909
+ std: 'Replace target $(TARGET) by one of $(SORTED_ARTS); can\'t auto-redirect this association if multiple projections exist in this service',
910
+ // eslint-disable-next-line max-len
911
+ two: 'Replace target $(TARGET) by $(SORTED_ARTS) or $(SECOND); can\'t auto-redirect this association if multiple projections exist in this service',
912
+ });
913
+ // continuation semantics: no auto-redirection
914
+ }
915
+ else {
916
+ // referred (and probably inferred) assoc (without a user-provided target at that place)
917
+ // HINT: consider bin/cdsv2m.js when changing the following message text
918
+ // No grouped and sub messages yet (TODO v3): mention at all target places with all assocs
919
+ const withAnno = annotationVal( exposed[0]['@cds.redirection.target'] );
920
+ for (const proj of exposed) {
921
+ // TODO: def-ambiguous-target (just v3, as the current is infamous and used in options),
922
+ message( 'redirected-implicitly-ambiguous',
923
+ [ weakLocation( proj.location ), proj ],
924
+ {
925
+ '#': withAnno && 'justOne',
926
+ target,
927
+ art: elem,
928
+ // art: definitionScope( target ), - TODO extra debug info in message
929
+ anno: 'cds.redirection.target',
930
+ sorted_arts: exposed,
931
+ }, {
932
+ // eslint-disable-next-line max-len
933
+ std: 'Add $(ANNO) to one of $(SORTED_ARTS) to select the entity as redirection target for $(TARGET) in this service; can\'t auto-redirect $(ART) otherwise',
934
+ // eslint-disable-next-line max-len
935
+ two: 'Add $(ANNO) to either $(SORTED_ARTS) or $(SECOND) to select the entity as redirection target for $(TARGET) in this service; can\'t auto-redirect $(ART) otherwise',
936
+ // eslint-disable-next-line max-len
937
+ justOne: 'Remove $(ANNO) from all but one of $(SORTED_ARTS) to have a unique redirection target for $(TARGET) in this service; can\'t auto-redirect $(ART) otherwise',
938
+ } );
939
+ }
940
+ // continuation semantics: no implicit redirections
941
+ }
942
+ return target;
943
+ }
944
+
945
+ // Return projections of `target` in `service`. Shorted by
946
+ // - first, only consider projections with @cds.redirection.target=true
947
+ // - exclude all indirect projections, i.e. those which are projection on others in list
948
+ //
949
+ // To avoid repeated messages: if already tried to do autoexposure, return
950
+ // autoexposed entity when successful, or `target` otherwise (no/failed autoexposure)
951
+ function minimalExposure( target, service, elemScope ) {
952
+ const descendants = scopedExposure( target._descendants &&
953
+ target._descendants[service.name.absolute] ||
954
+ [],
955
+ elemScope, target );
956
+ const preferred = descendants.filter( d => annotationVal( d['@cds.redirection.target'] ) );
957
+ const exposed = preferred.length ? preferred : descendants;
958
+ if (exposed.length < 2)
959
+ return exposed || [];
960
+ let min = null;
961
+ for (const e of exposed) {
962
+ if (!min || min._ancestors && min._ancestors.includes(e)) {
963
+ min = e;
964
+ }
965
+ else if (!e._ancestors || !e._ancestors.includes( min )) {
966
+ if (elemScope === '')
967
+ return [];
968
+ return exposed;
969
+ }
970
+ }
971
+ return [ min ];
972
+ }
973
+
974
+ // Scoped redirections -----------------------------------------------------
975
+
976
+
977
+ function preferredElemScope( target, service, elem, assocMain ) {
978
+ const assocScope = definitionScope( assocMain );
979
+ const targetScope = definitionScope( target );
980
+ if (targetScope === assocScope) { // intra-scope in model
981
+ const elemScope = definitionScope( elem._main || elem );
982
+ if (targetScope === target || // unscoped target in model
983
+ assocScope === assocMain || // unscoped assoc source in model
984
+ elemScope !== (elem._main || elem)) // scoped assoc source in service
985
+ return elemScope; // own scope, then global
986
+ }
987
+ if (targetScope === target) // unscoped target in model / other service
988
+ return false; // all (there could be no scoped autoexposed)
989
+ // scoped target in model:
990
+ const exposed = minimalExposure( targetScope, service, false );
991
+ // console.log('PES:',elem.name.absolute,elem.name.element,exposed.map(e=>e.name.absolute))
992
+ if (exposed.length === 1) // unique redirection for target scope: use that
993
+ return exposed[0];
994
+ // TODO: warning if exposed.length >= 2? Probably not
995
+ // TODO: use excessive testing for the following
996
+ // Now re-scope according to naming of auto-exposed entity:
997
+ const autoScopeName = autoExposedName( targetScope, service, false );
998
+ const autoScope = model.definitions[autoScopeName];
999
+ // console.log('AEN:',autoScopeName,autoScope&&(autoScope.$inferred || autoScope.kind))
1000
+ if (autoScope)
1001
+ return autoScope;
1002
+ const { location } = service.name;
1003
+ const nullScope = {
1004
+ kind: 'namespace', name: { absolute: autoScopeName, location }, location,
1005
+ };
1006
+ model.definitions[autoScopeName] = nullScope;
1007
+ initArtifact( nullScope );
1008
+ return nullScope;
1009
+ }
1010
+
1011
+ function scopedExposure( descendants, elemScope, target ) {
1012
+ if (!elemScope) // no scoped redirections
1013
+ return descendants;
1014
+ if (elemScope === true || elemScope === 'auto') {
1015
+ // cross-scope navigation, scoped model target, but there is no unique
1016
+ // redirection target for target model scope -> unsure redirection scope
1017
+ const unscoped = descendants.filter( d => d === definitionScope( d ) );
1018
+ if (unscoped.length) // use unscoped new targets if present
1019
+ return unscoped;
1020
+ // Need to filter out auto-exposed, otherwise the behavior is
1021
+ // processing-order dependent (not storing the autoexposed in
1022
+ // _descendents would only be an alternative w/o recompilation)
1023
+ return descendants.filter( d => !d.$generated && !annotationVal( d['@cds.autoexposed'] ) );
1024
+ }
1025
+ // try scope as target first, even if it has @cds.redirection.target: false
1026
+ if (isDirectProjection( elemScope, target ))
1027
+ return [ elemScope ];
1028
+ const scoped = descendants.filter( d => elemScope === definitionScope( d ) );
1029
+ if (scoped.length) // use scoped new targets if present
1030
+ return scoped;
1031
+ // otherwise return new targets outside any scope
1032
+ return descendants.filter( d => d === definitionScope( d ) );
1033
+ }
1034
+
1035
+ // Return the scope of a definition. It is the last parent of the definition
1036
+ // which is not a context/service/namespace, or the definition itself.
1037
+ // If inside service, it is the direct child of the (most inner) service.
1038
+ function definitionScope( art ) {
1039
+ if (art._base) // with deprecated.generatedEntityNameWithUnderscore
1040
+ return art._base;
1041
+ let base = art;
1042
+ while (art._parent) {
1043
+ if (art._parent.kind === 'service')
1044
+ return art;
1045
+ art = art._parent;
1046
+ if (!kindProperties[art.kind].artifacts)
1047
+ base = art;
1048
+ }
1049
+ return base;
1050
+ }
1051
+
1052
+ function isDirectProjection( proj, base ) {
1053
+ return proj.kind === 'entity' && // not event
1054
+ projectionAncestor( base, proj.params ) && // same params
1055
+ // direct proj (TODO: or should we add them to another list?)
1056
+ proj.query && proj.query.op && proj.query.op.val === 'SELECT' &&
1057
+ proj._from && proj._from.length === 1 &&
1058
+ base === resolvePath( proj._from[0], 'from', proj );
1059
+ }
1060
+
1061
+ // Auto-exposure -----------------------------------------------------------
1062
+
1063
+ // TODO: do something in kick-start.js ?
1064
+ function isAutoExposed( target ) {
1065
+ if (target.$autoexpose !== undefined)
1066
+ return target.$autoexpose;
1067
+ const origTarget = target;
1068
+ const chain = [];
1069
+ let source = target._from && resolvePath( target._from[0], 'from', target );
1070
+ // query source ref might not have been resolved yet, cycle avoided as
1071
+ // setAutoExposed() sets $autoexpose and a second call on same art would
1072
+ // return false
1073
+ while (target.$autoexpose === undefined && setAutoExposed( target ) && source) {
1074
+ // stop at first ancestor with annotation or at non-query entity
1075
+ chain.push( target );
1076
+ target = source;
1077
+ source = target._from && resolvePath( target._from[0], 'from', target );
1078
+ }
1079
+ const autoexpose = target.$autoexpose;
1080
+ if (typeof autoexpose === 'boolean') {
1081
+ for (const a of chain)
1082
+ a.$autoexpose = autoexpose;
1083
+ }
1084
+ return origTarget.$autoexpose;
1085
+ }
1086
+
1087
+ // TODO: less auto-exposed for compositions (see lengthy discussions)
1088
+ function setAutoExposed( art ) {
1089
+ const anno = art['@cds.autoexpose'];
1090
+ if (anno && anno.val !== null) { // XSN TODO: set val, but no location for anno short form
1091
+ // @cds.autoexpose:true or @cds.autoexpose:false
1092
+ art.$autoexpose = anno.val === undefined || !!anno.val;
1093
+ return false;
1094
+ }
1095
+ // no @cds.autoexpose or @cds.autoexpose:null
1096
+ // TODO: introduce deprecated.noInheritedAutoexposeViaComposition
1097
+ art.$autoexpose = model.$compositionTargets[art.name.absolute]
1098
+ ? autoexposeViaComposition
1099
+ : null;
1100
+ return true; // still check for inherited @cds.autoexpose
1101
+ }
1102
+
1103
+ function autoExposedName( target, service, elemScope ) {
1104
+ const { absolute } = target.name;
1105
+ if (isDeprecatedEnabled( options, 'shortAutoexposed' )) {
1106
+ const parent = definitionScope( target )._parent;
1107
+ const name = (parent) ? absolute.substring( parent.name.absolute.length + 1 ) : absolute;
1108
+ // no need for dedot here (as opposed to deprecated.longAutoexposed), as
1109
+ // the name for dependent entities have already been created using `_` then
1110
+ return `${ service.name.absolute }.${ name }`;
1111
+ }
1112
+ if (isDeprecatedEnabled( options, 'longAutoexposed' )) {
1113
+ const dedot = isDeprecatedEnabled( options, 'generatedEntityNameWithUnderscore' );
1114
+ return `${ service.name.absolute }.${ dedot ? absolute.replace( /\./g, '_' ) : absolute }`;
1115
+ }
1116
+ const base = definitionScope( target );
1117
+ if (base === target)
1118
+ return `${ service.name.absolute }.${ absolute.substring( absolute.lastIndexOf('.') + 1 ) }`;
1119
+ // for scoped (e.g. calculated) entities, use exposed name of base:
1120
+ const exposed = minimalExposure( base, service, elemScope );
1121
+ // console.log(exposed.map( a => a.name.absolute ));
1122
+ const sbasename = (exposed.length === 1 && exposed[0] !== base) // same with no/failed expose
1123
+ ? exposed[0].name.absolute
1124
+ : autoExposedName( base, service, elemScope );
1125
+ return sbasename + absolute.slice( base.name.absolute.length );
1126
+ }
1127
+
1128
+
1129
+ function createAutoExposed( target, service, elemScope ) {
1130
+ const absolute = autoExposedName( target, service, elemScope );
1131
+ const autoexposed = model.definitions[absolute];
1132
+ if (autoexposed && (autoexposed.kind !== 'namespace' || !scopedRedirections)) {
1133
+ if (isDirectProjection( autoexposed, target )) {
1134
+ if (options.testMode)
1135
+ throw new Error( `Tried to auto-expose ${ target.name.absolute } twice`);
1136
+ return autoexposed;
1137
+ }
1138
+ error( 'duplicate-autoexposed', [ service.name.location, service ],
1139
+ { target, art: absolute },
1140
+ 'Name $(ART) of autoexposed entity for $(TARGET) collides with other definition' );
1141
+ info( null, [ target.name.location, target ],
1142
+ { art: service },
1143
+ 'Expose this (or the competing) entity explicitly in service $(ART)' );
1144
+ if (autoexposed.$inferred !== 'autoexposed')
1145
+ return target;
1146
+ const firstTarget = autoexposed.query.from._artifact;
1147
+ error( 'duplicate-autoexposed', [ service.name.location, service ],
1148
+ { target: firstTarget, art: absolute },
1149
+ 'Name $(ART) of autoexposed entity for $(TARGET) collides with other definition' );
1150
+ info( null, [ firstTarget.name.location, firstTarget ],
1151
+ { art: service },
1152
+ 'Expose this (or the competing) entity explicitly in service $(ART)' );
1153
+ autoexposed.$inferred = 'duplicate-autoexposed';
1154
+ return target;
1155
+ }
1156
+ // console.log(absolute)
1157
+ const { location } = target.name;
1158
+ const from = augmentPath( location, target.name.absolute );
1159
+ let art = {
1160
+ kind: 'entity',
1161
+ name: { location, path: splitIntoPath( location, absolute ), absolute },
1162
+ location: target.location,
1163
+ query: { location, op: { val: 'SELECT', location }, from },
1164
+ $syntax: 'projection',
1165
+ $inferred: 'autoexposed',
1166
+ '@cds.autoexposed': {
1167
+ name: { path: [ { id: 'cds.autoexposed', location } ], location },
1168
+ $inferred: '$generated',
1169
+ },
1170
+ };
1171
+ // TODO: do we need to tag the generated entity with elemScope = 'auto'?
1172
+ if (autoexposed) {
1173
+ Object.assign( autoexposed, art );
1174
+ art = autoexposed;
1175
+ }
1176
+ else {
1177
+ model.definitions[absolute] = art;
1178
+ }
1179
+ setLink( art, '_service', service );
1180
+ setLink( art, '_block', model.$internal );
1181
+ initArtifact( art, !!autoexposed );
1182
+ // populate view (phase 2 of resolver has to be repeated as the view was created afterwards)
1183
+ populateView( art );
1184
+ // TODO: try to set locations of elements locations of orig target elements
1185
+ newAutoExposed.push( art );
1186
+ return art;
1187
+ }
1188
+ }
1189
+
1190
+ // Return condensed info about reference in select item
1191
+ // - tableAlias.elem -> { navigation: navElem, item: path[1], tableAlias }
1192
+ // - sourceElem (in query) -> { navigation: navElem, item: path[0], tableAlias }
1193
+ // - mixinElem -> { navigation: mixinElement, item: path[0] }
1194
+ // - $projection.elem -> also $self.item -> { item: path[1], tableAlias: $self }
1195
+ // - $self -> { item: undefined, tableAlias: $self }
1196
+ // - $parameters.P, :P -> {}
1197
+ // - $now, current_date -> {}
1198
+ // - undef, redef -> {}
1199
+ // With 'navigation': store that navigation._artifact is projected
1200
+ // With 'navigation': rewrite its ON condition
1201
+ // With navigation: Do KEY propagation
1202
+ //
1203
+ // TODO: copy of fn in resolve.js; used just once here - do it differently here
1204
+ // and then delete the function here
1205
+ function pathNavigation( ref ) {
1206
+ // currently, indirectly projectable elements are not included - we might
1207
+ // keep it this way! If we want them to be included - be aware: cycles
1208
+ if (!ref._artifact)
1209
+ return {};
1210
+ let item = ref.path && ref.path[0];
1211
+ const root = item && item._navigation;
1212
+ if (!root)
1213
+ return {};
1214
+ if (root.kind === '$navElement')
1215
+ return { navigation: root, item, tableAlias: root._parent };
1216
+ if (root.kind === 'mixin')
1217
+ return { navigation: root, item };
1218
+ item = ref.path[1];
1219
+ if (root.kind === '$self')
1220
+ return { item, tableAlias: root };
1221
+ if (root.kind !== '$tableAlias' || ref.path.length < 2)
1222
+ return {}; // should not happen
1223
+ return { navigation: root.elements[item.id], item, tableAlias: root };
1224
+ }
1225
+
1226
+ module.exports = populate;