fix calendar events and messages post hooks (#12034)

Context
workspaceMemberId is not always present in AuthContext. Solution
implemented here is to fetch workspaceMemberId via userId.

QRQC coming ... 

Tested
- FindManyCalendarEvents / FindOneCalendarEvent
- FindManyMessages / FindOneMessage

closes https://github.com/twentyhq/twenty/issues/12027
This commit is contained in:
Etienne
2025-05-14 15:52:52 +02:00
committed by GitHub
parent 0c60fa9c23
commit 929f7876de
8 changed files with 263 additions and 49 deletions

View File

@ -1,9 +1,11 @@
import { isDefined } from 'twenty-shared/utils';
import { WorkspacePostQueryHookInstance } 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 { ForbiddenError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
import { ApplyCalendarEventsVisibilityRestrictionsService } from 'src/modules/calendar/common/query-hooks/calendar-event/services/apply-calendar-events-visibility-restrictions.service';
import { CalendarEventWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-event.workspace-entity';
@ -23,13 +25,15 @@ export class CalendarEventFindManyPostQueryHook
_objectName: string,
payload: CalendarEventWorkspaceEntity[],
): Promise<void> {
if (!authContext.workspaceMemberId) {
throw new UserInputError('Workspace member id is required');
const { user, apiKey } = authContext;
if (!isDefined(user) && !isDefined(apiKey)) {
throw new ForbiddenError('User is required');
}
await this.applyCalendarEventsVisibilityRestrictionsService.applyCalendarEventsVisibilityRestrictions(
authContext.workspaceMemberId,
payload,
user?.id,
);
}
}

View File

@ -1,9 +1,11 @@
import { isDefined } from 'twenty-shared/utils';
import { WorkspacePostQueryHookInstance } 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 { ForbiddenError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
import { ApplyCalendarEventsVisibilityRestrictionsService } from 'src/modules/calendar/common/query-hooks/calendar-event/services/apply-calendar-events-visibility-restrictions.service';
import { CalendarEventWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-event.workspace-entity';
@ -23,13 +25,15 @@ export class CalendarEventFindOnePostQueryHook
_objectName: string,
payload: CalendarEventWorkspaceEntity[],
): Promise<void> {
if (!authContext.workspaceMemberId) {
throw new UserInputError('Workspace member id is required');
const { user, apiKey } = authContext;
if (!isDefined(user) && !isDefined(apiKey)) {
throw new ForbiddenError('User is required');
}
await this.applyCalendarEventsVisibilityRestrictionsService.applyCalendarEventsVisibilityRestrictions(
authContext.workspaceMemberId,
payload,
user?.id,
);
}
}

View File

@ -48,6 +48,10 @@ describe('ApplyCalendarEventsVisibilityRestrictionsService', () => {
find: jest.fn(),
};
const mockWorkspaceMemberRepository = {
findOneByOrFail: jest.fn(),
};
const mockTwentyORMManager = {
getRepository: jest.fn().mockImplementation((name) => {
if (name === 'calendarChannelEventAssociation') {
@ -56,6 +60,9 @@ describe('ApplyCalendarEventsVisibilityRestrictionsService', () => {
if (name === 'connectedAccount') {
return mockConnectedAccountRepository;
}
if (name === 'workspaceMember') {
return mockWorkspaceMemberRepository;
}
}),
};
@ -83,6 +90,10 @@ describe('ApplyCalendarEventsVisibilityRestrictionsService', () => {
createMockCalendarEvent('1', 'Test Event', 'Test Description'),
];
mockWorkspaceMemberRepository.findOneByOrFail.mockResolvedValue({
id: 'workspace-member-id',
});
mockCalendarEventAssociationRepository.find.mockResolvedValue([
{
calendarEventId: '1',
@ -94,8 +105,8 @@ describe('ApplyCalendarEventsVisibilityRestrictionsService', () => {
]);
const result = await service.applyCalendarEventsVisibilityRestrictions(
'workspace-member-id',
calendarEvents,
'user-id',
);
expect(result).toEqual(calendarEvents);
@ -124,11 +135,15 @@ describe('ApplyCalendarEventsVisibilityRestrictionsService', () => {
},
]);
mockWorkspaceMemberRepository.findOneByOrFail.mockResolvedValue({
id: 'workspace-member-id',
});
mockConnectedAccountRepository.find.mockResolvedValue([]);
const result = await service.applyCalendarEventsVisibilityRestrictions(
'workspace-member-id',
calendarEvents,
'user-id',
);
expect(result).toEqual([
@ -155,11 +170,15 @@ describe('ApplyCalendarEventsVisibilityRestrictionsService', () => {
},
]);
mockWorkspaceMemberRepository.findOneByOrFail.mockResolvedValue({
id: 'workspace-member-account-owner-id',
});
mockConnectedAccountRepository.find.mockResolvedValue([{ id: '1' }]);
const result = await service.applyCalendarEventsVisibilityRestrictions(
'workspace-member-id',
calendarEvents,
'user-id',
);
expect(result).toEqual(calendarEvents);
@ -186,11 +205,15 @@ describe('ApplyCalendarEventsVisibilityRestrictionsService', () => {
},
]);
mockWorkspaceMemberRepository.findOneByOrFail.mockResolvedValue({
id: 'workspace-member-not-account-owner-id',
});
mockConnectedAccountRepository.find.mockResolvedValue([]);
const result = await service.applyCalendarEventsVisibilityRestrictions(
'workspace-member-id',
calendarEvents,
'user-id',
);
expect(result).toEqual([]);
@ -203,6 +226,10 @@ describe('ApplyCalendarEventsVisibilityRestrictionsService', () => {
createMockCalendarEvent('3', 'Event 3', 'Description 3'),
];
mockWorkspaceMemberRepository.findOneByOrFail.mockResolvedValue({
id: 'workspace-member-id',
});
mockCalendarEventAssociationRepository.find.mockResolvedValue([
{
calendarEventId: '1',
@ -232,8 +259,63 @@ describe('ApplyCalendarEventsVisibilityRestrictionsService', () => {
.mockResolvedValueOnce([{ id: '1' }]); // request for calendar event 2
const result = await service.applyCalendarEventsVisibilityRestrictions(
'workspace-member-id',
calendarEvents,
'user-id',
);
expect(result).toEqual([
calendarEvents[0],
calendarEvents[1],
{
...calendarEvents[2],
title: FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED,
description: FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED,
},
]);
});
it('should return all calendar events with the right visibility when userId is undefined (api key request)', async () => {
const calendarEvents = [
createMockCalendarEvent('1', 'Event 1', 'Description 1'),
createMockCalendarEvent('2', 'Event 2', 'Description 2'),
createMockCalendarEvent('3', 'Event 3', 'Description 3'),
];
mockWorkspaceMemberRepository.findOneByOrFail.mockResolvedValue({
id: 'workspace-member-id',
});
mockCalendarEventAssociationRepository.find.mockResolvedValue([
{
calendarEventId: '1',
calendarChannel: {
id: '1',
visibility: CalendarChannelVisibility.SHARE_EVERYTHING,
},
},
{
calendarEventId: '2',
calendarChannel: {
id: '2',
visibility: CalendarChannelVisibility.METADATA,
},
},
{
calendarEventId: '3',
calendarChannel: {
id: '3',
visibility: CalendarChannelVisibility.METADATA,
},
},
]);
mockConnectedAccountRepository.find
.mockResolvedValueOnce([]) // request for calendar event 3
.mockResolvedValueOnce([{ id: '1' }]); // request for calendar event 2
const result = await service.applyCalendarEventsVisibilityRestrictions(
calendarEvents,
undefined,
);
expect(result).toEqual([

View File

@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common';
import groupBy from 'lodash.groupby';
import { FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED } from 'twenty-shared/constants';
import { isDefined } from 'twenty-shared/utils';
import { In } from 'typeorm';
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
@ -9,14 +10,15 @@ import { CalendarChannelEventAssociationWorkspaceEntity } from 'src/modules/cale
import { CalendarChannelVisibility } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
import { CalendarEventWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-event.workspace-entity';
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
@Injectable()
export class ApplyCalendarEventsVisibilityRestrictionsService {
constructor(private readonly twentyORMManager: TwentyORMManager) {}
public async applyCalendarEventsVisibilityRestrictions(
workspaceMemberId: string,
calendarEvents: CalendarEventWorkspaceEntity[],
userId?: string, // undefined when request is made with api key
) {
const calendarChannelEventAssociationRepository =
await this.twentyORMManager.getRepository<CalendarChannelEventAssociationWorkspaceEntity>(
@ -36,6 +38,11 @@ export class ApplyCalendarEventsVisibilityRestrictionsService {
'connectedAccount',
);
const workspaceMemberRepository =
await this.twentyORMManager.getRepository<WorkspaceMemberWorkspaceEntity>(
'workspaceMember',
);
for (let i = calendarEvents.length - 1; i >= 0; i--) {
const calendarChannelCalendarEventAssociations =
calendarChannelCalendarEventsAssociations.filter(
@ -59,18 +66,26 @@ export class ApplyCalendarEventsVisibilityRestrictionsService {
continue;
}
const connectedAccounts = await connectedAccountRepository.find({
select: ['id'],
where: {
calendarChannels: {
id: In(calendarChannels.map((channel) => channel.id)),
if (isDefined(userId)) {
const workspaceMember = await workspaceMemberRepository.findOneByOrFail(
{
userId: userId,
},
accountOwnerId: workspaceMemberId,
},
});
);
if (connectedAccounts.length > 0) {
continue;
const connectedAccounts = await connectedAccountRepository.find({
select: ['id'],
where: {
calendarChannels: {
id: In(calendarChannels.map((channel) => channel.id)),
},
accountOwnerId: workspaceMember.id,
},
});
if (connectedAccounts.length > 0) {
continue;
}
}
if (

View File

@ -38,6 +38,10 @@ describe('ApplyMessagesVisibilityRestrictionsService', () => {
find: jest.fn(),
};
const mockWorkspaceMemberRepository = {
findOneByOrFail: jest.fn(),
};
const mockTwentyORMManager = {
getRepository: jest.fn().mockImplementation((name) => {
if (name === 'messageChannelMessageAssociation') {
@ -46,6 +50,9 @@ describe('ApplyMessagesVisibilityRestrictionsService', () => {
if (name === 'connectedAccount') {
return mockConnectedAccountRepository;
}
if (name === 'workspaceMember') {
return mockWorkspaceMemberRepository;
}
}),
};
@ -83,8 +90,8 @@ describe('ApplyMessagesVisibilityRestrictionsService', () => {
]);
const result = await service.applyMessagesVisibilityRestrictions(
'workspace-member-id',
messages,
'user-id',
);
expect(result).toEqual(messages);
@ -114,9 +121,13 @@ describe('ApplyMessagesVisibilityRestrictionsService', () => {
mockConnectedAccountRepository.find.mockResolvedValue([]);
mockWorkspaceMemberRepository.findOneByOrFail.mockResolvedValue({
id: 'workspace-member-id',
});
const result = await service.applyMessagesVisibilityRestrictions(
'workspace-member-id',
messages,
'user-id',
);
expect(result).toEqual([
@ -144,9 +155,13 @@ describe('ApplyMessagesVisibilityRestrictionsService', () => {
mockConnectedAccountRepository.find.mockResolvedValue([]);
mockWorkspaceMemberRepository.findOneByOrFail.mockResolvedValue({
id: 'workspace-member-id',
});
const result = await service.applyMessagesVisibilityRestrictions(
'workspace-member-id',
messages,
'user-id',
);
expect(result).toEqual([
@ -173,11 +188,15 @@ describe('ApplyMessagesVisibilityRestrictionsService', () => {
},
]);
mockWorkspaceMemberRepository.findOneByOrFail.mockResolvedValue({
id: 'workspace-member-account-owner-id',
});
mockConnectedAccountRepository.find.mockResolvedValue([{ id: '1' }]);
const result = await service.applyMessagesVisibilityRestrictions(
'workspace-member-id',
messages,
'user-id',
);
expect(result).toEqual(messages);
@ -203,11 +222,15 @@ describe('ApplyMessagesVisibilityRestrictionsService', () => {
},
]);
mockWorkspaceMemberRepository.findOneByOrFail.mockResolvedValue({
id: 'workspace-member-not-account-owner-id',
});
mockConnectedAccountRepository.find.mockResolvedValue([]);
const result = await service.applyMessagesVisibilityRestrictions(
'workspace-member-id',
messages,
'user-id',
);
expect(result).toEqual([]);
@ -244,13 +267,75 @@ describe('ApplyMessagesVisibilityRestrictionsService', () => {
},
]);
mockWorkspaceMemberRepository.findOneByOrFail.mockResolvedValue({
id: 'workspace-member-id',
});
mockConnectedAccountRepository.find
.mockResolvedValueOnce([]) // request for message 3
.mockResolvedValueOnce([]); // request for message 2
const result = await service.applyMessagesVisibilityRestrictions(
'workspace-member-id',
messages,
'user-id',
);
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,
},
]);
});
it('should return all messages with the right visibility when userId is undefined (api key request)', 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,
},
},
]);
mockWorkspaceMemberRepository.findOneByOrFail.mockResolvedValue({
id: 'workspace-member-id',
});
mockConnectedAccountRepository.find
.mockResolvedValueOnce([]) // request for message 3
.mockResolvedValueOnce([]); // request for message 2
const result = await service.applyMessagesVisibilityRestrictions(
messages,
undefined,
);
expect(result).toEqual([

View File

@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common';
import groupBy from 'lodash.groupby';
import { FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED } from 'twenty-shared/constants';
import { isDefined } from 'twenty-shared/utils';
import { In } from 'typeorm';
import { NotFoundError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
@ -10,14 +11,15 @@ import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/s
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';
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
@Injectable()
export class ApplyMessagesVisibilityRestrictionsService {
constructor(private readonly twentyORMManager: TwentyORMManager) {}
public async applyMessagesVisibilityRestrictions(
workspaceMemberId: string,
messages: MessageWorkspaceEntity[],
userId?: string, // undefined when request is made with api key
) {
const messageChannelMessageAssociationRepository =
await this.twentyORMManager.getRepository<MessageChannelMessageAssociationWorkspaceEntity>(
@ -37,6 +39,11 @@ export class ApplyMessagesVisibilityRestrictionsService {
'connectedAccount',
);
const workspaceMemberRepository =
await this.twentyORMManager.getRepository<WorkspaceMemberWorkspaceEntity>(
'workspaceMember',
);
for (let i = messages.length - 1; i >= 0; i--) {
const messageChannelMessageAssociations =
messageChannelMessagesAssociations.filter(
@ -66,19 +73,28 @@ export class ApplyMessagesVisibilityRestrictionsService {
continue;
}
const connectedAccounts = await connectedAccountRepository.find({
select: ['id'],
where: {
messageChannels: {
id: In(messageChannels.map((channel) => channel.id)),
if (isDefined(userId)) {
const workspaceMember = await workspaceMemberRepository.findOneByOrFail(
{
userId,
},
accountOwnerId: workspaceMemberId,
},
});
);
if (connectedAccounts.length > 0) {
continue;
const connectedAccounts = await connectedAccountRepository.find({
select: ['id'],
where: {
messageChannels: {
id: In(messageChannels.map((channel) => channel.id)),
},
accountOwnerId: workspaceMember.id,
},
});
if (connectedAccounts.length > 0) {
continue;
}
}
if (messageChannelsGroupByVisibility[MessageChannelVisibility.SUBJECT]) {
messages[i].text = FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED;
continue;

View File

@ -1,9 +1,11 @@
import { isDefined } from 'twenty-shared/utils';
import { WorkspacePostQueryHookInstance } 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 { ForbiddenError } 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';
@ -23,13 +25,15 @@ export class MessageFindManyPostQueryHook
_objectName: string,
payload: MessageWorkspaceEntity[],
): Promise<void> {
if (!authContext.workspaceMemberId) {
throw new UserInputError('Workspace member id is required');
const { user, apiKey } = authContext;
if (!isDefined(user) && !isDefined(apiKey)) {
throw new ForbiddenError('User is required');
}
await this.applyMessagesVisibilityRestrictionsService.applyMessagesVisibilityRestrictions(
authContext.workspaceMemberId,
payload,
user?.id,
);
}
}

View File

@ -1,9 +1,11 @@
import { isDefined } from 'twenty-shared/utils';
import { WorkspacePostQueryHookInstance } 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 { ForbiddenError } 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';
@ -23,13 +25,15 @@ export class MessageFindOnePostQueryHook
_objectName: string,
payload: MessageWorkspaceEntity[],
): Promise<void> {
if (!authContext.workspaceMemberId) {
throw new UserInputError('Workspace member id is required');
const { user, apiKey } = authContext;
if (!isDefined(user) && !isDefined(apiKey)) {
throw new ForbiddenError('User is required');
}
await this.applyMessagesVisibilityRestrictionsService.applyMessagesVisibilityRestrictions(
authContext.workspaceMemberId,
payload,
user?.id,
);
}
}