mirror of
https://github.com/strapi/strapi.git
synced 2025-11-02 02:44:55 +00:00
Merge branch 'deits/architecture-rework' into deits/transfer-protocol
This commit is contained in:
commit
60d8434fad
@ -1,7 +1,6 @@
|
||||
'use strict';
|
||||
|
||||
// eslint-disable-next-line import/no-unresolved, node/no-missing-require
|
||||
const { register: registerDataTransfer } = require('@strapi/data-transfer');
|
||||
const { register: registerDataTransfer } = require('@strapi/data-transfer/lib/strapi');
|
||||
|
||||
const registerAdminPanelRoute = require('./routes/serve-admin-panel');
|
||||
const adminAuthStrategy = require('./strategies/admin');
|
||||
|
||||
1
packages/core/data-transfer/.eslintignore
Normal file
1
packages/core/data-transfer/.eslintignore
Normal file
@ -0,0 +1 @@
|
||||
lib
|
||||
1
packages/core/data-transfer/.gitignore
vendored
Normal file
1
packages/core/data-transfer/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
lib/
|
||||
@ -1,9 +1,9 @@
|
||||
#############################
|
||||
# DATA TRANSFER X
|
||||
# DATA TRANSFER
|
||||
############################
|
||||
|
||||
*.js.map
|
||||
lib/
|
||||
src/
|
||||
types/
|
||||
tsconfig.json
|
||||
|
||||
@ -19,14 +19,12 @@ Icon
|
||||
.Trashes
|
||||
._*
|
||||
|
||||
|
||||
############################
|
||||
# Linux
|
||||
############################
|
||||
|
||||
*~
|
||||
|
||||
|
||||
############################
|
||||
# Windows
|
||||
############################
|
||||
@ -40,7 +38,6 @@ $RECYCLE.BIN/
|
||||
*.msm
|
||||
*.msp
|
||||
|
||||
|
||||
############################
|
||||
# Packages
|
||||
############################
|
||||
@ -69,7 +66,6 @@ $RECYCLE.BIN/
|
||||
*.out
|
||||
*.pid
|
||||
|
||||
|
||||
############################
|
||||
# Logs and databases
|
||||
############################
|
||||
@ -79,7 +75,6 @@ $RECYCLE.BIN/
|
||||
*.sql
|
||||
*.sqlite
|
||||
|
||||
|
||||
############################
|
||||
# Misc.
|
||||
############################
|
||||
@ -89,7 +84,6 @@ $RECYCLE.BIN/
|
||||
nbproject
|
||||
.vscode/
|
||||
|
||||
|
||||
############################
|
||||
# Node.js
|
||||
############################
|
||||
@ -107,7 +101,6 @@ package-lock.json
|
||||
!docs/package-lock.json
|
||||
*.heapsnapshot
|
||||
|
||||
|
||||
############################
|
||||
# Tests
|
||||
############################
|
||||
|
||||
@ -1,2 +0,0 @@
|
||||
export * from './push';
|
||||
export { default as createTransferController } from './transfer';
|
||||
@ -1,4 +0,0 @@
|
||||
export * from './engine';
|
||||
export * from './providers';
|
||||
|
||||
export { default as register } from './register';
|
||||
@ -1,32 +0,0 @@
|
||||
import { createTransferController } from '../../bootstrap/controllers';
|
||||
import register from '../../register';
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const strapiMock = {
|
||||
admin: {
|
||||
routes: {
|
||||
push: jest.fn(),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
jest.mock('../../bootstrap/controllers', () => ({
|
||||
createTransferController: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('Register the Transfer route', () => {
|
||||
test('registers the /transfer route', () => {
|
||||
register(strapiMock);
|
||||
expect(strapiMock.admin.routes.push).toHaveBeenCalledWith({
|
||||
method: 'GET',
|
||||
path: '/transfer',
|
||||
handler: createTransferController(),
|
||||
config: {
|
||||
auth: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -1,9 +0,0 @@
|
||||
// source providers
|
||||
export * from './local-file-source-provider';
|
||||
export * from './local-strapi-source-provider';
|
||||
|
||||
// destination providers
|
||||
export * from './local-file-destination-provider';
|
||||
export * from './local-strapi-destination-provider';
|
||||
|
||||
export * from './remote-strapi-destination-provider';
|
||||
@ -1 +0,0 @@
|
||||
export * as strapi from './strapi';
|
||||
@ -1,83 +0,0 @@
|
||||
import { Readable } from 'stream';
|
||||
|
||||
/**
|
||||
* Collect every entity in a Readable stream
|
||||
*/
|
||||
export const collect = <T = unknown>(stream: Readable): Promise<T[]> => {
|
||||
const chunks: T[] = [];
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
stream
|
||||
.on('data', (chunk) => chunks.push(chunk))
|
||||
.on('close', () => resolve(chunks))
|
||||
.on('error', reject);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a "Strapi" like object factory based on the
|
||||
* given params and cast it to the correct type
|
||||
*/
|
||||
export const getStrapiFactory =
|
||||
<
|
||||
T extends {
|
||||
[key in keyof Partial<Strapi.Strapi>]: unknown;
|
||||
}
|
||||
>(
|
||||
properties?: T
|
||||
) =>
|
||||
() => {
|
||||
return { ...properties } as Strapi.Strapi;
|
||||
};
|
||||
|
||||
/**
|
||||
* Union type used to represent the default content types available
|
||||
*/
|
||||
export type ContentType = 'foo' | 'bar';
|
||||
|
||||
/**
|
||||
* Factory to get default content types test values
|
||||
*/
|
||||
export const getContentTypes = (): {
|
||||
[key in ContentType]: { uid: key; attributes: { [attribute: string]: unknown } };
|
||||
} => ({
|
||||
foo: { uid: 'foo', attributes: { title: { type: 'string' } } },
|
||||
bar: { uid: 'bar', attributes: { age: { type: 'number' } } },
|
||||
});
|
||||
|
||||
/**
|
||||
* Create a factory of readable streams (wrapped with a jest mock function)
|
||||
*/
|
||||
export const createMockedReadableFactory = <T extends string = ContentType>(source: {
|
||||
[ct in T]: Array<{ id: number; [key: string]: unknown }>;
|
||||
}) =>
|
||||
jest.fn((uid: T) => {
|
||||
return Readable.from(source[uid] || []);
|
||||
});
|
||||
|
||||
/**
|
||||
* Create a factory of mocked query builders
|
||||
*/
|
||||
export const createMockedQueryBuilder = <T extends string = ContentType>(data: {
|
||||
[key in T]: unknown[];
|
||||
}) =>
|
||||
jest.fn((uid: T) => {
|
||||
const state: { [key: string]: unknown } = { populate: undefined };
|
||||
|
||||
return {
|
||||
populate(populate: unknown) {
|
||||
state.populate = populate;
|
||||
return this;
|
||||
},
|
||||
stream() {
|
||||
return Readable.from(data[uid]);
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Update the global store with the given strapi value
|
||||
*/
|
||||
export const setGlobalStrapi = (strapi: Strapi.Strapi): void => {
|
||||
(global as unknown as Global).strapi = strapi;
|
||||
};
|
||||
@ -1,16 +0,0 @@
|
||||
import { createTransferController } from './bootstrap/controllers';
|
||||
|
||||
const registerTransferRoute = (strapi: any) => {
|
||||
strapi.admin.routes.push({
|
||||
method: 'GET',
|
||||
path: '/transfer',
|
||||
handler: createTransferController(),
|
||||
config: { auth: false },
|
||||
});
|
||||
};
|
||||
|
||||
const register = (strapi: any) => {
|
||||
registerTransferRoute(strapi);
|
||||
};
|
||||
|
||||
export default register;
|
||||
@ -1,20 +0,0 @@
|
||||
import type { Schema } from '@strapi/strapi';
|
||||
import { mapValues, pick } from 'lodash/fp';
|
||||
|
||||
const schemaSelectedKeys = [
|
||||
'collectionName',
|
||||
'info',
|
||||
'options',
|
||||
'pluginOptions',
|
||||
'attributes',
|
||||
'kind',
|
||||
'modelType',
|
||||
'modelName',
|
||||
'uid',
|
||||
'plugin',
|
||||
'globalId',
|
||||
];
|
||||
|
||||
export const mapSchemasValues = (schemas: Record<string, Schema>) => {
|
||||
return mapValues(pick(schemaSelectedKeys), schemas);
|
||||
};
|
||||
@ -1,48 +0,0 @@
|
||||
import { Transform, Readable } from 'stream';
|
||||
|
||||
type TransformOptions = ConstructorParameters<typeof Transform>[0];
|
||||
|
||||
export const filter = <T>(
|
||||
predicate: (value: T) => boolean | Promise<boolean>,
|
||||
options: TransformOptions = { objectMode: true }
|
||||
): Transform => {
|
||||
return new Transform({
|
||||
...options,
|
||||
|
||||
async transform(chunk, _encoding, callback) {
|
||||
const keep = await predicate(chunk);
|
||||
|
||||
callback(null, keep ? chunk : undefined);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const map = <T>(
|
||||
predicate: (value: T) => T | Promise<T>,
|
||||
options: TransformOptions = { objectMode: true }
|
||||
): Transform => {
|
||||
return new Transform({
|
||||
...options,
|
||||
|
||||
async transform(chunk, _encoding, callback) {
|
||||
const mappedValue = await predicate(chunk);
|
||||
|
||||
callback(null, mappedValue);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Collect every entity in a Readable stream
|
||||
*/
|
||||
export const collect = <T = unknown>(stream: Readable): Promise<T[]> => {
|
||||
const chunks: T[] = [];
|
||||
|
||||
return new Promise((resolve) => {
|
||||
stream.on('data', (chunk) => chunks.push(chunk));
|
||||
stream.on('end', () => {
|
||||
stream.destroy();
|
||||
resolve(chunks);
|
||||
});
|
||||
});
|
||||
};
|
||||
@ -24,8 +24,8 @@
|
||||
"url": "https://strapi.io"
|
||||
}
|
||||
],
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"main": "./lib/index.js",
|
||||
"types": "./lib/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "yarn build:ts",
|
||||
"build:ts": "tsc -p tsconfig.json",
|
||||
@ -36,7 +36,7 @@
|
||||
"watch": "yarn build:ts -w --preserveWatchOutput"
|
||||
},
|
||||
"directories": {
|
||||
"lib": "./dist"
|
||||
"lib": "./lib"
|
||||
},
|
||||
"dependencies": {
|
||||
"@strapi/logger": "4.5.5",
|
||||
@ -63,7 +63,7 @@
|
||||
"@types/tar": "6.1.3",
|
||||
"@types/tar-stream": "2.2.2",
|
||||
"@types/uuid": "9.0.0",
|
||||
"koa": "2.14.1",
|
||||
"@types/koa": "2.13.1",
|
||||
"rimraf": "3.0.2",
|
||||
"typescript": "4.6.2"
|
||||
},
|
||||
|
||||
@ -102,6 +102,13 @@ export const destinationStages = [
|
||||
'getSchemasStream',
|
||||
];
|
||||
|
||||
/**
|
||||
* Update the global store with the given strapi value
|
||||
*/
|
||||
export const setGlobalStrapi = (strapi: Strapi.Strapi): void => {
|
||||
(global as unknown as Global).strapi = strapi;
|
||||
};
|
||||
|
||||
/**
|
||||
* Add jest expect helpers
|
||||
*/
|
||||
@ -19,7 +19,7 @@ import type {
|
||||
} from '../../types';
|
||||
import type { Diff } from '../utils/json';
|
||||
|
||||
import compareSchemas from '../strategies';
|
||||
import { compareSchemas } from './validation/schemas';
|
||||
import { filter, map } from '../utils/stream';
|
||||
|
||||
export const TRANSFER_STAGES: ReadonlyArray<TransferStage> = Object.freeze([
|
||||
@ -0,0 +1 @@
|
||||
export * as schemas from './schemas';
|
||||
@ -1,5 +1,5 @@
|
||||
import type { Diff } from '../utils/json';
|
||||
import * as utils from '../utils';
|
||||
import type { Diff } from '../../../utils/json';
|
||||
import * as utils from '../../../utils';
|
||||
|
||||
const strategies = {
|
||||
// No diffs
|
||||
@ -32,4 +32,4 @@ const compareSchemas = <T, P>(a: T, b: P, strategy: keyof typeof strategies) =>
|
||||
return strategies[strategy](diffs);
|
||||
};
|
||||
|
||||
export default compareSchemas;
|
||||
export { compareSchemas };
|
||||
1
packages/core/data-transfer/src/file/index.ts
Normal file
1
packages/core/data-transfer/src/file/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * as providers from './providers';
|
||||
@ -6,7 +6,7 @@ jest.mock('fs');
|
||||
import fs from 'fs-extra';
|
||||
import { Writable } from 'stream-chain';
|
||||
import { createLocalFileDestinationProvider, ILocalFileDestinationProviderOptions } from '..';
|
||||
import * as encryption from '../../../encryption/encrypt';
|
||||
import * as encryption from '../../../../utils/encryption';
|
||||
import { createFilePathFactory, createTarEntryStream } from '../utils';
|
||||
|
||||
fs.createWriteStream = jest.fn().mockReturnValue(
|
||||
@ -18,14 +18,14 @@ fs.createWriteStream = jest.fn().mockReturnValue(
|
||||
|
||||
const filePath = './test-file';
|
||||
|
||||
jest.mock('../../../encryption/encrypt', () => {
|
||||
jest.mock('../../../../utils/encryption', () => {
|
||||
return {
|
||||
__esModule: true,
|
||||
createEncryptionCipher() {},
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('../../local-file-destination-provider/utils');
|
||||
jest.mock('../utils');
|
||||
|
||||
describe('Local File Destination Provider', () => {
|
||||
(createFilePathFactory as jest.Mock).mockImplementation(jest.fn());
|
||||
@ -6,7 +6,7 @@ import { stringer } from 'stream-json/jsonl/Stringer';
|
||||
import { chain } from 'stream-chain';
|
||||
import { Readable, Writable } from 'stream';
|
||||
|
||||
import { createEncryptionCipher } from '../../encryption/encrypt';
|
||||
import { createEncryptionCipher } from '../../../utils/encryption';
|
||||
import type {
|
||||
IAsset,
|
||||
IDestinationProvider,
|
||||
@ -14,7 +14,7 @@ import type {
|
||||
IMetadata,
|
||||
ProviderType,
|
||||
Stream,
|
||||
} from '../../../types';
|
||||
} from '../../../../types';
|
||||
import { createFilePathFactory, createTarEntryStream } from './utils';
|
||||
|
||||
export interface ILocalFileDestinationProviderOptions {
|
||||
2
packages/core/data-transfer/src/file/providers/index.ts
Normal file
2
packages/core/data-transfer/src/file/providers/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './source';
|
||||
export * from './destination';
|
||||
@ -8,10 +8,9 @@ import { keyBy } from 'lodash/fp';
|
||||
import { chain } from 'stream-chain';
|
||||
import { pipeline, PassThrough } from 'stream';
|
||||
import { parser } from 'stream-json/jsonl/Parser';
|
||||
import type { IAsset, IMetadata, ISourceProvider, ProviderType } from '../../../types';
|
||||
import type { IAsset, IMetadata, ISourceProvider, ProviderType } from '../../../../types';
|
||||
|
||||
import { createDecryptionCipher } from '../../encryption';
|
||||
import * as utils from '../../utils';
|
||||
import * as utils from '../../../utils';
|
||||
|
||||
type StreamItemArray = Parameters<typeof chain>[0];
|
||||
|
||||
@ -152,7 +151,7 @@ class LocalFileSourceProvider implements ISourceProvider {
|
||||
}
|
||||
|
||||
if (encryption.enabled && encryption.key) {
|
||||
streams.push(createDecryptionCipher(encryption.key));
|
||||
streams.push(utils.encryption.createDecryptionCipher(encryption.key));
|
||||
}
|
||||
|
||||
if (compression.enabled) {
|
||||
@ -227,20 +226,22 @@ class LocalFileSourceProvider implements ISourceProvider {
|
||||
return entryPath === filePath && entry.type === 'File';
|
||||
},
|
||||
|
||||
/**
|
||||
* Whenever an entry passes the filter method, process it
|
||||
*/
|
||||
async onentry(entry) {
|
||||
// Collect all the content of the entry file
|
||||
const content = await entry.collect();
|
||||
// Parse from buffer to string to JSON
|
||||
const parsedContent = JSON.parse(content.toString());
|
||||
|
||||
// Resolve the Promise with the parsed content
|
||||
resolve(parsedContent);
|
||||
try {
|
||||
// Parse from buffer to string to JSON
|
||||
const parsedContent = JSON.parse(content.toString());
|
||||
|
||||
// Cleanup (close the stream associated to the entry)
|
||||
entry.destroy();
|
||||
// Resolve the Promise with the parsed content
|
||||
resolve(parsedContent);
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
} finally {
|
||||
// Cleanup (close the stream associated to the entry)
|
||||
entry.destroy();
|
||||
}
|
||||
},
|
||||
}),
|
||||
],
|
||||
4
packages/core/data-transfer/src/index.ts
Normal file
4
packages/core/data-transfer/src/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export * as engine from './engine';
|
||||
export * as strapi from './strapi';
|
||||
export * as file from './file';
|
||||
export * as utils from './utils';
|
||||
@ -0,0 +1,36 @@
|
||||
import { getStrapiFactory } from '../../__tests__/test-utils';
|
||||
|
||||
import { createTransferHandler } from '../remote/handlers';
|
||||
import register from '../register';
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const strapiMockFactory = getStrapiFactory({
|
||||
admin: {
|
||||
routes: {
|
||||
push: jest.fn(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
jest.mock('../remote/handlers', () => ({
|
||||
createTransferHandler: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('Register the Transfer route', () => {
|
||||
test('registers the /transfer route', () => {
|
||||
const strapi = strapiMockFactory();
|
||||
|
||||
register(strapi);
|
||||
expect(strapi.admin.routes.push).toHaveBeenCalledWith({
|
||||
method: 'GET',
|
||||
path: '/transfer',
|
||||
handler: createTransferHandler(),
|
||||
config: {
|
||||
auth: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
5
packages/core/data-transfer/src/strapi/index.ts
Normal file
5
packages/core/data-transfer/src/strapi/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export * as providers from './providers';
|
||||
export * as queries from './queries';
|
||||
export * as remote from './remote';
|
||||
|
||||
export { default as register } from './register';
|
||||
@ -0,0 +1,6 @@
|
||||
// Local
|
||||
export * from './local-destination';
|
||||
export * from './local-source';
|
||||
|
||||
// Remote
|
||||
export * from './remote-destination';
|
||||
@ -1,8 +1,8 @@
|
||||
import fse from 'fs-extra';
|
||||
import { Writable, Readable } from 'stream';
|
||||
import type { IAsset } from '../../../../types';
|
||||
import type { IAsset } from '../../../../../types';
|
||||
|
||||
import { getStrapiFactory } from '../../../__tests__/test-utils';
|
||||
import { getStrapiFactory } from '../../../../__tests__/test-utils';
|
||||
import { createLocalStrapiDestinationProvider } from '../index';
|
||||
|
||||
const write = jest.fn((_chunk, _encoding, callback) => {
|
||||
@ -1,6 +1,10 @@
|
||||
import { createLocalStrapiDestinationProvider } from '../index';
|
||||
import * as restoreApi from '../strategies/restore';
|
||||
import { getStrapiFactory, getContentTypes, setGlobalStrapi } from '../../test-utils';
|
||||
import {
|
||||
getStrapiFactory,
|
||||
getContentTypes,
|
||||
setGlobalStrapi,
|
||||
} from '../../../../__tests__/test-utils';
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
@ -1,6 +1,10 @@
|
||||
import { deleteRecords, restoreConfigs } from '../strategies/restore';
|
||||
import { getStrapiFactory, getContentTypes, setGlobalStrapi } from '../../test-utils';
|
||||
import { IConfiguration } from '../../../../types';
|
||||
import {
|
||||
getStrapiFactory,
|
||||
getContentTypes,
|
||||
setGlobalStrapi,
|
||||
} from '../../../../__tests__/test-utils';
|
||||
import { IConfiguration } from '../../../../../types';
|
||||
|
||||
const entities = [
|
||||
{
|
||||
@ -1,10 +1,10 @@
|
||||
import { Writable } from 'stream';
|
||||
import path from 'path';
|
||||
import * as fse from 'fs-extra';
|
||||
import type { IAsset, IDestinationProvider, IMetadata, ProviderType } from '../../../types';
|
||||
import type { IAsset, IDestinationProvider, IMetadata, ProviderType } from '../../../../types';
|
||||
|
||||
import { restore } from './strategies';
|
||||
import * as utils from '../../utils';
|
||||
import * as utils from '../../../utils';
|
||||
|
||||
export const VALID_CONFLICT_STRATEGIES = ['restore', 'merge'];
|
||||
export const DEFAULT_CONFLICT_STRATEGY = 'restore';
|
||||
@ -25,6 +25,9 @@ class LocalStrapiDestinationProvider implements IDestinationProvider {
|
||||
|
||||
strapi?: Strapi.Strapi;
|
||||
|
||||
/**
|
||||
* The entities mapper is used to map old entities to their new IDs
|
||||
*/
|
||||
#entitiesMapper: { [type: string]: { [id: number]: number } };
|
||||
|
||||
constructor(options: ILocalStrapiDestinationProviderOptions) {
|
||||
@ -123,6 +126,7 @@ class LocalStrapiDestinationProvider implements IDestinationProvider {
|
||||
throw new Error(`Invalid strategy supplied: "${strategy}"`);
|
||||
}
|
||||
|
||||
// TODO: Move this logic to the restore strategy
|
||||
async getAssetsStream(): Promise<Writable> {
|
||||
if (!this.strapi) {
|
||||
throw new Error('Not able to stream Assets. Strapi instance not found');
|
||||
@ -1,6 +1,6 @@
|
||||
import { Writable } from 'stream';
|
||||
import chalk from 'chalk';
|
||||
import { IConfiguration } from '../../../../../types';
|
||||
import { IConfiguration } from '../../../../../../types';
|
||||
|
||||
const restoreCoreStore = async <T extends { value: unknown }>(strapi: Strapi.Strapi, data: T) => {
|
||||
return strapi.db.query('strapi::core-store').create({
|
||||
@ -3,9 +3,9 @@ import type { SchemaUID } from '@strapi/strapi/lib/types/utils';
|
||||
import { get } from 'lodash/fp';
|
||||
import { Writable } from 'stream';
|
||||
|
||||
import type { IEntity } from '../../../../../types';
|
||||
import { json } from '../../../../utils';
|
||||
import * as shared from '../../../shared';
|
||||
import type { IEntity } from '../../../../../../types';
|
||||
import { json } from '../../../../../utils';
|
||||
import * as queries from '../../../../queries';
|
||||
|
||||
interface IEntitiesRestoreStreamOptions {
|
||||
strapi: Strapi.Strapi;
|
||||
@ -14,7 +14,7 @@ interface IEntitiesRestoreStreamOptions {
|
||||
|
||||
const createEntitiesWriteStream = (options: IEntitiesRestoreStreamOptions) => {
|
||||
const { strapi, updateMappingTable } = options;
|
||||
const query = shared.strapi.entity.createEntityQuery(strapi);
|
||||
const query = queries.entity.createEntityQuery(strapi);
|
||||
|
||||
return new Writable({
|
||||
objectMode: true,
|
||||
@ -24,13 +24,19 @@ const createEntitiesWriteStream = (options: IEntitiesRestoreStreamOptions) => {
|
||||
const { create, getDeepPopulateComponentLikeQuery } = query(type);
|
||||
const contentType = strapi.getModel(type);
|
||||
|
||||
const resolveType = (paths: string[]) => {
|
||||
/**
|
||||
* Resolve the component UID of an entity's attribute based
|
||||
* on a given path (components & dynamic zones only)
|
||||
*/
|
||||
const resolveType = (paths: string[]): string | undefined => {
|
||||
let cType = contentType;
|
||||
let value: unknown = data;
|
||||
|
||||
for (const path of paths) {
|
||||
value = get(path, value);
|
||||
|
||||
// Needed when the value of cType should be computed
|
||||
// based on the next value (eg: dynamic zones)
|
||||
if (typeof cType === 'function') {
|
||||
cType = cType(value);
|
||||
}
|
||||
@ -48,24 +54,32 @@ const createEntitiesWriteStream = (options: IEntitiesRestoreStreamOptions) => {
|
||||
}
|
||||
}
|
||||
|
||||
return cType.uid;
|
||||
return cType?.uid;
|
||||
};
|
||||
|
||||
try {
|
||||
// Create the entity
|
||||
const created = await create({
|
||||
data,
|
||||
populate: getDeepPopulateComponentLikeQuery(contentType, { select: 'id' }),
|
||||
select: 'id',
|
||||
});
|
||||
|
||||
// Compute differences between original & new entities
|
||||
const diffs = json.diff(data, created);
|
||||
|
||||
updateMappingTable(type, id, created.id);
|
||||
|
||||
// For each difference found on an ID attribute,
|
||||
// update the mapping the table accordingly
|
||||
diffs.forEach((diff) => {
|
||||
if (diff.kind === 'modified' && diff.path.at(-1) === 'id') {
|
||||
const target = resolveType(diff.path);
|
||||
|
||||
// If no type is found for the given path, then ignore the diff
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [oldID, newID] = diff.values as [number, number];
|
||||
|
||||
updateMappingTable(target, oldID, newID);
|
||||
@ -1,5 +1,5 @@
|
||||
import type { ContentTypeSchema } from '@strapi/strapi';
|
||||
import * as shared from '../../../shared';
|
||||
import * as queries from '../../../../queries';
|
||||
|
||||
export interface IRestoreOptions {
|
||||
assets?: boolean;
|
||||
@ -36,7 +36,7 @@ const deleteEntitiesRecord = async (
|
||||
options: IRestoreOptions = {}
|
||||
): Promise<IDeleteResults> => {
|
||||
const { entities } = options;
|
||||
const query = shared.strapi.entity.createEntityQuery(strapi);
|
||||
const query = queries.entity.createEntityQuery(strapi);
|
||||
const contentTypes = Object.values<ContentTypeSchema>(strapi.contentTypes);
|
||||
|
||||
const contentTypesToClear = contentTypes.filter((contentType) => {
|
||||
@ -1,6 +1,6 @@
|
||||
import { Writable } from 'stream';
|
||||
import { ILink } from '../../../../../types';
|
||||
import { createLinkQuery } from '../../../shared/strapi/link';
|
||||
import { ILink } from '../../../../../../types';
|
||||
import { createLinkQuery } from '../../../../queries/link';
|
||||
|
||||
export const createLinksWriteStream = (
|
||||
mapID: (uid: string, id: number) => number | undefined,
|
||||
@ -1,6 +1,10 @@
|
||||
import { Readable } from 'stream';
|
||||
|
||||
import { collect, createMockedQueryBuilder, getStrapiFactory } from '../../../__tests__/test-utils';
|
||||
import {
|
||||
collect,
|
||||
createMockedQueryBuilder,
|
||||
getStrapiFactory,
|
||||
} from '../../../../__tests__/test-utils';
|
||||
import { createConfigurationStream } from '../configuration';
|
||||
|
||||
describe('Configuration', () => {
|
||||
@ -1,12 +1,12 @@
|
||||
import { Readable, PassThrough } from 'stream';
|
||||
import type { IEntity } from '../../../../types';
|
||||
import type { IEntity } from '../../../../../types';
|
||||
|
||||
import {
|
||||
collect,
|
||||
getStrapiFactory,
|
||||
getContentTypes,
|
||||
createMockedQueryBuilder,
|
||||
} from '../../../__tests__/test-utils';
|
||||
} from '../../../../__tests__/test-utils';
|
||||
import { createEntitiesStream, createEntitiesTransformStream } from '../entities';
|
||||
|
||||
describe('Local Strapi Source Provider - Entities Streaming', () => {
|
||||
@ -1,7 +1,11 @@
|
||||
import { Readable } from 'stream';
|
||||
import type { IEntity } from '../../../../types';
|
||||
import type { IEntity } from '../../../../../types';
|
||||
|
||||
import { collect, createMockedQueryBuilder, getStrapiFactory } from '../../../__tests__/test-utils';
|
||||
import {
|
||||
collect,
|
||||
createMockedQueryBuilder,
|
||||
getStrapiFactory,
|
||||
} from '../../../../__tests__/test-utils';
|
||||
import { createLocalStrapiSourceProvider } from '..';
|
||||
|
||||
describe('Local Strapi Source Provider', () => {
|
||||
@ -1,7 +1,7 @@
|
||||
import { Readable } from 'stream';
|
||||
|
||||
import { createLinksStream } from '../links';
|
||||
import { collect, getStrapiFactory } from '../../../__tests__/test-utils';
|
||||
import { collect, getStrapiFactory } from '../../../../__tests__/test-utils';
|
||||
|
||||
// TODO: entityService needs to be replaced with a mocked wrapper of db.connection and provide real metadata
|
||||
describe.skip('Local Strapi Source Provider - Entities Streaming', () => {
|
||||
@ -2,7 +2,7 @@ import { join } from 'path';
|
||||
import { readdir, stat, createReadStream } from 'fs-extra';
|
||||
import { Duplex } from 'stream';
|
||||
|
||||
import type { IAsset } from '../../../types';
|
||||
import type { IAsset } from '../../../../types';
|
||||
|
||||
const IGNORED_FILES = ['.gitkeep'];
|
||||
|
||||
@ -2,7 +2,7 @@ import { chain } from 'stream-chain';
|
||||
import { Readable } from 'stream';
|
||||
import { set } from 'lodash/fp';
|
||||
|
||||
import type { IConfiguration } from '../../../types';
|
||||
import type { IConfiguration } from '../../../../types';
|
||||
|
||||
/**
|
||||
* Create a readable stream that export the Strapi app configuration
|
||||
@ -1,8 +1,8 @@
|
||||
import type { ContentTypeSchema } from '@strapi/strapi';
|
||||
|
||||
import { Readable, PassThrough } from 'stream';
|
||||
import * as shared from '../shared/strapi';
|
||||
import { IEntity } from '../../../types';
|
||||
import * as shared from '../../queries';
|
||||
import { IEntity } from '../../../../types';
|
||||
|
||||
/**
|
||||
* Generate and consume content-types streams in order to stream each entity individually
|
||||
@ -1,12 +1,12 @@
|
||||
import { chain } from 'stream-chain';
|
||||
import { Readable } from 'stream';
|
||||
|
||||
import type { IMetadata, ISourceProvider, ProviderType } from '../../../types';
|
||||
import type { IMetadata, ISourceProvider, ProviderType } from '../../../../types';
|
||||
import { createEntitiesStream, createEntitiesTransformStream } from './entities';
|
||||
import { createLinksStream } from './links';
|
||||
import { createConfigurationStream } from './configuration';
|
||||
import { createAssetsStream } from './assets';
|
||||
import * as utils from '../../utils';
|
||||
import * as utils from '../../../utils';
|
||||
|
||||
export interface ILocalStrapiSourceProviderOptions {
|
||||
getStrapi(): Strapi.Strapi | Promise<Strapi.Strapi>;
|
||||
@ -1,7 +1,7 @@
|
||||
import { Readable } from 'stream';
|
||||
|
||||
import type { ILink } from '../../../types';
|
||||
import { createLinkQuery } from '../shared/strapi/link';
|
||||
import type { ILink } from '../../../../types';
|
||||
import { createLinkQuery } from '../../queries/link';
|
||||
|
||||
/**
|
||||
* Create a Readable which will stream all the links from a Strapi instance
|
||||
@ -11,8 +11,8 @@ import type {
|
||||
IConfiguration,
|
||||
TransferStage,
|
||||
IAsset,
|
||||
} from '../../../types';
|
||||
import type { ILocalStrapiDestinationProviderOptions } from '../local-strapi-destination-provider';
|
||||
} from '../../../../types';
|
||||
import type { ILocalStrapiDestinationProviderOptions } from '../local-destination';
|
||||
import { dispatch } from './utils';
|
||||
|
||||
interface ITokenAuth {
|
||||
@ -1,5 +1,6 @@
|
||||
import { RelationAttribute } from '@strapi/strapi';
|
||||
import { clone, isNil } from 'lodash/fp';
|
||||
import { ILink } from '../../../../types';
|
||||
import { ILink } from '../../../types';
|
||||
|
||||
// TODO: Remove any types when we'll have types for DB metadata
|
||||
|
||||
12
packages/core/data-transfer/src/strapi/register.ts
Normal file
12
packages/core/data-transfer/src/strapi/register.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { routes } from './remote';
|
||||
|
||||
/**
|
||||
* This is intended to be called on Strapi register phase.
|
||||
*
|
||||
* It registers a transfer route in the Strapi admin router.
|
||||
*/
|
||||
const register = (strapi: Strapi.Strapi) => {
|
||||
routes.registerAdminTransferRoute(strapi);
|
||||
};
|
||||
|
||||
export default register;
|
||||
@ -0,0 +1 @@
|
||||
export * from './push';
|
||||
@ -1,6 +1,6 @@
|
||||
import { PassThrough, Writable } from 'stream-chain';
|
||||
|
||||
import { IAsset, IMetadata, PushTransferMessage, PushTransferStage } from '../../../types';
|
||||
import { IAsset, IMetadata, PushTransferMessage, PushTransferStage } from '../../../../types';
|
||||
import {
|
||||
createLocalStrapiDestinationProvider,
|
||||
ILocalStrapiDestinationProviderOptions,
|
||||
@ -71,7 +71,7 @@ const createPushController = (options: ILocalStrapiDestinationProviderOptions):
|
||||
streams.entities = provider.getEntitiesStream();
|
||||
}
|
||||
|
||||
await writeAsync(streams.entities!, entity);
|
||||
await writeAsync(streams.entities, entity);
|
||||
},
|
||||
|
||||
async links(link) {
|
||||
@ -79,7 +79,7 @@ const createPushController = (options: ILocalStrapiDestinationProviderOptions):
|
||||
streams.links = await provider.getLinksStream();
|
||||
}
|
||||
|
||||
await writeAsync(streams.links!, link);
|
||||
await writeAsync(streams.links, link);
|
||||
},
|
||||
|
||||
async configuration(config) {
|
||||
@ -87,7 +87,7 @@ const createPushController = (options: ILocalStrapiDestinationProviderOptions):
|
||||
streams.configuration = await provider.getConfigurationStream();
|
||||
}
|
||||
|
||||
await writeAsync(streams.configuration!, config);
|
||||
await writeAsync(streams.configuration, config);
|
||||
},
|
||||
|
||||
async assets(payload) {
|
||||
@ -1,13 +1,14 @@
|
||||
// eslint-disable-next-line node/no-extraneous-import
|
||||
import type { Context } from 'koa';
|
||||
import type { ServerOptions } from 'ws';
|
||||
|
||||
import { v4 } from 'uuid';
|
||||
import { WebSocket } from 'ws';
|
||||
|
||||
import type { IPushController } from './push';
|
||||
import type { IPushController } from './controllers/push';
|
||||
|
||||
import { InitMessage, Message, TransferKind } from '../../../types';
|
||||
import createPushController from './push';
|
||||
import createPushController from './controllers/push';
|
||||
|
||||
interface ITransferState {
|
||||
kind?: TransferKind;
|
||||
@ -15,7 +16,7 @@ interface ITransferState {
|
||||
controller?: IPushController;
|
||||
}
|
||||
|
||||
const createTransferController =
|
||||
export const createTransferHandler =
|
||||
(options: ServerOptions = {}) =>
|
||||
async (ctx: Context) => {
|
||||
const upgradeHeader = (ctx.request.headers.upgrade || '')
|
||||
@ -143,5 +144,3 @@ const createTransferController =
|
||||
ctx.respond = false;
|
||||
}
|
||||
};
|
||||
|
||||
export default createTransferController;
|
||||
2
packages/core/data-transfer/src/strapi/remote/index.ts
Normal file
2
packages/core/data-transfer/src/strapi/remote/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * as controllers from './controllers';
|
||||
export * as routes from './routes';
|
||||
35
packages/core/data-transfer/src/strapi/remote/routes.ts
Normal file
35
packages/core/data-transfer/src/strapi/remote/routes.ts
Normal file
@ -0,0 +1,35 @@
|
||||
// eslint-disable-next-line node/no-extraneous-import
|
||||
import type { Context } from 'koa';
|
||||
|
||||
import { createTransferHandler } from './handlers';
|
||||
|
||||
// Extend Strapi interface type to access the admin routes' API
|
||||
// TODO: Remove this when the Strapi instances will be better typed
|
||||
declare module '@strapi/strapi' {
|
||||
interface Strapi {
|
||||
admin: {
|
||||
routes: {
|
||||
method: string;
|
||||
path: string;
|
||||
handler: (ctx: Context) => Promise<void>;
|
||||
config: unknown;
|
||||
}[];
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a transfer route in the Strapi admin router.
|
||||
*
|
||||
* It exposes a WS server that can be used to run and manage transfer processes.
|
||||
*
|
||||
* @param strapi - A Strapi instance
|
||||
*/
|
||||
export const registerAdminTransferRoute = (strapi: Strapi.Strapi) => {
|
||||
strapi.admin.routes.push({
|
||||
method: 'GET',
|
||||
path: '/transfer',
|
||||
handler: createTransferHandler(),
|
||||
config: { auth: false },
|
||||
});
|
||||
};
|
||||
@ -1,5 +1,5 @@
|
||||
import { Cipher, scryptSync, CipherKey, BinaryLike, createDecipheriv } from 'crypto';
|
||||
import { EncryptionStrategy, Strategies, Algorithm } from '../../types';
|
||||
import { EncryptionStrategy, Strategies, Algorithm } from '../../../types';
|
||||
|
||||
// different key values depending on algorithm chosen
|
||||
const getDecryptionStrategy = (algorithm: Algorithm): EncryptionStrategy => {
|
||||
@ -33,6 +33,14 @@ const getDecryptionStrategy = (algorithm: Algorithm): EncryptionStrategy => {
|
||||
return strategies[algorithm];
|
||||
};
|
||||
|
||||
/**
|
||||
* It creates a cipher instance used for decryption
|
||||
*
|
||||
* @param key - The decryption key
|
||||
* @param algorithm - The algorithm to use to create the Cipher
|
||||
*
|
||||
* @returns A {@link Cipher} instance created with the given key & algorithm
|
||||
*/
|
||||
export const createDecryptionCipher = (
|
||||
key: string,
|
||||
algorithm: Algorithm = 'aes-128-ecb'
|
||||
@ -1,5 +1,5 @@
|
||||
import { createCipheriv, Cipher, scryptSync, CipherKey, BinaryLike } from 'crypto';
|
||||
import { EncryptionStrategy, Strategies, Algorithm } from '../../types';
|
||||
import { EncryptionStrategy, Strategies, Algorithm } from '../../../types';
|
||||
|
||||
// different key values depending on algorithm chosen
|
||||
const getEncryptionStrategy = (algorithm: Algorithm): EncryptionStrategy => {
|
||||
@ -33,6 +33,14 @@ const getEncryptionStrategy = (algorithm: Algorithm): EncryptionStrategy => {
|
||||
return strategies[algorithm];
|
||||
};
|
||||
|
||||
/**
|
||||
* It creates a cipher instance used for encryption
|
||||
*
|
||||
* @param key - The encryption key
|
||||
* @param algorithm - The algorithm to use to create the Cipher
|
||||
*
|
||||
* @returns A {@link Cipher} instance created with the given key & algorithm
|
||||
*/
|
||||
export const createEncryptionCipher = (
|
||||
key: string,
|
||||
algorithm: Algorithm = 'aes-128-ecb'
|
||||
@ -1,3 +1,4 @@
|
||||
export * as encryption from './encryption';
|
||||
export * as stream from './stream';
|
||||
export * as json from './json';
|
||||
export * as schema from './schema';
|
||||
@ -2,6 +2,13 @@ import { isArray, isObject, zip, isEqual, uniq } from 'lodash/fp';
|
||||
|
||||
const createContext = (): Context => ({ path: [] });
|
||||
|
||||
/**
|
||||
* Compute differences between two JSON objects and returns them
|
||||
*
|
||||
* @param a - First object
|
||||
* @param b - Second object
|
||||
* @param ctx - Context used to keep track of the current path during recursion
|
||||
*/
|
||||
export const diff = (a: unknown, b: unknown, ctx: Context = createContext()): Diff[] => {
|
||||
const diffs: Diff[] = [];
|
||||
const { path } = ctx;
|
||||
@ -70,7 +77,7 @@ export const diff = (a: unknown, b: unknown, ctx: Context = createContext()): Di
|
||||
}
|
||||
|
||||
if (!isEqual(a, b)) {
|
||||
modified();
|
||||
return modified();
|
||||
}
|
||||
|
||||
return diffs;
|
||||
27
packages/core/data-transfer/src/utils/schema.ts
Normal file
27
packages/core/data-transfer/src/utils/schema.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import type { Schema } from '@strapi/strapi';
|
||||
import { mapValues, pick } from 'lodash/fp';
|
||||
|
||||
/**
|
||||
* List of schema properties that should be kept when sanitizing schemas
|
||||
*/
|
||||
const VALID_SCHEMA_PROPERTIES = [
|
||||
'collectionName',
|
||||
'info',
|
||||
'options',
|
||||
'pluginOptions',
|
||||
'attributes',
|
||||
'kind',
|
||||
'modelType',
|
||||
'modelName',
|
||||
'uid',
|
||||
'plugin',
|
||||
'globalId',
|
||||
];
|
||||
|
||||
/**
|
||||
* Sanitize a schemas dictionnary by omiting unwanted properties
|
||||
* The list of allowed properties can be found here: {@link VALID_SCHEMA_PROPERTIES}
|
||||
*/
|
||||
export const mapSchemasValues = (schemas: Record<string, Schema>) => {
|
||||
return mapValues(pick(VALID_SCHEMA_PROPERTIES), schemas);
|
||||
};
|
||||
72
packages/core/data-transfer/src/utils/stream.ts
Normal file
72
packages/core/data-transfer/src/utils/stream.ts
Normal file
@ -0,0 +1,72 @@
|
||||
import { Transform, Readable } from 'stream';
|
||||
|
||||
type TransformOptions = ConstructorParameters<typeof Transform>[0];
|
||||
|
||||
/**
|
||||
* Create a filter stream that discard chunks which doesn't satisfies the given predicate
|
||||
*
|
||||
* @param predicate - A filter predicate, takes a stream data chunk as parameter and returns a boolean value
|
||||
* @param options - Transform stream options
|
||||
*/
|
||||
export const filter = <T>(
|
||||
predicate: (value: T) => boolean | Promise<boolean>,
|
||||
options: TransformOptions = { objectMode: true }
|
||||
): Transform => {
|
||||
return new Transform({
|
||||
...options,
|
||||
|
||||
async transform(chunk, _encoding, callback) {
|
||||
const keep = await predicate(chunk);
|
||||
|
||||
callback(null, keep ? chunk : undefined);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a map stream that transform chunks using the given predicate
|
||||
*
|
||||
* @param predicate - A map predicate, takes a stream data chunk as parameter and returns a mapped value
|
||||
* @param options - Transform stream options
|
||||
*/
|
||||
export const map = <T, U = T>(
|
||||
predicate: (value: T) => U | Promise<U>,
|
||||
options: TransformOptions = { objectMode: true }
|
||||
): Transform => {
|
||||
return new Transform({
|
||||
...options,
|
||||
|
||||
async transform(chunk, _encoding, callback) {
|
||||
const mappedValue = await predicate(chunk);
|
||||
|
||||
callback(null, mappedValue);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Collect every chunks from a Readable stream.
|
||||
*
|
||||
* @param stream - The redable stream to collect data from
|
||||
* @param options.destroy - If set to true, it automatically calls `destroy()` on the given stream upon receiving the 'end' event
|
||||
*/
|
||||
export const collect = <T = unknown>(
|
||||
stream: Readable,
|
||||
options: { destroy: boolean } = { destroy: true }
|
||||
): Promise<T[]> => {
|
||||
const chunks: T[] = [];
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
stream
|
||||
.on('close', () => resolve(chunks))
|
||||
.on('error', reject)
|
||||
.on('data', (chunk) => chunks.push(chunk))
|
||||
.on('end', () => {
|
||||
if (options.destroy) {
|
||||
stream.destroy();
|
||||
}
|
||||
|
||||
resolve(chunks);
|
||||
});
|
||||
});
|
||||
};
|
||||
@ -4,11 +4,11 @@
|
||||
"strict": true,
|
||||
"lib": ["ESNEXT"],
|
||||
"skipLibCheck": true,
|
||||
"outDir": "dist",
|
||||
"outDir": "lib",
|
||||
"declaration": true,
|
||||
"sourceMap": true,
|
||||
"esModuleInterop": true
|
||||
},
|
||||
"include": ["types", "lib/**/*.ts"],
|
||||
"exclude": ["node_modules", "lib/**/__tests__"]
|
||||
"include": ["types", "src/**/*.ts"],
|
||||
"exclude": ["node_modules", "src/**/__tests__"]
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { ILocalStrapiDestinationProviderOptions } from '../lib';
|
||||
import type { ILocalStrapiDestinationProviderOptions } from '../src/strapi/providers';
|
||||
import type { IAsset, IConfiguration, IEntity, ILink } from './common-entities';
|
||||
|
||||
/**
|
||||
|
||||
@ -1,33 +1,39 @@
|
||||
'use strict';
|
||||
|
||||
describe('export', () => {
|
||||
describe('Export', () => {
|
||||
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: {
|
||||
file: {
|
||||
providers: {
|
||||
createLocalFileDestinationProvider: jest.fn().mockReturnValue({ name: 'testDest' }),
|
||||
},
|
||||
},
|
||||
strapi: {
|
||||
providers: {
|
||||
createLocalStrapiSourceProvider: jest.fn().mockReturnValue({ name: 'testSource' }),
|
||||
},
|
||||
},
|
||||
engine: {
|
||||
createTransferEngine() {
|
||||
return {
|
||||
transfer: jest.fn().mockReturnValue(Promise.resolve({})),
|
||||
progress: {
|
||||
on: jest.fn(),
|
||||
stream: {
|
||||
on: jest.fn(),
|
||||
},
|
||||
},
|
||||
},
|
||||
sourceProvider: { name: 'testSource' },
|
||||
destinationProvider: { name: 'testDestination' },
|
||||
};
|
||||
sourceProvider: { name: 'testSource' },
|
||||
destinationProvider: { name: 'testDestination' },
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
jest.mock(
|
||||
'@strapi/data-transfer',
|
||||
() => {
|
||||
return mockDataTransfer;
|
||||
},
|
||||
{ virtual: true }
|
||||
);
|
||||
|
||||
jest.mock('@strapi/data-transfer/lib/engine', () => mockDataTransfer.engine, { virtual: true });
|
||||
jest.mock('@strapi/data-transfer/lib/strapi', () => mockDataTransfer.strapi, { virtual: true });
|
||||
jest.mock('@strapi/data-transfer/lib/file', () => mockDataTransfer.file, { virtual: true });
|
||||
|
||||
// mock utils
|
||||
const mockUtils = {
|
||||
@ -76,7 +82,7 @@ describe('export', () => {
|
||||
await exportCommand({ file: filename });
|
||||
});
|
||||
|
||||
expect(mockDataTransfer.createLocalFileDestinationProvider).toHaveBeenCalledWith(
|
||||
expect(mockDataTransfer.file.providers.createLocalFileDestinationProvider).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
file: { path: filename },
|
||||
})
|
||||
@ -90,7 +96,7 @@ describe('export', () => {
|
||||
});
|
||||
|
||||
expect(mockUtils.getDefaultExportName).toHaveBeenCalledTimes(1);
|
||||
expect(mockDataTransfer.createLocalFileDestinationProvider).toHaveBeenCalledWith(
|
||||
expect(mockDataTransfer.file.providers.createLocalFileDestinationProvider).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
file: { path: defaultFileName },
|
||||
})
|
||||
@ -103,7 +109,7 @@ describe('export', () => {
|
||||
await exportCommand({ encrypt });
|
||||
});
|
||||
|
||||
expect(mockDataTransfer.createLocalFileDestinationProvider).toHaveBeenCalledWith(
|
||||
expect(mockDataTransfer.file.providers.createLocalFileDestinationProvider).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
encryption: { enabled: encrypt },
|
||||
})
|
||||
@ -117,7 +123,7 @@ describe('export', () => {
|
||||
await exportCommand({ encrypt, key });
|
||||
});
|
||||
|
||||
expect(mockDataTransfer.createLocalFileDestinationProvider).toHaveBeenCalledWith(
|
||||
expect(mockDataTransfer.file.providers.createLocalFileDestinationProvider).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
encryption: { enabled: encrypt, key },
|
||||
})
|
||||
@ -129,7 +135,7 @@ describe('export', () => {
|
||||
await exportCommand({ compress: false });
|
||||
});
|
||||
|
||||
expect(mockDataTransfer.createLocalFileDestinationProvider).toHaveBeenCalledWith(
|
||||
expect(mockDataTransfer.file.providers.createLocalFileDestinationProvider).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
compression: { enabled: false },
|
||||
})
|
||||
@ -137,7 +143,7 @@ describe('export', () => {
|
||||
await expectExit(1, async () => {
|
||||
await exportCommand({ compress: true });
|
||||
});
|
||||
expect(mockDataTransfer.createLocalFileDestinationProvider).toHaveBeenCalledWith(
|
||||
expect(mockDataTransfer.file.providers.createLocalFileDestinationProvider).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
compression: { enabled: true },
|
||||
})
|
||||
|
||||
@ -3,20 +3,21 @@
|
||||
const utils = require('../../transfer/utils');
|
||||
|
||||
const mockDataTransfer = {
|
||||
createRemoteStrapiDestinationProvider: jest.fn(),
|
||||
createLocalStrapiSourceProvider: jest.fn(),
|
||||
createTransferEngine: jest.fn().mockReturnValue({
|
||||
transfer: jest.fn().mockReturnValue(Promise.resolve({})),
|
||||
}),
|
||||
strapi: {
|
||||
providers: {
|
||||
createRemoteStrapiDestinationProvider: jest.fn(),
|
||||
createLocalStrapiSourceProvider: jest.fn(),
|
||||
},
|
||||
},
|
||||
engine: {
|
||||
createTransferEngine: jest.fn().mockReturnValue({
|
||||
transfer: jest.fn().mockReturnValue(Promise.resolve({})),
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
jest.mock(
|
||||
'@strapi/data-transfer',
|
||||
() => {
|
||||
return mockDataTransfer;
|
||||
},
|
||||
{ virtual: true }
|
||||
);
|
||||
jest.mock('@strapi/data-transfer/lib/engine', () => mockDataTransfer.engine, { virtual: true });
|
||||
jest.mock('@strapi/data-transfer/lib/strapi', () => mockDataTransfer.strapi, { virtual: true });
|
||||
|
||||
const expectExit = async (code, fn) => {
|
||||
const exit = jest.spyOn(process, 'exit').mockImplementation((number) => {
|
||||
@ -37,7 +38,7 @@ jest.mock('../../transfer/utils');
|
||||
|
||||
const destinationUrl = 'ws://strapi.com';
|
||||
|
||||
describe('transfer', () => {
|
||||
describe('Transfer', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
@ -47,7 +48,9 @@ describe('transfer', () => {
|
||||
await transferCommand({ from: 'local', to: destinationUrl });
|
||||
});
|
||||
|
||||
expect(mockDataTransfer.createRemoteStrapiDestinationProvider).toHaveBeenCalledWith(
|
||||
expect(
|
||||
mockDataTransfer.strapi.providers.createRemoteStrapiDestinationProvider
|
||||
).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
url: destinationUrl,
|
||||
})
|
||||
@ -61,7 +64,9 @@ describe('transfer', () => {
|
||||
await transferCommand({ from: 'local', to: destinationUrl });
|
||||
});
|
||||
|
||||
expect(mockDataTransfer.createRemoteStrapiDestinationProvider).toHaveBeenCalledWith(
|
||||
expect(
|
||||
mockDataTransfer.strapi.providers.createRemoteStrapiDestinationProvider
|
||||
).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
strategy: 'restore',
|
||||
})
|
||||
@ -72,7 +77,9 @@ describe('transfer', () => {
|
||||
await transferCommand({ from: 'local', to: destinationUrl });
|
||||
});
|
||||
|
||||
expect(mockDataTransfer.createRemoteStrapiDestinationProvider).toHaveBeenCalledWith(
|
||||
expect(
|
||||
mockDataTransfer.strapi.providers.createRemoteStrapiDestinationProvider
|
||||
).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
url: destinationUrl,
|
||||
})
|
||||
@ -84,7 +91,9 @@ describe('transfer', () => {
|
||||
await transferCommand({ from: 'local', to: destinationUrl });
|
||||
});
|
||||
|
||||
expect(mockDataTransfer.createRemoteStrapiDestinationProvider).toHaveBeenCalledWith(
|
||||
expect(
|
||||
mockDataTransfer.strapi.providers.createRemoteStrapiDestinationProvider
|
||||
).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
strategy: 'restore',
|
||||
})
|
||||
@ -96,7 +105,7 @@ describe('transfer', () => {
|
||||
await transferCommand({ from: 'local', to: destinationUrl });
|
||||
});
|
||||
|
||||
expect(mockDataTransfer.createLocalStrapiSourceProvider).toHaveBeenCalled();
|
||||
expect(mockDataTransfer.strapi.providers.createLocalStrapiSourceProvider).toHaveBeenCalled();
|
||||
expect(utils.createStrapiInstance).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
'use strict';
|
||||
|
||||
const {
|
||||
createLocalFileDestinationProvider,
|
||||
createLocalStrapiSourceProvider,
|
||||
createTransferEngine,
|
||||
} = require('@strapi/data-transfer');
|
||||
providers: { createLocalFileDestinationProvider },
|
||||
} = require('@strapi/data-transfer/lib/file');
|
||||
const {
|
||||
providers: { createLocalStrapiSourceProvider },
|
||||
} = require('@strapi/data-transfer/lib/strapi');
|
||||
const { createTransferEngine } = require('@strapi/data-transfer/lib/engine');
|
||||
const { isObject, isString, isFinite, toNumber } = require('lodash/fp');
|
||||
const fs = require('fs-extra');
|
||||
const chalk = require('chalk');
|
||||
|
||||
@ -1,13 +1,17 @@
|
||||
'use strict';
|
||||
|
||||
const {
|
||||
createLocalFileSourceProvider,
|
||||
createLocalStrapiDestinationProvider,
|
||||
providers: { createLocalFileSourceProvider },
|
||||
} = require('@strapi/data-transfer/lib/file');
|
||||
const {
|
||||
providers: { createLocalStrapiDestinationProvider, DEFAULT_CONFLICT_STRATEGY },
|
||||
} = require('@strapi/data-transfer/lib/strapi');
|
||||
const {
|
||||
createTransferEngine,
|
||||
DEFAULT_VERSION_STRATEGY,
|
||||
DEFAULT_SCHEMA_STRATEGY,
|
||||
DEFAULT_CONFLICT_STRATEGY,
|
||||
} = require('@strapi/data-transfer');
|
||||
} = require('@strapi/data-transfer/lib/engine');
|
||||
|
||||
const { isObject } = require('lodash/fp');
|
||||
const path = require('path');
|
||||
|
||||
@ -43,6 +47,7 @@ module.exports = async (opts) => {
|
||||
async getStrapi() {
|
||||
return strapiInstance;
|
||||
},
|
||||
autoDestroy: false,
|
||||
strategy: opts.conflictStrategy || DEFAULT_CONFLICT_STRATEGY,
|
||||
restore: {
|
||||
entities: { exclude: DEFAULT_IGNORED_CONTENT_TYPES },
|
||||
@ -107,7 +112,9 @@ module.exports = async (opts) => {
|
||||
}
|
||||
|
||||
// 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());
|
||||
await strapiInstance.telemetry.send('didDEITSProcessFinish', getTelemetryPayload());
|
||||
await strapiInstance.destroy();
|
||||
|
||||
process.exit(0);
|
||||
};
|
||||
|
||||
|
||||
@ -1,12 +1,9 @@
|
||||
'use strict';
|
||||
|
||||
const { createTransferEngine } = require('@strapi/data-transfer/lib/engine');
|
||||
const {
|
||||
createRemoteStrapiDestinationProvider,
|
||||
createLocalStrapiSourceProvider,
|
||||
createTransferEngine,
|
||||
// TODO: we need to solve this issue with typescript modules
|
||||
// eslint-disable-next-line import/no-unresolved, node/no-missing-require
|
||||
} = require('@strapi/data-transfer');
|
||||
providers: { createRemoteStrapiDestinationProvider, createLocalStrapiSourceProvider },
|
||||
} = require('@strapi/data-transfer/lib/strapi');
|
||||
const { isObject } = require('lodash/fp');
|
||||
const chalk = require('chalk');
|
||||
|
||||
|
||||
45
yarn.lock
45
yarn.lock
@ -6353,6 +6353,20 @@
|
||||
"@types/koa-compose" "*"
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/koa@2.13.1":
|
||||
version "2.13.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/koa/-/koa-2.13.1.tgz#e29877a6b5ad3744ab1024f6ec75b8cbf6ec45db"
|
||||
integrity sha512-Qbno7FWom9nNqu0yHZ6A0+RWt4mrYBhw3wpBAQ3+IuzGcLlfeYkzZrnMq5wsxulN2np8M4KKeUpTodsOsSad5Q==
|
||||
dependencies:
|
||||
"@types/accepts" "*"
|
||||
"@types/content-disposition" "*"
|
||||
"@types/cookies" "*"
|
||||
"@types/http-assert" "*"
|
||||
"@types/http-errors" "*"
|
||||
"@types/keygrip" "*"
|
||||
"@types/koa-compose" "*"
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/koa__cors@^3.0.1":
|
||||
version "3.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/koa__cors/-/koa__cors-3.3.0.tgz#2986b320d3d7ddf05c4e2e472b25a321cb16bd3b"
|
||||
@ -15530,35 +15544,6 @@ koa@2.13.4, koa@^2.13.4:
|
||||
type-is "^1.6.16"
|
||||
vary "^1.1.2"
|
||||
|
||||
koa@2.14.1:
|
||||
version "2.14.1"
|
||||
resolved "https://registry.yarnpkg.com/koa/-/koa-2.14.1.tgz#defb9589297d8eb1859936e777f3feecfc26925c"
|
||||
integrity sha512-USJFyZgi2l0wDgqkfD27gL4YGno7TfUkcmOe6UOLFOVuN+J7FwnNu4Dydl4CUQzraM1lBAiGed0M9OVJoT0Kqw==
|
||||
dependencies:
|
||||
accepts "^1.3.5"
|
||||
cache-content-type "^1.0.0"
|
||||
content-disposition "~0.5.2"
|
||||
content-type "^1.0.4"
|
||||
cookies "~0.8.0"
|
||||
debug "^4.3.2"
|
||||
delegates "^1.0.0"
|
||||
depd "^2.0.0"
|
||||
destroy "^1.0.4"
|
||||
encodeurl "^1.0.2"
|
||||
escape-html "^1.0.3"
|
||||
fresh "~0.5.2"
|
||||
http-assert "^1.3.0"
|
||||
http-errors "^1.6.3"
|
||||
is-generator-function "^1.0.7"
|
||||
koa-compose "^4.1.0"
|
||||
koa-convert "^2.0.0"
|
||||
on-finished "^2.3.0"
|
||||
only "~0.0.2"
|
||||
parseurl "^1.3.2"
|
||||
statuses "^1.5.0"
|
||||
type-is "^1.6.16"
|
||||
vary "^1.1.2"
|
||||
|
||||
kuler@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/kuler/-/kuler-2.0.0.tgz#e2c570a3800388fb44407e851531c1d670b061b3"
|
||||
@ -18296,6 +18281,8 @@ path-case@^2.1.0:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/path-case/-/path-case-2.1.1.tgz#94b8037c372d3fe2906e465bb45e25d226e8eea5"
|
||||
integrity sha1-lLgDfDctP+KQbkZbtF4l0ibo7qU=
|
||||
dependencies:
|
||||
no-case "^2.2.0"
|
||||
|
||||
path-dirname@^1.0.0:
|
||||
version "1.0.2"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user