From fa0615d41ee5ce40929f0d10870e87ad27121083 Mon Sep 17 00:00:00 2001 From: "Abdullah." <125115953+mabdullahabaid@users.noreply.github.com> Date: Sun, 25 Jan 2026 02:17:24 +0500 Subject: [PATCH] Migrate attachments to morph relations + fix morph join column filtering (#17381) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes [1744](https://github.com/twentyhq/core-team-issues/issues/1744). This PR migrates attachments to morph relations behind a feature flag, following the TimelineActivity pattern. it introduces the `IS_ATTACHMENT_MIGRATED` flag, updates standard field metadata and indexes to use morph relations, adds a workspace migration that renames `attachment.*Id` columns to `target*Id` and converts the corresponding field metadata to `MORPH_RELATION` with a shared `morphId`. On the frontend, attachment read/write paths now switch to `target*Id` when the flag is enabled. It also fixes optimistic filtering for morph join columns. The metadata API deduplicates morph fields, so attachments now expose a single target field of type `MORPH_RELATION` plus a `morphRelations` array listing each target object. Because only one `settings.joinColumnName` is returned (e.g. `targetRocketId`), filters like `targetCompanyId` don’t map to any field and the optimistic cache code throws. `doesMorphRelationJoinColumnMatch` resolves this by computing all valid join column names from `morphRelations` using `computeMorphRelationFieldName` and comparing them to the filter key. That makes filters like `targetCompanyId` resolvable even with a single target field, so attachment uploads and list matching no longer crash. image

Today the metadata API returns one morph field called `target` and a list of possible targets (`morphRelations`), but it does not tell us the join column for each target. That’s why the Frontend had to compute join column names. If we want to fix this at the API level, there are two options: - Add join column names to each target in `morphRelations` (e.g. company → `targetCompanyId`). This is additive and low‑risk. - Return each target as its own field (`targetCompany`, `targetPerson`, etc.) instead of a single target. This is a larger change because it changes the shape of metadata and would require more UI updates. --- > [!NOTE] > Introduces morph relations for attachments behind `IS_ATTACHMENT_MIGRATED`, aligning server schema/metadata and frontend behavior. > > - Adds `IS_ATTACHMENT_MIGRATED` flag (frontend/server) and seeds/defaults; updates generated GraphQL enums > - New workspace upgrade `1.17` command migrates data: renames `attachment.*Id` → `target*Id` and converts related fields to `MORPH_RELATION` with shared `morphId` > - Updates standard field metadata and indexes to `target*` (attachment + related objects), dev seeds, snapshots, and workspace entity types > - Frontend: switches attachment read/write filters via `getActivityTargetObjectFieldIdName` using the feature flag; updates hooks/components (`useAttachments`, `useUploadAttachmentFile`, editors); expands `Attachment` type > - Fixes optimistic cache filtering to recognize morph join columns in `isRecordMatchingFilter` by computing valid join-column keys from `morphRelations` > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit f208fa23b1135af114f484f6d65041a816d6e718. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). --------- Co-authored-by: Félix Malfait --- .../src/generated-metadata/graphql.ts | 1 + .../twenty-front/src/generated/graphql.ts | 1 + .../components/ActivityRichTextEditor.tsx | 27 +- .../hooks/__tests__/useAttachments.test.ts | 11 + .../activities/files/hooks/useAttachments.tsx | 7 + .../files/hooks/useUploadAttachmentFile.tsx | 6 + .../activities/files/types/Attachment.ts | 16 +- .../getActivityTargetObjectFieldIdName.ts | 8 + .../utils/isRecordMatchingFilter.ts | 47 +++- .../components/StandaloneRichTextWidget.tsx | 12 +- .../standard-index-field-names.constant.ts | 18 +- ...e-attachment-to-morph-relations.command.ts | 258 ++++++++++++++++++ .../1-17-upgrade-version-command.module.ts | 40 +++ .../upgrade-version-command.module.ts | 2 + .../upgrade.command.ts | 9 + .../enums/feature-flag-key.enum.ts | 1 + ...-field-metadatas-for-custom-object.util.ts | 7 +- .../workspace-entity-manager.spec.ts | 1 + .../core/utils/seed-feature-flags.util.ts | 5 + .../attachment-data-seeds.constant.ts | 51 ++-- .../constants/standard-object.constant.ts | 28 +- ...adata-related-entity-ids.util.spec.ts.snap | 28 +- ...hment-standard-flat-field-metadata.util.ts | 71 ++--- ...mpany-standard-flat-field-metadata.util.ts | 2 +- ...board-standard-flat-field-metadata.util.ts | 2 +- ...-note-standard-flat-field-metadata.util.ts | 2 +- ...unity-standard-flat-field-metadata.util.ts | 2 +- ...erson-standard-flat-field-metadata.util.ts | 2 +- ...-task-standard-flat-field-metadata.util.ts | 2 +- ...kflow-standard-flat-field-metadata.util.ts | 2 +- ...hment-standard-flat-index-metadata.util.ts | 14 +- .../constant/default-feature-flags.ts | 1 + .../constant/standard-field-ids.ts | 17 +- .../attachment.workspace-entity.ts | 28 +- .../attachments.integration-spec.ts | 28 +- ...bject-metadata-v2.integration-spec.ts.snap | 70 ++--- 36 files changed, 625 insertions(+), 202 deletions(-) create mode 100644 packages/twenty-server/src/database/commands/upgrade-version-command/1-17/1-17-migrate-attachment-to-morph-relations.command.ts create mode 100644 packages/twenty-server/src/database/commands/upgrade-version-command/1-17/1-17-upgrade-version-command.module.ts diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index cbb47a4eae..0bf19901dc 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -1452,6 +1452,7 @@ export enum FeatureFlagKey { IS_AI_ENABLED = 'IS_AI_ENABLED', IS_APPLICATION_ENABLED = 'IS_APPLICATION_ENABLED', IS_APPLICATION_INSTALLATION_FROM_TARBALL_ENABLED = 'IS_APPLICATION_INSTALLATION_FROM_TARBALL_ENABLED', + IS_ATTACHMENT_MIGRATED = 'IS_ATTACHMENT_MIGRATED', IS_COMMAND_MENU_ITEM_ENABLED = 'IS_COMMAND_MENU_ITEM_ENABLED', IS_DASHBOARD_V2_ENABLED = 'IS_DASHBOARD_V2_ENABLED', IS_EMAILING_DOMAIN_ENABLED = 'IS_EMAILING_DOMAIN_ENABLED', diff --git a/packages/twenty-front/src/generated/graphql.ts b/packages/twenty-front/src/generated/graphql.ts index 1249b47ee4..b43ccdd27d 100644 --- a/packages/twenty-front/src/generated/graphql.ts +++ b/packages/twenty-front/src/generated/graphql.ts @@ -1419,6 +1419,7 @@ export enum FeatureFlagKey { IS_AI_ENABLED = 'IS_AI_ENABLED', IS_APPLICATION_ENABLED = 'IS_APPLICATION_ENABLED', IS_APPLICATION_INSTALLATION_FROM_TARBALL_ENABLED = 'IS_APPLICATION_INSTALLATION_FROM_TARBALL_ENABLED', + IS_ATTACHMENT_MIGRATED = 'IS_ATTACHMENT_MIGRATED', IS_COMMAND_MENU_ITEM_ENABLED = 'IS_COMMAND_MENU_ITEM_ENABLED', IS_DASHBOARD_V2_ENABLED = 'IS_DASHBOARD_V2_ENABLED', IS_EMAILING_DOMAIN_ENABLED = 'IS_EMAILING_DOMAIN_ENABLED', diff --git a/packages/twenty-front/src/modules/activities/components/ActivityRichTextEditor.tsx b/packages/twenty-front/src/modules/activities/components/ActivityRichTextEditor.tsx index 7c6bd62f84..db0f29f3fb 100644 --- a/packages/twenty-front/src/modules/activities/components/ActivityRichTextEditor.tsx +++ b/packages/twenty-front/src/modules/activities/components/ActivityRichTextEditor.tsx @@ -5,6 +5,7 @@ import { v4 } from 'uuid'; import { useUploadAttachmentFile } from '@/activities/files/hooks/useUploadAttachmentFile'; import { useUpsertActivity } from '@/activities/hooks/useUpsertActivity'; import { canCreateActivityState } from '@/activities/states/canCreateActivityState'; +import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getActivityTargetObjectFieldIdName'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { modifyRecordFromCache } from '@/object-record/cache/utils/modifyRecordFromCache'; @@ -35,11 +36,13 @@ import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePush import { useRemoveFocusItemFromFocusStackById } from '@/ui/utilities/focus/hooks/useRemoveFocusItemFromFocusStackById'; import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType'; import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement'; +import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; import '@blocknote/core/fonts/inter.css'; import '@blocknote/mantine/style.css'; import { useCreateBlockNote } from '@blocknote/react'; import '@blocknote/react/style.css'; import { isDefined } from 'twenty-shared/utils'; +import { FeatureFlagKey } from '~/generated/graphql'; type ActivityRichTextEditorProps = { activityId: string; @@ -70,21 +73,21 @@ export const ActivityRichTextEditor = ({ const { removeFocusItemFromFocusStackById } = useRemoveFocusItemFromFocusStackById(); + const isAttachmentMigrated = useIsFeatureEnabled( + FeatureFlagKey.IS_ATTACHMENT_MIGRATED, + ); + + const attachmentTargetFieldIdName = getActivityTargetObjectFieldIdName({ + nameSingular: activityObjectNameSingular, + isMorphRelation: isAttachmentMigrated, + }); + const { records: attachments } = useFindManyRecords({ objectNameSingular: CoreObjectNameSingular.Attachment, filter: { - or: [ - { - noteId: { - eq: activityId, - }, - }, - { - taskId: { - eq: activityId, - }, - }, - ], + [attachmentTargetFieldIdName]: { + eq: activityId, + }, }, }); diff --git a/packages/twenty-front/src/modules/activities/files/hooks/__tests__/useAttachments.test.ts b/packages/twenty-front/src/modules/activities/files/hooks/__tests__/useAttachments.test.ts index 42ebbefdb4..31bf961b21 100644 --- a/packages/twenty-front/src/modules/activities/files/hooks/__tests__/useAttachments.test.ts +++ b/packages/twenty-front/src/modules/activities/files/hooks/__tests__/useAttachments.test.ts @@ -5,6 +5,9 @@ import { useAttachments } from '@/activities/files/hooks/useAttachments'; jest.mock('@/object-record/hooks/useFindManyRecords', () => ({ useFindManyRecords: jest.fn(), })); +jest.mock('@/workspace/hooks/useIsFeatureEnabled', () => ({ + useIsFeatureEnabled: jest.fn(), +})); describe('useAttachments', () => { afterEach(() => { @@ -24,9 +27,13 @@ describe('useAttachments', () => { const useFindManyRecordsMock = jest.requireMock( '@/object-record/hooks/useFindManyRecords', ); + const useIsFeatureEnabledMock = jest.requireMock( + '@/workspace/hooks/useIsFeatureEnabled', + ); useFindManyRecordsMock.useFindManyRecords.mockReturnValue({ records: mockAttachments, }); + useIsFeatureEnabledMock.useIsFeatureEnabled.mockReturnValue(false); const { result } = renderHook(() => useAttachments(mockTargetableObject)); @@ -42,7 +49,11 @@ describe('useAttachments', () => { const useFindManyRecordsMock = jest.requireMock( '@/object-record/hooks/useFindManyRecords', ); + const useIsFeatureEnabledMock = jest.requireMock( + '@/workspace/hooks/useIsFeatureEnabled', + ); useFindManyRecordsMock.useFindManyRecords.mockReturnValue({ records: [] }); + useIsFeatureEnabledMock.useIsFeatureEnabled.mockReturnValue(false); const { result } = renderHook(() => useAttachments(mockTargetableObject)); diff --git a/packages/twenty-front/src/modules/activities/files/hooks/useAttachments.tsx b/packages/twenty-front/src/modules/activities/files/hooks/useAttachments.tsx index bf8576da91..848bf8b9e5 100644 --- a/packages/twenty-front/src/modules/activities/files/hooks/useAttachments.tsx +++ b/packages/twenty-front/src/modules/activities/files/hooks/useAttachments.tsx @@ -3,10 +3,17 @@ import { type ActivityTargetableObject } from '@/activities/types/ActivityTarget import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getActivityTargetObjectFieldIdName'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; +import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; +import { FeatureFlagKey } from '~/generated/graphql'; export const useAttachments = (targetableObject: ActivityTargetableObject) => { + const isAttachmentMigrated = useIsFeatureEnabled( + FeatureFlagKey.IS_ATTACHMENT_MIGRATED, + ); + const targetableObjectFieldIdName = getActivityTargetObjectFieldIdName({ nameSingular: targetableObject.targetObjectNameSingular, + isMorphRelation: isAttachmentMigrated, }); const { records: attachments, loading } = useFindManyRecords({ diff --git a/packages/twenty-front/src/modules/activities/files/hooks/useUploadAttachmentFile.tsx b/packages/twenty-front/src/modules/activities/files/hooks/useUploadAttachmentFile.tsx index 1012af116b..b00e61cb03 100644 --- a/packages/twenty-front/src/modules/activities/files/hooks/useUploadAttachmentFile.tsx +++ b/packages/twenty-front/src/modules/activities/files/hooks/useUploadAttachmentFile.tsx @@ -5,15 +5,20 @@ import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getActivi import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord'; +import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; import { isDefined } from 'twenty-shared/utils'; import { FileFolder, useUploadFileMutation, } from '~/generated-metadata/graphql'; +import { FeatureFlagKey } from '~/generated/graphql'; export const useUploadAttachmentFile = () => { const coreClient = useApolloCoreClient(); const [uploadFile] = useUploadFileMutation({ client: coreClient }); + const isAttachmentMigrated = useIsFeatureEnabled( + FeatureFlagKey.IS_ATTACHMENT_MIGRATED, + ); const { createOneRecord: createOneAttachment } = useCreateOneRecord({ @@ -42,6 +47,7 @@ export const useUploadAttachmentFile = () => { const targetableObjectFieldIdName = getActivityTargetObjectFieldIdName({ nameSingular: targetableObject.targetObjectNameSingular, + isMorphRelation: isAttachmentMigrated, }); const attachmentToCreate = { diff --git a/packages/twenty-front/src/modules/activities/files/types/Attachment.ts b/packages/twenty-front/src/modules/activities/files/types/Attachment.ts index d947e85732..70422f5731 100644 --- a/packages/twenty-front/src/modules/activities/files/types/Attachment.ts +++ b/packages/twenty-front/src/modules/activities/files/types/Attachment.ts @@ -7,8 +7,20 @@ export type Attachment = { name: string; fullPath: string; fileCategory: AttachmentFileCategory; - companyId: string; - personId: string; + companyId?: string | null; + personId?: string | null; + taskId?: string | null; + noteId?: string | null; + opportunityId?: string | null; + dashboardId?: string | null; + workflowId?: string | null; + targetCompanyId?: string | null; + targetPersonId?: string | null; + targetTaskId?: string | null; + targetNoteId?: string | null; + targetOpportunityId?: string | null; + targetDashboardId?: string | null; + targetWorkflowId?: string | null; createdBy?: { source: string; workspaceMemberId: string | null; diff --git a/packages/twenty-front/src/modules/activities/utils/getActivityTargetObjectFieldIdName.ts b/packages/twenty-front/src/modules/activities/utils/getActivityTargetObjectFieldIdName.ts index 6932fbf123..71384a2099 100644 --- a/packages/twenty-front/src/modules/activities/utils/getActivityTargetObjectFieldIdName.ts +++ b/packages/twenty-front/src/modules/activities/utils/getActivityTargetObjectFieldIdName.ts @@ -1,7 +1,15 @@ +import { capitalize } from 'twenty-shared/utils'; + export const getActivityTargetObjectFieldIdName = ({ nameSingular, + isMorphRelation = false, }: { nameSingular: string; + isMorphRelation?: boolean; }) => { + if (isMorphRelation) { + return `target${capitalize(nameSingular)}Id`; + } + return `${nameSingular}Id`; }; diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/isRecordMatchingFilter.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/isRecordMatchingFilter.ts index 3f60141ffa..79bfcc0e05 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/utils/isRecordMatchingFilter.ts +++ b/packages/twenty-front/src/modules/object-record/record-filter/utils/isRecordMatchingFilter.ts @@ -1,6 +1,5 @@ import { isObject } from '@sniptt/guards'; -import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { FieldMetadataType, type ActorFilter, @@ -46,6 +45,10 @@ import { isMatchingUUIDFilter, } from 'twenty-shared/utils'; +import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; +import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { computePossibleMorphGqlFieldForFieldName } from '@/object-record/cache/utils/computePossibleMorphGqlFieldForFieldName'; + const isLeafFilter = ( filter: RecordGqlOperationFilter, ): filter is LeafObjectRecordFilter => { @@ -59,6 +62,27 @@ const isAndFilter = ( const isImplicitAndFilter = (filter: RecordGqlOperationFilter) => Object.keys(filter).length > 1; +const isMorphRelationJoinColumnKey = ({ + fieldMetadataItem, + key, +}: { + fieldMetadataItem: FieldMetadataItem; + key: string; +}): boolean => { + if (!fieldMetadataItem.morphRelations?.length) { + return false; + } + + const possibleJoinColumnNames = computePossibleMorphGqlFieldForFieldName({ + fieldMetadata: { + morphRelations: fieldMetadataItem.morphRelations, + fieldName: fieldMetadataItem.name, + }, + }).map((name) => `${name}Id`); + + return possibleJoinColumnNames.includes(key); +}; + const isOrFilter = ( filter: RecordGqlOperationFilter, ): filter is OrObjectRecordFilter => 'or' in filter && !!filter.or; @@ -175,8 +199,17 @@ export const isRecordMatchingFilter = ({ objectMetadataItem.fields.find((field) => field.name === filterKey) ?? objectMetadataItem.fields.find( (field) => - field.type === FieldMetadataType.RELATION && + (field.type === FieldMetadataType.RELATION || + field.type === FieldMetadataType.MORPH_RELATION) && field.settings?.joinColumnName === filterKey, + ) ?? + objectMetadataItem.fields.find( + (field) => + field.type === FieldMetadataType.MORPH_RELATION && + isMorphRelationJoinColumnKey({ + fieldMetadataItem: field, + key: filterKey, + }), ); if (!isDefined(objectMetadataField)) { @@ -366,9 +399,15 @@ export const isRecordMatchingFilter = ({ }); }); } - case FieldMetadataType.RELATION: { + case FieldMetadataType.RELATION: + case FieldMetadataType.MORPH_RELATION: { const isJoinColumn = - objectMetadataField.settings?.joinColumnName === filterKey; + objectMetadataField.settings?.joinColumnName === filterKey || + (objectMetadataField.type === FieldMetadataType.MORPH_RELATION && + isMorphRelationJoinColumnKey({ + fieldMetadataItem: objectMetadataField, + key: filterKey, + })); if (isJoinColumn) { return isMatchingUUIDFilter({ diff --git a/packages/twenty-front/src/modules/page-layout/widgets/standalone-rich-text/components/StandaloneRichTextWidget.tsx b/packages/twenty-front/src/modules/page-layout/widgets/standalone-rich-text/components/StandaloneRichTextWidget.tsx index 855182ebfd..308b4b2d04 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/standalone-rich-text/components/StandaloneRichTextWidget.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/standalone-rich-text/components/StandaloneRichTextWidget.tsx @@ -3,6 +3,7 @@ import { useCallback, useMemo, useRef } from 'react'; import { BLOCK_SCHEMA } from '@/activities/blocks/constants/Schema'; import { useUploadAttachmentFile } from '@/activities/files/hooks/useUploadAttachmentFile'; import { type Attachment } from '@/activities/files/types/Attachment'; +import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getActivityTargetObjectFieldIdName'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; import { useUpdatePageLayoutWidget } from '@/page-layout/hooks/useUpdatePageLayoutWidget'; @@ -20,6 +21,7 @@ import { useRemoveFocusItemFromFocusStackById } from '@/ui/utilities/focus/hooks import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType'; import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; +import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; import '@blocknote/core/fonts/inter.css'; import '@blocknote/mantine/style.css'; import { useCreateBlockNote } from '@blocknote/react'; @@ -28,6 +30,7 @@ import styled from '@emotion/styled'; import { isDefined } from 'twenty-shared/utils'; import { useDebouncedCallback } from 'use-debounce'; import { + FeatureFlagKey, PageLayoutType, WidgetConfigurationType, type StandaloneRichTextConfiguration, @@ -63,6 +66,9 @@ export const StandaloneRichTextWidget = ({ const { updatePageLayoutWidget } = useUpdatePageLayoutWidget(); const { targetRecordIdentifier, layoutType } = useLayoutRenderingContext(); const { uploadAttachmentFile } = useUploadAttachmentFile(); + const isAttachmentMigrated = useIsFeatureEnabled( + FeatureFlagKey.IS_ATTACHMENT_MIGRATED, + ); const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack(); const { removeFocusItemFromFocusStackById } = @@ -70,6 +76,10 @@ export const StandaloneRichTextWidget = ({ const isDashboard = layoutType === PageLayoutType.DASHBOARD; const dashboardId = isDashboard ? targetRecordIdentifier?.id : undefined; + const attachmentTargetFieldIdName = getActivityTargetObjectFieldIdName({ + nameSingular: CoreObjectNameSingular.Dashboard, + isMorphRelation: isAttachmentMigrated, + }); const configuration = widget.configuration as | StandaloneRichTextConfiguration @@ -80,7 +90,7 @@ export const StandaloneRichTextWidget = ({ const { records: attachments } = useFindManyRecords({ objectNameSingular: CoreObjectNameSingular.Attachment, filter: isDefined(dashboardId) - ? { dashboardId: { eq: dashboardId } } + ? { [attachmentTargetFieldIdName]: { eq: dashboardId } } : undefined, skip: !isDefined(dashboardId), }); diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-16/constants/standard-index-field-names.constant.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-16/constants/standard-index-field-names.constant.ts index f45d8c3249..e7fa7fa7fc 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-16/constants/standard-index-field-names.constant.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-16/constants/standard-index-field-names.constant.ts @@ -6,22 +6,26 @@ export const STANDARD_INDEX_FIELD_UNIVERSAL_IDENTIFIERS: Record< Record > = { attachment: { - taskIdIndex: [STANDARD_OBJECTS.attachment.fields.task.universalIdentifier], - noteIdIndex: [STANDARD_OBJECTS.attachment.fields.note.universalIdentifier], + taskIdIndex: [ + STANDARD_OBJECTS.attachment.fields.targetTask.universalIdentifier, + ], + noteIdIndex: [ + STANDARD_OBJECTS.attachment.fields.targetNote.universalIdentifier, + ], personIdIndex: [ - STANDARD_OBJECTS.attachment.fields.person.universalIdentifier, + STANDARD_OBJECTS.attachment.fields.targetPerson.universalIdentifier, ], companyIdIndex: [ - STANDARD_OBJECTS.attachment.fields.company.universalIdentifier, + STANDARD_OBJECTS.attachment.fields.targetCompany.universalIdentifier, ], opportunityIdIndex: [ - STANDARD_OBJECTS.attachment.fields.opportunity.universalIdentifier, + STANDARD_OBJECTS.attachment.fields.targetOpportunity.universalIdentifier, ], dashboardIdIndex: [ - STANDARD_OBJECTS.attachment.fields.dashboard.universalIdentifier, + STANDARD_OBJECTS.attachment.fields.targetDashboard.universalIdentifier, ], workflowIdIndex: [ - STANDARD_OBJECTS.attachment.fields.workflow.universalIdentifier, + STANDARD_OBJECTS.attachment.fields.targetWorkflow.universalIdentifier, ], }, blocklist: { diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-17/1-17-migrate-attachment-to-morph-relations.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-17/1-17-migrate-attachment-to-morph-relations.command.ts new file mode 100644 index 0000000000..cb6a6acb52 --- /dev/null +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-17/1-17-migrate-attachment-to-morph-relations.command.ts @@ -0,0 +1,258 @@ +import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; + +import { Command } from 'nest-commander'; +import { capitalize } from 'twenty-shared/utils'; +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 { 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 { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; +import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util'; +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +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 { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util'; +import { ATTACHMENT_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-migration/constant/standard-field-ids'; + +@Command({ + name: 'upgrade:1-17:migrate-attachment-to-morph-relations', + description: + 'Migrate attachment relations to morph relation fields and set feature flag', +}) +export class MigrateAttachmentToMorphRelationsCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner { + constructor( + @InjectRepository(WorkspaceEntity) + protected readonly workspaceRepository: Repository, + private readonly featureFlagService: FeatureFlagService, + @InjectRepository(ObjectMetadataEntity) + private readonly objectMetadataRepository: Repository, + @InjectDataSource() + private readonly coreDataSource: DataSource, + private readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager, + protected readonly dataSourceService: DataSourceService, + private readonly workspaceCacheStorageService: WorkspaceCacheStorageService, + private readonly workspaceCacheService: WorkspaceCacheService, + private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService, + ) { + super(workspaceRepository, twentyORMGlobalManager, dataSourceService); + } + + override async runOnWorkspace({ + workspaceId, + options, + }: RunOnWorkspaceArgs): Promise { + const isMigrated = await this.featureFlagService.isFeatureEnabled( + FeatureFlagKey.IS_ATTACHMENT_MIGRATED, + workspaceId, + ); + + this.logger.log(`Migrating attachments for workspace ${workspaceId}`); + + if (isMigrated) { + this.logger.log(`Attachment migration already completed. Skipping...`); + + return; + } + + if (options.dryRun) { + this.logger.log( + `Would have migrated attachments for workspace ${workspaceId}. Skipping...`, + ); + + return; + } + + const queryRunner = this.coreDataSource.createQueryRunner(); + + await queryRunner.connect(); + await queryRunner.startTransaction(); + + try { + const schemaName = getWorkspaceSchemaName(workspaceId); + const tableName = 'attachment'; + + const customObjectMetadata = await this.objectMetadataRepository.find({ + where: { + workspaceId, + isCustom: true, + }, + }); + const customObjectMetadataNames = customObjectMetadata.map( + (objectMetadata) => objectMetadata.nameSingular, + ); + + const fieldNameMigrations = [ + { old: 'task', new: 'targetTask' }, + { old: 'note', new: 'targetNote' }, + { old: 'person', new: 'targetPerson' }, + { old: 'company', new: 'targetCompany' }, + { old: 'opportunity', new: 'targetOpportunity' }, + { old: 'dashboard', new: 'targetDashboard' }, + { old: 'workflow', new: 'targetWorkflow' }, + ...customObjectMetadataNames.map((customObjectName) => ({ + old: `${customObjectName}`, + new: `target${capitalize(customObjectName)}`, + })), + ]; + + const fieldMigrations = fieldNameMigrations.map( + ({ old: oldFieldName, new: newFieldName }) => ({ + old: `${oldFieldName}Id`, + new: `${newFieldName}Id`, + }), + ); + + const attachmentObjectMetadata = + await this.objectMetadataRepository.findOne({ + where: { + workspaceId, + nameSingular: 'attachment', + }, + relations: ['fields'], + }); + + if (!attachmentObjectMetadata) { + this.logger.error( + `🟥 Attachment object metadata not found for workspace ${workspaceId}`, + ); + + return; + } + + for (const { new: newField, old: oldField } of fieldMigrations) { + try { + await queryRunner.query( + `ALTER TABLE "${schemaName}"."${tableName}" + RENAME COLUMN "${oldField}" TO "${newField}"`, + ); + this.logger.log( + `Renamed column "${oldField}" to "${newField}" for "${tableName}"`, + ); + } catch (error) { + this.logger.error( + `Error renaming column "${oldField}" to "${newField}" for "${tableName}" in workspace ${workspaceId}`, + error, + ); + + throw error; + } + } + + this.logger.log(`✅ Successfully migrated attachment records`); + + const objectNamesToMigrate = fieldNameMigrations.map( + ({ old: oldFieldName }) => oldFieldName, + ); + + const relatedObjectMetadata = await this.objectMetadataRepository.find({ + where: { + workspaceId, + }, + }); + + const relatedObjectMetadataMap = new Map( + relatedObjectMetadata.map((obj) => [obj.nameSingular, obj]), + ); + + const morphId = ATTACHMENT_STANDARD_FIELD_IDS.targetMorphId; + + for (const objectName of objectNamesToMigrate) { + const relatedObject = relatedObjectMetadataMap.get(objectName); + + if (!relatedObject) { + this.logger.warn( + `Related object metadata not found for ${objectName} in workspace ${workspaceId}`, + ); + continue; + } + + const fieldToMigrate = attachmentObjectMetadata.fields.find( + (field) => + field.type === 'RELATION' && + field.relationTargetObjectMetadataId === relatedObject.id, + ); + + if (!fieldToMigrate) { + this.logger.log( + `No RELATION field found for ${objectName} in attachment`, + ); + continue; + } + + const newFieldName = `target${capitalize(fieldToMigrate.name)}`; + + const settings = { + ...fieldToMigrate.settings, + joinColumnName: computeMorphOrRelationFieldJoinColumnName({ + name: newFieldName, + }), + }; + + try { + const result = await queryRunner.query( + `UPDATE core."fieldMetadata" + SET name = $1, type = 'MORPH_RELATION', "morphId" = $3, settings = $4 + WHERE id = $2`, + [newFieldName, fieldToMigrate.id, morphId, settings], + ); + + const rowsUpdated = result[1] || 0; + + if (rowsUpdated > 0) { + this.logger.log( + `Updated fieldMetadata: ${fieldToMigrate.name} → ${newFieldName} (type: MORPH_RELATION)`, + ); + } + } catch (error) { + this.logger.error( + `Error updating fieldMetadata for ${objectName} in workspace ${workspaceId}`, + error, + ); + + throw error; + } + } + + this.logger.log(`✅ Successfully migrated attachment fieldmetadata`); + + await queryRunner.commitTransaction(); + + await this.featureFlagService.enableFeatureFlags( + [FeatureFlagKey.IS_ATTACHMENT_MIGRATED], + workspaceId, + ); + + await this.workspaceCacheService.invalidateAndRecompute(workspaceId, [ + 'flatFieldMetadataMaps', + 'flatObjectMetadataMaps', + 'featureFlagsMap', + ]); + + await this.workspaceMetadataVersionService.incrementMetadataVersion( + workspaceId, + ); + this.logger.log(`Cache flushed`); + + this.logger.log( + `Set IS_ATTACHMENT_MIGRATED feature flag for workspace ${workspaceId}`, + ); + + this.logger.log(`Flush cache for workspace ${workspaceId}`); + await this.workspaceCacheStorageService.flush(workspaceId); + } catch (error) { + await queryRunner.rollbackTransaction(); + this.logger.error( + `Error migrating attachment to morph relations (rolled transaction back on ${workspaceId})`, + error, + ); + throw error; + } finally { + await queryRunner.release(); + } + } +} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-17/1-17-upgrade-version-command.module.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-17/1-17-upgrade-version-command.module.ts new file mode 100644 index 0000000000..10df6f0620 --- /dev/null +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-17/1-17-upgrade-version-command.module.ts @@ -0,0 +1,40 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +import { MigrateAttachmentToMorphRelationsCommand } from 'src/database/commands/upgrade-version-command/1-17/1-17-migrate-attachment-to-morph-relations.command'; +import { ApplicationModule } from 'src/engine/core-modules/application/application.module'; +import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; +import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; +import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; +import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module'; +import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { FieldMetadataModule } from 'src/engine/metadata-modules/field-metadata/field-metadata.module'; +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 { 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'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([ + WorkspaceEntity, + ObjectMetadataEntity, + FieldMetadataEntity, + FeatureFlagEntity, + AttachmentWorkspaceEntity, + ]), + DataSourceModule, + WorkspaceCacheStorageModule, + WorkspaceMetadataVersionModule, + FeatureFlagModule, + WorkspaceCacheModule, + FieldMetadataModule, + ObjectMetadataModule, + ApplicationModule, + ], + providers: [MigrateAttachmentToMorphRelationsCommand], + exports: [MigrateAttachmentToMorphRelationsCommand], +}) +export class V1_17_UpgradeVersionCommandModule {} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade-version-command.module.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade-version-command.module.ts index 9eff74051a..6cfcea8aac 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade-version-command.module.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade-version-command.module.ts @@ -5,6 +5,7 @@ import { V1_13_UpgradeVersionCommandModule } from 'src/database/commands/upgrade import { V1_14_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-14/1-14-upgrade-version-command.module'; import { V1_15_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-15/1-15-upgrade-version-command.module'; import { V1_16_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-16/1-16-upgrade-version-command.module'; +import { V1_17_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-17/1-17-upgrade-version-command.module'; import { UpgradeCommand } from 'src/database/commands/upgrade-version-command/upgrade.command'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module'; @@ -16,6 +17,7 @@ import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-s V1_14_UpgradeVersionCommandModule, V1_15_UpgradeVersionCommandModule, V1_16_UpgradeVersionCommandModule, + V1_17_UpgradeVersionCommandModule, DataSourceModule, ], providers: [UpgradeCommand], diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade.command.ts index c96eb18a05..09fc327337 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade.command.ts @@ -45,6 +45,7 @@ import { MakeViewFilterUniversalIdentifierAndApplicationIdNotNullableMigrationCo import { MakeViewGroupUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-make-view-group-universal-identifier-and-application-id-not-nullable-migration.command'; import { MakeViewUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-make-view-universal-identifier-and-application-id-not-nullable-migration.command'; import { UpdateTaskOnDeleteActionCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-update-task-on-delete-action.command'; +import { MigrateAttachmentToMorphRelationsCommand } from 'src/database/commands/upgrade-version-command/1-17/1-17-migrate-attachment-to-morph-relations.command'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; @@ -107,6 +108,9 @@ export class UpgradeCommand extends UpgradeCommandRunner { protected readonly makeIndexMetadataUniversalIdentifierAndApplicationIdNotNullableMigrationCommand: MakeIndexMetadataUniversalIdentifierAndApplicationIdNotNullableMigrationCommand, protected readonly identifyRemainingEntitiesMetadataCommand: IdentifyRemainingEntitiesMetadataCommand, protected readonly makeRemainingEntitiesUniversalIdentifierAndApplicationIdNotNullableMigrationCommand: MakeRemainingEntitiesUniversalIdentifierAndApplicationIdNotNullableMigrationCommand, + + // 1.17 Commands + protected readonly migrateAttachmentToMorphRelationsCommand: MigrateAttachmentToMorphRelationsCommand, ) { super( workspaceRepository, @@ -176,12 +180,17 @@ export class UpgradeCommand extends UpgradeCommandRunner { .makeRemainingEntitiesUniversalIdentifierAndApplicationIdNotNullableMigrationCommand, ]; + const commands_1170: VersionCommands = [ + this.migrateAttachmentToMorphRelationsCommand, + ]; + this.allCommands = { '1.12.0': commands_1120, '1.13.0': commands_1130, '1.14.0': commands_1140, '1.15.0': commands_1150, '1.16.0': commands_1160, + '1.17.0': commands_1170, }; } diff --git a/packages/twenty-server/src/engine/core-modules/feature-flag/enums/feature-flag-key.enum.ts b/packages/twenty-server/src/engine/core-modules/feature-flag/enums/feature-flag-key.enum.ts index 1da6db90fe..4710bdae20 100644 --- a/packages/twenty-server/src/engine/core-modules/feature-flag/enums/feature-flag-key.enum.ts +++ b/packages/twenty-server/src/engine/core-modules/feature-flag/enums/feature-flag-key.enum.ts @@ -9,6 +9,7 @@ export enum FeatureFlagKey { 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_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED = 'IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED', IS_JUNCTION_RELATIONS_ENABLED = 'IS_JUNCTION_RELATIONS_ENABLED', IS_SSE_DB_EVENTS_ENABLED = 'IS_SSE_DB_EVENTS_ENABLED', diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/build-default-relation-flat-field-metadatas-for-custom-object.util.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/build-default-relation-flat-field-metadatas-for-custom-object.util.ts index c553ad4c40..949cbe6223 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/build-default-relation-flat-field-metadatas-for-custom-object.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/build-default-relation-flat-field-metadatas-for-custom-object.util.ts @@ -18,6 +18,7 @@ import { } from 'src/engine/metadata-modules/object-metadata/object-metadata.exception'; import { CUSTOM_OBJECT_STANDARD_FIELD_IDS, + ATTACHMENT_STANDARD_FIELD_IDS, TIMELINE_ACTIVITY_STANDARD_FIELD_IDS, } from 'src/engine/workspace-manager/workspace-migration/constant/standard-field-ids'; import { STANDARD_OBJECT_ICONS } from 'src/engine/workspace-manager/workspace-migration/constant/standard-object-icons'; @@ -33,7 +34,7 @@ const DEFAULT_RELATIONS_OBJECTS_STANDARD_IDS = [ const morphIdByRelationObjectNameSingular = { timelineActivity: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.targetMorphId, favorite: null, - attachment: null, + attachment: ATTACHMENT_STANDARD_FIELD_IDS.targetMorphId, noteTarget: null, taskTarget: null, } satisfies Record< @@ -96,7 +97,9 @@ export const buildDefaultRelationFlatFieldMetadatasForCustomObject = ({ (objectMetadataNameSingular === 'timelineActivity' && existingFeatureFlagsMap[ FeatureFlagKey.IS_TIMELINE_ACTIVITY_MIGRATED - ]) ?? + ]) || + (objectMetadataNameSingular === 'attachment' && + existingFeatureFlagsMap[FeatureFlagKey.IS_ATTACHMENT_MIGRATED]) || false; const isObjectMigratedToMorphRelations = isObjectMigratedFromOlderReleases || isFeatureFlagEnabled; diff --git a/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts b/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts index 907aef2847..7ad06902ce 100644 --- a/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts +++ b/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts @@ -216,6 +216,7 @@ describe('WorkspaceEntityManager', () => { IS_PUBLIC_DOMAIN_ENABLED: false, IS_EMAILING_DOMAIN_ENABLED: false, IS_DASHBOARD_V2_ENABLED: false, + IS_ATTACHMENT_MIGRATED: false, IS_TIMELINE_ACTIVITY_MIGRATED: false, IS_GLOBAL_WORKSPACE_DATASOURCE_ENABLED: false, IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED: false, diff --git a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/seed-feature-flags.util.ts b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/seed-feature-flags.util.ts index b67eaa6a0e..3c9b53d1ef 100644 --- a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/seed-feature-flags.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/seed-feature-flags.util.ts @@ -61,6 +61,11 @@ export const seedFeatureFlags = async ({ workspaceId: workspaceId, value: true, }, + { + key: FeatureFlagKey.IS_ATTACHMENT_MIGRATED, + workspaceId: workspaceId, + value: true, + }, { key: FeatureFlagKey.IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED, workspaceId: workspaceId, diff --git a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/constants/attachment-data-seeds.constant.ts b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/constants/attachment-data-seeds.constant.ts index 7cad4d5945..35095c0541 100644 --- a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/constants/attachment-data-seeds.constant.ts +++ b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/constants/attachment-data-seeds.constant.ts @@ -18,11 +18,11 @@ type AttachmentDataSeed = { updatedBySource: string; updatedByWorkspaceMemberId: string; updatedByName: string; - personId: string | null; - companyId: string | null; - noteId: string | null; - taskId: string | null; - opportunityId: string | null; + targetPersonId: string | null; + targetCompanyId: string | null; + targetNoteId: string | null; + targetTaskId: string | null; + targetOpportunityId: string | null; }; export const ATTACHMENT_DATA_SEED_COLUMNS: (keyof AttachmentDataSeed)[] = [ @@ -36,11 +36,11 @@ export const ATTACHMENT_DATA_SEED_COLUMNS: (keyof AttachmentDataSeed)[] = [ 'updatedBySource', 'updatedByWorkspaceMemberId', 'updatedByName', - 'personId', - 'companyId', - 'noteId', - 'taskId', - 'opportunityId', + 'targetPersonId', + 'targetCompanyId', + 'targetNoteId', + 'targetTaskId', + 'targetOpportunityId', ]; const GENERATE_ATTACHMENT_IDS = (): Record => { @@ -219,33 +219,34 @@ const GENERATE_ATTACHMENT_SEEDS = (): AttachmentDataSeed[] => { // Determine which entity this attachment belongs to // Distribution: ~30% person, ~30% company, ~20% note, ~15% task, ~5% opportunity - let personId: string | null = null; - let companyId: string | null = null; - let noteId: string | null = null; - let taskId: string | null = null; - let opportunityId: string | null = null; + let targetPersonId: string | null = null; + let targetCompanyId: string | null = null; + let targetNoteId: string | null = null; + let targetTaskId: string | null = null; + let targetOpportunityId: string | null = null; const DISTRIBUTION_VALUE = INDEX % 100; if (DISTRIBUTION_VALUE < 30) { // 30% Person attachments - personId = PERSON_IDS[entityIndex % PERSON_IDS.length]; + targetPersonId = PERSON_IDS[entityIndex % PERSON_IDS.length]; entityIndex++; } else if (DISTRIBUTION_VALUE < 60) { // 30% Company attachments - companyId = COMPANY_IDS[entityIndex % COMPANY_IDS.length]; + targetCompanyId = COMPANY_IDS[entityIndex % COMPANY_IDS.length]; entityIndex++; } else if (DISTRIBUTION_VALUE < 80) { // 20% Note attachments - noteId = NOTE_IDS[entityIndex % NOTE_IDS.length]; + targetNoteId = NOTE_IDS[entityIndex % NOTE_IDS.length]; entityIndex++; } else if (DISTRIBUTION_VALUE < 95) { // 15% Task attachments - taskId = TASK_IDS[entityIndex % TASK_IDS.length]; + targetTaskId = TASK_IDS[entityIndex % TASK_IDS.length]; entityIndex++; } else { // 5% Opportunity attachments - opportunityId = OPPORTUNITY_IDS[entityIndex % OPPORTUNITY_IDS.length]; + targetOpportunityId = + OPPORTUNITY_IDS[entityIndex % OPPORTUNITY_IDS.length]; entityIndex++; } @@ -260,11 +261,11 @@ const GENERATE_ATTACHMENT_SEEDS = (): AttachmentDataSeed[] => { updatedBySource: FieldActorSource.MANUAL, updatedByWorkspaceMemberId: WORKSPACE_MEMBER_DATA_SEED_IDS.TIM, updatedByName: 'Tim A', - personId, - companyId, - noteId, - taskId, - opportunityId, + targetPersonId, + targetCompanyId, + targetNoteId, + targetTaskId, + targetOpportunityId, }); } diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/constants/standard-object.constant.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/constants/standard-object.constant.ts index 30b15486eb..268f3e00da 100644 --- a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/constants/standard-object.constant.ts +++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/constants/standard-object.constant.ts @@ -57,17 +57,27 @@ export const STANDARD_OBJECTS = { updatedBy: { universalIdentifier: ATTACHMENT_STANDARD_FIELD_IDS.updatedBy, }, - task: { universalIdentifier: ATTACHMENT_STANDARD_FIELD_IDS.task }, - note: { universalIdentifier: ATTACHMENT_STANDARD_FIELD_IDS.note }, - person: { universalIdentifier: ATTACHMENT_STANDARD_FIELD_IDS.person }, - company: { universalIdentifier: ATTACHMENT_STANDARD_FIELD_IDS.company }, - opportunity: { - universalIdentifier: ATTACHMENT_STANDARD_FIELD_IDS.opportunity, + targetTask: { + universalIdentifier: ATTACHMENT_STANDARD_FIELD_IDS.targetTask, }, - dashboard: { - universalIdentifier: ATTACHMENT_STANDARD_FIELD_IDS.dashboard, + targetNote: { + universalIdentifier: ATTACHMENT_STANDARD_FIELD_IDS.targetNote, + }, + targetPerson: { + universalIdentifier: ATTACHMENT_STANDARD_FIELD_IDS.targetPerson, + }, + targetCompany: { + universalIdentifier: ATTACHMENT_STANDARD_FIELD_IDS.targetCompany, + }, + targetOpportunity: { + universalIdentifier: ATTACHMENT_STANDARD_FIELD_IDS.targetOpportunity, + }, + targetDashboard: { + universalIdentifier: ATTACHMENT_STANDARD_FIELD_IDS.targetDashboard, + }, + targetWorkflow: { + universalIdentifier: ATTACHMENT_STANDARD_FIELD_IDS.targetWorkflow, }, - workflow: { universalIdentifier: ATTACHMENT_STANDARD_FIELD_IDS.workflow }, }, indexes: { taskIdIndex: { diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/__tests__/__snapshots__/get-standard-object-metadata-related-entity-ids.util.spec.ts.snap b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/__tests__/__snapshots__/get-standard-object-metadata-related-entity-ids.util.spec.ts.snap index 4e9ff9250d..c2bb9d6f8f 100644 --- a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/__tests__/__snapshots__/get-standard-object-metadata-related-entity-ids.util.spec.ts.snap +++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/__tests__/__snapshots__/get-standard-object-metadata-related-entity-ids.util.spec.ts.snap @@ -1,21 +1,15 @@ -// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing +// Jest Snapshot v1, https://goo.gl/fbAQLP exports[`getStandardObjectMetadataRelatedEntityIds should return standard object metadata related entity ids 1`] = ` { "attachment": { "fields": { - "company": { - "id": "00000000-0000-0000-0000-000000000013", - }, "createdAt": { "id": "00000000-0000-0000-0000-000000000002", }, "createdBy": { "id": "00000000-0000-0000-0000-000000000008", }, - "dashboard": { - "id": "00000000-0000-0000-0000-000000000015", - }, "deletedAt": { "id": "00000000-0000-0000-0000-000000000004", }, @@ -31,27 +25,33 @@ exports[`getStandardObjectMetadataRelatedEntityIds should return standard object "name": { "id": "00000000-0000-0000-0000-000000000005", }, - "note": { + "targetCompany": { + "id": "00000000-0000-0000-0000-000000000013", + }, + "targetDashboard": { + "id": "00000000-0000-0000-0000-000000000015", + }, + "targetNote": { "id": "00000000-0000-0000-0000-000000000011", }, - "opportunity": { + "targetOpportunity": { "id": "00000000-0000-0000-0000-000000000014", }, - "person": { + "targetPerson": { "id": "00000000-0000-0000-0000-000000000012", }, - "task": { + "targetTask": { "id": "00000000-0000-0000-0000-000000000010", }, + "targetWorkflow": { + "id": "00000000-0000-0000-0000-000000000016", + }, "updatedAt": { "id": "00000000-0000-0000-0000-000000000003", }, "updatedBy": { "id": "00000000-0000-0000-0000-000000000009", }, - "workflow": { - "id": "00000000-0000-0000-0000-000000000016", - }, }, "id": "00000000-0000-0000-0000-000000000017", "views": undefined, diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-attachment-standard-flat-field-metadata.util.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-attachment-standard-flat-field-metadata.util.ts index 7175aec8d5..5e9ffc8885 100644 --- a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-attachment-standard-flat-field-metadata.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-attachment-standard-flat-field-metadata.util.ts @@ -6,6 +6,7 @@ import { } from 'twenty-shared/types'; import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { ATTACHMENT_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-migration/constant/standard-field-ids'; import { type AllStandardObjectFieldName } from 'src/engine/workspace-manager/twenty-standard-application/types/all-standard-object-field-name.type'; import { type CreateStandardFieldArgs, @@ -231,13 +232,13 @@ export const buildAttachmentStandardFlatFieldMetadatas = ({ }), // Relation fields - task: createStandardRelationFieldFlatMetadata({ + targetTask: createStandardRelationFieldFlatMetadata({ objectName, workspaceId, context: { - type: FieldMetadataType.RELATION, - morphId: null, - fieldName: 'task', + type: FieldMetadataType.MORPH_RELATION, + morphId: ATTACHMENT_STANDARD_FIELD_IDS.targetMorphId, + fieldName: 'targetTask', label: 'Task', description: 'Attachment task', icon: 'IconNotes', @@ -248,7 +249,7 @@ export const buildAttachmentStandardFlatFieldMetadatas = ({ settings: { relationType: RelationType.MANY_TO_ONE, onDelete: RelationOnDeleteAction.SET_NULL, - joinColumnName: 'taskId', + joinColumnName: 'targetTaskId', }, }, standardObjectMetadataRelatedEntityIds, @@ -256,13 +257,13 @@ export const buildAttachmentStandardFlatFieldMetadatas = ({ twentyStandardApplicationId, now, }), - note: createStandardRelationFieldFlatMetadata({ + targetNote: createStandardRelationFieldFlatMetadata({ objectName, workspaceId, context: { - type: FieldMetadataType.RELATION, - morphId: null, - fieldName: 'note', + type: FieldMetadataType.MORPH_RELATION, + morphId: ATTACHMENT_STANDARD_FIELD_IDS.targetMorphId, + fieldName: 'targetNote', label: 'Note', description: 'Attachment note', icon: 'IconNotes', @@ -273,7 +274,7 @@ export const buildAttachmentStandardFlatFieldMetadatas = ({ settings: { relationType: RelationType.MANY_TO_ONE, onDelete: RelationOnDeleteAction.SET_NULL, - joinColumnName: 'noteId', + joinColumnName: 'targetNoteId', }, }, standardObjectMetadataRelatedEntityIds, @@ -281,13 +282,13 @@ export const buildAttachmentStandardFlatFieldMetadatas = ({ twentyStandardApplicationId, now, }), - person: createStandardRelationFieldFlatMetadata({ + targetPerson: createStandardRelationFieldFlatMetadata({ objectName, workspaceId, context: { - type: FieldMetadataType.RELATION, - morphId: null, - fieldName: 'person', + type: FieldMetadataType.MORPH_RELATION, + morphId: ATTACHMENT_STANDARD_FIELD_IDS.targetMorphId, + fieldName: 'targetPerson', label: 'Person', description: 'Attachment person', icon: 'IconUser', @@ -298,7 +299,7 @@ export const buildAttachmentStandardFlatFieldMetadatas = ({ settings: { relationType: RelationType.MANY_TO_ONE, onDelete: RelationOnDeleteAction.CASCADE, - joinColumnName: 'personId', + joinColumnName: 'targetPersonId', }, }, standardObjectMetadataRelatedEntityIds, @@ -306,13 +307,13 @@ export const buildAttachmentStandardFlatFieldMetadatas = ({ twentyStandardApplicationId, now, }), - company: createStandardRelationFieldFlatMetadata({ + targetCompany: createStandardRelationFieldFlatMetadata({ objectName, workspaceId, context: { - type: FieldMetadataType.RELATION, - morphId: null, - fieldName: 'company', + type: FieldMetadataType.MORPH_RELATION, + morphId: ATTACHMENT_STANDARD_FIELD_IDS.targetMorphId, + fieldName: 'targetCompany', label: 'Company', description: 'Attachment company', icon: 'IconBuildingSkyscraper', @@ -323,7 +324,7 @@ export const buildAttachmentStandardFlatFieldMetadatas = ({ settings: { relationType: RelationType.MANY_TO_ONE, onDelete: RelationOnDeleteAction.CASCADE, - joinColumnName: 'companyId', + joinColumnName: 'targetCompanyId', }, }, standardObjectMetadataRelatedEntityIds, @@ -331,13 +332,13 @@ export const buildAttachmentStandardFlatFieldMetadatas = ({ twentyStandardApplicationId, now, }), - opportunity: createStandardRelationFieldFlatMetadata({ + targetOpportunity: createStandardRelationFieldFlatMetadata({ objectName, workspaceId, context: { - type: FieldMetadataType.RELATION, - morphId: null, - fieldName: 'opportunity', + type: FieldMetadataType.MORPH_RELATION, + morphId: ATTACHMENT_STANDARD_FIELD_IDS.targetMorphId, + fieldName: 'targetOpportunity', label: 'Opportunity', description: 'Attachment opportunity', icon: 'IconBuildingSkyscraper', @@ -348,7 +349,7 @@ export const buildAttachmentStandardFlatFieldMetadatas = ({ settings: { relationType: RelationType.MANY_TO_ONE, onDelete: RelationOnDeleteAction.CASCADE, - joinColumnName: 'opportunityId', + joinColumnName: 'targetOpportunityId', }, }, standardObjectMetadataRelatedEntityIds, @@ -356,13 +357,13 @@ export const buildAttachmentStandardFlatFieldMetadatas = ({ twentyStandardApplicationId, now, }), - dashboard: createStandardRelationFieldFlatMetadata({ + targetDashboard: createStandardRelationFieldFlatMetadata({ objectName, workspaceId, context: { - type: FieldMetadataType.RELATION, - morphId: null, - fieldName: 'dashboard', + type: FieldMetadataType.MORPH_RELATION, + morphId: ATTACHMENT_STANDARD_FIELD_IDS.targetMorphId, + fieldName: 'targetDashboard', label: 'Dashboard', description: 'Attachment dashboard', icon: 'IconLayout', @@ -373,7 +374,7 @@ export const buildAttachmentStandardFlatFieldMetadatas = ({ settings: { relationType: RelationType.MANY_TO_ONE, onDelete: RelationOnDeleteAction.CASCADE, - joinColumnName: 'dashboardId', + joinColumnName: 'targetDashboardId', }, }, standardObjectMetadataRelatedEntityIds, @@ -381,13 +382,13 @@ export const buildAttachmentStandardFlatFieldMetadatas = ({ twentyStandardApplicationId, now, }), - workflow: createStandardRelationFieldFlatMetadata({ + targetWorkflow: createStandardRelationFieldFlatMetadata({ objectName, workspaceId, context: { - type: FieldMetadataType.RELATION, - morphId: null, - fieldName: 'workflow', + type: FieldMetadataType.MORPH_RELATION, + morphId: ATTACHMENT_STANDARD_FIELD_IDS.targetMorphId, + fieldName: 'targetWorkflow', label: 'Workflow', description: 'Attachment workflow', icon: 'IconSettingsAutomation', @@ -398,7 +399,7 @@ export const buildAttachmentStandardFlatFieldMetadatas = ({ settings: { relationType: RelationType.MANY_TO_ONE, onDelete: RelationOnDeleteAction.CASCADE, - joinColumnName: 'workflowId', + joinColumnName: 'targetWorkflowId', }, }, standardObjectMetadataRelatedEntityIds, diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-company-standard-flat-field-metadata.util.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-company-standard-flat-field-metadata.util.ts index 0942ab400c..099ad9e741 100644 --- a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-company-standard-flat-field-metadata.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-company-standard-flat-field-metadata.util.ts @@ -484,7 +484,7 @@ export const buildCompanyStandardFlatFieldMetadatas = ({ isSystem: true, isNullable: true, targetObjectName: 'attachment', - targetFieldName: 'company', + targetFieldName: 'targetCompany', settings: { relationType: RelationType.ONE_TO_MANY, }, diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-dashboard-standard-flat-field-metadata.util.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-dashboard-standard-flat-field-metadata.util.ts index 406623fa57..62f7311dd0 100644 --- a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-dashboard-standard-flat-field-metadata.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-dashboard-standard-flat-field-metadata.util.ts @@ -282,7 +282,7 @@ export const buildDashboardStandardFlatFieldMetadatas = ({ isSystem: true, isNullable: true, targetObjectName: 'attachment', - targetFieldName: 'dashboard', + targetFieldName: 'targetDashboard', settings: { relationType: RelationType.ONE_TO_MANY, }, diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-note-standard-flat-field-metadata.util.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-note-standard-flat-field-metadata.util.ts index 25f8d1422e..a0b56e6632 100644 --- a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-note-standard-flat-field-metadata.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-note-standard-flat-field-metadata.util.ts @@ -264,7 +264,7 @@ export const buildNoteStandardFlatFieldMetadatas = ({ isSystem: true, isNullable: true, targetObjectName: 'attachment', - targetFieldName: 'note', + targetFieldName: 'targetNote', settings: { relationType: RelationType.ONE_TO_MANY, }, diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-opportunity-standard-flat-field-metadata.util.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-opportunity-standard-flat-field-metadata.util.ts index 43bcf6be8a..8874454a72 100644 --- a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-opportunity-standard-flat-field-metadata.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-opportunity-standard-flat-field-metadata.util.ts @@ -409,7 +409,7 @@ export const buildOpportunityStandardFlatFieldMetadatas = ({ isSystem: true, isNullable: true, targetObjectName: 'attachment', - targetFieldName: 'opportunity', + targetFieldName: 'targetOpportunity', settings: { relationType: RelationType.ONE_TO_MANY, }, diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-person-standard-flat-field-metadata.util.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-person-standard-flat-field-metadata.util.ts index 623b49fec3..04d14fd736 100644 --- a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-person-standard-flat-field-metadata.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-person-standard-flat-field-metadata.util.ts @@ -439,7 +439,7 @@ export const buildPersonStandardFlatFieldMetadatas = ({ isSystem: true, isNullable: true, targetObjectName: 'attachment', - targetFieldName: 'person', + targetFieldName: 'targetPerson', settings: { relationType: RelationType.ONE_TO_MANY, }, diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-task-standard-flat-field-metadata.util.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-task-standard-flat-field-metadata.util.ts index f371e0c6e3..97f78602e9 100644 --- a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-task-standard-flat-field-metadata.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-task-standard-flat-field-metadata.util.ts @@ -308,7 +308,7 @@ export const buildTaskStandardFlatFieldMetadatas = ({ isSystem: true, isNullable: true, targetObjectName: 'attachment', - targetFieldName: 'task', + targetFieldName: 'targetTask', settings: { relationType: RelationType.ONE_TO_MANY, }, diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-workflow-standard-flat-field-metadata.util.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-workflow-standard-flat-field-metadata.util.ts index b0a0f11f6a..9bdcd6e116 100644 --- a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-workflow-standard-flat-field-metadata.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-workflow-standard-flat-field-metadata.util.ts @@ -374,7 +374,7 @@ export const buildWorkflowStandardFlatFieldMetadatas = ({ isSystem: true, isNullable: false, targetObjectName: 'attachment', - targetFieldName: 'workflow', + targetFieldName: 'targetWorkflow', settings: { relationType: RelationType.ONE_TO_MANY, }, diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/index/compute-attachment-standard-flat-index-metadata.util.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/index/compute-attachment-standard-flat-index-metadata.util.ts index 503186457f..121504fa4f 100644 --- a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/index/compute-attachment-standard-flat-index-metadata.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/index/compute-attachment-standard-flat-index-metadata.util.ts @@ -21,7 +21,7 @@ export const buildAttachmentStandardFlatIndexMetadatas = ({ workspaceId, context: { indexName: 'taskIdIndex', - relatedFieldNames: ['task'], + relatedFieldNames: ['targetTask'], }, standardObjectMetadataRelatedEntityIds, dependencyFlatEntityMaps, @@ -33,7 +33,7 @@ export const buildAttachmentStandardFlatIndexMetadatas = ({ workspaceId, context: { indexName: 'noteIdIndex', - relatedFieldNames: ['note'], + relatedFieldNames: ['targetNote'], }, standardObjectMetadataRelatedEntityIds, dependencyFlatEntityMaps, @@ -45,7 +45,7 @@ export const buildAttachmentStandardFlatIndexMetadatas = ({ workspaceId, context: { indexName: 'personIdIndex', - relatedFieldNames: ['person'], + relatedFieldNames: ['targetPerson'], }, standardObjectMetadataRelatedEntityIds, dependencyFlatEntityMaps, @@ -57,7 +57,7 @@ export const buildAttachmentStandardFlatIndexMetadatas = ({ workspaceId, context: { indexName: 'companyIdIndex', - relatedFieldNames: ['company'], + relatedFieldNames: ['targetCompany'], }, standardObjectMetadataRelatedEntityIds, dependencyFlatEntityMaps, @@ -69,7 +69,7 @@ export const buildAttachmentStandardFlatIndexMetadatas = ({ workspaceId, context: { indexName: 'opportunityIdIndex', - relatedFieldNames: ['opportunity'], + relatedFieldNames: ['targetOpportunity'], }, standardObjectMetadataRelatedEntityIds, dependencyFlatEntityMaps, @@ -81,7 +81,7 @@ export const buildAttachmentStandardFlatIndexMetadatas = ({ workspaceId, context: { indexName: 'dashboardIdIndex', - relatedFieldNames: ['dashboard'], + relatedFieldNames: ['targetDashboard'], }, standardObjectMetadataRelatedEntityIds, dependencyFlatEntityMaps, @@ -93,7 +93,7 @@ export const buildAttachmentStandardFlatIndexMetadatas = ({ workspaceId, context: { indexName: 'workflowIdIndex', - relatedFieldNames: ['workflow'], + relatedFieldNames: ['targetWorkflow'], }, standardObjectMetadataRelatedEntityIds, dependencyFlatEntityMaps, diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/constant/default-feature-flags.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/constant/default-feature-flags.ts index ea92f74cd1..48651d321c 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/constant/default-feature-flags.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/constant/default-feature-flags.ts @@ -2,4 +2,5 @@ import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/featu export const DEFAULT_FEATURE_FLAGS = [ FeatureFlagKey.IS_TIMELINE_ACTIVITY_MIGRATED, + FeatureFlagKey.IS_ATTACHMENT_MIGRATED, ] as const satisfies FeatureFlagKey[]; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/constant/standard-field-ids.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/constant/standard-field-ids.ts index 3c4c2fc41a..4c717c6377 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/constant/standard-field-ids.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/constant/standard-field-ids.ts @@ -38,14 +38,15 @@ export const ATTACHMENT_STANDARD_FIELD_IDS = { updatedBy: '376239d1-3e65-4cb6-b5d8-e0917d43cc93', author: '20202020-6501-4ac5-a4ef-b2f8522ef6cd', activity: '20202020-b569-481b-a13f-9b94e47e54fe', - task: '20202020-51e5-4621-9cf8-215487951c4b', - note: '20202020-4f4b-4503-a6fc-6b982f3dffb5', - person: '20202020-0158-4aa2-965c-5cdafe21ffa2', - company: '20202020-ceab-4a28-b546-73b06b4c08d5', - opportunity: '20202020-7374-499d-bea3-9354890755b5', - dashboard: '20202020-5324-43f3-9dbf-1a33e7de0ce6', - workflow: '20202020-f1e8-4c9d-8a7b-3f5e1d2c9a8b', - custom: '20202020-302d-43b3-9aea-aa4f89282a9f', + targetTask: '20202020-51e5-4621-9cf8-215487951c4b', + targetNote: '20202020-4f4b-4503-a6fc-6b982f3dffb5', + targetPerson: '20202020-0158-4aa2-965c-5cdafe21ffa2', + targetCompany: '20202020-ceab-4a28-b546-73b06b4c08d5', + targetOpportunity: '20202020-7374-499d-bea3-9354890755b5', + targetDashboard: '20202020-5324-43f3-9dbf-1a33e7de0ce6', + targetWorkflow: '20202020-f1e8-4c9d-8a7b-3f5e1d2c9a8b', + targetCustom: '20202020-302d-43b3-9aea-aa4f89282a9f', + targetMorphId: '20202020-f634-435d-ab8d-e1168b375c69', } as const; export const BASE_OBJECT_STANDARD_FIELD_IDS = { diff --git a/packages/twenty-server/src/modules/attachment/standard-objects/attachment.workspace-entity.ts b/packages/twenty-server/src/modules/attachment/standard-objects/attachment.workspace-entity.ts index b76c5c91e8..34200a08a7 100644 --- a/packages/twenty-server/src/modules/attachment/standard-objects/attachment.workspace-entity.ts +++ b/packages/twenty-server/src/modules/attachment/standard-objects/attachment.workspace-entity.ts @@ -23,19 +23,19 @@ export class AttachmentWorkspaceEntity extends BaseWorkspaceEntity { /** @deprecated */ author: EntityRelation | null; authorId: string | null; - task: EntityRelation | null; - taskId: string | null; - note: EntityRelation | null; - noteId: string | null; - person: EntityRelation | null; - personId: string | null; - company: EntityRelation | null; - companyId: string | null; - opportunity: EntityRelation | null; - opportunityId: string | null; - dashboard: EntityRelation | null; - dashboardId: string | null; - workflow: EntityRelation | null; - workflowId: string | null; + targetTask: EntityRelation | null; + targetTaskId: string | null; + targetNote: EntityRelation | null; + targetNoteId: string | null; + targetPerson: EntityRelation | null; + targetPersonId: string | null; + targetCompany: EntityRelation | null; + targetCompanyId: string | null; + targetOpportunity: EntityRelation | null; + targetOpportunityId: string | null; + targetDashboard: EntityRelation | null; + targetDashboardId: string | null; + targetWorkflow: EntityRelation | null; + targetWorkflowId: string | null; custom: EntityRelation; } diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/attachments.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/attachments.integration-spec.ts index 6a1c1a99ef..6a036eee63 100644 --- a/packages/twenty-server/test/integration/graphql/suites/object-generated/attachments.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/attachments.integration-spec.ts @@ -16,13 +16,13 @@ describe('attachmentsResolver (e2e)', () => { createdAt updatedAt deletedAt - taskId - noteId - personId - companyId - opportunityId - petId - surveyResultId + targetTaskId + targetNoteId + targetPersonId + targetCompanyId + targetOpportunityId + targetPetId + targetSurveyResultId } } } @@ -56,13 +56,13 @@ describe('attachmentsResolver (e2e)', () => { expect(attachments).toHaveProperty('createdAt'); expect(attachments).toHaveProperty('updatedAt'); expect(attachments).toHaveProperty('deletedAt'); - expect(attachments).toHaveProperty('taskId'); - expect(attachments).toHaveProperty('noteId'); - expect(attachments).toHaveProperty('personId'); - expect(attachments).toHaveProperty('companyId'); - expect(attachments).toHaveProperty('opportunityId'); - expect(attachments).toHaveProperty('petId'); - expect(attachments).toHaveProperty('surveyResultId'); + expect(attachments).toHaveProperty('targetTaskId'); + expect(attachments).toHaveProperty('targetNoteId'); + expect(attachments).toHaveProperty('targetPersonId'); + expect(attachments).toHaveProperty('targetCompanyId'); + expect(attachments).toHaveProperty('targetOpportunityId'); + expect(attachments).toHaveProperty('targetPetId'); + expect(attachments).toHaveProperty('targetSurveyResultId'); } }); }); diff --git a/packages/twenty-server/test/integration/metadata/suites/object-metadata/__snapshots__/failing-create-one-object-metadata-v2.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/object-metadata/__snapshots__/failing-create-one-object-metadata-v2.integration-spec.ts.snap index f0a6a9d6ec..043abfe4f1 100644 --- a/packages/twenty-server/test/integration/metadata/suites/object-metadata/__snapshots__/failing-create-one-object-metadata-v2.integration-spec.ts.snap +++ b/packages/twenty-server/test/integration/metadata/suites/object-metadata/__snapshots__/failing-create-one-object-metadata-v2.integration-spec.ts.snap @@ -304,7 +304,7 @@ exports[`Object metadata creation should fail v2 when labelPlural contains only ], "flatEntityMinimalInformation": { "id": Any, - "name": "listinga", + "name": "targetListinga", "objectMetadataId": Any, "universalIdentifier": Any, }, @@ -1011,7 +1011,7 @@ exports[`Object metadata creation should fail v2 when labelPlural exceeds maximu ], "flatEntityMinimalInformation": { "id": Any, - "name": "listinga", + "name": "targetListinga", "objectMetadataId": Any, "universalIdentifier": Any, }, @@ -1729,7 +1729,7 @@ exports[`Object metadata creation should fail v2 when labelSingular contains onl ], "flatEntityMinimalInformation": { "id": Any, - "name": "listinga", + "name": "targetListinga", "objectMetadataId": Any, "universalIdentifier": Any, }, @@ -2436,7 +2436,7 @@ exports[`Object metadata creation should fail v2 when labelSingular exceeds maxi ], "flatEntityMinimalInformation": { "id": Any, - "name": "listinga", + "name": "targetListinga", "objectMetadataId": Any, "universalIdentifier": Any, }, @@ -3154,7 +3154,7 @@ exports[`Object metadata creation should fail v2 when labels are identical 1`] = ], "flatEntityMinimalInformation": { "id": Any, - "name": "listinga", + "name": "targetListinga", "objectMetadataId": Any, "universalIdentifier": Any, }, @@ -3861,7 +3861,7 @@ exports[`Object metadata creation should fail v2 when labels with whitespaces re ], "flatEntityMinimalInformation": { "id": Any, - "name": "listinga", + "name": "targetListinga", "objectMetadataId": Any, "universalIdentifier": Any, }, @@ -4576,7 +4576,7 @@ exports[`Object metadata creation should fail v2 when name exceeds maximum lengt "code": "INVALID_FIELD_INPUT", "message": "Name is too long", "userFriendlyMessage": "Name is too long", - "value": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "value": "targetAaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", }, { "code": "OBJECT_METADATA_NOT_FOUND", @@ -4586,7 +4586,7 @@ exports[`Object metadata creation should fail v2 when name exceeds maximum lengt ], "flatEntityMinimalInformation": { "id": Any, - "name": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "name": "targetAaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "objectMetadataId": Any, "universalIdentifier": Any, }, @@ -5305,7 +5305,7 @@ exports[`Object metadata creation should fail v2 when namePlural has invalid cha ], "flatEntityMinimalInformation": { "id": Any, - "name": "listinga", + "name": "targetListinga", "objectMetadataId": Any, "universalIdentifier": Any, }, @@ -6012,7 +6012,7 @@ exports[`Object metadata creation should fail v2 when namePlural is a reserved k ], "flatEntityMinimalInformation": { "id": Any, - "name": "listinga", + "name": "targetListinga", "objectMetadataId": Any, "universalIdentifier": Any, }, @@ -6730,7 +6730,7 @@ exports[`Object metadata creation should fail v2 when namePlural is not camelCas ], "flatEntityMinimalInformation": { "id": Any, - "name": "listinga", + "name": "targetListinga", "objectMetadataId": Any, "universalIdentifier": Any, }, @@ -7463,13 +7463,13 @@ exports[`Object metadata creation should fail v2 when nameSingular contains only "code": "INVALID_FIELD_INPUT", "message": "Name should be in camelCase", "userFriendlyMessage": "Name should be in camelCase", - "value": "a a", + "value": "targetA a", }, { "code": "INVALID_FIELD_INPUT", "message": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters", "userFriendlyMessage": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters", - "value": "a a", + "value": "targetA a", }, { "code": "OBJECT_METADATA_NOT_FOUND", @@ -7479,7 +7479,7 @@ exports[`Object metadata creation should fail v2 when nameSingular contains only ], "flatEntityMinimalInformation": { "id": Any, - "name": "a a", + "name": "targetA a", "objectMetadataId": Any, "universalIdentifier": Any, }, @@ -8220,18 +8220,6 @@ exports[`Object metadata creation should fail v2 when nameSingular contains only }, { "errors": [ - { - "code": "INVALID_FIELD_INPUT", - "message": "Name is too short", - "userFriendlyMessage": "Name is too short", - "value": "", - }, - { - "code": "INVALID_FIELD_INPUT", - "message": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters", - "userFriendlyMessage": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters", - "value": "", - }, { "code": "OBJECT_METADATA_NOT_FOUND", "message": "Relation target object metadata not found", @@ -8240,7 +8228,7 @@ exports[`Object metadata creation should fail v2 when nameSingular contains only ], "flatEntityMinimalInformation": { "id": Any, - "name": "", + "name": "target", "objectMetadataId": Any, "universalIdentifier": Any, }, @@ -8987,11 +8975,17 @@ exports[`Object metadata creation should fail v2 when nameSingular has invalid c }, { "errors": [ + { + "code": "INVALID_FIELD_INPUT", + "message": "Name should be in camelCase", + "userFriendlyMessage": "Name should be in camelCase", + "value": "targetΜ", + }, { "code": "INVALID_FIELD_INPUT", "message": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters", "userFriendlyMessage": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters", - "value": "μ", + "value": "targetΜ", }, { "code": "OBJECT_METADATA_NOT_FOUND", @@ -9001,7 +8995,7 @@ exports[`Object metadata creation should fail v2 when nameSingular has invalid c ], "flatEntityMinimalInformation": { "id": Any, - "name": "μ", + "name": "targetΜ", "objectMetadataId": Any, "universalIdentifier": Any, }, @@ -9718,12 +9712,6 @@ exports[`Object metadata creation should fail v2 when nameSingular is a reserved }, { "errors": [ - { - "code": "INVALID_FIELD_INPUT", - "message": "This name is reserved. Use a different name or the system will add "Custom" suffix.", - "userFriendlyMessage": "This name is reserved. Use a different name or the system will add "Custom" suffix.", - "value": "user", - }, { "code": "OBJECT_METADATA_NOT_FOUND", "message": "Relation target object metadata not found", @@ -9732,7 +9720,7 @@ exports[`Object metadata creation should fail v2 when nameSingular is a reserved ], "flatEntityMinimalInformation": { "id": Any, - "name": "user", + "name": "targetUser", "objectMetadataId": Any, "universalIdentifier": Any, }, @@ -10482,13 +10470,13 @@ exports[`Object metadata creation should fail v2 when nameSingular is not camelC "code": "INVALID_FIELD_INPUT", "message": "Name should be in camelCase", "userFriendlyMessage": "Name should be in camelCase", - "value": "Not_Camel_Case", + "value": "targetNot_Camel_Case", }, { "code": "INVALID_FIELD_INPUT", "message": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters", "userFriendlyMessage": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters", - "value": "Not_Camel_Case", + "value": "targetNot_Camel_Case", }, { "code": "OBJECT_METADATA_NOT_FOUND", @@ -10498,7 +10486,7 @@ exports[`Object metadata creation should fail v2 when nameSingular is not camelC ], "flatEntityMinimalInformation": { "id": Any, - "name": "Not_Camel_Case", + "name": "targetNot_Camel_Case", "objectMetadataId": Any, "universalIdentifier": Any, }, @@ -11235,7 +11223,7 @@ exports[`Object metadata creation should fail v2 when names are identical 1`] = ], "flatEntityMinimalInformation": { "id": Any, - "name": "fooBar", + "name": "targetFooBar", "objectMetadataId": Any, "universalIdentifier": Any, }, @@ -11942,7 +11930,7 @@ exports[`Object metadata creation should fail v2 when names with whitespaces res ], "flatEntityMinimalInformation": { "id": Any, - "name": "fooBar", + "name": "targetFooBar", "objectMetadataId": Any, "universalIdentifier": Any, },