@sap/cds-compiler
Version:
CDS (Core Data Services) compiler and backends
1,244 lines (1,155 loc) • 44.3 kB
JavaScript
;
const edmUtils = require('../edmUtils.js');
const {
EdmTypeFacetMap,
EdmTypeFacetNames,
EdmPrimitiveTypeMap,
} = require('../EdmPrimitiveTypeDefinitions.js');
const { isBuiltinType, isAnnotationExpression } = require('../../base/builtins');
const { transformExpression } = require('../../transform/db/applyTransformations.js');
const { conditionAsTree, expressionAsTree } = require('../../model/xprAsTree');
/**
* Translate a given token stream expression into an edmJson representation
*
* Returns $edmJson AST or val if val had no expression
* @param {object} carrier
* @param {object} anno
* @param {object} location
* @param {object} messageFunctions
* @returns {object}
*/
function xpr2edmJson( carrier, anno, location, options, messageFunctions, genericTranslationHelpers ) {
const { message, error } = messageFunctions;
const annoVal = carrier[anno];
if (!annoVal)
return annoVal;
const canonicFunctionDefinitions = {
'odata.fillUriTemplate': { min: 2 },
'odata.uriEncode': { exact: 1 },
// OData-ABNF
// string
'odata.concat': { min: 2 },
'odata.contains': { exact: 2 },
'odata.endswith': { exact: 2 },
'odata.indexof': { exact: 2 },
'odata.length': { exact: 1 },
'odata.matchesPattern': { exact: 2 },
'odata.startswith': { exact: 2 },
'odata.substring': { min: 2, max: 3 },
'odata.tolower': { exact: 1 },
'odata.toupper': { exact: 1 },
'odata.trim': { exact: 1 },
// collection
'odata.hassubset': { exact: 2 },
'odata.hassubsequence': { exact: 2 },
// date & time
'odata.year': { exact: 1 },
'odata.month': { exact: 1 },
'odata.day': { exact: 1 },
'odata.hour': { exact: 1 },
'odata.minute': { exact: 1 },
'odata.second': { exact: 1 },
'odata.fractionalseconds': { exact: 1 },
'odata.totalseconds': { exact: 1 },
'odata.date': { exact: 1 },
'odata.time': { exact: 1 },
'odata.totaloffsetminutes': { exact: 1 },
'odata.mindatetime': { exact: 0 },
'odata.maxdatetime': { exact: 0 },
'odata.now': { exact: 0 },
// arithmetic
'odata.round': { exact: 1 },
'odata.floor': { exact: 1 },
'odata.ceiling': { exact: 1 },
// geo
'odata.geo.distance': { exact: 2 },
'odata.geo.length': { exact: 1 },
'odata.geo.intersects': { exact: 2 },
// deprecated
'odata.cast': { use: 'cast(…)' },
'odata.isof': { use: 'IsOf(…)' },
'odata.case': { use: '?:' },
};
//----------------------------------
// Error transformer
const notADynExpr = (parent, op, xpr, csnPath, parentParent, parentProp, ctx) => {
error('odata-anno-xpr', location, {
anno, op: ctx.txt ?? op, '#': 'notadynexpr',
});
delete parent[op];
};
const noOp = () => {};
//----------------------------------
// Create the transformer dictionary
const transform = {
args: noOp,
param: noOp,
literal: noOp,
//----------------------------------
// operators not supported as dynamic expression
'.': notADynExpr,
exists: notADynExpr,
SELECT: notADynExpr,
SET: (p, o, xpr, csnPath, parentParent, parentProp, ctx) => notADynExpr(p, o, null, null, null, null, Object.assign(ctx, { txt: 'UNION' })),
like: notADynExpr,
new: notADynExpr,
};
//----------------------------------
// list is a $Collection => []
transform.list = (parent, prop, xpr, csnPath, parentParent, parentProp, ctx) => {
parentParent[parentProp] = xpr.filter(a => a);
transformExpression(parentParent, parentProp, transform, csnPath, ctx);
};
// XPR
transform.xpr = (parent, prop, xpr, csnPath, parentParent, parentProp, ctx) => {
// eliminate 'xpr' node by pulling up xpr node to its parent
parentParent[parentProp] = xpr;
transformExpression(parentParent, parentProp, transform, csnPath, ctx);
};
//----------------------------------
// CASE
transform.case = (parent, prop, caseExpr, csnPath, parentParent, parentProp, ctx) => {
// transform simple case expression into search case expression
// case <expr1> when <expr2> ... ===> case when <expr1> = <expr2> ...
let i = 0;
// single leg is not an array
if (!Array.isArray(caseExpr))
caseExpr = [ caseExpr ];
if (!caseExpr[i].when) {
caseExpr.filter(elt => elt.when).forEach((when) => {
when.when[0] = { '=': [ caseExpr[i], when.when[0] ] };
});
i++;
}
const edmIf = { $If: [ caseExpr[i].when[0], caseExpr[i].when[1] ] };
let curIf = edmIf;
for (i++; i < caseExpr.length && caseExpr[i].when; i++) {
const newEdmIf = { $If: [ caseExpr[i].when[0], caseExpr[i].when[1] ] };
curIf.$If.push(newEdmIf);
curIf = newEdmIf;
}
// else
if (i < caseExpr.length)
curIf.$If.push(caseExpr[i]);
parent.$If = edmIf.$If;
delete parent.case;
transformExpression(parent, undefined, transform, csnPath, ctx);
};
transform.$If = (_parent, _prop, expr, csnPath, parentParent, parentProp, ctx) => {
transformExpression(expr, undefined, transform, csnPath, ctx);
};
//----------------------------------
// Cast => $Cast
transform.cast = (parent, prop, castExpr, csnPath, parentParent, parentProp, ctx) => {
const csnType = castExpr[0];
// try to resolve to final scalar base type and use that instead of derived type
if (!isBuiltinType(csnType.type)) {
const finalType = options.getFinalTypeInfo(csnType.type);
if (finalType?.type && isBuiltinType(finalType.type)) {
csnType.type = finalType.type;
[ 'length', 'precision', 'scale', 'unicode', 'srid' ].forEach((facet) => {
csnType[facet] ??= finalType[facet];
});
}
}
const edmTypeName = edmUtils.mapCdsToEdmType(csnType, messageFunctions, options, false, location);
const typeFunc = { func: 'Type', args: [ { val: edmTypeName } ] };
const castFunc = { func: '$Cast', args: [ typeFunc, castExpr[1] ] };
if (csnType.length != null)
typeFunc.args.push( { func: 'MaxLength', args: [ { val: csnType.length } ] });
if (csnType.srid != null)
typeFunc.args.push( { func: 'SRID', args: [ { val: csnType.srid } ] });
if (csnType.unicode != null)
typeFunc.args.push( { func: 'Unicode', args: [ { val: csnType.unicode } ] });
if (csnType.precision != null)
typeFunc.args.push( { func: 'Precision', args: [ { val: csnType.precision } ] });
else if (csnType.type === 'cds.Timestamp' && edmTypeName === 'Edm.DateTimeOffset')
typeFunc.args.push( { func: 'Precision', args: [ { val: 7 } ] });
if (edmTypeName === 'Edm.Decimal' && csnType.precision == null && csnType.scale == null || csnType.scale === 'floating')
typeFunc.args.push( { func: 'Scale', args: [ { val: 'variable' } ] });
else if (csnType.scale != null)
typeFunc.args.push( { func: 'Scale', args: [ { val: csnType.scale } ] });
parentParent[parentProp] = castFunc;
transformExpression(parentParent, parentProp, transform, csnPath, ctx);
};
//----------------------------------
const evalArgs = (argDef, args, propName) => {
if (Array.isArray(args)) {
args = args.filter(a => a);
if (argDef.min != null && (!args || argDef.min > args.length)) {
error('odata-anno-xpr-args', location, {
anno, op: `${ propName }(…)`, count: argDef.min, '#': 'atleast',
});
}
if (argDef.max != null && (!args || argDef.max < args.length)) {
error('odata-anno-xpr-args', location, {
anno, op: `${ propName }(…)`, count: argDef.max, '#': 'atmost',
});
}
if (argDef.exact != null && (!args || argDef.exact !== args.length)) {
if (argDef.exact === 0) {
error('odata-anno-xpr-args', location, {
anno, op: `${ propName }(…)`,
});
}
else {
error('odata-anno-xpr-args', location, {
anno, op: `${ propName }(…)`, count: argDef.exact, '#': 'exactly',
});
}
}
}
};
// Binary Operator Macro
const op = (opStr, exact = 2) => (parent, prop, xpr, csnPath, parentParent, parentProp, ctx) => {
evalArgs({ exact }, xpr, prop);
parent[opStr] = xpr;
delete parent[prop];
transformExpression(parent, undefined, transform, csnPath, ctx);
};
//----------------------------------
// LOGICAL
transform.and = op('$And');
transform.$And = noOp;
transform.or = op('$Or');
transform.$Or = noOp;
transform.not = op('$Not', 1);
transform.$Not = noOp;
//----------------------------------
// RELATIONAL
transform['='] = op('$Eq');
transform['=='] = op('$Eq');
transform.$Eq = noOp;
transform['<>'] = op('$Ne');
transform['!='] = op('$Ne');
transform.$Ne = noOp;
transform['>'] = op('$Gt');
transform.$Gt = noOp;
transform['>='] = op('$Ge');
transform.$Ge = noOp;
transform['<'] = op('$Lt');
transform.$Lt = noOp;
transform['<='] = op('$Le');
transform.$Le = noOp;
transform.in = (parent, prop, xpr, csnPath, parentParent, parentProp, ctx) => {
let args = xpr[1].list;
if (!args) {
if (Array.isArray(xpr[1].xpr))
args = xpr[1].xpr;
else
args = [ xpr[1] ];
}
evalArgs({ min: 1 }, args, prop);
parent.$In = [ xpr[0], args ];
delete parent[prop];
transformExpression(parent, undefined, transform, csnPath, ctx);
};
transform.$In = noOp;
transform.between = (parent, prop, xpr, csnPath, parentParent, parentProp, ctx) => {
evalArgs({ exact: 2 }, xpr.slice(1), prop);
transformExpression(xpr, undefined, transform, csnPath, ctx);
delete parent[prop];
parentParent[parentProp]
= {
$And: [
{ $Le: [ xpr[1], xpr[0] ] },
{ $Le: [ xpr[0], xpr[2] ] },
],
};
};
transform['||'] = (parent, prop, xpr, csnPath, parentParent, parentProp, ctx) => {
evalArgs({ exact: 2 }, xpr, prop);
transformExpression(xpr, undefined, transform, csnPath, ctx);
delete parent[prop];
parentParent[parentProp].$Apply = xpr;
parentParent[parentProp].$Function = 'odata.concat';
};
//----------------------------------
// ARITHMETICAL AND UNARY
transform['+'] = (parent, prop, xpr, csnPath, parentParent, parentProp, ctx) => {
if (Array.isArray(xpr)) {
op('$Add')(parent, prop, xpr);
}
else {
delete parent[prop];
parentParent[parentProp] = xpr;
transformExpression(parentParent, parentProp, transform, csnPath, ctx);
}
};
transform.$Add = noOp;
transform['-'] = (parent, prop, xpr, csnPath, parentParent, parentProp, ctx) => {
op(Array.isArray(xpr) ? '$Sub' : '$Neg')(parent, prop, xpr, csnPath, parentParent, parentProp, ctx);
};
transform.$Sub = noOp;
transform.$Neg = noOp;
transform['*'] = op('$Mul');
transform.$Mul = noOp;
transform['/'] = op('$DivBy');
transform.$DivBy = noOp;
transform.isNull = op('$Eq');
transform.isNotNull = op('$Ne');
// $Div, $Mod are functions
//----------------------------------
// LITERALS
transform.val = (parent, prop, xpr, csnPath, parentParent, parentProp) => {
if (xpr === null)
parent.$Null = true;
else
parentParent[parentProp] = xpr;
};
transform['#'] = (parent, prop, xpr, csnPath, parentParent, parentParentName, ctx) => {
// enum reference that was resolved by the compiler
if (!(parent['#'] && parent.val))
parent.EnumMember = getEnumMemberValue(xpr, ctx);
delete parent['#'];
};
//----------------------------------
// Enum type resolving helper functions
function getEnumMemberValue( enumMember, ctx ) {
// inner annotations play a role here?
const annoNameParts = anno.slice(1).split('.');
// The term of an annotation is the part between the first '@' and the second '.'
const termName = `${ annoNameParts[0] }.${ annoNameParts[1] }`;
const term = genericTranslationHelpers.getDictTerm(termName, options);
// get term's type
let typeName = term?.Type;
let type = genericTranslationHelpers.getDictType(typeName);
({ type, typeName } = resolveNestedTypePath(annoNameParts.slice(2), typeName, type));
if (type?.$kind === 'EnumType')
return valiteAndGetFullEnumMemberNameFromEnumType(enumMember, type, typeName);
if (type?.$kind === 'ComplexType') {
// If we end up with a ComplexType here, the enum member is defined in a property of the complex type, which is
// a collection. Only in that case the flattening of annotation stops in the core compiler.
// Here we need to continue resolving the substructure inside a collection item.
const indexOfAnnoInCsnPath = ctx.xprCsnPath.indexOf(anno);
// we take the part of the csnPath after the annotation and remove the numeric values, as these are the array indexes
const subStructAnnoNames = ctx.xprCsnPath.slice(indexOfAnnoInCsnPath + 1).filter(e => typeof e === 'string');
({ type, typeName } = resolveNestedTypePath(subStructAnnoNames, typeName, type));
if (type?.$kind === 'EnumType')
return valiteAndGetFullEnumMemberNameFromEnumType(enumMember, type, typeName);
}
if (type && type.$kind !== 'EnumType') {
// resolved to not an EnumType -> warn about it and return what the input value
message('odata-anno-value', location, {
anno, value: `"#${ enumMember }"`, type: typeName, '#': 'std',
});
}
return enumMember;
}
function resolveNestedTypePath( annoNameParts, typeName, type ) {
for (let i = 0; i < annoNameParts.length; i++) {
if (type?.$kind === 'ComplexType') {
const properties = type.Properties;
const propName = Object.keys(properties).find(p => p === annoNameParts[i]);
if (propName) {
if (properties[propName].startsWith('Collection(')) {
// strip the 'Collection(...)' wrapper
typeName = properties[propName].slice(11, -1);
type = genericTranslationHelpers.getDictType(typeName);
}
else {
type = genericTranslationHelpers.getDictType(properties[propName]);
typeName = properties[propName];
}
}
}
}
return { type, typeName };
}
function valiteAndGetFullEnumMemberNameFromEnumType( member, enumType, typeName ) {
const members = enumType?.Members;
if (members && !members.includes(member)) {
message('odata-anno-value', location, {
anno, type: typeName, value: `"#${ member }"`, rawvalues: members.map(m => `#${ m }`), '#': 'enum',
});
return member;
}
return `${ typeName }/${ member }`;
}
//----------------------------------
transform.ref = (parent, prop, xpr, csnPath, parentParent, parentProp) => {
// until empty filter syntax is introduced for the annotation expressions,
// we ignore the filters in order to generate EDMX
if (xpr.some(ps => ps.args/* || ps.where */)) {
error('odata-anno-xpr-ref', location, {
anno, elemref: parent, '#': 'args',
});
}
const [ head, ...tail ] = xpr;
if ((head.id || head) === '$self')
xpr = tail;
parentParent[parentProp] = { $Path: xpr.map(ps => ps.id || ps).join('/') };
};
//----------------------------------
// Functions
transform.func = (parent, prop, xpr, csnPath, parentParent, parentProp, ctx) => {
const rewriteArgs = (argDefs, evalVal = true) => {
Object.entries(argDefs).forEach(([ argName, argDef ]) => {
const [ foundProps, newArgs ]
= parent.args
? parent.args.reduce((acc, arg) => {
((arg.func === argName) ? acc[0] : acc[1]).push(arg);
return acc;
}, [ [], [] ] )
: [ [], [] ];
parent.args = newArgs;
if (foundProps.length !== 1) {
error('odata-anno-xpr-args', location, {
anno, op: `${ xpr }(…)`, prop: `${ argName }(…)`, '#': 'wrongcount',
});
}
else {
const func = foundProps[0];
evalArgs(argDef, func.args, argName);
if (func.args.length) {
// set prop (eventually undefined)
parent[argName] = /* func.args[0].ref?.join('.') || */ func.args[0].val;
if (evalVal && !parent[argName]) {
error('odata-anno-xpr-args', location, {
anno, op: `${ argName }(…)`, meta: argDef.meta || 'literal', '#': 'wrongval_meta',
});
}
}
}
});
};
const rewriteType = () => {
let rc = true;
const isDollarFunc = xpr[0] === '$';
// Map Edm primitive type funcs to $Type funcs
let [ foundTypeProps, newArgs ]
= parent.args
? parent.args.reduce((acc, arg) => {
(arg.func === '$Collection' || arg.func === 'Collection' ? acc[0] : acc[1]).push(arg);
return acc;
}, [ [], [] ] )
: [ [], [] ];
if (foundTypeProps.length === 1) {
const type = foundTypeProps[0];
evalArgs({ exact: 1 }, type.args, type.func);
if (type.args?.length === 1) {
const typeName = type.args[0].func;
if (EdmPrimitiveTypeMap[`Edm.${ type.args[0].func }`])
newArgs.push({ func: typeName, args: type.args[0].args || [] });
else
newArgs.push(type);
parent.$Collection = true;
}
parent.args = newArgs;
}
let typePropName = isDollarFunc ? '$Type' : 'Type';
[ foundTypeProps, newArgs ]
= parent.args
? parent.args.reduce((acc, arg) => {
(EdmPrimitiveTypeMap[`Edm.${ arg.func }`] ? acc[0] : acc[1]).push(arg);
return acc;
}, [ [], [] ] )
: [ [], [] ];
if (foundTypeProps.length) {
foundTypeProps.forEach((type) => {
const edmType = `Edm.${ type.func }`;
const td = EdmPrimitiveTypeMap[edmType];
const typeFuncDef = {
func: typePropName,
args: [ { val: edmType } ],
};
evalArgs(td, type.args, type.func);
if (type.args.length) {
let i = 0;
EdmTypeFacetNames.forEach((facetName) => {
const facetDef = td[facetName];
if (facetDef && i < type.args.length) {
const facetFuncDef = {
func: `${ facetName }`,
args: [ type.args[i++] ],
};
typeFuncDef.args.push(facetFuncDef);
}
});
}
newArgs.push(typeFuncDef);
});
parent.args = newArgs;
}
[ foundTypeProps, newArgs ]
= parent.args
? parent.args.reduce((acc, arg) => {
((arg.func === '$Type' || arg.func === 'Type') ? acc[0] : acc[1]).push(arg);
return acc;
}, [ [], [] ] )
: [ [], [] ];
parent.args = newArgs;
if (foundTypeProps.length !== 1) {
typePropName = isDollarFunc ? '$Type' : 'Type';
error('odata-anno-xpr-type', location, { anno, op: `${ xpr }(…)` });
rc = false;
}
else {
let typeArg;
const typeProp = foundTypeProps[0];
typePropName = typeProp.func;
const [ collTypes, newTypeArgs ]
= typeProp.args
? typeProp.args.reduce((acc, arg) => {
((arg.func === '$Collection' || arg.func === 'Collection') ? acc[0] : acc[1]).push(arg);
return acc;
}, [ [], [] ] )
: [ [], [] ];
typeProp.args = newTypeArgs;
const [ scalarTypes, typeFacets ]
= typeProp.args.reduce((acc, arg) => {
((/* arg.ref || */ arg.val) ? acc[0] : acc[1]).push(arg);
return acc;
}, [ [], [] ] );
let typeOpStr = collTypes.length
? `${ typePropName }(${ isDollarFunc ? '$Collection' : 'Collection' }(…))`
: `${ typePropName }(…)`;
if (collTypes.length) {
if (collTypes.length > 1 || scalarTypes.length) {
error('odata-anno-xpr-type', location, { anno, op: `${ xpr }(…)` });
}
else {
typeOpStr = `${ typePropName }(${ collTypes[0].func }(…))`;
if (collTypes[0].args.length !== 1) {
error('odata-anno-xpr-type', location, { anno, op: `${ xpr }(…)` });
}
else {
typeArg = collTypes[0].args[0];
parent.$Collection = true;
}
}
}
else if (scalarTypes.length !== 1) {
error('odata-anno-xpr-type', location, { anno, op: `${ xpr }(…)` });
rc = false;
}
else {
typeArg = scalarTypes[0];
}
if (typeArg && rc) {
// do final type checks and assignment
const typeDef = typeArg?.ref?.join('.') || typeArg?.val;
if (typeof typeDef !== 'string')
error('odata-anno-xpr-type', location, { anno, op: `${ xpr }(…)` });
else
parent.$Type = typeDef;
const td = EdmPrimitiveTypeMap[typeDef];
if (td) {
if (td.v2 !== options.isV2() && td.v4 !== options.isV4()) {
message('odata-unexpected-edm-type', location,
{
anno,
type: typeDef,
version: (options.isV4() ? '4.0' : '2.0'),
'#': 'anno',
});
}
evalArgs(td, typeFacets, typeDef);
EdmTypeFacetNames.forEach((facetName) => {
const facetDef = EdmTypeFacetMap[facetName];
const optional = (facetDef.optional !== undefined) &&
(Array.isArray(facetDef.optional)
? facetDef.optional.includes(typeDef)
: facetDef.optional);
if (td[facetName]) {
// ignore facets that are not type relevant
const facetFuncName = isDollarFunc ? `$${ facetName }` : facetName;
const facetArgs = typeFacets.filter(arg => arg.func === facetName || arg.func === `$${ facetName }`);
if (facetArgs.length === 0 && !optional && (options.isV2() === facetDef.v2 || options.isV4() === facetDef.v4)) {
message('odata-unexpected-edm-facet', location,
{
anno,
type: typeDef,
name: facetName,
version: (options.isV4() ? '4.0' : '2.0'),
'#': 'anno',
});
}
else if (facetArgs.length > 1) {
error('odata-anno-xpr-args', location, {
anno, op: typeOpStr, prop: `${ facetFuncName }(…)`, '#': 'wrongcount',
});
}
else if (facetArgs.length === 1) {
const facetArg = facetArgs[0];
if (facetArg.args.length !== 1) {
error('odata-anno-xpr-args', location, {
anno, op: `${ facetFuncName }(…)`, count: 1, '#': 'exactly',
});
}
else {
const facetVal = facetArg.args[0].val;
const isNan = Number.isNaN(Number.parseInt(facetVal, 10));
if (isNan && options.isV4() && facetName === 'Scale' && facetVal !== 'variable') {
error('odata-anno-xpr-args', location, {
anno,
op: `${ facetFuncName }(…)`,
meta: 'number',
rawvalues: [ 'variable' ],
'#': 'wrongval_meta_list',
});
}
else if (isNan && facetName !== 'Scale') {
error('odata-anno-xpr-args', location, {
anno, op: `${ facetFuncName }(…)`, meta: 'number', '#': 'wrongval_meta',
});
}
else {
parent[`$${ facetName }`] = facetVal;
}
}
}
}
});
if (typeDef === 'Edm.Decimal') {
if (parent.$Precision && parent.$Scale) {
const precision = Number.parseInt(parent.$Precision, 10);
const scale = Number.parseInt(parent.$Scale, 10);
if (!Number.isNaN(precision) && !Number.isNaN(scale) && scale > precision) {
message('odata-invalid-scale', location, {
'#': 'anno',
anno,
number: scale,
rawvalue: precision,
});
}
}
if (options.isV2() && parent.$Scale === 'variable') {
parent['@sap.variable.scale'] = true;
delete parent.$Scale;
}
}
}
else {
// Error out for arbitrary types until we know better
// probably todo: Check for reachability of arb type names such as namespace
// reqDef entry etc...
if (typeDef) { // eslint-disable-line no-lonely-if
error('odata-anno-xpr-type', location, {
anno, op: `${ xpr }(…)`, type: `${ typeDef }`, '#': 'edm',
});
}
/*
typeFacets.forEach((facet) => {
if (facet.args.length === 1 && facet.args[0].val) {
const facetName = facet.func.startsWith('$') ? facet.func.slice(1) : facet.func;
if (EdmTypeFacetMap[facetName])
parent[`$${facetName}`] = facet.args[0].val;
}
});
*/
}
delete typeProp.args;
}
}
return rc;
};
const standard = (tgt = parent, x = xpr) => {
tgt[x] = parent.args?.filter(a => a);
delete parent.func;
delete parent.args;
};
const exactArgs = (tgt = parent, x = xpr, count = undefined) => {
standard(tgt, x);
evalArgs({ exact: count }, tgt[x], xpr);
};
const oneArg = (tgt = parent, x = xpr) => {
exactArgs(tgt, x, 1);
};
const twoArgs = (tgt = parent, x = xpr) => {
exactArgs(tgt, x, 2);
};
const dollar = () => {
parent[`$${ xpr }`] = parent[xpr];
delete parent[xpr];
};
const apply = (argDefs, propName) => {
rewriteArgs(argDefs); // $Function
standard();
let funcName = parent[propName];
if (funcName) {
if (!funcName.startsWith('odata.'))
funcName = `odata.${ funcName }`;
const argDef = canonicFunctionDefinitions[funcName];
if (argDef) {
if (argDef.use) {
error('odata-anno-xpr', location, {
anno, op: parent[propName], code: argDef.use, '#': 'use',
});
}
else {
evalArgs(argDef, parent[xpr], xpr);
}
}
else {
funcName = parent[propName];
if (funcName.split('.').length !== 2) {
error('odata-anno-xpr', location, {
anno, op: `${ propName }(…)`, code: funcName, meta: 'namespace', othermeta: 'function', '#': 'canonfuncalias',
});
}
}
}
};
// these are the function transformers
const funcDefs = {
$Has: twoArgs,
Has: [ twoArgs, dollar ],
$Div: twoArgs,
Div: [ twoArgs, dollar ],
$Mod: twoArgs,
Mod: [ twoArgs, dollar ],
$Apply: () => {
apply( { $Function: { exact: 1 } }, '$Function');
},
Apply: () => {
apply({ Function: { exact: 1 } }, 'Function');
dollar();
},
$Cast: () => {
if (rewriteType())
oneArg();
else
standard();
},
$IsOf: () => {
if (rewriteType())
oneArg();
else
standard();
},
IsOf: () => {
if (rewriteType())
oneArg();
else
standard();
dollar();
},
$LabeledElement: () => {
rewriteArgs({ $Name: { exact: 1, meta: 'qualified name' } });
oneArg();
},
LabeledElement: () => {
rewriteArgs({ Name: { exact: 1, meta: 'qualified name' } });
parent.$Name = parent.Name; // Make it an attribute or rendering fails.
delete parent.Name;
oneArg();
dollar();
},
$LabeledElementReference: () => {
oneArg();
if (parent[xpr].length === 1 && typeof parent[xpr][0].val !== 'string') {
error('odata-anno-xpr-args', location, {
anno, op: `${ xpr }(…)`, meta: 'literal', '#': 'wrongval_meta',
});
}
},
$UrlRef: oneArg,
UrlRef: [ oneArg, dollar ],
// $Record ???
$Collection: () => {
standard(parentParent, parentProp);
transformExpression(parentParent, parentProp, transform, csnPath, ctx);
},
$Path: () => {
oneArg(parent, xpr);
const args = parent[xpr];
if (args?.length && typeof args[0].val !== 'string') {
error('odata-anno-xpr-args', location, {
anno, op: `${ xpr }(…)`, meta: 'string', '#': 'wrongval_meta',
});
}
transformExpression(parentParent, parentProp, transform, csnPath, ctx);
},
$Null: () => {
parent[xpr] = true;
delete parent.func;
if (parent.args?.length)
error('odata-anno-xpr-args', location, { anno, op: `${ xpr }(…)` });
delete parent.args;
},
};
funcDefs.LabeledElementReference = [ funcDefs.$LabeledElementReference, dollar ];
funcDefs.Collection = funcDefs.$Collection;
funcDefs.Path = funcDefs.$Path;
const funcDef = funcDefs[xpr];
if (funcDef) {
if (Array.isArray(funcDef))
funcDef.forEach(f => f());
else
funcDef();
transformExpression(parent, undefined, transform, csnPath, ctx);
}
else {
const funcName = xpr.startsWith('odata.') ? xpr : `odata.${ xpr }`;
const argDef = canonicFunctionDefinitions[funcName];
if (argDef) {
if (argDef.use) {
error('odata-anno-xpr', location, {
anno, op: `${ xpr }(…)`, code: argDef.use, '#': 'use',
});
}
else {
evalArgs(argDef, parent.args, xpr);
parentParent[parentProp].$Apply = [ ...(parent.args || []) ];
parentParent[parentProp].$Function = funcName;
delete parentParent[parentProp].func;
delete parentParent[parentProp].args;
transformExpression(parentParent, parentProp, transform, csnPath, ctx);
}
}
else {
error('odata-anno-xpr', location, {
anno, op: `${ xpr }(…)`, '#': 'notadynexpr',
});
}
}
delete parent[prop];
};
return transformExpression(carrier, anno, {
'=': (parent, prop, xpr, csnPath, parentParent, parentProp) => {
if (isAnnotationExpression(parent)) {
delete parent['='];
const edmJson = preTransformXpr(parent);
// csnPath to this certain expression is passed inside of the 'ctx' variable, in case it is needed for resolving types from the
// dictionary, for instance in enum symbols resolving, see transform['#']
parentParent[parentProp] = transformExpression({ $edmJson: edmJson }, undefined, transform, csnPath, { xprCsnPath: csnPath });
}
},
}, location);
/**
* Pre-transform the CSN expression into a tree structure for further EDM processing.
* Example:
* `[…, '+' …]` -> `{'+': [ …, … ]}`
*
* @param {object|Array} xpr
*/
function preTransformXpr( xpr ) {
xpr = Array.isArray(xpr) ? conditionAsTree(xpr) : expressionAsTree(xpr);
xpr = preTransformExpression(xpr);
if (Array.isArray(xpr)) {
xpr = xpr.flat(Infinity);
return (xpr.length === 1) ? xpr[0] : xpr;
}
return xpr;
}
/**
* Reject any invalid expression such as `1 +`.
*/
function rejectInvalidExpression( expr ) {
if (expr.length === 3 && expr[1] === 'is' && expr[2] === 'null') {
messageFunctions.error('odata-anno-xpr', location, {
'#': 'notadynexpr', anno, op: 'is null',
});
return;
}
if (expr.length === 4 && expr[1] === 'is' && expr[2] === 'not' && expr[3] === 'null') {
messageFunctions.error('odata-anno-xpr', location, {
'#': 'notadynexpr', anno, op: 'is not null',
});
return;
}
if (expr.length === 4 && expr[1] === 'not' && expr[2] === 'like') {
messageFunctions.error('odata-anno-xpr', location, {
'#': 'notadynexpr', anno, op: 'not like',
});
return;
}
if (expr.length === 3 && expr[1] === 'like') {
messageFunctions.error('odata-anno-xpr', location, {
'#': 'notadynexpr', anno, op: 'not like',
});
return;
}
// Any left-over operator is not recognized: reject it
for (let i = 0; i < expr.length; i++) {
if (expr[i] === 'not' && typeof expr[i + 1] === 'string')
++i;
if (typeof expr[i] === 'string') {
messageFunctions.error('odata-anno-xpr', location, {
'#': 'invalid',
anno,
op: expr[i],
});
break;
}
}
}
/**
* Pre-transform a _structurized_ expression.
*/
function preTransformExpression( xpr ) {
if (!xpr || typeof xpr !== 'object')
return xpr;
if (Array.isArray(xpr)) {
xpr = xpr.map(preTransformExpression); // not `preTransformXpr`, as that would re-structurize the array
if (xpr.length === 1)
return xpr[0]; // single entry, e.g. via structurizer
if (xpr[0]?.toLowerCase?.() === 'case')
return preTransformCase(xpr);
if (xpr[1]?.toLowerCase?.() === 'between')
return preTransformBetween(xpr);
if (xpr[1]?.toLowerCase?.() === 'not' &&
xpr[2]?.toLowerCase?.() === 'between')
return { not: preTransformExpression([ xpr[0], ...xpr.slice(2) ]) };
if (xpr[1]?.toLowerCase?.() === 'not' &&
xpr[2]?.toLowerCase?.() === 'in')
return { not: preTransformExpression([ xpr[0], ...xpr.slice(2) ]) };
// unary operators
if (xpr.length === 2 && (xpr[0] === '+' || xpr[0] === '-' || xpr[0] === 'not' || xpr[0] === 'new'))
return { [xpr[0]]: xpr[1] };
// is null
if (xpr.length === 3 && xpr[1] === 'is' && xpr[2] === 'null')
return { isNull: [ xpr[0], { val: null } ] };
// is not null
if (xpr.length === 4 && xpr[1] === 'is' && xpr[2] === 'not' && xpr[3] === 'null')
return { isNotNull: [ xpr[0], { val: null } ] };
// binary operators: '=', '<>', 'like', ...
if (xpr.length === 3 && typeof xpr[1] === 'string')
return { [xpr[1]]: [ xpr[0], xpr[2] ] };
rejectInvalidExpression(xpr);
return xpr;
}
if (xpr.list)
xpr.list.forEach( preTransformExpression );
if (xpr.args) {
if (!Array.isArray( xpr.args ))
Object.values( xpr.args ).forEach(preTransformExpression );
else if (xpr.args.length)
xpr.args.forEach( preTransformExpression );
}
if (xpr.ref)
xpr.ref.forEach( preTransformExpression );
if (xpr.xpr)
xpr.xpr = preTransformExpression( xpr.xpr );
if (xpr.cast) {
const castKeys = Object.keys(xpr).filter(k => k !== 'cast');
if (castKeys.length === 1)
return { cast: [ xpr.cast, { [castKeys[0]]: xpr[castKeys[0]] } ] };
}
return xpr;
}
function preTransformBetween( xpr ) {
return {
between: [
xpr[0],
xpr[2],
xpr[4],
],
};
}
function preTransformCase( xpr ) {
const caseObj = [ ];
let pos = 1;
// CASE val WHEN val THEN …
if (xpr[pos]?.toLowerCase?.() !== 'when') {
caseObj.push( xpr[pos] );
pos++;
}
while (xpr[pos]?.toLowerCase?.() === 'when') {
if (xpr[pos + 2]?.toLowerCase?.() !== 'then')
return xpr;
caseObj.push({ when: [ xpr[pos + 1], xpr[pos + 3] ] });
pos += 4;
}
if (xpr[pos]?.toLowerCase?.() === 'else' ) {
caseObj.push(xpr[pos + 1]);
pos += 2;
}
if (xpr[pos++]?.toLowerCase?.() !== 'end')
return xpr;
return {
case: caseObj,
};
}
}
// Not everything that can occur in OData annotations can be expressed with
// corresponding constructs in cds annotations. For these special cases
// we have a kind of "inline assembler" mode, i.e. you can in cds provide
// as annotation value a json snippet that looks like the final edm-json.
// See example in test/odataAnnotations/smallTests/edmJson_noReverse_ok
// and test3/ODataBackends/DynExpr
function getEdmJsonHandler( Edm, options, messageFunctions, handleTerm ) {
const { message } = messageFunctions;
const { v } = options;
const dynamicExpressions = {
$And: { create: () => new Edm.Expr(v, 'And'), anno: true },
$Or: { create: () => new Edm.Expr(v, 'Or'), anno: true },
$Not: { create: () => new Edm.Expr(v, 'Not'), anno: true },
$Eq: { create: () => new Edm.Expr(v, 'Eq'), anno: true },
$Ne: { create: () => new Edm.Expr(v, 'Ne'), anno: true },
$Gt: { create: () => new Edm.Expr(v, 'Gt'), anno: true },
$Ge: { create: () => new Edm.Expr(v, 'Ge'), anno: true },
$Lt: { create: () => new Edm.Expr(v, 'Lt'), anno: true },
$Le: { create: () => new Edm.Expr(v, 'Le'), anno: true },
// valueThingName: 'EnumMember' Implicit Cast Rule String => Primitive Type is OK
$Has: { create: () => new Edm.Expr(v, 'Has'), anno: true },
$In: { create: () => new Edm.Expr(v, 'In'), anno: true },
$Add: { create: () => new Edm.Expr(v, 'Add'), anno: true },
$Sub: { create: () => new Edm.Expr(v, 'Sub'), anno: true },
$Neg: { create: () => new Edm.Expr(v, 'Neg'), anno: true },
$Mul: { create: () => new Edm.Expr(v, 'Mul'), anno: true },
$Div: { create: () => new Edm.Expr(v, 'Div'), anno: true },
$DivBy: { create: () => new Edm.Expr(v, 'DivBy'), anno: true },
$Mod: { create: () => new Edm.Expr(v, 'Mod'), anno: true },
$Apply: {
create: () => new Edm.Apply(v),
attr: [ '$Function' ],
anno: true,
},
$Cast: {
create: () => new Edm.Cast(v),
attr: [ '$Type', ...EdmTypeFacetNames.map(n => `$${ n }`), '@sap.variable.scale' ],
jsonAttr: [ '$Collection' ],
anno: true,
},
$IsOf: {
create: () => new Edm.IsOf(v),
attr: [ '$Type', ...EdmTypeFacetNames.map(n => `$${ n }`), '@sap.variable.scale' ],
jsonAttr: [ '$Collection' ],
anno: true,
},
$If: { create: () => new Edm.If(v), anno: true },
$LabeledElement: {
create: () => new Edm.LabeledElement(v),
attr: [ '$Name' ],
anno: true,
},
$LabeledElementReference: {
create: obj => new Edm.LabeledElementReference(v, obj.$LabeledElementReference),
},
$UrlRef: { create: () => new Edm.UrlRef(v), anno: true },
$Null: { create: () => new Edm.Null(v), anno: true, children: false },
};
Object.entries(dynamicExpressions).forEach(([ k, dv ]) => {
if (!dv.name)
dv.name = k.slice(1);
if (dv.children === undefined)
dv.children = true;
});
const dynamicExpressionNames = Object.keys(dynamicExpressions);
return { handleEdmJson };
function handleEdmJson( obj, msgContext, exprDef ) {
let edmNode;
if (obj == null)
return edmNode;
const dynExprs = edmUtils.intersect(dynamicExpressionNames, Object.keys(obj));
if (dynExprs.length > 1) {
message('odata-anno-value', msgContext.location,
{ anno: msgContext.anno(), rawvalues: dynExprs, '#': 'multexpr' });
return edmNode;
}
if (dynExprs.length === 0) {
if (typeof obj === 'object' && !Array.isArray(obj) && Object.keys(obj).length === 1) {
const k = Object.keys(obj)[0];
const val = obj[k];
edmNode = new Edm.ValueThing(v, k[0] === '$' ? k.slice(1) : k, val );
edmNode.setJSON( { [edmNode.kind]: val } );
}
// This thing is either a record or a collection or a literal
else if (Array.isArray(obj)) {
// EDM JSON doesn't mention annotations on collections
edmNode = new Edm.Collection(v);
obj.forEach(o => edmNode.append(handleEdmJson(o, msgContext)));
}
else if (typeof obj === 'object') {
edmNode = new Edm.Record(v);
const annos = Object.create(null);
const props = Object.create(null);
Object.entries(obj).forEach(([ k, val ]) => {
if (k === '@type') {
edmNode.setJSON({ Type: val });
// try to shorten full qualified type URI to short type name
const parts = val.split('#');
const shortTypeName = parts[parts.length - 1];
edmNode.setXml({ Type: shortTypeName });
}
else {
let child;
const [ head, tail ] = k.split('@');
if (tail) {
child = handleTerm(tail, val, msgContext);
}
else {
child = new Edm.PropertyValue(v, head);
child.append(handleEdmJson(val, msgContext));
}
if (child) {
if (tail && head.length) {
if (!annos[head])
annos[head] = [ child ];
else
annos[head].push(child);
}
else {
if (head.length)
props[head] = child;
edmNode.append(child);
}
}
}
});
// add collected annotations to record members
Object.entries(annos).forEach(([ n, val ]) => {
if (props[n])
props[n].prepend(...val);
});
}
else { // literal
edmNode = new Edm.ValueThing(v,
exprDef?.valueThingName || getXmlTypeName(obj), obj);
// typename for static expression rendering
edmNode.setJSON( { [getJsonTypeName(obj)]: obj } );
}
}
else {
// name of special property determines element kind
exprDef = dynamicExpressions[dynExprs[0]];
edmNode = exprDef.create(obj);
// iterate over each obj.property and translate expression into EDM
Object.entries(obj).forEach(([ name, val ]) => {
if (exprDef) {
if (exprDef.anno && name[0] === '@' && !name.startsWith('@sap.')) {
edmNode.append(handleTerm(name.slice(1), val, msgContext));
}
else if (exprDef.attr && exprDef.attr.includes(name)) {
if (options.isV2() && name.startsWith('@sap.'))
edmNode.setXml( { [`sap:${ name.slice(5).replace(/\./g, '-') }`]: val } );
if (name[0] === '$')
edmNode.setEdmAttribute(name.slice(1), val);
}
else if (exprDef.jsonAttr && exprDef.jsonAttr.includes(name)) {
if (name[0] === '$')
edmNode.setJSON( { [name.slice(1)]: val });
}
else if (exprDef.children) {
if (Array.isArray(val)) {
val.forEach((a) => {
edmNode.append(handleEdmJson(a, msgContext, exprDef));
});
}
else {
edmNode.append(handleEdmJson(val, msgContext, exprDef));
}
}
}
});
}
return edmNode;
function getXmlTypeName( val ) {
let typeName = 'String';
if (typeof val === 'boolean')
typeName = 'Bool';
else if (typeof val === 'number')
typeName = Number.isInteger(val) ? 'Int' : 'Decimal';
return typeName;
}
function getJsonTypeName( val ) {
const typeName = getXmlTypeName(val);
if (typeName === 'Int')
return 'Edm.Int32';
return `Edm.${ typeName }`;
}
}
}
module.exports = { xpr2edmJson, getEdmJsonHandler };