@strapi/strapi 4.6.0-alpha.1 → 4.6.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
@@ -5,13 +5,16 @@
5
5
  // FIXME
6
6
  /* eslint-disable import/extensions */
7
7
  const _ = require('lodash');
8
+ const path = require('path');
8
9
  const resolveCwd = require('resolve-cwd');
9
10
  const { yellow } = require('chalk');
10
- const { Command } = require('commander');
11
+ const { Command, Option } = require('commander');
12
+ const inquirer = require('inquirer');
11
13
 
12
14
  const program = new Command();
13
15
 
14
16
  const packageJSON = require('../package.json');
17
+ const { promptEncryptionKey, confirmMessage } = require('../lib/commands/utils/commander');
15
18
 
16
19
  const checkCwdIsStrapiApp = (name) => {
17
20
  const logErrorAndExit = () => {
@@ -255,4 +258,68 @@ program
255
258
  .option('-s, --silent', `Run the generation silently, without any output`, false)
256
259
  .action(getLocalScript('ts/generate-types'));
257
260
 
261
+ // `$ strapi export`
262
+ program
263
+ .command('export')
264
+ .description('Export data from Strapi to file')
265
+ .addOption(
266
+ new Option('--no-encrypt', `Disables 'aes-128-ecb' encryption of the output file`).default(true)
267
+ )
268
+ .addOption(new Option('--no-compress', 'Disables gzip compression of output file').default(true))
269
+ .addOption(
270
+ new Option(
271
+ '-k, --key <string>',
272
+ 'Provide encryption key in command instead of using the prompt'
273
+ )
274
+ )
275
+ .addOption(new Option('-f, --file <file>', 'name to use for exported file (without extensions)'))
276
+ .allowExcessArguments(false)
277
+ .hook('preAction', promptEncryptionKey)
278
+ .action(getLocalScript('transfer/export'));
279
+
280
+ // `$ strapi import`
281
+ program
282
+ .command('import')
283
+ .description('Import data from file to Strapi')
284
+ .requiredOption(
285
+ '-f, --file <file>',
286
+ 'path and filename for the Strapi export file you want to import'
287
+ )
288
+ .addOption(
289
+ new Option(
290
+ '-k, --key <string>',
291
+ 'Provide encryption key in command instead of using the prompt'
292
+ )
293
+ )
294
+ .allowExcessArguments(false)
295
+ .hook('preAction', async (thisCommand) => {
296
+ const opts = thisCommand.opts();
297
+ const ext = path.extname(String(opts.file));
298
+
299
+ // check extension to guess if we should prompt for key
300
+ if (ext === '.enc') {
301
+ if (!opts.key) {
302
+ const answers = await inquirer.prompt([
303
+ {
304
+ type: 'password',
305
+ message: 'Please enter your decryption key',
306
+ name: 'key',
307
+ },
308
+ ]);
309
+ if (!answers.key?.length) {
310
+ console.log('No key entered, aborting import.');
311
+ process.exit(0);
312
+ }
313
+ opts.key = answers.key;
314
+ }
315
+ }
316
+ })
317
+ .hook(
318
+ 'preAction',
319
+ confirmMessage(
320
+ 'The import will delete all data in your database. Are you sure you want to proceed?'
321
+ )
322
+ )
323
+ .action(getLocalScript('transfer/import'));
324
+
258
325
  program.parseAsync(process.argv);
package/lib/Strapi.js CHANGED
@@ -225,7 +225,7 @@ class Strapi {
225
225
 
226
226
  await this.runLifecyclesFunctions(LIFECYCLES.DESTROY);
227
227
 
228
- this.eventHub.destroy();
228
+ this.eventHub.removeAllListeners();
229
229
 
230
230
  if (_.has(this, 'db')) {
231
231
  await this.db.destroy();
@@ -242,11 +242,14 @@ class Strapi {
242
242
  sendStartupTelemetry() {
243
243
  // Emit started event.
244
244
  // do not await to avoid slower startup
245
+ // This event is anonymous
245
246
  this.telemetry.send('didStartServer', {
246
- database: strapi.config.get('database.connection.client'),
247
- plugins: Object.keys(strapi.plugins),
248
- // TODO: to add back
249
- // providers: this.config.installedProviders,
247
+ groupProperties: {
248
+ database: strapi.config.get('database.connection.client'),
249
+ plugins: Object.keys(strapi.plugins),
250
+ // TODO: to add back
251
+ // providers: this.config.installedProviders,
252
+ },
250
253
  });
251
254
  }
252
255
 
@@ -53,12 +53,12 @@ const generateNewPackageJSON = (packageObj) => {
53
53
 
54
54
  const sendEvent = async (uuid) => {
55
55
  try {
56
- await fetch('https://analytics.strapi.io/track', {
56
+ await fetch('https://analytics.strapi.io/api/v2/track', {
57
57
  method: 'POST',
58
58
  body: JSON.stringify({
59
59
  event: 'didOptInTelemetry',
60
- uuid,
61
60
  deviceId: machineID(),
61
+ groupProperties: { projectId: uuid },
62
62
  }),
63
63
  headers: { 'Content-Type': 'application/json' },
64
64
  });
@@ -28,12 +28,12 @@ const writePackageJSON = async (path, file, spacing) => {
28
28
 
29
29
  const sendEvent = async (uuid) => {
30
30
  try {
31
- await fetch('https://analytics.strapi.io/track', {
31
+ await fetch('https://analytics.strapi.io/api/v2/track', {
32
32
  method: 'POST',
33
33
  body: JSON.stringify({
34
34
  event: 'didOptOutTelemetry',
35
- uuid,
36
35
  deviceId: machineID(),
36
+ groupProperties: { projectId: uuid },
37
37
  }),
38
38
  headers: { 'Content-Type': 'application/json' },
39
39
  });
@@ -0,0 +1,161 @@
1
+ 'use strict';
2
+
3
+ const {
4
+ createLocalFileDestinationProvider,
5
+ createLocalStrapiSourceProvider,
6
+ createTransferEngine,
7
+ // TODO: we need to solve this issue with typescript modules
8
+ // eslint-disable-next-line import/no-unresolved, node/no-missing-require
9
+ } = require('@strapi/data-transfer');
10
+ const { isObject, isString, isFinite, toNumber } = require('lodash/fp');
11
+ const fs = require('fs-extra');
12
+ const chalk = require('chalk');
13
+
14
+ const {
15
+ getDefaultExportName,
16
+ buildTransferTable,
17
+ DEFAULT_IGNORED_CONTENT_TYPES,
18
+ createStrapiInstance,
19
+ } = require('./utils');
20
+
21
+ /**
22
+ * @typedef ImportCommandOptions Options given to the CLI import command
23
+ *
24
+ * @property {string} [file] The file path to import
25
+ * @property {boolean} [encrypt] Used to encrypt the final archive
26
+ * @property {string} [key] Encryption key, only useful when encryption is enabled
27
+ * @property {boolean} [compress] Used to compress the final archive
28
+ */
29
+
30
+ const logger = console;
31
+
32
+ const BYTES_IN_MB = 1024 * 1024;
33
+
34
+ /**
35
+ * Import command.
36
+ *
37
+ * It transfers data from a local file to a local strapi instance
38
+ *
39
+ * @param {ImportCommandOptions} opts
40
+ */
41
+ module.exports = async (opts) => {
42
+ // Validate inputs from Commander
43
+ if (!isObject(opts)) {
44
+ logger.error('Could not parse command arguments');
45
+ process.exit(1);
46
+ }
47
+
48
+ const strapi = await createStrapiInstance();
49
+
50
+ const source = createSourceProvider(strapi);
51
+ const destination = createDestinationProvider(opts);
52
+
53
+ const engine = createTransferEngine(source, destination, {
54
+ versionStrategy: 'ignore', // for an export to file, versionStrategy will always be skipped
55
+ schemaStrategy: 'ignore', // for an export to file, schemaStrategy will always be skipped
56
+ transforms: {
57
+ links: [
58
+ {
59
+ filter(link) {
60
+ return (
61
+ !DEFAULT_IGNORED_CONTENT_TYPES.includes(link.left.type) &&
62
+ !DEFAULT_IGNORED_CONTENT_TYPES.includes(link.right.type)
63
+ );
64
+ },
65
+ },
66
+ ],
67
+ entities: [
68
+ {
69
+ filter(entity) {
70
+ return !DEFAULT_IGNORED_CONTENT_TYPES.includes(entity.type);
71
+ },
72
+ },
73
+ ],
74
+ },
75
+ });
76
+
77
+ try {
78
+ logger.log(`Starting export...`);
79
+
80
+ const progress = engine.progress.stream;
81
+
82
+ const telemetryPayload = (/* payload */) => {
83
+ return {
84
+ eventProperties: {
85
+ source: engine.sourceProvider.name,
86
+ destination: engine.destinationProvider.name,
87
+ },
88
+ };
89
+ };
90
+
91
+ progress.on('transfer::start', (payload) => {
92
+ strapi.telemetry.send('didDEITSProcessStart', telemetryPayload(payload));
93
+ });
94
+
95
+ progress.on('transfer::finish', (payload) => {
96
+ strapi.telemetry.send('didDEITSProcessFinish', telemetryPayload(payload));
97
+ });
98
+
99
+ progress.on('transfer::error', (payload) => {
100
+ strapi.telemetry.send('didDEITSProcessFail', telemetryPayload(payload));
101
+ });
102
+
103
+ const results = await engine.transfer();
104
+ const outFile = results.destination.file.path;
105
+
106
+ const table = buildTransferTable(results.engine);
107
+ logger.log(table.toString());
108
+
109
+ const outFileExists = await fs.pathExists(outFile);
110
+ if (!outFileExists) {
111
+ throw new Error(`Export file not created "${outFile}"`);
112
+ }
113
+
114
+ logger.log(`${chalk.bold('Export process has been completed successfully!')}`);
115
+ logger.log(`Export archive is in ${chalk.green(outFile)}`);
116
+ process.exit(0);
117
+ } catch (e) {
118
+ logger.error('Export process failed unexpectedly:', e.toString());
119
+ process.exit(1);
120
+ }
121
+ };
122
+
123
+ /**
124
+ * It creates a local strapi destination provider
125
+ */
126
+ const createSourceProvider = (strapi) => {
127
+ return createLocalStrapiSourceProvider({
128
+ async getStrapi() {
129
+ return strapi;
130
+ },
131
+ });
132
+ };
133
+
134
+ /**
135
+ * It creates a local file destination provider based on the given options
136
+ *
137
+ * @param {ImportCommandOptions} opts
138
+ */
139
+ const createDestinationProvider = (opts) => {
140
+ const { file, compress, encrypt, key, maxSizeJsonl } = opts;
141
+
142
+ const filepath = isString(file) && file.length > 0 ? file : getDefaultExportName();
143
+
144
+ const maxSizeJsonlInMb = isFinite(toNumber(maxSizeJsonl))
145
+ ? toNumber(maxSizeJsonl) * BYTES_IN_MB
146
+ : undefined;
147
+
148
+ return createLocalFileDestinationProvider({
149
+ file: {
150
+ path: filepath,
151
+ maxSizeJsonl: maxSizeJsonlInMb,
152
+ },
153
+ encryption: {
154
+ enabled: encrypt,
155
+ key: encrypt ? key : undefined,
156
+ },
157
+ compression: {
158
+ enabled: compress,
159
+ },
160
+ });
161
+ };
@@ -0,0 +1,152 @@
1
+ 'use strict';
2
+
3
+ const {
4
+ createLocalFileSourceProvider,
5
+ createLocalStrapiDestinationProvider,
6
+ createTransferEngine,
7
+ DEFAULT_VERSION_STRATEGY,
8
+ DEFAULT_SCHEMA_STRATEGY,
9
+ DEFAULT_CONFLICT_STRATEGY,
10
+ // TODO: we need to solve this issue with typescript modules
11
+ // eslint-disable-next-line import/no-unresolved, node/no-missing-require
12
+ } = require('@strapi/data-transfer');
13
+ const { isObject } = require('lodash/fp');
14
+ const path = require('path');
15
+
16
+ const strapi = require('../../index');
17
+ const { buildTransferTable, DEFAULT_IGNORED_CONTENT_TYPES } = require('./utils');
18
+
19
+ /**
20
+ * @typedef {import('@strapi/data-transfer').ILocalFileSourceProviderOptions} ILocalFileSourceProviderOptions
21
+ */
22
+
23
+ const logger = console;
24
+
25
+ module.exports = async (opts) => {
26
+ // validate inputs from Commander
27
+ if (!isObject(opts)) {
28
+ logger.error('Could not parse arguments');
29
+ process.exit(1);
30
+ }
31
+
32
+ /**
33
+ * From strapi backup file
34
+ */
35
+ const sourceOptions = getLocalFileSourceOptions(opts);
36
+
37
+ const source = createLocalFileSourceProvider(sourceOptions);
38
+
39
+ /**
40
+ * To local Strapi instance
41
+ */
42
+ const strapiInstance = await strapi(await strapi.compile()).load();
43
+
44
+ const destinationOptions = {
45
+ async getStrapi() {
46
+ return strapiInstance;
47
+ },
48
+ strategy: opts.conflictStrategy || DEFAULT_CONFLICT_STRATEGY,
49
+ restore: {
50
+ entities: { exclude: DEFAULT_IGNORED_CONTENT_TYPES },
51
+ },
52
+ };
53
+ const destination = createLocalStrapiDestinationProvider(destinationOptions);
54
+
55
+ /**
56
+ * Configure and run the transfer engine
57
+ */
58
+ const engineOptions = {
59
+ versionStrategy: opts.versionStrategy || DEFAULT_VERSION_STRATEGY,
60
+ schemaStrategy: opts.schemaStrategy || DEFAULT_SCHEMA_STRATEGY,
61
+ exclude: opts.exclude,
62
+ rules: {
63
+ links: [
64
+ {
65
+ filter(link) {
66
+ return (
67
+ !DEFAULT_IGNORED_CONTENT_TYPES.includes(link.left.type) &&
68
+ !DEFAULT_IGNORED_CONTENT_TYPES.includes(link.right.type)
69
+ );
70
+ },
71
+ },
72
+ ],
73
+ entities: [
74
+ {
75
+ filter: (entity) => !DEFAULT_IGNORED_CONTENT_TYPES.includes(entity.type),
76
+ },
77
+ ],
78
+ },
79
+ };
80
+ const engine = createTransferEngine(source, destination, engineOptions);
81
+
82
+ try {
83
+ logger.info('Starting import...');
84
+
85
+ const progress = engine.progress.stream;
86
+ const telemetryPayload = (/* payload */) => {
87
+ return {
88
+ eventProperties: {
89
+ source: engine.sourceProvider.name,
90
+ destination: engine.destinationProvider.name,
91
+ },
92
+ };
93
+ };
94
+
95
+ progress.on('transfer::start', (payload) => {
96
+ strapiInstance.telemetry.send('didDEITSProcessStart', telemetryPayload(payload));
97
+ });
98
+
99
+ progress.on('transfer::finish', (payload) => {
100
+ strapiInstance.telemetry.send('didDEITSProcessFinish', telemetryPayload(payload));
101
+ });
102
+
103
+ progress.on('transfer::error', (payload) => {
104
+ strapiInstance.telemetry.send('didDEITSProcessFail', telemetryPayload(payload));
105
+ });
106
+
107
+ const results = await engine.transfer();
108
+ const table = buildTransferTable(results.engine);
109
+ logger.info(table.toString());
110
+
111
+ logger.info('Import process has been completed successfully!');
112
+ process.exit(0);
113
+ } catch (e) {
114
+ logger.error('Import process failed unexpectedly:');
115
+ logger.error(e);
116
+ process.exit(1);
117
+ }
118
+ };
119
+
120
+ /**
121
+ * Infer local file source provider options based on a given filename
122
+ *
123
+ * @param {{ file: string; key?: string }} opts
124
+ *
125
+ * @return {ILocalFileSourceProviderOptions}
126
+ */
127
+ const getLocalFileSourceOptions = (opts) => {
128
+ /**
129
+ * @type {ILocalFileSourceProviderOptions}
130
+ */
131
+ const options = {
132
+ file: { path: opts.file },
133
+ compression: { enabled: false },
134
+ encryption: { enabled: false },
135
+ };
136
+
137
+ const { extname, parse } = path;
138
+
139
+ let file = options.file.path;
140
+
141
+ if (extname(file) === '.enc') {
142
+ file = parse(file).name;
143
+ options.encryption = { enabled: true, key: opts.key };
144
+ }
145
+
146
+ if (extname(file) === '.gz') {
147
+ file = parse(file).name;
148
+ options.compression = { enabled: true };
149
+ }
150
+
151
+ return options;
152
+ };
@@ -0,0 +1,94 @@
1
+ 'use strict';
2
+
3
+ const chalk = require('chalk');
4
+ const Table = require('cli-table3');
5
+ const { readableBytes } = require('../utils');
6
+ const strapi = require('../../index');
7
+
8
+ const pad = (n) => {
9
+ return (n < 10 ? '0' : '') + String(n);
10
+ };
11
+
12
+ const yyyymmddHHMMSS = () => {
13
+ const date = new Date();
14
+
15
+ return (
16
+ date.getFullYear() +
17
+ pad(date.getMonth() + 1) +
18
+ pad(date.getDate()) +
19
+ pad(date.getHours()) +
20
+ pad(date.getMinutes()) +
21
+ pad(date.getSeconds())
22
+ );
23
+ };
24
+
25
+ const getDefaultExportName = () => {
26
+ return `export_${yyyymmddHHMMSS()}`;
27
+ };
28
+
29
+ const buildTransferTable = (resultData) => {
30
+ // Build pretty table
31
+ const table = new Table({
32
+ head: ['Type', 'Count', 'Size'].map((text) => chalk.bold.blue(text)),
33
+ });
34
+
35
+ let totalBytes = 0;
36
+ let totalItems = 0;
37
+ Object.keys(resultData).forEach((key) => {
38
+ const item = resultData[key];
39
+
40
+ table.push([
41
+ { hAlign: 'left', content: chalk.bold(key) },
42
+ { hAlign: 'right', content: item.count },
43
+ { hAlign: 'right', content: `${readableBytes(item.bytes, 1, 11)} ` },
44
+ ]);
45
+ totalBytes += item.bytes;
46
+ totalItems += item.count;
47
+
48
+ if (item.aggregates) {
49
+ Object.keys(item.aggregates)
50
+ .sort()
51
+ .forEach((subkey) => {
52
+ const subitem = item.aggregates[subkey];
53
+
54
+ table.push([
55
+ { hAlign: 'left', content: `-- ${chalk.bold.grey(subkey)}` },
56
+ { hAlign: 'right', content: chalk.grey(subitem.count) },
57
+ { hAlign: 'right', content: chalk.grey(`(${readableBytes(subitem.bytes, 1, 11)})`) },
58
+ ]);
59
+ });
60
+ }
61
+ });
62
+ table.push([
63
+ { hAlign: 'left', content: chalk.bold.green('Total') },
64
+ { hAlign: 'right', content: chalk.bold.green(totalItems) },
65
+ { hAlign: 'right', content: `${chalk.bold.green(readableBytes(totalBytes, 1, 11))} ` },
66
+ ]);
67
+
68
+ return table;
69
+ };
70
+
71
+ const DEFAULT_IGNORED_CONTENT_TYPES = [
72
+ 'admin::permission',
73
+ 'admin::user',
74
+ 'admin::role',
75
+ 'admin::api-token',
76
+ 'admin::api-token-permission',
77
+ ];
78
+
79
+ const createStrapiInstance = async (logLevel = 'error') => {
80
+ const appContext = await strapi.compile();
81
+ const app = strapi(appContext);
82
+
83
+ app.log.level = logLevel;
84
+
85
+ return app.load();
86
+ };
87
+
88
+ module.exports = {
89
+ buildTransferTable,
90
+ getDefaultExportName,
91
+ yyyymmddHHMMSS,
92
+ DEFAULT_IGNORED_CONTENT_TYPES,
93
+ createStrapiInstance,
94
+ };
@@ -0,0 +1,73 @@
1
+ 'use strict';
2
+
3
+ const inquirer = require('inquirer');
4
+
5
+ /**
6
+ * argsParser: Parse a comma-delimited string as an array
7
+ */
8
+ const parseInputList = (value) => {
9
+ return value.split(',');
10
+ };
11
+
12
+ /**
13
+ * hook: if encrypt==true and key not provided, prompt for it
14
+ */
15
+ const promptEncryptionKey = async (thisCommand) => {
16
+ const opts = thisCommand.opts();
17
+
18
+ if (!opts.encrypt && opts.key) {
19
+ console.error('Key may not be present unless encryption is used');
20
+ process.exit(1);
21
+ }
22
+
23
+ // if encrypt==true but we have no key, prompt for it
24
+ if (opts.encrypt && !(opts.key && opts.key.length > 0)) {
25
+ try {
26
+ const answers = await inquirer.prompt([
27
+ {
28
+ type: 'password',
29
+ message: 'Please enter an encryption key',
30
+ name: 'key',
31
+ validate(key) {
32
+ if (key.length > 0) return true;
33
+
34
+ return 'Key must be present when using the encrypt option';
35
+ },
36
+ },
37
+ ]);
38
+ opts.key = answers.key;
39
+ } catch (e) {
40
+ console.error('Failed to get encryption key');
41
+ process.exit(1);
42
+ }
43
+ if (!opts.key) {
44
+ console.error('Failed to get encryption key');
45
+ process.exit(1);
46
+ }
47
+ }
48
+ };
49
+
50
+ /**
51
+ * hook: require a confirmation message to be accepted
52
+ */
53
+ const confirmMessage = (message) => {
54
+ return async () => {
55
+ const answers = await inquirer.prompt([
56
+ {
57
+ type: 'confirm',
58
+ message,
59
+ name: `confirm`,
60
+ default: false,
61
+ },
62
+ ]);
63
+ if (!answers.confirm) {
64
+ process.exit(0);
65
+ }
66
+ };
67
+ };
68
+
69
+ module.exports = {
70
+ parseInputList,
71
+ promptEncryptionKey,
72
+ confirmMessage,
73
+ };
@@ -0,0 +1,20 @@
1
+ 'use strict';
2
+
3
+ const bytesPerKb = 1024;
4
+ const sizes = ['B ', 'KB', 'MB', 'GB', 'TB', 'PB'];
5
+
6
+ const readableBytes = (bytes, decimals = 1, padStart = 0) => {
7
+ if (!bytes) {
8
+ return '0';
9
+ }
10
+ const i = Math.floor(Math.log(bytes) / Math.log(bytesPerKb));
11
+ const result = `${parseFloat((bytes / bytesPerKb ** i).toFixed(decimals))} ${sizes[i].padStart(
12
+ 2
13
+ )}`;
14
+
15
+ return result.padStart(padStart);
16
+ };
17
+
18
+ module.exports = {
19
+ readableBytes,
20
+ };
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const _ = require('lodash');
4
- const { has, prop, omit, toString } = require('lodash/fp');
4
+ const { has, prop, omit, toString, pipe, assign } = require('lodash/fp');
5
5
 
6
6
  const { contentTypes: contentTypesUtils } = require('@strapi/utils');
7
7
  const { ApplicationError } = require('@strapi/utils').errors;
@@ -311,10 +311,16 @@ const createComponent = async (uid, data) => {
311
311
  const model = strapi.getModel(uid);
312
312
 
313
313
  const componentData = await createComponents(uid, data);
314
+ const transform = pipe(
315
+ // Make sure we don't save the component with a pre-defined ID
316
+ omit('id'),
317
+ // Remove the component data from the original data object ...
318
+ (payload) => omitComponentData(model, payload),
319
+ // ... and assign the newly created component instead
320
+ assign(componentData)
321
+ );
314
322
 
315
- return strapi.query(uid).create({
316
- data: Object.assign(omitComponentData(model, data), componentData),
317
- });
323
+ return strapi.query(uid).create({ data: transform(data) });
318
324
  };
319
325
 
320
326
  // components can have nested compos so this must be recursive
@@ -293,40 +293,48 @@ const buildRelationsStore = ({ uid, data }) => {
293
293
  break;
294
294
  }
295
295
  case 'component': {
296
- return castArray(value).reduce(
297
- (relationsStore, componentValue) =>
298
- mergeWith(
299
- relationsStore,
300
- buildRelationsStore({
301
- uid: attribute.component,
302
- data: componentValue,
303
- }),
304
- (objValue, srcValue) => {
305
- if (isArray(objValue)) {
306
- return objValue.concat(srcValue);
307
- }
296
+ return castArray(value).reduce((relationsStore, componentValue) => {
297
+ if (!attribute.component) {
298
+ throw new ValidationError(
299
+ `Cannot build relations store from component, component identifier is undefined`
300
+ );
301
+ }
302
+
303
+ return mergeWith(
304
+ relationsStore,
305
+ buildRelationsStore({
306
+ uid: attribute.component,
307
+ data: componentValue,
308
+ }),
309
+ (objValue, srcValue) => {
310
+ if (isArray(objValue)) {
311
+ return objValue.concat(srcValue);
308
312
  }
309
- ),
310
- result
311
- );
313
+ }
314
+ );
315
+ }, result);
312
316
  }
313
317
  case 'dynamiczone': {
314
- return value.reduce(
315
- (relationsStore, dzValue) =>
316
- mergeWith(
317
- relationsStore,
318
- buildRelationsStore({
319
- uid: dzValue.__component,
320
- data: dzValue,
321
- }),
322
- (objValue, srcValue) => {
323
- if (isArray(objValue)) {
324
- return objValue.concat(srcValue);
325
- }
318
+ return value.reduce((relationsStore, dzValue) => {
319
+ if (!dzValue.__component) {
320
+ throw new ValidationError(
321
+ `Cannot build relations store from dynamiczone, component identifier is undefined`
322
+ );
323
+ }
324
+
325
+ return mergeWith(
326
+ relationsStore,
327
+ buildRelationsStore({
328
+ uid: dzValue.__component,
329
+ data: dzValue,
330
+ }),
331
+ (objValue, srcValue) => {
332
+ if (isArray(objValue)) {
333
+ return objValue.concat(srcValue);
326
334
  }
327
- ),
328
- result
329
- );
335
+ }
336
+ );
337
+ }, result);
330
338
  }
331
339
  default:
332
340
  break;
@@ -1,78 +1,16 @@
1
- 'use strict';
2
-
3
1
  /**
4
2
  * The event hub is Strapi's event control center.
5
3
  */
6
- module.exports = function createEventHub() {
7
- const listeners = new Map();
8
-
9
- // Default subscriber to easily add listeners with the on() method
10
- const defaultSubscriber = async (eventName, ...args) => {
11
- if (listeners.has(eventName)) {
12
- for (const listener of listeners.get(eventName)) {
13
- await listener(...args);
14
- }
15
- }
16
- };
17
-
18
- // Store of subscribers that will be called when an event is emitted
19
- const subscribers = [defaultSubscriber];
20
-
21
- const eventHub = {
22
- async emit(eventName, ...args) {
23
- for (const subscriber of subscribers) {
24
- await subscriber(eventName, ...args);
25
- }
26
- },
27
-
28
- subscribe(subscriber) {
29
- subscribers.push(subscriber);
30
-
31
- // Return a function to remove the subscriber
32
- return () => {
33
- eventHub.unsubscribe(subscriber);
34
- };
35
- },
36
4
 
37
- unsubscribe(subscriber) {
38
- subscribers.splice(subscribers.indexOf(subscriber), 1);
39
- },
40
-
41
- on(eventName, listener) {
42
- if (!listeners.has(eventName)) {
43
- listeners.set(eventName, []);
44
- }
45
-
46
- listeners.get(eventName).push(listener);
47
-
48
- // Return a function to remove the listener
49
- return () => {
50
- eventHub.off(eventName, listener);
51
- };
52
- },
53
-
54
- off(eventName, listener) {
55
- listeners.get(eventName).splice(listeners.get(eventName).indexOf(listener), 1);
56
- },
5
+ 'use strict';
57
6
 
58
- once(eventName, listener) {
59
- return eventHub.on(eventName, async (...args) => {
60
- eventHub.off(eventName, listener);
61
- return listener(...args);
62
- });
63
- },
7
+ const EventEmitter = require('events');
64
8
 
65
- destroy() {
66
- listeners.clear();
67
- subscribers.length = 0;
68
- return this;
69
- },
70
- };
9
+ class EventHub extends EventEmitter {}
71
10
 
72
- return {
73
- ...eventHub,
74
- removeListener: eventHub.off,
75
- removeAllListeners: eventHub.destroy,
76
- addListener: eventHub.on,
77
- };
11
+ /**
12
+ * Expose a factory function instead of the class
13
+ */
14
+ module.exports = function createEventHub(opts) {
15
+ return new EventHub(opts);
78
16
  };
@@ -0,0 +1,21 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+
5
+ /**
6
+ * Generate an admin user hash
7
+ *
8
+ * @param {Strapi.Strapi} strapi
9
+ * @returns {string}
10
+ */
11
+ const generateAdminUserHash = (strapi) => {
12
+ const ctx = strapi?.requestContext?.get();
13
+ if (!ctx?.state?.user) {
14
+ return '';
15
+ }
16
+ return crypto.createHash('sha256').update(ctx.state.user.email).digest('hex');
17
+ };
18
+
19
+ module.exports = {
20
+ generateAdminUserHash,
21
+ };
@@ -55,10 +55,12 @@ const createTelemetryInstance = (strapi) => {
55
55
  return sendEvent(
56
56
  'didCheckLicense',
57
57
  {
58
- licenseInfo: {
59
- ...ee.licenseInfo,
60
- projectHash: hashProject(strapi),
61
- dependencyHash: hashDep(strapi),
58
+ groupProperties: {
59
+ licenseInfo: {
60
+ ...ee.licenseInfo,
61
+ projectHash: hashProject(strapi),
62
+ dependencyHash: hashDep(strapi),
63
+ },
62
64
  },
63
65
  },
64
66
  {
@@ -19,7 +19,7 @@ const createMiddleware = ({ sendEvent }) => {
19
19
 
20
20
  // Send max. 1000 events per day.
21
21
  if (_state.counter < 1000) {
22
- sendEvent('didReceiveRequest', { url: ctx.request.url });
22
+ sendEvent('didReceiveRequest', { eventProperties: { url: ctx.request.url } });
23
23
 
24
24
  // Increase counter.
25
25
  _state.counter += 1;
@@ -10,7 +10,7 @@ const { isUsingTypeScriptSync } = require('@strapi/typescript-utils');
10
10
  const { env } = require('@strapi/utils');
11
11
  const ee = require('../../utils/ee');
12
12
  const machineID = require('../../utils/machine-id');
13
- const stringifyDeep = require('./stringify-deep');
13
+ const { generateAdminUserHash } = require('./admin-user-hash');
14
14
 
15
15
  const defaultQueryOpts = {
16
16
  timeout: 1000,
@@ -42,41 +42,49 @@ module.exports = (strapi) => {
42
42
  const serverRootPath = strapi.dirs.app.root;
43
43
  const adminRootPath = path.join(strapi.dirs.app.root, 'src', 'admin');
44
44
 
45
- const anonymousMetadata = {
45
+ const anonymousUserProperties = {
46
46
  environment: strapi.config.environment,
47
47
  os: os.type(),
48
48
  osPlatform: os.platform(),
49
49
  osArch: os.arch(),
50
50
  osRelease: os.release(),
51
51
  nodeVersion: process.versions.node,
52
+ };
53
+
54
+ const anonymousGroupProperties = {
52
55
  docker: process.env.DOCKER || isDocker(),
53
56
  isCI: ciEnv.isCI,
54
57
  version: strapi.config.get('info.strapi'),
55
58
  projectType: isEE ? 'Enterprise' : 'Community',
56
59
  useTypescriptOnServer: isUsingTypeScriptSync(serverRootPath),
57
60
  useTypescriptOnAdmin: isUsingTypeScriptSync(adminRootPath),
61
+ projectId: uuid,
58
62
  isHostedOnStrapiCloud: env('STRAPI_HOSTING', null) === 'strapi.cloud',
59
63
  };
60
64
 
61
- addPackageJsonStrapiMetadata(anonymousMetadata, strapi);
65
+ addPackageJsonStrapiMetadata(anonymousGroupProperties, strapi);
62
66
 
63
67
  return async (event, payload = {}, opts = {}) => {
68
+ const userId = generateAdminUserHash(strapi);
69
+
64
70
  const reqParams = {
65
71
  method: 'POST',
66
72
  body: JSON.stringify({
67
73
  event,
68
- uuid,
74
+ userId,
69
75
  deviceId,
70
- properties: stringifyDeep({
71
- ...payload,
72
- ...anonymousMetadata,
73
- }),
76
+ eventProperties: payload.eventProperties,
77
+ userProperties: userId ? { ...anonymousUserProperties, ...payload.userProperties } : {},
78
+ groupProperties: {
79
+ ...anonymousGroupProperties,
80
+ ...payload.groupProperties,
81
+ },
74
82
  }),
75
83
  ..._.merge({}, defaultQueryOpts, opts),
76
84
  };
77
85
 
78
86
  try {
79
- const res = await fetch(`${ANALYTICS_URI}/track`, reqParams);
87
+ const res = await fetch(`${ANALYTICS_URI}/api/v2/track`, reqParams);
80
88
  return res.ok;
81
89
  } catch (err) {
82
90
  return false;
@@ -3,7 +3,7 @@ import { Attribute, ConfigurableOption, PrivateOption } from './base';
3
3
  import { GetAttributesByType, GetAttributesValues } from './utils';
4
4
 
5
5
  export type BasicRelationsType = 'oneToOne' | 'oneToMany' | 'manyToOne' | 'manyToMany';
6
- export type PolymorphicRelationsType = 'morphToOne' | 'morphToMany' | 'morphOne' | 'morphMany';
6
+ export type PolymorphicRelationsType = 'morphToOne' | 'morphToMany' | 'morphOne' | 'morphMany';
7
7
  export type RelationsType = BasicRelationsType | PolymorphicRelationsType;
8
8
 
9
9
  export interface BasicRelationAttributeProperties<
@@ -17,16 +17,14 @@ export interface BasicRelationAttributeProperties<
17
17
  mappedBy?: RelationsKeysFromTo<T, S>;
18
18
  }
19
19
 
20
- export interface PolymorphicRelationAttributeProperties<
21
- R extends RelationsType,
22
- > {
20
+ export interface PolymorphicRelationAttributeProperties<R extends RelationsType> {
23
21
  relation: R;
24
22
  }
25
23
 
26
24
  export type RelationAttribute<
27
25
  S extends SchemaUID,
28
26
  R extends RelationsType,
29
- T extends R extends PolymorphicRelationsType ? never: SchemaUID = never
27
+ T extends R extends PolymorphicRelationsType ? never : SchemaUID = never
30
28
  > = Attribute<'relation'> &
31
29
  // Properties
32
30
  (R extends BasicRelationsType
@@ -34,22 +32,21 @@ export type RelationAttribute<
34
32
  : PolymorphicRelationAttributeProperties<R>) &
35
33
  // Options
36
34
  ConfigurableOption &
37
- PrivateOption
35
+ PrivateOption;
38
36
 
39
37
  export type RelationsKeysFromTo<
40
38
  TTarget extends SchemaUID,
41
39
  TSource extends SchemaUID
42
40
  > = keyof PickRelationsFromTo<TTarget, TSource>;
43
41
 
44
- export type PickRelationsFromTo<TTarget extends SchemaUID, TSource extends SchemaUID> = GetAttributesByType<
45
- TTarget,
46
- 'relation',
47
- { target: TSource }
48
- >;
42
+ export type PickRelationsFromTo<
43
+ TTarget extends SchemaUID,
44
+ TSource extends SchemaUID
45
+ > = GetAttributesByType<TTarget, 'relation', { target: TSource }>;
49
46
 
50
47
  export type RelationPluralityModifier<
51
48
  TRelation extends RelationsType,
52
- TValue extends Object
49
+ TValue extends Record<string, unknown>
53
50
  > = TRelation extends `${string}Many` ? TValue[] : TValue;
54
51
 
55
52
  export type RelationValue<
@@ -1,5 +1,5 @@
1
1
  import { Attribute, ComponentAttribute } from '../attributes';
2
- import { KeysBy, StringRecord } from '../../utils';
2
+ import { KeysBy, SchemaUID, StringRecord } from '../../utils';
3
3
 
4
4
  /**
5
5
  * Literal union type representing the possible natures of a content type
@@ -98,6 +98,11 @@ export interface PluginOptions {}
98
98
  export interface ContentTypeSchema extends Schema {
99
99
  modelType: 'contentType';
100
100
 
101
+ /**
102
+ * Unique identifier of the schema
103
+ */
104
+ uid: SchemaUID;
105
+
101
106
  /**
102
107
  * Determine the type of the content type (single-type or collection-type)
103
108
  */
@@ -2,8 +2,8 @@ import type Koa from 'koa';
2
2
  import { Database } from '@strapi/database';
3
3
 
4
4
  import type { StringMap } from './utils';
5
- import type { GenericController } from '../../../core-api/controller'
6
- import type { GenericService } from '../../../core-api/service'
5
+ import type { GenericController } from '../../../core-api/controller';
6
+ import type { GenericService } from '../../../core-api/service';
7
7
 
8
8
  // TODO move custom fields types to a separate file
9
9
  interface CustomFieldServerOptions {
@@ -92,9 +92,16 @@ export interface Strapi {
92
92
  */
93
93
  contentType(uid: string): any;
94
94
 
95
+ /**
96
+ * Getter for the Strapi component container
97
+ *
98
+ * It returns all the registered components
99
+ */
100
+ readonly components: any;
101
+
95
102
  /**
96
103
  * The custom fields registry
97
- *
104
+ *
98
105
  * It returns the custom fields interface
99
106
  */
100
107
  readonly customFields: CustomFields;
@@ -361,7 +368,6 @@ export interface Strapi {
361
368
  */
362
369
  log: any;
363
370
 
364
-
365
371
  /**
366
372
  * Used to manage cron within Strapi
367
373
  */
@@ -1,8 +1,8 @@
1
- import { Service,GenericService } from '../core-api/service';
1
+ import { Service, GenericService } from '../core-api/service';
2
2
  import { Controller, GenericController } from '../core-api/controller';
3
3
  import { Middleware } from '../middlewares';
4
4
  import { Policy } from '../core/registries/policies';
5
- import { Strapi } from '@strapi/strapi';
5
+ import { Strapi } from './core/strapi';
6
6
 
7
7
  type ControllerConfig<T extends Controller = Controller> = T;
8
8
 
@@ -17,7 +17,7 @@ try {
17
17
  process.env.npm_config_global === 'true' ||
18
18
  JSON.parse(process.env.npm_config_argv).original.includes('global')
19
19
  ) {
20
- fetch('https://analytics.strapi.io/track', {
20
+ fetch('https://analytics.strapi.io/api/v2/track', {
21
21
  method: 'POST',
22
22
  body: JSON.stringify({
23
23
  event: 'didInstallStrapi',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@strapi/strapi",
3
- "version": "4.6.0-alpha.1",
3
+ "version": "4.6.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",
@@ -80,18 +80,19 @@
80
80
  "dependencies": {
81
81
  "@koa/cors": "3.4.3",
82
82
  "@koa/router": "10.1.1",
83
- "@strapi/admin": "4.6.0-alpha.1",
84
- "@strapi/database": "4.6.0-alpha.1",
85
- "@strapi/generate-new": "4.6.0-alpha.1",
86
- "@strapi/generators": "4.6.0-alpha.1",
87
- "@strapi/logger": "4.6.0-alpha.1",
88
- "@strapi/permissions": "4.6.0-alpha.1",
89
- "@strapi/plugin-content-manager": "4.6.0-alpha.1",
90
- "@strapi/plugin-content-type-builder": "4.6.0-alpha.1",
91
- "@strapi/plugin-email": "4.6.0-alpha.1",
92
- "@strapi/plugin-upload": "4.6.0-alpha.1",
93
- "@strapi/typescript-utils": "4.6.0-alpha.1",
94
- "@strapi/utils": "4.6.0-alpha.1",
83
+ "@strapi/admin": "4.6.0-beta.1",
84
+ "@strapi/data-transfer": "4.6.0-beta.1",
85
+ "@strapi/database": "4.6.0-beta.1",
86
+ "@strapi/generate-new": "4.6.0-beta.1",
87
+ "@strapi/generators": "4.6.0-beta.1",
88
+ "@strapi/logger": "4.6.0-beta.1",
89
+ "@strapi/permissions": "4.6.0-beta.1",
90
+ "@strapi/plugin-content-manager": "4.6.0-beta.1",
91
+ "@strapi/plugin-content-type-builder": "4.6.0-beta.1",
92
+ "@strapi/plugin-email": "4.6.0-beta.1",
93
+ "@strapi/plugin-upload": "4.6.0-beta.1",
94
+ "@strapi/typescript-utils": "4.6.0-beta.1",
95
+ "@strapi/utils": "4.6.0-beta.1",
95
96
  "bcryptjs": "2.4.3",
96
97
  "boxen": "5.1.2",
97
98
  "chalk": "4.1.2",
@@ -100,7 +101,7 @@
100
101
  "cli-table3": "0.6.2",
101
102
  "commander": "8.2.0",
102
103
  "configstore": "5.0.1",
103
- "debug": "4.3.2",
104
+ "debug": "4.3.4",
104
105
  "delegates": "1.0.0",
105
106
  "dotenv": "10.0.0",
106
107
  "execa": "5.1.1",
@@ -126,7 +127,7 @@
126
127
  "open": "8.4.0",
127
128
  "ora": "5.4.1",
128
129
  "package-json": "7.0.0",
129
- "qs": "6.10.1",
130
+ "qs": "6.11.0",
130
131
  "resolve-cwd": "3.0.0",
131
132
  "semver": "7.3.8",
132
133
  "statuses": "2.0.1",
@@ -140,5 +141,5 @@
140
141
  "node": ">=14.19.1 <=18.x.x",
141
142
  "npm": ">=6.0.0"
142
143
  },
143
- "gitHead": "9171c48104548f5f6da21abf2a8098009f1a40e9"
144
+ "gitHead": "2c0bcabdf0bf2a269fed50c6f23ba777845968a0"
144
145
  }