@strapi/strapi 4.6.0-beta.0 → 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.
@@ -0,0 +1,132 @@
1
+ 'use strict';
2
+
3
+ const chalk = require('chalk');
4
+ const Table = require('cli-table3');
5
+ const { Option } = require('commander');
6
+ const { TransferGroupPresets } = require('@strapi/data-transfer/lib/engine');
7
+ const { readableBytes, exitWith } = require('../utils/helpers');
8
+ const strapi = require('../../index');
9
+ const { getParseListWithChoices } = require('../utils/commander');
10
+
11
+ const pad = (n) => {
12
+ return (n < 10 ? '0' : '') + String(n);
13
+ };
14
+
15
+ const yyyymmddHHMMSS = () => {
16
+ const date = new Date();
17
+
18
+ return (
19
+ date.getFullYear() +
20
+ pad(date.getMonth() + 1) +
21
+ pad(date.getDate()) +
22
+ pad(date.getHours()) +
23
+ pad(date.getMinutes()) +
24
+ pad(date.getSeconds())
25
+ );
26
+ };
27
+
28
+ const getDefaultExportName = () => {
29
+ return `export_${yyyymmddHHMMSS()}`;
30
+ };
31
+
32
+ const buildTransferTable = (resultData) => {
33
+ // Build pretty table
34
+ const table = new Table({
35
+ head: ['Type', 'Count', 'Size'].map((text) => chalk.bold.blue(text)),
36
+ });
37
+
38
+ let totalBytes = 0;
39
+ let totalItems = 0;
40
+ Object.keys(resultData).forEach((key) => {
41
+ const item = resultData[key];
42
+
43
+ table.push([
44
+ { hAlign: 'left', content: chalk.bold(key) },
45
+ { hAlign: 'right', content: item.count },
46
+ { hAlign: 'right', content: `${readableBytes(item.bytes, 1, 11)} ` },
47
+ ]);
48
+ totalBytes += item.bytes;
49
+ totalItems += item.count;
50
+
51
+ if (item.aggregates) {
52
+ Object.keys(item.aggregates)
53
+ .sort()
54
+ .forEach((subkey) => {
55
+ const subitem = item.aggregates[subkey];
56
+
57
+ table.push([
58
+ { hAlign: 'left', content: `-- ${chalk.bold.grey(subkey)}` },
59
+ { hAlign: 'right', content: chalk.grey(subitem.count) },
60
+ { hAlign: 'right', content: chalk.grey(`(${readableBytes(subitem.bytes, 1, 11)})`) },
61
+ ]);
62
+ });
63
+ }
64
+ });
65
+ table.push([
66
+ { hAlign: 'left', content: chalk.bold.green('Total') },
67
+ { hAlign: 'right', content: chalk.bold.green(totalItems) },
68
+ { hAlign: 'right', content: `${chalk.bold.green(readableBytes(totalBytes, 1, 11))} ` },
69
+ ]);
70
+
71
+ return table;
72
+ };
73
+
74
+ const DEFAULT_IGNORED_CONTENT_TYPES = [
75
+ 'admin::permission',
76
+ 'admin::user',
77
+ 'admin::role',
78
+ 'admin::api-token',
79
+ 'admin::api-token-permission',
80
+ ];
81
+
82
+ const createStrapiInstance = async (logLevel = 'error') => {
83
+ const appContext = await strapi.compile();
84
+ const app = strapi(appContext);
85
+
86
+ app.log.level = logLevel;
87
+
88
+ return app.load();
89
+ };
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
+
124
+ module.exports = {
125
+ buildTransferTable,
126
+ getDefaultExportName,
127
+ DEFAULT_IGNORED_CONTENT_TYPES,
128
+ createStrapiInstance,
129
+ excludeOption,
130
+ onlyOption,
131
+ validateExcludeOnly,
132
+ };
@@ -0,0 +1,136 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * This file includes hooks to use for commander.hook and argParsers for commander.argParser
5
+ */
6
+
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
+ };
42
+
43
+ /**
44
+ * argParser: Parse a string as a URL object
45
+ */
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
+ }
57
+ };
58
+
59
+ /**
60
+ * hook: if encrypt==true and key not provided, prompt for it
61
+ */
62
+ const promptEncryptionKey = async (thisCommand) => {
63
+ const opts = thisCommand.opts();
64
+
65
+ if (!opts.encrypt && opts.key) {
66
+ return exitWith(1, 'Key may not be present unless encryption is used');
67
+ }
68
+
69
+ // if encrypt==true but we have no key, prompt for it
70
+ if (opts.encrypt && !(opts.key && opts.key.length > 0)) {
71
+ try {
72
+ const answers = await inquirer.prompt([
73
+ {
74
+ type: 'password',
75
+ message: 'Please enter an encryption key',
76
+ name: 'key',
77
+ validate(key) {
78
+ if (key.length > 0) return true;
79
+
80
+ return 'Key must be present when using the encrypt option';
81
+ },
82
+ },
83
+ ]);
84
+ opts.key = answers.key;
85
+ } catch (e) {
86
+ return exitWith(1, 'Failed to get encryption key');
87
+ }
88
+ if (!opts.key) {
89
+ return exitWith(1, 'Failed to get encryption key');
90
+ }
91
+ }
92
+ };
93
+
94
+ /**
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
99
+ */
100
+ const confirmMessage = (message) => {
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
+
110
+ const answers = await inquirer.prompt([
111
+ {
112
+ type: 'confirm',
113
+ message,
114
+ name: `confirm`,
115
+ default: false,
116
+ },
117
+ ]);
118
+ if (!answers.confirm) {
119
+ exitWith(0);
120
+ }
121
+ };
122
+ };
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
+
129
+ module.exports = {
130
+ getParseListWithChoices,
131
+ parseList,
132
+ parseURL,
133
+ promptEncryptionKey,
134
+ confirmMessage,
135
+ forceOption,
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
 
@@ -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
@@ -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
  };
@@ -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;