UNPKG

@strapi/data-transfer

Version:

Data transfer capabilities for Strapi

231 lines (225 loc) • 10.3 kB
'use strict'; var _ = require('lodash'); var fp = require('lodash/fp'); var utils = require('@strapi/utils'); function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; } var ___default = /*#__PURE__*/_interopDefault(_); const isDialectMySQL = ()=>strapi.db?.dialect.client === 'mysql'; function omitComponentData(contentType, data) { const { attributes } = contentType; const componentAttributes = Object.keys(attributes).filter((attributeName)=>utils.contentTypes.isComponentAttribute(attributes[attributeName])); return fp.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 (!fp.has(attributeName, data) || !utils.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 utils.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 utils.async.map(dynamiczoneValues, createDynamicZoneComponents, { concurrency: isDialectMySQL() && !strapi.db?.inTransaction() ? 1 : Infinity }); continue; } } return componentBody; }; const getComponents = async (uid, entity)=>{ const componentAttributes = utils.contentTypes.getComponentAttributes(strapi.getModel(uid)); if (___default.default.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 utils.async.map(___default.default.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 utils.async.map(___default.default.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 = fp.pipe(// Make sure we don't save the component with a pre-defined ID fp.omit('id'), // Remove the component data from the original data object ... (payload)=>omitComponentData(model, payload), // ... and assign the newly created component instead fp.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 (!___default.default.isObjectLike(oldValue) || !___default.default.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 (!___default.default.isObjectLike(oldData) || !___default.default.isObjectLike(newData)) { return; } const { attributes = {} } = currentSchema; for (const [attributeName, attribute] of Object.entries(attributes)){ const oldValue = oldData[attributeName]; const newValue = newData[attributeName]; if (___default.default.isNil(oldValue) || ___default.default.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; }; exports.collectComponentIdMappings = collectComponentIdMappings; exports.createComponents = createComponents; exports.deleteComponent = deleteComponent; exports.deleteComponents = deleteComponents; exports.getComponents = getComponents; exports.omitComponentData = omitComponentData; //# sourceMappingURL=components.js.map