Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Phone country code unique #backend #9035

Open
wants to merge 21 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import { WorkspaceSyncMetadataModule } from 'src/engine/workspace-manager/worksp
UpgradeTo0_32CommandModule,
UpgradeTo0_33CommandModule,
UpgradeTo0_34CommandModule,
UpgradeTo0_40CommandModule,
FeatureFlagModule,
UpgradeTo0_40CommandModule,
guillim marked this conversation as resolved.
Show resolved Hide resolved
],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,344 @@
import { InjectRepository } from '@nestjs/typeorm';

import chalk from 'chalk';
import { getCountries, getCountryCallingCode } from 'libphonenumber-js';
import { Command } from 'nest-commander';
import { Repository } from 'typeorm';
import { v4 } from 'uuid';

import {
ActiveWorkspacesCommandOptions,
ActiveWorkspacesCommandRunner,
} from 'src/database/commands/active-workspaces.command';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import {
FieldMetadataEntity,
FieldMetadataType,
} from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service';
import { generateMigrationName } from 'src/engine/metadata-modules/workspace-migration/utils/generate-migration-name.util';
import {
WorkspaceMigrationColumnActionType,
WorkspaceMigrationTableAction,
WorkspaceMigrationTableActionType,
} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity';
import { WorkspaceMigrationFactory } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.factory';
import { WorkspaceMigrationService } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.service';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service';
import { isDefined } from 'src/utils/is-defined';

const callingCodeToCountryCode = (callingCode: string): string => {
if (!callingCode) return '';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We tend to prefer

  if (!callingCode) {
    return '';
  }

let callingCodeWithoutPlus = callingCode;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

name is misleading, according to the if below callingCodeWithoutPlus could actually start with a "+"?


if (callingCode.startsWith('+')) {
callingCodeWithoutPlus = callingCode.slice(1);
}

return (
getCountries().find(
(countryCode) =>
getCountryCallingCode(countryCode) === callingCodeWithoutPlus,
) || ''
);
};

// console.log('33', callingCodeToCountryCode('33'));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

console.log

// console.log('+33', callingCodeToCountryCode('+33'));
// console.log('', callingCodeToCountryCode(''));
// console.log('FR', callingCodeToCountryCode('FR'));

const isCallingCode = (callingCode: string): boolean => {
return callingCodeToCountryCode(callingCode) !== '';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find this one a bit confusing

};

// console.log('33', isCallingCode('33'));
// console.log('+33', isCallingCode('+33'));
// console.log('', isCallingCode(''));
// console.log('FR', isCallingCode('FR'));

@Command({
name: 'upgrade-0.40:phone-calling-code',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"migrate data"?

description: 'Add calling code and change country code with default one',
})
export class PhoneCallingCodeCommand 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>,
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly workspaceMigrationService: WorkspaceMigrationService,
private readonly workspaceMigrationFactory: WorkspaceMigrationFactory,
private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService,
private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService,
) {
super(workspaceRepository);
}

async executeActiveWorkspacesCommand(
_passedParam: string[],
_options: ActiveWorkspacesCommandOptions,
workspaceIds: string[],
): Promise<void> {
this.logger.log(
'Running command to add calling code and change country code with default one',
);

// Prerequisite : the field callingcode was creted in the codebase (PHONES type modification)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need to keep those comments, however I guess you can simplify a bit by splitting your code into smaller functions 🙂

// Note : SyncMetadata will fail since composite field modification is not handled

// Migration to be applied, in this order :
// ---------------------------------Workspace-------------------------------------------------------------
// 1 - Add the calling code field in all the workspaces tables having the PHONES type
// the column name should be `${nameSingular}PrimaryPhoneCallingCode`
// 2 - calling code value should be what was previously in the primary country code
// 3 - update the primary country code to be one of the counties with this calling code: +33 => FR | +1 => US (if mulitple, select first one)
// Note: ask Felix if tit's ok for this breaking change TEXT to TEXT. At the same time, should we tranform countrycode en ENUM postgres & graphql ? answer : no need to worry about this breaking change.
// 4 - [IMO, not necessary] if we think it's important, update all additioanl phones to add a country code following the same logic

console.log(
` Note : other consequesnces to check : zapier @martin + REST api @martin
Note : other consequesnces to check : timeline activities @coco : ✅
Note : test with cutom object (not cutom field)
`,
);
// ---------------------------------FieldMetada-----------------------------------------------------------
// 1 - Add the calling code prop in the field-metadata defaultvalue
// 1bis - cahnge country code prop in the field-metadata defaultvalue cf above +33 => FR
// Note: no additionalPhgones in the default value

// ---------------------------------Seeds-----------------------------------------------------------
// 1 - Adjust seeders : update getDevSeedCompanyCustomFields and seedPeople

this.logger.log(`Part 1 - Workspace`);

// ------------------------------------------------------------------------------------------------------------------
// ------------------------------------------------------------------------------------------------------------------
let workspaceIterator = 1;

for (const workspaceId of workspaceIds) {
this.logger.log(
`Running command for workspace ${workspaceId} ${workspaceIterator}/${workspaceIds.length}`,
);

this.logger.log(
`P1 Step 1 - let's find all the fieldsMetadata that have the PHONES type, and extract the objectMetadataId`,
);

try {
const phonesFieldMetadata = await this.fieldMetadataRepository.find({
where: {
workspaceId,
type: FieldMetadataType.PHONES,
},
});

for (const phoneFieldMetadata of phonesFieldMetadata) {
if (
isDefined(phoneFieldMetadata) &&
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same comments as in the other command

isDefined(phoneFieldMetadata.name)
) {
const [objectMetadata] = await this.objectMetadataRepository.find({
where: {
id: phoneFieldMetadata?.objectMetadataId,
},
});

this.logger.log(
`P1 Step 1 - Let's find the "nameSingular" of this objectMetadata: ${objectMetadata.nameSingular || 'not found'}`,
);

if (!objectMetadata || !objectMetadata.nameSingular) break;
guillim marked this conversation as resolved.
Show resolved Hide resolved

this.logger.log(
`P1 Step 1 - Create migration for field ${phoneFieldMetadata.name}`,
);

await this.workspaceMigrationService.createCustomMigration(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You probably didn't mean to keep the field/column creation code in this command?

generateMigrationName(
`create-${objectMetadata.nameSingular}PrimaryPhoneCallingCode-for-field-${phoneFieldMetadata.name}`,
),
workspaceId,
[
{
name: computeObjectTargetTable(objectMetadata),
action: WorkspaceMigrationTableActionType.ALTER,
columns: this.workspaceMigrationFactory.createColumnActions(
WorkspaceMigrationColumnActionType.CREATE,
{
id: v4(),
type: FieldMetadataType.TEXT,
name: `${phoneFieldMetadata.name}PrimaryPhoneCallingCode`,
label: `${phoneFieldMetadata.name}PrimaryPhoneCallingCode`,
objectMetadataId: objectMetadata.id,
workspaceId: workspaceId,
isNullable: true,
defaultValue: "''",
},
),
} satisfies WorkspaceMigrationTableAction,
],
);
}
}

this.logger.log(
`P1 Step 1 - RUN migration to create callingCodes for ${workspaceId.slice(0, 5)}`,
);
await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations(
workspaceId,
);

await this.workspaceMetadataVersionService.incrementMetadataVersion(
workspaceId,
);

this.logger.log(
`P1 Step 2 - Migrations for callingCode must be first. Now can use twentyORMGlobalManager to update countryCode`,
);

this.logger.log(
`P1 Step 3 (same time) - update CountryCode to letters: +33 => FR || +1 => US (if mulitple, first one)`,
);

this.logger.log(
`P1 Step 4 (same time) - update all additioanl phones to add a country code following the same logic`,
);

for (const phoneFieldMetadata of phonesFieldMetadata) {
this.logger.log(`P1 Step 2 - for ${phoneFieldMetadata.name}`);
if (
isDefined(phoneFieldMetadata) &&
isDefined(phoneFieldMetadata.name)
) {
const [objectMetadata] = await this.objectMetadataRepository.find({
where: {
id: phoneFieldMetadata?.objectMetadataId,
},
});

const repository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
workspaceId,
objectMetadata.nameSingular,
);
const records = await repository.find();

for (const record of records) {
// this.logger.log( `P1 Step 2 - obj.nameSingular: ${objectMetadata.nameSingular} (objId: ${objectMetadata.id}) - record.id: ${record.id} - phoneFieldMetadata.name: ${phoneFieldMetadata.name}` );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comments

// this.logger.log(
// `P1 Step 3 - obj.nameSingular: ${objectMetadata.nameSingular} (objId: ${objectMetadata.id}) - record.id: ${record.id} - fieldMetadata: ${phoneFieldMetadata.name} - code : ${record[phoneFieldMetadata.name]?.primaryPhoneCountryCode} `,
// );
if (
record &&
record[phoneFieldMetadata.name] &&
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can simplify chaining "?." operator

record[phoneFieldMetadata.name].primaryPhoneCountryCode &&
isCallingCode(
record[phoneFieldMetadata.name].primaryPhoneCountryCode,
)
) {
guillim marked this conversation as resolved.
Show resolved Hide resolved
// let's process the additional phones
let additionalPhones = null;

if (record[phoneFieldMetadata.name].additionalPhones) {
additionalPhones = record[
phoneFieldMetadata.name
].additionalPhones.map((phone) => {
return {
...phone,
countryCode: callingCodeToCountryCode(phone.callingCode),
};
});
}
guillim marked this conversation as resolved.
Show resolved Hide resolved

await repository.update(record.id, {
[`${phoneFieldMetadata.name}PrimaryPhoneCallingCode`]:
record[phoneFieldMetadata.name].primaryPhoneCountryCode,
[`${phoneFieldMetadata.name}PrimaryPhoneCountryCode`]:
callingCodeToCountryCode(
record[phoneFieldMetadata.name].primaryPhoneCountryCode,
),
[`${phoneFieldMetadata.name}AdditionalPhones`]:
additionalPhones,
});
}
}
}
}
} catch (error) {
throw new Error(`Error in workspace ${workspaceId} : ${error}`);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as the other command I'm not sure we want to throw there yet!

}
workspaceIterator++;
}

this.logger.log(`

Part 2 - FieldMetadata`);

workspaceIterator = 1;
for (const workspaceId of workspaceIds) {
this.logger.log(
`Running command for workspace ${workspaceId} ${workspaceIterator}/${workspaceIds.length}`,
);

this.logger.log(
`P2 Step 1 - let's find all the fieldsMetadata that have the PHONES type, and extract the objectMetadataId`,
);

try {
const phonesFieldMetadata = await this.fieldMetadataRepository.find({
where: {
workspaceId,
type: FieldMetadataType.PHONES,
},
});

for (const phoneFieldMetadata of phonesFieldMetadata) {
if (
!isDefined(phoneFieldMetadata) ||
!isDefined(phoneFieldMetadata.defaultValue)
)
continue;
let defaultValue = phoneFieldMetadata.defaultValue;

// some cases look like it's an array. let's flatten it (not sure the case is supposed to happen but I saw it in my local db)
if (Array.isArray(defaultValue) && isDefined(defaultValue[0]))
defaultValue = phoneFieldMetadata.defaultValue[0];

if (!isDefined(defaultValue)) continue;
if (typeof defaultValue !== 'object') continue;
if (!('primaryPhoneCountryCode' in defaultValue)) continue;
if (!defaultValue.primaryPhoneCountryCode) continue;

const primaryPhoneCountryCode = defaultValue.primaryPhoneCountryCode;

const countryCode = callingCodeToCountryCode(
primaryPhoneCountryCode.replace(/["']/g, ''),
);

await this.fieldMetadataRepository.update(phoneFieldMetadata.id, {
defaultValue: {
...defaultValue,
primaryPhoneCountryCode: countryCode ? `'${countryCode}'` : "''",
primaryPhoneCallingCode: isCallingCode(
primaryPhoneCountryCode.replace(/["']/g, ''),
)
? primaryPhoneCountryCode
: "''",
},
});
}
} catch (error) {
throw new Error(`Error in workspace ${workspaceId} : ${error}`);
}
workspaceIterator++;
}
this.logger.log(chalk.green(`Command completed!`));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Repository } from 'typeorm';

import { ActiveWorkspacesCommandRunner } from 'src/database/commands/active-workspaces.command';
import { BaseCommandOptions } from 'src/database/commands/base.command';
import { PhoneCallingCodeCommand } from 'src/database/commands/upgrade-version/0-40/0-40-phone-calling-code.command';
import { RecordPositionBackfillCommand } from 'src/database/commands/upgrade-version/0-40/0-40-record-position-backfill.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';
Expand All @@ -17,8 +18,9 @@ export class UpgradeTo0_40Command extends ActiveWorkspacesCommandRunner {
constructor(
@InjectRepository(Workspace, 'core')
protected readonly workspaceRepository: Repository<Workspace>,
private readonly recordPositionBackfillCommand: RecordPositionBackfillCommand,
private readonly syncWorkspaceMetadataCommand: SyncWorkspaceMetadataCommand,
private readonly phoneCallingCodeCommand: PhoneCallingCodeCommand,
private readonly recordPositionBackfillCommand: RecordPositionBackfillCommand,
) {
super(workspaceRepository);
}
Expand All @@ -28,6 +30,16 @@ export class UpgradeTo0_40Command extends ActiveWorkspacesCommandRunner {
options: BaseCommandOptions,
workspaceIds: string[],
): Promise<void> {
this.logger.log(
'Running command to upgrade to 0.40: must start with phone calling code otherwise SyncMetadata will fail',
);

await this.phoneCallingCodeCommand.executeActiveWorkspacesCommand(
passedParam,
options,
workspaceIds,
);

await this.recordPositionBackfillCommand.executeActiveWorkspacesCommand(
passedParam,
options,
Expand Down
Loading
Loading