UNPKG

@sap/cds-compiler

Version:

CDS (Core Data Services) compiler and backends

346 lines (324 loc) 13.9 kB
'use strict'; const { forEachMember, forEachMemberRecursively, cardinality2str, hasPersistenceSkipAnnotation, } = require('../model/csnUtils'); const { isBuiltinType } = require('../base/builtins'); const { isGeoTypeName } = require('../compiler/builtins'); const { setProp } = require('../base/model'); // Only to be used with validator.js - a correct `this` value needs to be provided! /** * Checks artifact's primary keys and an error is registered if some of the keys * is of type 'cds.hana.ST_POINT', 'cds.hana.ST_GEOMETRY' or if it is arrayed * * @param {CSN.Artifact} art The artifacts that will be checked */ function checkPrimaryKey( art ) { if (art.kind !== 'entity' && art.kind !== 'aspect') return; forEachMember(art, (member, memberName, prop, path) => { checkIfPrimaryKeyIsOfGeoType.bind(this)(member, memberName); checkIfPrimaryKeyIsArray.bind(this)(member, memberName); if (member.elements) { forEachMemberRecursively(member, (subMember, subMemberName) => { checkIfPrimaryKeyIsOfGeoType.bind(this)(subMember, subMemberName, member.key); checkIfPrimaryKeyIsArray.bind(this)(subMember, subMemberName, member.key); }, path); } }); /** * * @param {CSN.Element} member The member * @param {string} elemFqName Full name of the element following the structure, * concatenated with '/', used for error reporting * @param {boolean} parentIsKey Whether parent is a key * @param {CSN.Path} parentPath The path of the parent element (optional) */ function checkIfPrimaryKeyIsOfGeoType( member, elemFqName, parentIsKey, parentPath ) { if (member.key || parentIsKey) { const finalBaseType = this.csnUtils.getFinalTypeInfo(member.type); if (isGeoTypeName(finalBaseType?.type)) { this.error('ref-unsupported-type', parentPath || member.$path, { '#': 'key', type: finalBaseType.type, name: elemFqName }); } else if (finalBaseType && this.csnUtils.isStructured(finalBaseType) && !finalBaseType.$visited) { setProp(finalBaseType, '$visited', true); forEachMemberRecursively(finalBaseType, (subMember, subMemberName) => checkIfPrimaryKeyIsOfGeoType .bind(this)(subMember, `${ elemFqName }/${ subMemberName }`, member.key || parentIsKey, member.$path)); delete finalBaseType.$visited; } } } /** * * @param {CSN.Element} member The member * @param {string} elemFqName Full name of the element following the structure, * concatenated with '/', used for error reporting * @param {boolean} parentIsKey Whether parent is a key * @param {CSN.Path} parentPath The path of the parent element (optional) */ function checkIfPrimaryKeyIsArray( member, elemFqName, parentIsKey, parentPath ) { if (member.key || parentIsKey) { const finalBaseType = this.csnUtils.getFinalTypeInfo(member.type); if (member.items || (finalBaseType && finalBaseType.items)) { let msg = 'std'; if (member.target) msg = this.csnUtils.isComposition(member) ? 'comp' : 'assoc'; this.error('def-invalid-key-cardinality', parentPath || member.$path, { name: elemFqName, value: cardinality2str(member, false), '#': msg, }, 'Array-like type in element $(NAME) can\'t be used as primary key'); } else if (finalBaseType && this.csnUtils.isStructured(finalBaseType) && !finalBaseType.$visited) { setProp(finalBaseType, '$visited', true); forEachMemberRecursively(finalBaseType, (subMember, subMemberName) => checkIfPrimaryKeyIsArray .bind(this)(subMember, `${ elemFqName }/${ subMemberName }`, member.key || parentIsKey, member.$path)); delete finalBaseType.$visited; } } } } /** * Checks virtual elements and throws an error if some is either structured or * an association * * @param {CSN.Element} member Element to be checked */ function checkVirtualElement( member ) { if (member.virtual) { if (this.csnUtils.isAssocOrComposition(member)) this.error('def-unexpected-virtual', member.$path, { keyword: 'virtual' }); } } /** * Checks whether managed associations with cardinality 'to many' have no ON-condition. * If there isn't, and if _all_ key elements on the target side are covered by the foreign key, * then the association is effectively to-one -> warning. * * @param {CSN.Artifact} member The member (e.g. element artifact) */ function checkManagedAssoc( member ) { if (!member.target || isManagedComposition.bind(this)(member)) return; const targetMax = member.cardinality?.max ?? 1; if (targetMax === 1 || member.on) return; // Special case for "--with-mocks" of the cds cli: For testing databases such as H2 and SQLite, we allow // associations with neither on-condition nor foreign keys if the CSN is mocked. // See #13916 for details. const allowForMocked = this.csn._mocked && (this.options.sqlDialect === 'h2' || this.options.sqlDialect === 'sqlite'); const isPersisted = !hasPersistenceSkipAnnotation(this.artifact) && !this.artifact['@cds.persistence.exists']; if (isPersisted && !allowForMocked && !member.keys && (targetMax === '*' || Number(targetMax) > 1) && this.options.transformation === 'sql') { // Since cds-compiler v6, managed to-many no longer get 'keys'. // As this would lead to DROP COLUMNs, emit an error instead. this.error('type-missing-on-condition', member.cardinality?.$path || member.$path, { value: cardinality2str(member, false), '#': this.csnUtils.isComposition(member) ? 'comp' : 'std', }, { // same as 'to-many-no-on', but as error std: 'Expected association with target cardinality $(VALUE) to have an ON-condition', comp: 'Expected composition with target cardinality $(VALUE) to have an ON-condition', }); } // Implementation note: Imported services (i.e. external ones) may contain to-many associations // with an empty foreign key list. If the user (in this case importer) explicitly sets an empty // foreign key array, we won't emit a warning to avoid spamming the user. if (!member.keys || member.keys.length === 0) return; // We use the fact that `key` is only supported top-level (warning otherwise). // And if an element of a structured _type_ is "key", we get a warning for the key. // However, we may get false negatives for our warning, which is acceptable, e.g. for // `type T : { key i: String; }; entity A { id : T; };` with `… to many A { id };` const target = typeof member.target === 'object' ? member.target : this.csnUtils.getCsnDef(member.target); const targetKeys = Object.entries(target.elements || {}).filter(elem => !!elem[1].key); const foreignKeys = structurizeForeignKeys(member.keys); if (!coversAllTargetKeys.call(this, foreignKeys, targetKeys)) return; // foreign key does not cover at least one target key -> can be to-many this.warning(!isPersisted ? 'to-many-no-on-noDB' : 'to-many-no-on', member.cardinality?.$path || member.$path, { value: cardinality2str(member, false), '#': this.csnUtils.isComposition(member) ? 'comp' : 'std', }, { std: 'Expected association with target cardinality $(VALUE) to have an ON-condition', comp: 'Expected composition with target cardinality $(VALUE) to have an ON-condition', }); } /** * Returns true if the foreign keys cover _all_ of the target keys. * Returns false otherwise. * * @see checkManagedAssoc() * * @param {object} foreignKeys Structure returned by `structurizeForeignKeys()`. * @param {Array} targetKeys Object.entries() value of target keys. * @returns {boolean} Whether all target keys are covered */ function coversAllTargetKeys( foreignKeys, targetKeys ) { if (foreignKeys.length < targetKeys.length || targetKeys.length === 0) { // there are fewer foreign keys than keys on the target side // or there are no keys on the target side, in which case there is no // possibility to cover all keys. return false; } for (const [ targetKeyName, targetKey ] of targetKeys) { const foreignKey = foreignKeys.entries[targetKeyName]; if (!foreignKey) return false; // foreign key does not cover this target key if (foreignKey.length > 0) { // foreign key only selects sub-structures, not whole structured key const elements = targetKey.elements || this.csnUtils.getFinalTypeInfo(targetKey.type)?.elements; if (!elements) return false; // model error (e.g. 'many type') if (!coversAllTargetKeys( foreignKey, Object.entries(elements) )) return false; } } return true; } /** * Structurizes a foreign key into an object that can be used to compare foreign * keys against their corresponding keys on target side. * * For `Association to T { a.b, a.c, b }` this structure will be returned: * `{ length: 2, entries: { a: { length: 2, entries: { b: { length: 0, … }, …} } }}` * * @param {object[]} keys Foreign key array. * @returns {object} Structured foreign key. Custom format, i.e. not via `elements`. */ function structurizeForeignKeys( keys ) { const map = { entries: Object.create(null), length: 0 }; for (const key of keys) { let entry = map; for (const step of key.ref) { if (!entry.entries[step]) { entry.entries[step] = { entries: Object.create(null), length: 0 }; ++entry.length; } entry = entry.entries[step]; } } return map; } /** * * @param {CSN.Element} member The member * @returns {boolean} Whether the member is managed composition */ function isManagedComposition( member ) { if (member.targetAspect) return true; if (!member.target) return false; const target = typeof member.target === 'object' ? member.target : this.csnUtils.getCsnDef(member.target); return target.kind !== 'entity'; } /** * All DB & OData flat mode must reject recursive type usages in entities * 'items' break recursion as 'items' will turn into an NCLOB and the path * prefix to 'items' can be flattened in the DB. * In OData flat mode the first appearance of 'items' breaks out into structured * mode producing (legal) recursive complex types. * * @param {CSN.Artifact} art The artifact */ function checkRecursiveTypeUsage( art ) { const visit = (def) => { const loc = def.$path; // recursive types are allowed inside arrays if (def.items) return; let { type } = def; let prevType; let isDeref = false; if (type && !isBuiltinType(type) && !def.elements) { do { prevType = type; // TODO: `type.ref.length > 1`, but OData backend must be tested first (#5144) // e.g. `{ ref: [ "MyType" ] }` if (type.ref) { def = this.artifactRef(type); isDeref = true; } else { def = this.csn.definitions[type]; } type = def.type; } while (type && !isBuiltinType(type) && !def.items && !def.elements && prevType !== type); } if (def.$visited || (type && prevType === type)) { // Recursion via type is allowed in V4 struct, but not via dereferencing if (!isDeref && this.options.odataVersion === 'v4' && this.options.odataFormat === 'structured') return; if (!def.$recErr) { this.error('ref-cyclic', loc, { '#': 'type', type: type ?? prevType }); setProp(def, '$recErr', true); } } else if (def.elements) { setProp(def, '$visited', true); for (const n in def.elements) visit(def.elements[n]); delete def.$visited; } }; // elements & params are flattening candidates // FUTURE: // Once we have universal CSN for the runtimes // Validate service members only for OData if (art.kind === 'entity') { for (const n in art.elements) visit(art.elements[n]); for (const n in art.params) visit(art.params[n]); } if (this.options.odataVersion) { // func/action params/returns don't allow recursive type derefs if (art.kind === 'action' || art.kind === 'function') { for (const n in art.params) visit(art.params[n]); if (art.returns) visit(art.returns); } } } /** * Member validator to check that certain annotations (@cds.valid { from, to, key }) are not * assigned to calculated elements in an entity. * * TODO: Allow @cds.valid on persisted calculated elements (when they become available). * * @param {CSN.Element} member the element to be checked * @param {string} _memberName the elements name * @param {string} _prop which kind of member are we looking at -> only prop "elements" * @param {CSN.Path} _path the path to the member */ function rejectAnnotationsOnCalcElement( member, _memberName, _prop, _path ) { if (this.artifact.kind === 'entity' && !(this.artifact.query && this.artifact.projection)) { if (member.value) { for (const anno in member) { if (anno.startsWith('@cds.valid.')) { this.error('anno-unexpected-temporal', member.$path, { anno }, 'Unexpected $(ANNO) assigned to a calculated element'); } } } } } module.exports = { checkPrimaryKey, checkVirtualElement, checkManagedAssoc, checkRecursiveTypeUsage, rejectAnnotationsOnCalcElement, };