UNPKG

@sap/cds-compiler

Version:

CDS (Core Data Services) compiler and backends

1,225 lines (1,127 loc) 51.9 kB
'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, isBetaEnabled } = require('../base/model'); const { copyAnnotations, applyTransformations, isDollarSelfOrProjectionOperand, 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 RestrictedOperators = ['<', '>', '>=', '<=']; const RelationalOperators = ['=', '<>', '==', '!=', 'is' /*, 'like'*/,...RestrictedOperators]; // 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 { message, error, warning, info } = msgFunctions; const csnUtils = getUtils(model); const { getCsnDef, getFinalTypeInfo, inspectRef, isStructured, effectiveType, } = csnUtils; return { csnUtils, resolvePath, flattenPath, addDefaultTypeFacets, flattenStructuredElement, flattenStructStepsInRef, toFinalBaseType, createExposingProjection, createAndAddDraftAdminDataProjection, isValidDraftAdminDataMessagesType, createScalarElement, createAssociationElement, createAssociationPathComparison, createForeignKey, addForeignKey, addElement, copyAndAddElement, createAction, assignAction, extractValidFromToKeyElement, checkMultipleAssignments, checkAssignment, recurseElements, renameAnnotation, setAnnotation, resetAnnotation, expandStructsInExpression, }; /** * 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); art.elements && Object.entries(art.elements).forEach(([elemName, artElem]) => { const elem = Object.assign({}, 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] = elem; }); 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 ((isBetaEnabled(options, 'draftMessages') || 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); // 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); // InProcessByUser : String(256); const inProcessByUser = createScalarElement('InProcessByUser', 'cds.String'); inProcessByUser['InProcessByUser'].length = 256; inProcessByUser.InProcessByUser['@Common.Label'] = '{i18n>Draft_InProcessByUser}'; addElement(inProcessByUser, 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 (isBetaEnabled(options, 'draftMessages') || 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); targetArt.elements && 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] = {}; elem && 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 = [], validTos = [], validKeys = []; if (element['@cds.valid.from']) validFroms.push({ element, path: [...path] }); if (element['@cds.valid.to']) validTos.push({ element, path: [...path] }); 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.elements; 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; } /* Resolve the type of an artifact If art is undefined, stop If art has elements or items.elements, stop If art has a type and the type is scalar, stop If art has a named type or a type ref, resolve it */ function resolveType(art) { while(art && !((art.items && art.items.elements) || art.elements) && (art.type && ((!art.type.ref && !isBuiltinType(art.type)) || art.type.ref))) { if(art.type.ref) art = resolvePath(art.type); else art = model.definitions[art.type]; } return art; } /** * Path resolution, attach artifact to each path step, if found, * Dereference types and follow associations. * * @param {any} path ref object * @param {any} art start environment * @returns {any} path with resolved artifacts or artifact * (if called with simple ref paths) */ function resolvePath(path, art=undefined) { let notFound = false; for(let i = 0; i < path.ref.length && !notFound; i++) { const ps = path.ref[i]; const id = ps.id || ps; if(art) { if(art.target) art = model.definitions[art.target].elements[id]; else if(art.items && art.items.elements || art.elements) { art = (art.items && art.items.elements || art.elements)[id]; } else art = undefined; } else { art = model.definitions[id]; } art = resolveType(art); // if path step has id, store art if(ps.id && art) ps._art = art; notFound = !art; } // if resolve was called on constraint path, path has id. // Store art and return path, if called recursively for model ref paths, // return artifact only if(path.ref[0].id) { if(art) path._art = art; return path; } else return art; } /* Flatten structured leaf types and return an array of paths. Argument 'path' must be an object of the form { _art: <leaf_artifact>, ref: [...] } with _art identifying ref[ref.length-1] A produced path has the form { _art: <ref>, ref: [ <id> (, <id>)* ] } Flattening stops on all non structured elements, if followMgdAssoc=false. If fullRef is true, a path step is produced as { id: <id>, _art: <link> } */ function flattenPath(path, fullRef=false, followMgdAssoc=false) { let art = path._art; if(art) { if(art && !((art.items && art.items.elements) || art.elements)) { if(followMgdAssoc && art.target && art.keys) { const rc = []; for(const k of art.keys) { const nps = { ref: k.ref.map(p => fullRef ? { id: p } : p ) }; setProp(nps, '_art', k._art); const paths = flattenPath( nps, fullRef, followMgdAssoc ); // prepend prefix path paths.forEach(p=>p.ref.splice(0, 0, ...path.ref)); rc.push(...paths); } return rc; } if(art.type && art.type.ref) art = resolvePath(art.type); else if(art.type && !isBuiltinType(art.type)) art = model.definitions[art.type]; } const elements = art.items && art.items.elements || art.elements; if(elements) { const rc = [] Object.entries(elements).forEach(([en, elt]) => { const nps = { ref: [ (fullRef ? { id: en, _art: elt } : en )] }; setProp(nps, '_art', elt); const paths = flattenPath( nps, fullRef, followMgdAssoc ); // prepend prefix path paths.forEach(p=>p.ref.splice(0, 0, ...path.ref)); rc.push(...paths); }); return rc; } else setProp(path, '_art', art); } return [path]; } /** * Expand structured expression arguments to flat reference paths. * Structured elements are real sub element lists and managed associations. * All unmanaged association definitions are rewritten if applicable (elements/mixins). * Also, HAVING and WHERE clauses are rewritten. We also check for infix filters and * .xpr in columns. * * @todo Check if can be skipped for abstract entity and or cds.persistence.skip ? * @param {CSN.Model} csn * @param {object} [options={}] "skipArtifact": (artifact, name) => Boolean to skip certain artifacts */ function expandStructsInExpression(csn, options = {}) { applyTransformations(csn, { 'on': (parent, name, on, path) => { parent.on = expand(parent.on, path.concat(name)); }, 'having': (parent, name, having, path) => { parent.having = expand(parent.having, path.concat(name)); }, 'where': (parent, name, where, path) => { parent.where = expand(parent.where, path.concat(name)); }, 'xpr': (parent, name, xpr, path) => { parent.xpr = expand(parent.xpr, path.concat(name)); } }, [], options); /* flatten structured leaf types and return array of paths Flattening stops on all non-structured types. */ function expand(expr, location) { if (!Array.isArray(expr)) return expr; // don't traverse strings, etc. const rc = []; for(let i = 0; i < expr.length; i++) { if(Array.isArray(expr[i])) rc.push(expr[i].map(e => expand(e, location))); if(i < expr.length-2) { let [lhs, op, not, rhs] = expr.slice(i); if(not !== 'not') { rhs = not; not = false; } if(lhs === undefined || op === undefined || rhs === undefined) return expr; // we might have to ad-hoc resolve a ref, since handleExists is run before hand and generates new refs. const lhsArt = lhs._art || lhs.ref && !lhs.$scope && inspectRef(location.concat(i)).art; const rhsArt = rhs._art || rhs.ref && !rhs.$scope && inspectRef(location.concat(i+2)).art; const lhsIsVal = (lhs.val !== undefined); // if ever rhs should be allowed to be a value uncomment this const rhsIsVal = (rhs === 'null' /*|| rhs.val !== undefined*/); // lhs & rhs must be expandable types (structures or managed associations) // if ever lhs should be allowed to be a value uncomment this if(!(lhsIsVal /*&& rhsIsVal*/) && !(isDollarSelfOrProjectionOperand(lhs) || isDollarSelfOrProjectionOperand(rhs)) && RelationalOperators.includes(op) && (lhsIsVal || (lhsArt && lhs.ref && isExpandable(lhsArt))) && (rhsIsVal || (rhsArt && rhs.ref && isExpandable(rhsArt))) ) { if(RestrictedOperators.includes(op)) { message('expr-unexpected-operator', location, { op }, 'Unexpected operator $(OP) in structural comparison'); } // if path is scalar and no assoc or has no type (@Core.Computed) use original expression // only do the expansion on (managed) assocs and (items.)elements, array of check in ON cond is done elsewhere const lhspaths = lhsIsVal ? [] : flattenPath({ _art: lhsArt, ref: lhs.ref }, false, true ); const rhspaths = rhsIsVal ? [] : flattenPath({ _art: rhsArt, ref: rhs.ref }, false, true ); // mapping dict for lhs/rhs for mismatch check // strip lhs/rhs prefix from flattened paths to check remaining common trailing path // if path is idempotent, it doesn't produce new flattened paths (ends on scalar type) // key is then empty string on both sides '' (=> equality) // Path matches if lhs/rhs are available const xref = createXRef(lhspaths, rhspaths, lhs, rhs, lhsIsVal, rhsIsVal); const xrefkeys = Object.keys(xref); const xrefvalues = Object.values(xref); let cont = true; const prefix = (lhs, op, rhs) => { return `${lhsIsVal ? lhs.val : lhs.ref.join('.')} ${op} ${rhsIsVal ? rhs : rhs.ref.join('.')}` } if(op === 'like' && xrefvalues.reduce((a, v) => { return (v.lhs && v.rhs) ? a + 1: a; }, 0) === 0) { // error if intersection of paths is zero error(null, location, { prefix: prefix(lhs, op, rhs) }, 'Expected compatible types for $(PREFIX)'); cont = false; } cont && xrefkeys.forEach(xn => { const x = xref[xn]; // do the paths match? if(op !== 'like' && !(x.lhs && x.rhs)) { if(xn.length) { error('expr-invalid-expansion', location, { value: prefix(lhs, op, rhs), name: xn, alias: (x.lhs ? rhs : lhs).ref.join('.') }, 'Missing sub path $(NAME) in $(ALIAS) for tuple expansion of $(VALUE); both sides must expand to the same sub paths'); } else { error(null, location, { prefix: prefix(lhs, op, rhs), name: (x.lhs ? lhs : rhs).ref.join('.'), alias: (x.lhs ? rhs : lhs).ref.join('.') }, '$(PREFIX): Path $(NAME) does not match $(ALIAS)'); } cont = false; } // lhs && rhs are present, consistency checks that affect both ends else { // is lhs scalar? // eslint-disable-next-line sonarjs/no-gratuitous-expressions if(!lhsIsVal && x.lhs && !isScalarOrNoType(x.lhs._art)) { error(null, location, { prefix: prefix(lhs, op, rhs), name: `${x.lhs.ref.join('.')}${(xn.length ? '.' + xn : '')}` }, '$(PREFIX): Path $(NAME) must end on a scalar type') cont = false; } // is rhs scalar? if(!rhsIsVal && x.rhs && !isScalarOrNoType(x.rhs._art)) { error(null, location, { prefix: prefix(lhs, op, rhs), name: `${x.rhs.ref.join('.')}${(xn.length ? '.' + xn : '')}` }, '$(PREFIX): Path $(NAME) must end on a scalar type'); cont = false; } // info about type incompatibility if no other errors occurred // eslint-disable-next-line sonarjs/no-gratuitous-expressions if(!(lhsIsVal || rhsIsVal) && x.lhs && x.rhs && xn && cont) { const lhst = getType(x.lhs._art); const rhst = getType(x.rhs._art); if(lhst !== rhst) { info(null, location, { prefix: prefix(lhs, op, rhs), name: xn }, '$(PREFIX): Types for sub path $(NAME) don\'t match'); } } } }); // don't continue if there are path errors if(!cont) return expr; // if lhs and rhs are refs set operator from 'like' to '=' // eslint-disable-next-line sonarjs/no-gratuitous-expressions if(op === 'like' && !(lhsIsVal || rhsIsVal)) { op = '='; } // t_0 OR ... OR t_n with t = (a <not equal> b) const bop = (op === 'is' && not) || op === '!=' || op === '<>' ? 'or' : 'and'; const xpr = { xpr: [] }; xrefvalues.filter(x => x.lhs && x.rhs).forEach((x,i) => { xpr.i = i; if(i>0) { xpr.xpr.push(bop); } xpr.xpr.push(x.lhs); xpr.xpr.push(op); if(not) xpr.xpr.push('not') xpr.xpr.push(x.rhs); }); if(xpr.i > 0) { delete xpr.i; rc.push(xpr); } else rc.push(...xpr.xpr); i += not ? 3 : 2; } else rc.push(expr[i]); } else rc.push(expr[i]); } return rc; function createXRef(lhspaths, rhspaths, lhs, rhs, lhsIsVal, rhsIsVal) { // mapping dict for lhs/rhs for mismatch check // strip lhs/rhs prefix from flattened paths to check remaining common trailing path // if path is idempotent, it doesn't produce new flattened paths (ends on scalar type) // key is then empty string on both sides '' (=> equality) // Path matches if lhs/rhs are available let xref; if(!lhsIsVal) { xref = lhspaths.reduce((a, v) => { a[v.ref.slice(lhs.ref.length).join('.')] = rhsIsVal ? { lhs: v, rhs } : { lhs: v }; return a; }, Object.create(null)); rhspaths.forEach(v => { const k = v.ref.slice(rhs.ref.length).join('.'); if(xref[k]) xref[k].rhs = v; else xref[k] = { rhs: v }; }); } else if(!rhsIsVal) { xref = rhspaths.reduce((a, v) => { a[v.ref.slice(rhs.ref.length).join('.')] = lhsIsVal ? { lhs, rhs: v } : { rhs: v }; return a; }, Object.create(null)); lhspaths.forEach(v => { const k = v.ref.slice(lhs.ref.length).join('.'); if(xref[k])