@strapi/data-transfer
Version:
Data transfer capabilities for Strapi
220 lines (217 loc) • 9.78 kB
JavaScript
import _ from 'lodash';
import { has, pipe, omit, assign } from 'lodash/fp';
import { async, contentTypes } from '@strapi/utils';
const isDialectMySQL = ()=>strapi.db?.dialect.client === 'mysql';
function omitComponentData(contentType, data) {
const { attributes } = contentType;
const componentAttributes = Object.keys(attributes).filter((attributeName)=>contentTypes.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 = {};
const attributeNames = Object.keys(attributes);
for (const attributeName of attributeNames){
const attribute = attributes[attributeName];
if (!has(attributeName, data) || !contentTypes.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 async.map(componentValue, (value)=>createComponent(componentUID, value), {
concurrency: isDialectMySQL() && !strapi.db?.inTransaction() ? 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 async.map(dynamiczoneValues, createDynamicZoneComponents, {
concurrency: isDialectMySQL() && !strapi.db?.inTransaction() ? 1 : Infinity
});
continue;
}
}
return componentBody;
};
const getComponents = async (uid, entity)=>{
const componentAttributes = contentTypes.getComponentAttributes(strapi.getModel(uid));
if (_.isEmpty(componentAttributes)) {
return {};
}
return strapi.db.query(uid).load(entity, componentAttributes);
};
const deleteComponents = async (uid, entityToDelete, { loadComponents = true } = {})=>{
const { attributes = {} } = strapi.getModel(uid);
const attributeNames = Object.keys(attributes);
for (const attributeName of attributeNames){
const attribute = attributes[attributeName];
if (attribute.type === 'component' || attribute.type === 'dynamiczone') {
let value;
if (loadComponents) {
value = await strapi.db.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 async.map(_.castArray(value), (subValue)=>deleteComponent(componentUID, subValue), {
concurrency: isDialectMySQL() && !strapi.db?.inTransaction() ? 1 : Infinity
});
} else {
// delete dynamic zone components
// MySQL/MariaDB can cause deadlocks here if concurrency higher than 1
await async.map(_.castArray(value), (subValue)=>deleteComponent(subValue.__component, subValue), {
concurrency: isDialectMySQL() && !strapi.db?.inTransaction() ? 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.db.query(uid).create({
data: transform(data)
});
};
const deleteComponent = async (uid, componentToDelete)=>{
await deleteComponents(uid, componentToDelete);
await strapi.db.query(uid).delete({
where: {
id: componentToDelete.id
}
});
};
/**
* Walk the source data and the newly created entity in parallel and collect
* the [old ID, new ID] couple of every component instance found in both trees
* (components & dynamic zones, including nested ones).
*
* Unlike a JSON diff, couples are also collected when the ID did not change,
* so the result is an exhaustive map of every component instance that was
* re-created with the entity.
*/ const collectComponentIdMappings = ({ data, created, schema, strapi: strapi1 })=>{
const mappings = [];
const visitComponent = (uid, oldValue, newValue)=>{
if (!_.isObjectLike(oldValue) || !_.isObjectLike(newValue)) {
return;
}
if (typeof oldValue.id === 'number' && typeof newValue.id === 'number') {
mappings.push({
uid,
oldID: oldValue.id,
newID: newValue.id
});
}
const componentSchema = strapi1.getModel(uid);
// Collect nested components & dynamic zones
if (componentSchema) {
visitSchema(componentSchema, oldValue, newValue);
}
};
const visitSchema = (currentSchema, oldData, newData)=>{
if (!_.isObjectLike(oldData) || !_.isObjectLike(newData)) {
return;
}
const { attributes = {} } = currentSchema;
for (const [attributeName, attribute] of Object.entries(attributes)){
const oldValue = oldData[attributeName];
const newValue = newData[attributeName];
if (_.isNil(oldValue) || _.isNil(newValue)) {
continue;
}
if (attribute.type === 'component') {
const { component: componentUID, repeatable = false } = attribute;
if (repeatable === true) {
if (!Array.isArray(oldValue) || !Array.isArray(newValue)) {
continue;
}
// Components are created (and thus populated back) in the same
// order as the source data, pair them by index
const length = Math.min(oldValue.length, newValue.length);
for(let i = 0; i < length; i += 1){
visitComponent(componentUID, oldValue[i], newValue[i]);
}
} else {
visitComponent(componentUID, oldValue, newValue);
}
}
if (attribute.type === 'dynamiczone') {
if (!Array.isArray(oldValue) || !Array.isArray(newValue)) {
continue;
}
const length = Math.min(oldValue.length, newValue.length);
for(let i = 0; i < length; i += 1){
const oldItem = oldValue[i];
const newItem = newValue[i];
const componentUID = oldItem?.__component;
// Both items must reference the same dynamic zone component for the
// ID couple to be meaningful
if (!componentUID || newItem?.__component && newItem.__component !== componentUID) {
continue;
}
visitComponent(componentUID, oldItem, newItem);
}
}
}
};
visitSchema(schema, data, created);
return mappings;
};
export { collectComponentIdMappings, createComponents, deleteComponent, deleteComponents, getComponents, omitComponentData };
//# sourceMappingURL=components.mjs.map