mirror of
https://github.com/strapi/strapi.git
synced 2025-09-20 14:00:48 +00:00
Merge branch 'features/deits' into deits/import-assets
This commit is contained in:
commit
e177ac32a0
19
.eslintrc.back.type-definitions.js
Normal file
19
.eslintrc.back.type-definitions.js
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
parserOptions: {
|
||||||
|
project: './tsconfig.eslint.json',
|
||||||
|
},
|
||||||
|
plugins: ['@typescript-eslint'],
|
||||||
|
// TODO: This should be turned on but causes hundreds of violations in .d.ts files throughout Strapi that would need to be fixed
|
||||||
|
// extends: ['@strapi/eslint-config/typescript'],
|
||||||
|
globals: {
|
||||||
|
strapi: false,
|
||||||
|
},
|
||||||
|
// Instead of extending (which includes values that interfere with this configuration), only take the rules field
|
||||||
|
rules: {
|
||||||
|
...require('./.eslintrc.back.js').rules,
|
||||||
|
'import/no-extraneous-dependencies': ['error', { devDependencies: ['**/*.test.ts'] }],
|
||||||
|
},
|
||||||
|
};
|
@ -2,11 +2,11 @@
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
parser: '@typescript-eslint/parser',
|
parser: '@typescript-eslint/parser',
|
||||||
|
parserOptions: {
|
||||||
|
project: './tsconfig.eslint.json',
|
||||||
|
},
|
||||||
plugins: ['@typescript-eslint'],
|
plugins: ['@typescript-eslint'],
|
||||||
/**
|
extends: ['@strapi/eslint-config/typescript'],
|
||||||
* TODO: this should extend @strapi/eslint-config/typescript but doing so requires configuring parserOption.project, which requires tsconfig.json configuration
|
|
||||||
*/
|
|
||||||
// extends: ['plugin:@typescript-eslint/recommended'],
|
|
||||||
globals: {
|
globals: {
|
||||||
strapi: false,
|
strapi: false,
|
||||||
},
|
},
|
||||||
@ -14,5 +14,22 @@ module.exports = {
|
|||||||
rules: {
|
rules: {
|
||||||
...require('./.eslintrc.back.js').rules,
|
...require('./.eslintrc.back.js').rules,
|
||||||
'import/no-extraneous-dependencies': ['error', { devDependencies: ['**/*.test.ts'] }],
|
'import/no-extraneous-dependencies': ['error', { devDependencies: ['**/*.test.ts'] }],
|
||||||
|
// TODO: The following rules from @strapi/eslint-config/typescript are disabled because they're causing problems we need to solve or fix
|
||||||
|
// to be solved in configuration
|
||||||
|
'node/no-unsupported-features/es-syntax': 'off',
|
||||||
|
'import/prefer-default-export': 'off',
|
||||||
|
'node/no-missing-import': 'off',
|
||||||
|
'@typescript-eslint/brace-style': 'off', // TODO: fix conflict with prettier/prettier in data-transfer/engine/index.ts
|
||||||
|
// to be cleaned up throughout codebase (too many to fix at the moment)
|
||||||
|
'@typescript-eslint/no-use-before-define': 'warn',
|
||||||
},
|
},
|
||||||
|
// Disable only for tests
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
files: ['**.test.ts'],
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/ban-ts-comment': 'warn', // as long as javascript is allowed in our codebase, we want to test erroneous typescript usage
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
@ -22,10 +22,17 @@ module.exports = {
|
|||||||
// Backend typescript
|
// Backend typescript
|
||||||
{
|
{
|
||||||
files: ['packages/**/*.ts', 'test/**/*.ts', 'scripts/**/*.ts', 'jest.*.ts'],
|
files: ['packages/**/*.ts', 'test/**/*.ts', 'scripts/**/*.ts', 'jest.*.ts'],
|
||||||
excludedFiles: frontPaths,
|
excludedFiles: [...frontPaths, '**/*.d.ts'],
|
||||||
...require('./.eslintrc.back.typescript.js'),
|
...require('./.eslintrc.back.typescript.js'),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Type definitions
|
||||||
|
{
|
||||||
|
files: ['packages/**/*.d.ts', 'test/**/*.d.ts', 'scripts/**/*.d.ts'],
|
||||||
|
excludedFiles: frontPaths,
|
||||||
|
...require('./.eslintrc.back.type-definitions.js'),
|
||||||
|
},
|
||||||
|
|
||||||
// Frontend
|
// Frontend
|
||||||
{
|
{
|
||||||
files: frontPaths,
|
files: frontPaths,
|
||||||
|
@ -29,7 +29,8 @@
|
|||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"prepare": "husky install",
|
"prepare": "husky install",
|
||||||
"setup": "yarn && yarn build",
|
"setup": "yarn clean && yarn && yarn build",
|
||||||
|
"clean": "lerna run --stream clean --no-private",
|
||||||
"watch": "lerna run --stream watch --no-private",
|
"watch": "lerna run --stream watch --no-private",
|
||||||
"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",
|
||||||
|
@ -130,7 +130,7 @@ export const extendExpectForDataTransferTests = () => {
|
|||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
pass: true,
|
pass: true,
|
||||||
message: () => `Expected engine not to be valid`,
|
message: () => 'Expected engine not to be valid',
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
toHaveSourceStagesCalledTimes(provider: ISourceProvider, times: number) {
|
toHaveSourceStagesCalledTimes(provider: ISourceProvider, times: number) {
|
||||||
@ -145,6 +145,7 @@ export const extendExpectForDataTransferTests = () => {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (missing.length) {
|
if (missing.length) {
|
||||||
@ -156,7 +157,7 @@ export const extendExpectForDataTransferTests = () => {
|
|||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
pass: true,
|
pass: true,
|
||||||
message: () => `Expected source provider not to have all stages called`,
|
message: () => 'Expected source provider not to have all stages called',
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
toHaveDestinationStagesCalledTimes(provider: IDestinationProvider, times: number) {
|
toHaveDestinationStagesCalledTimes(provider: IDestinationProvider, times: number) {
|
||||||
@ -170,6 +171,8 @@ export const extendExpectForDataTransferTests = () => {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (missing.length) {
|
if (missing.length) {
|
||||||
@ -183,7 +186,7 @@ export const extendExpectForDataTransferTests = () => {
|
|||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
pass: true,
|
pass: true,
|
||||||
message: () => `Expected destination provider not to have all stages called`,
|
message: () => 'Expected destination provider not to have all stages called',
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
toBeValidSourceProvider(provider: ISourceProvider) {
|
toBeValidSourceProvider(provider: ISourceProvider) {
|
||||||
@ -199,7 +202,7 @@ export const extendExpectForDataTransferTests = () => {
|
|||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
pass: true,
|
pass: true,
|
||||||
message: () => `Expected source provider not to be valid`,
|
message: () => 'Expected source provider not to be valid',
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
toBeValidDestinationProvider(provider: IDestinationProvider) {
|
toBeValidDestinationProvider(provider: IDestinationProvider) {
|
||||||
@ -215,7 +218,7 @@ export const extendExpectForDataTransferTests = () => {
|
|||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
pass: true,
|
pass: true,
|
||||||
message: () => `Expected destination provider not to be valid`,
|
message: () => 'Expected destination provider not to be valid',
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -4,25 +4,25 @@ import { EncryptionStrategy, Strategies, Algorithm } from '../../types';
|
|||||||
// different key values depending on algorithm chosen
|
// different key values depending on algorithm chosen
|
||||||
const getDecryptionStrategy = (algorithm: Algorithm): EncryptionStrategy => {
|
const getDecryptionStrategy = (algorithm: Algorithm): EncryptionStrategy => {
|
||||||
const strategies: Strategies = {
|
const strategies: Strategies = {
|
||||||
'aes-128-ecb': (key: string): Cipher => {
|
'aes-128-ecb'(key: string): Cipher {
|
||||||
const hashedKey = scryptSync(key, '', 16);
|
const hashedKey = scryptSync(key, '', 16);
|
||||||
const initVector: BinaryLike | null = null;
|
const initVector: BinaryLike | null = null;
|
||||||
const securityKey: CipherKey = hashedKey;
|
const securityKey: CipherKey = hashedKey;
|
||||||
return createDecipheriv(algorithm, securityKey, initVector);
|
return createDecipheriv(algorithm, securityKey, initVector);
|
||||||
},
|
},
|
||||||
aes128: (key: string): Cipher => {
|
aes128(key: string): Cipher {
|
||||||
const hashedKey = scryptSync(key, '', 32);
|
const hashedKey = scryptSync(key, '', 32);
|
||||||
const initVector: BinaryLike | null = hashedKey.slice(16);
|
const initVector: BinaryLike | null = hashedKey.slice(16);
|
||||||
const securityKey: CipherKey = hashedKey.slice(0, 16);
|
const securityKey: CipherKey = hashedKey.slice(0, 16);
|
||||||
return createDecipheriv(algorithm, securityKey, initVector);
|
return createDecipheriv(algorithm, securityKey, initVector);
|
||||||
},
|
},
|
||||||
aes192: (key: string): Cipher => {
|
aes192(key: string): Cipher {
|
||||||
const hashedKey = scryptSync(key, '', 40);
|
const hashedKey = scryptSync(key, '', 40);
|
||||||
const initVector: BinaryLike | null = hashedKey.slice(24);
|
const initVector: BinaryLike | null = hashedKey.slice(24);
|
||||||
const securityKey: CipherKey = hashedKey.slice(0, 24);
|
const securityKey: CipherKey = hashedKey.slice(0, 24);
|
||||||
return createDecipheriv(algorithm, securityKey, initVector);
|
return createDecipheriv(algorithm, securityKey, initVector);
|
||||||
},
|
},
|
||||||
aes256: (key: string): Cipher => {
|
aes256(key: string): Cipher {
|
||||||
const hashedKey = scryptSync(key, '', 48);
|
const hashedKey = scryptSync(key, '', 48);
|
||||||
const initVector: BinaryLike | null = hashedKey.slice(32);
|
const initVector: BinaryLike | null = hashedKey.slice(32);
|
||||||
const securityKey: CipherKey = hashedKey.slice(0, 32);
|
const securityKey: CipherKey = hashedKey.slice(0, 32);
|
||||||
|
@ -4,25 +4,25 @@ import { EncryptionStrategy, Strategies, Algorithm } from '../../types';
|
|||||||
// different key values depending on algorithm chosen
|
// different key values depending on algorithm chosen
|
||||||
const getEncryptionStrategy = (algorithm: Algorithm): EncryptionStrategy => {
|
const getEncryptionStrategy = (algorithm: Algorithm): EncryptionStrategy => {
|
||||||
const strategies: Strategies = {
|
const strategies: Strategies = {
|
||||||
'aes-128-ecb': (key: string): Cipher => {
|
'aes-128-ecb'(key: string): Cipher {
|
||||||
const hashedKey = scryptSync(key, '', 16);
|
const hashedKey = scryptSync(key, '', 16);
|
||||||
const initVector: BinaryLike | null = null;
|
const initVector: BinaryLike | null = null;
|
||||||
const securityKey: CipherKey = hashedKey;
|
const securityKey: CipherKey = hashedKey;
|
||||||
return createCipheriv(algorithm, securityKey, initVector);
|
return createCipheriv(algorithm, securityKey, initVector);
|
||||||
},
|
},
|
||||||
aes128: (key: string): Cipher => {
|
aes128(key: string): Cipher {
|
||||||
const hashedKey = scryptSync(key, '', 32);
|
const hashedKey = scryptSync(key, '', 32);
|
||||||
const initVector: BinaryLike | null = hashedKey.slice(16);
|
const initVector: BinaryLike | null = hashedKey.slice(16);
|
||||||
const securityKey: CipherKey = hashedKey.slice(0, 16);
|
const securityKey: CipherKey = hashedKey.slice(0, 16);
|
||||||
return createCipheriv(algorithm, securityKey, initVector);
|
return createCipheriv(algorithm, securityKey, initVector);
|
||||||
},
|
},
|
||||||
aes192: (key: string): Cipher => {
|
aes192(key: string): Cipher {
|
||||||
const hashedKey = scryptSync(key, '', 40);
|
const hashedKey = scryptSync(key, '', 40);
|
||||||
const initVector: BinaryLike | null = hashedKey.slice(24);
|
const initVector: BinaryLike | null = hashedKey.slice(24);
|
||||||
const securityKey: CipherKey = hashedKey.slice(0, 24);
|
const securityKey: CipherKey = hashedKey.slice(0, 24);
|
||||||
return createCipheriv(algorithm, securityKey, initVector);
|
return createCipheriv(algorithm, securityKey, initVector);
|
||||||
},
|
},
|
||||||
aes256: (key: string): Cipher => {
|
aes256(key: string): Cipher {
|
||||||
const hashedKey = scryptSync(key, '', 48);
|
const hashedKey = scryptSync(key, '', 48);
|
||||||
const initVector: BinaryLike | null = hashedKey.slice(32);
|
const initVector: BinaryLike | null = hashedKey.slice(32);
|
||||||
const securityKey: CipherKey = hashedKey.slice(0, 32);
|
const securityKey: CipherKey = hashedKey.slice(0, 32);
|
||||||
|
@ -9,7 +9,6 @@ import type {
|
|||||||
IEntity,
|
IEntity,
|
||||||
ILink,
|
ILink,
|
||||||
ISourceProvider,
|
ISourceProvider,
|
||||||
ITransferEngine,
|
|
||||||
ITransferEngineOptions,
|
ITransferEngineOptions,
|
||||||
} from '../../../types';
|
} from '../../../types';
|
||||||
import {
|
import {
|
||||||
@ -393,7 +392,7 @@ describe('Transfer engine', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('progressStream', () => {
|
describe('progressStream', () => {
|
||||||
test(`emits 'progress' events`, async () => {
|
test("emits 'progress' events", async () => {
|
||||||
const source = createSource();
|
const source = createSource();
|
||||||
const engine = createTransferEngine(source, completeDestination, defaultOptions);
|
const engine = createTransferEngine(source, completeDestination, defaultOptions);
|
||||||
|
|
||||||
@ -412,8 +411,8 @@ describe('Transfer engine', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// TODO: to implement these, the mocked streams need to be improved
|
// TODO: to implement these, the mocked streams need to be improved
|
||||||
test.todo(`emits 'start' events`);
|
test.todo("emits 'start' events");
|
||||||
test.todo(`emits 'complete' events`);
|
test.todo("emits 'complete' events");
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('integrity checks', () => {
|
describe('integrity checks', () => {
|
||||||
|
@ -48,7 +48,7 @@ describe('Local File Destination Provider - Utils', () => {
|
|||||||
const tarEntryStream = createTarEntryStream(archive, pathFactory, maxSize);
|
const tarEntryStream = createTarEntryStream(archive, pathFactory, maxSize);
|
||||||
|
|
||||||
const write = async () =>
|
const write = async () =>
|
||||||
await new Promise((resolve, reject) => {
|
new Promise((resolve, reject) => {
|
||||||
tarEntryStream.on('finish', resolve);
|
tarEntryStream.on('finish', resolve);
|
||||||
tarEntryStream.on('error', reject);
|
tarEntryStream.on('error', reject);
|
||||||
tarEntryStream.write(chunk);
|
tarEntryStream.write(chunk);
|
||||||
@ -64,13 +64,13 @@ describe('Local File Destination Provider - Utils', () => {
|
|||||||
const tarEntryStream = createTarEntryStream(archive, pathFactory, maxSize);
|
const tarEntryStream = createTarEntryStream(archive, pathFactory, maxSize);
|
||||||
|
|
||||||
const write = async () =>
|
const write = async () =>
|
||||||
await new Promise((resolve, reject) => {
|
new Promise((resolve, reject) => {
|
||||||
tarEntryStream.on('finish', resolve);
|
tarEntryStream.on('finish', resolve);
|
||||||
tarEntryStream.on('error', reject);
|
tarEntryStream.on('error', reject);
|
||||||
tarEntryStream.write(chunk);
|
tarEntryStream.write(chunk);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(write).resolves;
|
expect(write()).resolves.not.toBe(null);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,12 +1,3 @@
|
|||||||
import type {
|
|
||||||
IAsset,
|
|
||||||
IDestinationProvider,
|
|
||||||
IDestinationProviderTransferResults,
|
|
||||||
IMetadata,
|
|
||||||
ProviderType,
|
|
||||||
Stream,
|
|
||||||
} from '../../../types';
|
|
||||||
|
|
||||||
import fs from 'fs-extra';
|
import fs from 'fs-extra';
|
||||||
import tar from 'tar-stream';
|
import tar from 'tar-stream';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
@ -16,20 +7,26 @@ import { stringer } from 'stream-json/jsonl/Stringer';
|
|||||||
import { chain, Writable } from 'stream-chain';
|
import { chain, Writable } from 'stream-chain';
|
||||||
|
|
||||||
import { createEncryptionCipher } from '../../encryption/encrypt';
|
import { createEncryptionCipher } from '../../encryption/encrypt';
|
||||||
|
import type {
|
||||||
|
IAsset,
|
||||||
|
IDestinationProvider,
|
||||||
|
IDestinationProviderTransferResults,
|
||||||
|
IMetadata,
|
||||||
|
ProviderType,
|
||||||
|
Stream,
|
||||||
|
} from '../../../types';
|
||||||
import { createFilePathFactory, createTarEntryStream } from './utils';
|
import { createFilePathFactory, createTarEntryStream } from './utils';
|
||||||
|
|
||||||
export interface ILocalFileDestinationProviderOptions {
|
export interface ILocalFileDestinationProviderOptions {
|
||||||
// Encryption
|
|
||||||
encryption: {
|
encryption: {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
key?: string;
|
key?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Compression
|
|
||||||
compression: {
|
compression: {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
// File
|
|
||||||
file: {
|
file: {
|
||||||
path: string;
|
path: string;
|
||||||
maxSize?: number;
|
maxSize?: number;
|
||||||
@ -51,12 +48,16 @@ export const createLocalFileDestinationProvider = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
class LocalFileDestinationProvider implements IDestinationProvider {
|
class LocalFileDestinationProvider implements IDestinationProvider {
|
||||||
name: string = 'destination::local-file';
|
name = 'destination::local-file';
|
||||||
|
|
||||||
type: ProviderType = 'destination';
|
type: ProviderType = 'destination';
|
||||||
|
|
||||||
options: ILocalFileDestinationProviderOptions;
|
options: ILocalFileDestinationProviderOptions;
|
||||||
|
|
||||||
results: ILocalFileDestinationProviderTransferResults = {};
|
results: ILocalFileDestinationProviderTransferResults = {};
|
||||||
|
|
||||||
#providersMetadata: { source?: IMetadata; destination?: IMetadata } = {};
|
#providersMetadata: { source?: IMetadata; destination?: IMetadata } = {};
|
||||||
|
|
||||||
#archive: { stream?: tar.Pack; pipeline?: Stream } = {};
|
#archive: { stream?: tar.Pack; pipeline?: Stream } = {};
|
||||||
|
|
||||||
constructor(options: ILocalFileDestinationProviderOptions) {
|
constructor(options: ILocalFileDestinationProviderOptions) {
|
||||||
@ -66,17 +67,17 @@ class LocalFileDestinationProvider implements IDestinationProvider {
|
|||||||
get #archivePath() {
|
get #archivePath() {
|
||||||
const { encryption, compression, file } = this.options;
|
const { encryption, compression, file } = this.options;
|
||||||
|
|
||||||
let path = `${file.path}.tar`;
|
let filePath = `${file.path}.tar`;
|
||||||
|
|
||||||
if (compression.enabled) {
|
if (compression.enabled) {
|
||||||
path += '.gz';
|
filePath += '.gz';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (encryption.enabled) {
|
if (encryption.enabled) {
|
||||||
path += '.enc';
|
filePath += '.enc';
|
||||||
}
|
}
|
||||||
|
|
||||||
return path;
|
return filePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
setMetadata(target: ProviderType, metadata: IMetadata): IDestinationProvider {
|
setMetadata(target: ProviderType, metadata: IMetadata): IDestinationProvider {
|
||||||
|
@ -8,7 +8,7 @@ import tar from 'tar-stream';
|
|||||||
*/
|
*/
|
||||||
export const createFilePathFactory =
|
export const createFilePathFactory =
|
||||||
(type: string) =>
|
(type: string) =>
|
||||||
(fileIndex: number = 0): string => {
|
(fileIndex = 0): string => {
|
||||||
return path.join(
|
return path.join(
|
||||||
// "{type}" directory
|
// "{type}" directory
|
||||||
type,
|
type,
|
||||||
@ -20,7 +20,7 @@ export const createFilePathFactory =
|
|||||||
export const createTarEntryStream = (
|
export const createTarEntryStream = (
|
||||||
archive: tar.Pack,
|
archive: tar.Pack,
|
||||||
pathFactory: (index?: number) => string,
|
pathFactory: (index?: number) => string,
|
||||||
maxSize: number = 2.56e8
|
maxSize = 2.56e8
|
||||||
) => {
|
) => {
|
||||||
let fileIndex = 0;
|
let fileIndex = 0;
|
||||||
let buffer = '';
|
let buffer = '';
|
||||||
@ -30,7 +30,8 @@ export const createTarEntryStream = (
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const name = pathFactory(fileIndex++);
|
fileIndex += 1;
|
||||||
|
const name = pathFactory(fileIndex);
|
||||||
const size = buffer.length;
|
const size = buffer.length;
|
||||||
|
|
||||||
await new Promise<void>((resolve, reject) => {
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
@ -1,18 +1,17 @@
|
|||||||
import type { Readable } from 'stream';
|
import type { Readable } from 'stream';
|
||||||
|
|
||||||
import type { IMetadata, ISourceProvider, ProviderType } from '../../../types';
|
|
||||||
|
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import zip from 'zlib';
|
import zip from 'zlib';
|
||||||
import tar from 'tar';
|
import tar from 'tar';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { keyBy } from 'lodash/fp';
|
import { keyBy } from 'lodash/fp';
|
||||||
import { chain } from 'stream-chain';
|
import { chain } from 'stream-chain';
|
||||||
import { pipeline, PassThrough, Readable as ReadStream } from 'stream';
|
import { pipeline, PassThrough } from 'stream';
|
||||||
import { parser } from 'stream-json/jsonl/Parser';
|
import { parser } from 'stream-json/jsonl/Parser';
|
||||||
|
import type { IMetadata, ISourceProvider, ProviderType } from '../../../types';
|
||||||
|
|
||||||
import { createDecryptionCipher } from '../../encryption';
|
|
||||||
import { collect } from '../../utils';
|
import { collect } from '../../utils';
|
||||||
|
import { createDecryptionCipher } from '../../encryption';
|
||||||
|
|
||||||
type StreamItemArray = Parameters<typeof chain>[0];
|
type StreamItemArray = Parameters<typeof chain>[0];
|
||||||
|
|
||||||
@ -25,25 +24,18 @@ const METADATA_FILE_PATH = 'metadata.json';
|
|||||||
* Provider options
|
* Provider options
|
||||||
*/
|
*/
|
||||||
export interface ILocalFileSourceProviderOptions {
|
export interface ILocalFileSourceProviderOptions {
|
||||||
/**
|
file: {
|
||||||
* Path to the backup archive
|
path: string;
|
||||||
*/
|
};
|
||||||
backupFilePath: string;
|
|
||||||
|
|
||||||
/**
|
encryption: {
|
||||||
* Whether the backup data is encrypted or not
|
enabled: boolean;
|
||||||
*/
|
key?: string;
|
||||||
encrypted?: boolean;
|
};
|
||||||
|
|
||||||
/**
|
compression: {
|
||||||
* Encryption key used to decrypt the encrypted data (if necessary)
|
enabled: boolean;
|
||||||
*/
|
};
|
||||||
encryptionKey?: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether the backup data is compressed or not
|
|
||||||
*/
|
|
||||||
compressed?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createLocalFileSourceProvider = (options: ILocalFileSourceProviderOptions) => {
|
export const createLocalFileSourceProvider = (options: ILocalFileSourceProviderOptions) => {
|
||||||
@ -52,14 +44,17 @@ export const createLocalFileSourceProvider = (options: ILocalFileSourceProviderO
|
|||||||
|
|
||||||
class LocalFileSourceProvider implements ISourceProvider {
|
class LocalFileSourceProvider implements ISourceProvider {
|
||||||
type: ProviderType = 'source';
|
type: ProviderType = 'source';
|
||||||
name: string = 'source::local-file';
|
|
||||||
|
name = 'source::local-file';
|
||||||
|
|
||||||
options: ILocalFileSourceProviderOptions;
|
options: ILocalFileSourceProviderOptions;
|
||||||
|
|
||||||
constructor(options: ILocalFileSourceProviderOptions) {
|
constructor(options: ILocalFileSourceProviderOptions) {
|
||||||
this.options = options;
|
this.options = options;
|
||||||
|
|
||||||
if (this.options.encrypted && this.options.encryptionKey === undefined) {
|
const { encryption } = this.options;
|
||||||
|
|
||||||
|
if (encryption.enabled && encryption.key === undefined) {
|
||||||
throw new Error('Missing encryption key');
|
throw new Error('Missing encryption key');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -68,13 +63,13 @@ 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 provided path is correct, etc...)
|
||||||
*/
|
*/
|
||||||
bootstrap() {
|
bootstrap() {
|
||||||
const path = this.options.backupFilePath;
|
const { path: filePath } = this.options.file;
|
||||||
const isValidBackupPath = fs.existsSync(path);
|
const isValidBackupPath = fs.existsSync(filePath);
|
||||||
|
|
||||||
// Check if the provided path exists
|
// Check if the provided path exists
|
||||||
if (!isValidBackupPath) {
|
if (!isValidBackupPath) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Invalid backup file path provided. "${path}" does not exist on the filesystem.`
|
`Invalid backup file path provided. "${filePath}" does not exist on the filesystem.`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -117,13 +112,13 @@ class LocalFileSourceProvider implements ISourceProvider {
|
|||||||
[
|
[
|
||||||
inStream,
|
inStream,
|
||||||
new tar.Parse({
|
new tar.Parse({
|
||||||
filter(path, entry) {
|
filter(filePath, entry) {
|
||||||
if (entry.type !== 'File') {
|
if (entry.type !== 'File') {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const parts = path.split('/');
|
const parts = filePath.split('/');
|
||||||
return parts[0] === 'assets' && parts[1] == 'uploads';
|
return parts[0] === 'assets' && parts[1] === 'uploads';
|
||||||
},
|
},
|
||||||
onentry(entry) {
|
onentry(entry) {
|
||||||
const { path: filePath, size } = entry;
|
const { path: filePath, size } = entry;
|
||||||
@ -138,13 +133,17 @@ class LocalFileSourceProvider implements ISourceProvider {
|
|||||||
return outStream;
|
return outStream;
|
||||||
}
|
}
|
||||||
|
|
||||||
#getBackupStream(decompress: boolean = true) {
|
#getBackupStream() {
|
||||||
const path = this.options.backupFilePath;
|
const { file, encryption, compression } = this.options;
|
||||||
const readStream = fs.createReadStream(path);
|
|
||||||
const streams: StreamItemArray = [readStream];
|
|
||||||
|
|
||||||
// Handle decompression
|
const fileStream = fs.createReadStream(file.path);
|
||||||
if (decompress) {
|
const streams: StreamItemArray = [fileStream];
|
||||||
|
|
||||||
|
if (encryption.enabled && encryption.key) {
|
||||||
|
streams.push(createDecryptionCipher(encryption.key));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (compression.enabled) {
|
||||||
streams.push(zip.createGunzip());
|
streams.push(zip.createGunzip());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -152,7 +151,6 @@ class LocalFileSourceProvider implements ISourceProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#streamJsonlDirectory(directory: string) {
|
#streamJsonlDirectory(directory: string) {
|
||||||
const options = this.options;
|
|
||||||
const inStream = this.#getBackupStream();
|
const inStream = this.#getBackupStream();
|
||||||
|
|
||||||
const outStream = new PassThrough({ objectMode: true });
|
const outStream = new PassThrough({ objectMode: true });
|
||||||
@ -161,12 +159,12 @@ class LocalFileSourceProvider implements ISourceProvider {
|
|||||||
[
|
[
|
||||||
inStream,
|
inStream,
|
||||||
new tar.Parse({
|
new tar.Parse({
|
||||||
filter(path, entry) {
|
filter(filePath, entry) {
|
||||||
if (entry.type !== 'File') {
|
if (entry.type !== 'File') {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const parts = path.split('/');
|
const parts = filePath.split('/');
|
||||||
|
|
||||||
if (parts.length !== 2) {
|
if (parts.length !== 2) {
|
||||||
return false;
|
return false;
|
||||||
@ -176,22 +174,12 @@ class LocalFileSourceProvider implements ISourceProvider {
|
|||||||
},
|
},
|
||||||
|
|
||||||
onentry(entry) {
|
onentry(entry) {
|
||||||
const transforms = [];
|
const transforms = [
|
||||||
|
|
||||||
if (options.encrypted) {
|
|
||||||
transforms.push(createDecryptionCipher(options.encryptionKey!));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.compressed) {
|
|
||||||
transforms.push(zip.createGunzip());
|
|
||||||
}
|
|
||||||
|
|
||||||
transforms.push(
|
|
||||||
// JSONL parser to read the data chunks one by one (line by line)
|
// JSONL parser to read the data chunks one by one (line by line)
|
||||||
parser(),
|
parser(),
|
||||||
// The JSONL parser returns each line as key/value
|
// The JSONL parser returns each line as key/value
|
||||||
(line: { key: string; value: any }) => line.value
|
(line: { key: string; value: any }) => line.value,
|
||||||
);
|
];
|
||||||
|
|
||||||
entry
|
entry
|
||||||
// Pipe transforms
|
// Pipe transforms
|
||||||
@ -213,7 +201,7 @@ class LocalFileSourceProvider implements ISourceProvider {
|
|||||||
return outStream;
|
return outStream;
|
||||||
}
|
}
|
||||||
|
|
||||||
async #parseJSONFile<T extends {} = any>(
|
async #parseJSONFile<T extends Record<string, any> = any>(
|
||||||
fileStream: NodeJS.ReadableStream,
|
fileStream: NodeJS.ReadableStream,
|
||||||
filePath: string
|
filePath: string
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
@ -226,8 +214,8 @@ class LocalFileSourceProvider implements ISourceProvider {
|
|||||||
/**
|
/**
|
||||||
* Filter the parsed entries to only keep the one that matches the given filepath
|
* Filter the parsed entries to only keep the one that matches the given filepath
|
||||||
*/
|
*/
|
||||||
filter(path, entry) {
|
filter(entryPath, entry) {
|
||||||
return path === filePath && entry.type === 'File';
|
return entryPath === filePath && entry.type === 'File';
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -250,7 +238,7 @@ class LocalFileSourceProvider implements ISourceProvider {
|
|||||||
() => {
|
() => {
|
||||||
// If the promise hasn't been resolved and we've parsed all
|
// If the promise hasn't been resolved and we've parsed all
|
||||||
// the archive entries, then the file doesn't exist
|
// the archive entries, then the file doesn't exist
|
||||||
reject(`${filePath} not found in the archive stream`);
|
reject(new Error(`File "${filePath}" not found`));
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { deleteAllRecords } from '../restore';
|
import { deleteAllRecords, restoreConfigs } from '../restore';
|
||||||
import { getStrapiFactory, getContentTypes } from '../../test-utils';
|
import { getStrapiFactory, getContentTypes } from '../../test-utils';
|
||||||
|
|
||||||
const entities = [
|
const entities = [
|
||||||
@ -32,6 +32,10 @@ const entities = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
const deleteMany = jest.fn(async (uid: string) => ({
|
const deleteMany = jest.fn(async (uid: string) => ({
|
||||||
count: entities.filter((entity) => entity.contentType.uid === uid).length,
|
count: entities.filter((entity) => entity.contentType.uid === uid).length,
|
||||||
}));
|
}));
|
||||||
@ -41,6 +45,7 @@ const query = jest.fn(() => {
|
|||||||
deleteMany: jest.fn(() => ({
|
deleteMany: jest.fn(() => ({
|
||||||
count: 0,
|
count: 0,
|
||||||
})),
|
})),
|
||||||
|
create: jest.fn((data) => data),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -72,8 +77,66 @@ describe('Restore ', () => {
|
|||||||
|
|
||||||
const { count } = await deleteAllRecords(strapi, {
|
const { count } = await deleteAllRecords(strapi, {
|
||||||
/* @ts-ignore: disable-next-line */
|
/* @ts-ignore: disable-next-line */
|
||||||
contentTypes: [getContentTypes()['foo']],
|
contentTypes: [getContentTypes().foo],
|
||||||
});
|
});
|
||||||
expect(count).toBe(3);
|
expect(count).toBe(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Should add core store data', async () => {
|
||||||
|
const strapi = getStrapiFactory({
|
||||||
|
contentTypes: getContentTypes(),
|
||||||
|
db: {
|
||||||
|
query,
|
||||||
|
},
|
||||||
|
})();
|
||||||
|
const config = {
|
||||||
|
type: 'core-store',
|
||||||
|
value: {
|
||||||
|
key: 'test-key',
|
||||||
|
type: 'test-type',
|
||||||
|
environment: null,
|
||||||
|
tag: 'tag',
|
||||||
|
value: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const result = await restoreConfigs(strapi, config);
|
||||||
|
|
||||||
|
expect(strapi.db.query).toBeCalledTimes(1);
|
||||||
|
expect(strapi.db.query).toBeCalledWith('strapi::core-store');
|
||||||
|
expect(result.data).toMatchObject(config.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Should add webhook data', async () => {
|
||||||
|
const strapi = getStrapiFactory({
|
||||||
|
contentTypes: getContentTypes(),
|
||||||
|
db: {
|
||||||
|
query,
|
||||||
|
},
|
||||||
|
})();
|
||||||
|
const config = {
|
||||||
|
type: 'webhook',
|
||||||
|
value: {
|
||||||
|
id: 4,
|
||||||
|
name: 'christian',
|
||||||
|
url: 'https://facebook.com',
|
||||||
|
headers: { null: '' },
|
||||||
|
events: [
|
||||||
|
'entry.create',
|
||||||
|
'entry.update',
|
||||||
|
'entry.delete',
|
||||||
|
'entry.publish',
|
||||||
|
'entry.unpublish',
|
||||||
|
'media.create',
|
||||||
|
'media.update',
|
||||||
|
'media.delete',
|
||||||
|
],
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const result = await restoreConfigs(strapi, config);
|
||||||
|
|
||||||
|
expect(strapi.db.query).toBeCalledTimes(1);
|
||||||
|
expect(strapi.db.query).toBeCalledWith('webhook');
|
||||||
|
expect(result.data).toMatchObject(config.value);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
// import { createLogger } from '@strapi/logger';
|
// import { createLogger } from '@strapi/logger';
|
||||||
import type { IDestinationProvider, IMetadata, ProviderType } from '../../../types';
|
|
||||||
import { deleteAllRecords, DeleteOptions } from './restore';
|
|
||||||
|
|
||||||
|
import chalk from 'chalk';
|
||||||
import { Writable } from 'stream';
|
import { Writable } from 'stream';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import * as fse from 'fs-extra';
|
import * as fse from 'fs-extra';
|
||||||
|
import type { IConfiguration, IDestinationProvider, IMetadata, ProviderType } from '../../../types';
|
||||||
|
|
||||||
|
import { deleteAllRecords, DeleteOptions, restoreConfigs } from './restore';
|
||||||
import { mapSchemasValues } from '../../utils';
|
import { mapSchemasValues } from '../../utils';
|
||||||
|
|
||||||
export const VALID_STRATEGIES = ['restore', 'merge'];
|
export const VALID_STRATEGIES = ['restore', 'merge'];
|
||||||
@ -16,10 +17,6 @@ interface ILocalStrapiDestinationProviderOptions {
|
|||||||
strategy: 'restore' | 'merge';
|
strategy: 'restore' | 'merge';
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: getting some type errors with @strapi/logger that need to be resolved first
|
|
||||||
// const log = createLogger();
|
|
||||||
const log = console;
|
|
||||||
|
|
||||||
export const createLocalStrapiDestinationProvider = (
|
export const createLocalStrapiDestinationProvider = (
|
||||||
options: ILocalStrapiDestinationProviderOptions
|
options: ILocalStrapiDestinationProviderOptions
|
||||||
) => {
|
) => {
|
||||||
@ -27,10 +24,12 @@ export const createLocalStrapiDestinationProvider = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
class LocalStrapiDestinationProvider implements IDestinationProvider {
|
class LocalStrapiDestinationProvider implements IDestinationProvider {
|
||||||
name: string = 'destination::local-strapi';
|
name = 'destination::local-strapi';
|
||||||
|
|
||||||
type: ProviderType = 'destination';
|
type: ProviderType = 'destination';
|
||||||
|
|
||||||
options: ILocalStrapiDestinationProviderOptions;
|
options: ILocalStrapiDestinationProviderOptions;
|
||||||
|
|
||||||
strapi?: Strapi.Strapi;
|
strapi?: Strapi.Strapi;
|
||||||
|
|
||||||
constructor(options: ILocalStrapiDestinationProviderOptions) {
|
constructor(options: ILocalStrapiDestinationProviderOptions) {
|
||||||
@ -48,7 +47,7 @@ class LocalStrapiDestinationProvider implements IDestinationProvider {
|
|||||||
|
|
||||||
#validateOptions() {
|
#validateOptions() {
|
||||||
if (!VALID_STRATEGIES.includes(this.options.strategy)) {
|
if (!VALID_STRATEGIES.includes(this.options.strategy)) {
|
||||||
throw new Error('Invalid stategy ' + this.options.strategy);
|
throw new Error(`Invalid stategy ${this.options.strategy}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -56,7 +55,7 @@ class LocalStrapiDestinationProvider implements IDestinationProvider {
|
|||||||
if (!this.strapi) {
|
if (!this.strapi) {
|
||||||
throw new Error('Strapi instance not found');
|
throw new Error('Strapi instance not found');
|
||||||
}
|
}
|
||||||
return await deleteAllRecords(this.strapi, this.options.restore);
|
return deleteAllRecords(this.strapi, this.options.restore);
|
||||||
}
|
}
|
||||||
|
|
||||||
async beforeTransfer() {
|
async beforeTransfer() {
|
||||||
@ -117,4 +116,30 @@ class LocalStrapiDestinationProvider implements IDestinationProvider {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getConfigurationStream(): Promise<Writable> {
|
||||||
|
if (!this.strapi) {
|
||||||
|
throw new Error('Not able to stream Configurations. Strapi instance not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Writable({
|
||||||
|
objectMode: true,
|
||||||
|
write: async (config: IConfiguration<any>, _encoding, callback) => {
|
||||||
|
try {
|
||||||
|
if (this.options.strategy === 'restore' && this.strapi) {
|
||||||
|
await restoreConfigs(this.strapi, config);
|
||||||
|
}
|
||||||
|
callback();
|
||||||
|
} catch (error) {
|
||||||
|
callback(
|
||||||
|
new Error(
|
||||||
|
`Failed to import ${chalk.yellowBright(config.type)} (${chalk.greenBright(
|
||||||
|
config.value.id
|
||||||
|
)}`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import type { ContentTypeSchema } from '@strapi/strapi';
|
import type { ContentTypeSchema } from '@strapi/strapi';
|
||||||
|
import type { IConfiguration } from '../../../types';
|
||||||
|
|
||||||
export type DeleteOptions = {
|
export type DeleteOptions = {
|
||||||
contentTypes?: ContentTypeSchema[];
|
contentTypes?: ContentTypeSchema[];
|
||||||
@ -33,3 +34,28 @@ export const deleteAllRecords = async (strapi: Strapi.Strapi, deleteOptions?: De
|
|||||||
|
|
||||||
return { count };
|
return { count };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const restoreCoreStore = async (strapi: Strapi.Strapi, data: any) => {
|
||||||
|
return strapi.db.query('strapi::core-store').create({
|
||||||
|
data: {
|
||||||
|
...data,
|
||||||
|
value: JSON.stringify(data.value),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const restoreWebhooks = async (strapi: Strapi.Strapi, data: any) => {
|
||||||
|
return strapi.db.query('webhook').create({
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const restoreConfigs = async (strapi: Strapi.Strapi, config: IConfiguration) => {
|
||||||
|
if (config.type === 'core-store') {
|
||||||
|
return restoreCoreStore(strapi, config.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.type === 'webhook') {
|
||||||
|
return restoreWebhooks(strapi, config.value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import type { IEntity } from '../../../../types';
|
|
||||||
|
|
||||||
import { Readable, PassThrough } from 'stream';
|
import { Readable, PassThrough } from 'stream';
|
||||||
|
import type { IEntity } from '../../../../types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
collect,
|
collect,
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
|
import { Readable } from 'stream';
|
||||||
import type { IEntity } from '../../../../types';
|
import type { IEntity } from '../../../../types';
|
||||||
|
|
||||||
import { Readable } from 'stream';
|
|
||||||
|
|
||||||
import { collect, createMockedQueryBuilder, getStrapiFactory } from '../../../__tests__/test-utils';
|
import { collect, createMockedQueryBuilder, getStrapiFactory } from '../../../__tests__/test-utils';
|
||||||
import { createLocalStrapiSourceProvider } from '../';
|
import { createLocalStrapiSourceProvider } from '..';
|
||||||
|
|
||||||
describe('Local Strapi Source Provider', () => {
|
describe('Local Strapi Source Provider', () => {
|
||||||
describe('Bootstrap', () => {
|
describe('Bootstrap', () => {
|
||||||
|
@ -347,7 +347,7 @@ describe('Local Strapi Source Provider - Entities Streaming', () => {
|
|||||||
expect(populate).toEqual({});
|
expect(populate).toEqual({});
|
||||||
});
|
});
|
||||||
|
|
||||||
test(`Should return an empty object if there are components or dynamic zones but they doesn't contain relations`, () => {
|
test("Should return an empty object if there are components or dynamic zones but they doesn't contain relations", () => {
|
||||||
const strapi = getStrapiFactory({
|
const strapi = getStrapiFactory({
|
||||||
contentTypes: {
|
contentTypes: {
|
||||||
blog: {
|
blog: {
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { chain } from 'stream-chain';
|
import { chain } from 'stream-chain';
|
||||||
import { mapValues, pick } from 'lodash/fp';
|
|
||||||
import { Readable } from 'stream';
|
import { Readable } from 'stream';
|
||||||
|
|
||||||
import type { IMetadata, ISourceProvider, ProviderType } from '../../../types';
|
import type { IMetadata, ISourceProvider, ProviderType } from '../../../types';
|
||||||
@ -20,10 +19,12 @@ export const createLocalStrapiSourceProvider = (options: ILocalStrapiSourceProvi
|
|||||||
};
|
};
|
||||||
|
|
||||||
class LocalStrapiSourceProvider implements ISourceProvider {
|
class LocalStrapiSourceProvider implements ISourceProvider {
|
||||||
name: string = 'source::local-strapi';
|
name = 'source::local-strapi';
|
||||||
|
|
||||||
type: ProviderType = 'source';
|
type: ProviderType = 'source';
|
||||||
|
|
||||||
options: ILocalStrapiSourceProviderOptions;
|
options: ILocalStrapiSourceProviderOptions;
|
||||||
|
|
||||||
strapi?: Strapi.Strapi;
|
strapi?: Strapi.Strapi;
|
||||||
|
|
||||||
constructor(options: ILocalStrapiSourceProviderOptions) {
|
constructor(options: ILocalStrapiSourceProviderOptions) {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import type { ContentTypeSchema, GetAttributesValues, RelationsType } from '@strapi/strapi';
|
import type { ContentTypeSchema } from '@strapi/strapi';
|
||||||
|
|
||||||
import { Readable } from 'stream';
|
import { Readable } from 'stream';
|
||||||
import { castArray } from 'lodash/fp';
|
import { castArray } from 'lodash/fp';
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import type { RelationsType } from '@strapi/strapi';
|
import type { RelationsType } from '@strapi/strapi';
|
||||||
import type { ILink } from '../../../../types';
|
|
||||||
|
|
||||||
import { concat, set, isEmpty } from 'lodash/fp';
|
import { concat, set, isEmpty } from 'lodash/fp';
|
||||||
|
import type { ILink } from '../../../../types';
|
||||||
|
|
||||||
// TODO: Fix any typings when we'll have better Strapi types
|
// TODO: Fix any typings when we'll have better Strapi types
|
||||||
|
|
||||||
@ -49,7 +48,7 @@ export const parseEntityLinks = (entity: any, populate: any, schema: any, strapi
|
|||||||
.map(({ __component, ...item }: any) =>
|
.map(({ __component, ...item }: any) =>
|
||||||
parseEntityLinks(item, subPopulate.populate, strapi.components[__component], strapi)
|
parseEntityLinks(item, subPopulate.populate, strapi.components[__component], strapi)
|
||||||
)
|
)
|
||||||
.reduce((acc: any, links: any) => acc.concat(...links), []);
|
.reduce((acc: any, rlinks: any) => acc.concat(...rlinks), []);
|
||||||
|
|
||||||
links.push(...dzLinks);
|
links.push(...dzLinks);
|
||||||
}
|
}
|
||||||
@ -90,6 +89,7 @@ export const parseRelationLinks = ({ entity, schema, fieldName, value }: any): I
|
|||||||
const isMorphRelation = relation.startsWith('morph');
|
const isMorphRelation = relation.startsWith('morph');
|
||||||
const isCircularRelation = !isMorphRelation && target === schema.uid;
|
const isCircularRelation = !isMorphRelation && target === schema.uid;
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-nested-ternary
|
||||||
const kind: ILink['kind'] = isMorphRelation
|
const kind: ILink['kind'] = isMorphRelation
|
||||||
? // Polymorphic relations
|
? // Polymorphic relations
|
||||||
'relation.morph'
|
'relation.morph'
|
||||||
|
@ -4,12 +4,12 @@ import { jsonDiffs } from '../utils';
|
|||||||
|
|
||||||
const strategies = {
|
const strategies = {
|
||||||
// No diffs
|
// No diffs
|
||||||
exact: (diffs: Diff[]) => {
|
exact(diffs: Diff[]) {
|
||||||
return diffs;
|
return diffs;
|
||||||
},
|
},
|
||||||
|
|
||||||
// Diffs allowed on specific attributes properties
|
// Diffs allowed on specific attributes properties
|
||||||
strict: (diffs: Diff[]) => {
|
strict(diffs: Diff[]) {
|
||||||
const isIgnorableDiff = ({ path }: Diff) => {
|
const isIgnorableDiff = ({ path }: Diff) => {
|
||||||
return (
|
return (
|
||||||
path.length === 3 &&
|
path.length === 3 &&
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import type { Readable } from 'stream';
|
import type { Readable } from 'stream';
|
||||||
import type { Context, Diff } from '../types';
|
|
||||||
|
|
||||||
import { isArray, isObject, zip, isEqual, uniq, mapValues, pick } from 'lodash/fp';
|
import { isArray, isObject, zip, isEqual, uniq, mapValues, pick } from 'lodash/fp';
|
||||||
|
import type { Context, Diff } from '../types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Collect every entity in a Readable stream
|
* Collect every entity in a Readable stream
|
||||||
@ -24,6 +24,9 @@ export const jsonDiffs = (a: unknown, b: unknown, ctx: Context = createContext()
|
|||||||
const diffs: Diff[] = [];
|
const diffs: Diff[] = [];
|
||||||
const { path } = ctx;
|
const { path } = ctx;
|
||||||
|
|
||||||
|
const aType = typeof a;
|
||||||
|
const bType = typeof b;
|
||||||
|
|
||||||
// Define helpers
|
// Define helpers
|
||||||
|
|
||||||
const added = () => {
|
const added = () => {
|
||||||
@ -46,9 +49,6 @@ export const jsonDiffs = (a: unknown, b: unknown, ctx: Context = createContext()
|
|||||||
return diffs;
|
return diffs;
|
||||||
};
|
};
|
||||||
|
|
||||||
const aType = typeof a;
|
|
||||||
const bType = typeof b;
|
|
||||||
|
|
||||||
if (aType === 'undefined') {
|
if (aType === 'undefined') {
|
||||||
return added();
|
return added();
|
||||||
}
|
}
|
||||||
@ -66,7 +66,7 @@ export const jsonDiffs = (a: unknown, b: unknown, ctx: Context = createContext()
|
|||||||
|
|
||||||
diffs.push(...kDiffs);
|
diffs.push(...kDiffs);
|
||||||
|
|
||||||
k++;
|
k += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
return diffs;
|
return diffs;
|
||||||
|
@ -122,6 +122,13 @@ interface ICircularLink extends IDefaultLink {
|
|||||||
kind: 'relation.circular';
|
kind: 'relation.circular';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strapi configurations
|
||||||
|
*/
|
||||||
|
interface IConfiguration<T = unknown> {
|
||||||
|
type: 'core-store' | 'webhook';
|
||||||
|
value: T;
|
||||||
|
}
|
||||||
export interface IAsset {
|
export interface IAsset {
|
||||||
filename: string;
|
filename: string;
|
||||||
filepath: string;
|
filepath: string;
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
// FIXME
|
// FIXME
|
||||||
/* eslint-disable import/extensions */
|
/* eslint-disable import/extensions */
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
|
const path = require('path');
|
||||||
const resolveCwd = require('resolve-cwd');
|
const resolveCwd = require('resolve-cwd');
|
||||||
const { yellow } = require('chalk');
|
const { yellow } = require('chalk');
|
||||||
const { Command, Option } = require('commander');
|
const { Command, Option } = require('commander');
|
||||||
@ -316,14 +317,15 @@ program
|
|||||||
'path and filename to the Strapi export file you want to import'
|
'path and filename to the Strapi export file you want to import'
|
||||||
)
|
)
|
||||||
.addOption(
|
.addOption(
|
||||||
new Option('--key <string>', 'Provide encryption key in command instead of using a prompt')
|
new Option('-k, --key <string>', 'Provide encryption key in command instead of using a prompt')
|
||||||
)
|
)
|
||||||
.allowExcessArguments(false)
|
.allowExcessArguments(false)
|
||||||
.hook('preAction', async (thisCommand) => {
|
.hook('preAction', async (thisCommand) => {
|
||||||
const opts = thisCommand.opts();
|
const opts = thisCommand.opts();
|
||||||
|
const ext = path.extname(String(opts.file));
|
||||||
|
|
||||||
// check extension to guess if we should prompt for key
|
// check extension to guess if we should prompt for key
|
||||||
if (String(opts.file).endsWith('.enc')) {
|
if (ext === '.enc') {
|
||||||
if (!opts.key) {
|
if (!opts.key) {
|
||||||
const answers = await inquirer.prompt([
|
const answers = await inquirer.prompt([
|
||||||
{
|
{
|
||||||
|
@ -8,10 +8,15 @@ const {
|
|||||||
// eslint-disable-next-line import/no-unresolved, node/no-missing-require
|
// eslint-disable-next-line import/no-unresolved, node/no-missing-require
|
||||||
} = require('@strapi/data-transfer');
|
} = require('@strapi/data-transfer');
|
||||||
const { isObject } = require('lodash/fp');
|
const { isObject } = require('lodash/fp');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
const strapi = require('../../index');
|
const strapi = require('../../index');
|
||||||
const { buildTransferTable } = require('./utils');
|
const { buildTransferTable } = require('./utils');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {import('@strapi/data-transfer').ILocalFileSourceProviderOptions} ILocalFileSourceProviderOptions
|
||||||
|
*/
|
||||||
|
|
||||||
const logger = console;
|
const logger = console;
|
||||||
|
|
||||||
module.exports = async (opts) => {
|
module.exports = async (opts) => {
|
||||||
@ -20,14 +25,12 @@ module.exports = async (opts) => {
|
|||||||
logger.error('Could not parse arguments');
|
logger.error('Could not parse arguments');
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
const filename = opts.file;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* From strapi backup file
|
* From strapi backup file
|
||||||
*/
|
*/
|
||||||
const sourceOptions = {
|
const sourceOptions = getLocalFileSourceOptions(opts);
|
||||||
backupFilePath: filename,
|
|
||||||
};
|
|
||||||
const source = createLocalFileSourceProvider(sourceOptions);
|
const source = createLocalFileSourceProvider(sourceOptions);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -69,16 +72,50 @@ module.exports = async (opts) => {
|
|||||||
const engine = createTransferEngine(source, destination, engineOptions);
|
const engine = createTransferEngine(source, destination, engineOptions);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
logger.log('Starting import...');
|
logger.info('Starting import...');
|
||||||
|
|
||||||
const results = await engine.transfer();
|
const results = await engine.transfer();
|
||||||
const table = buildTransferTable(results.engine);
|
const table = buildTransferTable(results.engine);
|
||||||
logger.log(table.toString());
|
logger.info(table.toString());
|
||||||
|
|
||||||
logger.log('Import process has been completed successfully!');
|
logger.info('Import process has been completed successfully!');
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.log(`Import process failed unexpectedly: ${e.message}`);
|
logger.error(`Import process failed unexpectedly: ${e.message}`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Infer local file source provider options based on a given filename
|
||||||
|
*
|
||||||
|
* @param {{ file: string; key?: string }} opts
|
||||||
|
*
|
||||||
|
* @return {ILocalFileSourceProviderOptions}
|
||||||
|
*/
|
||||||
|
const getLocalFileSourceOptions = (opts) => {
|
||||||
|
/**
|
||||||
|
* @type {ILocalFileSourceProviderOptions}
|
||||||
|
*/
|
||||||
|
const options = {
|
||||||
|
file: { path: opts.file },
|
||||||
|
compression: { enabled: false },
|
||||||
|
encryption: { enabled: false },
|
||||||
|
};
|
||||||
|
|
||||||
|
const { extname, parse } = path;
|
||||||
|
|
||||||
|
let file = options.file.path;
|
||||||
|
|
||||||
|
if (extname(file) === '.enc') {
|
||||||
|
file = parse(file).name;
|
||||||
|
options.encryption = { enabled: true, key: opts.key };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extname(file) === '.gz') {
|
||||||
|
file = parse(file).name;
|
||||||
|
options.compression = { enabled: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
return options;
|
||||||
|
};
|
||||||
|
8
tsconfig.eslint.json
Normal file
8
tsconfig.eslint.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"incremental": true
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user