UNPKG

@sap/eslint-plugin-cds

Version:

ESLint plugin including recommended SAP Cloud Application Programming model and environment rules

184 lines (176 loc) 5.69 kB
'use strict' const cds = require('@sap/cds') /** @type {import('../types').Rule} */ module.exports = { meta: { schema: [{/* to avoid deprecation warning for ESLint 9 */}], docs: { description: 'Ambiguous key with a `TO MANY` relationship since entries could appear multiple times with the same key.', category: 'Model Validation', recommended: true, url: 'https://cap.cloud.sap/docs/tools/cds-lint/rules/assoc2many-ambiguous-key', }, messages: { ambiguous: `Ambiguous key in '{{name}}'. Element '{{column-name}}' leads to multiple entries so that key '{{key-name}}' is not unique.`, }, type: 'problem', model: 'inferred' }, create (context) { return checkAssocs function checkAssocs () { let csnOdata const m = context.getModel() if (!m) return if (m && m.definitions) { try { csnOdata = cds.compile.for.odata(m) const csnOdataLinked = cds.linked(csnOdata) associationCardinalityFlaw(csnOdataLinked, context) } catch { // FIXME: there are currently too many issues with this rule, e.g. // assumptions that properties exist, etc. } } } } } /** * @param {object} csn * @param {CDSRuleContext} context */ function associationCardinalityFlaw (csn, context) { processEntity(csn, (definition, sourceEntity, sourceAlias) => { let refCardinalityMult = false let refPlainElement = false processElement( csn, definition, sourceEntity, sourceAlias, () => { refCardinalityMult = false refPlainElement = false }, (refEntity, refElement) => { if (refElement.type === 'cds.Association' || refElement.type === 'cds.Composition') { if (refElement.cardinality && refElement.cardinality.max === '*') { refCardinalityMult = true } } else { refPlainElement = true } }, column => { if ( definition.keys && Object.keys(definition.keys).length === 1 && Object.keys(definition.keys)[0] === 'ID' && refCardinalityMult && refPlainElement ) { const keyName = Object.keys(definition.keys)[0] const key = definition.keys[keyName] const keyLoc = context.getLocation(keyName, key, csn) const colName = column.as ? column.as : column.name context.report({ messageId: 'ambiguous', data: { name: definition.name, 'column-name': colName, 'key-name': keyName }, loc: keyLoc, file: key.$location.file }) } } ) }) } /** * @param {object} csn * @param {Function} eachCallback */ function processEntity (csn, eachCallback) { Object.keys(csn.definitions).forEach(name => { if (name.startsWith('localized.')) { return } const definition = csn.definitions[name] if ( definition.kind === 'entity' && definition.query && definition.query.SELECT && definition.query.SELECT.columns ) { let sourceEntity const sourceAlias = [] if (definition.query.SELECT.from.ref) { // From sourceEntity = csn.definitions[definition.query.SELECT.from.ref.join('_')] sourceAlias.push({ from: sourceEntity.name, as: definition.query.SELECT.from.as || definition.query.SELECT.from.ref.slice(-1)[0].split('.').pop() }) } else if (definition.query.SELECT.from.args && definition.query.SELECT.from.args[0].ref) { // Join sourceEntity = csn.definitions[definition.query.SELECT.from.args[0].ref.join('_')] definition.query.SELECT.from.args.forEach(arg => { sourceAlias.push({ from: arg.ref.join('_'), as: arg.as || arg.ref.slice(-1)[0].split('.').pop() }) }) } if (!sourceEntity) { return } eachCallback(definition, sourceEntity, sourceAlias) } }) } /** * @param {object} csn * @param {object} definition * @param {object} sourceEntity * @param {string} sourceAlias * @param {Function} beforeCallback * @param {Function} eachCallback * @param {Function} afterCallback */ function processElement (csn, definition, sourceEntity, sourceAlias, beforeCallback, eachCallback, afterCallback) { definition.query.SELECT.columns.forEach(column => { if (column.ref && column.ref.length > 1) { let refEntity = sourceEntity let refAlias = sourceAlias beforeCallback() column.ref.forEach(ref => { ref = ref.id || ref // Alias const matchAlias = refAlias.find(alias => { return alias.as === ref }) let refElement if (matchAlias) { refEntity = csn.definitions[matchAlias.from] } else { refElement = refEntity.elements[ref] // Mixin if (!refElement) { refElement = definition.elements[ref] if (!refElement && definition.query.SELECT.mixin) { refElement = definition.query.SELECT.mixin[ref] if (!refElement && definition.query.SELECT.mixin[column.ref[0]]) { refElement = definition.query.SELECT.mixin[column.ref[0]]._target.elements[ref] } } } eachCallback(refEntity, refElement) if (refElement.type === 'cds.Association' || refElement.type === 'cds.Composition') { refEntity = csn.definitions[refElement.target] } } refAlias = [] }) afterCallback(column) } }) }