Merge branch 'features/deits' into deits/transfer-push

This commit is contained in:
Christian Capeans 2022-12-29 11:14:08 +01:00
commit c40cfa9a18
7 changed files with 147 additions and 104 deletions

View File

@ -31,7 +31,7 @@
"prepare": "husky install",
"setup": "yarn && yarn clean && yarn build",
"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",
"generate": "plop --plopfile ./packages/generators/admin/plopfile.js",
"lint": "npm-run-all -p lint:code lint:css",

View File

@ -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);
}
@ -352,9 +352,8 @@ class TransferEngine<
// reset data between transfers
this.progress.data = {};
this.#emitTransferUpdate('start');
try {
this.#emitTransferUpdate('init');
await this.bootstrap();
await this.init();
@ -367,6 +366,8 @@ class TransferEngine<
);
}
this.#emitTransferUpdate('start');
await this.beforeTransfer();
// Run the transfer stages

View File

@ -49,6 +49,8 @@ class LocalFileSourceProvider implements ISourceProvider {
options: ILocalFileSourceProviderOptions;
#metadata?: IMetadata;
constructor(options: ILocalFileSourceProviderOptions) {
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() {
const { path: filePath } = this.options.file;
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
await fs.access(filePath, fs.constants.R_OK);
// Read the metadata to ensure the file can be parsed
this.#metadata = await this.getMetadata();
} catch (e) {
throw new Error(`Can't access file "${filePath}".`);
throw new Error(`Can't read file "${filePath}".`);
}
}

View File

@ -30,7 +30,7 @@
"build": "tsc -p tsconfig.json",
"clean": "rimraf ./dist",
"build:clean": "yarn clean && yarn build",
"watch": "yarn build -w",
"watch": "yarn build -w --preserveWatchOutput",
"test:unit": "jest --verbose"
},
"directories": {

View File

@ -1,98 +1,146 @@
'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', () => {
beforeEach(() => {
jest.resetAllMocks();
});
const defaultFileName = 'defaultFilename';
// 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 () => {
const filename = 'testfile';
const filename = 'test';
await exportCommand({ file: filename });
await expectExit(1, async () => {
await exportCommand({ file: filename });
});
expect(mockDataTransfer.createLocalFileDestinationProvider).toHaveBeenCalledWith(
expect.objectContaining({
file: { path: filename },
})
);
expect(utils.getDefaultExportName).not.toHaveBeenCalled();
expect(exit).toHaveBeenCalled();
expect(mockUtils.getDefaultExportName).not.toHaveBeenCalled();
});
it('uses default path if not provided by user', async () => {
utils.getDefaultExportName.mockReturnValue(defaultFileName);
await exportCommand({});
await expectExit(1, async () => {
await exportCommand({});
});
expect(mockUtils.getDefaultExportName).toHaveBeenCalledTimes(1);
expect(mockDataTransfer.createLocalFileDestinationProvider).toHaveBeenCalledWith(
expect.objectContaining({
file: { path: defaultFileName },
})
);
expect(utils.getDefaultExportName).toHaveBeenCalled();
expect(exit).toHaveBeenCalled();
});
it('encrypts the output file if specified', async () => {
const encrypt = true;
await exportCommand({ encrypt });
await expectExit(1, async () => {
await exportCommand({ encrypt });
});
expect(mockDataTransfer.createLocalFileDestinationProvider).toHaveBeenCalledWith(
expect.objectContaining({
encryption: { enabled: encrypt },
})
);
expect(exit).toHaveBeenCalled();
});
it('encrypts the output file with the given key', async () => {
const key = 'secret-key';
const encrypt = true;
await expectExit(1, async () => {
await exportCommand({ encrypt, key });
});
await exportCommand({ encrypt, key });
expect(mockDataTransfer.createLocalFileDestinationProvider).toHaveBeenCalledWith(
expect.objectContaining({
encryption: { enabled: encrypt, key },
})
);
expect(exit).toHaveBeenCalled();
});
it('compresses the output file if specified', async () => {
const compress = true;
await exportCommand({ compress });
it('uses compress option', async () => {
await expectExit(1, async () => {
await exportCommand({ compress: false });
});
expect(mockDataTransfer.createLocalFileDestinationProvider).toHaveBeenCalledWith(
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();
});
});

View File

@ -74,32 +74,23 @@ module.exports = async (opts) => {
},
});
try {
logger.log(`Starting export...`);
const progress = engine.progress.stream;
const progress = engine.progress.stream;
const telemetryPayload = (/* payload */) => {
return {
eventProperties: {
source: engine.sourceProvider.name,
destination: engine.destinationProvider.name,
},
};
const getTelemetryPayload = (/* payload */) => {
return {
eventProperties: {
source: engine.sourceProvider.name,
destination: engine.destinationProvider.name,
},
};
};
progress.on('transfer::start', (payload) => {
strapi.telemetry.send('didDEITSProcessStart', telemetryPayload(payload));
});
progress.on('transfer::finish', (payload) => {
strapi.telemetry.send('didDEITSProcessFinish', telemetryPayload(payload));
});
progress.on('transfer::error', (payload) => {
strapi.telemetry.send('didDEITSProcessFail', telemetryPayload(payload));
});
progress.on('transfer::start', async () => {
logger.log(`Starting export...`);
await strapi.telemetry.send('didDEITSProcessStart', getTelemetryPayload());
});
try {
const results = await engine.transfer();
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(`Export archive is in ${chalk.green(outFile)}`);
process.exit(0);
} catch (e) {
await strapi.telemetry.send('didDEITSProcessFail', getTelemetryPayload());
logger.error('Export process failed unexpectedly:', e.toString());
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);
};
/**

View File

@ -77,44 +77,40 @@ module.exports = async (opts) => {
],
},
};
const engine = createTransferEngine(source, destination, engineOptions);
try {
logger.info('Starting import...');
const progress = engine.progress.stream;
const telemetryPayload = (/* payload */) => {
return {
eventProperties: {
source: engine.sourceProvider.name,
destination: engine.destinationProvider.name,
},
};
const progress = engine.progress.stream;
const getTelemetryPayload = () => {
return {
eventProperties: {
source: engine.sourceProvider.name,
destination: engine.destinationProvider.name,
},
};
};
progress.on('transfer::start', (payload) => {
strapiInstance.telemetry.send('didDEITSProcessStart', telemetryPayload(payload));
});
progress.on('transfer::finish', (payload) => {
strapiInstance.telemetry.send('didDEITSProcessFinish', telemetryPayload(payload));
});
progress.on('transfer::error', (payload) => {
strapiInstance.telemetry.send('didDEITSProcessFail', telemetryPayload(payload));
});
progress.on('transfer::start', async () => {
logger.info('Starting import...');
await strapiInstance.telemetry.send('didDEITSProcessStart', getTelemetryPayload());
});
try {
const results = await engine.transfer();
const table = buildTransferTable(results.engine);
logger.info(table.toString());
logger.info('Import process has been completed successfully!');
process.exit(0);
} catch (e) {
await strapiInstance.telemetry.send('didDEITSProcessFail', getTelemetryPayload());
logger.error('Import process failed unexpectedly:');
logger.error(e);
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);
};
/**