diff --git a/packages/core/data-transfer/src/engine/index.ts b/packages/core/data-transfer/src/engine/index.ts index 39fe046fb2..dbe5ef837b 100644 --- a/packages/core/data-transfer/src/engine/index.ts +++ b/packages/core/data-transfer/src/engine/index.ts @@ -5,6 +5,7 @@ import { isEmpty, uniq, last, isNumber, difference, omit, set } from 'lodash/fp' import { diff as semverDiff } from 'semver'; import type { Schema } from '@strapi/strapi'; +import chalk from 'chalk'; import type { IAsset, IDestinationProvider, @@ -567,7 +568,11 @@ class TransferEngine< // Cause an ongoing transfer to abort gracefully async abortTransfer(): Promise { - this.#currentStream?.destroy(new TransferEngineError('fatal', 'Transfer aborted.')); + const err = new TransferEngineError('fatal', 'Transfer aborted.'); + if (!this.#currentStream) { + throw err; + } + this.#currentStream?.destroy(err); } async init(): Promise { @@ -659,12 +664,62 @@ class TransferEngine< destination: this.destinationProvider, }; + let workflowsStatus; + const source = 'Schema Integrity'; + Object.entries(context.diffs).forEach(([uid, diffs]) => { for (const diff of diffs) { - this.#reportWarning(`${diff.path.join('.')} for ${uid}`, 'Schema Integrity Check'); + const path = `${uid}.${diff.path.join('.')}`; + const endPath = diff.path[diff.path.length - 1]; + + // Catch known features + // TODO: can this be moved outside of the engine? + if ( + uid === 'admin::workflow' || + uid === 'admin::workflow-stage' || + endPath.startsWith('strapi_reviewWorkflows_') + ) { + workflowsStatus = diff.kind; + } + // handle generic cases + else if (diff.kind === 'added') { + this.#reportWarning( + chalk.red(`${chalk.bold(path)} does not exist on destination`), + source + ); + } else if (diff.kind === 'deleted') { + this.#reportWarning( + chalk.red(`${chalk.bold(path)} does not exist on source`), + source + ); + } else if (diff.kind === 'modified') { + this.#reportWarning( + chalk.red(`${chalk.bold(path)} has a different data type`), + source + ); + } } }); + // output the known feature warnings + if (workflowsStatus === 'added') { + this.#reportWarning( + chalk.red(`Review workflows feature does not exist on destination`), + source + ); + } else if (workflowsStatus === 'deleted') { + this.#reportWarning( + chalk.red(`Review workflows feature does not exist on source`), + source + ); + } else if (workflowsStatus === 'modified') { + this.#panic( + new TransferEngineInitializationError( + 'Unresolved differences in schema [review workflows]' + ) + ); + } + await runMiddleware(context, this.#handlers.schemaDiff); if (Object.keys(context.diffs).length) { diff --git a/packages/core/strapi/lib/commands/actions/export/action.js b/packages/core/strapi/lib/commands/actions/export/action.js index fba3db1a45..23417fb6eb 100644 --- a/packages/core/strapi/lib/commands/actions/export/action.js +++ b/packages/core/strapi/lib/commands/actions/export/action.js @@ -25,6 +25,7 @@ const { exitMessageText, abortTransfer, getTransferTelemetryPayload, + setSignalHandler, } = require('../../utils/data-transfer'); const { exitWith } = require('../../utils/helpers'); @@ -115,10 +116,7 @@ module.exports = async (opts) => { let outFile; try { // Abort transfer if user interrupts process - ['SIGTERM', 'SIGINT', 'SIGQUIT'].forEach((signal) => { - process.removeAllListeners(signal); - process.on(signal, () => abortTransfer({ engine, strapi })); - }); + setSignalHandler(() => abortTransfer({ engine, strapi })); results = await engine.transfer(); outFile = results.destination.file.path; diff --git a/packages/core/strapi/lib/commands/actions/import/action.js b/packages/core/strapi/lib/commands/actions/import/action.js index c2cb97d19a..5fde4b5f94 100644 --- a/packages/core/strapi/lib/commands/actions/import/action.js +++ b/packages/core/strapi/lib/commands/actions/import/action.js @@ -21,6 +21,7 @@ const { exitMessageText, abortTransfer, getTransferTelemetryPayload, + setSignalHandler, } = require('../../utils/data-transfer'); const { exitWith } = require('../../utils/helpers'); const { confirmMessage } = require('../../utils/commander'); @@ -113,6 +114,12 @@ module.exports = async (opts) => { const { updateLoader } = loadersFactory(); engine.onSchemaDiff(async (context, next) => { + // if we abort here, we need to actually exit the process because of conflict with inquirer prompt + setSignalHandler(async () => { + await abortTransfer({ engine, strapi }); + exitWith(1, exitMessageText('import', true)); + }); + const confirmed = await confirmMessage( 'There are differences in schema between the source and destination, and the data listed above will be lost. Are you sure you want to continue?', { @@ -120,6 +127,9 @@ module.exports = async (opts) => { } ); + // reset handler back to normal + setSignalHandler(() => abortTransfer({ engine, strapi })); + if (confirmed) { context.diffs = []; return next(context); @@ -151,10 +161,7 @@ module.exports = async (opts) => { let results; try { // Abort transfer if user interrupts process - ['SIGTERM', 'SIGINT', 'SIGQUIT'].forEach((signal) => { - process.removeAllListeners(signal); - process.on(signal, () => abortTransfer({ engine, strapi })); - }); + setSignalHandler(() => abortTransfer({ engine, strapi })); results = await engine.transfer(); } catch (e) { diff --git a/packages/core/strapi/lib/commands/actions/transfer/action.js b/packages/core/strapi/lib/commands/actions/transfer/action.js index 3e7bd827bb..bfb0576c91 100644 --- a/packages/core/strapi/lib/commands/actions/transfer/action.js +++ b/packages/core/strapi/lib/commands/actions/transfer/action.js @@ -22,6 +22,7 @@ const { exitMessageText, abortTransfer, getTransferTelemetryPayload, + setSignalHandler, } = require('../../utils/data-transfer'); const { exitWith } = require('../../utils/helpers'); const { confirmMessage } = require('../../utils/commander'); @@ -148,6 +149,12 @@ module.exports = async (opts) => { const { updateLoader } = loadersFactory(); engine.onSchemaDiff(async (context, next) => { + // if we abort here, we need to actually exit the process because of conflict with inquirer prompt + setSignalHandler(async () => { + await abortTransfer({ engine, strapi }); + exitWith(1, exitMessageText('transfer', true)); + }); + const confirmed = await confirmMessage( 'There are differences in schema between the source and destination, and the data listed above will be lost. Are you sure you want to continue?', { @@ -155,6 +162,9 @@ module.exports = async (opts) => { } ); + // reset handler back to normal + setSignalHandler(() => abortTransfer({ engine, strapi })); + if (confirmed) { context.diffs = []; return next(context); @@ -188,10 +198,7 @@ module.exports = async (opts) => { let results; try { // Abort transfer if user interrupts process - ['SIGTERM', 'SIGINT', 'SIGQUIT'].forEach((signal) => { - process.removeAllListeners(signal); - process.on(signal, () => abortTransfer({ engine, strapi })); - }); + setSignalHandler(() => abortTransfer({ engine, strapi })); results = await engine.transfer(); } catch (e) { diff --git a/packages/core/strapi/lib/commands/utils/data-transfer.js b/packages/core/strapi/lib/commands/utils/data-transfer.js index bab089da3e..413f7ee95c 100644 --- a/packages/core/strapi/lib/commands/utils/data-transfer.js +++ b/packages/core/strapi/lib/commands/utils/data-transfer.js @@ -113,6 +113,15 @@ const abortTransfer = async ({ engine, strapi }) => { return true; }; +const setSignalHandler = async (handler, signals = ['SIGINT', 'SIGTERM', 'SIGQUIT']) => { + signals.forEach((signal) => { + // We specifically remove ALL listeners because we have to clear the one added in Strapi bootstrap that has a process.exit + // TODO: Ideally Strapi bootstrap would not add that listener, and then this could be more flexible and add/remove only what it needs to + process.removeAllListeners(signal); + process.on(signal, handler); + }); +}; + const createStrapiInstance = async (opts = {}) => { try { const appContext = await strapi.compile(); @@ -271,4 +280,5 @@ module.exports = { validateExcludeOnly, formatDiagnostic, abortTransfer, + setSignalHandler, };