@strapi/strapi 4.6.0-beta.1 → 4.6.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.
@@ -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
+ };
@@ -85,7 +85,15 @@ const loadPlugins = async (strapi) => {
85
85
  for (const pluginName of Object.keys(enabledPlugins)) {
86
86
  const enabledPlugin = enabledPlugins[pluginName];
87
87
 
88
- const serverEntrypointPath = join(enabledPlugin.pathToPlugin, 'strapi-server.js');
88
+ let serverEntrypointPath;
89
+
90
+ try {
91
+ serverEntrypointPath = join(enabledPlugin.pathToPlugin, 'strapi-server.js');
92
+ } catch (e) {
93
+ throw new Error(
94
+ `Error loading the plugin ${pluginName} because ${pluginName} is not installed. Please either install the plugin or remove it's configuration.`
95
+ );
96
+ }
89
97
 
90
98
  // only load plugins with a server entrypoint
91
99
  if (!(await fse.pathExists(serverEntrypointPath))) {
@@ -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
 
@@ -43,9 +43,10 @@ const createComponents = async (uid, data) => {
43
43
  throw new Error('Expected an array to create repeatable component');
44
44
  }
45
45
 
46
- const components = await Promise.all(
47
- componentValue.map((value) => createComponent(componentUID, value))
48
- );
46
+ const components = [];
47
+ for (const value of componentValue) {
48
+ components.push(await createComponent(componentUID, value));
49
+ }
49
50
 
50
51
  componentBody[attributeName] = components.map(({ id }) => {
51
52
  return {
@@ -77,18 +78,19 @@ const createComponents = async (uid, data) => {
77
78
  throw new Error('Expected an array to create repeatable component');
78
79
  }
79
80
 
80
- componentBody[attributeName] = await Promise.all(
81
- dynamiczoneValues.map(async (value) => {
82
- const { id } = await createComponent(value.__component, value);
83
- return {
84
- id,
85
- __component: value.__component,
86
- __pivot: {
87
- field: attributeName,
88
- },
89
- };
90
- })
91
- );
81
+ const dynamicZoneData = [];
82
+ for (const value of dynamiczoneValues) {
83
+ const { id } = await createComponent(value.__component, value);
84
+ dynamicZoneData.push({
85
+ id,
86
+ __component: value.__component,
87
+ __pivot: {
88
+ field: attributeName,
89
+ },
90
+ });
91
+ }
92
+
93
+ componentBody[attributeName] = dynamicZoneData;
92
94
 
93
95
  continue;
94
96
  }
@@ -137,9 +139,10 @@ const updateComponents = async (uid, entityToUpdate, data) => {
137
139
  throw new Error('Expected an array to create repeatable component');
138
140
  }
139
141
 
140
- const components = await Promise.all(
141
- componentValue.map((value) => updateOrCreateComponent(componentUID, value))
142
- );
142
+ const components = [];
143
+ for (const value of componentValue) {
144
+ components.push(await updateOrCreateComponent(componentUID, value));
145
+ }
143
146
 
144
147
  componentBody[attributeName] = components.filter(_.negate(_.isNil)).map(({ id }) => {
145
148
  return {
@@ -173,19 +176,19 @@ const updateComponents = async (uid, entityToUpdate, data) => {
173
176
  throw new Error('Expected an array to create repeatable component');
174
177
  }
175
178
 
176
- componentBody[attributeName] = await Promise.all(
177
- dynamiczoneValues.map(async (value) => {
178
- const { id } = await updateOrCreateComponent(value.__component, value);
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
+ }
179
190
 
180
- return {
181
- id,
182
- __component: value.__component,
183
- __pivot: {
184
- field: attributeName,
185
- },
186
- };
187
- })
188
- );
191
+ componentBody[attributeName] = dynamicZoneData;
189
192
 
190
193
  continue;
191
194
  }
@@ -287,14 +290,14 @@ const deleteComponents = async (uid, entityToDelete, { loadComponents = true } =
287
290
 
288
291
  if (attribute.type === 'component') {
289
292
  const { component: componentUID } = attribute;
290
- await Promise.all(
291
- _.castArray(value).map((subValue) => deleteComponent(componentUID, subValue))
292
- );
293
+ for (const subValue of _.castArray(value)) {
294
+ await deleteComponent(componentUID, subValue);
295
+ }
293
296
  } else {
294
297
  // delete dynamic zone components
295
- await Promise.all(
296
- _.castArray(value).map((subValue) => deleteComponent(subValue.__component, subValue))
297
- );
298
+ for (const subValue of _.castArray(value)) {
299
+ await deleteComponent(subValue.__component, subValue);
300
+ }
298
301
  }
299
302
 
300
303
  continue;
@@ -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,11 +60,17 @@ 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
 
61
71
  eventHub.emit(event, {
62
72
  model: model.modelName,
73
+ uid: model.uid,
63
74
  entry: sanitizedEntity,
64
75
  });
65
76
  },
@@ -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
  };
@@ -5,12 +5,8 @@
5
5
  * You can learn more at https://docs.strapi.io/developer-docs/latest/getting-started/usage-information.html
6
6
  */
7
7
 
8
- const crypto = require('crypto');
9
- const fs = require('fs');
10
- const path = require('path');
11
8
  const { scheduleJob } = require('node-schedule');
12
9
 
13
- const ee = require('../../utils/ee');
14
10
  const wrapWithRateLimit = require('./rate-limiter');
15
11
  const createSender = require('./sender');
16
12
  const createMiddleware = require('./middleware');
@@ -46,41 +42,14 @@ const createTelemetryInstance = (strapi) => {
46
42
  strapi.server.use(createMiddleware({ sendEvent }));
47
43
  }
48
44
  },
49
- bootstrap() {
50
- if (strapi.EE === true && ee.isEE === true) {
51
- const pingDisabled =
52
- isTruthy(process.env.STRAPI_LICENSE_PING_DISABLED) && ee.licenseInfo.type === 'gold';
53
45
 
54
- const sendLicenseCheck = () => {
55
- return sendEvent(
56
- 'didCheckLicense',
57
- {
58
- groupProperties: {
59
- licenseInfo: {
60
- ...ee.licenseInfo,
61
- projectHash: hashProject(strapi),
62
- dependencyHash: hashDep(strapi),
63
- },
64
- },
65
- },
66
- {
67
- headers: { 'x-strapi-project': 'enterprise' },
68
- }
69
- );
70
- };
46
+ bootstrap() {},
71
47
 
72
- if (!pingDisabled) {
73
- const licenseCron = scheduleJob('0 0 0 * * 7', () => sendLicenseCheck());
74
- crons.push(licenseCron);
75
-
76
- sendLicenseCheck();
77
- }
78
- }
79
- },
80
48
  destroy() {
81
- // clear open handles
49
+ // Clear open handles
82
50
  crons.forEach((cron) => cron.cancel());
83
51
  },
52
+
84
53
  async send(event, payload) {
85
54
  if (isDisabled) return true;
86
55
  return sendEvent(event, payload);
@@ -88,24 +57,4 @@ const createTelemetryInstance = (strapi) => {
88
57
  };
89
58
  };
90
59
 
91
- const hash = (str) => crypto.createHash('sha256').update(str).digest('hex');
92
-
93
- const hashProject = (strapi) =>
94
- hash(`${strapi.config.get('info.name')}${strapi.config.get('info.description')}`);
95
-
96
- const hashDep = (strapi) => {
97
- const depStr = JSON.stringify(strapi.config.info.dependencies);
98
- const readmePath = path.join(strapi.dirs.app.root, 'README.md');
99
-
100
- try {
101
- if (fs.existsSync(readmePath)) {
102
- return hash(`${depStr}${fs.readFileSync(readmePath)}`);
103
- }
104
- } catch (err) {
105
- return hash(`${depStr}`);
106
- }
107
-
108
- return hash(`${depStr}`);
109
- };
110
-
111
60
  module.exports = createTelemetryInstance;
@@ -8,7 +8,6 @@ const fetch = require('node-fetch');
8
8
  const ciEnv = require('ci-info');
9
9
  const { isUsingTypeScriptSync } = require('@strapi/typescript-utils');
10
10
  const { env } = require('@strapi/utils');
11
- const ee = require('../../utils/ee');
12
11
  const machineID = require('../../utils/machine-id');
13
12
  const { generateAdminUserHash } = require('./admin-user-hash');
14
13
 
@@ -37,7 +36,6 @@ const addPackageJsonStrapiMetadata = (metadata, strapi) => {
37
36
  module.exports = (strapi) => {
38
37
  const { uuid } = strapi.config;
39
38
  const deviceId = machineID();
40
- const isEE = strapi.EE === true && ee.isEE === true;
41
39
 
42
40
  const serverRootPath = strapi.dirs.app.root;
43
41
  const adminRootPath = path.join(strapi.dirs.app.root, 'src', 'admin');
@@ -55,7 +53,6 @@ module.exports = (strapi) => {
55
53
  docker: process.env.DOCKER || isDocker(),
56
54
  isCI: ciEnv.isCI,
57
55
  version: strapi.config.get('info.strapi'),
58
- projectType: isEE ? 'Enterprise' : 'Community',
59
56
  useTypescriptOnServer: isUsingTypeScriptSync(serverRootPath),
60
57
  useTypescriptOnAdmin: isUsingTypeScriptSync(adminRootPath),
61
58
  projectId: uuid,
@@ -77,6 +74,7 @@ module.exports = (strapi) => {
77
74
  userProperties: userId ? { ...anonymousUserProperties, ...payload.userProperties } : {},
78
75
  groupProperties: {
79
76
  ...anonymousGroupProperties,
77
+ projectType: strapi.EE ? 'Enterprise' : 'Community',
80
78
  ...payload.groupProperties,
81
79
  },
82
80
  }),
@@ -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
  };
@@ -0,0 +1,56 @@
1
+ 'use strict';
2
+
3
+ const { isEmpty, negate } = require('lodash/fp');
4
+
5
+ const INTEGER_REGEX = /^\d+$/;
6
+ const STEP_REGEX = /^\*\/\d+$/;
7
+ const COMPONENTS = [
8
+ { limit: 60, zeroBasedIndices: true, functionName: 'getSeconds' },
9
+ { limit: 60, zeroBasedIndices: true, functionName: 'getMinutes' },
10
+ { limit: 24, zeroBasedIndices: true, functionName: 'getHours' },
11
+ { limit: 31, zeroBasedIndices: false, functionName: 'getDate' },
12
+ { limit: 12, zeroBasedIndices: false, functionName: 'getMonth' },
13
+ { limit: 7, zeroBasedIndices: true, functionName: 'getDay' },
14
+ ];
15
+
16
+ const shift = (component, index, date) => {
17
+ if (component === '*') {
18
+ return '*';
19
+ }
20
+
21
+ const { limit, zeroBasedIndices, functionName } = COMPONENTS[index];
22
+ const offset = +!zeroBasedIndices;
23
+ const currentValue = date[functionName]();
24
+
25
+ if (INTEGER_REGEX.test(component)) {
26
+ return ((Number.parseInt(component, 10) + currentValue) % limit) + offset;
27
+ }
28
+
29
+ if (STEP_REGEX.test(component)) {
30
+ const [, step] = component.split('/');
31
+ const frequency = Math.floor(limit / step);
32
+ const list = Array.from({ length: frequency }, (_, index) => index * step);
33
+ return list.map((value) => ((value + currentValue) % limit) + offset).sort((a, b) => a - b);
34
+ }
35
+
36
+ // Unsupported syntax
37
+ return component;
38
+ };
39
+
40
+ /**
41
+ * Simulate an interval by shifting a cron expression using the specified date.
42
+ * @param {string} rule A cron expression you want to shift.
43
+ * @param {Date} date The date that's gonna be used as the start of the "interval", it defaults to now.
44
+ * @returns The shifted cron expression.
45
+ */
46
+ const shiftCronExpression = (rule, date = new Date()) => {
47
+ const components = rule.trim().split(' ').filter(negate(isEmpty));
48
+ const secondsIncluded = components.length === 6;
49
+ return components
50
+ .map((component, index) => shift(component, secondsIncluded ? index : index + 1, date))
51
+ .join(' ');
52
+ };
53
+
54
+ module.exports = {
55
+ shiftCronExpression,
56
+ };