diff --git a/docs/3.0.0-beta.x/cli/CLI.md b/docs/3.0.0-beta.x/cli/CLI.md
index dd5d5512ef..9218e3fc83 100644
--- a/docs/3.0.0-beta.x/cli/CLI.md
+++ b/docs/3.0.0-beta.x/cli/CLI.md
@@ -78,6 +78,68 @@ options: [--no-optimization]
- **strapi build --no-optimization**
Builds the administration panel without minimizing the assets. The build duration is faster.
+## strapi configuration:dump|config:dump
+
+Dumps configurations to a file or stdout to help you migrate to production.
+
+The dump format will be a JSON array.
+
+```
+strapi configuration:dump
+
+Options:
+ -f, --file Output file, default output is stdout
+```
+
+**Examples**
+
+- `strapi configuration:dump -f dump.json`
+- `strapi config:dump --file dump.json`
+- `strapi config:dump > dump.json`
+
+All these examples are equivalent.
+
+::: warning
+When configuring your application you often enter credentials for thrid party services (e.g authentication providers). Be aware that those credentials will also be dumped into the output of this command.
+In case of doubt, you should avoid committing the dump file into a versioning system. Here are some methods you can explore:
+
+- Copy the file directly to the environment you want and run the restore command there.
+- Put the file in a secure location and download it at deploy time with the right credentials.
+- Encrypt the file before committing and decrypt it when running the restore command.
+
+:::
+
+## strapi configuration:restore|config:restore
+
+Restores a configuration dump into your application.
+
+The input format must be a JSON array.
+
+```
+strapi configuration:restore
+
+Options:
+ -f, --file Input file, default input is stdin
+ -s, --strategy Strategy name, one of: "replace", "merge", "keep". Defaults to: "replace"
+```
+
+**Examples**
+
+- `strapi configuration:restore -f dump.json`
+- `strapi config:restore --file dump.json -s replace`
+- `cat dump.json | strapi config:restore`
+- `strapi config:restore < dump.json`
+
+All these examples are equivalent.
+
+**Strategies**
+
+When running the restore command, you can choose from three different strategies:
+
+- **replace**: Will create missing keys and replace existing ones.
+- **merge**: Will create missing keys and merge existing keys whith there new value.
+- **keep**: Will create missing keys and keep existing keys as is.
+
## strapi generate:api
Scaffold a complete API with its configurations, controller, model and service.
diff --git a/packages/strapi/bin/strapi.js b/packages/strapi/bin/strapi.js
index cefc542648..47acca5c6b 100755
--- a/packages/strapi/bin/strapi.js
+++ b/packages/strapi/bin/strapi.js
@@ -49,7 +49,16 @@ const getLocalScript = name => (...args) => {
process.exit(1);
}
- return require(cmdPath)(...args);
+ const script = require(cmdPath);
+
+ Promise.resolve()
+ .then(() => {
+ return script(...args);
+ })
+ .catch(error => {
+ console.error(`Error while running command ${name}: ${error.message}`);
+ process.exit(1);
+ });
};
/**
@@ -205,6 +214,19 @@ program
.description('Starts the admin dev server')
.action(getLocalScript('watchAdmin'));
+program
+ .command('configuration:dump')
+ .alias('config:dump')
+ .option('-f, --file ', 'Output file, default output is stdout')
+ .action(getLocalScript('configurationDump'));
+
+program
+ .command('configuration:restore')
+ .alias('config:restore')
+ .option('-f, --file ', 'Input file, default input is stdin')
+ .option('-s, --strategy ', 'Strategy name, one of: "replace", "merge", "keep"')
+ .action(getLocalScript('configurationRestore'));
+
/**
* Normalize help argument
*/
diff --git a/packages/strapi/lib/Strapi.js b/packages/strapi/lib/Strapi.js
index f2a5bfad43..ceffb5cbf7 100644
--- a/packages/strapi/lib/Strapi.js
+++ b/packages/strapi/lib/Strapi.js
@@ -53,6 +53,7 @@ class Strapi {
this.admin = {};
this.plugins = {};
this.config = loadConfiguration(this.dir, opts);
+ this.isLoaded = false;
// internal services.
this.fs = createStrapiFs(this);
@@ -157,12 +158,9 @@ class Strapi {
async start(cb) {
try {
- await this.load();
-
- // Run bootstrap function.
- await this.runBootstrapFunctions();
- // Freeze object.
- await this.freeze();
+ if (!this.isLoaded) {
+ await this.load();
+ }
this.app.use(this.router.routes()).use(this.router.allowedMethods());
@@ -324,6 +322,13 @@ class Strapi {
// Initialize hooks and middlewares.
await initializeMiddlewares.call(this);
await initializeHooks.call(this);
+
+ await this.runBootstrapFunctions();
+ await this.freeze();
+
+ this.isLoaded = true;
+
+ return this;
}
async startWebhooks() {
diff --git a/packages/strapi/lib/commands/configurationDump.js b/packages/strapi/lib/commands/configurationDump.js
new file mode 100644
index 0000000000..58c08ee799
--- /dev/null
+++ b/packages/strapi/lib/commands/configurationDump.js
@@ -0,0 +1,50 @@
+'use strict';
+
+const fs = require('fs');
+const strapi = require('../index');
+
+const CHUNK_SIZE = 100;
+
+/**
+ * Will dump configurations to a file or stdout
+ * @param {string} file filepath to use as output
+ */
+module.exports = async function({ file: filePath }) {
+ const output = filePath ? fs.createWriteStream(filePath) : process.stdout;
+
+ const app = await strapi().load();
+
+ const count = await app.query('core_store').count();
+
+ const exportData = [];
+
+ const pageCount = Math.ceil(count / CHUNK_SIZE);
+
+ for (let page = 0; page < pageCount; page++) {
+ const results = await app
+ .query('core_store')
+ .find({ _limit: CHUNK_SIZE, _start: page * CHUNK_SIZE, _sort: 'key' });
+
+ results
+ .filter(result => result.key.startsWith('plugin_'))
+ .forEach(result => {
+ exportData.push({
+ key: result.key,
+ value: result.value,
+ type: result.type,
+ environment: result.environment,
+ tag: result.tag,
+ });
+ });
+ }
+
+ output.write(JSON.stringify(exportData));
+ output.write('\n');
+ output.end();
+
+ // log success only when writting to file
+ if (filePath) {
+ console.log(`Successfully exported ${exportData.length} configuration entries`);
+ }
+ process.exit(0);
+};
diff --git a/packages/strapi/lib/commands/configurationRestore.js b/packages/strapi/lib/commands/configurationRestore.js
new file mode 100644
index 0000000000..706164bcac
--- /dev/null
+++ b/packages/strapi/lib/commands/configurationRestore.js
@@ -0,0 +1,160 @@
+'use strict';
+
+const _ = require('lodash');
+const fs = require('fs');
+const strapi = require('../index');
+
+/**
+ * Will restore configurations. It reads from a file or stdin
+ * @param {string} file filepath to use as input
+ * @param {string} strategy import strategy. one of (replace, merge, keep, default: replace)
+ */
+module.exports = async function({ file: filePath, strategy = 'replace' }) {
+ const input = filePath ? fs.readFileSync(filePath) : await readStdin(process.stdin);
+
+ const app = await strapi().load();
+
+ let dataToImport;
+ try {
+ dataToImport = JSON.parse(input);
+ } catch (error) {
+ throw new Error(`Invalid input data: ${error.message}. Expected a valid JSON array.`);
+ }
+
+ if (!Array.isArray(dataToImport)) {
+ throw new Error(`Invalid input data. Expected a valid JSON array.`);
+ }
+
+ const importer = createImporter(app.db, strategy);
+
+ for (const config of dataToImport) {
+ await importer.import(config);
+ }
+
+ console.log(
+ `Successfully imported configuration with ${strategy} strategy. Statistics: ${importer.printStatistics()}.`
+ );
+
+ process.exit(0);
+};
+
+const readStdin = () => {
+ const { stdin } = process;
+ let result = '';
+
+ if (stdin.isTTY) return Promise.resolve(result);
+
+ return new Promise((resolve, reject) => {
+ stdin.setEncoding('utf8');
+ stdin.on('readable', () => {
+ let chunk;
+ while ((chunk = stdin.read())) {
+ result += chunk;
+ }
+ });
+
+ stdin.on('end', () => {
+ resolve(result);
+ });
+
+ stdin.on('error', reject);
+ });
+};
+
+const createImporter = (db, strategy) => {
+ switch (strategy) {
+ case 'replace':
+ return createReplaceImporter(db);
+ case 'merge':
+ return createMergeImporter(db);
+ case 'keep':
+ return createKeepImporter(db);
+ default:
+ throw new Error(`No importer available for strategy "${strategy}"`);
+ }
+};
+
+/**
+ * Replace importer. Will replace the keys that already exist and create the new ones
+ * @param {Object} db - DatabaseManager instance
+ */
+const createReplaceImporter = db => {
+ const stats = {
+ created: 0,
+ replaced: 0,
+ };
+
+ return {
+ printStatistics() {
+ return `${stats.created} created, ${stats.replaced} replaced`;
+ },
+
+ async import(conf) {
+ const matching = await db.query('core_store').count({ key: conf.key });
+ if (matching > 0) {
+ stats.replaced += 1;
+ await db.query('core_store').update({ key: conf.key }, conf);
+ } else {
+ stats.created += 1;
+ await db.query('core_store').create(conf);
+ }
+ },
+ };
+};
+
+/**
+ * Merge importer. Will merge the keys that already exist with their new value and create the new ones
+ * @param {Object} db - DatabaseManager instance
+ */
+const createMergeImporter = db => {
+ const stats = {
+ created: 0,
+ merged: 0,
+ };
+
+ return {
+ printStatistics() {
+ return `${stats.created} created, ${stats.merged} merged`;
+ },
+
+ async import(conf) {
+ const existingConf = await db.query('core_store').find({ key: conf.key });
+ if (existingConf) {
+ stats.merged += 1;
+ await db.query('core_store').update({ key: conf.key }, _.merge(existingConf, conf));
+ } else {
+ stats.created += 1;
+ await db.query('core_store').create(conf);
+ }
+ },
+ };
+};
+
+/**
+ * Merge importer. Will keep the keys that already exist without changing them and create the new ones
+ * @param {Object} db - DatabaseManager instance
+ */
+const createKeepImporter = db => {
+ const stats = {
+ created: 0,
+ untouched: 0,
+ };
+
+ return {
+ printStatistics() {
+ return `${stats.created} created, ${stats.untouched} untouched`;
+ },
+
+ async import(conf) {
+ const matching = await db.query('core_store').count({ key: conf.key });
+ if (matching > 0) {
+ stats.untouched += 1;
+ // if configuration already exists do not overwrite it
+ return;
+ }
+
+ stats.created += 1;
+ await db.query('core_store').create(conf);
+ },
+ };
+};