UNPKG

@sap/cds

Version:

SAP Cloud Application Programming Model - CDS for Node.js

300 lines (257 loc) 10.4 kB
const _normalizedRef = o => (o && o.ref && o.ref.length > 1 && o.ref[0] === '$self' ? { ref: o.ref.slice(1) } : o) const _sub = (newOn, subOns = []) => { let currArr = [] for (let i = 0; i < newOn.length; i++) { const onEl = newOn[i] if (onEl === 'or') { // abort condition for or subOns.push([]) return subOns } if (onEl.xpr) { _sub(onEl.xpr, subOns) continue } if (currArr.length === 0 && onEl !== 'and') { subOns.push(currArr) } if (onEl !== 'and') { currArr.push(onEl) } else { currArr = [] } } return subOns } const _getSubOns = element => { // this only works for on conds with `and`, once we support `or` this needs to be adjusted // TODO : check that no 'or' is in on const newOn = element.on || [] const subOns = _sub(newOn) for (const subOn of subOns) { // We don't support anything else than // A = B AND C = D AND ... if (subOn.length !== 3) return [] } return subOns.map(subOn => subOn.map(ref => _normalizedRef(ref))) } const _parentFieldsFromSimpleOnCond = (element, subOn) => { const idxChildField = subOn.findIndex(o => o.ref && o.ref[0] === element.name) if (idxChildField === -1 || subOn[1] !== '=') return const childFieldName = subOn[idxChildField].ref && subOn[idxChildField].ref.slice(1).join('_') const childElement = element._target.elements[childFieldName] const idxParentField = idxChildField === 2 ? 0 : 2 let parentRef = Array.isArray(subOn[idxParentField].ref) && [...subOn[idxParentField].ref] if (parentRef && parentRef.length > 1) { const idxChildInParent = parentRef.findIndex(e => e === element.name) if (idxChildInParent > -1) parentRef.splice(idxChildInParent, 1) parentRef = [parentRef.join('_')] } const parentElement = parentRef && element.parent.elements[parentRef[0]] if (!childElement) { // update on view with key in parent return [{ fillChild: false, parentElement, childElement }] } if (!childElement.on) { const propagations = [] if ('val' in subOn[idxParentField]) propagations.push({ fillChild: true, parentFieldValue: subOn[idxParentField].val, childElement }) if (element._isSelfManaged) return [...propagations, ..._foreignKeyPropagationsFromToManyOn(element, childFieldName)] if (parentElement) return [...propagations, { fillChild: true, parentElement, childElement }] } if ('val' in subOn[idxParentField]) { return [{ fillChild: true, parentFieldValue: subOn[idxParentField].val, childElement }] } if (childElement._isAssociationStrict && childElement.on) { return _foreignKeyPropagationsFromCustomBacklink(element, childElement) } } const _foreignKeyPropagationsFromToManyOn = (element, childFieldName) => { const foreignKeys = _foreignKeysForTarget(element, childFieldName) // REVISIT foreignKeys is empty if we have deep operations where a sub element is annotated with persistence skip if (foreignKeys && foreignKeys.length) { return _resolvedKeys(foreignKeys, true) } return [] } const _foreignKeyPropagationsFromCustomBacklink = (element, childElement) => { const foreignKeyPropagations = [] const subOns = _getSubOns(childElement) for (const subOn of subOns) { if (subOn[1] === '=') { const parentFieldIdx = subOn.findIndex(o => o.ref && o.ref[0] === childElement.name) const otherFieldIdx = parentFieldIdx === 0 ? 2 : 0 const otherField = subOn[otherFieldIdx] if (parentFieldIdx === -1 && subOn[otherFieldIdx === 0 ? 2 : 0].val !== undefined) { const parentField = subOn[otherFieldIdx === 0 ? 2 : 0] foreignKeyPropagations.push({ fillChild: false, parentFieldValue: parentField.val, childElement: element._target.elements[otherField.ref[0]] }) } else if (otherField.ref && otherField.ref.length === 1) { const parentFieldName = subOn[parentFieldIdx].ref[1] foreignKeyPropagations.push({ fillChild: true, parentElement: element.parent.elements[parentFieldName], childElement: element._target.elements[otherField.ref[0]] }) } else if (otherField.val !== undefined) { const parentFieldName = subOn[parentFieldIdx] && subOn[parentFieldIdx].ref[1] const parentField = subOn[otherFieldIdx === 2 ? 0 : 2] foreignKeyPropagations.push({ fillChild: true, parentElement: element.parent.elements[parentFieldName], parentFieldValue: parentField.val, childFieldValue: otherField.val }) } } } return foreignKeyPropagations } const _foreignKeyPropagationsFromOn = element => { const subOns = _getSubOns(element) const foreignKeyPropagations = [] for (const subOn of subOns) { const subParentFields = _parentFieldsFromSimpleOnCond(element, subOn) if (subParentFields) foreignKeyPropagations.push(...subParentFields) } return foreignKeyPropagations } const _resolveTargetForeignKey = targetKey => { const targetName = targetKey._foreignKey4 if (!targetName) return const parentElements = targetKey.parent.elements const _foreignKeyProps = foreignKeyPropagations(parentElements[targetName]) const propagation = _foreignKeyProps.find(_fkp => _fkp.parentElement && targetKey.name === _fkp.parentElement.name) return { targetName, propagation } } const _resolveColumnsFromQuery = query => { if (query && query.SET) return _resolveColumnsFromQuery(query.SET.args[0]) if (query && query.SELECT && query.SELECT.columns) return query.SELECT.columns return [] } const _resolvedKeys = (keys, fillChild) => { const foreignKeys = fillChild ? keys.map(fk => Object.getPrototypeOf(fk)) : keys const targetKeys = fillChild ? keys : keys.map(fk => Object.getPrototypeOf(fk)) const foreignKeyPropagations = [] for (let i = 0; i < foreignKeys.length; i++) { const fk = foreignKeys[i] const tk = targetKeys[i] const propagation = { fillChild, parentElement: fk, childElement: tk, // needed only for child -> parent propagation since template loops in other direction deep: !fillChild && _resolveTargetForeignKey(tk) } foreignKeyPropagations.push(propagation) } return foreignKeyPropagations } const foreignKeyPropagations = element => { if (element.is2many && element.on) { return _foreignKeyPropagationsFromOn(element) } if (element.is2one) { if (element.on) { // It's a link through a backlink return _foreignKeyPropagationsFromOn(element) } const foreignKeys = _foreignKeys(element) if (foreignKeys) return _resolvedKeys(foreignKeys, false) } return [] } // REVISIT: Flattening shouldn't be necessary in the future. // It's better to deal with structures instead, but // that would require changing a lot of code. const _foreignKeys = element => { const foreignKeys = element.foreignKeys const path = [element.name] const parent = element.parent const result = [] _addToForeignKeysRec(foreignKeys, path, parent, result) return result } /* * REVISIT: poor man's look-up of target key * Look at elements, then try to find it in query and resolve recursively until you have the full path. * Once you have the full path, you can find it in the target entity. * NOTE: There can be projections upon projections and renamings in every projection. -> not yet covered!!! */ const _poorMansLookup = (el, name, foreignKeySource) => { // REVISIT: Dirty hack const tkCol = _resolveColumnsFromQuery(el.parent.query).find( c => c.ref && `${foreignKeySource}_${c.ref.join('_')}` === name ) return tkCol && Object.values(el.parent.elements).find(tk => tk.name === (tkCol.as ? tkCol.as : tkCol.ref.join('_'))) } const _createForeignKey = (name, el, parent, foreignKeySource) => { const tk = _poorMansLookup(el, name, foreignKeySource) const navigationCsn = parent.elements[foreignKeySource] const key = navigationCsn.key // IMPORTANT: Object.create is used to override inherited non-enumerable properties. Object.assign would not work. const foreignKeyCsn = Object.create(tk || el, { parent: { value: parent }, name: { value: name }, key: { value: key }, foreignKeySource: { value: foreignKeySource } }) // REVISIT: Overwrite previously defined annotations, maybe there's a better way. // We might need to be careful with cached information (__xxx) for (const prop in tk || el) { if (prop.startsWith('@')) foreignKeyCsn[prop] = undefined } for (const key in navigationCsn) { if (!key.startsWith('@')) continue foreignKeyCsn[key] = navigationCsn[key] } if ('notNull' in navigationCsn) foreignKeyCsn.notNull = navigationCsn.notNull return foreignKeyCsn } const _addToForeignKeysRec = (elements, path, parent, result) => { for (const elName in elements) { const el = elements[elName] const foreignKeySource = path[0] const newPath = [...path, elName] if (el.isAssociation) { const foreignKeysOftarget = _foreignKeys(el) for (const fk of foreignKeysOftarget) { const name = [...path, fk.name].join('_') if (result.some(x => x.name === name)) return const foreignKeyCsn = _createForeignKey(name, fk, parent, foreignKeySource) result.push(foreignKeyCsn) } } else if (!el.elements) { const name = newPath.join('_') if (result.some(x => x.name === name)) return const foreignKeyCsn = _createForeignKey(name, el, parent, foreignKeySource) result.push(foreignKeyCsn) } else _addToForeignKeysRec(el.elements, newPath, parent, result) } } const _foreignKeysForTarget = (csnElement, name) => { const target = csnElement._target.elements[name || csnElement.name] return _foreignKeys(target) } const foreignKey4 = element => { if (!element || !element.parent) return const parentElements = element.parent.elements for (const assoc of Object.keys(parentElements) .map(n => parentElements[n]) .filter(e => e.isAssociation)) { const foreignKeys = _foreignKeys(assoc) if (!foreignKeys.length) continue const target = foreignKeys.find(fk => fk.name === element.name) if (target) { return target && target.foreignKeySource } } } module.exports = { foreignKeyPropagations, foreignKey4 }