@sap/cds-compiler 2.11.2 → 2.11.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.
- package/CHANGELOG.md +23 -2
- package/bin/.eslintrc.json +1 -2
- package/bin/cds_update_identifiers.js +3 -1
- package/bin/cdsc.js +8 -1
- package/bin/cdsv2m.js +3 -2
- package/lib/api/main.js +2 -16
- package/lib/api/options.js +3 -2
- package/lib/api/validate.js +7 -1
- package/lib/backends.js +3 -5
- package/lib/base/keywords.js +3 -2
- package/lib/base/message-registry.js +24 -8
- package/lib/base/messages.js +15 -9
- package/lib/base/optionProcessorHelper.js +1 -1
- package/lib/checks/emptyOrOnlyVirtual.js +2 -2
- package/lib/checks/unknownMagic.js +1 -1
- package/lib/compiler/assert-consistency.js +2 -2
- package/lib/compiler/builtins.js +34 -15
- package/lib/compiler/definer.js +8 -17
- package/lib/compiler/index.js +13 -25
- package/lib/compiler/resolver.js +89 -23
- package/lib/compiler/shared.js +25 -28
- package/lib/compiler/utils.js +11 -0
- package/lib/gen/language.checksum +1 -1
- package/lib/json/to-csn.js +60 -14
- package/lib/language/errorStrategy.js +26 -8
- package/lib/language/genericAntlrParser.js +2 -1
- package/lib/language/language.g4 +6 -3
- package/lib/main.d.ts +79 -1
- package/lib/model/csnRefs.js +11 -4
- package/lib/model/csnUtils.js +2 -107
- package/lib/model/enrichCsn.js +33 -35
- package/lib/model/revealInternalProperties.js +5 -4
- package/lib/model/sortViews.js +8 -1
- package/lib/optionProcessor.js +5 -1
- package/lib/render/.eslintrc.json +1 -2
- package/lib/render/toHdbcds.js +2 -7
- package/lib/render/toSql.js +16 -11
- package/lib/transform/db/applyTransformations.js +189 -0
- package/lib/transform/db/flattening.js +1 -1
- package/lib/transform/db/transformExists.js +9 -0
- package/lib/transform/db/views.js +89 -42
- package/lib/transform/forHanaNew.js +34 -12
- package/lib/transform/translateAssocsToJoins.js +3 -3
- package/lib/utils/file.js +6 -2
- package/package.json +1 -1
- package/lib/transform/db/helpers.js +0 -58
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Module for general (partial) CSN looper functions, respecting dictionaries and allowing
|
|
5
|
+
* to pass custom callbacks for certain properties like "ref".
|
|
6
|
+
*
|
|
7
|
+
* Functions are also published in csnUtils.js for convenience.
|
|
8
|
+
*
|
|
9
|
+
* They should stay here due to the stricter linter rules for the time being.
|
|
10
|
+
*
|
|
11
|
+
* @module lib/transform/db/applyTransformations
|
|
12
|
+
*/
|
|
13
|
+
const { setProp } = require('../../base/model');
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param {object} parent The "parent" of which we transform a property of
|
|
18
|
+
* @param {string} prop The property of parent to start at
|
|
19
|
+
* @param {object} customTransformers Map of prop to transform and function to apply
|
|
20
|
+
* @param {Function[]} [artifactTransformers=[]] Transformations to run on the artifacts, like forEachDefinition
|
|
21
|
+
* @param {boolean} [skipIgnore=true] Wether to skip _ignore elements or not
|
|
22
|
+
* @param {object} [options={}] "skipArtifact": (artifact, name) => Boolean to skip certain artifacts, drillRef: boolean - whether to drill into infix/args
|
|
23
|
+
* @param {CSN.Path} path Path to parent
|
|
24
|
+
* @returns {object} parent with transformations applied
|
|
25
|
+
*/
|
|
26
|
+
function applyTransformationsInternal(parent, prop, customTransformers, artifactTransformers, skipIgnore, options, path = []) {
|
|
27
|
+
const transformers = {
|
|
28
|
+
elements: dictionary,
|
|
29
|
+
definitions: dictionary,
|
|
30
|
+
actions: dictionary,
|
|
31
|
+
params: dictionary,
|
|
32
|
+
enum: dictionary,
|
|
33
|
+
mixin: dictionary,
|
|
34
|
+
ref: pathRef,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const csnPath = [ ...path ];
|
|
38
|
+
if (prop === 'definitions')
|
|
39
|
+
definitions( parent, 'definitions', parent.definitions );
|
|
40
|
+
else
|
|
41
|
+
standard(parent, prop, parent[prop]);
|
|
42
|
+
return parent;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Default transformer for things that are not dictionaries, like "type" or "keys".
|
|
46
|
+
* The customTransformers are applied here (and only here).
|
|
47
|
+
*
|
|
48
|
+
* @param {object | Array} _parent the thing that has _prop
|
|
49
|
+
* @param {string|number} _prop the name of the current property
|
|
50
|
+
* @param {object} node The value of node[_prop]
|
|
51
|
+
*/
|
|
52
|
+
function standard( _parent, _prop, node ) {
|
|
53
|
+
if (!node || typeof node !== 'object' || !{}.propertyIsEnumerable.call( _parent, _prop ) || (typeof _prop === 'string' && _prop.startsWith('@')) || (skipIgnore && node._ignore))
|
|
54
|
+
return;
|
|
55
|
+
|
|
56
|
+
csnPath.push( _prop );
|
|
57
|
+
|
|
58
|
+
if (Array.isArray(node)) {
|
|
59
|
+
node.forEach( (n, i) => standard( node, i, n ) );
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
else {
|
|
63
|
+
for (const name of Object.getOwnPropertyNames( node )) {
|
|
64
|
+
const trans = transformers[name] || standard;
|
|
65
|
+
if (customTransformers[name])
|
|
66
|
+
customTransformers[name](node, name, node[name], csnPath, _parent, _prop);
|
|
67
|
+
|
|
68
|
+
trans( node, name, node[name], csnPath );
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
csnPath.pop();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Transformer for things that are dictionaries - like "elements".
|
|
76
|
+
*
|
|
77
|
+
* @param {object | Array} node the thing that has _prop
|
|
78
|
+
* @param {string|number} _prop the name of the current property
|
|
79
|
+
* @param {object} dict The value of node[_prop]
|
|
80
|
+
*/
|
|
81
|
+
function dictionary( node, _prop, dict ) {
|
|
82
|
+
// Allow skipping dicts like actions in forHanaNew
|
|
83
|
+
if (options.skipDict && options.skipDict[_prop])
|
|
84
|
+
return;
|
|
85
|
+
csnPath.push( _prop );
|
|
86
|
+
for (const name of Object.getOwnPropertyNames( dict ))
|
|
87
|
+
standard( dict, name, dict[name] );
|
|
88
|
+
|
|
89
|
+
if (!Object.prototype.propertyIsEnumerable.call( node, _prop ))
|
|
90
|
+
setProp(node, `$${_prop}`, dict);
|
|
91
|
+
csnPath.pop();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Special version of "dictionary" to apply artifactTransformers.
|
|
96
|
+
*
|
|
97
|
+
* @param {object | Array} node the thing that has _prop
|
|
98
|
+
* @param {string|number} _prop the name of the current property
|
|
99
|
+
* @param {object} dict The value of node[_prop]
|
|
100
|
+
*/
|
|
101
|
+
function definitions( node, _prop, dict ) {
|
|
102
|
+
csnPath.push( _prop );
|
|
103
|
+
for (const name of Object.getOwnPropertyNames( dict )) {
|
|
104
|
+
const skip = options && options.skipArtifact && options.skipArtifact(dict[name], name) || false;
|
|
105
|
+
if (!skip) {
|
|
106
|
+
artifactTransformers.forEach(fn => fn(dict, name, dict[name]));
|
|
107
|
+
standard( dict, name, dict[name] );
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (!Object.prototype.propertyIsEnumerable.call( node, _prop ))
|
|
111
|
+
setProp(node, `$${_prop}`, dict);
|
|
112
|
+
csnPath.pop();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Keep looping through the pathRef - because in a .ref we can have .args and .where
|
|
117
|
+
*
|
|
118
|
+
* @param {object | Array} node the thing that has _prop
|
|
119
|
+
* @param {string|number} _prop the name of the current property
|
|
120
|
+
* @param {any} _path The value of node[_prop]
|
|
121
|
+
*/
|
|
122
|
+
function pathRef( node, _prop, _path ) {
|
|
123
|
+
csnPath.push( _prop );
|
|
124
|
+
_path.forEach( ( s, i ) => {
|
|
125
|
+
if (s && typeof s === 'object') {
|
|
126
|
+
csnPath.push( i );
|
|
127
|
+
if (options.drillRef) {
|
|
128
|
+
standard(_path, i, s);
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
if (s.args)
|
|
132
|
+
standard( s, 'args', s.args );
|
|
133
|
+
if (s.where)
|
|
134
|
+
standard( s, 'where', s.where );
|
|
135
|
+
}
|
|
136
|
+
csnPath.pop();
|
|
137
|
+
}
|
|
138
|
+
} );
|
|
139
|
+
csnPath.pop();
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Loop through the model, applying the custom transformations on the node's matching.
|
|
145
|
+
*
|
|
146
|
+
* Each transformer gets:
|
|
147
|
+
* - the parent having the property
|
|
148
|
+
* - the name of the property
|
|
149
|
+
* - the value of the property
|
|
150
|
+
* - the path to the property
|
|
151
|
+
*
|
|
152
|
+
* @param {object} csn CSN to enrich in-place
|
|
153
|
+
* @param {object} customTransformers Map of _prop to transform and function to apply
|
|
154
|
+
* @param {Function[]} [artifactTransformers=[]] Transformations to run on the artifacts, like forEachDefinition
|
|
155
|
+
* @param {boolean} [skipIgnore=true] Wether to skip _ignore elements or not
|
|
156
|
+
* @param {object} [options={}] "skipArtifact": (artifact, name) => Boolean to skip certain artifacts, drillRef: boolean - whether to drill into infix/args
|
|
157
|
+
* @returns {object} CSN with transformations applied
|
|
158
|
+
*/
|
|
159
|
+
function applyTransformations( csn, customTransformers = {}, artifactTransformers = [], skipIgnore = true, options = {} ) {
|
|
160
|
+
if (csn && csn.definitions)
|
|
161
|
+
return applyTransformationsInternal(csn, 'definitions', customTransformers, artifactTransformers, skipIgnore, options);
|
|
162
|
+
return csn;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Instead of looping through the whole model, start at a given thing (like an on-condition),
|
|
168
|
+
* as long as it is not a dictionary.
|
|
169
|
+
*
|
|
170
|
+
* Each transformer gets:
|
|
171
|
+
* - the parent having the property
|
|
172
|
+
* - the name of the property
|
|
173
|
+
* - the value of the property
|
|
174
|
+
* - the path to the property
|
|
175
|
+
*
|
|
176
|
+
* @param {object} parent The "parent" of which we transform a property of
|
|
177
|
+
* @param {string} prop The property of parent to start at
|
|
178
|
+
* @param {object} customTransformers Map of prop to transform and function to apply
|
|
179
|
+
* @param {CSN.Path} path Path pointing to parent
|
|
180
|
+
* @returns {object} parent[prop] with transformations applied
|
|
181
|
+
*/
|
|
182
|
+
function applyTransformationsOnNonDictionary(parent, prop, customTransformers = {}, path = []) {
|
|
183
|
+
return applyTransformationsInternal(parent, prop, customTransformers, [], true, {}, path)[prop];
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
module.exports = {
|
|
187
|
+
applyTransformations,
|
|
188
|
+
applyTransformationsOnNonDictionary,
|
|
189
|
+
};
|
|
@@ -14,7 +14,7 @@ const { setProp } = require('../../base/model');
|
|
|
14
14
|
* @param {CSN.Model} csn
|
|
15
15
|
*/
|
|
16
16
|
function removeLeadingSelf(csn) {
|
|
17
|
-
const magicVars = [ '$now' ];
|
|
17
|
+
const magicVars = [ '$now', '$self', '$projection', '$user', '$session', '$at' ];
|
|
18
18
|
forEachDefinition(csn, (artifact, artifactName) => {
|
|
19
19
|
if (artifact.kind === 'entity' || artifact.kind === 'view') {
|
|
20
20
|
forAllElements(artifact, artifactName, (parent, elements) => {
|
|
@@ -51,6 +51,9 @@ function handleExists(csn, options, error) {
|
|
|
51
51
|
const { inspectRef } = csnRefs(csn);
|
|
52
52
|
const generatedExists = new WeakMap();
|
|
53
53
|
forEachDefinition(csn, (artifact, artifactName) => {
|
|
54
|
+
if (artifact.projection) // do the same hack we do for the other stuff...
|
|
55
|
+
artifact.query = { SELECT: artifact.projection };
|
|
56
|
+
|
|
54
57
|
if (artifact.query) {
|
|
55
58
|
forAllQueries(artifact.query, (query, path) => {
|
|
56
59
|
if (!generatedExists.has(query)) {
|
|
@@ -83,6 +86,12 @@ function handleExists(csn, options, error) {
|
|
|
83
86
|
}
|
|
84
87
|
}, [ 'definitions', artifactName, 'query' ]);
|
|
85
88
|
}
|
|
89
|
+
|
|
90
|
+
if (artifact.projection) { // undo our hack
|
|
91
|
+
artifact.projection = artifact.query.SELECT;
|
|
92
|
+
|
|
93
|
+
delete artifact.query;
|
|
94
|
+
}
|
|
86
95
|
});
|
|
87
96
|
|
|
88
97
|
/**
|
|
@@ -1,10 +1,62 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const {
|
|
4
|
-
|
|
3
|
+
const {
|
|
4
|
+
getUtils, cloneCsn, applyTransformationsOnNonDictionary,
|
|
5
|
+
} = require('../../model/csnUtils');
|
|
5
6
|
const { implicitAs, csnRefs } = require('../../model/csnRefs');
|
|
6
7
|
const { isBetaEnabled } = require('../../base/model');
|
|
7
8
|
|
|
9
|
+
/**
|
|
10
|
+
* If a mixin association is published, return the mixin association.
|
|
11
|
+
*
|
|
12
|
+
* @param {CSN.Query} query Query of the artifact to check
|
|
13
|
+
* @param {object} association Association (Element) published by the view
|
|
14
|
+
* @param {string} associationName
|
|
15
|
+
* @returns {object} The mixin association
|
|
16
|
+
*/
|
|
17
|
+
function getMixinAssocOfQueryIfPublished(query, association, associationName) {
|
|
18
|
+
if (query && query.SELECT && query.SELECT.mixin) {
|
|
19
|
+
const aliasedColumnsMap = Object.create(null);
|
|
20
|
+
if (query.SELECT.columns) {
|
|
21
|
+
for (const column of query.SELECT.columns) {
|
|
22
|
+
if (column.as && column.ref && column.ref.length === 1)
|
|
23
|
+
aliasedColumnsMap[column.as] = column;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
for (const elem of Object.keys(query.SELECT.mixin)) {
|
|
28
|
+
const mixinElement = query.SELECT.mixin[elem];
|
|
29
|
+
let originalName = associationName;
|
|
30
|
+
if (aliasedColumnsMap[associationName])
|
|
31
|
+
originalName = aliasedColumnsMap[associationName].ref[0];
|
|
32
|
+
|
|
33
|
+
if (elem === originalName)
|
|
34
|
+
return { mixinElement, mixinName: originalName };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return {};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Check wether the given artifact uses the given mixin association.
|
|
42
|
+
*
|
|
43
|
+
* We can rely on the fact that there can be no usage starting with $self/$projection,
|
|
44
|
+
* since lib/checks/selectItems.js forbids that.
|
|
45
|
+
*
|
|
46
|
+
* @param {CSN.Query} query Query of the artifact to check
|
|
47
|
+
* @param {object} association Mixin association (Element) to check for
|
|
48
|
+
* @param {string} associationName
|
|
49
|
+
* @returns {boolean} True if used
|
|
50
|
+
*/
|
|
51
|
+
function usesMixinAssociation(query, association, associationName) {
|
|
52
|
+
if (query && query.SELECT && query.SELECT.columns) {
|
|
53
|
+
for (const column of query.SELECT.columns) {
|
|
54
|
+
if (typeof column === 'object' && column.ref && column.ref.length > 1 && (column.ref[0] === associationName || column.ref[0].id === associationName))
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
8
60
|
|
|
9
61
|
/**
|
|
10
62
|
* @param {CSN.Model} csn
|
|
@@ -15,7 +67,7 @@ const { isBetaEnabled } = require('../../base/model');
|
|
|
15
67
|
*/
|
|
16
68
|
function getViewTransformer(csn, options, messageFunctions, transformCommon) {
|
|
17
69
|
const {
|
|
18
|
-
get$combined,
|
|
70
|
+
get$combined, isAssocOrComposition,
|
|
19
71
|
} = getUtils(csn);
|
|
20
72
|
const { inspectRef, queryOrMain } = csnRefs(csn);
|
|
21
73
|
const pathDelimiter = (options.forHana.names === 'hdbcds') ? '.' : '_';
|
|
@@ -181,41 +233,38 @@ function getViewTransformer(csn, options, messageFunctions, transformCommon) {
|
|
|
181
233
|
* create the __clone for publishing stuff.
|
|
182
234
|
*
|
|
183
235
|
* @todo Factor out the checks
|
|
184
|
-
* @todo Union, Join, Subqueries? Assoc usage in there? __clone?
|
|
185
236
|
* @param {CSN.Query} query
|
|
186
237
|
* @param {object} elements
|
|
187
238
|
* @param {object} columnMap
|
|
188
239
|
* @param {WeakMap} publishedMixins Map to collect the published mixins
|
|
189
240
|
* @param {CSN.Element} elem
|
|
190
241
|
* @param {string} elemName
|
|
191
|
-
* @param {CSN.Path}
|
|
242
|
+
* @param {CSN.Path} elementsPath Path pointing to elements
|
|
243
|
+
* @param {CSN.Path} queryPath Path pointing to the query
|
|
192
244
|
*/
|
|
193
|
-
function handleAssociationElement(query, elements, columnMap, publishedMixins, elem, elemName,
|
|
194
|
-
if (isUnion(
|
|
245
|
+
function handleAssociationElement(query, elements, columnMap, publishedMixins, elem, elemName, elementsPath, queryPath) {
|
|
246
|
+
if (isUnion(queryPath) && options.transformation === 'hdbcds') {
|
|
195
247
|
if (isBetaEnabled(options, 'ignoreAssocPublishingInUnion') && doA2J) {
|
|
196
248
|
if (elem.keys)
|
|
197
|
-
info(null,
|
|
249
|
+
info(null, queryPath, `Managed association "${elemName}", published in a UNION, will be ignored`);
|
|
198
250
|
else
|
|
199
|
-
info(null,
|
|
251
|
+
info(null, queryPath, `Association "${elemName}", published in a UNION, will be ignored`);
|
|
200
252
|
|
|
201
253
|
elem._ignore = true;
|
|
202
254
|
}
|
|
203
255
|
else {
|
|
204
|
-
error(null,
|
|
256
|
+
error(null, queryPath, `Association "${elemName}" can't be published in a SAP HANA CDS UNION`);
|
|
205
257
|
}
|
|
206
258
|
}
|
|
207
|
-
else if (
|
|
208
|
-
error(null,
|
|
259
|
+
else if (queryPath.length > 4 && options.transformation === 'hdbcds') { // path.length > 4 -> is a subquery
|
|
260
|
+
error(null, queryPath, { name: elemName },
|
|
209
261
|
'Association $(NAME) can\'t be published in a subquery');
|
|
210
262
|
}
|
|
211
263
|
else {
|
|
212
|
-
/* Old implementation:
|
|
213
|
-
const isNotMixinByItself = !(elem.value && elem.value.path && elem.value.path.length == 1 && art.query && art.query.mixin && art.query.mixin[elem.value.path[0].id]);
|
|
214
|
-
*/
|
|
215
264
|
const isNotMixinByItself = checkIsNotMixinByItself(query, columnMap, elemName);
|
|
216
265
|
const { mixinElement, mixinName } = getMixinAssocOfQueryIfPublished(query, elem, elemName);
|
|
217
266
|
if (isNotMixinByItself || mixinElement !== undefined) {
|
|
218
|
-
|
|
267
|
+
// If the mixin is only published and not used, only display the __ clone. Kill the "original".
|
|
219
268
|
if (mixinElement !== undefined && !usesMixinAssociation(query, elem, elemName))
|
|
220
269
|
delete query.SELECT.mixin[mixinName];
|
|
221
270
|
|
|
@@ -226,8 +275,8 @@ function getViewTransformer(csn, options, messageFunctions, transformCommon) {
|
|
|
226
275
|
mixinElemName = `_${mixinElemName}`;
|
|
227
276
|
|
|
228
277
|
// Copy the association element to the MIXIN clause under its alias name
|
|
229
|
-
//
|
|
230
|
-
const mixinElem =
|
|
278
|
+
// Needs to be a deep copy, as we transform the on-condition
|
|
279
|
+
const mixinElem = cloneCsn(elem, options);
|
|
231
280
|
// Perform common transformations on the newly generated MIXIN element (won't be reached otherwise)
|
|
232
281
|
transformCommon(mixinElem, mixinElemName);
|
|
233
282
|
|
|
@@ -238,30 +287,24 @@ function getViewTransformer(csn, options, messageFunctions, transformCommon) {
|
|
|
238
287
|
// and fixing the association alias just created
|
|
239
288
|
|
|
240
289
|
if (mixinElem.on) {
|
|
241
|
-
mixinElem.on =
|
|
242
|
-
ref: (ref) => {
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
// Prepend '$projection' to the path, unless the first path step is the (mixin) element itself or starts with '$')
|
|
246
|
-
if (clonedPath[0] === elemName) {
|
|
247
|
-
clonedPath[0] = mixinElemName;
|
|
290
|
+
mixinElem.on = applyTransformationsOnNonDictionary(mixinElem, 'on', {
|
|
291
|
+
ref: (parent, prop, ref, refpath) => {
|
|
292
|
+
if (ref[0] === elemName) {
|
|
293
|
+
ref[0] = mixinElemName;
|
|
248
294
|
}
|
|
249
|
-
else if (!(
|
|
250
|
-
|
|
251
|
-
clonedPath.unshift(projectionId);
|
|
295
|
+
else if (!(ref[0] && ref[0].startsWith('$'))) {
|
|
296
|
+
ref.unshift('$projection');
|
|
252
297
|
}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
return func;
|
|
298
|
+
else if (ref[0] && ref[0].startsWith('$')) {
|
|
299
|
+
// TODO: I think this is non-sense. Stuff with $ is either magic or must start with $self, right?
|
|
300
|
+
const { scope } = inspectRef(refpath);
|
|
301
|
+
if (scope !== '$magic' && scope !== '$self')
|
|
302
|
+
ref.unshift('$projection');
|
|
303
|
+
}
|
|
304
|
+
parent.ref = ref;
|
|
305
|
+
return ref;
|
|
263
306
|
},
|
|
264
|
-
});
|
|
307
|
+
}, elementsPath.concat(elemName));
|
|
265
308
|
}
|
|
266
309
|
|
|
267
310
|
if (!mixinElem._ignore)
|
|
@@ -304,6 +347,10 @@ function getViewTransformer(csn, options, messageFunctions, transformCommon) {
|
|
|
304
347
|
// eslint-disable-next-line complexity
|
|
305
348
|
function transformViewOrEntity(query, artifact, artName, path) {
|
|
306
349
|
const { elements } = queryOrMain(query, artifact);
|
|
350
|
+
// We use the elements from the leading query/main artifact - adapt the path
|
|
351
|
+
const elementsPath = elements === artifact.elements ? path.slice(0, 2).concat('elements') : path.concat('elements');
|
|
352
|
+
const queryPath = path;
|
|
353
|
+
|
|
307
354
|
let hasNonAssocElements = false;
|
|
308
355
|
const isSelect = query && query.SELECT;
|
|
309
356
|
const isProjection = !!artifact.projection || query && query.SELECT && !query.SELECT.columns;
|
|
@@ -338,12 +385,12 @@ function getViewTransformer(csn, options, messageFunctions, transformCommon) {
|
|
|
338
385
|
// (180 b) Create MIXINs for association elements in projections or views (those that are not mixins by themselves)
|
|
339
386
|
// CDXCORE-585: Allow mixin associations to be used and published in parallel
|
|
340
387
|
if (query !== undefined && elem.target)
|
|
341
|
-
handleAssociationElement(query, elements, columnMap, publishedMixins, elem, elemName,
|
|
388
|
+
handleAssociationElement(query, elements, columnMap, publishedMixins, elem, elemName, elementsPath, queryPath);
|
|
342
389
|
}
|
|
343
390
|
|
|
344
391
|
if (query && !hasNonAssocElements) {
|
|
345
|
-
|
|
346
|
-
|
|
392
|
+
// Complain if there are no elements other than unmanaged associations
|
|
393
|
+
// Allow with plain
|
|
347
394
|
error(null, [ 'definitions', artName ], { $reviewed: true },
|
|
348
395
|
'Expecting view or projection to have at least one element that is not an unmanaged association');
|
|
349
396
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
const { setProp, isBetaEnabled } = require('../base/model');
|
|
4
4
|
const { getUtils, cloneCsn, forEachGeneric,
|
|
5
5
|
forEachMember,
|
|
6
|
-
forEachMemberRecursively, forEachRef,
|
|
6
|
+
forEachMemberRecursively, forEachRef, getNamespace, getResultingName,
|
|
7
7
|
forAllQueries, forAllElements, hasAnnotationValue, getArtifactDatabaseNameOf,
|
|
8
8
|
getElementDatabaseNameOf, isBuiltinType, applyTransformations,
|
|
9
9
|
isPersistedOnDatabase, getNormalizedQuery, isAspect, walkCsnPath,
|
|
@@ -213,10 +213,6 @@ function transformForHanaWithCsn(inputModel, options, moduleName) {
|
|
|
213
213
|
}
|
|
214
214
|
});
|
|
215
215
|
|
|
216
|
-
// Must happen after A2J, as A2J needs $self to correctly resolve stuff
|
|
217
|
-
if(doA2J)
|
|
218
|
-
flattening.removeLeadingSelf(csn);
|
|
219
|
-
|
|
220
216
|
const {
|
|
221
217
|
flattenStructuredElement,
|
|
222
218
|
flattenStructStepsInRef, getForeignKeyArtifact,
|
|
@@ -232,6 +228,7 @@ function transformForHanaWithCsn(inputModel, options, moduleName) {
|
|
|
232
228
|
isStructured,
|
|
233
229
|
addStringAnnotationTo,
|
|
234
230
|
cloneWithTransformations,
|
|
231
|
+
getContextOfArtifact,
|
|
235
232
|
} = getUtils(csn);
|
|
236
233
|
|
|
237
234
|
// (000) Rename primitive types, make UUID a String
|
|
@@ -394,6 +391,10 @@ function transformForHanaWithCsn(inputModel, options, moduleName) {
|
|
|
394
391
|
removeKeyPropInType,
|
|
395
392
|
]);
|
|
396
393
|
|
|
394
|
+
// Remove leading $self to keep renderer-diffs smaller
|
|
395
|
+
if(doA2J)
|
|
396
|
+
flattening.removeLeadingSelf(csn);
|
|
397
|
+
|
|
397
398
|
throwWithError();
|
|
398
399
|
|
|
399
400
|
timetrace.stop();
|
|
@@ -797,14 +798,31 @@ function transformForHanaWithCsn(inputModel, options, moduleName) {
|
|
|
797
798
|
|| hasAnnotationValue(artifact, '@cds.persistence.exists'))
|
|
798
799
|
artifact._ignore = true;
|
|
799
800
|
|
|
801
|
+
const namingMode = options.forHana && options.forHana.names;
|
|
800
802
|
// issue #3450 HANA CDS can not handle external artifacts which are part of a HANA CDS context
|
|
801
|
-
if (
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
803
|
+
if (hasAnnotationValue(artifact, '@cds.persistence.exists') &&
|
|
804
|
+
options.transformation === 'hdbcds' &&
|
|
805
|
+
(namingMode === 'quoted' || namingMode === 'hdbcds')) {
|
|
806
|
+
let hanaCDSContextName;
|
|
807
|
+
if(namingMode === 'hdbcds') {
|
|
808
|
+
// for hdbcds names we only create a context if you defined a context/service in your cdl model
|
|
809
|
+
hanaCDSContextName = getContextOfArtifact(artifactName);
|
|
810
|
+
}
|
|
811
|
+
else {
|
|
812
|
+
// for quoted naming mode, we create a context if you either defined a context/service
|
|
813
|
+
// or a namespace in your cdl model
|
|
814
|
+
hanaCDSContextName = getContextOfArtifact(artifactName) || getNamespace(csn, artifactName);
|
|
815
|
+
}
|
|
816
|
+
if (hanaCDSContextName) {
|
|
817
|
+
warning('anno-unstable-hdbcds', [ 'definitions', artifactName ],
|
|
818
|
+
{
|
|
819
|
+
id: getResultingName(csn, options.forHana.names, artifactName),
|
|
820
|
+
name: getResultingName(csn, options.forHana.names, hanaCDSContextName),
|
|
821
|
+
anno: 'cds.persistence.exists',
|
|
822
|
+
},
|
|
823
|
+
'Do not use $(ANNO) on an entity named $(ID) in SAP HANA CDS when we also create a SAP HANA CDS context named $(NAME)'
|
|
824
|
+
);
|
|
825
|
+
}
|
|
808
826
|
}
|
|
809
827
|
}
|
|
810
828
|
}
|
|
@@ -1324,6 +1342,10 @@ function transformForHanaWithCsn(inputModel, options, moduleName) {
|
|
|
1324
1342
|
if (ref[0] === assocName) // we are in the "path" from the forwarding assoc => need to remove the first part of the path
|
|
1325
1343
|
{
|
|
1326
1344
|
ref.shift();
|
|
1345
|
+
} else if(ref && ref.length > 1 && ref[0] === '$self' && ref[1] === assocName) {
|
|
1346
|
+
// We could also have a $self infront of the assoc name - so we would need to shift twice
|
|
1347
|
+
ref.shift();
|
|
1348
|
+
ref.shift();
|
|
1327
1349
|
}
|
|
1328
1350
|
else { // we are in the backlink assoc "path" => need to push at the beginning the association's id
|
|
1329
1351
|
ref.unshift(elemName);
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const { setProp, forEachGeneric, forEachDefinition, isBetaEnabled } = require('../base/model');
|
|
4
|
-
|
|
4
|
+
const { makeMessageFunction } = require('../base/messages');
|
|
5
5
|
const { recompileX } = require('../compiler/index');
|
|
6
|
-
|
|
6
|
+
const { linkToOrigin, pathName } = require('../compiler/utils');
|
|
7
7
|
const {compactModel, compactExpr} = require('../json/to-csn');
|
|
8
8
|
const { deduplicateMessages } = require('../base/messages');
|
|
9
9
|
const { timetrace } = require('../utils/timetrace');
|
|
@@ -309,7 +309,7 @@ function translateAssocsToJoins(model, inputOptions = {})
|
|
|
309
309
|
let [QA, ps] = rightMostQA(tail, head._navigation._parent.$QA || head._navigation.$QA);
|
|
310
310
|
if(!QA) {
|
|
311
311
|
error(null, pathNode.$location,
|
|
312
|
-
{ name: pathNode.path
|
|
312
|
+
{ name: pathName(pathNode.path) },
|
|
313
313
|
'Please debug me: No QA found for generic path rewriting in $(NAME)')
|
|
314
314
|
return;
|
|
315
315
|
}
|
package/lib/utils/file.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
'use strict';
|
|
4
4
|
|
|
5
5
|
const fs = require('fs');
|
|
6
|
+
const util = require('util');
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Split the given source string into its lines. Respects Unix,
|
|
@@ -19,8 +20,8 @@ function splitLines(src) {
|
|
|
19
20
|
* Returns filesystem utils readFile(), isFile(), realpath() for _CDS_ usage.
|
|
20
21
|
* This includes a trace as well as usage of a file cache.
|
|
21
22
|
*
|
|
22
|
-
* Note: The synchronous versions accept a callback
|
|
23
|
-
* immediately! This is different from NodeJS's readFileSync()!
|
|
23
|
+
* Note: The synchronous versions accept a callback instead of being async (duh!), which
|
|
24
|
+
* is executed immediately! This is different from NodeJS's readFileSync()!
|
|
24
25
|
* This is done to allow using it in places where fs.readFile (async) is used.
|
|
25
26
|
*
|
|
26
27
|
* @param {object} fileCache
|
|
@@ -47,10 +48,13 @@ function cdsFs(fileCache, enableTrace) {
|
|
|
47
48
|
});
|
|
48
49
|
|
|
49
50
|
return {
|
|
51
|
+
readFileAsync: util.promisify(readFile),
|
|
50
52
|
readFile,
|
|
51
53
|
readFileSync,
|
|
54
|
+
isFileAsync: util.promisify(isFile),
|
|
52
55
|
isFile,
|
|
53
56
|
isFileSync,
|
|
57
|
+
realpathAsync: util.promisify(realpath),
|
|
54
58
|
realpath,
|
|
55
59
|
realpathSync,
|
|
56
60
|
};
|
package/package.json
CHANGED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* If a mixin association is published, return the mixin association.
|
|
5
|
-
*
|
|
6
|
-
* @param {CSN.Query} query Query of the artifact to check
|
|
7
|
-
* @param {object} association Association (Element) published by the view
|
|
8
|
-
* @param {string} associationName
|
|
9
|
-
* @returns {object} The mixin association
|
|
10
|
-
*/
|
|
11
|
-
function getMixinAssocOfQueryIfPublished(query, association, associationName) {
|
|
12
|
-
if (query && query.SELECT && query.SELECT.mixin) {
|
|
13
|
-
const aliasedColumnsMap = Object.create(null);
|
|
14
|
-
if (query.SELECT.columns) {
|
|
15
|
-
for (const column of query.SELECT.columns) {
|
|
16
|
-
if (column.as && column.ref && column.ref.length === 1)
|
|
17
|
-
aliasedColumnsMap[column.as] = column;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
for (const elem of Object.keys(query.SELECT.mixin)) {
|
|
22
|
-
const mixinElement = query.SELECT.mixin[elem];
|
|
23
|
-
let originalName = associationName;
|
|
24
|
-
if (aliasedColumnsMap[associationName])
|
|
25
|
-
originalName = aliasedColumnsMap[associationName].ref[0];
|
|
26
|
-
|
|
27
|
-
if (elem === originalName)
|
|
28
|
-
return { mixinElement, mixinName: originalName };
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
return {};
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Check wether the given artifact uses the given mixin association.
|
|
36
|
-
*
|
|
37
|
-
* @param {CSN.Query} query Query of the artifact to check
|
|
38
|
-
* @param {object} association Mixin association (Element) to check for
|
|
39
|
-
* @param {string} associationName
|
|
40
|
-
* @returns {boolean} True if used
|
|
41
|
-
*/
|
|
42
|
-
function usesMixinAssociation(query, association, associationName) {
|
|
43
|
-
if (query && query.SELECT && query.SELECT.columns) {
|
|
44
|
-
for (const column of query.SELECT.columns) {
|
|
45
|
-
if (typeof column === 'object' && column.ref && column.ref.length > 1 && (column.ref[0] === associationName || column.ref[0].id === associationName)) {
|
|
46
|
-
// FIXME: This is not necessarily correct: the assoc name needs not be the first component, as e.g. $projection.assoc
|
|
47
|
-
// would be also valid. Check other paths like $self.assoc ....
|
|
48
|
-
return true;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
return false;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
module.exports = {
|
|
56
|
-
usesMixinAssociation,
|
|
57
|
-
getMixinAssocOfQueryIfPublished,
|
|
58
|
-
};
|