mirror of
https://github.com/strapi/strapi.git
synced 2025-09-27 17:29:14 +00:00
refactor
This commit is contained in:
parent
544fb057c2
commit
11b8d3c9f3
@ -2,6 +2,7 @@ import { getStrapiFactory } from '../../__tests__/test-utils';
|
|||||||
|
|
||||||
import { createTransferHandler } from '../remote/handlers';
|
import { createTransferHandler } from '../remote/handlers';
|
||||||
import register from '../register';
|
import register from '../register';
|
||||||
|
import { TRANSFER_PATH } from '../../../lib/strapi/remote/constants';
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
@ -20,13 +21,13 @@ jest.mock('../remote/handlers', () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
describe('Register the Transfer route', () => {
|
describe('Register the Transfer route', () => {
|
||||||
test('registers the /transfer route', () => {
|
test('registers the transfer route', () => {
|
||||||
const strapi = strapiMockFactory();
|
const strapi = strapiMockFactory();
|
||||||
|
|
||||||
register(strapi);
|
register(strapi);
|
||||||
expect(strapi.admin.routes.push).toHaveBeenCalledWith({
|
expect(strapi.admin.routes.push).toHaveBeenCalledWith({
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
path: '/transfer',
|
path: TRANSFER_PATH,
|
||||||
handler: createTransferHandler(),
|
handler: createTransferHandler(),
|
||||||
config: {
|
config: {
|
||||||
auth: false,
|
auth: false,
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { WebSocket } from 'ws';
|
import { WebSocket } from 'ws';
|
||||||
|
import { TRANSFER_PATH } from '../../../../../lib/strapi/remote/constants';
|
||||||
import { CommandMessage } from '../../../../../types/remote/protocol/client';
|
import { CommandMessage } from '../../../../../types/remote/protocol/client';
|
||||||
import { createDispatcher } from '../utils';
|
import { createDispatcher } from '../utils';
|
||||||
|
|
||||||
@ -18,7 +19,7 @@ afterEach(() => {
|
|||||||
|
|
||||||
describe('Remote Strapi Destination Utils', () => {
|
describe('Remote Strapi Destination Utils', () => {
|
||||||
test('Dispatch method sends payload', () => {
|
test('Dispatch method sends payload', () => {
|
||||||
const ws = new WebSocket('ws://test/admin/transfer');
|
const ws = new WebSocket(`ws://test/admin${TRANSFER_PATH}`);
|
||||||
const message: CommandMessage = {
|
const message: CommandMessage = {
|
||||||
type: 'command',
|
type: 'command',
|
||||||
command: 'status',
|
command: 'status',
|
||||||
|
@ -15,6 +15,7 @@ import type {
|
|||||||
} from '../../../../types';
|
} from '../../../../types';
|
||||||
import type { client, server } from '../../../../types/remote/protocol';
|
import type { client, server } from '../../../../types/remote/protocol';
|
||||||
import type { ILocalStrapiDestinationProviderOptions } from '../local-destination';
|
import type { ILocalStrapiDestinationProviderOptions } from '../local-destination';
|
||||||
|
import { TRANSFER_PATH } from '../../remote/constants';
|
||||||
|
|
||||||
interface ITokenAuth {
|
interface ITokenAuth {
|
||||||
type: 'token';
|
type: 'token';
|
||||||
@ -29,7 +30,7 @@ interface ICredentialsAuth {
|
|||||||
|
|
||||||
export interface IRemoteStrapiDestinationProviderOptions
|
export interface IRemoteStrapiDestinationProviderOptions
|
||||||
extends Pick<ILocalStrapiDestinationProviderOptions, 'restore' | 'strategy'> {
|
extends Pick<ILocalStrapiDestinationProviderOptions, 'restore' | 'strategy'> {
|
||||||
url: string;
|
url: URL;
|
||||||
auth?: ITokenAuth | ICredentialsAuth;
|
auth?: ITokenAuth | ICredentialsAuth;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -100,16 +101,19 @@ class RemoteStrapiDestinationProvider implements IDestinationProvider {
|
|||||||
|
|
||||||
let ws: WebSocket;
|
let ws: WebSocket;
|
||||||
|
|
||||||
|
const wsUrl = `${url.protocol === 'https:' ? 'wss:' : 'ws:'}//${url.host}${
|
||||||
|
url.pathname
|
||||||
|
}${TRANSFER_PATH}`;
|
||||||
|
|
||||||
// No auth defined, trying public access for transfer
|
// No auth defined, trying public access for transfer
|
||||||
if (!auth) {
|
if (!auth) {
|
||||||
ws = new WebSocket(url);
|
ws = new WebSocket(wsUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Common token auth, this should be the main auth method
|
// Common token auth, this should be the main auth method
|
||||||
else if (auth.type === 'token') {
|
else if (auth.type === 'token') {
|
||||||
const headers = { Authentication: `Bearer ${auth.token}` };
|
const headers = { Authorization: `Bearer ${auth.token}` };
|
||||||
|
ws = new WebSocket(wsUrl, { headers });
|
||||||
ws = new WebSocket(this.options.url, { headers });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Invalid auth method provided
|
// Invalid auth method provided
|
||||||
|
@ -1 +1 @@
|
|||||||
export const TRANSFER_URL = '/transfer';
|
export const TRANSFER_PATH = '/transfer';
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
// eslint-disable-next-line node/no-extraneous-import
|
// eslint-disable-next-line node/no-extraneous-import
|
||||||
import type { Context } from 'koa';
|
import type { Context } from 'koa';
|
||||||
|
|
||||||
import { TRANSFER_URL } from './constants';
|
import { TRANSFER_PATH } from './constants';
|
||||||
import { createTransferHandler } from './handlers';
|
import { createTransferHandler } from './handlers';
|
||||||
|
|
||||||
// Extend Strapi interface type to access the admin routes' API
|
// Extend Strapi interface type to access the admin routes' API
|
||||||
@ -29,7 +29,7 @@ declare module '@strapi/strapi' {
|
|||||||
export const registerAdminTransferRoute = (strapi: Strapi.Strapi) => {
|
export const registerAdminTransferRoute = (strapi: Strapi.Strapi) => {
|
||||||
strapi.admin.routes.push({
|
strapi.admin.routes.push({
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
path: TRANSFER_URL,
|
path: TRANSFER_PATH,
|
||||||
handler: createTransferHandler(),
|
handler: createTransferHandler(),
|
||||||
config: { auth: false },
|
config: { auth: false },
|
||||||
});
|
});
|
||||||
|
@ -14,7 +14,12 @@ const inquirer = require('inquirer');
|
|||||||
const program = new Command();
|
const program = new Command();
|
||||||
|
|
||||||
const packageJSON = require('../package.json');
|
const packageJSON = require('../package.json');
|
||||||
const { promptEncryptionKey, confirmMessage } = require('../lib/commands/utils/commander');
|
const {
|
||||||
|
promptEncryptionKey,
|
||||||
|
confirmMessage,
|
||||||
|
parseURL,
|
||||||
|
} = require('../lib/commands/utils/commander');
|
||||||
|
const { ifOptions, assertUrlHasProtocol, exitWith } = require('../lib/commands/utils/helpers');
|
||||||
|
|
||||||
const checkCwdIsStrapiApp = (name) => {
|
const checkCwdIsStrapiApp = (name) => {
|
||||||
const logErrorAndExit = () => {
|
const logErrorAndExit = () => {
|
||||||
@ -263,16 +268,39 @@ if (process.env.STRAPI_EXPERIMENTAL) {
|
|||||||
program
|
program
|
||||||
.command('transfer')
|
.command('transfer')
|
||||||
.description('Transfer data from one source to another')
|
.description('Transfer data from one source to another')
|
||||||
.addOption(new Option('--from <sourceURL>', `URL of remote Strapi instance to get data from.`))
|
.addOption(
|
||||||
.addOption(new Option('--to <destinationURL>', `URL of remote Strapi instance to send data to`))
|
new Option('--from <sourceURL>', `URL of remote Strapi instance to get data from.`).argParser(
|
||||||
.hook('preAction', async (thisCommand) => {
|
parseURL
|
||||||
const opts = thisCommand.opts();
|
)
|
||||||
|
)
|
||||||
if (!opts.from && !opts.to) {
|
.addOption(
|
||||||
console.error('At least one source (from) or destination (to) option must be provided');
|
new Option(
|
||||||
process.exit(1);
|
'--to <destinationURL>',
|
||||||
}
|
`URL of remote Strapi instance to send data to`
|
||||||
})
|
).argParser(parseURL)
|
||||||
|
)
|
||||||
|
// Validate URLs
|
||||||
|
.hook(
|
||||||
|
'preAction',
|
||||||
|
ifOptions(
|
||||||
|
(opts) => opts.from,
|
||||||
|
(thisCommand) => assertUrlHasProtocol(thisCommand.opts().from, ['https:', 'http:'])
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.hook(
|
||||||
|
'preAction',
|
||||||
|
ifOptions(
|
||||||
|
(opts) => opts.to,
|
||||||
|
(thisCommand) => assertUrlHasProtocol(thisCommand.opts().to, ['https:', 'http:'])
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.hook(
|
||||||
|
'preAction',
|
||||||
|
ifOptions(
|
||||||
|
(opts) => !opts.from && !opts.to,
|
||||||
|
() => exitWith(1, 'At least one source (from) or destination (to) option must be provided')
|
||||||
|
)
|
||||||
|
)
|
||||||
.allowExcessArguments(false)
|
.allowExcessArguments(false)
|
||||||
.action(getLocalScript('transfer/transfer'));
|
.action(getLocalScript('transfer/transfer'));
|
||||||
}
|
}
|
||||||
|
@ -38,7 +38,7 @@ jest.spyOn(console, 'log').mockImplementation(() => {});
|
|||||||
|
|
||||||
jest.mock('../../transfer/utils');
|
jest.mock('../../transfer/utils');
|
||||||
|
|
||||||
const destinationUrl = 'ws://strapi.com';
|
const destinationUrl = new URL('http://strapi.com/admin');
|
||||||
|
|
||||||
describe('Transfer', () => {
|
describe('Transfer', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
@ -22,8 +22,8 @@ const logger = console;
|
|||||||
/**
|
/**
|
||||||
* @typedef TransferCommandOptions Options given to the CLI transfer command
|
* @typedef TransferCommandOptions Options given to the CLI transfer command
|
||||||
*
|
*
|
||||||
* @property {string|undefined} [to] The url of a remote Strapi to use as remote destination
|
* @property {URL|undefined} [to] The url of a remote Strapi to use as remote destination
|
||||||
* @property {string|undefined} [from] The url of a remote Strapi to use as remote source
|
* @property {URL|undefined} [from] The url of a remote Strapi to use as remote source
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -86,8 +86,8 @@ module.exports = async (opts) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const engine = createTransferEngine(source, destination, {
|
const engine = createTransferEngine(source, destination, {
|
||||||
versionStrategy: 'ignore', // for an export to file, versionStrategy will always be skipped
|
versionStrategy: 'exact', // for an export to file, versionStrategy will always be skipped
|
||||||
schemaStrategy: 'ignore', // for an export to file, schemaStrategy will always be skipped
|
schemaStrategy: 'exact', // for an export to file, schemaStrategy will always be skipped
|
||||||
transforms: {
|
transforms: {
|
||||||
links: [
|
links: [
|
||||||
{
|
{
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
const chalk = require('chalk');
|
const chalk = require('chalk');
|
||||||
const Table = require('cli-table3');
|
const Table = require('cli-table3');
|
||||||
const { readableBytes } = require('../utils');
|
const { readableBytes } = require('../utils/helpers');
|
||||||
const strapi = require('../../index');
|
const strapi = require('../../index');
|
||||||
|
|
||||||
const pad = (n) => {
|
const pad = (n) => {
|
||||||
|
@ -1,14 +1,36 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This file includes hooks to use for commander.hook and argParsers for commander.argParser
|
||||||
|
*/
|
||||||
|
|
||||||
const inquirer = require('inquirer');
|
const inquirer = require('inquirer');
|
||||||
|
const { InvalidOptionArgumentError } = require('commander');
|
||||||
|
const { exitWith } = require('./helpers');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* argsParser: Parse a comma-delimited string as an array
|
* argParser: Parse a comma-delimited string as an array
|
||||||
*/
|
*/
|
||||||
const parseInputList = (value) => {
|
const parseInputList = (value) => {
|
||||||
return value.split(',');
|
return value.split(',');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* argParser: Parse a string as a URL object
|
||||||
|
*/
|
||||||
|
const parseURL = (value) => {
|
||||||
|
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
|
* hook: if encrypt==true and key not provided, prompt for it
|
||||||
*/
|
*/
|
||||||
@ -16,8 +38,7 @@ const promptEncryptionKey = async (thisCommand) => {
|
|||||||
const opts = thisCommand.opts();
|
const opts = thisCommand.opts();
|
||||||
|
|
||||||
if (!opts.encrypt && opts.key) {
|
if (!opts.encrypt && opts.key) {
|
||||||
console.error('Key may not be present unless encryption is used');
|
return exitWith(1, 'Key may not be present unless encryption is used');
|
||||||
process.exit(1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// if encrypt==true but we have no key, prompt for it
|
// if encrypt==true but we have no key, prompt for it
|
||||||
@ -37,12 +58,10 @@ const promptEncryptionKey = async (thisCommand) => {
|
|||||||
]);
|
]);
|
||||||
opts.key = answers.key;
|
opts.key = answers.key;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to get encryption key');
|
return exitWith(1, 'Failed to get encryption key');
|
||||||
process.exit(1);
|
|
||||||
}
|
}
|
||||||
if (!opts.key) {
|
if (!opts.key) {
|
||||||
console.error('Failed to get encryption key');
|
return exitWith(1, 'Failed to get encryption key');
|
||||||
process.exit(1);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -61,13 +80,15 @@ const confirmMessage = (message) => {
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
if (!answers.confirm) {
|
if (!answers.confirm) {
|
||||||
process.exit(0);
|
exitWith(0);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
parseInputList,
|
parseInputList,
|
||||||
|
parseURL,
|
||||||
promptEncryptionKey,
|
promptEncryptionKey,
|
||||||
confirmMessage,
|
confirmMessage,
|
||||||
|
exitWith,
|
||||||
};
|
};
|
||||||
|
108
packages/core/strapi/lib/commands/utils/helpers.js
Normal file
108
packages/core/strapi/lib/commands/utils/helpers.js
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper functions for the Strapi CLI
|
||||||
|
*/
|
||||||
|
|
||||||
|
const chalk = require('chalk');
|
||||||
|
const { isString, isArray } = require('lodash/fp');
|
||||||
|
|
||||||
|
const bytesPerKb = 1024;
|
||||||
|
const sizes = ['B ', 'KB', 'MB', 'GB', 'TB', 'PB'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert bytes to a human readable formatted string, for example "1024" becomes "1KB"
|
||||||
|
*
|
||||||
|
* @param {number} bytes The bytes to be converted
|
||||||
|
* @param {number} decimals How many decimals to include in the final number
|
||||||
|
* @param {number} padStart Pad the string with space at the beginning so that it has at least this many characters
|
||||||
|
*/
|
||||||
|
const readableBytes = (bytes, 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);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
* @param {number} code Code to exit process with
|
||||||
|
* @param {string | Array} message Message(s) to display before exiting
|
||||||
|
*/
|
||||||
|
const exitWith = (code, message = undefined) => {
|
||||||
|
const logger = (message) => {
|
||||||
|
if (code === 0) {
|
||||||
|
console.log(chalk.green(message));
|
||||||
|
} else {
|
||||||
|
console.log(chalk.red(message));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isString(message)) {
|
||||||
|
logger(message);
|
||||||
|
} else if (isArray(message)) {
|
||||||
|
message.forEach((msg) => logger(msg));
|
||||||
|
}
|
||||||
|
process.exit(code);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* assert that a URL object has a protocol value
|
||||||
|
*
|
||||||
|
* @param {URL} url
|
||||||
|
* @param {string[]|string|undefined} [protocol]
|
||||||
|
*/
|
||||||
|
const assertUrlHasProtocol = (url, protocol = undefined) => {
|
||||||
|
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(',')}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Passes commander options to conditionCallback(). If it returns true, call isMetCallback otherwise call isNotMetCallback
|
||||||
|
*/
|
||||||
|
const ifOptions = (conditionCallback, isMetCallback = () => {}, isNotMetCallback = () => {}) => {
|
||||||
|
return async (command) => {
|
||||||
|
const opts = command.opts();
|
||||||
|
if (await conditionCallback(opts)) {
|
||||||
|
await isMetCallback(command);
|
||||||
|
} else {
|
||||||
|
await isNotMetCallback(command);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
exitWith,
|
||||||
|
assertUrlHasProtocol,
|
||||||
|
ifOptions,
|
||||||
|
readableBytes,
|
||||||
|
};
|
@ -1,20 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
const bytesPerKb = 1024;
|
|
||||||
const sizes = ['B ', 'KB', 'MB', 'GB', 'TB', 'PB'];
|
|
||||||
|
|
||||||
const readableBytes = (bytes, 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);
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
readableBytes,
|
|
||||||
};
|
|
Loading…
x
Reference in New Issue
Block a user