@sap/cds-compiler 4.6.0 → 4.7.4
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 +44 -0
- package/bin/cds_update_identifiers.js +6 -2
- package/bin/cdsc.js +1 -1
- package/doc/CHANGELOG_ARCHIVE.md +9 -9
- package/doc/CHANGELOG_BETA.md +6 -0
- package/lib/api/main.js +56 -9
- package/lib/api/options.js +6 -3
- package/lib/api/validate.js +20 -29
- package/lib/base/message-registry.js +27 -3
- package/lib/base/messages.js +8 -3
- package/lib/base/model.js +2 -0
- package/lib/checks/dbFeatureFlags.js +28 -0
- package/lib/checks/elements.js +81 -13
- package/lib/checks/enricher.js +3 -2
- package/lib/checks/validator.js +38 -4
- package/lib/compiler/assert-consistency.js +4 -4
- package/lib/compiler/checks.js +5 -4
- package/lib/compiler/define.js +2 -2
- package/lib/compiler/generate.js +2 -1
- package/lib/compiler/populate.js +5 -1
- package/lib/compiler/propagator.js +3 -11
- package/lib/compiler/shared.js +2 -1
- package/lib/compiler/tweak-assocs.js +43 -24
- package/lib/edm/annotations/edmJson.js +3 -0
- package/lib/edm/annotations/genericTranslation.js +156 -106
- package/lib/edm/annotations/preprocessAnnotations.js +11 -14
- package/lib/edm/csn2edm.js +27 -24
- package/lib/edm/edm.js +8 -8
- package/lib/edm/edmPreprocessor.js +135 -37
- package/lib/edm/edmUtils.js +20 -7
- package/lib/gen/Dictionary.json +2 -1
- package/lib/gen/language.checksum +1 -1
- package/lib/gen/language.interp +9 -11
- package/lib/gen/languageParser.js +5942 -5446
- package/lib/json/to-csn.js +7 -114
- package/lib/language/genericAntlrParser.js +106 -48
- package/lib/model/cloneCsn.js +203 -0
- package/lib/model/csnRefs.js +11 -3
- package/lib/model/csnUtils.js +42 -85
- package/lib/optionProcessor.js +2 -2
- package/lib/render/manageConstraints.js +1 -1
- package/lib/render/toCdl.js +133 -88
- package/lib/render/toHdbcds.js +1 -5
- package/lib/render/toSql.js +7 -9
- package/lib/render/utils/common.js +9 -16
- package/lib/transform/addTenantFields.js +277 -102
- package/lib/transform/db/applyTransformations.js +14 -9
- package/lib/transform/db/backlinks.js +2 -1
- package/lib/transform/db/constraints.js +60 -82
- package/lib/transform/db/expansion.js +6 -6
- package/lib/transform/db/featureFlags.js +5 -0
- package/lib/transform/db/flattening.js +4 -4
- package/lib/transform/db/killAnnotations.js +1 -0
- package/lib/transform/db/rewriteCalculatedElements.js +2 -2
- package/lib/transform/db/transformExists.js +12 -0
- package/lib/transform/db/views.js +5 -2
- package/lib/transform/draft/odata.js +7 -6
- package/lib/transform/effective/associations.js +2 -1
- package/lib/transform/effective/main.js +3 -2
- package/lib/transform/effective/types.js +6 -3
- package/lib/transform/forOdata.js +39 -24
- package/lib/transform/forRelationalDB.js +34 -27
- package/lib/transform/localized.js +29 -9
- package/lib/transform/odata/flattening.js +419 -0
- package/lib/transform/odata/toFinalBaseType.js +95 -15
- package/lib/transform/odata/typesExposure.js +9 -7
- package/lib/transform/transformUtils.js +7 -6
- package/lib/transform/translateAssocsToJoins.js +3 -3
- package/lib/utils/objectUtils.js +14 -0
- package/package.json +1 -1
package/lib/json/to-csn.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
const { locationString } = require('../base/messages');
|
|
15
15
|
const { isBetaEnabled, isDeprecatedEnabled } = require('../base/model');
|
|
16
16
|
const { pathName } = require('../compiler/utils');
|
|
17
|
-
const { CompilerAssertion
|
|
17
|
+
const { CompilerAssertion } = require('../base/error');
|
|
18
18
|
|
|
19
19
|
const compilerVersion = require('../../package.json').version;
|
|
20
20
|
const creator = `CDS Compiler v${ compilerVersion }`;
|
|
@@ -212,116 +212,10 @@ const castProperties = [
|
|
|
212
212
|
];
|
|
213
213
|
|
|
214
214
|
const csnDictionaries = [
|
|
215
|
-
'args', 'params', 'enum', 'mixin', 'elements', 'actions', 'definitions',
|
|
215
|
+
'args', 'params', 'enum', 'mixin', 'elements', 'actions', 'definitions', 'vocabularies',
|
|
216
216
|
];
|
|
217
217
|
const csnDirectValues = [ 'val' ]; // + all starting with '@' - TODO: still relevant
|
|
218
218
|
|
|
219
|
-
/**
|
|
220
|
-
* Sort property names of CSN according to sequence which is also used by the compactModel function
|
|
221
|
-
* Only returns enumerable properties, except for certain hidden properties
|
|
222
|
-
* if requested (cloneOptions != false): $location, elements.
|
|
223
|
-
*
|
|
224
|
-
* If cloneOptions is false or if either cloneOptions.testMode or cloneOptions.testSortCsn
|
|
225
|
-
* are set, definitions are also sorted.
|
|
226
|
-
*
|
|
227
|
-
* @param {object} csn
|
|
228
|
-
* @param {CSN.Options|false} cloneOptions
|
|
229
|
-
*/
|
|
230
|
-
function sortCsn( csn, cloneOptions = false ) {
|
|
231
|
-
if (cloneOptions && typeof cloneOptions === 'object')
|
|
232
|
-
initModuleVars( cloneOptions );
|
|
233
|
-
|
|
234
|
-
if (Array.isArray(csn))
|
|
235
|
-
return csn.map( v => (!v || typeof v !== 'object' ? v : sortCsn(v, cloneOptions) ) );
|
|
236
|
-
const r = {};
|
|
237
|
-
for (const n of Object.keys(csn).sort( compareProperties ) ) {
|
|
238
|
-
const sortDict = n === 'definitions' &&
|
|
239
|
-
(!cloneOptions || cloneOptions.testMode || cloneOptions.testSortCsn);
|
|
240
|
-
const val = csn[n];
|
|
241
|
-
if (!val || typeof val !== 'object' || csnDirectValues.includes(n))
|
|
242
|
-
r[n] = val;
|
|
243
|
-
else if (n.charAt(0) === '@')
|
|
244
|
-
r[n] = cloneAnnotationValue( val );
|
|
245
|
-
else if (csnDictionaries.includes(n) && !Array.isArray(val))
|
|
246
|
-
// Array check for property `args` which may either be a dictionary or an array.
|
|
247
|
-
r[n] = csnDictionary( val, sortDict, cloneOptions );
|
|
248
|
-
|
|
249
|
-
else
|
|
250
|
-
r[n] = sortCsn(val, cloneOptions);
|
|
251
|
-
}
|
|
252
|
-
if (cloneOptions && typeof csn === 'object') {
|
|
253
|
-
if ({}.hasOwnProperty.call( csn, '$sources' ) && !r.$sources)
|
|
254
|
-
setHidden( r, '$sources', csn.$sources );
|
|
255
|
-
if ({}.hasOwnProperty.call( csn, '$location' ) && !r.$location)
|
|
256
|
-
setHidden( r, '$location', csn.$location );
|
|
257
|
-
if ({}.hasOwnProperty.call( csn, '$path' )) // used in generic reference flattener
|
|
258
|
-
setHidden( r, '$path', csn.$path );
|
|
259
|
-
if ({}.hasOwnProperty.call( csn, '$paths' )) // used in generic reference flattener
|
|
260
|
-
setHidden( r, '$paths', csn.$paths );
|
|
261
|
-
if (hasNonEnumerable( csn, 'elements' ) && !r.elements) // non-enumerable 'elements'
|
|
262
|
-
setHidden( r, 'elements', csnDictionary( csn.elements, false, cloneOptions ) );
|
|
263
|
-
if (hasNonEnumerable( csn, '$tableConstraints' ) && !r.$tableConstraints)
|
|
264
|
-
setHidden( r, '$tableConstraints', csn.$tableConstraints );
|
|
265
|
-
if (cloneOptions.hiddenPropertiesToClone) {
|
|
266
|
-
cloneOptions.hiddenPropertiesToClone.forEach((property) => {
|
|
267
|
-
if ({}.hasOwnProperty.call( csn, property )) // used in generic reference flattener
|
|
268
|
-
setHidden( r, property, csn[property] );
|
|
269
|
-
});
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
return r;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
function cloneAnnotationValue( val ) {
|
|
276
|
-
if (typeof val !== 'object') // scalar
|
|
277
|
-
return val;
|
|
278
|
-
return JSON.parse( JSON.stringify( val ) );
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
/**
|
|
282
|
-
* Check whether the given object has non enumerable property.
|
|
283
|
-
* Ensure that we don't take it from the prototype, only "directly" - we accidentally
|
|
284
|
-
* cloned elements with a cds.linked input otherwise.
|
|
285
|
-
*
|
|
286
|
-
* @param {object} object
|
|
287
|
-
* @param {string} property
|
|
288
|
-
* @returns
|
|
289
|
-
*/
|
|
290
|
-
function hasNonEnumerable( object, property ) {
|
|
291
|
-
return {}.hasOwnProperty.call( object, property ) &&
|
|
292
|
-
!{}.propertyIsEnumerable.call( object, property );
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
/**
|
|
296
|
-
* @param {object} csn
|
|
297
|
-
* @param {boolean} sort
|
|
298
|
-
* @param {CSN.Options | false} cloneOptions If != false,
|
|
299
|
-
* cloneOptions.dictionaryPrototype is used and cloneOptions are
|
|
300
|
-
* passed to sort().
|
|
301
|
-
* @returns {object}
|
|
302
|
-
*/
|
|
303
|
-
function csnDictionary( csn, sort, cloneOptions = false ) {
|
|
304
|
-
if (!csn || Array.isArray(csn)) // null or strange CSN
|
|
305
|
-
return csn;
|
|
306
|
-
const proto = cloneOptions && (typeof cloneOptions === 'object') &&
|
|
307
|
-
cloneOptions.dictionaryPrototype;
|
|
308
|
-
// eslint-disable-next-line no-nested-ternary
|
|
309
|
-
const dictProto = (typeof proto === 'object') // including null
|
|
310
|
-
? proto
|
|
311
|
-
: (proto) ? Object.prototype : null;
|
|
312
|
-
const r = Object.create( dictProto );
|
|
313
|
-
for (const n of (sort) ? Object.keys(csn).sort() : Object.keys(csn)) {
|
|
314
|
-
// CSN does not allow any dictionary that are not objects.
|
|
315
|
-
// The compiler handles it, but a pre-transformed OData CSN won't trigger recompilation.
|
|
316
|
-
if (csn[n] && typeof csn[n] === 'object')
|
|
317
|
-
r[n] = sortCsn(csn[n], cloneOptions);
|
|
318
|
-
else
|
|
319
|
-
throw new ModelError(`Found non-object dictionary entry: "${ n }" of type "${ typeof csn[n] }"`);
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
return r;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
219
|
/**
|
|
326
220
|
* Compact the given XSN model and transform it into CSN.
|
|
327
221
|
*
|
|
@@ -1320,7 +1214,7 @@ function exprInternal( node, xprParens ) {
|
|
|
1320
1214
|
return extra( call, node );
|
|
1321
1215
|
}
|
|
1322
1216
|
if (node.query)
|
|
1323
|
-
return query( node.query, null, null, null, 1 );
|
|
1217
|
+
return query( node.query, null, null, null, (node.$parens ? 1 - node.$parens.length : 1) );
|
|
1324
1218
|
if (!node.op) // parse error
|
|
1325
1219
|
return { xpr: [] };
|
|
1326
1220
|
|
|
@@ -1336,7 +1230,7 @@ function exprInternal( node, xprParens ) {
|
|
|
1336
1230
|
return cast( expression( node.args[0] ), node );
|
|
1337
1231
|
case 'list':
|
|
1338
1232
|
return extra( { list: node.args.map( expression ) }, node, 0 );
|
|
1339
|
-
default: {
|
|
1233
|
+
default: { // CSN v0 input (A2J: '='/'and'): binary (n-ary) and unary prefix
|
|
1340
1234
|
if (!node.args.length)
|
|
1341
1235
|
return { xpr: [] };
|
|
1342
1236
|
const nary = [];
|
|
@@ -1345,7 +1239,7 @@ function exprInternal( node, xprParens ) {
|
|
|
1345
1239
|
val = 'nary';
|
|
1346
1240
|
node = {
|
|
1347
1241
|
op: { val },
|
|
1348
|
-
args: (nary.length > 2 ? nary.slice(1) : nary),
|
|
1242
|
+
args: (nary.length > 2 ? nary.slice(1) : nary),
|
|
1349
1243
|
$parens: node.$parens,
|
|
1350
1244
|
};
|
|
1351
1245
|
}
|
|
@@ -1361,6 +1255,7 @@ function flattenInternalXpr( array, op ) {
|
|
|
1361
1255
|
return array.flat( Infinity );
|
|
1362
1256
|
if (array.length < 5 || op !== 'nary')
|
|
1363
1257
|
return array;
|
|
1258
|
+
// nary: [ ‹a›, '+', ‹b›, '+', ‹c› ] → [ [ ‹a›, '+', ‹b› ], '+', ‹c› ]
|
|
1364
1259
|
let left = array.slice( 0, 3 );
|
|
1365
1260
|
let index = 3;
|
|
1366
1261
|
while (index < array.length)
|
|
@@ -1612,12 +1507,10 @@ function initModuleVars( options = { csnFlavor: 'gensrc' } ) {
|
|
|
1612
1507
|
}
|
|
1613
1508
|
|
|
1614
1509
|
module.exports = {
|
|
1615
|
-
cloneCsnDictionary: (csn, options) => csnDictionary(csn, false, options),
|
|
1616
|
-
cloneAnnotationValue,
|
|
1617
1510
|
compactModel,
|
|
1618
1511
|
compactQuery,
|
|
1619
1512
|
compactExpr,
|
|
1620
|
-
sortCsn,
|
|
1621
1513
|
csnDictionaries,
|
|
1622
1514
|
csnDirectValues,
|
|
1515
|
+
csnPropertyOrder: propertyOrder,
|
|
1623
1516
|
};
|
|
@@ -103,6 +103,7 @@ Object.assign(GenericAntlrParser.prototype, {
|
|
|
103
103
|
fragileAlias,
|
|
104
104
|
identAst,
|
|
105
105
|
pushXprToken,
|
|
106
|
+
pushOpToken,
|
|
106
107
|
argsExpression,
|
|
107
108
|
valuePathAst,
|
|
108
109
|
fixNewKeywordPlacement,
|
|
@@ -360,6 +361,8 @@ function attachLocation( art ) {
|
|
|
360
361
|
art.location = this.tokenLocation(this._ctx.start, this._ctx.stop);
|
|
361
362
|
return art;
|
|
362
363
|
}
|
|
364
|
+
if (!this._ctx.stop)
|
|
365
|
+
return art;
|
|
363
366
|
|
|
364
367
|
// The last token (this._ctx.stop) may be a multi-line string literal, in which
|
|
365
368
|
// case we can't rely on `this._ctx.stop.line`.
|
|
@@ -639,14 +642,16 @@ function surroundByParens( expr, open, close, asQuery = false ) {
|
|
|
639
642
|
expr.$parens.push( location );
|
|
640
643
|
else
|
|
641
644
|
expr.$parens = [ location ];
|
|
645
|
+
if (expr.$opPrecedence)
|
|
646
|
+
expr.$opPrecedence = null;
|
|
642
647
|
return (asQuery) ? { query: expr, location } : expr;
|
|
643
648
|
}
|
|
644
649
|
|
|
645
650
|
|
|
646
|
-
function tokensToStringRepresentation(
|
|
651
|
+
function tokensToStringRepresentation( start, stop ) {
|
|
647
652
|
const tokens = this._input.getTokens(
|
|
648
|
-
|
|
649
|
-
|
|
653
|
+
start.tokenIndex,
|
|
654
|
+
stop.tokenIndex + 1, null
|
|
650
655
|
).filter(tok => tok.channel === antlr4.Token.DEFAULT_CHANNEL);
|
|
651
656
|
if (tokens.length === 0)
|
|
652
657
|
return '';
|
|
@@ -772,33 +777,6 @@ function identAst( token, category, noTokenTypeCheck = false ) {
|
|
|
772
777
|
return { id, $delimited: true, location: this.tokenLocation( token ) };
|
|
773
778
|
}
|
|
774
779
|
|
|
775
|
-
// only to be used in @after
|
|
776
|
-
// TODO: remove compatible stuff (A2J/checks use op: 'and'/'=')
|
|
777
|
-
function argsExpression( args, nary, location ) {
|
|
778
|
-
// console.log('AE:',args);
|
|
779
|
-
if (args.length === 1)
|
|
780
|
-
return args[0];
|
|
781
|
-
if (nary && args.length === 3 && args[1]?.val === nary) {
|
|
782
|
-
return this.attachLocation( {
|
|
783
|
-
op: { val: nary, location: args[1].location },
|
|
784
|
-
args: [ args[0], args[2] ],
|
|
785
|
-
location: undefined,
|
|
786
|
-
} );
|
|
787
|
-
}
|
|
788
|
-
// eslint-disable-next-line no-nested-ternary
|
|
789
|
-
const val = nary === '?:' ? nary
|
|
790
|
-
: (nary && nary !== '=' ? 'nary' : 'ixpr');
|
|
791
|
-
const op = {
|
|
792
|
-
val, // there is no n-ary in rule conditionTerm
|
|
793
|
-
location: this.startLocation(),
|
|
794
|
-
};
|
|
795
|
-
return this.attachLocation( {
|
|
796
|
-
op,
|
|
797
|
-
args,
|
|
798
|
-
location: location && { __proto__: CsnLocation.prototype, ...location },
|
|
799
|
-
} );
|
|
800
|
-
}
|
|
801
|
-
|
|
802
780
|
function pushXprToken( args ) {
|
|
803
781
|
const token = this._input.LT(-1);
|
|
804
782
|
args.push( {
|
|
@@ -910,11 +888,11 @@ function fixNewKeywordPlacement( args ) {
|
|
|
910
888
|
args.push(ixpr);
|
|
911
889
|
}
|
|
912
890
|
|
|
913
|
-
function expressionAsAnnotationValue( assignment, cond ) {
|
|
914
|
-
if (!cond
|
|
891
|
+
function expressionAsAnnotationValue( assignment, cond, start, stop ) {
|
|
892
|
+
if (!cond) // parse error
|
|
915
893
|
return;
|
|
916
|
-
Object.assign(assignment, cond
|
|
917
|
-
assignment.$tokenTexts = this.tokensToStringRepresentation(
|
|
894
|
+
Object.assign(assignment, cond);
|
|
895
|
+
assignment.$tokenTexts = this.tokensToStringRepresentation( start, stop );
|
|
918
896
|
}
|
|
919
897
|
|
|
920
898
|
// If a '-' is directly before an unsigned number, consider it part of the number;
|
|
@@ -1030,7 +1008,22 @@ function assignAnnotationValue( anno, value ) {
|
|
|
1030
1008
|
}
|
|
1031
1009
|
|
|
1032
1010
|
function relevantDigits( val ) {
|
|
1033
|
-
|
|
1011
|
+
val = val.replace( /e.+$/i, '' );
|
|
1012
|
+
|
|
1013
|
+
// To avoid the super-linear RegEx `0+$`, use the non-backtracking version and
|
|
1014
|
+
// simply check if we're at the end.
|
|
1015
|
+
const trailingZeroes = /0+/g;
|
|
1016
|
+
let re;
|
|
1017
|
+
while ((re = trailingZeroes.exec(val)) !== null) {
|
|
1018
|
+
if (trailingZeroes.lastIndex === val.length) {
|
|
1019
|
+
val = val.slice(0, re.index);
|
|
1020
|
+
break;
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
return val
|
|
1025
|
+
.replace( /\./, '' )
|
|
1026
|
+
.replace( /^[-+0]+/, '' );
|
|
1034
1027
|
}
|
|
1035
1028
|
|
|
1036
1029
|
// Create AST node for quoted literals like string and e.g. date'2017-02-22'.
|
|
@@ -1277,25 +1270,90 @@ function createSource() {
|
|
|
1277
1270
|
return new XsnSource();
|
|
1278
1271
|
}
|
|
1279
1272
|
|
|
1273
|
+
const operatorPrecedences = {
|
|
1274
|
+
// query:
|
|
1275
|
+
union: 1,
|
|
1276
|
+
except: 1,
|
|
1277
|
+
minus: 1,
|
|
1278
|
+
intersect: 2,
|
|
1279
|
+
};
|
|
1280
|
+
|
|
1280
1281
|
// Create AST node for binary operator `op` and arguments `args`
|
|
1281
|
-
function leftAssocBinaryOp(
|
|
1282
|
-
|
|
1282
|
+
function leftAssocBinaryOp( expr, right, opToken, eToken, extraProp ) {
|
|
1283
|
+
if (!right)
|
|
1284
|
+
return expr;
|
|
1285
|
+
const op = this.valueWithTokenLocation( opToken.text.toLowerCase(), opToken );
|
|
1283
1286
|
const extra = eToken
|
|
1284
1287
|
? this.valueWithTokenLocation( eToken.text.toLowerCase(), eToken )
|
|
1285
1288
|
: undefined;
|
|
1286
|
-
if (!
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1289
|
+
if (!expr.$parens && expr.op?.val === op.val && expr[extraProp]?.val === extra?.val) {
|
|
1290
|
+
expr.args.push( right );
|
|
1291
|
+
return expr;
|
|
1292
|
+
}
|
|
1293
|
+
const opPrec = operatorPrecedences[op.val] || 0;
|
|
1294
|
+
let left = expr;
|
|
1295
|
+
let args;
|
|
1296
|
+
while (opPrec > nodePrecedence( left )) {
|
|
1297
|
+
args = left.args;
|
|
1298
|
+
left = args[args.length - 1];
|
|
1299
|
+
}
|
|
1300
|
+
// TODO: location correct?
|
|
1301
|
+
const node = (extra) // eslint-disable-next-line
|
|
1302
|
+
? { op, [extraProp]: extra, args: [ left, right ], location: left.location }
|
|
1303
|
+
: { op, args: [ left, right ], location: left.location };
|
|
1304
|
+
if (!args)
|
|
1305
|
+
return node;
|
|
1306
|
+
args[args.length - 1] = node;
|
|
1307
|
+
return expr;
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
function nodePrecedence( node ) {
|
|
1311
|
+
const { op } = node;
|
|
1312
|
+
return op && !node.$parens && operatorPrecedences[op.val] || Infinity;
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
function pushOpToken( args, precedence ) { // for nary only; uses LT(-1) as operator token
|
|
1316
|
+
let node = null;
|
|
1317
|
+
let left = args;
|
|
1318
|
+
while (left?.$opPrecedence && left.$opPrecedence < precedence) {
|
|
1319
|
+
args = left;
|
|
1320
|
+
node = args[args.length - 1]; // last sub node of left side
|
|
1321
|
+
left = node.args;
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
if (left?.$opPrecedence === precedence ) { // nary
|
|
1325
|
+
args = left;
|
|
1326
|
+
}
|
|
1327
|
+
else if (node) {
|
|
1328
|
+
const sub = this.argsExpression( [ node, null ], true );
|
|
1329
|
+
args[args.length - 1] = sub;
|
|
1330
|
+
args = sub.args;
|
|
1331
|
+
args.length = 1;
|
|
1332
|
+
}
|
|
1333
|
+
else if (args.length > 1) { // new top-level op & op on left
|
|
1334
|
+
args[0] = this.argsExpression( [ ...args ], args.$opPrecedence != null ); // finish expresion
|
|
1335
|
+
args.length = 1;
|
|
1296
1336
|
}
|
|
1337
|
+
args.$opPrecedence = precedence;
|
|
1338
|
+
// TODO (if necessary): `location` for sub expessions, top-level is be properly set
|
|
1339
|
+
this.pushXprToken( args );
|
|
1340
|
+
return args;
|
|
1341
|
+
}
|
|
1297
1342
|
|
|
1298
|
-
|
|
1343
|
+
// only to be used in @after or via pushOpToken
|
|
1344
|
+
function argsExpression( args, nary ) {
|
|
1345
|
+
if (args.length === 1) // args.length === 0 is ok (for OVER…)
|
|
1346
|
+
return args[0];
|
|
1347
|
+
const $parens = args[0]?.$parens;
|
|
1348
|
+
const loc = ($parens) ? $parens[$parens.length - 1] : args[0]?.location;
|
|
1349
|
+
const location = loc ? { __proto__: CsnLocation.prototype, ...loc } : this.startLocation();
|
|
1350
|
+
// console.log('AE:',args);
|
|
1351
|
+
const op = {
|
|
1352
|
+
// eslint-disable-next-line no-nested-ternary
|
|
1353
|
+
val: nary === '?:' ? nary : nary ? 'nary' : 'ixpr',
|
|
1354
|
+
location,
|
|
1355
|
+
};
|
|
1356
|
+
return this.attachLocation( { op, args, location } );
|
|
1299
1357
|
}
|
|
1300
1358
|
|
|
1301
1359
|
const maxCardinalityKeywords = { 1: 'one', '*': 'many' };
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { csnPropertyOrder } = require('../json/to-csn');
|
|
4
|
+
const { ModelError } = require('../base/error');
|
|
5
|
+
const { setHidden } = require('../utils/objectUtils');
|
|
6
|
+
const { isAnnotationExpression } = require('../compiler/builtins');
|
|
7
|
+
|
|
8
|
+
const csnDictionaries = [
|
|
9
|
+
'args',
|
|
10
|
+
'params',
|
|
11
|
+
'enum',
|
|
12
|
+
'mixin',
|
|
13
|
+
'elements',
|
|
14
|
+
'actions',
|
|
15
|
+
'definitions',
|
|
16
|
+
'vocabularies',
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
function shallowCopy( val, _options, _sort ) {
|
|
20
|
+
return val;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const internalCsnProps = {
|
|
24
|
+
$sources: shallowCopy,
|
|
25
|
+
$location: shallowCopy,
|
|
26
|
+
$path: shallowCopy,
|
|
27
|
+
$paths: shallowCopy,
|
|
28
|
+
elements: cloneCsnDict,
|
|
29
|
+
$tableConstraints: shallowCopy,
|
|
30
|
+
};
|
|
31
|
+
const internalCsnPropertyNames = Object.keys(internalCsnProps);
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Deeply clone the given CSN model and return it.
|
|
36
|
+
* In testMode (or with testSortCsn), definitions are sorted.
|
|
37
|
+
*
|
|
38
|
+
* This function is CSN aware! Don't put annotation values into it, or
|
|
39
|
+
* keys such as "elements" will be interpreted according to CSN rules!
|
|
40
|
+
*
|
|
41
|
+
* @see cloneAnnotationValue()
|
|
42
|
+
* @see cloneCsnDict()
|
|
43
|
+
*
|
|
44
|
+
* @param {object} csn Top-level CSN. You can pass non-dictionary values.
|
|
45
|
+
* @param {CSN.Options} options CSN Options, only used for `dictionaryPrototype`, `testMode`, and `testSortCsn`.
|
|
46
|
+
* @param {boolean} sort Whether to sort CSN properties.
|
|
47
|
+
*/
|
|
48
|
+
function cloneCsn( csn, options, sort ) {
|
|
49
|
+
if (!csn || typeof csn !== 'object')
|
|
50
|
+
return csn;
|
|
51
|
+
if (Array.isArray(csn))
|
|
52
|
+
return csn.map( v => cloneCsn(v, options, sort) );
|
|
53
|
+
|
|
54
|
+
const keys = Object.keys(csn);
|
|
55
|
+
if (sort)
|
|
56
|
+
keys.sort( compareProperties );
|
|
57
|
+
|
|
58
|
+
const r = {};
|
|
59
|
+
for (const n of keys) {
|
|
60
|
+
const val = csn[n];
|
|
61
|
+
if (!val || typeof val !== 'object') {
|
|
62
|
+
r[n] = val;
|
|
63
|
+
}
|
|
64
|
+
else if (n.charAt(0) === '@') {
|
|
65
|
+
r[n] = cloneAnnotationValue(val, options, false); // TODO: pass 'sort'
|
|
66
|
+
}
|
|
67
|
+
else if (csnDictionaries.includes(n) && !Array.isArray(val)) {
|
|
68
|
+
const sortDict = n === 'definitions' &&
|
|
69
|
+
(!options || options.testMode || options.testSortCsn);
|
|
70
|
+
// Array check for property `args` which may either be a dictionary or an array.
|
|
71
|
+
r[n] = cloneCsnDict(val, options, sort, sortDict);
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
r[n] = cloneCsn(val, options, sort);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Note: internal properties with value `undefined` are _not_ cloned!
|
|
79
|
+
// The `hasNonEnumerable()` is required to work with cds.linked() CSN!
|
|
80
|
+
// It _must_ appear before csn[prop] or it may invoke getters!
|
|
81
|
+
internalCsnPropertyNames.forEach((prop) => {
|
|
82
|
+
if (r[prop] === undefined && hasNonEnumerable( csn, prop ) && csn[prop] !== undefined)
|
|
83
|
+
setHidden( r, prop, internalCsnProps[prop](csn[prop], options, sort) );
|
|
84
|
+
});
|
|
85
|
+
options?.hiddenPropertiesToClone?.forEach((prop) => {
|
|
86
|
+
if (r[prop] === undefined && hasNonEnumerable( csn, prop ) && csn[prop] !== undefined)
|
|
87
|
+
setHidden( r, prop, csn[prop] );
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
return r;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function hasNonEnumerable( object, property ) {
|
|
94
|
+
return Object.prototype.hasOwnProperty.call( object, property ) &&
|
|
95
|
+
!Object.prototype.propertyIsEnumerable.call( object, property );
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Deeply clone the given CSN dictionary and return it.
|
|
100
|
+
* This function does _not_ sort the given dictionary.
|
|
101
|
+
* See cloneCsnNonDict() if you want sorted definitions.
|
|
102
|
+
*
|
|
103
|
+
* This function is CSN aware! Don't put annotation values into it, or
|
|
104
|
+
* keys such as "elements" will be interpreted according to CSN rules!
|
|
105
|
+
*
|
|
106
|
+
* @see cloneAnnotationValue
|
|
107
|
+
* @see cloneCsnNonDict
|
|
108
|
+
*
|
|
109
|
+
* @param {object} csn
|
|
110
|
+
* @param {CSN.Options} options Only cloneOptions.dictionaryPrototype is
|
|
111
|
+
* used and cloneOptions are passed to sortCsn().
|
|
112
|
+
*/
|
|
113
|
+
function cloneCsnDict( csn, options, sortProps, sortDict ) {
|
|
114
|
+
const proto = options?.dictionaryPrototype;
|
|
115
|
+
const dictProto = (typeof proto === 'object') // including null
|
|
116
|
+
? proto
|
|
117
|
+
: null;
|
|
118
|
+
const r = Object.create( dictProto );
|
|
119
|
+
const keys = Object.keys(csn);
|
|
120
|
+
if (sortDict)
|
|
121
|
+
keys.sort();
|
|
122
|
+
for (const n of keys) {
|
|
123
|
+
// CSN does not allow any dictionary that are not objects.
|
|
124
|
+
// The compiler handles it, but a pre-transformed OData CSN won't trigger recompilation.
|
|
125
|
+
if (csn[n] && typeof csn[n] === 'object')
|
|
126
|
+
r[n] = cloneCsn(csn[n], options, sortProps);
|
|
127
|
+
else
|
|
128
|
+
throw new ModelError(`Found non-object dictionary entry: "${ n }" of type "${ typeof csn[n] }"`);
|
|
129
|
+
}
|
|
130
|
+
return r;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Clones the given annotation _value_. `value` must not be an object with annotations,
|
|
135
|
+
* but the annotation value itself, e.g. `[ { a: 1 } ]`, not `@anno: [...]`.
|
|
136
|
+
*
|
|
137
|
+
* @param {any} value
|
|
138
|
+
* @param {object} options
|
|
139
|
+
* @param {boolean} sort Whether to sort properties inside expressions.
|
|
140
|
+
* @returns {any}
|
|
141
|
+
*/
|
|
142
|
+
function cloneAnnotationValue( value, options, sort ) {
|
|
143
|
+
if (!value || typeof value !== 'object') // scalar
|
|
144
|
+
return value;
|
|
145
|
+
if (!Array.isArray(value) && isAnnotationExpression( value ))
|
|
146
|
+
return cloneCsn( value, options, sort );
|
|
147
|
+
return JSON.parse( JSON.stringify( value ) );
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Sorts the definition dictionary in tests mode.
|
|
152
|
+
*
|
|
153
|
+
* @param {CSN.Model} csn
|
|
154
|
+
* @param {CSN.Options} options
|
|
155
|
+
* @returns The input csn model.
|
|
156
|
+
*/
|
|
157
|
+
function sortCsnDefinitionsForTests( csn, options ) {
|
|
158
|
+
if (!options.testMode && !options.testSortCsn)
|
|
159
|
+
return csn;
|
|
160
|
+
const sorted = Object.create(null);
|
|
161
|
+
Object.keys(csn.definitions || {}).sort().forEach((name) => {
|
|
162
|
+
sorted[name] = csn.definitions[name];
|
|
163
|
+
});
|
|
164
|
+
csn.definitions = sorted;
|
|
165
|
+
return csn;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function sortCsnForTests( csn, options ) {
|
|
169
|
+
if (options.testMode)
|
|
170
|
+
return cloneCsn(csn, options, true);
|
|
171
|
+
if (options.testSortCsn)
|
|
172
|
+
return sortCsnDefinitionsForTests( csn, options );
|
|
173
|
+
return csn;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Difference to to-csn.js: Annotations are always sorted
|
|
177
|
+
function compareProperties( a, b ) {
|
|
178
|
+
if (a === b)
|
|
179
|
+
return 0;
|
|
180
|
+
const oa = csnPropertyOrder[a] || csnPropertyOrder[a.charAt(0)] || 9999;
|
|
181
|
+
const ob = csnPropertyOrder[b] || csnPropertyOrder[b.charAt(0)] || 9999;
|
|
182
|
+
return oa - ob || (a < b ? -1 : 1);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
module.exports = {
|
|
186
|
+
cloneCsnDict(csn, options) {
|
|
187
|
+
return cloneCsnDict(csn, options, false, false);
|
|
188
|
+
},
|
|
189
|
+
cloneCsnNonDict(csn, options) {
|
|
190
|
+
return cloneCsn(csn, options, false);
|
|
191
|
+
},
|
|
192
|
+
cloneFullCsn(csn, options) {
|
|
193
|
+
return cloneCsn(csn, options, false);
|
|
194
|
+
},
|
|
195
|
+
cloneAnnotationValue(csn) {
|
|
196
|
+
return cloneAnnotationValue(csn, {}, false);
|
|
197
|
+
},
|
|
198
|
+
sortCsn(csn, options) {
|
|
199
|
+
return cloneCsn(csn, options, true);
|
|
200
|
+
},
|
|
201
|
+
sortCsnDefinitionsForTests,
|
|
202
|
+
sortCsnForTests,
|
|
203
|
+
};
|
package/lib/model/csnRefs.js
CHANGED
|
@@ -171,6 +171,8 @@
|
|
|
171
171
|
// hierarchy, query number, table aliases and links from a column to its
|
|
172
172
|
// respective inferred element.
|
|
173
173
|
|
|
174
|
+
// TODO: some `name` property would be useful (also set with `initDefinition`)
|
|
175
|
+
|
|
174
176
|
// Properties in cache:
|
|
175
177
|
//
|
|
176
178
|
// - _effectiveType on def/member/items: cached result of effectiveType()
|
|
@@ -896,9 +898,14 @@ function queryOrMain( query, main ) {
|
|
|
896
898
|
/**
|
|
897
899
|
* Traverse query in pre-order
|
|
898
900
|
*
|
|
901
|
+
* The callback is called on the following XSN nodes inside the query `query`:
|
|
902
|
+
* - a query node, which has property `SET` or `SELECT` (or `projection`),
|
|
903
|
+
* - a query source node inside `from` if it has property `ref`,
|
|
904
|
+
* - NOT on a `join` node inside `from`.
|
|
905
|
+
*
|
|
899
906
|
* @param {CSN.Query} query
|
|
900
|
-
* @param {CSN.QuerySelect} fromSelect
|
|
901
|
-
* @param {CSN.Query} parentQuery
|
|
907
|
+
* @param {CSN.QuerySelect} fromSelect: for query in `from`
|
|
908
|
+
* @param {CSN.Query} parentQuery: for a sub query (ex those in `from`)
|
|
902
909
|
* @param {(query: CSN.Query&CSN.QueryFrom, select: CSN.QuerySelectEnriched, parentQuery: CSN.Query) => void} callback
|
|
903
910
|
*/
|
|
904
911
|
function traverseQuery( query, fromSelect, parentQuery, callback ) {
|
|
@@ -1006,7 +1013,8 @@ function pathId( item ) {
|
|
|
1006
1013
|
}
|
|
1007
1014
|
|
|
1008
1015
|
function implicitAs( ref ) {
|
|
1009
|
-
const
|
|
1016
|
+
const item = ref[ref.length - 1];
|
|
1017
|
+
const id = (typeof item === 'string') ? item : item.id; // inlined `pathId`
|
|
1010
1018
|
return id.substring( id.lastIndexOf('.') + 1 );
|
|
1011
1019
|
}
|
|
1012
1020
|
|