add confirmation to transfer and import

This commit is contained in:
Ben Irvin 2023-05-08 10:44:29 +02:00
parent 4b37d806ca
commit 58b8c18870
6 changed files with 74 additions and 50 deletions

View File

@ -413,6 +413,7 @@ class TransferEngine<
}
});
// TODO: make these messages friendlier, especially for internal strapi features like review workflows
if (!isEmpty(diffs)) {
const formattedDiffs = Object.entries(diffs)
.map(([uid, ctDiffs]) => {
@ -424,11 +425,11 @@ class TransferEngine<
const path = diff.path.join('.');
if (diff.kind === 'added') {
return `${path} exists in destination schema but not in source schema`;
return `${path} exists in destination schema but not in source schema and the data will not be transfered.`;
}
if (diff.kind === 'deleted') {
return `${path} exists in source schema but not in destination schema`;
return `${path} exists in source schema but not in destination schema and the data will not be transfered.`;
}
if (diff.kind === 'modified') {
@ -630,25 +631,26 @@ class TransferEngine<
#schemaDiffs: Record<string, Diff[]> = {};
async integrityCheck() {
const sourceMetadata = await this.sourceProvider.getMetadata();
const destinationMetadata = await this.destinationProvider.getMetadata();
if (sourceMetadata && destinationMetadata) {
this.#assertStrapiVersionIntegrity(
sourceMetadata?.strapi?.version,
destinationMetadata?.strapi?.version
);
}
const sourceSchemas = (await this.sourceProvider.getSchemas?.()) as SchemaMap;
const destinationSchemas = (await this.destinationProvider.getSchemas?.()) as SchemaMap;
try {
const sourceMetadata = await this.sourceProvider.getMetadata();
const destinationMetadata = await this.destinationProvider.getMetadata();
if (sourceMetadata && destinationMetadata) {
this.#assertStrapiVersionIntegrity(
sourceMetadata?.strapi?.version,
destinationMetadata?.strapi?.version
);
}
const sourceSchemas = (await this.sourceProvider.getSchemas?.()) as SchemaMap;
const destinationSchemas = (await this.destinationProvider.getSchemas?.()) as SchemaMap;
if (sourceSchemas && destinationSchemas) {
this.#assertSchemasMatching(sourceSchemas, destinationSchemas);
}
} catch (error) {
if (error instanceof TransferEngineValidationError) {
// if this is a schema matching error
if (error instanceof TransferEngineValidationError && error.details?.details?.diffs) {
this.#schemaDiffs = error.details?.details?.diffs as Record<string, Diff[]>;
const context = {
@ -670,6 +672,7 @@ class TransferEngine<
}
return;
}
throw error;
}
}

View File

@ -11,7 +11,6 @@ const {
} = require('@strapi/data-transfer');
const { isObject } = require('lodash/fp');
const inquirer = require('inquirer');
const {
buildTransferTable,
@ -24,6 +23,7 @@ const {
getTransferTelemetryPayload,
} = require('../../utils/data-transfer');
const { exitWith } = require('../../utils/helpers');
const { confirmMessage } = require('../../utils/commander');
/**
* @typedef {import('@strapi/data-transfer/src/file/providers').ILocalFileSourceProviderOptions} ILocalFileSourceProviderOptions
@ -113,18 +113,16 @@ module.exports = async (opts) => {
const { updateLoader } = loadersFactory();
engine.onSchemaDiff(async (context, next) => {
// TODO: work with "force"
// TODO: yes/no prompt should look like commander prompt
const answers = await inquirer.prompt([
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?',
{
type: 'confirm',
message: 'Are you sure you want to continue?',
name: 'confirmSchemaDiff',
},
]);
if (answers.confirmSchemaDiff) {
// diffs have been resolved by user
force: opts.force,
}
);
if (confirmed) {
context.diffs = [];
return next(context);
}
return next(context);

View File

@ -9,7 +9,7 @@ const {
throttleOption,
validateExcludeOnly,
} = require('../../utils/data-transfer');
const { confirmMessage, forceOption } = require('../../utils/commander');
const { getCommanderConfirmMessage, forceOption } = require('../../utils/commander');
const { getLocalScript, exitWith } = require('../../utils/helpers');
/**
@ -88,7 +88,7 @@ module.exports = ({ command }) => {
})
.hook(
'preAction',
confirmMessage(
getCommanderConfirmMessage(
'The import will delete all assets and data in your database. Are you sure you want to proceed?',
{ failMessage: 'Import process aborted' }
)

View File

@ -24,6 +24,7 @@ const {
getTransferTelemetryPayload,
} = require('../../utils/data-transfer');
const { exitWith } = require('../../utils/helpers');
const { confirmMessage } = require('../../utils/commander');
/**
* @typedef TransferCommandOptions Options given to the CLI transfer command
@ -146,6 +147,22 @@ module.exports = async (opts) => {
const { updateLoader } = loadersFactory();
engine.onSchemaDiff(async (context, next) => {
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?',
{
force: opts.force,
}
);
if (confirmed) {
context.diffs = [];
return next(context);
}
return next(context);
});
progress.on(`stage::start`, ({ stage, data }) => {
updateLoader(stage, data).start();
});

View File

@ -2,7 +2,7 @@
const inquirer = require('inquirer');
const { Option } = require('commander');
const { confirmMessage, forceOption, parseURL } = require('../../utils/commander');
const { getCommanderConfirmMessage, forceOption, parseURL } = require('../../utils/commander');
const {
getLocalScript,
exitWith,
@ -76,7 +76,7 @@ module.exports = ({ command }) => {
thisCommand.opts().fromToken = answers.fromToken;
}
await confirmMessage(
await getCommanderConfirmMessage(
'The transfer will delete all the local Strapi assets and its database. Are you sure you want to proceed?',
{ failMessage: 'Transfer process aborted' }
)(thisCommand);
@ -104,7 +104,7 @@ module.exports = ({ command }) => {
thisCommand.opts().toToken = answers.toToken;
}
await confirmMessage(
await getCommanderConfirmMessage(
'The transfer will delete all the remote Strapi assets and its database. Are you sure you want to proceed?',
{ failMessage: 'Transfer process aborted' }
)(thisCommand);

View File

@ -111,30 +111,35 @@ const promptEncryptionKey = async (thisCommand) => {
* @param {object} options Additional options
* @param {string|undefined} options.failMessage The message to display when prompt is not confirmed
*/
const confirmMessage = (message, { failMessage } = {}) => {
const getCommanderConfirmMessage = (message, { failMessage } = {}) => {
return async (command) => {
// if we have a force option, assume yes
const opts = command.opts();
if (opts?.force === true) {
// attempt to mimic the inquirer prompt exactly
console.log(`${green('?')} ${bold(message)} ${cyan('Yes')}`);
return;
}
const answers = await inquirer.prompt([
{
type: 'confirm',
message,
name: `confirm`,
default: false,
},
]);
if (!answers.confirm) {
const confirmed = await confirmMessage(message, { force: command.opts().force });
if (!confirmed) {
exitWith(1, failMessage);
}
};
};
const confirmMessage = async (message, { force } = {}) => {
// if we have a force option, respond yes
if (force === true) {
// attempt to mimic the inquirer prompt exactly
console.log(`${green('?')} ${bold(message)} ${cyan('Yes')}`);
return true;
}
const answers = await inquirer.prompt([
{
type: 'confirm',
message,
name: `confirm`,
default: false,
},
]);
return answers.confirm;
};
const forceOption = new Option(
'--force',
`Automatically answer "yes" to all prompts, including potentially destructive requests, and run non-interactively.`
@ -146,6 +151,7 @@ module.exports = {
parseURL,
parseInteger,
promptEncryptionKey,
getCommanderConfirmMessage,
confirmMessage,
forceOption,
};