UNPKG

@sap/cds-compiler

Version:

CDS (Core Data Services) compiler and backends

432 lines (391 loc) 15.5 kB
// Add tenant field to entities, check validity // Prerequisites: // - the input CSN is a `client` style CSN from the Core Compiler // - using structure types with unmanaged associations is not supported by the // Core Compiler (due to missing ON-rewrite) // TODO clarify: // // - do we have to do something for secondary keys? // Implementation remark: // // - the functions `forEachDefinition` & friends in csnUtils.js have become quite // (too) general and are probably slow → not used here 'use strict'; const { createMessageFunctions } = require( '../base/messages' ); const { csnRefs, traverseQuery, implicitAs, pathId, } = require( '../model/csnRefs' ); const annoTenantIndep = '@cds.tenant.independent'; const tenantDef = { key: true, type: 'cds.String', length: 36, '@cds.api.ignore': true, // and/or $generated: 'tenant' for the full Universal CSN? }; function addTenantFields( csn, options, messageFunctions ) { const { error, throwWithError } = messageFunctions ?? createMessageFunctions( options, 'tenant', csn ); const { tenantDiscriminator } = options; const tenantName = tenantDiscriminator === true ? 'tenant' : tenantDiscriminator; if (tenantName !== 'tenant') { error( 'api-invalid-option', null, { '#': 'value2', option: 'tenantDiscriminator', value: 'tenant', rawvalue: true, othervalue: tenantName, } ); throwWithError(); } const { definitions } = csn; if (!definitions) return csn; const { initDefinition, artifactRef, effectiveType, queryForElements, $getQueries, msgLocations, } = csnRefs( csn, true ); const typeCache = new WeakMap(); const csnPath = [ null ]; let independent; let projection; for (const name in definitions) { const art = initDefinition( name ); csnPath[0] = art; independent = art[annoTenantIndep]; projection = art.query || art.projection && art; if (art.kind === 'entity') { independent = !!independent; // value should not influence message variant if (independent && art.includes && !checkIncludes( art )) continue; handleElements( art ); if (projection) traverseQuery( projection, null, null, handleQuery ); // Note: handleQuery sets csnPath[0]; store if needed afterwards } else if (!independent && independent != null) { error( 'tenant-invalid-anno-value', msgLocations( csnPath ), { anno: annoTenantIndep, value: independent }, // eslint-disable-next-line @stylistic/max-len 'Can\'t add $(ANNO) with value $(VALUE) to a non-entity, which is always tenant-independent' ); } else if (art.includes) { independent = art.kind; // might be used for message variant checkIncludes( art ); // recompile should work } else if (projection) { // events, types - TODO: mention in doc independent = art.kind; // might be used for message variant // recompile should work: no new `tenant` source element for `select *` traverseQuery( projection, null, null, handleQuery ); } } // Finally add the `tenant` element (do separately in order not to confuse // the cache of csnRefs): for (const name in definitions) { const art = definitions[name]; if (addTenantFieldToArt( art, options ) && (art.query || art.projection)) { for (const qcache of $getQueries( art ).slice( 1 )) addTenantFieldToArt( qcache._select, options, true ); } } (csn.extensions || []).forEach( ( ext, idx ) => { const tenant = ext.elements?.[tenantName]; const name = ext.annotate || ext.extend; // extend should not happen if (tenant && isTenantDepEntity( definitions[name] )) { error( 'tenant-unexpected-ext', msgLocations( [ 'extensions', idx, 'elements', 'tenant' ] ), { name: tenantName }, 'Can\'t annotate element $(NAME) of a tenant-dependent entity' ); } } ); throwWithError(); csn.meta ??= {}; csn.meta.tenantDiscriminator = tenantName; return csn; // input CSN changed by side effect function checkIncludes( art ) { const names = art.includes .filter( name => isTenantDepEntity( csn.definitions[name] ) ); if (names.length) { error( 'tenant-invalid-include', msgLocations( csnPath ), { names }, { // eslint-disable-next-line @stylistic/max-len std: 'Can\'t include the tenant-dependent entities $(NAMES) into a tenant-independent definition', // eslint-disable-next-line @stylistic/max-len one: 'Can\'t include the tenant-dependent entity $(NAMES) into a tenant-independent definition', } ); } return !names.length; } function handleElements( art ) { const { elements } = art; if (elements[tenantName]) { error( 'tenant-unexpected-element', msgLocations( [ ...csnPath, 'elements', tenantName ] ), { name: tenantName, option: 'tenantDiscriminator' }, 'Can\'t have entity with element $(NAME) when using option $(OPTION)' ); } else if (!independent && !Object.values( elements ).some( e => e.key )) { error( 'tenant-expecting-key', msgLocations( csnPath ), {}, 'There must be a key in a tenant-dependent entity' ); } else { traverse( art, handleAssociations ); } } // Queries -------------------------------------------------------------------- function handleQuery( query, fromSelect, parentQuery ) { const select = query.SELECT || query.projection; if (select) csnPath[0] = query; if (!projection || query.ref && handleQuerySource( query, fromSelect )) return; if (query !== projection && !independent && (fromSelect && !fromSelect.from.ref || !parentQuery?.SET)) { // If a sub query would be supported in ORDER BY or LIMIT, the test above // would not be enough error( 'tenant-unsupported-query', msgLocations( csnPath ), { '#': (fromSelect?.from?.join ? 'join' : 'subquery') }, { std: 'Can\'t have tenant-dependent non-simple query entities', join: 'Can\'t use a join in a tenant-dependent entity', subquery: 'Can\'t use a sub query in a tenant-dependent entity', } ); projection = null; // no further error return; } if (!select) return; // query.SET or query.join csnPath.push( query.SELECT ? 'SELECT' : 'projection' ); const qcache = queryForElements( query ); if (qcache.$queryNumber > 1) handleElements( qcache ); if (select.mixin) checkMixins( select.mixin ); if (!independent) { if (select.excluding) checkExcluding( select.excluding ); if (select.columns) handleColumns( select.columns ); handleGroupBy( select ); } csnPath.length = 1; } function handleQuerySource( query, fromSelect ) { if (independent) { const art = pathId(query.ref[0]); // yes, the base if (csn.definitions[art][annoTenantIndep]) return true; error( 'tenant-invalid-query-source', msgLocations( csnPath ), { art, '#': independent }, { std: 'Can\'t use a tenant-dependent query source $(ART) in a tenant-independent entity', event: 'Can\'t use a tenant-dependent query source $(ART) in an event', type: 'Can\'t use a tenant-dependent query source $(ART) in a type definition', } ); return true; } if (fromSelect && fromSelect.from !== query) // with JOIN return false; // issue better error later if ((query.as || implicitAs( query.ref )) === tenantName) { error( 'tenant-invalid-alias-name', msgLocations( csnPath ), { name: tenantName, '#': (query.as ? 'std' : 'implicit') } ); } const art = artifactRef.from( query ); if (art[annoTenantIndep]) { error( 'tenant-expecting-tenant-source', msgLocations( csnPath ), { art: query }, // TODO: better the final entity name of assoc navigation in FROM // eslint-disable-next-line @stylistic/max-len 'Expecting the query source $(ART) to be tenant-dependent for a tenant-dependent query entity' ); } return true; } function checkMixins( mixin ) { csnPath.push( 'mixin', '' ); for (const name in mixin) { csnPath[csnPath.length - 1] = name; if (name === tenantName && !independent) error( 'tenant-invalid-alias-name', msgLocations( csnPath ), { name, '#': 'mixin' } ); handleAssociations( mixin[name], null ); } csnPath.length -= 2; } function checkExcluding( excludeList ) { if (excludeList.includes( tenantName )) { error( 'tenant-invalid-excluding', msgLocations( csnPath ), { name: tenantName }, 'Can\'t exclude $(NAME) from the query source of a tenant-dependent entity' ); } } function handleGroupBy( select ) { // TODO: in the future, we allow model-wise keyless views when using // aggregation function, and add a GROUP BY for MANDT in this case. Now, also // views with agg functions need to have a key element → it very likely // already contains a GROUP BY. And anyway: if we miss to add GROUP BY MANDT, // the database will complain → no safetly risk. if (select.groupBy) select.groupBy.unshift( { ref: [ tenantName ] } ); } function handleColumns( columns ) { let specifiedKey = false; csnPath.push( 'columns', -1 ); for (const col of columns) { ++csnPath[csnPath.length - 1]; if (col.expand || col.inline) { error( 'tenant-unsupported-expand-inline', msgLocations( csnPath ), {}, 'Can\'t use expand/inline in a tenant-dependent entity' ); } if (col.key != null) // yes, also with key: false specifiedKey = true; // REDIRECTED TO: also check new target here? (main query: already checked via elements) } csnPath.length -= 2; columns.unshift( specifiedKey ? { key: true, ref: [ tenantName ] } : { ref: [ tenantName ] } ); } // Associations --------------------------------------------------------------- function handleAssociations( elem, afterRecursion ) { if (afterRecursion != null) return null; if (elem.target) { const { target } = elem; if (csn.definitions[target][annoTenantIndep]) { if (!independent && isComposition( elem )) error( 'tenant-invalid-composition', msgLocations( csnPath ), { target } ); } else if (independent) { if (target.endsWith( '.DraftAdministrativeData' ) && csnPath.length === 3 && csnPath[1] === 'elements' && csnPath[2] === 'DraftAdministrativeData') { error( 'tenant-invalid-draft', msgLocations( csnPath ), {}, 'A tenant-independent entity can\'t be draft-enabled' ); } else { error( 'tenant-invalid-target', msgLocations( csnPath ), { target } ); } } } else if (elem.type && (independent || !elem.elements && !elem.items)) { // check type, but not with expanded elements in dependent entity, because // composition could have redirected tenant-dependent target const dep = typeDependency( artifactRef( elem.type, null ) ); if (independent) { if (!dep || dep === 'Composition') return true; // check elements (assocs could be redirected) error( 'tenant-invalid-target', msgLocations( csnPath ), { type: elem.type, '#': 'type' } ); } else if (dep && dep !== 'dependent') { error( 'tenant-invalid-composition', msgLocations( csnPath ), { type: elem.type, '#': 'type' } ); } } else { return true; } return null; } /** * Returns “type dependency”, a string, for type `assoc`: * * - '': type does not contain associations other than non-composition associations to * tenant-independent entities * - 'Composition': type contains associations (and at least one composition) to * tenant-independent entities, and no associations to tenant-dependent entities * - 'dependent': type contains associations, at least one to a tenant-dependent entity, * but no compositions to tenant-independent entities * - 'ERR': type contains associations, at least one to a tenant-dependent entity, * and at least one composition to a tenant-independent entity * * Type references are followed, but only without sibling `elements` or `items`. */ function typeDependency( assoc ) { assoc = assoc ? effectiveType( assoc ) : assoc; if (!assoc) return ''; const assocDep = typeCache.get( assoc ); if (assocDep != null) return assocDep; let parentDep = ''; traverse( assoc, typeCallback ); return parentDep; function typeCallback( type, savedDep ) { let currentDep = typeCache.get( type ); if (currentDep != null) { // nothing } else if (savedDep != null) { currentDep = parentDep; parentDep = savedDep; } else if (type.target) { const annoDep = !csn.definitions[type.target][annoTenantIndep]; currentDep = (annoDep) ? 'dependent' : isComposition( type ) && 'Composition'; } else if (type.elements || type.items) { savedDep = parentDep; parentDep = ''; return savedDep || ''; // recurse } else if (type.type) { currentDep = typeDependency( artifactRef( type.type, null ) ); } else { currentDep = ''; } typeCache.set( type, currentDep ); if (!currentDep || !parentDep) parentDep ||= currentDep; else if (currentDep !== parentDep) parentDep = 'ERR'; return null; // do not (further) recurse } } // Generic functions ---------------------------------------------------------- function traverse( elem, callback ) { const recurse = callback( elem, null ); if (recurse == null) return; const { elements } = elem; if (elements) { csnPath.push( 'elements', '' ); for (const name in elements) { csnPath[csnPath.length - 1] = name; traverse( elements[name], callback ); } csnPath.length -= 2; } else if (elem.items) { csnPath.push( 'items' ); traverse( elem.items, callback ); --csnPath.length; } callback( elem, recurse ); } function isComposition( assoc ) { while (assoc && assoc.type !== 'cds.Association') { const { type } = assoc; if (type === 'cds.Composition') return true; assoc = artifactRef( type, null ); } return false; } } function isTenantDepEntity( art ) { return art?.kind === 'entity' && !art[annoTenantIndep]; } function addTenantFieldToArt( art, options, isQuery = false ) { if (!isQuery && !isTenantDepEntity( art )) return false; const tenantName = options.tenantDiscriminator === true ? 'tenant' : options.tenantDiscriminator; const elements = Object.getOwnPropertyDescriptor( art, 'elements' ); // `query.elements` is usually non-enumerable elements.value = { [tenantName]: { ...tenantDef }, ...elements.value }; Object.defineProperty( art, 'elements', elements ); return true; } module.exports = { addTenantFields, addTenantFieldToArt, };