mirror of
				https://github.com/strapi/strapi.git
				synced 2025-11-03 19:36:20 +00:00 
			
		
		
		
	Merge branch 'features/deits' into deits/transfer-push
This commit is contained in:
		
						commit
						c40cfa9a18
					
				@ -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",
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
@ -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}".`);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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": {
 | 
			
		||||
 | 
			
		||||
@ -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();
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
@ -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);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 | 
			
		||||
@ -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);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user