Migrate Company and Person standard fields in preparation for the enrichment app (#21171)
# Migrate Company and Person standard fields in preparation for the
enrichment app
## Why
Our standard `Person`/`Company` objects accumulated fields that aren't
generic to every
business, while missing a more universal revenue field that essentially
every CRM ships.
This PR makes the **Standard application** hold a tighter, more
universal set of fields,
and sets the stage for a follow-up PR that introduces a **People Data
Labs enrichment app**
to populate them.
## What changes
### Standard fields
**Demoted (Standard → Workspace Custom application)** — not generic
enough to ship as standard:
| Object | Field | Type |
| ------- | ------------------------------ | -------- |
| Company | annualRecurringRevenue (ARR) | CURRENCY |
| Company | employees | NUMBER |
| Company | idealCustomerProfile (ICP) | BOOLEAN |
| Company | xLink (X/Twitter) | LINKS |
| Person | xLink (X/Twitter) | LINKS |
| Person | city | TEXT |
**Added (new generic Standard field)** — present in
Salesforce/HubSpot/Zoho, PDL-populatable:
| Object | Field | Type |
| ------- | ------------- |
-------------------------------------------------------- |
| Company | annualRevenue | CURRENCY (generic total revenue; replaces
the niche ARR) |
### Behavior by workspace
* **New workspaces:** demoted fields are gone; `annualRevenue` is
**active**.
* **Existing workspaces:** demoted fields are **preserved as active
custom fields, data intact**;
`annualRevenue` is created **inactive (opt-in)** with its column ready,
so a later activation
is a metadata-only toggle.
### Upgrade commands (v2.9)
Three idempotent, per-workspace commands, run in timestamp order:
1. **`upgrade:2-9:move-demoted-standard-fields-to-custom-application`**
(1799000040000) —
re-owns the 6 demoted fields to the workspace custom application
(`isCustom = true`,
new `applicationId` + fresh `universalIdentifier`), keeping their data
and active state.
2. **`upgrade:2-9:rename-conflicting-custom-fields`** (1799000045000) —
if a workspace already
has a *custom* field named `annualRevenue`, renames it to
`annualRevenueCustom`
(data preserved via column rename) so the standard field can be added.
Skips non-custom matches.
3. **`upgrade:2-9:add-inactive-generic-standard-fields`**
(1799000050000) — creates
`Company.annualRevenue` on existing workspaces as inactive, guarded to
skip workspaces
missing the target object or where the name is still taken.
**Failure model:** the workspace iterator isolates failures per
workspace (one workspace failing
never affects others); within a workspace the runner records per-command
status and resumes on the
next run, and every command is idempotent, so partial runs self-heal.
### Supporting changes
* **Field-option color palette:** widened the `TagColor` union
(`twenty-shared` `FieldMetadataOptions`
+ the field-metadata `options.input` DTO) from 10 colors to the full
theme palette, benefiting any
future SELECT/MULTI_SELECT field.
* **Dev seeder:**
* The default "Annual Recurring Revenue" dashboard widget now points at
the generic
`annualRevenue` field (renamed to "Annual Revenue").
* Removed the "Companies by Size (Stacked by City)" widget (relied on
the demoted `employees`).
* `employees` is dropped from company data seeds and re-added as a
**custom** field seed, so dev
workspaces still get an `employees` column matching the demoted
behavior.
### Cleanup
Front-end record types (`Company.ts`/`Person.ts`), the
`getDisplayNameFromParticipant` test mock,
metadata integration specs, the Zapier `crud_record` test, and the
regenerated
`get-standard-object-metadata-related-entity-ids` snapshot.
## ⚠️ Breaking change (intentional)
Removes standard fields `Company.annualRecurringRevenue`,
`Company.employees`,
`Company.idealCustomerProfile`, `Company.xLink`, `Person.xLink`, and
`Person.city` from the core
GraphQL schema (replaced by `Company.annualRevenue`).
This is why the breaking-changes check reports a large number of
removals — `graphql-inspector`
flags any removed object field plus its derived
aggregate/order-by/filter/update types.
**Mitigation:** the
`upgrade:2-9:move-demoted-standard-fields-to-custom-application` command
re-owns these fields as custom fields per workspace, preserving their
name and data, so existing
tenants keep working. New workspaces won't have them.
This commit is contained in:
+29
@@ -0,0 +1,29 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { WorkspaceIteratorModule } from 'src/database/commands/command-runners/workspace-iterator.module';
|
||||
import { AddInactiveGenericStandardFieldsCommand } from 'src/database/commands/upgrade-version-command/2-10/2-10-workspace-command-1799000050000-add-inactive-generic-standard-fields.command';
|
||||
import { MoveDemotedStandardFieldsToCustomApplicationCommand } from 'src/database/commands/upgrade-version-command/2-10/2-10-workspace-command-1799000040000-move-demoted-standard-fields-to-custom-application.command';
|
||||
import { RenameConflictingCustomFieldsCommand } from 'src/database/commands/upgrade-version-command/2-10/2-10-workspace-command-1799000045000-rename-conflicting-custom-fields.command';
|
||||
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module';
|
||||
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
|
||||
import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ApplicationModule,
|
||||
TypeOrmModule.forFeature([FieldMetadataEntity]),
|
||||
WorkspaceCacheModule,
|
||||
WorkspaceIteratorModule,
|
||||
WorkspaceMetadataVersionModule,
|
||||
WorkspaceMigrationModule,
|
||||
],
|
||||
providers: [
|
||||
MoveDemotedStandardFieldsToCustomApplicationCommand,
|
||||
RenameConflictingCustomFieldsCommand,
|
||||
AddInactiveGenericStandardFieldsCommand,
|
||||
],
|
||||
})
|
||||
export class V2_10_UpgradeVersionCommandModule {}
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
import { Command } from 'nest-commander';
|
||||
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { Repository } from 'typeorm';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { ActiveOrSuspendedWorkspaceCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspace.command-runner';
|
||||
import { WorkspaceIteratorService } from 'src/database/commands/command-runners/workspace-iterator.service';
|
||||
import { type RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspace.command-runner';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { RegisteredWorkspaceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-workspace-command.decorator';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { findFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier.util';
|
||||
import { getMetadataFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-flat-entity-maps-key.util';
|
||||
import { getMetadataRelatedMetadataNames } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-related-metadata-names.util';
|
||||
import { getMetadataSerializedRelationNames } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-serialized-relation-names.util';
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service';
|
||||
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
|
||||
const DEMOTED_STANDARD_FIELDS: {
|
||||
universalIdentifier: string;
|
||||
label: string;
|
||||
}[] = [
|
||||
{
|
||||
universalIdentifier: '20202020-602a-495c-9776-f5d5b11d227b',
|
||||
label: 'Company.annualRecurringRevenue',
|
||||
},
|
||||
{
|
||||
universalIdentifier: '20202020-8965-464a-8a75-74bafc152a0b',
|
||||
label: 'Company.employees',
|
||||
},
|
||||
{
|
||||
universalIdentifier: '20202020-ba6b-438a-8213-2c5ba28d76a2',
|
||||
label: 'Company.idealCustomerProfile',
|
||||
},
|
||||
{
|
||||
universalIdentifier: '20202020-6f64-4fd9-9580-9c1991c7d8c3',
|
||||
label: 'Company.xLink',
|
||||
},
|
||||
{
|
||||
universalIdentifier: '20202020-8fc2-487c-b84a-55a99b145cfd',
|
||||
label: 'Person.xLink',
|
||||
},
|
||||
{
|
||||
universalIdentifier: '20202020-5243-4ffb-afc5-2c675da41346',
|
||||
label: 'Person.city',
|
||||
},
|
||||
];
|
||||
|
||||
@RegisteredWorkspaceCommand('2.10.0', 1799000040000)
|
||||
@Command({
|
||||
name: 'upgrade:2-10:move-demoted-standard-fields-to-custom-application',
|
||||
description:
|
||||
'Re-own the demoted Company ARR / ICP / Employees, Company/Person X (Twitter) and Person City standard fields to the workspace custom application, preserving their data and keeping them active',
|
||||
})
|
||||
export class MoveDemotedStandardFieldsToCustomApplicationCommand extends ActiveOrSuspendedWorkspaceCommandRunner {
|
||||
constructor(
|
||||
protected readonly workspaceIteratorService: WorkspaceIteratorService,
|
||||
private readonly applicationService: ApplicationService,
|
||||
private readonly workspaceCacheService: WorkspaceCacheService,
|
||||
private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService,
|
||||
@InjectRepository(FieldMetadataEntity)
|
||||
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
|
||||
) {
|
||||
super(workspaceIteratorService);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
workspaceId,
|
||||
options,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
const isDryRun = options.dryRun ?? false;
|
||||
|
||||
const { twentyStandardFlatApplication, workspaceCustomFlatApplication } =
|
||||
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
||||
{ workspaceId },
|
||||
);
|
||||
|
||||
const { flatFieldMetadataMaps } =
|
||||
await this.workspaceCacheService.getOrRecompute(workspaceId, [
|
||||
'flatFieldMetadataMaps',
|
||||
]);
|
||||
|
||||
const fieldsToReown: { id: string; label: string }[] = [];
|
||||
|
||||
for (const {
|
||||
universalIdentifier,
|
||||
label,
|
||||
} of DEMOTED_STANDARD_FIELDS) {
|
||||
const flatFieldMetadata =
|
||||
findFlatEntityByUniversalIdentifier<FlatFieldMetadata>({
|
||||
flatEntityMaps: flatFieldMetadataMaps,
|
||||
universalIdentifier,
|
||||
});
|
||||
|
||||
if (!isDefined(flatFieldMetadata)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const isStillOwnedByStandardApplication =
|
||||
flatFieldMetadata.applicationId === twentyStandardFlatApplication.id &&
|
||||
!flatFieldMetadata.isCustom;
|
||||
|
||||
if (!isStillOwnedByStandardApplication) {
|
||||
continue;
|
||||
}
|
||||
|
||||
fieldsToReown.push({ id: flatFieldMetadata.id, label });
|
||||
}
|
||||
|
||||
if (fieldsToReown.length === 0) {
|
||||
this.logger.log(
|
||||
`No standard fields to move to the custom application for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`${isDryRun ? '[DRY RUN] ' : ''}Moving ${fieldsToReown.length} field(s) to the custom application for workspace ${workspaceId}: ${fieldsToReown.map(({ label }) => label).join(', ')}`,
|
||||
);
|
||||
|
||||
if (isDryRun) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const { id } of fieldsToReown) {
|
||||
await this.fieldMetadataRepository.update(
|
||||
{ id },
|
||||
{
|
||||
applicationId: workspaceCustomFlatApplication.id,
|
||||
isCustom: true,
|
||||
universalIdentifier: uuidv4(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const fieldMetadataRelatedNames = [
|
||||
'fieldMetadata',
|
||||
...getMetadataRelatedMetadataNames('fieldMetadata'),
|
||||
...getMetadataSerializedRelationNames('fieldMetadata'),
|
||||
] as const;
|
||||
const cacheKeysToFlush = [
|
||||
...new Set(fieldMetadataRelatedNames.map(getMetadataFlatEntityMapsKey)),
|
||||
];
|
||||
|
||||
await this.workspaceCacheService.flush(workspaceId, cacheKeysToFlush);
|
||||
|
||||
await this.workspaceMetadataVersionService.incrementMetadataVersion(
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
this.logger.log(
|
||||
`Moved ${fieldsToReown.length} field(s) to the custom application for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
+196
@@ -0,0 +1,196 @@
|
||||
import { Command } from 'nest-commander';
|
||||
|
||||
import { STANDARD_OBJECTS } from 'twenty-shared/metadata';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { ActiveOrSuspendedWorkspaceCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspace.command-runner';
|
||||
import { WorkspaceIteratorService } from 'src/database/commands/command-runners/workspace-iterator.service';
|
||||
import { type RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspace.command-runner';
|
||||
import { RegisteredWorkspaceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-workspace-command.decorator';
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
|
||||
|
||||
const NEW_STANDARD_FIELDS: {
|
||||
fieldName: string;
|
||||
objectUniversalIdentifier: string;
|
||||
}[] = [
|
||||
{
|
||||
fieldName: 'annualRevenue',
|
||||
objectUniversalIdentifier: STANDARD_OBJECTS.company.universalIdentifier,
|
||||
},
|
||||
];
|
||||
|
||||
const computeAvailableFieldName = (
|
||||
baseFieldName: string,
|
||||
takenFieldNames: Set<string>,
|
||||
): string => {
|
||||
let candidateFieldName = `${baseFieldName}Custom`;
|
||||
let suffix = 2;
|
||||
|
||||
while (takenFieldNames.has(candidateFieldName)) {
|
||||
candidateFieldName = `${baseFieldName}Custom${suffix}`;
|
||||
suffix++;
|
||||
}
|
||||
|
||||
return candidateFieldName;
|
||||
};
|
||||
|
||||
@RegisteredWorkspaceCommand('2.10.0', 1799000045000)
|
||||
@Command({
|
||||
name: 'upgrade:2-10:rename-conflicting-custom-fields',
|
||||
description:
|
||||
'Rename a pre-existing custom field whose name collides with the new generic standard field (Company annualRevenue), preserving its data, so the standard field can be added',
|
||||
})
|
||||
export class RenameConflictingCustomFieldsCommand extends ActiveOrSuspendedWorkspaceCommandRunner {
|
||||
constructor(
|
||||
protected readonly workspaceIteratorService: WorkspaceIteratorService,
|
||||
private readonly workspaceCacheService: WorkspaceCacheService,
|
||||
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
|
||||
) {
|
||||
super(workspaceIteratorService);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
workspaceId,
|
||||
options,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
const isDryRun = options.dryRun ?? false;
|
||||
|
||||
const { flatFieldMetadataMaps } =
|
||||
await this.workspaceCacheService.getOrRecompute(workspaceId, [
|
||||
'flatFieldMetadataMaps',
|
||||
]);
|
||||
|
||||
const allFlatFieldMetadatas = Object.values(
|
||||
flatFieldMetadataMaps.byUniversalIdentifier,
|
||||
).filter(isDefined);
|
||||
|
||||
const takenFieldNamesByObject = new Map<string, Set<string>>();
|
||||
|
||||
for (const flatFieldMetadata of allFlatFieldMetadatas) {
|
||||
const takenFieldNames =
|
||||
takenFieldNamesByObject.get(
|
||||
flatFieldMetadata.objectMetadataUniversalIdentifier,
|
||||
) ?? new Set<string>();
|
||||
|
||||
takenFieldNames.add(flatFieldMetadata.name);
|
||||
takenFieldNamesByObject.set(
|
||||
flatFieldMetadata.objectMetadataUniversalIdentifier,
|
||||
takenFieldNames,
|
||||
);
|
||||
}
|
||||
|
||||
const fieldsToRename: { field: FlatFieldMetadata; newName: string }[] = [];
|
||||
|
||||
for (const {
|
||||
fieldName,
|
||||
objectUniversalIdentifier,
|
||||
} of NEW_STANDARD_FIELDS) {
|
||||
const conflictingField = allFlatFieldMetadatas.find(
|
||||
(flatFieldMetadata) =>
|
||||
flatFieldMetadata.objectMetadataUniversalIdentifier ===
|
||||
objectUniversalIdentifier && flatFieldMetadata.name === fieldName,
|
||||
);
|
||||
|
||||
if (!isDefined(conflictingField)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!conflictingField.isCustom) {
|
||||
this.logger.warn(
|
||||
`Non-custom field named "${fieldName}" exists on object ${objectUniversalIdentifier} for workspace ${workspaceId}; skipping rename`,
|
||||
);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
const takenFieldNames =
|
||||
takenFieldNamesByObject.get(objectUniversalIdentifier) ??
|
||||
new Set<string>();
|
||||
const newName = computeAvailableFieldName(fieldName, takenFieldNames);
|
||||
|
||||
takenFieldNames.add(newName);
|
||||
fieldsToRename.push({ field: conflictingField, newName });
|
||||
}
|
||||
|
||||
if (fieldsToRename.length === 0) {
|
||||
this.logger.log(
|
||||
`No custom fields conflict with the new standard field names for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`${isDryRun ? '[DRY RUN] ' : ''}Renaming ${fieldsToRename.length} conflicting custom field(s) for workspace ${workspaceId}: ${fieldsToRename
|
||||
.map(({ field, newName }) => `${field.name} -> ${newName}`)
|
||||
.join(', ')}`,
|
||||
);
|
||||
|
||||
if (isDryRun) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fieldsToRenameByApplication = fieldsToRename.reduce<
|
||||
Map<string, typeof fieldsToRename>
|
||||
>((fieldsByApplication, fieldToRename) => {
|
||||
const applicationUniversalIdentifier =
|
||||
fieldToRename.field.applicationUniversalIdentifier;
|
||||
const fieldsForApplication =
|
||||
fieldsByApplication.get(applicationUniversalIdentifier) ?? [];
|
||||
|
||||
fieldsForApplication.push(fieldToRename);
|
||||
fieldsByApplication.set(
|
||||
applicationUniversalIdentifier,
|
||||
fieldsForApplication,
|
||||
);
|
||||
|
||||
return fieldsByApplication;
|
||||
}, new Map());
|
||||
|
||||
for (const [
|
||||
applicationUniversalIdentifier,
|
||||
fieldsForApplication,
|
||||
] of fieldsToRenameByApplication) {
|
||||
const flatEntityToUpdate = fieldsForApplication.map(
|
||||
({ field, newName }) => ({
|
||||
...field,
|
||||
name: newName,
|
||||
label: `${field.label} (custom)`,
|
||||
isLabelSyncedWithName: false,
|
||||
}),
|
||||
);
|
||||
|
||||
const validateAndBuildResult =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
||||
{
|
||||
allFlatEntityOperationByMetadataName: {
|
||||
fieldMetadata: {
|
||||
flatEntityToCreate: [],
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate,
|
||||
},
|
||||
},
|
||||
workspaceId,
|
||||
isSystemBuild: true,
|
||||
applicationUniversalIdentifier,
|
||||
},
|
||||
);
|
||||
|
||||
if (validateAndBuildResult.status === 'fail') {
|
||||
this.logger.error(
|
||||
`Failed to rename conflicting custom fields:\n${JSON.stringify(validateAndBuildResult, null, 2)}`,
|
||||
);
|
||||
|
||||
throw new Error(
|
||||
`Failed to rename conflicting custom fields for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Renamed ${fieldsToRename.length} conflicting custom field(s) for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
+182
@@ -0,0 +1,182 @@
|
||||
import { Command } from 'nest-commander';
|
||||
|
||||
import { STANDARD_OBJECTS } from 'twenty-shared/metadata';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { ActiveOrSuspendedWorkspaceCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspace.command-runner';
|
||||
import { WorkspaceIteratorService } from 'src/database/commands/command-runners/workspace-iterator.service';
|
||||
import { type RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspace.command-runner';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { RegisteredWorkspaceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-workspace-command.decorator';
|
||||
import { findFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier.util';
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
import { computeTwentyStandardApplicationAllFlatEntityMaps } from 'src/engine/workspace-manager/twenty-standard-application/utils/twenty-standard-application-all-flat-entity-maps.constant';
|
||||
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
|
||||
|
||||
const NEW_STANDARD_FIELD_UNIVERSAL_IDENTIFIERS = [
|
||||
STANDARD_OBJECTS.company.fields.annualRevenue.universalIdentifier,
|
||||
];
|
||||
|
||||
@RegisteredWorkspaceCommand('2.10.0', 1799000050000)
|
||||
@Command({
|
||||
name: 'upgrade:2-10:add-inactive-generic-standard-fields',
|
||||
description:
|
||||
'Create the new generic standard field (Company annualRevenue) on existing workspaces as inactive (opt-in), with its column ready for activation',
|
||||
})
|
||||
export class AddInactiveGenericStandardFieldsCommand extends ActiveOrSuspendedWorkspaceCommandRunner {
|
||||
constructor(
|
||||
protected readonly workspaceIteratorService: WorkspaceIteratorService,
|
||||
private readonly applicationService: ApplicationService,
|
||||
private readonly workspaceCacheService: WorkspaceCacheService,
|
||||
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
|
||||
) {
|
||||
super(workspaceIteratorService);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
workspaceId,
|
||||
options,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
const isDryRun = options.dryRun ?? false;
|
||||
|
||||
const { twentyStandardFlatApplication } =
|
||||
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
||||
{ workspaceId },
|
||||
);
|
||||
|
||||
const {
|
||||
flatFieldMetadataMaps: existingFlatFieldMetadataMaps,
|
||||
flatObjectMetadataMaps,
|
||||
} = await this.workspaceCacheService.getOrRecompute(workspaceId, [
|
||||
'flatFieldMetadataMaps',
|
||||
'flatObjectMetadataMaps',
|
||||
]);
|
||||
|
||||
const { allFlatEntityMaps: standardAllFlatEntityMaps } =
|
||||
computeTwentyStandardApplicationAllFlatEntityMaps({
|
||||
now: new Date().toISOString(),
|
||||
workspaceId,
|
||||
twentyStandardApplicationId: twentyStandardFlatApplication.id,
|
||||
});
|
||||
|
||||
const takenFieldNamesByObject = new Map<string, Set<string>>();
|
||||
|
||||
for (const existingFlatFieldMetadata of Object.values(
|
||||
existingFlatFieldMetadataMaps.byUniversalIdentifier,
|
||||
).filter(isDefined)) {
|
||||
const takenFieldNames =
|
||||
takenFieldNamesByObject.get(
|
||||
existingFlatFieldMetadata.objectMetadataUniversalIdentifier,
|
||||
) ?? new Set<string>();
|
||||
|
||||
takenFieldNames.add(existingFlatFieldMetadata.name);
|
||||
takenFieldNamesByObject.set(
|
||||
existingFlatFieldMetadata.objectMetadataUniversalIdentifier,
|
||||
takenFieldNames,
|
||||
);
|
||||
}
|
||||
|
||||
const fieldsToCreate: FlatFieldMetadata[] = [];
|
||||
|
||||
for (const universalIdentifier of NEW_STANDARD_FIELD_UNIVERSAL_IDENTIFIERS) {
|
||||
const standardFlatFieldMetadata =
|
||||
standardAllFlatEntityMaps.flatFieldMetadataMaps.byUniversalIdentifier[
|
||||
universalIdentifier
|
||||
];
|
||||
|
||||
if (!isDefined(standardFlatFieldMetadata)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const fieldAlreadyExists = isDefined(
|
||||
existingFlatFieldMetadataMaps.byUniversalIdentifier[
|
||||
standardFlatFieldMetadata.universalIdentifier
|
||||
],
|
||||
);
|
||||
|
||||
if (fieldAlreadyExists) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const targetObjectExists = isDefined(
|
||||
findFlatEntityByUniversalIdentifier<FlatObjectMetadata>({
|
||||
flatEntityMaps: flatObjectMetadataMaps,
|
||||
universalIdentifier:
|
||||
standardFlatFieldMetadata.objectMetadataUniversalIdentifier,
|
||||
}),
|
||||
);
|
||||
|
||||
if (!targetObjectExists) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const fieldNameIsTaken =
|
||||
takenFieldNamesByObject
|
||||
.get(standardFlatFieldMetadata.objectMetadataUniversalIdentifier)
|
||||
?.has(standardFlatFieldMetadata.name) === true;
|
||||
|
||||
if (fieldNameIsTaken) {
|
||||
this.logger.warn(
|
||||
`Field name "${standardFlatFieldMetadata.name}" is already taken on its object for workspace ${workspaceId}; skipping (run upgrade:2-10:rename-conflicting-custom-fields first)`,
|
||||
);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
fieldsToCreate.push({
|
||||
...standardFlatFieldMetadata,
|
||||
isActive: false,
|
||||
viewFieldIds: [],
|
||||
viewFieldUniversalIdentifiers: [],
|
||||
});
|
||||
}
|
||||
|
||||
if (fieldsToCreate.length === 0) {
|
||||
this.logger.log(
|
||||
`All new generic standard fields already exist for workspace ${workspaceId}, skipping`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`${isDryRun ? '[DRY RUN] ' : ''}Creating ${fieldsToCreate.length} inactive standard field(s) for workspace ${workspaceId}: ${fieldsToCreate.map(({ name }) => name).join(', ')}`,
|
||||
);
|
||||
|
||||
if (isDryRun) {
|
||||
return;
|
||||
}
|
||||
|
||||
const validateAndBuildResult =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
||||
{
|
||||
allFlatEntityOperationByMetadataName: {
|
||||
fieldMetadata: {
|
||||
flatEntityToCreate: fieldsToCreate,
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
},
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier:
|
||||
twentyStandardFlatApplication.universalIdentifier,
|
||||
},
|
||||
);
|
||||
|
||||
if (validateAndBuildResult.status === 'fail') {
|
||||
this.logger.error(
|
||||
`Failed to create inactive generic standard fields:\n${JSON.stringify(validateAndBuildResult, null, 2)}`,
|
||||
);
|
||||
|
||||
throw new Error(
|
||||
`Failed to create inactive generic standard fields for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Successfully created ${fieldsToCreate.length} inactive standard field(s) for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
+2
@@ -12,6 +12,7 @@ import { V2_5_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-
|
||||
import { V2_7_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/2-7/2-7-upgrade-version-command.module';
|
||||
import { V2_8_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/2-8/2-8-upgrade-version-command.module';
|
||||
import { V2_9_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/2-9/2-9-upgrade-version-command.module';
|
||||
import { V2_10_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/2-10/2-10-upgrade-version-command.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -27,6 +28,7 @@ import { V2_9_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-
|
||||
V2_7_UpgradeVersionCommandModule,
|
||||
V2_8_UpgradeVersionCommandModule,
|
||||
V2_9_UpgradeVersionCommandModule,
|
||||
V2_10_UpgradeVersionCommandModule,
|
||||
],
|
||||
})
|
||||
export class WorkspaceCommandProviderModule {}
|
||||
|
||||
Reference in New Issue
Block a user