mirror of
https://github.com/strapi/strapi.git
synced 2025-12-12 15:32:42 +00:00
Clear API with rollback system
This commit is contained in:
parent
05e895ecaf
commit
23ee9c9ebe
@ -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 = {
|
||||
|
||||
@ -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,
|
||||
};
|
||||
@ -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();
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@ -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]);
|
||||
}
|
||||
});
|
||||
|
||||
@ -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 => {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user