Get ready for 1000+ members (#14313)

The goal of this PR is to test if Twenty can support a large number of
members, a question which was raised by a large company that is
considering moving away from Salesforce.

I was expecting the currentWorkspaceMembersState to cause a lot more
issue. It would be very hard to get rid of it in the context of actors,
I think that would require a big refactoring. I thought we'd have to do
it but it turns out the perf are pretty good. One thing we need to
improve is the pagination on the roles page, we'll wait for @Bonapara to
update that
This commit is contained in:
Félix Malfait
2025-09-05 12:37:22 +02:00
committed by GitHub
parent 094d670590
commit d5f88e566c
246 changed files with 5467 additions and 703 deletions
@@ -1,9 +1,12 @@
import { CALENDAR_EVENT_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/data/constants/calendar-event-data-seeds.constant';
import { PERSON_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/data/constants/person-data-seeds.constant';
import { WORKSPACE_MEMBER_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/data/constants/workspace-member-data-seeds.constant';
import {
WORKSPACE_MEMBER_DATA_SEED_IDS,
getWorkspaceMemberDataSeeds,
} from 'src/engine/workspace-manager/dev-seeder/data/constants/workspace-member-data-seeds.constant';
import { CalendarEventParticipantResponseStatus } from 'src/modules/calendar/common/standard-objects/calendar-event-participant.workspace-entity';
type CalendarEventParticipantDataSeed = {
export type CalendarEventParticipantDataSeed = {
id: string;
calendarEventId: string;
handle: string;
@@ -268,37 +271,43 @@ const CREATE_EVENT_PARTICIPANTS = (
return { participants: PARTICIPANTS, nextIndex: participantIndex };
};
const GENERATE_CALENDAR_EVENT_PARTICIPANT_SEEDS =
(): CalendarEventParticipantDataSeed[] => {
const PARTICIPANT_SEEDS: CalendarEventParticipantDataSeed[] = [];
let PARTICIPANT_INDEX = 1;
const GENERATE_CALENDAR_EVENT_PARTICIPANT_SEEDS = (
workspaceId: string,
): CalendarEventParticipantDataSeed[] => {
const PARTICIPANT_SEEDS: CalendarEventParticipantDataSeed[] = [];
let PARTICIPANT_INDEX = 1;
const EVENT_IDS = Object.keys(CALENDAR_EVENT_DATA_SEED_IDS).map(
(key) =>
CALENDAR_EVENT_DATA_SEED_IDS[
key as keyof typeof CALENDAR_EVENT_DATA_SEED_IDS
],
const EVENT_IDS = Object.keys(CALENDAR_EVENT_DATA_SEED_IDS).map(
(key) =>
CALENDAR_EVENT_DATA_SEED_IDS[
key as keyof typeof CALENDAR_EVENT_DATA_SEED_IDS
],
);
const PERSON_IDS = Object.keys(PERSON_DATA_SEED_IDS).map(
(key) => PERSON_DATA_SEED_IDS[key as keyof typeof PERSON_DATA_SEED_IDS],
);
const WORKSPACE_MEMBER_IDS = getWorkspaceMemberDataSeeds(workspaceId).map(
(member) => member.id,
);
for (const EVENT_ID of EVENT_IDS) {
const RESULT = CREATE_EVENT_PARTICIPANTS(
EVENT_ID,
PERSON_IDS,
WORKSPACE_MEMBER_IDS,
PARTICIPANT_INDEX,
);
const PERSON_IDS = Object.keys(PERSON_DATA_SEED_IDS).map(
(key) => PERSON_DATA_SEED_IDS[key as keyof typeof PERSON_DATA_SEED_IDS],
);
const WORKSPACE_MEMBER_IDS = Object.values(WORKSPACE_MEMBER_DATA_SEED_IDS);
PARTICIPANT_SEEDS.push(...RESULT.participants);
PARTICIPANT_INDEX = RESULT.nextIndex;
}
for (const EVENT_ID of EVENT_IDS) {
const RESULT = CREATE_EVENT_PARTICIPANTS(
EVENT_ID,
PERSON_IDS,
WORKSPACE_MEMBER_IDS,
PARTICIPANT_INDEX,
);
return PARTICIPANT_SEEDS;
};
PARTICIPANT_SEEDS.push(...RESULT.participants);
PARTICIPANT_INDEX = RESULT.nextIndex;
}
return PARTICIPANT_SEEDS;
};
export const CALENDAR_EVENT_PARTICIPANT_DATA_SEEDS =
GENERATE_CALENDAR_EVENT_PARTICIPANT_SEEDS();
export const getCalendarEventParticipantDataSeeds = (
workspaceId: string,
): CalendarEventParticipantDataSeed[] => {
return GENERATE_CALENDAR_EVENT_PARTICIPANT_SEEDS(workspaceId);
};
@@ -1,8 +1,11 @@
import { MESSAGE_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/data/constants/message-data-seeds.constant';
import { PERSON_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/data/constants/person-data-seeds.constant';
import { WORKSPACE_MEMBER_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/data/constants/workspace-member-data-seeds.constant';
import {
WORKSPACE_MEMBER_DATA_SEED_IDS,
getWorkspaceMemberDataSeeds,
} from 'src/engine/workspace-manager/dev-seeder/data/constants/workspace-member-data-seeds.constant';
type MessageParticipantDataSeed = {
export type MessageParticipantDataSeed = {
id: string;
createdAt: Date;
updatedAt: Date;
@@ -259,7 +262,9 @@ const CREATE_MESSAGE_PARTICIPANTS = (
return { participants: PARTICIPANTS, nextIndex: participantIndex };
};
const GENERATE_MESSAGE_PARTICIPANT_SEEDS = (): MessageParticipantDataSeed[] => {
const GENERATE_MESSAGE_PARTICIPANT_SEEDS = (
workspaceId: string,
): MessageParticipantDataSeed[] => {
const PARTICIPANT_SEEDS: MessageParticipantDataSeed[] = [];
let PARTICIPANT_INDEX = 1;
@@ -270,7 +275,9 @@ const GENERATE_MESSAGE_PARTICIPANT_SEEDS = (): MessageParticipantDataSeed[] => {
const PERSON_IDS = Object.keys(PERSON_DATA_SEED_IDS).map(
(key) => PERSON_DATA_SEED_IDS[key as keyof typeof PERSON_DATA_SEED_IDS],
);
const WORKSPACE_MEMBER_IDS = Object.values(WORKSPACE_MEMBER_DATA_SEED_IDS);
const WORKSPACE_MEMBER_IDS = getWorkspaceMemberDataSeeds(workspaceId).map(
(member) => member.id,
);
for (const MESSAGE_ID of MESSAGE_IDS) {
const RESULT = CREATE_MESSAGE_PARTICIPANTS(
@@ -287,5 +294,8 @@ const GENERATE_MESSAGE_PARTICIPANT_SEEDS = (): MessageParticipantDataSeed[] => {
return PARTICIPANT_SEEDS;
};
export const MESSAGE_PARTICIPANT_DATA_SEEDS =
GENERATE_MESSAGE_PARTICIPANT_SEEDS();
export const getMessageParticipantDataSeeds = (
workspaceId: string,
): MessageParticipantDataSeed[] => {
return GENERATE_MESSAGE_PARTICIPANT_SEEDS(workspaceId);
};
@@ -1,4 +1,9 @@
import { generateRandomUsers } from 'src/engine/workspace-manager/dev-seeder/core/utils/generate-random-users.util';
import { USER_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-users.util';
import {
SEED_APPLE_WORKSPACE_ID,
SEED_YCOMBINATOR_WORKSPACE_ID,
} from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-workspaces.util';
type WorkspaceMemberDataSeed = {
id: string;
@@ -28,7 +33,14 @@ export const WORKSPACE_MEMBER_DATA_SEED_IDS = {
JANE: '20202020-463f-435b-828c-107e007a2711',
};
export const WORKSPACE_MEMBER_DATA_SEEDS: WorkspaceMemberDataSeed[] = [
const {
workspaceMembers: randomWorkspaceMembers,
workspaceMemberIds: randomWorkspaceMemberIds,
} = generateRandomUsers();
export const RANDOM_WORKSPACE_MEMBER_IDS = randomWorkspaceMemberIds;
const originalWorkspaceMembers: WorkspaceMemberDataSeed[] = [
{
id: WORKSPACE_MEMBER_DATA_SEED_IDS.TIM,
nameFirstName: 'Tim',
@@ -66,3 +78,27 @@ export const WORKSPACE_MEMBER_DATA_SEEDS: WorkspaceMemberDataSeed[] = [
userId: USER_DATA_SEED_IDS.JANE,
},
];
export const WORKSPACE_MEMBER_DATA_SEEDS: WorkspaceMemberDataSeed[] = [
...originalWorkspaceMembers,
...randomWorkspaceMembers,
];
export const getWorkspaceMemberDataSeeds = (
workspaceId: string,
): WorkspaceMemberDataSeed[] => {
// In test environment, only return original members to avoid conflicts
if (process.env.NODE_ENV === 'test') {
return originalWorkspaceMembers;
}
if (workspaceId === SEED_APPLE_WORKSPACE_ID) {
// Apple workspace gets all workspace members (original + random)
return WORKSPACE_MEMBER_DATA_SEEDS;
} else if (workspaceId === SEED_YCOMBINATOR_WORKSPACE_ID) {
// YC workspace gets all 4 original workspace members
return originalWorkspaceMembers;
}
return originalWorkspaceMembers;
};
@@ -20,7 +20,7 @@ import {
} from 'src/engine/workspace-manager/dev-seeder/data/constants/calendar-event-data-seeds.constant';
import {
CALENDAR_EVENT_PARTICIPANT_DATA_SEED_COLUMNS,
CALENDAR_EVENT_PARTICIPANT_DATA_SEEDS,
getCalendarEventParticipantDataSeeds,
} from 'src/engine/workspace-manager/dev-seeder/data/constants/calendar-event-participant-data-seeds.constant';
import {
COMPANY_DATA_SEED_COLUMNS,
@@ -43,8 +43,8 @@ import {
MESSAGE_DATA_SEEDS,
} from 'src/engine/workspace-manager/dev-seeder/data/constants/message-data-seeds.constant';
import {
getMessageParticipantDataSeeds,
MESSAGE_PARTICIPANT_DATA_SEED_COLUMNS,
MESSAGE_PARTICIPANT_DATA_SEEDS,
} from 'src/engine/workspace-manager/dev-seeder/data/constants/message-participant-data-seeds.constant';
import {
MESSAGE_THREAD_DATA_SEED_COLUMNS,
@@ -83,17 +83,17 @@ import {
TASK_TARGET_DATA_SEEDS,
} from 'src/engine/workspace-manager/dev-seeder/data/constants/task-target-data-seeds.constant';
import {
getWorkspaceMemberDataSeeds,
WORKSPACE_MEMBER_DATA_SEED_COLUMNS,
WORKSPACE_MEMBER_DATA_SEEDS,
} from 'src/engine/workspace-manager/dev-seeder/data/constants/workspace-member-data-seeds.constant';
import { TimelineActivitySeederService } from 'src/engine/workspace-manager/dev-seeder/data/services/timeline-activity-seeder.service';
import { prefillWorkflows } from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-workflows';
const RECORD_SEEDS_CONFIGS = [
const getRecordSeedsConfigs = (workspaceId: string) => [
{
tableName: 'workspaceMember',
pgColumns: WORKSPACE_MEMBER_DATA_SEED_COLUMNS,
recordSeeds: WORKSPACE_MEMBER_DATA_SEEDS,
recordSeeds: getWorkspaceMemberDataSeeds(workspaceId),
},
{
tableName: 'company',
@@ -143,7 +143,7 @@ const RECORD_SEEDS_CONFIGS = [
{
tableName: 'calendarEventParticipant',
pgColumns: CALENDAR_EVENT_PARTICIPANT_DATA_SEED_COLUMNS,
recordSeeds: CALENDAR_EVENT_PARTICIPANT_DATA_SEEDS,
recordSeeds: getCalendarEventParticipantDataSeeds(workspaceId),
},
{
tableName: 'messageChannel',
@@ -168,7 +168,7 @@ const RECORD_SEEDS_CONFIGS = [
{
tableName: 'messageParticipant',
pgColumns: MESSAGE_PARTICIPANT_DATA_SEED_COLUMNS,
recordSeeds: MESSAGE_PARTICIPANT_DATA_SEEDS,
recordSeeds: getMessageParticipantDataSeeds(workspaceId),
},
{
tableName: '_pet',
@@ -213,7 +213,7 @@ export class DevSeederDataService {
await this.coreDataSource.transaction(
async (entityManager: WorkspaceEntityManager) => {
for (const recordSeedsConfig of RECORD_SEEDS_CONFIGS) {
for (const recordSeedsConfig of getRecordSeedsConfigs(workspaceId)) {
const objectMetadata = objectMetadataItems.find(
(item) =>
computeTableName(item.nameSingular, item.isCustom) ===
@@ -5,10 +5,16 @@ import chunk from 'lodash.chunk';
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
import { type WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager';
import { CALENDAR_EVENT_DATA_SEEDS } from 'src/engine/workspace-manager/dev-seeder/data/constants/calendar-event-data-seeds.constant';
import { CALENDAR_EVENT_PARTICIPANT_DATA_SEEDS } from 'src/engine/workspace-manager/dev-seeder/data/constants/calendar-event-participant-data-seeds.constant';
import {
CalendarEventParticipantDataSeed,
getCalendarEventParticipantDataSeeds,
} from 'src/engine/workspace-manager/dev-seeder/data/constants/calendar-event-participant-data-seeds.constant';
import { COMPANY_DATA_SEEDS } from 'src/engine/workspace-manager/dev-seeder/data/constants/company-data-seeds.constant';
import { MESSAGE_DATA_SEEDS } from 'src/engine/workspace-manager/dev-seeder/data/constants/message-data-seeds.constant';
import { MESSAGE_PARTICIPANT_DATA_SEEDS } from 'src/engine/workspace-manager/dev-seeder/data/constants/message-participant-data-seeds.constant';
import {
getMessageParticipantDataSeeds,
MessageParticipantDataSeed,
} from 'src/engine/workspace-manager/dev-seeder/data/constants/message-participant-data-seeds.constant';
import { NOTE_DATA_SEEDS } from 'src/engine/workspace-manager/dev-seeder/data/constants/note-data-seeds.constant';
import { NOTE_TARGET_DATA_SEEDS } from 'src/engine/workspace-manager/dev-seeder/data/constants/note-target-data-seeds.constant';
import { OPPORTUNITY_DATA_SEEDS } from 'src/engine/workspace-manager/dev-seeder/data/constants/opportunity-data-seeds.constant';
@@ -116,6 +122,10 @@ export class TimelineActivitySeederService {
schemaName: string;
workspaceId: string;
}) {
// Get workspace-specific participant data
const calendarEventParticipants =
getCalendarEventParticipantDataSeeds(workspaceId);
const messageParticipants = getMessageParticipantDataSeeds(workspaceId);
const timelineActivities: TimelineActivitySeedData[] = [];
const metadataIds = await this.getObjectMetadataIds(workspaceId);
let activityIndex = 0;
@@ -159,6 +169,8 @@ export class TimelineActivitySeederService {
index,
activityIndex,
linkedObjectMetadataId,
calendarEventParticipants,
messageParticipants,
});
timelineActivities.push(...linkedActivities);
@@ -179,6 +191,8 @@ export class TimelineActivitySeederService {
index,
activityIndex,
linkedObjectMetadataId,
calendarEventParticipants,
messageParticipants,
});
timelineActivities.push(...linkedActivities);
@@ -367,14 +381,23 @@ export class TimelineActivitySeederService {
index,
activityIndex,
linkedObjectMetadataId,
calendarEventParticipants,
messageParticipants,
}: {
activityType: 'note' | 'task' | 'calendarEvent' | 'message';
recordSeed: Record<string, unknown>;
index: number;
activityIndex: number;
linkedObjectMetadataId: string;
calendarEventParticipants: CalendarEventParticipantDataSeed[];
messageParticipants: MessageParticipantDataSeed[];
}): TimelineActivitySeedData[] {
const targetInfos = this.getActivityTargetInfos(activityType, recordSeed);
const targetInfos = this.getActivityTargetInfos(
activityType,
recordSeed,
calendarEventParticipants,
messageParticipants,
);
if (targetInfos.length === 0) {
return [];
@@ -395,12 +418,16 @@ export class TimelineActivitySeederService {
private getActivityTargetInfos(
activityType: 'note' | 'task' | 'calendarEvent' | 'message',
recordSeed: Record<string, unknown>,
calendarEventParticipants: CalendarEventParticipantDataSeed[],
messageParticipants: MessageParticipantDataSeed[],
): ActivityTargetInfo[] {
const targetGetters = {
note: () => this.getNoteTargetInfos(recordSeed),
task: () => this.getTaskTargetInfos(recordSeed),
calendarEvent: () => this.getCalendarEventTargetInfos(recordSeed),
message: () => this.getMessageTargetInfos(recordSeed),
calendarEvent: () =>
this.getCalendarEventTargetInfos(recordSeed, calendarEventParticipants),
message: () =>
this.getMessageTargetInfos(recordSeed, messageParticipants),
};
const getter = targetGetters[activityType];
@@ -462,8 +489,9 @@ export class TimelineActivitySeederService {
private getCalendarEventTargetInfos(
recordSeed: Record<string, unknown>,
calendarEventParticipants: CalendarEventParticipantDataSeed[],
): ActivityTargetInfo[] {
const eventParticipants = CALENDAR_EVENT_PARTICIPANT_DATA_SEEDS.filter(
const eventParticipants = calendarEventParticipants.filter(
(participant) => participant.calendarEventId === recordSeed.id,
);
@@ -473,7 +501,7 @@ export class TimelineActivitySeederService {
if (participant.personId) {
targetInfos.push({
targetType: 'person',
targetId: participant.personId as string,
targetId: participant.personId,
});
const person = PERSON_DATA_SEEDS.find(
@@ -494,14 +522,15 @@ export class TimelineActivitySeederService {
private getMessageTargetInfos(
recordSeed: Record<string, unknown>,
messageParticipants: MessageParticipantDataSeed[],
): ActivityTargetInfo[] {
const messageParticipants = MESSAGE_PARTICIPANT_DATA_SEEDS.filter(
const filteredMessageParticipants = messageParticipants.filter(
(participant) => participant.messageId === recordSeed.id,
);
const targetInfos: ActivityTargetInfo[] = [];
messageParticipants.forEach((participant) => {
filteredMessageParticipants.forEach((participant) => {
if (participant.personId) {
targetInfos.push({
targetType: 'person',
@@ -515,7 +544,7 @@ export class TimelineActivitySeederService {
if (person?.companyId) {
targetInfos.push({
targetType: 'company',
targetId: person.companyId as string,
targetId: person.companyId,
});
}
}