mirror of
https://github.com/strapi/strapi.git
synced 2025-11-13 08:38:09 +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/>
|
- **strapi build --no-optimization**<br/>
|
||||||
Builds the administration panel without minimizing the assets. The build duration is faster.
|
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
|
## strapi generate:api
|
||||||
|
|
||||||
Scaffold a complete API with its configurations, controller, model and service.
|
Scaffold a complete API with its configurations, controller, model and service.
|
||||||
|
|||||||
@ -49,7 +49,16 @@ const getLocalScript = name => (...args) => {
|
|||||||
process.exit(1);
|
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')
|
.description('Starts the admin dev server')
|
||||||
.action(getLocalScript('watchAdmin'));
|
.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
|
* Normalize help argument
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -53,6 +53,7 @@ class Strapi {
|
|||||||
this.admin = {};
|
this.admin = {};
|
||||||
this.plugins = {};
|
this.plugins = {};
|
||||||
this.config = loadConfiguration(this.dir, opts);
|
this.config = loadConfiguration(this.dir, opts);
|
||||||
|
this.isLoaded = false;
|
||||||
|
|
||||||
// internal services.
|
// internal services.
|
||||||
this.fs = createStrapiFs(this);
|
this.fs = createStrapiFs(this);
|
||||||
@ -157,12 +158,9 @@ class Strapi {
|
|||||||
|
|
||||||
async start(cb) {
|
async start(cb) {
|
||||||
try {
|
try {
|
||||||
await this.load();
|
if (!this.isLoaded) {
|
||||||
|
await this.load();
|
||||||
// Run bootstrap function.
|
}
|
||||||
await this.runBootstrapFunctions();
|
|
||||||
// Freeze object.
|
|
||||||
await this.freeze();
|
|
||||||
|
|
||||||
this.app.use(this.router.routes()).use(this.router.allowedMethods());
|
this.app.use(this.router.routes()).use(this.router.allowedMethods());
|
||||||
|
|
||||||
@ -324,6 +322,13 @@ class Strapi {
|
|||||||
// Initialize hooks and middlewares.
|
// Initialize hooks and middlewares.
|
||||||
await initializeMiddlewares.call(this);
|
await initializeMiddlewares.call(this);
|
||||||
await initializeHooks.call(this);
|
await initializeHooks.call(this);
|
||||||
|
|
||||||
|
await this.runBootstrapFunctions();
|
||||||
|
await this.freeze();
|
||||||
|
|
||||||
|
this.isLoaded = true;
|
||||||
|
|
||||||
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
async startWebhooks() {
|
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