UNPKG

@sap/cds-compiler

Version:

CDS (Core Data Services) compiler and backends

915 lines (837 loc) 34.6 kB
// Generate: localized data and managed compositions 'use strict'; /* eslint-disable no-nested-ternary */ const { isDeprecatedEnabled } = require('../base/specialOptions'); const { dictAdd } = require('./dictionaries'); const { setLink, setArtifactLink, setAnnotation, linkToOrigin, setMemberParent, createAndLinkCalcDepElement, augmentPath, isDirectComposition, copyExpr, forEachGeneric, } = require('./utils'); const { weakLocation, weakRefLocation, weakEndLocation } = require('../base/location'); const $location = Symbol.for( 'cds.$location' ); function generate( model ) { const { options } = model; // Get simplified "resolve" functionality and the message function: const { error, warning, info, } = model.$messageFunctions; const Functions = model.$functions; const { resolvePath, resolveUncheckedPath, initMainArtifact, extendArtifactBefore, applyIncludes, } = model.$functions; Object.assign( model.$functions, { hasTruthyProp, generateForEntity, populateGeneratedEntity, checkGenerateConditions, } ); const commonLanguages = model.definitions['sap.common.Languages']; const textsAspect = model.definitions['sap.common.TextsAspect']; let enableLanguageAssoc = null; // → addLanguageAssoc() let enableTextsAspect = null; // → useTextsAspect() return; /** * Process "composition of" artifacts. * * @param {string} name */ function generateForEntity( art ) { // TODO: write dependency on base entity for final generateForEntity() if (!art.$duplicates && art.elements) { processAspectComposition( art ); processLocalizedData( art ); } } function addLanguageAssoc( report = false ) { if (!options.addTextsLanguageAssoc) return false; if (!report && enableLanguageAssoc != null) return enableLanguageAssoc; const codeElement = Functions.effectiveType( commonLanguages )?.elements?.code; // eslint-disable no-nested-ternary const err = (!commonLanguages || commonLanguages.$inferred ) ? 'missing' : (commonLanguages.kind !== 'entity' || commonLanguages.query) ? 'kind' : (!codeElement || codeElement.$inferred) ? 'code' : null; // TODO: better config of eslint @stylistic/indent if (report && err) { const loc = commonLanguages?.name?.location || null; warning( 'api-ignoring-language-assoc', [ loc, commonLanguages ], { '#': err, option: 'addTextsLanguageAssoc', art: 'sap.common.Languages', name: 'code', }, { std: 'Ignoring option $(OPTION)', missing: 'Ignoring option $(OPTION) because entity $(ART) is missing', kind: 'Ignoring option $(OPTION) because entity $(ART) is no entity', code: 'Ignoring option $(OPTION) because entity $(ART) is missing direct element $(NAME)', } ); } enableLanguageAssoc = (err == null); return enableLanguageAssoc; } /** * Check that special `sap.common.*` aspects for `.texts` entities are * consistent with compiler expectations. Emits messages and returns * false if the aspects are not valid. * * @return {boolean} */ function useTextsAspect( report = false ) { if (!report && enableTextsAspect != null) return enableTextsAspect; const locale = Functions.effectiveType( textsAspect )?.elements?.locale; const err = (!textsAspect || textsAspect.$inferred ) ? false : (textsAspect.kind !== 'aspect' || !textsAspect.elements) ? 'no-aspect' : (!locale || locale.$inferred) ? 'missing' : (locale.key?.val ? null : 'key'); // TODO: better eslint @stylistic/indent if (report && err) { const loc = (err === 'key' ? locale : textsAspect)?.name?.location || null; error( 'def-invalid-texts-aspect', [ loc, (err === 'key' ? locale : textsAspect) ], { '#': err, art: textsAspect, name: 'locale' } ); } enableTextsAspect = (err == null); return enableTextsAspect; } function checkGenerateConditions() { addLanguageAssoc( true ); if (!useTextsAspect( true )) return; if (enableLanguageAssoc && textsAspect.elements.language) { const lang = textsAspect.elements.language; error( 'def-unexpected-element', [ lang.name.location, lang ], { option: 'addTextsLanguageAssoc', art: textsAspect, name: 'language' }, 'Element $(NAME) of $(ART) is not used because option $(OPTION) is set' ); } for (const name in textsAspect.elements) { const elem = textsAspect.elements[name]; const include = elem.$inferred === 'include'; if (elem.key && name !== 'locale') { const loc = include ? elem.location : elem.key.location; error( 'def-unexpected-key', [ loc, elem ], { '#': !include ? 'std' : 'include', art: textsAspect } ); } else if (hasTruthyProp( elem, 'localized' )) { // TODO: T:loc, i.e. "localized" from other type (needs resolver?) // Not supported anyway, but important for recompilation (which fails correctly). const loc = elem.localized?.location || elem.location; error( 'def-unexpected-localized', [ loc, elem ], { '#': !include ? 'elements' : 'include', art: textsAspect } ); } else if (elem.targetAspect) { error( 'def-unexpected-composition', [ elem.targetAspect.location, elem ], { art: textsAspect }, '$(ART) can\'t have composition of aspects' ); } } } // localized texts entities --------------------------------------------------- /** * Process localized data for `art`. This includes creating `.texts` entities * and `locale` associations. * * @param {XSN.Artifact} art */ function processLocalizedData( art ) { // do not create texts entity for a texts entity (might be induced by erroneous // sap.common.TextsAspect): if (art.$inferred === 'localized-entity') return; // TODO: test with `annotate … with @fiori.draft.enabled` const fioriAnno = art['@fiori.draft.enabled']; const fioriEnabled = fioriAnno && (fioriAnno.val === undefined || !!fioriAnno.val); const textsName = `${ art.name.id }.texts`; const textsEntity = model.definitions[textsName]; const localized = localizedData( art, textsEntity, fioriEnabled ); if (!localized) return; if (textsEntity && textsEntity.kind !== 'namespace') return; // expanded texts entity in source -> nothing to do createTextsEntity( art, textsName, localized, fioriEnabled ); addTextsAssociations( art, textsName, localized ); } /** * Returns `false`, if there is no localized data or an array of elements * that are required for `.texts` entities such as keys and localized elements. * * @param {XSN.Artifact} art * @param {XSN.Artifact|undefined} textsEntity * @param {boolean} fioriEnabled * @returns {false|XSN.Element[]} */ function localizedData( art, textsEntity, fioriEnabled ) { let keys = 0; const textElems = []; const conflictingElements = []; // These elements are required or the localized-mechanism does not work. // Other elements from sap.common.TextsAspect may be "overridden" as per // usual include-mechanism. const protectedElements = [ 'locale', 'texts', 'localized' ]; if (fioriEnabled) protectedElements.push( 'ID_texts' ); if (addLanguageAssoc()) protectedElements.push( 'language' ); for (const name in art.elements) { const elem = art.elements[name]; if (elem.$duplicates) return false; // no localized-data unfold with redefined elems if (protectedElements.includes( name )) conflictingElements.push( elem ); const isKey = elem.key && elem.key.val; const isLocalized = elem.$syntax !== 'calc' && hasTruthyProp( elem, 'localized' ); if (isKey) { keys += 1; textElems.push( elem ); } else if (isLocalized) { textElems.push( elem ); } if (isKey && isLocalized) { const errpos = elem.localized || elem.type || elem.name; warning( 'def-ignoring-localized', [ errpos.location, elem ], { keyword: 'localized' }, 'Keyword $(KEYWORD) is ignored for primary keys' ); // continuation semantics as stated: counts as key field in texts entity } } if (textElems.length <= keys) return false; if (!keys) { warning( 'def-expecting-key', [ art.name.location, art ], {}, 'No texts entity can be created when no key element exists' ); return false; } if (textsEntity?.kind === 'namespace') { // namespace Base.texts textsEntity = null; } else if (textsEntity) { if (textsEntity.$duplicates) return false; if (textsEntity.kind !== 'entity' || textsEntity.query || // already have elements "texts" and "localized" (and optionally ID_texts) conflictingElements.length !== 2 || art.elements.locale || (fioriEnabled && art.elements.ID_texts)) { // TODO if we have too much time: check all elements of texts entity for safety warning( null, [ art.name.location, art ], { art: textsEntity }, // eslint-disable-next-line @stylistic/max-len 'Texts entity $(ART) can\'t be created as there is another definition with that name' ); info( null, [ textsEntity.name.location, textsEntity ], { art }, 'Texts entity for $(ART) can\'t be created with this definition' ); } else if (!art._block || art._block.$frontend !== 'json') { info( null, [ art.name.location, art ], {}, 'Localized data expansions has already been done' ); return textElems; // make double-compilation even with after toHana } else if (!art._block.$withLocalized && !options.$recompile) { art._block.$withLocalized = true; // no semantic loc: message only emitted once info( 'def-unexpected-texts-entities', [ art.name.location, null ], {}, 'Input CSN contains expansions for localized data' ); return textElems; // make compilation idempotent } else { return textElems; } } for (const elem of conflictingElements) { warning( null, [ elem.name.location, art ], { name: elem.name.id }, 'No texts entity can be created when element $(NAME) exists' ); } return !textsEntity && !conflictingElements.length && textElems; } /** * Create the `.texts` entity for the given base artifact. * * @param {XSN.Artifact} base * @param {string} absolute * @param {XSN.Element[]} textElems * @param {boolean} fioriEnabled */ function createTextsEntity( base, absolute, textElems, fioriEnabled ) { const location = weakLocation( base.elements[$location] || base.location ); const art = Functions.registerGeneratedEntity( { kind: 'entity', name: { id: absolute, location }, location, elements: Object.create( null ), $inferred: 'localized-entity', } ); setLink( art, '_block', model.$internal ); // console.log('Texts:',require('../model/revealInternalProperties').ref(art)) extendArtifactBefore( art ); // having extensions here would be wrong if (!fioriEnabled) { // To be compatible, we switch off draft without @fiori.draft.enabled setAnnotation( art, '@odata.draft.enabled', art.location, false ); } else { const textId = { name: { location, id: 'ID_texts' }, kind: 'element', key: { val: true, location }, type: linkMainArtifact( location, 'cds.UUID' ), location, }; art.elements.ID_texts = textId; } // _service links and "consider as composition target" must be set already here setLink( art, '_service', base._service ); model.$compositionTargets[absolute] = true; setGenExtensions( art, { _base: base, _textElems: textElems, fioriEnabled } ); } function populateTextsEntity( art, base, textElems, fioriEnabled ) { const { location } = art; const enrich = useTextsAspect() ? enrichTextsEntityWithInclude : enrichTextsEntityWithDefaultElements; enrich( art, fioriEnabled ); if (addLanguageAssoc()) { const language = { name: { location, id: 'language' }, kind: 'element', location, type: linkMainArtifact( location, 'cds.Association' ), target: linkMainArtifact( location, 'sap.common.Languages' ), on: { op: { val: '=', location }, args: [ { path: [ { id: 'language', location }, { id: 'code', location } ], location }, { path: [ { id: 'locale', location } ], location }, ], location, }, }; setLink( language, '_block', model.$internal ); dictAdd( art.elements, 'language', language ); } // assertUnique array value, first entry is 'locale' const assertUniqueValue = []; for (const orig of textElems) addElementToTextsEntity( orig, art, fioriEnabled, assertUniqueValue ); initMainArtifact( art ); // do the kick-start relevant stuff: _service, there are no _ancestors, // _descendants would have been set already for a gap artifact if (art.includes) { // i.e. sap.common.TextsAspect // do not insert sap.common.TextsAspect elements before `locale`: // add elements `locale`, etc. which are required below. art.includes.$original = []; // do not apply via extendArtifactAdd() applyIncludes( art, art ); // apply now } if (fioriEnabled) { // The includes mechanism puts TextsAspect's elements before .texts' elements. // Because ID_texts is not copied from TextsAspect, the order is messed // up. Fix it. TODO: introduce $includeAfter from Extensions.md const { elements } = art; art.elements = Object.create( null ); const names = [ 'ID_texts', 'locale', ...Object.keys( elements ) ]; for (const name of names) art.elements[name] = elements[name]; const { locale } = art.elements; assertUniqueValue.unshift({ path: [ { id: locale.name.id, location: locale.location } ], location: locale.location, }); setAnnotation( art, '@assert.unique.locale', art.location, assertUniqueValue, 'array' ); } // TODO: first copy persistence annos from cds.TextsAspect ? copyPersistenceAnnotations( art, base ); return art; } function addElementToTextsEntity( orig, art, fioriEnabled, assertUniqueValue ) { const elem = linkToOrigin( orig, orig.name.id, art, 'elements' ); // To keep the locations of non-inferred original elements, do not set $inferred: if (orig.$inferred) elem.$inferred = 'localized-origin'; const { location } = elem; if (orig.key && orig.key.val) { // elem.key = { val: fioriEnabled ? null : true, $inferred: 'localized', location }; // TODO: the previous would be better, but currently not supported in toCDL if (!fioriEnabled) { elem.key = { val: true, $inferred: 'localized', location }; // If the propagated elements remain key (that is not fiori.draft.enabled) // they should be omitted from OData containment EDM setAnnotation( elem, '@odata.containment.ignore', location ); } else { // add the former key paths to the unique constraint assertUniqueValue.push( { path: [ { id: orig.name.id, location } ], location } ); } } if (hasTruthyProp( orig, 'localized' )) { // use location of LOCALIZED keyword elem.localized = { val: null, $inferred: 'localized', location }; } } /** * Enrich the `.texts` entity for the given base artifact. * In contrast to createTextsEntityWithDefaultElements(), this one creates * an include for `sap.common.TextsAspect`. * * Does NOT apply the include! * * @param {XSN.Artifact} art * @param {boolean} fioriEnabled */ function enrichTextsEntityWithInclude( art, fioriEnabled ) { const textsAspectName = 'sap.common.TextsAspect'; const { location } = art; art.includes = [ createInclude( textsAspectName, location ) ]; propagateEarly( art, '@cds.autoexpose' ); propagateEarly( art, '@fiori.draft.enabled' ); if (fioriEnabled) { // "Early" include; only for element `locale`, which has its `key` property // removed (or rather: it is not copied). linkToOrigin( textsAspect.elements.locale, 'locale', art, 'elements', location ); art.elements.locale.$inferred = 'localized'; } if (addLanguageAssoc() && art.elements.language) art.elements.language = undefined; // TODO: Message? Ignore? // TODO: what is this necessary? We do not create a text entity in this case } /** * @param {XSN.Artifact} art * @param {boolean} fioriEnabled */ function enrichTextsEntityWithDefaultElements( art, fioriEnabled ) { // If there is a type `sap.common.Locale`, then use it as the type for the element `locale`. // If not, use the default `cds.String` with a length of 14. const hasLocaleType = model.definitions['sap.common.Locale']?.kind === 'type'; const { location } = art; // is already a weak location const locale = { name: { location, id: 'locale' }, kind: 'element', type: linkMainArtifact( location, hasLocaleType ? 'sap.common.Locale' : 'cds.String' ), location, $inferred: 'localized', // $generated in Universal CSN, no $location }; if (!hasLocaleType) locale.length = { literal: 'number', val: 14, location }; if (!fioriEnabled) locale.key = { val: true, location }; dictAdd( art.elements, 'locale', locale ); } /** * @param {XSN.Artifact} art * @param {string} textsName * @param {XSN.Element[]} textElems */ function addTextsAssociations( art, textsName, textElems ) { // texts : Composition of many Books.texts on texts.ID=ID; /** @type {array} */ const keys = textElems.filter( e => e.key && e.key.val ); const location = weakEndLocation( art.elements[$location] ) || weakLocation( art.location ); const texts = { name: { location, id: 'texts' }, kind: 'element', location, $inferred: 'localized', type: linkMainArtifact( location, 'cds.Composition' ), cardinality: { targetMax: { literal: 'string', val: '*', location }, location }, target: linkMainArtifact( location, textsName ), on: augmentEqual( location, 'texts', keys ), }; setMemberParent( texts, 'texts', art, 'elements' ); setLink( texts, '_block', model.$internal ); // localized : Association to Books.texts on // localized.ID=ID and localized.locale = $user.locale; keys.push( [ 'localized.locale', '$user.locale' ] ); const localized = { name: { location, id: 'localized' }, kind: 'element', location, $inferred: 'localized', type: linkMainArtifact( location, 'cds.Association' ), target: linkMainArtifact( location, textsName ), on: augmentEqual( location, 'localized', keys ), }; setMemberParent( localized, 'localized', art, 'elements' ); setLink( localized, '_block', model.$internal ); } /** * Create a structure that can be used as an item in `includes`. * TODO: replace by linkMainArtifact() * * @param {string} name * @param {XSN.Location} location */ function createInclude( name, location ) { const include = { path: [ { id: name, location } ], location, }; setArtifactLink( include.path[0], model.definitions[name] ); setArtifactLink( include, model.definitions[name] ); return include; } /** * Returns whether `art` directly or indirectly has the property 'prop', * following the 'origin' and the 'type' (not involving elements). * * DON'T USE FOR ANNOTATIONS (see TODO below) * * TODO: we should issue a warning if we get localized via TYPE OF * TODO: XSN: for anno short form, use { val: true, location, <no literal prop> } * ...then this function also works with annotations * * @param {XSN.Artifact} art * @param {string} prop * @returns {boolean} */ function hasTruthyProp( art, prop ) { const processed = Object.create( null ); // avoid infloops with circular refs let name = (art._main || art).name.id; // is ok, since no recursive type possible while (art && !processed[name]) { if (art[prop]) return art[prop].val; processed[name] = art; if (art._origin) { art = art._origin; if (!art.name) // anonymous aspect return false; name = (art._main || art)?.name?.id; } else if (art.type) { // TODO: also do something special for TYPE OF inside `art`s own elements // TODO: check for own - add test case with Type:elem (not TYPE OF elem) name = resolveUncheckedPath( art.type, 'type', art ); art = name && model.definitions[name]; } else { return false; } } return false; } // managed composition of aspects ------------------------------------------ function processAspectComposition( base ) { // TODO: we need to forbid COMPOSITION of entity w/o keys and ON anyway // TODO: consider entity includes // TODO: nested containment // TODO: better do circular checks in the aspect! if (base.kind !== 'entity' || base.query) return; const keys = baseKeys(); if (keys) forEachGeneric( base, 'elements', expand ); // TODO: recursively here? return; function baseKeys() { const k = Object.create( null ); for (const name in base.elements) { const elem = base.elements[name]; if (elem.$duplicates) return false; // no composition-of-type unfold with redefined elems if (elem.key?.val) k[name] = elem; } return k; } function expand( elem ) { if (elem.target) return; let origin = elem; // included element do not have target aspect directly // (remark: this will get far more complex with compositions of aspects // in sub elements) while (origin && !origin.targetAspect && origin._origin) origin = origin._origin; let target = origin.targetAspect; if (target?.path) { target = resolvePath( origin.targetAspect, 'targetAspect', origin ); Functions.effectiveType( target ); // should have been good! } if (!target || !target.elements) return; const entityName = `${ base.name.id }.${ elem.name.id }`; const entity = allowAspectComposition( target, elem, keys, entityName ) && createTargetEntity( target, elem, keys, entityName, base ); elem.target = { location: (elem.targetAspect || elem).location, $inferred: 'aspect-composition', }; setArtifactLink( elem.target, entity ); if (entity) { // Support using the up_ element in the generated entity to be used // inside the anonymous aspect: const { up_ } = target.$tableAliases; // TODO: invalidate "up_" alias (at least further navigation) if it // already has an _origin (when the managed composition is included) if (up_) setLink( up_, '_origin', entity.elements.up_ ); model.$compositionTargets[entity.name.id] = true; } } } /** * @returns {boolean|0} `true`, if allowed, `false` if forbidden, `0` if circular containment. */ function allowAspectComposition( target, elem, keys, entityName ) { if (!target.elements || Object.values( target.elements ).some( e => e.$duplicates )) return false; // no elements or with redefinitions const location = elem.targetAspect?.location || elem.location; if ((elem._main._upperAspects || []).includes( target )) return 0; // circular containment of the same aspect const keyNames = Object.keys( keys ); if (!keyNames.length) { // TODO: for "inner aspect-compositions", signal already in type error( null, [ location, elem ], { target }, 'An aspect $(TARGET) can\'t be used as target in an entity without keys' ); return false; } const place = model.definitions[entityName]; if (place && place.kind !== 'namespace') { error( null, [ location, elem ], { art: entityName }, // eslint-disable-next-line @stylistic/max-len 'Target entity $(ART) can\'t be created as there is another definition with this name' ); return false; } if (elem.type && !isDirectComposition( elem )) { // Only issue warning for direct usages, not for projections, includes, etc. // TODO: Make it configurable error; v6: error // TODO: move to resolve.js where we test the targetAspect, warning( 'type-expecting-composition', [ elem.type.location, elem ], { newcode: 'Composition of', code: 'Association to' }, 'Expecting $(NEWCODE), not $(CODE) for the anonymous target aspect' ); // auto-correct to avoid additional error 'type-unexpected-target-aspect' if // cds.Association: const { path, $inferred } = elem.type; if (!$inferred && path?.length === 1 && path[0].id === 'cds.Association') path[0].id = 'cds.Composition'; } return true; } function createTargetEntity( target, elem, keys, entityName, base ) { // Remark: keys for v2 option deprecated.unmanagedUpInComponent const location = weakRefLocation( elem.targetAspect || elem.target || elem ); // Since there is no user-written up_ element, use a weak location to the beginning of {…}. elem.on = { // element on base entity location, op: { val: '=', location }, args: [ augmentPath( location, elem.name.id, 'up_' ), augmentPath( location, '$self' ), ], $inferred: 'aspect-composition', }; const up_ = { // elements.up_ = ... name: { location, id: 'up_' }, kind: 'element', location, key: { location, val: true }, notNull: { location, val: true }, // managed associations must be explicitly set to not null // even if target cardinality is 1..1 $inferred: 'aspect-composition', type: linkMainArtifact( location, 'cds.Association' ), target: linkMainArtifact( location, base.name.id ), cardinality: { targetMin: { val: 1, literal: 'number', location }, targetMax: { val: 1, literal: 'number', location }, location, }, }; const art = Functions.registerGeneratedEntity( { kind: 'entity', name: { id: entityName, // for code navigation (e.g. via `extend`s): point to the element's name location: weakLocation( elem.name.location ), }, location, elements: { __proto__: null, up_ }, $inferred: 'composition-entity', } ); // console.log('Composition:',require('../model/revealInternalProperties').ref(art)) setLink( art, '_block', model.$internal ); // do the kick-start relevant stuff: _service, there are no _ancestors, // _descendants would have been set already for a gap artifact setLink( art, '_service', base._service ); model.$compositionTargets[entityName] = true; // Apply annotations to generated artifact, prepare (not apply!) element // annotations (remark: adding elements is not allowed for generated artifacts): extendArtifactBefore( art ); setGenExtensions( art, { _composition: elem } ); setLink( art, '_origin', target ); // TODO: does this hurt? return art; } function populateTargetEntity( art, elem ) { const location = weakRefLocation( elem.targetAspect || elem.target || elem ); const targetAspect = art._origin; Functions.effectiveType( targetAspect ); const { elements } = targetAspect; if (elements.up_) { // TODO: for "inner aspect-compositions", signal already in type // TODO: if anonymous type, use location of "up_" element // FUTURE: if named type, add sub info with location of "up_" element error( null, [ location, elem ], { target: targetAspect, name: 'up_' }, 'An aspect $(TARGET) with an element named $(NAME) can\'t be used as target' ); delete elements.up_; // continuation semantics: don't use up_ from aspect } if (targetAspect.name) { // named target aspect if (!isDeprecatedEnabled( options, 'noCompositionIncludes' )) { art.includes = [ createInclude( targetAspect.name.id, location ) ]; art.includes.$origin = []; // included elements after up_ // TODO: propagate in effectiveType() propagateEarly( art, '@cds.autoexpose' ); propagateEarly( art, '@fiori.draft.enabled' ); } setLink( art, '_upperAspects', [ targetAspect, ...(elem._main._upperAspects || []) ] ); } else { // TODO: do we need to give the anonymous target aspect a kind and name? setLink( art, '_upperAspects', elem._main._upperAspects || [] ); // TODO: _upperAspects can probably now be removed } Functions.effectiveType( elem ); // To keep the locations of non-inferred original elements, do not set $inferred: const enforceLocation = targetAspect.name || elem.$inferred; addProxyElements( art, elements, 'aspect-composition', enforceLocation && location ); initMainArtifact( art ); // Copy persistence annotations from aspect. if (targetAspect.kind === 'aspect') // proper aspect copyPersistenceAnnotations( art, targetAspect ); // after extendArtifactBefore() copyPersistenceAnnotations( art, elem._parent ); return art; } function populateGeneratedEntity( art ) { const gen = art._extensions?.$gen; if (gen?._composition) populateTargetEntity( art, gen._composition ); else if (gen?._textElems) populateTextsEntity( art, gen._base, gen._textElems, gen.fioriEnabled ); } function addProxyElements( proxyDict, elements, inferred, location, prefix = '', anno = '' ) { // TODO: also use for includeMembers()? Both are similar. Combine! for (const name in elements) { const pname = `${ prefix }${ name }`; const origin = elements[name]; const proxy = linkToOrigin( origin, pname, null, null, location, true ); setLink( proxy, '_block', origin._block ); if (location) proxy.$inferred = inferred; if (origin.masked) proxy.masked = Object.assign( { $inferred: 'include' }, origin.masked ); if (origin.key) proxy.key = Object.assign( { $inferred: 'include' }, origin.key ); if (origin.value && origin.$syntax === 'calc') { // TODO: If paths become invalid in the new artifact, should we mark // all usages in the expressions? Possibly just the first one? // TODO: Unify with coding in extend.js proxy.value = Object.assign( { $inferred: 'include' }, copyExpr( origin.value )); proxy.$syntax = 'calc'; createAndLinkCalcDepElement( proxy ); // TODO: re-check _calcOrigin setLink( proxy, '_calcOrigin', origin._calcOrigin || origin ); } if (anno) setAnnotation( proxy, anno ); dictAdd( proxyDict.elements, pname, proxy ); } } /** * Copy relevant annotations from * source to target if present on source but not target. * * Persistence annos from target/text aspect have precedence. * * @param {object} target * @param {object} source */ function copyPersistenceAnnotations( target, source ) { if (!source) return; // Copied since v6 const copyJournal = !isDeprecatedEnabled( options, 'noPersistenceJournalForGeneratedEntities' ); if (copyJournal) copy( '@cds.persistence.journal' ); const copyExists = !isDeprecatedEnabled( options, '_eagerPersistenceForGeneratedEntities' ); if (copyExists) copy( '@cds.persistence.exists' ); copy( '@cds.persistence.skip' ); copy( '@cds.tenant.independent' ); /** @param {string} anno */ function copy( anno ) { if ( source[anno] && !target[anno] ) target[anno] = { ...source[anno], $inferred: 'parent-origin' }; } } function linkMainArtifact( location, absolute ) { const r = { location, path: [ { id: absolute, location } ], scope: 'global' }; setArtifactLink( r, model.definitions[absolute] ); return r; } } function augmentEqual( location, assocname, relations, prefix = '' ) { const args = relations.map( eq ); return (args.length === 1) ? args[0] : { op: { val: 'and', location }, args, location }; function eq( refs ) { if (Array.isArray( refs )) return { op: { val: '=', location }, args: refs.map( ref ), location }; const { id } = refs.name; return { op: { val: '=', location }, args: [ { path: [ { id: assocname, location }, { id, location } ], location }, { path: [ { id: `${ prefix }${ id }`, location } ], location }, ], location, }; } function ref( path ) { return { path: path.split( '.' ).map( id => ({ id, location }) ), location }; } } /** * Propagate the given `prop` (e.g. annotation) early, i.e. copy it from all `.includes` * if they have the property. * TEMPORARY copy from ./extend.js * * @param {XSN.Definition} art * @param {string} prop */ function propagateEarly( art, prop ) { if (art[prop]) return; for (const ref of art.includes) { const aspect = ref._artifact; if (aspect) { const anno = aspect[prop]; if (anno && (anno.val !== null || !art[prop])) art[prop] = Object.assign( { $inferred: 'include' }, anno ); } } } function setGenExtensions( art, gen ) { const $gen = {}; for (const prop of Object.keys( gen )) { Object.defineProperty( $gen, prop, { configurable: true, enumerable: prop.charAt( 0 ) !== '_', value: gen[prop], writable: true, } ); } if (art._extensions) art._extensions.$gen = $gen; else art._extensions = { $gen }; } module.exports = generate;