Merge branch 'v5/main' into chore/refactor-env-to-config

This commit is contained in:
Ben Irvin 2024-01-18 17:04:45 +01:00
commit d1e7f41244
21 changed files with 352 additions and 601 deletions

View File

@ -40,7 +40,6 @@
"watch": "pack-up watch"
},
"dependencies": {
"@strapi/core": "4.17.1",
"@strapi/logger": "4.17.1",
"@strapi/types": "4.17.1",
"@strapi/utils": "4.17.1",

View File

@ -1,154 +0,0 @@
/**
* This file includes hooks to use for commander.hook and argParsers for commander.argParser
*/
import inquirer from 'inquirer';
import { Command, InvalidOptionArgumentError, Option } from 'commander';
import chalk from 'chalk';
import { isNaN } from 'lodash/fp';
import { exitWith } from './helpers';
/**
* argParser: Parse a comma-delimited string as an array
*/
const parseList = (value: string) => {
try {
return value.split(',').map((item) => item.trim()); // trim shouldn't be necessary but might help catch unexpected whitespace characters
} catch (e) {
exitWith(1, `Unrecognized input: ${value}`);
}
return [];
};
/**
* Returns an argParser that returns a list
*/
const getParseListWithChoices = (choices: string[], errorMessage = 'Invalid options:') => {
return (value: string) => {
const list = parseList(value);
const invalid = list.filter((item) => {
return !choices.includes(item);
});
if (invalid.length > 0) {
exitWith(1, `${errorMessage}: ${invalid.join(',')}`);
}
return list;
};
};
/**
* argParser: Parse a string as an integer
*/
const parseInteger = (value: string) => {
// parseInt takes a string and a radix
const parsedValue = parseInt(value, 10);
if (isNaN(parsedValue)) {
throw new InvalidOptionArgumentError(`Not an integer: ${value}`);
}
return parsedValue;
};
/**
* argParser: Parse a string as a URL object
*/
const parseURL = (value: string) => {
try {
const url = new URL(value);
if (!url.host) {
throw new InvalidOptionArgumentError(`Could not parse url ${value}`);
}
return url;
} catch (e) {
throw new InvalidOptionArgumentError(`Could not parse url ${value}`);
}
};
/**
* hook: if encrypt==true and key not provided, prompt for it
*/
const promptEncryptionKey = async (thisCommand: Command) => {
const opts = thisCommand.opts();
if (!opts.encrypt && opts.key) {
return exitWith(1, 'Key may not be present unless encryption is used');
}
// if encrypt==true but we have no key, prompt for it
if (opts.encrypt && !(opts.key && opts.key.length > 0)) {
try {
const answers = await inquirer.prompt([
{
type: 'password',
message: 'Please enter an encryption key',
name: 'key',
validate(key) {
if (key.length > 0) return true;
return 'Key must be present when using the encrypt option';
},
},
]);
opts.key = answers.key;
} catch (e) {
return exitWith(1, 'Failed to get encryption key');
}
if (!opts.key) {
return exitWith(1, 'Failed to get encryption key');
}
}
};
/**
* hook: require a confirmation message to be accepted unless forceOption (-f,--force) is used
*/
const getCommanderConfirmMessage = (
message: string,
{ failMessage }: { failMessage?: string } = {}
) => {
return async (command: Command) => {
const confirmed = await confirmMessage(message, { force: command.opts().force });
if (!confirmed) {
exitWith(1, failMessage);
}
};
};
const confirmMessage = async (message: string, { force }: { force?: boolean } = {}) => {
// if we have a force option, respond yes
if (force === true) {
// attempt to mimic the inquirer prompt exactly
console.log(`${chalk.green('?')} ${chalk.bold(message)} ${chalk.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.`
);
export {
getParseListWithChoices,
parseList,
parseURL,
parseInteger,
promptEncryptionKey,
getCommanderConfirmMessage,
confirmMessage,
forceOption,
};

View File

@ -1,106 +0,0 @@
import chalk from 'chalk';
import { isString, isArray } from 'lodash/fp';
import type { Command } from 'commander';
const bytesPerKb = 1024;
const sizes = ['B ', 'KB', 'MB', 'GB', 'TB', 'PB'];
/**
* Convert bytes to a human readable formatted string, for example "1024" becomes "1KB"
*/
const readableBytes = (bytes: number, decimals = 1, padStart = 0) => {
if (!bytes) {
return '0';
}
const i = Math.floor(Math.log(bytes) / Math.log(bytesPerKb));
const result = `${parseFloat((bytes / bytesPerKb ** i).toFixed(decimals))} ${sizes[i].padStart(
2
)}`;
return result.padStart(padStart);
};
interface ExitWithOptions {
logger?: Console;
prc?: NodeJS.Process;
}
/**
*
* Display message(s) to console and then call process.exit with code.
* If code is zero, console.log and green text is used for messages, otherwise console.error and red text.
*
*/
const exitWith = (code: number, message?: string | string[], options: ExitWithOptions = {}) => {
const { logger = console, prc = process } = options;
const log = (message: string) => {
if (code === 0) {
logger.log(chalk.green(message));
} else {
logger.error(chalk.red(message));
}
};
if (isString(message)) {
log(message);
} else if (isArray(message)) {
message.forEach((msg) => log(msg));
}
prc.exit(code);
};
/**
* assert that a URL object has a protocol value
*
*/
const assertUrlHasProtocol = (url: URL, protocol?: string | string[]) => {
if (!url.protocol) {
exitWith(1, `${url.toString()} does not have a protocol`);
}
// if just checking for the existence of a protocol, return
if (!protocol) {
return;
}
if (isString(protocol)) {
if (protocol !== url.protocol) {
exitWith(1, `${url.toString()} must have the protocol ${protocol}`);
}
return;
}
// assume an array
if (!protocol.some((protocol) => url.protocol === protocol)) {
return exitWith(
1,
`${url.toString()} must have one of the following protocols: ${protocol.join(',')}`
);
}
};
type ConditionCallback = (opts: Record<string, any>) => Promise<boolean>;
type IsMetCallback = (command: Command) => Promise<void>;
type IsNotMetCallback = (command: Command) => Promise<void>;
/**
* Passes commander options to conditionCallback(). If it returns true, call isMetCallback otherwise call isNotMetCallback
*/
const ifOptions = (
conditionCallback: ConditionCallback,
isMetCallback: IsMetCallback = async () => {},
isNotMetCallback: IsNotMetCallback = async () => {}
) => {
return async (command: Command) => {
const opts = command.opts();
if (await conditionCallback(opts)) {
await isMetCallback(command);
} else {
await isNotMetCallback(command);
}
};
};
export { exitWith, assertUrlHasProtocol, ifOptions, readableBytes };

View File

@ -1,146 +0,0 @@
import importAction from '../action';
import { expectExit } from '../../__tests__/commands.test.utils';
import * as engineDatatransfer from '../../../engine';
import * as strapiDatatransfer from '../../../strapi';
import * as fileDatatransfer from '../../../file';
jest.mock('../../data-transfer', () => {
return {
...jest.requireActual('../../data-transfer'),
getTransferTelemetryPayload: jest.fn().mockReturnValue({}),
loadersFactory: jest.fn().mockReturnValue({ updateLoader: jest.fn() }),
formatDiagnostic: jest.fn(),
createStrapiInstance: jest.fn().mockReturnValue({
telemetry: {
send: jest.fn(),
},
destroy: jest.fn(),
}),
buildTransferTable: jest.fn(() => {
return {
toString() {
return 'table';
},
};
}),
exitMessageText: jest.fn(),
getDiffHandler: jest.fn(),
setSignalHandler: jest.fn(),
};
});
jest.mock('../../../engine', () => {
const actual = jest.requireActual('../../../engine');
return {
...actual,
createTransferEngine: jest.fn(() => {
return {
transfer: jest.fn(() => {
return {
engine: {},
};
}),
progress: {
on: jest.fn(),
stream: {
on: jest.fn(),
},
},
sourceProvider: { name: 'testFileSource', type: 'source', getMetadata: jest.fn() },
destinationProvider: {
name: 'testStrapiDest',
type: 'destination',
getMetadata: jest.fn(),
},
diagnostics: {
on: jest.fn().mockReturnThis(),
onDiagnostic: jest.fn().mockReturnThis(),
},
onSchemaDiff: jest.fn(),
};
}),
};
});
jest.mock('../../../file', () => {
const actual = jest.requireActual('../../../file');
return {
...actual,
providers: {
...actual.providers,
createLocalFileSourceProvider: jest
.fn()
.mockReturnValue({ name: 'testFileSource', type: 'source', getMetadata: jest.fn() }),
},
};
});
jest.mock('../../../strapi', () => {
const actual = jest.requireActual('../../../strapi');
return {
...actual,
providers: {
...actual.providers,
createLocalStrapiDestinationProvider: jest
.fn()
.mockReturnValue({ name: 'testStrapiDest', type: 'destination', getMetadata: jest.fn() }),
},
};
});
describe('Import', () => {
// mock command utils
// console spies
jest.spyOn(console, 'log').mockImplementation(() => {});
jest.spyOn(console, 'warn').mockImplementation(() => {});
jest.spyOn(console, 'info').mockImplementation(() => {});
jest.spyOn(console, 'error').mockImplementation(() => {});
beforeEach(() => {
jest.clearAllMocks();
});
it('creates providers with correct options ', async () => {
const options = {
file: 'test.tar.gz.enc',
decrypt: true,
decompress: true,
exclude: [],
only: [],
};
await expectExit(0, async () => {
await importAction(options);
});
// strapi options
expect(strapiDatatransfer.providers.createLocalStrapiDestinationProvider).toHaveBeenCalledWith(
expect.objectContaining({
strategy: strapiDatatransfer.providers.DEFAULT_CONFLICT_STRATEGY,
})
);
// file options
expect(fileDatatransfer.providers.createLocalFileSourceProvider).toHaveBeenCalledWith(
expect.objectContaining({
file: { path: 'test.tar.gz.enc' },
encryption: { enabled: options.decrypt },
compression: { enabled: options.decompress },
})
);
// engine options
expect(engineDatatransfer.createTransferEngine).toHaveBeenCalledWith(
expect.objectContaining({ name: 'testFileSource' }),
expect.objectContaining({ name: 'testStrapiDest' }),
expect.objectContaining({
schemaStrategy: engineDatatransfer.DEFAULT_SCHEMA_STRATEGY,
versionStrategy: engineDatatransfer.DEFAULT_VERSION_STRATEGY,
})
);
});
});

View File

@ -1,5 +0,0 @@
import exportCmd from './export/command';
import importCmd from './import/command';
import transferCmd from './transfer/command';
export const commands = [exportCmd, importCmd, transferCmd];

View File

@ -2,4 +2,3 @@ export * as engine from './engine';
export * as strapi from './strapi';
export * as file from './file';
export * as utils from './utils';
export { commands } from './commands';

View File

@ -1,5 +1,10 @@
{
"extends": "tsconfig/base.json",
"include": ["types", "src", "packup.config.ts"],
"include": [
"types",
"src",
"packup.config.ts",
"../strapi/src/cli/commands/__tests__/commands.test.utils.ts"
],
"exclude": ["node_modules"]
}

View File

@ -1,7 +1,8 @@
import { file as fileDataTransfer } from '@strapi/data-transfer';
import exportAction from '../action';
import * as mockUtils from '../../data-transfer';
import * as mockUtils from '../../../utils/data-transfer';
import { expectExit } from '../../__tests__/commands.test.utils';
import * as fileDatatransfer from '../../../file';
jest.mock('fs-extra', () => ({
...jest.requireActual('fs-extra'),
@ -10,9 +11,9 @@ jest.mock('fs-extra', () => ({
const defaultFileName = 'defaultFilename';
jest.mock('../../data-transfer', () => {
jest.mock('../../../utils/data-transfer', () => {
return {
...jest.requireActual('../../data-transfer'),
...jest.requireActual('../../../utils/data-transfer'),
getTransferTelemetryPayload: jest.fn().mockReturnValue({}),
loadersFactory: jest.fn().mockReturnValue({ updateLoader: jest.fn() }),
formatDiagnostic: jest.fn(),
@ -36,75 +37,68 @@ jest.mock('../../data-transfer', () => {
};
});
jest.mock('../../../engine', () => {
const actual = jest.requireActual('../../../engine');
jest.mock('@strapi/data-transfer', () => {
const actual = jest.requireActual('@strapi/data-transfer');
return {
...actual,
createTransferEngine: jest.fn(() => {
return {
transfer: jest.fn(() => {
return {
engine: {},
destination: {
file: {
path: 'path',
},
},
};
}),
progress: {
on: jest.fn(),
stream: {
on: jest.fn(),
},
},
sourceProvider: { name: 'testFileSource', type: 'source', getMetadata: jest.fn() },
destinationProvider: {
name: 'testStrapiDest',
file: {
...actual.file,
providers: {
...actual.file.providers,
createLocalFileSourceProvider: jest
.fn()
.mockReturnValue({ name: 'testFileSource', type: 'source', getMetadata: jest.fn() }),
createLocalFileDestinationProvider: jest.fn().mockReturnValue({
name: 'testFileDestination',
type: 'destination',
getMetadata: jest.fn(),
},
diagnostics: {
on: jest.fn().mockReturnThis(),
onDiagnostic: jest.fn().mockReturnThis(),
},
onSchemaDiff: jest.fn(),
addErrorHandler: jest.fn(),
};
}),
};
});
jest.mock('../../../file', () => {
const actual = jest.requireActual('../../../file');
return {
...actual,
providers: {
...actual.providers,
createLocalFileSourceProvider: jest
.fn()
.mockReturnValue({ name: 'testFileSource', type: 'source', getMetadata: jest.fn() }),
createLocalFileDestinationProvider: jest.fn().mockReturnValue({
name: 'testFileDestination',
type: 'destination',
getMetadata: jest.fn(),
}),
}),
},
},
};
});
jest.mock('../../../strapi', () => {
const actual = jest.requireActual('../../../strapi');
return {
...actual,
providers: {
...actual.providers,
createLocalStrapiDestinationProvider: jest
.fn()
.mockReturnValue({ name: 'testStrapiDest', type: 'destination', getMetadata: jest.fn() }),
strapi: {
...actual.strapi,
providers: {
...actual.strapi.providers,
createLocalStrapiDestinationProvider: jest
.fn()
.mockReturnValue({ name: 'testStrapiDest', type: 'destination', getMetadata: jest.fn() }),
},
},
engine: {
...actual.engine,
createTransferEngine: jest.fn(() => {
return {
transfer: jest.fn(() => {
return {
engine: {},
destination: {
file: {
path: 'path',
},
},
};
}),
progress: {
on: jest.fn(),
stream: {
on: jest.fn(),
},
},
sourceProvider: { name: 'testFileSource', type: 'source', getMetadata: jest.fn() },
destinationProvider: {
name: 'testStrapiDest',
type: 'destination',
getMetadata: jest.fn(),
},
diagnostics: {
on: jest.fn().mockReturnThis(),
onDiagnostic: jest.fn().mockReturnThis(),
},
onSchemaDiff: jest.fn(),
addErrorHandler: jest.fn(),
};
}),
},
};
});
@ -130,7 +124,7 @@ describe('Export', () => {
});
expect(console.error).not.toHaveBeenCalled();
expect(fileDatatransfer.providers.createLocalFileDestinationProvider).toHaveBeenCalledWith(
expect(fileDataTransfer.providers.createLocalFileDestinationProvider).toHaveBeenCalledWith(
expect.objectContaining({
file: { path: filename },
})
@ -144,7 +138,7 @@ describe('Export', () => {
});
expect(mockUtils.getDefaultExportName).toHaveBeenCalledTimes(1);
expect(fileDatatransfer.providers.createLocalFileDestinationProvider).toHaveBeenCalledWith(
expect(fileDataTransfer.providers.createLocalFileDestinationProvider).toHaveBeenCalledWith(
expect.objectContaining({
file: { path: defaultFileName },
})
@ -157,7 +151,7 @@ describe('Export', () => {
await exportAction({ encrypt });
});
expect(fileDatatransfer.providers.createLocalFileDestinationProvider).toHaveBeenCalledWith(
expect(fileDataTransfer.providers.createLocalFileDestinationProvider).toHaveBeenCalledWith(
expect.objectContaining({
encryption: { enabled: encrypt },
})
@ -171,7 +165,7 @@ describe('Export', () => {
await exportAction({ encrypt, key });
});
expect(fileDatatransfer.providers.createLocalFileDestinationProvider).toHaveBeenCalledWith(
expect(fileDataTransfer.providers.createLocalFileDestinationProvider).toHaveBeenCalledWith(
expect.objectContaining({
encryption: { enabled: encrypt, key },
})
@ -183,7 +177,7 @@ describe('Export', () => {
await exportAction({ compress: false });
});
expect(fileDatatransfer.providers.createLocalFileDestinationProvider).toHaveBeenCalledWith(
expect(fileDataTransfer.providers.createLocalFileDestinationProvider).toHaveBeenCalledWith(
expect.objectContaining({
compression: { enabled: false },
})
@ -191,7 +185,7 @@ describe('Export', () => {
await expectExit(0, async () => {
await exportAction({ compress: true });
});
expect(fileDatatransfer.providers.createLocalFileDestinationProvider).toHaveBeenCalledWith(
expect(fileDataTransfer.providers.createLocalFileDestinationProvider).toHaveBeenCalledWith(
expect.objectContaining({
compression: { enabled: true },
})

View File

@ -3,6 +3,12 @@ import fs from 'fs-extra';
import chalk from 'chalk';
import type { LoadedStrapi } from '@strapi/types';
import {
engine as engineDataTransfer,
strapi as strapiDataTransfer,
file as fileDataTransfer,
} from '@strapi/data-transfer';
import {
getDefaultExportName,
buildTransferTable,
@ -14,18 +20,15 @@ import {
abortTransfer,
getTransferTelemetryPayload,
setSignalHandler,
} from '../data-transfer';
import { exitWith } from '../helpers';
import { TransferGroupFilter, createTransferEngine, ITransferResults, errors } from '../../engine';
import * as strapiDatatransfer from '../../strapi';
import * as file from '../../file';
} from '../../utils/data-transfer';
import { exitWith } from '../../utils/helpers';
const {
providers: { createLocalFileDestinationProvider },
} = file;
} = fileDataTransfer;
const {
providers: { createLocalStrapiSourceProvider },
} = strapiDatatransfer;
} = strapiDataTransfer;
const BYTES_IN_MB = 1024 * 1024;
@ -34,8 +37,8 @@ interface CmdOptions {
encrypt?: boolean;
key?: string;
compress?: boolean;
only?: (keyof TransferGroupFilter)[];
exclude?: (keyof TransferGroupFilter)[];
only?: (keyof engineDataTransfer.TransferGroupFilter)[];
exclude?: (keyof engineDataTransfer.TransferGroupFilter)[];
throttle?: number;
maxSizeJsonl?: number;
}
@ -58,7 +61,7 @@ export default async (opts: CmdOptions) => {
const source = createSourceProvider(strapi);
const destination = createDestinationProvider(opts);
const engine = createTransferEngine(source, destination, {
const engine = engineDataTransfer.createTransferEngine(source, destination, {
versionStrategy: 'ignore', // for an export to file, versionStrategy will always be skipped
schemaStrategy: 'ignore', // for an export to file, schemaStrategy will always be skipped
exclude: opts.exclude,
@ -109,7 +112,7 @@ export default async (opts: CmdOptions) => {
await strapi.telemetry.send('didDEITSProcessStart', getTransferTelemetryPayload(engine));
});
let results: ITransferResults<typeof source, typeof destination>;
let results: engineDataTransfer.ITransferResults<typeof source, typeof destination>;
let outFile: string;
try {
// Abort transfer if user interrupts process
@ -119,7 +122,9 @@ export default async (opts: CmdOptions) => {
outFile = results.destination?.file?.path ?? '';
const outFileExists = await fs.pathExists(outFile);
if (!outFileExists) {
throw new errors.TransferEngineTransferError(`Export file not created "${outFile}"`);
throw new engineDataTransfer.errors.TransferEngineTransferError(
`Export file not created "${outFile}"`
);
}
// Note: we need to await telemetry or else the process ends before it is sent

View File

@ -1,6 +1,12 @@
import { createCommand, Option } from 'commander';
import { excludeOption, onlyOption, throttleOption, validateExcludeOnly } from '../data-transfer';
import { promptEncryptionKey } from '../commander';
import {
excludeOption,
onlyOption,
throttleOption,
validateExcludeOnly,
} from '../../utils/data-transfer';
import { promptEncryptionKey } from '../../utils/commander';
import action from './action';
/**

View File

@ -0,0 +1,142 @@
import {
engine as engineDataTransfer,
strapi as strapiDataTransfer,
file as fileDataTransfer,
} from '@strapi/data-transfer';
import importAction from '../action';
import { expectExit } from '../../__tests__/commands.test.utils';
jest.mock('../../../utils/data-transfer', () => {
return {
...jest.requireActual('../../../utils/data-transfer'),
getTransferTelemetryPayload: jest.fn().mockReturnValue({}),
loadersFactory: jest.fn().mockReturnValue({ updateLoader: jest.fn() }),
formatDiagnostic: jest.fn(),
createStrapiInstance: jest.fn().mockReturnValue({
telemetry: {
send: jest.fn(),
},
destroy: jest.fn(),
}),
buildTransferTable: jest.fn(() => {
return {
toString() {
return 'table';
},
};
}),
exitMessageText: jest.fn(),
getDiffHandler: jest.fn(),
setSignalHandler: jest.fn(),
};
});
jest.mock('@strapi/data-transfer', () => {
const actual = jest.requireActual('@strapi/data-transfer');
return {
...actual,
file: {
...actual.file,
providers: {
...actual.file.providers,
createLocalFileSourceProvider: jest
.fn()
.mockReturnValue({ name: 'testFileSource', type: 'source', getMetadata: jest.fn() }),
},
},
strapi: {
...actual.strapi,
providers: {
...actual.strapi.providers,
createLocalStrapiDestinationProvider: jest
.fn()
.mockReturnValue({ name: 'testStrapiDest', type: 'destination', getMetadata: jest.fn() }),
},
},
engine: {
...actual.engine,
createTransferEngine: jest.fn(() => {
return {
transfer: jest.fn(() => {
return {
engine: {},
};
}),
progress: {
on: jest.fn(),
stream: {
on: jest.fn(),
},
},
sourceProvider: { name: 'testFileSource', type: 'source', getMetadata: jest.fn() },
destinationProvider: {
name: 'testStrapiDest',
type: 'destination',
getMetadata: jest.fn(),
},
diagnostics: {
on: jest.fn().mockReturnThis(),
onDiagnostic: jest.fn().mockReturnThis(),
},
onSchemaDiff: jest.fn(),
};
}),
},
};
});
describe('Import', () => {
// mock command utils
// console spies
jest.spyOn(console, 'log').mockImplementation(() => {});
jest.spyOn(console, 'warn').mockImplementation(() => {});
jest.spyOn(console, 'info').mockImplementation(() => {});
jest.spyOn(console, 'error').mockImplementation(() => {});
beforeEach(() => {
jest.clearAllMocks();
});
it('creates providers with correct options ', async () => {
const options = {
file: 'test.tar.gz.enc',
decrypt: true,
decompress: true,
exclude: [],
only: [],
};
await expectExit(0, async () => {
await importAction(options);
});
// strapi options
expect(strapiDataTransfer.providers.createLocalStrapiDestinationProvider).toHaveBeenCalledWith(
expect.objectContaining({
strategy: strapiDataTransfer.providers.DEFAULT_CONFLICT_STRATEGY,
})
);
// file options
expect(fileDataTransfer.providers.createLocalFileSourceProvider).toHaveBeenCalledWith(
expect.objectContaining({
file: { path: 'test.tar.gz.enc' },
encryption: { enabled: options.decrypt },
compression: { enabled: options.decompress },
})
);
// engine options
expect(engineDataTransfer.createTransferEngine).toHaveBeenCalledWith(
expect.objectContaining({ name: 'testFileSource' }),
expect.objectContaining({ name: 'testStrapiDest' }),
expect.objectContaining({
schemaStrategy: engineDataTransfer.DEFAULT_SCHEMA_STRATEGY,
versionStrategy: engineDataTransfer.DEFAULT_VERSION_STRATEGY,
})
);
});
});

View File

@ -1,7 +1,13 @@
import type { LoadedStrapi } from '@strapi/types';
import { isObject } from 'lodash/fp';
import chalk from 'chalk';
import {
engine as engineDataTransfer,
strapi as strapiDataTransfer,
file as fileDataTransfer,
} from '@strapi/data-transfer';
import {
buildTransferTable,
DEFAULT_IGNORED_CONTENT_TYPES,
@ -14,21 +20,19 @@ import {
setSignalHandler,
getDiffHandler,
parseRestoreFromOptions,
} from '../data-transfer';
import { exitWith } from '../helpers';
import * as engine from '../../engine';
import * as strapiDatatransfer from '../../strapi';
import * as file from '../../file';
} from '../../utils/data-transfer';
import { exitWith } from '../../utils/helpers';
const {
providers: { createLocalFileSourceProvider },
} = file;
} = fileDataTransfer;
const {
providers: { createLocalStrapiDestinationProvider, DEFAULT_CONFLICT_STRATEGY },
} = strapiDatatransfer;
} = strapiDataTransfer;
const { createTransferEngine, DEFAULT_VERSION_STRATEGY, DEFAULT_SCHEMA_STRATEGY } = engine;
const { createTransferEngine, DEFAULT_VERSION_STRATEGY, DEFAULT_SCHEMA_STRATEGY } =
engineDataTransfer;
interface CmdOptions {
file?: string;
@ -37,8 +41,8 @@ interface CmdOptions {
key?: string;
conflictStrategy?: 'restore';
force?: boolean;
only?: (keyof engine.TransferGroupFilter)[];
exclude?: (keyof engine.TransferGroupFilter)[];
only?: (keyof engineDataTransfer.TransferGroupFilter)[];
exclude?: (keyof engineDataTransfer.TransferGroupFilter)[];
throttle?: number;
}
@ -137,7 +141,7 @@ export default async (opts: CmdOptions) => {
);
});
let results: engine.ITransferResults<typeof source, typeof destination>;
let results: engineDataTransfer.ITransferResults<typeof source, typeof destination>;
try {
// Abort transfer if user interrupts process
setSignalHandler(() => abortTransfer({ engine, strapi: strapi as LoadedStrapi }));
@ -174,7 +178,7 @@ const getLocalFileSourceOptions = (opts: {
decrypt?: boolean;
key?: string;
}) => {
const options: file.providers.ILocalFileSourceProviderOptions = {
const options: fileDataTransfer.providers.ILocalFileSourceProviderOptions = {
file: { path: opts.file ?? '' },
compression: { enabled: !!opts.decompress },
encryption: { enabled: !!opts.decrypt, key: opts.key },

View File

@ -1,9 +1,14 @@
import path from 'path';
import { createCommand, Option } from 'commander';
import inquirer from 'inquirer';
import { excludeOption, onlyOption, throttleOption, validateExcludeOnly } from '../data-transfer';
import { getCommanderConfirmMessage, forceOption } from '../commander';
import { exitWith } from '../helpers';
import {
excludeOption,
onlyOption,
throttleOption,
validateExcludeOnly,
} from '../../utils/data-transfer';
import { getCommanderConfirmMessage, forceOption } from '../../utils/commander';
import { exitWith } from '../../utils/helpers';
import action from './action';
/**

View File

@ -21,6 +21,9 @@ import { command as generateCommand } from './generate';
import { command as reportCommand } from './report';
import { command as startCommand } from './start';
import { command as versionCommand } from './version';
import exportCommand from './export/command';
import importCommand from './import/command';
import transferCommand from './transfer/command';
import { command as buildPluginCommand } from './plugin/build';
import { command as initPluginCommand } from './plugin/init';
@ -53,6 +56,9 @@ export const commands: StrapiCommand[] = [
versionCommand,
buildCommand,
developCommand,
exportCommand,
importCommand,
transferCommand,
/**
* Plugins
*/

View File

@ -1,10 +1,11 @@
import * as mockDataTransfer from '@strapi/data-transfer';
import transferAction from '../action';
import { expectExit } from '../../__tests__/commands.test.utils';
import * as mockDataTransfer from '../../..';
jest.mock('../../data-transfer', () => {
jest.mock('../../../utils/data-transfer', () => {
return {
...jest.requireActual('../../data-transfer'),
...jest.requireActual('../../../utils/data-transfer'),
getTransferTelemetryPayload: jest.fn().mockReturnValue({}),
loadersFactory: jest.fn().mockReturnValue({ updateLoader: jest.fn() }),
formatDiagnostic: jest.fn(),
@ -31,44 +32,46 @@ jest.mock('../../data-transfer', () => {
});
// mock data transfer
jest.mock('../../../engine', () => {
const acutal = jest.requireActual('../../../engine');
jest.mock('@strapi/data-transfer', () => {
const acutal = jest.requireActual('@strapi/data-transfer');
return {
...acutal,
createTransferEngine() {
return {
transfer: jest.fn(() => {
return {
engine: {},
};
}),
progress: {
on: jest.fn(),
stream: {
on: jest.fn(),
},
},
sourceProvider: { name: 'testSource' },
destinationProvider: { name: 'testDestination' },
diagnostics: {
on: jest.fn().mockReturnThis(),
onDiagnostic: jest.fn().mockReturnThis(),
},
onSchemaDiff: jest.fn(),
addErrorHandler: jest.fn(),
};
strapi: {
...acutal.strapi,
providers: {
...acutal.strapi.providers,
createLocalStrapiSourceProvider: jest.fn().mockReturnValue({ name: 'testLocalSource' }),
createLocalStrapiDestinationProvider: jest.fn().mockReturnValue({ name: 'testLocalDest' }),
createRemoteStrapiDestinationProvider: jest
.fn()
.mockReturnValue({ name: 'testRemoteDest' }),
},
},
};
});
jest.mock('../../../strapi', () => {
const actual = jest.requireActual('../../../strapi');
return {
...actual,
providers: {
createLocalStrapiSourceProvider: jest.fn().mockReturnValue({ name: 'testLocalSource' }),
createLocalStrapiDestinationProvider: jest.fn().mockReturnValue({ name: 'testLocalDest' }),
createRemoteStrapiDestinationProvider: jest.fn().mockReturnValue({ name: 'testRemoteDest' }),
engine: {
...acutal.engine,
createTransferEngine() {
return {
transfer: jest.fn(() => {
return {
engine: {},
};
}),
progress: {
on: jest.fn(),
stream: {
on: jest.fn(),
},
},
sourceProvider: { name: 'testSource' },
destinationProvider: { name: 'testDestination' },
diagnostics: {
on: jest.fn().mockReturnThis(),
onDiagnostic: jest.fn().mockReturnThis(),
},
onSchemaDiff: jest.fn(),
addErrorHandler: jest.fn(),
};
},
},
};
});

View File

@ -1,6 +1,5 @@
import { isObject } from 'lodash/fp';
import * as engineDatatransfer from '../../engine';
import * as strapiDatatransfer from '../../strapi';
import { engine as engineDataTransfer, strapi as strapiDataTransfer } from '@strapi/data-transfer';
import {
buildTransferTable,
@ -15,10 +14,10 @@ import {
getDiffHandler,
getAssetsBackupHandler,
parseRestoreFromOptions,
} from '../data-transfer';
import { exitWith } from '../helpers';
} from '../../utils/data-transfer';
import { exitWith } from '../../utils/helpers';
const { createTransferEngine } = engineDatatransfer;
const { createTransferEngine } = engineDataTransfer;
const {
providers: {
createRemoteStrapiDestinationProvider,
@ -26,15 +25,15 @@ const {
createLocalStrapiDestinationProvider,
createRemoteStrapiSourceProvider,
},
} = strapiDatatransfer;
} = strapiDataTransfer;
interface CmdOptions {
from?: URL;
fromToken: string;
to: URL;
toToken: string;
only?: (keyof engineDatatransfer.TransferGroupFilter)[];
exclude?: (keyof engineDatatransfer.TransferGroupFilter)[];
only?: (keyof engineDataTransfer.TransferGroupFilter)[];
exclude?: (keyof engineDataTransfer.TransferGroupFilter)[];
throttle?: number;
force?: boolean;
}

View File

@ -1,8 +1,14 @@
import inquirer from 'inquirer';
import { createCommand, Option } from 'commander';
import { getCommanderConfirmMessage, forceOption, parseURL } from '../commander';
import { exitWith, assertUrlHasProtocol, ifOptions } from '../helpers';
import { excludeOption, onlyOption, throttleOption, validateExcludeOnly } from '../data-transfer';
import { getCommanderConfirmMessage, forceOption, parseURL } from '../../utils/commander';
import { exitWith, assertUrlHasProtocol, ifOptions } from '../../utils/helpers';
import {
excludeOption,
onlyOption,
throttleOption,
validateExcludeOnly,
} from '../../utils/data-transfer';
import action from './action';
/**

View File

@ -7,15 +7,6 @@ import { loadTsConfig } from './utils/tsconfig';
import { CLIContext } from './types';
const createCLI = async (argv: string[], command = new Command()) => {
try {
// NOTE: this is a hack to allow loading dts commands without make dts a dependency of strapi and thus avoiding circular dependencies
// eslint-disable-next-line @typescript-eslint/no-var-requires
const dtsCommands = require(require.resolve('@strapi/data-transfer')).commands;
strapiCommands.push(...dtsCommands);
} catch (e) {
// noop
}
// Initial program setup
command.storeOptionsAsProperties(false).allowUnknownOption(true);

View File

@ -6,15 +6,14 @@ import { strapiFactory } from '@strapi/core';
import ora from 'ora';
import { merge } from 'lodash/fp';
import type { LoadedStrapi, Strapi } from '@strapi/types';
import { engine as engineDataTransfer, strapi as strapiDataTransfer } from '@strapi/data-transfer';
import { readableBytes, exitWith } from './helpers';
import { getParseListWithChoices, parseInteger, confirmMessage } from './commander';
import * as engineDatatransfer from '../engine';
import * as strapiDataTransfer from '../strapi';
const {
errors: { TransferEngineInitializationError },
} = engineDatatransfer;
} = engineDataTransfer;
const exitMessageText = (process: string, error = false) => {
const processCapitalized = process[0].toUpperCase() + process.slice(1);
@ -49,9 +48,9 @@ const getDefaultExportName = () => {
return `export_${yyyymmddHHMMSS()}`;
};
type ResultData = engineDatatransfer.ITransferResults<
engineDatatransfer.ISourceProvider,
engineDatatransfer.IDestinationProvider
type ResultData = engineDataTransfer.ITransferResults<
engineDataTransfer.ISourceProvider,
engineDataTransfer.IDestinationProvider
>['engine'];
const buildTransferTable = (resultData: ResultData) => {
@ -66,7 +65,7 @@ const buildTransferTable = (resultData: ResultData) => {
let totalBytes = 0;
let totalItems = 0;
(Object.keys(resultData) as engineDatatransfer.TransferStage[]).forEach((stage) => {
(Object.keys(resultData) as engineDataTransfer.TransferStage[]).forEach((stage) => {
const item = resultData[stage];
if (!item) {
@ -123,7 +122,7 @@ const abortTransfer = async ({
engine,
strapi,
}: {
engine: engineDatatransfer.TransferEngine;
engine: engineDataTransfer.TransferEngine;
strapi: LoadedStrapi;
}) => {
try {
@ -166,7 +165,7 @@ const createStrapiInstance = async (
}
};
const transferDataTypes = Object.keys(engineDatatransfer.TransferGroupPresets);
const transferDataTypes = Object.keys(engineDataTransfer.TransferGroupPresets);
const throttleOption = new Option(
'--throttle <delay after each entity>',
@ -213,7 +212,7 @@ const errorColors = {
const formatDiagnostic =
(
operation: string
): Parameters<engineDatatransfer.TransferEngine['diagnostics']['onDiagnostic']>[0] =>
): Parameters<engineDataTransfer.TransferEngine['diagnostics']['onDiagnostic']>[0] =>
({ details, kind }) => {
const logger = createLogger(
configs.createOutputFileConfiguration(`${operation}_error_log_${Date.now()}.log`)
@ -245,11 +244,11 @@ const formatDiagnostic =
};
type Loaders = {
[key in engineDatatransfer.TransferStage]: ora.Ora;
[key in engineDataTransfer.TransferStage]: ora.Ora;
};
type Data = {
[key in engineDatatransfer.TransferStage]?: {
[key in engineDataTransfer.TransferStage]?: {
startTime?: number;
endTime?: number;
bytes?: number;
@ -259,7 +258,7 @@ type Data = {
const loadersFactory = (defaultLoaders: Loaders = {} as Loaders) => {
const loaders = defaultLoaders;
const updateLoader = (stage: engineDatatransfer.TransferStage, data: Data) => {
const updateLoader = (stage: engineDataTransfer.TransferStage, data: Data) => {
if (!(stage in loaders)) {
createLoader(stage);
}
@ -280,12 +279,12 @@ const loadersFactory = (defaultLoaders: Loaders = {} as Loaders) => {
return loaders[stage];
};
const createLoader = (stage: engineDatatransfer.TransferStage) => {
const createLoader = (stage: engineDataTransfer.TransferStage) => {
Object.assign(loaders, { [stage]: ora() });
return loaders[stage];
};
const getLoader = (stage: engineDatatransfer.TransferStage) => {
const getLoader = (stage: engineDataTransfer.TransferStage) => {
return loaders[stage];
};
@ -299,7 +298,7 @@ const loadersFactory = (defaultLoaders: Loaders = {} as Loaders) => {
/**
* Get the telemetry data to be sent for a didDEITSProcess* event from an initialized transfer engine object
*/
const getTransferTelemetryPayload = (engine: engineDatatransfer.TransferEngine) => {
const getTransferTelemetryPayload = (engine: engineDataTransfer.TransferEngine) => {
return {
eventProperties: {
source: engine?.sourceProvider?.name,
@ -312,7 +311,7 @@ const getTransferTelemetryPayload = (engine: engineDatatransfer.TransferEngine)
* Get a transfer engine schema diff handler that confirms with the user before bypassing a schema check
*/
const getDiffHandler = (
engine: engineDatatransfer.TransferEngine,
engine: engineDataTransfer.TransferEngine,
{
force,
action,
@ -322,8 +321,8 @@ const getDiffHandler = (
}
) => {
return async (
context: engineDatatransfer.SchemaDiffHandlerContext,
next: (ctx: engineDatatransfer.SchemaDiffHandlerContext) => void
context: engineDataTransfer.SchemaDiffHandlerContext,
next: (ctx: engineDataTransfer.SchemaDiffHandlerContext) => void
) => {
// if we abort here, we need to actually exit the process because of conflict with inquirer prompt
setSignalHandler(async () => {
@ -395,7 +394,7 @@ const getDiffHandler = (
};
const getAssetsBackupHandler = (
engine: engineDatatransfer.TransferEngine,
engine: engineDataTransfer.TransferEngine,
{
force,
action,
@ -405,8 +404,8 @@ const getAssetsBackupHandler = (
}
) => {
return async (
context: engineDatatransfer.ErrorHandlerContext,
next: (ctx: engineDatatransfer.ErrorHandlerContext) => void
context: engineDataTransfer.ErrorHandlerContext,
next: (ctx: engineDataTransfer.ErrorHandlerContext) => void
) => {
// if we abort here, we need to actually exit the process because of conflict with inquirer prompt
setSignalHandler(async () => {
@ -435,8 +434,8 @@ const getAssetsBackupHandler = (
};
const shouldSkipStage = (
opts: Partial<engineDatatransfer.ITransferEngineOptions>,
dataKind: engineDatatransfer.TransferFilterPreset
opts: Partial<engineDataTransfer.ITransferEngineOptions>,
dataKind: engineDataTransfer.TransferFilterPreset
) => {
if (opts.exclude?.includes(dataKind)) {
return true;
@ -453,7 +452,7 @@ type RestoreConfig = NonNullable<
>;
// Based on exclude/only from options, create the restore object to match
const parseRestoreFromOptions = (opts: Partial<engineDatatransfer.ITransferEngineOptions>) => {
const parseRestoreFromOptions = (opts: Partial<engineDataTransfer.ITransferEngineOptions>) => {
const entitiesOptions: RestoreConfig['entities'] = {
exclude: DEFAULT_IGNORED_CONTENT_TYPES,
include: undefined,

View File

@ -9439,7 +9439,6 @@ __metadata:
version: 0.0.0-use.local
resolution: "@strapi/data-transfer@workspace:packages/core/data-transfer"
dependencies:
"@strapi/core": "npm:4.17.1"
"@strapi/logger": "npm:4.17.1"
"@strapi/pack-up": "npm:4.17.1"
"@strapi/types": "npm:4.17.1"