@strapi/strapi 4.8.2 → 4.9.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/strapi.js CHANGED
@@ -24,13 +24,10 @@ const { exitWith, ifOptions, assertUrlHasProtocol } = require('../lib/commands/u
24
24
  const {
25
25
  excludeOption,
26
26
  onlyOption,
27
+ throttleOption,
27
28
  validateExcludeOnly,
28
29
  } = require('../lib/commands/transfer/utils');
29
30
 
30
- process.on('SIGINT', () => {
31
- process.exit();
32
- });
33
-
34
31
  const checkCwdIsStrapiApp = (name) => {
35
32
  const logErrorAndExit = () => {
36
33
  console.log(
@@ -279,13 +276,12 @@ program
279
276
  .description('Transfer data from one source to another')
280
277
  .allowExcessArguments(false)
281
278
  .addOption(
282
- new Option('--from <sourceURL>', `URL of the remote Strapi instance to get data from`)
283
- .argParser(parseURL)
284
- .hideHelp() // Hidden until pull feature is released
285
- )
286
- .addOption(
287
- new Option('--from-token <token>', `Transfer token for the remote Strapi source`).hideHelp() // Hidden until pull feature is released
279
+ new Option(
280
+ '--from <sourceURL>',
281
+ `URL of the remote Strapi instance to get data from`
282
+ ).argParser(parseURL)
288
283
  )
284
+ .addOption(new Option('--from-token <token>', `Transfer token for the remote Strapi source`))
289
285
  .addOption(
290
286
  new Option(
291
287
  '--to <destinationURL>',
@@ -296,7 +292,16 @@ program
296
292
  .addOption(forceOption)
297
293
  .addOption(excludeOption)
298
294
  .addOption(onlyOption)
295
+ .addOption(throttleOption)
299
296
  .hook('preAction', validateExcludeOnly)
297
+ .hook(
298
+ 'preAction',
299
+ ifOptions(
300
+ (opts) => !(opts.from || opts.to) || (opts.from && opts.to),
301
+ () =>
302
+ exitWith(1, 'Exactly one remote source (from) or destination (to) option must be provided')
303
+ )
304
+ )
300
305
  // If --from is used, validate the URL and token
301
306
  .hook(
302
307
  'preAction',
@@ -313,7 +318,7 @@ program
313
318
  },
314
319
  ]);
315
320
  if (!answers.fromToken?.length) {
316
- exitWith(1, 'No token entered, aborting transfer.');
321
+ exitWith(1, 'No token provided for remote source, aborting transfer.');
317
322
  }
318
323
  thisCommand.opts().fromToken = answers.fromToken;
319
324
  }
@@ -336,7 +341,7 @@ program
336
341
  },
337
342
  ]);
338
343
  if (!answers.toToken?.length) {
339
- exitWith(1, 'No token entered, aborting transfer.');
344
+ exitWith(1, 'No token provided for remote destination, aborting transfer.');
340
345
  }
341
346
  thisCommand.opts().toToken = answers.toToken;
342
347
  }
@@ -348,13 +353,6 @@ program
348
353
  }
349
354
  )
350
355
  )
351
- // .hook(
352
- // 'preAction',
353
- // ifOptions(
354
- // (opts) => !opts.from && !opts.to,
355
- // () => exitWith(1, 'At least one source (from) or destination (to) option must be provided')
356
- // )
357
- // )
358
356
  .action(getLocalScript('transfer/transfer'));
359
357
 
360
358
  // `$ strapi export`
@@ -375,6 +373,7 @@ program
375
373
  .addOption(new Option('-f, --file <file>', 'name to use for exported file (without extensions)'))
376
374
  .addOption(excludeOption)
377
375
  .addOption(onlyOption)
376
+ .addOption(throttleOption)
378
377
  .hook('preAction', validateExcludeOnly)
379
378
  .hook('preAction', promptEncryptionKey)
380
379
  .action(getLocalScript('transfer/export'));
@@ -397,6 +396,7 @@ program
397
396
  .addOption(forceOption)
398
397
  .addOption(excludeOption)
399
398
  .addOption(onlyOption)
399
+ .addOption(throttleOption)
400
400
  .hook('preAction', validateExcludeOnly)
401
401
  .hook('preAction', async (thisCommand) => {
402
402
  const opts = thisCommand.opts();
@@ -22,15 +22,20 @@ const {
22
22
  createStrapiInstance,
23
23
  formatDiagnostic,
24
24
  loadersFactory,
25
+ exitMessageText,
26
+ abortTransfer,
25
27
  } = require('./utils');
26
28
  const { exitWith } = require('../utils/helpers');
27
29
  /**
28
30
  * @typedef ExportCommandOptions Options given to the CLI import command
29
31
  *
30
- * @property {string} [file] The file path to import
32
+ * @property {string} [file] The file path to export to
31
33
  * @property {boolean} [encrypt] Used to encrypt the final archive
32
- * @property {string} [key] Encryption key, only useful when encryption is enabled
34
+ * @property {string} [key] Encryption key, used only when encryption is enabled
33
35
  * @property {boolean} [compress] Used to compress the final archive
36
+ * @property {(keyof import('@strapi/data-transfer/src/engine').TransferGroupFilter)[]} [only] If present, only include these filtered groups of data
37
+ * @property {(keyof import('@strapi/data-transfer/src/engine').TransferGroupFilter)[]} [exclude] If present, exclude these filtered groups of data
38
+ * @property {number|undefined} [throttle] Delay in ms after each record
34
39
  */
35
40
 
36
41
  const BYTES_IN_MB = 1024 * 1024;
@@ -58,6 +63,7 @@ module.exports = async (opts) => {
58
63
  schemaStrategy: 'ignore', // for an export to file, schemaStrategy will always be skipped
59
64
  exclude: opts.exclude,
60
65
  only: opts.only,
66
+ throttle: opts.throttle,
61
67
  transforms: {
62
68
  links: [
63
69
  {
@@ -108,12 +114,19 @@ module.exports = async (opts) => {
108
114
 
109
115
  progress.on('transfer::start', async () => {
110
116
  console.log(`Starting export...`);
117
+
111
118
  await strapi.telemetry.send('didDEITSProcessStart', getTelemetryPayload());
112
119
  });
113
120
 
114
121
  let results;
115
122
  let outFile;
116
123
  try {
124
+ // Abort transfer if user interrupts process
125
+ ['SIGTERM', 'SIGINT', 'SIGQUIT'].forEach((signal) => {
126
+ process.removeAllListeners(signal);
127
+ process.on(signal, () => abortTransfer({ engine, strapi }));
128
+ });
129
+
117
130
  results = await engine.transfer();
118
131
  outFile = results.destination.file.path;
119
132
  const outFileExists = await fs.pathExists(outFile);
@@ -122,7 +135,7 @@ module.exports = async (opts) => {
122
135
  }
123
136
  } catch {
124
137
  await strapi.telemetry.send('didDEITSProcessFail', getTelemetryPayload());
125
- exitWith(1, 'Export process failed.');
138
+ exitWith(1, exitMessageText('export', true));
126
139
  }
127
140
 
128
141
  await strapi.telemetry.send('didDEITSProcessFinish', getTelemetryPayload());
@@ -133,8 +146,8 @@ module.exports = async (opts) => {
133
146
  console.error('There was an error displaying the results of the transfer.');
134
147
  }
135
148
 
136
- console.log(`${chalk.bold('Export process has been completed successfully!')}`);
137
- exitWith(0, `Export archive is in ${chalk.green(outFile)}`);
149
+ console.log(`Export archive is in ${chalk.green(outFile)}`);
150
+ exitWith(0, exitMessageText('export'));
138
151
  };
139
152
 
140
153
  /**
@@ -18,13 +18,32 @@ const {
18
18
  createStrapiInstance,
19
19
  formatDiagnostic,
20
20
  loadersFactory,
21
+ exitMessageText,
22
+ abortTransfer,
21
23
  } = require('./utils');
22
24
  const { exitWith } = require('../utils/helpers');
23
25
 
24
26
  /**
25
- * @typedef {import('@strapi/data-transfer').ILocalFileSourceProviderOptions} ILocalFileSourceProviderOptions
27
+ * @typedef {import('@strapi/data-transfer/src/file/providers').ILocalFileSourceProviderOptions} ILocalFileSourceProviderOptions
26
28
  */
27
29
 
30
+ /**
31
+ * @typedef ImportCommandOptions Options given to the CLI import command
32
+ *
33
+ * @property {string} [file] The file path to import
34
+ * @property {string} [key] Encryption key, used when encryption is enabled
35
+ * @property {(keyof import('@strapi/data-transfer/src/engine').TransferGroupFilter)[]} [only] If present, only include these filtered groups of data
36
+ * @property {(keyof import('@strapi/data-transfer/src/engine').TransferGroupFilter)[]} [exclude] If present, exclude these filtered groups of data
37
+ * @property {number|undefined} [throttle] Delay in ms after each record
38
+ */
39
+
40
+ /**
41
+ * Import command.
42
+ *
43
+ * It transfers data from a file to a local Strapi instance
44
+ *
45
+ * @param {ImportCommandOptions} opts
46
+ */
28
47
  module.exports = async (opts) => {
29
48
  // validate inputs from Commander
30
49
  if (!isObject(opts)) {
@@ -63,6 +82,7 @@ module.exports = async (opts) => {
63
82
  schemaStrategy: opts.schemaStrategy || DEFAULT_SCHEMA_STRATEGY,
64
83
  exclude: opts.exclude,
65
84
  only: opts.only,
85
+ throttle: opts.throttle,
66
86
  rules: {
67
87
  links: [
68
88
  {
@@ -118,11 +138,16 @@ module.exports = async (opts) => {
118
138
 
119
139
  let results;
120
140
  try {
141
+ // Abort transfer if user interrupts process
142
+ ['SIGTERM', 'SIGINT', 'SIGQUIT'].forEach((signal) => {
143
+ process.removeAllListeners(signal);
144
+ process.on(signal, () => abortTransfer({ engine, strapi }));
145
+ });
146
+
121
147
  results = await engine.transfer();
122
148
  } catch (e) {
123
149
  await strapiInstance.telemetry.send('didDEITSProcessFail', getTelemetryPayload());
124
- console.error('Import process failed.');
125
- process.exit(1);
150
+ exitWith(1, exitMessageText('import', true));
126
151
  }
127
152
 
128
153
  try {
@@ -135,7 +160,7 @@ module.exports = async (opts) => {
135
160
  await strapiInstance.telemetry.send('didDEITSProcessFinish', getTelemetryPayload());
136
161
  await strapiInstance.destroy();
137
162
 
138
- exitWith(0, 'Import process has been completed successfully!');
163
+ exitWith(0, exitMessageText('import'));
139
164
  };
140
165
 
141
166
  /**
@@ -7,11 +7,11 @@ const {
7
7
  createRemoteStrapiDestinationProvider,
8
8
  createLocalStrapiSourceProvider,
9
9
  createLocalStrapiDestinationProvider,
10
+ createRemoteStrapiSourceProvider,
10
11
  },
11
12
  },
12
13
  } = require('@strapi/data-transfer');
13
14
  const { isObject } = require('lodash/fp');
14
- const chalk = require('chalk');
15
15
 
16
16
  const {
17
17
  buildTransferTable,
@@ -19,6 +19,8 @@ const {
19
19
  DEFAULT_IGNORED_CONTENT_TYPES,
20
20
  formatDiagnostic,
21
21
  loadersFactory,
22
+ exitMessageText,
23
+ abortTransfer,
22
24
  } = require('./utils');
23
25
  const { exitWith } = require('../utils/helpers');
24
26
 
@@ -29,6 +31,9 @@ const { exitWith } = require('../utils/helpers');
29
31
  * @property {URL|undefined} [from] The url of a remote Strapi to use as remote source
30
32
  * @property {string|undefined} [toToken] The transfer token for the remote Strapi destination
31
33
  * @property {string|undefined} [fromToken] The transfer token for the remote Strapi source
34
+ * @property {(keyof import('@strapi/data-transfer/src/engine').TransferGroupFilter)[]} [only] If present, only include these filtered groups of data
35
+ * @property {(keyof import('@strapi/data-transfer/src/engine').TransferGroupFilter)[]} [exclude] If present, exclude these filtered groups of data
36
+ * @property {number|undefined} [throttle] Delay in ms after each record
32
37
  */
33
38
 
34
39
  /**
@@ -44,15 +49,14 @@ module.exports = async (opts) => {
44
49
  exitWith(1, 'Could not parse command arguments');
45
50
  }
46
51
 
47
- const strapi = await createStrapiInstance();
52
+ if (!(opts.from || opts.to) || (opts.from && opts.to)) {
53
+ exitWith(1, 'Exactly one source (from) or destination (to) option must be provided');
54
+ }
48
55
 
56
+ const strapi = await createStrapiInstance();
49
57
  let source;
50
58
  let destination;
51
59
 
52
- if (!opts.from && !opts.to) {
53
- exitWith(1, 'At least one source (from) or destination (to) option must be provided');
54
- }
55
-
56
60
  // if no URL provided, use local Strapi
57
61
  if (!opts.from) {
58
62
  source = createLocalStrapiSourceProvider({
@@ -61,13 +65,28 @@ module.exports = async (opts) => {
61
65
  }
62
66
  // if URL provided, set up a remote source provider
63
67
  else {
64
- exitWith(1, `Remote Strapi source provider not yet implemented`);
68
+ if (!opts.fromToken) {
69
+ exitWith(1, 'Missing token for remote destination');
70
+ }
71
+
72
+ source = createRemoteStrapiSourceProvider({
73
+ getStrapi: () => strapi,
74
+ url: opts.from,
75
+ auth: {
76
+ type: 'token',
77
+ token: opts.fromToken,
78
+ },
79
+ });
65
80
  }
66
81
 
67
82
  // if no URL provided, use local Strapi
68
83
  if (!opts.to) {
69
84
  destination = createLocalStrapiDestinationProvider({
70
85
  getStrapi: () => strapi,
86
+ strategy: 'restore',
87
+ restore: {
88
+ entities: { exclude: DEFAULT_IGNORED_CONTENT_TYPES },
89
+ },
71
90
  });
72
91
  }
73
92
  // if URL provided, set up a remote destination provider
@@ -96,6 +115,9 @@ module.exports = async (opts) => {
96
115
  const engine = createTransferEngine(source, destination, {
97
116
  versionStrategy: 'exact',
98
117
  schemaStrategy: 'strict',
118
+ exclude: opts.exclude,
119
+ only: opts.only,
120
+ throttle: opts.throttle,
99
121
  transforms: {
100
122
  links: [
101
123
  {
@@ -135,15 +157,26 @@ module.exports = async (opts) => {
135
157
  updateLoader(stage, data);
136
158
  });
137
159
 
160
+ progress.on('stage::error', ({ stage, data }) => {
161
+ updateLoader(stage, data).fail();
162
+ });
163
+
138
164
  let results;
139
165
  try {
140
166
  console.log(`Starting transfer...`);
167
+
168
+ // Abort transfer if user interrupts process
169
+ ['SIGTERM', 'SIGINT', 'SIGQUIT'].forEach((signal) => {
170
+ process.removeAllListeners(signal);
171
+ process.on(signal, () => abortTransfer({ engine, strapi }));
172
+ });
173
+
141
174
  results = await engine.transfer();
142
175
  } catch (e) {
143
- exitWith(1, 'Transfer process failed.');
176
+ exitWith(1, exitMessageText('transfer', true));
144
177
  }
145
178
 
146
179
  const table = buildTransferTable(results.engine);
147
180
  console.log(table.toString());
148
- exitWith(0, `${chalk.bold('Transfer process has been completed successfully!')}`);
181
+ exitWith(0, exitMessageText('transfer'));
149
182
  };
@@ -14,7 +14,19 @@ const {
14
14
  const ora = require('ora');
15
15
  const { readableBytes, exitWith } = require('../utils/helpers');
16
16
  const strapi = require('../../index');
17
- const { getParseListWithChoices } = require('../utils/commander');
17
+ const { getParseListWithChoices, parseInteger } = require('../utils/commander');
18
+
19
+ const exitMessageText = (process, error = false) => {
20
+ const processCapitalized = process[0].toUpperCase() + process.slice(1);
21
+
22
+ if (!error) {
23
+ return chalk.bold(
24
+ chalk.green(`${processCapitalized} process has been completed successfully!`)
25
+ );
26
+ }
27
+
28
+ return chalk.bold(chalk.red(`${processCapitalized} process failed.`));
29
+ };
18
30
 
19
31
  const pad = (n) => {
20
32
  return (n < 10 ? '0' : '') + String(n);
@@ -90,12 +102,23 @@ const DEFAULT_IGNORED_CONTENT_TYPES = [
90
102
  'admin::audit-log',
91
103
  ];
92
104
 
93
- const createStrapiInstance = async (logLevel = 'error') => {
105
+ const abortTransfer = async ({ engine, strapi }) => {
106
+ try {
107
+ await engine.abortTransfer();
108
+ await strapi.destroy();
109
+ } catch (e) {
110
+ // ignore because there's not much else we can do
111
+ return false;
112
+ }
113
+ return true;
114
+ };
115
+
116
+ const createStrapiInstance = async (opts = {}) => {
94
117
  try {
95
118
  const appContext = await strapi.compile();
96
- const app = strapi(appContext);
119
+ const app = strapi({ ...opts, ...appContext });
97
120
 
98
- app.log.level = logLevel;
121
+ app.log.level = opts.logLevel || 'error';
99
122
  return await app.load();
100
123
  } catch (err) {
101
124
  if (err.code === 'ECONNREFUSED') {
@@ -107,6 +130,13 @@ const createStrapiInstance = async (logLevel = 'error') => {
107
130
 
108
131
  const transferDataTypes = Object.keys(TransferGroupPresets);
109
132
 
133
+ const throttleOption = new Option(
134
+ '--throttle <delay after each entity>',
135
+ `Add a delay in milliseconds between each transferred entity`
136
+ )
137
+ .argParser(parseInteger)
138
+ .hideHelp(); // This option is not publicly documented
139
+
110
140
  const excludeOption = new Option(
111
141
  '--exclude <comma-separated data types>',
112
142
  `Exclude data using comma-separated types. Available types: ${transferDataTypes.join(',')}`
@@ -219,7 +249,10 @@ module.exports = {
219
249
  DEFAULT_IGNORED_CONTENT_TYPES,
220
250
  createStrapiInstance,
221
251
  excludeOption,
252
+ exitMessageText,
222
253
  onlyOption,
254
+ throttleOption,
223
255
  validateExcludeOnly,
224
256
  formatDiagnostic,
257
+ abortTransfer,
225
258
  };
@@ -7,6 +7,7 @@
7
7
  const inquirer = require('inquirer');
8
8
  const { InvalidOptionArgumentError, Option } = require('commander');
9
9
  const { bold, green, cyan } = require('chalk');
10
+ const { isNaN } = require('lodash/fp');
10
11
  const { exitWith } = require('./helpers');
11
12
 
12
13
  /**
@@ -40,6 +41,18 @@ const getParseListWithChoices = (choices, errorMessage = 'Invalid options:') =>
40
41
  };
41
42
  };
42
43
 
44
+ /**
45
+ * argParser: Parse a string as an integer
46
+ */
47
+ const parseInteger = (value) => {
48
+ // parseInt takes a string and a radix
49
+ const parsedValue = parseInt(value, 10);
50
+ if (isNaN(parsedValue)) {
51
+ throw new InvalidOptionArgumentError(`Not an integer: ${value}`);
52
+ }
53
+ return parsedValue;
54
+ };
55
+
43
56
  /**
44
57
  * argParser: Parse a string as a URL object
45
58
  */
@@ -131,6 +144,7 @@ module.exports = {
131
144
  getParseListWithChoices,
132
145
  parseList,
133
146
  parseURL,
147
+ parseInteger,
134
148
  promptEncryptionKey,
135
149
  confirmMessage,
136
150
  forceOption,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@strapi/strapi",
3
- "version": "4.8.2",
3
+ "version": "4.9.0-beta.1",
4
4
  "description": "An open source headless CMS solution to create and manage your own API. It provides a powerful dashboard and features to make your life easier. Databases supported: MySQL, MariaDB, PostgreSQL, SQLite",
5
5
  "keywords": [
6
6
  "strapi",
@@ -81,19 +81,19 @@
81
81
  "dependencies": {
82
82
  "@koa/cors": "3.4.3",
83
83
  "@koa/router": "10.1.1",
84
- "@strapi/admin": "4.8.2",
85
- "@strapi/data-transfer": "4.8.2",
86
- "@strapi/database": "4.8.2",
87
- "@strapi/generate-new": "4.8.2",
88
- "@strapi/generators": "4.8.2",
89
- "@strapi/logger": "4.8.2",
90
- "@strapi/permissions": "4.8.2",
91
- "@strapi/plugin-content-manager": "4.8.2",
92
- "@strapi/plugin-content-type-builder": "4.8.2",
93
- "@strapi/plugin-email": "4.8.2",
94
- "@strapi/plugin-upload": "4.8.2",
95
- "@strapi/typescript-utils": "4.8.2",
96
- "@strapi/utils": "4.8.2",
84
+ "@strapi/admin": "4.9.0-beta.1",
85
+ "@strapi/data-transfer": "4.9.0-beta.1",
86
+ "@strapi/database": "4.9.0-beta.1",
87
+ "@strapi/generate-new": "4.9.0-beta.1",
88
+ "@strapi/generators": "4.9.0-beta.1",
89
+ "@strapi/logger": "4.9.0-beta.1",
90
+ "@strapi/permissions": "4.9.0-beta.1",
91
+ "@strapi/plugin-content-manager": "4.9.0-beta.1",
92
+ "@strapi/plugin-content-type-builder": "4.9.0-beta.1",
93
+ "@strapi/plugin-email": "4.9.0-beta.1",
94
+ "@strapi/plugin-upload": "4.9.0-beta.1",
95
+ "@strapi/typescript-utils": "4.9.0-beta.1",
96
+ "@strapi/utils": "4.9.0-beta.1",
97
97
  "bcryptjs": "2.4.3",
98
98
  "boxen": "5.1.2",
99
99
  "chalk": "4.1.2",
@@ -142,5 +142,5 @@
142
142
  "node": ">=14.19.1 <=18.x.x",
143
143
  "npm": ">=6.0.0"
144
144
  },
145
- "gitHead": "cc73bed97b19d44f7c7999ab44f1a4288590d7ab"
145
+ "gitHead": "ff37d666d0634fc84827e3d6419d916618275572"
146
146
  }