mirror of
https://github.com/strapi/strapi.git
synced 2025-11-12 00:03:40 +00:00
Merge branch 'features/deits' into deits/transfer-push
This commit is contained in:
commit
c40cfa9a18
@ -31,7 +31,7 @@
|
|||||||
"prepare": "husky install",
|
"prepare": "husky install",
|
||||||
"setup": "yarn && yarn clean && yarn build",
|
"setup": "yarn && yarn clean && yarn build",
|
||||||
"clean": "lerna run --stream clean --no-private",
|
"clean": "lerna run --stream clean --no-private",
|
||||||
"watch": "lerna run --stream watch --no-private",
|
"watch": "lerna run --stream watch --no-private --parallel",
|
||||||
"build": "lerna run --stream build --no-private",
|
"build": "lerna run --stream build --no-private",
|
||||||
"generate": "plop --plopfile ./packages/generators/admin/plopfile.js",
|
"generate": "plop --plopfile ./packages/generators/admin/plopfile.js",
|
||||||
"lint": "npm-run-all -p lint:code lint:css",
|
"lint": "npm-run-all -p lint:code lint:css",
|
||||||
|
|||||||
@ -159,7 +159,7 @@ class TransferEngine<
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#emitTransferUpdate(type: 'start' | 'finish' | 'error', payload?: object) {
|
#emitTransferUpdate(type: 'init' | 'start' | 'finish' | 'error', payload?: object) {
|
||||||
this.progress.stream.emit(`transfer::${type}`, payload);
|
this.progress.stream.emit(`transfer::${type}`, payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -352,9 +352,8 @@ class TransferEngine<
|
|||||||
// reset data between transfers
|
// reset data between transfers
|
||||||
this.progress.data = {};
|
this.progress.data = {};
|
||||||
|
|
||||||
this.#emitTransferUpdate('start');
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
this.#emitTransferUpdate('init');
|
||||||
await this.bootstrap();
|
await this.bootstrap();
|
||||||
await this.init();
|
await this.init();
|
||||||
|
|
||||||
@ -367,6 +366,8 @@ class TransferEngine<
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.#emitTransferUpdate('start');
|
||||||
|
|
||||||
await this.beforeTransfer();
|
await this.beforeTransfer();
|
||||||
|
|
||||||
// Run the transfer stages
|
// Run the transfer stages
|
||||||
|
|||||||
@ -49,6 +49,8 @@ class LocalFileSourceProvider implements ISourceProvider {
|
|||||||
|
|
||||||
options: ILocalFileSourceProviderOptions;
|
options: ILocalFileSourceProviderOptions;
|
||||||
|
|
||||||
|
#metadata?: IMetadata;
|
||||||
|
|
||||||
constructor(options: ILocalFileSourceProviderOptions) {
|
constructor(options: ILocalFileSourceProviderOptions) {
|
||||||
this.options = options;
|
this.options = options;
|
||||||
|
|
||||||
@ -60,15 +62,16 @@ class LocalFileSourceProvider implements ISourceProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pre flight checks regarding the provided options (making sure that the provided path is correct, etc...)
|
* Pre flight checks regarding the provided options, making sure that the file can be opened (decrypted, decompressed), etc.
|
||||||
*/
|
*/
|
||||||
async bootstrap() {
|
async bootstrap() {
|
||||||
const { path: filePath } = this.options.file;
|
const { path: filePath } = this.options.file;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// This is only to show a nicer error, it doesn't ensure the file will still exist when we try to open it later
|
// Read the metadata to ensure the file can be parsed
|
||||||
await fs.access(filePath, fs.constants.R_OK);
|
this.#metadata = await this.getMetadata();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(`Can't access file "${filePath}".`);
|
throw new Error(`Can't read file "${filePath}".`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -30,7 +30,7 @@
|
|||||||
"build": "tsc -p tsconfig.json",
|
"build": "tsc -p tsconfig.json",
|
||||||
"clean": "rimraf ./dist",
|
"clean": "rimraf ./dist",
|
||||||
"build:clean": "yarn clean && yarn build",
|
"build:clean": "yarn clean && yarn build",
|
||||||
"watch": "yarn build -w",
|
"watch": "yarn build -w --preserveWatchOutput",
|
||||||
"test:unit": "jest --verbose"
|
"test:unit": "jest --verbose"
|
||||||
},
|
},
|
||||||
"directories": {
|
"directories": {
|
||||||
|
|||||||
@ -1,98 +1,146 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const utils = require('../transfer/utils');
|
|
||||||
|
|
||||||
const mockDataTransfer = {
|
|
||||||
createLocalFileDestinationProvider: jest.fn(),
|
|
||||||
createLocalStrapiSourceProvider: jest.fn(),
|
|
||||||
createTransferEngine: jest.fn().mockReturnValue({
|
|
||||||
transfer: jest.fn().mockReturnValue(Promise.resolve({})),
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
jest.mock(
|
|
||||||
'@strapi/data-transfer',
|
|
||||||
() => {
|
|
||||||
return mockDataTransfer;
|
|
||||||
},
|
|
||||||
{ virtual: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
const exportCommand = require('../transfer/export');
|
|
||||||
|
|
||||||
const exit = jest.spyOn(process, 'exit').mockImplementation(() => {});
|
|
||||||
jest.spyOn(console, 'error').mockImplementation(() => {});
|
|
||||||
|
|
||||||
jest.mock('../transfer/utils');
|
|
||||||
|
|
||||||
const defaultFileName = 'defaultFilename';
|
|
||||||
|
|
||||||
describe('export', () => {
|
describe('export', () => {
|
||||||
beforeEach(() => {
|
const defaultFileName = 'defaultFilename';
|
||||||
jest.resetAllMocks();
|
|
||||||
});
|
// mock @strapi/data-transfer
|
||||||
|
const mockDataTransfer = {
|
||||||
|
createLocalFileDestinationProvider: jest.fn().mockReturnValue({ name: 'testDest' }),
|
||||||
|
createLocalStrapiSourceProvider: jest.fn().mockReturnValue({ name: 'testSource' }),
|
||||||
|
createTransferEngine() {
|
||||||
|
return {
|
||||||
|
transfer: jest.fn().mockReturnValue(Promise.resolve({})),
|
||||||
|
progress: {
|
||||||
|
on: jest.fn(),
|
||||||
|
stream: {
|
||||||
|
on: jest.fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sourceProvider: { name: 'testSource' },
|
||||||
|
destinationProvider: { name: 'testDestination' },
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
jest.mock(
|
||||||
|
'@strapi/data-transfer',
|
||||||
|
() => {
|
||||||
|
return mockDataTransfer;
|
||||||
|
},
|
||||||
|
{ virtual: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
// mock utils
|
||||||
|
const mockUtils = {
|
||||||
|
createStrapiInstance() {
|
||||||
|
return {
|
||||||
|
telemetry: {
|
||||||
|
send: jest.fn(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
getDefaultExportName: jest.fn(() => defaultFileName),
|
||||||
|
};
|
||||||
|
jest.mock(
|
||||||
|
'../transfer/utils',
|
||||||
|
() => {
|
||||||
|
return mockUtils;
|
||||||
|
},
|
||||||
|
{ virtual: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
// other spies=
|
||||||
|
jest.spyOn(console, 'log').mockImplementation(() => {});
|
||||||
|
jest.spyOn(console, 'warn').mockImplementation(() => {});
|
||||||
|
jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||||
|
|
||||||
|
// Now that everything is mocked, import export command
|
||||||
|
const exportCommand = require('../transfer/export');
|
||||||
|
|
||||||
|
const expectExit = async (code, fn) => {
|
||||||
|
const exit = jest.spyOn(process, 'exit').mockImplementation((number) => {
|
||||||
|
throw new Error(`process.exit: ${number}`);
|
||||||
|
});
|
||||||
|
await expect(async () => {
|
||||||
|
await fn();
|
||||||
|
}).rejects.toThrow();
|
||||||
|
expect(exit).toHaveBeenCalledWith(code);
|
||||||
|
exit.mockRestore();
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {});
|
||||||
|
|
||||||
it('uses path provided by user', async () => {
|
it('uses path provided by user', async () => {
|
||||||
const filename = 'testfile';
|
const filename = 'test';
|
||||||
|
|
||||||
await exportCommand({ file: filename });
|
await expectExit(1, async () => {
|
||||||
|
await exportCommand({ file: filename });
|
||||||
|
});
|
||||||
|
|
||||||
expect(mockDataTransfer.createLocalFileDestinationProvider).toHaveBeenCalledWith(
|
expect(mockDataTransfer.createLocalFileDestinationProvider).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
file: { path: filename },
|
file: { path: filename },
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
expect(utils.getDefaultExportName).not.toHaveBeenCalled();
|
expect(mockUtils.getDefaultExportName).not.toHaveBeenCalled();
|
||||||
expect(exit).toHaveBeenCalled();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('uses default path if not provided by user', async () => {
|
it('uses default path if not provided by user', async () => {
|
||||||
utils.getDefaultExportName.mockReturnValue(defaultFileName);
|
await expectExit(1, async () => {
|
||||||
|
await exportCommand({});
|
||||||
await exportCommand({});
|
});
|
||||||
|
|
||||||
|
expect(mockUtils.getDefaultExportName).toHaveBeenCalledTimes(1);
|
||||||
expect(mockDataTransfer.createLocalFileDestinationProvider).toHaveBeenCalledWith(
|
expect(mockDataTransfer.createLocalFileDestinationProvider).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
file: { path: defaultFileName },
|
file: { path: defaultFileName },
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(utils.getDefaultExportName).toHaveBeenCalled();
|
|
||||||
expect(exit).toHaveBeenCalled();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('encrypts the output file if specified', async () => {
|
it('encrypts the output file if specified', async () => {
|
||||||
const encrypt = true;
|
const encrypt = true;
|
||||||
await exportCommand({ encrypt });
|
await expectExit(1, async () => {
|
||||||
|
await exportCommand({ encrypt });
|
||||||
|
});
|
||||||
|
|
||||||
expect(mockDataTransfer.createLocalFileDestinationProvider).toHaveBeenCalledWith(
|
expect(mockDataTransfer.createLocalFileDestinationProvider).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
encryption: { enabled: encrypt },
|
encryption: { enabled: encrypt },
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
expect(exit).toHaveBeenCalled();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('encrypts the output file with the given key', async () => {
|
it('encrypts the output file with the given key', async () => {
|
||||||
const key = 'secret-key';
|
const key = 'secret-key';
|
||||||
const encrypt = true;
|
const encrypt = true;
|
||||||
|
await expectExit(1, async () => {
|
||||||
|
await exportCommand({ encrypt, key });
|
||||||
|
});
|
||||||
|
|
||||||
await exportCommand({ encrypt, key });
|
|
||||||
expect(mockDataTransfer.createLocalFileDestinationProvider).toHaveBeenCalledWith(
|
expect(mockDataTransfer.createLocalFileDestinationProvider).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
encryption: { enabled: encrypt, key },
|
encryption: { enabled: encrypt, key },
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
expect(exit).toHaveBeenCalled();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('compresses the output file if specified', async () => {
|
it('uses compress option', async () => {
|
||||||
const compress = true;
|
await expectExit(1, async () => {
|
||||||
await exportCommand({ compress });
|
await exportCommand({ compress: false });
|
||||||
|
});
|
||||||
|
|
||||||
expect(mockDataTransfer.createLocalFileDestinationProvider).toHaveBeenCalledWith(
|
expect(mockDataTransfer.createLocalFileDestinationProvider).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
compression: { enabled: compress },
|
compression: { enabled: false },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
await expectExit(1, async () => {
|
||||||
|
await exportCommand({ compress: true });
|
||||||
|
});
|
||||||
|
expect(mockDataTransfer.createLocalFileDestinationProvider).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
compression: { enabled: true },
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
expect(exit).toHaveBeenCalled();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -74,32 +74,23 @@ module.exports = async (opts) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
const progress = engine.progress.stream;
|
||||||
logger.log(`Starting export...`);
|
|
||||||
|
|
||||||
const progress = engine.progress.stream;
|
const getTelemetryPayload = (/* payload */) => {
|
||||||
|
return {
|
||||||
const telemetryPayload = (/* payload */) => {
|
eventProperties: {
|
||||||
return {
|
source: engine.sourceProvider.name,
|
||||||
eventProperties: {
|
destination: engine.destinationProvider.name,
|
||||||
source: engine.sourceProvider.name,
|
},
|
||||||
destination: engine.destinationProvider.name,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
};
|
||||||
|
|
||||||
progress.on('transfer::start', (payload) => {
|
progress.on('transfer::start', async () => {
|
||||||
strapi.telemetry.send('didDEITSProcessStart', telemetryPayload(payload));
|
logger.log(`Starting export...`);
|
||||||
});
|
await strapi.telemetry.send('didDEITSProcessStart', getTelemetryPayload());
|
||||||
|
});
|
||||||
progress.on('transfer::finish', (payload) => {
|
|
||||||
strapi.telemetry.send('didDEITSProcessFinish', telemetryPayload(payload));
|
|
||||||
});
|
|
||||||
|
|
||||||
progress.on('transfer::error', (payload) => {
|
|
||||||
strapi.telemetry.send('didDEITSProcessFail', telemetryPayload(payload));
|
|
||||||
});
|
|
||||||
|
|
||||||
|
try {
|
||||||
const results = await engine.transfer();
|
const results = await engine.transfer();
|
||||||
const outFile = results.destination.file.path;
|
const outFile = results.destination.file.path;
|
||||||
|
|
||||||
@ -113,11 +104,15 @@ module.exports = async (opts) => {
|
|||||||
|
|
||||||
logger.log(`${chalk.bold('Export process has been completed successfully!')}`);
|
logger.log(`${chalk.bold('Export process has been completed successfully!')}`);
|
||||||
logger.log(`Export archive is in ${chalk.green(outFile)}`);
|
logger.log(`Export archive is in ${chalk.green(outFile)}`);
|
||||||
process.exit(0);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
await strapi.telemetry.send('didDEITSProcessFail', getTelemetryPayload());
|
||||||
logger.error('Export process failed unexpectedly:', e.toString());
|
logger.error('Export process failed unexpectedly:', e.toString());
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Note: Telemetry can't be sent in a finish event, because it runs async after this block but we can't await it, so if process.exit is used it won't send
|
||||||
|
await strapi.telemetry.send('didDEITSProcessFinish', getTelemetryPayload());
|
||||||
|
process.exit(0);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -77,44 +77,40 @@ module.exports = async (opts) => {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const engine = createTransferEngine(source, destination, engineOptions);
|
const engine = createTransferEngine(source, destination, engineOptions);
|
||||||
|
|
||||||
try {
|
const progress = engine.progress.stream;
|
||||||
logger.info('Starting import...');
|
const getTelemetryPayload = () => {
|
||||||
|
return {
|
||||||
const progress = engine.progress.stream;
|
eventProperties: {
|
||||||
const telemetryPayload = (/* payload */) => {
|
source: engine.sourceProvider.name,
|
||||||
return {
|
destination: engine.destinationProvider.name,
|
||||||
eventProperties: {
|
},
|
||||||
source: engine.sourceProvider.name,
|
|
||||||
destination: engine.destinationProvider.name,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
};
|
||||||
|
|
||||||
progress.on('transfer::start', (payload) => {
|
progress.on('transfer::start', async () => {
|
||||||
strapiInstance.telemetry.send('didDEITSProcessStart', telemetryPayload(payload));
|
logger.info('Starting import...');
|
||||||
});
|
await strapiInstance.telemetry.send('didDEITSProcessStart', getTelemetryPayload());
|
||||||
|
});
|
||||||
progress.on('transfer::finish', (payload) => {
|
|
||||||
strapiInstance.telemetry.send('didDEITSProcessFinish', telemetryPayload(payload));
|
|
||||||
});
|
|
||||||
|
|
||||||
progress.on('transfer::error', (payload) => {
|
|
||||||
strapiInstance.telemetry.send('didDEITSProcessFail', telemetryPayload(payload));
|
|
||||||
});
|
|
||||||
|
|
||||||
|
try {
|
||||||
const results = await engine.transfer();
|
const results = await engine.transfer();
|
||||||
const table = buildTransferTable(results.engine);
|
const table = buildTransferTable(results.engine);
|
||||||
logger.info(table.toString());
|
logger.info(table.toString());
|
||||||
|
|
||||||
logger.info('Import process has been completed successfully!');
|
logger.info('Import process has been completed successfully!');
|
||||||
process.exit(0);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
await strapiInstance.telemetry.send('didDEITSProcessFail', getTelemetryPayload());
|
||||||
logger.error('Import process failed unexpectedly:');
|
logger.error('Import process failed unexpectedly:');
|
||||||
logger.error(e);
|
logger.error(e);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Note: Telemetry can't be sent in a finish event, because it runs async after this block but we can't await it, so if process.exit is used it won't send
|
||||||
|
await strapi.telemetry.send('didDEITSProcessFinish', getTelemetryPayload());
|
||||||
|
process.exit(0);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user