Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Solve foreign key error on push transfers #19870

Merged
merged 3 commits into from Mar 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
@@ -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;
Expand All @@ -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);

Expand All @@ -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,
Expand All @@ -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) {
Expand All @@ -114,5 +73,3 @@ const createEntitiesWriteStream = (options: IEntitiesRestoreStreamOptions) => {
},
});
};

export { createEntitiesWriteStream };
118 changes: 118 additions & 0 deletions 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);
});
});
55 changes: 53 additions & 2 deletions 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<TUID extends Common.UID.Schema> = Attribute.GetValues<
TUID,
Expand Down Expand Up @@ -596,6 +596,56 @@ const cloneComponent = async <TUID extends Common.UID.Component>(
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,
Expand All @@ -604,4 +654,5 @@ export {
deleteComponents,
deleteComponent,
cloneComponents,
resolveComponentUID,
};