@strapi/strapi 4.6.0 → 4.7.0-beta.0

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
@@ -18,8 +18,9 @@ const {
18
18
  promptEncryptionKey,
19
19
  confirmMessage,
20
20
  forceOption,
21
+ parseURL,
21
22
  } = require('../lib/commands/utils/commander');
22
- const { exitWith } = require('../lib/commands/utils/helpers');
23
+ const { exitWith, ifOptions, assertUrlHasProtocol } = require('../lib/commands/utils/helpers');
23
24
  const {
24
25
  excludeOption,
25
26
  onlyOption,
@@ -272,59 +273,88 @@ program
272
273
  .option('-s, --silent', `Run the generation silently, without any output`, false)
273
274
  .action(getLocalScript('ts/generate-types'));
274
275
 
275
- // if (process.env.STRAPI_EXPERIMENTAL === 'true') {
276
- // // `$ strapi transfer`
277
- // program
278
- // .command('transfer')
279
- // .description('Transfer data from one source to another')
280
- // .allowExcessArguments(false)
281
- // .addOption(
282
- // new Option(
283
- // '--from <sourceURL>',
284
- // `URL of the remote Strapi instance to get data from`
285
- // ).argParser(parseURL)
286
- // )
287
- // .addOption(
288
- // new Option(
289
- // '--to <destinationURL>',
290
- // `URL of the remote Strapi instance to send data to`
291
- // ).argParser(parseURL)
292
- // )
293
- // .addOption(forceOption)
294
- // // Validate URLs
295
- // .hook(
296
- // 'preAction',
297
- // ifOptions(
298
- // (opts) => opts.from,
299
- // (thisCommand) => assertUrlHasProtocol(thisCommand.opts().from, ['https:', 'http:'])
300
- // )
301
- // )
302
- // .hook(
303
- // 'preAction',
304
- // ifOptions(
305
- // (opts) => opts.to,
306
- // (thisCommand) => assertUrlHasProtocol(thisCommand.opts().to, ['https:', 'http:'])
307
- // )
308
- // )
309
- // .hook(
310
- // 'preAction',
311
- // ifOptions(
312
- // (opts) => !opts.from && !opts.to,
313
- // () => exitWith(1, 'At least one source (from) or destination (to) option must be provided')
314
- // )
315
- // )
316
- // .addOption(forceOption)
317
- // .addOption(excludeOption)
318
- // .addOption(onlyOption)
319
- // .hook('preAction', validateExcludeOnly)
320
- // .hook(
321
- // 'preAction',
322
- // confirmMessage(
323
- // 'The import will delete all data in the remote database. Are you sure you want to proceed?'
324
- // )
325
- // )
326
- // .action(getLocalScript('transfer/transfer'));
327
- // }
276
+ // `$ strapi transfer`
277
+ program
278
+ .command('transfer')
279
+ .description('Transfer data from one source to another')
280
+ .allowExcessArguments(false)
281
+ .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
288
+ )
289
+ .addOption(
290
+ new Option(
291
+ '--to <destinationURL>',
292
+ `URL of the remote Strapi instance to send data to`
293
+ ).argParser(parseURL)
294
+ )
295
+ .addOption(new Option('--to-token <token>', `Transfer token for the remote Strapi destination`))
296
+ .addOption(forceOption)
297
+ .addOption(excludeOption)
298
+ .addOption(onlyOption)
299
+ .hook('preAction', validateExcludeOnly)
300
+ // If --from is used, validate the URL and token
301
+ .hook(
302
+ 'preAction',
303
+ ifOptions(
304
+ (opts) => opts.from,
305
+ async (thisCommand) => {
306
+ assertUrlHasProtocol(thisCommand.opts().from, ['https:', 'http:']);
307
+ if (!thisCommand.opts().fromToken) {
308
+ const answers = await inquirer.prompt([
309
+ {
310
+ type: 'password',
311
+ message: 'Please enter your transfer token for the remote Strapi source',
312
+ name: 'fromToken',
313
+ },
314
+ ]);
315
+ if (!answers.fromToken?.length) {
316
+ exitWith(0, 'No token entered, aborting transfer.');
317
+ }
318
+ thisCommand.opts().fromToken = answers.fromToken;
319
+ }
320
+ }
321
+ )
322
+ )
323
+ // If --to is used, validate the URL, token, and confirm restore
324
+ .hook(
325
+ 'preAction',
326
+ ifOptions(
327
+ (opts) => opts.to,
328
+ async (thisCommand) => {
329
+ assertUrlHasProtocol(thisCommand.opts().to, ['https:', 'http:']);
330
+ if (!thisCommand.opts().toToken) {
331
+ const answers = await inquirer.prompt([
332
+ {
333
+ type: 'password',
334
+ message: 'Please enter your transfer token for the remote Strapi destination',
335
+ name: 'toToken',
336
+ },
337
+ ]);
338
+ if (!answers.toToken?.length) {
339
+ exitWith(0, 'No token entered, aborting transfer.');
340
+ }
341
+ thisCommand.opts().toToken = answers.toToken;
342
+ }
343
+
344
+ await confirmMessage(
345
+ 'The transfer will delete all data in the remote database and media files. Are you sure you want to proceed?'
346
+ )(thisCommand);
347
+ }
348
+ )
349
+ )
350
+ // .hook(
351
+ // 'preAction',
352
+ // ifOptions(
353
+ // (opts) => !opts.from && !opts.to,
354
+ // () => exitWith(1, 'At least one source (from) or destination (to) option must be provided')
355
+ // )
356
+ // )
357
+ .action(getLocalScript('transfer/transfer'));
328
358
 
329
359
  // `$ strapi export`
330
360
  program
@@ -409,11 +439,18 @@ program
409
439
  } else {
410
440
  thisCommand.opts().decompress = false;
411
441
  }
442
+
443
+ if (extname(file) !== '.tar') {
444
+ exitWith(
445
+ 1,
446
+ `The file '${opts.file}' does not appear to be a valid Strapi data file. It must have an extension ending in .tar[.gz][.enc]`
447
+ );
448
+ }
412
449
  })
413
450
  .hook(
414
451
  'preAction',
415
452
  confirmMessage(
416
- 'The import will delete all data in your database. Are you sure you want to proceed?'
453
+ 'The import will delete all data in your database and media files. Are you sure you want to proceed?'
417
454
  )
418
455
  )
419
456
  .action(getLocalScript('transfer/import'));
@@ -19,6 +19,7 @@ const {
19
19
  createStrapiInstance,
20
20
  formatDiagnostic,
21
21
  } = require('./utils');
22
+ const { exitWith } = require('../utils/helpers');
22
23
 
23
24
  /**
24
25
  * @typedef ExportCommandOptions Options given to the CLI import command
@@ -29,8 +30,6 @@ const {
29
30
  * @property {boolean} [compress] Used to compress the final archive
30
31
  */
31
32
 
32
- const logger = console;
33
-
34
33
  const BYTES_IN_MB = 1024 * 1024;
35
34
 
36
35
  /**
@@ -43,8 +42,7 @@ const BYTES_IN_MB = 1024 * 1024;
43
42
  module.exports = async (opts) => {
44
43
  // Validate inputs from Commander
45
44
  if (!isObject(opts)) {
46
- logger.error('Could not parse command arguments');
47
- process.exit(1);
45
+ exitWith(1, 'Could not parse command arguments');
48
46
  }
49
47
 
50
48
  const strapi = await createStrapiInstance();
@@ -92,33 +90,34 @@ module.exports = async (opts) => {
92
90
  };
93
91
 
94
92
  progress.on('transfer::start', async () => {
95
- logger.log(`Starting export...`);
93
+ console.log(`Starting export...`);
96
94
  await strapi.telemetry.send('didDEITSProcessStart', getTelemetryPayload());
97
95
  });
98
96
 
97
+ let results;
98
+ let outFile;
99
99
  try {
100
- const results = await engine.transfer();
101
- const outFile = results.destination.file.path;
102
-
103
- const table = buildTransferTable(results.engine);
104
- logger.log(table.toString());
105
-
100
+ results = await engine.transfer();
101
+ outFile = results.destination.file.path;
106
102
  const outFileExists = await fs.pathExists(outFile);
107
103
  if (!outFileExists) {
108
104
  throw new TransferEngineTransferError(`Export file not created "${outFile}"`);
109
105
  }
110
-
111
- logger.log(`${chalk.bold('Export process has been completed successfully!')}`);
112
- logger.log(`Export archive is in ${chalk.green(outFile)}`);
113
106
  } catch {
114
107
  await strapi.telemetry.send('didDEITSProcessFail', getTelemetryPayload());
115
- logger.error('Export process failed.');
116
- process.exit(1);
108
+ exitWith(1, 'Export process failed.');
117
109
  }
118
110
 
119
- // Note: Telemetry can't be sent in a finish event, because it runs async after this block but we can't await it, so if process.exit is used it won't send
120
111
  await strapi.telemetry.send('didDEITSProcessFinish', getTelemetryPayload());
121
- process.exit(0);
112
+ try {
113
+ const table = buildTransferTable(results.engine);
114
+ console.log(table.toString());
115
+ } catch (e) {
116
+ console.error('There was an error displaying the results of the transfer.');
117
+ }
118
+
119
+ console.log(`${chalk.bold('Export process has been completed successfully!')}`);
120
+ exitWith(0, `Export archive is in ${chalk.green(outFile)}`);
122
121
  };
123
122
 
124
123
  /**
@@ -20,18 +20,16 @@ const {
20
20
  createStrapiInstance,
21
21
  formatDiagnostic,
22
22
  } = require('./utils');
23
+ const { exitWith } = require('../utils/helpers');
23
24
 
24
25
  /**
25
26
  * @typedef {import('@strapi/data-transfer').ILocalFileSourceProviderOptions} ILocalFileSourceProviderOptions
26
27
  */
27
28
 
28
- const logger = console;
29
-
30
29
  module.exports = async (opts) => {
31
30
  // validate inputs from Commander
32
31
  if (!isObject(opts)) {
33
- logger.error('Could not parse arguments');
34
- process.exit(1);
32
+ exitWith(1, 'Could not parse arguments');
35
33
  }
36
34
 
37
35
  /**
@@ -100,27 +98,30 @@ module.exports = async (opts) => {
100
98
  };
101
99
 
102
100
  progress.on('transfer::start', async () => {
103
- logger.info('Starting import...');
101
+ console.log('Starting import...');
104
102
  await strapiInstance.telemetry.send('didDEITSProcessStart', getTelemetryPayload());
105
103
  });
106
104
 
105
+ let results;
107
106
  try {
108
- const results = await engine.transfer();
109
- const table = buildTransferTable(results.engine);
110
- logger.info(table.toString());
111
-
112
- logger.info('Import process has been completed successfully!');
107
+ results = await engine.transfer();
113
108
  } catch (e) {
114
109
  await strapiInstance.telemetry.send('didDEITSProcessFail', getTelemetryPayload());
115
- logger.error('Import process failed.');
110
+ console.error('Import process failed.');
116
111
  process.exit(1);
117
112
  }
118
113
 
119
- // Note: Telemetry can't be sent in a finish event, because it runs async after this block but we can't await it, so if process.exit is used it won't send
114
+ try {
115
+ const table = buildTransferTable(results.engine);
116
+ console.log(table.toString());
117
+ } catch (e) {
118
+ console.error('There was an error displaying the results of the transfer.');
119
+ }
120
+
120
121
  await strapiInstance.telemetry.send('didDEITSProcessFinish', getTelemetryPayload());
121
122
  await strapiInstance.destroy();
122
123
 
123
- process.exit(0);
124
+ exitWith(0, 'Import process has been completed successfully!');
124
125
  };
125
126
 
126
127
  /**
@@ -17,28 +17,28 @@ const {
17
17
  DEFAULT_IGNORED_CONTENT_TYPES,
18
18
  formatDiagnostic,
19
19
  } = require('./utils');
20
-
21
- const logger = console;
20
+ const { exitWith } = require('../utils/helpers');
22
21
 
23
22
  /**
24
23
  * @typedef TransferCommandOptions Options given to the CLI transfer command
25
24
  *
26
25
  * @property {URL|undefined} [to] The url of a remote Strapi to use as remote destination
27
26
  * @property {URL|undefined} [from] The url of a remote Strapi to use as remote source
27
+ * @property {string|undefined} [toToken] The transfer token for the remote Strapi destination
28
+ * @property {string|undefined} [fromToken] The transfer token for the remote Strapi source
28
29
  */
29
30
 
30
31
  /**
31
32
  * Transfer command.
32
33
  *
33
- * It transfers data from a local file to a local strapi instance
34
+ * Transfers data between local Strapi and remote Strapi instances
34
35
  *
35
36
  * @param {TransferCommandOptions} opts
36
37
  */
37
38
  module.exports = async (opts) => {
38
39
  // Validate inputs from Commander
39
40
  if (!isObject(opts)) {
40
- logger.error('Could not parse command arguments');
41
- process.exit(1);
41
+ exitWith(1, 'Could not parse command arguments');
42
42
  }
43
43
 
44
44
  const strapi = await createStrapiInstance();
@@ -47,8 +47,7 @@ module.exports = async (opts) => {
47
47
  let destination;
48
48
 
49
49
  if (!opts.from && !opts.to) {
50
- logger.error('At least one source (from) or destination (to) option must be provided');
51
- process.exit(1);
50
+ exitWith(1, 'At least one source (from) or destination (to) option must be provided');
52
51
  }
53
52
 
54
53
  // if no URL provided, use local Strapi
@@ -59,8 +58,7 @@ module.exports = async (opts) => {
59
58
  }
60
59
  // if URL provided, set up a remote source provider
61
60
  else {
62
- logger.error(`Remote Strapi source provider not yet implemented`);
63
- process.exit(1);
61
+ exitWith(1, `Remote Strapi source provider not yet implemented`);
64
62
  }
65
63
 
66
64
  // if no URL provided, use local Strapi
@@ -71,9 +69,16 @@ module.exports = async (opts) => {
71
69
  }
72
70
  // if URL provided, set up a remote destination provider
73
71
  else {
72
+ if (!opts.toToken) {
73
+ exitWith(1, 'Missing token for remote destination');
74
+ }
75
+
74
76
  destination = createRemoteStrapiDestinationProvider({
75
77
  url: opts.to,
76
- auth: false,
78
+ auth: {
79
+ type: 'token',
80
+ token: opts.toToken,
81
+ },
77
82
  strategy: 'restore',
78
83
  restore: {
79
84
  entities: { exclude: DEFAULT_IGNORED_CONTENT_TYPES },
@@ -82,12 +87,11 @@ module.exports = async (opts) => {
82
87
  }
83
88
 
84
89
  if (!source || !destination) {
85
- logger.error('Could not create providers');
86
- process.exit(1);
90
+ exitWith(1, 'Could not create providers');
87
91
  }
88
92
 
89
93
  const engine = createTransferEngine(source, destination, {
90
- versionStrategy: 'strict',
94
+ versionStrategy: 'exact',
91
95
  schemaStrategy: 'strict',
92
96
  transforms: {
93
97
  links: [
@@ -112,18 +116,15 @@ module.exports = async (opts) => {
112
116
 
113
117
  engine.diagnostics.onDiagnostic(formatDiagnostic('transfer'));
114
118
 
119
+ let results;
115
120
  try {
116
- logger.log(`Starting transfer...`);
117
-
118
- const results = await engine.transfer();
119
-
120
- const table = buildTransferTable(results.engine);
121
- logger.log(table.toString());
122
-
123
- logger.log(`${chalk.bold('Transfer process has been completed successfully!')}`);
124
- process.exit(0);
121
+ console.log(`Starting transfer...`);
122
+ results = await engine.transfer();
125
123
  } catch (e) {
126
- logger.error('Transfer process failed.');
127
- process.exit(1);
124
+ exitWith(1, 'Transfer process failed.');
128
125
  }
126
+
127
+ const table = buildTransferTable(results.engine);
128
+ console.log(table.toString());
129
+ exitWith(0, `${chalk.bold('Transfer process has been completed successfully!')}`);
129
130
  };
@@ -82,6 +82,8 @@ const DEFAULT_IGNORED_CONTENT_TYPES = [
82
82
  'admin::role',
83
83
  'admin::api-token',
84
84
  'admin::api-token-permission',
85
+ 'admin::transfer-token',
86
+ 'admin::transfer-token-permission',
85
87
  'admin::audit-log',
86
88
  ];
87
89
 
@@ -36,22 +36,29 @@ const readableBytes = (bytes, decimals = 1, padStart = 0) => {
36
36
  *
37
37
  * @param {number} code Code to exit process with
38
38
  * @param {string | Array} message Message(s) to display before exiting
39
+ * @param {Object} options
40
+ * @param {console} options.logger - logger object, defaults to console
41
+ * @param {process} options.prc - process object, defaults to process
42
+ *
39
43
  */
40
- const exitWith = (code, message = undefined) => {
41
- const logger = (message) => {
44
+ const exitWith = (code, message = undefined, options = {}) => {
45
+ const { logger = console, prc = process } = options;
46
+
47
+ const log = (message) => {
42
48
  if (code === 0) {
43
- console.log(chalk.green(message));
49
+ logger.log(chalk.green(message));
44
50
  } else {
45
- console.error(chalk.red(message));
51
+ logger.error(chalk.red(message));
46
52
  }
47
53
  };
48
54
 
49
55
  if (isString(message)) {
50
- logger(message);
56
+ log(message);
51
57
  } else if (isArray(message)) {
52
- message.forEach((msg) => logger(msg));
58
+ message.forEach((msg) => log(msg));
53
59
  }
54
- process.exit(code);
60
+
61
+ prc.exit(code);
55
62
  };
56
63
 
57
64
  /**
@@ -43,7 +43,28 @@ const createAuthentication = () => {
43
43
  return next();
44
44
  }
45
45
 
46
- const strategiesToUse = strategies[route.info.type];
46
+ const routeStrategies = strategies[route.info.type];
47
+ const configStrategies = config?.strategies ?? routeStrategies ?? [];
48
+
49
+ const strategiesToUse = configStrategies.reduce((acc, strategy) => {
50
+ // Resolve by strategy name
51
+ if (typeof strategy === 'string') {
52
+ const routeStrategy = routeStrategies.find((rs) => rs.name === strategy);
53
+
54
+ if (routeStrategy) {
55
+ acc.push(routeStrategy);
56
+ }
57
+ }
58
+
59
+ // Use the given strategy as is
60
+ else if (typeof strategy === 'object') {
61
+ validStrategy(strategy);
62
+
63
+ acc.push(strategy);
64
+ }
65
+
66
+ return acc;
67
+ }, []);
47
68
 
48
69
  for (const strategy of strategiesToUse) {
49
70
  const result = await strategy.authenticate(ctx);
@@ -3,10 +3,12 @@
3
3
  const _ = require('lodash');
4
4
  const { has, prop, omit, toString, pipe, assign } = require('lodash/fp');
5
5
 
6
- const { contentTypes: contentTypesUtils } = require('@strapi/utils');
6
+ const { contentTypes: contentTypesUtils, mapAsync } = require('@strapi/utils');
7
7
  const { ApplicationError } = require('@strapi/utils').errors;
8
8
  const { getComponentAttributes } = require('@strapi/utils').contentTypes;
9
9
 
10
+ const isDialectMySQL = () => strapi.db.dialect.client === 'mysql';
11
+
10
12
  const omitComponentData = (contentType, data) => {
11
13
  const { attributes } = contentType;
12
14
  const componentAttributes = Object.keys(attributes).filter((attributeName) =>
@@ -43,10 +45,12 @@ const createComponents = async (uid, data) => {
43
45
  throw new Error('Expected an array to create repeatable component');
44
46
  }
45
47
 
46
- const components = [];
47
- for (const value of componentValue) {
48
- components.push(await createComponent(componentUID, value));
49
- }
48
+ // MySQL/MariaDB can cause deadlocks here if concurrency higher than 1
49
+ const components = await mapAsync(
50
+ componentValue,
51
+ (value) => createComponent(componentUID, value),
52
+ { concurrency: isDialectMySQL() ? 1 : Infinity }
53
+ );
50
54
 
51
55
  componentBody[attributeName] = components.map(({ id }) => {
52
56
  return {
@@ -78,19 +82,23 @@ const createComponents = async (uid, data) => {
78
82
  throw new Error('Expected an array to create repeatable component');
79
83
  }
80
84
 
81
- const dynamicZoneData = [];
82
- for (const value of dynamiczoneValues) {
85
+ const createDynamicZoneComponents = async (value) => {
83
86
  const { id } = await createComponent(value.__component, value);
84
- dynamicZoneData.push({
87
+ return {
85
88
  id,
86
89
  __component: value.__component,
87
90
  __pivot: {
88
91
  field: attributeName,
89
92
  },
90
- });
91
- }
93
+ };
94
+ };
92
95
 
93
- componentBody[attributeName] = dynamicZoneData;
96
+ // MySQL/MariaDB can cause deadlocks here if concurrency higher than 1
97
+ componentBody[attributeName] = await mapAsync(
98
+ dynamiczoneValues,
99
+ createDynamicZoneComponents,
100
+ { concurrency: isDialectMySQL() ? 1 : Infinity }
101
+ );
94
102
 
95
103
  continue;
96
104
  }
@@ -139,10 +147,12 @@ const updateComponents = async (uid, entityToUpdate, data) => {
139
147
  throw new Error('Expected an array to create repeatable component');
140
148
  }
141
149
 
142
- const components = [];
143
- for (const value of componentValue) {
144
- components.push(await updateOrCreateComponent(componentUID, value));
145
- }
150
+ // MySQL/MariaDB can cause deadlocks here if concurrency higher than 1
151
+ const components = await mapAsync(
152
+ componentValue,
153
+ (value) => updateOrCreateComponent(componentUID, value),
154
+ { concurrency: isDialectMySQL() ? 1 : Infinity }
155
+ );
146
156
 
147
157
  componentBody[attributeName] = components.filter(_.negate(_.isNil)).map(({ id }) => {
148
158
  return {
@@ -176,19 +186,22 @@ const updateComponents = async (uid, entityToUpdate, data) => {
176
186
  throw new Error('Expected an array to create repeatable component');
177
187
  }
178
188
 
179
- const dynamicZoneData = [];
180
- for (const value of dynamiczoneValues) {
181
- const { id } = await updateOrCreateComponent(value.__component, value);
182
- dynamicZoneData.push({
183
- id,
184
- __component: value.__component,
185
- __pivot: {
186
- field: attributeName,
187
- },
188
- });
189
- }
189
+ // MySQL/MariaDB can cause deadlocks here if concurrency higher than 1
190
+ componentBody[attributeName] = await mapAsync(
191
+ dynamiczoneValues,
192
+ async (value) => {
193
+ const { id } = await updateOrCreateComponent(value.__component, value);
190
194
 
191
- componentBody[attributeName] = dynamicZoneData;
195
+ return {
196
+ id,
197
+ __component: value.__component,
198
+ __pivot: {
199
+ field: attributeName,
200
+ },
201
+ };
202
+ },
203
+ { concurrency: isDialectMySQL() ? 1 : Infinity }
204
+ );
192
205
 
193
206
  continue;
194
207
  }
@@ -290,14 +303,18 @@ const deleteComponents = async (uid, entityToDelete, { loadComponents = true } =
290
303
 
291
304
  if (attribute.type === 'component') {
292
305
  const { component: componentUID } = attribute;
293
- for (const subValue of _.castArray(value)) {
294
- await deleteComponent(componentUID, subValue);
295
- }
306
+ // MySQL/MariaDB can cause deadlocks here if concurrency higher than 1
307
+ await mapAsync(_.castArray(value), (subValue) => deleteComponent(componentUID, subValue), {
308
+ concurrency: isDialectMySQL() ? 1 : Infinity,
309
+ });
296
310
  } else {
297
311
  // delete dynamic zone components
298
- for (const subValue of _.castArray(value)) {
299
- await deleteComponent(subValue.__component, subValue);
300
- }
312
+ // MySQL/MariaDB can cause deadlocks here if concurrency higher than 1
313
+ await mapAsync(
314
+ _.castArray(value),
315
+ (subValue) => deleteComponent(subValue.__component, subValue),
316
+ { concurrency: isDialectMySQL() ? 1 : Infinity }
317
+ );
301
318
  }
302
319
 
303
320
  continue;
@@ -3,6 +3,7 @@
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');
6
7
  const isDocker = require('is-docker');
7
8
  const fetch = require('node-fetch');
8
9
  const ciEnv = require('ci-info');
@@ -40,6 +41,14 @@ module.exports = (strapi) => {
40
41
  const serverRootPath = strapi.dirs.app.root;
41
42
  const adminRootPath = path.join(strapi.dirs.app.root, 'src', 'admin');
42
43
 
44
+ const getNumberOfDynamicZones = () => {
45
+ return pipe(
46
+ map('attributes'),
47
+ flatMap(values),
48
+ sumBy(propEq('type', 'dynamiczone'))
49
+ )(strapi.contentTypes);
50
+ };
51
+
43
52
  const anonymousUserProperties = {
44
53
  environment: strapi.config.environment,
45
54
  os: os.type(),
@@ -57,6 +66,9 @@ module.exports = (strapi) => {
57
66
  useTypescriptOnAdmin: isUsingTypeScriptSync(adminRootPath),
58
67
  projectId: uuid,
59
68
  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(),
60
72
  };
61
73
 
62
74
  addPackageJsonStrapiMetadata(anonymousGroupProperties, strapi);
@@ -30,7 +30,10 @@ export type ConfigurableAttribute = { configurable: true };
30
30
  export type NonConfigurableAttribute = { configurable: false };
31
31
 
32
32
  // custom field
33
- export type CustomField<T extends string, P extends object = undefined> = { customField: T, options?: P };
33
+ export type CustomField<T extends string, P extends object = undefined> = {
34
+ customField: T;
35
+ options?: P;
36
+ };
34
37
 
35
38
  // min/max
36
39
  export type SetMinMax<T extends MinMaxOption<U>, U = number> = T;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@strapi/strapi",
3
- "version": "4.6.0",
3
+ "version": "4.7.0-beta.0",
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.6.0",
85
- "@strapi/data-transfer": "4.6.0",
86
- "@strapi/database": "4.6.0",
87
- "@strapi/generate-new": "4.6.0",
88
- "@strapi/generators": "4.6.0",
89
- "@strapi/logger": "4.6.0",
90
- "@strapi/permissions": "4.6.0",
91
- "@strapi/plugin-content-manager": "4.6.0",
92
- "@strapi/plugin-content-type-builder": "4.6.0",
93
- "@strapi/plugin-email": "4.6.0",
94
- "@strapi/plugin-upload": "4.6.0",
95
- "@strapi/typescript-utils": "4.6.0",
96
- "@strapi/utils": "4.6.0",
84
+ "@strapi/admin": "4.7.0-beta.0",
85
+ "@strapi/data-transfer": "4.7.0-beta.0",
86
+ "@strapi/database": "4.7.0-beta.0",
87
+ "@strapi/generate-new": "4.7.0-beta.0",
88
+ "@strapi/generators": "4.7.0-beta.0",
89
+ "@strapi/logger": "4.7.0-beta.0",
90
+ "@strapi/permissions": "4.7.0-beta.0",
91
+ "@strapi/plugin-content-manager": "4.7.0-beta.0",
92
+ "@strapi/plugin-content-type-builder": "4.7.0-beta.0",
93
+ "@strapi/plugin-email": "4.7.0-beta.0",
94
+ "@strapi/plugin-upload": "4.7.0-beta.0",
95
+ "@strapi/typescript-utils": "4.7.0-beta.0",
96
+ "@strapi/utils": "4.7.0-beta.0",
97
97
  "bcryptjs": "2.4.3",
98
98
  "boxen": "5.1.2",
99
99
  "chalk": "4.1.2",
@@ -109,7 +109,7 @@
109
109
  "fs-extra": "10.0.0",
110
110
  "glob": "7.2.0",
111
111
  "http-errors": "1.8.1",
112
- "inquirer": "8.2.4",
112
+ "inquirer": "8.2.5",
113
113
  "is-docker": "2.2.1",
114
114
  "koa": "2.13.4",
115
115
  "koa-body": "4.2.0",
@@ -142,5 +142,5 @@
142
142
  "node": ">=14.19.1 <=18.x.x",
143
143
  "npm": ">=6.0.0"
144
144
  },
145
- "gitHead": "a9e55435c489f3379d88565bf3f729deb29bfb45"
145
+ "gitHead": "880ba7af867ad43c4cc45b47467b76a9b5606b6e"
146
146
  }