@directus/api
Version:
Directus is a real-time API and App dashboard for managing SQL database content
185 lines (184 loc) • 8.55 kB
JavaScript
import { parseJSON } from '@directus/utils';
import chalk from 'chalk';
import { promises as fs } from 'fs';
import inquirer from 'inquirer';
import { load as loadYaml } from 'js-yaml';
import path from 'path';
import getDatabase, { isInstalled, validateDatabaseConnection } from '../../../database/index.js';
import { useLogger } from '../../../logger/index.js';
import { DiffKind } from '../../../types/index.js';
import { isNestedMetaUpdate } from '../../../utils/apply-diff.js';
import { applySnapshot } from '../../../utils/apply-snapshot.js';
import { getSnapshotDiff } from '../../../utils/get-snapshot-diff.js';
import { getSnapshot } from '../../../utils/get-snapshot.js';
function filterSnapshotDiff(snapshot, filters) {
const filterSet = new Set(filters);
function shouldKeep(item) {
if (filterSet.has(item.collection))
return false;
if (item.field && filterSet.has(`${item.collection}.${item.field}`))
return false;
return true;
}
const filteredDiff = {
collections: snapshot.collections.filter((item) => shouldKeep(item)),
fields: snapshot.fields.filter((item) => shouldKeep(item)),
relations: snapshot.relations.filter((item) => shouldKeep(item)),
};
return filteredDiff;
}
export async function apply(snapshotPath, options) {
const logger = useLogger();
const filename = path.resolve(process.cwd(), snapshotPath);
const database = getDatabase();
await validateDatabaseConnection(database);
if ((await isInstalled()) === false) {
logger.error(`Directus isn't installed on this database. Please run "directus bootstrap" first.`);
database.destroy();
process.exit(0);
}
let snapshot;
try {
const fileContents = await fs.readFile(filename, 'utf8');
if (filename.endsWith('.yaml') || filename.endsWith('.yml')) {
snapshot = (await loadYaml(fileContents));
}
else {
snapshot = parseJSON(fileContents);
}
const currentSnapshot = await getSnapshot({ database });
let snapshotDiff = getSnapshotDiff(currentSnapshot, snapshot);
if (options?.ignoreRules) {
snapshotDiff = filterSnapshotDiff(snapshotDiff, options.ignoreRules.split(','));
}
if (snapshotDiff.collections.length === 0 &&
snapshotDiff.fields.length === 0 &&
snapshotDiff.relations.length === 0) {
logger.info('No changes to apply.');
database.destroy();
process.exit(0);
}
const dryRun = options?.dryRun === true;
const promptForChanges = !dryRun && options?.yes !== true;
if (dryRun || promptForChanges) {
const sections = [];
if (snapshotDiff.collections.length > 0) {
const lines = [chalk.underline.bold('Collections:')];
for (const { collection, diff } of snapshotDiff.collections) {
if (diff[0]?.kind === DiffKind.EDIT) {
lines.push(` - ${chalk.magenta('Update')} ${collection}`);
for (const change of diff) {
if (change.kind === DiffKind.EDIT) {
const path = formatPath(change.path);
lines.push(` - Set ${path} to ${change.rhs}`);
}
}
}
else if (diff[0]?.kind === DiffKind.DELETE) {
lines.push(` - ${chalk.red('Delete')} ${collection}`);
}
else if (diff[0]?.kind === DiffKind.NEW) {
lines.push(` - ${chalk.green('Create')} ${collection}`);
}
else if (diff[0]?.kind === DiffKind.ARRAY) {
lines.push(` - ${chalk.magenta('Update')} ${collection}`);
}
}
sections.push(lines.join('\n'));
}
if (snapshotDiff.fields.length > 0) {
const lines = [chalk.underline.bold('Fields:')];
for (const { collection, field, diff } of snapshotDiff.fields) {
if (diff[0]?.kind === DiffKind.EDIT || isNestedMetaUpdate(diff[0])) {
lines.push(` - ${chalk.magenta('Update')} ${collection}.${field}`);
for (const change of diff) {
const path = formatPath(change.path);
if (change.kind === DiffKind.EDIT) {
lines.push(` - Set ${path} to ${change.rhs}`);
}
else if (change.kind === DiffKind.DELETE) {
lines.push(` - Remove ${path}`);
}
else if (change.kind === DiffKind.NEW) {
lines.push(` - Add ${path} and set it to ${change.rhs}`);
}
}
}
else if (diff[0]?.kind === DiffKind.DELETE) {
lines.push(` - ${chalk.red('Delete')} ${collection}.${field}`);
}
else if (diff[0]?.kind === DiffKind.NEW) {
lines.push(` - ${chalk.green('Create')} ${collection}.${field}`);
}
else if (diff[0]?.kind === DiffKind.ARRAY) {
lines.push(` - ${chalk.magenta('Update')} ${collection}.${field}`);
}
}
sections.push(lines.join('\n'));
}
if (snapshotDiff.relations.length > 0) {
const lines = [chalk.underline.bold('Relations:')];
for (const { collection, field, related_collection, diff } of snapshotDiff.relations) {
const relatedCollection = formatRelatedCollection(related_collection);
if (diff[0]?.kind === DiffKind.EDIT) {
lines.push(` - ${chalk.magenta('Update')} ${collection}.${field}${relatedCollection}`);
for (const change of diff) {
if (change.kind === DiffKind.EDIT) {
const path = formatPath(change.path);
lines.push(` - Set ${path} to ${change.rhs}`);
}
}
}
else if (diff[0]?.kind === DiffKind.DELETE) {
lines.push(` - ${chalk.red('Delete')} ${collection}.${field}${relatedCollection}`);
}
else if (diff[0]?.kind === DiffKind.NEW) {
lines.push(` - ${chalk.green('Create')} ${collection}.${field}${relatedCollection}`);
}
else if (diff[0]?.kind === DiffKind.ARRAY) {
lines.push(` - ${chalk.magenta('Update')} ${collection}.${field}${relatedCollection}`);
}
}
sections.push(lines.join('\n'));
}
const message = 'The following changes will be applied:\n\n' + sections.join('\n\n');
if (dryRun) {
// eslint-disable-next-line no-console
console.log(message);
process.exit(0);
}
const { proceed } = await inquirer.prompt([
{
type: 'confirm',
name: 'proceed',
message: message + '\n\n' + 'Would you like to continue?',
},
]);
if (proceed === false) {
process.exit(0);
}
}
await applySnapshot(snapshot, { current: currentSnapshot, diff: snapshotDiff, database });
logger.info(`Snapshot applied successfully`);
database.destroy();
process.exit(0);
}
catch (err) {
logger.error(err);
database.destroy();
process.exit(1);
}
}
function formatPath(path) {
if (path.length === 1) {
return path.toString();
}
return path.slice(1).join('.');
}
function formatRelatedCollection(relatedCollection) {
// Related collection doesn't exist for a2o relationship types
if (relatedCollection) {
return ` → ${relatedCollection}`;
}
return '';
}