@sap/cds-compiler 2.11.4 → 2.13.8
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 +159 -1
- package/bin/cds_update_identifiers.js +7 -7
- package/bin/cdsc.js +22 -23
- package/bin/cdsse.js +2 -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 +30 -63
- package/lib/api/options.js +5 -5
- package/lib/api/validate.js +0 -5
- 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 +52 -2
- package/lib/base/messages.js +16 -26
- 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/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/validator.js +12 -7
- package/lib/compiler/assert-consistency.js +10 -6
- package/lib/compiler/base.js +0 -1
- package/lib/compiler/builtins.js +8 -6
- 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 +33 -14
- 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 +76 -38
- package/lib/compiler/tweak-assocs.js +529 -0
- package/lib/compiler/utils.js +204 -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 +143 -71
- package/lib/language/antlrParser.js +3 -3
- package/lib/language/docCommentParser.js +3 -3
- package/lib/language/genericAntlrParser.js +144 -54
- package/lib/language/language.g4 +424 -203
- package/lib/language/multiLineStringParser.js +536 -0
- package/lib/main.d.ts +472 -61
- package/lib/main.js +38 -11
- package/lib/model/api.js +3 -1
- package/lib/model/csnRefs.js +321 -204
- package/lib/model/csnUtils.js +224 -263
- package/lib/model/enrichCsn.js +97 -40
- package/lib/model/revealInternalProperties.js +27 -6
- package/lib/model/sortViews.js +2 -1
- package/lib/modelCompare/compare.js +17 -12
- package/lib/optionProcessor.js +7 -6
- 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 +201 -115
- package/lib/render/toRename.js +7 -10
- package/lib/render/toSql.js +149 -75
- 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 +35 -12
- 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 +552 -105
- package/lib/transform/db/groupByOrderBy.js +3 -1
- package/lib/transform/db/temporal.js +236 -0
- package/lib/transform/db/transformExists.js +94 -28
- package/lib/transform/db/views.js +5 -4
- 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 +94 -801
- 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 +10 -27
- 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 +2 -1
- 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 -2340
- package/lib/compiler/resolver.js +0 -2988
- package/lib/transform/universalCsnEnricher.js +0 -67
|
@@ -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
|
});
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
getUtils, getNormalizedQuery, hasAnnotationValue, forEachMember,
|
|
5
|
+
} = require('../../model/csnUtils');
|
|
6
|
+
const { implicitAs } = require('../../model/csnRefs');
|
|
7
|
+
const { setProp } = require('../../base/model');
|
|
8
|
+
const { getTransformers } = require('../transformUtilsNew');
|
|
9
|
+
|
|
10
|
+
const validToString = '@cds.valid.to';
|
|
11
|
+
const validFromString = '@cds.valid.from';
|
|
12
|
+
/**
|
|
13
|
+
* Get the forEachDefinition callback function that adds a where condition to views that
|
|
14
|
+
* - are annotated with @cds.valid.from and @cds.valid.to,
|
|
15
|
+
* - have only one @cds.valid.from and @cds.valid.to,
|
|
16
|
+
* - and both annotations come from the same entity
|
|
17
|
+
*
|
|
18
|
+
* If the view has one of the annotations but the other conditions are not met, an error will be raised.
|
|
19
|
+
*
|
|
20
|
+
* @param {CSN.Model} csn
|
|
21
|
+
* @param {object} messageFunctions
|
|
22
|
+
* @param {Function} messageFunctions.info
|
|
23
|
+
* @returns {(artifact: CSN.Artifact, artifactName: string) => void} Callback for forEachDefinition applying the where-condition to views.
|
|
24
|
+
*/
|
|
25
|
+
function getViewDecorator(csn, messageFunctions) {
|
|
26
|
+
const { info } = messageFunctions;
|
|
27
|
+
const { get$combined } = getUtils(csn);
|
|
28
|
+
return addTemporalWhereConditionToView;
|
|
29
|
+
/**
|
|
30
|
+
* Add a where condition to views that
|
|
31
|
+
* - are annotated with @cds.valid.from and @cds.valid.to,
|
|
32
|
+
* - have only one @cds.valid.from and @cds.valid.to,
|
|
33
|
+
* - and both annotations come from the same entity
|
|
34
|
+
*
|
|
35
|
+
* If the view has one of the annotations but the other conditions are not met, an error will be raised.
|
|
36
|
+
*
|
|
37
|
+
* @param {CSN.Artifact} artifact
|
|
38
|
+
* @param {string} artifactName
|
|
39
|
+
*/
|
|
40
|
+
function addTemporalWhereConditionToView(artifact, artifactName) {
|
|
41
|
+
const normalizedQuery = getNormalizedQuery(artifact);
|
|
42
|
+
if (normalizedQuery && normalizedQuery.query && normalizedQuery.query.SELECT) {
|
|
43
|
+
// BLOCKER: We need information to handle $combined
|
|
44
|
+
// What we are trying to achieve by this:
|
|
45
|
+
// Forbid joining/selecting from two or more temporal entities
|
|
46
|
+
// Idea: Follow the query-tree and check each from
|
|
47
|
+
// Collect all source-entities and compute our own $combined
|
|
48
|
+
const $combined = get$combined(normalizedQuery.query);
|
|
49
|
+
const [ from, to ] = getFromToElements($combined);
|
|
50
|
+
// exactly one validFrom & validTo
|
|
51
|
+
if (from.length === 1 && to.length === 1) {
|
|
52
|
+
// and both are from the same origin
|
|
53
|
+
if (from[0].source === to[0].source && from[0].parent === to[0].parent) {
|
|
54
|
+
if (!hasFalsyTemporalAnnotations(normalizedQuery.query.SELECT, artifact.elements, from[0], to[0])) {
|
|
55
|
+
const fromPath = {
|
|
56
|
+
ref: [
|
|
57
|
+
from[0].parent,
|
|
58
|
+
from[0].name,
|
|
59
|
+
],
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const toPath = {
|
|
63
|
+
ref: [
|
|
64
|
+
to[0].parent,
|
|
65
|
+
to[0].name,
|
|
66
|
+
],
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
const atFrom = { ref: [ '$at', 'from' ] };
|
|
71
|
+
const atTo = { ref: [ '$at', 'to' ] };
|
|
72
|
+
|
|
73
|
+
const cond = [ '(', fromPath, '<', atTo, 'and', toPath, '>', atFrom, ')' ];
|
|
74
|
+
|
|
75
|
+
if (normalizedQuery.query.SELECT.where) { // if there is an existing where-clause, extend it by adding 'and (temporal clause)'
|
|
76
|
+
normalizedQuery.query.SELECT.where = [ '(', ...normalizedQuery.query.SELECT.where, ')', 'and', ...cond ];
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
normalizedQuery.query.SELECT.where = cond;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
info(null, [ 'definitions', artifactName ], `No temporal WHERE clause added as "${from[0].error_parent}"."${from[0].name}" and "${to[0].error_parent}"."${to[0].name}" are not of same origin`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
else if (from.length > 0 || to.length > 0) {
|
|
88
|
+
const missingAnnotation = from.length > to.length ? validToString : validFromString;
|
|
89
|
+
info(null, [ 'definitions', artifactName ],
|
|
90
|
+
{ anno: missingAnnotation },
|
|
91
|
+
'No temporal WHERE clause added because $(ANNO) is missing');
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Get all elements tagged with @cds.valid.from/to from the union of all entities of the from-clause.
|
|
98
|
+
*
|
|
99
|
+
* @param {any} combined union of all entities of the from-clause
|
|
100
|
+
* @returns {Array[]} Array where first field is array of elements with @cds.valid.from, second field is array of elements with @cds.valid.to.
|
|
101
|
+
*/
|
|
102
|
+
function getFromToElements(combined) {
|
|
103
|
+
const from = [];
|
|
104
|
+
const to = [];
|
|
105
|
+
for (const name in combined) {
|
|
106
|
+
let elt = combined[name];
|
|
107
|
+
if (!Array.isArray(elt))
|
|
108
|
+
elt = [ elt ];
|
|
109
|
+
elt.forEach((e) => {
|
|
110
|
+
if (hasAnnotationValue(e.element, validFromString))
|
|
111
|
+
from.push(e);
|
|
112
|
+
|
|
113
|
+
if (hasAnnotationValue(e.element, validToString))
|
|
114
|
+
to.push(e);
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return [ from, to ];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Check if the given SELECT has a falsy @cds.valid.from and a falsy @cds.valid.to
|
|
123
|
+
*
|
|
124
|
+
* @param {CSN.QuerySelect} SELECT
|
|
125
|
+
* @param {CSN.Elements} elements
|
|
126
|
+
* @param {object} from
|
|
127
|
+
* @param {object} to
|
|
128
|
+
* @returns {boolean} True if both are present and false.
|
|
129
|
+
*/
|
|
130
|
+
function hasFalsyTemporalAnnotations(SELECT, elements, from, to) {
|
|
131
|
+
let fromElement = elements[from.name];
|
|
132
|
+
let toElement = elements[to.name];
|
|
133
|
+
|
|
134
|
+
if (SELECT.columns) {
|
|
135
|
+
for (const col of SELECT.columns) {
|
|
136
|
+
if (col.ref) {
|
|
137
|
+
const implicitAlias = implicitAs(col.ref);
|
|
138
|
+
if (implicitAlias === from.name)
|
|
139
|
+
fromElement = elements[col.as || implicitAlias];
|
|
140
|
+
else if (implicitAlias === to.name)
|
|
141
|
+
toElement = elements[col.as || implicitAlias];
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return fromElement && toElement &&
|
|
146
|
+
hasAnnotationValue(fromElement, validFromString, false) &&
|
|
147
|
+
hasAnnotationValue(toElement, validToString, false);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Get the forEachDefinition callback function that collects all usages of @cds.valid.from/to/key and checks that
|
|
153
|
+
* - the assignment is on a valid element
|
|
154
|
+
* - the annotation is only assigned once
|
|
155
|
+
* - key is only used in conjunction with from and to
|
|
156
|
+
*
|
|
157
|
+
* Furthermore, @cds.valid.from and @cds.valid.key is processed - @cds.valid.from is marked as key or marked as unique if @cds.valid.key is used.
|
|
158
|
+
* If @cds.valid.key is used, the real key-elements have their key-property removed (set non-enumerable as $key) and instead the @cds.valid.key-marked elements have it added.
|
|
159
|
+
*
|
|
160
|
+
* @param {CSN.Model} csn
|
|
161
|
+
* @param {CSN.Options} options
|
|
162
|
+
* @param {string} pathDelimiter
|
|
163
|
+
* @param {object} messageFunctions
|
|
164
|
+
* @param {Function} messageFunctions.error
|
|
165
|
+
* @returns {(artifact: CSN.Artifact, artifactName: string) => void} Callback for forEachDefinition processing the annotations.
|
|
166
|
+
*/
|
|
167
|
+
function getAnnotationHandler(csn, options, pathDelimiter, messageFunctions) {
|
|
168
|
+
const { error } = messageFunctions;
|
|
169
|
+
const {
|
|
170
|
+
extractValidFromToKeyElement, checkAssignment, checkMultipleAssignments, recurseElements,
|
|
171
|
+
} = getTransformers(csn, options, pathDelimiter);
|
|
172
|
+
|
|
173
|
+
return handleTemporalAnnotations;
|
|
174
|
+
/**
|
|
175
|
+
* @param {CSN.Artifact} artifact
|
|
176
|
+
* @param {string} artifactName
|
|
177
|
+
*/
|
|
178
|
+
function handleTemporalAnnotations(artifact, artifactName) {
|
|
179
|
+
const validFrom = [];
|
|
180
|
+
const validTo = [];
|
|
181
|
+
const validKey = [];
|
|
182
|
+
|
|
183
|
+
recurseElements(artifact, [ 'definitions', artifactName ], (member, path) => {
|
|
184
|
+
const [ f, t, k ] = extractValidFromToKeyElement(member, path);
|
|
185
|
+
validFrom.push(...f);
|
|
186
|
+
validTo.push(...t);
|
|
187
|
+
validKey.push(...k);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
if (artifact.kind === 'entity' && !artifact.query) {
|
|
191
|
+
validFrom.forEach(obj => checkAssignment(validFromString, obj.element, obj.path, artifact));
|
|
192
|
+
validTo.forEach(obj => checkAssignment(validToString, obj.element, obj.path, artifact));
|
|
193
|
+
validKey.forEach(obj => checkAssignment('@cds.valid.key', obj.element, obj.path, artifact));
|
|
194
|
+
checkMultipleAssignments(validFrom, validFromString, artifact, artifactName);
|
|
195
|
+
checkMultipleAssignments(validTo, validToString, artifact, artifactName, true);
|
|
196
|
+
checkMultipleAssignments(validKey, '@cds.valid.key', artifact, artifactName);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// if there is an cds.valid.key, make this the only primary key
|
|
200
|
+
// otherwise add all cds.valid.from to primary key tuple
|
|
201
|
+
if (validKey.length) {
|
|
202
|
+
if (!validFrom.length || !validTo.length) {
|
|
203
|
+
error(null, [ 'definitions', artifactName ],
|
|
204
|
+
'Expecting “@cds.valid.from” and “@cds.valid.to” if “@cds.valid.key” is used');
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
forEachMember(artifact, (member) => {
|
|
208
|
+
if (member.key) {
|
|
209
|
+
member.unique = true;
|
|
210
|
+
delete member.key;
|
|
211
|
+
// Remember that this element was a key in the original artifact.
|
|
212
|
+
// This is needed for localized convenience view generation.
|
|
213
|
+
setProp(member, '$key', true);
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
validKey.forEach((member) => {
|
|
217
|
+
member.element.key = true;
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
validFrom.forEach((member) => {
|
|
221
|
+
member.element.unique = true;
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
validFrom.forEach((member) => {
|
|
226
|
+
member.element.key = true;
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
module.exports = {
|
|
234
|
+
getViewDecorator,
|
|
235
|
+
getAnnotationHandler,
|
|
236
|
+
};
|
|
@@ -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
|
}
|
|
@@ -365,16 +368,23 @@ function handleExists(csn, options, error) {
|
|
|
365
368
|
const subselect = getSubselect(root.target, ref, sources);
|
|
366
369
|
|
|
367
370
|
const target = subselect.SELECT.from.as; // use subquery alias as target - prevent shadowing
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
371
|
+
const extension = root.keys ? translateManagedAssocToWhere(root, target, isPrefixedWithTableAlias, base, current) : translateUnmanagedAssocToWhere(root, target, isPrefixedWithTableAlias, base, current);
|
|
372
|
+
if (extension.length > 3)
|
|
373
|
+
subselect.SELECT.where.push('('); // add braces around the on-condition part to ensure precedence is kept
|
|
374
|
+
|
|
375
|
+
subselect.SELECT.where.push(...extension);
|
|
376
|
+
|
|
377
|
+
if (extension.length > 3)
|
|
378
|
+
subselect.SELECT.where.push(')');
|
|
374
379
|
|
|
375
380
|
newExpr.push('exists');
|
|
376
|
-
if (ref && ref.where)
|
|
377
|
-
|
|
381
|
+
if (ref && ref.where) {
|
|
382
|
+
const remappedWhere = remapExistingWhere(target, ref.where);
|
|
383
|
+
if (remappedWhere.length > 3)
|
|
384
|
+
subselect.SELECT.where.push(...[ 'and', '(', ...remappedWhere, ')' ]);
|
|
385
|
+
else
|
|
386
|
+
subselect.SELECT.where.push(...[ 'and', ...remappedWhere ]);
|
|
387
|
+
}
|
|
378
388
|
|
|
379
389
|
newExpr.push(subselect);
|
|
380
390
|
toContinue.push([ exprPath.concat(newExpr.length - 1), exprPath.concat([ newExpr.length - 1, 'SELECT', 'where' ]) ]);
|
|
@@ -412,21 +422,24 @@ function handleExists(csn, options, error) {
|
|
|
412
422
|
*
|
|
413
423
|
* @param {CSN.Element} root
|
|
414
424
|
* @param {string} target
|
|
415
|
-
* @param {CSN.Query} subselect This subselect will in the end replace <assoc> in EXISTS <assoc>
|
|
416
425
|
* @param {boolean} isPrefixedWithTableAlias
|
|
417
426
|
* @param {string} base
|
|
418
427
|
* @param {Token} current
|
|
428
|
+
* @returns {object[]} The stuff to add to the where
|
|
419
429
|
*/
|
|
420
|
-
function translateManagedAssocToWhere(root, target,
|
|
430
|
+
function translateManagedAssocToWhere(root, target, isPrefixedWithTableAlias, base, current) {
|
|
431
|
+
const whereExtension = [];
|
|
421
432
|
for (let j = 0; j < root.keys.length; j++) {
|
|
422
433
|
const lop = { ref: [ target, ...root.keys[j].ref ] }; // target side
|
|
423
434
|
const rop = { ref: (isPrefixedWithTableAlias ? [] : [ base ]).concat([ ...toRawRef(current.ref), ...root.keys[j].ref ]) }; // source side
|
|
424
435
|
|
|
425
436
|
if (j > 0)
|
|
426
|
-
|
|
437
|
+
whereExtension.push('and');
|
|
427
438
|
|
|
428
|
-
|
|
439
|
+
whereExtension.push(...[ lop, '=', rop ]);
|
|
429
440
|
}
|
|
441
|
+
|
|
442
|
+
return whereExtension;
|
|
430
443
|
}
|
|
431
444
|
|
|
432
445
|
/**
|
|
@@ -452,12 +465,13 @@ function handleExists(csn, options, error) {
|
|
|
452
465
|
*
|
|
453
466
|
* @param {CSN.Element} root
|
|
454
467
|
* @param {string} target
|
|
455
|
-
* @param {CSN.Query} subselect This subselect will in the end replace <assoc> in EXISTS <assoc>
|
|
456
468
|
* @param {boolean} isPrefixedWithTableAlias
|
|
457
469
|
* @param {string} base
|
|
458
470
|
* @param {Token} current
|
|
471
|
+
* @returns {object[]} The stuff to add to the where
|
|
459
472
|
*/
|
|
460
|
-
function translateUnmanagedAssocToWhere(root, target,
|
|
473
|
+
function translateUnmanagedAssocToWhere(root, target, isPrefixedWithTableAlias, base, current) {
|
|
474
|
+
const whereExtension = [];
|
|
461
475
|
for (let j = 0; j < root.on.length; j++) {
|
|
462
476
|
const part = root.on[j];
|
|
463
477
|
|
|
@@ -465,7 +479,7 @@ function handleExists(csn, options, error) {
|
|
|
465
479
|
// but also keep along stuff like null and undefined, so compiler
|
|
466
480
|
// can have a chance to complain/ we can fail later nicely maybe
|
|
467
481
|
if (!(part && part.ref)) {
|
|
468
|
-
|
|
482
|
+
whereExtension.push(part);
|
|
469
483
|
continue;
|
|
470
484
|
}
|
|
471
485
|
|
|
@@ -475,30 +489,82 @@ function handleExists(csn, options, error) {
|
|
|
475
489
|
// Dollar Self Backlink
|
|
476
490
|
if (isValidDollarSelf(root.on[j], root.$path.concat([ 'on', j ]), root.on[j + 1], root.on[j + 2], root.$path.concat([ 'on', j + 2 ]))) {
|
|
477
491
|
if (root.on[j].ref[0] === '$self' && root.on[j].ref.length === 1)
|
|
478
|
-
|
|
492
|
+
whereExtension.push(...translateDollarSelfToWhere(base, target, root.on[j + 2], root.$path.concat([ 'on', j + 2 ])));
|
|
479
493
|
else
|
|
480
|
-
|
|
494
|
+
whereExtension.push(...translateDollarSelfToWhere(base, target, root.on[j], root.$path.concat([ 'on', j ])));
|
|
481
495
|
|
|
482
496
|
j += 2;
|
|
483
497
|
}
|
|
484
498
|
else if (links && links[0].art === root) { // target side
|
|
485
|
-
|
|
499
|
+
whereExtension.push({ ref: [ target, ...part.ref.slice(1) ] });
|
|
486
500
|
}
|
|
487
501
|
else if (part.$scope === '$self') { // source side - "absolute" scope
|
|
488
|
-
|
|
489
|
-
|
|
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
|
+
}
|
|
490
509
|
}
|
|
491
510
|
else if (art) { // source side - with local scope
|
|
492
|
-
if (isPrefixedWithTableAlias)
|
|
493
|
-
|
|
511
|
+
if (isPrefixedWithTableAlias || part.$scope === 'alias')
|
|
512
|
+
whereExtension.push({ ref: [ ...current.ref.slice(0, -1), ...part.ref ] });
|
|
494
513
|
else
|
|
495
|
-
|
|
514
|
+
whereExtension.push({ ref: [ base, ...current.ref.slice(0, -1), ...part.ref ] });
|
|
496
515
|
}
|
|
497
516
|
else { // operator - or any other leftover
|
|
498
|
-
|
|
517
|
+
whereExtension.push(part);
|
|
499
518
|
}
|
|
500
519
|
}
|
|
501
520
|
|
|
521
|
+
return whereExtension;
|
|
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
|
+
|
|
502
568
|
/**
|
|
503
569
|
* Check that an expression triple is a valid $self
|
|
504
570
|
*
|
|
@@ -571,7 +637,7 @@ function handleExists(csn, options, error) {
|
|
|
571
637
|
}
|
|
572
638
|
|
|
573
639
|
/**
|
|
574
|
-
* Check (using inspectRef -> links),
|
|
640
|
+
* Check (using inspectRef -> links), whether the first path step is an entity or query source
|
|
575
641
|
*
|
|
576
642
|
* @param {CSN.Path} path
|
|
577
643
|
* @returns {boolean}
|
|
@@ -621,7 +687,7 @@ function handleExists(csn, options, error) {
|
|
|
621
687
|
return error(null, xpr.$path, 'Boolean $env is not handled yet - please report this error!');
|
|
622
688
|
}
|
|
623
689
|
else if (xpr.ref) {
|
|
624
|
-
throw new
|
|
690
|
+
throw new ModelError('Missing $env and missing leading artifact ref - throwing to trigger recompilation!');
|
|
625
691
|
}
|
|
626
692
|
}
|
|
627
693
|
|
|
@@ -706,7 +772,7 @@ function handleExists(csn, options, error) {
|
|
|
706
772
|
*
|
|
707
773
|
* @param {string} base The source entity/query source name
|
|
708
774
|
* @param {string} target The target entity/query source name
|
|
709
|
-
* @param {
|
|
775
|
+
* @param {object} assoc The association element - the "not-$self" side of the comparison
|
|
710
776
|
* @param {CSN.Path} path
|
|
711
777
|
* @returns {TokenStream} The WHERE representing the $self comparison
|
|
712
778
|
*/
|
|
@@ -5,6 +5,7 @@ const {
|
|
|
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
|
}
|
|
@@ -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)
|
|
@@ -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 = [];
|