@sap/cds-compiler 2.11.4 → 2.12.0
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 +58 -1
- package/bin/cds_update_identifiers.js +7 -7
- package/bin/cdsc.js +9 -10
- package/doc/CHANGELOG_ARCHIVE.md +1 -1
- package/doc/CHANGELOG_BETA.md +12 -0
- package/lib/api/main.js +2 -0
- package/lib/api/options.js +2 -2
- package/lib/base/message-registry.js +31 -2
- package/lib/base/model.js +1 -0
- package/lib/base/optionProcessorHelper.js +97 -69
- package/lib/checks/.eslintrc.json +2 -0
- package/lib/checks/actionsFunctions.js +2 -1
- package/lib/checks/foreignKeys.js +4 -4
- package/lib/checks/managedInType.js +4 -4
- package/lib/checks/queryNoDbArtifacts.js +1 -3
- package/lib/checks/sql-snippets.js +93 -0
- package/lib/checks/validator.js +8 -0
- package/lib/compiler/assert-consistency.js +5 -3
- package/lib/compiler/base.js +0 -1
- package/lib/compiler/checks.js +32 -9
- package/lib/compiler/definer.js +25 -4
- package/lib/compiler/index.js +1 -1
- package/lib/compiler/propagator.js +3 -2
- package/lib/compiler/resolver.js +97 -6
- package/lib/compiler/shared.js +12 -1
- package/lib/compiler/utils.js +7 -0
- package/lib/edm/annotations/genericTranslation.js +34 -17
- package/lib/edm/annotations/preprocessAnnotations.js +1 -1
- package/lib/edm/csn2edm.js +1 -1
- package/lib/edm/edm.js +8 -8
- package/lib/edm/edmPreprocessor.js +30 -23
- package/lib/edm/edmUtils.js +11 -12
- package/lib/gen/Dictionary.json +82 -40
- package/lib/gen/language.checksum +1 -1
- package/lib/gen/language.interp +3 -1
- package/lib/gen/language.tokens +15 -14
- package/lib/gen/languageLexer.interp +9 -1
- package/lib/gen/languageLexer.js +830 -779
- package/lib/gen/languageLexer.tokens +7 -6
- package/lib/gen/languageParser.js +2401 -2282
- package/lib/json/from-csn.js +47 -16
- package/lib/json/to-csn.js +17 -5
- package/lib/language/antlrParser.js +3 -3
- package/lib/language/docCommentParser.js +1 -1
- package/lib/language/genericAntlrParser.js +68 -51
- package/lib/language/language.g4 +128 -74
- package/lib/language/multiLineStringParser.js +536 -0
- package/lib/main.d.ts +5 -3
- package/lib/main.js +3 -2
- package/lib/model/csnRefs.js +116 -68
- package/lib/model/csnUtils.js +40 -48
- package/lib/model/enrichCsn.js +30 -14
- package/lib/optionProcessor.js +3 -3
- package/lib/render/DuplicateChecker.js +1 -1
- package/lib/render/manageConstraints.js +1 -1
- package/lib/render/toCdl.js +193 -79
- package/lib/render/toHdbcds.js +179 -95
- package/lib/render/toRename.js +7 -10
- package/lib/render/toSql.js +57 -40
- package/lib/render/utils/common.js +24 -5
- package/lib/render/utils/sql.js +6 -4
- package/lib/transform/braceExpression.js +4 -2
- package/lib/transform/db/associations.js +389 -0
- package/lib/transform/db/cdsPersistence.js +150 -0
- package/lib/transform/db/constraints.js +6 -4
- package/lib/transform/db/draft.js +3 -2
- package/lib/transform/db/expansion.js +4 -5
- package/lib/transform/db/flattening.js +5 -6
- package/lib/transform/db/temporal.js +236 -0
- package/lib/transform/db/transformExists.js +36 -23
- package/lib/transform/forHanaNew.js +35 -626
- package/lib/transform/forOdataNew.js +5 -4
- package/lib/transform/localized.js +3 -14
- package/lib/transform/odata/generateForeignKeyElements.js +2 -2
- package/lib/transform/transformUtilsNew.js +13 -13
- package/lib/transform/translateAssocsToJoins.js +8 -8
- package/lib/transform/universalCsnEnricher.js +217 -47
- package/lib/utils/file.js +2 -1
- package/lib/utils/timetrace.js +8 -2
- package/package.json +1 -1
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
cloneCsn, forEachGeneric,
|
|
5
|
+
forAllElements,
|
|
6
|
+
getUtils,
|
|
7
|
+
} = require('../../model/csnUtils');
|
|
8
|
+
|
|
9
|
+
const transformUtils = require('../transformUtilsNew');
|
|
10
|
+
const { setProp } = require('../../base/model');
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Return a callback function for forEachDefinition that flattens the foreign keys of managed associations.
|
|
15
|
+
* Foreign keys that are managed associations are replaced by their respective foreign keys.
|
|
16
|
+
*
|
|
17
|
+
* @param {CSN.Model} csn
|
|
18
|
+
* @param {CSN.Options} options
|
|
19
|
+
* @param {string} pathDelimiter
|
|
20
|
+
* @returns {(artifact: CSN.Artifact, artifactName: string) => void} Callback for forEachDefinition
|
|
21
|
+
*/
|
|
22
|
+
function getForeignKeyFlattener(csn, options, pathDelimiter) {
|
|
23
|
+
const doA2J = !(options.transformation === 'hdbcds' && options.sqlMapping === 'hdbcds');
|
|
24
|
+
const {
|
|
25
|
+
isManagedAssociationElement,
|
|
26
|
+
isStructured,
|
|
27
|
+
inspectRef,
|
|
28
|
+
} = getUtils(csn);
|
|
29
|
+
|
|
30
|
+
const {
|
|
31
|
+
flattenStructuredElement,
|
|
32
|
+
flattenStructStepsInRef,
|
|
33
|
+
} = transformUtils.getTransformers(csn, options, pathDelimiter);
|
|
34
|
+
|
|
35
|
+
return handleManagedAssociationFKs;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Flatten and create the foreign key elements of managed associaitons
|
|
39
|
+
*
|
|
40
|
+
* @param {CSN.Artifact} art
|
|
41
|
+
* @param {string} artName
|
|
42
|
+
*/
|
|
43
|
+
function handleManagedAssociationFKs(art, artName) {
|
|
44
|
+
if ((art.kind === 'entity' || art.kind === 'view') && doA2J) {
|
|
45
|
+
forAllElements(art, artName, (parent, elements, pathToElements) => {
|
|
46
|
+
forEachGeneric(parent, 'elements', (element, elemName) => {
|
|
47
|
+
if (element.keys && isManagedAssociationElement(element)) {
|
|
48
|
+
// replace foreign keys that are managed associations by their respective foreign keys
|
|
49
|
+
flattenFKs(element, elemName, [ ...pathToElements, elemName ]);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Flattens all foreign keys
|
|
58
|
+
*
|
|
59
|
+
* Structures will be resolved to individual elements with scalar types
|
|
60
|
+
*
|
|
61
|
+
* Associations will be replaced by their respective foreign keys
|
|
62
|
+
*
|
|
63
|
+
* If a structure contains an assoc, this will also be resolved and vice versa
|
|
64
|
+
*
|
|
65
|
+
* @param {CSN.Element} assoc
|
|
66
|
+
* @param {string} assocName
|
|
67
|
+
* @param {CSN.Path} path
|
|
68
|
+
*/
|
|
69
|
+
function flattenFKs(assoc, assocName, path) {
|
|
70
|
+
let finished = false;
|
|
71
|
+
while (!finished) {
|
|
72
|
+
const newKeys = [];
|
|
73
|
+
finished = processKeys(assoc, assocName, path, newKeys);
|
|
74
|
+
assoc.keys = newKeys;
|
|
75
|
+
}
|
|
76
|
+
assoc.keys = assoc.keys.filter(o => !o.$toDelete);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Walk over the keys and replace structures by their leafs, managed associations by their foreign keys and keep scalar values as-is.
|
|
81
|
+
*
|
|
82
|
+
* @param {CSN.Element} assoc
|
|
83
|
+
* @param {string} assocName
|
|
84
|
+
* @param {CSN.Path} path
|
|
85
|
+
* @param {object[]} collector New keys array to collect the flattened stuff in
|
|
86
|
+
* @returns {boolean} True if all keys are scalar - false if there are things that still need to be processed.
|
|
87
|
+
*/
|
|
88
|
+
function processKeys(assoc, assocName, path, collector) {
|
|
89
|
+
let finished = true;
|
|
90
|
+
for (let i = 0; i < assoc.keys.length; i++) {
|
|
91
|
+
const pathToKey = path.concat([ 'keys', i ]);
|
|
92
|
+
const { art } = inspectRef(pathToKey);
|
|
93
|
+
const { ref } = assoc.keys[i];
|
|
94
|
+
if (isStructured(art)) {
|
|
95
|
+
finished = false;
|
|
96
|
+
// Mark this element to filter it later - not needed after expansion
|
|
97
|
+
setProp(assoc.keys[i], '$toDelete', true);
|
|
98
|
+
const flat = flattenStructuredElement(art, ref[ref.length - 1], [], pathToKey);
|
|
99
|
+
Object.keys(flat).forEach((flatElemName) => {
|
|
100
|
+
const key = assoc.keys[i];
|
|
101
|
+
const clone = cloneCsn(assoc.keys[i], options);
|
|
102
|
+
if (clone.as) {
|
|
103
|
+
const lastRef = clone.ref[clone.ref.length - 1];
|
|
104
|
+
// Cut off the last ref part from the beginning of the flat name
|
|
105
|
+
const flatBaseName = flatElemName.slice(lastRef.length);
|
|
106
|
+
// Join it to the existing table alias
|
|
107
|
+
clone.as += flatBaseName;
|
|
108
|
+
// do not loose the $ref for nested keys
|
|
109
|
+
if (key.$ref) {
|
|
110
|
+
let aliasedLeaf = key.$ref[key.$ref.length - 1];
|
|
111
|
+
aliasedLeaf += flatBaseName;
|
|
112
|
+
setProp(clone, '$ref', key.$ref.slice(0, key.$ref.length - 1).concat(aliasedLeaf));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (clone.ref) {
|
|
116
|
+
clone.ref[clone.ref.length - 1] = flatElemName;
|
|
117
|
+
// Now we need to properly flatten the whole ref
|
|
118
|
+
clone.ref = flattenStructStepsInRef(clone.ref, pathToKey);
|
|
119
|
+
}
|
|
120
|
+
if (!clone.as)
|
|
121
|
+
clone.as = flatElemName;
|
|
122
|
+
// Directly work on csn.definitions - this way the changes take effect in csnRefs/inspectRef immediately
|
|
123
|
+
// Add the newly generated foreign keys to the end - they will be picked up later on
|
|
124
|
+
// Recursive solutions run into call stack issues
|
|
125
|
+
collector.push(clone);
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
else if (art.target) {
|
|
129
|
+
finished = false;
|
|
130
|
+
// Mark this element to filter it later - not needed after expansion
|
|
131
|
+
setProp(assoc.keys[i], '$toDelete', true);
|
|
132
|
+
// Directly work on csn.definitions - this way the changes take effect in csnRefs/inspectRef immediately
|
|
133
|
+
// Add the newly generated foreign keys to the end - they will be picked up later on
|
|
134
|
+
// Recursive solutions run into call stack issues
|
|
135
|
+
art.keys.forEach(key => collector.push(cloneAndExtendRef(key, assoc.keys[i])));
|
|
136
|
+
}
|
|
137
|
+
else if (assoc.keys[i].ref && !assoc.keys[i].as) {
|
|
138
|
+
assoc.keys[i].as = assoc.keys[i].ref[assoc.keys[i].ref.length - 1];
|
|
139
|
+
collector.push(assoc.keys[i]);
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
collector.push(assoc.keys[i]);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return finished;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Clone base and extend the .ref and .as of the clone with the .ref and .as of ref.
|
|
150
|
+
*
|
|
151
|
+
* @param {object} key A foreign key entry (of a managed assoc as a fk of another assoc)
|
|
152
|
+
* @param {object} base The fk-ref that has key as a fk
|
|
153
|
+
* @returns {object} The clone of base
|
|
154
|
+
*/
|
|
155
|
+
function cloneAndExtendRef(key, base) {
|
|
156
|
+
const { ref } = base;
|
|
157
|
+
const clone = cloneCsn(base, options);
|
|
158
|
+
if (key.ref) {
|
|
159
|
+
// We build a ref that contains the aliased fk - that element will be created later on, so this ref is not resolvable yet
|
|
160
|
+
// Therefore we keep it as $ref - ref is the non-aliased, resolvable "clone"
|
|
161
|
+
// Later on, after we know that these foreign key elements are created, we replace ref with this $ref
|
|
162
|
+
let $ref;
|
|
163
|
+
if (base.$ref) {
|
|
164
|
+
// if a base $ref is provided, use it to correctly resolve association chains
|
|
165
|
+
const refChain = [ base.$ref[base.$ref.length - 1] ].concat(key.as || key.ref);
|
|
166
|
+
$ref = base.$ref.slice(0, base.$ref.length - 1).concat(refChain);
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
$ref = base.ref.concat( key.as || key.ref); // Keep along the aliases
|
|
170
|
+
}
|
|
171
|
+
setProp(clone, '$ref', $ref);
|
|
172
|
+
clone.ref = clone.ref.concat(key.ref);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (!clone.as && clone.ref && clone.ref.length > 0)
|
|
176
|
+
clone.as = ref[ref.length - 1] + pathDelimiter + (key.as || key.ref.join(pathDelimiter));
|
|
177
|
+
else
|
|
178
|
+
clone.as += pathDelimiter + (key.as || key.ref.join(pathDelimiter));
|
|
179
|
+
|
|
180
|
+
return clone;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Return a callback function for forEachDefinition that
|
|
187
|
+
*
|
|
188
|
+
* @param {CSN.Model} csn
|
|
189
|
+
* @param {CSN.Options} options
|
|
190
|
+
* @param {string} pathDelimiter
|
|
191
|
+
* @param {object} messageFunctions
|
|
192
|
+
* @param {Function} messageFunctions.error
|
|
193
|
+
* @returns {(artifact: CSN.Artifact, artifactName: string) => void} Callback for forEachDefinition
|
|
194
|
+
*/
|
|
195
|
+
function getForeignKeyElementCreator(csn, options, pathDelimiter, messageFunctions) {
|
|
196
|
+
const { error } = messageFunctions;
|
|
197
|
+
const doA2J = !(options.transformation === 'hdbcds' && options.sqlMapping === 'hdbcds');
|
|
198
|
+
const {
|
|
199
|
+
isManagedAssociationElement,
|
|
200
|
+
} = getUtils(csn);
|
|
201
|
+
|
|
202
|
+
const {
|
|
203
|
+
flattenStructStepsInRef, getForeignKeyArtifact,
|
|
204
|
+
} = transformUtils.getTransformers(csn, options, pathDelimiter);
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
return createForeignKeyElements;
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Create the foreign key elements for managed associations.
|
|
211
|
+
* Create them in-place, right after the corresponding association.
|
|
212
|
+
*
|
|
213
|
+
*
|
|
214
|
+
* @param {CSN.Artifact} art
|
|
215
|
+
* @param {string} artName
|
|
216
|
+
*/
|
|
217
|
+
function createForeignKeyElements(art, artName) {
|
|
218
|
+
if ((art.kind === 'entity' || art.kind === 'view') && doA2J) {
|
|
219
|
+
forAllElements(art, artName, (parent, elements, pathToElements) => {
|
|
220
|
+
const elementsArray = [];
|
|
221
|
+
forEachGeneric(parent, 'elements', (element, elemName) => {
|
|
222
|
+
elementsArray.push([ elemName, element ]);
|
|
223
|
+
if (element.keys && isManagedAssociationElement(element)) {
|
|
224
|
+
for (let i = 0; i < element.keys.length; i++) {
|
|
225
|
+
const foreignKey = element.keys[i];
|
|
226
|
+
const path = [ ...pathToElements, elemName, 'keys', i ];
|
|
227
|
+
foreignKey.ref = flattenStructStepsInRef(foreignKey.ref, path);
|
|
228
|
+
const [ fkName, fkElem ] = getForeignKeyArtifact(element, elemName, foreignKey, path);
|
|
229
|
+
if (parent.elements[fkName]) {
|
|
230
|
+
error(null, [ ...pathToElements, elemName ], { name: fkName, art: elemName },
|
|
231
|
+
'Generated foreign key element $(NAME) for association $(ART) conflicts with existing element');
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
elementsArray.push([ fkName, fkElem ]);
|
|
235
|
+
}
|
|
236
|
+
applyCachedAlias(foreignKey);
|
|
237
|
+
// join ref array as the struct / assoc steps are not necessary anymore
|
|
238
|
+
foreignKey.ref = [ foreignKey.ref.join(pathDelimiter) ];
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// Don't fake consistency of the model by adding empty elements {}
|
|
244
|
+
if (elementsArray.length === 0)
|
|
245
|
+
return;
|
|
246
|
+
|
|
247
|
+
parent.elements = elementsArray.reduce((previous, [ name, element ]) => {
|
|
248
|
+
previous[name] = element;
|
|
249
|
+
return previous;
|
|
250
|
+
}, Object.create(null));
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* We save the aliased representation of the fk in $ref - I suppose to stay resolvable?
|
|
256
|
+
*
|
|
257
|
+
* We now use this $ref for ref and delete it.
|
|
258
|
+
*
|
|
259
|
+
* @param {object} foreignKey
|
|
260
|
+
* @todo With nested projections, we solve similar problems, maybe adapt?
|
|
261
|
+
*/
|
|
262
|
+
function applyCachedAlias(foreignKey) {
|
|
263
|
+
// If we have a $ref use that - it resolves aliased FKs correctly
|
|
264
|
+
if (foreignKey.$ref) {
|
|
265
|
+
foreignKey.ref = foreignKey.$ref;
|
|
266
|
+
delete foreignKey.$ref;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Return a callback function for forEachDefinition that
|
|
274
|
+
*
|
|
275
|
+
* @param {CSN.Model} csn
|
|
276
|
+
* @param {CSN.Options} options
|
|
277
|
+
* @param {string} pathDelimiter
|
|
278
|
+
* @returns {(artifact: CSN.Artifact, artifactName: string) => void} Callback for forEachDefinition
|
|
279
|
+
*/
|
|
280
|
+
function getManagedAssociationTransformer(csn, options, pathDelimiter) {
|
|
281
|
+
const doA2J = !(options.transformation === 'hdbcds' && options.sqlMapping === 'hdbcds');
|
|
282
|
+
const {
|
|
283
|
+
isManagedAssociationElement,
|
|
284
|
+
} = getUtils(csn);
|
|
285
|
+
|
|
286
|
+
return handleAssociations;
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
*
|
|
290
|
+
* Generate foreign keys for managed associations
|
|
291
|
+
* Forbid aliases for foreign keys
|
|
292
|
+
*
|
|
293
|
+
* @param {CSN.Artifact} artifact
|
|
294
|
+
* @param {string} artifactName
|
|
295
|
+
*/
|
|
296
|
+
function handleAssociations(artifact, artifactName) {
|
|
297
|
+
// Do things specific for entities and views (pass 1)
|
|
298
|
+
if (artifact.kind === 'entity' || artifact.kind === 'view') {
|
|
299
|
+
const alreadyHandled = new WeakMap();
|
|
300
|
+
forAllElements(artifact, artifactName, (parent, elements) => {
|
|
301
|
+
for (const elemName in elements) {
|
|
302
|
+
const elem = elements[elemName];
|
|
303
|
+
// (140) Generate foreign key elements and ON-condition for managed associations
|
|
304
|
+
// (unless explicitly asked to keep assocs unchanged)
|
|
305
|
+
if (doA2J && isManagedAssociationElement(elem))
|
|
306
|
+
transformManagedAssociation(parent, artifactName, elem, elemName, alreadyHandled);
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Create the foreign key elements for a managed association and build the on-condition
|
|
314
|
+
*
|
|
315
|
+
* @param {CSN.Artifact} artifact
|
|
316
|
+
* @param {string} artifactName
|
|
317
|
+
* @param {Object} elem The association to process
|
|
318
|
+
* @param {string} elemName
|
|
319
|
+
* @param {WeakMap} alreadyHandled To cache which elements were already processed
|
|
320
|
+
* @returns {void}
|
|
321
|
+
*/
|
|
322
|
+
function transformManagedAssociation(artifact, artifactName, elem, elemName, alreadyHandled) {
|
|
323
|
+
// No need to run over this - we already did, possibly because it was referenced in the ON-Condition
|
|
324
|
+
// of another association - see a few lines lower
|
|
325
|
+
if (alreadyHandled.has(elem))
|
|
326
|
+
return;
|
|
327
|
+
// Generate foreign key elements for managed associations, and assemble an ON-condition with them
|
|
328
|
+
const onCondParts = [];
|
|
329
|
+
let joinWithAnd = false;
|
|
330
|
+
if (elem.keys.length === 0) {
|
|
331
|
+
elem._ignore = true;
|
|
332
|
+
}
|
|
333
|
+
else {
|
|
334
|
+
for (const foreignKey of elem.keys) {
|
|
335
|
+
// Assemble left hand side of 'assoc.key = fkey'
|
|
336
|
+
const assocKeyArg = {
|
|
337
|
+
ref: [
|
|
338
|
+
elemName,
|
|
339
|
+
].concat(foreignKey.ref),
|
|
340
|
+
};
|
|
341
|
+
const fkName = `${elemName}${pathDelimiter}${foreignKey.as}`;
|
|
342
|
+
const fKeyArg = {
|
|
343
|
+
ref: [
|
|
344
|
+
fkName,
|
|
345
|
+
],
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
if (joinWithAnd) { // more than one FK
|
|
349
|
+
onCondParts.push('and');
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
onCondParts.push(
|
|
353
|
+
assocKeyArg
|
|
354
|
+
);
|
|
355
|
+
onCondParts.push('=');
|
|
356
|
+
onCondParts.push(fKeyArg);
|
|
357
|
+
|
|
358
|
+
if (!joinWithAnd)
|
|
359
|
+
joinWithAnd = true;
|
|
360
|
+
}
|
|
361
|
+
elem.on = onCondParts;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// If the managed association has a 'key' property => remove it as unmanaged assocs cannot be keys
|
|
365
|
+
// TODO: Are there other modifiers (like 'key') that are valid for managed, but not valid for unmanaged assocs?
|
|
366
|
+
if (elem.key)
|
|
367
|
+
delete elem.key;
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
// If the managed association has a 'not null' property => remove it
|
|
371
|
+
if (elem.notNull)
|
|
372
|
+
delete elem.notNull;
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
// The association is now unmanaged, i.e. actually it should no longer have foreign keys
|
|
376
|
+
// at all. But the processing of backlink associations below expects to have them, so
|
|
377
|
+
// we don't delete them (but mark them as implicit so that toCdl does not render them)
|
|
378
|
+
|
|
379
|
+
// Remember that we already processed this
|
|
380
|
+
alreadyHandled.set(elem, true);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
module.exports = {
|
|
386
|
+
getForeignKeyFlattener,
|
|
387
|
+
getForeignKeyElementCreator,
|
|
388
|
+
getManagedAssociationTransformer,
|
|
389
|
+
};
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
forEachGeneric, forEachMemberRecursively, hasAnnotationValue, isPersistedOnDatabase,
|
|
5
|
+
getUtils,
|
|
6
|
+
} = require('../../model/csnUtils');
|
|
7
|
+
const transformUtils = require('../transformUtilsNew');
|
|
8
|
+
|
|
9
|
+
const exists = '@cds.persistence.exists';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Return a callback function for forEachDefinition that marks artifacts that are abstract or @cds.persistence.exists/skip
|
|
13
|
+
* with _ignore.
|
|
14
|
+
*
|
|
15
|
+
* @returns {(artifact: CSN.Artifact, artifactName) => void} Callback function for forEachDefinition
|
|
16
|
+
*/
|
|
17
|
+
function getAnnoProcessor() {
|
|
18
|
+
return handleCdsPersistence;
|
|
19
|
+
/**
|
|
20
|
+
* @param {CSN.Artifact} artifact
|
|
21
|
+
*/
|
|
22
|
+
function handleCdsPersistence(artifact) {
|
|
23
|
+
const ignoreArtifact = (artifact.kind === 'entity' || artifact.kind === 'view') &&
|
|
24
|
+
(artifact.abstract ||
|
|
25
|
+
hasAnnotationValue(artifact, '@cds.persistence.skip') ||
|
|
26
|
+
hasAnnotationValue(artifact, exists));
|
|
27
|
+
if (ignoreArtifact)
|
|
28
|
+
artifact._ignore = true;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Return a callback function for forEachDefinition that marks associations with _ignore
|
|
34
|
+
* if their target does not reach the database, i.e. marked with @cds.persistence.skip or is abstract
|
|
35
|
+
*
|
|
36
|
+
* @param {CSN.Model} csn
|
|
37
|
+
* @param {CSN.Options} options
|
|
38
|
+
* @param {object} messageFunctions
|
|
39
|
+
* @param {Function} messageFunctions.info
|
|
40
|
+
* @returns {(artifact: CSN.Artifact, artifactName: string, prop: string, path: CSN.Path) => void} Callback function for forEachDefinition
|
|
41
|
+
*/
|
|
42
|
+
function getAssocToSkippedIgnorer(csn, options, messageFunctions) {
|
|
43
|
+
const { info } = messageFunctions;
|
|
44
|
+
const doA2J = !(options.transformation === 'hdbcds' && options.sqlMapping === 'hdbcds');
|
|
45
|
+
|
|
46
|
+
const { isAssocOrComposition } = getUtils(csn);
|
|
47
|
+
|
|
48
|
+
return ignoreAssociationToSkippedTarget;
|
|
49
|
+
/**
|
|
50
|
+
* Associations that target a @cds.persistence.skip artifact must be removed
|
|
51
|
+
* from the persistence model
|
|
52
|
+
*
|
|
53
|
+
* @param {CSN.Artifact} artifact
|
|
54
|
+
* @param {string} artifactName
|
|
55
|
+
* @param {string} prop
|
|
56
|
+
* @param {CSN.Path} path
|
|
57
|
+
*/
|
|
58
|
+
function ignoreAssociationToSkippedTarget(artifact, artifactName, prop, path) {
|
|
59
|
+
if (isPersistedOnDatabase(artifact)) {
|
|
60
|
+
// TODO: structure in CSN is artifact.query.[SELECT/SET].mixin
|
|
61
|
+
if (artifact.query) {
|
|
62
|
+
// If we do A2J, we don't need to check the mixin. Either it is used -> a join
|
|
63
|
+
// or published -> handled via elements/members. Unused mixins are removed anyway.
|
|
64
|
+
if (!doA2J && artifact.query.SELECT && artifact.query.SELECT.mixin)
|
|
65
|
+
forEachGeneric(artifact.query.SELECT, 'mixin', ignore, path.concat([ 'query', 'SELECT' ]));
|
|
66
|
+
|
|
67
|
+
else if (!doA2J && artifact.query.SET && artifact.query.SET.mixin)
|
|
68
|
+
forEachGeneric(artifact.query.SET, 'mixin', ignore, path.concat([ 'query', 'SET' ]));
|
|
69
|
+
}
|
|
70
|
+
forEachMemberRecursively(artifact, ignore, [ 'definitions', artifactName ]);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Mark the given member with _ignore if it is an association/composition and it's target is unreachable.
|
|
76
|
+
*
|
|
77
|
+
* @param {CSN.Element} member
|
|
78
|
+
* @param {string} memberName
|
|
79
|
+
* @param {string} prop
|
|
80
|
+
* @param {CSN.Path} path
|
|
81
|
+
* @todo Why do we check for @cds.persistence.exists here if the parent-function only calls this for skip/abstract?
|
|
82
|
+
*/
|
|
83
|
+
function ignore(member, memberName, prop, path) {
|
|
84
|
+
if (options.sqlDialect === 'hana' && !member._ignore && member.target && isAssocOrComposition(member.type) && isUnreachableAssociationTarget(csn.definitions[member.target])) {
|
|
85
|
+
const targetAnnotation = hasAnnotationValue(csn.definitions[member.target], exists) ? exists : '@cds.persistence.skip';
|
|
86
|
+
info(null, path,
|
|
87
|
+
{ target: member.target, anno: targetAnnotation },
|
|
88
|
+
'Association has been removed as it\'s target $(TARGET) is annotated with $(ANNO)');
|
|
89
|
+
member._ignore = true;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Check wether the given artifact is an unreachable association target because it will not "realy" hit the database:
|
|
95
|
+
* - @cds.persistence.skip/exists
|
|
96
|
+
* - abstract
|
|
97
|
+
*
|
|
98
|
+
* @param {CSN.Artifact} art
|
|
99
|
+
* @returns {boolean}
|
|
100
|
+
*/
|
|
101
|
+
function isUnreachableAssociationTarget(art) {
|
|
102
|
+
return !isPersistedOnDatabase(art) || hasAnnotationValue(art, exists);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Return a callback function for forEachDefinition that handles artifacts marked with @cds.persistence.table.
|
|
108
|
+
* If a .query artifact has this annotation, the .query will be deleted and it will be treated like a table.
|
|
109
|
+
*
|
|
110
|
+
* @param {CSN.Model} csn
|
|
111
|
+
* @param {CSN.Options} options
|
|
112
|
+
* @param {object} messageFunctions
|
|
113
|
+
* @param {Function} messageFunctions.error
|
|
114
|
+
* @returns {(artifact: CSN.Artifact, artifactName) => void} Callback function for forEachDefinition
|
|
115
|
+
*/
|
|
116
|
+
function getPersistenceTableProcessor(csn, options, messageFunctions ) {
|
|
117
|
+
const { error } = messageFunctions;
|
|
118
|
+
const {
|
|
119
|
+
recurseElements,
|
|
120
|
+
} = transformUtils.getTransformers(csn, options, '_');
|
|
121
|
+
|
|
122
|
+
return handleQueryish;
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* @param {CSN.Artifact} artifact
|
|
127
|
+
* @param {string} artifactName
|
|
128
|
+
*/
|
|
129
|
+
function handleQueryish(artifact, artifactName) {
|
|
130
|
+
const stripQueryish = artifact.query && hasAnnotationValue(artifact, '@cds.persistence.table');
|
|
131
|
+
|
|
132
|
+
if (stripQueryish) {
|
|
133
|
+
artifact.kind = 'entity';
|
|
134
|
+
delete artifact.query;
|
|
135
|
+
|
|
136
|
+
recurseElements(artifact, [ 'definitions', artifactName ], (member, path) => {
|
|
137
|
+
// All elements must have a type for this to work
|
|
138
|
+
if (!member._ignore && !member.kind && !member.type)
|
|
139
|
+
error(null, path, 'Expecting element to have a type if view is annotated with “@cds.persistence.table“');
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
module.exports = {
|
|
147
|
+
getAnnoProcessor,
|
|
148
|
+
getAssocToSkippedIgnorer,
|
|
149
|
+
getPersistenceTableProcessor,
|
|
150
|
+
};
|
|
@@ -299,13 +299,15 @@ function createReferentialConstraints(csn, options) {
|
|
|
299
299
|
|
|
300
300
|
return true;
|
|
301
301
|
}
|
|
302
|
+
const runtimeChecks = options.assertIntegrityType && options.assertIntegrityType.toUpperCase() === RT;
|
|
303
|
+
const compilerChecks = options.assertIntegrityType && options.assertIntegrityType.toUpperCase() === DB;
|
|
302
304
|
|
|
303
305
|
if ((!options.assertIntegrity || options.assertIntegrity === true || options.assertIntegrity === 'true') &&
|
|
304
|
-
(!options.assertIntegrityType ||
|
|
306
|
+
(!options.assertIntegrityType || runtimeChecks))
|
|
305
307
|
return assertForIntegrityTypeRT();
|
|
306
308
|
|
|
307
309
|
if ((!options.assertIntegrity || options.assertIntegrity === true || options.assertIntegrity === 'true') &&
|
|
308
|
-
|
|
310
|
+
compilerChecks)
|
|
309
311
|
return assertForIntegrityTypeDB();
|
|
310
312
|
|
|
311
313
|
if ((options.assertIntegrity === 'individual'))
|
|
@@ -313,7 +315,7 @@ function createReferentialConstraints(csn, options) {
|
|
|
313
315
|
|
|
314
316
|
// The default for the assertIntegrityType is 'RT', no constraints in that case
|
|
315
317
|
if ((!options.assertIntegrity || options.assertIntegrity === true) &&
|
|
316
|
-
(!options.assertIntegrityType ||
|
|
318
|
+
(!options.assertIntegrityType || runtimeChecks))
|
|
317
319
|
return true;
|
|
318
320
|
|
|
319
321
|
if (!element.keys || !isToOne(element))
|
|
@@ -385,7 +387,7 @@ function createReferentialConstraints(csn, options) {
|
|
|
385
387
|
* @returns {boolean}
|
|
386
388
|
*/
|
|
387
389
|
function isAssertIntegrityAnnotationSetTo(value) {
|
|
388
|
-
return hasAnnotationValue(element, '@assert.integrity', value);
|
|
390
|
+
return hasAnnotationValue(element, '@assert.integrity', value, true);
|
|
389
391
|
}
|
|
390
392
|
|
|
391
393
|
/**
|
|
@@ -134,7 +134,8 @@ function generateDrafts(csn, options, pathDelimiter, messageFunctions) {
|
|
|
134
134
|
warning(null, [ 'definitions', artifactName ], 'Entity annotated with “@odata.draft.enabled” should have one key element of type “cds.UUID”');
|
|
135
135
|
|
|
136
136
|
|
|
137
|
-
|
|
137
|
+
// Ignore boolean return value. We know that we're inside a service or else we wouldn't have reached this code.
|
|
138
|
+
const matchingService = getMatchingService(artifactName) || '';
|
|
138
139
|
// Generate the DraftAdministrativeData projection into the service, unless there is already one
|
|
139
140
|
const draftAdminDataProjectionName = `${matchingService}.DraftAdministrativeData`;
|
|
140
141
|
let draftAdminDataProjection = csn.definitions[draftAdminDataProjectionName];
|
|
@@ -336,7 +337,7 @@ function generateDrafts(csn, options, pathDelimiter, messageFunctions) {
|
|
|
336
337
|
* Get the service name containing the artifact.
|
|
337
338
|
*
|
|
338
339
|
* @param {string} artifactName Absolute name of the artifact
|
|
339
|
-
* @returns {
|
|
340
|
+
* @returns {false|string} Name of the service or false if no match is found.
|
|
340
341
|
*/
|
|
341
342
|
function getMatchingService(artifactName) {
|
|
342
343
|
const matches = [];
|
|
@@ -239,15 +239,14 @@ function expandStructureReferences(csn, options, pathDelimiter, { error, info, t
|
|
|
239
239
|
* @param {CSN.Artifact} root All elements visible fromt he query source ($combined)
|
|
240
240
|
* @param {CSN.Column[]} columns
|
|
241
241
|
* @param {string[]} excluding
|
|
242
|
-
* @returns {{columns: Array,
|
|
242
|
+
* @returns {{columns: Array, toMany: Array}} Object with rewritten columns (.expand/.inline) and with any .expand + to-many
|
|
243
243
|
*/
|
|
244
244
|
function rewrite(root, columns, excluding) {
|
|
245
245
|
const allToMany = [];
|
|
246
246
|
const newThing = [];
|
|
247
247
|
// Replace stars - needs to happen here since the .expand/.inline first path step affects the root *
|
|
248
248
|
columns = replaceStar(root, columns, excluding);
|
|
249
|
-
for (
|
|
250
|
-
const col = columns[i];
|
|
249
|
+
for (const col of columns) {
|
|
251
250
|
if (col.expand) {
|
|
252
251
|
// TODO: Can col.ref be empty without an as? Assumption is it cannot - if it has, it's an error, we throw, compiler checks.
|
|
253
252
|
const { expanded, toManys } = expandInline(root, col, col.ref || [], [ dbName(col) ]);
|
|
@@ -477,6 +476,7 @@ function expandStructureReferences(csn, options, pathDelimiter, { error, info, t
|
|
|
477
476
|
*/
|
|
478
477
|
function expandRef(art, ref, alias, isKey, withAlias) {
|
|
479
478
|
const expanded = [];
|
|
479
|
+
/** @type {Array<[CSN.Element, any[], any[]]>} */
|
|
480
480
|
const stack = [ [ art, ref, [ alias || ref[ref.length - 1] ] ] ];
|
|
481
481
|
while (stack.length > 0) {
|
|
482
482
|
const [ current, currentRef, currentAlias ] = stack.pop();
|
|
@@ -564,8 +564,7 @@ function expandStructureReferences(csn, options, pathDelimiter, { error, info, t
|
|
|
564
564
|
}
|
|
565
565
|
}
|
|
566
566
|
// Finally: Replace the stars and leave out the shadowed things
|
|
567
|
-
for (
|
|
568
|
-
const sub = subs[i];
|
|
567
|
+
for (const sub of subs) {
|
|
569
568
|
if (sub !== '*' && !replaced[dbName(sub)])
|
|
570
569
|
final.push(sub);
|
|
571
570
|
else if (sub === '*')
|
|
@@ -249,10 +249,9 @@ function flattenElements(csn, options, pathDelimiter, error) {
|
|
|
249
249
|
|
|
250
250
|
if (flatElement.type && isAssocOrComposition(flatElement.type) && flatElement.on) {
|
|
251
251
|
// Make refs resolvable by fixing the first ref step
|
|
252
|
-
for (
|
|
253
|
-
const onPart = flatElement.on[i];
|
|
252
|
+
for (const onPart of flatElement.on) {
|
|
254
253
|
if (onPart.ref) {
|
|
255
|
-
const firstRef =
|
|
254
|
+
const firstRef = onPart.ref[0];
|
|
256
255
|
|
|
257
256
|
/*
|
|
258
257
|
when element is defined in the current name resolution scope, like
|
|
@@ -269,7 +268,7 @@ function flattenElements(csn, options, pathDelimiter, error) {
|
|
|
269
268
|
const possibleFlatName = prefix + pathDelimiter + firstRef;
|
|
270
269
|
|
|
271
270
|
if (flatElems[possibleFlatName])
|
|
272
|
-
|
|
271
|
+
onPart.ref[0] = possibleFlatName;
|
|
273
272
|
}
|
|
274
273
|
}
|
|
275
274
|
}
|
|
@@ -305,11 +304,11 @@ function flattenElements(csn, options, pathDelimiter, error) {
|
|
|
305
304
|
/**
|
|
306
305
|
* Walk the element chain
|
|
307
306
|
*
|
|
308
|
-
* @param {
|
|
307
|
+
* @param {object} e
|
|
309
308
|
* @param {string} name
|
|
310
309
|
*/
|
|
311
310
|
function walkElements(e, name) {
|
|
312
|
-
if (isBuiltinType(e)) {
|
|
311
|
+
if (isBuiltinType(e.type)) {
|
|
313
312
|
branches[subbranchNames.concat(name).join(pathDelimiter)] = subbranchElements.concat(e);
|
|
314
313
|
}
|
|
315
314
|
else {
|