Fix author attachment field (#15065)

# Migrate Attachment Author to CreatedBy Field

**Twill Task**: https://twill.ai/twentyhq/ENG/tasks/7

## Summary

This PR implements a migration to transition the `Attachment` object
from using an `author` relation field to using the standard `createdBy`
field, addressing issue
https://github.com/twentyhq/core-team-issues/issues/1594.

## Changes

- **Added migration command**
(`1-8-migrate-attachment-author-to-created-by.command.ts`):
- Migrates existing attachment data to use `createdBy` instead of
`author`
- Ensures data integrity during the transition to the standard field
pattern

- **Updated Attachment workspace entity**:
  - Added `createdBy` relation field to the `Attachment` standard object
  - Registered new field ID in `standard-field-ids.ts` constants

- **Integrated migration into upgrade pipeline**:
  - Added migration module for version 1.8
  - Registered in the main upgrade version command module

This change aligns the `Attachment` object with Twenty's standard field
conventions by using the built-in `createdBy` field instead of a custom
`author` field.

---

Fixes https://github.com/twentyhq/core-team-issues/issues/1594

---------

Co-authored-by: Twill <agent@twill.ai>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
Co-authored-by: Félix Malfait <felix@twenty.com>
This commit is contained in:
twill-hq[bot]
2025-10-21 09:56:28 +02:00
committed by GitHub
parent 2903e45bef
commit 187cf400aa
26 changed files with 686 additions and 223 deletions
+3 -3
View File
@@ -31,7 +31,7 @@ npx nx storybook:build twenty-front # Build Storybook
npx nx storybook:serve-and-test:static twenty-front # Run Storybook tests
When testing the UI end to end, click on "Continue with Email" and use the prefilled credentials.
When testing the UI end to end, click on "Continue with Email" and use the prefilled credentials.
```
### Code Quality
@@ -68,7 +68,7 @@ npx nx run twenty-server:database:migrate:prod # Run migrations
npx nx run twenty-server:typeorm migration:generate src/database/typeorm/core/migrations/[name] -d src/database/typeorm/core/core.datasource.ts
# Sync metadata
npx nx run twenty-server:command workspace:sync-metadata -f
npx nx run twenty-server:command workspace:sync-metadata
```
### GraphQL
@@ -149,4 +149,4 @@ IMPORTANT: Use Context7 for code generation, setup or configuration steps, or li
- `nx.json` - Nx workspace configuration with task definitions
- `tsconfig.base.json` - Base TypeScript configuration
- `package.json` - Root package with workspace definitions
- `.cursor/rules/` - Development guidelines and best practices
- `.cursor/rules/` - Development guidelines and best practices
@@ -4,11 +4,11 @@ import { isNonEmptyString } from '@sniptt/guards';
import { type ChangeEvent, useRef } from 'react';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
import { type AttachmentFileCategory } from '@/activities/files/types/AttachmentFileCategory';
import { getFileType } from '@/activities/files/utils/getFileType';
import { FileIcon } from '@/file/components/FileIcon';
import { isDefined } from 'twenty-shared/utils';
import { Button } from 'twenty-ui/input';
import { type AttachmentType } from '../../files/types/Attachment';
import { getFileType } from '../../files/utils/getFileType';
const StyledFileInput = styled.input`
display: none;
@@ -46,8 +46,8 @@ export const FileBlock = createReactBlockSpec(
name: {
default: '' as string,
},
fileType: {
default: 'Other' as AttachmentType,
fileCategory: {
default: 'OTHER' as AttachmentFileCategory,
},
},
content: 'none',
@@ -72,7 +72,7 @@ export const FileBlock = createReactBlockSpec(
...block.props,
...{
url: fileUrl,
fileType: getFileType(file.name),
fileCategory: getFileType(file.name),
name: file.name,
},
},
@@ -89,7 +89,9 @@ export const FileBlock = createReactBlockSpec(
if (isNonEmptyString(block.props.url)) {
return (
<StyledFileLine>
<FileIcon fileType={block.props.fileType as AttachmentType} />
<FileIcon
fileCategory={block.props.fileCategory as AttachmentFileCategory}
/>
<StyledLink href={block.props.url} target="__blank">
{block.props.name}
</StyledLink>
@@ -162,7 +162,7 @@ export const AttachmentRow = ({
>
<ActivityRow disabled>
<StyledLeftContent>
<FileIcon fileType={attachment.type} />
<FileIcon fileCategory={attachment.fileCategory} />
{isEditing ? (
<SettingsTextInput
instanceId={`attachment-${attachment.id}-name`}
@@ -1,10 +1,7 @@
import { useRecoilValue } from 'recoil';
import { type Attachment } from '@/activities/files/types/Attachment';
import { getFileType } from '@/activities/files/utils/getFileType';
import { type ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getActivityTargetObjectFieldIdName';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
@@ -15,7 +12,6 @@ import {
} from '~/generated-metadata/graphql';
export const useUploadAttachmentFile = () => {
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
const coreClient = useApolloCoreClient();
const [uploadFile] = useUploadFileMutation({ client: coreClient });
@@ -49,13 +45,10 @@ export const useUploadAttachmentFile = () => {
});
const attachmentToCreate = {
authorId: currentWorkspaceMember?.id,
name: file.name,
fullPath: attachmentPath,
type: getFileType(file.name),
fileCategory: getFileType(file.name),
[targetableObjectFieldIdName]: targetableObject.id,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
} as Partial<Attachment>;
const createdAttachment = await createOneAttachment(attachmentToCreate);
@@ -1,21 +1,21 @@
import { type AttachmentFileCategory } from './AttachmentFileCategory';
export type { AttachmentFileCategory };
export type Attachment = {
id: string;
name: string;
fullPath: string;
type: AttachmentType;
fileCategory: AttachmentFileCategory;
companyId: string;
personId: string;
authorId: string;
// Deprecated: Use createdBy instead
authorId?: string;
createdBy?: {
source: string;
workspaceMemberId: string | null;
name: string;
};
createdAt: string;
__typename: string;
};
export type AttachmentType =
| 'Archive'
| 'Audio'
| 'Image'
| 'Presentation'
| 'Spreadsheet'
| 'TextDocument'
| 'Video'
| 'Other';
@@ -0,0 +1,9 @@
export type AttachmentFileCategory =
| 'ARCHIVE'
| 'AUDIO'
| 'IMAGE'
| 'PRESENTATION'
| 'SPREADSHEET'
| 'TEXT_DOCUMENT'
| 'VIDEO'
| 'OTHER';
@@ -1,13 +1,13 @@
import { getFileType } from '../getFileType';
describe('getFileType', () => {
it('should return the correct file type for a given file name', () => {
expect(getFileType('test.doc')).toBe('TextDocument');
expect(getFileType('test.xls')).toBe('Spreadsheet');
expect(getFileType('test.ppt')).toBe('Presentation');
expect(getFileType('test.png')).toBe('Image');
expect(getFileType('test.mp4')).toBe('Video');
expect(getFileType('test.mp3')).toBe('Audio');
expect(getFileType('test.zip')).toBe('Archive');
it('should return the correct file category for a given file name', () => {
expect(getFileType('test.doc')).toBe('TEXT_DOCUMENT');
expect(getFileType('test.xls')).toBe('SPREADSHEET');
expect(getFileType('test.ppt')).toBe('PRESENTATION');
expect(getFileType('test.png')).toBe('IMAGE');
expect(getFileType('test.mp4')).toBe('VIDEO');
expect(getFileType('test.mp3')).toBe('AUDIO');
expect(getFileType('test.zip')).toBe('ARCHIVE');
});
});
@@ -1,67 +1,69 @@
import { type AttachmentType } from '@/activities/files/types/Attachment';
import { type AttachmentFileCategory } from '@/activities/files/types/Attachment';
const FileExtensionMapping: { [key: string]: AttachmentType } = {
doc: 'TextDocument',
docm: 'TextDocument',
docx: 'TextDocument',
dot: 'TextDocument',
dotx: 'TextDocument',
odt: 'TextDocument',
pdf: 'TextDocument',
txt: 'TextDocument',
rtf: 'TextDocument',
ps: 'TextDocument',
tex: 'TextDocument',
pages: 'TextDocument',
xls: 'Spreadsheet',
xlsb: 'Spreadsheet',
xlsm: 'Spreadsheet',
xlsx: 'Spreadsheet',
xltx: 'Spreadsheet',
csv: 'Spreadsheet',
tsv: 'Spreadsheet',
ods: 'Spreadsheet',
numbers: 'Spreadsheet',
ppt: 'Presentation',
pptx: 'Presentation',
potx: 'Presentation',
odp: 'Presentation',
html: 'Presentation',
key: 'Presentation',
kth: 'Presentation',
png: 'Image',
jpg: 'Image',
jpeg: 'Image',
svg: 'Image',
gif: 'Image',
webp: 'Image',
heif: 'Image',
tif: 'Image',
tiff: 'Image',
bmp: 'Image',
ico: 'Image',
mp4: 'Video',
avi: 'Video',
mov: 'Video',
wmv: 'Video',
mpg: 'Video',
mpeg: 'Video',
mp3: 'Audio',
wav: 'Audio',
ogg: 'Audio',
wma: 'Audio',
zip: 'Archive',
tar: 'Archive',
iso: 'Archive',
gz: 'Archive',
rar: 'Archive',
'7z': 'Archive',
const FileExtensionMapping: {
[key: string]: AttachmentFileCategory;
} = {
doc: 'TEXT_DOCUMENT',
docm: 'TEXT_DOCUMENT',
docx: 'TEXT_DOCUMENT',
dot: 'TEXT_DOCUMENT',
dotx: 'TEXT_DOCUMENT',
odt: 'TEXT_DOCUMENT',
pdf: 'TEXT_DOCUMENT',
txt: 'TEXT_DOCUMENT',
rtf: 'TEXT_DOCUMENT',
ps: 'TEXT_DOCUMENT',
tex: 'TEXT_DOCUMENT',
pages: 'TEXT_DOCUMENT',
xls: 'SPREADSHEET',
xlsb: 'SPREADSHEET',
xlsm: 'SPREADSHEET',
xlsx: 'SPREADSHEET',
xltx: 'SPREADSHEET',
csv: 'SPREADSHEET',
tsv: 'SPREADSHEET',
ods: 'SPREADSHEET',
numbers: 'SPREADSHEET',
ppt: 'PRESENTATION',
pptx: 'PRESENTATION',
potx: 'PRESENTATION',
odp: 'PRESENTATION',
html: 'PRESENTATION',
key: 'PRESENTATION',
kth: 'PRESENTATION',
png: 'IMAGE',
jpg: 'IMAGE',
jpeg: 'IMAGE',
svg: 'IMAGE',
gif: 'IMAGE',
webp: 'IMAGE',
heif: 'IMAGE',
tif: 'IMAGE',
tiff: 'IMAGE',
bmp: 'IMAGE',
ico: 'IMAGE',
mp4: 'VIDEO',
avi: 'VIDEO',
mov: 'VIDEO',
wmv: 'VIDEO',
mpg: 'VIDEO',
mpeg: 'VIDEO',
mp3: 'AUDIO',
wav: 'AUDIO',
ogg: 'AUDIO',
wma: 'AUDIO',
zip: 'ARCHIVE',
tar: 'ARCHIVE',
iso: 'ARCHIVE',
gz: 'ARCHIVE',
rar: 'ARCHIVE',
'7z': 'ARCHIVE',
};
export const getFileType = (fileName: string): AttachmentType => {
export const getFileType = (fileName: string): AttachmentFileCategory => {
const fileExtension = fileName.split('.').at(-1);
if (!fileExtension) {
return 'Other';
return 'OTHER';
}
return FileExtensionMapping[fileExtension.toLowerCase()] ?? 'Other';
return FileExtensionMapping[fileExtension.toLowerCase()] ?? 'OTHER';
};
@@ -34,7 +34,13 @@ export const findActivitiesOperationSignatureFactory: RecordGqlOperationSignatur
name: true,
__typename: true,
},
// Deprecated: Use createdBy instead
authorId: true,
createdBy: {
source: true,
workspaceMemberId: true,
name: true,
},
assigneeId: true,
assignee: {
id: true,
@@ -1,9 +1,11 @@
import { type AttachmentFileCategory } from '@/activities/files/types/AttachmentFileCategory';
import { getFileType } from '@/activities/files/utils/getFileType';
import { IconMapping, useFileTypeColors } from '@/file/utils/fileIconMappings';
import { useFileCategoryColors } from '@/file/hooks/useFileCategoryColors';
import { IconMapping } from '@/file/utils/fileIconMappings';
import { useTheme } from '@emotion/react';
import { type FileUIPart } from 'ai';
import { AvatarChip, Chip, ChipVariant } from 'twenty-ui/components';
import { IconX } from 'twenty-ui/display';
import { type IconComponent, IconX } from 'twenty-ui/display';
import { Loader } from 'twenty-ui/feedback';
export const AgentChatFilePreview = ({
@@ -16,11 +18,17 @@ export const AgentChatFilePreview = ({
isUploading?: boolean;
}) => {
const theme = useTheme();
const iconColors = useFileTypeColors();
const iconColors: Record<AttachmentFileCategory, string> =
useFileCategoryColors();
const fileName =
file instanceof File ? file.name : (file.filename ?? 'Unknown file');
const fileCategory: AttachmentFileCategory = getFileType(fileName);
const FileCategoryIcon: IconComponent = IconMapping[fileCategory];
const iconBackgroundColor: string = iconColors[fileCategory];
return (
<Chip
label={fileName}
@@ -30,8 +38,8 @@ export const AgentChatFilePreview = ({
<Loader color="yellow" />
) : (
<AvatarChip
Icon={IconMapping[getFileType(fileName)]}
IconBackgroundColor={iconColors[getFileType(fileName)]}
Icon={FileCategoryIcon}
IconBackgroundColor={iconBackgroundColor}
/>
)
}
@@ -1,5 +1,6 @@
import { type AttachmentType } from '@/activities/files/types/Attachment';
import { IconMapping, useFileTypeColors } from '@/file/utils/fileIconMappings';
import { type AttachmentFileCategory } from '@/activities/files/types/AttachmentFileCategory';
import { useFileCategoryColors } from '@/file/hooks/useFileCategoryColors';
import { IconMapping } from '@/file/utils/fileIconMappings';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
@@ -14,14 +15,18 @@ const StyledIconContainer = styled.div<{ background: string }>`
padding: ${({ theme }) => theme.spacing(1.25)};
`;
export const FileIcon = ({ fileType }: { fileType: AttachmentType }) => {
export const FileIcon = ({
fileCategory,
}: {
fileCategory: AttachmentFileCategory;
}) => {
const theme = useTheme();
const iconColors = useFileTypeColors();
const iconColors = useFileCategoryColors();
const Icon = IconMapping[fileType];
const Icon = IconMapping[fileCategory];
return (
<StyledIconContainer background={iconColors[fileType]}>
<StyledIconContainer background={iconColors[fileCategory]}>
{Icon && <Icon size={theme.icon.size.sm} />}
</StyledIconContainer>
);
@@ -0,0 +1,47 @@
import { type AttachmentFileCategory } from '@/activities/files/types/AttachmentFileCategory';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useTheme } from '@emotion/react';
import { isDefined } from 'twenty-shared/utils';
import { type ThemeColor } from 'twenty-ui/theme';
export const useFileCategoryColors = (): Record<
AttachmentFileCategory,
string
> => {
const theme = useTheme();
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular: CoreObjectNameSingular.Attachment,
});
const fileCategoryField = objectMetadataItem.fields.find(
(field) => field.name === 'fileCategory',
);
const colorMap: Record<AttachmentFileCategory, string> = {
ARCHIVE: theme.color.gray,
AUDIO: theme.color.pink,
IMAGE: theme.color.yellow,
PRESENTATION: theme.color.orange,
SPREADSHEET: theme.color.turquoise,
TEXT_DOCUMENT: theme.color.blue,
VIDEO: theme.color.purple,
OTHER: theme.color.gray,
};
if (isDefined(fileCategoryField?.options)) {
fileCategoryField.options.forEach((option) => {
const category = option.value as AttachmentFileCategory;
const color = option.color as ThemeColor;
if (
isDefined(category) &&
isDefined(color) &&
isDefined(theme.color[color])
) {
colorMap[category] = theme.color[color];
}
});
}
return colorMap;
};
@@ -1,5 +1,4 @@
import { type AttachmentType } from '@/activities/files/types/Attachment';
import { useTheme } from '@emotion/react';
import { type AttachmentFileCategory } from '@/activities/files/types/AttachmentFileCategory';
import {
type IconComponent,
IconFile,
@@ -12,29 +11,15 @@ import {
IconVideo,
} from 'twenty-ui/display';
export const IconMapping: { [key in AttachmentType]: IconComponent } = {
Archive: IconFileZip,
Audio: IconHeadphones,
Image: IconPhoto,
Presentation: IconPresentation,
Spreadsheet: IconTable,
TextDocument: IconFileText,
Video: IconVideo,
Other: IconFile,
};
const getIconColors = (theme: any): { [key in AttachmentType]: string } => ({
Archive: theme.color.gray,
Audio: theme.color.pink,
Image: theme.color.yellow,
Presentation: theme.color.orange,
Spreadsheet: theme.color.turquoise,
TextDocument: theme.color.blue,
Video: theme.color.purple,
Other: theme.color.gray,
});
export const useFileTypeColors = () => {
const theme = useTheme();
return getIconColors(theme);
export const IconMapping: {
[key in AttachmentFileCategory]: IconComponent;
} = {
ARCHIVE: IconFileZip,
AUDIO: IconHeadphones,
IMAGE: IconPhoto,
PRESENTATION: IconPresentation,
SPREADSHEET: IconTable,
TEXT_DOCUMENT: IconFileText,
VIDEO: IconVideo,
OTHER: IconFile,
};
@@ -160,6 +160,11 @@ describe('UpgradeCommandRunner', () => {
});
upgradeCommandRunner = module.get(commandRunner);
jest.spyOn(upgradeCommandRunner['logger'], 'log').mockImplementation();
jest.spyOn(upgradeCommandRunner['logger'], 'error').mockImplementation();
jest.spyOn(upgradeCommandRunner['logger'], 'warn').mockImplementation();
runBeforeSyncMetadataSpy = jest.spyOn(
upgradeCommandRunner,
'runBeforeSyncMetadata',
@@ -0,0 +1,108 @@
import { InjectRepository } from '@nestjs/typeorm';
import { Command } from 'nest-commander';
import { isDefined } from 'twenty-shared/utils';
import { IsNull, Not, Repository } from 'typeorm';
import {
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
type RunOnWorkspaceArgs,
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { FieldActorSource } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { type AttachmentWorkspaceEntity } from 'src/modules/attachment/standard-objects/attachment.workspace-entity';
import { type WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
@Command({
name: 'upgrade:1-10:migrate-attachment-author-to-created-by',
description:
'Migrate attachment author field data to createdBy composite field',
})
export class MigrateAttachmentAuthorToCreatedByCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
constructor(
@InjectRepository(Workspace)
protected readonly workspaceRepository: Repository<Workspace>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
) {
super(workspaceRepository, twentyORMGlobalManager);
}
override async runOnWorkspace({
workspaceId,
}: RunOnWorkspaceArgs): Promise<void> {
this.logger.log(
`Migrating attachment author to createdBy for workspace ${workspaceId}`,
);
const attachmentRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<AttachmentWorkspaceEntity>(
workspaceId,
'attachment',
);
const workspaceMemberRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>(
workspaceId,
'workspaceMember',
);
const attachments = await attachmentRepository.find({
where: {
authorId: Not(IsNull()),
createdBy: {
workspaceMemberId: IsNull(),
},
},
select: ['id', 'authorId'],
});
this.logger.log(
`Found ${attachments.length} attachments to migrate for workspace ${workspaceId}`,
);
let migratedCount = 0;
for (const attachment of attachments) {
const { id, authorId } = attachment;
if (!isDefined(authorId)) {
continue;
}
const workspaceMember = await workspaceMemberRepository.findOne({
where: { id: authorId },
});
if (!isDefined(workspaceMember)) {
this.logger.warn(
`Workspace member ${authorId} not found for attachment ${id}, skipping`,
);
continue;
}
const firstName = workspaceMember.name?.firstName || '';
const lastName = workspaceMember.name?.lastName || '';
const displayName =
firstName || lastName ? `${firstName} ${lastName}`.trim() : 'Unknown';
await attachmentRepository.update(
{ id },
{
createdBy: {
source: FieldActorSource.MANUAL,
workspaceMemberId: workspaceMember.id,
name: displayName,
context: {},
},
},
);
migratedCount++;
}
this.logger.log(
`Successfully migrated ${migratedCount} attachments for workspace ${workspaceId}`,
);
}
}
@@ -0,0 +1,83 @@
import { InjectRepository } from '@nestjs/typeorm';
import { Command } from 'nest-commander';
import { Repository } from 'typeorm';
import {
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
type RunOnWorkspaceArgs,
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { type AttachmentWorkspaceEntity } from 'src/modules/attachment/standard-objects/attachment.workspace-entity';
const TYPE_TO_FILE_CATEGORY_MAPPING: Record<string, string> = {
Archive: 'ARCHIVE',
Audio: 'AUDIO',
Image: 'IMAGE',
Presentation: 'PRESENTATION',
Spreadsheet: 'SPREADSHEET',
TextDocument: 'TEXT_DOCUMENT',
Video: 'VIDEO',
Other: 'OTHER',
};
@Command({
name: 'upgrade:1-10:migrate-attachment-type-to-file-category',
description:
'Migrate attachment type field data to fileCategory SELECT field',
})
export class MigrateAttachmentTypeToFileCategoryCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
constructor(
@InjectRepository(Workspace)
protected readonly workspaceRepository: Repository<Workspace>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
) {
super(workspaceRepository, twentyORMGlobalManager);
}
override async runOnWorkspace({
workspaceId,
}: RunOnWorkspaceArgs): Promise<void> {
this.logger.log(
`Migrating attachment type to fileCategory for workspace ${workspaceId}`,
);
const attachmentRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<AttachmentWorkspaceEntity>(
workspaceId,
'attachment',
);
const attachments = await attachmentRepository.find({
select: ['id', 'type'],
});
this.logger.log(
`Found ${attachments.length} attachments to migrate for workspace ${workspaceId}`,
);
let migratedCount = 0;
for (const attachment of attachments) {
const { id, type } = attachment;
const fileCategory =
TYPE_TO_FILE_CATEGORY_MAPPING[type] ||
TYPE_TO_FILE_CATEGORY_MAPPING.Other;
await attachmentRepository.update(
{ id },
{
fileCategory,
},
);
migratedCount++;
}
this.logger.log(
`Successfully migrated ${migratedCount} attachments for workspace ${workspaceId}`,
);
}
}
@@ -0,0 +1,23 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { MigrateAttachmentAuthorToCreatedByCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-migrate-attachment-author-to-created-by.command';
import { MigrateAttachmentTypeToFileCategoryCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-migrate-attachment-type-to-file-category.command';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { WorkspaceSchemaManagerModule } from 'src/engine/twenty-orm/workspace-schema-manager/workspace-schema-manager.module';
@Module({
imports: [
TypeOrmModule.forFeature([Workspace]),
WorkspaceSchemaManagerModule,
],
providers: [
MigrateAttachmentAuthorToCreatedByCommand,
MigrateAttachmentTypeToFileCategoryCommand,
],
exports: [
MigrateAttachmentAuthorToCreatedByCommand,
MigrateAttachmentTypeToFileCategoryCommand,
],
})
export class V1_10_UpgradeVersionCommandModule {}
@@ -2,6 +2,7 @@ import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { DeduplicateUniqueFieldsCommand } from 'src/database/commands/upgrade-version-command/1-8/1-8-deduplicate-unique-fields.command';
import { FillNullServerlessFunctionLayerIdCommand } from 'src/database/commands/upgrade-version-command/1-8/1-8-fill-null-serverless-function-layer-id.command';
import { MigrateChannelSyncStagesCommand } from 'src/database/commands/upgrade-version-command/1-8/1-8-migrate-channel-sync-stages.command';
import { MigrateWorkflowStepFilterOperandValueCommand } from 'src/database/commands/upgrade-version-command/1-8/1-8-migrate-workflow-step-filter-operand-value';
import { RegeneratePersonSearchVectorWithPhonesCommand } from 'src/database/commands/upgrade-version-command/1-8/1-8-regenerate-person-search-vector-with-phones.command';
@@ -10,12 +11,11 @@ import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/
import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
import { IndexMetadataModule } from 'src/engine/metadata-modules/index-metadata/index-metadata.module';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { ServerlessFunctionLayerEntity } from 'src/engine/metadata-modules/serverless-function-layer/serverless-function-layer.entity';
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
import { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.module';
import { WorkspaceSchemaManagerModule } from 'src/engine/twenty-orm/workspace-schema-manager/workspace-schema-manager.module';
import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.module';
import { FillNullServerlessFunctionLayerIdCommand } from 'src/database/commands/upgrade-version-command/1-8/1-8-fill-null-serverless-function-layer-id.command';
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
import { ServerlessFunctionLayerEntity } from 'src/engine/metadata-modules/serverless-function-layer/serverless-function-layer.entity';
@Module({
imports: [
@@ -10,6 +10,7 @@ import { V1_5_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-
import { V1_6_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-6/1-6-upgrade-version-command.module';
import { V1_7_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-7/1-7-upgrade-version-command.module';
import { V1_8_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-8/1-8-upgrade-version-command.module';
import { V1_10_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-10/1-10-upgrade-version-command.module';
import { UpgradeCommand } from 'src/database/commands/upgrade-version-command/upgrade.command';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { WorkspaceSyncMetadataModule } from 'src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.module';
@@ -26,6 +27,7 @@ import { WorkspaceSyncMetadataModule } from 'src/engine/workspace-manager/worksp
V1_6_UpgradeVersionCommandModule,
V1_7_UpgradeVersionCommandModule,
V1_8_UpgradeVersionCommandModule,
V1_10_UpgradeVersionCommandModule,
WorkspaceSyncMetadataModule,
],
providers: [UpgradeCommand],
@@ -19,6 +19,8 @@ import { AddEnqueuedStatusToWorkflowRunCommand } from 'src/database/commands/upg
import { FixSchemaArrayTypeCommand } from 'src/database/commands/upgrade-version-command/1-1/1-1-fix-schema-array-type.command';
import { FixUpdateStandardFieldsIsLabelSyncedWithName } from 'src/database/commands/upgrade-version-command/1-1/1-1-fix-update-standard-field-is-label-synced-with-name.command';
import { MigrateWorkflowRunStatesCommand } from 'src/database/commands/upgrade-version-command/1-1/1-1-migrate-workflow-run-state.command';
import { MigrateAttachmentAuthorToCreatedByCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-migrate-attachment-author-to-created-by.command';
import { MigrateAttachmentTypeToFileCategoryCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-migrate-attachment-type-to-file-category.command';
import { AddEnqueuedStatusToWorkflowRunV2Command } from 'src/database/commands/upgrade-version-command/1-2/1-2-add-enqueued-status-to-workflow-run-v2.command';
import { AddNextStepIdsToWorkflowVersionTriggers } from 'src/database/commands/upgrade-version-command/1-2/1-2-add-next-step-ids-to-workflow-version-triggers.command';
import { RemoveWorkflowRunsWithoutState } from 'src/database/commands/upgrade-version-command/1-2/1-2-remove-workflow-runs-without-state.command';
@@ -30,6 +32,7 @@ import { RemoveFavoriteViewRelationCommand } from 'src/database/commands/upgrade
import { FixLabelIdentifierPositionAndVisibilityCommand } from 'src/database/commands/upgrade-version-command/1-6/1-6-fix-label-identifier-position-and-visibility.command';
import { BackfillWorkflowManualTriggerAvailabilityCommand } from 'src/database/commands/upgrade-version-command/1-7/1-7-backfill-workflow-manual-trigger-availability.command';
import { DeduplicateUniqueFieldsCommand } from 'src/database/commands/upgrade-version-command/1-8/1-8-deduplicate-unique-fields.command';
import { FillNullServerlessFunctionLayerIdCommand } from 'src/database/commands/upgrade-version-command/1-8/1-8-fill-null-serverless-function-layer-id.command';
import { MigrateChannelSyncStagesCommand } from 'src/database/commands/upgrade-version-command/1-8/1-8-migrate-channel-sync-stages.command';
import { MigrateWorkflowStepFilterOperandValueCommand } from 'src/database/commands/upgrade-version-command/1-8/1-8-migrate-workflow-step-filter-operand-value';
import { RegeneratePersonSearchVectorWithPhonesCommand } from 'src/database/commands/upgrade-version-command/1-8/1-8-regenerate-person-search-vector-with-phones.command';
@@ -37,7 +40,6 @@ import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twent
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { SyncWorkspaceMetadataCommand } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/sync-workspace-metadata.command';
import { FillNullServerlessFunctionLayerIdCommand } from 'src/database/commands/upgrade-version-command/1-8/1-8-fill-null-serverless-function-layer-id.command';
@Command({
name: 'upgrade',
@@ -90,11 +92,15 @@ export class UpgradeCommand extends UpgradeCommandRunner {
protected readonly backfillWorkflowManualTriggerAvailabilityCommand: BackfillWorkflowManualTriggerAvailabilityCommand,
// 1.8 Commands
protected readonly fillNullServerlessFunctionLayerIdCommand: FillNullServerlessFunctionLayerIdCommand,
protected readonly migrateWorkflowStepFilterOperandValueCommand: MigrateWorkflowStepFilterOperandValueCommand,
protected readonly deduplicateUniqueFieldsCommand: DeduplicateUniqueFieldsCommand,
protected readonly regeneratePersonSearchVectorWithPhonesCommand: RegeneratePersonSearchVectorWithPhonesCommand,
protected readonly migrateChannelSyncStagesCommand: MigrateChannelSyncStagesCommand,
protected readonly fillNullServerlessFunctionLayerIdCommand: FillNullServerlessFunctionLayerIdCommand,
// 1.10 Commands
protected readonly migrateAttachmentAuthorToCreatedByCommand: MigrateAttachmentAuthorToCreatedByCommand,
protected readonly migrateAttachmentTypeToFileCategoryCommand: MigrateAttachmentTypeToFileCategoryCommand,
) {
super(
workspaceRepository,
@@ -199,6 +205,14 @@ export class UpgradeCommand extends UpgradeCommandRunner {
afterSyncMetadata: [],
};
const commands_1100: VersionCommands = {
beforeSyncMetadata: [],
afterSyncMetadata: [
this.migrateAttachmentAuthorToCreatedByCommand,
this.migrateAttachmentTypeToFileCategoryCommand,
],
};
this.allCommands = {
'0.53.0': commands_053,
'0.54.0': commands_054,
@@ -213,6 +227,7 @@ export class UpgradeCommand extends UpgradeCommandRunner {
'1.6.0': commands_160,
'1.7.0': commands_170,
'1.8.0': commands_180,
'1.10.0': commands_1100,
};
}
@@ -35,6 +35,7 @@ describe('WorkerHealthIndicator', () => {
const mockRedisService = {
getClient: () => mockRedis,
getQueueClient: () => mockRedis,
} as unknown as RedisClientService;
healthIndicatorService = {
@@ -1,3 +1,4 @@
import { FieldActorSource } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type';
import { COMPANY_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/data/constants/company-data-seeds.constant';
import { NOTE_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/data/constants/note-data-seeds.constant';
import { OPPORTUNITY_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/data/constants/opportunity-data-seeds.constant';
@@ -10,10 +11,12 @@ type AttachmentDataSeed = {
name: string;
fullPath: string;
type: string;
fileCategory?: string;
// Deprecated: Use createdBy instead
authorId: string | null;
// createdBySource: string;
// createdByWorkspaceMemberId: string;
// createdByName: string;
createdBySource: string;
createdByWorkspaceMemberId: string;
createdByName: string;
personId: string | null;
companyId: string | null;
noteId: string | null;
@@ -26,10 +29,11 @@ export const ATTACHMENT_DATA_SEED_COLUMNS: (keyof AttachmentDataSeed)[] = [
'name',
'fullPath',
'type',
'authorId',
// 'createdBySource',
// 'createdByWorkspaceMemberId',
// 'createdByName',
'fileCategory',
'authorId', // Deprecated: kept for backward compatibility during migration
'createdBySource',
'createdByWorkspaceMemberId',
'createdByName',
'personId',
'companyId',
'noteId',
@@ -52,69 +56,169 @@ const GENERATE_ATTACHMENT_IDS = (): Record<string, string> => {
export const ATTACHMENT_DATA_SEED_IDS = GENERATE_ATTACHMENT_IDS();
// Pool of 5 reusable file templates
const FILE_TEMPLATES = [
{
name: 'Contract Agreement.pdf',
fullPath: 'attachment/sample-contract.pdf',
type: 'TextDocument',
},
{
name: 'Budget 2024.xlsx',
fullPath: 'attachment/budget-2024.xlsx',
type: 'Spreadsheet',
},
{
name: 'Product Presentation.pptx',
fullPath: 'attachment/presentation.pptx',
type: 'Presentation',
},
{
name: 'Screenshot.png',
fullPath: 'attachment/screenshot.png',
type: 'Image',
},
{
name: 'Archive.zip',
fullPath: 'attachment/archive.zip',
type: 'Archive',
},
// Pool of 5 reusable file paths for attachments
const FILE_TEMPLATE_PATHS = [
'attachment/sample-contract.pdf',
'attachment/budget-2024.xlsx',
'attachment/presentation.pptx',
'attachment/screenshot.png',
'attachment/archive.zip',
];
// Additional name variations for more realistic variety
const FILE_NAME_VARIATIONS = [
// Documents
{ name: 'Service Agreement.pdf', type: 'TextDocument', pathIndex: 0 },
{ name: 'NDA Document.pdf', type: 'TextDocument', pathIndex: 0 },
{ name: 'Project Proposal.pdf', type: 'TextDocument', pathIndex: 0 },
{ name: 'Invoice Q1 2024.pdf', type: 'TextDocument', pathIndex: 0 },
{ name: 'Meeting Notes.pdf', type: 'TextDocument', pathIndex: 0 },
{ name: 'Report Final.pdf', type: 'TextDocument', pathIndex: 0 },
{ name: 'Contract Signed.pdf', type: 'TextDocument', pathIndex: 0 },
{
name: 'Service Agreement.pdf',
type: 'TextDocument',
fileCategory: 'TEXT_DOCUMENT',
pathIndex: 0,
},
{
name: 'NDA Document.pdf',
type: 'TextDocument',
fileCategory: 'TEXT_DOCUMENT',
pathIndex: 0,
},
{
name: 'Project Proposal.pdf',
type: 'TextDocument',
fileCategory: 'TEXT_DOCUMENT',
pathIndex: 0,
},
{
name: 'Invoice Q1 2024.pdf',
type: 'TextDocument',
fileCategory: 'TEXT_DOCUMENT',
pathIndex: 0,
},
{
name: 'Meeting Notes.pdf',
type: 'TextDocument',
fileCategory: 'TEXT_DOCUMENT',
pathIndex: 0,
},
{
name: 'Report Final.pdf',
type: 'TextDocument',
fileCategory: 'TEXT_DOCUMENT',
pathIndex: 0,
},
{
name: 'Contract Signed.pdf',
type: 'TextDocument',
fileCategory: 'TEXT_DOCUMENT',
pathIndex: 0,
},
// Spreadsheets
{ name: 'Financial Forecast.xlsx', type: 'Spreadsheet', pathIndex: 1 },
{ name: 'Sales Report Q4.xlsx', type: 'Spreadsheet', pathIndex: 1 },
{ name: 'Team Roster.xlsx', type: 'Spreadsheet', pathIndex: 1 },
{ name: 'Expense Report.xlsx', type: 'Spreadsheet', pathIndex: 1 },
{ name: 'Inventory List.xlsx', type: 'Spreadsheet', pathIndex: 1 },
{ name: 'Data Export.csv', type: 'Spreadsheet', pathIndex: 1 },
{
name: 'Financial Forecast.xlsx',
type: 'Spreadsheet',
fileCategory: 'SPREADSHEET',
pathIndex: 1,
},
{
name: 'Sales Report Q4.xlsx',
type: 'Spreadsheet',
fileCategory: 'SPREADSHEET',
pathIndex: 1,
},
{
name: 'Team Roster.xlsx',
type: 'Spreadsheet',
fileCategory: 'SPREADSHEET',
pathIndex: 1,
},
{
name: 'Expense Report.xlsx',
type: 'Spreadsheet',
fileCategory: 'SPREADSHEET',
pathIndex: 1,
},
{
name: 'Inventory List.xlsx',
type: 'Spreadsheet',
fileCategory: 'SPREADSHEET',
pathIndex: 1,
},
{
name: 'Data Export.csv',
type: 'Spreadsheet',
fileCategory: 'SPREADSHEET',
pathIndex: 1,
},
// Presentations
{ name: 'Pitch Deck.pptx', type: 'Presentation', pathIndex: 2 },
{ name: 'Q4 Results.pptx', type: 'Presentation', pathIndex: 2 },
{ name: 'Roadmap 2024.pptx', type: 'Presentation', pathIndex: 2 },
{ name: 'Company Overview.pptx', type: 'Presentation', pathIndex: 2 },
{ name: 'Training Materials.pptx', type: 'Presentation', pathIndex: 2 },
{
name: 'Pitch Deck.pptx',
type: 'Presentation',
fileCategory: 'PRESENTATION',
pathIndex: 2,
},
{
name: 'Q4 Results.pptx',
type: 'Presentation',
fileCategory: 'PRESENTATION',
pathIndex: 2,
},
{
name: 'Roadmap 2024.pptx',
type: 'Presentation',
fileCategory: 'PRESENTATION',
pathIndex: 2,
},
{
name: 'Company Overview.pptx',
type: 'Presentation',
fileCategory: 'PRESENTATION',
pathIndex: 2,
},
{
name: 'Training Materials.pptx',
type: 'Presentation',
fileCategory: 'PRESENTATION',
pathIndex: 2,
},
// Images
{ name: 'Company Logo.png', type: 'Image', pathIndex: 3 },
{ name: 'Product Photo.jpg', type: 'Image', pathIndex: 3 },
{ name: 'Diagram.png', type: 'Image', pathIndex: 3 },
{ name: 'Wireframe.png', type: 'Image', pathIndex: 3 },
{ name: 'Mockup Design.png', type: 'Image', pathIndex: 3 },
{ name: 'Headshot.jpg', type: 'Image', pathIndex: 3 },
{
name: 'Company Logo.png',
type: 'Image',
fileCategory: 'IMAGE',
pathIndex: 3,
},
{
name: 'Product Photo.jpg',
type: 'Image',
fileCategory: 'IMAGE',
pathIndex: 3,
},
{ name: 'Diagram.png', type: 'Image', fileCategory: 'IMAGE', pathIndex: 3 },
{ name: 'Wireframe.png', type: 'Image', fileCategory: 'IMAGE', pathIndex: 3 },
{
name: 'Mockup Design.png',
type: 'Image',
fileCategory: 'IMAGE',
pathIndex: 3,
},
{ name: 'Headshot.jpg', type: 'Image', fileCategory: 'IMAGE', pathIndex: 3 },
// Archives
{ name: 'Project Files.zip', type: 'Archive', pathIndex: 4 },
{ name: 'Backup Data.zip', type: 'Archive', pathIndex: 4 },
{ name: 'Source Code.zip', type: 'Archive', pathIndex: 4 },
{
name: 'Project Files.zip',
type: 'Archive',
fileCategory: 'ARCHIVE',
pathIndex: 4,
},
{
name: 'Backup Data.zip',
type: 'Archive',
fileCategory: 'ARCHIVE',
pathIndex: 4,
},
{
name: 'Source Code.zip',
type: 'Archive',
fileCategory: 'ARCHIVE',
pathIndex: 4,
},
];
const GENERATE_ATTACHMENT_SEEDS = (): AttachmentDataSeed[] => {
@@ -133,7 +237,7 @@ const GENERATE_ATTACHMENT_SEEDS = (): AttachmentDataSeed[] => {
// Cycle through file name variations
const NAME_VARIATION_INDEX = INDEX % FILE_NAME_VARIATIONS.length;
const NAME_VARIATION = FILE_NAME_VARIATIONS[NAME_VARIATION_INDEX];
const FILE_TEMPLATE = FILE_TEMPLATES[NAME_VARIATION.pathIndex];
const FILE_PATH = FILE_TEMPLATE_PATHS[NAME_VARIATION.pathIndex];
// Determine which entity this attachment belongs to
// Distribution: ~30% person, ~30% company, ~20% note, ~15% task, ~5% opportunity
@@ -170,12 +274,14 @@ const GENERATE_ATTACHMENT_SEEDS = (): AttachmentDataSeed[] => {
ATTACHMENT_SEEDS.push({
id: ATTACHMENT_DATA_SEED_IDS[`ID_${INDEX}`],
name: NAME_VARIATION.name,
fullPath: FILE_TEMPLATE.fullPath,
fullPath: FILE_PATH,
type: NAME_VARIATION.type,
fileCategory: NAME_VARIATION.fileCategory,
// Deprecated: Use createdBy fields instead
authorId: WORKSPACE_MEMBER_DATA_SEED_IDS.TIM,
// createdBySource: 'MANUAL',
//createdByWorkspaceMemberId: WORKSPACE_MEMBER_DATA_SEED_IDS.TIM,
//createdByName: 'Tim A',
createdBySource: FieldActorSource.MANUAL,
createdByWorkspaceMemberId: WORKSPACE_MEMBER_DATA_SEED_IDS.TIM,
createdByName: 'Tim A',
personId,
companyId,
noteId,
@@ -33,6 +33,8 @@ export const ATTACHMENT_STANDARD_FIELD_IDS = {
name: '20202020-87a5-48f8-bbf7-ade388825a57',
fullPath: '20202020-0d19-453d-8e8d-fbcda8ca3747',
type: '20202020-a417-49b8-a40b-f6a7874caa0d',
fileCategory: '20202020-8c3f-4d9e-9a1b-2e5f7a8c9d0e',
createdBy: '395be3bd-a5c9-463d-aafe-9bc3bbec3f15',
author: '20202020-6501-4ac5-a4ef-b2f8522ef6cd',
activity: '20202020-b569-481b-a13f-9b94e47e54fe',
task: '20202020-51e5-4621-9cf8-215487951c4b',
@@ -6,12 +6,14 @@ import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfa
import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { ActorMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type';
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
import { CustomWorkspaceEntity } from 'src/engine/twenty-orm/custom.workspace-entity';
import { WorkspaceDynamicRelation } from 'src/engine/twenty-orm/decorators/workspace-dynamic-relation.decorator';
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
import { WorkspaceGate } from 'src/engine/twenty-orm/decorators/workspace-gate.decorator';
import { WorkspaceIsFieldUIReadOnly } from 'src/engine/twenty-orm/decorators/workspace-is-field-ui-readonly.decorator';
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
import { WorkspaceJoinColumn } from 'src/engine/twenty-orm/decorators/workspace-join-column.decorator';
@@ -57,20 +59,92 @@ export class AttachmentWorkspaceEntity extends BaseWorkspaceEntity {
})
fullPath: string;
// Deprecated: Use fileCategory instead
@WorkspaceField({
standardId: ATTACHMENT_STANDARD_FIELD_IDS.type,
type: FieldMetadataType.TEXT,
label: msg`Type`,
description: msg`Attachment type`,
label: msg`Type (deprecated)`,
description: msg`Attachment type (deprecated - use fileCategory)`,
icon: 'IconList',
})
type: string;
@WorkspaceField({
standardId: ATTACHMENT_STANDARD_FIELD_IDS.fileCategory,
type: FieldMetadataType.SELECT,
label: msg`File category`,
description: msg`Attachment file category`,
icon: 'IconList',
options: [
{
value: 'ARCHIVE',
label: 'Archive',
position: 0,
color: 'gray',
},
{
value: 'AUDIO',
label: 'Audio',
position: 1,
color: 'pink',
},
{
value: 'IMAGE',
label: 'Image',
position: 2,
color: 'yellow',
},
{
value: 'PRESENTATION',
label: 'Presentation',
position: 3,
color: 'orange',
},
{
value: 'SPREADSHEET',
label: 'Spreadsheet',
position: 4,
color: 'turquoise',
},
{
value: 'TEXT_DOCUMENT',
label: 'Text Document',
position: 5,
color: 'blue',
},
{
value: 'VIDEO',
label: 'Video',
position: 6,
color: 'purple',
},
{
value: 'OTHER',
label: 'Other',
position: 7,
color: 'gray',
},
],
defaultValue: "'OTHER'",
})
fileCategory: string;
@WorkspaceField({
standardId: ATTACHMENT_STANDARD_FIELD_IDS.createdBy,
type: FieldMetadataType.ACTOR,
label: msg`Created by`,
icon: 'IconCreativeCommonsSa',
description: msg`The creator of the record`,
})
@WorkspaceIsFieldUIReadOnly()
createdBy: ActorMetadata;
// Deprecated: Use createdBy composite field instead
@WorkspaceRelation({
standardId: ATTACHMENT_STANDARD_FIELD_IDS.author,
type: RelationType.MANY_TO_ONE,
label: msg`Author`,
description: msg`Attachment author`,
description: msg`Attachment author (deprecated - use createdBy)`,
icon: 'IconCircleUser',
inverseSideTarget: () => WorkspaceMemberWorkspaceEntity,
inverseSideFieldKey: 'authoredAttachments',
@@ -15,7 +15,6 @@ import {
PermissionsExceptionCode,
} from 'src/engine/metadata-modules/permissions/permissions.exception';
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
import { type AttachmentWorkspaceEntity } from 'src/modules/attachment/standard-objects/attachment.workspace-entity';
import { WorkspaceMemberPreQueryHookService } from 'src/modules/workspace-member/query-hooks/workspace-member-pre-query-hook.service';
import { type WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
@@ -59,17 +58,6 @@ export class WorkspaceMemberDeleteOnePostQueryHook
},
);
const attachmentRepository =
await this.twentyORMManager.getRepository<AttachmentWorkspaceEntity>(
'attachment',
);
const authorId = targettedWorkspaceMemberId;
await attachmentRepository.delete({
authorId,
});
const workspaceMemberRepository =
await this.twentyORMManager.getRepository<WorkspaceMemberWorkspaceEntity>(
'workspaceMember',
@@ -59,7 +59,6 @@ describe('attachmentsResolver (e2e)', () => {
expect(attachments).toHaveProperty('createdAt');
expect(attachments).toHaveProperty('updatedAt');
expect(attachments).toHaveProperty('deletedAt');
expect(attachments).toHaveProperty('authorId');
expect(attachments).toHaveProperty('taskId');
expect(attachments).toHaveProperty('noteId');
expect(attachments).toHaveProperty('personId');