mirror of
				https://github.com/strapi/strapi.git
				synced 2025-11-03 19:36:20 +00:00 
			
		
		
		
	Merge pull request #6112 from strapi/chore/configuration-dump-cli
Configuration dump and restore cli
This commit is contained in:
		
						commit
						2f84e42c44
					
				@ -78,6 +78,68 @@ options: [--no-optimization]
 | 
			
		||||
- **strapi build --no-optimization**<br/>
 | 
			
		||||
  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 <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 <file>          Input file, default input is stdin
 | 
			
		||||
  -s, --strategy <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.
 | 
			
		||||
 | 
			
		||||
@ -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 <file>', 'Output file, default output is stdout')
 | 
			
		||||
  .action(getLocalScript('configurationDump'));
 | 
			
		||||
 | 
			
		||||
program
 | 
			
		||||
  .command('configuration:restore')
 | 
			
		||||
  .alias('config:restore')
 | 
			
		||||
  .option('-f, --file <file>', 'Input file, default input is stdin')
 | 
			
		||||
  .option('-s, --strategy <strategy>', 'Strategy name, one of: "replace", "merge", "keep"')
 | 
			
		||||
  .action(getLocalScript('configurationRestore'));
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Normalize help argument
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
@ -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() {
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										50
									
								
								packages/strapi/lib/commands/configurationDump.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								packages/strapi/lib/commands/configurationDump.js
									
									
									
									
									
										Normal file
									
								
							@ -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);
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										160
									
								
								packages/strapi/lib/commands/configurationRestore.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										160
									
								
								packages/strapi/lib/commands/configurationRestore.js
									
									
									
									
									
										Normal file
									
								
							@ -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);
 | 
			
		||||
    },
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user