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 { 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 { type PersonWorkspaceEntity } from 'src/modules/person/standard-objects/person.workspace-entity'; describe('people merge resolvers (integration)', () => { let createdPersonIdsForCleaning: string[] = []; afterEach(async () => { if (createdPersonIdsForCleaning.length > 0) { await deleteRecordsByIds('person', createdPersonIdsForCleaning); createdPersonIdsForCleaning = []; } }); 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([ 'john.alt@example.com', 'john.work@example.com', 'jane.alt@example.com', 'jane.personal@example.com', ]), ); expect(mergedPerson.emails.additionalEmails).toHaveLength(4); }); 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(3); expect(additionalEmails).toEqual( expect.arrayContaining([ '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.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([ '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: '5559876543' }), expect.objectContaining({ number: '4441112222' }), ]), ); expect(mergedPerson.phones.additionalPhones).toHaveLength(2); 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: '8104078034' }), expect.objectContaining({ number: '123456789' }), ]), ); expect(mergedPerson.whatsapp.additionalPhones).toHaveLength(2); }); }); });