Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions packages/twenty-front/src/generated-metadata/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1268,6 +1268,7 @@ export enum FeatureFlagKey {
IS_PUBLIC_DOMAIN_ENABLED = 'IS_PUBLIC_DOMAIN_ENABLED',
IS_RECORD_PAGE_LAYOUT_ENABLED = 'IS_RECORD_PAGE_LAYOUT_ENABLED',
IS_STRIPE_INTEGRATION_ENABLED = 'IS_STRIPE_INTEGRATION_ENABLED',
IS_TIMELINE_ACTIVITY_MIGRATED = 'IS_TIMELINE_ACTIVITY_MIGRATED',
IS_UNIQUE_INDEXES_ENABLED = 'IS_UNIQUE_INDEXES_ENABLED',
IS_WORKFLOW_RUN_STOPPAGE_ENABLED = 'IS_WORKFLOW_RUN_STOPPAGE_ENABLED',
IS_WORKSPACE_MIGRATION_V2_ENABLED = 'IS_WORKSPACE_MIGRATION_V2_ENABLED'
Expand Down
1 change: 1 addition & 0 deletions packages/twenty-front/src/generated/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1232,6 +1232,7 @@ export enum FeatureFlagKey {
IS_PUBLIC_DOMAIN_ENABLED = 'IS_PUBLIC_DOMAIN_ENABLED',
IS_RECORD_PAGE_LAYOUT_ENABLED = 'IS_RECORD_PAGE_LAYOUT_ENABLED',
IS_STRIPE_INTEGRATION_ENABLED = 'IS_STRIPE_INTEGRATION_ENABLED',
IS_TIMELINE_ACTIVITY_MIGRATED = 'IS_TIMELINE_ACTIVITY_MIGRATED',
IS_UNIQUE_INDEXES_ENABLED = 'IS_UNIQUE_INDEXES_ENABLED',
IS_WORKFLOW_RUN_STOPPAGE_ENABLED = 'IS_WORKFLOW_RUN_STOPPAGE_ENABLED',
IS_WORKSPACE_MIGRATION_V2_ENABLED = 'IS_WORKSPACE_MIGRATION_V2_ENABLED'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,23 @@ import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getActivi
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useGenerateDepthRecordGqlFieldsFromObject } from '@/object-record/graphql/record-gql-fields/hooks/useGenerateDepthRecordGqlFieldsFromObject';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { isDefined } from 'twenty-shared/utils';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { capitalize, isDefined } from 'twenty-shared/utils';
import { FeatureFlagKey } from '~/generated/graphql';

// do we need to test this?
export const useTimelineActivities = (
targetableObject: ActivityTargetableObject,
) => {
const targetableObjectFieldIdName = getActivityTargetObjectFieldIdName({
nameSingular: targetableObject.targetObjectNameSingular,
});
const isTimelineActivityMigrated = useIsFeatureEnabled(
FeatureFlagKey.IS_TIMELINE_ACTIVITY_MIGRATED,
);

const targetableObjectFieldIdName = isTimelineActivityMigrated
? `target${capitalize(targetableObject.targetObjectNameSingular)}Id`
: getActivityTargetObjectFieldIdName({
nameSingular: targetableObject.targetObjectNameSingular,
});

const { recordGqlFields: depthOneRecordGqlFields } =
useGenerateDepthRecordGqlFieldsFromObject({
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';

import { Command } from 'nest-commander';
import { capitalize } from 'twenty-shared/utils';
import { DataSource, Repository } from 'typeorm';

import {
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
type RunOnWorkspaceArgs,
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util';

@Command({
name: 'upgrade:1-11:migrate-timeline-activity-to-morph-relations',
description:
'Migrate timeline activity relations to morph relation fields and set feature flag',
})
export class MigrateTimelineActivityToMorphRelationsCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
constructor(
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly featureFlagService: FeatureFlagService,
@InjectRepository(ObjectMetadataEntity)
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
@InjectDataSource()
private readonly coreDataSource: DataSource,
) {
super(workspaceRepository, twentyORMGlobalManager);
}

override async runOnWorkspace({
workspaceId,
options,
}: RunOnWorkspaceArgs): Promise<void> {
const isMigrated = await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IS_TIMELINE_ACTIVITY_MIGRATED,
workspaceId,
);

this.logger.log(`Migrating timelineActivity for workspace ${workspaceId}`);

if (isMigrated) {
this.logger.log(
`Timeline activity migration already completed. Skipping...`,
);

return;
}

if (options.dryRun) {
this.logger.log(
`Would have migrated timeline activities for workspace ${workspaceId}. Skipping...`,
);

return;
}

const schemaName = getWorkspaceSchemaName(workspaceId);
const tableName = 'timelineActivity';

const customObjectMetadata = await this.objectMetadataRepository.find({
where: {
workspaceId,
isCustom: true,
},
});
const customObjectMetadataNames = customObjectMetadata.map(
(objectMetadata) => objectMetadata.nameSingular,
);

const fieldMigrations = [
{ old: 'companyId', new: 'targetCompanyId' },
{ old: 'personId', new: 'targetPersonId' },
{ old: 'opportunityId', new: 'targetOpportunityId' },
{ old: 'noteId', new: 'targetNoteId' },
{ old: 'taskId', new: 'targetTaskId' },
{ old: 'workflowId', new: 'targetWorkflowId' },
{ old: 'workflowVersionId', new: 'targetWorkflowVersionId' },
{ old: 'workflowRunId', new: 'targetWorkflowRunId' },
{ old: 'dashboardId', new: 'targetDashboardId' },
...customObjectMetadataNames.map((customObjectName) => ({
old: `${customObjectName}Id`,
new: `target${capitalize(customObjectName)}Id`,
})),
];

for (const customObjectName of customObjectMetadataNames) {
const newColumn = `target${capitalize(customObjectName)}Id`;

try {
await this.coreDataSource.query(
`ALTER TABLE "${schemaName}"."${tableName}"
ADD COLUMN IF NOT EXISTS "${newColumn}" uuid NULL`,
);
this.logger.log(
`Created column "${newColumn}" for custom object "${customObjectName}"`,
);
} catch (error) {
this.logger.error(
`Error creating column "${newColumn}" for custom object "${customObjectName}" in workspace ${workspaceId}`,
error,
);

return;
}
}

for (const { old: oldField, new: newField } of fieldMigrations) {
try {
const result = await this.coreDataSource.query(
`UPDATE "${schemaName}"."${tableName}"
SET "${newField}" = "${oldField}"
WHERE "${oldField}" IS NOT NULL
AND "${newField}" IS NULL`,
);

const rowsUpdated = result[1] || 0;

if (rowsUpdated > 0) {
this.logger.log(
`Migrated ${rowsUpdated} records for ${oldField} → ${newField}`,
);
}
} catch (error) {
this.logger.error(
`Error migrating ${oldField} → ${newField} for workspace ${workspaceId}`,
error,
);

return;
}
}

this.logger.log(`✅ Successfully migrated timeline activity records`);

await this.featureFlagService.enableFeatureFlags(
[FeatureFlagKey.IS_TIMELINE_ACTIVITY_MIGRATED],
workspaceId,
);

this.logger.log(
`Set IS_TIMELINE_ACTIVITY_MIGRATED feature flag for workspace ${workspaceId}`,
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,19 @@ import { TypeOrmModule } from '@nestjs/typeorm';

import { CleanOrphanedRoleTargetsCommand } from 'src/database/commands/upgrade-version-command/1-11/1-11-clean-orphaned-role-targets.command';
import { CleanOrphanedUserWorkspacesCommand } from 'src/database/commands/upgrade-version-command/1-11/1-11-clean-orphaned-user-workspaces.command';
import { MigrateTimelineActivityToMorphRelationsCommand } from 'src/database/commands/upgrade-version-command/1-11/1-11-migrate-timeline-activity-to-morph-relations.command';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
import { WorkspaceSchemaManagerModule } from 'src/engine/twenty-orm/workspace-schema-manager/workspace-schema-manager.module';
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';

@Module({
imports: [
Expand All @@ -22,16 +27,22 @@ import { WorkspaceSchemaManagerModule } from 'src/engine/twenty-orm/workspace-sc
ViewEntity,
UserWorkspaceEntity,
RoleTargetsEntity,
DataSourceEntity,
]),
WorkspaceSchemaManagerModule,
FeatureFlagModule,
WorkspaceCacheStorageModule,
ObjectMetadataModule,
],
providers: [
CleanOrphanedUserWorkspacesCommand,
CleanOrphanedRoleTargetsCommand,
MigrateTimelineActivityToMorphRelationsCommand,
],
exports: [
CleanOrphanedUserWorkspacesCommand,
CleanOrphanedRoleTargetsCommand,
MigrateTimelineActivityToMorphRelationsCommand,
],
})
export class V1_11_UpgradeVersionCommandModule {}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { RegenerateSearchVectorsCommand } from 'src/database/commands/upgrade-ve
import { SeedDashboardViewCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-seed-dashboard-view.command';
import { CleanOrphanedRoleTargetsCommand } from 'src/database/commands/upgrade-version-command/1-11/1-11-clean-orphaned-role-targets.command';
import { CleanOrphanedUserWorkspacesCommand } from 'src/database/commands/upgrade-version-command/1-11/1-11-clean-orphaned-user-workspaces.command';
import { MigrateTimelineActivityToMorphRelationsCommand } from 'src/database/commands/upgrade-version-command/1-11/1-11-migrate-timeline-activity-to-morph-relations.command';
import { FixLabelIdentifierPositionAndVisibilityCommand } from 'src/database/commands/upgrade-version-command/1-6/1-6-fix-label-identifier-position-and-visibility.command';
import { BackfillWorkflowManualTriggerAvailabilityCommand } from 'src/database/commands/upgrade-version-command/1-7/1-7-backfill-workflow-manual-trigger-availability.command';
import { DeduplicateUniqueFieldsCommand } from 'src/database/commands/upgrade-version-command/1-8/1-8-deduplicate-unique-fields.command';
Expand Down Expand Up @@ -75,6 +76,7 @@ export class UpgradeCommand extends UpgradeCommandRunner {
// 1.11 Commands
protected readonly cleanOrphanedUserWorkspacesCommand: CleanOrphanedUserWorkspacesCommand,
protected readonly cleanOrphanedRoleTargetsCommand: CleanOrphanedRoleTargetsCommand,
protected readonly migrateTimelineActivityToMorphRelationsCommand: MigrateTimelineActivityToMorphRelationsCommand,
) {
super(
workspaceRepository,
Expand Down Expand Up @@ -126,6 +128,7 @@ export class UpgradeCommand extends UpgradeCommandRunner {
const commands_1110: VersionCommands = {
beforeSyncMetadata: [],
afterSyncMetadata: [
this.migrateTimelineActivityToMorphRelationsCommand,
this.cleanOrphanedUserWorkspacesCommand,
this.cleanOrphanedRoleTargetsCommand,
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const baseNote = {
noteTargets: [],
attachments: [],
timelineActivities: [],
timelineActivities2: [],
favorites: [],
searchVector: '',
deletedAt: null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@ export enum FeatureFlagKey {
IS_EMAILING_DOMAIN_ENABLED = 'IS_EMAILING_DOMAIN_ENABLED',
IS_WORKFLOW_RUN_STOPPAGE_ENABLED = 'IS_WORKFLOW_RUN_STOPPAGE_ENABLED',
IS_DASHBOARD_V2_ENABLED = 'IS_DASHBOARD_V2_ENABLED',
IS_TIMELINE_ACTIVITY_MIGRATED = 'IS_TIMELINE_ACTIVITY_MIGRATED',
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,13 @@ import {
import { featureFlagValidator } from 'src/engine/core-modules/feature-flag/validates/feature-flag.validate';
import { publicFeatureFlagValidator } from 'src/engine/core-modules/feature-flag/validates/is-public-feature-flag.validate';
import { WorkspaceFeatureFlagsMapCacheService } from 'src/engine/metadata-modules/workspace-feature-flags-map-cache/workspace-feature-flags-map-cache.service';
import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service';

@Injectable()
export class FeatureFlagService {
constructor(
@InjectRepository(FeatureFlagEntity)
private readonly featureFlagRepository: Repository<FeatureFlagEntity>,
private readonly workspaceFeatureFlagsMapCacheService: WorkspaceFeatureFlagsMapCacheService,
private readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService,
) {}

public async isFeatureEnabled(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,24 @@ export class CustomWorkspaceEntity extends BaseWorkspaceEntity {
@WorkspaceIsSystem()
timelineActivities: TimelineActivityWorkspaceEntity[];

@WorkspaceRelation({
standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.timelineActivities2,
label: msg`Timeline Activities`,
type: RelationType.ONE_TO_MANY,
description: (objectMetadata) => {
const label = objectMetadata.labelSingular;

return msg`Timeline Activities tied to the ${label}`;
},
icon: 'IconIconTimelineEvent',
inverseSideTarget: () => TimelineActivityWorkspaceEntity,
inverseSideFieldKey: 'targetCustom',
onDelete: RelationOnDeleteAction.CASCADE,
})
@WorkspaceIsNullable()
@WorkspaceIsSystem()
timelineActivities2: TimelineActivityWorkspaceEntity[];

@WorkspaceField({
standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.searchVector,
type: FieldMetadataType.TS_VECTOR,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ describe('WorkspaceEntityManager', () => {
IS_EMAILING_DOMAIN_ENABLED: false,
IS_WORKFLOW_RUN_STOPPAGE_ENABLED: false,
IS_DASHBOARD_V2_ENABLED: false,
IS_TIMELINE_ACTIVITY_MIGRATED: false,
},
eventEmitterService: {
emitMutationEvent: jest.fn(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,11 @@ export const seedFeatureFlags = async (
workspaceId: workspaceId,
value: true,
},
{
key: FeatureFlagKey.IS_TIMELINE_ACTIVITY_MIGRATED,
workspaceId: workspaceId,
value: false,
},
])
.execute();
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ type TimelineActivitySeedData = Pick<
| 'noteId'
| 'taskId'
| 'opportunityId'
| 'targetNoteId'
| 'targetTaskId'
| 'targetPersonId'
| 'targetCompanyId'
| 'targetOpportunityId'
> & {
properties: string; // JSON stringified for raw insertion
createdAt: string; // ISO string for raw insertion
Expand Down Expand Up @@ -258,6 +263,11 @@ export class TimelineActivitySeederService {
'noteId',
'taskId',
'opportunityId',
'targetNoteId',
'targetTaskId',
'targetPersonId',
'targetCompanyId',
'targetOpportunityId',
'createdAt',
'updatedAt',
'happensAt',
Expand Down Expand Up @@ -295,6 +305,11 @@ export class TimelineActivitySeederService {
noteId: null,
taskId: null,
opportunityId: null,
targetNoteId: null,
targetTaskId: null,
targetPersonId: null,
targetCompanyId: null,
targetOpportunityId: null,
createdAt: creationDate,
updatedAt: creationDate,
happensAt: creationDate,
Expand Down Expand Up @@ -581,6 +596,11 @@ export class TimelineActivitySeederService {
noteId: null,
taskId: null,
opportunityId: null,
targetNoteId: null,
targetTaskId: null,
targetPersonId: null,
targetCompanyId: null,
targetOpportunityId: null,
createdAt: creationDate,
updatedAt: creationDate,
happensAt: creationDate,
Expand Down
Loading