feat: configure standard views and migrate attachment seeds to FILES field (#17958)

## Summary

- Add default visible view fields for `timelineActivity`, `attachment`,
`noteTarget`, `taskTarget`, and `workspaceMember` objects so they
display useful columns out of the box
- Standardize morph relation field labels to "Target" with
`IconArrowUpRight` for consistency across all pivot/junction tables
- Mark deprecated fields (`fullPath`, `fileCategory`,
`linkedRecordCachedName`, `linkedRecordId`, `linkedObjectMetadataId`) as
`isSystem` to hide them from the UI column picker
- Fix morph field deduplication logic (`pickMorphGroupSurvivor`) to
prefer active, non-system fields over auto-generated system fields from
custom objects
- Migrate attachment seeds from legacy `fullPath`/`fileCategory` to the
new `FILES` field type, creating proper `FileEntity` records in
`core.file` via `fileStorageService.writeFile()`
- Restore `customDomain` in the user query fragment


<img width="825" height="754" alt="Screenshot 2026-02-15 at 15 44 27"
src="https://github.com/user-attachments/assets/9596a3dd-8d3a-43c0-925a-0adef9ee68a8"
/>
<img width="736" height="731" alt="Screenshot 2026-02-15 at 15 44 13"
src="https://github.com/user-attachments/assets/cd1a66c5-731d-43e6-bbc3-703cbeda1652"
/>
<img width="722" height="757" alt="Screenshot 2026-02-15 at 15 44 03"
src="https://github.com/user-attachments/assets/b5210546-6a40-4940-8e4f-874818a614fb"
/>
<img width="907" height="757" alt="Screenshot 2026-02-15 at 15 43 52"
src="https://github.com/user-attachments/assets/ead5b9a8-1989-4d68-9640-583da6233711"
/>
<img width="1002" height="731" alt="Screenshot 2026-02-15 at 15 43 38"
src="https://github.com/user-attachments/assets/38accb8c-f5d5-4bfc-b245-06389849810b"
/>




<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Touches migration/upgrade commands that write to core metadata tables
and adjust field/view definitions, plus changes dev seeding to create
`core.file` records; mistakes could affect UI visibility or seed
integrity across workspaces.
> 
> **Overview**
> Adds a new `upgrade:1-18:backfill-standard-views-and-field-metadata`
command that, per workspace, marks specific fields as `isSystem`,
normalizes morph-relation field `label`/`icon` to
`Target`/`IconArrowUpRight`, and backfills missing standard
`view`/`viewField` rows for `attachment`, `noteTarget`, `taskTarget`,
`timelineActivity`, and `workspaceMember`, followed by cache
invalidation + metadata version bump.
> 
> Refactors morph-relation deduplication to pick a single survivor per
`morphId` using a new `pickMorphGroupSurvivor` rule (prefer active +
non-system, then smallest id), with new unit tests.
> 
> Updates standard metadata generators and snapshots to reflect the new
system flags and default view fields, and rewrites attachment dev
seeding to populate the new `file` (FILES field) JSON and create
corresponding `core.file` entries via `FileStorageService.writeFile`
with workspace-scoped file IDs.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
b1939bbf6f8cce294f9b4cdec06b19778daa205e. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
This commit is contained in:
Félix Malfait
2026-02-16 13:34:56 +01:00
committed by GitHub
parent b80146c890
commit c775d65952
29 changed files with 3153 additions and 1308 deletions
@@ -0,0 +1,753 @@
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import { Command } from 'nest-commander';
import { STANDARD_OBJECTS } from 'twenty-shared/metadata';
import { DataSource, Repository } from 'typeorm';
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
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 { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
import { type WorkspaceCacheKeyName } from 'src/engine/workspace-cache/types/workspace-cache-key.type';
import { TWENTY_STANDARD_APPLICATION } from 'src/engine/workspace-manager/twenty-standard-application/constants/twenty-standard-applications';
// Fields that should be marked as isSystem: true
const FIELDS_TO_MARK_AS_SYSTEM = [
// attachment: deprecated fields
STANDARD_OBJECTS.attachment.fields.fullPath.universalIdentifier,
STANDARD_OBJECTS.attachment.fields.fileCategory.universalIdentifier,
// timelineActivity: linked record fields
STANDARD_OBJECTS.timelineActivity.fields.linkedRecordCachedName
.universalIdentifier,
STANDARD_OBJECTS.timelineActivity.fields.linkedRecordId.universalIdentifier,
STANDARD_OBJECTS.timelineActivity.fields.linkedObjectMetadataId
.universalIdentifier,
// workspaceMember: relational fields
STANDARD_OBJECTS.workspaceMember.fields.favorites.universalIdentifier,
STANDARD_OBJECTS.workspaceMember.fields.accountOwnerForCompanies
.universalIdentifier,
STANDARD_OBJECTS.workspaceMember.fields.connectedAccounts.universalIdentifier,
STANDARD_OBJECTS.workspaceMember.fields.messageParticipants
.universalIdentifier,
STANDARD_OBJECTS.workspaceMember.fields.blocklist.universalIdentifier,
STANDARD_OBJECTS.workspaceMember.fields.calendarEventParticipants
.universalIdentifier,
];
// Morph target fields that should have label "Target" and icon "IconArrowUpRight"
const MORPH_FIELDS_TO_RELABEL = [
// attachment
STANDARD_OBJECTS.attachment.fields.targetTask.universalIdentifier,
STANDARD_OBJECTS.attachment.fields.targetNote.universalIdentifier,
STANDARD_OBJECTS.attachment.fields.targetPerson.universalIdentifier,
STANDARD_OBJECTS.attachment.fields.targetCompany.universalIdentifier,
STANDARD_OBJECTS.attachment.fields.targetOpportunity.universalIdentifier,
STANDARD_OBJECTS.attachment.fields.targetDashboard.universalIdentifier,
STANDARD_OBJECTS.attachment.fields.targetWorkflow.universalIdentifier,
// noteTarget
STANDARD_OBJECTS.noteTarget.fields.targetPerson.universalIdentifier,
STANDARD_OBJECTS.noteTarget.fields.targetCompany.universalIdentifier,
STANDARD_OBJECTS.noteTarget.fields.targetOpportunity.universalIdentifier,
// taskTarget
STANDARD_OBJECTS.taskTarget.fields.targetPerson.universalIdentifier,
STANDARD_OBJECTS.taskTarget.fields.targetCompany.universalIdentifier,
STANDARD_OBJECTS.taskTarget.fields.targetOpportunity.universalIdentifier,
// timelineActivity
STANDARD_OBJECTS.timelineActivity.fields.targetPerson.universalIdentifier,
STANDARD_OBJECTS.timelineActivity.fields.targetCompany.universalIdentifier,
STANDARD_OBJECTS.timelineActivity.fields.targetOpportunity
.universalIdentifier,
STANDARD_OBJECTS.timelineActivity.fields.targetTask.universalIdentifier,
STANDARD_OBJECTS.timelineActivity.fields.targetNote.universalIdentifier,
STANDARD_OBJECTS.timelineActivity.fields.targetWorkflow.universalIdentifier,
STANDARD_OBJECTS.timelineActivity.fields.targetWorkflowVersion
.universalIdentifier,
STANDARD_OBJECTS.timelineActivity.fields.targetWorkflowRun
.universalIdentifier,
STANDARD_OBJECTS.timelineActivity.fields.targetDashboard.universalIdentifier,
];
interface ViewFieldDef {
fieldUI: string;
viewFieldUI: string;
position: number;
}
interface ViewToBackfill {
objectUI: string;
viewName: string;
viewIcon: string;
viewUI: string;
viewFields: ViewFieldDef[];
}
const VIEWS_TO_BACKFILL: ViewToBackfill[] = [
{
objectUI: STANDARD_OBJECTS.attachment.universalIdentifier,
viewName: 'All Attachments',
viewIcon: 'IconList',
viewUI:
STANDARD_OBJECTS.attachment.views.allAttachments.universalIdentifier,
viewFields: [
{
fieldUI: STANDARD_OBJECTS.attachment.fields.name.universalIdentifier,
viewFieldUI:
STANDARD_OBJECTS.attachment.views.allAttachments.viewFields.name
.universalIdentifier,
position: 0,
},
{
fieldUI: STANDARD_OBJECTS.attachment.fields.file.universalIdentifier,
viewFieldUI:
STANDARD_OBJECTS.attachment.views.allAttachments.viewFields.file
.universalIdentifier,
position: 1,
},
{
fieldUI:
STANDARD_OBJECTS.attachment.fields.targetPerson.universalIdentifier,
viewFieldUI:
STANDARD_OBJECTS.attachment.views.allAttachments.viewFields
.targetPerson.universalIdentifier,
position: 2,
},
{
fieldUI:
STANDARD_OBJECTS.attachment.fields.targetCompany.universalIdentifier,
viewFieldUI:
STANDARD_OBJECTS.attachment.views.allAttachments.viewFields
.targetCompany.universalIdentifier,
position: 3,
},
{
fieldUI:
STANDARD_OBJECTS.attachment.fields.targetOpportunity
.universalIdentifier,
viewFieldUI:
STANDARD_OBJECTS.attachment.views.allAttachments.viewFields
.targetOpportunity.universalIdentifier,
position: 4,
},
{
fieldUI:
STANDARD_OBJECTS.attachment.fields.targetTask.universalIdentifier,
viewFieldUI:
STANDARD_OBJECTS.attachment.views.allAttachments.viewFields.targetTask
.universalIdentifier,
position: 5,
},
{
fieldUI:
STANDARD_OBJECTS.attachment.fields.targetNote.universalIdentifier,
viewFieldUI:
STANDARD_OBJECTS.attachment.views.allAttachments.viewFields.targetNote
.universalIdentifier,
position: 6,
},
{
fieldUI:
STANDARD_OBJECTS.attachment.fields.targetDashboard
.universalIdentifier,
viewFieldUI:
STANDARD_OBJECTS.attachment.views.allAttachments.viewFields
.targetDashboard.universalIdentifier,
position: 7,
},
{
fieldUI:
STANDARD_OBJECTS.attachment.fields.targetWorkflow.universalIdentifier,
viewFieldUI:
STANDARD_OBJECTS.attachment.views.allAttachments.viewFields
.targetWorkflow.universalIdentifier,
position: 8,
},
{
fieldUI:
STANDARD_OBJECTS.attachment.fields.createdBy.universalIdentifier,
viewFieldUI:
STANDARD_OBJECTS.attachment.views.allAttachments.viewFields.createdBy
.universalIdentifier,
position: 9,
},
{
fieldUI:
STANDARD_OBJECTS.attachment.fields.createdAt.universalIdentifier,
viewFieldUI:
STANDARD_OBJECTS.attachment.views.allAttachments.viewFields.createdAt
.universalIdentifier,
position: 10,
},
],
},
{
objectUI: STANDARD_OBJECTS.noteTarget.universalIdentifier,
viewName: 'All Note Targets',
viewIcon: 'IconList',
viewUI:
STANDARD_OBJECTS.noteTarget.views.allNoteTargets.universalIdentifier,
viewFields: [
{
fieldUI: STANDARD_OBJECTS.noteTarget.fields.id.universalIdentifier,
viewFieldUI:
STANDARD_OBJECTS.noteTarget.views.allNoteTargets.viewFields.id
.universalIdentifier,
position: 0,
},
{
fieldUI: STANDARD_OBJECTS.noteTarget.fields.note.universalIdentifier,
viewFieldUI:
STANDARD_OBJECTS.noteTarget.views.allNoteTargets.viewFields.note
.universalIdentifier,
position: 1,
},
{
fieldUI:
STANDARD_OBJECTS.noteTarget.fields.targetPerson.universalIdentifier,
viewFieldUI:
STANDARD_OBJECTS.noteTarget.views.allNoteTargets.viewFields
.targetPerson.universalIdentifier,
position: 2,
},
{
fieldUI:
STANDARD_OBJECTS.noteTarget.fields.targetCompany.universalIdentifier,
viewFieldUI:
STANDARD_OBJECTS.noteTarget.views.allNoteTargets.viewFields
.targetCompany.universalIdentifier,
position: 3,
},
{
fieldUI:
STANDARD_OBJECTS.noteTarget.fields.targetOpportunity
.universalIdentifier,
viewFieldUI:
STANDARD_OBJECTS.noteTarget.views.allNoteTargets.viewFields
.targetOpportunity.universalIdentifier,
position: 4,
},
],
},
{
objectUI: STANDARD_OBJECTS.taskTarget.universalIdentifier,
viewName: 'All Task Targets',
viewIcon: 'IconList',
viewUI:
STANDARD_OBJECTS.taskTarget.views.allTaskTargets.universalIdentifier,
viewFields: [
{
fieldUI: STANDARD_OBJECTS.taskTarget.fields.id.universalIdentifier,
viewFieldUI:
STANDARD_OBJECTS.taskTarget.views.allTaskTargets.viewFields.id
.universalIdentifier,
position: 0,
},
{
fieldUI: STANDARD_OBJECTS.taskTarget.fields.task.universalIdentifier,
viewFieldUI:
STANDARD_OBJECTS.taskTarget.views.allTaskTargets.viewFields.task
.universalIdentifier,
position: 1,
},
{
fieldUI:
STANDARD_OBJECTS.taskTarget.fields.targetPerson.universalIdentifier,
viewFieldUI:
STANDARD_OBJECTS.taskTarget.views.allTaskTargets.viewFields
.targetPerson.universalIdentifier,
position: 2,
},
{
fieldUI:
STANDARD_OBJECTS.taskTarget.fields.targetCompany.universalIdentifier,
viewFieldUI:
STANDARD_OBJECTS.taskTarget.views.allTaskTargets.viewFields
.targetCompany.universalIdentifier,
position: 3,
},
{
fieldUI:
STANDARD_OBJECTS.taskTarget.fields.targetOpportunity
.universalIdentifier,
viewFieldUI:
STANDARD_OBJECTS.taskTarget.views.allTaskTargets.viewFields
.targetOpportunity.universalIdentifier,
position: 4,
},
],
},
{
objectUI: STANDARD_OBJECTS.timelineActivity.universalIdentifier,
viewName: 'All Timeline Activities',
viewIcon: 'IconList',
viewUI:
STANDARD_OBJECTS.timelineActivity.views.allTimelineActivities
.universalIdentifier,
viewFields: [
{
fieldUI:
STANDARD_OBJECTS.timelineActivity.fields.name.universalIdentifier,
viewFieldUI:
STANDARD_OBJECTS.timelineActivity.views.allTimelineActivities
.viewFields.name.universalIdentifier,
position: 0,
},
{
fieldUI:
STANDARD_OBJECTS.timelineActivity.fields.happensAt
.universalIdentifier,
viewFieldUI:
STANDARD_OBJECTS.timelineActivity.views.allTimelineActivities
.viewFields.happensAt.universalIdentifier,
position: 1,
},
{
fieldUI:
STANDARD_OBJECTS.timelineActivity.fields.targetPerson
.universalIdentifier,
viewFieldUI:
STANDARD_OBJECTS.timelineActivity.views.allTimelineActivities
.viewFields.targetPerson.universalIdentifier,
position: 2,
},
{
fieldUI:
STANDARD_OBJECTS.timelineActivity.fields.targetCompany
.universalIdentifier,
viewFieldUI:
STANDARD_OBJECTS.timelineActivity.views.allTimelineActivities
.viewFields.targetCompany.universalIdentifier,
position: 3,
},
{
fieldUI:
STANDARD_OBJECTS.timelineActivity.fields.targetOpportunity
.universalIdentifier,
viewFieldUI:
STANDARD_OBJECTS.timelineActivity.views.allTimelineActivities
.viewFields.targetOpportunity.universalIdentifier,
position: 4,
},
{
fieldUI:
STANDARD_OBJECTS.timelineActivity.fields.targetTask
.universalIdentifier,
viewFieldUI:
STANDARD_OBJECTS.timelineActivity.views.allTimelineActivities
.viewFields.targetTask.universalIdentifier,
position: 5,
},
{
fieldUI:
STANDARD_OBJECTS.timelineActivity.fields.targetNote
.universalIdentifier,
viewFieldUI:
STANDARD_OBJECTS.timelineActivity.views.allTimelineActivities
.viewFields.targetNote.universalIdentifier,
position: 6,
},
{
fieldUI:
STANDARD_OBJECTS.timelineActivity.fields.targetWorkflow
.universalIdentifier,
viewFieldUI:
STANDARD_OBJECTS.timelineActivity.views.allTimelineActivities
.viewFields.targetWorkflow.universalIdentifier,
position: 7,
},
{
fieldUI:
STANDARD_OBJECTS.timelineActivity.fields.targetWorkflowVersion
.universalIdentifier,
viewFieldUI:
STANDARD_OBJECTS.timelineActivity.views.allTimelineActivities
.viewFields.targetWorkflowVersion.universalIdentifier,
position: 8,
},
{
fieldUI:
STANDARD_OBJECTS.timelineActivity.fields.targetWorkflowRun
.universalIdentifier,
viewFieldUI:
STANDARD_OBJECTS.timelineActivity.views.allTimelineActivities
.viewFields.targetWorkflowRun.universalIdentifier,
position: 9,
},
{
fieldUI:
STANDARD_OBJECTS.timelineActivity.fields.targetDashboard
.universalIdentifier,
viewFieldUI:
STANDARD_OBJECTS.timelineActivity.views.allTimelineActivities
.viewFields.targetDashboard.universalIdentifier,
position: 10,
},
{
fieldUI:
STANDARD_OBJECTS.timelineActivity.fields.workspaceMember
.universalIdentifier,
viewFieldUI:
STANDARD_OBJECTS.timelineActivity.views.allTimelineActivities
.viewFields.workspaceMember.universalIdentifier,
position: 11,
},
{
fieldUI:
STANDARD_OBJECTS.timelineActivity.fields.properties
.universalIdentifier,
viewFieldUI:
STANDARD_OBJECTS.timelineActivity.views.allTimelineActivities
.viewFields.properties.universalIdentifier,
position: 12,
},
{
fieldUI:
STANDARD_OBJECTS.timelineActivity.fields.linkedRecordCachedName
.universalIdentifier,
viewFieldUI:
STANDARD_OBJECTS.timelineActivity.views.allTimelineActivities
.viewFields.linkedRecordCachedName.universalIdentifier,
position: 13,
},
],
},
{
objectUI: STANDARD_OBJECTS.workspaceMember.universalIdentifier,
viewName: 'All Workspace Members',
viewIcon: 'IconList',
viewUI:
STANDARD_OBJECTS.workspaceMember.views.allWorkspaceMembers
.universalIdentifier,
viewFields: [
{
fieldUI:
STANDARD_OBJECTS.workspaceMember.fields.name.universalIdentifier,
viewFieldUI:
STANDARD_OBJECTS.workspaceMember.views.allWorkspaceMembers.viewFields
.name.universalIdentifier,
position: 0,
},
{
fieldUI:
STANDARD_OBJECTS.workspaceMember.fields.userEmail.universalIdentifier,
viewFieldUI:
STANDARD_OBJECTS.workspaceMember.views.allWorkspaceMembers.viewFields
.userEmail.universalIdentifier,
position: 1,
},
{
fieldUI:
STANDARD_OBJECTS.workspaceMember.fields.avatarUrl.universalIdentifier,
viewFieldUI:
STANDARD_OBJECTS.workspaceMember.views.allWorkspaceMembers.viewFields
.avatarUrl.universalIdentifier,
position: 2,
},
{
fieldUI:
STANDARD_OBJECTS.workspaceMember.fields.colorScheme
.universalIdentifier,
viewFieldUI:
STANDARD_OBJECTS.workspaceMember.views.allWorkspaceMembers.viewFields
.colorScheme.universalIdentifier,
position: 3,
},
{
fieldUI:
STANDARD_OBJECTS.workspaceMember.fields.locale.universalIdentifier,
viewFieldUI:
STANDARD_OBJECTS.workspaceMember.views.allWorkspaceMembers.viewFields
.locale.universalIdentifier,
position: 4,
},
{
fieldUI:
STANDARD_OBJECTS.workspaceMember.fields.timeZone.universalIdentifier,
viewFieldUI:
STANDARD_OBJECTS.workspaceMember.views.allWorkspaceMembers.viewFields
.timeZone.universalIdentifier,
position: 5,
},
{
fieldUI:
STANDARD_OBJECTS.workspaceMember.fields.dateFormat
.universalIdentifier,
viewFieldUI:
STANDARD_OBJECTS.workspaceMember.views.allWorkspaceMembers.viewFields
.dateFormat.universalIdentifier,
position: 6,
},
{
fieldUI:
STANDARD_OBJECTS.workspaceMember.fields.timeFormat
.universalIdentifier,
viewFieldUI:
STANDARD_OBJECTS.workspaceMember.views.allWorkspaceMembers.viewFields
.timeFormat.universalIdentifier,
position: 7,
},
{
fieldUI:
STANDARD_OBJECTS.workspaceMember.fields.createdAt.universalIdentifier,
viewFieldUI:
STANDARD_OBJECTS.workspaceMember.views.allWorkspaceMembers.viewFields
.createdAt.universalIdentifier,
position: 8,
},
{
fieldUI:
STANDARD_OBJECTS.workspaceMember.fields.ownedOpportunities
.universalIdentifier,
viewFieldUI:
STANDARD_OBJECTS.workspaceMember.views.allWorkspaceMembers.viewFields
.ownedOpportunities.universalIdentifier,
position: 9,
},
{
fieldUI:
STANDARD_OBJECTS.workspaceMember.fields.assignedTasks
.universalIdentifier,
viewFieldUI:
STANDARD_OBJECTS.workspaceMember.views.allWorkspaceMembers.viewFields
.assignedTasks.universalIdentifier,
position: 10,
},
],
},
];
@Command({
name: 'upgrade:1-18:backfill-standard-views-and-field-metadata',
description:
'Backfill standard views and fix field metadata (isSystem, labels, icons) for attachment, noteTarget, taskTarget, timelineActivity, workspaceMember',
})
export class BackfillStandardViewsAndFieldMetadataCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
constructor(
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
@InjectDataSource()
private readonly coreDataSource: DataSource,
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
protected readonly dataSourceService: DataSourceService,
private readonly workspaceCacheService: WorkspaceCacheService,
private readonly workspaceCacheStorageService: WorkspaceCacheStorageService,
private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService,
) {
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
}
override async runOnWorkspace({
workspaceId,
options,
}: RunOnWorkspaceArgs): Promise<void> {
const dryRun = options?.dryRun ?? false;
this.logger.log(
`${dryRun ? '[DRY RUN] ' : ''}Backfilling views and field metadata for workspace ${workspaceId}`,
);
if (dryRun) {
this.logger.log(
`[DRY RUN] Would update isSystem, labels/icons, backfill views and viewFields for workspace ${workspaceId}. Skipping.`,
);
return;
}
const queryRunner = this.coreDataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
// Part 1: Mark fields as isSystem
const systemResult = await queryRunner.query(
`UPDATE core."fieldMetadata"
SET "isSystem" = true
WHERE "workspaceId" = $1
AND "universalIdentifier" = ANY($2)
AND "isSystem" = false`,
[workspaceId, FIELDS_TO_MARK_AS_SYSTEM],
);
const systemCount = systemResult?.[1] ?? 0;
if (systemCount > 0) {
this.logger.log(
`Marked ${systemCount} field(s) as isSystem for workspace ${workspaceId}`,
);
}
// Part 1b: Update morph field labels and icons to "Target"
const labelResult = await queryRunner.query(
`UPDATE core."fieldMetadata"
SET label = 'Target', icon = 'IconArrowUpRight'
WHERE "workspaceId" = $1
AND "universalIdentifier" = ANY($2)
AND (label != 'Target' OR icon != 'IconArrowUpRight')`,
[workspaceId, MORPH_FIELDS_TO_RELABEL],
);
const labelCount = labelResult?.[1] ?? 0;
if (labelCount > 0) {
this.logger.log(
`Relabeled ${labelCount} morph field(s) to "Target" for workspace ${workspaceId}`,
);
}
// Part 2: Resolve applicationId for twenty-standard app
const applicationRows = await queryRunner.query(
`SELECT id FROM core."application"
WHERE "workspaceId" = $1
AND "universalIdentifier" = $2`,
[workspaceId, TWENTY_STANDARD_APPLICATION.universalIdentifier],
);
if (applicationRows.length === 0) {
this.logger.warn(
`Twenty Standard Application not found for workspace ${workspaceId}. Skipping view backfill.`,
);
await queryRunner.commitTransaction();
await this.invalidateCaches(workspaceId);
return;
}
const applicationId = applicationRows[0].id;
// Part 3 & 4: Backfill views and viewFields
for (const view of VIEWS_TO_BACKFILL) {
// Insert view if it doesn't exist
const viewInsertResult = await queryRunner.query(
`INSERT INTO core."view" (
id, name, "objectMetadataId", type, icon, position,
visibility, "workspaceId", "applicationId", "universalIdentifier"
)
SELECT
gen_random_uuid(), $2, om.id, 'TABLE', $3, 0,
'WORKSPACE', $1, $4, $5
FROM core."objectMetadata" om
WHERE om."workspaceId" = $1
AND om."universalIdentifier" = $6
AND NOT EXISTS (
SELECT 1 FROM core."view" v
WHERE v."workspaceId" = $1
AND v."universalIdentifier" = $5
AND v."deletedAt" IS NULL
)
RETURNING 1`,
[
workspaceId,
view.viewName,
view.viewIcon,
applicationId,
view.viewUI,
view.objectUI,
],
);
const viewInserted = viewInsertResult?.length ?? 0;
if (viewInserted > 0) {
this.logger.log(
`Created view "${view.viewName}" for workspace ${workspaceId}`,
);
}
// Insert viewFields
for (const viewField of view.viewFields) {
await queryRunner.query(
`INSERT INTO core."viewField" (
id, "fieldMetadataId", "isVisible", size, position,
"viewId", "workspaceId", "applicationId", "universalIdentifier"
)
SELECT
gen_random_uuid(), fm.id, true, 150, $4,
v.id, $1, $5, $6
FROM core."view" v
JOIN core."fieldMetadata" fm
ON fm."workspaceId" = $1
AND fm."universalIdentifier" = $3
WHERE v."workspaceId" = $1
AND v."universalIdentifier" = $2
AND v."deletedAt" IS NULL
AND NOT EXISTS (
SELECT 1 FROM core."viewField" vf
WHERE vf."workspaceId" = $1
AND vf."universalIdentifier" = $6
AND vf."deletedAt" IS NULL
)`,
[
workspaceId,
view.viewUI,
viewField.fieldUI,
viewField.position,
applicationId,
viewField.viewFieldUI,
],
);
}
}
await queryRunner.commitTransaction();
this.logger.log(`Completed for workspace ${workspaceId}`);
await this.invalidateCaches(workspaceId);
} catch (error) {
if (queryRunner.isTransactionActive) {
await queryRunner.rollbackTransaction();
this.logger.error(
`Error backfilling views and field metadata (rolled back) for workspace ${workspaceId}`,
error,
);
} else {
this.logger.error(
`Error backfilling views and field metadata (after commit) for workspace ${workspaceId}`,
error,
);
}
throw error;
} finally {
await queryRunner.release();
}
}
private async invalidateCaches(workspaceId: string): Promise<void> {
const modifiedMetadataNames = [
'fieldMetadata',
'view',
'viewField',
] as const;
const cacheKeysToInvalidate: WorkspaceCacheKeyName[] = [
...new Set(
modifiedMetadataNames
.flatMap((name) => [name, ...getMetadataRelatedMetadataNames(name)])
.map(getMetadataFlatEntityMapsKey),
),
'ORMEntityMetadatas',
];
await this.workspaceCacheService.invalidateAndRecompute(
workspaceId,
cacheKeysToInvalidate,
);
await this.workspaceMetadataVersionService.incrementMetadataVersion(
workspaceId,
);
await this.workspaceCacheStorageService.flush(workspaceId);
this.logger.log(
`Cache invalidated and metadata version incremented for workspace ${workspaceId}`,
);
}
}
@@ -3,6 +3,7 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { BackfillFileSizeAndMimeTypeCommand } from 'src/database/commands/upgrade-version-command/1-18/1-18-backfill-file-size-and-mime-type.command';
import { BackfillMessageChannelThrottleRetryAfterCommand } from 'src/database/commands/upgrade-version-command/1-18/1-18-backfill-message-channel-throttle-retry-after.command';
import { BackfillStandardViewsAndFieldMetadataCommand } from 'src/database/commands/upgrade-version-command/1-18/1-18-backfill-standard-views-and-field-metadata.command';
import { MigrateActivityRichTextAttachmentFileIdsCommand } from 'src/database/commands/upgrade-version-command/1-18/1-18-migrate-activity-rich-text-attachment-file-ids.command';
import { MigrateAttachmentFilesCommand } from 'src/database/commands/upgrade-version-command/1-18/1-18-migrate-attachment-files.command';
import { MigratePersonAvatarFilesCommand } from 'src/database/commands/upgrade-version-command/1-18/1-18-migrate-person-avatar-files.command';
@@ -15,6 +16,8 @@ import { FilesFieldModule } from 'src/engine/core-modules/file/files-field/files
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
import { FieldMetadataModule } from 'src/engine/metadata-modules/field-metadata/field-metadata.module';
import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module';
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
import { AttachmentWorkspaceEntity } from 'src/modules/attachment/standard-objects/attachment.workspace-entity';
import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/person.workspace-entity';
@@ -32,6 +35,8 @@ import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/perso
FeatureFlagModule,
FileStorageModule.forRoot(),
WorkspaceCacheModule,
WorkspaceCacheStorageModule,
WorkspaceMetadataVersionModule,
FieldMetadataModule,
ApplicationModule,
FilesFieldModule,
@@ -42,6 +47,7 @@ import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/perso
BackfillFileSizeAndMimeTypeCommand,
MigrateActivityRichTextAttachmentFileIdsCommand,
BackfillMessageChannelThrottleRetryAfterCommand,
BackfillStandardViewsAndFieldMetadataCommand,
],
exports: [
MigratePersonAvatarFilesCommand,
@@ -49,6 +55,7 @@ import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/perso
BackfillFileSizeAndMimeTypeCommand,
MigrateActivityRichTextAttachmentFileIdsCommand,
BackfillMessageChannelThrottleRetryAfterCommand,
BackfillStandardViewsAndFieldMetadataCommand,
],
})
export class V1_18_UpgradeVersionCommandModule {}
@@ -21,6 +21,7 @@ import { MigrateTaskTargetToMorphRelationsCommand } from 'src/database/commands/
import { MigrateWorkflowCodeStepsCommand } from 'src/database/commands/upgrade-version-command/1-17/1-17-migrate-workflow-code-steps.command';
import { BackfillFileSizeAndMimeTypeCommand } from 'src/database/commands/upgrade-version-command/1-18/1-18-backfill-file-size-and-mime-type.command';
import { BackfillMessageChannelThrottleRetryAfterCommand } from 'src/database/commands/upgrade-version-command/1-18/1-18-backfill-message-channel-throttle-retry-after.command';
import { BackfillStandardViewsAndFieldMetadataCommand } from 'src/database/commands/upgrade-version-command/1-18/1-18-backfill-standard-views-and-field-metadata.command';
import { MigrateActivityRichTextAttachmentFileIdsCommand } from 'src/database/commands/upgrade-version-command/1-18/1-18-migrate-activity-rich-text-attachment-file-ids.command';
import { MigrateAttachmentFilesCommand } from 'src/database/commands/upgrade-version-command/1-18/1-18-migrate-attachment-files.command';
import { MigratePersonAvatarFilesCommand } from 'src/database/commands/upgrade-version-command/1-18/1-18-migrate-person-avatar-files.command';
@@ -61,6 +62,7 @@ export class UpgradeCommand extends UpgradeCommandRunner {
protected readonly migrateAttachmentFilesCommand: MigrateAttachmentFilesCommand,
protected readonly migrateActivityRichTextAttachmentFileIdsCommand: MigrateActivityRichTextAttachmentFileIdsCommand,
protected readonly backfillMessageChannelThrottleRetryAfterCommand: BackfillMessageChannelThrottleRetryAfterCommand,
protected readonly backfillStandardViewsAndFieldMetadataCommand: BackfillStandardViewsAndFieldMetadataCommand,
) {
super(
workspaceRepository,
@@ -92,6 +94,7 @@ export class UpgradeCommand extends UpgradeCommandRunner {
this.migrateActivityRichTextAttachmentFileIdsCommand,
this.backfillFileSizeAndMimeTypeCommand,
this.backfillMessageChannelThrottleRetryAfterCommand,
this.backfillStandardViewsAndFieldMetadataCommand,
];
this.allCommands = {