[Fix] fix timelineActivities on notes and tasks (#17814)

[Fixes
sentry](https://twenty-v7.sentry.io/issues/7238708876/?environment=prod&environment=prod-eu&project=4507072499810304&query=Field%20metadata%20for%20field&referrer=issue-stream&sort=date):
_Field metadata for field "targetTargetPersonId" is missing in object
metadata timelineActivity_

Regression caused by migration of note and task targets - fields were
renamed from personId to targetPersonId, impacting the event names,
while we still expected them in their previous shape.

---------

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Félix Malfait <FelixMalfait@users.noreply.github.com>
This commit is contained in:
Marie
2026-02-09 21:47:43 +01:00
committed by GitHub
parent 05eca08ad2
commit 11fd1a41f3
14 changed files with 66 additions and 79 deletions
@@ -1503,7 +1503,6 @@ export enum FeatureFlagKey {
IS_RECORD_PAGE_LAYOUT_ENABLED = 'IS_RECORD_PAGE_LAYOUT_ENABLED',
IS_SSE_DB_EVENTS_ENABLED = 'IS_SSE_DB_EVENTS_ENABLED',
IS_TASK_TARGET_MIGRATED = 'IS_TASK_TARGET_MIGRATED',
IS_TIMELINE_ACTIVITY_MIGRATED = 'IS_TIMELINE_ACTIVITY_MIGRATED',
IS_UNIQUE_INDEXES_ENABLED = 'IS_UNIQUE_INDEXES_ENABLED'
}
@@ -1475,7 +1475,6 @@ export enum FeatureFlagKey {
IS_RECORD_PAGE_LAYOUT_ENABLED = 'IS_RECORD_PAGE_LAYOUT_ENABLED',
IS_SSE_DB_EVENTS_ENABLED = 'IS_SSE_DB_EVENTS_ENABLED',
IS_TASK_TARGET_MIGRATED = 'IS_TASK_TARGET_MIGRATED',
IS_TIMELINE_ACTIVITY_MIGRATED = 'IS_TIMELINE_ACTIVITY_MIGRATED',
IS_UNIQUE_INDEXES_ENABLED = 'IS_UNIQUE_INDEXES_ENABLED'
}
@@ -1,28 +1,17 @@
import { useLinkedObjectsTitle } from '@/activities/timeline-activities/hooks/useLinkedObjectsTitle';
import { type TimelineActivity } from '@/activities/timeline-activities/types/TimelineActivity';
import { type ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getActivityTargetObjectFieldIdName';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
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 { 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 isTimelineActivityMigrated = useIsFeatureEnabled(
FeatureFlagKey.IS_TIMELINE_ACTIVITY_MIGRATED,
);
const targetableObjectFieldIdName = isTimelineActivityMigrated
? `target${capitalize(targetableObject.targetObjectNameSingular)}Id`
: getActivityTargetObjectFieldIdName({
nameSingular: targetableObject.targetObjectNameSingular,
});
const targetableObjectFieldIdName = `target${capitalize(targetableObject.targetObjectNameSingular)}Id`;
const { objectMetadataItem: timelineActivityMetadata } =
useObjectMetadataItem({
@@ -10,7 +10,6 @@ export enum FeatureFlagKey {
IS_PUBLIC_DOMAIN_ENABLED = 'IS_PUBLIC_DOMAIN_ENABLED',
IS_EMAILING_DOMAIN_ENABLED = 'IS_EMAILING_DOMAIN_ENABLED',
IS_DASHBOARD_V2_ENABLED = 'IS_DASHBOARD_V2_ENABLED',
IS_TIMELINE_ACTIVITY_MIGRATED = 'IS_TIMELINE_ACTIVITY_MIGRATED',
IS_ATTACHMENT_MIGRATED = 'IS_ATTACHMENT_MIGRATED',
IS_NOTE_TARGET_MIGRATED = 'IS_NOTE_TARGET_MIGRATED',
IS_TASK_TARGET_MIGRATED = 'IS_TASK_TARGET_MIGRATED',
@@ -90,10 +90,7 @@ export const buildDefaultRelationFlatFieldMetadatasForCustomObject = ({
objectMetadataNameSingular,
);
const isFeatureFlagEnabled =
(objectMetadataNameSingular === 'timelineActivity' &&
existingFeatureFlagsMap[
FeatureFlagKey.IS_TIMELINE_ACTIVITY_MIGRATED
]) ||
objectMetadataNameSingular === 'timelineActivity' ||
(objectMetadataNameSingular === 'attachment' &&
existingFeatureFlagsMap[FeatureFlagKey.IS_ATTACHMENT_MIGRATED]) ||
(objectMetadataNameSingular === 'noteTarget' &&
@@ -56,11 +56,6 @@ export const seedFeatureFlags = async ({
workspaceId: workspaceId,
value: true,
},
{
key: FeatureFlagKey.IS_TIMELINE_ACTIVITY_MIGRATED,
workspaceId: workspaceId,
value: true,
},
{
key: FeatureFlagKey.IS_ATTACHMENT_MIGRATED,
workspaceId: workspaceId,
@@ -1,7 +1,6 @@
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
export const DEFAULT_FEATURE_FLAGS = [
FeatureFlagKey.IS_TIMELINE_ACTIVITY_MIGRATED,
FeatureFlagKey.IS_ATTACHMENT_MIGRATED,
FeatureFlagKey.IS_NOTE_TARGET_MIGRATED,
FeatureFlagKey.IS_TASK_TARGET_MIGRATED,
@@ -5,7 +5,6 @@ import { isDefined } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
import { OnCustomBatchEvent } from 'src/engine/api/graphql/graphql-query-runner/decorators/on-custom-batch-event.decorator';
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 { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
@@ -75,17 +74,10 @@ export class CalendarEventParticipantListener {
.filter(isDefined);
});
const isFeatureFlagTimelineActivityMigrated =
await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IS_TIMELINE_ACTIVITY_MIGRATED,
batchEvent.workspaceId,
);
await this.timelineActivityRepository.upsertTimelineActivities({
objectSingularName: 'person',
workspaceId: batchEvent.workspaceId,
payloads: timelineActivityPayloads.filter(isDefined),
isFeatureFlagTimelineActivityMigrated,
});
}
}
@@ -5,7 +5,6 @@ import { isDefined } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
import { OnCustomBatchEvent } from 'src/engine/api/graphql/graphql-query-runner/decorators/on-custom-batch-event.decorator';
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 { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
@@ -74,17 +73,10 @@ export class MessageParticipantListener {
.filter(isDefined);
});
const isFeatureFlagTimelineActivityMigrated =
await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IS_TIMELINE_ACTIVITY_MIGRATED,
batchEvent.workspaceId,
);
await this.timelineActivityRepository.upsertTimelineActivities({
objectSingularName: 'person',
workspaceId: batchEvent.workspaceId,
payloads: timelineActivityPayloads.filter(isDefined),
isFeatureFlagTimelineActivityMigrated,
});
}
}
@@ -16,7 +16,6 @@ type TimelineActivityPayloadWorkspaceIdAndObjectSingularName = {
})[];
workspaceId: string;
objectSingularName: string;
isFeatureFlagTimelineActivityMigrated: boolean;
};
@Injectable()
@@ -29,7 +28,6 @@ export class TimelineActivityRepository {
objectSingularName,
workspaceId,
payloads,
isFeatureFlagTimelineActivityMigrated,
}: TimelineActivityPayloadWorkspaceIdAndObjectSingularName) {
const authContext = buildSystemAuthContext(workspaceId);
@@ -38,7 +36,6 @@ export class TimelineActivityRepository {
objectSingularName,
workspaceId,
payloads,
isFeatureFlagTimelineActivityMigrated,
});
const payloadsWithDiff = payloads
@@ -61,10 +58,7 @@ export class TimelineActivityRepository {
[];
const timelineActivityPropertyName =
await this.getTimelineActivityPropertyName(
objectSingularName,
isFeatureFlagTimelineActivityMigrated,
);
await this.getTimelineActivityPropertyName(objectSingularName);
for (const payload of payloadsWithDiff) {
const recentTimelineActivity = recentTimelineActivities.find(
@@ -98,7 +92,6 @@ export class TimelineActivityRepository {
objectSingularName,
payloads: payloadsToInsert,
workspaceId,
isFeatureFlagTimelineActivityMigrated,
});
}, authContext);
}
@@ -107,7 +100,6 @@ export class TimelineActivityRepository {
objectSingularName,
workspaceId,
payloads,
isFeatureFlagTimelineActivityMigrated,
}: TimelineActivityPayloadWorkspaceIdAndObjectSingularName) {
const timelineActivityTypeORMRepository =
await this.globalWorkspaceOrmManager.getRepository(
@@ -121,10 +113,7 @@ export class TimelineActivityRepository {
const tenMinutesAgo = new Date(Date.now() - 10 * 60 * 1000);
const timelineActivityPropertyName =
await this.getTimelineActivityPropertyName(
objectSingularName,
isFeatureFlagTimelineActivityMigrated,
);
await this.getTimelineActivityPropertyName(objectSingularName);
const whereConditions: Record<string, unknown> = {
[timelineActivityPropertyName]: In(
@@ -148,7 +137,6 @@ export class TimelineActivityRepository {
objectSingularName,
workspaceId,
payloads,
isFeatureFlagTimelineActivityMigrated,
}: TimelineActivityPayloadWorkspaceIdAndObjectSingularName) {
if (payloads.length === 0) {
return;
@@ -164,10 +152,7 @@ export class TimelineActivityRepository {
);
const timelineActivityPropertyName =
await this.getTimelineActivityPropertyName(
objectSingularName,
isFeatureFlagTimelineActivityMigrated,
);
await this.getTimelineActivityPropertyName(objectSingularName);
return timelineActivityTypeORMRepository.insert(
payloads.map((payload) => ({
@@ -208,12 +193,7 @@ export class TimelineActivityRepository {
});
}
private async getTimelineActivityPropertyName(
objectSingularName: string,
isFeatureFlagTimelineActivityMigrated: boolean,
) {
return isFeatureFlagTimelineActivityMigrated
? `${buildTimelineActivityRelatedMorphFieldMetadataName(objectSingularName)}Id`
: `${objectSingularName}Id`;
private async getTimelineActivityPropertyName(objectSingularName: string) {
return `${buildTimelineActivityRelatedMorphFieldMetadataName(objectSingularName)}Id`;
}
}
@@ -1,12 +1,11 @@
import { Injectable } from '@nestjs/common';
import { type ObjectRecordBaseEvent } from 'twenty-shared/database-events';
import { type ObjectRecord } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { In } from 'typeorm';
import { type ObjectRecordBaseEvent } from 'twenty-shared/database-events';
import { getFlatFieldsFromFlatObjectMetadata } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-flat-fields-for-flat-object-metadata.util';
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 { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
@@ -19,6 +18,7 @@ import { TaskWorkspaceEntity } from 'src/modules/task/standard-objects/task.work
import { TimelineActivityRepository } from 'src/modules/timeline/repositories/timeline-activity.repository';
import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-objects/timeline-activity.workspace-entity';
import { type TimelineActivityPayload } from 'src/modules/timeline/types/timeline-activity-payload';
import { extractObjectSingularNameFromTargetColumnName } from 'src/modules/timeline/utils/extract-object-singular-name-from-target-column-name.util';
type ActivityType = 'note' | 'task';
@@ -67,7 +67,7 @@ export class TimelineActivityService {
const payloadsByObjectSingularName = timelineActivitiesPayloads.reduce(
(acc, payload) => {
const computedObjectSingularName =
payload.overrideObjectSingularName ?? objectSingularName;
payload.objectSingularName ?? objectSingularName;
acc[computedObjectSingularName] = [
...(acc[computedObjectSingularName] || []),
@@ -79,18 +79,11 @@ export class TimelineActivityService {
{} as Record<string, TimelineActivityPayload[]>,
);
const isFeatureFlagTimelineActivityMigrated =
await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IS_TIMELINE_ACTIVITY_MIGRATED,
workspaceId,
);
for (const objectSingularName in payloadsByObjectSingularName) {
await this.timelineActivityRepository.upsertTimelineActivities({
objectSingularName,
workspaceId,
payloads: payloadsByObjectSingularName[objectSingularName],
isFeatureFlagTimelineActivityMigrated,
});
}
}
@@ -255,7 +248,7 @@ export class TimelineActivityService {
linkedRecordId: activityId,
linkedObjectMetadataId: objectMetadata.id,
properties: event.properties,
overrideObjectSingularName: objectMetadata.nameSingular,
objectSingularName: objectMetadata.nameSingular,
} satisfies TimelineActivityPayload;
});
})
@@ -361,9 +354,12 @@ export class TimelineActivityService {
targetColumnName
];
const objectSingularName =
extractObjectSingularNameFromTargetColumnName(targetColumnName);
return {
name: `linked-${activityType}.${action}`,
overrideObjectSingularName: targetColumnName.replace(/Id$/, ''),
objectSingularName,
recordId,
linkedRecordCachedName: activity.title,
linkedRecordId: activity.id,
@@ -8,5 +8,5 @@ export type TimelineActivityPayload = {
workspaceMemberId?: string;
name: string;
recordId: string;
overrideObjectSingularName?: string;
objectSingularName?: string;
};
@@ -0,0 +1,27 @@
import { extractObjectSingularNameFromTargetColumnName } from 'src/modules/timeline/utils/extract-object-singular-name-from-target-column-name.util';
describe('extractObjectSingularNameFromTargetColumnName', () => {
it('should extract "person" from "targetPersonId"', () => {
expect(
extractObjectSingularNameFromTargetColumnName('targetPersonId'),
).toBe('person');
});
it('should extract "arrowTarget" from "targetArrowTargetId"', () => {
expect(
extractObjectSingularNameFromTargetColumnName('targetArrowTargetId'),
).toBe('arrowTarget');
});
it('should extract "idCard" from "targetIdCardId"', () => {
expect(
extractObjectSingularNameFromTargetColumnName('targetIdCardId'),
).toBe('idCard');
});
it('should extract "cardId" from "targetCardIdId"', () => {
expect(
extractObjectSingularNameFromTargetColumnName('targetCardIdId'),
).toBe('cardId');
});
});
@@ -0,0 +1,23 @@
const ID_SUFFIX = 'Id';
const TARGET_PREFIX = 'target';
export const extractObjectSingularNameFromTargetColumnName = (
targetColumnName: string,
): string => {
let result = targetColumnName;
if (result.endsWith(ID_SUFFIX)) {
result = result.slice(0, -ID_SUFFIX.length);
}
if (
result.startsWith(TARGET_PREFIX) &&
result.length > TARGET_PREFIX.length
) {
result =
result.charAt(TARGET_PREFIX.length).toLowerCase() +
result.slice(TARGET_PREFIX.length + 1);
}
return result;
};