@strapi/strapi 4.6.0-beta.1 → 4.6.0-beta.2

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/README.md CHANGED
@@ -80,14 +80,18 @@ Complete installation requirements can be found in the documentation under <a hr
80
80
  - CentOS/RHEL 8
81
81
  - macOS Mojave
82
82
  - Windows 10
83
- - Docker - [Docker-Repo](https://github.com/strapi/strapi-docker)
83
+ - Docker
84
84
 
85
85
  (Please note that Strapi may work on other operating systems, but these are not tested nor officially supported at this time.)
86
86
 
87
87
  **Node:**
88
88
 
89
- - NodeJS >= 14 <= 18
90
- - NPM >= 6.x
89
+ Strapi only supports maintenance and LTS versions of Node.js. Please refer to the <a href="https://nodejs.org/en/about/releases/">Node.js release schedule</a> for more information. NPM versions installed by default with Node.js are supported. Generally it's recommended to use yarn over npm where possible.
90
+
91
+ | Strapi Version | Recommended | Minimum |
92
+ | -------------- | ----------- | ------- |
93
+ | 4.3.9 and up | 18.x | 14.x |
94
+ | 4.0.x to 4.3.8 | 16.x | 14.x |
91
95
 
92
96
  **Database:**
93
97
 
@@ -98,7 +102,7 @@ Complete installation requirements can be found in the documentation under <a hr
98
102
  | PostgreSQL | 11.0 | 14.0 |
99
103
  | SQLite | 3 | 3 |
100
104
 
101
- **We recommend always using the latest version of Strapi to start your new projects**.
105
+ **We recommend always using the latest version of Strapi stable to start your new projects**.
102
106
 
103
107
  ## Features
104
108
 
package/bin/strapi.js CHANGED
@@ -14,7 +14,18 @@ const inquirer = require('inquirer');
14
14
  const program = new Command();
15
15
 
16
16
  const packageJSON = require('../package.json');
17
- const { promptEncryptionKey, confirmMessage } = require('../lib/commands/utils/commander');
17
+ const {
18
+ promptEncryptionKey,
19
+ confirmMessage,
20
+ parseURL,
21
+ forceOption,
22
+ } = require('../lib/commands/utils/commander');
23
+ const { ifOptions, assertUrlHasProtocol, exitWith } = require('../lib/commands/utils/helpers');
24
+ const {
25
+ excludeOption,
26
+ onlyOption,
27
+ validateExcludeOnly,
28
+ } = require('../lib/commands/transfer/utils');
18
29
 
19
30
  const checkCwdIsStrapiApp = (name) => {
20
31
  const logErrorAndExit = () => {
@@ -258,10 +269,65 @@ program
258
269
  .option('-s, --silent', `Run the generation silently, without any output`, false)
259
270
  .action(getLocalScript('ts/generate-types'));
260
271
 
272
+ if (process.env.STRAPI_EXPERIMENTAL === 'true') {
273
+ // `$ strapi transfer`
274
+ program
275
+ .command('transfer')
276
+ .description('Transfer data from one source to another')
277
+ .allowExcessArguments(false)
278
+ .addOption(
279
+ new Option(
280
+ '--from <sourceURL>',
281
+ `URL of the remote Strapi instance to get data from`
282
+ ).argParser(parseURL)
283
+ )
284
+ .addOption(
285
+ new Option(
286
+ '--to <destinationURL>',
287
+ `URL of the remote Strapi instance to send data to`
288
+ ).argParser(parseURL)
289
+ )
290
+ .addOption(forceOption)
291
+ // Validate URLs
292
+ .hook(
293
+ 'preAction',
294
+ ifOptions(
295
+ (opts) => opts.from,
296
+ (thisCommand) => assertUrlHasProtocol(thisCommand.opts().from, ['https:', 'http:'])
297
+ )
298
+ )
299
+ .hook(
300
+ 'preAction',
301
+ ifOptions(
302
+ (opts) => opts.to,
303
+ (thisCommand) => assertUrlHasProtocol(thisCommand.opts().to, ['https:', 'http:'])
304
+ )
305
+ )
306
+ .hook(
307
+ 'preAction',
308
+ ifOptions(
309
+ (opts) => !opts.from && !opts.to,
310
+ () => exitWith(1, 'At least one source (from) or destination (to) option must be provided')
311
+ )
312
+ )
313
+ .addOption(forceOption)
314
+ .addOption(excludeOption)
315
+ .addOption(onlyOption)
316
+ .hook('preAction', validateExcludeOnly)
317
+ .hook(
318
+ 'preAction',
319
+ confirmMessage(
320
+ 'The import will delete all data in the remote database. Are you sure you want to proceed?'
321
+ )
322
+ )
323
+ .action(getLocalScript('transfer/transfer'));
324
+ }
325
+
261
326
  // `$ strapi export`
262
327
  program
263
328
  .command('export')
264
329
  .description('Export data from Strapi to file')
330
+ .allowExcessArguments(false)
265
331
  .addOption(
266
332
  new Option('--no-encrypt', `Disables 'aes-128-ecb' encryption of the output file`).default(true)
267
333
  )
@@ -273,7 +339,9 @@ program
273
339
  )
274
340
  )
275
341
  .addOption(new Option('-f, --file <file>', 'name to use for exported file (without extensions)'))
276
- .allowExcessArguments(false)
342
+ .addOption(excludeOption)
343
+ .addOption(onlyOption)
344
+ .hook('preAction', validateExcludeOnly)
277
345
  .hook('preAction', promptEncryptionKey)
278
346
  .action(getLocalScript('transfer/export'));
279
347
 
@@ -281,6 +349,7 @@ program
281
349
  program
282
350
  .command('import')
283
351
  .description('Import data from file to Strapi')
352
+ .allowExcessArguments(false)
284
353
  .requiredOption(
285
354
  '-f, --file <file>',
286
355
  'path and filename for the Strapi export file you want to import'
@@ -291,7 +360,10 @@ program
291
360
  'Provide encryption key in command instead of using the prompt'
292
361
  )
293
362
  )
294
- .allowExcessArguments(false)
363
+ .addOption(forceOption)
364
+ .addOption(excludeOption)
365
+ .addOption(onlyOption)
366
+ .hook('preAction', validateExcludeOnly)
295
367
  .hook('preAction', async (thisCommand) => {
296
368
  const opts = thisCommand.opts();
297
369
  const ext = path.extname(String(opts.file));
@@ -307,8 +379,7 @@ program
307
379
  },
308
380
  ]);
309
381
  if (!answers.key?.length) {
310
- console.log('No key entered, aborting import.');
311
- process.exit(0);
382
+ exitWith(0, 'No key entered, aborting import.');
312
383
  }
313
384
  opts.key = answers.key;
314
385
  }
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.removeAllListeners();
228
+ this.eventHub.destroy();
229
229
 
230
230
  if (_.has(this, 'db')) {
231
231
  await this.db.destroy();
@@ -1,12 +1,12 @@
1
1
  'use strict';
2
2
 
3
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');
4
+ providers: { createLocalFileDestinationProvider },
5
+ } = require('@strapi/data-transfer/lib/file');
6
+ const {
7
+ providers: { createLocalStrapiSourceProvider },
8
+ } = require('@strapi/data-transfer/lib/strapi');
9
+ const { createTransferEngine } = require('@strapi/data-transfer/lib/engine');
10
10
  const { isObject, isString, isFinite, toNumber } = require('lodash/fp');
11
11
  const fs = require('fs-extra');
12
12
  const chalk = require('chalk');
@@ -53,6 +53,8 @@ module.exports = async (opts) => {
53
53
  const engine = createTransferEngine(source, destination, {
54
54
  versionStrategy: 'ignore', // for an export to file, versionStrategy will always be skipped
55
55
  schemaStrategy: 'ignore', // for an export to file, schemaStrategy will always be skipped
56
+ exclude: opts.exclude,
57
+ only: opts.only,
56
58
  transforms: {
57
59
  links: [
58
60
  {
@@ -74,32 +76,23 @@ module.exports = async (opts) => {
74
76
  },
75
77
  });
76
78
 
77
- try {
78
- logger.log(`Starting export...`);
79
-
80
- const progress = engine.progress.stream;
79
+ const progress = engine.progress.stream;
81
80
 
82
- const telemetryPayload = (/* payload */) => {
83
- return {
84
- eventProperties: {
85
- source: engine.sourceProvider.name,
86
- destination: engine.destinationProvider.name,
87
- },
88
- };
81
+ const getTelemetryPayload = (/* payload */) => {
82
+ return {
83
+ eventProperties: {
84
+ source: engine.sourceProvider.name,
85
+ destination: engine.destinationProvider.name,
86
+ },
89
87
  };
88
+ };
90
89
 
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
- });
90
+ progress.on('transfer::start', async () => {
91
+ logger.log(`Starting export...`);
92
+ await strapi.telemetry.send('didDEITSProcessStart', getTelemetryPayload());
93
+ });
102
94
 
95
+ try {
103
96
  const results = await engine.transfer();
104
97
  const outFile = results.destination.file.path;
105
98
 
@@ -113,11 +106,15 @@ module.exports = async (opts) => {
113
106
 
114
107
  logger.log(`${chalk.bold('Export process has been completed successfully!')}`);
115
108
  logger.log(`Export archive is in ${chalk.green(outFile)}`);
116
- process.exit(0);
117
109
  } catch (e) {
110
+ await strapi.telemetry.send('didDEITSProcessFail', getTelemetryPayload());
118
111
  logger.error('Export process failed unexpectedly:', e.toString());
119
112
  process.exit(1);
120
113
  }
114
+
115
+ // 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
116
+ await strapi.telemetry.send('didDEITSProcessFinish', getTelemetryPayload());
117
+ process.exit(0);
121
118
  };
122
119
 
123
120
  /**
@@ -1,15 +1,17 @@
1
1
  'use strict';
2
2
 
3
3
  const {
4
- createLocalFileSourceProvider,
5
- createLocalStrapiDestinationProvider,
4
+ providers: { createLocalFileSourceProvider },
5
+ } = require('@strapi/data-transfer/lib/file');
6
+ const {
7
+ providers: { createLocalStrapiDestinationProvider, DEFAULT_CONFLICT_STRATEGY },
8
+ } = require('@strapi/data-transfer/lib/strapi');
9
+ const {
6
10
  createTransferEngine,
7
11
  DEFAULT_VERSION_STRATEGY,
8
12
  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
+ } = require('@strapi/data-transfer/lib/engine');
14
+
13
15
  const { isObject } = require('lodash/fp');
14
16
  const path = require('path');
15
17
 
@@ -45,6 +47,7 @@ module.exports = async (opts) => {
45
47
  async getStrapi() {
46
48
  return strapiInstance;
47
49
  },
50
+ autoDestroy: false,
48
51
  strategy: opts.conflictStrategy || DEFAULT_CONFLICT_STRATEGY,
49
52
  restore: {
50
53
  entities: { exclude: DEFAULT_IGNORED_CONTENT_TYPES },
@@ -59,6 +62,7 @@ module.exports = async (opts) => {
59
62
  versionStrategy: opts.versionStrategy || DEFAULT_VERSION_STRATEGY,
60
63
  schemaStrategy: opts.schemaStrategy || DEFAULT_SCHEMA_STRATEGY,
61
64
  exclude: opts.exclude,
65
+ only: opts.only,
62
66
  rules: {
63
67
  links: [
64
68
  {
@@ -77,44 +81,42 @@ module.exports = async (opts) => {
77
81
  ],
78
82
  },
79
83
  };
80
- const engine = createTransferEngine(source, destination, engineOptions);
81
84
 
82
- try {
83
- logger.info('Starting import...');
85
+ const engine = createTransferEngine(source, destination, engineOptions);
84
86
 
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
- };
87
+ const progress = engine.progress.stream;
88
+ const getTelemetryPayload = () => {
89
+ return {
90
+ eventProperties: {
91
+ source: engine.sourceProvider.name,
92
+ destination: engine.destinationProvider.name,
93
+ },
93
94
  };
95
+ };
94
96
 
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
- });
97
+ progress.on('transfer::start', async () => {
98
+ logger.info('Starting import...');
99
+ await strapiInstance.telemetry.send('didDEITSProcessStart', getTelemetryPayload());
100
+ });
106
101
 
102
+ try {
107
103
  const results = await engine.transfer();
108
104
  const table = buildTransferTable(results.engine);
109
105
  logger.info(table.toString());
110
106
 
111
107
  logger.info('Import process has been completed successfully!');
112
- process.exit(0);
113
108
  } catch (e) {
109
+ await strapiInstance.telemetry.send('didDEITSProcessFail', getTelemetryPayload());
114
110
  logger.error('Import process failed unexpectedly:');
115
111
  logger.error(e);
116
112
  process.exit(1);
117
113
  }
114
+
115
+ // 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
116
+ await strapiInstance.telemetry.send('didDEITSProcessFinish', getTelemetryPayload());
117
+ await strapiInstance.destroy();
118
+
119
+ process.exit(0);
118
120
  };
119
121
 
120
122
  /**
@@ -0,0 +1,127 @@
1
+ 'use strict';
2
+
3
+ const { createTransferEngine } = require('@strapi/data-transfer/lib/engine');
4
+ const {
5
+ providers: {
6
+ createRemoteStrapiDestinationProvider,
7
+ createLocalStrapiSourceProvider,
8
+ createLocalStrapiDestinationProvider,
9
+ },
10
+ } = require('@strapi/data-transfer/lib/strapi');
11
+ const { isObject } = require('lodash/fp');
12
+ const chalk = require('chalk');
13
+
14
+ const {
15
+ buildTransferTable,
16
+ createStrapiInstance,
17
+ DEFAULT_IGNORED_CONTENT_TYPES,
18
+ } = require('./utils');
19
+
20
+ const logger = console;
21
+
22
+ /**
23
+ * @typedef TransferCommandOptions Options given to the CLI transfer command
24
+ *
25
+ * @property {URL|undefined} [to] The url of a remote Strapi to use as remote destination
26
+ * @property {URL|undefined} [from] The url of a remote Strapi to use as remote source
27
+ */
28
+
29
+ /**
30
+ * Transfer command.
31
+ *
32
+ * It transfers data from a local file to a local strapi instance
33
+ *
34
+ * @param {TransferCommandOptions} opts
35
+ */
36
+ module.exports = async (opts) => {
37
+ // Validate inputs from Commander
38
+ if (!isObject(opts)) {
39
+ logger.error('Could not parse command arguments');
40
+ process.exit(1);
41
+ }
42
+
43
+ const strapi = await createStrapiInstance();
44
+
45
+ let source;
46
+ let destination;
47
+
48
+ if (!opts.from && !opts.to) {
49
+ logger.error('At least one source (from) or destination (to) option must be provided');
50
+ process.exit(1);
51
+ }
52
+
53
+ // if no URL provided, use local Strapi
54
+ if (!opts.from) {
55
+ source = createLocalStrapiSourceProvider({
56
+ getStrapi: () => strapi,
57
+ });
58
+ }
59
+ // if URL provided, set up a remote source provider
60
+ else {
61
+ logger.error(`Remote Strapi source provider not yet implemented`);
62
+ process.exit(1);
63
+ }
64
+
65
+ // if no URL provided, use local Strapi
66
+ if (!opts.to) {
67
+ destination = createLocalStrapiDestinationProvider({
68
+ getStrapi: () => strapi,
69
+ });
70
+ }
71
+ // if URL provided, set up a remote destination provider
72
+ else {
73
+ destination = createRemoteStrapiDestinationProvider({
74
+ url: opts.to,
75
+ auth: false,
76
+ strategy: 'restore',
77
+ restore: {
78
+ entities: { exclude: DEFAULT_IGNORED_CONTENT_TYPES },
79
+ },
80
+ });
81
+ }
82
+
83
+ if (!source || !destination) {
84
+ logger.error('Could not create providers');
85
+ process.exit(1);
86
+ }
87
+
88
+ const engine = createTransferEngine(source, destination, {
89
+ versionStrategy: 'strict',
90
+ schemaStrategy: 'strict',
91
+ transforms: {
92
+ links: [
93
+ {
94
+ filter(link) {
95
+ return (
96
+ !DEFAULT_IGNORED_CONTENT_TYPES.includes(link.left.type) &&
97
+ !DEFAULT_IGNORED_CONTENT_TYPES.includes(link.right.type)
98
+ );
99
+ },
100
+ },
101
+ ],
102
+ entities: [
103
+ {
104
+ filter(entity) {
105
+ return !DEFAULT_IGNORED_CONTENT_TYPES.includes(entity.type);
106
+ },
107
+ },
108
+ ],
109
+ },
110
+ });
111
+
112
+ try {
113
+ logger.log(`Starting transfer...`);
114
+
115
+ const results = await engine.transfer();
116
+
117
+ const table = buildTransferTable(results.engine);
118
+ logger.log(table.toString());
119
+
120
+ logger.log(`${chalk.bold('Transfer process has been completed successfully!')}`);
121
+ process.exit(0);
122
+ } catch (e) {
123
+ logger.error('Transfer process failed unexpectedly');
124
+ logger.error(e);
125
+ process.exit(1);
126
+ }
127
+ };
@@ -2,8 +2,11 @@
2
2
 
3
3
  const chalk = require('chalk');
4
4
  const Table = require('cli-table3');
5
- const { readableBytes } = require('../utils');
5
+ const { Option } = require('commander');
6
+ const { TransferGroupPresets } = require('@strapi/data-transfer/lib/engine');
7
+ const { readableBytes, exitWith } = require('../utils/helpers');
6
8
  const strapi = require('../../index');
9
+ const { getParseListWithChoices } = require('../utils/commander');
7
10
 
8
11
  const pad = (n) => {
9
12
  return (n < 10 ? '0' : '') + String(n);
@@ -85,10 +88,45 @@ const createStrapiInstance = async (logLevel = 'error') => {
85
88
  return app.load();
86
89
  };
87
90
 
91
+ const transferDataTypes = Object.keys(TransferGroupPresets);
92
+
93
+ const excludeOption = new Option(
94
+ '--exclude <comma-separated data types>',
95
+ `Exclude this data. Options used here override --only. Available types: ${transferDataTypes.join(
96
+ ','
97
+ )}`
98
+ ).argParser(getParseListWithChoices(transferDataTypes, 'Invalid options for "exclude"'));
99
+
100
+ const onlyOption = new Option(
101
+ '--only <command-separated data types>',
102
+ `Include only this data (plus schemas). Available types: ${transferDataTypes.join(',')}`
103
+ ).argParser(getParseListWithChoices(transferDataTypes, 'Invalid options for "only"'));
104
+
105
+ const validateExcludeOnly = (command) => {
106
+ const { exclude, only } = command.opts();
107
+ if (!only || !exclude) {
108
+ return;
109
+ }
110
+
111
+ const choicesInBoth = only.filter((n) => {
112
+ return exclude.indexOf(n) !== -1;
113
+ });
114
+ if (choicesInBoth.length > 0) {
115
+ exitWith(
116
+ 1,
117
+ `Data types may not be used in both "exclude" and "only" in the same command. Found in both: ${choicesInBoth.join(
118
+ ','
119
+ )}`
120
+ );
121
+ }
122
+ };
123
+
88
124
  module.exports = {
89
125
  buildTransferTable,
90
126
  getDefaultExportName,
91
- yyyymmddHHMMSS,
92
127
  DEFAULT_IGNORED_CONTENT_TYPES,
93
128
  createStrapiInstance,
129
+ excludeOption,
130
+ onlyOption,
131
+ validateExcludeOnly,
94
132
  };
@@ -1,12 +1,59 @@
1
1
  'use strict';
2
2
 
3
+ /**
4
+ * This file includes hooks to use for commander.hook and argParsers for commander.argParser
5
+ */
6
+
3
7
  const inquirer = require('inquirer');
8
+ const { InvalidOptionArgumentError, Option } = require('commander');
9
+ const { bold, green, cyan } = require('chalk');
10
+ const { exitWith } = require('./helpers');
11
+
12
+ /**
13
+ * argParser: Parse a comma-delimited string as an array
14
+ */
15
+ const parseList = (value) => {
16
+ let list;
17
+ try {
18
+ list = value.split(',').map((item) => item.trim()); // trim shouldn't be necessary but might help catch unexpected whitespace characters
19
+ } catch (e) {
20
+ exitWith(1, `Unrecognized input: ${value}`);
21
+ }
22
+ return list;
23
+ };
24
+
25
+ /**
26
+ * Returns an argParser that returns a list
27
+ */
28
+ const getParseListWithChoices = (choices, errorMessage = 'Invalid options:') => {
29
+ return (value) => {
30
+ const list = parseList(value);
31
+ const invalid = list.filter((item) => {
32
+ return !choices.includes(item);
33
+ });
34
+
35
+ if (invalid.length > 0) {
36
+ exitWith(1, `${errorMessage}: ${invalid.join(',')}`);
37
+ }
38
+
39
+ return list;
40
+ };
41
+ };
4
42
 
5
43
  /**
6
- * argsParser: Parse a comma-delimited string as an array
44
+ * argParser: Parse a string as a URL object
7
45
  */
8
- const parseInputList = (value) => {
9
- return value.split(',');
46
+ const parseURL = (value) => {
47
+ try {
48
+ const url = new URL(value);
49
+ if (!url.host) {
50
+ throw new InvalidOptionArgumentError(`Could not parse url ${value}`);
51
+ }
52
+
53
+ return url;
54
+ } catch (e) {
55
+ throw new InvalidOptionArgumentError(`Could not parse url ${value}`);
56
+ }
10
57
  };
11
58
 
12
59
  /**
@@ -16,8 +63,7 @@ const promptEncryptionKey = async (thisCommand) => {
16
63
  const opts = thisCommand.opts();
17
64
 
18
65
  if (!opts.encrypt && opts.key) {
19
- console.error('Key may not be present unless encryption is used');
20
- process.exit(1);
66
+ return exitWith(1, 'Key may not be present unless encryption is used');
21
67
  }
22
68
 
23
69
  // if encrypt==true but we have no key, prompt for it
@@ -37,21 +83,30 @@ const promptEncryptionKey = async (thisCommand) => {
37
83
  ]);
38
84
  opts.key = answers.key;
39
85
  } catch (e) {
40
- console.error('Failed to get encryption key');
41
- process.exit(1);
86
+ return exitWith(1, 'Failed to get encryption key');
42
87
  }
43
88
  if (!opts.key) {
44
- console.error('Failed to get encryption key');
45
- process.exit(1);
89
+ return exitWith(1, 'Failed to get encryption key');
46
90
  }
47
91
  }
48
92
  };
49
93
 
50
94
  /**
51
- * hook: require a confirmation message to be accepted
95
+ * hook: require a confirmation message to be accepted unless forceOption (-f,--force) is used
96
+ *
97
+ * @param {string} message The message to confirm with user
98
+ * @param {object} options Additional options
52
99
  */
53
100
  const confirmMessage = (message) => {
54
- return async () => {
101
+ return async (command) => {
102
+ // if we have a force option, assume yes
103
+ const opts = command.opts();
104
+ if (opts?.force === true) {
105
+ // attempt to mimic the inquirer prompt exactly
106
+ console.log(`${green('?')} ${bold(message)} ${cyan('Yes')}`);
107
+ return;
108
+ }
109
+
55
110
  const answers = await inquirer.prompt([
56
111
  {
57
112
  type: 'confirm',
@@ -61,13 +116,21 @@ const confirmMessage = (message) => {
61
116
  },
62
117
  ]);
63
118
  if (!answers.confirm) {
64
- process.exit(0);
119
+ exitWith(0);
65
120
  }
66
121
  };
67
122
  };
68
123
 
124
+ const forceOption = new Option(
125
+ '-f, --force',
126
+ `Automatically answer "yes" to all prompts, including potentially destructive requests, and run non-interactively.`
127
+ );
128
+
69
129
  module.exports = {
70
- parseInputList,
130
+ getParseListWithChoices,
131
+ parseList,
132
+ parseURL,
71
133
  promptEncryptionKey,
72
134
  confirmMessage,
135
+ forceOption,
73
136
  };
@@ -0,0 +1,108 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Helper functions for the Strapi CLI
5
+ */
6
+
7
+ const chalk = require('chalk');
8
+ const { isString, isArray } = require('lodash/fp');
9
+
10
+ const bytesPerKb = 1024;
11
+ const sizes = ['B ', 'KB', 'MB', 'GB', 'TB', 'PB'];
12
+
13
+ /**
14
+ * Convert bytes to a human readable formatted string, for example "1024" becomes "1KB"
15
+ *
16
+ * @param {number} bytes The bytes to be converted
17
+ * @param {number} decimals How many decimals to include in the final number
18
+ * @param {number} padStart Pad the string with space at the beginning so that it has at least this many characters
19
+ */
20
+ const readableBytes = (bytes, decimals = 1, padStart = 0) => {
21
+ if (!bytes) {
22
+ return '0';
23
+ }
24
+ const i = Math.floor(Math.log(bytes) / Math.log(bytesPerKb));
25
+ const result = `${parseFloat((bytes / bytesPerKb ** i).toFixed(decimals))} ${sizes[i].padStart(
26
+ 2
27
+ )}`;
28
+
29
+ return result.padStart(padStart);
30
+ };
31
+
32
+ /**
33
+ *
34
+ * Display message(s) to console and then call process.exit with code.
35
+ * If code is zero, console.log and green text is used for messages, otherwise console.error and red text.
36
+ *
37
+ * @param {number} code Code to exit process with
38
+ * @param {string | Array} message Message(s) to display before exiting
39
+ */
40
+ const exitWith = (code, message = undefined) => {
41
+ const logger = (message) => {
42
+ if (code === 0) {
43
+ console.log(chalk.green(message));
44
+ } else {
45
+ console.error(chalk.red(message));
46
+ }
47
+ };
48
+
49
+ if (isString(message)) {
50
+ logger(message);
51
+ } else if (isArray(message)) {
52
+ message.forEach((msg) => logger(msg));
53
+ }
54
+ process.exit(code);
55
+ };
56
+
57
+ /**
58
+ * assert that a URL object has a protocol value
59
+ *
60
+ * @param {URL} url
61
+ * @param {string[]|string|undefined} [protocol]
62
+ */
63
+ const assertUrlHasProtocol = (url, protocol = undefined) => {
64
+ if (!url.protocol) {
65
+ exitWith(1, `${url.toString()} does not have a protocol`);
66
+ }
67
+
68
+ // if just checking for the existence of a protocol, return
69
+ if (!protocol) {
70
+ return;
71
+ }
72
+
73
+ if (isString(protocol)) {
74
+ if (protocol !== url.protocol) {
75
+ exitWith(1, `${url.toString()} must have the protocol ${protocol}`);
76
+ }
77
+ return;
78
+ }
79
+
80
+ // assume an array
81
+ if (!protocol.some((protocol) => url.protocol === protocol)) {
82
+ return exitWith(
83
+ 1,
84
+ `${url.toString()} must have one of the following protocols: ${protocol.join(',')}`
85
+ );
86
+ }
87
+ };
88
+
89
+ /**
90
+ * Passes commander options to conditionCallback(). If it returns true, call isMetCallback otherwise call isNotMetCallback
91
+ */
92
+ const ifOptions = (conditionCallback, isMetCallback = () => {}, isNotMetCallback = () => {}) => {
93
+ return async (command) => {
94
+ const opts = command.opts();
95
+ if (await conditionCallback(opts)) {
96
+ await isMetCallback(command);
97
+ } else {
98
+ await isNotMetCallback(command);
99
+ }
100
+ };
101
+ };
102
+
103
+ module.exports = {
104
+ exitWith,
105
+ assertUrlHasProtocol,
106
+ ifOptions,
107
+ readableBytes,
108
+ };
@@ -21,5 +21,5 @@ export interface CollectionTypeService extends BaseService {
21
21
  export type Service = SingleTypeService | CollectionTypeService;
22
22
 
23
23
  export type GenericService = Partial<Service> & {
24
- [method: string | number | symbol]: <T = any>(...args: any) => T;
24
+ [method: string | number | symbol]: (...args: any) => any;
25
25
  };
@@ -1,6 +1,16 @@
1
1
  'use strict';
2
2
 
3
+ const { propOr } = require('lodash/fp');
4
+
3
5
  const { ValidationError } = require('@strapi/utils').errors;
6
+ const {
7
+ hasDraftAndPublish,
8
+ constants: { PUBLISHED_AT_ATTRIBUTE },
9
+ } = require('@strapi/utils').contentTypes;
10
+
11
+ const setPublishedAt = (data) => {
12
+ data[PUBLISHED_AT_ATTRIBUTE] = propOr(new Date(), PUBLISHED_AT_ATTRIBUTE, data);
13
+ };
4
14
 
5
15
  /**
6
16
  * Returns a single type service to handle default core-api actions
@@ -27,7 +37,7 @@ const createSingleTypeService = ({ contentType }) => {
27
37
  * @return {Promise}
28
38
  */
29
39
  async createOrUpdate({ data, ...params } = {}) {
30
- const entity = await this.find(params);
40
+ const entity = await this.find({ ...params, publicationState: 'preview' });
31
41
 
32
42
  if (!entity) {
33
43
  const count = await strapi.query(uid).count();
@@ -35,6 +45,9 @@ const createSingleTypeService = ({ contentType }) => {
35
45
  throw new ValidationError('singleType.alreadyExists');
36
46
  }
37
47
 
48
+ if (hasDraftAndPublish(contentType)) {
49
+ setPublishedAt(data);
50
+ }
38
51
  return strapi.entityService.create(uid, { ...params, data });
39
52
  }
40
53
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  const _ = require('lodash');
4
4
  const delegate = require('delegates');
5
- const { InvalidTimeError, InvalidDateError, InvalidDateTimeError } =
5
+ const { InvalidTimeError, InvalidDateError, InvalidDateTimeError, InvalidRelationError } =
6
6
  require('@strapi/database').errors;
7
7
  const {
8
8
  webhook: webhookUtils,
@@ -34,7 +34,12 @@ const transformLoadParamsToQuery = (uid, field, params = {}, pagination = {}) =>
34
34
  // TODO: those should be strapi events used by the webhooks not the other way arround
35
35
  const { ENTRY_CREATE, ENTRY_UPDATE, ENTRY_DELETE } = webhookUtils.webhookEvents;
36
36
 
37
- const databaseErrorsToTransform = [InvalidTimeError, InvalidDateTimeError, InvalidDateError];
37
+ const databaseErrorsToTransform = [
38
+ InvalidTimeError,
39
+ InvalidDateTimeError,
40
+ InvalidDateError,
41
+ InvalidRelationError,
42
+ ];
38
43
 
39
44
  const creationPipeline = (data, context) => {
40
45
  return applyTransforms(data, context);
@@ -55,6 +60,11 @@ const createDefaultImplementation = ({ strapi, db, eventHub, entityValidator })
55
60
  },
56
61
 
57
62
  async emitEvent(uid, event, entity) {
63
+ // Ignore audit log events to prevent infinite loops
64
+ if (uid === 'admin::audit-log') {
65
+ return;
66
+ }
67
+
58
68
  const model = strapi.getModel(uid);
59
69
  const sanitizedEntity = await sanitize.sanitizers.defaultSanitizeOutput(model, entity);
60
70
 
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const createError = require('http-errors');
4
- const { NotFoundError, UnauthorizedError, ForbiddenError, PayloadTooLargeError } =
4
+ const { NotFoundError, UnauthorizedError, ForbiddenError, PayloadTooLargeError, RateLimitError } =
5
5
  require('@strapi/utils').errors;
6
6
 
7
7
  const mapErrorsAndStatus = [
@@ -21,6 +21,10 @@ const mapErrorsAndStatus = [
21
21
  classError: PayloadTooLargeError,
22
22
  status: 413,
23
23
  },
24
+ {
25
+ classError: RateLimitError,
26
+ status: 429,
27
+ },
24
28
  ];
25
29
 
26
30
  const formatApplicationError = (error) => {
@@ -1,16 +1,78 @@
1
+ 'use strict';
2
+
1
3
  /**
2
4
  * The event hub is Strapi's event control center.
3
5
  */
6
+ module.exports = function createEventHub() {
7
+ const listeners = new Map();
4
8
 
5
- 'use strict';
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
+ };
6
17
 
7
- const EventEmitter = require('events');
18
+ // Store of subscribers that will be called when an event is emitted
19
+ const subscribers = [defaultSubscriber];
8
20
 
9
- class EventHub extends EventEmitter {}
21
+ const eventHub = {
22
+ async emit(eventName, ...args) {
23
+ for (const subscriber of subscribers) {
24
+ await subscriber(eventName, ...args);
25
+ }
26
+ },
10
27
 
11
- /**
12
- * Expose a factory function instead of the class
13
- */
14
- module.exports = function createEventHub(opts) {
15
- return new EventHub(opts);
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
+
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, [listener]);
44
+ } else {
45
+ listeners.get(eventName).push(listener);
46
+ }
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
+ },
57
+
58
+ once(eventName, listener) {
59
+ return eventHub.on(eventName, async (...args) => {
60
+ eventHub.off(eventName, listener);
61
+ return listener(...args);
62
+ });
63
+ },
64
+
65
+ destroy() {
66
+ listeners.clear();
67
+ subscribers.length = 0;
68
+ return this;
69
+ },
70
+ };
71
+
72
+ return {
73
+ ...eventHub,
74
+ removeListener: eventHub.off,
75
+ removeAllListeners: eventHub.destroy,
76
+ addListener: eventHub.on,
77
+ };
16
78
  };
@@ -378,6 +378,11 @@ export interface Strapi {
378
378
  */
379
379
  telemetry: any;
380
380
 
381
+ /**
382
+ * Used to access ctx from anywhere within the Strapi application
383
+ */
384
+ requestContext: any;
385
+
381
386
  /**
382
387
  * Strapi DB layer instance
383
388
  */
@@ -30,7 +30,7 @@ type CollectionTypeRouterConfig = {
30
30
 
31
31
  type RouterConfig = {
32
32
  prefix?: string;
33
- only: string[];
33
+ only?: string[];
34
34
  except?: string[];
35
35
  config: SingleTypeRouterConfig | CollectionTypeRouterConfig;
36
36
  };
package/lib/utils/ee.js CHANGED
@@ -18,7 +18,7 @@ const internals = {};
18
18
  const features = {
19
19
  bronze: [],
20
20
  silver: [],
21
- gold: ['sso'],
21
+ gold: ['sso', 'audit-logs'],
22
22
  };
23
23
 
24
24
  module.exports = ({ dir, logger = noLog }) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@strapi/strapi",
3
- "version": "4.6.0-beta.1",
3
+ "version": "4.6.0-beta.2",
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,19 +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-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",
83
+ "@strapi/admin": "4.6.0-beta.2",
84
+ "@strapi/data-transfer": "4.6.0-beta.2",
85
+ "@strapi/database": "4.6.0-beta.2",
86
+ "@strapi/generate-new": "4.6.0-beta.2",
87
+ "@strapi/generators": "4.6.0-beta.2",
88
+ "@strapi/logger": "4.6.0-beta.2",
89
+ "@strapi/permissions": "4.6.0-beta.2",
90
+ "@strapi/plugin-content-manager": "4.6.0-beta.2",
91
+ "@strapi/plugin-content-type-builder": "4.6.0-beta.2",
92
+ "@strapi/plugin-email": "4.6.0-beta.2",
93
+ "@strapi/plugin-upload": "4.6.0-beta.2",
94
+ "@strapi/typescript-utils": "4.6.0-beta.2",
95
+ "@strapi/utils": "4.6.0-beta.2",
96
96
  "bcryptjs": "2.4.3",
97
97
  "boxen": "5.1.2",
98
98
  "chalk": "4.1.2",
@@ -141,5 +141,5 @@
141
141
  "node": ">=14.19.1 <=18.x.x",
142
142
  "npm": ">=6.0.0"
143
143
  },
144
- "gitHead": "2c0bcabdf0bf2a269fed50c6f23ba777845968a0"
144
+ "gitHead": "b852090f931cd21868c4016f24db2f9fdfc7a7ab"
145
145
  }
@@ -1,20 +0,0 @@
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
- };