@strapi/strapi 4.10.2 → 4.10.5

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/lib/Strapi.js CHANGED
@@ -110,7 +110,7 @@ class Strapi {
110
110
  // Instantiate the Koa app & the HTTP server
111
111
  this.server = createServer(this);
112
112
 
113
- // Strapi utils instanciation
113
+ // Strapi utils instantiation
114
114
  this.fs = createStrapiFs(this);
115
115
  this.eventHub = createEventHub();
116
116
  this.startupLogger = createStartupLogger(this);
@@ -25,6 +25,7 @@ const {
25
25
  exitMessageText,
26
26
  abortTransfer,
27
27
  getTransferTelemetryPayload,
28
+ setSignalHandler,
28
29
  } = require('../../utils/data-transfer');
29
30
  const { exitWith } = require('../../utils/helpers');
30
31
 
@@ -115,10 +116,7 @@ module.exports = async (opts) => {
115
116
  let outFile;
116
117
  try {
117
118
  // Abort transfer if user interrupts process
118
- ['SIGTERM', 'SIGINT', 'SIGQUIT'].forEach((signal) => {
119
- process.removeAllListeners(signal);
120
- process.on(signal, () => abortTransfer({ engine, strapi }));
121
- });
119
+ setSignalHandler(() => abortTransfer({ engine, strapi }));
122
120
 
123
121
  results = await engine.transfer();
124
122
  outFile = results.destination.file.path;
@@ -21,6 +21,8 @@ const {
21
21
  exitMessageText,
22
22
  abortTransfer,
23
23
  getTransferTelemetryPayload,
24
+ setSignalHandler,
25
+ getDiffHandler,
24
26
  } = require('../../utils/data-transfer');
25
27
  const { exitWith } = require('../../utils/helpers');
26
28
 
@@ -111,6 +113,8 @@ module.exports = async (opts) => {
111
113
 
112
114
  const { updateLoader } = loadersFactory();
113
115
 
116
+ engine.onSchemaDiff(getDiffHandler(engine, { force: opts.force, action: 'import' }));
117
+
114
118
  progress.on(`stage::start`, ({ stage, data }) => {
115
119
  updateLoader(stage, data).start();
116
120
  });
@@ -134,10 +138,7 @@ module.exports = async (opts) => {
134
138
  let results;
135
139
  try {
136
140
  // Abort transfer if user interrupts process
137
- ['SIGTERM', 'SIGINT', 'SIGQUIT'].forEach((signal) => {
138
- process.removeAllListeners(signal);
139
- process.on(signal, () => abortTransfer({ engine, strapi }));
140
- });
141
+ setSignalHandler(() => abortTransfer({ engine, strapi }));
141
142
 
142
143
  results = await engine.transfer();
143
144
  } catch (e) {
@@ -9,7 +9,7 @@ const {
9
9
  throttleOption,
10
10
  validateExcludeOnly,
11
11
  } = require('../../utils/data-transfer');
12
- const { confirmMessage, forceOption } = require('../../utils/commander');
12
+ const { getCommanderConfirmMessage, forceOption } = require('../../utils/commander');
13
13
  const { getLocalScript, exitWith } = require('../../utils/helpers');
14
14
 
15
15
  /**
@@ -88,7 +88,7 @@ module.exports = ({ command }) => {
88
88
  })
89
89
  .hook(
90
90
  'preAction',
91
- confirmMessage(
91
+ getCommanderConfirmMessage(
92
92
  'The import will delete all assets and data in your database. Are you sure you want to proceed?',
93
93
  { failMessage: 'Import process aborted' }
94
94
  )
@@ -22,6 +22,8 @@ const {
22
22
  exitMessageText,
23
23
  abortTransfer,
24
24
  getTransferTelemetryPayload,
25
+ setSignalHandler,
26
+ getDiffHandler,
25
27
  } = require('../../utils/data-transfer');
26
28
  const { exitWith } = require('../../utils/helpers');
27
29
 
@@ -146,6 +148,8 @@ module.exports = async (opts) => {
146
148
 
147
149
  const { updateLoader } = loadersFactory();
148
150
 
151
+ engine.onSchemaDiff(getDiffHandler(engine, { force: opts.force, action: 'transfer' }));
152
+
149
153
  progress.on(`stage::start`, ({ stage, data }) => {
150
154
  updateLoader(stage, data).start();
151
155
  });
@@ -171,10 +175,7 @@ module.exports = async (opts) => {
171
175
  let results;
172
176
  try {
173
177
  // Abort transfer if user interrupts process
174
- ['SIGTERM', 'SIGINT', 'SIGQUIT'].forEach((signal) => {
175
- process.removeAllListeners(signal);
176
- process.on(signal, () => abortTransfer({ engine, strapi }));
177
- });
178
+ setSignalHandler(() => abortTransfer({ engine, strapi }));
178
179
 
179
180
  results = await engine.transfer();
180
181
  } catch (e) {
@@ -2,7 +2,7 @@
2
2
 
3
3
  const inquirer = require('inquirer');
4
4
  const { Option } = require('commander');
5
- const { confirmMessage, forceOption, parseURL } = require('../../utils/commander');
5
+ const { getCommanderConfirmMessage, forceOption, parseURL } = require('../../utils/commander');
6
6
  const {
7
7
  getLocalScript,
8
8
  exitWith,
@@ -76,7 +76,7 @@ module.exports = ({ command }) => {
76
76
  thisCommand.opts().fromToken = answers.fromToken;
77
77
  }
78
78
 
79
- await confirmMessage(
79
+ await getCommanderConfirmMessage(
80
80
  'The transfer will delete all the local Strapi assets and its database. Are you sure you want to proceed?',
81
81
  { failMessage: 'Transfer process aborted' }
82
82
  )(thisCommand);
@@ -104,7 +104,7 @@ module.exports = ({ command }) => {
104
104
  thisCommand.opts().toToken = answers.toToken;
105
105
  }
106
106
 
107
- await confirmMessage(
107
+ await getCommanderConfirmMessage(
108
108
  'The transfer will delete all the remote Strapi assets and its database. Are you sure you want to proceed?',
109
109
  { failMessage: 'Transfer process aborted' }
110
110
  )(thisCommand);
@@ -111,30 +111,35 @@ const promptEncryptionKey = async (thisCommand) => {
111
111
  * @param {object} options Additional options
112
112
  * @param {string|undefined} options.failMessage The message to display when prompt is not confirmed
113
113
  */
114
- const confirmMessage = (message, { failMessage } = {}) => {
114
+ const getCommanderConfirmMessage = (message, { failMessage } = {}) => {
115
115
  return async (command) => {
116
- // if we have a force option, assume yes
117
- const opts = command.opts();
118
- if (opts?.force === true) {
119
- // attempt to mimic the inquirer prompt exactly
120
- console.log(`${green('?')} ${bold(message)} ${cyan('Yes')}`);
121
- return;
122
- }
123
-
124
- const answers = await inquirer.prompt([
125
- {
126
- type: 'confirm',
127
- message,
128
- name: `confirm`,
129
- default: false,
130
- },
131
- ]);
132
- if (!answers.confirm) {
116
+ const confirmed = await confirmMessage(message, { force: command.opts().force });
117
+ if (!confirmed) {
133
118
  exitWith(1, failMessage);
134
119
  }
135
120
  };
136
121
  };
137
122
 
123
+ const confirmMessage = async (message, { force } = {}) => {
124
+ // if we have a force option, respond yes
125
+ if (force === true) {
126
+ // attempt to mimic the inquirer prompt exactly
127
+ console.log(`${green('?')} ${bold(message)} ${cyan('Yes')}`);
128
+ return true;
129
+ }
130
+
131
+ const answers = await inquirer.prompt([
132
+ {
133
+ type: 'confirm',
134
+ message,
135
+ name: `confirm`,
136
+ default: false,
137
+ },
138
+ ]);
139
+
140
+ return answers.confirm;
141
+ };
142
+
138
143
  const forceOption = new Option(
139
144
  '--force',
140
145
  `Automatically answer "yes" to all prompts, including potentially destructive requests, and run non-interactively.`
@@ -146,6 +151,7 @@ module.exports = {
146
151
  parseURL,
147
152
  parseInteger,
148
153
  promptEncryptionKey,
154
+ getCommanderConfirmMessage,
149
155
  confirmMessage,
150
156
  forceOption,
151
157
  };
@@ -12,9 +12,11 @@ const {
12
12
  createLogger,
13
13
  } = require('@strapi/logger');
14
14
  const ora = require('ora');
15
+ const { TransferEngineInitializationError } = require('@strapi/data-transfer/dist/engine/errors');
16
+ const { merge } = require('lodash/fp');
15
17
  const { readableBytes, exitWith } = require('./helpers');
16
18
  const strapi = require('../../index');
17
- const { getParseListWithChoices, parseInteger } = require('./commander');
19
+ const { getParseListWithChoices, parseInteger, confirmMessage } = require('./commander');
18
20
 
19
21
  const exitMessageText = (process, error = false) => {
20
22
  const processCapitalized = process[0].toUpperCase() + process.slice(1);
@@ -113,6 +115,15 @@ const abortTransfer = async ({ engine, strapi }) => {
113
115
  return true;
114
116
  };
115
117
 
118
+ const setSignalHandler = async (handler, signals = ['SIGINT', 'SIGTERM', 'SIGQUIT']) => {
119
+ signals.forEach((signal) => {
120
+ // We specifically remove ALL listeners because we have to clear the one added in Strapi bootstrap that has a process.exit
121
+ // TODO: Ideally Strapi bootstrap would not add that listener, and then this could be more flexible and add/remove only what it needs to
122
+ process.removeAllListeners(signal);
123
+ process.on(signal, handler);
124
+ });
125
+ };
126
+
116
127
  const createStrapiInstance = async (opts = {}) => {
117
128
  try {
118
129
  const appContext = await strapi.compile();
@@ -257,6 +268,79 @@ const getTransferTelemetryPayload = (engine) => {
257
268
  };
258
269
  };
259
270
 
271
+ /**
272
+ * Get a transfer engine schema diff handler that confirms with the user before bypassing a schema check
273
+ */
274
+ const getDiffHandler = (engine, { force, action }) => {
275
+ return async (context, next) => {
276
+ // if we abort here, we need to actually exit the process because of conflict with inquirer prompt
277
+ setSignalHandler(async () => {
278
+ await abortTransfer({ engine, strapi });
279
+ exitWith(1, exitMessageText(action, true));
280
+ });
281
+
282
+ let workflowsStatus;
283
+ const source = 'Schema Integrity';
284
+
285
+ Object.entries(context.diffs).forEach(([uid, diffs]) => {
286
+ for (const diff of diffs) {
287
+ const path = [uid].concat(diff.path).join('.');
288
+ const endPath = diff.path[diff.path.length - 1];
289
+
290
+ // Catch known features
291
+ if (
292
+ uid === 'admin::workflow' ||
293
+ uid === 'admin::workflow-stage' ||
294
+ endPath?.startsWith('strapi_reviewWorkflows_')
295
+ ) {
296
+ workflowsStatus = diff.kind;
297
+ }
298
+ // handle generic cases
299
+ else if (diff.kind === 'added') {
300
+ engine.reportWarning(chalk.red(`${chalk.bold(path)} does not exist on source`), source);
301
+ } else if (diff.kind === 'deleted') {
302
+ engine.reportWarning(
303
+ chalk.red(`${chalk.bold(path)} does not exist on destination`),
304
+ source
305
+ );
306
+ } else if (diff.kind === 'modified') {
307
+ engine.reportWarning(chalk.red(`${chalk.bold(path)} has a different data type`), source);
308
+ }
309
+ }
310
+ });
311
+
312
+ // output the known feature warnings
313
+ if (workflowsStatus === 'added') {
314
+ engine.reportWarning(chalk.red(`Review workflows feature does not exist on source`), source);
315
+ } else if (workflowsStatus === 'deleted') {
316
+ engine.reportWarning(
317
+ chalk.red(`Review workflows feature does not exist on destination`),
318
+ source
319
+ );
320
+ } else if (workflowsStatus === 'modified') {
321
+ engine.panic(
322
+ new TransferEngineInitializationError('Unresolved differences in schema [review workflows]')
323
+ );
324
+ }
325
+
326
+ const confirmed = await confirmMessage(
327
+ 'There are differences in schema between the source and destination, and the data listed above will be lost. Are you sure you want to continue?',
328
+ {
329
+ force,
330
+ }
331
+ );
332
+
333
+ // reset handler back to normal
334
+ setSignalHandler(() => abortTransfer({ engine, strapi }));
335
+
336
+ if (confirmed) {
337
+ context.ignoredDiffs = merge(context.diffs, context.ignoredDiffs);
338
+ }
339
+
340
+ return next(context);
341
+ };
342
+ };
343
+
260
344
  module.exports = {
261
345
  loadersFactory,
262
346
  buildTransferTable,
@@ -271,4 +355,6 @@ module.exports = {
271
355
  validateExcludeOnly,
272
356
  formatDiagnostic,
273
357
  abortTransfer,
358
+ setSignalHandler,
359
+ getDiffHandler,
274
360
  };
@@ -49,7 +49,7 @@ const createComponents = async (uid, data) => {
49
49
  const components = await mapAsync(
50
50
  componentValue,
51
51
  (value) => createComponent(componentUID, value),
52
- { concurrency: isDialectMySQL() ? 1 : Infinity }
52
+ { concurrency: isDialectMySQL() && !strapi.db.inTransaction() ? 1 : Infinity }
53
53
  );
54
54
 
55
55
  componentBody[attributeName] = components.map(({ id }) => {
@@ -97,7 +97,7 @@ const createComponents = async (uid, data) => {
97
97
  componentBody[attributeName] = await mapAsync(
98
98
  dynamiczoneValues,
99
99
  createDynamicZoneComponents,
100
- { concurrency: isDialectMySQL() ? 1 : Infinity }
100
+ { concurrency: isDialectMySQL() && !strapi.db.inTransaction() ? 1 : Infinity }
101
101
  );
102
102
 
103
103
  continue;
@@ -151,7 +151,7 @@ const updateComponents = async (uid, entityToUpdate, data) => {
151
151
  const components = await mapAsync(
152
152
  componentValue,
153
153
  (value) => updateOrCreateComponent(componentUID, value),
154
- { concurrency: isDialectMySQL() ? 1 : Infinity }
154
+ { concurrency: isDialectMySQL() && !strapi.db.inTransaction() ? 1 : Infinity }
155
155
  );
156
156
 
157
157
  componentBody[attributeName] = components.filter(_.negate(_.isNil)).map(({ id }) => {
@@ -200,7 +200,7 @@ const updateComponents = async (uid, entityToUpdate, data) => {
200
200
  },
201
201
  };
202
202
  },
203
- { concurrency: isDialectMySQL() ? 1 : Infinity }
203
+ { concurrency: isDialectMySQL() && !strapi.db.inTransaction() ? 1 : Infinity }
204
204
  );
205
205
 
206
206
  continue;
@@ -305,7 +305,7 @@ const deleteComponents = async (uid, entityToDelete, { loadComponents = true } =
305
305
  const { component: componentUID } = attribute;
306
306
  // MySQL/MariaDB can cause deadlocks here if concurrency higher than 1
307
307
  await mapAsync(_.castArray(value), (subValue) => deleteComponent(componentUID, subValue), {
308
- concurrency: isDialectMySQL() ? 1 : Infinity,
308
+ concurrency: isDialectMySQL() && !strapi.db.inTransaction() ? 1 : Infinity,
309
309
  });
310
310
  } else {
311
311
  // delete dynamic zone components
@@ -313,7 +313,7 @@ const deleteComponents = async (uid, entityToDelete, { loadComponents = true } =
313
313
  await mapAsync(
314
314
  _.castArray(value),
315
315
  (subValue) => deleteComponent(subValue.__component, subValue),
316
- { concurrency: isDialectMySQL() ? 1 : Infinity }
316
+ { concurrency: isDialectMySQL() && !strapi.db.inTransaction() ? 1 : Infinity }
317
317
  );
318
318
  }
319
319
 
@@ -33,15 +33,14 @@ module.exports = (strapi) => {
33
33
  const registerAdminRoutes = (strapi) => {
34
34
  const generateRouteScope = createRouteScopeGenerator(`admin::`);
35
35
 
36
- strapi.admin.routes.forEach((route) => {
37
- generateRouteScope(route);
38
- route.info = { pluginName: 'admin' };
39
- });
40
-
41
- strapi.server.routes({
42
- type: 'admin',
43
- prefix: '/admin',
44
- routes: strapi.admin.routes,
36
+ _.forEach(strapi.admin.routes, (router) => {
37
+ router.type = router.type || 'admin';
38
+ router.prefix = router.prefix || `/admin`;
39
+ router.routes.forEach((route) => {
40
+ generateRouteScope(route);
41
+ route.info = { pluginName: 'admin' };
42
+ });
43
+ strapi.server.routes(router);
45
44
  });
46
45
  };
47
46
 
@@ -21,6 +21,14 @@ interface CustomFieldServerOptions {
21
21
  * The existing Strapi data type the custom field uses
22
22
  */
23
23
  type: string;
24
+
25
+ /**
26
+ * Settings for the input size in the Admin UI
27
+ */
28
+ inputSize?: {
29
+ default: 4 | 6 | 8 | 12;
30
+ isResizable: boolean;
31
+ };
24
32
  }
25
33
 
26
34
  interface CustomFields {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@strapi/strapi",
3
- "version": "4.10.2",
3
+ "version": "4.10.5",
4
4
  "description": "An open source headless CMS solution to create and manage your own API. It provides a powerful dashboard and features to make your life easier. Databases supported: MySQL, MariaDB, PostgreSQL, SQLite",
5
5
  "keywords": [
6
6
  "strapi",
@@ -81,19 +81,19 @@
81
81
  "dependencies": {
82
82
  "@koa/cors": "3.4.3",
83
83
  "@koa/router": "10.1.1",
84
- "@strapi/admin": "4.10.2",
85
- "@strapi/data-transfer": "4.10.2",
86
- "@strapi/database": "4.10.2",
87
- "@strapi/generate-new": "4.10.2",
88
- "@strapi/generators": "4.10.2",
89
- "@strapi/logger": "4.10.2",
90
- "@strapi/permissions": "4.10.2",
91
- "@strapi/plugin-content-manager": "4.10.2",
92
- "@strapi/plugin-content-type-builder": "4.10.2",
93
- "@strapi/plugin-email": "4.10.2",
94
- "@strapi/plugin-upload": "4.10.2",
95
- "@strapi/typescript-utils": "4.10.2",
96
- "@strapi/utils": "4.10.2",
84
+ "@strapi/admin": "4.10.5",
85
+ "@strapi/data-transfer": "4.10.5",
86
+ "@strapi/database": "4.10.5",
87
+ "@strapi/generate-new": "4.10.5",
88
+ "@strapi/generators": "4.10.5",
89
+ "@strapi/logger": "4.10.5",
90
+ "@strapi/permissions": "4.10.5",
91
+ "@strapi/plugin-content-manager": "4.10.5",
92
+ "@strapi/plugin-content-type-builder": "4.10.5",
93
+ "@strapi/plugin-email": "4.10.5",
94
+ "@strapi/plugin-upload": "4.10.5",
95
+ "@strapi/typescript-utils": "4.10.5",
96
+ "@strapi/utils": "4.10.5",
97
97
  "bcryptjs": "2.4.3",
98
98
  "boxen": "5.1.2",
99
99
  "chalk": "4.1.2",
@@ -117,7 +117,7 @@
117
117
  "koa-compose": "4.1.0",
118
118
  "koa-compress": "5.1.0",
119
119
  "koa-favicon": "2.1.0",
120
- "koa-helmet": "6.1.0",
120
+ "koa-helmet": "7.0.2",
121
121
  "koa-ip": "^2.1.2",
122
122
  "koa-session": "6.4.0",
123
123
  "koa-static": "5.0.0",
@@ -142,5 +142,5 @@
142
142
  "node": ">=14.19.1 <=18.x.x",
143
143
  "npm": ">=6.0.0"
144
144
  },
145
- "gitHead": "a02b19866a3bcc1f30c2395698afae6e9c3e0515"
145
+ "gitHead": "d9277d616b4478a3839e79e47330a4aaf167a2f1"
146
146
  }