UNPKG

@sap/cds-dk

Version:

Command line client and development toolkit for the SAP Cloud Application Programming Model

225 lines (202 loc) 9.04 kB
const path = require('path') const parser = require('./migrationtable') const { BuildError, BuildMessage } = require('../../util') const { SEVERITY_WARNING } = require('../../constants') const cds = require('../../../cds'), { compiler: cdsc } = cds const cdscVersion = `-- generated by cds-compiler version ${cdsc.version()}` const { getArtifactCdsPersistenceName } = cdsc const LOG = cds.log('cli|build'), DEBUG = cds.debug('cli|build') async function compileToHdbMigration(model, beforeImage, srcPath, options = {}) { const { definitions, migrations, afterImage, migrationTableNames } = _compileToHana(model, options, beforeImage) const definitionResult = [] for (const { name, suffix, sql } of definitions) { let definitionEntry = { name, suffix, content: sql } if (suffix === '.hdbtable') { if (migrationTableNames.has(name)) { const migration = migrations.find(migration => migration.name === name) definitionEntry = await _2migrationtable(srcPath, migration || _emptyMigration(name), sql) } } if (definitionEntry) { definitionResult.push(definitionEntry) } } return { definitions: definitionResult, afterImage } } function getJournalEntities(model, options = {}) { return cds.reflect(model).all(item => { if (item.kind === 'entity' && item['@cds.persistence.journal'] === true) { if (item['@cds.persistence.skip'] === true || item['@cds.persistence.exists'] === true) { options.messages?.push(new BuildMessage(`[hdbmigrationtable] annotation @cds.persistence.journal skipped for entity '${item.name}' as persistence exists`, SEVERITY_WARNING)) } return true } return false }) } function getMigrationTableName(entity, model) { return getArtifactCdsPersistenceName(entity.name, 'quoted', model, 'hana') } function _compileToHana(model, options, beforeImage) { let definitions = [] let migrations = [] let afterImage const result = cds.compile.to.hana(model, options, beforeImage); for (const [content, { file }] of result) { switch (file) { case 'deletions.json': break case 'migrations.json': migrations = content break case 'afterImage.json': afterImage = content break default: { const { name, ext: suffix } = path.parse(file) definitions.push({ name, suffix, sql: content }) } } } let migrationTableNames = new Set() if (afterImage) { // 1. determine journal file names before _filterJournalArtifacts is called as getArtifactCdsPersistenceName // might no longer return the proper file name for the reduced CSN, e.g. for draft entities migrationTableNames = _getMigrationTableNames(afterImage, options) // 2. leave only persisted journal-related entities in afterImage afterImage = _filterJournalArtifacts(afterImage) } _debug(definitions, migrations, migrationTableNames) return { definitions, migrations, afterImage, migrationTableNames } } /** * Returns an object providing access to the .hdbmigrationtable file content and its corresponding filename. * * @param {String} srcPath Fully qualified path of the directory containing .hdbmigrationtable files * @param {String} migration the migration descriptor * @param {String} tableSql SQL TABLE definition * @returns {Promise<Object>} Providing access to 'content' and 'fileName'. */ async function _2migrationtable(srcPath, migration, tableSql) { let migrationTableModel = null const file = path.join(srcPath, migration.name + migration.suffix) try { migrationTableModel = await parser.read(file) } catch (e) { // abort build in order to ensure consistent afterImage model state / hdbmigrationtable file state throw new BuildError(`${path.relative(process.cwd(), file)}: ${e.message}`) } if (migrationTableModel) { // adding new changeset if change exist, ignore otherwise const migrationEntry = _getNewMigrationEntry(migration.changeset, migrationTableModel.versionNumber) if (migrationEntry) { const versionNumber = migrationTableModel.versionNumber + 1 const migrationCount = migrationTableModel.migrations.entries.length const migrations = `${migrationEntry.content}${migrationCount > 0 ? '\n' : ''}${migrationTableModel.migrations.toString()}${migrationCount > 0 ? '\n' : ''}` return { name: migration.name, suffix: migration.suffix, content: `== version=${versionNumber}\n${tableSql}\n\n${migrations}`, changed: true, dropColumns: migrationEntry.dropColumns } } else { // existing migration file version return { name: migration.name, suffix: migration.suffix, content: migrationTableModel.toString(), changed: false } } } // initial migration file version return { name: migration.name, suffix: migration.suffix, content: `== version=1\n${tableSql}\n`, changed: true } } function _getNewMigrationEntry(changeset, currentVersion) { if (changeset && changeset.length > 0) { const dropColumns = changeset.some(e => e.drop) const manualChange = changeset.some(e => !e.sql) const enableDrop = cds.env.hana?.journal?.['enable-drop'] const content = changeset.reduce((acc, e) => { if (!acc) { acc = `== migration=${currentVersion + 1}\n` acc += `${cdscVersion}\n` if (dropColumns && enableDrop !== true) { acc += `>>>>> Manual resolution required - DROP statements causing data loss are disabled by default. >>>>> You may either: >>>>> uncomment statements to allow incompatible changes, or >>>>> refactor statements, e.g. replace DROP/ADD by single RENAME statement >>>>> After manual resolution delete all lines starting with >>>>>\n` } else if (manualChange) { acc += `>>>>> Manual resolution required - insert ALTER statement(s) as described below. >>>>> After manual resolution delete all lines starting with >>>>>\n` } } if (e.sql) { if (e.drop && enableDrop !== true) { acc += `${e.sql.replace(/^/gm, '-- ')}\n`; } else { acc += `${e.sql}\n` } } else { acc += `>>>>> Insert ALTER statement for: ${e.description}\n` } return acc }, null) return { dropColumns, content } } return null } function _emptyMigration(name) { return { name, suffix: ".hdbmigrationtable", changeset: [] } } function _getMigrationTableNames(model, options) { const migrationTableNames = getJournalEntities(model, options).map(entity => getMigrationTableName(entity, model)) DEBUG?.(`\n[hdbmigrationtable] found ${migrationTableNames.length} model entities annotated with '@cds.persistence.journal`) DEBUG?.(`[hdbmigrationtable] ${migrationTableNames.join(', ')}\n`) return new Set(migrationTableNames) } // only keep journal related entities function _filterJournalArtifacts(afterImage) { if (!afterImage) { return } const dict = afterImage.definitions for (const name in dict) { if (!_isPersistedAsJournalTable(dict[name])) { delete dict[name] } } // mark CSN as modified by cds afterImage.meta.build = `CDS Build v${cds.version}` return afterImage } // see cds-compiler/lib/model/csnUtils.js#isPersistedAsTable function _isPersistedAsJournalTable(artifact) { return ( artifact.kind === 'entity' && artifact['@cds.persistence.journal'] === true && !artifact.abstract && artifact['@cds.persistence.skip'] !== true && // might have value 'if-unused' artifact['@cds.persistence.exists'] !== true && (!artifact.query && !artifact.projection || artifact['@cds.persistence.table'] === true) ) } function _debug(definitions, migrations, journalFileNames) { if (LOG._debug) { DEBUG('compile.to.hana returned') for (const { name, suffix, sql } of definitions) { if (suffix === '.hdbtable' && journalFileNames.has(name)) { const migration = migrations.find(migration => migration.name === name) DEBUG(`File ${name + '.hdbmigrationtable'} - ${migration ? migration.changeset.length : 0} new changes ${sql} ${migration ? migration.changeset.map(change => change.sql).join('\n') : 'Empty changeset'} `) } } } } module.exports = { compileToHdbMigration, getJournalEntities, getMigrationTableName }