@sap/cds-compiler 2.11.2 → 2.13.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +175 -2
- package/bin/.eslintrc.json +1 -2
- package/bin/cds_update_identifiers.js +10 -8
- package/bin/cdsc.js +23 -17
- package/bin/cdsse.js +2 -2
- package/bin/cdsv2m.js +3 -2
- package/doc/CHANGELOG_ARCHIVE.md +1 -1
- package/doc/CHANGELOG_BETA.md +25 -6
- package/doc/CHANGELOG_DEPRECATED.md +22 -6
- package/doc/NameResolution.md +21 -16
- package/lib/api/main.js +32 -79
- package/lib/api/options.js +3 -2
- package/lib/api/validate.js +2 -1
- package/lib/backends.js +16 -26
- package/lib/base/dictionaries.js +0 -8
- package/lib/base/error.js +26 -0
- package/lib/base/keywords.js +10 -19
- package/lib/base/location.js +9 -4
- package/lib/base/message-registry.js +75 -9
- package/lib/base/messages.js +31 -35
- package/lib/base/model.js +2 -62
- package/lib/base/optionProcessorHelper.js +246 -183
- package/lib/checks/.eslintrc.json +2 -0
- package/lib/checks/actionsFunctions.js +2 -1
- package/lib/checks/annotationsOData.js +1 -1
- package/lib/checks/cdsPersistence.js +2 -1
- package/lib/checks/emptyOrOnlyVirtual.js +2 -2
- package/lib/checks/enricher.js +17 -1
- package/lib/checks/foreignKeys.js +4 -4
- package/lib/checks/invalidTarget.js +3 -1
- package/lib/checks/managedInType.js +4 -4
- package/lib/checks/managedWithoutKeys.js +3 -1
- package/lib/checks/queryNoDbArtifacts.js +1 -3
- package/lib/checks/selectItems.js +4 -4
- package/lib/checks/sql-snippets.js +94 -0
- package/lib/checks/types.js +1 -1
- package/lib/checks/unknownMagic.js +1 -1
- package/lib/checks/validator.js +12 -7
- package/lib/compiler/assert-consistency.js +12 -8
- package/lib/compiler/base.js +0 -1
- package/lib/compiler/builtins.js +42 -21
- package/lib/compiler/checks.js +46 -12
- package/lib/compiler/cycle-detector.js +1 -1
- package/lib/compiler/define.js +1103 -0
- package/lib/compiler/extend.js +983 -0
- package/lib/compiler/finalize-parse-cdl.js +231 -0
- package/lib/compiler/index.js +46 -39
- package/lib/compiler/kick-start.js +190 -0
- package/lib/compiler/moduleLayers.js +4 -4
- package/lib/compiler/populate.js +1226 -0
- package/lib/compiler/propagator.js +113 -47
- package/lib/compiler/resolve.js +1433 -0
- package/lib/compiler/shared.js +100 -65
- package/lib/compiler/tweak-assocs.js +529 -0
- package/lib/compiler/utils.js +215 -33
- package/lib/edm/.eslintrc.json +5 -0
- package/lib/edm/annotations/genericTranslation.js +38 -25
- package/lib/edm/annotations/preprocessAnnotations.js +3 -3
- package/lib/edm/csn2edm.js +10 -9
- package/lib/edm/edm.js +19 -20
- package/lib/edm/edmPreprocessor.js +166 -95
- package/lib/edm/edmUtils.js +127 -34
- package/lib/gen/Dictionary.json +92 -43
- package/lib/gen/language.checksum +1 -1
- package/lib/gen/language.interp +11 -1
- package/lib/gen/language.tokens +86 -82
- package/lib/gen/languageLexer.interp +18 -1
- package/lib/gen/languageLexer.js +925 -847
- package/lib/gen/languageLexer.tokens +78 -74
- package/lib/gen/languageParser.js +5434 -4298
- package/lib/json/from-csn.js +59 -17
- package/lib/json/to-csn.js +189 -71
- package/lib/language/antlrParser.js +3 -3
- package/lib/language/docCommentParser.js +3 -3
- package/lib/language/errorStrategy.js +26 -8
- package/lib/language/genericAntlrParser.js +144 -53
- package/lib/language/language.g4 +424 -200
- package/lib/language/multiLineStringParser.js +536 -0
- package/lib/main.d.ts +550 -61
- package/lib/main.js +38 -11
- package/lib/model/api.js +3 -1
- package/lib/model/csnRefs.js +322 -198
- package/lib/model/csnUtils.js +226 -370
- package/lib/model/enrichCsn.js +124 -69
- package/lib/model/revealInternalProperties.js +29 -7
- package/lib/model/sortViews.js +10 -2
- package/lib/modelCompare/compare.js +17 -12
- package/lib/optionProcessor.js +8 -3
- package/lib/render/.eslintrc.json +1 -2
- package/lib/render/DuplicateChecker.js +1 -1
- package/lib/render/manageConstraints.js +36 -33
- package/lib/render/toCdl.js +174 -275
- package/lib/render/toHdbcds.js +203 -122
- package/lib/render/toRename.js +7 -10
- package/lib/render/toSql.js +161 -82
- package/lib/render/utils/common.js +22 -8
- package/lib/render/utils/sql.js +10 -7
- package/lib/render/utils/stringEscapes.js +111 -0
- package/lib/sql-identifier.js +1 -1
- package/lib/transform/.eslintrc.json +5 -0
- package/lib/transform/braceExpression.js +4 -2
- package/lib/transform/db/.eslintrc.json +2 -0
- package/lib/transform/db/applyTransformations.js +212 -0
- package/lib/transform/db/assertUnique.js +1 -1
- package/lib/transform/db/associations.js +187 -0
- package/lib/transform/db/cdsPersistence.js +150 -0
- package/lib/transform/db/constraints.js +61 -56
- package/lib/transform/db/expansion.js +50 -29
- package/lib/transform/db/flattening.js +556 -106
- package/lib/transform/db/groupByOrderBy.js +3 -1
- package/lib/transform/db/temporal.js +236 -0
- package/lib/transform/db/transformExists.js +103 -28
- package/lib/transform/db/views.js +92 -44
- package/lib/transform/draft/.eslintrc.json +38 -0
- package/lib/transform/{db/draft.js → draft/db.js} +9 -7
- package/lib/transform/draft/odata.js +227 -0
- package/lib/transform/forHanaNew.js +98 -783
- package/lib/transform/forOdataNew.js +22 -175
- package/lib/transform/localized.js +36 -32
- package/lib/transform/odata/generateForeignKeyElements.js +3 -3
- package/lib/transform/odata/referenceFlattener.js +95 -89
- package/lib/transform/odata/structureFlattener.js +1 -1
- package/lib/transform/odata/toFinalBaseType.js +86 -12
- package/lib/transform/odata/typesExposure.js +5 -5
- package/lib/transform/odata/utils.js +2 -2
- package/lib/transform/transformUtilsNew.js +47 -33
- package/lib/transform/translateAssocsToJoins.js +13 -30
- package/lib/transform/universalCsn/.eslintrc.json +36 -0
- package/lib/transform/universalCsn/coreComputed.js +170 -0
- package/lib/transform/universalCsn/universalCsnEnricher.js +715 -0
- package/lib/transform/universalCsn/utils.js +63 -0
- package/lib/utils/file.js +8 -3
- package/lib/utils/objectUtils.js +30 -0
- package/lib/utils/timetrace.js +8 -2
- package/package.json +1 -1
- package/share/messages/README.md +26 -0
- package/lib/compiler/definer.js +0 -2349
- package/lib/compiler/resolver.js +0 -2922
- package/lib/transform/db/helpers.js +0 -58
- package/lib/transform/universalCsnEnricher.js +0 -67
|
@@ -1,10 +1,63 @@
|
|
|
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');
|
|
8
|
+
const { ModelError } = require('../../base/error');
|
|
7
9
|
|
|
10
|
+
/**
|
|
11
|
+
* If a mixin association is published, return the mixin association.
|
|
12
|
+
*
|
|
13
|
+
* @param {CSN.Query} query Query of the artifact to check
|
|
14
|
+
* @param {object} association Association (Element) published by the view
|
|
15
|
+
* @param {string} associationName
|
|
16
|
+
* @returns {object} The mixin association
|
|
17
|
+
*/
|
|
18
|
+
function getMixinAssocOfQueryIfPublished(query, association, associationName) {
|
|
19
|
+
if (query && query.SELECT && query.SELECT.mixin) {
|
|
20
|
+
const aliasedColumnsMap = Object.create(null);
|
|
21
|
+
if (query.SELECT.columns) {
|
|
22
|
+
for (const column of query.SELECT.columns) {
|
|
23
|
+
if (column.as && column.ref && column.ref.length === 1)
|
|
24
|
+
aliasedColumnsMap[column.as] = column;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
for (const elem of Object.keys(query.SELECT.mixin)) {
|
|
29
|
+
const mixinElement = query.SELECT.mixin[elem];
|
|
30
|
+
let originalName = associationName;
|
|
31
|
+
if (aliasedColumnsMap[associationName])
|
|
32
|
+
originalName = aliasedColumnsMap[associationName].ref[0];
|
|
33
|
+
|
|
34
|
+
if (elem === originalName)
|
|
35
|
+
return { mixinElement, mixinName: originalName };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return {};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Check whether the given artifact uses the given mixin association.
|
|
43
|
+
*
|
|
44
|
+
* We can rely on the fact that there can be no usage starting with $self/$projection,
|
|
45
|
+
* since lib/checks/selectItems.js forbids that.
|
|
46
|
+
*
|
|
47
|
+
* @param {CSN.Query} query Query of the artifact to check
|
|
48
|
+
* @param {object} association Mixin association (Element) to check for
|
|
49
|
+
* @param {string} associationName
|
|
50
|
+
* @returns {boolean} True if used
|
|
51
|
+
*/
|
|
52
|
+
function usesMixinAssociation(query, association, associationName) {
|
|
53
|
+
if (query && query.SELECT && query.SELECT.columns) {
|
|
54
|
+
for (const column of query.SELECT.columns) {
|
|
55
|
+
if (typeof column === 'object' && column.ref && column.ref.length > 1 && (column.ref[0] === associationName || column.ref[0].id === associationName))
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
8
61
|
|
|
9
62
|
/**
|
|
10
63
|
* @param {CSN.Model} csn
|
|
@@ -15,7 +68,7 @@ const { isBetaEnabled } = require('../../base/model');
|
|
|
15
68
|
*/
|
|
16
69
|
function getViewTransformer(csn, options, messageFunctions, transformCommon) {
|
|
17
70
|
const {
|
|
18
|
-
get$combined,
|
|
71
|
+
get$combined, isAssocOrComposition,
|
|
19
72
|
} = getUtils(csn);
|
|
20
73
|
const { inspectRef, queryOrMain } = csnRefs(csn);
|
|
21
74
|
const pathDelimiter = (options.forHana.names === 'hdbcds') ? '.' : '_';
|
|
@@ -130,10 +183,10 @@ function getViewTransformer(csn, options, messageFunctions, transformCommon) {
|
|
|
130
183
|
const matchingCombined = $combined[elemName];
|
|
131
184
|
// Internal errors - this should never happen!
|
|
132
185
|
if (matchingCombined.length > 1) { // should already be caught by compiler
|
|
133
|
-
throw new
|
|
186
|
+
throw new ModelError(`Ambiguous name - can't be resolved: ${elemName}. Found in: ${matchingCombined.map(o => o.parent)}`);
|
|
134
187
|
}
|
|
135
188
|
else if (matchingCombined.length === 0) { // no clue how this could happen? Invalid CSN?
|
|
136
|
-
throw new
|
|
189
|
+
throw new ModelError(`No matching entry found in UNION of all elements for: ${elemName}`);
|
|
137
190
|
}
|
|
138
191
|
alias = matchingCombined[0].parent;
|
|
139
192
|
}
|
|
@@ -181,41 +234,38 @@ function getViewTransformer(csn, options, messageFunctions, transformCommon) {
|
|
|
181
234
|
* create the __clone for publishing stuff.
|
|
182
235
|
*
|
|
183
236
|
* @todo Factor out the checks
|
|
184
|
-
* @todo Union, Join, Subqueries? Assoc usage in there? __clone?
|
|
185
237
|
* @param {CSN.Query} query
|
|
186
238
|
* @param {object} elements
|
|
187
239
|
* @param {object} columnMap
|
|
188
240
|
* @param {WeakMap} publishedMixins Map to collect the published mixins
|
|
189
241
|
* @param {CSN.Element} elem
|
|
190
242
|
* @param {string} elemName
|
|
191
|
-
* @param {CSN.Path}
|
|
243
|
+
* @param {CSN.Path} elementsPath Path pointing to elements
|
|
244
|
+
* @param {CSN.Path} queryPath Path pointing to the query
|
|
192
245
|
*/
|
|
193
|
-
function handleAssociationElement(query, elements, columnMap, publishedMixins, elem, elemName,
|
|
194
|
-
if (isUnion(
|
|
246
|
+
function handleAssociationElement(query, elements, columnMap, publishedMixins, elem, elemName, elementsPath, queryPath) {
|
|
247
|
+
if (isUnion(queryPath) && options.transformation === 'hdbcds') {
|
|
195
248
|
if (isBetaEnabled(options, 'ignoreAssocPublishingInUnion') && doA2J) {
|
|
196
249
|
if (elem.keys)
|
|
197
|
-
info(null,
|
|
250
|
+
info(null, queryPath, `Managed association "${elemName}", published in a UNION, will be ignored`);
|
|
198
251
|
else
|
|
199
|
-
info(null,
|
|
252
|
+
info(null, queryPath, `Association "${elemName}", published in a UNION, will be ignored`);
|
|
200
253
|
|
|
201
254
|
elem._ignore = true;
|
|
202
255
|
}
|
|
203
256
|
else {
|
|
204
|
-
error(null,
|
|
257
|
+
error(null, queryPath, `Association "${elemName}" can't be published in a SAP HANA CDS UNION`);
|
|
205
258
|
}
|
|
206
259
|
}
|
|
207
|
-
else if (
|
|
208
|
-
error(null,
|
|
260
|
+
else if (queryPath.length > 4 && options.transformation === 'hdbcds') { // path.length > 4 -> is a subquery
|
|
261
|
+
error(null, queryPath, { name: elemName },
|
|
209
262
|
'Association $(NAME) can\'t be published in a subquery');
|
|
210
263
|
}
|
|
211
264
|
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
265
|
const isNotMixinByItself = checkIsNotMixinByItself(query, columnMap, elemName);
|
|
216
266
|
const { mixinElement, mixinName } = getMixinAssocOfQueryIfPublished(query, elem, elemName);
|
|
217
267
|
if (isNotMixinByItself || mixinElement !== undefined) {
|
|
218
|
-
|
|
268
|
+
// If the mixin is only published and not used, only display the __ clone. Kill the "original".
|
|
219
269
|
if (mixinElement !== undefined && !usesMixinAssociation(query, elem, elemName))
|
|
220
270
|
delete query.SELECT.mixin[mixinName];
|
|
221
271
|
|
|
@@ -226,8 +276,8 @@ function getViewTransformer(csn, options, messageFunctions, transformCommon) {
|
|
|
226
276
|
mixinElemName = `_${mixinElemName}`;
|
|
227
277
|
|
|
228
278
|
// Copy the association element to the MIXIN clause under its alias name
|
|
229
|
-
//
|
|
230
|
-
const mixinElem =
|
|
279
|
+
// Needs to be a deep copy, as we transform the on-condition
|
|
280
|
+
const mixinElem = cloneCsn(elem, options);
|
|
231
281
|
// Perform common transformations on the newly generated MIXIN element (won't be reached otherwise)
|
|
232
282
|
transformCommon(mixinElem, mixinElemName);
|
|
233
283
|
|
|
@@ -238,30 +288,24 @@ function getViewTransformer(csn, options, messageFunctions, transformCommon) {
|
|
|
238
288
|
// and fixing the association alias just created
|
|
239
289
|
|
|
240
290
|
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;
|
|
291
|
+
mixinElem.on = applyTransformationsOnNonDictionary(mixinElem, 'on', {
|
|
292
|
+
ref: (parent, prop, ref, refpath) => {
|
|
293
|
+
if (ref[0] === elemName) {
|
|
294
|
+
ref[0] = mixinElemName;
|
|
248
295
|
}
|
|
249
|
-
else if (!(
|
|
250
|
-
|
|
251
|
-
clonedPath.unshift(projectionId);
|
|
296
|
+
else if (!(ref[0] && ref[0].startsWith('$'))) {
|
|
297
|
+
ref.unshift('$projection');
|
|
252
298
|
}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
return func;
|
|
299
|
+
else if (ref[0] && ref[0].startsWith('$')) {
|
|
300
|
+
// TODO: I think this is non-sense. Stuff with $ is either magic or must start with $self, right?
|
|
301
|
+
const { scope } = inspectRef(refpath);
|
|
302
|
+
if (scope !== '$magic' && scope !== '$self')
|
|
303
|
+
ref.unshift('$projection');
|
|
304
|
+
}
|
|
305
|
+
parent.ref = ref;
|
|
306
|
+
return ref;
|
|
263
307
|
},
|
|
264
|
-
});
|
|
308
|
+
}, {}, elementsPath.concat(elemName));
|
|
265
309
|
}
|
|
266
310
|
|
|
267
311
|
if (!mixinElem._ignore)
|
|
@@ -304,6 +348,10 @@ function getViewTransformer(csn, options, messageFunctions, transformCommon) {
|
|
|
304
348
|
// eslint-disable-next-line complexity
|
|
305
349
|
function transformViewOrEntity(query, artifact, artName, path) {
|
|
306
350
|
const { elements } = queryOrMain(query, artifact);
|
|
351
|
+
// We use the elements from the leading query/main artifact - adapt the path
|
|
352
|
+
const elementsPath = elements === artifact.elements ? path.slice(0, 2).concat('elements') : path.concat('elements');
|
|
353
|
+
const queryPath = path;
|
|
354
|
+
|
|
307
355
|
let hasNonAssocElements = false;
|
|
308
356
|
const isSelect = query && query.SELECT;
|
|
309
357
|
const isProjection = !!artifact.projection || query && query.SELECT && !query.SELECT.columns;
|
|
@@ -338,12 +386,12 @@ function getViewTransformer(csn, options, messageFunctions, transformCommon) {
|
|
|
338
386
|
// (180 b) Create MIXINs for association elements in projections or views (those that are not mixins by themselves)
|
|
339
387
|
// CDXCORE-585: Allow mixin associations to be used and published in parallel
|
|
340
388
|
if (query !== undefined && elem.target)
|
|
341
|
-
handleAssociationElement(query, elements, columnMap, publishedMixins, elem, elemName,
|
|
389
|
+
handleAssociationElement(query, elements, columnMap, publishedMixins, elem, elemName, elementsPath, queryPath);
|
|
342
390
|
}
|
|
343
391
|
|
|
344
392
|
if (query && !hasNonAssocElements) {
|
|
345
|
-
|
|
346
|
-
|
|
393
|
+
// Complain if there are no elements other than unmanaged associations
|
|
394
|
+
// Allow with plain
|
|
347
395
|
error(null, [ 'definitions', artName ], { $reviewed: true },
|
|
348
396
|
'Expecting view or projection to have at least one element that is not an unmanaged association');
|
|
349
397
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"root": true,
|
|
3
|
+
"plugins": ["sonarjs", "jsdoc"],
|
|
4
|
+
"extends": ["../../../.eslintrc-ydkjsi.json", "plugin:sonarjs/recommended", "plugin:jsdoc/recommended"],
|
|
5
|
+
"rules": {
|
|
6
|
+
"prefer-const": "error",
|
|
7
|
+
"quotes": ["error", "single", "avoid-escape"],
|
|
8
|
+
"prefer-template": "error",
|
|
9
|
+
"no-trailing-spaces": "error",
|
|
10
|
+
"template-curly-spacing":["error", "never"],
|
|
11
|
+
"complexity": ["warn", 30],
|
|
12
|
+
"max-len": "off",
|
|
13
|
+
// Don't enforce stupid descriptions
|
|
14
|
+
"jsdoc/require-param-description": "off",
|
|
15
|
+
"jsdoc/require-returns-description": "off",
|
|
16
|
+
// Sometimes if-else's are more specific
|
|
17
|
+
"sonarjs/prefer-single-boolean-return": "off",
|
|
18
|
+
// Very whiny and nitpicky
|
|
19
|
+
"sonarjs/cognitive-complexity": "off",
|
|
20
|
+
// Does not recognize TS types
|
|
21
|
+
"jsdoc/no-undefined-types": "off",
|
|
22
|
+
// Whiny and annoying
|
|
23
|
+
"sonarjs/no-duplicate-string": "off"
|
|
24
|
+
},
|
|
25
|
+
"parserOptions": {
|
|
26
|
+
"ecmaVersion": 2018,
|
|
27
|
+
"sourceType": "script"
|
|
28
|
+
},
|
|
29
|
+
"env": {
|
|
30
|
+
"es6": true,
|
|
31
|
+
"node": true
|
|
32
|
+
},
|
|
33
|
+
"settings": {
|
|
34
|
+
"jsdoc": {
|
|
35
|
+
"mode": "typescript"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -6,6 +6,7 @@ const {
|
|
|
6
6
|
} = require('../../model/csnUtils');
|
|
7
7
|
const { setProp, isDeprecatedEnabled } = require('../../base/model');
|
|
8
8
|
const { getTransformers } = require('../transformUtilsNew');
|
|
9
|
+
const { ModelError } = require('../../base/error');
|
|
9
10
|
const draftAnnotation = '@odata.draft.enabled';
|
|
10
11
|
const booleanBuiltin = 'cds.Boolean';
|
|
11
12
|
|
|
@@ -38,7 +39,7 @@ function generateDrafts(csn, options, pathDelimiter, messageFunctions) {
|
|
|
38
39
|
* @param {string} artifactName
|
|
39
40
|
*/
|
|
40
41
|
function generateDraft(artifact, artifactName) {
|
|
41
|
-
if ((artifact.kind === 'entity'
|
|
42
|
+
if ((artifact.kind === 'entity') &&
|
|
42
43
|
hasAnnotationValue(artifact, draftAnnotation) &&
|
|
43
44
|
isPartOfService(artifactName)) {
|
|
44
45
|
// Determine the set of target draft nodes belonging to this draft root (the draft root
|
|
@@ -79,7 +80,7 @@ function generateDrafts(csn, options, pathDelimiter, messageFunctions) {
|
|
|
79
80
|
const draftNodeName = elem.target;
|
|
80
81
|
// Sanity check
|
|
81
82
|
if (!draftNode)
|
|
82
|
-
throw new
|
|
83
|
+
throw new ModelError(`Expecting target to be resolved: ${JSON.stringify(elem, null, 2)}`);
|
|
83
84
|
|
|
84
85
|
// Ignore composition if not part of a service
|
|
85
86
|
if (!isPartOfService(draftNodeName)) {
|
|
@@ -89,7 +90,7 @@ function generateDrafts(csn, options, pathDelimiter, messageFunctions) {
|
|
|
89
90
|
}
|
|
90
91
|
// Barf if a draft node other than the root has @odata.draft.enabled itself
|
|
91
92
|
if (draftNode !== rootArtifact && hasAnnotationValue(draftNode, draftAnnotation)) {
|
|
92
|
-
error(
|
|
93
|
+
error('ref-unexpected-draft-enabled', [ 'definitions', artifactName, 'elements', elemName ], { anno: '@odata.draft.enabled' });
|
|
93
94
|
delete draftNodes[draftNodeName];
|
|
94
95
|
continue;
|
|
95
96
|
}
|
|
@@ -110,7 +111,7 @@ function generateDrafts(csn, options, pathDelimiter, messageFunctions) {
|
|
|
110
111
|
function generateDraftForHana(artifact, artifactName, draftRootName) {
|
|
111
112
|
// Sanity check
|
|
112
113
|
if (!isPartOfService(artifactName))
|
|
113
|
-
throw new
|
|
114
|
+
throw new ModelError(`Expecting artifact to be part of a service: ${JSON.stringify(artifact)}`);
|
|
114
115
|
|
|
115
116
|
|
|
116
117
|
// The name of the draft shadow entity we should generate
|
|
@@ -134,7 +135,8 @@ function generateDrafts(csn, options, pathDelimiter, messageFunctions) {
|
|
|
134
135
|
warning(null, [ 'definitions', artifactName ], 'Entity annotated with “@odata.draft.enabled” should have one key element of type “cds.UUID”');
|
|
135
136
|
|
|
136
137
|
|
|
137
|
-
|
|
138
|
+
// Ignore boolean return value. We know that we're inside a service or else we wouldn't have reached this code.
|
|
139
|
+
const matchingService = getMatchingService(artifactName) || '';
|
|
138
140
|
// Generate the DraftAdministrativeData projection into the service, unless there is already one
|
|
139
141
|
const draftAdminDataProjectionName = `${matchingService}.DraftAdministrativeData`;
|
|
140
142
|
let draftAdminDataProjection = csn.definitions[draftAdminDataProjectionName];
|
|
@@ -311,7 +313,7 @@ function generateDrafts(csn, options, pathDelimiter, messageFunctions) {
|
|
|
311
313
|
function getDraftShadowEntityFor(draftNode, draftNodeName) {
|
|
312
314
|
// Sanity check
|
|
313
315
|
if (!draftNodes[draftNodeName])
|
|
314
|
-
throw new
|
|
316
|
+
throw new ModelError(`Not a draft node: ${draftNodeName}`);
|
|
315
317
|
|
|
316
318
|
return { shadowTarget: csn.definitions[`${draftNodeName}${draftSuffix}`], shadowTargetName: `${draftNodeName}${draftSuffix}` };
|
|
317
319
|
}
|
|
@@ -336,7 +338,7 @@ function generateDrafts(csn, options, pathDelimiter, messageFunctions) {
|
|
|
336
338
|
* Get the service name containing the artifact.
|
|
337
339
|
*
|
|
338
340
|
* @param {string} artifactName Absolute name of the artifact
|
|
339
|
-
* @returns {
|
|
341
|
+
* @returns {false|string} Name of the service or false if no match is found.
|
|
340
342
|
*/
|
|
341
343
|
function getMatchingService(artifactName) {
|
|
342
344
|
const matches = [];
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { forEachDefinition, getUtils, getServiceNames } = require('../../model/csnUtils');
|
|
4
|
+
const { forEach } = require('../../utils/objectUtils');
|
|
5
|
+
const { isArtifactInSomeService, getServiceOfArtifact } = require('../odata/utils');
|
|
6
|
+
const { getTransformers } = require('../transformUtilsNew');
|
|
7
|
+
const { ModelError } = require('../../base/error');
|
|
8
|
+
const { makeMessageFunction } = require('../../base/messages');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* - Generate artificial draft fields if requested
|
|
12
|
+
*
|
|
13
|
+
* - Check associations for:
|
|
14
|
+
* - exposed associations do not point to non-exposed targets
|
|
15
|
+
* - structured types must not contain associations for OData V2
|
|
16
|
+
* - Element must not be an 'array of' for OData V2 TODO: move to the validator
|
|
17
|
+
* - Perform checks for exposed non-abstract entities and views - check media type and key-ness
|
|
18
|
+
*
|
|
19
|
+
* @param {CSN.Model} csn
|
|
20
|
+
* @param {CSN.Options} options
|
|
21
|
+
* @param {Array} [services] Will be calculated JIT if not provided
|
|
22
|
+
* @returns {CSN.Model} Returns the transformed input model
|
|
23
|
+
* @todo should be done by the compiler - Check associations for valid foreign keys
|
|
24
|
+
* @todo check if needed at all: Remove '$projection' from paths in the element's ON-condition
|
|
25
|
+
*/
|
|
26
|
+
function generateDrafts(csn, options, services) {
|
|
27
|
+
const {
|
|
28
|
+
createForeignKeyElement,
|
|
29
|
+
createAndAddDraftAdminDataProjection, createScalarElement,
|
|
30
|
+
createAssociationElement, createAssociationPathComparison,
|
|
31
|
+
addElement, createAction, assignAction,
|
|
32
|
+
resetAnnotation,
|
|
33
|
+
} = getTransformers(csn, options);
|
|
34
|
+
|
|
35
|
+
const {
|
|
36
|
+
getFinalType,
|
|
37
|
+
getServiceName,
|
|
38
|
+
hasAnnotationValue,
|
|
39
|
+
getFinalBaseType,
|
|
40
|
+
getFinalTypeDef,
|
|
41
|
+
} = getUtils(csn);
|
|
42
|
+
|
|
43
|
+
const { error, info } = makeMessageFunction(csn, options, 'for.odata');
|
|
44
|
+
|
|
45
|
+
if (!services)
|
|
46
|
+
services = getServiceNames(csn);
|
|
47
|
+
|
|
48
|
+
const visitedArtifacts = Object.create(null);
|
|
49
|
+
// @ts-ignore
|
|
50
|
+
const externalServices = services.filter(serviceName => csn.definitions[serviceName]['@cds.external']);
|
|
51
|
+
// @ts-ignore
|
|
52
|
+
const isExternalServiceMember = (_art, name) => externalServices.includes(getServiceName(name));
|
|
53
|
+
|
|
54
|
+
forEachDefinition(csn, (def, defName) => {
|
|
55
|
+
// Generate artificial draft fields for entities/views if requested, ignore if not part of a service
|
|
56
|
+
if (def.kind === 'entity' && def['@odata.draft.enabled'] && isArtifactInSomeService(defName, services))
|
|
57
|
+
generateDraftForOdata(def, defName, def);
|
|
58
|
+
|
|
59
|
+
visitedArtifacts[defName] = true;
|
|
60
|
+
}, { skipArtifact: isExternalServiceMember });
|
|
61
|
+
|
|
62
|
+
return csn;
|
|
63
|
+
/**
|
|
64
|
+
* Generate all that is required in ODATA for draft enablement of 'artifact' into the artifact,
|
|
65
|
+
* into its transitively reachable composition targets, and into the model.
|
|
66
|
+
* 'rootArtifact' is the root artifact where composition traversal started.
|
|
67
|
+
*
|
|
68
|
+
* Constraints
|
|
69
|
+
* Draft Root: Exactly one PK of type UUID
|
|
70
|
+
* Draft Node: One PK of type UUID + 0..1 PK of another type
|
|
71
|
+
* Draft Node: Must not be reachable from multiple draft roots
|
|
72
|
+
*
|
|
73
|
+
* @param {CSN.Artifact} artifact
|
|
74
|
+
* @param {string} artifactName
|
|
75
|
+
* @param {CSN.Artifact} rootArtifact artifact where composition traversal started
|
|
76
|
+
*/
|
|
77
|
+
function generateDraftForOdata(artifact, artifactName, rootArtifact) {
|
|
78
|
+
// Sanity check
|
|
79
|
+
// @ts-ignore
|
|
80
|
+
if (!isArtifactInSomeService(artifactName, services))
|
|
81
|
+
throw new ModelError(`Expecting artifact to be part of a service: ${JSON.stringify(artifact)}`);
|
|
82
|
+
|
|
83
|
+
// Nothing to do if already draft-enabled (composition traversal may have circles)
|
|
84
|
+
if ((artifact['@Common.DraftRoot.PreparationAction'] || artifact['@Common.DraftNode.PreparationAction']) &&
|
|
85
|
+
artifact.actions && artifact.actions.draftPrepare)
|
|
86
|
+
return;
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
// Generate the DraftAdministrativeData projection into the service, unless there is already one
|
|
90
|
+
// @ts-ignore
|
|
91
|
+
const draftAdminDataProjectionName = `${getServiceOfArtifact(artifactName, services)}.DraftAdministrativeData`;
|
|
92
|
+
let draftAdminDataProjection = csn.definitions[draftAdminDataProjectionName];
|
|
93
|
+
if (!draftAdminDataProjection) {
|
|
94
|
+
// @ts-ignore
|
|
95
|
+
draftAdminDataProjection = createAndAddDraftAdminDataProjection(getServiceOfArtifact(artifactName, services));
|
|
96
|
+
}
|
|
97
|
+
// Report an error if it is not an entity or not what we expect
|
|
98
|
+
if (draftAdminDataProjection.kind !== 'entity' || !draftAdminDataProjection.elements.DraftUUID) {
|
|
99
|
+
error(null, [ 'definitions', draftAdminDataProjectionName ], { name: draftAdminDataProjectionName },
|
|
100
|
+
'Generated entity $(NAME) conflicts with existing artifact');
|
|
101
|
+
}
|
|
102
|
+
// Generate the annotations describing the draft actions (only draft roots can be activated/edited)
|
|
103
|
+
if (artifact === rootArtifact) {
|
|
104
|
+
resetAnnotation(artifact, '@Common.DraftRoot.ActivationAction', 'draftActivate', info, [ 'definitions', draftAdminDataProjectionName ]);
|
|
105
|
+
resetAnnotation(artifact, '@Common.DraftRoot.EditAction', 'draftEdit', info, [ 'definitions', draftAdminDataProjectionName ]);
|
|
106
|
+
resetAnnotation(artifact, '@Common.DraftRoot.PreparationAction', 'draftPrepare', info, [ 'definitions', draftAdminDataProjectionName ]);
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
resetAnnotation(artifact, '@Common.DraftNode.PreparationAction', 'draftPrepare', info, [ 'definitions', draftAdminDataProjectionName ]);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
Object.values(artifact.elements || {}).forEach( (elem) => {
|
|
113
|
+
// Make all non-key elements nullable
|
|
114
|
+
if (elem.notNull && elem.key !== true)
|
|
115
|
+
delete elem.notNull;
|
|
116
|
+
});
|
|
117
|
+
// Generate the additional elements into the draft-enabled artifact
|
|
118
|
+
|
|
119
|
+
// key IsActiveEntity : Boolean default true
|
|
120
|
+
const isActiveEntity = createScalarElement('IsActiveEntity', 'cds.Boolean', true, true, false);
|
|
121
|
+
isActiveEntity.IsActiveEntity['@UI.Hidden'] = true;
|
|
122
|
+
addElement(isActiveEntity, artifact, artifactName);
|
|
123
|
+
|
|
124
|
+
// HasActiveEntity : Boolean default false
|
|
125
|
+
const hasActiveEntity = createScalarElement('HasActiveEntity', 'cds.Boolean', false, false, true);
|
|
126
|
+
hasActiveEntity.HasActiveEntity['@UI.Hidden'] = true;
|
|
127
|
+
addElement(hasActiveEntity, artifact, artifactName);
|
|
128
|
+
|
|
129
|
+
// HasDraftEntity : Boolean default false;
|
|
130
|
+
const hasDraftEntity = createScalarElement('HasDraftEntity', 'cds.Boolean', false, false, true);
|
|
131
|
+
hasDraftEntity.HasDraftEntity['@UI.Hidden'] = true;
|
|
132
|
+
addElement(hasDraftEntity, artifact, artifactName);
|
|
133
|
+
|
|
134
|
+
// @odata.contained: true
|
|
135
|
+
// DraftAdministrativeData : Association to one DraftAdministrativeData;
|
|
136
|
+
const draftAdministrativeData = createAssociationElement('DraftAdministrativeData', draftAdminDataProjectionName, true);
|
|
137
|
+
draftAdministrativeData.DraftAdministrativeData.cardinality = { max: 1 };
|
|
138
|
+
draftAdministrativeData.DraftAdministrativeData['@odata.contained'] = true;
|
|
139
|
+
draftAdministrativeData.DraftAdministrativeData['@UI.Hidden'] = true;
|
|
140
|
+
addElement(draftAdministrativeData, artifact, artifactName);
|
|
141
|
+
|
|
142
|
+
// Note that we need to do the ODATA transformation steps for managed associations
|
|
143
|
+
// (foreign key field generation, generatedFieldName) by hand, because the corresponding
|
|
144
|
+
// transformation steps have already been done on all artifacts when we come here)
|
|
145
|
+
let uuidDraftKey = draftAdministrativeData.DraftAdministrativeData.keys.filter(key => key.ref && key.ref.length === 1 && key.ref[0] === 'DraftUUID');
|
|
146
|
+
if (uuidDraftKey && uuidDraftKey[0]) {
|
|
147
|
+
uuidDraftKey = uuidDraftKey[0]; // filter returns an array, but it has only one element
|
|
148
|
+
const path = [ 'definitions', artifactName, 'elements', 'DraftAdministrativeData', 'keys', 0 ];
|
|
149
|
+
createForeignKeyElement(draftAdministrativeData.DraftAdministrativeData, 'DraftAdministrativeData', uuidDraftKey, artifact, artifactName, path);
|
|
150
|
+
}
|
|
151
|
+
// SiblingEntity : Association to one <artifact> on (... IsActiveEntity unequal, all other key fields equal ...)
|
|
152
|
+
const siblingEntity = createAssociationElement('SiblingEntity', artifactName, false);
|
|
153
|
+
siblingEntity.SiblingEntity.cardinality = { max: 1 };
|
|
154
|
+
addElement(siblingEntity, artifact, artifactName);
|
|
155
|
+
// ... on SiblingEntity.IsActiveEntity != IsActiveEntity ...
|
|
156
|
+
siblingEntity.SiblingEntity.on = createAssociationPathComparison('SiblingEntity', 'IsActiveEntity', '!=', 'IsActiveEntity');
|
|
157
|
+
|
|
158
|
+
// Iterate elements
|
|
159
|
+
// TODO: Iterative vs recursive? What is more likely: Super deep nesting or cycles via malicious CSN?
|
|
160
|
+
if (artifact.elements) {
|
|
161
|
+
// No need to reverse the stack, not order dependent
|
|
162
|
+
const stack = [ artifact ];
|
|
163
|
+
while (stack.length > 0) {
|
|
164
|
+
const { elements } = stack.pop();
|
|
165
|
+
forEach(elements, (elemName, elem) => {
|
|
166
|
+
if (elemName !== 'IsActiveEntity' && elem.key) {
|
|
167
|
+
// Amend the ON-condition above:
|
|
168
|
+
// ... and SiblingEntity.<keyfield> = <keyfield> ... (for all key fields except 'IsActiveEntity')
|
|
169
|
+
const cond = createAssociationPathComparison('SiblingEntity', elemName, '=', elemName);
|
|
170
|
+
cond.push('and');
|
|
171
|
+
cond.push(...siblingEntity.SiblingEntity.on);
|
|
172
|
+
siblingEntity.SiblingEntity.on = cond;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Draft-enable the targets of composition elements (draft nodes), too
|
|
176
|
+
// TODO rewrite
|
|
177
|
+
if (elem.target && elem.type && getFinalType(elem.type) === 'cds.Composition') {
|
|
178
|
+
const draftNode = csn.definitions[elem.target];
|
|
179
|
+
|
|
180
|
+
// Ignore if that is our own draft root
|
|
181
|
+
if (draftNode !== rootArtifact) {
|
|
182
|
+
// Report error when the draft node has @odata.draft.enabled itself
|
|
183
|
+
if (hasAnnotationValue(draftNode, '@odata.draft.enabled', true)) {
|
|
184
|
+
error('ref-unexpected-draft-enabled', [ 'definitions', artifactName, 'elements', elemName ], { anno: '@odata.draft.enabled' });
|
|
185
|
+
}
|
|
186
|
+
// Ignore composition if it's target is not part of a service or explicitly draft disabled
|
|
187
|
+
else if (!getServiceName(elem.target) || hasAnnotationValue(draftNode, '@odata.draft.enabled', false)) {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
// Generate draft stuff into the target
|
|
192
|
+
generateDraftForOdata(draftNode, elem.target, rootArtifact);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
else if (elem.elements) { // anonymous structure
|
|
197
|
+
stack.push(elem);
|
|
198
|
+
}
|
|
199
|
+
else if (elem.type) { // types - possibly structured
|
|
200
|
+
const typeDef = elem.type.ref ? getFinalBaseType(elem.type) : getFinalTypeDef(elem.type);
|
|
201
|
+
if (typeDef.elements)
|
|
202
|
+
stack.push(typeDef);
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
// Generate the actions into the draft-enabled artifact (only draft roots can be activated/edited)
|
|
210
|
+
|
|
211
|
+
// action draftPrepare (SideEffectsQualifier: String) return <artifact>;
|
|
212
|
+
const draftPrepare = createAction('draftPrepare', artifactName, 'SideEffectsQualifier', 'cds.String');
|
|
213
|
+
assignAction(draftPrepare, artifact);
|
|
214
|
+
|
|
215
|
+
if (artifact === rootArtifact) {
|
|
216
|
+
// action draftActivate() return <artifact>;
|
|
217
|
+
const draftActivate = createAction('draftActivate', artifactName);
|
|
218
|
+
assignAction(draftActivate, artifact);
|
|
219
|
+
|
|
220
|
+
// action draftEdit (PreserveChanges: Boolean) return <artifact>;
|
|
221
|
+
const draftEdit = createAction('draftEdit', artifactName, 'PreserveChanges', 'cds.Boolean');
|
|
222
|
+
assignAction(draftEdit, artifact);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
module.exports = generateDrafts;
|