@digicms/cms
Version:
An open source headless CMS solution to create and manage your own API. It provides a powerful dashboard and features to make your life easier. Databases supported: MySQL, MariaDB, PostgreSQL, SQLite
388 lines (314 loc) • 11.2 kB
JavaScript
;
const _ = require('lodash');
const { has, prop, omit, toString, pipe, assign } = require('lodash/fp');
const { contentTypes: contentTypesUtils, mapAsync } = require('@strapi/utils');
const { ApplicationError } = require('@strapi/utils').errors;
const { getComponentAttributes } = require('@strapi/utils').contentTypes;
const isDialectMySQL = () => strapi.db.dialect.client === 'mysql';
const omitComponentData = (contentType, data) => {
const { attributes } = contentType;
const componentAttributes = Object.keys(attributes).filter((attributeName) =>
contentTypesUtils.isComponentAttribute(attributes[attributeName])
);
return omit(componentAttributes, data);
};
// NOTE: we could generalize the logic to allow CRUD of relation directly in the DB layer
const createComponents = async (uid, data) => {
const { attributes = {} } = strapi.getModel(uid);
const componentBody = {};
for (const attributeName of Object.keys(attributes)) {
const attribute = attributes[attributeName];
if (!has(attributeName, data) || !contentTypesUtils.isComponentAttribute(attribute)) {
continue;
}
if (attribute.type === 'component') {
const { component: componentUID, repeatable = false } = attribute;
const componentValue = data[attributeName];
if (componentValue === null) {
continue;
}
if (repeatable === true) {
if (!Array.isArray(componentValue)) {
throw new Error('Expected an array to create repeatable component');
}
// MySQL/MariaDB can cause deadlocks here if concurrency higher than 1
const components = await mapAsync(
componentValue,
(value) => createComponent(componentUID, value),
{ concurrency: isDialectMySQL() ? 1 : Infinity }
);
componentBody[attributeName] = components.map(({ id }) => {
return {
id,
__pivot: {
field: attributeName,
component_type: componentUID,
},
};
});
} else {
const component = await createComponent(componentUID, componentValue);
componentBody[attributeName] = {
id: component.id,
__pivot: {
field: attributeName,
component_type: componentUID,
},
};
}
continue;
}
if (attribute.type === 'dynamiczone') {
const dynamiczoneValues = data[attributeName];
if (!Array.isArray(dynamiczoneValues)) {
throw new Error('Expected an array to create repeatable component');
}
const createDynamicZoneComponents = async (value) => {
const { id } = await createComponent(value.__component, value);
return {
id,
__component: value.__component,
__pivot: {
field: attributeName,
},
};
};
// MySQL/MariaDB can cause deadlocks here if concurrency higher than 1
componentBody[attributeName] = await mapAsync(
dynamiczoneValues,
createDynamicZoneComponents,
{ concurrency: isDialectMySQL() ? 1 : Infinity }
);
continue;
}
}
return componentBody;
};
/**
* @param {str} uid
* @param {object} entity
* @return {Promise<{uid: string, entity: object}>}
*/
const getComponents = async (uid, entity) => {
const componentAttributes = getComponentAttributes(strapi.getModel(uid));
if (_.isEmpty(componentAttributes)) return {};
return strapi.query(uid).load(entity, componentAttributes);
};
/*
delete old components
create or update
*/
const updateComponents = async (uid, entityToUpdate, data) => {
const { attributes = {} } = strapi.getModel(uid);
const componentBody = {};
for (const attributeName of Object.keys(attributes)) {
const attribute = attributes[attributeName];
if (!has(attributeName, data)) {
continue;
}
if (attribute.type === 'component') {
const { component: componentUID, repeatable = false } = attribute;
const componentValue = data[attributeName];
await deleteOldComponents(uid, componentUID, entityToUpdate, attributeName, componentValue);
if (repeatable === true) {
if (!Array.isArray(componentValue)) {
throw new Error('Expected an array to create repeatable component');
}
// MySQL/MariaDB can cause deadlocks here if concurrency higher than 1
const components = await mapAsync(
componentValue,
(value) => updateOrCreateComponent(componentUID, value),
{ concurrency: isDialectMySQL() ? 1 : Infinity }
);
componentBody[attributeName] = components.filter(_.negate(_.isNil)).map(({ id }) => {
return {
id,
__pivot: {
field: attributeName,
component_type: componentUID,
},
};
});
} else {
const component = await updateOrCreateComponent(componentUID, componentValue);
componentBody[attributeName] = component && {
id: component.id,
__pivot: {
field: attributeName,
component_type: componentUID,
},
};
}
continue;
}
if (attribute.type === 'dynamiczone') {
const dynamiczoneValues = data[attributeName];
await deleteOldDZComponents(uid, entityToUpdate, attributeName, dynamiczoneValues);
if (!Array.isArray(dynamiczoneValues)) {
throw new Error('Expected an array to create repeatable component');
}
// MySQL/MariaDB can cause deadlocks here if concurrency higher than 1
componentBody[attributeName] = await mapAsync(
dynamiczoneValues,
async (value) => {
const { id } = await updateOrCreateComponent(value.__component, value);
return {
id,
__component: value.__component,
__pivot: {
field: attributeName,
},
};
},
{ concurrency: isDialectMySQL() ? 1 : Infinity }
);
continue;
}
}
return componentBody;
};
const deleteOldComponents = async (
uid,
componentUID,
entityToUpdate,
attributeName,
componentValue
) => {
const previousValue = await strapi.query(uid).load(entityToUpdate, attributeName);
const idsToKeep = _.castArray(componentValue).filter(has('id')).map(prop('id')).map(toString);
const allIds = _.castArray(previousValue).filter(has('id')).map(prop('id')).map(toString);
idsToKeep.forEach((id) => {
if (!allIds.includes(id)) {
throw new ApplicationError(
`Some of the provided components in ${attributeName} are not related to the entity`
);
}
});
const idsToDelete = _.difference(allIds, idsToKeep);
if (idsToDelete.length > 0) {
for (const idToDelete of idsToDelete) {
await deleteComponent(componentUID, { id: idToDelete });
}
}
};
const deleteOldDZComponents = async (uid, entityToUpdate, attributeName, dynamiczoneValues) => {
const previousValue = await strapi.query(uid).load(entityToUpdate, attributeName);
const idsToKeep = _.castArray(dynamiczoneValues)
.filter(has('id'))
.map(({ id, __component }) => ({
id: toString(id),
__component,
}));
const allIds = _.castArray(previousValue)
.filter(has('id'))
.map(({ id, __component }) => ({
id: toString(id),
__component,
}));
idsToKeep.forEach(({ id, __component }) => {
if (!allIds.find((el) => el.id === id && el.__component === __component)) {
const err = new Error(
`Some of the provided components in ${attributeName} are not related to the entity`
);
err.status = 400;
throw err;
}
});
const idsToDelete = allIds.reduce((acc, { id, __component }) => {
if (!idsToKeep.find((el) => el.id === id && el.__component === __component)) {
acc.push({ id, __component });
}
return acc;
}, []);
if (idsToDelete.length > 0) {
for (const idToDelete of idsToDelete) {
const { id, __component } = idToDelete;
await deleteComponent(__component, { id });
}
}
};
const deleteComponents = async (uid, entityToDelete, { loadComponents = true } = {}) => {
const { attributes = {} } = strapi.getModel(uid);
for (const attributeName of Object.keys(attributes)) {
const attribute = attributes[attributeName];
if (attribute.type === 'component' || attribute.type === 'dynamiczone') {
let value;
if (loadComponents) {
value = await strapi.query(uid).load(entityToDelete, attributeName);
} else {
value = entityToDelete[attributeName];
}
if (!value) {
continue;
}
if (attribute.type === 'component') {
const { component: componentUID } = attribute;
// MySQL/MariaDB can cause deadlocks here if concurrency higher than 1
await mapAsync(_.castArray(value), (subValue) => deleteComponent(componentUID, subValue), {
concurrency: isDialectMySQL() ? 1 : Infinity,
});
} else {
// delete dynamic zone components
// MySQL/MariaDB can cause deadlocks here if concurrency higher than 1
await mapAsync(
_.castArray(value),
(subValue) => deleteComponent(subValue.__component, subValue),
{ concurrency: isDialectMySQL() ? 1 : Infinity }
);
}
continue;
}
}
};
/** *************************
Component queries
************************** */
// components can have nested compos so this must be recursive
const createComponent = async (uid, data) => {
const model = strapi.getModel(uid);
const componentData = await createComponents(uid, data);
const transform = pipe(
// Make sure we don't save the component with a pre-defined ID
omit('id'),
// Remove the component data from the original data object ...
(payload) => omitComponentData(model, payload),
// ... and assign the newly created component instead
assign(componentData)
);
return strapi.query(uid).create({ data: transform(data) });
};
// components can have nested compos so this must be recursive
const updateComponent = async (uid, componentToUpdate, data) => {
const model = strapi.getModel(uid);
const componentData = await updateComponents(uid, componentToUpdate, data);
return strapi.query(uid).update({
where: {
id: componentToUpdate.id,
},
data: Object.assign(omitComponentData(model, data), componentData),
});
};
const updateOrCreateComponent = (componentUID, value) => {
if (value === null) {
return null;
}
// update
if (has('id', value)) {
// TODO: verify the compo is associated with the entity
return updateComponent(componentUID, { id: value.id }, value);
}
// create
return createComponent(componentUID, value);
};
const deleteComponent = async (uid, componentToDelete) => {
await deleteComponents(uid, componentToDelete);
await strapi.query(uid).delete({ where: { id: componentToDelete.id } });
};
module.exports = {
omitComponentData,
getComponents,
createComponents,
updateComponents,
deleteComponents,
deleteComponent,
};