Clear API with rollback system

This commit is contained in:
Alexandre Bodin 2019-11-25 11:30:49 +01:00
parent 05e895ecaf
commit 23ee9c9ebe
5 changed files with 239 additions and 105 deletions

View File

@ -1,15 +1,34 @@
'use strict';
const _ = require('lodash');
const path = require('path');
const fse = require('fs-extra');
const pluralize = require('pluralize');
const generator = require('strapi-generate');
const { formatAttributes } = require('../utils/attributes');
const getSchemaManager = require('./schema-manager');
const apiCleaner = require('./clear-api');
const { formatAttributes } = require('../utils/attributes');
const { nameToSlug } = require('../utils/helpers');
/**
* Format a contentType info to be used by the front-end
* @param {Object} contentType
*/
const formatContentType = contentType => {
const { uid, plugin, connection, collectionName, info } = contentType;
return {
uid,
plugin,
schema: {
name: _.get(info, 'name') || _.upperFirst(pluralize(uid)),
description: _.get(info, 'description', ''),
connection,
collectionName,
attributes: formatAttributes(contentType),
},
};
};
/**
* Creates a component and handle the nested components sent with it
* @param {Object} params params object
@ -34,9 +53,8 @@ const createContentType = async ({ contentType, components = [] }) => {
};
/**
* Generate a squeleton API
* @param {*} name
* @param {*} contentType
* Generate an API squeleton
* @param {string} name
*/
const generateAPI = name => {
return new Promise((resolve, reject) => {
@ -80,96 +98,31 @@ const editContentType = (uid, { contentType, components = [] }) => {
});
};
/**
* Deletes a content type and the api files related to it
* @param {string} uid content type uid
*/
const deleteContentType = async uid => {
const compo = await getSchemaManager().edit(ctx =>
ctx.deleteContentType(uid)
);
// make a backup
await apiCleaner.backup(uid);
try {
await clearApiFolder(uid);
return compo;
} catch (err) {
throw new Error(`Error clearing the api folder ${err.message}`);
}
};
return getSchemaManager().edit(async ctx => {
const component = ctx.deleteContentType(uid);
const clearApiFolder = async uid => {
const { apiName, __filename__ } = strapi.contentTypes[uid];
try {
await ctx.flush();
await apiCleaner.clear(uid);
} catch (error) {
await ctx.rollback();
await apiCleaner.rollback(uid);
const baseName = path.basename(__filename__, '.settings.json');
const apiFolder = path.join(strapi.dir, 'api', apiName);
const deleteFile = async filePath => {
const fileName = path.basename(filePath);
if (_.startsWith(_.toLower(fileName), _.toLower(baseName) + '.')) {
await fse.remove(filePath);
throw new Error(
`Error delete ContentType: ${error.message}. Changes were rollbacked`
);
}
if (fileName === 'routes.json') {
const { routes } = await fse.readJSON(filePath);
const clearedRoutes = routes.filter(route => {
return !_.startsWith(
_.toLower(route.handler),
_.toLower(baseName) + '.'
);
});
if (clearedRoutes.length === 0) {
await fse.remove(filePath);
} else {
await fse.writeJSON(
filePath,
{
routes: clearedRoutes,
},
{
spaces: 2,
}
);
}
}
};
const recursiveRemoveFiles = async folder => {
const filesName = await fse.readdir(folder);
for (const fileName of filesName) {
const filePath = path.join(folder, fileName);
const stat = await fse.stat(filePath);
if (stat.isDirectory()) {
await recursiveRemoveFiles(filePath);
} else {
await deleteFile(filePath);
}
}
const files = await fse.readdir(folder);
if (files.length === 0) {
await fse.remove(folder);
}
};
await recursiveRemoveFiles(apiFolder);
};
const formatContentType = contentType => {
const { uid, plugin, connection, collectionName, info } = contentType;
return {
uid,
plugin,
schema: {
name: _.get(info, 'name') || _.upperFirst(pluralize(uid)),
description: _.get(info, 'description', ''),
connection,
collectionName,
attributes: formatAttributes(contentType),
},
};
return component;
});
};
module.exports = {

View File

@ -0,0 +1,159 @@
'use strict';
const path = require('path');
const fse = require('fs-extra');
const _ = require('lodash');
/**
* Deletes the API folder of a contentType
* @param {string} uid content type uid
*/
async function clear(uid) {
const { apiName, __filename__ } = strapi.contentTypes[uid];
const apiFolder = path.join(strapi.dir, 'api', apiName);
// base name of the model file that will be use as comparator
const baseName = path.basename(__filename__, '.settings.json');
await recursiveRemoveFiles(apiFolder, createDeleteApiFunction(baseName));
await deleteBackup(uid);
}
/**
* Backups the API folder of a contentType
* @param {string} uid content type uid
*/
async function backup(uid) {
const { apiName } = strapi.contentTypes[uid];
const apiFolder = path.join(strapi.dir, 'api', apiName);
const backupFolder = path.join(strapi.dir, 'api', '.backup', apiName);
// backup the api folder
await fse.copy(apiFolder, backupFolder);
}
/**
* Deletes an API backup folder
* @param {string} uid content type uid
*/
async function deleteBackup(uid) {
const { apiName } = strapi.contentTypes[uid];
const backupFolder = path.join(strapi.dir, 'api', '.backup');
const apiBackupFolder = path.join(strapi.dir, 'api', '.backup', apiName);
await fse.remove(apiBackupFolder);
const list = await fse.readdir(backupFolder);
if (list.length === 0) {
await fse.remove(backupFolder);
}
}
/**
* Rollbacks the API folder of a contentType
* @param {string} uid content type uid
*/
async function rollback(uid) {
const { apiName } = strapi.contentTypes[uid];
const apiFolder = path.join(strapi.dir, 'api', apiName);
const backupFolder = path.join(strapi.dir, 'api', '.backup', apiName);
const exists = await fse.exists(backupFolder);
if (!exists) {
throw new Error('Cannot rollback api that was not backed up');
}
await fse.remove(apiFolder);
await fse.copy(backupFolder, apiFolder);
await deleteBackup(uid);
}
/**
* Creates a delete function to clear an api folder
* @param {str} baseName
*/
const createDeleteApiFunction = baseName => {
/**
* Delets a file in an api.
* Will only update routes.json instead of deleting it if other routes are present
* @param {string} filePath file path to delete
*/
return async filePath => {
const fileName = path.basename(filePath);
const startWithBaseName = startWithName(baseName + '.');
if (startWithBaseName(fileName)) return fse.remove(filePath);
if (fileName === 'routes.json') {
const { routes } = await fse.readJSON(filePath);
const routesToKeep = routes.filter(
route => !startWithBaseName(route.handler)
);
if (routesToKeep.length === 0) {
return fse.remove(filePath);
}
await fse.writeJSON(
filePath,
{
routes: routesToKeep,
},
{
spaces: 2,
}
);
}
};
};
/**
* Returns a function that checks if the passed string starts with the name
* @param {string} prefix
* @returns {Function} a comparing function
*/
const startWithName = prefix => {
/**
* Checks if str starts with prefix case insensitive
* @param {string} str string to compare
*/
return str => _.startsWith(_.toLower(str), _.toLower(prefix));
};
/**
* Deletes a folder recursively using a delete function
* @param {string} folder folder to delete
* @param {Function} deleteFn function to call with the file path to delete
*/
const recursiveRemoveFiles = async (folder, deleteFn) => {
const filesName = await fse.readdir(folder);
for (const fileName of filesName) {
const filePath = path.join(folder, fileName);
const stat = await fse.stat(filePath);
if (stat.isDirectory()) {
await recursiveRemoveFiles(filePath, deleteFn);
} else {
await deleteFn(filePath);
}
}
const files = await fse.readdir(folder);
if (files.length === 0) {
await fse.remove(folder);
}
};
module.exports = {
clear,
backup,
rollback,
};

View File

@ -8,6 +8,9 @@ module.exports = function createSchemaBuilder({ components, contentTypes }) {
const tmpComponents = new Map();
const tmpContentTypes = new Map();
let flushed = false;
let rollbacked = false;
// init temporary ContentTypes
Object.keys(contentTypes).forEach(key => {
tmpContentTypes.set(
@ -36,20 +39,31 @@ module.exports = function createSchemaBuilder({ components, contentTypes }) {
...createContentTypeBuilder({ tmpComponents, tmpContentTypes }),
flush() {
return Promise.all(
[
...Array.from(tmpComponents.values()),
...Array.from(tmpContentTypes.values()),
].map(schema => schema.flush())
);
if (!flushed) {
flushed = true;
return Promise.all(
[
...Array.from(tmpComponents.values()),
...Array.from(tmpContentTypes.values()),
].map(schema => schema.flush())
);
}
return Promise.resolve();
},
rollback() {
return Promise.all(
[
...Array.from(tmpComponents.values()),
...Array.from(tmpContentTypes.values()),
].map(schema => schema.rollback())
);
if (!rollbacked) {
rollbacked = true;
return Promise.all(
[
...Array.from(tmpComponents.values()),
...Array.from(tmpContentTypes.values()),
].map(schema => schema.rollback())
);
}
return Promise.resolve();
},
};

View File

@ -3,6 +3,7 @@
const path = require('path');
const fse = require('fs-extra');
const _ = require('lodash');
const { toUID } = require('../../utils/attributes');
module.exports = function createSchemaHandler(infos) {
const { modelName, plugin, uid, dir, filename, schema } = infos;
@ -96,8 +97,11 @@ module.exports = function createSchemaHandler(infos) {
Object.keys(attributes).forEach(key => {
const attr = attributes[key];
const target = attr.model || attr.collection;
const plugin = attr.plugin;
if (target === uid) {
const relationUID = toUID(target, plugin);
if (relationUID === uid) {
this.unset(['attributes', key]);
}
});

View File

@ -6,8 +6,12 @@ const MODEL_RELATIONS = ['oneWay', 'oneToOne', 'manyToOne'];
const COLLECTION_RELATIONS = ['manyWay', 'manyToMany', 'oneToMany'];
const toUID = (name, plugin) => {
const model = strapi.getModel(name, plugin);
return model.uid;
const modelUID = Object.keys(strapi.contentTypes).find(key => {
const ct = strapi.contentTypes[key];
if (ct.modelName === name && ct.plugin === plugin) return true;
});
return modelUID;
};
const fromUID = uid => {