UNPKG

@sap/cds-mtxs

Version:

SAP Cloud Application Programming Model - Multitenancy library

169 lines (144 loc) 6.57 kB
const LinterMessage = require('./message') const { Allowlist, isBuiltIn } = require('./config') // annotations with specific checks or messages const checkedExtensionAnnotations = new Map([ ['@requires', _createSecurityAnnotationWarning], ['@restrict', _createSecurityAnnotationWarning], ['@cds.persistence.journal', _createJournalAnnotationWarning], ['@mandatory', _createMandatoryAnnotationWarning], ['@readonly', _createMandatoryAnnotationWarning], ['@assert.notNull', _createMandatoryAnnotationWarning], ['@assert.range', _createMandatoryAnnotationWarning] ]) const criticalNewEntityAnnotations = /@sql.prepend|@sql.append|@cds.persistence.(?!skip)\w*/ const criticalExtensionAnnotations = new RegExp('^@(?:' + 'requires' + '|restrict' + '|readonly' + '|mandatory' + '|assert.*' + '|cds.persistence.*' + '|sql.append' + '|sql.prepend' // service annotations + '|path' + '|impl' + '|cds.autoexpose' + '|cds.api.ignore' + '|odata.etag' + '|cds.query.limit' + '|cds.localized' + '|cds.valid.*' + '|cds.search)' ); function locationString(element) { const loc = element?.$location if (!loc) return '' const line = loc.line ? `${loc.line}:` : '' return loc.col ? `${loc.file}:${line}${loc.col}:` : `${loc.file}:${line}` } function _createGenericAnnotationWarning(annotationName, { element, parent, name }) { const message = `Annotation '${annotationName}' in '${ element.annotate || element.extend || element.name ||parent?.extend || parent.annotate || name }' is not supported in extensions` return new LinterMessage(locationString(element) + message, element) } function _createMandatoryAnnotationWarning(annotationName, { element, parent }) { if (element.default || element[annotationName] === false) return const message = `Annotation '${annotationName}' in '${ element.annotate || element.name || parent.extend || parent.annotate }' is not supported in extensions without default value` return new LinterMessage(locationString(element) + message, element) } function _createSecurityAnnotationWarning(annotationName, { element }) { const message = `Security relevant annotation '${annotationName}' in '${ element.annotate || element.name }' cannot be overwritten` return new LinterMessage(locationString(element) + message, element) } function _createJournalAnnotationWarning(annotationName, { element }) { const message = `Enabling schema evolution in extensions using '${annotationName}' in '${ element.annotate || element.name }' not supported` return new LinterMessage(locationString(element) + message, element) } function _createJournalEntityExtensionNotAllowedWarning(element) { const message = `Extending entity '${element.extend}' is not supported as the corresponding database table has been enabled for schema evolution` return new LinterMessage(locationString(element) + message, element) } module.exports = class AnnotationsChecker { check(reflectedCsn, fullCsn, compileDir, mtxConfig = {}, originalExtCsn) { const allowList = new Allowlist(mtxConfig, fullCsn) if (!reflectedCsn.extensions && !reflectedCsn.definitions) { return [] } const annotationExtensions = [] const messages = [] // check annotations for extensions including fields reflectedCsn.forall( (element) => !isBuiltIn(element.name), (element, name, parent) => { if (Object.getOwnPropertyNames(element).filter(property => property.startsWith('@') && criticalExtensionAnnotations.test(property)).length) { annotationExtensions.push({ element, name, parent }) } if (element.extend) { // check base entity for incompatible annotations this._checkExtendedEntityAnnotations(fullCsn, element, messages) // checks e. g. for journal annotations in base entity } }, reflectedCsn.extensions ?? {} ) // check entities and fields from new definitions const annotatedDefinitions = [] reflectedCsn.forall( (element) => !isBuiltIn(element.name), (element, name, parent) => { if (Object.getOwnPropertyNames(element).filter(property => criticalNewEntityAnnotations.test(property)).length) { annotatedDefinitions.push({element, name, parent}) } }, reflectedCsn.definitions ?? {} ) for (const annotation of [...annotationExtensions, ...annotatedDefinitions]) { const warnings = this._checkExtensionAnnotation(annotation, reflectedCsn, fullCsn, compileDir, allowList, originalExtCsn) if (warnings?.length) { messages.push(...warnings); } } return messages } _checkExtensionAnnotation(annotation, extCsn, fullCsn, dir, allowList, originalExtCsn) { const { element, parent, name } = annotation const entityOrService = element.annotate ?? element.extend ?? parent?.annotate ?? parent?.extend ?? element.name ?? name const annotationNames = Object.getOwnPropertyNames(element).filter(property => property.startsWith('@')) if (!originalExtCsn.definitions[entityOrService] || annotationNames.filter(property => criticalNewEntityAnnotations.test(property)).length) { const warnings = [] for (const annotationName of annotationNames) { if (!criticalExtensionAnnotations.test(annotationName)) continue // get element permissions from allowlist const kind = this._getExtendedKind(fullCsn, extCsn, entityOrService) const permissions = allowList.getPermission(kind, entityOrService) // check if annotation is allowed if (permissions && permissions.annotations && permissions.annotations.includes(annotationName)) return null const fn = checkedExtensionAnnotations.get(annotationName) ?? _createGenericAnnotationWarning const warning = fn(annotationName, annotation); if (warning) { warnings.push(warning); } } return warnings } return null } _getExtendedKind(fullCsn, reflectedExtensionCsn, extendedEntity) { const elementFromBase = fullCsn.definitions?.[extendedEntity] || reflectedExtensionCsn.definitions?.[extendedEntity] return elementFromBase ? elementFromBase.kind : null } _checkExtendedEntityAnnotations(fullCsn, element, messages) { const kind = fullCsn.definitions[element.extend]?.kind if (kind === 'entity' && fullCsn.definitions[element.extend]?.['@cds.persistence.journal']) { messages.push(_createJournalEntityExtensionNotAllowedWarning(element)) } } }