UNPKG

@sap/cds-compiler

Version:

CDS (Core Data Services) compiler and backends

1,294 lines (1,139 loc) 54.1 kB
/** @module API */ 'use strict'; const lazyload = require('../base/lazyload')( module ); const prepareOptions = lazyload('./options'); const baseModel = lazyload('../base/model'); const location = lazyload('../base/location'); const messages = lazyload('../base/messages'); const compiler = lazyload('../compiler/index'); const toCsn = lazyload('../json/to-csn'); const forOdataNew = lazyload('../transform/forOdata.js'); const generateDrafts = lazyload('../transform/draft/odata'); const tenant = lazyload('../transform/addTenantFields'); const toSql = lazyload('../render/toSql'); const toCdl = require('../render/toCdl'); const sqlRenderUtils = lazyload('../render/utils/sql'); const modelCompare = lazyload('../modelCompare/compare'); const diffFilter = lazyload('../modelCompare/utils/filter'); const sortViews = lazyload('../model/sortViews'); const csnUtils = lazyload('../model/csnUtils'); const timetrace = lazyload('../utils/timetrace'); const forRelationalDB = lazyload('../transform/forRelationalDB'); const sqlUtils = lazyload('../render/utils/sql'); const effective = lazyload('../transform/effective/main'); const toHdbcds = lazyload('../render/toHdbcds'); const baseError = lazyload('../base/error'); const csnToEdm = lazyload('../edm/csn2edm'); const trace = lazyload('./trace'); const cloneCsn = lazyload('../model/cloneCsn'); const objectUtils = lazyload('../utils/objectUtils'); /** * Return the artifact name for use for the hdbresult object * So that it stays compatible with v1 .texts * * @param {string} artifactName Name to map * @param {CSN.Model} csn SQL transformed model * @returns {string} Name with . replaced as _ in some places */ function getFileName( artifactName, csn ) { return csnUtils.getResultingName(csn, 'quoted', artifactName); } const relevantGeneralOptions = [ /* for future generic options */ ]; const relevantOdataOptions = [ 'sqlMapping', 'odataFormat' ]; const warnAboutMismatchOdata = [ 'odataVersion' ]; /** * Attach options and transformation name to the $meta tag * * @param {CSN.Model} csn CSN to attach to * @param {string} transformation Name of the transformation - odata or hana * @param {NestedOptions} options Options used for the transformation * @param {string[]} relevantOptionNames Option names that are defining characteristics * @param {string[]} [optionalOptionNames] Option names that should be attached as a fyi */ function attachTransformerCharacteristics( csn, transformation, options, relevantOptionNames, optionalOptionNames = [] ) { const relevant = {}; for (const name of relevantOptionNames) { if (options[name] !== undefined) relevant[name] = options[name]; } for (const name of optionalOptionNames) { if (options[name] !== undefined) relevant[name] = options[name]; } // eslint-disable-next-line sonarjs/no-empty-collection for (const name of relevantGeneralOptions) { if (options[name] !== undefined) relevant[name] = options[name]; } if (!csn.meta) baseModel.setProp(csn, 'meta', {}); baseModel.setProp(csn.meta, 'options', relevant); baseModel.setProp(csn.meta, 'transformation', transformation); } /** * Check the characteristics of the provided, already transformed CSN * Report an error if they do not match with the currently requested options * V2 vs V4, plain vs hdbcds etc. * * @param {CSN.Model} csn CSN to check * @param {NestedOptions} options Options used for the transformation - scanned top-level * @param {string[]} relevantOptionNames Option names that are defining characteristics * @param {string[]} warnAboutMismatch Option names to warn about, but not error on * @param {object} messageFunctions Message functions such as `error()`, `info()`, … */ function checkPreTransformedCsn( csn, options, relevantOptionNames, warnAboutMismatch, messageFunctions ) { if (!csn.meta?.options) { // Not able to check return; } const { error, warning, throwWithAnyError } = messageFunctions; for (const name of relevantOptionNames ) { if (options[name] !== csn.meta.options[name]) { error('api-invalid-option-preprocessed', null, { prop: name, value: options[name], othervalue: csn.meta.options[name] }, 'Expected pre-processed CSN to have option $(PROP) set to $(VALUE). Found: $(OTHERVALUE)'); } } for (const name of warnAboutMismatch ) { if (options[name] !== csn.meta.options[name]) { warning('api-mismatched-option-preprocessed', null, { prop: name, value: options[name], othervalue: csn.meta.options[name] }, 'Expected pre-processed CSN to have option $(PROP) set to $(VALUE). Found: $(OTHERVALUE)'); } } throwWithAnyError(); } /** * Check if the CSN was already run through the appropriate transformer * * - Currently only check for odata, as hana is not exposed * * @param {CSN.Model} csn CSN * @param {string} transformation Name of the transformation * @returns {boolean} Return true if it is pre-transformed */ function isPreTransformed( csn, transformation ) { return csn && csn.meta && csn.meta.transformation === transformation; } /** * Get an odata-CSN without option handling. * * @param {CSN.Model} csn Clean input CSN * @param {object} internalOptions processed options * @param {object} messageFunctions Message functions such as `error()`, `info()`, … * @returns {object} Return an oData-pre-processed CSN */ function odataInternal( csn, internalOptions, messageFunctions ) { internalOptions.transformation = 'odata'; let oDataCsn = forOdataNew.transform4odataWithCsn(csn, internalOptions, messageFunctions); oDataCsn = cloneCsn.sortCsnForTests(oDataCsn, internalOptions); messageFunctions.setModel(oDataCsn); attachTransformerCharacteristics(oDataCsn, 'odata', internalOptions, relevantOdataOptions, warnAboutMismatchOdata); return oDataCsn; } /** * Return a odata-transformed CSN * * @param {CSN.Model} csn Clean input CSN * @param {ODataOptions} options Options * @param {object} messageFunctions Message functions such as `error()`, `info()`, … * @returns {oDataCSN} Return an oData-pre-processed CSN */ function odata( csn, options, messageFunctions ) { const internalOptions = prepareOptions.for.odata(options); messageFunctions.setOptions( internalOptions ); return odataInternal(csn, internalOptions, messageFunctions); } /** * Return a structured CSN for the Java Runtime: with drafts and tenant support * * @param {CSN.Model} csn Clean input CSN * @param {ODataOptions} options Options * @param {object} messageFunctions Message functions such as `error()`, `info()`, … * @returns {CSN.Model} a CSN for the Java Runtime */ function java( csn, options, messageFunctions ) { const internalOptions = prepareOptions.for.java(options); internalOptions.transformation = 'odata'; // otherwise generateDrafts adds tenant messageFunctions.setOptions( internalOptions ); handleTenantDiscriminator(options, internalOptions, messageFunctions); const clone = cloneCsn.cloneFullCsn(csn, internalOptions); const draft = generateDrafts(clone, internalOptions, undefined, messageFunctions); if (internalOptions.tenantDiscriminator) tenant.addTenantFields(draft, internalOptions, messageFunctions ); return draft; } /** * Process the given csn back to cdl. * * @param {object} csn CSN to process * @param {object} options Options * @param {object} messageFunctions Message functions such as `error()`, `info()`, … * @returns {object} { model: string, namespace: string } */ function cdl( csn, options, messageFunctions ) { const internalOptions = prepareOptions.to.cdl(options); messageFunctions.setOptions( internalOptions ); return toCdl.csnToCdl(csn, internalOptions, messageFunctions); } /** * Transform a CSN like to.sql(). * Expects that internalOptions have been validated via prepareOptions.to.sql(). * * @param {CSN.Model} csn Plain input CSN * @param {SqlOptions} internalOptions Options * @param {object} messageFunctions Message functions such as `error()`, `info()`, … * @returns {CSN.Model} CSN transformed like to.sql * @private */ function csnForSql( csn, internalOptions, messageFunctions ) { internalOptions.transformation = 'sql'; const transformedCsn = forRelationalDB.transformForRelationalDBWithCsn( csn, internalOptions, messageFunctions ); return cloneCsn.sortCsnForTests(transformedCsn, internalOptions); } /** * Transform a CSN like to.sql(). Also performs options-checks. * Pseudo-public version of csnForSql(). * * @param {CSN.Model} csn Plain input CSN * @param {SqlOptions} options Options * @param {object} messageFunctions Message functions such as `error()`, `info()`, … * @returns {CSN.Model} CSN transformed like to.sql * @private */ function forSql( csn, options, messageFunctions ) { const internalOptions = prepareOptions.to.sql(options); messageFunctions.setOptions( internalOptions ); return csnForSql(csn, internalOptions, messageFunctions); // already sorted for test mode } /** * Transform a CSN like to.hdi * * @param {CSN.Model} csn Plain input CSN * @param {HdiOptions} options Options * @param {object} messageFunctions Message functions such as `error()`, `info()`, … * @returns {CSN.Model} CSN transformed like to.hdi * @private */ function forHdi( csn, options, messageFunctions ) { const internalOptions = prepareOptions.to.hdi(options); internalOptions.transformation = 'sql'; messageFunctions.setOptions( internalOptions ); const transformedCsn = forRelationalDB.transformForRelationalDBWithCsn( csn, internalOptions, messageFunctions ); return cloneCsn.sortCsnForTests(transformedCsn, internalOptions); } /** * Transform a CSN like to.hdbcds * * @param {CSN.Model} csn Plain input CSN * @param {HdbcdsOptions} options Options * @param {object} messageFunctions Message functions such as `error()`, `info()`, … * @returns {CSN.Model} CSN transformed like to.hdbcds * @private */ function forHdbcds( csn, options, messageFunctions ) { const internalOptions = prepareOptions.to.hdbcds(options); internalOptions.transformation = 'hdbcds'; messageFunctions.setOptions( internalOptions ); const hanaCsn = forRelationalDB.transformForRelationalDBWithCsn( csn, internalOptions, messageFunctions ); return cloneCsn.sortCsnForTests(hanaCsn, internalOptions); } /** * Effective CSN transformation * * @param {CSN.Model} csn Plain input CSN * @param {EffectiveCsnOptions} options Options * @param {EffectiveCsnOptions} internalOptions Options that were already processed * @param {object} messageFunctions Message functions such as `error()`, `info()`, … * @returns {CSN.Model} CSN transformed * @private */ function forEffectiveInternal( csn, options, internalOptions, messageFunctions ) { messageFunctions.setOptions( internalOptions ); if (options.tenantDiscriminator) { messageFunctions.error('api-invalid-option', null, { '#': 'forbidden', option: 'tenantDiscriminator', module: 'for.effective', }); messageFunctions.throwWithAnyError(); } const eCsn = effective.effectiveCsn(csn, internalOptions, messageFunctions); return cloneCsn.sortCsnForTests(eCsn, internalOptions); } /** * SEAL CSN transformation * * @param {CSN.Model} csn Plain input CSN * @param {EffectiveCsnOptions} options Options * @param {object} messageFunctions Message functions such as `error()`, `info()`, … * @returns {CSN.Model} CSN transformed * @private */ function forSeal( csn, options, messageFunctions ) { const internalOptions = prepareOptions.for.seal(options); internalOptions.transformation = 'effective'; return forEffectiveInternal(csn, options, internalOptions, messageFunctions); } /** * Effective CSN transformation * * @param {CSN.Model} csn Plain input CSN * @param {EffectiveCsnOptions} options Options * @param {object} messageFunctions Message functions such as `error()`, `info()`, … * @returns {CSN.Model} CSN transformed * @private */ function forEffective( csn, options, messageFunctions ) { const internalOptions = prepareOptions.for.effective(options); internalOptions.transformation = 'effective'; // for.effective is still beta mode if (!baseModel.isBetaEnabled(options, 'effectiveCsn')) throw new baseError.CompilerAssertion('effective CSN is only supported with beta flag `effectiveCsn`!'); return forEffectiveInternal(csn, options, internalOptions, messageFunctions); } /** * Process the given CSN into SQL. * * @param {CSN.Model} csn A clean input CSN * @param {SqlOptions} options Options * @param {object} messageFunctions Message functions such as `error()`, `info()`, … * @returns {SQL[]} Array of SQL statements, tables first, views second */ function sql( csn, options, messageFunctions ) { const internalOptions = prepareOptions.to.sql(options); internalOptions.transformation = 'sql'; messageFunctions.setOptions( internalOptions ); handleTenantDiscriminator(options, internalOptions, messageFunctions); // we need the CSN for view sorting const transformedCsn = csnForSql(csn, internalOptions, messageFunctions); messageFunctions.setModel(transformedCsn); const sqls = toSql.toSqlDdl(transformedCsn, internalOptions, messageFunctions); const result = sortViews({ csn: transformedCsn, sql: sqls.sql }); return [ ...result.map(obj => obj.sql).filter(create => create), ...Object.values(sqls.constraints || {}), ]; } /** * Process the given CSN into HDI artifacts. * * @param {CSN.Model} csn A clean input CSN * @param {HdiOptions} options Options * @param {object} messageFunctions Message functions such as `error()`, `info()`, … * @returns {HDIArtifacts} { <filename>:<content>, ...} */ function hdi( csn, options, messageFunctions ) { const internalOptions = prepareOptions.to.hdi(options); messageFunctions.setOptions( internalOptions ); handleTenantDiscriminator(options, internalOptions, messageFunctions); // we need the CSN for view sorting const sqlCSN = forHdi(csn, options, messageFunctions); messageFunctions.setModel(sqlCSN); const sqls = toSql.toSqlDdl(sqlCSN, internalOptions, messageFunctions); if (internalOptions.testMode) { // All this mapping is needed because sortViews crossmatches // passed in SQLs with the CSN artifact name // But we also need to return it with the correct file ending in the end // so remember and do lot's of mapping here. const flat = flattenResultStructure(sqls); const nameMapping = Object.create(null); const sqlArtifactsWithCSNNamesToSort = Object.create(null); const sqlArtifactsNotToSort = Object.create(null); objectUtils.forEach(flat, (key) => { const artifactNameLikeInCsn = key.replace(/\.[^/.]+$/, ''); if (key.endsWith('.hdbtable') || key.endsWith('.hdbview') || key.endsWith('.hdbprojectionview')) { nameMapping[artifactNameLikeInCsn] = key; sqlArtifactsWithCSNNamesToSort[artifactNameLikeInCsn] = flat[key]; } else { sqlArtifactsNotToSort[key] = flat[key]; } }); const sorted = sortViews({ sql: sqlArtifactsWithCSNNamesToSort, csn: sqlCSN }) .filter(obj => obj.sql) .reduce((previous, current) => { const hdiArtifactName = remapName(nameMapping[current.name], sqlCSN, k => !k.endsWith('.hdbindex')); previous[hdiArtifactName] = current.sql; return previous; }, Object.create(null)); // now add the not-sorted stuff, like indices objectUtils.forEach(sqlArtifactsNotToSort, (key) => { sorted[remapName(key, sqlCSN, k => !k.endsWith('.hdbindex'))] = sqlArtifactsNotToSort[key]; }); return sorted; } return remapNames(flattenResultStructure(sqls), sqlCSN, k => !k.endsWith('.hdbindex')); } /** * Remap names so that they stay consistent between v1 and v2 * * Mainly important for _texts -> .texts * * @param {object} dict Result dictionary by toSql * @param {CSN.Model} csn SQL transformed CSN * @param {Function} filter Filter for keys not to remap * @returns {object} New result structure */ function remapNames( dict, csn, filter ) { const result = Object.create(null); objectUtils.forEach(dict, (key, value) => { const name = remapName(key, csn, filter); result[name] = value; }); return result; } /** * Remap names so that it stays consistent between v1 and v2 * * Mainly important for _texts -> .texts * * @param {string} key Filename * @param {CSN.Model} csn SQL transformed CSN * @param {Function} filter Filter for keys not to remap * @returns {string} Remapped filename */ function remapName( key, csn, filter = () => true ) { if (filter(key)) { const lastDot = key.lastIndexOf('.'); const prefix = key.slice(0, lastDot); const suffix = key.slice(lastDot); const remappedName = getFileName(prefix, csn); return remappedName + suffix; } return key; } /** * Return all changes in artifacts between two given models. * Note: Only supports changes in artifacts compiled/rendered as db-CSN/SQL. * * @param {CSN.Model} csn A clean input CSN representing the desired "after-image" * @param {HdiOptions} options Options * @param {object} messageFunctions Message functions such as `error()`, `info()`, … * @param {CSN.Model} beforeImage A db-transformed CSN representing the "before-image", or null in case no such image * is known, i.e. for the very first migration step. * @returns {object} An object with three properties: * - afterImage: A db-transformed CSN representing the "after-image" * - drops: An array of SQL statements to drop views/tables * - createsAndAlters: An array of SQL statements to ALTER/CREATE tables/views */ function sqlMigration( csn, options, messageFunctions, beforeImage ) { const internalOptions = prepareOptions.to.sql(options); messageFunctions.setOptions( internalOptions ); if (internalOptions.script) messageFunctions.setModuleName( `${ messageFunctions.moduleName }-script` ); handleTenantDiscriminator(options, internalOptions, messageFunctions); if (!options.dry && internalOptions.script) { messageFunctions.error('api-invalid-combination', null, { '#': 'dry-and-script', value: options.dry || 'undefined' }); messageFunctions.throwWithError(); } if (!internalOptions.script && internalOptions.sqlDialect === 'hana') messageFunctions.warning('api-invalid-combination', null, { '#': 'hana-migration', value: internalOptions.sqlDialect }); if (internalOptions.script && !internalOptions.severities?.['migration-unsupported-key-change']) { internalOptions.severities = Object.assign({}, internalOptions.severities ?? {}); internalOptions.severities['migration-unsupported-key-change'] = 'Warning'; } if (internalOptions.script) { internalOptions.severities = Object.assign({}, internalOptions.severities ?? {}); const turnToWarning = [ 'migration-unsupported-element-drop', 'migration-unsupported-length-change', 'migration-unsupported-scale-change', 'migration-unsupported-precision-change', 'migration-unsupported-change', 'migration-unsupported-table-drop' ]; turnToWarning.forEach((id) => { internalOptions.severities[id] = 'Warning'; }); } const { throwWithError } = messageFunctions; // Prepare after-image. let afterImage = csnForSql(csn, internalOptions, messageFunctions); if (internalOptions.filterCsn) afterImage = diffFilter.csn(afterImage); // Compare both images. const diff = modelCompare.compareModels(beforeImage || afterImage, afterImage, internalOptions); messageFunctions.setModel(diff); const diffFilterObj = diffFilter.getFilter(internalOptions); if (diffFilterObj) { diff.extensions = diff.extensions.filter(ex => diffFilterObj.extension(ex, messageFunctions)); diff.migrations.forEach(migration => diffFilterObj.migration(migration, messageFunctions)); Object.entries(diff.deletions).forEach(entry => diffFilterObj.deletion(entry, messageFunctions)); diff.changedPrimaryKeys = diff.changedPrimaryKeys .filter(an => diffFilterObj.changedPrimaryKeys(an)); if (internalOptions.script && diffFilterObj.hasLossyChanges()) messageFunctions.warning('def-unsupported-changes', null, null, 'Found potentially lossy changes - check generated SQL statements'); } const identifierUtils = sqlUtils.getIdentifierUtils(csn, internalOptions); const drops = { creates: {}, final: Object.entries(diff.deletions).reduce((previous, [ name, artifact ]) => { if (artifact.query || artifact.projection) previous[name] = `DROP VIEW ${ identifierUtils.renderArtifactName(name) };`; else previous[name] = `-- [WARNING] this statement is lossy\nDROP TABLE ${ identifierUtils.renderArtifactName(name) };`; return previous; }, {}), }; const markedSkipByUs = {}; const cleanup = []; // Delete artifacts that are already present in csn if (beforeImage?.definitions) { Object.keys(beforeImage.definitions).forEach((artifactName) => { const beforeArtifact = beforeImage.definitions[artifactName]; const diffArtifact = diff.definitions[artifactName]; // TODO: exists, abstract? isPersistedOnDb? if (diffArtifact && diffArtifact['@cds.persistence.name'] && csnUtils.isPersistedAsView(diffArtifact) && (diffArtifact[modelCompare.isChanged] === true || // we know it changed because we compared two views diffArtifact[modelCompare.isChanged] === undefined)) { // if it was removed in the after, then we don't have the flag drops.creates[artifactName] = `DROP VIEW ${ identifierUtils.renderArtifactName(artifactName) };`; } // TODO: What happens with a changed kind -> entity becomes a view? else if (diffArtifact && diffArtifact['@cds.persistence.skip'] !== true && diffArtifact.kind === beforeArtifact.kind && // detect action -> entity csnUtils.isPersistedAsTable(diffArtifact) === csnUtils.isPersistedAsTable(beforeArtifact) && // detect removal of @cds.persistence.exists csnUtils.isPersistedAsView(diffArtifact) === csnUtils.isPersistedAsView(beforeArtifact) // detect view -> entity ) { // don't render again, but need info for primary key extension diffArtifact['@cds.persistence.skip'] = true; cleanup.push(() => delete diffArtifact['@cds.persistence.skip']); markedSkipByUs[artifactName] = true; } }); } const sortOrder = sortViews({ sql: {}, csn: afterImage }); const dependentsDict = {}; sortOrder.forEach(({ name, dependents }) => { dependentsDict[name] = dependents; }); const stack = Object.keys(drops.creates); while (stack.length > 0) { const name = stack.pop(); const artifact = diff.definitions[name]; if (drops.creates[name] === undefined) { if (csnUtils.hasPersistenceSkipAnnotation(artifact) && markedSkipByUs[name]) { // Remove the skip so we render a CREATE VIEW diff.definitions[name]['@cds.persistence.skip'] = false; drops.creates[name] = `DROP VIEW ${ identifierUtils.renderArtifactName(name) };`; } } const dependents = dependentsDict[name]; if (dependents) { // schedule any dependents for processing that don't have a drop-create yet for (const dependantName in dependents) { if (!drops.creates[dependantName]) stack.push(dependantName); } } } // Convert the diff to SQL. if (!internalOptions.beta) internalOptions.beta = {}; internalOptions.beta.sqlExtensions = true; const { // eslint-disable-next-line no-unused-vars deletions, constraintDeletions, migrations, constraints, ...hdbkinds } = toSql.toSqlDdl(diff, internalOptions, messageFunctions); cleanup.forEach(fn => fn()); // TODO: Handle `ADD CONSTRAINT` etc! const dropSqls = []; const createAndAlterSqls = []; // Turn the structured result into just a flat dictionary of "artifact name": "sql" const flatSqlDict = Object.values(hdbkinds).reduce((prev, curr) => { objectUtils.forEach(curr, (name, value) => { prev[name] = value; }); return prev; }, Object.create(null)); // Sort all the SQL statements according to the overall order for (const { name } of sortOrder) { if (drops.final[name]) dropSqls.push(drops.final[name]); else if (drops.creates[name]) dropSqls.push(drops.creates[name]); // No else-if, since we have drop-creates for views! if (flatSqlDict[name]) createAndAlterSqls.push(flatSqlDict[name]); else if (migrations[name]) createAndAlterSqls.push(...migrations[name].map(m => m.sql)); } if (constraints) Object.values(constraints).forEach(constraint => createAndAlterSqls.push(constraint)); if (constraintDeletions) Object.values(constraintDeletions).forEach(constraint => dropSqls.push(constraint)); if (Object.keys(drops.final).length > 0) { const order = sortViews({ sql: {}, csn: beforeImage }); for (const { name } of order) { if (drops.final[name]) dropSqls.push(drops.final[name]); } } // We need to drop the things without dependants first - so inversely sorted dropSqls.reverse(); throwWithError(); return { afterImage, drops: dropSqls, createsAndAlters: createAndAlterSqls, }; } /** * Return all changes in artifacts between two given models. * Note: Only supports changes in entities (not views etc.) compiled/rendered as HANA-CSN/SQL. * * @param {CSN.Model} csn A clean input CSN representing the desired "after-image" * @param {HdiOptions} options Options * @param {object} messageFunctions Message functions such as `error()`, `info()`, … * @param {CSN.Model} beforeImage A HANA-transformed CSN representing the "before-image", or null in case no such image * is known, i.e. for the very first migration step * @returns {migration} The migration result */ function hdiMigration( csn, options, messageFunctions, beforeImage ) { const internalOptions = prepareOptions.to.hdi(options); messageFunctions.setOptions( internalOptions ); handleTenantDiscriminator(options, internalOptions, messageFunctions); // Prepare after-image. const afterImage = forHdi(csn, options, messageFunctions); const diff = modelCompare.compareModels(beforeImage || afterImage, afterImage, internalOptions); messageFunctions.setModel(diff); // Convert the diff to SQL. if (!internalOptions.beta) internalOptions.beta = {}; internalOptions.beta.sqlExtensions = true; // Ignore constraint drops - that is handled by .hdbconstraint et. al. const { // eslint-disable-next-line no-unused-vars deletions, migrations, constraintDeletions, ...hdbkinds } = toSql.toSqlDdl(diff, internalOptions, messageFunctions); return { afterImage, definitions: createSqlDefinitions(hdbkinds, afterImage), deletions: createSqlDeletions(deletions, beforeImage, options), migrations: createSqlMigrations(migrations, afterImage), }; } /** * From the given SQLs, create the correct result structure. * * @param {object} hdbkinds Object of hdbkinds (such as `hdbindex`) mapped to dictionary of artifacts. * @param {CSN.Model} afterImage CSN, used to create correct file names in result structure. * @returns {object[]} Array of objects, each having: name, suffix and sql */ function createSqlDefinitions( hdbkinds, afterImage ) { const result = []; objectUtils.forEach(hdbkinds, (kind, artifacts) => { const suffix = `.${ kind }`; objectUtils.forEach(artifacts, (name, sqlStatement) => { if ( kind !== 'hdbindex' ) result.push({ name: getFileName(name, afterImage), suffix, sql: sqlStatement }); else result.push({ name, suffix, sql: sqlStatement }); }); }); return result; } /** * From the given deletions, create the correct result structure. * * @param {object} deletions Dictionary of deletions, only keys are used. * @param {CSN.Model} beforeImage CSN used to create correct file names in result structure. * @returns {object[]} Array of objects, each having: name and suffix - only .hdbtable as suffix for now */ function createSqlDeletions( deletions, beforeImage, options ) { const result = []; objectUtils.forEach(deletions, name => result.push({ name: getFileName(name, beforeImage), suffix: getSuffix(beforeImage, beforeImage.definitions[name], options), })); return result; } /** * Determines the appropriate file suffix based on the type of the provided artifact. * * @param {CSN.Artifact} artifact - The artifact object to evaluate. * @returns {string} The file suffix corresponding to the artifact type: * - '.hdbview' for query artifacts * - '.hdbprojectionview' for projection artifacts * - '.hdbtable' for other artifacts */ function getSuffix( csn, artifact, options ) { if (artifact.query || artifact.projection) return sqlRenderUtils.isProjectionView(csn, artifact, options) ? '.hdbprojectionview' : '.hdbview'; return '.hdbtable'; } /** * From the given migrations, create the correct result structure. * * @param {object} migrations Dictionary of changesets (migrations). * @param {CSN.Model} afterImage CSN used to create correct file names in result structure. * @returns {object[]} Array of objects, each having: name, suffix and changeset. */ function createSqlMigrations( migrations, afterImage ) { const result = []; objectUtils.forEach(migrations, (name, changeset) => result.push({ name: getFileName(name, afterImage), suffix: '.hdbmigrationtable', changeset })); return result; } hdi.migration = hdiMigration; sql.migration = sqlMigration; /** * Process the given CSN into HDBCDS artifacts. * * @param {any} csn A clean input CSN * @param {HdbcdsOptions} options Options * @param {object} messageFunctions Message functions such as `error()`, `info()`, … * @returns {HDBCDS} { <filename>:<content>, ...} */ function hdbcds( csn, options, messageFunctions ) { const internalOptions = prepareOptions.to.hdbcds(options); internalOptions.transformation = 'hdbcds'; messageFunctions.setOptions( internalOptions ); // Since v5, the HDBCDS backend is considered deprecated. // Since v6, it is a configurable error. messageFunctions.message('api-deprecated-hdbcds', null, null); if (options.tenantDiscriminator) { messageFunctions.error('api-invalid-option', null, { '#': 'forbidden', option: 'tenantDiscriminator', module: 'to.hdbcds', }); messageFunctions.throwWithAnyError(); } const hanaCsn = forHdbcds(csn, internalOptions, messageFunctions); messageFunctions.setModel(hanaCsn); const result = toHdbcds.toHdbcdsSource(hanaCsn, internalOptions, messageFunctions); return flattenResultStructure(result); } /** * Generate an edm document for the given service * * @param {CSN|oDataCSN} csn Clean input CSN or a pre-transformed CSN * @param {ODataOptions} options Options * @param {object} messageFunctions Message functions such as `error()`, `info()`, … * @returns {edm} The JSON representation of the service */ function edm( csn, options, messageFunctions ) { // If not provided at all, set service to 'undefined' to trigger validation const internalOptions = prepareOptions.to.edm( options.service ? options : Object.assign({ service: undefined }, options) ); messageFunctions.setOptions( internalOptions ); const { service } = options; let servicesEdmj; if (isPreTransformed(csn, 'odata')) { checkPreTransformedCsn(csn, internalOptions, relevantOdataOptions, warnAboutMismatchOdata, messageFunctions); servicesEdmj = preparedCsnToEdm(csn, service, internalOptions, messageFunctions); } else { const oDataCsn = odataInternal(csn, internalOptions, messageFunctions); messageFunctions.setModel(oDataCsn); servicesEdmj = preparedCsnToEdm(oDataCsn, service, internalOptions, messageFunctions); } return servicesEdmj.edmj; } edm.all = edmall; /** * Generate edm documents for all services * * @param {CSN|oDataCSN} csn Clean input CSN or a pre-transformed CSN * @param {ODataOptions} options Options * @param {object} messageFunctions Message functions such as `error()`, `info()`, … * @returns {edms} { <service>:<JSON representation>, ...} */ function edmall( csn, options, messageFunctions ) { const internalOptions = prepareOptions.to.edm(options); messageFunctions.setOptions( internalOptions ); const { error } = messageFunctions; if (internalOptions.odataVersion === 'v2') error('api-invalid-option', null, { '#': 'odataV2json' }); const result = {}; let oDataCsn = csn; if (isPreTransformed(csn, 'odata')) { checkPreTransformedCsn(csn, internalOptions, relevantOdataOptions, warnAboutMismatchOdata, messageFunctions); } else { oDataCsn = odataInternal(csn, internalOptions, messageFunctions); } messageFunctions.setModel(oDataCsn); const servicesJson = preparedCsnToEdmAll(oDataCsn, internalOptions, messageFunctions); const services = servicesJson.edmj; for (const serviceName in services) result[serviceName] = services[serviceName]; return result; } /** * Generate an edmx document for the given service * * @param {CSN|oDataCSN} csn Clean input CSN or a pre-transformed CSN * @param {ODataOptions} options Options * @param {object} messageFunctions Message functions such as `error()`, `info()`, … * @returns {edmx} The XML representation of the service */ function edmx( csn, options, messageFunctions ) { // If not provided at all, set service to 'undefined' to trigger validation const internalOptions = prepareOptions.to.edmx( options.service ? options : Object.assign({ service: undefined }, options) ); messageFunctions.setOptions( internalOptions ); const { service } = options; let services; if (isPreTransformed(csn, 'odata')) { checkPreTransformedCsn(csn, internalOptions, relevantOdataOptions, warnAboutMismatchOdata, messageFunctions); services = preparedCsnToEdmx(csn, service, internalOptions, messageFunctions); } else { const oDataCsn = odataInternal(csn, internalOptions, messageFunctions); messageFunctions.setModel(oDataCsn); services = preparedCsnToEdmx(oDataCsn, service, internalOptions, messageFunctions); } return services.edmx; } edmx.all = edmxall; /** * Generate edmx documents for all services * * @param {CSN|oDataCSN} csn Clean input CSN or a pre-transformed CSN * @param {ODataOptions} options Options * @param {object} messageFunctions Message functions such as `error()`, `info()`, … * @returns {edmxs} { <service>:<XML representation>, ...} */ function edmxall( csn, options, messageFunctions ) { const internalOptions = prepareOptions.to.edmx(options); messageFunctions.setOptions( internalOptions ); const result = {}; let oDataCsn = csn; if (isPreTransformed(csn, 'odata')) { checkPreTransformedCsn(csn, internalOptions, relevantOdataOptions, warnAboutMismatchOdata, messageFunctions); } else { oDataCsn = odataInternal(csn, internalOptions, messageFunctions); } messageFunctions.setModel(oDataCsn); const servicesEdmx = preparedCsnToEdmxAll(oDataCsn, internalOptions, messageFunctions); const services = servicesEdmx.edmx; // Create annotations and metadata once per service for (const serviceName in services) { const lEdm = services[serviceName]; result[serviceName] = lEdm; } return result; } /** * Generate an EDM document for the given service in XML and JSON representation * If odataVersion is not 'v4', then no JSON is rendered * * @param {CSN|oDataCSN} csn Clean input CSN or a pre-transformed CSN * @param {ODataOptions} options Options * @param {object} messageFunctions Message functions such as `error()`, `info()`, … * @returns {object} { <protocol> : { <ServiceName>: { edmx: <XML representation>, edm: <JSON representation> } } } */ // @ts-ignore function odata2( csn, options, messageFunctions ) { // If not provided at all, set service to 'undefined' to trigger validation const internalOptions = prepareOptions.to.odata( options.service ? options : Object.assign({ service: undefined }, options) ); messageFunctions.setOptions( internalOptions ); const { service } = options; let oDataCsn = csn; if (isPreTransformed(csn, 'odata')) { checkPreTransformedCsn(csn, internalOptions, relevantOdataOptions, warnAboutMismatchOdata, messageFunctions); } else { oDataCsn = odataInternal(csn, internalOptions, messageFunctions); messageFunctions.setModel(oDataCsn); } const edmIR = csnToEdm.csn2edm(oDataCsn, service, internalOptions, messageFunctions); const version = internalOptions.odataVersion; const result = { [version]: { [service]: {} } }; if (edmIR) { result[version][service].edmx = edmIR.toXML(); if (version === 'v4') result[version][service].edm = edmIR.toJSON(); } return result; } odata2.all = odataall; /** * Generate EDM documents for all services in XML and JSON representation * If odataVersion is not 'v4', then no JSON is rendered * * @param {CSN|oDataCSN} csn Clean input CSN or a pre-transformed CSN * @param {ODataOptions} options Options * @param {object} messageFunctions Message functions such as `error()`, `info()`, … * @returns {object} { <protocol>: { <serviceName>: { edmx: <XML representation>, edm: <JSON representation> } } } */ function odataall( csn, options, messageFunctions ) { const internalOptions = prepareOptions.to.odata(options); messageFunctions.setOptions( internalOptions ); const { error } = messageFunctions; if (internalOptions.odataVersion === 'v2') error('api-invalid-option', null, { '#': 'odataV2json' }); let oDataCsn = csn; if (isPreTransformed(csn, 'odata')) { checkPreTransformedCsn(csn, internalOptions, relevantOdataOptions, warnAboutMismatchOdata, messageFunctions); } else { oDataCsn = odataInternal(csn, internalOptions, messageFunctions); messageFunctions.setModel(oDataCsn); } const edmIR = csnToEdm.csn2edmAll(oDataCsn, internalOptions, undefined, messageFunctions); const version = internalOptions.odataVersion; const result = {}; result[version] = {}; if (edmIR) { for (const serviceName in edmIR) { result[version][serviceName] = { edmx: edmIR[serviceName].toXML() }; if (internalOptions.odataVersion === 'v4') result[version][serviceName].edm = edmIR[serviceName].toJSON(); } } return result; } /** * Generate edmx for given 'service' based on 'csn' (new-style compact, already prepared for OData) * using 'options' * * @param {CSN.Model} csn Input CSN model. Must be OData transformed CSN. * @param {string} service Service name to use. If you want all services, use preparedCsnToEdmxAll() * @param {ODataOptions} options OData / EDMX specific options. * @param {object} messageFunctions Message functions such as `error()`, `info()`, … * @returns {object} Rendered EDMX string for the given service. */ function preparedCsnToEdmx( csn, service, options, messageFunctions ) { timetrace.timetrace.start('EDMX rendering'); const e = csnToEdm.csn2edm(csn, service, options, messageFunctions)?.toXML('all'); timetrace.timetrace.stop('EDMX rendering'); return { edmx: e }; } /** * Generate edmx for given 'service' based on 'csn' (new-style compact, already prepared for OData) * using 'options'. * * @param {CSN.Model} csn Input CSN model. Must be OData transformed CSN. * @param {ODataOptions} options OData / EDMX specific options. * @param {object} messageFunctions Message functions such as `error()`, `info()`, … * @returns {object} Dictionary of rendered EDMX strings for each service. */ function preparedCsnToEdmxAll( csn, options, messageFunctions ) { timetrace.timetrace.start('EDMX all rendering'); const edmxResult = csnToEdm.csn2edmAll(csn, options, undefined, messageFunctions); for (const service in edmxResult) edmxResult[service] = edmxResult[service].toXML('all'); timetrace.timetrace.stop('EDMX all rendering'); return { edmx: edmxResult }; } /** * Generate edm-json for given 'service' based on 'csn' (new-style compact, already prepared for OData) * using 'options' * * @param {CSN.Model} csn Input CSN model. Must be OData transformed CSN. * @param {string} service Service name for which EDMX should be rendered. * @param {ODataOptions} options OData / EDMX specific options. * @param {object} [messageFunctions] Message functions such as `error()`, `info()`, … * @returns {object} Rendered EDM JSON object for of the given service. */ function preparedCsnToEdm( csn, service, options, messageFunctions ) { timetrace.timetrace.start('EDM rendering'); // Override OData version as edm json is always v4 options.odataVersion = 'v4'; const e = csnToEdm.csn2edm(csn, service, options, messageFunctions)?.toJSON(); timetrace.timetrace.stop('EDM rendering'); return { edmj: e }; } /** * Generate edm-json for given 'service' based on 'csn' (new-style compact, already prepared for OData) * using 'options' * * @param {CSN.Model} csn Input CSN model. Must be OData transformed CSN. * @param {ODataOptions} options OData / EDMX specific options. * @param {object} [messageFunctions] Message functions such as `error()`, `info()`, … * @returns {object} Dictionary of rendered EDM JSON objects for each service. */ function preparedCsnToEdmAll( csn, options, messageFunctions ) { timetrace.timetrace.start('EDM all rendering'); // Override OData version as edm json is always v4 options.odataVersion = 'v4'; const edmj = csnToEdm.csn2edmAll(csn, options, undefined, messageFunctions); for (const service in edmj) edmj[service] = edmj[service].toJSON(); timetrace.timetrace.stop('EDM all rendering'); return { edmj, }; } /** * Flatten the result structure to a flat map. * * Don't loop over messages. * * @param {object} toProcess { <type>: { <name>:<content>, ...}, <type>: ...} * @returns {object} { <name.type>:<content> } */ function flattenResultStructure( toProcess ) { const result = {}; objectUtils.forEach(toProcess, (fileType, artifacts) => { if (fileType === 'messages') return; objectUtils.forEach(artifacts, (filename) => { result[`${ filename }.${ fileType }`] = artifacts[filename]; }); }); return result; } module.exports = { odata: publishCsnProcessor(odata, 'for.odata'), java: publishCsnProcessor(java, 'for.java'), cdl: publishCsnProcessor(cdl, 'to.cdl'), sql: publishCsnProcessor(sql, 'to.sql'), hdi: publishCsnProcessor(hdi, 'to.hdi'), hdbcds: publishCsnProcessor(hdbcds, 'to.hdbcds'), edm: publishCsnProcessor(edm, 'to.edm'), edmx: publishCsnProcessor(edmx, 'to.edmx'), odata2: publishCsnProcessor(odata2, 'to.odata'), /** Internal only */ for_sql: publishCsnProcessor(forSql, 'for.sql'), for_hdi: publishCsnProcessor(forHdi, 'for.hdi'), for_hdbcds: publishCsnProcessor(forHdbcds, 'for.hdbcds'), for_effective: publishCsnProcessor(forEffective, 'for.effective'), for_seal: publishCsnProcessor(forSeal, 'for.seal'), }; /** * @param {any} processor CSN processor * @param {string} _name Name of the processor * @returns {any} Function that calls the processor and recompiles in case of internal errors */ function publishCsnProcessor( processor, _name ) { api.internal = processor; if (processor.all) api.all = publishCsnProcessor(processor.all, `${ _name }.all`); if (processor.migration) api.migration = publishCsnProcessor(processor.migration, `${ _name }.migration`); return api; /** * Function that calls the processor and re-compiles in case of internal errors * * @param {CSN.Model} csn CSN * @param {CSN.Options} options Options * @param {any} args Any additional arguments * @returns {any} What ever the processor returns */ function api( csn, options = {}, ...args ) { trace.traceApi(_name, options); const originalMessageLength = options.messages?.length; try { const messageFunctions = messages.makeMessageFunction(csn, options, _name); if (options.deprecated) baseModel.checkRemovedDeprecatedFlags( options, messageFunctions ); checkOutdatedOptions( options, messageFunctions ); csn = ensureClientCsn( csn, options, messageFunctions, _name ); messageFunctions.throwWithError(); messageFunctions.setModel(csn); timetrace.timetrace.start(_name); const result = processor( csn, options, messageFunctions, ...args ); timetrace.timetrace.stop(_name); return result; } catch (err) { timetrace.timetrace.reset('Exception in backend triggered'); if (err instanceof messages.CompilationError || options.noRecompile || isPreTransformed(csn, 'odata')) // we cannot recompile a pre-transformed CSN throw err; if (options.testMode && !(err instanceof TypeError) && !(err instanceof baseError.ModelError)) throw err; // Reset messages to what we had before the backend crashed. // Backends may report the same issues again after compilation. if (originalMessageLength !== undefined) options.messages.length = originalMessageLength; const messageFunctions = messages.makeMessageFunction( csn, options, _name ); const recompileMsg = messageFunctions.info( 'api-recompiled-csn', location.emptyLocation('csn.json'), {}, 'CSN input had to be recompiled' ); if (options.internalMsg || options.testMode) recompileMsg.error = err; // Attach original error; if (options.testMode) // Attach recompilation reason in testMode recompileMsg.message += `\n ↳ cause: ${ err.message }`; const xsn = compiler.recompileX(csn, options); const recompiledCsn = toCsn.compactModel(xsn); messageFunctions.setModel(recompiledCsn); return processor( recompiledCsn, options, messageFunctions, ...args ); } } } // Note: No toCsn, because @sap/cds may still use it (2022-06-15) const oldBackendOptionNames = [ 'toSql', 'toOdata', 'toHana', 'forHana' ]; /** * Checks if outdated options are used and if so, throw a compiler error. * These include: * - magicVars (now variableReplacements) * - toOdata/toSql/toHana/forHana -> now flat options * * @param {CSN.Options} options Backend options * @param {object} messageFunctions Functions returned by makeMessageFunction() */ function checkOutdatedOptions( options, messageFunctions ) { // This error has been emitted once, we don't need to emit it again. if (options.messages?.some(m => m.messageId === 'api-invalid-option' || m.messageId === 'api-invalid-variable-replacement')) return; for (const name of oldBackendOptionNames) { if (typeof options[name] === 'object') // may be a boolean due to internal options messageFunctions.error('api-invalid-option', null, { '#': 'deprecated', name }); } if (options.magicVars) messageFunctions.error('api-invalid-option', null, { '#': 'magicVars', prop: 'magicVars', otherprop: 'variableReplacements' }); // Don't check `options.magicVars`. It's likely that the user renamed `magicVars` but // forgot about user -> $user and locale -> $user.locale if (options.variableReplacements?.user) { messageFunctions.error('api-invalid-variable-replacement', null, { '#': 'user', option: 'variableReplacements', prop: '$user', otherprop: 'user', }); } if (options.variableReplacements?.locale) { messageFunctions.error('api-invalid-variable-replacement', null, { '#': 'locale', option: 'variableReplacements', prop: '$user.locale', otherprop: 'locale', }); } objectUtils.forEachKey(options.variableReplacements || {}, (name) => { if (!name.startsWith('$') && name !== 'user' && name !== 'locale') { messageFunctions.error('api-invalid-variable-replacement', null, { '#': 'noDollar', option: 'variableReplacements', code: '$', name, }); } }); } /** * Checks that the given CSN is usable by our backends, e.g. that * the CSN is not a gensrc (a.k.a. xtended) for most backends. * * Returns the input CSN if it is acceptable or compiles the input CSN if it does not * have the expected CSN flavor. * * The compiler does not set any marker in `meta`; we use the umbrella one * for easier debugging. * * For reference, cds-compiler/cds-dk CSN flavor map: * - client -> inferred * - gensrc -> xtended * - parseCdl -> parsed * * If this function becomes more complex (e.g. more module conditions), * move it from then generic api wrapper to the individual module. * * @param {CSN.Model} csn User CSN * @param {CSN.Options} options User options * @param {object} messageFunctions Functions returned by makeMessageFunction() * @param {string} module Backend module, e.g. to.cdl or to.sql * @returns {CSN.Model} CSN that works for backends. */ function ensureClientCsn( csn, options, messageFunctions, module ) { if (module === 'to.cdl' || !csn) return csn; // to.cdl allows every CSN flavor if (csn.meta?.flavor === 'xtended') { messageFunctions.error('api-unsupported-csn-flavor', null, { name: module, option: csn.meta?.flavor }); return csn; } // `parsed` CSN is allowed if it can be compiled (i.e. no `requires`). // Still return false, because it's not client CSN. The caller must handle it. if (csn.meta?.fl