Ej/fix message visibility (#11874)

<img width="257" alt="Screenshot 2025-05-05 at 15 30 09"
src="https://github.com/user-attachments/assets/5a8e18e0-efc5-4521-9c3a-bf73277ecdf9"
/>
<img width="257" alt="Screenshot 2025-05-05 at 15 29 05"
src="https://github.com/user-attachments/assets/c1a784af-a744-497a-b6ce-ec3a9e8b851a"
/>
<img width="257" alt="Screenshot 2025-05-05 at 15 33 06"
src="https://github.com/user-attachments/assets/c5fabd1d-a125-49d7-aade-0a208a0eff95"
/>

related to PR https://github.com/twentyhq/twenty/pull/11840 and issue
https://github.com/twentyhq/twenty/issues/9826
This commit is contained in:
Etienne
2025-05-05 17:23:27 +02:00
committed by GitHub
parent da0c7e679e
commit a60711c808
19 changed files with 578 additions and 481 deletions

View File

@ -1,62 +0,0 @@
import { BadRequestException, NotFoundException, Scope } from '@nestjs/common';
import { WorkspaceQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface';
import { FindManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
import { CanAccessCalendarEventsService } from 'src/modules/calendar/common/query-hooks/calendar-event/services/can-access-calendar-events.service';
import { CalendarChannelEventAssociationWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel-event-association.workspace-entity';
@WorkspaceQueryHook({
key: `calendarEvent.findMany`,
scope: Scope.REQUEST,
})
export class CalendarEventFindManyPreQueryHook
implements WorkspaceQueryHookInstance
{
constructor(
private readonly twentyORMManager: TwentyORMManager,
private readonly canAccessCalendarEventsService: CanAccessCalendarEventsService,
) {}
async execute(
authContext: AuthContext,
objectName: string,
payload: FindManyResolverArgs,
): Promise<FindManyResolverArgs> {
if (!payload?.filter?.id?.eq) {
throw new BadRequestException('id filter is required');
}
if (!authContext.user?.id) {
throw new BadRequestException('User id is required');
}
const calendarChannelEventAssociationRepository =
await this.twentyORMManager.getRepository<CalendarChannelEventAssociationWorkspaceEntity>(
'calendarChannelEventAssociation',
);
const calendarChannelCalendarEventAssociations =
await calendarChannelEventAssociationRepository.find({
where: {
calendarEventId: payload?.filter?.id?.eq,
},
relations: ['calendarChannel.connectedAccount'],
});
if (calendarChannelCalendarEventAssociations.length === 0) {
throw new NotFoundException();
}
await this.canAccessCalendarEventsService.canAccessCalendarEvents(
authContext.user.id,
authContext.workspace.id,
calendarChannelCalendarEventAssociations,
);
return payload;
}
}

View File

@ -1,62 +0,0 @@
import { BadRequestException, NotFoundException, Scope } from '@nestjs/common';
import { WorkspaceQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface';
import { FindOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
import { CanAccessCalendarEventsService } from 'src/modules/calendar/common/query-hooks/calendar-event/services/can-access-calendar-events.service';
import { CalendarChannelEventAssociationWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel-event-association.workspace-entity';
@WorkspaceQueryHook({
key: `calendarEvent.findOne`,
scope: Scope.REQUEST,
})
export class CalendarEventFindOnePreQueryHook
implements WorkspaceQueryHookInstance
{
constructor(
private readonly twentyORMManager: TwentyORMManager,
private readonly canAccessCalendarEventsService: CanAccessCalendarEventsService,
) {}
async execute(
authContext: AuthContext,
objectName: string,
payload: FindOneResolverArgs,
): Promise<FindOneResolverArgs> {
if (!payload?.filter?.id?.eq) {
throw new BadRequestException('id filter is required');
}
if (!authContext.user?.id) {
throw new BadRequestException('User id is required');
}
const calendarChannelEventAssociationRepository =
await this.twentyORMManager.getRepository<CalendarChannelEventAssociationWorkspaceEntity>(
'calendarChannelEventAssociation',
);
const calendarChannelCalendarEventAssociations =
await calendarChannelEventAssociationRepository.find({
where: {
calendarEventId: payload?.filter?.id?.eq,
},
relations: ['calendarChannel', 'calendarChannel.connectedAccount'],
});
if (calendarChannelCalendarEventAssociations.length === 0) {
throw new NotFoundException();
}
await this.canAccessCalendarEventsService.canAccessCalendarEvents(
authContext.user.id,
authContext.workspace.id,
calendarChannelCalendarEventAssociations,
);
return payload;
}
}

View File

@ -28,7 +28,7 @@ export class ApplyCalendarEventsVisibilityRestrictionsService {
where: {
calendarEventId: In(calendarEvents.map((event) => event.id)),
},
relations: ['calendarChannel', 'calendarChannel.connectedAccount'],
relations: ['calendarChannel'],
});
const connectedAccountRepository =

View File

@ -1,70 +0,0 @@
import { Injectable } from '@nestjs/common';
import groupBy from 'lodash.groupby';
import { In } from 'typeorm';
import { ForbiddenError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
import { CalendarChannelEventAssociationWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel-event-association.workspace-entity';
import { CalendarChannelVisibility } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { WorkspaceMemberRepository } from 'src/modules/workspace-member/repositories/workspace-member.repository';
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
@Injectable()
export class CanAccessCalendarEventsService {
constructor(
private readonly twentyORMManager: TwentyORMManager,
@InjectObjectMetadataRepository(WorkspaceMemberWorkspaceEntity)
private readonly workspaceMemberService: WorkspaceMemberRepository,
) {}
public async canAccessCalendarEvents(
userId: string,
workspaceId: string,
calendarChannelCalendarEventAssociations: CalendarChannelEventAssociationWorkspaceEntity[],
) {
const calendarChannels = calendarChannelCalendarEventAssociations.map(
(association) => association.calendarChannel,
);
const calendarChannelsGroupByVisibility = groupBy(
calendarChannels,
(channel) => channel.visibility,
);
if (
calendarChannelsGroupByVisibility[
CalendarChannelVisibility.SHARE_EVERYTHING
] ||
calendarChannelsGroupByVisibility[CalendarChannelVisibility.METADATA]
) {
return;
}
const currentWorkspaceMember =
await this.workspaceMemberService.getByIdOrFail(userId, workspaceId);
const connectedAccountRepository =
await this.twentyORMManager.getRepository<ConnectedAccountWorkspaceEntity>(
'connectedAccount',
);
const connectedAccounts = await connectedAccountRepository.find({
select: ['id'],
where: {
calendarChannels: {
id: In(calendarChannels.map((channel) => channel.id)),
},
accountOwnerId: currentWorkspaceMember.id,
},
});
if (connectedAccounts.length > 0) {
return;
}
throw new ForbiddenError('Calendar events not shared');
}
}

View File

@ -2,11 +2,8 @@ import { Module } from '@nestjs/common';
import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module';
import { CalendarEventFindManyPostQueryHook } from 'src/modules/calendar/common/query-hooks/calendar-event/calendar-event-find-many.post-query.hook';
import { CalendarEventFindManyPreQueryHook } from 'src/modules/calendar/common/query-hooks/calendar-event/calendar-event-find-many.pre-query.hook';
import { CalendarEventFindOnePostQueryHook } from 'src/modules/calendar/common/query-hooks/calendar-event/calendar-event-find-one.post-query.hook';
import { CalendarEventFindOnePreQueryHook } from 'src/modules/calendar/common/query-hooks/calendar-event/calendar-event-find-one.pre-query-hook';
import { ApplyCalendarEventsVisibilityRestrictionsService } from 'src/modules/calendar/common/query-hooks/calendar-event/services/apply-calendar-events-visibility-restrictions.service';
import { CanAccessCalendarEventsService } from 'src/modules/calendar/common/query-hooks/calendar-event/services/can-access-calendar-events.service';
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
@Module({
@ -14,10 +11,7 @@ import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/sta
ObjectMetadataRepositoryModule.forFeature([WorkspaceMemberWorkspaceEntity]),
],
providers: [
CanAccessCalendarEventsService,
ApplyCalendarEventsVisibilityRestrictionsService,
CalendarEventFindOnePreQueryHook,
CalendarEventFindManyPreQueryHook,
CalendarEventFindOnePostQueryHook,
CalendarEventFindManyPostQueryHook,
],

View File

@ -0,0 +1,269 @@
import { Test, TestingModule } from '@nestjs/testing';
import { FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED } from 'twenty-shared/constants';
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
import { MessageChannelVisibility } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import { MessageWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message.workspace-entity';
import { ApplyMessagesVisibilityRestrictionsService } from './apply-messages-visibility-restrictions.service';
const createMockMessage = (
id: string,
subject: string,
text: string,
): MessageWorkspaceEntity => ({
id,
subject,
text,
headerMessageId: '',
receivedAt: new Date('2024-03-20T10:00:00Z'),
messageThreadId: '',
messageThread: null,
messageChannelMessageAssociations: [],
messageParticipants: [],
deletedAt: null,
createdAt: '2024-03-20T09:00:00Z',
updatedAt: '2024-03-20T09:00:00Z',
});
describe('ApplyMessagesVisibilityRestrictionsService', () => {
let service: ApplyMessagesVisibilityRestrictionsService;
const mockMessageChannelMessageAssociationRepository = {
find: jest.fn(),
};
const mockConnectedAccountRepository = {
find: jest.fn(),
};
const mockTwentyORMManager = {
getRepository: jest.fn().mockImplementation((name) => {
if (name === 'messageChannelMessageAssociation') {
return mockMessageChannelMessageAssociationRepository;
}
if (name === 'connectedAccount') {
return mockConnectedAccountRepository;
}
}),
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
ApplyMessagesVisibilityRestrictionsService,
{
provide: TwentyORMManager,
useValue: mockTwentyORMManager,
},
],
}).compile();
service = module.get<ApplyMessagesVisibilityRestrictionsService>(
ApplyMessagesVisibilityRestrictionsService,
);
jest.clearAllMocks();
});
it('should return message without obfuscated subject and text if the visibility is SHARE_EVERYTHING', async () => {
const messages = [
createMockMessage('messageId', 'Test Subject', 'Test Message'),
];
mockMessageChannelMessageAssociationRepository.find.mockResolvedValue([
{
messageId: 'messageId',
messageChannel: {
id: 'messageChannelId',
visibility: MessageChannelVisibility.SHARE_EVERYTHING,
},
},
]);
const result = await service.applyMessagesVisibilityRestrictions(
'workspace-member-id',
messages,
);
expect(result).toEqual(messages);
expect(
result.every(
(item) =>
item.subject === 'Test Subject' && item.text === 'Test Message',
),
).toBe(true);
expect(mockConnectedAccountRepository.find).not.toHaveBeenCalled();
});
it('should return message without obfuscated subject and with obfuscated text if the visibility is SUBJECT', async () => {
const messages = [
createMockMessage('messageId', 'Test Subject', 'Test Message'),
];
mockMessageChannelMessageAssociationRepository.find.mockResolvedValue([
{
messageId: 'messageId',
messageChannel: {
id: 'messageChannelId',
visibility: MessageChannelVisibility.SUBJECT,
},
},
]);
mockConnectedAccountRepository.find.mockResolvedValue([]);
const result = await service.applyMessagesVisibilityRestrictions(
'workspace-member-id',
messages,
);
expect(result).toEqual([
{
...messages[0],
text: FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED,
},
]);
});
it('should return message with obfuscated subject and text if the visibility is METADATA', async () => {
const messages = [
createMockMessage('messageId', 'Test Subject', 'Test Message'),
];
mockMessageChannelMessageAssociationRepository.find.mockResolvedValue([
{
messageId: 'messageId',
messageChannel: {
id: 'messageChannelId',
visibility: MessageChannelVisibility.METADATA,
},
},
]);
mockConnectedAccountRepository.find.mockResolvedValue([]);
const result = await service.applyMessagesVisibilityRestrictions(
'workspace-member-id',
messages,
);
expect(result).toEqual([
{
...messages[0],
subject: FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED,
text: FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED,
},
]);
});
it('should return message without obfuscated subject and text if the visibility is METADATA and the workspace member is the channel owner', async () => {
const messages = [
createMockMessage('messageId', 'Test Subject', 'Test Message'),
];
mockMessageChannelMessageAssociationRepository.find.mockResolvedValue([
{
messageId: 'messageId',
messageChannel: {
id: 'messageChannelId',
visibility: MessageChannelVisibility.METADATA,
},
},
]);
mockConnectedAccountRepository.find.mockResolvedValue([{ id: '1' }]);
const result = await service.applyMessagesVisibilityRestrictions(
'workspace-member-id',
messages,
);
expect(result).toEqual(messages);
expect(
result.every(
(item) =>
item.subject === 'Test Subject' && item.text === 'Test Message',
),
).toBe(true);
});
it('should not return message if visibility is not SHARE_EVERYTHING, SUBJECT or METADATA and the workspace member is not the channel owner', async () => {
const messages = [
createMockMessage('messageId', 'Test Subject', 'Test Message'),
];
mockMessageChannelMessageAssociationRepository.find.mockResolvedValue([
{
messageId: 'messageId',
messageChannel: {
id: 'messageChannelId',
},
},
]);
mockConnectedAccountRepository.find.mockResolvedValue([]);
const result = await service.applyMessagesVisibilityRestrictions(
'workspace-member-id',
messages,
);
expect(result).toEqual([]);
});
it('should return all messages with the right visibility', async () => {
const messages = [
createMockMessage('1', 'Subject 1', 'Message 1'),
createMockMessage('2', 'Subject 2', 'Message 2'),
createMockMessage('3', 'Subject 3', 'Message 3'),
];
mockMessageChannelMessageAssociationRepository.find.mockResolvedValue([
{
messageId: '1',
messageChannel: {
id: '1',
visibility: MessageChannelVisibility.SHARE_EVERYTHING,
},
},
{
messageId: '2',
messageChannel: {
id: '2',
visibility: MessageChannelVisibility.SUBJECT,
},
},
{
messageId: '3',
messageChannel: {
id: '3',
visibility: MessageChannelVisibility.METADATA,
},
},
]);
mockConnectedAccountRepository.find
.mockResolvedValueOnce([]) // request for message 3
.mockResolvedValueOnce([]); // request for message 2
const result = await service.applyMessagesVisibilityRestrictions(
'workspace-member-id',
messages,
);
expect(result).toEqual([
messages[0],
{
...messages[1],
text: FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED,
},
{
...messages[2],
subject: FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED,
text: FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED,
},
]);
});
});

View File

@ -0,0 +1,98 @@
import { Injectable } from '@nestjs/common';
import groupBy from 'lodash.groupby';
import { FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED } from 'twenty-shared/constants';
import { In } from 'typeorm';
import { NotFoundError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { MessageChannelMessageAssociationWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel-message-association.workspace-entity';
import { MessageChannelVisibility } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import { MessageWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message.workspace-entity';
@Injectable()
export class ApplyMessagesVisibilityRestrictionsService {
constructor(private readonly twentyORMManager: TwentyORMManager) {}
public async applyMessagesVisibilityRestrictions(
workspaceMemberId: string,
messages: MessageWorkspaceEntity[],
) {
const messageChannelMessageAssociationRepository =
await this.twentyORMManager.getRepository<MessageChannelMessageAssociationWorkspaceEntity>(
'messageChannelMessageAssociation',
);
const messageChannelMessagesAssociations =
await messageChannelMessageAssociationRepository.find({
where: {
messageId: In(messages.map((message) => message.id)),
},
relations: ['messageChannel'],
});
const connectedAccountRepository =
await this.twentyORMManager.getRepository<ConnectedAccountWorkspaceEntity>(
'connectedAccount',
);
for (let i = messages.length - 1; i >= 0; i--) {
const messageChannelMessageAssociations =
messageChannelMessagesAssociations.filter(
(association) => association.messageId === messages[i].id,
);
const messageChannels = messageChannelMessageAssociations
.map((association) => association.messageChannel)
.filter(
(channel): channel is NonNullable<typeof channel> => channel !== null,
);
if (messageChannels.length === 0) {
throw new NotFoundError('Associated message channels not found');
}
const messageChannelsGroupByVisibility = groupBy(
messageChannels,
(channel) => channel.visibility,
);
if (
messageChannelsGroupByVisibility[
MessageChannelVisibility.SHARE_EVERYTHING
]
) {
continue;
}
const connectedAccounts = await connectedAccountRepository.find({
select: ['id'],
where: {
messageChannels: {
id: In(messageChannels.map((channel) => channel.id)),
},
accountOwnerId: workspaceMemberId,
},
});
if (connectedAccounts.length > 0) {
continue;
}
if (messageChannelsGroupByVisibility[MessageChannelVisibility.SUBJECT]) {
messages[i].text = FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED;
continue;
}
if (messageChannelsGroupByVisibility[MessageChannelVisibility.METADATA]) {
messages[i].subject = FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED;
messages[i].text = FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED;
continue;
}
messages.splice(i, 1);
}
return messages;
}
}

View File

@ -1,62 +0,0 @@
import { ForbiddenException } from '@nestjs/common';
import { In } from 'typeorm';
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { MessageChannelMessageAssociationWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel-message-association.workspace-entity';
import { MessageChannelVisibility } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import { WorkspaceMemberRepository } from 'src/modules/workspace-member/repositories/workspace-member.repository';
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
export class CanAccessMessageThreadService {
constructor(
@InjectObjectMetadataRepository(WorkspaceMemberWorkspaceEntity)
private readonly workspaceMemberRepository: WorkspaceMemberRepository,
private readonly twentyORMManager: TwentyORMManager,
) {}
public async canAccessMessageThread(
userId: string,
workspaceId: string,
messageChannelMessageAssociations: MessageChannelMessageAssociationWorkspaceEntity[],
) {
const messageChannelIds = messageChannelMessageAssociations.map(
(association) => association.messageChannelId,
);
const currentWorkspaceMember =
await this.workspaceMemberRepository.getByIdOrFail(userId, workspaceId);
const connectedAccountRepository =
await this.twentyORMManager.getRepository<ConnectedAccountWorkspaceEntity>(
'connectedAccount',
);
const connectedAccounts = await connectedAccountRepository.find({
select: {
id: true,
},
where: [
{
messageChannels: {
id: In(messageChannelIds),
visibility: MessageChannelVisibility.SHARE_EVERYTHING,
},
},
{
messageChannels: {
id: In(messageChannelIds),
},
accountOwnerId: currentWorkspaceMember.id,
},
],
take: 1,
});
if (connectedAccounts.length === 0) {
throw new ForbiddenException();
}
}
}

View File

@ -0,0 +1,35 @@
import { WorkspaceQueryPostHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface';
import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
import { WorkspaceQueryHookType } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/types/workspace-query-hook.type';
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
import { UserInputError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
import { ApplyMessagesVisibilityRestrictionsService } from 'src/modules/messaging/common/query-hooks/message/apply-messages-visibility-restrictions.service';
import { MessageWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message.workspace-entity';
@WorkspaceQueryHook({
key: `message.findMany`,
type: WorkspaceQueryHookType.PostHook,
})
export class MessageFindManyPostQueryHook
implements WorkspaceQueryPostHookInstance
{
constructor(
private readonly applyMessagesVisibilityRestrictionsService: ApplyMessagesVisibilityRestrictionsService,
) {}
async execute(
authContext: AuthContext,
_objectName: string,
payload: MessageWorkspaceEntity[],
): Promise<void> {
if (!authContext.workspaceMemberId) {
throw new UserInputError('Workspace member id is required');
}
await this.applyMessagesVisibilityRestrictionsService.applyMessagesVisibilityRestrictions(
authContext.workspaceMemberId,
payload,
);
}
}

View File

@ -1,58 +0,0 @@
import { BadRequestException, NotFoundException } from '@nestjs/common';
import { WorkspaceQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface';
import { FindManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
import { CanAccessMessageThreadService } from 'src/modules/messaging/common/query-hooks/message/can-access-message-thread.service';
import { MessageChannelMessageAssociationWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel-message-association.workspace-entity';
@WorkspaceQueryHook(`message.findMany`)
export class MessageFindManyPreQueryHook implements WorkspaceQueryHookInstance {
constructor(
private readonly canAccessMessageThreadService: CanAccessMessageThreadService,
private readonly twentyORMManager: TwentyORMManager,
) {}
async execute(
authContext: AuthContext,
objectName: string,
payload: FindManyResolverArgs,
): Promise<FindManyResolverArgs> {
if (!payload?.filter?.messageThreadId?.eq) {
throw new BadRequestException('messageThreadId filter is required');
}
if (!authContext.user?.id) {
throw new BadRequestException('User id is required');
}
const messageChannelMessageAssociationRepository =
await this.twentyORMManager.getRepository<MessageChannelMessageAssociationWorkspaceEntity>(
'messageChannelMessageAssociation',
);
const messageChannelMessageAssociations =
await messageChannelMessageAssociationRepository.find({
where: {
message: {
messageThreadId: payload.filter.messageThreadId.eq,
},
},
});
if (messageChannelMessageAssociations.length === 0) {
throw new NotFoundException();
}
await this.canAccessMessageThreadService.canAccessMessageThread(
authContext.user.id,
authContext.workspace.id,
messageChannelMessageAssociations,
);
return payload;
}
}

View File

@ -0,0 +1,35 @@
import { WorkspaceQueryPostHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface';
import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
import { WorkspaceQueryHookType } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/types/workspace-query-hook.type';
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
import { UserInputError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
import { ApplyMessagesVisibilityRestrictionsService } from 'src/modules/messaging/common/query-hooks/message/apply-messages-visibility-restrictions.service';
import { MessageWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message.workspace-entity';
@WorkspaceQueryHook({
key: `message.findOne`,
type: WorkspaceQueryHookType.PostHook,
})
export class MessageFindOnePostQueryHook
implements WorkspaceQueryPostHookInstance
{
constructor(
private readonly applyMessagesVisibilityRestrictionsService: ApplyMessagesVisibilityRestrictionsService,
) {}
async execute(
authContext: AuthContext,
_objectName: string,
payload: MessageWorkspaceEntity[],
): Promise<void> {
if (!authContext.workspaceMemberId) {
throw new UserInputError('Workspace member id is required');
}
await this.applyMessagesVisibilityRestrictionsService.applyMessagesVisibilityRestrictions(
authContext.workspaceMemberId,
payload,
);
}
}

View File

@ -1,53 +0,0 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { NotFoundException } from '@nestjs/common';
import { WorkspaceQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface';
import { FindOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
import { CanAccessMessageThreadService } from 'src/modules/messaging/common/query-hooks/message/can-access-message-thread.service';
import { MessageChannelMessageAssociationWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel-message-association.workspace-entity';
@WorkspaceQueryHook(`message.findOne`)
export class MessageFindOnePreQueryHook implements WorkspaceQueryHookInstance {
constructor(
private readonly canAccessMessageThreadService: CanAccessMessageThreadService,
private readonly twentyORMManager: TwentyORMManager,
) {}
async execute(
authContext: AuthContext,
objectName: string,
payload: FindOneResolverArgs,
): Promise<FindOneResolverArgs> {
if (!authContext.user?.id) {
throw new NotFoundException('User id is required');
}
const messageChannelMessageAssociationRepository =
await this.twentyORMManager.getRepository<MessageChannelMessageAssociationWorkspaceEntity>(
'messageChannelMessageAssociation',
);
const messageChannelMessageAssociations =
await messageChannelMessageAssociationRepository.find({
where: {
messageId: payload?.filter?.id?.eq,
},
});
if (messageChannelMessageAssociations.length === 0) {
throw new NotFoundException();
}
await this.canAccessMessageThreadService.canAccessMessageThread(
authContext.user.id,
authContext.workspace.id,
messageChannelMessageAssociations,
);
return payload;
}
}

View File

@ -1,9 +1,9 @@
import { Module } from '@nestjs/common';
import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module';
import { CanAccessMessageThreadService } from 'src/modules/messaging/common/query-hooks/message/can-access-message-thread.service';
import { MessageFindManyPreQueryHook } from 'src/modules/messaging/common/query-hooks/message/message-find-many.pre-query.hook';
import { MessageFindOnePreQueryHook } from 'src/modules/messaging/common/query-hooks/message/message-find-one.pre-query-hook';
import { ApplyMessagesVisibilityRestrictionsService } from 'src/modules/messaging/common/query-hooks/message/apply-messages-visibility-restrictions.service';
import { MessageFindManyPostQueryHook } from 'src/modules/messaging/common/query-hooks/message/message-find-many.post-query.hook';
import { MessageFindOnePostQueryHook } from 'src/modules/messaging/common/query-hooks/message/message-find-one.post-query.hook';
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
@Module({
@ -11,9 +11,9 @@ import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/sta
ObjectMetadataRepositoryModule.forFeature([WorkspaceMemberWorkspaceEntity]),
],
providers: [
CanAccessMessageThreadService,
MessageFindOnePreQueryHook,
MessageFindManyPreQueryHook,
ApplyMessagesVisibilityRestrictionsService,
MessageFindOnePostQueryHook,
MessageFindManyPostQueryHook,
],
})
export class MessagingQueryHookModule {}