mirror of
https://github.com/twentyhq/twenty.git
synced 2026-06-12 09:57:03 -04:00
merge records composite type (#14005)
/closes #13989 --------- Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
@@ -26,6 +26,7 @@ import {
|
||||
import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper';
|
||||
import { buildColumnsToReturn } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-return';
|
||||
import { hasRecordFieldValue } from 'src/engine/api/graphql/graphql-query-runner/utils/has-record-field-value.util';
|
||||
import { mergeFieldValues } from 'src/engine/api/graphql/graphql-query-runner/utils/merge-field-values.util';
|
||||
import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||
import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util';
|
||||
import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
@@ -188,15 +189,19 @@ export class GraphqlQueryMergeManyResolverService extends GraphqlQueryBaseResolv
|
||||
} else if (recordsWithValues.length === 1) {
|
||||
mergedResult[fieldName] = recordsWithValues[0].value;
|
||||
} else {
|
||||
const priorityValue = recordsWithValues.find(
|
||||
(item) => item.recordId === priorityRecordId,
|
||||
);
|
||||
const fieldMetadata = Object.values(
|
||||
objectMetadataItemWithFieldMaps.fieldsById,
|
||||
).find((field) => field?.name === fieldName);
|
||||
|
||||
if (priorityValue) {
|
||||
mergedResult[fieldName] = priorityValue.value;
|
||||
} else {
|
||||
mergedResult[fieldName] = recordsWithValues[0].value;
|
||||
if (!fieldMetadata) {
|
||||
return;
|
||||
}
|
||||
|
||||
mergedResult[fieldName] = mergeFieldValues(
|
||||
fieldMetadata.type,
|
||||
recordsWithValues,
|
||||
priorityRecordId,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -211,11 +216,7 @@ export class GraphqlQueryMergeManyResolverService extends GraphqlQueryBaseResolv
|
||||
objectMetadataItemWithFieldMaps.fieldsById,
|
||||
).find((field) => field?.name === fieldName);
|
||||
|
||||
if (fieldMetadata?.isSystem) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
return fieldMetadata?.isSystem ?? false;
|
||||
}
|
||||
|
||||
private createDryRunResponse(
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
import { mergeFieldValues } from 'src/engine/api/graphql/graphql-query-runner/utils/merge-field-values.util';
|
||||
|
||||
describe('mergeFieldValues', () => {
|
||||
const PRIORITY_RECORD_ID = 'priority-record-id';
|
||||
const RECORDS_WITH_VALUES = [
|
||||
{ value: 'value1', recordId: 'record1' },
|
||||
{ value: 'value2', recordId: PRIORITY_RECORD_ID },
|
||||
{ value: 'value3', recordId: 'record3' },
|
||||
];
|
||||
|
||||
describe('default field types', () => {
|
||||
it('should return priority field value for text fields', () => {
|
||||
const result = mergeFieldValues(
|
||||
FieldMetadataType.TEXT,
|
||||
RECORDS_WITH_VALUES,
|
||||
PRIORITY_RECORD_ID,
|
||||
);
|
||||
|
||||
expect(result).toBe('value2');
|
||||
});
|
||||
|
||||
it('should throw error when priority record is not found', () => {
|
||||
const recordsWithoutPriorityValue = [
|
||||
{ value: 'value1', recordId: 'record1' },
|
||||
{ value: null, recordId: PRIORITY_RECORD_ID },
|
||||
{ value: 'value3', recordId: 'record3' },
|
||||
];
|
||||
|
||||
expect(() =>
|
||||
mergeFieldValues(
|
||||
FieldMetadataType.TEXT,
|
||||
recordsWithoutPriorityValue,
|
||||
'non-existent-id',
|
||||
),
|
||||
).toThrow('Priority record with ID non-existent-id not found');
|
||||
});
|
||||
});
|
||||
|
||||
describe('array field types', () => {
|
||||
it('should merge array values', () => {
|
||||
const arrayRecords = [
|
||||
{ value: ['a', 'b'], recordId: 'record1' },
|
||||
{ value: ['b', 'c'], recordId: PRIORITY_RECORD_ID },
|
||||
{ value: ['c', 'd'], recordId: 'record3' },
|
||||
];
|
||||
|
||||
const result = mergeFieldValues(
|
||||
FieldMetadataType.ARRAY,
|
||||
arrayRecords,
|
||||
PRIORITY_RECORD_ID,
|
||||
);
|
||||
|
||||
expect(result).toEqual(['a', 'b', 'c', 'd']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('arrayable field types', () => {
|
||||
it('should merge emails for EMAILS field', () => {
|
||||
const emailRecords = [
|
||||
{
|
||||
value: {
|
||||
primaryEmail: 'first@example.com',
|
||||
additionalEmails: ['extra1@example.com'],
|
||||
},
|
||||
recordId: 'record1',
|
||||
},
|
||||
{
|
||||
value: {
|
||||
primaryEmail: 'priority@example.com',
|
||||
additionalEmails: ['extra2@example.com'],
|
||||
},
|
||||
recordId: PRIORITY_RECORD_ID,
|
||||
},
|
||||
];
|
||||
|
||||
const result = mergeFieldValues(
|
||||
FieldMetadataType.EMAILS,
|
||||
emailRecords,
|
||||
PRIORITY_RECORD_ID,
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
primaryEmail: 'priority@example.com',
|
||||
additionalEmails: ['extra1@example.com', 'extra2@example.com'],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,111 @@
|
||||
import { selectPriorityFieldValue } from 'src/engine/api/graphql/graphql-query-runner/utils/select-priority-field-value.util';
|
||||
|
||||
describe('selectPriorityFieldValue', () => {
|
||||
it('should return priority record value when available', () => {
|
||||
const recordsWithValues = [
|
||||
{
|
||||
recordId: '1',
|
||||
value: 'priority value',
|
||||
},
|
||||
{
|
||||
recordId: '2',
|
||||
value: 'other value',
|
||||
},
|
||||
];
|
||||
|
||||
const result = selectPriorityFieldValue(recordsWithValues, '1');
|
||||
|
||||
expect(result).toBe('priority value');
|
||||
});
|
||||
|
||||
it('should throw error when priority record not found', () => {
|
||||
const recordsWithValues = [
|
||||
{
|
||||
recordId: '2',
|
||||
value: 'first value',
|
||||
},
|
||||
{
|
||||
recordId: '3',
|
||||
value: 'second value',
|
||||
},
|
||||
];
|
||||
|
||||
expect(() => selectPriorityFieldValue(recordsWithValues, '1')).toThrow(
|
||||
'Priority record with ID 1 not found in merge candidates',
|
||||
);
|
||||
});
|
||||
|
||||
it('should return null when priority record has no value', () => {
|
||||
const recordsWithValues = [
|
||||
{
|
||||
recordId: '1',
|
||||
value: null,
|
||||
},
|
||||
{
|
||||
recordId: '2',
|
||||
value: 'fallback value',
|
||||
},
|
||||
];
|
||||
|
||||
const result = selectPriorityFieldValue(recordsWithValues, '1');
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null when priority record has empty string', () => {
|
||||
const recordsWithValues = [
|
||||
{
|
||||
recordId: '1',
|
||||
value: '',
|
||||
},
|
||||
{
|
||||
recordId: '2',
|
||||
value: 'fallback value',
|
||||
},
|
||||
];
|
||||
|
||||
const result = selectPriorityFieldValue(recordsWithValues, '1');
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null when priority record has undefined value', () => {
|
||||
const recordsWithValues = [
|
||||
{
|
||||
recordId: '1',
|
||||
value: undefined,
|
||||
},
|
||||
{
|
||||
recordId: '2',
|
||||
value: 'fallback value',
|
||||
},
|
||||
];
|
||||
|
||||
const result = selectPriorityFieldValue(recordsWithValues, '1');
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should throw error when no records exist', () => {
|
||||
expect(() => selectPriorityFieldValue([], '1')).toThrow(
|
||||
'Priority record with ID 1 not found in merge candidates',
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle complex object values', () => {
|
||||
const recordsWithValues = [
|
||||
{
|
||||
recordId: '1',
|
||||
value: { name: 'priority object', id: 1 },
|
||||
},
|
||||
{
|
||||
recordId: '2',
|
||||
value: { name: 'fallback object', id: 2 },
|
||||
},
|
||||
];
|
||||
|
||||
const result = selectPriorityFieldValue(recordsWithValues, '1');
|
||||
|
||||
expect(result).toEqual({ name: 'priority object', id: 1 });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
import { hasRecordFieldValue } from 'src/engine/api/graphql/graphql-query-runner/utils/has-record-field-value.util';
|
||||
|
||||
type RecordWithValue<T> = { value: T; recordId: string };
|
||||
|
||||
export const mergeArrayFieldValues = <T>(
|
||||
recordsWithValues: RecordWithValue<T>[],
|
||||
): T[] | null => {
|
||||
const allValues: T[] = [];
|
||||
|
||||
recordsWithValues.forEach((record) => {
|
||||
if (record.value === null || record.value === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Array.isArray(record.value)) {
|
||||
throw new Error(
|
||||
`Expected array value but received ${typeof record.value}`,
|
||||
);
|
||||
}
|
||||
|
||||
allValues.push(
|
||||
...record.value.filter((value) => hasRecordFieldValue(value)),
|
||||
);
|
||||
});
|
||||
|
||||
const uniqueValues = Array.from(new Set(allValues));
|
||||
|
||||
return uniqueValues.length > 0 ? uniqueValues : null;
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
import { hasRecordFieldValue } from 'src/engine/api/graphql/graphql-query-runner/utils/has-record-field-value.util';
|
||||
import { parseArrayOrJsonStringToArray } from 'src/engine/api/graphql/graphql-query-runner/utils/parse-additional-items.util';
|
||||
import { type EmailsMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/emails.composite-type';
|
||||
|
||||
export const mergeEmailsFieldValues = (
|
||||
recordsWithValues: { value: EmailsMetadata; recordId: string }[],
|
||||
priorityRecordId: string,
|
||||
): EmailsMetadata => {
|
||||
if (recordsWithValues.length === 0) {
|
||||
return {
|
||||
primaryEmail: '',
|
||||
additionalEmails: null,
|
||||
};
|
||||
}
|
||||
|
||||
let primaryEmail = '';
|
||||
const priorityRecord = recordsWithValues.find(
|
||||
(record) => record.recordId === priorityRecordId,
|
||||
);
|
||||
|
||||
if (
|
||||
priorityRecord &&
|
||||
hasRecordFieldValue(priorityRecord.value.primaryEmail)
|
||||
) {
|
||||
primaryEmail = priorityRecord.value.primaryEmail;
|
||||
} else {
|
||||
const fallbackRecord = recordsWithValues.find((record) =>
|
||||
hasRecordFieldValue(record.value.primaryEmail),
|
||||
);
|
||||
|
||||
primaryEmail = fallbackRecord?.value.primaryEmail || '';
|
||||
}
|
||||
|
||||
const allAdditionalEmails: string[] = [];
|
||||
|
||||
recordsWithValues.forEach((record) => {
|
||||
const additionalEmails = parseArrayOrJsonStringToArray<string>(
|
||||
record.value.additionalEmails,
|
||||
);
|
||||
|
||||
allAdditionalEmails.push(
|
||||
...additionalEmails.filter((email) => hasRecordFieldValue(email)),
|
||||
);
|
||||
});
|
||||
|
||||
const uniqueAdditionalEmails = Array.from(new Set(allAdditionalEmails));
|
||||
|
||||
return {
|
||||
primaryEmail,
|
||||
additionalEmails:
|
||||
uniqueAdditionalEmails.length > 0 ? uniqueAdditionalEmails : null,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
import { type EmailsMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/emails.composite-type';
|
||||
import { type LinksMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/links.composite-type';
|
||||
import { type PhonesMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/phones.composite-type';
|
||||
|
||||
import { mergeArrayFieldValues } from './merge-array-field-values.util';
|
||||
import { mergeEmailsFieldValues } from './merge-emails-field-values.util';
|
||||
import { mergeLinksFieldValues } from './merge-links-field-values.util';
|
||||
import { mergePhonesFieldValues } from './merge-phones-field-values.util';
|
||||
import { selectPriorityFieldValue } from './select-priority-field-value.util';
|
||||
|
||||
export const mergeFieldValues = (
|
||||
fieldType: FieldMetadataType,
|
||||
recordsWithValues: { value: unknown; recordId: string }[],
|
||||
priorityRecordId: string,
|
||||
): unknown => {
|
||||
switch (fieldType) {
|
||||
case FieldMetadataType.ARRAY:
|
||||
case FieldMetadataType.MULTI_SELECT:
|
||||
return mergeArrayFieldValues(recordsWithValues);
|
||||
|
||||
case FieldMetadataType.EMAILS:
|
||||
return mergeEmailsFieldValues(
|
||||
recordsWithValues as { value: EmailsMetadata; recordId: string }[],
|
||||
priorityRecordId,
|
||||
);
|
||||
|
||||
case FieldMetadataType.PHONES:
|
||||
return mergePhonesFieldValues(
|
||||
recordsWithValues as { value: PhonesMetadata; recordId: string }[],
|
||||
priorityRecordId,
|
||||
);
|
||||
|
||||
case FieldMetadataType.LINKS:
|
||||
return mergeLinksFieldValues(
|
||||
recordsWithValues as { value: LinksMetadata; recordId: string }[],
|
||||
priorityRecordId,
|
||||
);
|
||||
|
||||
default:
|
||||
return selectPriorityFieldValue(recordsWithValues, priorityRecordId);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,69 @@
|
||||
import uniqBy from 'lodash.uniqby';
|
||||
|
||||
import { hasRecordFieldValue } from 'src/engine/api/graphql/graphql-query-runner/utils/has-record-field-value.util';
|
||||
import {
|
||||
type LinkMetadata,
|
||||
type LinksMetadata,
|
||||
} from 'src/engine/metadata-modules/field-metadata/composite-types/links.composite-type';
|
||||
|
||||
import { parseArrayOrJsonStringToArray } from './parse-additional-items.util';
|
||||
|
||||
export const mergeLinksFieldValues = (
|
||||
recordsWithValues: { value: LinksMetadata; recordId: string }[],
|
||||
priorityRecordId: string,
|
||||
): LinksMetadata => {
|
||||
if (recordsWithValues.length === 0) {
|
||||
return {
|
||||
primaryLinkUrl: '',
|
||||
primaryLinkLabel: '',
|
||||
secondaryLinks: null,
|
||||
};
|
||||
}
|
||||
|
||||
let primaryLinkUrl = '';
|
||||
let primaryLinkLabel = '';
|
||||
|
||||
const priorityRecord = recordsWithValues.find(
|
||||
(record) => record.recordId === priorityRecordId,
|
||||
);
|
||||
|
||||
if (
|
||||
priorityRecord &&
|
||||
hasRecordFieldValue(priorityRecord.value.primaryLinkUrl)
|
||||
) {
|
||||
primaryLinkUrl = priorityRecord.value.primaryLinkUrl;
|
||||
primaryLinkLabel = priorityRecord.value.primaryLinkLabel;
|
||||
} else {
|
||||
const fallbackRecord = recordsWithValues.find((record) =>
|
||||
hasRecordFieldValue(record.value.primaryLinkUrl),
|
||||
);
|
||||
|
||||
if (fallbackRecord) {
|
||||
primaryLinkUrl = fallbackRecord.value.primaryLinkUrl;
|
||||
primaryLinkLabel = fallbackRecord.value.primaryLinkLabel;
|
||||
}
|
||||
}
|
||||
|
||||
const allSecondaryLinks: LinkMetadata[] = [];
|
||||
|
||||
recordsWithValues.forEach((record) => {
|
||||
const secondaryLinks = parseArrayOrJsonStringToArray<LinkMetadata>(
|
||||
record.value.secondaryLinks,
|
||||
);
|
||||
|
||||
allSecondaryLinks.push(
|
||||
...secondaryLinks.filter((link) => hasRecordFieldValue(link.url)),
|
||||
);
|
||||
});
|
||||
|
||||
const uniqueSecondaryLinks = uniqBy(allSecondaryLinks, 'url');
|
||||
|
||||
const result = {
|
||||
primaryLinkLabel,
|
||||
primaryLinkUrl,
|
||||
secondaryLinks:
|
||||
uniqueSecondaryLinks.length > 0 ? uniqueSecondaryLinks : null,
|
||||
};
|
||||
|
||||
return result;
|
||||
};
|
||||
@@ -0,0 +1,71 @@
|
||||
import { type CountryCode } from 'libphonenumber-js';
|
||||
import uniqBy from 'lodash.uniqby';
|
||||
|
||||
import { hasRecordFieldValue } from 'src/engine/api/graphql/graphql-query-runner/utils/has-record-field-value.util';
|
||||
import {
|
||||
type AdditionalPhoneMetadata,
|
||||
type PhonesMetadata,
|
||||
} from 'src/engine/metadata-modules/field-metadata/composite-types/phones.composite-type';
|
||||
|
||||
export const mergePhonesFieldValues = (
|
||||
recordsWithValues: { value: PhonesMetadata; recordId: string }[],
|
||||
priorityRecordId: string,
|
||||
): PhonesMetadata => {
|
||||
if (recordsWithValues.length === 0) {
|
||||
return {
|
||||
primaryPhoneNumber: '',
|
||||
primaryPhoneCountryCode: 'US',
|
||||
primaryPhoneCallingCode: '',
|
||||
additionalPhones: null,
|
||||
};
|
||||
}
|
||||
|
||||
let primaryPhoneNumber = '';
|
||||
let primaryPhoneCountryCode: CountryCode | null = null;
|
||||
let primaryPhoneCallingCode = '';
|
||||
|
||||
const priorityRecord = recordsWithValues.find(
|
||||
(record) => record.recordId === priorityRecordId,
|
||||
);
|
||||
|
||||
if (
|
||||
priorityRecord &&
|
||||
hasRecordFieldValue(priorityRecord.value.primaryPhoneNumber)
|
||||
) {
|
||||
primaryPhoneNumber = priorityRecord.value.primaryPhoneNumber;
|
||||
primaryPhoneCountryCode = priorityRecord.value.primaryPhoneCountryCode;
|
||||
primaryPhoneCallingCode = priorityRecord.value.primaryPhoneCallingCode;
|
||||
} else {
|
||||
const fallbackRecord = recordsWithValues.find((record) =>
|
||||
hasRecordFieldValue(record.value.primaryPhoneNumber),
|
||||
);
|
||||
|
||||
if (fallbackRecord) {
|
||||
primaryPhoneNumber = fallbackRecord.value.primaryPhoneNumber;
|
||||
primaryPhoneCountryCode = fallbackRecord.value.primaryPhoneCountryCode;
|
||||
primaryPhoneCallingCode = fallbackRecord.value.primaryPhoneCallingCode;
|
||||
}
|
||||
}
|
||||
|
||||
const allAdditionalPhones: AdditionalPhoneMetadata[] = [];
|
||||
|
||||
recordsWithValues.forEach((record) => {
|
||||
if (Array.isArray(record.value.additionalPhones)) {
|
||||
allAdditionalPhones.push(
|
||||
...record.value.additionalPhones.filter((phone) =>
|
||||
hasRecordFieldValue(phone.number),
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const uniqueAdditionalPhones = uniqBy(allAdditionalPhones, 'number');
|
||||
|
||||
return {
|
||||
primaryPhoneNumber,
|
||||
primaryPhoneCountryCode: primaryPhoneCountryCode!,
|
||||
primaryPhoneCallingCode,
|
||||
additionalPhones:
|
||||
uniqueAdditionalPhones.length > 0 ? uniqueAdditionalPhones : null,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
export const parseArrayOrJsonStringToArray = <T>(
|
||||
value: T[] | string | null | undefined,
|
||||
): T[] => {
|
||||
if (Array.isArray(value)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (typeof value === 'string') {
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
|
||||
return Array.isArray(parsed) ? parsed : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { hasRecordFieldValue } from 'src/engine/api/graphql/graphql-query-runner/utils/has-record-field-value.util';
|
||||
|
||||
export const selectPriorityFieldValue = <T>(
|
||||
recordsWithValues: { value: T; recordId: string }[],
|
||||
priorityRecordId: string,
|
||||
): T | null => {
|
||||
const priorityRecord = recordsWithValues.find(
|
||||
(record) => record.recordId === priorityRecordId,
|
||||
);
|
||||
|
||||
if (!isDefined(priorityRecord)) {
|
||||
throw new Error(
|
||||
`Priority record with ID ${priorityRecordId} not found in merge candidates`,
|
||||
);
|
||||
}
|
||||
|
||||
if (hasRecordFieldValue(priorityRecord.value)) {
|
||||
return priorityRecord.value;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
@@ -23,5 +23,5 @@ export const emailsCompositeType: CompositeType = {
|
||||
|
||||
export type EmailsMetadata = {
|
||||
primaryEmail: string;
|
||||
additionalEmails: object | null;
|
||||
additionalEmails: string[] | null;
|
||||
};
|
||||
|
||||
@@ -127,14 +127,14 @@ describe('findPersonByPrimaryOrAdditionalEmail', () => {
|
||||
id: 'person-1',
|
||||
emails: {
|
||||
primaryEmail: 'other@example.com',
|
||||
additionalEmails: [],
|
||||
additionalEmails: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'person-2',
|
||||
emails: {
|
||||
primaryEmail: 'test@example.com',
|
||||
additionalEmails: [],
|
||||
additionalEmails: null,
|
||||
},
|
||||
},
|
||||
] as PersonWorkspaceEntity[];
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
export const COMPANY_GQL_FIELDS = `
|
||||
id
|
||||
name
|
||||
domainName {
|
||||
primaryLinkLabel
|
||||
primaryLinkUrl
|
||||
secondaryLinks
|
||||
}
|
||||
linkedinLink {
|
||||
primaryLinkLabel
|
||||
primaryLinkUrl
|
||||
secondaryLinks
|
||||
}
|
||||
createdAt
|
||||
deletedAt
|
||||
`;
|
||||
@@ -11,6 +11,19 @@ export const PERSON_GQL_FIELDS = `
|
||||
}
|
||||
emails {
|
||||
primaryEmail
|
||||
additionalEmails
|
||||
}
|
||||
phones {
|
||||
primaryPhoneNumber
|
||||
primaryPhoneCountryCode
|
||||
primaryPhoneCallingCode
|
||||
additionalPhones
|
||||
}
|
||||
whatsapp {
|
||||
primaryPhoneNumber
|
||||
primaryPhoneCountryCode
|
||||
primaryPhoneCallingCode
|
||||
additionalPhones
|
||||
}
|
||||
createdAt
|
||||
deletedAt
|
||||
|
||||
@@ -0,0 +1,252 @@
|
||||
import { COMPANY_GQL_FIELDS } from 'test/integration/constants/company-gql-fields.constants';
|
||||
import { createManyOperationFactory } from 'test/integration/graphql/utils/create-many-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';
|
||||
|
||||
describe('companies merge resolvers (integration)', () => {
|
||||
let createdCompanyIds: string[] = [];
|
||||
|
||||
afterEach(async () => {
|
||||
if (createdCompanyIds.length > 0) {
|
||||
await deleteRecordsByIds('company', createdCompanyIds);
|
||||
createdCompanyIds = [];
|
||||
}
|
||||
});
|
||||
|
||||
describe('merging links composite fields', () => {
|
||||
it('should merge linkedinLink composite field correctly', async () => {
|
||||
const createCompaniesOperation = createManyOperationFactory({
|
||||
objectMetadataSingularName: 'company',
|
||||
objectMetadataPluralName: 'companies',
|
||||
gqlFields: COMPANY_GQL_FIELDS,
|
||||
data: [
|
||||
{
|
||||
name: 'Company A',
|
||||
linkedinLink: {
|
||||
primaryLinkUrl: 'https://linkedin.com/company/company-a',
|
||||
primaryLinkLabel: 'Main LinkedIn',
|
||||
secondaryLinks: [
|
||||
{
|
||||
url: 'linkedin.com/company/subsidiary-a1',
|
||||
label: 'Subsidiary A1',
|
||||
},
|
||||
{
|
||||
url: 'linkedin.com/company/subsidiary-a2',
|
||||
label: 'Subsidiary A2',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Company B',
|
||||
linkedinLink: {
|
||||
primaryLinkUrl: 'https://linkedin.com/company/company-b',
|
||||
primaryLinkLabel: 'Main LinkedIn',
|
||||
secondaryLinks: [
|
||||
{
|
||||
url: 'linkedin.com/company/subsidiary-b1',
|
||||
label: 'Subsidiary B1',
|
||||
},
|
||||
{
|
||||
url: 'linkedin.com/company/subsidiary-b2',
|
||||
label: 'Subsidiary B2',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const createResponse = await makeGraphqlAPIRequest(
|
||||
createCompaniesOperation,
|
||||
);
|
||||
|
||||
expect(createResponse.body.data.createCompanies).toHaveLength(2);
|
||||
|
||||
const company1Id = createResponse.body.data.createCompanies[0].id;
|
||||
const company2Id = createResponse.body.data.createCompanies[1].id;
|
||||
|
||||
createdCompanyIds.push(company1Id, company2Id);
|
||||
|
||||
const mergeOperation = mergeManyOperationFactory({
|
||||
objectMetadataPluralName: 'companies',
|
||||
gqlFields: COMPANY_GQL_FIELDS,
|
||||
ids: [company1Id, company2Id],
|
||||
conflictPriorityIndex: 0,
|
||||
});
|
||||
|
||||
const mergeResponse = await makeGraphqlAPIRequest(mergeOperation);
|
||||
|
||||
expect(mergeResponse.body.errors).toBeUndefined();
|
||||
|
||||
const mergedCompany = mergeResponse.body.data.mergeCompanies;
|
||||
|
||||
expect(mergedCompany.linkedinLink.primaryLinkUrl).toBe(
|
||||
'https://linkedin.com/company/company-a',
|
||||
);
|
||||
expect(mergedCompany.linkedinLink.primaryLinkLabel).toBe('Main LinkedIn');
|
||||
expect(mergedCompany.linkedinLink.secondaryLinks).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
url: 'linkedin.com/company/subsidiary-a1',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
url: 'linkedin.com/company/subsidiary-a2',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
url: 'linkedin.com/company/subsidiary-b1',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
url: 'linkedin.com/company/subsidiary-b2',
|
||||
}),
|
||||
]),
|
||||
);
|
||||
expect(mergedCompany.linkedinLink.secondaryLinks).toHaveLength(4);
|
||||
});
|
||||
|
||||
it('should merge links with deduplication', async () => {
|
||||
const createCompaniesOperation = createManyOperationFactory({
|
||||
objectMetadataSingularName: 'company',
|
||||
objectMetadataPluralName: 'companies',
|
||||
gqlFields: COMPANY_GQL_FIELDS,
|
||||
data: [
|
||||
{
|
||||
name: 'Tech Corp',
|
||||
linkedinLink: {
|
||||
primaryLinkUrl: 'https://linkedin.com/company/tech-corp',
|
||||
primaryLinkLabel: '',
|
||||
secondaryLinks: [
|
||||
{
|
||||
url: 'linkedin.com/company/shared-subsidiary',
|
||||
label: 'Shared Sub',
|
||||
},
|
||||
{
|
||||
url: 'linkedin.com/company/tech-division',
|
||||
label: 'Tech Division',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Corp Tech',
|
||||
linkedinLink: {
|
||||
primaryLinkUrl: 'https://linkedin.com/company/corp-tech',
|
||||
primaryLinkLabel: '',
|
||||
secondaryLinks: [
|
||||
{
|
||||
url: 'linkedin.com/company/shared-subsidiary',
|
||||
label: 'Shared Sub Different Label',
|
||||
},
|
||||
{
|
||||
url: 'linkedin.com/company/corp-division',
|
||||
label: 'Corp Division',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const createResponse = await makeGraphqlAPIRequest(
|
||||
createCompaniesOperation,
|
||||
);
|
||||
const company1Id = createResponse.body.data.createCompanies[0].id;
|
||||
const company2Id = createResponse.body.data.createCompanies[1].id;
|
||||
|
||||
createdCompanyIds.push(company1Id, company2Id);
|
||||
|
||||
const mergeOperation = mergeManyOperationFactory({
|
||||
objectMetadataPluralName: 'companies',
|
||||
gqlFields: COMPANY_GQL_FIELDS,
|
||||
ids: [company1Id, company2Id],
|
||||
conflictPriorityIndex: 0,
|
||||
});
|
||||
|
||||
const mergeResponse = await makeGraphqlAPIRequest(mergeOperation);
|
||||
const mergedCompany = mergeResponse.body.data.mergeCompanies;
|
||||
|
||||
expect(mergedCompany.linkedinLink.primaryLinkUrl).toBe(
|
||||
'https://linkedin.com/company/tech-corp',
|
||||
);
|
||||
const secondaryLinks = mergedCompany.linkedinLink.secondaryLinks;
|
||||
|
||||
expect(secondaryLinks).toHaveLength(3);
|
||||
|
||||
const urls = secondaryLinks.map((link: { url: string }) => link.url);
|
||||
|
||||
expect(urls).toEqual(
|
||||
expect.arrayContaining([
|
||||
'linkedin.com/company/shared-subsidiary',
|
||||
'linkedin.com/company/tech-division',
|
||||
'linkedin.com/company/corp-division',
|
||||
]),
|
||||
);
|
||||
|
||||
const duplicateCount = urls.filter(
|
||||
(url: string) => url === 'linkedin.com/company/shared-subsidiary',
|
||||
).length;
|
||||
|
||||
expect(duplicateCount).toBe(1);
|
||||
});
|
||||
|
||||
it('should respect priority index for links unique constraint', async () => {
|
||||
const createCompaniesOperation = createManyOperationFactory({
|
||||
objectMetadataSingularName: 'company',
|
||||
objectMetadataPluralName: 'companies',
|
||||
gqlFields: COMPANY_GQL_FIELDS,
|
||||
data: [
|
||||
{
|
||||
name: 'Priority First',
|
||||
linkedinLink: {
|
||||
primaryLinkUrl: 'https://linkedin.com/company/first-priority',
|
||||
primaryLinkLabel: 'First Label',
|
||||
secondaryLinks: [
|
||||
{ url: 'linkedin.com/company/first-sub', label: 'First Sub' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Priority Second',
|
||||
linkedinLink: {
|
||||
primaryLinkUrl: 'https://linkedin.com/company/second-priority',
|
||||
primaryLinkLabel: 'Second Label',
|
||||
secondaryLinks: [
|
||||
{ url: 'linkedin.com/company/second-sub', label: 'Second Sub' },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const createResponse = await makeGraphqlAPIRequest(
|
||||
createCompaniesOperation,
|
||||
);
|
||||
const company1Id = createResponse.body.data.createCompanies[0].id;
|
||||
const company2Id = createResponse.body.data.createCompanies[1].id;
|
||||
|
||||
createdCompanyIds.push(company1Id, company2Id);
|
||||
|
||||
const mergeWithPriority1 = mergeManyOperationFactory({
|
||||
objectMetadataPluralName: 'companies',
|
||||
gqlFields: COMPANY_GQL_FIELDS,
|
||||
ids: [company1Id, company2Id],
|
||||
conflictPriorityIndex: 1,
|
||||
});
|
||||
|
||||
const mergeResponse = await makeGraphqlAPIRequest(mergeWithPriority1);
|
||||
const mergedCompany = mergeResponse.body.data.mergeCompanies;
|
||||
|
||||
expect(mergedCompany.linkedinLink.primaryLinkUrl).toBe(
|
||||
'https://linkedin.com/company/second-priority',
|
||||
);
|
||||
expect(mergedCompany.linkedinLink.primaryLinkLabel).toBe('Second Label');
|
||||
expect(mergedCompany.linkedinLink.secondaryLinks).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ url: 'linkedin.com/company/first-sub' }),
|
||||
expect.objectContaining({ url: 'linkedin.com/company/second-sub' }),
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,408 @@
|
||||
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';
|
||||
|
||||
describe('people merge resolvers (integration)', () => {
|
||||
let createdPersonIds: string[] = [];
|
||||
|
||||
afterEach(async () => {
|
||||
if (createdPersonIds.length > 0) {
|
||||
await deleteRecordsByIds('person', createdPersonIds);
|
||||
createdPersonIds = [];
|
||||
}
|
||||
});
|
||||
|
||||
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 person1Id = createResponse.body.data.createPeople[0].id;
|
||||
const person2Id = createResponse.body.data.createPeople[1].id;
|
||||
|
||||
createdPersonIds.push(person1Id, person2Id);
|
||||
|
||||
const mergeOperation = mergeManyOperationFactory({
|
||||
objectMetadataPluralName: 'people',
|
||||
gqlFields: PERSON_GQL_FIELDS,
|
||||
ids: [person1Id, person2Id],
|
||||
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 person1Id = createResponse.body.data.createPeople[0].id;
|
||||
const person2Id = createResponse.body.data.createPeople[1].id;
|
||||
|
||||
createdPersonIds.push(person1Id, person2Id);
|
||||
|
||||
const mergeOperation = mergeManyOperationFactory({
|
||||
objectMetadataPluralName: 'people',
|
||||
gqlFields: PERSON_GQL_FIELDS,
|
||||
ids: [person1Id, person2Id],
|
||||
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 person1Id = createResponse.body.data.createPeople[0].id;
|
||||
const person2Id = createResponse.body.data.createPeople[1].id;
|
||||
|
||||
createdPersonIds.push(person1Id, person2Id);
|
||||
|
||||
const mergeWithPriority1 = mergeManyOperationFactory({
|
||||
objectMetadataPluralName: 'people',
|
||||
gqlFields: PERSON_GQL_FIELDS,
|
||||
ids: [person1Id, person2Id],
|
||||
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[0].id;
|
||||
const person2Id = createResponse.body.data.createPeople[1].id;
|
||||
|
||||
createdPersonIds.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 person1Id = createResponse.body.data.createPeople[0].id;
|
||||
const person2Id = createResponse.body.data.createPeople[1].id;
|
||||
|
||||
createdPersonIds.push(person1Id, person2Id);
|
||||
|
||||
const mergeOperation = mergeManyOperationFactory({
|
||||
objectMetadataPluralName: 'people',
|
||||
gqlFields: PERSON_GQL_FIELDS,
|
||||
ids: [person1Id, person2Id],
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
import gql from 'graphql-tag';
|
||||
import { capitalize } from 'twenty-shared/utils';
|
||||
|
||||
type MergeManyOperationFactoryParams = {
|
||||
objectMetadataPluralName: string;
|
||||
gqlFields: string;
|
||||
ids: string[];
|
||||
conflictPriorityIndex: number;
|
||||
dryRun?: boolean;
|
||||
};
|
||||
|
||||
export const mergeManyOperationFactory = ({
|
||||
objectMetadataPluralName,
|
||||
gqlFields,
|
||||
ids,
|
||||
conflictPriorityIndex,
|
||||
dryRun = false,
|
||||
}: MergeManyOperationFactoryParams) => {
|
||||
const capitalizedObjectName = capitalize(objectMetadataPluralName);
|
||||
const mutationName = `merge${capitalizedObjectName}`;
|
||||
|
||||
return {
|
||||
query: gql`
|
||||
mutation Merge${capitalizedObjectName}($ids: [UUID!]!, $conflictPriorityIndex: Int!, $dryRun: Boolean! = false) {
|
||||
${mutationName}(ids: $ids, conflictPriorityIndex: $conflictPriorityIndex, dryRun: $dryRun) {
|
||||
${gqlFields}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
ids,
|
||||
conflictPriorityIndex,
|
||||
dryRun,
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
const TEST_SCHEMA_NAME = 'workspace_1wgvd1injqtife6y4rvfbu3h5';
|
||||
|
||||
export const deleteRecordsByIds = async (
|
||||
objectNameSingular: string,
|
||||
recordIds: string[],
|
||||
) => {
|
||||
if (!recordIds.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Create placeholders for parameterized query: $1, $2, $3, etc.
|
||||
const placeholders = recordIds
|
||||
.map((_, index) => `$${index + 1}`)
|
||||
.join(', ');
|
||||
|
||||
// @ts-expect-error legacy noImplicitAny
|
||||
await global.testDataSource.query(
|
||||
`DELETE from "${TEST_SCHEMA_NAME}"."${objectNameSingular}" WHERE id IN (${placeholders})`,
|
||||
recordIds,
|
||||
);
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
};
|
||||
@@ -46,8 +46,8 @@
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"accessibility",
|
||||
"assets",
|
||||
"accessibility",
|
||||
"components",
|
||||
"display",
|
||||
"feedback",
|
||||
|
||||
Reference in New Issue
Block a user