@sap/cds-compiler 2.12.0 → 2.15.2
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 +221 -15
- package/bin/cdsc.js +125 -50
- package/bin/cdsse.js +2 -2
- package/doc/CHANGELOG_BETA.md +13 -6
- package/doc/CHANGELOG_DEPRECATED.md +22 -6
- package/doc/NameResolution.md +21 -16
- package/lib/api/main.js +47 -84
- package/lib/api/options.js +5 -6
- package/lib/api/validate.js +6 -11
- package/lib/backends.js +15 -23
- package/lib/base/dictionaries.js +0 -8
- package/lib/base/error.js +26 -0
- package/lib/base/keywords.js +7 -17
- package/lib/base/location.js +9 -4
- package/lib/base/message-registry.js +114 -18
- package/lib/base/messages.js +101 -90
- package/lib/base/model.js +2 -63
- package/lib/base/optionProcessorHelper.js +177 -123
- package/lib/checks/annotationsOData.js +12 -33
- package/lib/checks/arrayOfs.js +1 -34
- package/lib/checks/cdsPersistence.js +2 -1
- package/lib/checks/enricher.js +17 -1
- package/lib/checks/invalidTarget.js +3 -1
- package/lib/checks/managedWithoutKeys.js +3 -1
- package/lib/checks/selectItems.js +4 -4
- package/lib/checks/sql-snippets.js +27 -26
- package/lib/checks/types.js +1 -1
- package/lib/checks/validator.js +6 -11
- package/lib/compiler/assert-consistency.js +6 -3
- package/lib/compiler/base.js +1 -0
- package/lib/compiler/builtins.js +19 -6
- package/lib/compiler/checks.js +23 -60
- package/lib/compiler/cycle-detector.js +1 -1
- package/lib/compiler/define.js +1151 -0
- package/lib/compiler/extend.js +1000 -0
- package/lib/compiler/finalize-parse-cdl.js +237 -0
- package/lib/compiler/index.js +107 -39
- package/lib/compiler/kick-start.js +190 -0
- package/lib/compiler/moduleLayers.js +4 -4
- package/lib/compiler/populate.js +1227 -0
- package/lib/compiler/propagator.js +114 -46
- package/lib/compiler/resolve.js +1521 -0
- package/lib/compiler/shared.js +126 -65
- package/lib/compiler/tweak-assocs.js +535 -0
- package/lib/compiler/utils.js +197 -33
- package/lib/edm/.eslintrc.json +5 -0
- package/lib/edm/annotations/genericTranslation.js +38 -24
- package/lib/edm/annotations/preprocessAnnotations.js +2 -2
- package/lib/edm/csn2edm.js +219 -100
- package/lib/edm/edm.js +302 -230
- package/lib/edm/edmPreprocessor.js +554 -419
- package/lib/edm/edmUtils.js +138 -44
- package/lib/gen/Dictionary.json +100 -19
- package/lib/gen/language.checksum +1 -1
- package/lib/gen/language.interp +11 -1
- package/lib/gen/language.tokens +86 -83
- package/lib/gen/languageLexer.interp +10 -1
- package/lib/gen/languageLexer.js +860 -833
- package/lib/gen/languageLexer.tokens +78 -75
- package/lib/gen/languageParser.js +5765 -4480
- package/lib/json/csnVersion.js +10 -11
- package/lib/json/from-csn.js +15 -3
- package/lib/json/to-csn.js +126 -68
- package/lib/language/docCommentParser.js +4 -4
- package/lib/language/genericAntlrParser.js +123 -5
- package/lib/language/language.g4 +355 -156
- package/lib/language/multiLineStringParser.js +5 -5
- package/lib/main.d.ts +486 -59
- package/lib/main.js +41 -9
- package/lib/model/api.js +3 -1
- package/lib/model/csnRefs.js +252 -156
- package/lib/model/csnUtils.js +384 -297
- package/lib/model/enrichCsn.js +71 -29
- package/lib/model/revealInternalProperties.js +29 -8
- package/lib/model/sortViews.js +2 -1
- package/lib/modelCompare/compare.js +23 -18
- package/lib/optionProcessor.js +63 -26
- package/lib/render/manageConstraints.js +35 -32
- package/lib/render/toCdl.js +897 -947
- package/lib/render/toHdbcds.js +205 -257
- package/lib/render/toSql.js +264 -225
- package/lib/render/utils/common.js +136 -25
- package/lib/render/utils/sql.js +4 -3
- 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/db/.eslintrc.json +3 -1
- package/lib/transform/db/applyTransformations.js +35 -12
- package/lib/transform/db/assertUnique.js +1 -1
- package/lib/transform/db/associations.js +104 -306
- package/lib/transform/db/cdsPersistence.js +2 -2
- package/lib/transform/db/constraints.js +58 -53
- package/lib/transform/db/expansion.js +60 -33
- package/lib/transform/db/flattening.js +582 -104
- package/lib/transform/db/groupByOrderBy.js +3 -1
- package/lib/transform/db/transformExists.js +66 -13
- package/lib/transform/db/views.js +11 -7
- package/lib/transform/draft/.eslintrc.json +38 -0
- package/lib/transform/{db/draft.js → draft/db.js} +6 -5
- package/lib/transform/draft/odata.js +227 -0
- package/lib/transform/forHanaNew.js +109 -208
- package/lib/transform/forOdataNew.js +59 -212
- package/lib/transform/localized.js +46 -26
- package/lib/transform/odata/toFinalBaseType.js +85 -11
- package/lib/transform/odata/typesExposure.js +147 -199
- package/lib/transform/odata/utils.js +2 -2
- package/lib/transform/transformUtilsNew.js +44 -33
- package/lib/transform/translateAssocsToJoins.js +3 -20
- package/lib/transform/universalCsn/.eslintrc.json +36 -0
- package/lib/transform/universalCsn/coreComputed.js +172 -0
- package/lib/transform/universalCsn/universalCsnEnricher.js +737 -0
- package/lib/transform/universalCsn/utils.js +63 -0
- package/lib/utils/moduleResolve.js +13 -6
- package/lib/utils/objectUtils.js +30 -0
- package/package.json +1 -1
- package/share/messages/README.md +26 -0
- package/share/messages/message-explanations.json +2 -1
- package/share/messages/syntax-expected-integer.md +37 -0
- package/lib/compiler/definer.js +0 -2361
- package/lib/compiler/resolver.js +0 -3079
- package/lib/transform/odata/attachPath.js +0 -96
- package/lib/transform/odata/expandStructKeysInAssociations.js +0 -59
- package/lib/transform/odata/generateForeignKeyElements.js +0 -261
- package/lib/transform/odata/referenceFlattener.js +0 -290
- package/lib/transform/odata/sortByAssociationDependency.js +0 -105
- package/lib/transform/odata/structuralPath.js +0 -72
- package/lib/transform/odata/structureFlattener.js +0 -171
- package/lib/transform/universalCsnEnricher.js +0 -237
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const { ModelError } = require('../../base/error');
|
|
4
|
+
|
|
3
5
|
/**
|
|
4
6
|
* Replace (formerly) managed association in a GROUP BY/ORDER BY with its foreign keys.
|
|
5
7
|
*
|
|
@@ -89,7 +91,7 @@ function replaceAssociationsInGroupByOrderBy(inputQuery, options, inspectRef, er
|
|
|
89
91
|
function getForeignKeyRefs(assoc) {
|
|
90
92
|
return assoc.keys.map((fk) => {
|
|
91
93
|
if (!fk.$generatedFieldName)
|
|
92
|
-
throw new
|
|
94
|
+
throw new ModelError(`Expecting generated field name for foreign key: ${JSON.stringify(fk)}`);
|
|
93
95
|
|
|
94
96
|
return { ref: [ fk.$generatedFieldName ] };
|
|
95
97
|
});
|
|
@@ -4,6 +4,7 @@ const { forAllQueries, forEachDefinition, walkCsnPath } = require('../../model/c
|
|
|
4
4
|
const { setProp } = require('../../base/model');
|
|
5
5
|
const { getRealName } = require('../../render/utils/common');
|
|
6
6
|
const { csnRefs } = require('../../model/csnRefs');
|
|
7
|
+
const { ModelError } = require('../../base/error');
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Turn a `exists assoc[filter = 100]` into a `exists (select 1 as dummy from assoc.target where <assoc on condition> and assoc.target.filter = 100)`.
|
|
@@ -48,7 +49,7 @@ const { csnRefs } = require('../../model/csnRefs');
|
|
|
48
49
|
* @param {Function} error
|
|
49
50
|
*/
|
|
50
51
|
function handleExists(csn, options, error) {
|
|
51
|
-
|
|
52
|
+
let { inspectRef } = csnRefs(csn);
|
|
52
53
|
const generatedExists = new WeakMap();
|
|
53
54
|
forEachDefinition(csn, (artifact, artifactName) => {
|
|
54
55
|
if (artifact.projection) // do the same hack we do for the other stuff...
|
|
@@ -81,6 +82,8 @@ function handleExists(csn, options, error) {
|
|
|
81
82
|
// to check for further exists
|
|
82
83
|
const { result, leftovers } = processExists(queryPath, exprPath);
|
|
83
84
|
walkCsnPath(csn, exprPath.slice(0, -1))[exprPath[exprPath.length - 1]] = result;
|
|
85
|
+
if (leftovers.length > 0)
|
|
86
|
+
inspectRef = csnRefs(csn).inspectRef; // Refresh caches - we need to resolve stuff in the newly created subquery
|
|
84
87
|
toProcess.push(...leftovers.reverse()); // any leftovers - schedule for further processing
|
|
85
88
|
}
|
|
86
89
|
}
|
|
@@ -107,8 +110,8 @@ function handleExists(csn, options, error) {
|
|
|
107
110
|
sources[join.as] = join.as;
|
|
108
111
|
}
|
|
109
112
|
else if (join.args) {
|
|
110
|
-
const
|
|
111
|
-
sources = Object.assign(sources,
|
|
113
|
+
const subSources = getJoinSources(join.args);
|
|
114
|
+
sources = Object.assign(sources, subSources);
|
|
112
115
|
}
|
|
113
116
|
else if (join.ref) {
|
|
114
117
|
sources[join.ref[join.ref.length - 1]] = join.ref[join.ref.length - 1];
|
|
@@ -193,7 +196,7 @@ function handleExists(csn, options, error) {
|
|
|
193
196
|
const stack = [ [ null, startAssoc, startRest, startIndex ] ];
|
|
194
197
|
const { links } = inspectRef(path);
|
|
195
198
|
while (stack.length > 0) {
|
|
196
|
-
// previous: to nest "up" if the previous assoc did not
|
|
199
|
+
// previous: to nest "up" if the previous assoc did not originally have a filter
|
|
197
200
|
// assoc: the assoc path step
|
|
198
201
|
// rest: path steps after assoc
|
|
199
202
|
// index: index of after-assoc in the overall ref-array - so we know where to start looking for the next assoc
|
|
@@ -408,14 +411,14 @@ function handleExists(csn, options, error) {
|
|
|
408
411
|
* Translate an `EXISTS <managed assoc>` into a part of a WHERE condition.
|
|
409
412
|
*
|
|
410
413
|
* For each of the foreign keys, do:
|
|
411
|
-
* + build the target side by prefixing `target`
|
|
414
|
+
* + build the target side by prefixing `target` in front of the ref
|
|
412
415
|
* + build the source side by prefixing `base` (if not already part of `current`)
|
|
413
|
-
* and the assoc name itself (current)
|
|
416
|
+
* and the assoc name itself (current) in front of the ref
|
|
414
417
|
* + Compare source and target with `=`
|
|
415
418
|
*
|
|
416
419
|
* If there is more than one foreign key, join with `and`.
|
|
417
420
|
*
|
|
418
|
-
* The new tokens are
|
|
421
|
+
* The new tokens are immediately added to the WHERE of the subselect
|
|
419
422
|
*
|
|
420
423
|
* @param {CSN.Element} root
|
|
421
424
|
* @param {string} target
|
|
@@ -496,11 +499,16 @@ function handleExists(csn, options, error) {
|
|
|
496
499
|
whereExtension.push({ ref: [ target, ...part.ref.slice(1) ] });
|
|
497
500
|
}
|
|
498
501
|
else if (part.$scope === '$self') { // source side - "absolute" scope
|
|
499
|
-
|
|
500
|
-
|
|
502
|
+
const column = part._art._column;
|
|
503
|
+
if (column && column.as) { // Replace with the "original" expression (the .ref, .xpr etc.)
|
|
504
|
+
whereExtension.push(translateToSourceSide(column));
|
|
505
|
+
}
|
|
506
|
+
else {
|
|
507
|
+
whereExtension.push(assignAndDeleteAs({}, part, { ref: [ base, ...part.ref.slice(1) ] }));
|
|
508
|
+
}
|
|
501
509
|
}
|
|
502
510
|
else if (art) { // source side - with local scope
|
|
503
|
-
if (isPrefixedWithTableAlias)
|
|
511
|
+
if (isPrefixedWithTableAlias || part.$scope === 'alias')
|
|
504
512
|
whereExtension.push({ ref: [ ...current.ref.slice(0, -1), ...part.ref ] });
|
|
505
513
|
else
|
|
506
514
|
whereExtension.push({ ref: [ base, ...current.ref.slice(0, -1), ...part.ref ] });
|
|
@@ -512,6 +520,51 @@ function handleExists(csn, options, error) {
|
|
|
512
520
|
|
|
513
521
|
return whereExtension;
|
|
514
522
|
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Run Object.assign on all of the passed in parameters and delete a .as at the end
|
|
526
|
+
*
|
|
527
|
+
* @param {...any} args
|
|
528
|
+
* @returns {object} The merged args without an .as property
|
|
529
|
+
*/
|
|
530
|
+
function assignAndDeleteAs(...args) {
|
|
531
|
+
const obj = Object.assign.apply(null, args);
|
|
532
|
+
delete obj.as;
|
|
533
|
+
return obj;
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* Translate the given obj (a column-like thing) into an expression that we can use in the WHERE.
|
|
537
|
+
* - Strip off $self/$projection and correctly replace with source expression
|
|
538
|
+
* - Drill further down into .xpr
|
|
539
|
+
* - Correctly set table alias in front of ref
|
|
540
|
+
*
|
|
541
|
+
* @param {object} obj
|
|
542
|
+
* @returns {object}
|
|
543
|
+
*/
|
|
544
|
+
function translateToSourceSide(obj) {
|
|
545
|
+
if (obj.ref) {
|
|
546
|
+
if (obj.$scope === '$self') { // TODO: Check with this way down, do we keep the links?
|
|
547
|
+
const column = obj._art._column;
|
|
548
|
+
if (column && column.as)
|
|
549
|
+
return translateToSourceSide(column);
|
|
550
|
+
return assignAndDeleteAs({}, obj, { ref: [ base, ...obj.ref.slice(1) ] });
|
|
551
|
+
}
|
|
552
|
+
else if (typeof obj.$env === 'string') {
|
|
553
|
+
return assignAndDeleteAs({}, obj, { ref: [ obj.$env, ...obj.ref ] });
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
return assignAndDeleteAs({}, obj, { ref: [ ...obj.ref ] });
|
|
557
|
+
}
|
|
558
|
+
else if (obj.xpr) { // we need to drill further down into .xpr
|
|
559
|
+
return assignAndDeleteAs({}, obj, { xpr: obj.xpr.map(translateToSourceSide) });
|
|
560
|
+
}
|
|
561
|
+
else if (obj.args) {
|
|
562
|
+
return assignAndDeleteAs({}, obj, { args: obj.args.map(translateToSourceSide) });
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
return obj;
|
|
566
|
+
}
|
|
567
|
+
|
|
515
568
|
/**
|
|
516
569
|
* Check that an expression triple is a valid $self
|
|
517
570
|
*
|
|
@@ -584,7 +637,7 @@ function handleExists(csn, options, error) {
|
|
|
584
637
|
}
|
|
585
638
|
|
|
586
639
|
/**
|
|
587
|
-
* Check (using inspectRef -> links),
|
|
640
|
+
* Check (using inspectRef -> links), whether the first path step is an entity or query source
|
|
588
641
|
*
|
|
589
642
|
* @param {CSN.Path} path
|
|
590
643
|
* @returns {boolean}
|
|
@@ -634,7 +687,7 @@ function handleExists(csn, options, error) {
|
|
|
634
687
|
return error(null, xpr.$path, 'Boolean $env is not handled yet - please report this error!');
|
|
635
688
|
}
|
|
636
689
|
else if (xpr.ref) {
|
|
637
|
-
throw new
|
|
690
|
+
throw new ModelError('Missing $env and missing leading artifact ref - throwing to trigger recompilation!');
|
|
638
691
|
}
|
|
639
692
|
}
|
|
640
693
|
|
|
@@ -699,7 +752,7 @@ function handleExists(csn, options, error) {
|
|
|
699
752
|
*
|
|
700
753
|
* @param {string} target
|
|
701
754
|
* @param {TokenStream} where
|
|
702
|
-
* @returns {TokenStream} The input-where with the refs
|
|
755
|
+
* @returns {TokenStream} The input-where with the refs transformed to absolute ones
|
|
703
756
|
*/
|
|
704
757
|
function remapExistingWhere(target, where) {
|
|
705
758
|
return where.map((part) => {
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const {
|
|
4
|
-
getUtils,
|
|
4
|
+
getUtils, cloneCsnNonDict, applyTransformationsOnNonDictionary,
|
|
5
5
|
} = require('../../model/csnUtils');
|
|
6
6
|
const { implicitAs, csnRefs } = require('../../model/csnRefs');
|
|
7
7
|
const { isBetaEnabled } = require('../../base/model');
|
|
8
|
+
const { ModelError } = require('../../base/error');
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* If a mixin association is published, return the mixin association.
|
|
@@ -38,7 +39,7 @@ function getMixinAssocOfQueryIfPublished(query, association, associationName) {
|
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
/**
|
|
41
|
-
* Check
|
|
42
|
+
* Check whether the given artifact uses the given mixin association.
|
|
42
43
|
*
|
|
43
44
|
* We can rely on the fact that there can be no usage starting with $self/$projection,
|
|
44
45
|
* since lib/checks/selectItems.js forbids that.
|
|
@@ -182,10 +183,10 @@ function getViewTransformer(csn, options, messageFunctions, transformCommon) {
|
|
|
182
183
|
const matchingCombined = $combined[elemName];
|
|
183
184
|
// Internal errors - this should never happen!
|
|
184
185
|
if (matchingCombined.length > 1) { // should already be caught by compiler
|
|
185
|
-
throw new
|
|
186
|
+
throw new ModelError(`Ambiguous name - can't be resolved: ${elemName}. Found in: ${matchingCombined.map(o => o.parent)}`);
|
|
186
187
|
}
|
|
187
188
|
else if (matchingCombined.length === 0) { // no clue how this could happen? Invalid CSN?
|
|
188
|
-
throw new
|
|
189
|
+
throw new ModelError(`No matching entry found in UNION of all elements for: ${elemName}`);
|
|
189
190
|
}
|
|
190
191
|
alias = matchingCombined[0].parent;
|
|
191
192
|
}
|
|
@@ -208,7 +209,7 @@ function getViewTransformer(csn, options, messageFunctions, transformCommon) {
|
|
|
208
209
|
const assocCol = columnMap[elemName];
|
|
209
210
|
if (assocCol && assocCol.ref) {
|
|
210
211
|
elem.keys.forEach((foreignKey) => {
|
|
211
|
-
const ref =
|
|
212
|
+
const ref = cloneCsnNonDict(assocCol.ref, options);
|
|
212
213
|
ref[ref.length - 1] = [ getLastRefStepString(ref) ].concat(foreignKey.as).join(pathDelimiter);
|
|
213
214
|
const result = {
|
|
214
215
|
ref,
|
|
@@ -276,7 +277,7 @@ function getViewTransformer(csn, options, messageFunctions, transformCommon) {
|
|
|
276
277
|
|
|
277
278
|
// Copy the association element to the MIXIN clause under its alias name
|
|
278
279
|
// Needs to be a deep copy, as we transform the on-condition
|
|
279
|
-
const mixinElem =
|
|
280
|
+
const mixinElem = cloneCsnNonDict(elem, options);
|
|
280
281
|
// Perform common transformations on the newly generated MIXIN element (won't be reached otherwise)
|
|
281
282
|
transformCommon(mixinElem, mixinElemName);
|
|
282
283
|
|
|
@@ -304,7 +305,7 @@ function getViewTransformer(csn, options, messageFunctions, transformCommon) {
|
|
|
304
305
|
parent.ref = ref;
|
|
305
306
|
return ref;
|
|
306
307
|
},
|
|
307
|
-
}, elementsPath.concat(elemName));
|
|
308
|
+
}, {}, elementsPath.concat(elemName));
|
|
308
309
|
}
|
|
309
310
|
|
|
310
311
|
if (!mixinElem._ignore)
|
|
@@ -456,6 +457,9 @@ function checkIsNotMixinByItself(query, columnMap, elementName) {
|
|
|
456
457
|
if (query && query.SELECT && query.SELECT.mixin) {
|
|
457
458
|
const col = columnMap[elementName];
|
|
458
459
|
|
|
460
|
+
if (!col.ref) // No ref -> new association, but not a mixin.
|
|
461
|
+
return true;
|
|
462
|
+
|
|
459
463
|
// Use getLastRefStepString - with hdbcds.hdbcds and malicious CSN input we might have .id
|
|
460
464
|
const realName = getLastRefStepString(col.ref);
|
|
461
465
|
// If the element is not part of the mixin => True
|
|
@@ -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
|
|
@@ -312,7 +313,7 @@ function generateDrafts(csn, options, pathDelimiter, messageFunctions) {
|
|
|
312
313
|
function getDraftShadowEntityFor(draftNode, draftNodeName) {
|
|
313
314
|
// Sanity check
|
|
314
315
|
if (!draftNodes[draftNodeName])
|
|
315
|
-
throw new
|
|
316
|
+
throw new ModelError(`Not a draft node: ${draftNodeName}`);
|
|
316
317
|
|
|
317
318
|
return { shadowTarget: csn.definitions[`${draftNodeName}${draftSuffix}`], shadowTargetName: `${draftNodeName}${draftSuffix}` };
|
|
318
319
|
}
|
|
@@ -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;
|