Improve seeds 3 (#12740)

- Fix an issue where custom object were seeded with 2 views, and with
the wrong icon
- ACME becomes YCombinator
- Allow 2 workspaces to have different metadata seeded
- Add many seeds for messages
- Add many seeds for calendar events
- Randomize createdBy for person and companies

---------

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
This commit is contained in:
Félix Malfait
2025-06-20 07:05:44 +02:00
committed by GitHub
parent 79e5ccfd37
commit cb6a76fd43
25 changed files with 4511 additions and 3356 deletions
@@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common';
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
import { WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager';
import { computeTableName } from 'src/engine/utils/compute-table-name.util';
import { shouldSeedWorkspaceFavorite } from 'src/engine/utils/should-seed-workspace-favorite';
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
import {
@@ -244,6 +245,16 @@ export class DevSeederDataService {
await mainDataSource.transaction(
async (entityManager: WorkspaceEntityManager) => {
for (const recordSeedsConfig of RECORD_SEEDS_CONFIGS) {
const objectMetadata = objectMetadataItems.find(
(item) =>
computeTableName(item.nameSingular, item.isCustom) ===
recordSeedsConfig.tableName,
);
if (!objectMetadata) {
continue;
}
await this.seedRecords({
entityManager,
schemaName,
@@ -259,10 +270,14 @@ export class DevSeederDataService {
workspaceId,
});
// For now views/favorites are auto-created for custom
// objects but not for standard objects.
// This is probably something we want to fix in the future.
const viewDefinitionsWithId = await prefillViews(
entityManager,
schemaName,
objectMetadataItems,
objectMetadataItems.filter((item) => !item.isCustom),
);
await prefillWorkspaceFavorites(
@@ -4,7 +4,11 @@ import chunk from 'lodash.chunk';
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
import { 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 { 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 { 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';
@@ -14,7 +18,6 @@ import { TASK_TARGET_DATA_SEEDS } from 'src/engine/workspace-manager/dev-seeder/
import { WORKSPACE_MEMBER_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/data/constants/workspace-member-data-seeds.constant';
import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-objects/timeline-activity.workspace-entity';
// Seeding-specific type based on TimelineActivityWorkspaceEntity for raw data insertion
type TimelineActivitySeedData = Pick<
TimelineActivityWorkspaceEntity,
| 'id'
@@ -47,7 +50,7 @@ type CreateTimelineActivityParams = {
};
type CreateLinkedActivityParams = {
activityType: 'note' | 'task';
activityType: 'note' | 'task' | 'calendarEvent' | 'message';
recordSeed: Record<string, unknown>;
index: number;
activityIndex: number;
@@ -55,10 +58,55 @@ type CreateLinkedActivityParams = {
targetInfo: ActivityTargetInfo;
};
type EntityConfig = {
type: string;
seeds: Array<Record<string, unknown>>;
};
type ObjectMetadataIds = {
noteMetadataId: string;
taskMetadataId: string;
calendarEventMetadataId: string;
messageMetadataId: string;
};
@Injectable()
export class TimelineActivitySeederService {
private readonly ENTITY_CODES = {
company: '0001',
person: '0201',
note: '0601',
task: '0651',
opportunity: '0851',
calendarEvent: '0701',
message: '0751',
} as const;
private readonly TARGET_CODES = {
person: '1001',
company: '2001',
opportunity: '3001',
} as const;
private readonly LINKABLE_TYPES = new Set([
'note',
'task',
'calendarEvent',
'message',
]);
constructor(private readonly objectMetadataService: ObjectMetadataService) {}
private getLinkedActivityName(activityType: string): string {
// Notes and tasks use the legacy format: linked-{type}.created
if (activityType === 'note' || activityType === 'task') {
return `linked-${activityType}.created`;
}
// Calendar events and messages use the new format: {type}.linked
return `${activityType}.linked`;
}
async seedTimelineActivities({
entityManager,
schemaName,
@@ -69,13 +117,10 @@ export class TimelineActivitySeederService {
workspaceId: string;
}) {
const timelineActivities: TimelineActivitySeedData[] = [];
const { noteMetadataId, taskMetadataId } =
await this.getObjectMetadataIds(workspaceId);
const metadataIds = await this.getObjectMetadataIds(workspaceId);
let activityIndex = 0;
const entityConfigs = [
const entityConfigs: EntityConfig[] = [
{ type: 'company', seeds: COMPANY_DATA_SEEDS },
{ type: 'person', seeds: PERSON_DATA_SEEDS },
{ type: 'note', seeds: NOTE_DATA_SEEDS },
@@ -83,6 +128,12 @@ export class TimelineActivitySeederService {
{ type: 'opportunity', seeds: OPPORTUNITY_DATA_SEEDS },
];
// Calendar events and messages only appear as linked activities
const linkableConfigs: EntityConfig[] = [
{ type: 'calendarEvent', seeds: CALENDAR_EVENT_DATA_SEEDS },
{ type: 'message', seeds: MESSAGE_DATA_SEEDS },
];
entityConfigs.forEach(({ type, seeds }) => {
seeds.forEach((seed, index) => {
const activity = this.createTimelineActivity({
@@ -94,55 +145,106 @@ export class TimelineActivitySeederService {
timelineActivities.push(activity);
activityIndex++;
// For notes and tasks, create additional linked timeline activities on target objects
if (type === 'note' || type === 'task') {
const linkedObjectMetadataId =
type === 'note' ? noteMetadataId : taskMetadataId;
const linkedActivities = this.computeLinkedTimelineActivityRecords({
activityType: type,
recordSeed: seed,
index,
activityIndex,
linkedObjectMetadataId,
});
timelineActivities.push(...linkedActivities);
activityIndex += linkedActivities.length;
if (!this.LINKABLE_TYPES.has(type)) {
return;
}
const linkedObjectMetadataId = this.getLinkedObjectMetadataId(
type,
metadataIds,
);
const linkedActivities = this.computeLinkedTimelineActivityRecords({
activityType: type as 'note' | 'task' | 'calendarEvent' | 'message',
recordSeed: seed,
index,
activityIndex,
linkedObjectMetadataId,
});
timelineActivities.push(...linkedActivities);
activityIndex += linkedActivities.length;
});
});
if (timelineActivities.length > 0) {
const batchSize = 100;
const timelineActivityBatches = chunk(timelineActivities, batchSize);
// Create only linked activities for calendar events and messages
linkableConfigs.forEach(({ type, seeds }) => {
seeds.forEach((seed, index) => {
const linkedObjectMetadataId = this.getLinkedObjectMetadataId(
type,
metadataIds,
);
const linkedActivities = this.computeLinkedTimelineActivityRecords({
activityType: type as 'calendarEvent' | 'message',
recordSeed: seed,
index,
activityIndex,
linkedObjectMetadataId,
});
for (const batch of timelineActivityBatches) {
await entityManager
.createQueryBuilder(undefined, undefined, undefined, {
shouldBypassPermissionChecks: true,
})
.insert()
.into(`${schemaName}.timelineActivity`, [
'id',
'name',
'properties',
'linkedRecordCachedName',
'linkedRecordId',
'linkedObjectMetadataId',
'workspaceMemberId',
'companyId',
'personId',
'noteId',
'taskId',
'opportunityId',
'createdAt',
'updatedAt',
'happensAt',
])
.orIgnore()
.values(batch)
.execute();
}
timelineActivities.push(...linkedActivities);
activityIndex += linkedActivities.length;
});
});
await this.insertTimelineActivities(
entityManager,
schemaName,
timelineActivities,
);
}
private getLinkedObjectMetadataId(
type: string,
metadataIds: ObjectMetadataIds,
): string {
const metadataMap = {
note: metadataIds.noteMetadataId,
task: metadataIds.taskMetadataId,
calendarEvent: metadataIds.calendarEventMetadataId,
message: metadataIds.messageMetadataId,
};
return metadataMap[type as keyof typeof metadataMap] || '';
}
private async insertTimelineActivities(
entityManager: WorkspaceEntityManager,
schemaName: string,
timelineActivities: TimelineActivitySeedData[],
) {
if (timelineActivities.length === 0) {
return;
}
const batchSize = 100;
const timelineActivityBatches = chunk(timelineActivities, batchSize);
for (const batch of timelineActivityBatches) {
await entityManager
.createQueryBuilder(undefined, undefined, undefined, {
shouldBypassPermissionChecks: true,
})
.insert()
.into(`${schemaName}.timelineActivity`, [
'id',
'name',
'properties',
'linkedRecordCachedName',
'linkedRecordId',
'linkedObjectMetadataId',
'workspaceMemberId',
'companyId',
'personId',
'noteId',
'taskId',
'opportunityId',
'createdAt',
'updatedAt',
'happensAt',
])
.orIgnore()
.values(batch)
.execute();
}
}
@@ -151,34 +253,21 @@ export class TimelineActivitySeederService {
recordSeed,
index,
}: CreateTimelineActivityParams): TimelineActivitySeedData {
const generateTimelineActivityId = (type: string, idx: number): string => {
const prefix = '20202020';
const entityCodes: Record<string, string> = {
company: '0001',
person: '0201',
note: '0601',
task: '0651',
opportunity: '0851',
};
const code = entityCodes[type] || '0000';
const paddedIndex = String(idx).padStart(4, '0');
return `${prefix}-${code}-4000-8001-${paddedIndex}00000001`;
};
const timelineActivityId = this.generateTimelineActivityId(
entityType,
index + 1,
);
const creationDate = new Date().toISOString();
const recordId = String(recordSeed.id || '');
const timelineActivity: TimelineActivitySeedData = {
id: generateTimelineActivityId(entityType, index + 1),
id: timelineActivityId,
name: `${entityType}.created`,
properties: JSON.stringify({
after: this.getEventAfterRecordProperties({
type: entityType,
recordSeed,
}),
after: this.getEventAfterRecordProperties(entityType, recordSeed),
}),
linkedRecordCachedName: '',
linkedRecordId: recordSeed.id as string,
linkedRecordId: recordId,
linkedObjectMetadataId: null,
workspaceMemberId: WORKSPACE_MEMBER_DATA_SEED_IDS.TIM,
companyId: null,
@@ -191,68 +280,85 @@ export class TimelineActivitySeederService {
happensAt: creationDate,
};
// Set the appropriate entity ID
const entityIdKey = `${entityType}Id`;
// @ts-expect-error - This is okay for morph
// alternative is to be explicit but that makes
timelineActivity[`${entityType}Id`] = recordSeed.id;
timelineActivity[entityIdKey] = recordId;
return timelineActivity;
}
private getEventAfterRecordProperties({
type,
recordSeed,
}: {
type: string;
recordSeed: Record<string, unknown>;
}): Record<string, unknown> {
const commonProperties = {
id: recordSeed.id,
private generateTimelineActivityId(type: string, index: number): string {
const prefix = '20202020';
const code =
this.ENTITY_CODES[type as keyof typeof this.ENTITY_CODES] || '0000';
const paddedIndex = String(index).padStart(4, '0');
return `${prefix}-${code}-4000-8001-${paddedIndex}00000001`;
}
private getEventAfterRecordProperties(
type: string,
recordSeed: Record<string, unknown>,
): Record<string, unknown> {
const commonProperties = { id: recordSeed.id };
const propertyGetters = {
company: () => ({
...commonProperties,
name: recordSeed.name,
domainName: recordSeed.domainNamePrimaryLinkUrl,
employees: recordSeed.employees,
city: recordSeed.addressAddressCity,
}),
person: () => ({
...commonProperties,
name: {
firstName: recordSeed.nameFirstName,
lastName: recordSeed.nameLastName,
},
email: recordSeed.emailsPrimaryEmail,
jobTitle: recordSeed.jobTitle,
}),
note: () => ({
...commonProperties,
title: recordSeed.title,
body: recordSeed.body,
}),
task: () => ({
...commonProperties,
title: recordSeed.title,
body: recordSeed.body,
status: recordSeed.status,
dueAt: recordSeed.dueAt,
}),
opportunity: () => ({
...commonProperties,
name: recordSeed.name,
amount: recordSeed.amountAmountMicros,
stage: recordSeed.stage,
closeDate: recordSeed.closeDate,
}),
calendarEvent: () => ({
...commonProperties,
title: recordSeed.title,
description: recordSeed.description,
startsAt: recordSeed.startsAt,
endsAt: recordSeed.endsAt,
location: recordSeed.location,
}),
message: () => ({
...commonProperties,
subject: recordSeed.subject,
text: recordSeed.text,
receivedAt: recordSeed.receivedAt,
}),
};
switch (type) {
case 'company':
return {
...commonProperties,
name: recordSeed.name,
domainName: recordSeed.domainNamePrimaryLinkUrl,
employees: recordSeed.employees,
city: recordSeed.addressAddressCity,
};
case 'person':
return {
...commonProperties,
name: {
firstName: recordSeed.nameFirstName,
lastName: recordSeed.nameLastName,
},
email: recordSeed.emailsPrimaryEmail,
jobTitle: recordSeed.jobTitle,
};
case 'note':
return {
...commonProperties,
title: recordSeed.title,
body: recordSeed.body,
};
case 'task':
return {
...commonProperties,
title: recordSeed.title,
body: recordSeed.body,
status: recordSeed.status,
dueAt: recordSeed.dueAt,
};
case 'opportunity':
return {
...commonProperties,
name: recordSeed.name,
amount: recordSeed.amountAmountMicros,
stage: recordSeed.stage,
closeDate: recordSeed.closeDate,
};
default:
return commonProperties;
}
const getter = propertyGetters[type as keyof typeof propertyGetters];
return getter ? getter() : commonProperties;
}
private computeLinkedTimelineActivityRecords({
@@ -262,103 +368,160 @@ export class TimelineActivitySeederService {
activityIndex,
linkedObjectMetadataId,
}: {
activityType: 'note' | 'task';
activityType: 'note' | 'task' | 'calendarEvent' | 'message';
recordSeed: Record<string, unknown>;
index: number;
activityIndex: number;
linkedObjectMetadataId: string;
}): TimelineActivitySeedData[] {
const targetInfo = this.getActivityTargetInfo({
activityType,
recordSeed,
});
const targetInfos = this.getActivityTargetInfos(activityType, recordSeed);
if (!targetInfo) {
if (targetInfos.length === 0) {
return [];
}
const linkedActivity = this.computeLinkedActivityRecord({
activityType,
recordSeed,
index,
activityIndex,
linkedObjectMetadataId,
targetInfo,
});
return [linkedActivity];
return targetInfos.map((targetInfo, targetIndex) =>
this.computeLinkedActivityRecord({
activityType,
recordSeed,
index,
activityIndex: activityIndex + targetIndex,
linkedObjectMetadataId,
targetInfo,
}),
);
}
private getActivityTargetInfo({
activityType,
recordSeed,
}: {
activityType: 'note' | 'task';
recordSeed: Record<string, unknown>;
}): ActivityTargetInfo | null {
if (activityType === 'note') {
const noteTargetSeed = NOTE_TARGET_DATA_SEEDS.find(
(target) => target.noteId === recordSeed.id,
);
private getActivityTargetInfos(
activityType: 'note' | 'task' | 'calendarEvent' | 'message',
recordSeed: Record<string, unknown>,
): ActivityTargetInfo[] {
const targetGetters = {
note: () => this.getNoteTargetInfos(recordSeed),
task: () => this.getTaskTargetInfos(recordSeed),
calendarEvent: () => this.getCalendarEventTargetInfos(recordSeed),
message: () => this.getMessageTargetInfos(recordSeed),
};
if (!noteTargetSeed) {
return null;
}
const getter = targetGetters[activityType];
if (noteTargetSeed.personId) {
return {
targetType: 'person',
targetId: noteTargetSeed.personId,
};
}
return getter ? getter() : [];
}
if (noteTargetSeed.companyId) {
return {
targetType: 'company',
targetId: noteTargetSeed.companyId,
};
}
private getNoteTargetInfos(
recordSeed: Record<string, unknown>,
): ActivityTargetInfo[] {
const noteTargetSeed = NOTE_TARGET_DATA_SEEDS.find(
(target) => target.noteId === recordSeed.id,
);
if (noteTargetSeed.opportunityId) {
return {
targetType: 'opportunity',
targetId: noteTargetSeed.opportunityId,
};
if (!noteTargetSeed) {
return [];
}
const targetChecks = [
{ id: noteTargetSeed.personId, type: 'person' },
{ id: noteTargetSeed.companyId, type: 'company' },
{ id: noteTargetSeed.opportunityId, type: 'opportunity' },
];
for (const { id, type } of targetChecks) {
if (id) {
return [{ targetType: type, targetId: id }];
}
}
if (activityType === 'task') {
const taskTargetSeed = TASK_TARGET_DATA_SEEDS.find(
(target) => target.taskId === recordSeed.id,
);
return [];
}
if (!taskTargetSeed) {
return null;
}
private getTaskTargetInfos(
recordSeed: Record<string, unknown>,
): ActivityTargetInfo[] {
const taskTargetSeed = TASK_TARGET_DATA_SEEDS.find(
(target) => target.taskId === recordSeed.id,
);
if (taskTargetSeed.personId) {
return {
targetType: 'person',
targetId: taskTargetSeed.personId,
};
}
if (!taskTargetSeed) {
return [];
}
if (taskTargetSeed.companyId) {
return {
targetType: 'company',
targetId: taskTargetSeed.companyId,
};
}
const targetChecks = [
{ id: taskTargetSeed.personId, type: 'person' },
{ id: taskTargetSeed.companyId, type: 'company' },
{ id: taskTargetSeed.opportunityId, type: 'opportunity' },
];
if (taskTargetSeed.opportunityId) {
return {
targetType: 'opportunity',
targetId: taskTargetSeed.opportunityId,
};
for (const { id, type } of targetChecks) {
if (id) {
return [{ targetType: type, targetId: id }];
}
}
return null;
return [];
}
private getCalendarEventTargetInfos(
recordSeed: Record<string, unknown>,
): ActivityTargetInfo[] {
const eventParticipants = CALENDAR_EVENT_PARTICIPANT_DATA_SEEDS.filter(
(participant) => participant.calendarEventId === recordSeed.id,
);
const targetInfos: ActivityTargetInfo[] = [];
eventParticipants.forEach((participant) => {
if (participant.personId) {
targetInfos.push({
targetType: 'person',
targetId: participant.personId as string,
});
const person = PERSON_DATA_SEEDS.find(
(p) => p.id === participant.personId,
);
if (person?.companyId) {
targetInfos.push({
targetType: 'company',
targetId: person.companyId as string,
});
}
}
});
return targetInfos;
}
private getMessageTargetInfos(
recordSeed: Record<string, unknown>,
): ActivityTargetInfo[] {
const messageParticipants = MESSAGE_PARTICIPANT_DATA_SEEDS.filter(
(participant) => participant.messageId === recordSeed.id,
);
const targetInfos: ActivityTargetInfo[] = [];
messageParticipants.forEach((participant) => {
if (participant.personId) {
targetInfos.push({
targetType: 'person',
targetId: participant.personId,
});
const person = PERSON_DATA_SEEDS.find(
(p) => p.id === participant.personId,
);
if (person?.companyId) {
targetInfos.push({
targetType: 'company',
targetId: person.companyId as string,
});
}
}
});
return targetInfos;
}
private computeLinkedActivityRecord({
@@ -369,48 +532,21 @@ export class TimelineActivitySeederService {
linkedObjectMetadataId,
targetInfo,
}: CreateLinkedActivityParams): TimelineActivitySeedData {
const generateLinkedTimelineActivityId = (
type: string,
targetType: string,
idx: number,
): string => {
const prefix = '20202020';
const entityCodes: Record<string, string> = {
note: '0601',
task: '0651',
};
const targetCodes: Record<string, string> = {
person: '1001',
company: '2001',
opportunity: '3001',
};
const entityCode = entityCodes[type] || '0000';
const targetCode = targetCodes[targetType] || '0000';
const paddedIndex = idx.toString().padStart(4, '0');
return `${prefix}-${entityCode}-${targetCode}-8001-${paddedIndex}00000001`;
};
const linkedActivityId = this.generateLinkedTimelineActivityId(
activityType,
targetInfo.targetType,
activityIndex,
);
const creationDate = new Date().toISOString();
const { linkedRecordCachedName, linkedProperties } =
this.getLinkedRecordData(activityType, recordSeed, index);
const linkedActivity: TimelineActivitySeedData = {
id: generateLinkedTimelineActivityId(
activityType,
targetInfo.targetType,
activityIndex,
),
name: `linked-${activityType}.created`,
properties: JSON.stringify({
after: {
id: recordSeed.id,
title: recordSeed.title,
body: recordSeed.body,
},
}),
linkedRecordCachedName:
(recordSeed.title as string) || `${activityType} ${index + 1}`,
linkedRecordId: recordSeed.id as string,
id: linkedActivityId,
name: this.getLinkedActivityName(activityType),
properties: JSON.stringify({ after: linkedProperties }),
linkedRecordCachedName,
linkedRecordId: String(recordSeed.id || ''),
linkedObjectMetadataId,
workspaceMemberId: WORKSPACE_MEMBER_DATA_SEED_IDS.TIM,
companyId: null,
@@ -423,36 +559,127 @@ export class TimelineActivitySeederService {
happensAt: creationDate,
};
// Set target ID (person, company, or opportunity)
const targetIdKey = `${targetInfo.targetType}Id`;
// @ts-expect-error - This is okay for morph
// alternative is to be explicit but it's very verbose
linkedActivity[`${targetInfo.targetType}Id`] = targetInfo.targetId;
// @ts-expect-error - This is okay for morph
linkedActivity[`${activityType}Id`] = recordSeed.id;
linkedActivity[targetIdKey] = targetInfo.targetId;
// Only set activity ID for entities that have corresponding columns
const entitiesWithColumns = new Set(['note', 'task']);
if (entitiesWithColumns.has(activityType)) {
const activityIdKey = `${activityType}Id`;
// @ts-expect-error - This is okay for morph
linkedActivity[activityIdKey] = recordSeed.id;
}
return linkedActivity;
}
private async getObjectMetadataIds(workspaceId: string): Promise<{
noteMetadataId: string;
taskMetadataId: string;
}> {
const noteMetadata =
await this.objectMetadataService.findOneWithinWorkspace(workspaceId, {
where: { nameSingular: 'note' },
});
private getLinkedRecordData(
activityType: string,
recordSeed: Record<string, unknown>,
index: number,
): {
linkedRecordCachedName: string;
linkedProperties: Record<string, unknown>;
} {
const baseProperties = { id: recordSeed.id };
const taskMetadata =
await this.objectMetadataService.findOneWithinWorkspace(workspaceId, {
where: { nameSingular: 'task' },
});
const dataGetters = {
calendarEvent: () => ({
linkedRecordCachedName: String(
recordSeed.title || `Calendar Event ${index + 1}`,
),
linkedProperties: {
...baseProperties,
title: recordSeed.title,
description: recordSeed.description,
startsAt: recordSeed.startsAt,
endsAt: recordSeed.endsAt,
},
}),
message: () => ({
linkedRecordCachedName: String(
recordSeed.subject || `Message ${index + 1}`,
),
linkedProperties: {
...baseProperties,
subject: recordSeed.subject,
text: recordSeed.text,
receivedAt: recordSeed.receivedAt,
},
}),
default: () => ({
linkedRecordCachedName: String(
recordSeed.title || `${activityType} ${index + 1}`,
),
linkedProperties: {
...baseProperties,
title: recordSeed.title,
body: recordSeed.body,
},
}),
};
if (!noteMetadata || !taskMetadata) {
throw new Error('Could not find note or task metadata');
const getter =
dataGetters[activityType as keyof typeof dataGetters] ||
dataGetters.default;
return getter();
}
private generateLinkedTimelineActivityId(
type: string,
targetType: string,
index: number,
): string {
const prefix = '20202020';
const entityCode =
this.ENTITY_CODES[type as keyof typeof this.ENTITY_CODES] || '0000';
const targetCode =
this.TARGET_CODES[targetType as keyof typeof this.TARGET_CODES] || '0000';
// Ensure the last segment is exactly 12 hex characters
const indexHex = (index % 0xffffffff).toString(16).padStart(8, '0');
return `${prefix}-${entityCode}-${targetCode}-8001-${indexHex}0001`;
}
private async getObjectMetadataIds(
workspaceId: string,
): Promise<ObjectMetadataIds> {
const metadataQueries = [
{ name: 'note', key: 'noteMetadataId' },
{ name: 'task', key: 'taskMetadataId' },
{ name: 'calendarEvent', key: 'calendarEventMetadataId' },
{ name: 'message', key: 'messageMetadataId' },
];
const metadataResults = await Promise.all(
metadataQueries.map(({ name }) =>
this.objectMetadataService.findOneWithinWorkspace(workspaceId, {
where: { nameSingular: name },
}),
),
);
const missingMetadata = metadataResults
.map((result, index) => (result ? null : metadataQueries[index].name))
.filter(Boolean);
if (missingMetadata.length > 0) {
throw new Error(
`Could not find metadata for: ${missingMetadata.join(', ')}`,
);
}
return {
noteMetadataId: noteMetadata.id,
taskMetadataId: taskMetadata.id,
noteMetadataId: metadataResults[0]?.id || '',
taskMetadataId: metadataResults[1]?.id || '',
calendarEventMetadataId: metadataResults[2]?.id || '',
messageMetadataId: metadataResults[3]?.id || '',
};
}
}