@sap/cds-compiler 4.6.2 → 4.7.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/bin/cds_update_identifiers.js +6 -2
  3. package/bin/cdsc.js +1 -1
  4. package/doc/CHANGELOG_ARCHIVE.md +9 -9
  5. package/doc/CHANGELOG_BETA.md +6 -0
  6. package/lib/api/main.js +56 -9
  7. package/lib/api/options.js +6 -3
  8. package/lib/api/validate.js +20 -29
  9. package/lib/base/message-registry.js +27 -3
  10. package/lib/base/messages.js +8 -3
  11. package/lib/base/model.js +2 -0
  12. package/lib/checks/dbFeatureFlags.js +28 -0
  13. package/lib/checks/elements.js +81 -13
  14. package/lib/checks/enricher.js +3 -2
  15. package/lib/checks/validator.js +38 -4
  16. package/lib/compiler/assert-consistency.js +4 -4
  17. package/lib/compiler/checks.js +5 -4
  18. package/lib/compiler/define.js +2 -2
  19. package/lib/compiler/generate.js +2 -1
  20. package/lib/compiler/propagator.js +3 -11
  21. package/lib/compiler/shared.js +2 -1
  22. package/lib/compiler/tweak-assocs.js +43 -24
  23. package/lib/edm/annotations/edmJson.js +3 -0
  24. package/lib/edm/annotations/genericTranslation.js +156 -106
  25. package/lib/edm/annotations/preprocessAnnotations.js +11 -14
  26. package/lib/edm/csn2edm.js +27 -24
  27. package/lib/edm/edm.js +8 -8
  28. package/lib/edm/edmPreprocessor.js +135 -37
  29. package/lib/edm/edmUtils.js +20 -7
  30. package/lib/gen/Dictionary.json +1 -0
  31. package/lib/gen/language.checksum +1 -1
  32. package/lib/gen/language.interp +9 -11
  33. package/lib/gen/languageParser.js +5942 -5446
  34. package/lib/json/to-csn.js +7 -114
  35. package/lib/language/genericAntlrParser.js +106 -48
  36. package/lib/model/cloneCsn.js +203 -0
  37. package/lib/model/csnRefs.js +11 -3
  38. package/lib/model/csnUtils.js +42 -85
  39. package/lib/optionProcessor.js +2 -2
  40. package/lib/render/manageConstraints.js +1 -1
  41. package/lib/render/toCdl.js +133 -88
  42. package/lib/render/toHdbcds.js +1 -5
  43. package/lib/render/toSql.js +7 -9
  44. package/lib/render/utils/common.js +9 -16
  45. package/lib/transform/addTenantFields.js +277 -102
  46. package/lib/transform/db/applyTransformations.js +14 -9
  47. package/lib/transform/db/backlinks.js +2 -1
  48. package/lib/transform/db/constraints.js +60 -82
  49. package/lib/transform/db/expansion.js +6 -6
  50. package/lib/transform/db/featureFlags.js +5 -0
  51. package/lib/transform/db/flattening.js +4 -4
  52. package/lib/transform/db/killAnnotations.js +1 -0
  53. package/lib/transform/db/rewriteCalculatedElements.js +2 -2
  54. package/lib/transform/db/transformExists.js +12 -0
  55. package/lib/transform/db/views.js +5 -2
  56. package/lib/transform/draft/odata.js +7 -6
  57. package/lib/transform/effective/associations.js +2 -1
  58. package/lib/transform/effective/main.js +3 -2
  59. package/lib/transform/effective/types.js +6 -3
  60. package/lib/transform/forOdata.js +39 -24
  61. package/lib/transform/forRelationalDB.js +34 -27
  62. package/lib/transform/localized.js +29 -9
  63. package/lib/transform/odata/flattening.js +419 -0
  64. package/lib/transform/odata/toFinalBaseType.js +95 -15
  65. package/lib/transform/odata/typesExposure.js +9 -7
  66. package/lib/transform/transformUtils.js +7 -6
  67. package/lib/transform/translateAssocsToJoins.js +3 -3
  68. package/lib/utils/objectUtils.js +14 -0
  69. package/package.json +1 -1
@@ -0,0 +1,419 @@
1
+ 'use strict';
2
+
3
+ const { isBuiltinType, isMagicVariable, forEachDefinition,
4
+ copyAnnotations, forEachMemberRecursively,
5
+ transformExpression, findAnnotationExpression } = require('../../model/csnUtils');
6
+ const transformUtils = require('../transformUtils');
7
+ const { setProp, isBetaEnabled } = require('../../base/model');
8
+ const { applyTransformationsOnDictionary,
9
+ applyTransformationsOnNonDictionary } = require('../db/applyTransformations.js');
10
+ const { linkForeignKeyAnnotationExtensionsToAssociation,
11
+ handleManagedAssociationsAndCreateForeignKeys } = require('../db/flattening');
12
+ const { cloneCsnNonDict } = require('../../model/cloneCsn');
13
+ const { forEach } = require('../../utils/objectUtils');
14
+
15
+ function allInOneFlattening(csn, refFlattener, adaptRefs, inspectRef, isExternalServiceMember, error, csnUtils, options) {
16
+
17
+ const isExprAnno = (obj, pn) => {
18
+ return isBetaEnabled(options, 'odataPathsInAnnotationExpressions') && findAnnotationExpression(obj, pn);
19
+ }
20
+
21
+ forEachDefinition(csn, (def, defName) => {
22
+ if(def.kind === 'entity' && !isExternalServiceMember(def, defName)) {
23
+ ['elements', 'params'].forEach(dictName => {
24
+ const dict = def[dictName];
25
+ if(dict) {
26
+ const csnPath = ['definitions', defName, dictName];
27
+ const orderedElementList = [];
28
+
29
+ forEach(dict, (eltName, elt) => {
30
+ const location = [ ...csnPath, eltName ];
31
+ const resolvedElt = (elt.type && !elt.elements) ? csnUtils.getFinalTypeInfo(elt.type) : elt;
32
+ if(resolvedElt.elements) {
33
+ const flattenedSubTree = recurseIntoElement(dictName, elt, resolvedElt,
34
+ !!elt.notNull, location, [ defName, eltName ]);
35
+
36
+ flattenedSubTree.forEach(([flatEltName, flatElt]) => {
37
+ if (dict[flatEltName] || orderedElementList.some(elt => elt[0] === flatEltName))
38
+ error('name-duplicate-element', location,
39
+ { '#': 'flatten-element-exist', name: flatEltName });
40
+ propagateToFlatElem(elt, flatElt);
41
+ rewriteOnCondition(flatElt, flattenedSubTree);
42
+ orderedElementList.push([flatEltName, flatElt]);
43
+ });
44
+ }
45
+ else {
46
+ const flatElt = cloneElt(dictName, elt, location, [ defName, eltName ]);
47
+ orderedElementList.push([eltName, flatElt]);
48
+ }
49
+ });
50
+
51
+ const flatDict = orderedElementList.reduce((elements, [ flatEltName, flatElt ]) => {
52
+ if(flatElt.items) {
53
+ // rewrite annotation paths inside items.elements
54
+ forEachMemberRecursively(flatElt.items, (elt, _eltName, _prop, path) => {
55
+ const exprAnnos = Object.keys(elt).filter(pn => isExprAnno(elt, pn));
56
+ flattenAndPrefixExprPaths(elt, exprAnnos, elt.$path, path, 0, true);
57
+ }, [ flatEltName ], true, { pathWithoutProp: true } );
58
+ }
59
+ setProp(flatElt, '$path', [ ...csnPath, flatEltName ]);
60
+ elements[flatEltName] = flatElt;
61
+ return elements;
62
+ }, Object.create(null));
63
+ setProp(def, `$${dictName}`, flatDict);
64
+ }
65
+ });
66
+ const flatAnnos = Object.create(null);
67
+ const annoNames = copyAnnotations(def, flatAnnos).filter(an => isExprAnno(def, an));
68
+ flattenAndPrefixExprPaths(flatAnnos, annoNames, [ 'definitions', defName ], [ defName ], 0);
69
+ setProp(def, '$flatAnnotations', flatAnnos);
70
+ }
71
+ });
72
+
73
+ function recurseIntoElement(scope, elt, resolvedElt, rootPathIsNotNull, location, rootPrefix = [], typeIdx = 0) {
74
+ const eltName = rootPrefix[rootPrefix.length-1];
75
+ if(!resolvedElt.elements) {
76
+ const flatElt = cloneElt(scope, elt, location, rootPrefix, typeIdx);
77
+ if(rootPathIsNotNull)
78
+ flatElt.notNull = true;
79
+ else
80
+ delete flatElt.notNull;
81
+ return [[ eltName, flatElt ]];
82
+ }
83
+ else {
84
+ let flattenedSubTree = [];
85
+ forEach(resolvedElt.elements, (childName, child) => {
86
+ resolvedElt = child;
87
+ if(child.type && !child.elements) {
88
+ resolvedElt = csnUtils.getFinalTypeInfo(child.type);
89
+ if(resolvedElt.elements)
90
+ typeIdx = rootPrefix.length + 1;
91
+ }
92
+ flattenedSubTree = flattenedSubTree.concat(recurseIntoElement(scope, child, resolvedElt,
93
+ !!(child.notNull && rootPathIsNotNull),
94
+ [... location, 'elements', childName],
95
+ [ ...rootPrefix, childName ], typeIdx));
96
+ });
97
+ // 1) rename, 2) filter duplicates and finalize new elements
98
+ const duplicateDict = Object.create(null);
99
+ return flattenedSubTree.map(([flatEltName, flatElt]) => {
100
+ return [ `${eltName}_${flatEltName}`, flatElt ];
101
+ }).filter(([name, flatElt]) =>{
102
+ if(duplicateDict[name]) {
103
+ error('name-duplicate-element', location,
104
+ { '#': 'flatten-element-gen', name });
105
+ return false;
106
+ }
107
+ else {
108
+ propagateToFlatElem(elt, flatElt, rootPrefix, typeIdx);
109
+ duplicateDict[name] = flatElt;
110
+ return true;
111
+ }
112
+ })
113
+ }
114
+ }
115
+
116
+ function propagateToFlatElem(elt, flatElt, rootPrefix = [], typeIdx = 0) {
117
+ // Copy annotations from struct
118
+ // (not overwriting, because deep annotations should have precedence).
119
+ // This has historic reasons. We don't copy doc-comments because copying annotations
120
+ // is questionable to begin with. Only selected annotations should have been copied,
121
+ // if at all.
122
+ // When flattening structured elements for OData don't propagate the odata.Type annotations
123
+ // as these would falsify the flattened elements.
124
+
125
+ // TODO: directly refer to edmPrimitiveType facets
126
+ const excludes = {
127
+ '@odata.Type': 1,
128
+ '@odata.Scale': 1,
129
+ '@odata.Precision': 1,
130
+ '@odata.MaxLength': 1,
131
+ '@odata.SRID': 1,
132
+ '@odata.FixedLength': 1,
133
+ '@odata.Collation': 1,
134
+ '@odata.Unicode': 1,
135
+ };
136
+ // TODO: copy only those expression annotations that have no path into unreachable subtree, starting
137
+ // of typePathRoot (which could be some completely different path)
138
+ const exprAnnoNames = copyAnnotations(elt, flatElt, false, excludes).filter(pn => isExprAnno(flatElt, pn));
139
+ flattenAndPrefixExprPaths(flatElt, exprAnnoNames, elt.$path, rootPrefix, typeIdx);
140
+ // Copy selected type properties
141
+ ['key', 'virtual', 'masked', 'viaAll', 'localized'].forEach(p => {
142
+ if (elt[p] != null)
143
+ flatElt[p] = elt[p];
144
+ });
145
+ }
146
+
147
+ // Copy the original element
148
+ function cloneElt(scope, elt, location, rootPrefix = [], typeIdx = 0) {
149
+ const flatElt = cloneCsnNonDict(elt,
150
+ { ...options,
151
+ hiddenPropertiesToClone: [ '$structRef', '$fkExtensions' ]
152
+ } );
153
+
154
+ // needed for @cds.persistence.name
155
+ setProp(flatElt, '$defPath', rootPrefix);
156
+ setProp(flatElt, '$scope', scope);
157
+ retypeCloneWithFinalBaseType(flatElt, location);
158
+
159
+ const [ nonAnnoProps, exprAnnoProps ] = Object.keys(flatElt).reduce((acc, pn) => {
160
+ if(pn[0] !== '@' && pn !== 'value')
161
+ acc[0].push(pn);
162
+ if(isExprAnno(flatElt, pn) || pn === 'value')
163
+ acc[1].push(pn);
164
+ return acc;
165
+ }, [[],[]]);
166
+ // transform all non annotation properties for that flat element with the generic transformer
167
+ // we don't know what's inside the element clone (like anonymous sub elements behind 'items' etc.)
168
+ nonAnnoProps.forEach(pn => applyTransformationsOnNonDictionary(flatElt, pn, refFlattener, {}, elt.$path || location))
169
+ adaptRefs.forEach(fn => fn());
170
+ adaptRefs.length = 0;
171
+ // flatten and prefix annotations and 'value' paths
172
+ flattenAndPrefixExprPaths(flatElt, exprAnnoProps, elt.$path || location, rootPrefix, typeIdx);
173
+ return flatElt;
174
+
175
+ function retypeCloneWithFinalBaseType(elt, location) {
176
+ if(elt.type &&
177
+ !isBuiltinType(elt.type) &&
178
+ !isODataV4BuiltinFromService(elt.type, location)
179
+ && !isItemsType(elt.type)) {
180
+ let resolvedType = csnUtils.getFinalTypeInfo(elt);
181
+
182
+ delete resolvedType.kind;
183
+ if(resolvedType.items)
184
+ delete resolvedType.type;
185
+
186
+ if(elt.items) {
187
+ if(resolvedType.items) {
188
+ elt.items = resolvedType;
189
+ delete elt.items.type;
190
+ }
191
+ else
192
+ elt.items.type = resolvedType;
193
+ }
194
+ else {
195
+ if(resolvedType.items) {
196
+ elt.items = resolvedType.items;
197
+ delete elt.type;
198
+ }
199
+ else
200
+ elt.type = resolvedType;
201
+ }
202
+ }
203
+
204
+ function isItemsType(typeName) {
205
+ let typeDef = csn.definitions[typeName];
206
+ return !!typeDef?.items;
207
+ }
208
+
209
+ function isODataV4BuiltinFromService( typeName, path ) {
210
+ if (options.odataVersion === 'v2' || typeof typeName !== 'string')
211
+ return false;
212
+
213
+ const typeServiceName = csnUtils.getServiceName(typeName);
214
+ let finalBaseType = csnUtils.getFinalTypeInfo(typeName).type;
215
+ if(!isBuiltinType(finalBaseType)) {
216
+ const typeDef = csn.definitions[finalBaseType];
217
+ finalBaseType = typeDef?.items?.type || typeDef?.type;
218
+ }
219
+ // we need the service of the current definition
220
+ const currDefServiceName = csnUtils.getServiceName(path[1]);
221
+ return typeServiceName === currDefServiceName && isBuiltinType(finalBaseType);
222
+ }
223
+ }
224
+ }
225
+
226
+ // The path rewriting must be done with the current CSN path of that exact annotation location
227
+ // in the element tree and done exactly after the initial copy, (otherwise, csnRefs is not able
228
+ // to locate the relative location of the path expression in this element).
229
+ // At this time both annotations and values must be rewritten.
230
+ //
231
+ // Later, the query can/must be rewritten as long as a flat OData CSN is published
232
+ // but this then operates on the entity/view which has all struct infos available
233
+ function flattenAndPrefixExprPaths(carrier, propNames, csnPath, rootPrefix, typeIdx, refParentIsItems = false) {
234
+ refFlattener.$refParentIsItems = refParentIsItems;
235
+ const refCheck = {
236
+ ref: (parent, prop, xpr, path) => {
237
+ const { art } = inspectRef(path);
238
+ const ft = csnUtils.getFinalTypeInfo(art?.type);
239
+ if(!isBuiltinType(ft?.type) && refCheck.anno !== 'value') {
240
+ error('odata-anno-xpr-ref', path, { elemref: parent, name: refCheck.eltLocationStr, anno: refCheck.anno, '#': 'flatten_builtin' });
241
+ }
242
+ }
243
+ }
244
+
245
+ refCheck.eltLocationStr = (function() {
246
+ const [head, ...tail ] = rootPrefix;
247
+ return `${head}:${tail.join('.')}`;
248
+ })();
249
+
250
+ const absolutifier = {
251
+ ref : (parent, prop, xpr) => {
252
+ const head = xpr[0].id || xpr[0];
253
+ if(typeIdx > 0 && head === '$self' && !isMagicVariable(head)) {
254
+ const [xprHead, ...xprTail] = xpr.slice(1, xpr.length);
255
+ parent[prop] = [ rootPrefix.slice(1, typeIdx).concat(xprHead).join('_'), ...xprTail];
256
+ if(carrier.$scope === 'params')
257
+ parent.param = true;
258
+ else
259
+ parent[prop].unshift('$self');
260
+ }
261
+ else if(rootPrefix.length > 2 && head !== '$self' && !parent.param && !isMagicVariable(head)) {
262
+ const [xprHead, ...xprTail] = xpr;
263
+ if(!refParentIsItems)
264
+ parent[prop] = [ rootPrefix.slice(1, -1).concat(xprHead).join('_'), ...xprTail];
265
+ else
266
+ parent[prop] = [ ...rootPrefix.slice(0, rootPrefix.length-1), ...xpr];
267
+ if(carrier.$scope === 'params')
268
+ parent.param = true;
269
+ else
270
+ parent[prop].unshift('$self');
271
+ }
272
+ }
273
+ }
274
+
275
+ propNames.forEach(pn => {
276
+ refCheck.anno = pn;
277
+ transformExpression(carrier, pn, [ refCheck, refFlattener ], csnPath);
278
+ });
279
+ adaptRefs.forEach(fn => fn());
280
+ adaptRefs.length = 0;
281
+ refFlattener.$refParentIsItems = false;
282
+ propNames.forEach(pn => {
283
+ transformExpression(carrier, pn, absolutifier, csnPath)
284
+ })
285
+ }
286
+
287
+ // TODO: This should be part of the generic path rewriting algorithm
288
+ // Primitive approach here does not take into account absolute paths via $self etc...
289
+ function rewriteOnCondition(flatElt, flattenedSubTree) {
290
+ if (flatElt.on) {
291
+ // unmanaged relations can't be primary key
292
+ delete flatElt.key;
293
+ // Make refs resolvable by fixing the first ref step
294
+ transformExpression(flatElt, 'on', {
295
+ ref: (parent, prop, xpr) => {
296
+ const prefix = flatElt.$defPath.slice(1, -1).join('_');
297
+ const possibleFlatName = `${prefix}_${xpr[0]}`;
298
+ if (flattenedSubTree.find(entry => entry[0] === possibleFlatName))
299
+ xpr[0] = possibleFlatName;
300
+ }
301
+ });
302
+ }
303
+ }
304
+ }
305
+
306
+ /**
307
+ * Rewrite all structured references in the model such that it matches their flat counterparts
308
+ * @param {CSN.Model} csn
309
+ * @param {CSN.Options} options
310
+ * @param {WeakMap} resolved Cache for resolved refs
311
+ * @param {string} pathDelimiter
312
+ * @param {object} iterateOptions
313
+ */
314
+ function flattenAllStructStepsInRefs( csn, refFlattener, adaptRefs, iterateOptions = {} ) {
315
+
316
+ // All anno path flattening is already done, don't do it on locations where we don't want it!
317
+ forEachDefinition(csn, (def, defName) => {
318
+ if(def.kind === 'entity') {
319
+ ['query', 'projection'].forEach(dictName => {
320
+ applyTransformationsOnNonDictionary(csn.definitions[defName], dictName, refFlattener, iterateOptions, [ 'definitions', defName ]);
321
+ })
322
+ if(csn.definitions[defName].actions)
323
+ applyTransformationsOnDictionary(csn.definitions[defName].actions, refFlattener, iterateOptions, [ 'definitions', defName, 'actions' ]);
324
+ }
325
+ /*
326
+ TODO: In real hybrid flat/struct OData transformation, ref rewriting in types must not be done
327
+ together with non-rewriting ON condition refs during foreign key creation
328
+ */
329
+ if(['type'].includes(def.kind)) {
330
+ applyTransformationsOnNonDictionary(csn.definitions, defName, refFlattener, iterateOptions, [ 'definitions' ]);
331
+ }
332
+ });
333
+ adaptRefs.forEach(fn => fn());
334
+ adaptRefs.length = 0;
335
+ }
336
+
337
+ function getStructRefFlatteningTransformer(csn, inspectRef, effectiveType, options, resolved, pathDelimiter) {
338
+ const { flattenStructStepsInRef } = transformUtils.getTransformers(csn, options, pathDelimiter);
339
+ /**
340
+ * For each step of the links, check if there is a type reference.
341
+ * If there is, resolve it and store the result in a WeakMap.
342
+ *
343
+ * @param {Array} [links]
344
+ * @todo seems too hacky
345
+ * @returns {WeakMap} A WeakMap where a link is the key and the type is the value
346
+ */
347
+ function resolveLinkTypes( links = [] ) {
348
+ const resolvedLinkTypes = new WeakMap();
349
+ links.forEach((link) => {
350
+ const { art } = link;
351
+ if (art && art.type)
352
+ resolvedLinkTypes.set(link, effectiveType(art));
353
+ });
354
+ return resolvedLinkTypes;
355
+ }
356
+ const adaptRefs = [];
357
+ const transformer = {
358
+ $refParentIsItems: false,
359
+ ref: (parent, prop, ref, path) => {
360
+ const { links, art, scope } = inspectRef(path);
361
+ const resolvedLinkTypes = resolveLinkTypes(links);
362
+ setProp(parent, '$path', [ ...path ]);
363
+ const lastRef = ref[ref.length - 1];
364
+ const fn = () => {
365
+ const scopedPath = [ ...parent.$path ];
366
+ // TODO: If foreign key annotations should be assigned via
367
+ // full path into target, uncomment this line and
368
+ // comment/remove setProp in expansion.js
369
+ // setProp(parent, '$structRef', parent.ref);
370
+ parent.ref = flattenStructStepsInRef(ref, scopedPath, links, scope, resolvedLinkTypes, transformer.$refParentIsItems);
371
+ resolved.set(parent, { links, art, scope });
372
+ // Explicitly set implicit alias for things that are now flattened - but only in columns
373
+ // TODO: Can this be done elegantly during expand phase already?
374
+ if (parent.$implicitAlias) { // an expanded s -> s.a is marked with this - do not add implicit alias "a" there, we want s_a
375
+ if (parent.ref[parent.ref.length - 1] === parent.as) // for a simple s that was expanded - for s.substructure this would not apply
376
+ delete parent.as;
377
+ delete parent.$implicitAlias;
378
+ }
379
+ // To handle explicitly written s.a - add implicit alias a, since after flattening it would otherwise be s_a
380
+ else if (parent.ref[parent.ref.length - 1] !== lastRef &&
381
+ (insideColumns(scopedPath) || insideKeys(scopedPath)) &&
382
+ !parent.as) {
383
+ parent.as = lastRef;
384
+ }
385
+
386
+ /**
387
+ * Return true if the path points inside columns
388
+ *
389
+ * @param {CSN.Path} path
390
+ * @returns {boolean}
391
+ */
392
+ function insideColumns( path ) {
393
+ return path.length >= 3 && (path[path.length - 3] === 'SELECT' || path[path.length - 3] === 'projection') && path[path.length - 2] === 'columns';
394
+ }
395
+ /**
396
+ * Return true if the path points inside keys
397
+ *
398
+ * @param {CSN.Path} path
399
+ * @returns {boolean}
400
+ */
401
+ function insideKeys( path ) {
402
+ return path.length >= 3 && path[path.length - 2] === 'keys' && typeof path[path.length - 1] === 'number';
403
+ }
404
+ };
405
+ // adapt queries later
406
+ adaptRefs.push(fn);
407
+ },
408
+ }
409
+
410
+ return { adaptRefs, transformer, inspectRef, effectiveType };
411
+ }
412
+
413
+ module.exports = {
414
+ allInOneFlattening,
415
+ flattenAllStructStepsInRefs,
416
+ linkForeignKeyAnnotationExtensionsToAssociation,
417
+ handleManagedAssociationsAndCreateForeignKeys,
418
+ getStructRefFlatteningTransformer
419
+ };
@@ -1,18 +1,23 @@
1
1
  'use strict';
2
2
 
3
- const { isBetaEnabled } = require('../../base/model');
3
+ const { setProp, isBetaEnabled } = require('../../base/model');
4
4
  const {
5
- forEachDefinition, forEachGeneric, forEachMemberRecursively,
6
- isBuiltinType, cloneCsnDictionary, cloneCsnNonDict,
5
+ transformExpression,
6
+ forEachDefinition,
7
+ forEachGeneric,
8
+ forEachMemberRecursively,
9
+ isBuiltinType,
10
+ findAnnotationExpression,
7
11
  } = require('../../model/csnUtils');
8
12
  const { isArtifactInSomeService, isArtifactInService } = require('./utils');
13
+ const { cloneCsnDict, cloneCsnNonDict } = require('../../model/cloneCsn');
9
14
 
10
- function expandToFinalBaseType(csn, transformers, csnUtils, services, options) {
15
+ function expandToFinalBaseType(csn, transformers, csnUtils, services, options, error) {
11
16
  const isV4 = options.odataVersion === 'v4';
12
17
  const special$self = !csn?.definitions?.$self && '$self';
13
18
  forEachDefinition(csn, (def, defName) => {
14
19
  // Unravel derived type chains to final one for elements, actions, action parameters (propagating annotations)
15
- forEachMemberRecursively(def, (member) => {
20
+ forEachMemberRecursively(def, (member, _memberName) => {
16
21
  expandToFinalBaseType(member, defName);
17
22
  expandToFinalBaseType(member.items, defName);
18
23
  expandToFinalBaseType(member.returns, defName);
@@ -85,7 +90,7 @@ function expandToFinalBaseType(csn, transformers, csnUtils, services, options) {
85
90
  if (def.items.type && !isBuiltinType(def.items.type)) {
86
91
  let finalBaseType = csnUtils.getFinalTypeInfo(def.items.type);
87
92
  if (finalBaseType?.elements) {
88
- def.items.elements = cloneCsnDictionary(finalBaseType.elements, options);
93
+ def.items.elements = cloneCsnDict(finalBaseType.elements, options);
89
94
  delete def.items.type;
90
95
  }
91
96
  }
@@ -138,7 +143,7 @@ function expandToFinalBaseType(csn, transformers, csnUtils, services, options) {
138
143
  transformers.toFinalBaseType(node);
139
144
  }
140
145
  else if (csnUtils.isStructured(finalBaseType)) {
141
- cloneElements(finalBaseType);
146
+ cloneElements(node, finalBaseType);
142
147
  }
143
148
  else if (node.type && node.items)
144
149
  delete node.type;
@@ -169,8 +174,8 @@ function expandToFinalBaseType(csn, transformers, csnUtils, services, options) {
169
174
  transformers.toFinalBaseType(node);
170
175
  // node.type = finalType;
171
176
  }
172
- else if (node.type && node.type.ref) {
173
- cloneElements(finalBaseType);
177
+ else if (node.type.ref) {
178
+ cloneElements(node, finalBaseType);
174
179
  }
175
180
  }
176
181
  }
@@ -189,26 +194,78 @@ function expandToFinalBaseType(csn, transformers, csnUtils, services, options) {
189
194
  }
190
195
  }
191
196
 
192
- function cloneElements(finalBaseType) {
193
- // cloneCsn only works correctly if we start "from the top"
197
+ function cloneElements(node, finalBaseType) {
194
198
  let clone;
195
199
  // do the clone only if really needed
196
200
  if((finalBaseType.items && !node.items) ||
197
- (finalBaseType.elements && !node.elements))
198
- clone = cloneCsnNonDict({ definitions: { 'TypeDef': finalBaseType } }, options);
201
+ (finalBaseType.elements && !node.elements)) {
202
+ // use the 'real' type, don't clone a clone
203
+ let _type = node._type;
204
+ while(_type._type && !_type.items && !_type.elements)
205
+ _type = _type._type;
206
+ clone = cloneCsnNonDict(_type, { ...options, hiddenPropertiesToClone: [ '_type' ] });
207
+ fitClonedElementsIntoParent(clone, node, _type.$path);
208
+ }
199
209
  if (finalBaseType.items) {
200
210
  delete node.type;
201
211
  if(!node.items)
202
- Object.assign(node, { items: clone.definitions.TypeDef.items });
212
+ Object.assign(node, { items: clone.items });
203
213
  }
204
214
  if (finalBaseType.elements) {
205
215
  if(!finalBaseType.items)
206
216
  delete node.type;
207
217
  if(!node.elements)
208
- Object.assign(node, { elements: clone.definitions.TypeDef.elements });
218
+ Object.assign(node, { elements: clone.elements });
209
219
  }
210
220
  }
211
221
 
222
+ function fitClonedElementsIntoParent(clone, node, typeRefCsnPath) {
223
+ const f = (p) => {
224
+ const [h, ...t ] = p;
225
+ return `${h}:${t.join('.')}`;
226
+ }
227
+ const typeRefStr = f(node.type.ref);
228
+ const typeRefRootPath = $path2path(typeRefCsnPath);
229
+ forEachMemberRecursively(clone, (elt, eltName, prop, location) => {
230
+ const [ xprANames /*nxprANames */ ] = Object.keys(elt).reduce((acc, pn) => {
231
+ if (pn[0] === '@')
232
+ acc[findAnnotationExpression(elt, pn) ? 0 : 1].push(pn);
233
+ return acc;
234
+ }, [ [], [] ]);
235
+ const usingPositionStr = f($path2path(location));
236
+ const eltRootPath = $path2path(elt.$path);
237
+ xprANames.forEach(xprAName => {
238
+ //let isSubTreeSpan = true;
239
+ transformExpression(elt, xprAName, {
240
+ ref: (parent, prop, xpr, csnPath) => {
241
+ let prefixMatch = true;
242
+ const head = xpr[0].id || xpr[0];
243
+ if (head === '$self') {
244
+ for (let i = 1; i < typeRefRootPath.length && prefixMatch; i++){
245
+ prefixMatch = (xpr[i].id || xpr[i]) === typeRefRootPath[i];
246
+ }
247
+ if(prefixMatch && xpr.length > typeRefRootPath.length) {
248
+ parent[prop] = [ ...xpr.slice(eltRootPath.length-1)];
249
+ }
250
+ else {
251
+ error('odata-anno-xpr-ref', csnPath,
252
+ { anno: xprAName, elemref: xpr.join('.'), name: usingPositionStr, code: typeRefStr });
253
+ //isSubTreeSpan = false;
254
+ }
255
+ }
256
+ },
257
+ }, elt.$path);
258
+ // if(!isSubTreeSpan) {
259
+ // // remove annotation and sub annotations from cloned element
260
+ // delete elt[xprAName];
261
+ // nxprANames.filter(an => an.startsWith(`${xprAName}.`)).forEach((nxprAName) => {
262
+ // delete elt[nxprAName];
263
+ // });
264
+ // }
265
+ });
266
+ setProp(elt, '$path', location);
267
+ }, node.$path);
268
+ }
212
269
  /*
213
270
  Check, if a type needs to be expanded into the service
214
271
 
@@ -227,6 +284,29 @@ function expandToFinalBaseType(csn, transformers, csnUtils, services, options) {
227
284
  const isInCurServ = isArtifactInService(node.type, currService);
228
285
  return !isV4 || !(isBuiltin && !isAssoc && isInCurServ);
229
286
  }
287
+
288
+ // convert $path to path starting at main artifact
289
+ function $path2path( p ) {
290
+ const path = [];
291
+ let env = csn;
292
+ for (let i = 0; p && env && i < p.length; i++) {
293
+ const ps = p[i];
294
+ env = env[ps];
295
+ if (env && env.constructor === Object) {
296
+ path.push(ps);
297
+ // jump over many items but not if this is an element
298
+ if (env.items) {
299
+ env = env.items;
300
+ if (p[i + 1] === 'items')
301
+ i++;
302
+ }
303
+ if (env.type && !isBuiltinType(env.type) && !env.elements)
304
+ env = csn.definitions[env.type];
305
+ }
306
+ }
307
+ return path;
308
+ }
309
+
230
310
  }
231
311
  }
232
312
 
@@ -8,10 +8,10 @@
8
8
 
9
9
  const { setProp, isBetaEnabled } = require('../../base/model');
10
10
  const { defNameWithoutServiceOrContextName, isArtifactInService } = require('./utils');
11
- const { getNamespace, copyAnnotations, cloneCsnNonDict,
12
- isBuiltinType, isAnnotationExpression,
13
- forEachDefinition, forEachMember, forEachGeneric } = require('../../model/csnUtils');
11
+ const { getNamespace, copyAnnotations, isBuiltinType, findAnnotationExpression,
12
+ forEachDefinition, forEachMember, forEachGeneric, isEdmPropertyRendered } = require('../../model/csnUtils');
14
13
  const { CompilerAssertion } = require('../../base/error');
14
+ const { cloneCsnNonDict } = require('../../model/cloneCsn');
15
15
 
16
16
  /**
17
17
  * A given CDS model is a set of n definitions D = {v_1, ..., v_n } spanning a type dependency
@@ -144,7 +144,7 @@ function typesExposure(csn, whatsMyServiceName, requestedServiceNames, fallBackS
144
144
  * @param {String} serviceName
145
145
  * @param {String} newTypeName
146
146
  */
147
- function exposeTypeOf(node, isKey, memberName, defName, serviceName, newTypeName, path, parentName, isTermDef=false) {
147
+ function exposeTypeOf(node, isKey, memberName, defName, serviceName, newTypeName, path, parentName, isTermDef=false, ignoreInAPI=false) {
148
148
  const { isExposable, typeDef, typeName, elements, isAnonymous } = isTypeExposable();
149
149
  if (isExposable) {
150
150
  // this is the name used to register the new type in csn.definitions
@@ -183,10 +183,12 @@ function typesExposure(csn, whatsMyServiceName, requestedServiceNames, fallBackS
183
183
  if(elements) {
184
184
  newType = createNewStructType(elements);
185
185
  // if using node enforces open/closed, set it on type
186
- if(node['@open'] !== undefined)
186
+ if (node['@open'] !== undefined)
187
187
  newType['@open'] = node['@open']
188
188
  if (node.$location)
189
189
  setProp(newType, '$location', node.$location);
190
+ ignoreInAPI ||= !isEdmPropertyRendered(node, options);
191
+ setProp(newType, '$ignoreInAPI', ignoreInAPI);
190
192
 
191
193
  csn.definitions[fullQualifiedNewTypeName] = newType;
192
194
  exposedTypes[fullQualifiedNewTypeName] = 1;
@@ -198,7 +200,7 @@ function typesExposure(csn, whatsMyServiceName, requestedServiceNames, fallBackS
198
200
  defName = typeDef.kind === 'type' ? typeName : defName;
199
201
  {
200
202
  const { isExposable, typeDef, typeName } = exposeTypeOf(newElem, isKey, elemName, defName, serviceName,
201
- getNewTypeName(newElem, elemName, newTypeName, serviceName), path, fullQualifiedNewTypeName, isTermDef);
203
+ getNewTypeName(newElem, elemName, newTypeName, serviceName), path, fullQualifiedNewTypeName, isTermDef, ignoreInAPI);
202
204
  // if the type for the newElem was not exposed it may be a scalar type def from an external service that hasn't
203
205
  // been caught by expandToFinalBaseType() (forODataNew must not modify external imported services)
204
206
  if(!isExposable && isBuiltinType(typeName) && !isBuiltinType((newElem.items?.type || newElem.type))) {
@@ -223,7 +225,7 @@ function typesExposure(csn, whatsMyServiceName, requestedServiceNames, fallBackS
223
225
  // expression annos and their sub annotations are not propagated to type
224
226
  let [ xprANames, nxprANames ] = Object.keys(typeDef).reduce((acc, pn) => {
225
227
  if (pn[0] === '@')
226
- acc[isAnnotationExpression(typeDef[pn]) ? 0 : 1].push(pn);
228
+ acc[findAnnotationExpression(typeDef, pn) ? 0 : 1].push(pn);
227
229
  return acc;
228
230
  }, [ [], [] ]);
229
231
  nxprANames = nxprANames.filter(an => !xprANames.some(ean => an.startsWith(`${ean}.`)));