diff --git a/packages/core/data-transfer/src/strapi/providers/local-destination/strategies/restore/entities.ts b/packages/core/data-transfer/src/strapi/providers/local-destination/strategies/restore/entities.ts index 2730f88a4f8..ddcd8ba1365 100644 --- a/packages/core/data-transfer/src/strapi/providers/local-destination/strategies/restore/entities.ts +++ b/packages/core/data-transfer/src/strapi/providers/local-destination/strategies/restore/entities.ts @@ -1,12 +1,13 @@ import { Writable } from 'stream'; -import type { LoadedStrapi, Common, Schema } from '@strapi/types'; +import type { LoadedStrapi, Common } from '@strapi/types'; -import { get, last } from 'lodash/fp'; +import { last } from 'lodash/fp'; import { ProviderTransferError } from '../../../../../errors/providers'; import type { IEntity, Transaction } from '../../../../../../types'; import { json } from '../../../../../utils'; import * as queries from '../../../../queries'; +import { resolveComponentUID } from '../../../../../utils/components'; interface IEntitiesRestoreStreamOptions { strapi: LoadedStrapi; @@ -18,7 +19,7 @@ interface IEntitiesRestoreStreamOptions { transaction?: Transaction; } -const createEntitiesWriteStream = (options: IEntitiesRestoreStreamOptions) => { +export const createEntitiesWriteStream = (options: IEntitiesRestoreStreamOptions) => { const { strapi, updateMappingTable, transaction } = options; const query = queries.entity.createEntityQuery(strapi); @@ -31,48 +32,6 @@ const createEntitiesWriteStream = (options: IEntitiesRestoreStreamOptions) => { const { create, getDeepPopulateComponentLikeQuery } = query(type); const contentType = strapi.getModel(type); - let cType: - | Schema.ContentType - | Schema.Component - | ((...opts: any[]) => Schema.ContentType | Schema.Component) = contentType; - - /** - * Resolve the component UID of an entity's attribute based - * on a given path (components & dynamic zones only) - */ - const resolveType = (paths: string[]): Common.UID.Schema | undefined => { - let value: unknown = data; - - for (const path of paths) { - value = get(path, value); - - // Needed when the value of cType should be computed - // based on the next value (eg: dynamic zones) - if (typeof cType === 'function') { - cType = cType(value); - } - - if (path in cType.attributes) { - const attribute = cType.attributes[path]; - - if (attribute.type === 'component') { - cType = strapi.getModel(attribute.component); - } - - if (attribute.type === 'dynamiczone') { - cType = ({ __component }: { __component: Common.UID.Component }) => - strapi.getModel(__component); - } - } - } - - if ('uid' in cType) { - return cType.uid; - } - - return undefined; - }; - try { const created = await create({ data, @@ -89,7 +48,7 @@ const createEntitiesWriteStream = (options: IEntitiesRestoreStreamOptions) => { // update the mapping the table accordingly diffs.forEach((diff) => { if (diff.kind === 'modified' && last(diff.path) === 'id') { - const target = resolveType(diff.path); + const target = resolveComponentUID({ paths: diff.path, data, contentType, strapi }); // If no type is found for the given path, then ignore the diff if (!target) { @@ -114,5 +73,3 @@ const createEntitiesWriteStream = (options: IEntitiesRestoreStreamOptions) => { }, }); }; - -export { createEntitiesWriteStream }; diff --git a/packages/core/data-transfer/src/utils/__tests__/components.test.ts b/packages/core/data-transfer/src/utils/__tests__/components.test.ts new file mode 100644 index 00000000000..476c00768f2 --- /dev/null +++ b/packages/core/data-transfer/src/utils/__tests__/components.test.ts @@ -0,0 +1,118 @@ +import type { LoadedStrapi, Schema } from '@strapi/types'; +import { resolveComponentUID } from '../components'; + +const baseContentType: Schema.ContentType = { + collectionName: 'test', + info: { + singularName: 'test', + pluralName: 'tests', + displayName: 'Test', + }, + attributes: { + // To fill in the different tests + }, + options: { + draftAndPublish: false, + }, + kind: 'collectionType', + modelType: 'contentType', + modelName: 'user', + uid: 'api::test.test', + globalId: 'Test', +}; + +describe('resolveComponentUID', () => { + const uid = 'test.test'; + + it('should return the component UID when the path matches a repeatable component', () => { + const contentType: Schema.ContentType | Schema.Component = { + ...baseContentType, + attributes: { + relsRepeatable: { + type: 'component', + repeatable: true, + component: uid, + }, + }, + }; + const strapi = { + getModel: jest.fn().mockReturnValueOnce({ + collectionName: 'components_test_rels_repeatables', + attributes: { + // doesn't matter + }, + uid, + }), + } as unknown as LoadedStrapi; + const paths = ['relsRepeatable', '0', 'id']; + + const data = { + relsRepeatable: [{ id: 1, title: 'test' }], + }; + + const expectedUID = resolveComponentUID({ paths, strapi, data, contentType }); + + expect(expectedUID).toEqual(uid); + }); + + it('should return the component UID when the path matches a single component', () => { + const contentType: Schema.ContentType | Schema.Component = { + ...baseContentType, + attributes: { + rels: { + type: 'component', + repeatable: false, + component: uid, + }, + }, + }; + const strapi = { + getModel: jest.fn().mockReturnValueOnce({ + collectionName: 'components_test_rels', + attributes: { + // doesn't matter + }, + uid, + }), + } as unknown as LoadedStrapi; + const paths = ['rels', 'id']; + + const data = { + rels: { id: 1, title: 'test' }, + }; + + const expectedUID = resolveComponentUID({ paths, strapi, data, contentType }); + + expect(expectedUID).toEqual(uid); + }); + + it('should return the component UID when the path matches a dynamic zone', () => { + const contentType: Schema.ContentType | Schema.Component = { + ...baseContentType, + attributes: { + dz: { + type: 'dynamiczone', + components: [uid], + }, + }, + }; + const strapi = { + getModel: jest.fn().mockReturnValueOnce({ + collectionName: 'components_test_rels', + attributes: { + // doesn't matter + }, + uid, + }), + } as unknown as LoadedStrapi; + const paths = ['dz', '0', 'id']; + + const data = { + dz: [{ __component: 'test.rels', id: 1, title: 'test' }], + }; + + const expectedUID = resolveComponentUID({ paths, strapi, data, contentType }); + + expect(expectedUID).toEqual(uid); + }); +}); diff --git a/packages/core/data-transfer/src/utils/components.ts b/packages/core/data-transfer/src/utils/components.ts index 5be42441134..46f0e344710 100644 --- a/packages/core/data-transfer/src/utils/components.ts +++ b/packages/core/data-transfer/src/utils/components.ts @@ -1,8 +1,8 @@ import _ from 'lodash'; -import { has, omit, pipe, assign } from 'lodash/fp'; +import { get, has, omit, pipe, assign } from 'lodash/fp'; import { contentTypes as contentTypesUtils, mapAsync, errors } from '@strapi/utils'; -import type { Attribute, Common, Schema, Utils, EntityService } from '@strapi/types'; +import type { Attribute, Common, Schema, Utils, EntityService, LoadedStrapi } from '@strapi/types'; type LoadedComponents = Attribute.GetValues< TUID, @@ -596,6 +596,56 @@ const cloneComponent = async ( return strapi.query(uid).clone(data.id, { data: transform(data) }); }; +/** + * Resolve the component UID of an entity's attribute based + * on a given path (components & dynamic zones only) + */ +const resolveComponentUID = ({ + paths, + strapi, + data, + contentType, +}: { + paths: string[]; + strapi: LoadedStrapi; + data: any; + contentType: Schema.ContentType; +}): Common.UID.Schema | undefined => { + let value: unknown = data; + let cType: + | Schema.ContentType + | Schema.Component + | ((...opts: any[]) => Schema.ContentType | Schema.Component) = contentType; + for (const path of paths) { + value = get(path, value); + + // Needed when the value of cType should be computed + // based on the next value (eg: dynamic zones) + if (typeof cType === 'function') { + cType = cType(value); + } + + if (path in cType.attributes) { + const attribute: Attribute.Any = cType.attributes[path]; + + if (attribute.type === 'component') { + cType = strapi.getModel(attribute.component); + } + + if (attribute.type === 'dynamiczone') { + cType = ({ __component }: { __component: Common.UID.Component }) => + strapi.getModel(__component); + } + } + } + + if ('uid' in cType) { + return cType.uid; + } + + return undefined; +}; + export { omitComponentData, getComponents, @@ -604,4 +654,5 @@ export { deleteComponents, deleteComponent, cloneComponents, + resolveComponentUID, };