UNPKG

@sap/cds-compiler

Version:

CDS (Core Data Services) compiler and backends

806 lines (742 loc) 31.7 kB
'use strict'; const { applyTransformations, applyTransformationsOnNonDictionary, copyAnnotations, implicitAs, findAnnotationExpression, } = require('../../model/csnUtils'); const { isBuiltinType, isMagicVariable } = require('../../base/builtins'); const transformUtils = require('../transformUtils'); const { csnRefs } = require('../../model/csnRefs'); const { setProp } = require('../../base/model'); const { forEach } = require('../../utils/objectUtils'); const { transformExpression } = require('./applyTransformations'); const { cloneCsnNonDict } = require('../../model/cloneCsn'); const { EdmTypeFacetNames } = require('../../edm/EdmPrimitiveTypeDefinitions'); const { adaptAnnotationsRefs } = require('../odata/adaptAnnotationRefs'); /** * Strip off leading $self from refs where applicable. * Only relevant for HDBCDS, because handling of `$self` is not implemented there. * * @param {object} parent * @param {string} prop * @param {CSN.Elements} elements */ function removeLeadingSelf( parent, prop, elements ) { for (const [ elementName, element ] of Object.entries(elements)) { if (element.on) { applyTransformationsOnNonDictionary(elements, elementName, { ref: (root, name, ref) => { // HDBCDS renderers seem to expect it to not be there... if (ref[0] === '$self' && ref.length > 1 && !isMagicVariable(ref[1]) && ref[1] !== '$projection' && ref[1] !== '$self') root.ref.shift(); }, }); } } } /** * Resolve type references and turn things with `.items` into elements of type `LargeString`. * * Also, replace actions, events and functions with simply dummy artifacts. * * @param {CSN.Model} csn * @param {CSN.Options} options * @param {object} messageFunctions Message functions such as `error()`, `info()`, … * @param {WeakMap} resolved Cache for resolved refs * @param {string} pathDelimiter * @param {object} iterateOptions */ function resolveTypeReferences( csn, options, messageFunctions, resolved, pathDelimiter, iterateOptions = {} ) { /** * Remove .localized from the element and any sub-elements * * Only direct .localized usage should produce "localized things". * If we don't remove it here, the second compile step adds localized stuff again. * * @param {object} obj */ function removeLocalized( obj ) { const stack = [ obj ]; while (stack.length > 0) { const current = stack.pop(); if (current.localized) delete current.localized; if (current.elements) stack.push(...Object.values(current.elements)); } } const { toFinalBaseType, csnUtils } = transformUtils.getTransformers(csn, options, messageFunctions, pathDelimiter); // We don't want to iterate over actions if (iterateOptions.skipDict && !iterateOptions.skipDict.actions) iterateOptions.skipDict.actions = true; else iterateOptions.skipDict = { actions: true }; const replaceWithDummyKinds = { action: 1, function: 1, event: 1 }; const stripItems = options.transformation === 'hdbcds' || options.transformation === 'sql'; const removeItems = new Set(); applyTransformations(csn, { type: (node, prop, type, path, parent, parentProp) => { if (parentProp === 'cast') { const e = csnUtils.getFinalTypeInfo(type, t => resolved.get(t)?.art || csnUtils.artifactRef(t)); if (e.items && stripItems) removeItems.add(node); if (!e || e.items || e.elements) return; } if (!isBuiltinType(type)) { toFinalBaseType(node, resolved, true); if (node.items && stripItems) { removeItems.add(node); } else { if (node.items) // items could have unresolved types toFinalBaseType(node.items, resolved, true); // structured types might not have the child-types replaced. // Drill down to ensure this. const nextElements = node.elements || node.items?.elements; const stack = nextElements ? [ nextElements ] : []; while (stack.length > 0) { const elements = stack.pop(); for (const e of Object.values(elements)) { toFinalBaseType(e, resolved, true); if (stripItems && e.items) { removeItems.add(e); } else { if (!options.toOdata && e.items) // items could have unresolved types toFinalBaseType(e.items, resolved, true); const next = e.elements || e.items?.elements; if (next) stack.push(next); } } } } const directLocalized = node.localized || false; if (!directLocalized && !options.toOdata) removeLocalized(node); } }, items: node => removeItems.add(node), }, [ (definitions, artifactName, artifact) => { // Replace events, actions and functions with simple dummies - they don't have effect on forRelationalDB stuff // and that way they contain no references and don't hurt. // Do not do for OData // TODO:factor out somewhere else if (!options.toOdata && artifact.kind in replaceWithDummyKinds) { const dummy = { kind: artifact.kind }; if (artifact.kind === 'event') dummy.elements = {}; // events must be structured for recompilation if (artifact.$location) setProp(dummy, '$location', artifact.$location); definitions[artifactName] = dummy; } // TODO: skipDict options as default function arguments not via Object.assign } ], iterateOptions); // no support for array-of - turn into CLOB/Text for (const node of removeItems) { node.type = 'cds.LargeString'; delete node.items; } removeItems.clear(); } /** * @param {CSN.Model} csn * @param {CSN.Options} options * @param {object} messageFunctions Message functions such as `error()`, `info()`, … * @param {WeakMap} resolved Cache for resolved refs * @param {string} pathDelimiter * @param {object} iterateOptions */ function flattenAllStructStepsInRefs( csn, options, messageFunctions, resolved, pathDelimiter, iterateOptions = {} ) { const adaptRefs = []; applyTransformations(csn, getStructStepsFlattener(csn, options, messageFunctions, resolved, pathDelimiter, adaptRefs), [], iterateOptions); adaptRefs.forEach(fn => fn()); } /** * @param {CSN.Model} csn * @param {CSN.Options} options * @param {object} messageFunctions Message functions such as `error()`, `info()`, … * @param {WeakMap} resolved Cache for resolved refs * @param {string} pathDelimiter * @param {Function[]} adaptRefs * @returns {object} applyTransformations transformer */ function getStructStepsFlattener( csn, options, messageFunctions, resolved, pathDelimiter, adaptRefs ) { const { inspectRef, effectiveType } = csnRefs(csn); const { flattenStructStepsInRef } = transformUtils.getTransformers(csn, options, messageFunctions, pathDelimiter); /** * For each step of the links, check if there is a type reference. * If there is, resolve it and store the result in a WeakMap. * * @param {Array} [links] * @todo seems too hacky * @returns {WeakMap} A WeakMap where a link is the key and the type is the value */ function resolveLinkTypes( links = [] ) { const resolvedLinkTypes = new WeakMap(); links.forEach((link) => { const { art } = link; if (art && art.type) resolvedLinkTypes.set(link, effectiveType(art)); }); return resolvedLinkTypes; } const transformer = { // @ts-ignore ref: (parent, prop, ref, path, _parent, _prop, context) => { const { links, art, scope } = inspectRef(path); const resolvedLinkTypes = resolveLinkTypes(links); setProp(parent, '$path', [ ...path ]); const lastRef = ref[ref.length - 1]; const fn = (suspend = false, suspendPos = 0, refFilter = _parent => true) => { let refChanged = false; if (refFilter(parent)) { const scopedPath = [ ...parent.$path ]; // TODO: If foreign key annotations should be assigned via // full path into target, uncomment this line and // comment/remove setProp in expansion.js // setProp(parent, '$structRef', parent.ref); const flattenParameters = false; // structured parameters remain structured [ parent.ref, refChanged ] = flattenStructStepsInRef(ref, scopedPath, links, scope, resolvedLinkTypes, suspend, suspendPos, parent.$bparam, flattenParameters); resolved.set(parent, { links, art, scope }); // Explicitly set implicit alias for things that are now flattened - but only in columns // TODO: Can this be done elegantly during expand phase already? if (parent.$implicitAlias) { // an expanded s -> s.a is marked with this - do not add implicit alias "a" there, we want s_a if (parent.ref[parent.ref.length - 1] === parent.as) // for a simple s that was expanded - for s.substructure this would not apply delete parent.as; delete parent.$implicitAlias; } // To handle explicitly written s.a - add implicit alias a, since after flattening it would otherwise be s_a else if (parent.ref[parent.ref.length - 1] !== lastRef && (insideColumns(scopedPath) || insideKeys(scopedPath)) && !parent.as) { parent.as = lastRef; } } return refChanged; }; if (context?.$annotation) { const annotation = context.$annotation.value; adaptRefs.push((...args) => { const refChanged = fn(...args); if (refChanged && annotation['=']) annotation['='] = true; }); } else { // adapt queries later adaptRefs.push(fn); } }, }; /** * Return true if the path points inside columns * * @param {CSN.Path} path * @returns {boolean} */ function insideColumns( path ) { return path.length >= 3 && (path[path.length - 3] === 'SELECT' || path[path.length - 3] === 'projection') && path[path.length - 2] === 'columns'; } /** * Return true if the path points inside keys * * @param {CSN.Path} path * @returns {boolean} */ function insideKeys( path ) { return path.length >= 3 && path[path.length - 2] === 'keys' && typeof path[path.length - 1] === 'number'; } return transformer; } /** * @param {CSN.Model} csn * @param {CSN.Options} options * @param {object} messageFunctions Message functions such as `error()`, `info()`, … * @param {string} pathDelimiter * @param {object} iterateOptions */ function flattenElements( csn, options, messageFunctions, pathDelimiter, iterateOptions = {} ) { const { error } = messageFunctions; const { flattenStructuredElement, csnUtils } = transformUtils.getTransformers(csn, options, messageFunctions, pathDelimiter); const { isAssocOrComposition, effectiveType } = csnUtils; const transformers = { elements: flatten, }; applyTransformations(csn, transformers, [], iterateOptions); /** * Flatten a given .elements or .params dictionary - keeping the order consistent. * * @param {object} parent The parent object having dict at prop - parent[prop] === dict * @param {string} prop * @param {object} dict * @param {CSN.Path} path */ function flatten( parent, prop, dict, path ) { if (!parent[prop].$orderedElements) setProp(parent[prop], '$orderedElements', []); forEach(dict, (elementName, element) => { if (element.elements) { // Ignore the structured element, replace it by its flattened form element.$ignore = true; const branches = getBranches(element, elementName, effectiveType, pathDelimiter); const flatElems = flattenStructuredElement(element, elementName, [], path.concat([ 'elements', elementName ])); for (const flatElemName in flatElems) { if (parent[prop][flatElemName]) { // TODO: combine message ID with generated FK duplicate // do the duplicate check in the construct callback, requires to mark generated flat elements, // check: Error location should be the existing element like @odata.foreignKey4 error('name-duplicate-element', path.concat([ 'elements', elementName ]), { '#': 'flatten-element-exist', name: flatElemName }); } const flatElement = flatElems[flatElemName]; // Check if we have a valid notNull chain const branch = branches[flatElemName].steps; if (flatElement.notNull !== false && !branch.some(s => !s.notNull)) flatElement.notNull = true; if (flatElement.type && isAssocOrComposition(flatElement) && flatElement.on) { // unmanaged relations can't be primary key delete flatElement.key; if (options.transformation !== 'effective') { const process = endIndex => function processRef(_parent, _prop, xpr) { const prefix = flatElement._flatElementNameWithDots.split('.').slice(0, endIndex).join(pathDelimiter); const possibleFlatName = prefix + pathDelimiter + xpr[0]; /* when element is defined in the current name resolution scope, like entity E { key x: Integer; s : { y : Integer; a3 : association to E on a3.x = y; } } We need to replace y with s_y and a3 with s_a3 - we must take care to not escape our local scope */ if (flatElems[possibleFlatName]) xpr[0] = possibleFlatName; }; transformExpression(flatElement, 'on', { ref: process(-1), }); } } parent[prop].$orderedElements.push([ flatElemName, flatElement ]); // Still add them - otherwise we might not detect collisions between generated elements. parent[prop][flatElemName] = flatElement; } } else { parent[prop].$orderedElements.push([ elementName, element ]); } }); // $orderedElements is removed by reducing and assigning a new dictionary parent[prop] = parent[prop].$orderedElements.reduce((elements, [ name, element ]) => { // rewrite $path to match the flattened dictionary entry // ([ 'definitions', artName ] remain constant setProp(element, '$path', [ ...path, prop, name ]); elements[name] = element; return elements; }, Object.create(null)); } } /** * Get not just the leaves, but all branches of a structured element. * * @param {object} element Structured element * @param {string} elementName Name of the structured element * @param {Function} effectiveType * @param {string} pathDelimiter * @returns {object} Returns a dictionary, where the key is the flat name of the branch and the value is an array of element-steps. */ function getBranches( element, elementName, effectiveType, pathDelimiter ) { const branches = {}; const subbranchNames = []; const subbranchElements = []; walkElements(element, elementName); /** * Walk the element chain * * @param {object} e * @param {string} name */ function walkElements( e, name ) { if (isBuiltinType(e.type)) { branches[subbranchNames.concat(name).join(pathDelimiter)] = { steps: subbranchElements.concat(e), ref: subbranchNames.concat(name) }; } else { const subelements = e.elements || effectiveType(e).elements; if (subelements) { subbranchElements.push(e); subbranchNames.push(name); for (const [ subelementName, subelement ] of Object.entries(subelements)) walkElements(subelement, subelementName); subbranchNames.pop(); subbranchElements.pop(); } else { branches[subbranchNames.concat(name).join(pathDelimiter)] = { steps: subbranchElements.concat(e), ref: subbranchNames.concat(name) }; } } } return branches; } /** * @param {CSN.Model} csn * @param {CSN.Options} options * @param {object} messageFunctions Message functions such as `error()`, `info()`, … * @param {string} pathDelimiter * @param {boolean} flattenKeyRefs * @param {object} csnUtils * @param {object} iterateOptions */ function handleManagedAssociationsAndCreateForeignKeys( csn, options, messageFunctions, pathDelimiter, flattenKeyRefs, csnUtils, iterateOptions = {} ) { const { error } = messageFunctions; const { inspectRef, isStructured } = csnUtils; const { flattenStructStepsInRef, flattenStructuredElement } = transformUtils.getTransformers(csn, options, messageFunctions, pathDelimiter); if (flattenKeyRefs) { applyTransformations(csn, { keys: (element, prop, keys, path) => { // replace foreign keys that are managed associations by their respective foreign keys flattenFKs(element, path.at(-1), path); }, }, [], Object.assign({ skipIgnore: false, allowArtifact: artifact => (artifact.kind === 'entity' || artifact.kind === 'type'), skipDict: { actions: true }, }, iterateOptions)); } createForeignKeyElements(); /** * Flattens all foreign keys * * Structures will be resolved to individual elements with scalar types * * Associations will be replaced by their respective foreign keys * * If a structure contains an assoc, this will also be resolved and vice versa * * @param {*} assoc * @param {*} assocName * @param {*} path */ function flattenFKs( assoc, assocName, path ) { if (!assoc.keys) return; // managed to-many assoc // TODO Depth first search and not iterate mark and sweep approach let finished = false; while (!finished) { const newKeys = []; finished = processKeys(newKeys); assoc.keys = newKeys; } // @ts-ignore /** * Walk over the keys and replace structures by their leafs, managed associations by their foreign keys and keep scalar values as-is. * * @param {object[]} collector New keys array to collect the flattened stuff in * @returns {boolean} True if all keys are scalar - false if there are things that still need to be processed. */ function processKeys( collector ) { const inferredAlias = '$inferredAlias'; let done = true; for (let i = 0; i < assoc.keys.length; i++) { const pathToKey = path.concat([ 'keys', i ]); const { art } = inspectRef(pathToKey); const { ref } = assoc.keys[i]; if (isStructured(art)) { done = false; const flat = flattenStructuredElement(art, ref[ref.length - 1], [], pathToKey); Object.keys(flat).forEach((flatElemName) => { const key = assoc.keys[i]; const clone = cloneCsnNonDict(assoc.keys[i], options); if (clone.as) { const lastRef = clone.ref[clone.ref.length - 1]; // Cut off the last ref part from the beginning of the flat name const flatBaseName = flatElemName.slice(lastRef.length); // Join it to the existing table alias clone.as += flatBaseName; // do not loose the $ref for nested keys if (key.$ref) { let aliasedLeaf = key.$ref[key.$ref.length - 1]; aliasedLeaf += flatBaseName; setProp(clone, '$ref', key.$ref.slice(0, key.$ref.length - 1).concat(aliasedLeaf)); } } if (clone.ref) { clone.ref[clone.ref.length - 1] = flatElemName; // Now we need to properly flatten the whole ref [ clone.ref ] = flattenStructStepsInRef(clone.ref, pathToKey); } if (!clone.as) { clone.as = flatElemName; // TODO: can we use $inferred? Does it have other weird side-effects? setProp(clone, inferredAlias, true); } // Directly work on csn.definitions - this way the changes take effect in csnRefs/inspectRef immediately // Add the newly generated foreign keys to the end - they will be picked up later on // Recursive solutions run into call stack issues collector.push(clone); }); } else if (art.target) { done = false; // Directly work on csn.definitions - this way the changes take effect in csnRefs/inspectRef immediately // Add the newly generated foreign keys to the end - they will be picked up later on // Recursive solutions run into call stack issues art.keys?.forEach(key => collector.push(cloneAndExtendRef(key, assoc.keys[i], ref))); } else if (assoc.keys[i].ref && !assoc.keys[i].as) { setProp(assoc.keys[i], inferredAlias, true); assoc.keys[i].as = assoc.keys[i].ref[assoc.keys[i].ref.length - 1]; collector.push(assoc.keys[i]); } else { collector.push(assoc.keys[i]); } } return done; } /** * Clone base and extend the .ref and .as of the clone with the .ref and .as of ref. * * @param {object} key A foreign key entry (of a managed assoc as a fk of another assoc) * @param {object} base The fk-ref that has key as a fk * @param {Array} ref * @returns {object} The clone of base */ function cloneAndExtendRef( key, base, ref ) { const clone = cloneCsnNonDict(base, options ); if (key.ref) { // We build a ref that contains the aliased fk - that element will be created later on, so this ref is not resolvable yet // Therefore we keep it as $ref - ref is the non-aliased, resolvable "clone" // Later on, after we know that these foreign key elements are created, we replace ref with this $ref let $ref; if (base.$ref) { // if a base $ref is provided, use it to correctly resolve association chains const refChain = [ base.$ref[base.$ref.length - 1] ].concat(key.as || key.ref); $ref = base.$ref.slice(0, base.$ref.length - 1).concat(refChain); } else { $ref = base.ref.concat(key.as || key.ref); // Keep along the aliases } setProp(clone, '$ref', $ref); clone.ref = clone.ref.concat(key.ref); } if (!clone.as && clone.ref && clone.ref.length > 0) { clone.as = ref[ref.length - 1] + pathDelimiter + (key.as || key.ref.join(pathDelimiter)); // TODO: can we use $inferred? Does it have other weird side-effects? setProp(clone, '$inferredAlias', true); } else { clone.as += pathDelimiter + (key.as || key.ref.join(pathDelimiter)); } return clone; } } /** * Create the foreign key elements in all .elements things */ function createForeignKeyElements() { const transformers = { elements: createFks, }; applyTransformations(csn, transformers, [], Object.assign({ skipIgnore: false }, iterateOptions)); /** * Process a given .elements or .params dictionary and create foreign key elements * * @param {object} parent The thing HAVING params or elements * @param {string} prop * @param {object} dict The params or elements thing * @param {CSN.Path} path */ function createFks( parent, prop, dict, path ) { const orderedElements = []; Object.entries(dict).forEach(([ elementName, element ]) => { orderedElements.push([ elementName, element ]); const eltPath = path.concat(prop, elementName); const fks = createForeignKeys(csnUtils, eltPath, element, elementName, csn, options, pathDelimiter); // finalize the generated foreign keys const refCount = fks.reduce((acc, fk) => { // count duplicates if (acc[fk[0]]) acc[fk[0]]++; else acc[fk[0]] = 1; // check for name clash with existing elements if (parent[prop][fk[0]]) { // error location is the colliding element error('name-duplicate-element', eltPath, { '#': 'flatten-fkey-exists', name: fk[0], art: elementName }); } // attach a proper $path setProp(element, '$path', eltPath); return acc; }, Object.create(null)); // set default for single foreign key from association (if available) if (element.default?.val !== undefined && fks.length === 1) fks[0][1].default = element.default; // check for duplicate foreign keys Object.entries(refCount).forEach(([ name, occ ]) => { if (occ > 1) error('name-duplicate-element', eltPath, { '#': 'flatten-fkey-gen', name, art: elementName }); }); if (element.keys) { element.keys.forEach((key, i) => { // Assumption: If all key refs have been flattened, there is a // 1:1 match to the corresponding foreign key element. Order is the // same, so an index access should work if (flattenKeyRefs) { key.$generatedFieldName = fks[i][0]; key.ref = [ (key.$ref || key.ref).join(pathDelimiter) ]; delete key.$ref; const fk = fks[i][1]; if (options.transformation === 'effective') copyAnnotations(key, fk); } }); if (options.transformation === 'effective') delete element.default; } if (options.transformation === 'effective') { adaptAnnotationsRefs(fks, csnUtils, messageFunctions, eltPath); const validAnnoNames = Object.keys(element).filter(pn => pn[0] === '@' && findAnnotationExpression(element, pn)); fks.forEach(fk => copyAnnotations(element, fk[1], false, {}, validAnnoNames)); } orderedElements.push(...fks); }); parent[prop] = orderedElements.reduce((elementsAccumulator, [ name, element ]) => { elementsAccumulator[name] = element; return elementsAccumulator; }, Object.create(null)); } } } /** * This is the internal version of the foreign key procedure. * * If element is not a managed association, an empty array is returned * * @param {object} csnUtils * @param {Array|object} path CSN path pointing to element or the result of a previous call to inspectRef * @param {CSN.Element} element * @param {string} prefix Element name * @param {CSN.Model} csn * @param {object} options * @param {string} pathDelimiter * @param {number} lvl * @param {object} originalKey * @returns {Array[]} First element of every sub-array is the foreign key name, second is the foreign key definition */ function createForeignKeys( csnUtils, path, element, prefix, csn, options, pathDelimiter, lvl = 0, originalKey = { }) { const special$self = !csn?.definitions?.$self && '$self'; const isInspectRefResult = !Array.isArray(path); let fks = []; if (!element) return fks; let finalElement = element; let finalTypeName; // TODO: Find a way to not rely on $path? // TODO: effectiveType's return value is 'path' for the next inspectRef if (element.type && !isBuiltinType(element.type) && element.type !== special$self) { const tmpElt = csnUtils.effectiveType(element); // effective type resolves to structs and enums only but not scalars if (Object.keys(tmpElt).length) { finalElement = tmpElt; finalTypeName = finalElement.$path[1]; } else { // unwind a derived type chain to a scalar type while (finalElement?.type && !isBuiltinType(finalElement?.type)) { finalTypeName = finalElement.type; finalElement = csn.definitions[finalElement.type]; } } } if (!finalElement) return []; if (finalElement.target && !finalElement.on) { finalElement.keys?.forEach((key, keyIndex) => { const continuePath = getContinuePath([ 'keys', keyIndex ]); const alias = key.as || implicitAs(key.ref); const result = csnUtils.inspectRef(continuePath); fks = fks.concat(createForeignKeys(csnUtils, result, result.art, alias, csn, options, pathDelimiter, lvl + 1, lvl === 0 ? { ref: key.ref, as: key.as, $path: key.$path, $originalKeyRef: key.$originalKeyRef, } : originalKey)); }); } // return if the toplevel element is not a managed association else if (lvl === 0) { return fks; } else if (finalElement.elements) { Object.entries(finalElement.elements).forEach(([ elemName, elem ]) => { // Skip already produced foreign keys if (!elem['@odata.foreignKey4']) { const continuePath = getContinuePath([ 'elements', elemName ]); fks = fks.concat(createForeignKeys(csnUtils, continuePath, elem, elemName, csn, options, pathDelimiter, lvl + 1, originalKey)); } }); } // we have reached a leaf element, create a foreign key else if (finalElement.type == null || isBuiltinType(finalElement.type)) { const newFk = Object.create(null); [ 'type', 'length', 'scale', 'precision', 'srid', 'default', '@odata.Type', ...EdmTypeFacetNames.map(f => `@odata.${ f }`) ].forEach((prop) => { // copy props from original element to preserve derived types! if (element[prop] !== undefined) newFk[prop] = element[prop]; }); return [ [ prefix, newFk, originalKey ] ]; } fks.forEach((fk) => { // prepend current prefix fk[0] = `${ prefix }${ pathDelimiter }${ fk[0] }`; // if this is the entry association, decorate the final foreign keys with the association props if (lvl === 0) { if (options.transformation !== 'effective') fk[1]['@odata.foreignKey4'] = prefix; if (options.transformation === 'odata' || options.transformation === 'effective') { const validAnnoNames = Object.keys(element).filter(pn => pn[0] === '@' && !findAnnotationExpression(element, pn)); copyAnnotations(element, fk[1], true, {}, validAnnoNames); } // propagate not null to final foreign key for (const prop of [ 'notNull', 'key' ]) { if (element[prop] !== undefined) fk[1][prop] = element[prop]; } if (element.$location) setProp(fk[1], '$location', element.$location); } }); return fks; /** * Get the path to continue resolving references * * If we are currently inside of a type, we need to start our path fresh from that given type. * Otherwise, we would try to resolve .elements on a thing that does not exist. * * We also respect if we have a previous inspectRef result as our base. * * @param {Array} additions * @returns {CSN.Path} */ function getContinuePath( additions ) { if (csn.definitions[finalElement.type]) return [ 'definitions', finalElement.type, ...additions ]; else if (finalTypeName) return [ 'definitions', finalTypeName, ...additions ]; else if (isInspectRefResult) return [ path, ...additions ]; return [ ...path, ...additions ]; } } module.exports = { resolveTypeReferences, flattenAllStructStepsInRefs, flattenElements, removeLeadingSelf, handleManagedAssociationsAndCreateForeignKeys, getBranches, getStructStepsFlattener, };