@sap/cds-compiler
Version:
CDS (Core Data Services) compiler and backends
928 lines (825 loc) • 38.4 kB
JavaScript
'use strict';
// A "tools" collection of various transformation functions that might be helpful for
// different backends.
// The sibling of model/transform/TransformUtil.js which works with compacted new CSN.
const { setProp } = require('../base/model');
const { copyAnnotations, applyTransformations, isDeepEqual } = require('../model/csnUtils');
const { getUtils } = require('../model/csnUtils');
const { typeParameters } = require('../compiler/builtins');
const { isBuiltinType } = require('../base/builtins');
const { ModelError, CompilerAssertion } = require('../base/error');
const { forEach } = require('../utils/objectUtils');
const { cloneCsnNonDict, cloneCsnDict } = require('../model/cloneCsn');
const { addTenantFieldToArt } = require('./addTenantFields');
const { tupleExpansion } = require('./tupleExpansion');
// Return the public functions of this module, with 'model' captured in a closure (for definitions, options etc).
// Use 'pathDelimiter' for flattened names (e.g. of struct elements or foreign key elements).
// 'model' is compacted new style CSN
// TODO: Error and warnings handling with compacted CSN? - currently just throw new ModelError for everything
// TODO: check the situation with assocs with values. In compacted CSN such elements have only "@Core.Computed": true
function getTransformers(model, options, msgFunctions, pathDelimiter = '_') {
const { error, warning } = msgFunctions;
const csnUtils = getUtils(model);
const {
getCsnDef,
getFinalTypeInfo,
inspectRef,
isStructured,
effectiveType,
} = csnUtils;
const tuples = tupleExpansion(model, csnUtils, msgFunctions, options);
return {
csnUtils,
addDefaultTypeFacets,
flattenStructuredElement,
flattenStructStepsInRef,
toFinalBaseType,
createExposingProjection,
createAndAddDraftAdminDataProjection,
isValidDraftAdminDataMessagesType,
createScalarElement,
createAssociationElement,
createAssociationPathComparison,
createForeignKey,
addForeignKey,
addElement,
copyAndAddElement,
createAction,
assignAction,
extractValidFromToKeyElement,
checkMultipleAssignments,
checkAssignment,
recurseElements,
renameAnnotation,
setAnnotation,
resetAnnotation,
expandStructsInExpression: tuples.expandStructsInExpression,
flattenPath: tuples.flattenPath,
};
/**
* Try to apply length, precision, scale from options if no type facet is set on the primitive types 'cds.String' or 'cds.Decimal'.
* If 'obj' has primitive type 'cds.String' and no length try to apply length from options if available or set to default internalDefaultLengths[type].
* if 'obj' has primitive type 'cds.Decimal' try to apply precision, scale from options if available.
*
* @param {CSN.Element} element
* @param {null|object} [internalDefaultLengths] Either null (no implicit default) or an object `{ 'cds.String': N, 'cds.Binary': N }`.
* */
function addDefaultTypeFacets(element, internalDefaultLengths = null) {
if (!element || !element.type)
return;
if (element.type === 'cds.String' && element.length === undefined) {
if (options.defaultStringLength) {
element.length = options.defaultStringLength;
setProp(element, '$default', true);
}
else if (internalDefaultLengths !== null) {
element.length = internalDefaultLengths[element.type];
}
}
if (element.type === 'cds.Binary' && element.length === undefined) {
if (options.defaultBinaryLength) {
element.length = options.defaultBinaryLength;
setProp(element, '$default', true);
}
else if (internalDefaultLengths !== null) {
element.length = internalDefaultLengths[element.type];
}
}
/*
if (element.type === 'cds.Decimal' && element.precision === undefined && options.precision) {
element.precision = options.precision;
}
if (element.type === 'cds.Decimal' && element.scale === undefined && options.scale) {
element.scale = options.scale;
}
*/
}
// For a structured element 'elem', return a dictionary of flattened elements to
// replace it, flattening names with pathDelimiter's value and propagating all annotations and the
// type properties 'key', 'notNull', 'virtual', 'masked' to the flattened elements.
// example input:
// { elem: {
// key: true,
// @foo: true,
// elements:
// { a: { type: 'cds.Integer' } },
// { b: {
// elements:
// { b1: type: 'cds.String', length: 42 } } },
// } }
//
// result:
// { elem_a: {
// key: true,
// @foo: true,
// type: 'cds.Integer' },
// elem_b_b1: {
// key: true,
// @foo: true,
// type: 'cds.String',
// length: 42 },
// }
function flattenStructuredElement(elem, elemName, parentElementPath = [], pathInCsn = []) {
const elementPath = parentElementPath.concat(elemName); // elementPath contains only element names without the csn structure node names
// in case the element is of user defined type => take the definition of the type
// for type of 'x' -> elem.type is an object, not a string -> use directly
let elemType;
if (!elem.elements) // structures do not have final base type
elemType = getFinalTypeInfo(elem.type);
const struct = elemType ? elemType.elements : elem.elements;
// Collect all child elements (recursively) into 'result'
// TODO: Do not report collisions in the generated elements here, but instead
// leave that work to the receiver of this result
const result = Object.create(null);
const addGeneratedFlattenedElement = (e, eName) => {
if (result[eName])
error('name-duplicate-element', pathInCsn, { '#': 'flatten-element-gen', name: eName });
else
result[eName] = e;
};
forEach(struct, (childName, childElem) => {
if (isStructured(childElem)) {
// Descend recursively into structured children
const grandChildElems = flattenStructuredElement(childElem, childName, elementPath, pathInCsn.concat('elements', childName));
for (const grandChildName in grandChildElems) {
const flatElemName = elemName + pathDelimiter + grandChildName;
const flatElem = grandChildElems[grandChildName];
addGeneratedFlattenedElement(flatElem, flatElemName);
// TODO: check with values. In CSN such elements have only "@Core.Computed": true
// If the original element had a value, construct one for the flattened element
// if (elem.value) {
// createFlattenedValue(flatElem, flatElemName, grandChildName);
// }
// Preserve the generated element name as it would have been with 'hdbcds' names
}
}
else {
// Primitive child - clone it and restore its cross references
const flatElemName = elemName + pathDelimiter + childName;
const flatElem = cloneCsnNonDict(childElem, options);
// Don't take over notNull from leaf elements
delete flatElem.notNull;
setProp(flatElem, '_flatElementNameWithDots', elementPath.concat(childName).join('.'));
addGeneratedFlattenedElement(flatElem, flatElemName);
}
});
// Fix all collected flat elements (names, annotations, properties, origin ..)
forEach(result, (name, flatElem) => {
// Copy annotations from struct (not overwriting, because deep annotations should have precedence).
// Attention:
// This has historic reasons. We don't copy doc-comments because copying annotations
// is questionable to begin with. Only selected annotations should have been copied,
// if at all.
// When flattening structured elements for OData don't propagate the odata.Type annotations
// as these would falsify the flattened elements. Type facets must be aligned with
// EdmTypeFacetMap defined in edm.js
const excludes = options.toOdata
? {
'@odata.Type': 1,
'@odata.Scale': 1,
'@odata.Precision': 1,
'@odata.MaxLength': 1,
'@odata.SRID': 1,
'@odata.FixedLength': 1,
'@odata.Collation': 1,
'@odata.Unicode': 1,
} : {};
copyAnnotations(elem, flatElem, false, excludes);
// Copy selected type properties
const props = [ 'key', 'virtual', 'masked', 'viaAll' ];
// 'localized' is needed for OData
if (options.toOdata)
props.push('localized');
for (const p of props) {
if (elem[p])
flatElem[p] = elem[p];
}
});
return result;
}
/**
* Return a copy of 'ref' where all path steps resulting from struct traversal are
* fused together into one step, using '_' (so that the path fits again for flattened
* structs), e.g.
* [ (Entity), (struct1), (struct2), (assoc), (elem) ] should result in
* [ (Entity), (struct1_struct2_assoc), (elem) ]
*
* @param {CSN.Ref} ref
* @param {CSN.Path} path CSN path to the ref
* @param {object[]} [links] Pre-resolved links for the given ref - if not provided, will be calculated JIT
* @param {string} [scope] Pre-resolved scope for the given ref - if not provided, will be calculated JIT
* @param {WeakMap} [resolvedLinkTypes=new WeakMap()] A WeakMap with already resolved types for each link-step - safes an `artifactRef` call
* @param {boolean} [suspend] suspend flattening by caller until association path step
* @param {number} [suspendPos] suspend if starting pos is lower or equal to suspendPos and suspend is true
* @param {boolean} [revokeAtSuspendPos] revoke suspension after suspendPos (binding parameter path use case)
* @param {boolean} [flattenParameters] Whether to flatten references into structured parameters. OData flattens parameters, SQL/for.effective does not.
*
* @todo: Refactor to take config object instead of N boolean arguments.
* @returns [string[], bool]
*/
function flattenStructStepsInRef(ref, path, links, scope, resolvedLinkTypes = new WeakMap(), suspend = false, suspendPos = 0, revokeAtSuspendPos = false, flattenParameters = false) {
// A path is absolute if it starts with $self or a parameter. Then we must not flatten the first path step.
const pathIsAbsolute = scope === '$self' || (!flattenParameters && scope === 'param');
// Refs of length 1 cannot contain steps - no need to check
if (ref.length < 2 || (pathIsAbsolute && ref.length === 2))
return [ ref, false ];
const result = pathIsAbsolute ? [ ref[0] ] : [];
// let stack = []; // IDs of path steps not yet processed or part of a struct traversal
if (!links && !scope) { // calculate JIT if not supplied
const res = inspectRef(path);
links = res.links;
scope = res.scope;
}
if (scope === '$magic')
return [ ref, false ];
// Don't process a leading $self - it will a .art with .elements!
let i = pathIsAbsolute ? 1 : 0;
// read property from resolved path link
const art = propName => (links[i].art?.[propName] ||
effectiveType(links[i].art)[propName] ||
(resolvedLinkTypes.get(links[i]) || {})[propName]);
let refChanged = false;
let flattenStep = false;
suspend = !!art('items') || (suspend && i <= suspendPos);
for (; i < links.length; i++) {
if (flattenStep && !suspend) {
result[result.length - 1] += pathDelimiter + (ref[i].id ? ref[i].id : ref[i]);
// if we had a filter or args, we had an assoc so this step is done
// we then keep along the filter/args by updating the id of the current ref
if (ref[i].id) {
ref[i].id = result[result.length - 1];
result[result.length - 1] = ref[i];
}
refChanged = true;
// suspend flattening if the next path step has some 'items'
suspend = !!art('items');
}
else {
result.push(ref[i]);
suspend ||= !!art('items');
}
// revoke items suspension for next assoc step
if (suspend && art('target') || (revokeAtSuspendPos && i === suspendPos))
suspend = false;
flattenStep = !links[i].art?.kind &&
!links[i].art?.SELECT &&
!links[i].art?.from &&
art('elements');
}
return [ result, refChanged ];
}
/**
* Replace the type of 'nodeWithType' with its final base type, i.e. copy relevant type properties and
* set the `type` property to the builtin if scalar or delete it if structured/arrayed.
*
* @param {object} nodeWithType
* @param {WeakMap} [resolved] WeakMap containing already resolved refs
* @param {boolean} [keepLocalized=false] Whether to clone .localized from a type def
*/
function toFinalBaseType(nodeWithType, resolved = new WeakMap(), keepLocalized = false) {
const type = nodeWithType?.type;
if (!type || nodeWithType.elements || nodeWithType.items || resolved.has(nodeWithType))
return;
// The caller may use `{ art }` syntax for `{ ref }` objects, but we only use
// it to indicate that an artifact has been processed.
resolved.set(nodeWithType, nodeWithType);
// Nothing to copy from builtin.
if (typeof type === 'string' && isBuiltinType(type))
return;
const typeRef = getFinalTypeInfo(type, t => resolved.get(t)?.art || csnUtils.artifactRef(t));
if (!typeRef)
return;
if (typeRef.elements || typeRef.items) {
// Copy elements/items and we're finished. No need to look up actual base type,
// since it must also be structured and must contain at least as many elements,
// if not more (in client style CSN).
if (typeRef.elements && !(options.transformation === 'hdbcds' && options.sqlMapping === 'hdbcds'))
nodeWithType.elements = cloneCsnDict(typeRef.elements, options);
else if (typeRef.items)
nodeWithType.items = cloneCsnNonDict(typeRef.items, options);
return;
}
if (typeRef.enum && nodeWithType.enum === undefined)
nodeWithType.enum = cloneCsnDict(typeRef.enum, options);
// Copy type and type arguments (+ localized)
for (const param of typeParameters.list) {
if (nodeWithType[param] === undefined && typeRef[param] !== undefined && !typeRef.$default)
nodeWithType[param] = typeRef[param];
}
if (keepLocalized && nodeWithType.localized === undefined && typeRef.localized !== undefined)
nodeWithType.localized = typeRef.localized;
if (typeRef.type)
nodeWithType.type = typeRef.type;
}
// Return a full projection 'projectionId' of artifact 'art' for exposure in 'service'.
// Add the created projection to the model and complain if artifact already exists.
// Used by Draft generation
function createExposingProjection(art, artName, projectionId, service) {
const projectionAbsoluteName = `${ service }.${ projectionId }`;
// Create elements matching the artifact's elements
const elements = Object.create(null);
Object.entries(art.elements || {}).forEach(([ elemName, artElem ]) => {
// Transfer xrefs, that are redirected to the projection
// TODO: shall we remove the transferred elements from the original?
// if (artElem._xref) {
// setProp(elem, '_xref', artElem._xref.filter(xref => xref.user && xref.user._main && xref.user._main._service == service));
// }
// FIXME: Remove once the compactor no longer renders 'origin'
elements[elemName] = Object.assign({}, artElem);
});
const query = {
SELECT: {
from: {
ref: [
artName,
],
},
},
};
// Assemble the projection itself and add it into the model
const projection = {
kind: 'entity',
projection: query.SELECT, // it is important that projection and query refer to the same object!
elements,
};
// copy annotations from art to projection
for (const a of Object.keys(art).filter(x => x.startsWith('@')))
projection[a] = art[a];
model.definitions[projectionAbsoluteName] = projection;
return projection;
}
/**
* Create a 'DraftAdministrativeData' projection on entity 'DRAFT.DraftAdministrativeData'
* in service 'service' and add it to the model.
*
* For forRelationalDB, use String(36) instead of UUID and UTCTimestamp instead of Timestamp
*
* @param {string} service
* @param {boolean} [hanaMode=false] Turn UUID into String(36)
* @returns {CSN.Artifact}
*/
function createAndAddDraftAdminDataProjection(service, hanaMode = false) {
// Make sure we have a DRAFT.DraftAdministrativeData entity
let draftAdminDataEntity = model.definitions['DRAFT.DraftAdministrativeData'];
if (!draftAdminDataEntity) {
draftAdminDataEntity = createAndAddDraftAdminDataEntity();
model.definitions['DRAFT.DraftAdministrativeData'] = draftAdminDataEntity;
if (options.draftMessages && options.transformation === 'odata' &&
!model.definitions['DRAFT.DraftAdministrativeData_DraftMessage'])
model.definitions['DRAFT.DraftAdministrativeData_DraftMessage'] = createDraftAdminDataMessagesType();
if (options.tenantDiscriminator && options.transformation !== 'odata')
addTenantFieldToArt(model.definitions['DRAFT.DraftAdministrativeData'], options);
}
// Create a projection within this service
return createExposingProjection(draftAdminDataEntity, 'DRAFT.DraftAdministrativeData', 'DraftAdministrativeData', service);
/**
* Create the 'DRAFT.DraftAdministrativeData' entity (unless it already exist)
* Return the 'DRAFT.DraftAdministrativeData' entity.
*/
function createAndAddDraftAdminDataEntity(artifactName = 'DRAFT.DraftAdministrativeData') {
// Create the 'DRAFT.DraftAdministrativeData' entity
const artifact = {
kind: 'entity',
elements: Object.create(null),
'@Common.Label': '{i18n>Draft_DraftAdministrativeData}',
};
// key DraftUUID : UUID
const draftUuid = createScalarElement('DraftUUID', hanaMode ? 'cds.String' : 'cds.UUID', true);
if (hanaMode)
draftUuid.DraftUUID.length = 36;
draftUuid.DraftUUID['@UI.Hidden'] = true;
draftUuid.DraftUUID['@Common.Label'] = '{i18n>Draft_DraftUUID}';
addElement(draftUuid, artifact, artifactName);
// CreationDateTime : Timestamp;
const creationDateTime = createScalarElement('CreationDateTime', 'cds.Timestamp');
creationDateTime.CreationDateTime['@Common.Label'] = '{i18n>Draft_CreationDateTime}';
addElement(creationDateTime, artifact, artifactName);
// CreatedByUser : String(256);
const createdByUser = createScalarElement('CreatedByUser', 'cds.String');
createdByUser.CreatedByUser.length = 256;
createdByUser.CreatedByUser['@Common.Label'] = '{i18n>Draft_CreatedByUser}';
addElement(createdByUser, artifact, artifactName);
if ((options.draftUserDescription && options.transformation === 'odata') || options.transformation !== 'odata') {
// CreatedByUserDescription : String(256);
const createdByUserDescription = createScalarElement('CreatedByUserDescription', 'cds.String');
createdByUserDescription.CreatedByUserDescription.length = 256;
createdByUserDescription.CreatedByUserDescription['@Common.Label'] = '{i18n>Draft_CreatedByUserDescription}';
addElement(createdByUserDescription, artifact, artifactName);
}
// DraftIsCreatedByMe : Boolean;
const draftIsCreatedByMe = createScalarElement('DraftIsCreatedByMe', 'cds.Boolean');
draftIsCreatedByMe.DraftIsCreatedByMe['@UI.Hidden'] = true;
draftIsCreatedByMe.DraftIsCreatedByMe['@Common.Label'] = '{i18n>Draft_DraftIsCreatedByMe}';
addElement(draftIsCreatedByMe, artifact, artifactName);
// LastChangeDateTime : Timestamp;
const lastChangeDateTime = createScalarElement('LastChangeDateTime', 'cds.Timestamp');
lastChangeDateTime.LastChangeDateTime['@Common.Label'] = '{i18n>Draft_LastChangeDateTime}';
addElement(lastChangeDateTime, artifact, artifactName);
// LastChangedByUser : String(256);
const lastChangedByUser = createScalarElement('LastChangedByUser', 'cds.String');
lastChangedByUser.LastChangedByUser.length = 256;
lastChangedByUser.LastChangedByUser['@Common.Label'] = '{i18n>Draft_LastChangedByUser}';
addElement(lastChangedByUser, artifact, artifactName);
if (options.transformation !== 'odata' || options.draftUserDescription) {
// LastChangedByUserDescription : String(256);
const lastChangedByUserDescription = createScalarElement('LastChangedByUserDescription', 'cds.String');
lastChangedByUserDescription.LastChangedByUserDescription.length = 256;
lastChangedByUserDescription.LastChangedByUserDescription['@Common.Label'] = '{i18n>Draft_LastChangedByUserDescription}';
addElement(lastChangedByUserDescription, artifact, artifactName);
}
// InProcessByUser : String(256);
const inProcessByUser = createScalarElement('InProcessByUser', 'cds.String');
inProcessByUser.InProcessByUser.length = 256;
inProcessByUser.InProcessByUser['@Common.Label'] = '{i18n>Draft_InProcessByUser}';
addElement(inProcessByUser, artifact, artifactName);
if (options.transformation !== 'odata' || options.draftUserDescription) {
// InProcessByUserDescription : String(256);
const inProcessByUserDescription = createScalarElement('InProcessByUserDescription', 'cds.String');
inProcessByUserDescription.InProcessByUserDescription.length = 256;
inProcessByUserDescription.InProcessByUserDescription['@Common.Label'] = '{i18n>Draft_InProcessByUserDescription}';
addElement(inProcessByUserDescription, artifact, artifactName);
}
// DraftIsProcessedByMe : Boolean;
const draftIsProcessedByMe = createScalarElement('DraftIsProcessedByMe', 'cds.Boolean');
draftIsProcessedByMe.DraftIsProcessedByMe['@UI.Hidden'] = true;
draftIsProcessedByMe.DraftIsProcessedByMe['@Common.Label'] = '{i18n>Draft_DraftIsProcessedByMe}';
addElement(draftIsProcessedByMe, artifact, artifactName);
if (options.transformation !== 'odata' || options.draftMessages) {
const messages = { DraftMessages: { } };
if (options.transformation === 'odata')
messages.DraftMessages = { items: { type: 'DRAFT.DraftAdministrativeData_DraftMessage' } };
else
messages.DraftMessages = { type: 'cds.LargeString' };
messages.DraftMessages['@cds.api.ignore'] = true;
addElement(messages, artifact, artifactName);
}
return artifact;
}
}
// Create the artificial 'DRAFT.Draf tAdministrativeData_DraftMessage' type
// for the beta feature 'draftMessages'
function createDraftAdminDataMessagesType() {
const messagesType = {
kind: 'type',
elements: Object.create(null),
};
addElement(createScalarElement('code', 'cds.String'), messagesType, 'DRAFT.DraftAdministrativeData_DraftMessage');
addElement(createScalarElement('message', 'cds.String'), messagesType, 'DRAFT.DraftAdministrativeData_DraftMessage');
addElement(createScalarElement('target', 'cds.String'), messagesType, 'DRAFT.DraftAdministrativeData_DraftMessage');
addElement({ additionalTargets: createScalarElement('items', 'cds.String') }, messagesType, 'DRAFT.DraftAdministrativeData_DraftMessage');
addElement(createScalarElement('transition', 'cds.Boolean'), messagesType, 'DRAFT.DraftAdministrativeData_DraftMessage');
addElement(createScalarElement('numericSeverity', 'cds.UInt8'), messagesType, 'DRAFT.DraftAdministrativeData_DraftMessage');
addElement(createScalarElement('longtextUrl', 'cds.String'), messagesType, 'DRAFT.DraftAdministrativeData_DraftMessage');
// the tag element not needed for now, but might be added later on
// addElement(createScalarElement('tag', 'cds.String'), messagesType, 'DRAFT.DraftAdministrativeData_DraftMessage');
// setAnnotation(messagesType.tag, '@cds.api.ignore', true);
return messagesType;
}
// Checks if the given definition is a valid 'DRAFT.DraftAdministrativeData_DraftMessage' type
function isValidDraftAdminDataMessagesType(def) {
const expectedType = createDraftAdminDataMessagesType();
return isDeepEqual(def, expectedType, false);
}
// Create an artificial scalar element 'elemName' with final type 'typeName'.
// Make the element a key element if 'isKey' is true.
// Add a default value 'defaultVal' if supplied
// example result: { foo: { type: 'cds.Integer', key: true, default: { val: 6 }, notNull: true } }
// ^^^ ^^^^^^^^^ ^^^^ ^^ ^^
// elemName typeName isKey defaultVal notNull
function createScalarElement(elemName, typeName, isKey = false, defaultVal = undefined, notNull = false) {
if (!isBuiltinType(typeName) && !model.definitions[typeName])
throw new ModelError(`Expecting valid type name: ${ typeName }`);
const result = {
[elemName]: {
type: typeName,
},
};
if (isKey)
result[elemName].key = true;
if (defaultVal !== undefined) {
result[elemName].default = {
val: defaultVal,
};
}
if (notNull)
result[elemName].notNull = true;
return result;
}
// Create an artificial element 'elemName' of type 'cds.Association',
// having association target 'target'. If 'isManaged' is true, take all keys
// of 'target' as foreign keys.
// e.g. result:
// { toFoo: {
// type: 'cds.Association', target: 'Foo',
// keys: [{ ref: ['id'] }]
// } }
function createAssociationElement(elemName, target, isManaged = false) {
const elem = createScalarElement(elemName, 'cds.Association', false, undefined);
const assoc = elem[elemName];
assoc.target = target;
if (isManaged) {
assoc.keys = [];
const targetArt = getCsnDef(target);
Object.entries(targetArt.elements || {}).forEach(([ keyElemName, keyElem ]) => {
if (keyElem.key) {
const foreignKey = createForeignKey(keyElemName, keyElem);
addForeignKey(foreignKey, assoc);
}
});
}
return elem;
}
// Create a comparison operation <assoc>.<foreignElem> <op> <elem>.
// return an array to be spread in an on-condition
// e.g. [ { ref: ['SiblingEntity','ID'] }, '=', { ref: ['ID'] } ]
// ^^^^^ ^^^ ^^ ^^^
// assoc foreignElem op elem
function createAssociationPathComparison(assoc, foreignElem, op, elem) {
return [
{ ref: [ assoc, foreignElem ] }, op, { ref: [ elem ] },
];
}
// Create an artificial foreign key 'keyElemName' for key element 'keyElem'. Note that this
// only creates a foreign key, not the generated foreign key element.
// TODO: check the usage of this function's param 'keyElem' ?
function createForeignKey(keyElemName, keyElem = undefined) { /* eslint-disable-line no-unused-vars */
return {
ref: [ keyElemName ],
// TODO: do we need these two?
// calculated: true,
// $inferred: 'keys',
};
}
// Add foreign key 'foreignKey' to association element 'elem'.
function addForeignKey(foreignKey, elem) {
// Sanity checks
if (!elem.target || !elem.keys)
throw new ModelError('Expecting managed association element with foreign keys');
// Add the foreign key
elem.keys.push(foreignKey);
}
/**
* Add element 'elem' to 'artifact'
*
* @param {any} elem is in form: { b: { type: 'cds.String' } }
* @param {CSN.Artifact} artifact is: { kind: 'entity', elements: { a: { type: 'cds.Integer' } ... } }
* @param {string} [artifactName] Name of the artifact in `csn.definitions[]`.
* @returns {void}
*/
function addElement(elem, artifact, artifactName) {
// Sanity check
if (!artifact.elements)
throw new ModelError(`Expecting artifact with elements: ${ JSON.stringify(artifact) }`);
const elemName = Object.keys(elem)[0];
// Element must not exist
if (artifact.elements[elemName]) {
let path = null;
if (artifactName)
path = [ 'definitions', artifactName, 'elements', elemName ];
error(null, path, { name: elemName }, 'Generated element $(NAME) conflicts with existing element');
return;
}
// Add the element
Object.assign(artifact.elements, elem);
}
/**
* Make a copy of element 'elem' (e.g. { elem: { type: 'cds.Integer' } })
* and add it to 'artifact' under the new name 'elemName'.
* ( e.g. { artifact: { elements: { elemName: { type: 'cds.Integer' } } })
* Return the newly created element
* (e.g. { elemName: { type: 'cds.Integer' } })
*
* @param {object} elem
* @param {CSN.Artifact} artifact
* @param {string} artifactName
* @param {string} elementName
*/
function copyAndAddElement(elem, artifact, artifactName, elementName) {
if (!artifact.elements)
throw new ModelError('Expected structured artifact');
// Must not already have such an element
if (artifact.elements[elementName]) {
const path = [ 'definitions', artifactName, 'elements', elementName ];
error(null, path, { name: elementName }, 'Generated element $(NAME) conflicts with existing element');
}
const result = Object.create(null);
result[elementName] = {};
Object.entries(elem || {}).forEach(([ prop, value ]) => {
result[elementName][prop] = value;
});
Object.assign(artifact.elements, result);
return result;
}
// Create an artificial action 'actionName' with return type artifact 'returnType' optionally with one parameter 'paramName'
// of type name 'paramTypeName'
function createAction(actionName, returnTypeName = undefined, paramName = undefined, paramTypeName = undefined) {
// Assemble the action
const result = {
[actionName]: {
kind: 'action',
},
};
const action = result[actionName];
if (returnTypeName) {
if (!isBuiltinType(returnTypeName) && !model.definitions[returnTypeName])
throw new ModelError(`Expecting valid return type name: ${ returnTypeName }`);
action.returns = { type: returnTypeName };
// TODO: What about annotation propagation from return type to `returns`?
}
// Add parameter if provided
if (paramName && paramTypeName) {
if (!isBuiltinType(paramTypeName) && !model.definitions[paramTypeName])
throw new ModelError(`Expecting valid parameter type name: ${ paramTypeName }`);
action.params = Object.create(null);
action.params[paramName] = {
type: paramTypeName,
};
}
return result;
}
/**
* Add action 'action' to 'artifact' but don't overwrite existing action
*
* @param {object} action Action that shall be added to the given artifact.
* In form of `{ myAction: { kind: 'action', returns ... } }`
* @param {CSN.Artifact} artifact Artifact in the form of `{ kind: 'entity', elements: ... }`
* */
function assignAction(action, artifact) {
if (!artifact.actions)
artifact.actions = Object.create(null);
const actionName = Object.keys(action)[0];
// Element must not exist
if (!artifact.actions[actionName]) {
// Add the action
Object.assign(artifact.actions, action);
}
}
/**
* If the element has annotation @cds.valid.from or @cds.valid.to, return it.
*
* @param {any} element Element to check
* @param {Array} path path in CSN for error messages
* @returns {Array[]} Array of arrays, first filed has an array with the element if it has @cds.valid.from,
* second field if it has @cds.valid.to. Default value is [] for each field.
*/
function extractValidFromToKeyElement(element, path) {
const validFroms = [];
if (element['@cds.valid.from'])
validFroms.push({ element, path: [ ...path ] });
const validTos = [];
if (element['@cds.valid.to'])
validTos.push({ element, path: [ ...path ] });
const validKeys = [];
if (element['@cds.valid.key'])
validKeys.push({ element, path: [ ...path ] });
return [ validFroms, validTos, validKeys ];
}
/**
* Check if the element can be annotated with the given annotation.
* Only runs the check if:
* - The artifact is not a type
* - The artifact is not a view
*
* Signals an error, if:
* - The element is structured
* - Has a target
* - Has an element as _parent.kind
*
* @param {string} annoName Annotation name
* @param {object} element Element to be checked
* @param {CSN.Path} path
* @param {CSN.Artifact} artifact
* @returns {boolean} True if no errors
*/
function checkAssignment(annoName, element, path, artifact) {
if (artifact.kind !== 'type' && !artifact.query) {
// path.length > 4 to check for structured elements
if (element.elements || element.target || path.length > 4) {
error(null, path, { anno: annoName }, 'Element can\'t be annotated with $(ANNO)');
return false;
}
}
return true;
}
/**
* Signals an error/warning if an annotation has been assigned more than once
*
* @param {any} array Array of elements that have the annotation
* @param {any} annoName Name of the annotation
* @param {CSN.Artifact} artifact Root artifact containing the elements
* @param {string} artifactName Name of the root artifact
* @param {boolean} [err=true] Down-grade to a warning if set to false
*/
function checkMultipleAssignments(array, annoName, artifact, artifactName, err = true) {
if (array.length > 1) {
const loc = [ 'definitions', artifactName ];
if (err === true)
error(null, loc, { anno: annoName }, 'Annotation $(ANNO) must be assigned only once');
else
warning(null, loc, { anno: annoName }, 'Annotation $(ANNO) must be assigned only once');
}
}
/**
* Calls `callback` for each element in `elements` property of `artifact` recursively.
*
* @param {CSN.Artifact} artifact the artifact
* @param {CSN.Path} path path to get to `artifact` (mainly used for error messages)
* @param {(art: CSN.Artifact, path: CSN.Path) => any} callback Function called for each element recursively.
*/
function recurseElements(artifact, path, callback) {
callback(artifact, path);
const { elements } = artifact;
if (elements) {
path.push('elements', null);
forEach(elements, (name, obj) => {
path[path.length - 1] = name;
recurseElements(obj, path, callback);
});
// reset path for subsequent usages
path.length -= 2; // equivalent to 2x pop()
}
}
// Rename annotation 'fromName' in 'node' to 'toName' (both names including '@')
function renameAnnotation(node, fromName, toName) {
const annotation = node && node[fromName];
// Sanity checks
if (!fromName.startsWith('@'))
throw new CompilerAssertion(`Annotation name should start with "@": ${ fromName }`);
if (!toName.startsWith('@'))
throw new CompilerAssertion(`Annotation name should start with "@": ${ toName }`);
if (annotation === undefined)
throw new CompilerAssertion(`Annotation ${ fromName } not found in ${ JSON.stringify(node) }`);
if (node[toName] == null) {
delete node[fromName];
node[toName] = annotation;
}
}
/**
* Assign annotation to a node but do not overwrite already existing annotation assignment
* that is (assignment is either undefined or has null value)
*
* @param {object} node Assignee
* @param {string} name Annotation name
* @param {any} value Annotation value
* @returns {void}
*/
function setAnnotation(node, name, value) {
if (!name.startsWith('@'))
throw new CompilerAssertion(`Annotation name should start with "@": ${ name }`);
if (value === undefined)
throw new CompilerAssertion('Annotation value must not be undefined');
node[name] ??= value;
}
/**
* Assigns unconditionally annotation to a node, which means it overwrites already existing annotation assignment.
* Overwriting is when the assignment differs from undefined and null, also when differs from the already set value.
* Setting new assignment results false as return value and overwriting - true.
*
* @param {object} node Assignee
* @param {string} name Annotation name
* @param {any} value Annotation value
* @param {function} info function that reports info messages
* @param {CSN.Path} path location of the warning
* @returns {boolean} wasOverwritten true when the annotation was overwritten
*/
function resetAnnotation(node, name, value, info, path) {
if (!name.startsWith('@'))
throw new CompilerAssertion(`Annotation name should start with "@": ${ name }`);
if (value === undefined)
throw new CompilerAssertion('Annotation value must not be undefined');
const wasOverwritten = node[name] !== undefined && node[name] !== null && node[name] !== value;
const oldValue = node[name];
node[name] = value;
if (wasOverwritten) {
info(null, path, { anno: name, prop: value, otherprop: oldValue },
'Value $(OTHERPROP) of annotation $(ANNO) is overwritten with new value $(PROP)');
}
return wasOverwritten;
}
}
/**
* Mandatory input transformation for all backends:
* Replace
* type: { ref: [ 'cds.<type>' ] }
* with the direct type
* type: 'cds.<type>'
*
* @param {CSN.Model} csn
*/
function rewriteBuiltinTypeRef(csn) {
const special$self = !csn?.definitions?.$self && '$self';
applyTransformations(csn, {
type: (parent, _prop, type) => {
if (type?.ref && (
isBuiltinType(type.ref[0]) ||
type.ref[0] === special$self)
)
parent.type = type.ref[0];
},
});
}
module.exports = {
// This function retrieves the actual exports
getTransformers,
rewriteBuiltinTypeRef,
};