Some checks failed
App Prod-Parity E2E Dispatch / dispatch (push) Failing after 10s
CD Storybook UI / build (push) Failing after 3m25s
CD Storybook UI / dispatch (push) Has been skipped
CD deploy main / website-changed-files (push) Successful in 18m8s
CI Agent Skills / changed-files-check (push) Successful in 18m9s
CI Create App E2E minimal / changed-files-check (push) Successful in 24s
CI Create App / changed-files-check (push) Successful in 26s
CI Docs / changed-files-check (push) Successful in 26s
CI Emails / changed-files-check (push) Successful in 28s
CI Example App Hello World / changed-files-check (push) Successful in 23s
CI Front / changed-files-check (push) Has been skipped
CI Example App Postcard / changed-files-check (push) Successful in 27s
CI UI / changed-files-check (push) Has been skipped
CI Utils Test / changed-files-check (push) Successful in 45s
CI Twenty Apps / discover (push) Successful in 1m3s
CD deploy main / deploy-main (push) Failing after 3s
CI Agent Skills / agent-skills-validate (push) Has been skipped
CI Create App / create-app-test (lint) (push) Has been skipped
CI Emails / emails-test (push) Has been skipped
CI Front / front-task (lingui:compile) (push) Has been skipped
CI Front / front-task (lint) (push) Has been skipped
CI Example App Postcard / example-app-postcard (push) Has been skipped
CI UI / ui-task (lint) (push) Has been skipped
CI UI / ui-task (pack-size) (push) Has been skipped
CI UI / ui-task (size) (push) Has been skipped
CI UI / ui-task (test) (push) Has been skipped
CI UI / ui-task (typecheck) (push) Has been skipped
CI Utils Test / utils-test (push) Has been skipped
CI Twenty Apps / ${{ matrix.app.name }} (push) Has been skipped
CI Emails / ci-emails-status-check (push) Successful in 2s
CI Example App Hello World / ci-example-app-hello-world-status-check (push) Successful in 2s
CI Example App Postcard / ci-example-app-postcard-status-check (push) Successful in 3s
CI UI / ui-sb-test (push) Has been skipped
CI Utils Test / ci-utils-test-status-check (push) Successful in 2s
CI Twenty Apps / ci-twenty-apps-status-check (push) Successful in 2s
CI UI / ci-ui-status-check (push) Successful in 2s
CI Desktop / desktop (push) Failing after 1m8s
Push docs to Crowdin / Push documentation to Crowdin (push) Failing after 55s
Push translations to Crowdin / Extract and upload translations (push) Failing after 5s
Push website translations to Crowdin / Extract and upload website translations (push) Failing after 5s
CI Create App / create-app-test (test) (push) Has been skipped
CI Create App / create-app-test (typecheck) (push) Has been skipped
CI Docs / docs-lint (push) Has been skipped
CI Example App Hello World / example-app-hello-world (push) Has been skipped
CI Front / front-task (lingui:extract) (push) Has been skipped
CI Front / front-task (test) (push) Has been skipped
CI Front / front-task (typecheck) (push) Has been skipped
CI UI / ui-sb-build (push) Failing after 1m32s
CI Twenty Apps / ${{ matrix.app.name }} (${{ matrix.server-source }}) (push) Has been skipped
CI Agent Skills / agent-skills-publish (push) Has been skipped
CI Create App / ci-create-app-status-check (push) Successful in 2s
CI Agent Skills / agent-skills-remote-source (push) Has been skipped
CI Create App E2E minimal / create-app-e2e-minimal (push) Has been cancelled
CI E2E Main / e2e-test (push) Has been cancelled
CI Front / front-sb-build (push) Has been cancelled
CI Front / front-build (push) Has been cancelled
CI Create App E2E minimal / ci-create-app-e2e-minimal-status-check (push) Has been cancelled
CI E2E Main / ci-e2e-main-status-check (push) Has been cancelled
CI E2E Main / notify-main-ci-failure (push) Has been cancelled
CI E2E Main / qa-scout-comment (push) Has been cancelled
CI Front / front-sb-test (push) Has been cancelled
CI Front / front-sb-screenshots (push) Has been cancelled
CI Front / ci-front-status-check (push) Has been cancelled
DPA Sub-Processor Sync / sync-subprocessors (push) Failing after 3s
AI Catalog Sync / sync-catalog (push) Failing after 2s
785 lines
25 KiB
TypeScript
785 lines
25 KiB
TypeScript
import { PERSON_GQL_FIELDS } from 'test/integration/constants/person-gql-fields.constants';
|
|
import { createManyOperationFactory } from 'test/integration/graphql/utils/create-many-operation-factory.util';
|
|
import { createOneOperationFactory } from 'test/integration/graphql/utils/create-one-operation-factory.util';
|
|
import { findManyOperationFactory } from 'test/integration/graphql/utils/find-many-operation-factory.util';
|
|
import { findOneOperationFactory } from 'test/integration/graphql/utils/find-one-operation-factory.util';
|
|
import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util';
|
|
import { mergeManyOperationFactory } from 'test/integration/graphql/utils/merge-many-operation-factory.util';
|
|
import { deleteRecordsByIds } from 'test/integration/utils/delete-records-by-ids';
|
|
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
|
|
import { gql } from 'graphql-tag';
|
|
import { QUERY_MAX_RECORDS } from 'twenty-shared/constants';
|
|
import { isDefined } from 'twenty-shared/utils';
|
|
|
|
import { type PersonWorkspaceEntity } from 'src/modules/person/standard-objects/person.workspace-entity';
|
|
|
|
const FIND_CREATED_TIMELINE_ACTIVITY_TYPE = gql`
|
|
query FindCreatedTimelineActivityType {
|
|
timelineActivityTypes {
|
|
id
|
|
action
|
|
objectUniversalIdentifier
|
|
}
|
|
}
|
|
`;
|
|
|
|
describe('people merge resolvers (integration)', () => {
|
|
let createdPersonIdsForCleaning: string[] = [];
|
|
let createdMessageThreadIdsForCleaning: string[] = [];
|
|
let createdMessageThreadTargetIdsForCleaning: string[] = [];
|
|
let createdTimelineActivityIdsForCleaning: string[] = [];
|
|
let createdTimelineActivityTypeId: string;
|
|
|
|
const createPeoplePair = async (namePrefix: string) => {
|
|
const createPersonsResponse = await makeGraphqlAPIRequest(
|
|
createManyOperationFactory({
|
|
objectMetadataSingularName: 'person',
|
|
objectMetadataPluralName: 'people',
|
|
gqlFields: 'id',
|
|
data: [
|
|
{ name: { firstName: namePrefix, lastName: 'Priority' } },
|
|
{ name: { firstName: namePrefix, lastName: 'Duplicate' } },
|
|
],
|
|
}),
|
|
);
|
|
|
|
expect(createPersonsResponse.body.errors).toBeUndefined();
|
|
|
|
const [priorityPerson, duplicatePerson] =
|
|
createPersonsResponse.body.data.createPeople;
|
|
|
|
createdPersonIdsForCleaning.push(priorityPerson.id, duplicatePerson.id);
|
|
|
|
return {
|
|
priorityPersonId: priorityPerson.id,
|
|
duplicatePersonId: duplicatePerson.id,
|
|
};
|
|
};
|
|
|
|
beforeAll(async () => {
|
|
const response = await makeMetadataAPIRequest({
|
|
query: FIND_CREATED_TIMELINE_ACTIVITY_TYPE,
|
|
});
|
|
|
|
expect(response.body.errors).toBeUndefined();
|
|
|
|
const createdTimelineActivityType =
|
|
response.body.data.timelineActivityTypes.find(
|
|
({
|
|
action,
|
|
objectUniversalIdentifier,
|
|
}: {
|
|
action: string | null;
|
|
objectUniversalIdentifier: string | null;
|
|
}) => action === 'created' && !isDefined(objectUniversalIdentifier),
|
|
);
|
|
|
|
if (!isDefined(createdTimelineActivityType)) {
|
|
throw new Error('Shared created timeline activity type is not installed');
|
|
}
|
|
|
|
createdTimelineActivityTypeId = createdTimelineActivityType.id;
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await deleteRecordsByIds(
|
|
'messageThreadTarget',
|
|
createdMessageThreadTargetIdsForCleaning,
|
|
);
|
|
createdMessageThreadTargetIdsForCleaning = [];
|
|
|
|
await deleteRecordsByIds(
|
|
'messageThread',
|
|
createdMessageThreadIdsForCleaning,
|
|
);
|
|
createdMessageThreadIdsForCleaning = [];
|
|
|
|
await deleteRecordsByIds(
|
|
'timelineActivity',
|
|
createdTimelineActivityIdsForCleaning,
|
|
);
|
|
createdTimelineActivityIdsForCleaning = [];
|
|
|
|
await deleteRecordsByIds('person', createdPersonIdsForCleaning);
|
|
createdPersonIdsForCleaning = [];
|
|
});
|
|
|
|
describe('migrating related records', () => {
|
|
it('should migrate timeline activities targeting the merged person', async () => {
|
|
const createPersonsOperation = createManyOperationFactory({
|
|
objectMetadataSingularName: 'person',
|
|
objectMetadataPluralName: 'people',
|
|
gqlFields: PERSON_GQL_FIELDS,
|
|
data: [
|
|
{
|
|
name: {
|
|
firstName: 'Priority',
|
|
lastName: 'Person',
|
|
},
|
|
},
|
|
{
|
|
name: {
|
|
firstName: 'Duplicate',
|
|
lastName: 'Person',
|
|
},
|
|
},
|
|
],
|
|
});
|
|
|
|
const createPersonsResponse = await makeGraphqlAPIRequest(
|
|
createPersonsOperation,
|
|
);
|
|
const [priorityPerson, duplicatePerson] =
|
|
createPersonsResponse.body.data.createPeople;
|
|
|
|
createdPersonIdsForCleaning.push(priorityPerson.id, duplicatePerson.id);
|
|
|
|
const createTimelineActivityResponse = await makeGraphqlAPIRequest(
|
|
createOneOperationFactory({
|
|
objectMetadataSingularName: 'timelineActivity',
|
|
gqlFields: 'id targetPersonId',
|
|
data: {
|
|
happensAt: new Date().toISOString(),
|
|
timelineActivityTypeId: createdTimelineActivityTypeId,
|
|
targetPersonId: duplicatePerson.id,
|
|
},
|
|
}),
|
|
);
|
|
|
|
expect(createTimelineActivityResponse.body.errors).toBeUndefined();
|
|
|
|
const timelineActivity =
|
|
createTimelineActivityResponse.body.data.createTimelineActivity;
|
|
|
|
const mergeResponse = await makeGraphqlAPIRequest(
|
|
mergeManyOperationFactory({
|
|
objectMetadataPluralName: 'people',
|
|
gqlFields: PERSON_GQL_FIELDS,
|
|
ids: [priorityPerson.id, duplicatePerson.id],
|
|
conflictPriorityIndex: 0,
|
|
}),
|
|
);
|
|
|
|
expect(mergeResponse.body.errors).toBeUndefined();
|
|
|
|
const findTimelineActivityResponse = await makeGraphqlAPIRequest(
|
|
findOneOperationFactory({
|
|
objectMetadataSingularName: 'timelineActivity',
|
|
gqlFields: 'id targetPersonId',
|
|
filter: {
|
|
id: {
|
|
eq: timelineActivity.id,
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
|
|
expect(findTimelineActivityResponse.body.errors).toBeUndefined();
|
|
expect(
|
|
findTimelineActivityResponse.body.data.timelineActivity.targetPersonId,
|
|
).toBe(priorityPerson.id);
|
|
});
|
|
});
|
|
|
|
describe('migrating related records that carry a unique constraint', () => {
|
|
const createMessageThread = async () => {
|
|
const response = await makeGraphqlAPIRequest(
|
|
createOneOperationFactory({
|
|
objectMetadataSingularName: 'messageThread',
|
|
gqlFields: 'id',
|
|
data: {},
|
|
}),
|
|
);
|
|
|
|
expect(response.body.errors).toBeUndefined();
|
|
|
|
const messageThreadId = response.body.data.createMessageThread.id;
|
|
|
|
createdMessageThreadIdsForCleaning.push(messageThreadId);
|
|
|
|
return messageThreadId;
|
|
};
|
|
|
|
const attachPersonToMessageThread = async ({
|
|
messageThreadId,
|
|
targetPersonId,
|
|
}: {
|
|
messageThreadId: string;
|
|
targetPersonId: string;
|
|
}) => {
|
|
const response = await makeGraphqlAPIRequest(
|
|
createOneOperationFactory({
|
|
objectMetadataSingularName: 'messageThreadTarget',
|
|
gqlFields: 'id',
|
|
data: { messageThreadId, targetPersonId },
|
|
}),
|
|
);
|
|
|
|
expect(response.body.errors).toBeUndefined();
|
|
|
|
createdMessageThreadTargetIdsForCleaning.push(
|
|
response.body.data.createMessageThreadTarget.id,
|
|
);
|
|
};
|
|
|
|
const findMessageThreadTargetsOfPerson = async (targetPersonId: string) => {
|
|
const response = await makeGraphqlAPIRequest(
|
|
findManyOperationFactory({
|
|
objectMetadataSingularName: 'messageThreadTarget',
|
|
objectMetadataPluralName: 'messageThreadTargets',
|
|
gqlFields: 'id messageThreadId targetPersonId',
|
|
filter: { targetPersonId: { eq: targetPersonId } },
|
|
}),
|
|
);
|
|
|
|
expect(response.body.errors).toBeUndefined();
|
|
|
|
return response.body.data.messageThreadTargets.edges.map(
|
|
({ node }: { node: { messageThreadId: string } }) => node,
|
|
);
|
|
};
|
|
|
|
it('should merge two people attached to the same message thread', async () => {
|
|
const { priorityPersonId, duplicatePersonId } =
|
|
await createPeoplePair('Shared thread');
|
|
|
|
const sharedMessageThreadId = await createMessageThread();
|
|
const duplicateOnlyMessageThreadId = await createMessageThread();
|
|
|
|
await attachPersonToMessageThread({
|
|
messageThreadId: sharedMessageThreadId,
|
|
targetPersonId: priorityPersonId,
|
|
});
|
|
await attachPersonToMessageThread({
|
|
messageThreadId: sharedMessageThreadId,
|
|
targetPersonId: duplicatePersonId,
|
|
});
|
|
await attachPersonToMessageThread({
|
|
messageThreadId: duplicateOnlyMessageThreadId,
|
|
targetPersonId: duplicatePersonId,
|
|
});
|
|
|
|
const mergeResponse = await makeGraphqlAPIRequest(
|
|
mergeManyOperationFactory({
|
|
objectMetadataPluralName: 'people',
|
|
gqlFields: PERSON_GQL_FIELDS,
|
|
ids: [priorityPersonId, duplicatePersonId],
|
|
conflictPriorityIndex: 0,
|
|
}),
|
|
);
|
|
|
|
expect(mergeResponse.body.errors).toBeUndefined();
|
|
expect(mergeResponse.body.data.mergePeople.id).toBe(priorityPersonId);
|
|
|
|
const survivingTargets =
|
|
await findMessageThreadTargetsOfPerson(priorityPersonId);
|
|
|
|
expect(
|
|
survivingTargets.map(
|
|
({ messageThreadId }: { messageThreadId: string }) => messageThreadId,
|
|
),
|
|
).toEqual(
|
|
expect.arrayContaining([
|
|
sharedMessageThreadId,
|
|
duplicateOnlyMessageThreadId,
|
|
]),
|
|
);
|
|
expect(survivingTargets).toHaveLength(2);
|
|
|
|
expect(
|
|
await findMessageThreadTargetsOfPerson(duplicatePersonId),
|
|
).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
describe('migrating more related records than the mutation batch limit', () => {
|
|
it('should merge a person carrying more related records than a single batch allows', async () => {
|
|
const { priorityPersonId, duplicatePersonId } =
|
|
await createPeoplePair('Over batch limit');
|
|
|
|
const timelineActivityCount = QUERY_MAX_RECORDS + 1;
|
|
const timelineActivityPayloads = Array.from(
|
|
{ length: timelineActivityCount },
|
|
() => ({
|
|
happensAt: new Date().toISOString(),
|
|
timelineActivityTypeId: createdTimelineActivityTypeId,
|
|
targetPersonId: duplicatePersonId,
|
|
}),
|
|
);
|
|
|
|
for (
|
|
let batchStart = 0;
|
|
batchStart < timelineActivityCount;
|
|
batchStart += QUERY_MAX_RECORDS
|
|
) {
|
|
const createResponse = await makeGraphqlAPIRequest(
|
|
createManyOperationFactory({
|
|
objectMetadataSingularName: 'timelineActivity',
|
|
objectMetadataPluralName: 'timelineActivities',
|
|
gqlFields: 'id',
|
|
data: timelineActivityPayloads.slice(
|
|
batchStart,
|
|
batchStart + QUERY_MAX_RECORDS,
|
|
),
|
|
}),
|
|
);
|
|
|
|
expect(createResponse.body.errors).toBeUndefined();
|
|
|
|
createdTimelineActivityIdsForCleaning.push(
|
|
...createResponse.body.data.createTimelineActivities.map(
|
|
({ id }: { id: string }) => id,
|
|
),
|
|
);
|
|
}
|
|
|
|
expect(createdTimelineActivityIdsForCleaning).toHaveLength(
|
|
timelineActivityCount,
|
|
);
|
|
|
|
const mergeResponse = await makeGraphqlAPIRequest(
|
|
mergeManyOperationFactory({
|
|
objectMetadataPluralName: 'people',
|
|
gqlFields: PERSON_GQL_FIELDS,
|
|
ids: [priorityPersonId, duplicatePersonId],
|
|
conflictPriorityIndex: 0,
|
|
}),
|
|
);
|
|
|
|
expect(mergeResponse.body.errors).toBeUndefined();
|
|
|
|
const findMigratedResponse = await makeGraphqlAPIRequest(
|
|
findManyOperationFactory({
|
|
objectMetadataSingularName: 'timelineActivity',
|
|
objectMetadataPluralName: 'timelineActivities',
|
|
gqlFields: 'id targetPersonId',
|
|
filter: { id: { in: createdTimelineActivityIdsForCleaning } },
|
|
first: timelineActivityCount,
|
|
}),
|
|
);
|
|
|
|
expect(findMigratedResponse.body.errors).toBeUndefined();
|
|
|
|
const migratedTargetPersonIds =
|
|
findMigratedResponse.body.data.timelineActivities.edges.map(
|
|
({ node }: { node: { targetPersonId: string } }) =>
|
|
node.targetPersonId,
|
|
);
|
|
|
|
expect(migratedTargetPersonIds).toHaveLength(timelineActivityCount);
|
|
expect(new Set(migratedTargetPersonIds)).toEqual(
|
|
new Set([priorityPersonId]),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('merging composite fields', () => {
|
|
it('should merge emails composite field correctly', async () => {
|
|
const createPersonsOperation = createManyOperationFactory({
|
|
objectMetadataSingularName: 'person',
|
|
objectMetadataPluralName: 'people',
|
|
gqlFields: PERSON_GQL_FIELDS,
|
|
data: [
|
|
{
|
|
name: {
|
|
firstName: 'John',
|
|
lastName: 'Doe',
|
|
},
|
|
emails: {
|
|
primaryEmail: 'john@example.com',
|
|
additionalEmails: [
|
|
'john.alt@example.com',
|
|
'john.work@example.com',
|
|
],
|
|
},
|
|
},
|
|
{
|
|
name: {
|
|
firstName: 'Jane',
|
|
lastName: 'Doe',
|
|
},
|
|
emails: {
|
|
primaryEmail: 'jane@example.com',
|
|
additionalEmails: [
|
|
'jane.alt@example.com',
|
|
'jane.personal@example.com',
|
|
],
|
|
},
|
|
},
|
|
],
|
|
});
|
|
|
|
const createResponse = await makeGraphqlAPIRequest(
|
|
createPersonsOperation,
|
|
);
|
|
|
|
expect(createResponse.body.data.createPeople).toHaveLength(2);
|
|
|
|
const createdPersonIds = [...createResponse.body.data.createPeople].map(
|
|
({ id }: { id: string }) => id,
|
|
);
|
|
|
|
createdPersonIdsForCleaning.push(...createdPersonIds);
|
|
|
|
const mergeOperation = mergeManyOperationFactory({
|
|
objectMetadataPluralName: 'people',
|
|
gqlFields: PERSON_GQL_FIELDS,
|
|
ids: createdPersonIds,
|
|
conflictPriorityIndex: 0,
|
|
});
|
|
|
|
const mergeResponse = await makeGraphqlAPIRequest(mergeOperation);
|
|
|
|
expect(mergeResponse.body.errors).toBeUndefined();
|
|
|
|
const mergedPerson = mergeResponse.body.data.mergePeople;
|
|
|
|
expect(mergedPerson.emails.primaryEmail).toBe('john@example.com');
|
|
expect(mergedPerson.emails.additionalEmails).toEqual(
|
|
expect.arrayContaining([
|
|
'jane@example.com',
|
|
'john.alt@example.com',
|
|
'john.work@example.com',
|
|
'jane.alt@example.com',
|
|
'jane.personal@example.com',
|
|
]),
|
|
);
|
|
expect(mergedPerson.emails.additionalEmails).toHaveLength(5);
|
|
});
|
|
|
|
it('should merge emails with deduplication', async () => {
|
|
const createPersonsOperation = createManyOperationFactory({
|
|
objectMetadataSingularName: 'person',
|
|
objectMetadataPluralName: 'people',
|
|
gqlFields: PERSON_GQL_FIELDS,
|
|
data: [
|
|
{
|
|
name: {
|
|
firstName: 'Alice',
|
|
lastName: 'Smith',
|
|
},
|
|
emails: {
|
|
primaryEmail: 'alice@example.com',
|
|
additionalEmails: [
|
|
'shared@example.com',
|
|
'alice.work@example.com',
|
|
],
|
|
},
|
|
},
|
|
{
|
|
name: {
|
|
firstName: 'Bob',
|
|
lastName: 'Smith',
|
|
},
|
|
emails: {
|
|
primaryEmail: 'bob@example.com',
|
|
additionalEmails: ['shared@example.com', 'bob.work@example.com'],
|
|
},
|
|
},
|
|
],
|
|
});
|
|
|
|
const createResponse = await makeGraphqlAPIRequest(
|
|
createPersonsOperation,
|
|
);
|
|
|
|
const createdPersonIds = createResponse.body.data.createPeople.map(
|
|
({ id }: { id: string }) => id,
|
|
);
|
|
|
|
createdPersonIdsForCleaning.push(...createdPersonIds);
|
|
|
|
const mergeOperation = mergeManyOperationFactory({
|
|
objectMetadataPluralName: 'people',
|
|
gqlFields: PERSON_GQL_FIELDS,
|
|
ids: createdPersonIds,
|
|
conflictPriorityIndex: 0,
|
|
});
|
|
|
|
const mergeResponse = await makeGraphqlAPIRequest(mergeOperation);
|
|
const mergedPerson = mergeResponse.body.data.mergePeople;
|
|
|
|
expect(mergedPerson.emails.primaryEmail).toBe('alice@example.com');
|
|
const additionalEmails = mergedPerson.emails.additionalEmails;
|
|
|
|
expect(additionalEmails).toHaveLength(4);
|
|
expect(additionalEmails).toEqual(
|
|
expect.arrayContaining([
|
|
'bob@example.com',
|
|
'shared@example.com',
|
|
'alice.work@example.com',
|
|
'bob.work@example.com',
|
|
]),
|
|
);
|
|
|
|
const duplicateCount = additionalEmails.filter(
|
|
(email: string) => email === 'shared@example.com',
|
|
).length;
|
|
|
|
expect(duplicateCount).toBe(1);
|
|
});
|
|
|
|
it('should respect priority index for unique constraint fields', async () => {
|
|
const createPersonsOperation = createManyOperationFactory({
|
|
objectMetadataSingularName: 'person',
|
|
objectMetadataPluralName: 'people',
|
|
gqlFields: PERSON_GQL_FIELDS,
|
|
data: [
|
|
{
|
|
name: {
|
|
firstName: 'First',
|
|
lastName: 'Person',
|
|
},
|
|
emails: {
|
|
primaryEmail: 'first@example.com',
|
|
additionalEmails: ['first.extra@example.com'],
|
|
},
|
|
},
|
|
{
|
|
name: {
|
|
firstName: 'Second',
|
|
lastName: 'Person',
|
|
},
|
|
emails: {
|
|
primaryEmail: 'second@example.com',
|
|
additionalEmails: ['second.extra@example.com'],
|
|
},
|
|
},
|
|
],
|
|
});
|
|
|
|
const createResponse = await makeGraphqlAPIRequest(
|
|
createPersonsOperation,
|
|
);
|
|
|
|
const createdPersonIds = createResponse.body.data.createPeople.map(
|
|
({ id }: { id: string }) => id,
|
|
);
|
|
|
|
createdPersonIdsForCleaning.push(...createdPersonIds);
|
|
|
|
const mergeWithPriority1 = mergeManyOperationFactory({
|
|
objectMetadataPluralName: 'people',
|
|
gqlFields: PERSON_GQL_FIELDS,
|
|
ids: createdPersonIds,
|
|
conflictPriorityIndex: 1,
|
|
});
|
|
|
|
const mergeResponse = await makeGraphqlAPIRequest(mergeWithPriority1);
|
|
const mergedPerson = mergeResponse.body.data.mergePeople;
|
|
|
|
expect(mergedPerson.emails.primaryEmail).toBe('second@example.com');
|
|
expect(mergedPerson.emails.additionalEmails).toEqual(
|
|
expect.arrayContaining([
|
|
'first@example.com',
|
|
'first.extra@example.com',
|
|
'second.extra@example.com',
|
|
]),
|
|
);
|
|
});
|
|
|
|
it('should handle dry run mode', async () => {
|
|
const createPersonsOperation = createManyOperationFactory({
|
|
objectMetadataSingularName: 'person',
|
|
objectMetadataPluralName: 'people',
|
|
gqlFields: PERSON_GQL_FIELDS,
|
|
data: [
|
|
{
|
|
name: {
|
|
firstName: 'Test1',
|
|
lastName: 'User',
|
|
},
|
|
emails: {
|
|
primaryEmail: 'test1@example.com',
|
|
additionalEmails: ['test1.extra@example.com'],
|
|
},
|
|
},
|
|
{
|
|
name: {
|
|
firstName: 'Test2',
|
|
lastName: 'User',
|
|
},
|
|
emails: {
|
|
primaryEmail: 'test2@example.com',
|
|
additionalEmails: ['test2.extra@example.com'],
|
|
},
|
|
},
|
|
],
|
|
});
|
|
const createResponse = await makeGraphqlAPIRequest(
|
|
createPersonsOperation,
|
|
);
|
|
|
|
const person1Id = createResponse.body.data.createPeople.find(
|
|
(people: PersonWorkspaceEntity) =>
|
|
people.emails.primaryEmail === 'test1@example.com',
|
|
).id;
|
|
const person2Id = createResponse.body.data.createPeople.find(
|
|
(people: PersonWorkspaceEntity) =>
|
|
people.emails.primaryEmail === 'test2@example.com',
|
|
).id;
|
|
|
|
createdPersonIdsForCleaning.push(person1Id, person2Id);
|
|
|
|
const dryRunMergeOperation = mergeManyOperationFactory({
|
|
objectMetadataPluralName: 'people',
|
|
gqlFields: PERSON_GQL_FIELDS,
|
|
ids: [person1Id, person2Id],
|
|
conflictPriorityIndex: 0,
|
|
dryRun: true,
|
|
});
|
|
|
|
const dryRunResponse = await makeGraphqlAPIRequest(dryRunMergeOperation);
|
|
|
|
expect(dryRunResponse.body.errors).toBeUndefined();
|
|
|
|
const dryRunResult = dryRunResponse.body.data.mergePeople;
|
|
|
|
expect(dryRunResult.emails.primaryEmail).toBe('test1@example.com');
|
|
expect(dryRunResult.emails.additionalEmails).toEqual(
|
|
expect.arrayContaining([
|
|
'test2@example.com',
|
|
'test1.extra@example.com',
|
|
'test2.extra@example.com',
|
|
]),
|
|
);
|
|
|
|
const findOriginalPersons = findOneOperationFactory({
|
|
objectMetadataSingularName: 'person',
|
|
gqlFields: PERSON_GQL_FIELDS,
|
|
filter: {
|
|
id: {
|
|
eq: person2Id,
|
|
},
|
|
},
|
|
});
|
|
|
|
const findResponse = await makeGraphqlAPIRequest(findOriginalPersons);
|
|
|
|
expect(findResponse.body.data.person).toBeTruthy();
|
|
expect(findResponse.body.data.person.emails.primaryEmail).toBe(
|
|
'test2@example.com',
|
|
);
|
|
});
|
|
|
|
it('should merge phones and whatsapp composite fields correctly', async () => {
|
|
const createPersonsOperation = createManyOperationFactory({
|
|
objectMetadataSingularName: 'person',
|
|
objectMetadataPluralName: 'people',
|
|
gqlFields: PERSON_GQL_FIELDS,
|
|
data: [
|
|
{
|
|
name: {
|
|
firstName: 'Alice',
|
|
lastName: 'Johnson',
|
|
},
|
|
phones: {
|
|
primaryPhoneNumber: '5551234567',
|
|
primaryPhoneCountryCode: 'US',
|
|
primaryPhoneCallingCode: '+1',
|
|
additionalPhones: [
|
|
{
|
|
number: '5559876543',
|
|
callingCode: '+1',
|
|
countryCode: 'US',
|
|
},
|
|
],
|
|
},
|
|
whatsapp: {
|
|
primaryPhoneNumber: '810407803',
|
|
primaryPhoneCountryCode: 'FR',
|
|
primaryPhoneCallingCode: '+33',
|
|
additionalPhones: [
|
|
{
|
|
number: '8104078034',
|
|
callingCode: '+91',
|
|
countryCode: 'IN',
|
|
},
|
|
],
|
|
},
|
|
},
|
|
{
|
|
name: {
|
|
firstName: 'Bob',
|
|
lastName: 'Johnson',
|
|
},
|
|
phones: {
|
|
primaryPhoneNumber: '4445556789',
|
|
primaryPhoneCountryCode: 'US',
|
|
primaryPhoneCallingCode: '+1',
|
|
additionalPhones: [
|
|
{
|
|
number: '4441112222',
|
|
callingCode: '+1',
|
|
countryCode: 'US',
|
|
},
|
|
],
|
|
},
|
|
whatsapp: {
|
|
primaryPhoneNumber: '987654321',
|
|
primaryPhoneCountryCode: 'FR',
|
|
primaryPhoneCallingCode: '+33',
|
|
additionalPhones: [
|
|
{
|
|
number: '123456789',
|
|
callingCode: '+44',
|
|
countryCode: 'GB',
|
|
},
|
|
],
|
|
},
|
|
},
|
|
],
|
|
});
|
|
|
|
const createResponse = await makeGraphqlAPIRequest(
|
|
createPersonsOperation,
|
|
);
|
|
|
|
expect(createResponse.body.data.createPeople).toHaveLength(2);
|
|
|
|
const createdPersonIds = createResponse.body.data.createPeople.map(
|
|
({ id }: { id: string }) => id,
|
|
);
|
|
|
|
createdPersonIdsForCleaning.push(...createdPersonIds);
|
|
|
|
const mergeOperation = mergeManyOperationFactory({
|
|
objectMetadataPluralName: 'people',
|
|
gqlFields: PERSON_GQL_FIELDS,
|
|
ids: createdPersonIds,
|
|
conflictPriorityIndex: 0,
|
|
});
|
|
|
|
const mergeResponse = await makeGraphqlAPIRequest(mergeOperation);
|
|
|
|
expect(mergeResponse.body.errors).toBeUndefined();
|
|
|
|
const mergedPerson = mergeResponse.body.data.mergePeople;
|
|
|
|
expect(mergedPerson.phones.primaryPhoneNumber).toBe('5551234567');
|
|
expect(mergedPerson.phones.primaryPhoneCountryCode).toBe('US');
|
|
expect(mergedPerson.phones.primaryPhoneCallingCode).toBe('+1');
|
|
expect(mergedPerson.phones.additionalPhones).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({ number: '4445556789' }),
|
|
expect.objectContaining({ number: '5559876543' }),
|
|
expect.objectContaining({ number: '4441112222' }),
|
|
]),
|
|
);
|
|
expect(mergedPerson.phones.additionalPhones).toHaveLength(3);
|
|
|
|
expect(mergedPerson.whatsapp.primaryPhoneNumber).toBe('810407803');
|
|
expect(mergedPerson.whatsapp.primaryPhoneCountryCode).toBe('FR');
|
|
expect(mergedPerson.whatsapp.primaryPhoneCallingCode).toBe('+33');
|
|
expect(mergedPerson.whatsapp.additionalPhones).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({ number: '987654321' }),
|
|
expect.objectContaining({ number: '8104078034' }),
|
|
expect.objectContaining({ number: '123456789' }),
|
|
]),
|
|
);
|
|
expect(mergedPerson.whatsapp.additionalPhones).toHaveLength(3);
|
|
});
|
|
});
|
|
});
|