Handle migration of Phone field to Phones field (#7128)

This PR was created by [GitStart](https://gitstart.com/) to address the
requirements from this ticket:
[TWNTY-6260](https://clients.gitstart.com/twenty/5449/tickets/TWNTY-6260).
This ticket was imported from:
[TWNTY-6260](https://github.com/twentyhq/twenty/issues/6260)

 --- 

### Description

This is the second PR on TWNTY-6260 which handles data migration of
Phone field to Phones field.\
\
How to Test?\
 Follow the below steps:

- On the main branch, 
- go to
`packages/twenty-server/src/database/typeorm-seeds/workspace/people.ts`
and change any person's phone number to a string with characters for
example: "test invalid phone", and then reset the DB.
  - reset database using `npx nx database:reset twenty-server`
- This is to make sure that invalid numbers will be handled properly. We
should use the invalid value itself to avoid removing data and see how
the behavior is on the front end. should be the same as in the main, the
display shows the invalid value, but the input is empty when you click,
and then you can update.
- Checkout to `TWNTY-6260-phone-migration` branch
- Rebuild typescript using `npx nx build twenty-server`
- Run command `yarn command:prod upgrade-0.32` to do migration
- Run both backend and frontend to see the migrated field

### Demo

- **Loom Video:**\

<https://www.loom.com/share/4b9bcb423cee447d8ad09852a83b27da?sid=ed74ecaa-0339-4575-acdc-a863e95e94fd>

### Refs

#6260

---------

Co-authored-by: gitstart-twenty <gitstart-twenty@users.noreply.github.com>
Co-authored-by: Marie Stoppa <marie.stoppa@essec.edu>
Co-authored-by: Weiko <corentin@twenty.com>
This commit is contained in:
gitstart-app[bot]
2024-09-24 16:31:30 +02:00
committed by GitHub
parent b83f0f46e5
commit fa241fa4e9
25 changed files with 709 additions and 78 deletions

View File

@ -197,6 +197,10 @@ name
lastName lastName
} }
phone phone
{
primaryPhoneNumber
primaryPhoneCountryCode
}
linkedinLink linkedinLink
{ {
primaryLinkUrl primaryLinkUrl

View File

@ -46,7 +46,11 @@ describe('mapObjectMetadataToGraphQLQuery', () => {
primaryEmail primaryEmail
additionalEmails additionalEmails
} }
phone phone
{
primaryPhoneNumber
primaryPhoneCountryCode
}
createdAt createdAt
avatarUrl avatarUrl
jobTitle jobTitle

View File

@ -107,7 +107,7 @@ export const getObjectMetadataItemsMock = () => {
{ {
"__typename": "field", "__typename": "field",
"id": "194ff398-99f9-4cbb-b87a-e44408f9c1ed", "id": "194ff398-99f9-4cbb-b87a-e44408f9c1ed",
"type": "PHONE", "type": "PHONES",
"name": "whatsapp", "name": "whatsapp",
"label": "Whatsapp", "label": "Whatsapp",
"description": "Contact's Whatsapp Number", "description": "Contact's Whatsapp Number",
@ -614,7 +614,7 @@ export const getObjectMetadataItemsMock = () => {
{ {
"__typename": "field", "__typename": "field",
"id": "9c2bf923-304d-47b7-beb0-286e3229f6ac", "id": "9c2bf923-304d-47b7-beb0-286e3229f6ac",
"type": "TEXT", "type": "PHONES",
"name": "phone", "name": "phone",
"label": "Phone", "label": "Phone",
"description": "Contacts phone number", "description": "Contacts phone number",

View File

@ -2,7 +2,11 @@ export const PERSON_FRAGMENT = `
__typename __typename
updatedAt updatedAt
myCustomObjectId myCustomObjectId
whatsapp whatsapp {
primaryPhoneNumber
primaryPhoneCountryCode
additionalPhones
}
linkedinLink { linkedinLink {
primaryLinkUrl primaryLinkUrl
primaryLinkLabel primaryLinkLabel
@ -28,7 +32,11 @@ export const PERSON_FRAGMENT = `
} }
performanceRating performanceRating
createdAt createdAt
phone phone {
primaryPhoneNumber
primaryPhoneCountryCode
additionalPhones
}
id id
city city
companyId companyId

View File

@ -36,7 +36,10 @@ export const responseData = {
firstName: '', firstName: '',
lastName: '', lastName: '',
}, },
phone: '', phones: {
primaryPhoneCountryCode: '',
primaryPhoneNumber: '',
},
linkedinLink: { linkedinLink: {
primaryLinkUrl: '', primaryLinkUrl: '',
primaryLinkLabel: '', primaryLinkLabel: '',

View File

@ -41,7 +41,10 @@ export const responseData = {
firstName: '', firstName: '',
lastName: '', lastName: '',
}, },
phone: '', phones: {
primaryPhoneCountryCode: '',
primaryPhoneNumber: '',
},
linkedinLink: { linkedinLink: {
primaryLinkUrl: '', primaryLinkUrl: '',
primaryLinkLabel: '', primaryLinkLabel: '',

View File

@ -24,7 +24,6 @@ const basePerson = {
firstName: '', firstName: '',
lastName: '', lastName: '',
}, },
phone: '',
linkedinLink: { linkedinLink: {
primaryLinkUrl: '', primaryLinkUrl: '',
primaryLinkLabel: '', primaryLinkLabel: '',

View File

@ -1,6 +1,6 @@
import { ReactNode } from 'react';
import { MockedProvider } from '@apollo/client/testing'; import { MockedProvider } from '@apollo/client/testing';
import { act, renderHook } from '@testing-library/react'; import { act, renderHook } from '@testing-library/react';
import { ReactNode } from 'react';
import { RecoilRoot } from 'recoil'; import { RecoilRoot } from 'recoil';
import { import {
@ -11,8 +11,17 @@ import {
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
const person = { id: '36abbb63-34ed-4a16-89f5-f549ac55d0f9' }; const person = { id: '36abbb63-34ed-4a16-89f5-f549ac55d0f9' };
const update = { name: { firstName: 'John', lastName: 'Doe' } }; const update = {
const updatePerson = { ...person, ...responseData, ...update }; name: {
firstName: 'John',
lastName: 'Doe',
},
};
const updatePerson = {
...person,
...responseData,
...update,
};
const mocks = [ const mocks = [
{ {

View File

@ -6,10 +6,12 @@ import { isFieldAddress } from '@/object-record/record-field/types/guards/isFiel
import { isFieldCurrency } from '@/object-record/record-field/types/guards/isFieldCurrency'; import { isFieldCurrency } from '@/object-record/record-field/types/guards/isFieldCurrency';
import { isFieldDateTime } from '@/object-record/record-field/types/guards/isFieldDateTime'; import { isFieldDateTime } from '@/object-record/record-field/types/guards/isFieldDateTime';
import { isFieldEmail } from '@/object-record/record-field/types/guards/isFieldEmail'; import { isFieldEmail } from '@/object-record/record-field/types/guards/isFieldEmail';
import { isFieldEmails } from '@/object-record/record-field/types/guards/isFieldEmails';
import { isFieldFullName } from '@/object-record/record-field/types/guards/isFieldFullName'; import { isFieldFullName } from '@/object-record/record-field/types/guards/isFieldFullName';
import { isFieldLink } from '@/object-record/record-field/types/guards/isFieldLink'; import { isFieldLink } from '@/object-record/record-field/types/guards/isFieldLink';
import { isFieldLinks } from '@/object-record/record-field/types/guards/isFieldLinks'; import { isFieldLinks } from '@/object-record/record-field/types/guards/isFieldLinks';
import { isFieldNumber } from '@/object-record/record-field/types/guards/isFieldNumber'; import { isFieldNumber } from '@/object-record/record-field/types/guards/isFieldNumber';
import { isFieldPhones } from '@/object-record/record-field/types/guards/isFieldPhones';
import { isFieldRelation } from '@/object-record/record-field/types/guards/isFieldRelation'; import { isFieldRelation } from '@/object-record/record-field/types/guards/isFieldRelation';
import { isFieldText } from '@/object-record/record-field/types/guards/isFieldText'; import { isFieldText } from '@/object-record/record-field/types/guards/isFieldText';
import { isFieldUuid } from '@/object-record/record-field/types/guards/isFieldUuid'; import { isFieldUuid } from '@/object-record/record-field/types/guards/isFieldUuid';
@ -66,5 +68,17 @@ export const computeDraftValueFromString = <FieldValue>({
} as FieldInputDraftValue<FieldValue>; } as FieldInputDraftValue<FieldValue>;
} }
if (isFieldEmails(fieldDefinition)) {
return {
primaryEmail: value,
} as FieldInputDraftValue<FieldValue>;
}
if (isFieldPhones(fieldDefinition)) {
return {
primaryPhoneNumber: value,
} as FieldInputDraftValue<FieldValue>;
}
throw new Error(`Record field type not supported : ${fieldDefinition.type}}`); throw new Error(`Record field type not supported : ${fieldDefinition.type}}`);
}; };

View File

@ -1,4 +1,5 @@
import { act, renderHook, waitFor } from '@testing-library/react'; import { act, renderHook, waitFor } from '@testing-library/react';
import { ReactNode } from 'react';
import { percentage, sleep, useTableData } from '../useTableData'; import { percentage, sleep, useTableData } from '../useTableData';
import { useRecordBoard } from '@/object-record/record-board/hooks/useRecordBoard'; import { useRecordBoard } from '@/object-record/record-board/hooks/useRecordBoard';
@ -9,7 +10,6 @@ import { extractComponentState } from '@/ui/utilities/state/component-state/util
import { ViewType } from '@/views/types/ViewType'; import { ViewType } from '@/views/types/ViewType';
import { MockedProvider, MockedResponse } from '@apollo/client/testing'; import { MockedProvider, MockedResponse } from '@apollo/client/testing';
import gql from 'graphql-tag'; import gql from 'graphql-tag';
import { ReactNode } from 'react';
import { BrowserRouter as Router } from 'react-router-dom'; import { BrowserRouter as Router } from 'react-router-dom';
import { RecoilRoot, useRecoilValue } from 'recoil'; import { RecoilRoot, useRecoilValue } from 'recoil';
@ -26,7 +26,11 @@ const mockPerson = {
__typename: 'Person', __typename: 'Person',
updatedAt: '2021-08-03T19:20:06.000Z', updatedAt: '2021-08-03T19:20:06.000Z',
myCustomObjectId: '123', myCustomObjectId: '123',
whatsapp: '123', whatsapp: {
primaryPhoneNumber: '+1',
primaryPhoneCountryCode: '234-567-890',
additionalPhones: [],
},
linkedinLink: { linkedinLink: {
primaryLinkUrl: 'https://www.linkedin.com', primaryLinkUrl: 'https://www.linkedin.com',
primaryLinkLabel: 'linkedin', primaryLinkLabel: 'linkedin',
@ -52,7 +56,11 @@ const mockPerson = {
}, },
performanceRating: 1, performanceRating: 1,
createdAt: '2021-08-03T19:20:06.000Z', createdAt: '2021-08-03T19:20:06.000Z',
phone: 'phone', phone: {
primaryPhoneNumber: '+1',
primaryPhoneCountryCode: '234-567-890',
additionalPhones: [],
},
id: '123', id: '123',
city: 'city', city: 'city',
companyId: '1', companyId: '1',
@ -80,7 +88,11 @@ const mocks: MockedResponse[] = [
__typename __typename
updatedAt updatedAt
myCustomObjectId myCustomObjectId
whatsapp whatsapp {
primaryPhoneNumber
primaryPhoneCountryCode
additionalPhones
}
linkedinLink { linkedinLink {
primaryLinkUrl primaryLinkUrl
primaryLinkLabel primaryLinkLabel
@ -106,7 +118,11 @@ const mocks: MockedResponse[] = [
} }
performanceRating performanceRating
createdAt createdAt
phone phone {
primaryPhoneNumber
primaryPhoneCountryCode
additionalPhones
}
id id
city city
companyId companyId

View File

@ -662,7 +662,10 @@ export const mockPerformance = {
}, },
id: '20202020-2d40-4e49-8df4-9c6a049191df', id: '20202020-2d40-4e49-8df4-9c6a049191df',
email: 'lorie.vladim@google.com', email: 'lorie.vladim@google.com',
phone: '+33788901235', phones: {
primaryPhoneCountryCode: '+33',
primaryPhoneNumber: '788901235',
},
linkedinLink: { linkedinLink: {
__typename: 'Link', __typename: 'Link',
primaryLinkLabel: '', primaryLinkLabel: '',

View File

@ -56,16 +56,27 @@ export const PhonesDisplay = ({ value, isFocused }: PhonesDisplayProps) => {
], ],
); );
const parsePhoneNumberOrReturnInvalidValue = (number: string) => {
try {
return { parsedPhone: parsePhoneNumber(number) };
} catch (e) {
return { invalidPhone: number };
}
};
return isFocused ? ( return isFocused ? (
<ExpandableList isChipCountDisplayed> <ExpandableList isChipCountDisplayed>
{phones.map(({ number, countryCode }, index) => { {phones.map(({ number, countryCode }, index) => {
const parsedPhone = parsePhoneNumber(countryCode + number); const { parsedPhone, invalidPhone } =
const URI = parsedPhone.getURI(); parsePhoneNumberOrReturnInvalidValue(countryCode + number);
const URI = parsedPhone?.getURI();
return ( return (
<RoundedLink <RoundedLink
key={index} key={index}
href={URI} href={URI || ''}
label={parsedPhone.formatInternational()} label={
parsedPhone ? parsedPhone.formatInternational() : invalidPhone
}
/> />
); );
})} })}
@ -73,13 +84,16 @@ export const PhonesDisplay = ({ value, isFocused }: PhonesDisplayProps) => {
) : ( ) : (
<StyledContainer> <StyledContainer>
{phones.map(({ number, countryCode }, index) => { {phones.map(({ number, countryCode }, index) => {
const parsedPhone = parsePhoneNumber(countryCode + number); const { parsedPhone, invalidPhone } =
const URI = parsedPhone.getURI(); parsePhoneNumberOrReturnInvalidValue(countryCode + number);
const URI = parsedPhone?.getURI();
return ( return (
<RoundedLink <RoundedLink
key={index} key={index}
href={URI} href={URI || ''}
label={parsedPhone.formatInternational()} label={
parsedPhone ? parsedPhone.formatInternational() : invalidPhone
}
/> />
); );
})} })}

View File

@ -45,7 +45,10 @@ export const peopleQueryResult: { people: RecordGqlConnection } = {
__typename: 'Person', __typename: 'Person',
createdAt: '2024-08-02T09:52:46.814Z', createdAt: '2024-08-02T09:52:46.814Z',
city: 'ASd', city: 'ASd',
phone: '', phones: {
primaryPhoneNumber: '781234562',
primaryPhoneCountryCode: '+33',
},
id: 'da3c2c4b-da01-4b81-9734-226069eb4cd0', id: 'da3c2c4b-da01-4b81-9734-226069eb4cd0',
jobTitle: '', jobTitle: '',
position: 0, position: 0,
@ -172,7 +175,10 @@ export const peopleQueryResult: { people: RecordGqlConnection } = {
__typename: 'Person', __typename: 'Person',
createdAt: '2024-08-01T09:50:00.000Z', createdAt: '2024-08-01T09:50:00.000Z',
city: 'Seattle', city: 'Seattle',
phone: '+33789012345', phones: {
primaryPhoneNumber: '781234562',
primaryPhoneCountryCode: '+33',
},
id: '20202020-1c0e-494c-a1b6-85b1c6fefaa5', id: '20202020-1c0e-494c-a1b6-85b1c6fefaa5',
jobTitle: '', jobTitle: '',
position: 1, position: 1,
@ -299,7 +305,10 @@ export const peopleQueryResult: { people: RecordGqlConnection } = {
__typename: 'Person', __typename: 'Person',
createdAt: '2024-08-02T09:48:36.193Z', createdAt: '2024-08-02T09:48:36.193Z',
city: 'Los Angeles', city: 'Los Angeles',
phone: '+33780123456', phones: {
primaryPhoneNumber: '781234576',
primaryPhoneCountryCode: '+33',
},
id: '20202020-ac73-4797-824e-87a1f5aea9e0', id: '20202020-ac73-4797-824e-87a1f5aea9e0',
jobTitle: '', jobTitle: '',
position: 2, position: 2,
@ -395,7 +404,10 @@ export const peopleQueryResult: { people: RecordGqlConnection } = {
__typename: 'Person', __typename: 'Person',
createdAt: '2024-08-02T09:48:36.193Z', createdAt: '2024-08-02T09:48:36.193Z',
city: 'Seattle', city: 'Seattle',
phone: '+33789012345', phones: {
primaryPhoneNumber: '781234545',
primaryPhoneCountryCode: '+33',
},
id: '20202020-f517-42fd-80ae-14173b3b70ae', id: '20202020-f517-42fd-80ae-14173b3b70ae',
jobTitle: '', jobTitle: '',
position: 3, position: 3,
@ -491,7 +503,10 @@ export const peopleQueryResult: { people: RecordGqlConnection } = {
__typename: 'Person', __typename: 'Person',
createdAt: '2024-08-02T09:48:36.193Z', createdAt: '2024-08-02T09:48:36.193Z',
city: 'Los Angeles', city: 'Los Angeles',
phone: '+33780123456', phones: {
primaryPhoneNumber: '781234587',
primaryPhoneCountryCode: '+33',
},
id: '20202020-eee1-4690-ad2c-8619e5b56a2e', id: '20202020-eee1-4690-ad2c-8619e5b56a2e',
jobTitle: '', jobTitle: '',
position: 4, position: 4,
@ -587,7 +602,10 @@ export const peopleQueryResult: { people: RecordGqlConnection } = {
__typename: 'Person', __typename: 'Person',
createdAt: '2024-08-02T09:48:36.193Z', createdAt: '2024-08-02T09:48:36.193Z',
city: 'Seattle', city: 'Seattle',
phone: '+33781234567', phones: {
primaryPhoneNumber: '781234599',
primaryPhoneCountryCode: '+33',
},
id: '20202020-6784-4449-afdf-dc62cb8702f2', id: '20202020-6784-4449-afdf-dc62cb8702f2',
jobTitle: '', jobTitle: '',
position: 5, position: 5,
@ -683,7 +701,10 @@ export const peopleQueryResult: { people: RecordGqlConnection } = {
__typename: 'Person', __typename: 'Person',
createdAt: '2024-08-02T09:48:36.193Z', createdAt: '2024-08-02T09:48:36.193Z',
city: 'New York', city: 'New York',
phone: '+33782345678', phones: {
primaryPhoneNumber: '781234572',
primaryPhoneCountryCode: '+33',
},
id: '20202020-490f-4466-8391-733cfd66a0c8', id: '20202020-490f-4466-8391-733cfd66a0c8',
jobTitle: '', jobTitle: '',
position: 6, position: 6,
@ -779,7 +800,10 @@ export const peopleQueryResult: { people: RecordGqlConnection } = {
__typename: 'Person', __typename: 'Person',
createdAt: '2024-08-02T09:48:36.193Z', createdAt: '2024-08-02T09:48:36.193Z',
city: 'Seattle', city: 'Seattle',
phone: '+33783456789', phones: {
primaryPhoneNumber: '781234582',
primaryPhoneCountryCode: '+33',
},
id: '20202020-80f1-4dff-b570-a74942528de3', id: '20202020-80f1-4dff-b570-a74942528de3',
jobTitle: '', jobTitle: '',
position: 7, position: 7,
@ -875,7 +899,10 @@ export const peopleQueryResult: { people: RecordGqlConnection } = {
__typename: 'Person', __typename: 'Person',
createdAt: '2024-08-02T09:48:36.193Z', createdAt: '2024-08-02T09:48:36.193Z',
city: 'New York', city: 'New York',
phone: '+33784567890', phones: {
primaryPhoneNumber: '781234569',
primaryPhoneCountryCode: '+33',
},
id: '20202020-338b-46df-8811-fa08c7d19d35', id: '20202020-338b-46df-8811-fa08c7d19d35',
jobTitle: '', jobTitle: '',
position: 8, position: 8,
@ -971,7 +998,10 @@ export const peopleQueryResult: { people: RecordGqlConnection } = {
__typename: 'Person', __typename: 'Person',
createdAt: '2024-08-02T09:48:36.193Z', createdAt: '2024-08-02T09:48:36.193Z',
city: 'San Francisco', city: 'San Francisco',
phone: '+33785678901', phones: {
primaryPhoneNumber: '781234962',
primaryPhoneCountryCode: '+33',
},
id: '20202020-64ad-4b0e-bbfd-e9fd795b7016', id: '20202020-64ad-4b0e-bbfd-e9fd795b7016',
jobTitle: '', jobTitle: '',
position: 9, position: 9,
@ -1067,7 +1097,10 @@ export const peopleQueryResult: { people: RecordGqlConnection } = {
__typename: 'Person', __typename: 'Person',
createdAt: '2024-08-02T09:48:36.193Z', createdAt: '2024-08-02T09:48:36.193Z',
city: 'New York', city: 'New York',
phone: '+33786789012', phones: {
primaryPhoneNumber: '781234502',
primaryPhoneCountryCode: '+33',
},
id: '20202020-5d54-41b7-ba36-f0d20e1417ae', id: '20202020-5d54-41b7-ba36-f0d20e1417ae',
jobTitle: '', jobTitle: '',
position: 10, position: 10,
@ -1163,7 +1196,10 @@ export const peopleQueryResult: { people: RecordGqlConnection } = {
__typename: 'Person', __typename: 'Person',
createdAt: '2024-08-02T09:48:36.193Z', createdAt: '2024-08-02T09:48:36.193Z',
city: 'Los Angeles', city: 'Los Angeles',
phone: '+33787890123', phones: {
primaryPhoneNumber: '781234563',
primaryPhoneCountryCode: '+33',
},
id: '20202020-623d-41fe-92e7-dd45b7c568e1', id: '20202020-623d-41fe-92e7-dd45b7c568e1',
jobTitle: '', jobTitle: '',
position: 11, position: 11,
@ -1259,7 +1295,10 @@ export const peopleQueryResult: { people: RecordGqlConnection } = {
__typename: 'Person', __typename: 'Person',
createdAt: '2024-08-02T09:48:36.193Z', createdAt: '2024-08-02T09:48:36.193Z',
city: 'Seattle', city: 'Seattle',
phone: '+33788901234', phones: {
primaryPhoneNumber: '781234542',
primaryPhoneCountryCode: '+33',
},
id: '20202020-2d40-4e49-8df4-9c6a049190ef', id: '20202020-2d40-4e49-8df4-9c6a049190ef',
jobTitle: '', jobTitle: '',
position: 12, position: 12,
@ -1355,7 +1394,10 @@ export const peopleQueryResult: { people: RecordGqlConnection } = {
__typename: 'Person', __typename: 'Person',
createdAt: '2024-08-02T09:48:36.193Z', createdAt: '2024-08-02T09:48:36.193Z',
city: 'Seattle', city: 'Seattle',
phone: '+33788901234', phones: {
primaryPhoneNumber: '782234562',
primaryPhoneCountryCode: '+33',
},
id: '20202020-2d40-4e49-8df4-9c6a049190df', id: '20202020-2d40-4e49-8df4-9c6a049190df',
jobTitle: '', jobTitle: '',
position: 13, position: 13,
@ -1451,7 +1493,10 @@ export const peopleQueryResult: { people: RecordGqlConnection } = {
__typename: 'Person', __typename: 'Person',
createdAt: '2024-08-02T09:48:36.193Z', createdAt: '2024-08-02T09:48:36.193Z',
city: 'Seattle', city: 'Seattle',
phone: '+33788901234', phones: {
primaryPhoneNumber: '781274562',
primaryPhoneCountryCode: '+33',
},
id: '20202020-2d40-4e49-8df4-9c6a049191de', id: '20202020-2d40-4e49-8df4-9c6a049191de',
jobTitle: '', jobTitle: '',
position: 14, position: 14,
@ -1547,7 +1592,10 @@ export const peopleQueryResult: { people: RecordGqlConnection } = {
__typename: 'Person', __typename: 'Person',
createdAt: '2024-08-02T09:48:36.193Z', createdAt: '2024-08-02T09:48:36.193Z',
city: 'Seattle', city: 'Seattle',
phone: '+33788901235', phones: {
primaryPhoneNumber: '781239562',
primaryPhoneCountryCode: '+33',
},
id: '20202020-2d40-4e49-8df4-9c6a049191df', id: '20202020-2d40-4e49-8df4-9c6a049191df',
jobTitle: '', jobTitle: '',
position: 15, position: 15,

View File

@ -9,6 +9,7 @@ import { DataSeedWorkspaceCommand } from 'src/database/commands/data-seed-dev-wo
import { ConfirmationQuestion } from 'src/database/commands/questions/confirmation.question'; import { ConfirmationQuestion } from 'src/database/commands/questions/confirmation.question';
import { UpgradeTo0_30CommandModule } from 'src/database/commands/upgrade-version/0-30/0-30-upgrade-version.module'; import { UpgradeTo0_30CommandModule } from 'src/database/commands/upgrade-version/0-30/0-30-upgrade-version.module';
import { UpgradeTo0_31CommandModule } from 'src/database/commands/upgrade-version/0-31/0-31-upgrade-version.module'; import { UpgradeTo0_31CommandModule } from 'src/database/commands/upgrade-version/0-31/0-31-upgrade-version.module';
import { UpgradeTo0_32CommandModule } from 'src/database/commands/upgrade-version/0-32/0-32-upgrade-version.module';
import { TypeORMModule } from 'src/database/typeorm/typeorm.module'; import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity'; import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
@ -50,6 +51,7 @@ import { WorkspaceSyncMetadataModule } from 'src/engine/workspace-manager/worksp
UpgradeTo0_30CommandModule, UpgradeTo0_30CommandModule,
UpgradeTo0_31CommandModule, UpgradeTo0_31CommandModule,
FeatureFlagModule, FeatureFlagModule,
UpgradeTo0_32CommandModule,
], ],
providers: [ providers: [
DataSeedWorkspaceCommand, DataSeedWorkspaceCommand,

View File

@ -0,0 +1,338 @@
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import chalk from 'chalk';
import { isDefined, isEmpty } from 'class-validator';
import { parsePhoneNumber } from 'libphonenumber-js';
import { Command } from 'nest-commander';
import { DataSource, QueryRunner, Repository } from 'typeorm';
import {
ActiveWorkspacesCommandOptions,
ActiveWorkspacesCommandRunner,
} from 'src/database/commands/active-workspaces.command';
import { TypeORMService } from 'src/database/typeorm/typeorm.service';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input';
import {
FieldMetadataEntity,
FieldMetadataType,
} from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { FieldMetadataService } from 'src/engine/metadata-modules/field-metadata/field-metadata.service';
import { computeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { computeTableName } from 'src/engine/utils/compute-table-name.util';
import { PERSON_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
import { ViewService } from 'src/modules/view/services/view.service';
import { ViewFieldWorkspaceEntity } from 'src/modules/view/standard-objects/view-field.workspace-entity';
type MigratePhoneFieldsToPhonesCommandOptions = ActiveWorkspacesCommandOptions;
@Command({
name: 'upgrade-0.32:migrate-phone-fields-to-phones',
description: 'Migrating fields of deprecated type PHONE to type PHONES',
})
export class MigratePhoneFieldsToPhonesCommand extends ActiveWorkspacesCommandRunner {
constructor(
@InjectRepository(Workspace, 'core')
protected readonly workspaceRepository: Repository<Workspace>,
@InjectRepository(FieldMetadataEntity, 'metadata')
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
@InjectRepository(ObjectMetadataEntity, 'metadata')
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
@InjectDataSource('metadata')
private readonly metadataDataSource: DataSource,
private readonly fieldMetadataService: FieldMetadataService,
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly typeORMService: TypeORMService,
private readonly dataSourceService: DataSourceService,
private readonly viewService: ViewService,
) {
super(workspaceRepository);
}
async executeActiveWorkspacesCommand(
_passedParam: string[],
_options: MigratePhoneFieldsToPhonesCommandOptions,
workspaceIds: string[],
): Promise<void> {
this.logger.log(
'Running command to migrate phone type fields to phones type',
);
for (const workspaceId of workspaceIds) {
this.logger.log(`Running command for workspace ${workspaceId}`);
try {
const dataSourceMetadata =
await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceId(
workspaceId,
);
if (!dataSourceMetadata) {
throw new Error(
`Could not find dataSourceMetadata for workspace ${workspaceId}`,
);
}
const workspaceDataSource =
await this.typeORMService.connectToDataSource(dataSourceMetadata);
if (!workspaceDataSource) {
throw new Error(
`Could not connect to dataSource for workspace ${workspaceId}`,
);
}
const standardPersonPhoneFieldWithTextType =
await this.fieldMetadataRepository.findOneBy({
workspaceId,
standardId: PERSON_STANDARD_FIELD_IDS.phone,
});
if (!standardPersonPhoneFieldWithTextType) {
throw new Error(
`Could not find standard phone field on person for workspace ${workspaceId}`,
);
}
await this.migrateStandardPersonPhoneField({
standardPersonPhoneField: standardPersonPhoneFieldWithTextType,
workspaceDataSource,
workspaceSchemaName: dataSourceMetadata.schema,
});
const fieldsWithPhoneType = await this.fieldMetadataRepository.find({
where: {
workspaceId,
type: FieldMetadataType.PHONE,
},
});
for (const deprecatedPhoneField of fieldsWithPhoneType) {
await this.migrateCustomPhoneField({
phoneField: deprecatedPhoneField,
workspaceDataSource,
workspaceSchemaName: dataSourceMetadata.schema,
});
}
} catch (error) {
this.logger.log(
chalk.red(
`Field migration on workspace ${workspaceId} failed with error: ${error}`,
),
);
continue;
}
this.logger.log(chalk.green(`Command completed!`));
}
}
private async migrateStandardPersonPhoneField({
standardPersonPhoneField,
workspaceDataSource,
workspaceSchemaName,
}: {
standardPersonPhoneField: FieldMetadataEntity;
workspaceDataSource: DataSource;
workspaceSchemaName: string;
}) {
const personObjectMetadata = await this.objectMetadataRepository.findOne({
where: { id: standardPersonPhoneField.objectMetadataId },
});
if (!personObjectMetadata) {
throw new Error(
`Could not find Person objectMetadata (id ${standardPersonPhoneField.objectMetadataId})`,
);
}
this.logger.log(`Attempting to migrate standard person phone field.`);
const workspaceQueryRunner = workspaceDataSource.createQueryRunner();
await workspaceQueryRunner.connect();
const { id: _id, ...deprecatedPhoneFieldWithoutId } =
standardPersonPhoneField;
const workspaceId = standardPersonPhoneField.workspaceId;
try {
let standardPersonPhonesFieldType =
await this.fieldMetadataRepository.findOneBy({
workspaceId,
standardId: PERSON_STANDARD_FIELD_IDS.phones,
});
if (!standardPersonPhonesFieldType) {
standardPersonPhonesFieldType =
await this.fieldMetadataService.createOne({
...deprecatedPhoneFieldWithoutId,
type: FieldMetadataType.PHONES,
defaultValue: null,
name: 'phones',
} satisfies CreateFieldInput);
}
// Copy phone data from Text type to Phones type
await this.copyAndParseDeprecatedPhoneFieldDataIntoNewPhonesField({
workspaceQueryRunner,
workspaceSchemaName,
});
// Add new phones field to views and hide deprecated phone field
const viewFieldRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<ViewFieldWorkspaceEntity>(
workspaceId,
'viewField',
);
const viewFieldsWithDeprecatedPhoneField = await viewFieldRepository.find(
{
where: {
fieldMetadataId: standardPersonPhoneField.id,
isVisible: true,
},
},
);
await this.viewService.addFieldToViews({
workspaceId: workspaceId,
fieldId: standardPersonPhonesFieldType.id,
viewsIds: viewFieldsWithDeprecatedPhoneField
.filter((viewField) => viewField.viewId !== null)
.map((viewField) => viewField.viewId as string),
positions: viewFieldsWithDeprecatedPhoneField.reduce(
(acc, viewField) => {
if (!viewField.viewId) {
return acc;
}
acc[viewField.viewId] = viewField.position;
return acc;
},
[],
),
});
await this.viewService.removeFieldFromViews({
workspaceId: workspaceId,
fieldId: standardPersonPhoneField.id,
});
this.logger.log(
`Migration of standard person phone field to phones is done!`,
);
} catch (error) {
this.logger.log(
chalk.red(
`Failed to migrate field standard person phone field to phones, rolling back. (Error: ${error})`,
),
);
// Delete new phones field if it was created
const newPhonesField =
await this.fieldMetadataService.findOneWithinWorkspace(workspaceId, {
where: {
name: 'phones',
objectMetadataId: standardPersonPhoneField.objectMetadataId,
},
});
if (newPhonesField) {
this.logger.log(
`Deleting phones field of type Phone as part of rolling back.`,
);
await this.fieldMetadataService.deleteOneField(
{ id: newPhonesField.id },
workspaceId,
);
}
} finally {
await workspaceQueryRunner.release();
}
}
private async migrateCustomPhoneField({
phoneField,
workspaceDataSource,
workspaceSchemaName,
}: {
phoneField: FieldMetadataEntity;
workspaceDataSource: DataSource;
workspaceSchemaName: string;
}) {
if (!phoneField) return;
const objectMetadata = await this.objectMetadataRepository.findOne({
where: { id: phoneField.objectMetadataId },
});
if (!objectMetadata) {
throw new Error(
`Could not find objectMetadata for field ${phoneField.name}`,
);
}
this.logger.log(
`Attempting to migrate field ${phoneField.name} on ${objectMetadata.nameSingular} from Phone to Text.`,
);
const workspaceQueryRunner = workspaceDataSource.createQueryRunner();
await workspaceQueryRunner.connect();
try {
await this.metadataDataSource.query(
`UPDATE "metadata"."fieldMetadata" SET "type" = $1 where "id"=$2`,
[FieldMetadataType.TEXT, phoneField.id],
);
await workspaceQueryRunner.query(
`ALTER TABLE "${workspaceSchemaName}"."${computeTableName(objectMetadata.nameSingular, objectMetadata.isCustom)}" ALTER COLUMN "${computeColumnName(phoneField.name)}" TYPE TEXT`,
);
} catch (error) {
this.logger.log(
chalk.red(
`Failed to migrate field ${phoneField.name} on ${objectMetadata.nameSingular} from Phone to Text.`,
),
);
} finally {
await workspaceQueryRunner.release();
}
}
private async copyAndParseDeprecatedPhoneFieldDataIntoNewPhonesField({
workspaceQueryRunner,
workspaceSchemaName,
}: {
workspaceQueryRunner: QueryRunner;
workspaceSchemaName: string;
}) {
const deprecatedPhoneFieldRows = await workspaceQueryRunner.query(
`SELECT id, phone FROM "${workspaceSchemaName}"."person" WHERE
phone IS NOT null`,
);
for (const row of deprecatedPhoneFieldRows) {
const phoneColumnValue = row['phone'];
if (isDefined(phoneColumnValue) && !isEmpty(phoneColumnValue)) {
const query = `UPDATE "${workspaceSchemaName}"."person" SET "phonesPrimaryPhoneCountryCode" = $1,"phonesPrimaryPhoneNumber" = $2 where "id"=$3 AND ("phonesPrimaryPhoneCountryCode" IS NULL OR "phonesPrimaryPhoneCountryCode" = '');`;
try {
const parsedPhoneColumnValue = parsePhoneNumber(phoneColumnValue);
await workspaceQueryRunner.query(query, [
`+${parsedPhoneColumnValue.countryCallingCode}`,
parsedPhoneColumnValue.nationalNumber,
row.id,
]);
} catch (error) {
this.logger.log(
chalk.red(
`Could not save phone number ${phoneColumnValue}, will try again storing value as is without parsing, with default country code.`,
),
);
// Store the invalid string for invalid phone numbers
await workspaceQueryRunner.query(query, [
'',
phoneColumnValue,
row.id,
]);
}
}
}
}
}

View File

@ -0,0 +1,46 @@
import { InjectRepository } from '@nestjs/typeorm';
import { Command } from 'nest-commander';
import { Repository } from 'typeorm';
import {
ActiveWorkspacesCommandOptions,
ActiveWorkspacesCommandRunner,
} from 'src/database/commands/active-workspaces.command';
import { MigratePhoneFieldsToPhonesCommand } from 'src/database/commands/upgrade-version/0-32/0-32-migrate-phone-fields-to-phones.command';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { SyncWorkspaceMetadataCommand } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/sync-workspace-metadata.command';
type UpdateTo0_32CommandOptions = ActiveWorkspacesCommandOptions;
@Command({
name: 'upgrade-0.32',
description: 'Upgrade to 0.32',
})
export class UpgradeTo0_32Command extends ActiveWorkspacesCommandRunner {
constructor(
@InjectRepository(Workspace, 'core')
protected readonly workspaceRepository: Repository<Workspace>,
private readonly syncWorkspaceMetadataCommand: SyncWorkspaceMetadataCommand,
private readonly migratePhoneFieldsToPhones: MigratePhoneFieldsToPhonesCommand,
) {
super(workspaceRepository);
}
async executeActiveWorkspacesCommand(
_passedParam: string[],
options: UpdateTo0_32CommandOptions,
workspaceIds: string[],
): Promise<void> {
await this.syncWorkspaceMetadataCommand.executeActiveWorkspacesCommand(
_passedParam,
{ ...options, force: true },
workspaceIds,
);
await this.migratePhoneFieldsToPhones.executeActiveWorkspacesCommand(
_passedParam,
options,
workspaceIds,
);
}
}

View File

@ -0,0 +1,32 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { MigratePhoneFieldsToPhonesCommand } from 'src/database/commands/upgrade-version/0-32/0-32-migrate-phone-fields-to-phones.command';
import { UpgradeTo0_32Command } from 'src/database/commands/upgrade-version/0-32/0-32-upgrade-version.command';
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { FieldMetadataModule } from 'src/engine/metadata-modules/field-metadata/field-metadata.module';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module';
import { WorkspaceSyncMetadataCommandsModule } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/workspace-sync-metadata-commands.module';
import { ViewModule } from 'src/modules/view/view.module';
@Module({
imports: [
TypeOrmModule.forFeature([Workspace], 'core'),
WorkspaceSyncMetadataCommandsModule,
DataSourceModule,
WorkspaceMetadataVersionModule,
FieldMetadataModule,
TypeOrmModule.forFeature(
[FieldMetadataEntity, ObjectMetadataEntity],
'metadata',
),
TypeORMModule,
ViewModule,
],
providers: [UpgradeTo0_32Command, MigratePhoneFieldsToPhonesCommand],
})
export class UpgradeTo0_32CommandModule {}

View File

@ -92,7 +92,7 @@ export const getDevSeedPeopleCustomFields = (
}, },
{ {
workspaceId, workspaceId,
type: FieldMetadataType.PHONE, type: FieldMetadataType.PHONES,
name: 'whatsapp', name: 'whatsapp',
label: 'Whatsapp', label: 'Whatsapp',
description: "Contact's Whatsapp Number", description: "Contact's Whatsapp Number",

View File

@ -34,12 +34,14 @@ export const seedPeople = async (
'id', 'id',
'nameFirstName', 'nameFirstName',
'nameLastName', 'nameLastName',
'phone', 'phonesPrimaryPhoneCountryCode',
'phonesPrimaryPhoneNumber',
'city', 'city',
'companyId', 'companyId',
'emailsPrimaryEmail', 'emailsPrimaryEmail',
'position', 'position',
'whatsapp', 'whatsappPrimaryPhoneCountryCode',
'whatsappPrimaryPhoneNumber',
'createdBySource', 'createdBySource',
'createdByWorkspaceMemberId', 'createdByWorkspaceMemberId',
'createdByName', 'createdByName',
@ -50,12 +52,14 @@ export const seedPeople = async (
id: DEV_SEED_PERSON_IDS.CHRISTOPH, id: DEV_SEED_PERSON_IDS.CHRISTOPH,
nameFirstName: 'Christoph', nameFirstName: 'Christoph',
nameLastName: 'Callisto', nameLastName: 'Callisto',
phone: '+33789012345', phonePrimaryPhoneCountryCode: '+33',
phonePrimaryPhoneNumber: '789012345',
city: 'Seattle', city: 'Seattle',
companyId: DEV_SEED_COMPANY_IDS.LINKEDIN, companyId: DEV_SEED_COMPANY_IDS.LINKEDIN,
emailsPrimaryEmail: 'christoph.calisto@linkedin.com', emailsPrimaryEmail: 'christoph.calisto@linkedin.com',
position: 1, position: 1,
whatsapp: '+33789012345', whatsappPrimaryPhoneCountryCode: '+33',
whatsappPrimaryPhoneNumber: '789012345',
createdBySource: 'MANUAL', createdBySource: 'MANUAL',
createdByWorkspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.TIM, createdByWorkspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.TIM,
createdByName: 'Tim Apple', createdByName: 'Tim Apple',
@ -64,12 +68,14 @@ export const seedPeople = async (
id: DEV_SEED_PERSON_IDS.SYLVIE, id: DEV_SEED_PERSON_IDS.SYLVIE,
nameFirstName: 'Sylvie', nameFirstName: 'Sylvie',
nameLastName: 'Palmer', nameLastName: 'Palmer',
phone: '+33780123456', phonePrimaryPhoneCountryCode: '+33',
phonePrimaryPhoneNumber: '780123456',
city: 'Los Angeles', city: 'Los Angeles',
companyId: DEV_SEED_COMPANY_IDS.LINKEDIN, companyId: DEV_SEED_COMPANY_IDS.LINKEDIN,
emailsPrimaryEmail: 'sylvie.palmer@linkedin.com', emailsPrimaryEmail: 'sylvie.palmer@linkedin.com',
position: 2, position: 2,
whatsapp: '+33780123456', whatsappPrimaryPhoneCountryCode: '+33',
whatsappPrimaryPhoneNumber: '780123456',
createdBySource: 'MANUAL', createdBySource: 'MANUAL',
createdByWorkspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.TIM, createdByWorkspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.TIM,
createdByName: 'Tim Apple', createdByName: 'Tim Apple',
@ -78,12 +84,14 @@ export const seedPeople = async (
id: DEV_SEED_PERSON_IDS.CHRISTOPHER_G, id: DEV_SEED_PERSON_IDS.CHRISTOPHER_G,
nameFirstName: 'Christopher', nameFirstName: 'Christopher',
nameLastName: 'Gonzalez', nameLastName: 'Gonzalez',
phone: '+33789012345', phonePrimaryPhoneCountryCode: '+33',
phonePrimaryPhoneNumber: '789012345',
city: 'Seattle', city: 'Seattle',
companyId: DEV_SEED_COMPANY_IDS.QONTO, companyId: DEV_SEED_COMPANY_IDS.QONTO,
emailsPrimaryEmail: 'christopher.gonzalez@qonto.com', emailsPrimaryEmail: 'christopher.gonzalez@qonto.com',
position: 3, position: 3,
whatsapp: '+33789012345', whatsappPrimaryPhoneCountryCode: '+33',
whatsappPrimaryPhoneNumber: '789012345',
createdBySource: 'MANUAL', createdBySource: 'MANUAL',
createdByWorkspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.TIM, createdByWorkspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.TIM,
createdByName: 'Tim Apple', createdByName: 'Tim Apple',
@ -92,12 +100,14 @@ export const seedPeople = async (
id: DEV_SEED_PERSON_IDS.ASHLEY, id: DEV_SEED_PERSON_IDS.ASHLEY,
nameFirstName: 'Ashley', nameFirstName: 'Ashley',
nameLastName: 'Parker', nameLastName: 'Parker',
phone: '+33780123456', phonePrimaryPhoneCountryCode: '+33',
phonePrimaryPhoneNumber: '780123456',
city: 'Los Angeles', city: 'Los Angeles',
companyId: DEV_SEED_COMPANY_IDS.QONTO, companyId: DEV_SEED_COMPANY_IDS.QONTO,
emailsPrimaryEmail: 'ashley.parker@qonto.com', emailsPrimaryEmail: 'ashley.parker@qonto.com',
position: 4, position: 4,
whatsapp: '+33780123456', whatsappPrimaryPhoneCountryCode: '+33',
whatsappPrimaryPhoneNumber: '780123456',
createdBySource: 'MANUAL', createdBySource: 'MANUAL',
createdByWorkspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.TIM, createdByWorkspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.TIM,
createdByName: 'Tim Apple', createdByName: 'Tim Apple',
@ -106,12 +116,14 @@ export const seedPeople = async (
id: DEV_SEED_PERSON_IDS.NICHOLAS, id: DEV_SEED_PERSON_IDS.NICHOLAS,
nameFirstName: 'Nicholas', nameFirstName: 'Nicholas',
nameLastName: 'Wright', nameLastName: 'Wright',
phone: '+33781234567', phonePrimaryPhoneCountryCode: '+33',
phonePrimaryPhoneNumber: '781234567',
city: 'Seattle', city: 'Seattle',
companyId: DEV_SEED_COMPANY_IDS.MICROSOFT, companyId: DEV_SEED_COMPANY_IDS.MICROSOFT,
emailsPrimaryEmail: 'nicholas.wright@microsoft.com', emailsPrimaryEmail: 'nicholas.wright@microsoft.com',
position: 5, position: 5,
whatsapp: '+33781234567', whatsappPrimaryPhoneCountryCode: '+33',
whatsappPrimaryPhoneNumber: '781234567',
createdBySource: 'MANUAL', createdBySource: 'MANUAL',
createdByWorkspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.TIM, createdByWorkspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.TIM,
createdByName: 'Tim Apple', createdByName: 'Tim Apple',
@ -120,12 +132,14 @@ export const seedPeople = async (
id: DEV_SEED_PERSON_IDS.ISABELLA, id: DEV_SEED_PERSON_IDS.ISABELLA,
nameFirstName: 'Isabella', nameFirstName: 'Isabella',
nameLastName: 'Scott', nameLastName: 'Scott',
phone: '+33782345678', phonePrimaryPhoneCountryCode: '+33',
phonePrimaryPhoneNumber: '782345678',
city: 'New York', city: 'New York',
companyId: DEV_SEED_COMPANY_IDS.MICROSOFT, companyId: DEV_SEED_COMPANY_IDS.MICROSOFT,
emailsPrimaryEmail: 'isabella.scott@microsoft.com', emailsPrimaryEmail: 'isabella.scott@microsoft.com',
position: 6, position: 6,
whatsapp: '+33782345678', whatsappPrimaryPhoneCountryCode: '+33',
whatsappPrimaryPhoneNumber: '782345678',
createdBySource: 'MANUAL', createdBySource: 'MANUAL',
createdByWorkspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.TIM, createdByWorkspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.TIM,
createdByName: 'Tim Apple', createdByName: 'Tim Apple',
@ -134,12 +148,14 @@ export const seedPeople = async (
id: DEV_SEED_PERSON_IDS.MATTHEW, id: DEV_SEED_PERSON_IDS.MATTHEW,
nameFirstName: 'Matthew', nameFirstName: 'Matthew',
nameLastName: 'Green', nameLastName: 'Green',
phone: '+33783456789', phonePrimaryPhoneCountryCode: '+33',
phonePrimaryPhoneNumber: '783456789',
city: 'Seattle', city: 'Seattle',
companyId: DEV_SEED_COMPANY_IDS.MICROSOFT, companyId: DEV_SEED_COMPANY_IDS.MICROSOFT,
emailsPrimaryEmail: 'matthew.green@microsoft.com', emailsPrimaryEmail: 'matthew.green@microsoft.com',
position: 7, position: 7,
whatsapp: '+33783456789', whatsappPrimaryPhoneCountryCode: '+33',
whatsappPrimaryPhoneNumber: '783456789',
createdBySource: 'MANUAL', createdBySource: 'MANUAL',
createdByWorkspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.TIM, createdByWorkspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.TIM,
createdByName: 'Tim Apple', createdByName: 'Tim Apple',
@ -148,12 +164,14 @@ export const seedPeople = async (
id: DEV_SEED_PERSON_IDS.ELIZABETH, id: DEV_SEED_PERSON_IDS.ELIZABETH,
nameFirstName: 'Elizabeth', nameFirstName: 'Elizabeth',
nameLastName: 'Baker', nameLastName: 'Baker',
phone: '+33784567890', phonePrimaryPhoneCountryCode: '+33',
phonePrimaryPhoneNumber: '784567890',
city: 'New York', city: 'New York',
companyId: DEV_SEED_COMPANY_IDS.AIRBNB, companyId: DEV_SEED_COMPANY_IDS.AIRBNB,
emailsPrimaryEmail: 'elizabeth.baker@airbnb.com', emailsPrimaryEmail: 'elizabeth.baker@airbnb.com',
position: 8, position: 8,
whatsapp: '+33784567890', whatsappPrimaryPhoneCountryCode: '+33',
whatsappPrimaryPhoneNumber: '784567890',
createdBySource: 'MANUAL', createdBySource: 'MANUAL',
createdByWorkspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.TIM, createdByWorkspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.TIM,
createdByName: 'Tim Apple', createdByName: 'Tim Apple',
@ -162,12 +180,14 @@ export const seedPeople = async (
id: DEV_SEED_PERSON_IDS.CHRISTOPHER_N, id: DEV_SEED_PERSON_IDS.CHRISTOPHER_N,
nameFirstName: 'Christopher', nameFirstName: 'Christopher',
nameLastName: 'Nelson', nameLastName: 'Nelson',
phone: '+33785678901', phonePrimaryPhoneCountryCode: '+33',
phonePrimaryPhoneNumber: '785678901',
city: 'San Francisco', city: 'San Francisco',
companyId: DEV_SEED_COMPANY_IDS.AIRBNB, companyId: DEV_SEED_COMPANY_IDS.AIRBNB,
emailsPrimaryEmail: 'christopher.nelson@airbnb.com', emailsPrimaryEmail: 'christopher.nelson@airbnb.com',
position: 9, position: 9,
whatsapp: '+33785678901', whatsappPrimaryPhoneCountryCode: '+33',
whatsappPrimaryPhoneNumber: '785678901',
createdBySource: 'MANUAL', createdBySource: 'MANUAL',
createdByWorkspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.TIM, createdByWorkspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.TIM,
createdByName: 'Tim Apple', createdByName: 'Tim Apple',
@ -176,12 +196,14 @@ export const seedPeople = async (
id: DEV_SEED_PERSON_IDS.AVERY, id: DEV_SEED_PERSON_IDS.AVERY,
nameFirstName: 'Avery', nameFirstName: 'Avery',
nameLastName: 'Carter', nameLastName: 'Carter',
phone: '+33786789012', phonePrimaryPhoneCountryCode: '+33',
phonePrimaryPhoneNumber: '786789012',
city: 'New York', city: 'New York',
companyId: DEV_SEED_COMPANY_IDS.AIRBNB, companyId: DEV_SEED_COMPANY_IDS.AIRBNB,
emailsPrimaryEmail: 'avery.carter@airbnb.com', emailsPrimaryEmail: 'avery.carter@airbnb.com',
position: 10, position: 10,
whatsapp: '+33786789012', whatsappPrimaryPhoneCountryCode: '+33',
whatsappPrimaryPhoneNumber: '786789012',
createdBySource: 'MANUAL', createdBySource: 'MANUAL',
createdByWorkspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.TIM, createdByWorkspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.TIM,
createdByName: 'Tim Apple', createdByName: 'Tim Apple',
@ -190,12 +212,14 @@ export const seedPeople = async (
id: DEV_SEED_PERSON_IDS.ETHAN, id: DEV_SEED_PERSON_IDS.ETHAN,
nameFirstName: 'Ethan', nameFirstName: 'Ethan',
nameLastName: 'Mitchell', nameLastName: 'Mitchell',
phone: '+33787890123', phonePrimaryPhoneCountryCode: '+33',
phonePrimaryPhoneNumber: '787890123',
city: 'Los Angeles', city: 'Los Angeles',
companyId: DEV_SEED_COMPANY_IDS.GOOGLE, companyId: DEV_SEED_COMPANY_IDS.GOOGLE,
emailsPrimaryEmail: 'ethan.mitchell@google.com', emailsPrimaryEmail: 'ethan.mitchell@google.com',
position: 11, position: 11,
whatsapp: '+33787890123', whatsappPrimaryPhoneCountryCode: '+33',
whatsappPrimaryPhoneNumber: '787890123',
createdBySource: 'MANUAL', createdBySource: 'MANUAL',
createdByWorkspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.TIM, createdByWorkspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.TIM,
createdByName: 'Tim Apple', createdByName: 'Tim Apple',
@ -204,12 +228,14 @@ export const seedPeople = async (
id: DEV_SEED_PERSON_IDS.MADISON, id: DEV_SEED_PERSON_IDS.MADISON,
nameFirstName: 'Madison', nameFirstName: 'Madison',
nameLastName: 'Perez', nameLastName: 'Perez',
phone: '+33788901234', phonePrimaryPhoneCountryCode: '+33',
phonePrimaryPhoneNumber: '788901234',
city: 'Seattle', city: 'Seattle',
companyId: DEV_SEED_COMPANY_IDS.GOOGLE, companyId: DEV_SEED_COMPANY_IDS.GOOGLE,
emailsPrimaryEmail: 'madison.perez@google.com', emailsPrimaryEmail: 'madison.perez@google.com',
position: 12, position: 12,
whatsapp: '+33788901234', whatsappPrimaryPhoneCountryCode: '+33',
whatsappPrimaryPhoneNumber: '788901234',
createdBySource: 'MANUAL', createdBySource: 'MANUAL',
createdByWorkspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.TIM, createdByWorkspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.TIM,
createdByName: 'Tim Apple', createdByName: 'Tim Apple',
@ -218,12 +244,14 @@ export const seedPeople = async (
id: DEV_SEED_PERSON_IDS.BERTRAND, id: DEV_SEED_PERSON_IDS.BERTRAND,
nameFirstName: 'Bertrand', nameFirstName: 'Bertrand',
nameLastName: 'Voulzy', nameLastName: 'Voulzy',
phone: '+33788901234', phonePrimaryPhoneCountryCode: '+33',
phonePrimaryPhoneNumber: '788901234',
city: 'Seattle', city: 'Seattle',
companyId: DEV_SEED_COMPANY_IDS.GOOGLE, companyId: DEV_SEED_COMPANY_IDS.GOOGLE,
emailsPrimaryEmail: 'bertrand.voulzy@google.com', emailsPrimaryEmail: 'bertrand.voulzy@google.com',
position: 13, position: 13,
whatsapp: '+33788901234', whatsappPrimaryPhoneCountryCode: '+33',
whatsappPrimaryPhoneNumber: '788901234',
createdBySource: 'MANUAL', createdBySource: 'MANUAL',
createdByWorkspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.TIM, createdByWorkspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.TIM,
createdByName: 'Tim Apple', createdByName: 'Tim Apple',
@ -232,12 +260,14 @@ export const seedPeople = async (
id: DEV_SEED_PERSON_IDS.LOUIS, id: DEV_SEED_PERSON_IDS.LOUIS,
nameFirstName: 'Louis', nameFirstName: 'Louis',
nameLastName: 'Duss', nameLastName: 'Duss',
phone: '+33788901234', phonePrimaryPhoneCountryCode: '+33',
phonePrimaryPhoneNumber: '789012345',
city: 'Seattle', city: 'Seattle',
companyId: DEV_SEED_COMPANY_IDS.GOOGLE, companyId: DEV_SEED_COMPANY_IDS.GOOGLE,
emailsPrimaryEmail: 'louis.duss@google.com', emailsPrimaryEmail: 'louis.duss@google.com',
position: 14, position: 14,
whatsapp: '+33788901234', whatsappPrimaryPhoneCountryCode: '+33',
whatsappPrimaryPhoneNumber: '789012345',
createdBySource: 'MANUAL', createdBySource: 'MANUAL',
createdByWorkspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.TIM, createdByWorkspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.TIM,
createdByName: 'Tim Apple', createdByName: 'Tim Apple',
@ -246,12 +276,14 @@ export const seedPeople = async (
id: DEV_SEED_PERSON_IDS.LORIE, id: DEV_SEED_PERSON_IDS.LORIE,
nameFirstName: 'Lorie', nameFirstName: 'Lorie',
nameLastName: 'Vladim', nameLastName: 'Vladim',
phone: '+33788901235', phonePrimaryPhoneCountryCode: '+33',
phonePrimaryPhoneNumber: '788901235',
city: 'Seattle', city: 'Seattle',
companyId: DEV_SEED_COMPANY_IDS.GOOGLE, companyId: DEV_SEED_COMPANY_IDS.GOOGLE,
emailsPrimaryEmail: 'lorie.vladim@google.com', emailsPrimaryEmail: 'lorie.vladim@google.com',
position: 15, position: 15,
whatsapp: '+33788901235', whatsappPrimaryPhoneCountryCode: '+33',
whatsappPrimaryPhoneNumber: '788901235',
createdBySource: 'MANUAL', createdBySource: 'MANUAL',
createdByWorkspaceMemberId: null, createdByWorkspaceMemberId: null,
createdByName: '', createdByName: '',

View File

@ -1,5 +1,11 @@
import { EntityManager } from 'typeorm'; import { EntityManager } from 'typeorm';
export const AIRBNB_ID = 'c776ee49-f608-4a77-8cc8-6fe96ae1e43f';
export const QONTO_ID = 'f45ee421-8a3e-4aa5-a1cf-7207cc6754e1';
export const STRIPE_ID = '1f70157c-4ea5-4d81-bc49-e1401abfbb94';
export const FIGMA_ID = '9d5bcf43-7d38-4e88-82cb-d6d4ce638bf0';
export const NOTION_ID = '06290608-8bf0-4806-99ae-a715a6a93fad';
export const companyPrefillData = async ( export const companyPrefillData = async (
entityManager: EntityManager, entityManager: EntityManager,
schemaName: string, schemaName: string,
@ -8,6 +14,7 @@ export const companyPrefillData = async (
.createQueryBuilder() .createQueryBuilder()
.insert() .insert()
.into(`${schemaName}.company`, [ .into(`${schemaName}.company`, [
'id',
'name', 'name',
'domainNamePrimaryLinkUrl', 'domainNamePrimaryLinkUrl',
'addressAddressStreet1', 'addressAddressStreet1',
@ -25,6 +32,7 @@ export const companyPrefillData = async (
.orIgnore() .orIgnore()
.values([ .values([
{ {
id: AIRBNB_ID,
name: 'Airbnb', name: 'Airbnb',
domainNamePrimaryLinkUrl: 'https://airbnb.com', domainNamePrimaryLinkUrl: 'https://airbnb.com',
addressAddressStreet1: '888 Brannan St', addressAddressStreet1: '888 Brannan St',
@ -40,6 +48,7 @@ export const companyPrefillData = async (
createdByName: 'System', createdByName: 'System',
}, },
{ {
id: QONTO_ID,
name: 'Qonto', name: 'Qonto',
domainNamePrimaryLinkUrl: 'https://qonto.com', domainNamePrimaryLinkUrl: 'https://qonto.com',
addressAddressStreet1: '18 rue de navarrin', addressAddressStreet1: '18 rue de navarrin',
@ -55,6 +64,7 @@ export const companyPrefillData = async (
createdByName: 'System', createdByName: 'System',
}, },
{ {
id: STRIPE_ID,
name: 'Stripe', name: 'Stripe',
domainNamePrimaryLinkUrl: 'https://stripe.com', domainNamePrimaryLinkUrl: 'https://stripe.com',
addressAddressStreet1: 'Eutaw Street', addressAddressStreet1: 'Eutaw Street',
@ -70,6 +80,7 @@ export const companyPrefillData = async (
createdByName: 'System', createdByName: 'System',
}, },
{ {
id: FIGMA_ID,
name: 'Figma', name: 'Figma',
domainNamePrimaryLinkUrl: 'https://figma.com', domainNamePrimaryLinkUrl: 'https://figma.com',
addressAddressStreet1: '760 Market St', addressAddressStreet1: '760 Market St',
@ -85,6 +96,7 @@ export const companyPrefillData = async (
createdByName: 'System', createdByName: 'System',
}, },
{ {
id: NOTION_ID,
name: 'Notion', name: 'Notion',
domainNamePrimaryLinkUrl: 'https://notion.com', domainNamePrimaryLinkUrl: 'https://notion.com',
addressAddressStreet1: '2300 Harrison St', addressAddressStreet1: '2300 Harrison St',

View File

@ -1,5 +1,13 @@
import { EntityManager } from 'typeorm'; import { EntityManager } from 'typeorm';
import {
AIRBNB_ID,
FIGMA_ID,
NOTION_ID,
QONTO_ID,
STRIPE_ID,
} from 'src/engine/workspace-manager/standard-objects-prefill-data/company';
// FixMe: Is this file a duplicate of src/database/typeorm-seeds/workspace/people.ts // FixMe: Is this file a duplicate of src/database/typeorm-seeds/workspace/people.ts
export const personPrefillData = async ( export const personPrefillData = async (
entityManager: EntityManager, entityManager: EntityManager,
@ -18,6 +26,9 @@ export const personPrefillData = async (
'createdBySource', 'createdBySource',
'createdByWorkspaceMemberId', 'createdByWorkspaceMemberId',
'createdByName', 'createdByName',
'phonesPrimaryPhoneNumber',
'phonesPrimaryPhoneCountryCode',
'companyId',
]) ])
.orIgnore() .orIgnore()
.values([ .values([
@ -32,6 +43,9 @@ export const personPrefillData = async (
createdBySource: 'MANUAL', createdBySource: 'MANUAL',
createdByWorkspaceMemberId: null, createdByWorkspaceMemberId: null,
createdByName: 'System', createdByName: 'System',
phonesPrimaryPhoneNumber: '1234567890',
phonesPrimaryPhoneCountryCode: '+1',
companyId: AIRBNB_ID,
}, },
{ {
nameFirstName: 'Alexandre', nameFirstName: 'Alexandre',
@ -44,6 +58,9 @@ export const personPrefillData = async (
createdBySource: 'MANUAL', createdBySource: 'MANUAL',
createdByWorkspaceMemberId: null, createdByWorkspaceMemberId: null,
createdByName: 'System', createdByName: 'System',
phonesPrimaryPhoneNumber: '677118822',
phonesPrimaryPhoneCountryCode: '+33',
companyId: QONTO_ID,
}, },
{ {
nameFirstName: 'Patrick', nameFirstName: 'Patrick',
@ -56,6 +73,9 @@ export const personPrefillData = async (
createdBySource: 'MANUAL', createdBySource: 'MANUAL',
createdByWorkspaceMemberId: null, createdByWorkspaceMemberId: null,
createdByName: 'System', createdByName: 'System',
phonesPrimaryPhoneNumber: '987625341',
phonesPrimaryPhoneCountryCode: '+1',
companyId: STRIPE_ID,
}, },
{ {
nameFirstName: 'Dylan', nameFirstName: 'Dylan',
@ -68,6 +88,9 @@ export const personPrefillData = async (
createdBySource: 'MANUAL', createdBySource: 'MANUAL',
createdByWorkspaceMemberId: null, createdByWorkspaceMemberId: null,
createdByName: 'System', createdByName: 'System',
phonesPrimaryPhoneNumber: '09882261',
phonesPrimaryPhoneCountryCode: '+1',
companyId: FIGMA_ID,
}, },
{ {
nameFirstName: 'Ivan', nameFirstName: 'Ivan',
@ -80,6 +103,9 @@ export const personPrefillData = async (
createdBySource: 'MANUAL', createdBySource: 'MANUAL',
createdByWorkspaceMemberId: null, createdByWorkspaceMemberId: null,
createdByName: 'System', createdByName: 'System',
phonesPrimaryPhoneNumber: '88226173',
phonesPrimaryPhoneCountryCode: '+1',
companyId: NOTION_ID,
}, },
]) ])
.returning('*') .returning('*')

View File

@ -57,7 +57,7 @@ export const peopleAllView = async (
{ {
fieldMetadataId: fieldMetadataId:
objectMetadataMap[STANDARD_OBJECT_IDS.person].fields[ objectMetadataMap[STANDARD_OBJECT_IDS.person].fields[
PERSON_STANDARD_FIELD_IDS.phone PERSON_STANDARD_FIELD_IDS.phones
], ],
position: 4, position: 4,
isVisible: true, isVisible: true,

View File

@ -310,6 +310,7 @@ export const PERSON_STANDARD_FIELD_IDS = {
xLink: '20202020-8fc2-487c-b84a-55a99b145cfd', xLink: '20202020-8fc2-487c-b84a-55a99b145cfd',
jobTitle: '20202020-b0d0-415a-bef9-640a26dacd9b', jobTitle: '20202020-b0d0-415a-bef9-640a26dacd9b',
phone: '20202020-4564-4b8b-a09f-05445f2e0bce', phone: '20202020-4564-4b8b-a09f-05445f2e0bce',
phones: '34becd3e-3c51-43fa-8b6e-af39e29368ab',
city: '20202020-5243-4ffb-afc5-2c675da41346', city: '20202020-5243-4ffb-afc5-2c675da41346',
avatarUrl: '20202020-b8a6-40df-961c-373dc5d2ec21', avatarUrl: '20202020-b8a6-40df-961c-373dc5d2ec21',
position: '20202020-fcd5-4231-aff5-fff583eaa0b1', position: '20202020-fcd5-4231-aff5-fff583eaa0b1',

View File

@ -7,6 +7,7 @@ import {
import { EmailsMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/emails.composite-type'; import { EmailsMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/emails.composite-type';
import { FullNameMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/full-name.composite-type'; import { FullNameMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/full-name.composite-type';
import { LinksMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/links.composite-type'; import { LinksMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/links.composite-type';
import { PhonesMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/phones.composite-type';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { import {
RelationMetadataType, RelationMetadataType,
@ -109,8 +110,18 @@ export class PersonWorkspaceEntity extends BaseWorkspaceEntity {
description: 'Contacts phone number', description: 'Contacts phone number',
icon: 'IconPhone', icon: 'IconPhone',
}) })
@WorkspaceIsDeprecated()
phone: string; phone: string;
@WorkspaceField({
standardId: PERSON_STANDARD_FIELD_IDS.phones,
type: FieldMetadataType.PHONES,
label: 'Phones',
description: 'Contacts phone numbers',
icon: 'IconPhone',
})
phones: PhonesMetadata;
@WorkspaceField({ @WorkspaceField({
standardId: PERSON_STANDARD_FIELD_IDS.city, standardId: PERSON_STANDARD_FIELD_IDS.city,
type: FieldMetadataType.TEXT, type: FieldMetadataType.TEXT,

View File

@ -11,7 +11,10 @@ describe('peopleResolver (integration)', () => {
edges { edges {
node { node {
jobTitle jobTitle
phone phones {
primaryPhoneNumber
primaryPhoneCountryCode
}
city city
avatarUrl avatarUrl
position position
@ -21,7 +24,9 @@ describe('peopleResolver (integration)', () => {
deletedAt deletedAt
companyId companyId
intro intro
whatsapp whatsapp {
primaryPhoneNumber
}
workPrefereance workPrefereance
performanceRating performanceRating
} }
@ -37,6 +42,7 @@ describe('peopleResolver (integration)', () => {
.send(queryData) .send(queryData)
.expect(200) .expect(200)
.expect((res) => { .expect((res) => {
console.log(res.body);
expect(res.body.data).toBeDefined(); expect(res.body.data).toBeDefined();
expect(res.body.errors).toBeUndefined(); expect(res.body.errors).toBeUndefined();
}) })
@ -52,7 +58,7 @@ describe('peopleResolver (integration)', () => {
const people = edges[0].node; const people = edges[0].node;
expect(people).toHaveProperty('jobTitle'); expect(people).toHaveProperty('jobTitle');
expect(people).toHaveProperty('phone'); expect(people).toHaveProperty('phones');
expect(people).toHaveProperty('city'); expect(people).toHaveProperty('city');
expect(people).toHaveProperty('avatarUrl'); expect(people).toHaveProperty('avatarUrl');
expect(people).toHaveProperty('position'); expect(people).toHaveProperty('position');