@strapi/strapi 4.10.0-beta.0 → 4.10.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/.eslintignore ADDED
@@ -0,0 +1,2 @@
1
+ node_modules/
2
+ .eslintrc.js
package/.eslintrc.js ADDED
@@ -0,0 +1,4 @@
1
+ module.exports = {
2
+ root: true,
3
+ extends: ['custom/back'],
4
+ };
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(
@@ -261,6 +258,14 @@ program
261
258
  .description('Enable anonymous telemetry and metadata sending to Strapi analytics')
262
259
  .action(getLocalScript('opt-in-telemetry'));
263
260
 
261
+ program
262
+ .command('report')
263
+ .description('Get system stats for debugging and submitting issues')
264
+ .option('-u, --uuid', 'Include Project UUID')
265
+ .option('-d, --dependencies', 'Include Project Dependencies')
266
+ .option('--all', 'Include All Information')
267
+ .action(getLocalScript('report'));
268
+
264
269
  program
265
270
  .command('ts:generate-types')
266
271
  .description(`Generate TypeScript typings for your schemas`)
@@ -279,13 +284,12 @@ program
279
284
  .description('Transfer data from one source to another')
280
285
  .allowExcessArguments(false)
281
286
  .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
287
+ new Option(
288
+ '--from <sourceURL>',
289
+ `URL of the remote Strapi instance to get data from`
290
+ ).argParser(parseURL)
288
291
  )
292
+ .addOption(new Option('--from-token <token>', `Transfer token for the remote Strapi source`))
289
293
  .addOption(
290
294
  new Option(
291
295
  '--to <destinationURL>',
@@ -296,7 +300,16 @@ program
296
300
  .addOption(forceOption)
297
301
  .addOption(excludeOption)
298
302
  .addOption(onlyOption)
303
+ .addOption(throttleOption)
299
304
  .hook('preAction', validateExcludeOnly)
305
+ .hook(
306
+ 'preAction',
307
+ ifOptions(
308
+ (opts) => !(opts.from || opts.to) || (opts.from && opts.to),
309
+ () =>
310
+ exitWith(1, 'Exactly one remote source (from) or destination (to) option must be provided')
311
+ )
312
+ )
300
313
  // If --from is used, validate the URL and token
301
314
  .hook(
302
315
  'preAction',
@@ -313,10 +326,15 @@ program
313
326
  },
314
327
  ]);
315
328
  if (!answers.fromToken?.length) {
316
- exitWith(1, 'No token entered, aborting transfer.');
329
+ exitWith(1, 'No token provided for remote source, aborting transfer.');
317
330
  }
318
331
  thisCommand.opts().fromToken = answers.fromToken;
319
332
  }
333
+
334
+ await confirmMessage(
335
+ 'The transfer will delete all the local Strapi assets and its database. Are you sure you want to proceed?',
336
+ { failMessage: 'Transfer process aborted' }
337
+ )(thisCommand);
320
338
  }
321
339
  )
322
340
  )
@@ -336,25 +354,18 @@ program
336
354
  },
337
355
  ]);
338
356
  if (!answers.toToken?.length) {
339
- exitWith(1, 'No token entered, aborting transfer.');
357
+ exitWith(1, 'No token provided for remote destination, aborting transfer.');
340
358
  }
341
359
  thisCommand.opts().toToken = answers.toToken;
342
360
  }
343
361
 
344
362
  await confirmMessage(
345
- 'The transfer will delete all data in the remote database and media files. Are you sure you want to proceed?',
363
+ 'The transfer will delete all the remote Strapi assets and its database. Are you sure you want to proceed?',
346
364
  { failMessage: 'Transfer process aborted' }
347
365
  )(thisCommand);
348
366
  }
349
367
  )
350
368
  )
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
369
  .action(getLocalScript('transfer/transfer'));
359
370
 
360
371
  // `$ strapi export`
@@ -375,6 +386,7 @@ program
375
386
  .addOption(new Option('-f, --file <file>', 'name to use for exported file (without extensions)'))
376
387
  .addOption(excludeOption)
377
388
  .addOption(onlyOption)
389
+ .addOption(throttleOption)
378
390
  .hook('preAction', validateExcludeOnly)
379
391
  .hook('preAction', promptEncryptionKey)
380
392
  .action(getLocalScript('transfer/export'));
@@ -397,6 +409,7 @@ program
397
409
  .addOption(forceOption)
398
410
  .addOption(excludeOption)
399
411
  .addOption(onlyOption)
412
+ .addOption(throttleOption)
400
413
  .hook('preAction', validateExcludeOnly)
401
414
  .hook('preAction', async (thisCommand) => {
402
415
  const opts = thisCommand.opts();
@@ -451,7 +464,7 @@ program
451
464
  .hook(
452
465
  'preAction',
453
466
  confirmMessage(
454
- 'The import will delete all data in your database and media files. Are you sure you want to proceed?',
467
+ 'The import will delete all assets and data in your database. Are you sure you want to proceed?',
455
468
  { failMessage: 'Import process aborted' }
456
469
  )
457
470
  )
package/ee/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- const { pick } = require('lodash/fp');
3
+ const { pick, isEqual } = require('lodash/fp');
4
4
 
5
5
  const { readLicense, verifyLicense, fetchLicense, LicenseCheckError } = require('./license');
6
6
  const { shiftCronExpression } = require('../lib/utils/cron');
@@ -13,10 +13,31 @@ const ee = {
13
13
  };
14
14
 
15
15
  const disable = (message) => {
16
+ // Prevent emitting ee.disable if it was already disabled
17
+ const shouldEmitEvent = ee.enabled !== false;
18
+
16
19
  ee.logger?.warn(`${message} Switching to CE.`);
17
20
  // Only keep the license key for potential re-enabling during a later check
18
21
  ee.licenseInfo = pick('licenseKey', ee.licenseInfo);
22
+
19
23
  ee.enabled = false;
24
+
25
+ if (shouldEmitEvent) {
26
+ // Notify EE features that they should be disabled
27
+ strapi.eventHub.emit('ee.disable');
28
+ }
29
+ };
30
+
31
+ const enable = () => {
32
+ // Prevent emitting ee.enable if it was already enabled
33
+ const shouldEmitEvent = ee.enabled !== true;
34
+
35
+ ee.enabled = true;
36
+
37
+ if (shouldEmitEvent) {
38
+ // Notify EE features that they should be disabled
39
+ strapi.eventHub.emit('ee.enable');
40
+ }
20
41
  };
21
42
 
22
43
  let initialized = false;
@@ -41,7 +62,7 @@ const init = (licenseDir, logger) => {
41
62
 
42
63
  if (license) {
43
64
  ee.licenseInfo = verifyLicense(license);
44
- ee.enabled = true;
65
+ enable();
45
66
  }
46
67
  } catch (error) {
47
68
  disable(error.message);
@@ -89,8 +110,22 @@ const onlineUpdate = async ({ strapi }) => {
89
110
 
90
111
  if (license) {
91
112
  try {
92
- ee.licenseInfo = verifyLicense(license);
113
+ // Verify license and check if its info changed
114
+ const newLicenseInfo = verifyLicense(license);
115
+ const licenseInfoChanged =
116
+ !isEqual(newLicenseInfo.features, ee.licenseInfo.features) ||
117
+ newLicenseInfo.seats !== ee.licenseInfo.seats ||
118
+ newLicenseInfo.type !== ee.licenseInfo.type;
119
+
120
+ // Store the new license info
121
+ ee.licenseInfo = newLicenseInfo;
122
+ const wasEnabled = ee.enabled;
93
123
  validateInfo();
124
+
125
+ // Notify EE features
126
+ if (licenseInfoChanged && wasEnabled) {
127
+ strapi.eventHub.emit('ee.update');
128
+ }
94
129
  } catch (error) {
95
130
  disable(error.message);
96
131
  }
@@ -125,7 +160,7 @@ const validateInfo = () => {
125
160
  return disable('License expired.');
126
161
  }
127
162
 
128
- ee.enabled = true;
163
+ enable();
129
164
  };
130
165
 
131
166
  const checkLicense = async ({ strapi }) => {
package/ee/license.js CHANGED
@@ -10,7 +10,9 @@ const machineId = require('../lib/utils/machine-id');
10
10
  const DEFAULT_FEATURES = {
11
11
  bronze: [],
12
12
  silver: [],
13
- gold: ['sso', { name: 'audit-logs', options: { retentionDays: 90 } }, 'review-workflows'],
13
+ // Set a null retention duration to allow the user to override it
14
+ // The default of 90 days is set in the audit logs service
15
+ gold: ['sso', { name: 'audit-logs', options: { retentionDays: null } }, 'review-workflows'],
14
16
  };
15
17
 
16
18
  const publicKey = fs.readFileSync(join(__dirname, 'resources/key.pub'));
package/lib/Strapi.js CHANGED
@@ -43,6 +43,7 @@ const apisRegistry = require('./core/registries/apis');
43
43
  const bootstrap = require('./core/bootstrap');
44
44
  const loaders = require('./core/loaders');
45
45
  const { destroyOnSignal } = require('./utils/signals');
46
+ const getNumberOfDynamicZones = require('./services/utils/dynamic-zones');
46
47
  const sanitizersRegistry = require('./core/registries/sanitizers');
47
48
  const convertCustomFieldType = require('./utils/convert-custom-field-type');
48
49
 
@@ -249,6 +250,9 @@ class Strapi {
249
250
  groupProperties: {
250
251
  database: strapi.config.get('database.connection.client'),
251
252
  plugins: Object.keys(strapi.plugins),
253
+ numberOfAllContentTypes: _.size(this.contentTypes), // TODO: V5: This event should be renamed numberOfContentTypes in V5 as the name is already taken to describe the number of content types using i18n.
254
+ numberOfComponents: _.size(this.components),
255
+ numberOfDynamicZones: getNumberOfDynamicZones(),
252
256
  // TODO: to add back
253
257
  // providers: this.config.installedProviders,
254
258
  },
@@ -468,7 +472,7 @@ class Strapi {
468
472
  await this.startWebhooks();
469
473
 
470
474
  await this.server.initMiddlewares();
471
- await this.server.initRouting();
475
+ this.server.initRouting();
472
476
 
473
477
  await this.contentAPI.permissions.registerActions();
474
478
 
@@ -536,17 +540,17 @@ class Strapi {
536
540
  // plugins
537
541
  await this.container.get('modules')[lifecycleName]();
538
542
 
539
- // user
540
- const userLifecycleFunction = this.app && this.app[lifecycleName];
541
- if (isFunction(userLifecycleFunction)) {
542
- await userLifecycleFunction({ strapi: this });
543
- }
544
-
545
543
  // admin
546
544
  const adminLifecycleFunction = this.admin && this.admin[lifecycleName];
547
545
  if (isFunction(adminLifecycleFunction)) {
548
546
  await adminLifecycleFunction({ strapi: this });
549
547
  }
548
+
549
+ // user
550
+ const userLifecycleFunction = this.app && this.app[lifecycleName];
551
+ if (isFunction(userLifecycleFunction)) {
552
+ await userLifecycleFunction({ strapi: this });
553
+ }
550
554
  }
551
555
 
552
556
  getModel(uid) {
@@ -0,0 +1,35 @@
1
+ 'use strict';
2
+
3
+ const { EOL } = require('os');
4
+ const strapi = require('../index');
5
+
6
+ module.exports = async ({ uuid, dependencies, all }) => {
7
+ const config = {
8
+ reportUUID: Boolean(all || uuid),
9
+ reportDependencies: Boolean(all || dependencies),
10
+ };
11
+
12
+ const appContext = await strapi.compile();
13
+ const app = await strapi(appContext).register();
14
+
15
+ let debugInfo = `Launched In: ${Date.now() - app.config.launchedAt} ms
16
+ Environment: ${app.config.environment}
17
+ OS: ${process.platform}-${process.arch}
18
+ Strapi Version: ${app.config.info.strapi}
19
+ Node/Yarn Version: ${process.env.npm_config_user_agent}
20
+ Edition: ${app.EE ? 'Enterprise' : 'Community'}
21
+ Database: ${app?.config?.database?.connection?.client ?? 'unknown'}`;
22
+
23
+ if (config.reportUUID) {
24
+ debugInfo += `${EOL}UUID: ${app.config.uuid}`;
25
+ }
26
+
27
+ if (config.reportDependencies) {
28
+ debugInfo += `${EOL}Dependencies: ${JSON.stringify(app.config.info.dependencies, null, 2)}
29
+ Dev Dependencies: ${JSON.stringify(app.config.info.devDependencies, null, 2)}`;
30
+ }
31
+
32
+ console.log(debugInfo);
33
+
34
+ await app.destroy();
35
+ };
@@ -22,15 +22,21 @@ const {
22
22
  createStrapiInstance,
23
23
  formatDiagnostic,
24
24
  loadersFactory,
25
+ exitMessageText,
26
+ abortTransfer,
27
+ getTransferTelemetryPayload,
25
28
  } = require('./utils');
26
29
  const { exitWith } = require('../utils/helpers');
27
30
  /**
28
31
  * @typedef ExportCommandOptions Options given to the CLI import command
29
32
  *
30
- * @property {string} [file] The file path to import
33
+ * @property {string} [file] The file path to export to
31
34
  * @property {boolean} [encrypt] Used to encrypt the final archive
32
- * @property {string} [key] Encryption key, only useful when encryption is enabled
35
+ * @property {string} [key] Encryption key, used only when encryption is enabled
33
36
  * @property {boolean} [compress] Used to compress the final archive
37
+ * @property {(keyof import('@strapi/data-transfer/src/engine').TransferGroupFilter)[]} [only] If present, only include these filtered groups of data
38
+ * @property {(keyof import('@strapi/data-transfer/src/engine').TransferGroupFilter)[]} [exclude] If present, exclude these filtered groups of data
39
+ * @property {number|undefined} [throttle] Delay in ms after each record
34
40
  */
35
41
 
36
42
  const BYTES_IN_MB = 1024 * 1024;
@@ -58,6 +64,7 @@ module.exports = async (opts) => {
58
64
  schemaStrategy: 'ignore', // for an export to file, schemaStrategy will always be skipped
59
65
  exclude: opts.exclude,
60
66
  only: opts.only,
67
+ throttle: opts.throttle,
61
68
  transforms: {
62
69
  links: [
63
70
  {
@@ -97,23 +104,21 @@ module.exports = async (opts) => {
97
104
  updateLoader(stage, data);
98
105
  });
99
106
 
100
- const getTelemetryPayload = (/* payload */) => {
101
- return {
102
- eventProperties: {
103
- source: engine.sourceProvider.name,
104
- destination: engine.destinationProvider.name,
105
- },
106
- };
107
- };
108
-
109
107
  progress.on('transfer::start', async () => {
110
108
  console.log(`Starting export...`);
111
- await strapi.telemetry.send('didDEITSProcessStart', getTelemetryPayload());
109
+
110
+ await strapi.telemetry.send('didDEITSProcessStart', getTransferTelemetryPayload(engine));
112
111
  });
113
112
 
114
113
  let results;
115
114
  let outFile;
116
115
  try {
116
+ // Abort transfer if user interrupts process
117
+ ['SIGTERM', 'SIGINT', 'SIGQUIT'].forEach((signal) => {
118
+ process.removeAllListeners(signal);
119
+ process.on(signal, () => abortTransfer({ engine, strapi }));
120
+ });
121
+
117
122
  results = await engine.transfer();
118
123
  outFile = results.destination.file.path;
119
124
  const outFileExists = await fs.pathExists(outFile);
@@ -121,11 +126,13 @@ module.exports = async (opts) => {
121
126
  throw new TransferEngineTransferError(`Export file not created "${outFile}"`);
122
127
  }
123
128
  } catch {
124
- await strapi.telemetry.send('didDEITSProcessFail', getTelemetryPayload());
125
- exitWith(1, 'Export process failed.');
129
+ await strapi.telemetry.send('didDEITSProcessFail', getTransferTelemetryPayload(engine));
130
+ exitWith(1, exitMessageText('export', true));
126
131
  }
127
132
 
128
- await strapi.telemetry.send('didDEITSProcessFinish', getTelemetryPayload());
133
+ // Note: we need to await telemetry or else the process ends before it is sent
134
+ await strapi.telemetry.send('didDEITSProcessFinish', getTransferTelemetryPayload(engine));
135
+
129
136
  try {
130
137
  const table = buildTransferTable(results.engine);
131
138
  console.log(table.toString());
@@ -133,8 +140,8 @@ module.exports = async (opts) => {
133
140
  console.error('There was an error displaying the results of the transfer.');
134
141
  }
135
142
 
136
- console.log(`${chalk.bold('Export process has been completed successfully!')}`);
137
- exitWith(0, `Export archive is in ${chalk.green(outFile)}`);
143
+ console.log(`Export archive is in ${chalk.green(outFile)}`);
144
+ exitWith(0, exitMessageText('export'));
138
145
  };
139
146
 
140
147
  /**
@@ -18,13 +18,33 @@ const {
18
18
  createStrapiInstance,
19
19
  formatDiagnostic,
20
20
  loadersFactory,
21
+ exitMessageText,
22
+ abortTransfer,
23
+ getTransferTelemetryPayload,
21
24
  } = require('./utils');
22
25
  const { exitWith } = require('../utils/helpers');
23
26
 
24
27
  /**
25
- * @typedef {import('@strapi/data-transfer').ILocalFileSourceProviderOptions} ILocalFileSourceProviderOptions
28
+ * @typedef {import('@strapi/data-transfer/src/file/providers').ILocalFileSourceProviderOptions} ILocalFileSourceProviderOptions
26
29
  */
27
30
 
31
+ /**
32
+ * @typedef ImportCommandOptions Options given to the CLI import command
33
+ *
34
+ * @property {string} [file] The file path to import
35
+ * @property {string} [key] Encryption key, used when encryption is enabled
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
39
+ */
40
+
41
+ /**
42
+ * Import command.
43
+ *
44
+ * It transfers data from a file to a local Strapi instance
45
+ *
46
+ * @param {ImportCommandOptions} opts
47
+ */
28
48
  module.exports = async (opts) => {
29
49
  // validate inputs from Commander
30
50
  if (!isObject(opts)) {
@@ -63,6 +83,7 @@ module.exports = async (opts) => {
63
83
  schemaStrategy: opts.schemaStrategy || DEFAULT_SCHEMA_STRATEGY,
64
84
  exclude: opts.exclude,
65
85
  only: opts.only,
86
+ throttle: opts.throttle,
66
87
  rules: {
67
88
  links: [
68
89
  {
@@ -102,27 +123,26 @@ module.exports = async (opts) => {
102
123
  updateLoader(stage, data);
103
124
  });
104
125
 
105
- const getTelemetryPayload = () => {
106
- return {
107
- eventProperties: {
108
- source: engine.sourceProvider.name,
109
- destination: engine.destinationProvider.name,
110
- },
111
- };
112
- };
113
-
114
126
  progress.on('transfer::start', async () => {
115
127
  console.log('Starting import...');
116
- await strapiInstance.telemetry.send('didDEITSProcessStart', getTelemetryPayload());
128
+ await strapiInstance.telemetry.send(
129
+ 'didDEITSProcessStart',
130
+ getTransferTelemetryPayload(engine)
131
+ );
117
132
  });
118
133
 
119
134
  let results;
120
135
  try {
136
+ // Abort transfer if user interrupts process
137
+ ['SIGTERM', 'SIGINT', 'SIGQUIT'].forEach((signal) => {
138
+ process.removeAllListeners(signal);
139
+ process.on(signal, () => abortTransfer({ engine, strapi }));
140
+ });
141
+
121
142
  results = await engine.transfer();
122
143
  } catch (e) {
123
- await strapiInstance.telemetry.send('didDEITSProcessFail', getTelemetryPayload());
124
- console.error('Import process failed.');
125
- process.exit(1);
144
+ await strapiInstance.telemetry.send('didDEITSProcessFail', getTransferTelemetryPayload(engine));
145
+ exitWith(1, exitMessageText('import', true));
126
146
  }
127
147
 
128
148
  try {
@@ -132,10 +152,11 @@ module.exports = async (opts) => {
132
152
  console.error('There was an error displaying the results of the transfer.');
133
153
  }
134
154
 
135
- await strapiInstance.telemetry.send('didDEITSProcessFinish', getTelemetryPayload());
155
+ // Note: we need to await telemetry or else the process ends before it is sent
156
+ await strapiInstance.telemetry.send('didDEITSProcessFinish', getTransferTelemetryPayload(engine));
136
157
  await strapiInstance.destroy();
137
158
 
138
- exitWith(0, 'Import process has been completed successfully!');
159
+ exitWith(0, exitMessageText('import'));
139
160
  };
140
161
 
141
162
  /**
@@ -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,9 @@ const {
19
19
  DEFAULT_IGNORED_CONTENT_TYPES,
20
20
  formatDiagnostic,
21
21
  loadersFactory,
22
+ exitMessageText,
23
+ abortTransfer,
24
+ getTransferTelemetryPayload,
22
25
  } = require('./utils');
23
26
  const { exitWith } = require('../utils/helpers');
24
27
 
@@ -29,6 +32,9 @@ const { exitWith } = require('../utils/helpers');
29
32
  * @property {URL|undefined} [from] The url of a remote Strapi to use as remote source
30
33
  * @property {string|undefined} [toToken] The transfer token for the remote Strapi destination
31
34
  * @property {string|undefined} [fromToken] The transfer token for the remote Strapi source
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
32
38
  */
33
39
 
34
40
  /**
@@ -44,15 +50,14 @@ module.exports = async (opts) => {
44
50
  exitWith(1, 'Could not parse command arguments');
45
51
  }
46
52
 
47
- const strapi = await createStrapiInstance();
53
+ if (!(opts.from || opts.to) || (opts.from && opts.to)) {
54
+ exitWith(1, 'Exactly one source (from) or destination (to) option must be provided');
55
+ }
48
56
 
57
+ const strapi = await createStrapiInstance();
49
58
  let source;
50
59
  let destination;
51
60
 
52
- if (!opts.from && !opts.to) {
53
- exitWith(1, 'At least one source (from) or destination (to) option must be provided');
54
- }
55
-
56
61
  // if no URL provided, use local Strapi
57
62
  if (!opts.from) {
58
63
  source = createLocalStrapiSourceProvider({
@@ -61,13 +66,28 @@ module.exports = async (opts) => {
61
66
  }
62
67
  // if URL provided, set up a remote source provider
63
68
  else {
64
- exitWith(1, `Remote Strapi source provider not yet implemented`);
69
+ if (!opts.fromToken) {
70
+ exitWith(1, 'Missing token for remote destination');
71
+ }
72
+
73
+ source = createRemoteStrapiSourceProvider({
74
+ getStrapi: () => strapi,
75
+ url: opts.from,
76
+ auth: {
77
+ type: 'token',
78
+ token: opts.fromToken,
79
+ },
80
+ });
65
81
  }
66
82
 
67
83
  // if no URL provided, use local Strapi
68
84
  if (!opts.to) {
69
85
  destination = createLocalStrapiDestinationProvider({
70
86
  getStrapi: () => strapi,
87
+ strategy: 'restore',
88
+ restore: {
89
+ entities: { exclude: DEFAULT_IGNORED_CONTENT_TYPES },
90
+ },
71
91
  });
72
92
  }
73
93
  // if URL provided, set up a remote destination provider
@@ -96,6 +116,9 @@ module.exports = async (opts) => {
96
116
  const engine = createTransferEngine(source, destination, {
97
117
  versionStrategy: 'exact',
98
118
  schemaStrategy: 'strict',
119
+ exclude: opts.exclude,
120
+ only: opts.only,
121
+ throttle: opts.throttle,
99
122
  transforms: {
100
123
  links: [
101
124
  {
@@ -135,15 +158,39 @@ module.exports = async (opts) => {
135
158
  updateLoader(stage, data);
136
159
  });
137
160
 
161
+ progress.on('stage::error', ({ stage, data }) => {
162
+ updateLoader(stage, data).fail();
163
+ });
164
+
165
+ progress.on('transfer::start', async () => {
166
+ console.log(`Starting transfer...`);
167
+
168
+ await strapi.telemetry.send('didDEITSProcessStart', getTransferTelemetryPayload(engine));
169
+ });
170
+
138
171
  let results;
139
172
  try {
140
- console.log(`Starting transfer...`);
173
+ // Abort transfer if user interrupts process
174
+ ['SIGTERM', 'SIGINT', 'SIGQUIT'].forEach((signal) => {
175
+ process.removeAllListeners(signal);
176
+ process.on(signal, () => abortTransfer({ engine, strapi }));
177
+ });
178
+
141
179
  results = await engine.transfer();
142
180
  } catch (e) {
143
- exitWith(1, 'Transfer process failed.');
181
+ await strapi.telemetry.send('didDEITSProcessFail', getTransferTelemetryPayload(engine));
182
+ exitWith(1, exitMessageText('transfer', true));
183
+ }
184
+
185
+ // Note: we need to await telemetry or else the process ends before it is sent
186
+ await strapi.telemetry.send('didDEITSProcessFinish', getTransferTelemetryPayload(engine));
187
+
188
+ try {
189
+ const table = buildTransferTable(results.engine);
190
+ console.log(table.toString());
191
+ } catch (e) {
192
+ console.error('There was an error displaying the results of the transfer.');
144
193
  }
145
194
 
146
- const table = buildTransferTable(results.engine);
147
- console.log(table.toString());
148
- exitWith(0, `${chalk.bold('Transfer process has been completed successfully!')}`);
195
+ exitWith(0, exitMessageText('transfer'));
149
196
  };
@@ -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(',')}`
@@ -212,14 +242,33 @@ const loadersFactory = (defaultLoaders = {}) => {
212
242
  };
213
243
  };
214
244
 
245
+ /**
246
+ * Get the telemetry data to be sent for a didDEITSProcess* event from an initialized transfer engine object
247
+ *
248
+ * @param {import('@strapi/data-transfer/types').ITransferEngine} engine Initialized transfer engine
249
+ * @returns {object} Telemetry properties object
250
+ */
251
+ const getTransferTelemetryPayload = (engine) => {
252
+ return {
253
+ eventProperties: {
254
+ source: engine?.sourceProvider?.name,
255
+ destination: engine?.destinationProvider?.name,
256
+ },
257
+ };
258
+ };
259
+
215
260
  module.exports = {
216
261
  loadersFactory,
217
262
  buildTransferTable,
218
263
  getDefaultExportName,
264
+ getTransferTelemetryPayload,
219
265
  DEFAULT_IGNORED_CONTENT_TYPES,
220
266
  createStrapiInstance,
221
267
  excludeOption,
268
+ exitMessageText,
222
269
  onlyOption,
270
+ throttleOption,
223
271
  validateExcludeOnly,
224
272
  formatDiagnostic,
273
+ abortTransfer,
225
274
  };
@@ -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,
@@ -61,7 +61,7 @@ const contentTypeSchemaValidator = yup.object().shape({
61
61
 
62
62
  // should match the GraphQL regex
63
63
  if (!regressedValues.every((value) => GRAPHQL_ENUM_REGEX.test(value))) {
64
- const message = `Invalid enumeration value. Values should have at least one alphabetical character preceeding the first occurence of a number. Update your enumeration '${attrName}'.`;
64
+ const message = `Invalid enumeration value. Values should have at least one alphabetical character preceding the first occurence of a number. Update your enumeration '${attrName}'.`;
65
65
 
66
66
  return this.createError({ message });
67
67
  }
@@ -2,7 +2,7 @@
2
2
 
3
3
  const { join } = require('path');
4
4
  const fse = require('fs-extra');
5
- const { defaultsDeep, getOr, get } = require('lodash/fp');
5
+ const { defaultsDeep, defaults, getOr, get } = require('lodash/fp');
6
6
  const { env } = require('@strapi/utils');
7
7
  const loadConfigFile = require('../../app-configuration/load-config-file');
8
8
  const loadFiles = require('../../../load/load-files');
@@ -101,7 +101,12 @@ const loadPlugins = async (strapi) => {
101
101
  }
102
102
 
103
103
  const pluginServer = loadConfigFile(serverEntrypointPath);
104
- plugins[pluginName] = defaultsDeep(defaultPlugin, pluginServer);
104
+ plugins[pluginName] = {
105
+ ...defaultPlugin,
106
+ ...pluginServer,
107
+ config: defaults(defaultPlugin.config, pluginServer.config),
108
+ routes: pluginServer.routes ?? defaultPlugin.routes,
109
+ };
105
110
  }
106
111
 
107
112
  // TODO: validate plugin format
@@ -1,9 +1,9 @@
1
- import { BaseContext } from 'koa';
1
+ import { ExtendableContext } from 'koa';
2
2
  import { Strapi } from '../../';
3
3
 
4
- interface PolicyContext extends BaseContext {
4
+ interface PolicyContext extends ExtendableContext {
5
5
  type: string;
6
- is(name): boolean;
6
+ is(name: string): boolean;
7
7
  }
8
8
 
9
9
  export type Policy<T = unknown> = (
@@ -22,7 +22,6 @@ const createCollectionTypeController = ({ contentType }) => {
22
22
  const sanitizedQuery = await this.sanitizeQuery(ctx);
23
23
  const { results, pagination } = await strapi.service(uid).find(sanitizedQuery);
24
24
  const sanitizedResults = await this.sanitizeOutput(results, ctx);
25
-
26
25
  return this.transformResponse(sanitizedResults, { pagination });
27
26
  },
28
27
 
@@ -18,7 +18,7 @@ const createController = ({ contentType }) => {
18
18
  return transformResponse(data, meta, { contentType });
19
19
  },
20
20
 
21
- sanitizeOutput(data, ctx) {
21
+ async sanitizeOutput(data, ctx) {
22
22
  const auth = getAuthFromKoaContext(ctx);
23
23
 
24
24
  return sanitize.contentAPI.output(data, contentType, { auth });
package/lib/factories.js CHANGED
@@ -45,13 +45,12 @@ const createCoreService = (uid, cfg = {}) => {
45
45
  };
46
46
 
47
47
  const createCoreRouter = (uid, cfg = {}) => {
48
- const { prefix, config = {}, only, except } = cfg;
48
+ const { prefix, config = {}, only, except, type } = cfg;
49
49
  let routes;
50
50
 
51
51
  return {
52
- get prefix() {
53
- return prefix;
54
- },
52
+ type,
53
+ prefix,
55
54
  get routes() {
56
55
  if (!routes) {
57
56
  const contentType = strapi.contentType(uid);
@@ -1,21 +1,5 @@
1
1
  'use strict';
2
2
 
3
- /* eslint-disable no-nested-ternary */
4
-
5
- const chalk = require('chalk');
6
-
7
- const codeToColor = (code) => {
8
- return code >= 500
9
- ? chalk.red(code)
10
- : code >= 400
11
- ? chalk.yellow(code)
12
- : code >= 300
13
- ? chalk.cyan(code)
14
- : code >= 200
15
- ? chalk.green(code)
16
- : code;
17
- };
18
-
19
3
  /**
20
4
  * @type {import('./').MiddlewareFactory}
21
5
  */
@@ -25,6 +9,6 @@ module.exports = (_, { strapi }) => {
25
9
  await next();
26
10
  const delta = Math.ceil(Date.now() - start);
27
11
 
28
- strapi.log.http(`${ctx.method} ${ctx.url} (${delta} ms) ${codeToColor(ctx.status)}`);
12
+ strapi.log.http(`${ctx.method} ${ctx.url} (${delta} ms) ${ctx.status}`);
29
13
  };
30
14
  };
@@ -12,7 +12,7 @@ const defaults = {
12
12
  useDefaults: true,
13
13
  directives: {
14
14
  'connect-src': ["'self'", 'https:'],
15
- 'img-src': ["'self'", 'data:', 'blob:', 'https://dl.airtable.com'],
15
+ 'img-src': ["'self'", 'data:', 'blob:', 'https://market-assets.strapi.io'],
16
16
  'media-src': ["'self'", 'data:', 'blob:'],
17
17
  upgradeInsecureRequests: null,
18
18
  },
@@ -35,7 +35,12 @@ module.exports = function createEventHub() {
35
35
  },
36
36
 
37
37
  unsubscribe(subscriber) {
38
- subscribers.splice(subscribers.indexOf(subscriber), 1);
38
+ const subscriberIndex = subscribers.indexOf(subscriber);
39
+
40
+ // Only remove the subscriber if it exists
41
+ if (subscriberIndex >= 0) {
42
+ subscribers.splice(subscriberIndex, 1);
43
+ }
39
44
  },
40
45
 
41
46
  on(eventName, listener) {
@@ -17,6 +17,7 @@ const LIMITED_EVENTS = [
17
17
  'didSaveMediaWithCaption',
18
18
  'didDisableResponsiveDimensions',
19
19
  'didEnableResponsiveDimensions',
20
+ 'didInitializePluginUpload',
20
21
  ];
21
22
 
22
23
  const createTelemetryInstance = (strapi) => {
@@ -3,7 +3,6 @@
3
3
  const os = require('os');
4
4
  const path = require('path');
5
5
  const _ = require('lodash');
6
- const { map, values, sumBy, pipe, flatMap, propEq } = require('lodash/fp');
7
6
  const isDocker = require('is-docker');
8
7
  const fetch = require('node-fetch');
9
8
  const ciEnv = require('ci-info');
@@ -41,14 +40,6 @@ module.exports = (strapi) => {
41
40
  const serverRootPath = strapi.dirs.app.root;
42
41
  const adminRootPath = path.join(strapi.dirs.app.root, 'src', 'admin');
43
42
 
44
- const getNumberOfDynamicZones = () => {
45
- return pipe(
46
- map('attributes'),
47
- flatMap(values),
48
- sumBy(propEq('type', 'dynamiczone'))
49
- )(strapi.contentTypes);
50
- };
51
-
52
43
  const anonymousUserProperties = {
53
44
  environment: strapi.config.environment,
54
45
  os: os.type(),
@@ -66,9 +57,6 @@ module.exports = (strapi) => {
66
57
  useTypescriptOnAdmin: isUsingTypeScriptSync(adminRootPath),
67
58
  projectId: uuid,
68
59
  isHostedOnStrapiCloud: env('STRAPI_HOSTING', null) === 'strapi.cloud',
69
- numberOfAllContentTypes: _.size(strapi.contentTypes), // TODO: V5: This event should be renamed numberOfContentTypes in V5 as the name is already taken to describe the number of content types using i18n.
70
- numberOfComponents: _.size(strapi.components),
71
- numberOfDynamicZones: getNumberOfDynamicZones(),
72
60
  };
73
61
 
74
62
  addPackageJsonStrapiMetadata(anonymousGroupProperties, strapi);
@@ -92,8 +92,8 @@ const createServer = (strapi) => {
92
92
  return this;
93
93
  },
94
94
 
95
- async initRouting() {
96
- await registerAllRoutes(strapi);
95
+ initRouting() {
96
+ registerAllRoutes(strapi);
97
97
 
98
98
  return this;
99
99
  },
@@ -69,7 +69,7 @@ const registerPluginRoutes = (strapi) => {
69
69
  } else {
70
70
  _.forEach(plugin.routes, (router) => {
71
71
  router.type = router.type || 'admin';
72
- router.prefix = `/${pluginName}`;
72
+ router.prefix = router.prefix || `/${pluginName}`;
73
73
  router.routes.forEach((route) => {
74
74
  generateRouteScope(route);
75
75
  route.info = { pluginName };
@@ -0,0 +1,13 @@
1
+ 'use strict';
2
+
3
+ const { map, values, sumBy, pipe, flatMap, propEq } = require('lodash/fp');
4
+
5
+ const getNumberOfDynamicZones = () => {
6
+ return pipe(
7
+ map('attributes'),
8
+ flatMap(values),
9
+ sumBy(propEq('type', 'dynamiczone'))
10
+ )(strapi.contentTypes);
11
+ };
12
+
13
+ module.exports = getNumberOfDynamicZones;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@strapi/strapi",
3
- "version": "4.10.0-beta.0",
3
+ "version": "4.10.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",
@@ -66,9 +66,7 @@
66
66
  ],
67
67
  "main": "./lib",
68
68
  "types": "./lib/index.d.ts",
69
- "bin": {
70
- "strapi": "./bin/strapi.js"
71
- },
69
+ "bin": "./bin/strapi.js",
72
70
  "directories": {
73
71
  "lib": "./lib",
74
72
  "bin": "./bin",
@@ -76,24 +74,26 @@
76
74
  },
77
75
  "scripts": {
78
76
  "postinstall": "node lib/utils/success.js",
79
- "test:unit": "jest --verbose"
77
+ "test:unit": "run -T jest",
78
+ "test:unit:watch": "run -T jest --watch",
79
+ "lint": "run -T eslint ."
80
80
  },
81
81
  "dependencies": {
82
82
  "@koa/cors": "3.4.3",
83
83
  "@koa/router": "10.1.1",
84
- "@strapi/admin": "4.10.0-beta.0",
85
- "@strapi/data-transfer": "4.10.0-beta.0",
86
- "@strapi/database": "4.10.0-beta.0",
87
- "@strapi/generate-new": "4.10.0-beta.0",
88
- "@strapi/generators": "4.10.0-beta.0",
89
- "@strapi/logger": "4.10.0-beta.0",
90
- "@strapi/permissions": "4.10.0-beta.0",
91
- "@strapi/plugin-content-manager": "4.10.0-beta.0",
92
- "@strapi/plugin-content-type-builder": "4.10.0-beta.0",
93
- "@strapi/plugin-email": "4.10.0-beta.0",
94
- "@strapi/plugin-upload": "4.10.0-beta.0",
95
- "@strapi/typescript-utils": "4.10.0-beta.0",
96
- "@strapi/utils": "4.10.0-beta.0",
84
+ "@strapi/admin": "4.10.0-beta.1",
85
+ "@strapi/data-transfer": "4.10.0-beta.1",
86
+ "@strapi/database": "4.10.0-beta.1",
87
+ "@strapi/generate-new": "4.10.0-beta.1",
88
+ "@strapi/generators": "4.10.0-beta.1",
89
+ "@strapi/logger": "4.10.0-beta.1",
90
+ "@strapi/permissions": "4.10.0-beta.1",
91
+ "@strapi/plugin-content-manager": "4.10.0-beta.1",
92
+ "@strapi/plugin-content-type-builder": "4.10.0-beta.1",
93
+ "@strapi/plugin-email": "4.10.0-beta.1",
94
+ "@strapi/plugin-upload": "4.10.0-beta.1",
95
+ "@strapi/typescript-utils": "4.10.0-beta.1",
96
+ "@strapi/utils": "4.10.0-beta.1",
97
97
  "bcryptjs": "2.4.3",
98
98
  "boxen": "5.1.2",
99
99
  "chalk": "4.1.2",
@@ -128,7 +128,7 @@
128
128
  "open": "8.4.0",
129
129
  "ora": "5.4.1",
130
130
  "package-json": "7.0.0",
131
- "qs": "6.11.0",
131
+ "qs": "6.11.1",
132
132
  "resolve-cwd": "3.0.0",
133
133
  "semver": "7.3.8",
134
134
  "statuses": "2.0.1",
@@ -142,5 +142,5 @@
142
142
  "node": ">=14.19.1 <=18.x.x",
143
143
  "npm": ">=6.0.0"
144
144
  },
145
- "gitHead": "1519ef0e56d27b738f24fc88223797651ad47aaf"
145
+ "gitHead": "95d581b31bee464af42e5d8db408fa578d8532c7"
146
146
  }