@sap/cds-compiler 5.3.0 → 5.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -2
- package/bin/cdsc.js +1 -1
- package/doc/CHANGELOG_BETA.md +2 -2
- package/lib/api/options.js +4 -2
- package/lib/base/builtins.js +0 -10
- package/lib/base/keywords.js +3 -31
- package/lib/base/message-registry.js +23 -5
- package/lib/base/messages.js +1 -1
- package/lib/checks/existsMustEndInAssoc.js +7 -2
- package/lib/checks/foreignKeys.js +12 -7
- package/lib/compiler/assert-consistency.js +11 -3
- package/lib/compiler/builtins.js +2 -0
- package/lib/compiler/checks.js +88 -38
- package/lib/compiler/define.js +2 -2
- package/lib/compiler/shared.js +9 -10
- package/lib/compiler/xpr-rewrite.js +11 -0
- package/lib/compiler/xsn-model.js +1 -1
- package/lib/edm/csn2edm.js +2 -0
- package/lib/edm/edm.js +2 -1
- package/lib/edm/edmPreprocessor.js +14 -1
- package/lib/edm/edmUtils.js +17 -2
- package/lib/gen/BaseParser.js +291 -197
- package/lib/gen/CdlParser.js +1631 -1605
- package/lib/gen/Dictionary.json +74 -6
- package/lib/gen/language.checksum +1 -1
- package/lib/gen/language.interp +1 -1
- package/lib/gen/languageParser.js +1808 -1804
- package/lib/language/antlrParser.js +8 -4
- package/lib/language/genericAntlrParser.js +3 -3
- package/lib/model/csnUtils.js +6 -1
- package/lib/optionProcessor.js +4 -0
- package/lib/parsers/AstBuildingParser.js +172 -108
- package/lib/parsers/CdlGrammar.g4 +154 -134
- package/lib/parsers/Lexer.js +3 -3
- package/lib/parsers/identifiers.js +59 -0
- package/lib/render/toCdl.js +5 -5
- package/lib/render/utils/common.js +5 -0
- package/lib/render/utils/delta.js +23 -5
- package/lib/transform/db/expansion.js +2 -1
- package/lib/transform/db/rewriteCalculatedElements.js +11 -5
- package/lib/transform/db/transformExists.js +52 -26
- package/lib/transform/effective/annotations.js +147 -0
- package/lib/transform/effective/main.js +17 -3
- package/lib/transform/forOdata.js +53 -10
- package/lib/transform/forRelationalDB.js +8 -1
- package/lib/transform/odata/createForeignKeys.js +180 -0
- package/lib/transform/odata/flattening.js +135 -19
- package/lib/transform/odata/typesExposure.js +4 -3
- package/lib/transform/transformUtils.js +6 -6
- package/package.json +1 -1
package/lib/parsers/Lexer.js
CHANGED
|
@@ -177,9 +177,9 @@ function string( text, lexer, parser, beg ) {
|
|
|
177
177
|
}
|
|
178
178
|
else { // try with previous date/time/timestamp/x
|
|
179
179
|
prefix = parser.tokens[parser.tokens.length - 1];
|
|
180
|
-
if (prefix.location.endLine !== lexer.location.line ||
|
|
180
|
+
if (prefix && (prefix.location.endLine !== lexer.location.line ||
|
|
181
181
|
prefix.location.endCol !== lexer.location.col ||
|
|
182
|
-
!quotedLiterals.includes( prefix.keyword ))
|
|
182
|
+
!quotedLiterals.includes( prefix.keyword )))
|
|
183
183
|
prefix = null;
|
|
184
184
|
while (re.test( lexer.input ) && lexer.input[re.lastIndex] === "'")
|
|
185
185
|
esc = ++re.lastIndex;
|
|
@@ -192,7 +192,7 @@ function string( text, lexer, parser, beg ) {
|
|
|
192
192
|
const before = (lastIndex) ? 'string' : 'multi';
|
|
193
193
|
// eslint-disable-next-line cds-compiler/message-texts
|
|
194
194
|
parser.error( 'syntax-missing-token-end', lexer.location,
|
|
195
|
-
{ '#': before,
|
|
195
|
+
{ '#': before, newcode: text }, {
|
|
196
196
|
string: 'The string literal must end with $(NEWCODE) before the end of line',
|
|
197
197
|
multi: 'The multi-line string literal must end with $(NEWCODE)',
|
|
198
198
|
} );
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/** RegEx identifying undelimited identifiers in CDL */
|
|
4
|
+
const undelimitedIdentifierRegex = /^[$_\p{ID_Start}][$\p{ID_Continue}\u200C\u200D]*$/u;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Functions without parentheses in CDL (common standard SQL-92 functions)
|
|
8
|
+
* (do not add more - make it part of the SQL renderer to remove parentheses for
|
|
9
|
+
* other funny SQL functions like CURRENT_UTCTIMESTAMP).
|
|
10
|
+
*/
|
|
11
|
+
const functionsWithoutParentheses = [
|
|
12
|
+
'CURRENT_DATE', 'CURRENT_TIME', 'CURRENT_TIMESTAMP',
|
|
13
|
+
'CURRENT_USER', 'SESSION_USER', 'SYSTEM_USER',
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
// CDL reserved keywords, used for automatic quoting in 'toCdl' renderer
|
|
17
|
+
// Keep in sync with reserved keywords in language.g4
|
|
18
|
+
const cdlKeywords = [
|
|
19
|
+
'ALL',
|
|
20
|
+
'ANY',
|
|
21
|
+
'AS',
|
|
22
|
+
'BY',
|
|
23
|
+
'CASE',
|
|
24
|
+
'CAST',
|
|
25
|
+
'DISTINCT',
|
|
26
|
+
'EXISTS',
|
|
27
|
+
'EXTRACT',
|
|
28
|
+
'FALSE', // boolean
|
|
29
|
+
'FROM',
|
|
30
|
+
'IN',
|
|
31
|
+
'KEY',
|
|
32
|
+
'NEW',
|
|
33
|
+
'NOT',
|
|
34
|
+
'NULL',
|
|
35
|
+
'OF',
|
|
36
|
+
'ON',
|
|
37
|
+
'SELECT',
|
|
38
|
+
'SOME',
|
|
39
|
+
'TRIM',
|
|
40
|
+
'TRUE', // boolean
|
|
41
|
+
'WHEN',
|
|
42
|
+
'WHERE',
|
|
43
|
+
'WITH',
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
function isSimpleCdlIdentifier( id ) {
|
|
47
|
+
if (undelimitedIdentifierRegex.test(id))
|
|
48
|
+
return true;
|
|
49
|
+
const upperId = id.toUpperCase();
|
|
50
|
+
return !cdlKeywords.includes(upperId) &&
|
|
51
|
+
!functionsWithoutParentheses.includes(upperId);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = {
|
|
55
|
+
undelimitedIdentifierRegex,
|
|
56
|
+
cdlKeywords,
|
|
57
|
+
functionsWithoutParentheses,
|
|
58
|
+
isSimpleCdlIdentifier,
|
|
59
|
+
};
|
package/lib/render/toCdl.js
CHANGED
|
@@ -18,8 +18,8 @@ const {
|
|
|
18
18
|
const { isBuiltinType } = require('../base/builtins');
|
|
19
19
|
const { cloneFullCsn } = require('../model/cloneCsn');
|
|
20
20
|
const { getKeysDict } = require('../model/csnRefs');
|
|
21
|
+
const { undelimitedIdentifierRegex } = require('../parsers/identifiers');
|
|
21
22
|
|
|
22
|
-
const identifierRegex = /^[$_a-zA-Z][$_a-zA-Z0-9]*$/;
|
|
23
23
|
const specialFunctionKeywords = Object.create(null);
|
|
24
24
|
|
|
25
25
|
/**
|
|
@@ -2104,7 +2104,7 @@ function csnToCdl( csn, options, msg ) {
|
|
|
2104
2104
|
return path.split('.').map((step, index) => {
|
|
2105
2105
|
if (index === 0)
|
|
2106
2106
|
return quoteNonIdentifierOrKeyword(step, env);
|
|
2107
|
-
else if (!
|
|
2107
|
+
else if (!undelimitedIdentifierRegex.test(step))
|
|
2108
2108
|
return delimitedId(step, env);
|
|
2109
2109
|
return step;
|
|
2110
2110
|
}).join('.');
|
|
@@ -2142,7 +2142,7 @@ function csnToCdl( csn, options, msg ) {
|
|
|
2142
2142
|
* @return {string}
|
|
2143
2143
|
*/
|
|
2144
2144
|
function quoteNonIdentifier( id, env ) {
|
|
2145
|
-
if (!
|
|
2145
|
+
if (!undelimitedIdentifierRegex.test(id))
|
|
2146
2146
|
return delimitedId(id, env);
|
|
2147
2147
|
return id;
|
|
2148
2148
|
}
|
|
@@ -2262,7 +2262,7 @@ function removeTrailingNewline( str ) {
|
|
|
2262
2262
|
* @return {boolean}
|
|
2263
2263
|
*/
|
|
2264
2264
|
function requiresQuotingForCdl( id, additionalKeywords ) {
|
|
2265
|
-
return !
|
|
2265
|
+
return !undelimitedIdentifierRegex.test(id) ||
|
|
2266
2266
|
keywords.cdl.includes(id.toUpperCase()) ||
|
|
2267
2267
|
keywords.cdl_functions.includes(id.toUpperCase()) ||
|
|
2268
2268
|
additionalKeywords.includes(id.toUpperCase());
|
|
@@ -2523,7 +2523,7 @@ function apiSmartId( id, insideFunction = null ) {
|
|
|
2523
2523
|
*/
|
|
2524
2524
|
function apiSmartFunctionId( funcName ) {
|
|
2525
2525
|
const funcId = funcName.toUpperCase();
|
|
2526
|
-
const requiresQuoting = !
|
|
2526
|
+
const requiresQuoting = !undelimitedIdentifierRegex.test(funcName) ||
|
|
2527
2527
|
(keywords.cdl.includes(funcId) && !specialFunctions[funcId]);
|
|
2528
2528
|
if (requiresQuoting)
|
|
2529
2529
|
return apiDelimitedId(funcName);
|
|
@@ -263,6 +263,7 @@ const cdsToSqlTypes = {
|
|
|
263
263
|
'cds.hana.ST_POINT': 'CHAR', // CHAR is implicit fallback used in toSql - make it explicit here
|
|
264
264
|
'cds.hana.ST_GEOMETRY': 'CHAR', // CHAR is implicit fallback used in toSql - make it explicit here
|
|
265
265
|
'cds.Vector': 'NVARCHAR', // Not supported; see #11725
|
|
266
|
+
'cds.Map': 'NCLOB', // Not supported; see #13149
|
|
266
267
|
},
|
|
267
268
|
hana: {
|
|
268
269
|
'cds.hana.SMALLDECIMAL': 'SMALLDECIMAL',
|
|
@@ -270,6 +271,7 @@ const cdsToSqlTypes = {
|
|
|
270
271
|
'cds.hana.ST_POINT': 'ST_POINT',
|
|
271
272
|
'cds.hana.ST_GEOMETRY': 'ST_GEOMETRY',
|
|
272
273
|
'cds.Vector': 'REAL_VECTOR', // FIXME: test me
|
|
274
|
+
'cds.Map': 'NCLOB',
|
|
273
275
|
},
|
|
274
276
|
sqlite: {
|
|
275
277
|
'cds.Date': 'DATE_TEXT',
|
|
@@ -280,6 +282,7 @@ const cdsToSqlTypes = {
|
|
|
280
282
|
'cds.hana.BINARY': 'BINARY_BLOB',
|
|
281
283
|
'cds.hana.SMALLDECIMAL': 'SMALLDECIMAL',
|
|
282
284
|
'cds.Vector': 'BINARY_BLOB', // Not supported; see #11725
|
|
285
|
+
'cds.Map': 'JSON',
|
|
283
286
|
},
|
|
284
287
|
plain: {
|
|
285
288
|
'cds.Binary': 'VARBINARY',
|
|
@@ -292,6 +295,7 @@ const cdsToSqlTypes = {
|
|
|
292
295
|
'cds.DecimalFloat': 'DECFLOAT', // Decimal and Decimal(p) is mapped to cds.DecimalFloat
|
|
293
296
|
'cds.DateTime': 'TIMESTAMP(0)',
|
|
294
297
|
'cds.Timestamp': 'TIMESTAMP(7)',
|
|
298
|
+
'cds.Map': 'JSON',
|
|
295
299
|
},
|
|
296
300
|
postgres: {
|
|
297
301
|
// See <https://www.postgresql.org/docs/current/datatype.html>
|
|
@@ -302,6 +306,7 @@ const cdsToSqlTypes = {
|
|
|
302
306
|
'cds.Double': 'FLOAT8',
|
|
303
307
|
'cds.UInt8': 'INTEGER', // Not equivalent
|
|
304
308
|
'cds.Vector': 'VARCHAR', // Not supported; see #11725
|
|
309
|
+
'cds.Map': 'JSONB',
|
|
305
310
|
},
|
|
306
311
|
};
|
|
307
312
|
|
|
@@ -179,10 +179,8 @@ class DeltaRendererPostgres extends DeltaRenderer {
|
|
|
179
179
|
*/
|
|
180
180
|
alterColumns(artifactName, columnName, delta, definitionsStr, eltName, env) {
|
|
181
181
|
const sqls = [];
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
else if (delta.new.notNull === false || delta.new.$notNull === false)
|
|
185
|
-
definitionsStr = definitionsStr.replace(' NULL', ''); // TODO: Is this robust enough?
|
|
182
|
+
|
|
183
|
+
definitionsStr = this.#removeNullabilityFromElementString(delta, definitionsStr);
|
|
186
184
|
|
|
187
185
|
if (delta.old.default && !delta.old.value) // Drop old default if any exists
|
|
188
186
|
sqls.push(`ALTER TABLE ${this.scopedFunctions.renderArtifactName(artifactName)} ALTER COLUMN ${columnName} DROP DEFAULT;`);
|
|
@@ -190,7 +188,7 @@ class DeltaRendererPostgres extends DeltaRenderer {
|
|
|
190
188
|
if (delta.new.default && !delta.new.value ) { // Alter column with default
|
|
191
189
|
const df = delta.new.default;
|
|
192
190
|
delete delta.new.default;
|
|
193
|
-
const eltStrNoDefault = this.scopedFunctions.renderElement(eltName, delta.new, null, null, env);
|
|
191
|
+
const eltStrNoDefault = this.#removeNullabilityFromElementString(delta, this.scopedFunctions.renderElement(eltName, delta.new, null, null, env));
|
|
194
192
|
delta.new.default = df;
|
|
195
193
|
sqls.push(`ALTER TABLE ${this.scopedFunctions.renderArtifactName(artifactName)} ALTER ${eltStrNoDefault};`);
|
|
196
194
|
sqls.push(`ALTER TABLE ${this.scopedFunctions.renderArtifactName(artifactName)} ALTER COLUMN ${columnName} SET DEFAULT ${this.scopedFunctions.renderExpr(delta.new.default, env.withSubPath('default'))};`);
|
|
@@ -206,6 +204,26 @@ class DeltaRendererPostgres extends DeltaRenderer {
|
|
|
206
204
|
|
|
207
205
|
return sqls;
|
|
208
206
|
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Postgres does not support changing a column AND doing [NOT] NULL things in one statement.
|
|
210
|
+
* So we filter it from the SQL String and render the appropriate SET/DROP for the NOT NULL separately.
|
|
211
|
+
*
|
|
212
|
+
* @param {object} delta
|
|
213
|
+
* @param {string} string
|
|
214
|
+
* @returns {string}
|
|
215
|
+
*/
|
|
216
|
+
#removeNullabilityFromElementString(delta, string) {
|
|
217
|
+
if (delta.new.notNull === true || delta.new.key === true)
|
|
218
|
+
string = string.replace(' NOT NULL', ''); // TODO: Is this robust enough?
|
|
219
|
+
else if (delta.new.notNull === false || delta.new.$notNull === false)
|
|
220
|
+
string = string.replace(' NULL', ''); // TODO: Is this robust enough?
|
|
221
|
+
else if (delta.new.notNull === delta.old.notNull)
|
|
222
|
+
string = string.replace( delta.new.notNull ? ' NOT NULL' : ' NULL', ''); // TODO: Is this robust enough?
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
return string;
|
|
226
|
+
}
|
|
209
227
|
}
|
|
210
228
|
|
|
211
229
|
class DeltaRendererH2 extends DeltaRenderer {
|
|
@@ -596,7 +596,8 @@ function expandStructureReferences( csn, options, pathDelimiter, messageFunction
|
|
|
596
596
|
while (stack.length > 0) {
|
|
597
597
|
const [ current, currentRef, currentAlias ] = stack.pop();
|
|
598
598
|
if (csnUtils.isStructured(current)) {
|
|
599
|
-
|
|
599
|
+
// `cds.Map` may also be used
|
|
600
|
+
const elements = Object.entries(current.elements || csnUtils.effectiveType(current).elements || {}).reverse();
|
|
600
601
|
for (const [ name, elem ] of elements)
|
|
601
602
|
stack.push([ elem, currentRef.concat(name), currentAlias.concat(name) ]);
|
|
602
603
|
}
|
|
@@ -660,11 +660,12 @@ function rewriteCalculatedElementsInViews( csn, options, csnUtils, pathDelimiter
|
|
|
660
660
|
|
|
661
661
|
/**
|
|
662
662
|
* @param {CSN.Model} csn
|
|
663
|
+
* @param {CSN.Options} options
|
|
663
664
|
*/
|
|
664
|
-
function processCalculatedElementsInEntities( csn ) {
|
|
665
|
+
function processCalculatedElementsInEntities( csn, options ) {
|
|
665
666
|
forEachDefinition(csn, (artifact, definitionName) => {
|
|
666
667
|
if (artifact.kind === 'entity' && !(artifact.query || artifact.projection))
|
|
667
|
-
removeDummyValueInEntity(artifact, [ 'definitions', definitionName ]);
|
|
668
|
+
removeDummyValueInEntity(artifact, [ 'definitions', definitionName ], options);
|
|
668
669
|
});
|
|
669
670
|
}
|
|
670
671
|
|
|
@@ -674,14 +675,19 @@ function processCalculatedElementsInEntities( csn ) {
|
|
|
674
675
|
*
|
|
675
676
|
* @param {CSN.Artifact} artifact
|
|
676
677
|
* @param {CSN.Path} path
|
|
678
|
+
* @param {CSN.Options} options
|
|
677
679
|
* @todo calculated elements that "live" on the database?
|
|
678
680
|
* @todo error when artifact is empty afterwards? Probably better as a CSN check!
|
|
679
681
|
*/
|
|
680
|
-
function removeDummyValueInEntity( artifact, path ) {
|
|
682
|
+
function removeDummyValueInEntity( artifact, path, options ) {
|
|
681
683
|
applyTransformationsOnDictionary(artifact.elements, {
|
|
682
684
|
value: (parent, prop, value, p, elements) => {
|
|
683
|
-
if (!value.stored)
|
|
684
|
-
|
|
685
|
+
if (!value.stored) {
|
|
686
|
+
if (options.transformation === 'effective' && parent.on)
|
|
687
|
+
delete parent.value;
|
|
688
|
+
else
|
|
689
|
+
delete elements[p.at(-1)];
|
|
690
|
+
}
|
|
685
691
|
},
|
|
686
692
|
}, {}, path.concat( 'elements' ));
|
|
687
693
|
}
|
|
@@ -409,6 +409,7 @@ function handleExists( csn, options, error, inspectRef, initDefinition, dropDefi
|
|
|
409
409
|
}
|
|
410
410
|
|
|
411
411
|
/**
|
|
412
|
+
*
|
|
412
413
|
* Translate an `EXISTS <unmanaged assoc>` into a part of a WHERE condition.
|
|
413
414
|
*
|
|
414
415
|
* A valid $self-backlink is handled in translateDollarSelfToWhere.
|
|
@@ -428,64 +429,89 @@ function handleExists( csn, options, error, inspectRef, initDefinition, dropDefi
|
|
|
428
429
|
*/
|
|
429
430
|
function translateUnmanagedAssocToWhere( root, target, isPrefixedWithTableAlias, base, current ) {
|
|
430
431
|
const whereExtension = [];
|
|
431
|
-
|
|
432
|
-
|
|
432
|
+
|
|
433
|
+
for (let j = 0; j < root.on.length; j++)
|
|
434
|
+
j = processExpressionPart(root.on, root.$path.concat('on'), j, whereExtension);
|
|
435
|
+
|
|
436
|
+
return whereExtension;
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Process the given expression and apply the steps described above.
|
|
440
|
+
*
|
|
441
|
+
* @param {Array} expression Expression we are processing
|
|
442
|
+
* @param {CSN.Path} path Path to the expression
|
|
443
|
+
* @param {number} expressionIndex Index in the current expression, imporant for paths and stuff
|
|
444
|
+
* @param {Array} collector Array to collect the processed expressionparts into
|
|
445
|
+
* @returns {number} How far along expression we have processed - so the main loop can jump ahead
|
|
446
|
+
*/
|
|
447
|
+
function processExpressionPart(expression, path, expressionIndex, collector) {
|
|
448
|
+
const part = expression[expressionIndex];
|
|
449
|
+
|
|
450
|
+
if (part?.xpr) {
|
|
451
|
+
const xpr = { xpr: [] };
|
|
452
|
+
for (let i = 0; i < part.xpr.length; i++)
|
|
453
|
+
i = processExpressionPart(part.xpr, path.concat(expressionIndex, 'xpr'), i, xpr.xpr);
|
|
454
|
+
|
|
455
|
+
collector.push(xpr);
|
|
456
|
+
return expressionIndex;
|
|
457
|
+
}
|
|
433
458
|
|
|
434
459
|
// we can only resolve stuff on refs - skip literals like =
|
|
435
460
|
// but also keep along stuff like null and undefined, so compiler
|
|
436
461
|
// can have a chance to complain/ we can fail later nicely maybe
|
|
437
462
|
if (!(part && part.ref)) {
|
|
438
|
-
|
|
439
|
-
|
|
463
|
+
collector.push(part);
|
|
464
|
+
return expressionIndex;
|
|
440
465
|
}
|
|
441
466
|
|
|
442
467
|
// root.$path should be safe - we can only reference things in exists that exist when we enrich
|
|
443
468
|
// so all of them should have a $path.
|
|
444
|
-
const { art, links } = inspectRef(
|
|
469
|
+
const { art, links } = inspectRef(path.concat(expressionIndex));
|
|
445
470
|
// Dollar Self Backlink
|
|
446
|
-
if (isValidDollarSelf(
|
|
447
|
-
if (
|
|
448
|
-
|
|
471
|
+
if (isValidDollarSelf(expression[expressionIndex], path.concat(expressionIndex), expression[expressionIndex + 1], expression[expressionIndex + 2], path.concat(expressionIndex + 2 ))) {
|
|
472
|
+
if (expression[expressionIndex].ref[0] === '$self' && expression[expressionIndex].ref.length === 1)
|
|
473
|
+
collector.push(...translateDollarSelfToWhere(base, target, expression[expressionIndex + 2], path.concat(expressionIndex + 2 )));
|
|
449
474
|
else
|
|
450
|
-
|
|
475
|
+
collector.push(...translateDollarSelfToWhere(base, target, expression[expressionIndex], path.concat(expressionIndex)));
|
|
451
476
|
|
|
452
|
-
|
|
477
|
+
return expressionIndex + 2;
|
|
453
478
|
}
|
|
454
479
|
else if (links && links[0].art === root) { // target side
|
|
455
|
-
|
|
480
|
+
collector.push({ ref: [ target, ...part.ref.slice(1) ] });
|
|
456
481
|
}
|
|
457
482
|
else if (part.$scope === '$self') { // source side - "absolute" scope
|
|
458
483
|
const column = part._art._column;
|
|
459
484
|
if (column && column.as) { // Replace with the "original" expression (the .ref, .xpr etc.)
|
|
460
|
-
|
|
485
|
+
collector.push(translateToSourceSide(column));
|
|
461
486
|
}
|
|
462
487
|
else {
|
|
463
|
-
|
|
488
|
+
collector.push(assignAndDeleteAsAndKey({}, part, { ref: [ base, ...part.ref.slice(1) ] }));
|
|
464
489
|
}
|
|
465
490
|
}
|
|
466
491
|
else if (art) { // source side - with local scope
|
|
467
492
|
if (isPrefixedWithTableAlias || part.$scope === 'alias')
|
|
468
|
-
|
|
493
|
+
collector.push({ ref: [ ...current.ref.slice(0, -1), ...part.ref ] });
|
|
469
494
|
else
|
|
470
|
-
|
|
495
|
+
collector.push({ ref: [ base, ...current.ref.slice(0, -1), ...part.ref ] });
|
|
471
496
|
}
|
|
472
497
|
else { // operator - or any other leftover
|
|
473
|
-
|
|
498
|
+
collector.push(part);
|
|
474
499
|
}
|
|
475
|
-
}
|
|
476
500
|
|
|
477
|
-
|
|
501
|
+
return expressionIndex;
|
|
502
|
+
}
|
|
478
503
|
|
|
479
504
|
|
|
480
505
|
/**
|
|
481
|
-
* Run Object.assign on all of the passed in parameters and delete a .as at the end
|
|
506
|
+
* Run Object.assign on all of the passed in parameters and delete a .as and .key at the end
|
|
482
507
|
*
|
|
483
508
|
* @param {...any} args
|
|
484
|
-
* @returns {object} The merged args without an .as property
|
|
509
|
+
* @returns {object} The merged args without an .as and .key property
|
|
485
510
|
*/
|
|
486
|
-
function
|
|
511
|
+
function assignAndDeleteAsAndKey( ...args ) {
|
|
487
512
|
const obj = Object.assign.apply(null, args);
|
|
488
513
|
delete obj.as;
|
|
514
|
+
delete obj.key;
|
|
489
515
|
return obj;
|
|
490
516
|
}
|
|
491
517
|
/**
|
|
@@ -503,19 +529,19 @@ function handleExists( csn, options, error, inspectRef, initDefinition, dropDefi
|
|
|
503
529
|
const column = obj._art._column;
|
|
504
530
|
if (column && column.as)
|
|
505
531
|
return translateToSourceSide(column);
|
|
506
|
-
return
|
|
532
|
+
return assignAndDeleteAsAndKey({}, obj, { ref: [ base, ...obj.ref.slice(1) ] });
|
|
507
533
|
}
|
|
508
534
|
else if (typeof obj.$env === 'string') {
|
|
509
|
-
return
|
|
535
|
+
return assignAndDeleteAsAndKey({}, obj, { ref: [ obj.$env, ...obj.ref ] });
|
|
510
536
|
}
|
|
511
537
|
|
|
512
|
-
return
|
|
538
|
+
return assignAndDeleteAsAndKey({}, obj, { ref: [ ...obj.ref ] });
|
|
513
539
|
}
|
|
514
540
|
else if (obj.xpr) { // we need to drill further down into .xpr
|
|
515
|
-
return
|
|
541
|
+
return assignAndDeleteAsAndKey({}, obj, { xpr: obj.xpr.map(translateToSourceSide) });
|
|
516
542
|
}
|
|
517
543
|
else if (obj.args) {
|
|
518
|
-
return
|
|
544
|
+
return assignAndDeleteAsAndKey({}, obj, { args: obj.args.map(translateToSourceSide) });
|
|
519
545
|
}
|
|
520
546
|
|
|
521
547
|
return obj;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { CompilerAssertion } = require('../../base/error');
|
|
4
|
+
const { forEach } = require('../../utils/objectUtils');
|
|
4
5
|
|
|
5
6
|
const directMappings = {
|
|
6
7
|
'@Common.IsDayOfCalendarMonth': replace('@Semantics.calendar.dayOfMonth'),
|
|
@@ -190,6 +191,152 @@ function remapODataAnnotations( csn ) {
|
|
|
190
191
|
};
|
|
191
192
|
}
|
|
192
193
|
|
|
194
|
+
/**
|
|
195
|
+
* Do the .texts anno magic if we can be reasonably sure that we are actually dealing with a .texts entity.
|
|
196
|
+
*
|
|
197
|
+
* @param {CSN.Model} csn
|
|
198
|
+
* @param {string} artifactName
|
|
199
|
+
* @param {CSN.Artifact} artifact
|
|
200
|
+
*/
|
|
201
|
+
function sealAnnoMagicForTexts(csn, artifactName, artifact) {
|
|
202
|
+
if (artifactName.endsWith('.texts') && artifact.elements?.locale) {
|
|
203
|
+
const firstNonKey = getFirstNonKeyElement(artifact);
|
|
204
|
+
if (firstNonKey && firstNonKey.type === 'cds.String') {
|
|
205
|
+
artifact['@ObjectModel.supportedCapabilities'] ??= [];
|
|
206
|
+
if (!artifact['@ObjectModel.supportedCapabilities'].find(part => part['#'] === 'LANGUAGE_DEPENDENT_TEXT'))
|
|
207
|
+
artifact['@ObjectModel.supportedCapabilities'].push({ '#': 'LANGUAGE_DEPENDENT_TEXT' });
|
|
208
|
+
if (artifact.elements.locale['@Semantics.language'] === undefined)
|
|
209
|
+
artifact.elements.locale['@Semantics.language'] = true;
|
|
210
|
+
if (firstNonKey['@Semantics.text'] === undefined)
|
|
211
|
+
firstNonKey['@Semantics.text'] = true;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
*
|
|
218
|
+
* @param {CSN.Artifact} artifact
|
|
219
|
+
* @returns {CSN.Element|null}
|
|
220
|
+
*/
|
|
221
|
+
function getFirstNonKeyElement(artifact) {
|
|
222
|
+
for (const elementName in artifact.elements) {
|
|
223
|
+
if (Object.prototype.hasOwnProperty.call(artifact.elements, elementName)) {
|
|
224
|
+
if (!artifact.elements[elementName].key)
|
|
225
|
+
return artifact.elements[elementName];
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
*
|
|
234
|
+
* @param {CSN.Model} csn
|
|
235
|
+
* @returns {object} Transfomer object for applyTransformations
|
|
236
|
+
*/
|
|
237
|
+
function sealAnnoMagic(csn) {
|
|
238
|
+
return {
|
|
239
|
+
'@ObjectModel.supportedCapabilities': (parent, prop, anno, path) => {
|
|
240
|
+
// Filter only for values we care about
|
|
241
|
+
const filteredAnno = anno.filter(value => value['#'] === 'ANALYTICAL_DIMENSION' || value['#'] === 'LANGUAGE_DEPENDENT_TEXT' || value['#'] === 'ANALYTICAL_PROVIDER');
|
|
242
|
+
if (filteredAnno.filter(value => value['#'] === 'ANALYTICAL_PROVIDER').length > 0 && parent.kind === 'entity' && isPartOfINAService(csn, path[1]) && parent.elements) {
|
|
243
|
+
forEach(parent.elements, (elementName, element) => {
|
|
244
|
+
if (element.target && csn.definitions[element.target]['@ObjectModel.supportedCapabilities']?.filter(value => value['#'] === 'ANALYTICAL_DIMENSION').length > 0) {
|
|
245
|
+
const tuples = getOnConditionAsComparisonTuples(element.on, elementName);
|
|
246
|
+
const targetEntity = csn.definitions[element.target];
|
|
247
|
+
if (element.on.length === 3 && tuples.length > 0 ) {
|
|
248
|
+
tuples.forEach(({ source }) => {
|
|
249
|
+
const sourceElement = parent.elements[source.ref[0]];
|
|
250
|
+
if (!sourceElement.target && sourceElement['@ObjectModel.foreignKey.association'] === undefined)
|
|
251
|
+
sourceElement['@ObjectModel.foreignKey.association'] = { '=': elementName };
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
else if (element.on.length > 3 && tuples.length > 0 && targetEntity['@ObjectModel.representativeKey']) {
|
|
256
|
+
tuples.forEach(({ source, target }) => {
|
|
257
|
+
if (target.ref[1] === targetEntity['@ObjectModel.representativeKey']['=']) {
|
|
258
|
+
const sourceElement = parent.elements[source.ref[0]];
|
|
259
|
+
if (!sourceElement.target && sourceElement['@ObjectModel.foreignKey.association'] === undefined)
|
|
260
|
+
sourceElement['@ObjectModel.foreignKey.association'] = { '=': elementName };
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (filteredAnno.filter(value => value['#'] === 'ANALYTICAL_DIMENSION').length > 0 && parent.kind === 'entity' && parent.elements) {
|
|
269
|
+
forEach(parent.elements, (_elementName, element) => {
|
|
270
|
+
if (element['@ObjectModel.text.element'] && parent.elements[element['@ObjectModel.text.element']['=']] && parent.elements[element['@ObjectModel.text.element']['=']]['@Semantics.text'] === undefined)
|
|
271
|
+
parent.elements[element['@ObjectModel.text.element']['=']]['@Semantics.text'] = true;
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (filteredAnno.length === 1 && parent.kind && parent['@ObjectModel.modelingPattern'] === undefined) {
|
|
276
|
+
if (filteredAnno[0]['#'] === 'ANALYTICAL_PROVIDER')
|
|
277
|
+
parent['@ObjectModel.modelingPattern'] = { '#': 'ANALYTICAL_CUBE' };
|
|
278
|
+
else
|
|
279
|
+
parent['@ObjectModel.modelingPattern'] = { '#': filteredAnno[0]['#'] };
|
|
280
|
+
}
|
|
281
|
+
},
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function isPartOfINAService(csn, artifactName) {
|
|
286
|
+
const parts = artifactName.split('.');
|
|
287
|
+
if (parts.length === 1)
|
|
288
|
+
return false; // No dots
|
|
289
|
+
for (let i = 0; i < parts.length; i++) {
|
|
290
|
+
const possibleServiceName = parts.slice(0, i).join('.');
|
|
291
|
+
const possibleDefinition = csn.definitions[possibleServiceName];
|
|
292
|
+
if (possibleDefinition?.kind === 'service')
|
|
293
|
+
return possibleDefinition['@protocol'] === 'ina';
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Split the given on-condition into bite-sized tuples IF
|
|
301
|
+
* - the operator is a =
|
|
302
|
+
* - one of the arguments is of the form <assoc>.<field>
|
|
303
|
+
* - one of the arguments is of the form <field>
|
|
304
|
+
* - there are no braces
|
|
305
|
+
* - each of the comparison tuples is "joined" via "and"
|
|
306
|
+
*
|
|
307
|
+
* Return an empty array if we encounter any tuples/things that do NOT match those criteria
|
|
308
|
+
* @param {CSN.OnCondition} on
|
|
309
|
+
* @param {string} assocName
|
|
310
|
+
* @returns {object[]}
|
|
311
|
+
*/
|
|
312
|
+
function getOnConditionAsComparisonTuples(on, assocName) {
|
|
313
|
+
const validTuples = [];
|
|
314
|
+
for (let i = 0; i < on.length - 2; i += 4) {
|
|
315
|
+
let isValid = false;
|
|
316
|
+
const arg1 = on[i];
|
|
317
|
+
const operator = on[i + 1];
|
|
318
|
+
const arg2 = on[i + 2];
|
|
319
|
+
const possibleAnd = i + 3 < on.length ? on[i + 3] : 'and';
|
|
320
|
+
if (possibleAnd === 'and' && operator === '=' && (arg1.ref?.length === 1 && arg2.ref?.length === 2 && arg2.ref[0] === assocName || arg1.ref?.length === 2 && arg1.ref[0] === assocName && arg2.ref?.length === 1 )) { // TODO: Do we care about filters? Filters could cause a crash here?
|
|
321
|
+
if (arg1.ref.length === 1) { // arg1 needs to point to be <field>, arg2 needs to be <assoc>.<field>
|
|
322
|
+
validTuples.push({ source: arg1, target: arg2 });
|
|
323
|
+
isValid = true;
|
|
324
|
+
}
|
|
325
|
+
else { // arg1 needs to point to be <assoc>.<field>, arg2 needs to be <field>
|
|
326
|
+
validTuples.push({ source: arg2, target: arg1 });
|
|
327
|
+
isValid = true;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (!isValid)
|
|
332
|
+
return [];
|
|
333
|
+
}
|
|
334
|
+
return validTuples;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
|
|
193
338
|
module.exports = {
|
|
194
339
|
remapODataAnnotations,
|
|
340
|
+
sealAnnoMagic,
|
|
341
|
+
sealAnnoMagicForTexts,
|
|
195
342
|
};
|
|
@@ -75,15 +75,29 @@ function effectiveCsn( model, options, messageFunctions ) {
|
|
|
75
75
|
// ensure getElement works on flattened struct_assoc columns
|
|
76
76
|
csnUtils = getUtils(csn, 'init-all');
|
|
77
77
|
|
|
78
|
-
processCalculatedElementsInEntities(csn);
|
|
78
|
+
processCalculatedElementsInEntities(csn, options);
|
|
79
79
|
associations.managedToUnmanaged(csn, options, csnUtils, messageFunctions);
|
|
80
80
|
associations.transformBacklinks(csn, options, csnUtils, messageFunctions);
|
|
81
|
-
const transformers = mergeTransformers([
|
|
82
|
-
|
|
81
|
+
const transformers = mergeTransformers([
|
|
82
|
+
options.addCdsPersistenceName ? misc.attachPersistenceName(csn, options, csnUtils) : {},
|
|
83
|
+
options.remapOdataAnnotations ? annotations.remapODataAnnotations(csn) : {},
|
|
84
|
+
misc.removeDefinitionsAndProperties(csn, options),
|
|
85
|
+
options.deriveAnalyticalAnnotations ? annotations.sealAnnoMagic(csn) : {},
|
|
86
|
+
], null);
|
|
87
|
+
|
|
88
|
+
const artifactTransformers = [];
|
|
89
|
+
if (options.deriveAnalyticalAnnotations)
|
|
90
|
+
artifactTransformers.push(annotations.sealAnnoMagicForTexts);
|
|
91
|
+
|
|
92
|
+
applyTransformations(csn, transformers, artifactTransformers, { skipIgnore: false, processAnnotations: true });
|
|
83
93
|
|
|
84
94
|
if (!options.resolveProjections)
|
|
85
95
|
redoProjections.forEach(fn => fn());
|
|
86
96
|
|
|
97
|
+
|
|
98
|
+
// Remove unapplied extensions/annotations
|
|
99
|
+
delete csn.extensions;
|
|
100
|
+
|
|
87
101
|
messageFunctions.throwWithError();
|
|
88
102
|
|
|
89
103
|
return csn;
|