Fix calendar events & messages fetching + fix timeline design (#11840)
Preview : <img width="501" alt="Screenshot 2025-05-02 at 16 24 34" src="https://github.com/user-attachments/assets/0c649df1-0e26-4ddc-8e13-ebd78af7ec09" /> Done : - Fix getCalendarEventsFromPersonIds and getCalendarEventsFromCompanyId (include accountOwner check) - Fix permission check on pre-hook - Pre-hook seems useless, calendar events are always on METADATA or SHARE_EVERYTHING visibility, else post hook always has the responsibility of returning the data user can access. >> To delete or to keep in case other visibility options are added ? - Add post hook to secure finOne / findMany calendarEvents resolver - Update design To do : - same on messages (PR to arrive) closes : https://github.com/twentyhq/twenty/issues/9826
This commit is contained in:
@ -7,6 +7,7 @@ import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/
|
||||
import { TIMELINE_CALENDAR_EVENTS_MAX_PAGE_SIZE } from 'src/engine/core-modules/calendar/constants/calendar.constants';
|
||||
import { TimelineCalendarEventsWithTotal } from 'src/engine/core-modules/calendar/dtos/timeline-calendar-events-with-total.dto';
|
||||
import { TimelineCalendarEventService } from 'src/engine/core-modules/calendar/timeline-calendar-event.service';
|
||||
import { AuthWorkspaceMemberId } from 'src/engine/decorators/auth/auth-workspace-member-id.decorator';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
|
||||
@ArgsType()
|
||||
@ -46,13 +47,15 @@ export class TimelineCalendarEventResolver {
|
||||
async getTimelineCalendarEventsFromPersonId(
|
||||
@Args()
|
||||
{ personId, page, pageSize }: GetTimelineCalendarEventsFromPersonIdArgs,
|
||||
@AuthWorkspaceMemberId() workspaceMemberId: string,
|
||||
) {
|
||||
const timelineCalendarEvents =
|
||||
await this.timelineCalendarEventService.getCalendarEventsFromPersonIds(
|
||||
[personId],
|
||||
await this.timelineCalendarEventService.getCalendarEventsFromPersonIds({
|
||||
currentWorkspaceMemberId: workspaceMemberId,
|
||||
personIds: [personId],
|
||||
page,
|
||||
pageSize,
|
||||
);
|
||||
});
|
||||
|
||||
return timelineCalendarEvents;
|
||||
}
|
||||
@ -61,13 +64,15 @@ export class TimelineCalendarEventResolver {
|
||||
async getTimelineCalendarEventsFromCompanyId(
|
||||
@Args()
|
||||
{ companyId, page, pageSize }: GetTimelineCalendarEventsFromCompanyIdArgs,
|
||||
@AuthWorkspaceMemberId() workspaceMemberId: string,
|
||||
) {
|
||||
const timelineCalendarEvents =
|
||||
await this.timelineCalendarEventService.getCalendarEventsFromCompanyId(
|
||||
await this.timelineCalendarEventService.getCalendarEventsFromCompanyId({
|
||||
currentWorkspaceMemberId: workspaceMemberId,
|
||||
companyId,
|
||||
page,
|
||||
pageSize,
|
||||
);
|
||||
});
|
||||
|
||||
return timelineCalendarEvents;
|
||||
}
|
||||
|
||||
@ -0,0 +1,176 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED } from 'twenty-shared/constants';
|
||||
|
||||
import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
|
||||
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
|
||||
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 { TimelineCalendarEventService } from './timeline-calendar-event.service';
|
||||
|
||||
type MockWorkspaceRepository = Partial<
|
||||
WorkspaceRepository<CalendarEventWorkspaceEntity>
|
||||
> & {
|
||||
find: jest.Mock;
|
||||
findAndCount: jest.Mock;
|
||||
};
|
||||
|
||||
describe('TimelineCalendarEventService', () => {
|
||||
let service: TimelineCalendarEventService;
|
||||
let mockCalendarEventRepository: MockWorkspaceRepository;
|
||||
|
||||
const mockCalendarEvent: Partial<CalendarEventWorkspaceEntity> = {
|
||||
id: '1',
|
||||
title: 'Test Event',
|
||||
description: 'Test Description',
|
||||
startsAt: '2024-01-01T00:00:00.000Z',
|
||||
endsAt: '2024-01-01T01:00:00.000Z',
|
||||
calendarEventParticipants: [],
|
||||
calendarChannelEventAssociations: [],
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
mockCalendarEventRepository = {
|
||||
find: jest.fn(),
|
||||
findAndCount: jest.fn(),
|
||||
};
|
||||
|
||||
const mockTwentyORMManager = {
|
||||
getRepository: jest.fn().mockResolvedValue(mockCalendarEventRepository),
|
||||
};
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
TimelineCalendarEventService,
|
||||
{
|
||||
provide: TwentyORMManager,
|
||||
useValue: mockTwentyORMManager,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<TimelineCalendarEventService>(
|
||||
TimelineCalendarEventService,
|
||||
);
|
||||
});
|
||||
|
||||
it('should return non-obfuscated calendar events if visibility is SHARE_EVERYTHING', async () => {
|
||||
const currentWorkspaceMemberId = 'current-workspace-member-id';
|
||||
const personIds = ['person-1'];
|
||||
|
||||
mockCalendarEventRepository.find.mockResolvedValue([
|
||||
{ id: '1', startsAt: new Date() },
|
||||
]);
|
||||
mockCalendarEventRepository.findAndCount.mockResolvedValue([
|
||||
[
|
||||
{
|
||||
...mockCalendarEvent,
|
||||
calendarChannelEventAssociations: [
|
||||
{
|
||||
calendarChannel: {
|
||||
visibility: CalendarChannelVisibility.SHARE_EVERYTHING,
|
||||
connectedAccount: {
|
||||
accountOwnerId: 'other-workspace-member-id',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
1,
|
||||
]);
|
||||
|
||||
const result = await service.getCalendarEventsFromPersonIds({
|
||||
currentWorkspaceMemberId,
|
||||
personIds,
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
});
|
||||
|
||||
expect(result.timelineCalendarEvents[0].title).toBe('Test Event');
|
||||
expect(result.timelineCalendarEvents[0].description).toBe(
|
||||
'Test Description',
|
||||
);
|
||||
});
|
||||
|
||||
it('should return obfuscated calendar events if visibility is METADATA', async () => {
|
||||
const currentWorkspaceMemberId = 'current-workspace-member-id';
|
||||
const personIds = ['person-1'];
|
||||
|
||||
mockCalendarEventRepository.find.mockResolvedValue([
|
||||
{ id: '1', startsAt: new Date() },
|
||||
]);
|
||||
mockCalendarEventRepository.findAndCount.mockResolvedValue([
|
||||
[
|
||||
{
|
||||
...mockCalendarEvent,
|
||||
calendarChannelEventAssociations: [
|
||||
{
|
||||
calendarChannel: {
|
||||
visibility: CalendarChannelVisibility.METADATA,
|
||||
connectedAccount: {
|
||||
accountOwnerId: 'other-workspace-member-id',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
1,
|
||||
]);
|
||||
|
||||
const result = await service.getCalendarEventsFromPersonIds({
|
||||
currentWorkspaceMemberId,
|
||||
personIds,
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
});
|
||||
|
||||
expect(result.timelineCalendarEvents[0].title).toBe(
|
||||
FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED,
|
||||
);
|
||||
expect(result.timelineCalendarEvents[0].description).toBe(
|
||||
FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED,
|
||||
);
|
||||
});
|
||||
|
||||
it('should return non-obfuscated calendar events if visibility is METADATA and user is calendar events owner', async () => {
|
||||
const currentWorkspaceMemberId = 'current-workspace-member-id';
|
||||
const personIds = ['person-1'];
|
||||
|
||||
mockCalendarEventRepository.find.mockResolvedValue([
|
||||
{ id: '1', startsAt: new Date() },
|
||||
]);
|
||||
mockCalendarEventRepository.findAndCount.mockResolvedValue([
|
||||
[
|
||||
{
|
||||
...mockCalendarEvent,
|
||||
calendarChannelEventAssociations: [
|
||||
{
|
||||
calendarChannel: {
|
||||
visibility: CalendarChannelVisibility.METADATA,
|
||||
connectedAccount: {
|
||||
accountOwnerId: 'current-workspace-member-id',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
1,
|
||||
]);
|
||||
|
||||
const result = await service.getCalendarEventsFromPersonIds({
|
||||
currentWorkspaceMemberId,
|
||||
personIds,
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
});
|
||||
|
||||
expect(result.timelineCalendarEvents[0].title).toBe('Test Event');
|
||||
expect(result.timelineCalendarEvents[0].description).toBe(
|
||||
'Test Description',
|
||||
);
|
||||
});
|
||||
});
|
||||
@ -1,6 +1,7 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import omit from 'lodash.omit';
|
||||
import { FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED } from 'twenty-shared/constants';
|
||||
import { Any } from 'typeorm';
|
||||
|
||||
import { TIMELINE_CALENDAR_EVENTS_DEFAULT_PAGE_SIZE } from 'src/engine/core-modules/calendar/constants/calendar.constants';
|
||||
@ -15,11 +16,17 @@ export class TimelineCalendarEventService {
|
||||
constructor(private readonly twentyORMManager: TwentyORMManager) {}
|
||||
|
||||
// TODO: Align return type with the entities to avoid mapping
|
||||
async getCalendarEventsFromPersonIds(
|
||||
personIds: string[],
|
||||
async getCalendarEventsFromPersonIds({
|
||||
currentWorkspaceMemberId,
|
||||
personIds,
|
||||
page = 1,
|
||||
pageSize: number = TIMELINE_CALENDAR_EVENTS_DEFAULT_PAGE_SIZE,
|
||||
): Promise<TimelineCalendarEventsWithTotal> {
|
||||
pageSize = TIMELINE_CALENDAR_EVENTS_DEFAULT_PAGE_SIZE,
|
||||
}: {
|
||||
currentWorkspaceMemberId: string;
|
||||
personIds: string[];
|
||||
page: number;
|
||||
pageSize: number;
|
||||
}): Promise<TimelineCalendarEventsWithTotal> {
|
||||
const offset = (page - 1) * pageSize;
|
||||
|
||||
const calendarEventRepository =
|
||||
@ -64,7 +71,11 @@ export class TimelineCalendarEventService {
|
||||
workspaceMember: true,
|
||||
},
|
||||
calendarChannelEventAssociations: {
|
||||
calendarChannel: true,
|
||||
calendarChannel: {
|
||||
connectedAccount: {
|
||||
accountOwner: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
@ -103,17 +114,35 @@ export class TimelineCalendarEventService {
|
||||
handle: participant.handle,
|
||||
}),
|
||||
);
|
||||
const visibility = event.calendarChannelEventAssociations.some(
|
||||
(association) => association.calendarChannel.visibility === 'METADATA',
|
||||
)
|
||||
? CalendarChannelVisibility.METADATA
|
||||
: CalendarChannelVisibility.SHARE_EVERYTHING;
|
||||
|
||||
const isCalendarEventImportedByCurrentWorkspaceMember =
|
||||
event.calendarChannelEventAssociations.some(
|
||||
(association) =>
|
||||
association.calendarChannel.connectedAccount.accountOwnerId ===
|
||||
currentWorkspaceMemberId,
|
||||
);
|
||||
|
||||
const visibility =
|
||||
event.calendarChannelEventAssociations.some(
|
||||
(association) =>
|
||||
association.calendarChannel.visibility === 'SHARE_EVERYTHING',
|
||||
) || isCalendarEventImportedByCurrentWorkspaceMember
|
||||
? CalendarChannelVisibility.SHARE_EVERYTHING
|
||||
: CalendarChannelVisibility.METADATA;
|
||||
|
||||
return {
|
||||
...omit(event, [
|
||||
'calendarEventParticipants',
|
||||
'calendarChannelEventAssociations',
|
||||
]),
|
||||
title:
|
||||
visibility === CalendarChannelVisibility.METADATA
|
||||
? FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED
|
||||
: event.title,
|
||||
description:
|
||||
visibility === CalendarChannelVisibility.METADATA
|
||||
? FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED
|
||||
: event.description,
|
||||
startsAt: event.startsAt as unknown as Date,
|
||||
endsAt: event.endsAt as unknown as Date,
|
||||
participants,
|
||||
@ -127,11 +156,17 @@ export class TimelineCalendarEventService {
|
||||
};
|
||||
}
|
||||
|
||||
async getCalendarEventsFromCompanyId(
|
||||
companyId: string,
|
||||
async getCalendarEventsFromCompanyId({
|
||||
currentWorkspaceMemberId,
|
||||
companyId,
|
||||
page = 1,
|
||||
pageSize: number = TIMELINE_CALENDAR_EVENTS_DEFAULT_PAGE_SIZE,
|
||||
): Promise<TimelineCalendarEventsWithTotal> {
|
||||
pageSize = TIMELINE_CALENDAR_EVENTS_DEFAULT_PAGE_SIZE,
|
||||
}: {
|
||||
currentWorkspaceMemberId: string;
|
||||
companyId: string;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
}): Promise<TimelineCalendarEventsWithTotal> {
|
||||
const personRepository =
|
||||
await this.twentyORMManager.getRepository<PersonWorkspaceEntity>(
|
||||
'person',
|
||||
@ -155,12 +190,13 @@ export class TimelineCalendarEventService {
|
||||
|
||||
const formattedPersonIds = personIds.map(({ id }) => id);
|
||||
|
||||
const messageThreads = await this.getCalendarEventsFromPersonIds(
|
||||
formattedPersonIds,
|
||||
const calendarEvents = await this.getCalendarEventsFromPersonIds({
|
||||
currentWorkspaceMemberId,
|
||||
personIds: formattedPersonIds,
|
||||
page,
|
||||
pageSize,
|
||||
);
|
||||
});
|
||||
|
||||
return messageThreads;
|
||||
return calendarEvents;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user