Skip to content

Commit

Permalink
4643 create a pre hook for calendar events (twentyhq#4666)
Browse files Browse the repository at this point in the history
* copy message pre hook

* add CalendarQueryHookModule to workspace-pre-query-hook.module

* use CalendarChannelVisibility enum

* add calendarEvent to workspace-pre-query-hook.config

* fix pre-hook

* fix findOne prehook in config

* rename fragments

* fix import

* update findOne prehook and create can-access-calendar-event.provider

* replace provider with service

* fix type

* renaming

* remove unnecessary eslint skip

---------

Co-authored-by: Weiko <[email protected]>
  • Loading branch information
bosiraphael and Weiko authored Mar 27, 2024
1 parent c3cc0f6 commit d687523
Show file tree
Hide file tree
Showing 13 changed files with 257 additions and 38 deletions.
21 changes: 11 additions & 10 deletions packages/twenty-front/src/generated/graphql.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -860,6 +860,7 @@ export type Object = {
imageIdentifierFieldMetadataId?: Maybe<Scalars['String']>;
isActive: Scalars['Boolean'];
isCustom: Scalars['Boolean'];
isRemote: Scalars['Boolean'];
isSystem: Scalars['Boolean'];
labelIdentifierFieldMetadataId?: Maybe<Scalars['String']>;
labelPlural: Scalars['String'];
Expand Down Expand Up @@ -905,9 +906,9 @@ export type RelationEdge = {
node: Relation;
};

export type AttendeeFragmentFragment = { __typename?: 'TimelineCalendarEventAttendee', personId?: string | null, workspaceMemberId?: string | null, firstName: string, lastName: string, displayName: string, avatarUrl: string, handle: string };
export type TimelineCalendarEventAttendeeFragmentFragment = { __typename?: 'TimelineCalendarEventAttendee', personId?: string | null, workspaceMemberId?: string | null, firstName: string, lastName: string, displayName: string, avatarUrl: string, handle: string };

export type CalendarEventFragmentFragment = { __typename?: 'TimelineCalendarEvent', id: string, title: string, description: string, location: string, startsAt: string, endsAt: string, isFullDay: boolean, visibility: TimelineCalendarEventVisibility, attendees: Array<{ __typename?: 'TimelineCalendarEventAttendee', personId?: string | null, workspaceMemberId?: string | null, firstName: string, lastName: string, displayName: string, avatarUrl: string, handle: string }> };
export type TimelineCalendarEventFragmentFragment = { __typename?: 'TimelineCalendarEvent', id: string, title: string, description: string, location: string, startsAt: string, endsAt: string, isFullDay: boolean, visibility: TimelineCalendarEventVisibility, attendees: Array<{ __typename?: 'TimelineCalendarEventAttendee', personId?: string | null, workspaceMemberId?: string | null, firstName: string, lastName: string, displayName: string, avatarUrl: string, handle: string }> };

export type TimelineCalendarEventsWithTotalFragmentFragment = { __typename?: 'TimelineCalendarEventsWithTotal', totalNumberOfCalendarEvents: number, timelineCalendarEvents: Array<{ __typename?: 'TimelineCalendarEvent', id: string, title: string, description: string, location: string, startsAt: string, endsAt: string, isFullDay: boolean, visibility: TimelineCalendarEventVisibility, attendees: Array<{ __typename?: 'TimelineCalendarEventAttendee', personId?: string | null, workspaceMemberId?: string | null, firstName: string, lastName: string, displayName: string, avatarUrl: string, handle: string }> }> };

Expand Down Expand Up @@ -1154,8 +1155,8 @@ export type GetWorkspaceFromInviteHashQueryVariables = Exact<{

export type GetWorkspaceFromInviteHashQuery = { __typename?: 'Query', findWorkspaceFromInviteHash: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, allowImpersonation: boolean } };

export const AttendeeFragmentFragmentDoc = gql`
fragment AttendeeFragment on TimelineCalendarEventAttendee {
export const TimelineCalendarEventAttendeeFragmentFragmentDoc = gql`
fragment TimelineCalendarEventAttendeeFragment on TimelineCalendarEventAttendee {
personId
workspaceMemberId
firstName
Expand All @@ -1165,8 +1166,8 @@ export const AttendeeFragmentFragmentDoc = gql`
handle
}
`;
export const CalendarEventFragmentFragmentDoc = gql`
fragment CalendarEventFragment on TimelineCalendarEvent {
export const TimelineCalendarEventFragmentFragmentDoc = gql`
fragment TimelineCalendarEventFragment on TimelineCalendarEvent {
id
title
description
Expand All @@ -1176,18 +1177,18 @@ export const CalendarEventFragmentFragmentDoc = gql`
isFullDay
visibility
attendees {
...AttendeeFragment
...TimelineCalendarEventAttendeeFragment
}
}
${AttendeeFragmentFragmentDoc}`;
${TimelineCalendarEventAttendeeFragmentFragmentDoc}`;
export const TimelineCalendarEventsWithTotalFragmentFragmentDoc = gql`
fragment TimelineCalendarEventsWithTotalFragment on TimelineCalendarEventsWithTotal {
totalNumberOfCalendarEvents
timelineCalendarEvents {
...CalendarEventFragment
...TimelineCalendarEventFragment
}
}
${CalendarEventFragmentFragmentDoc}`;
${TimelineCalendarEventFragmentFragmentDoc}`;
export const ParticipantFragmentFragmentDoc = gql`
fragment ParticipantFragment on TimelineThreadParticipant {
personId
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { gql } from '@apollo/client';

export const attendeeFragment = gql`
fragment AttendeeFragment on TimelineCalendarEventAttendee {
export const timelineCalendarEventAttendeeFragment = gql`
fragment TimelineCalendarEventAttendeeFragment on TimelineCalendarEventAttendee {
personId
workspaceMemberId
firstName
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { gql } from '@apollo/client';

import { timelineCalendarEventAttendeeFragment } from '@/activities/calendar/queries/fragments/timelineCalendarEventAttendeeFragment';

export const timelineCalendarEventFragment = gql`
fragment TimelineCalendarEventFragment on TimelineCalendarEvent {
id
title
description
location
startsAt
endsAt
isFullDay
visibility
attendees {
...TimelineCalendarEventAttendeeFragment
}
}
${timelineCalendarEventAttendeeFragment}
`;
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { gql } from '@apollo/client';

import { calendarEventFragment } from '@/activities/calendar/queries/fragments/calendarEventFragment';
import { timelineCalendarEventFragment } from '@/activities/calendar/queries/fragments/timelineCalendarEventFragment';

export const timelineCalendarEventWithTotalFragment = gql`
fragment TimelineCalendarEventsWithTotalFragment on TimelineCalendarEventsWithTotal {
totalNumberOfCalendarEvents
timelineCalendarEvents {
...CalendarEventFragment
...TimelineCalendarEventFragment
}
}
${calendarEventFragment}
${timelineCalendarEventFragment}
`;
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { gql } from '@apollo/client';

import { timelineCalendarEventWithTotalFragment } from '@/activities/calendar/queries/fragments/calendarEventFragmentWithTotalFragment';
import { timelineCalendarEventWithTotalFragment } from '@/activities/calendar/queries/fragments/timelineCalendarEventWithTotalFragment';

export const getTimelineCalendarEventsFromCompanyId = gql`
query GetTimelineCalendarEventsFromCompanyId(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { gql } from '@apollo/client';

import { timelineCalendarEventWithTotalFragment } from '@/activities/calendar/queries/fragments/calendarEventFragmentWithTotalFragment';
import { timelineCalendarEventWithTotalFragment } from '@/activities/calendar/queries/fragments/timelineCalendarEventWithTotalFragment';

export const getTimelineCalendarEventsFromPersonId = gql`
query GetTimelineCalendarEventsFromPersonId(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import { MessageFindManyPreQueryHook } from 'src/modules/messaging/query-hooks/message/message-find-many.pre-query.hook';
import { MessageFindOnePreQueryHook } from 'src/modules/messaging/query-hooks/message/message-find-one.pre-query-hook';
import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/types/workspace-query-hook.type';
import { CalendarEventFindManyPreQueryHook } from 'src/modules/calendar/query-hooks/calendar-event/calendar-event-find-many.pre-query.hook';
import { CalendarEventFindOnePreQueryHook } from 'src/modules/calendar/query-hooks/calendar-event/calendar-event-find-one.pre-query-hook';

// TODO: move to a decorator
export const workspacePreQueryHooks: WorkspaceQueryHook = {
message: {
findOne: [MessageFindOnePreQueryHook.name],
findMany: [MessageFindManyPreQueryHook.name],
},
calendarEvent: {
findOne: [CalendarEventFindOnePreQueryHook.name],
findMany: [CalendarEventFindManyPreQueryHook.name],
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import { Module } from '@nestjs/common';

import { MessagingQueryHookModule } from 'src/modules/messaging/query-hooks/messaging-query-hook.module';
import { WorkspacePreQueryHookService } from 'src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/workspace-pre-query-hook.service';
import { CalendarQueryHookModule } from 'src/modules/calendar/query-hooks/calendar-query-hook.module';

@Module({
imports: [MessagingQueryHookModule],
imports: [MessagingQueryHookModule, CalendarQueryHookModule],
providers: [WorkspacePreQueryHookService],
exports: [WorkspacePreQueryHookService],
})
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import {
BadRequestException,
Injectable,
NotFoundException,
} from '@nestjs/common';

import { WorkspacePreQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/interfaces/workspace-pre-query-hook.interface';
import { FindManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';

import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
import { CalendarChannelEventAssociationObjectMetadata } from 'src/modules/calendar/standard-objects/calendar-channel-event-association.object-metadata';
import { CalendarChannelEventAssociationRepository } from 'src/modules/calendar/repositories/calendar-channel-event-association.repository';
import { CanAccessCalendarEventService } from 'src/modules/calendar/query-hooks/calendar-event/services/can-access-calendar-event.service';

@Injectable()
export class CalendarEventFindManyPreQueryHook
implements WorkspacePreQueryHook
{
constructor(
@InjectObjectMetadataRepository(
CalendarChannelEventAssociationObjectMetadata,
)
private readonly calendarChannelEventAssociationRepository: CalendarChannelEventAssociationRepository,
private readonly canAccessCalendarEventService: CanAccessCalendarEventService,
) {}

async execute(
userId: string,
workspaceId: string,
payload: FindManyResolverArgs,
): Promise<void> {
if (!payload?.filter?.id?.eq) {
throw new BadRequestException('id filter is required');
}

const calendarChannelCalendarEventAssociations =
await this.calendarChannelEventAssociationRepository.getByCalendarEventIds(
[payload?.filter?.id?.eq],
workspaceId,
);

if (calendarChannelCalendarEventAssociations.length === 0) {
throw new NotFoundException();
}

await this.canAccessCalendarEventService.canAccessCalendarEvent(
userId,
workspaceId,
calendarChannelCalendarEventAssociations,
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import {
BadRequestException,
Injectable,
NotFoundException,
} from '@nestjs/common';

import { WorkspacePreQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/interfaces/workspace-pre-query-hook.interface';
import { FindOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';

import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
import { CanAccessCalendarEventService } from 'src/modules/calendar/query-hooks/calendar-event/services/can-access-calendar-event.service';
import { CalendarChannelEventAssociationRepository } from 'src/modules/calendar/repositories/calendar-channel-event-association.repository';
import { CalendarChannelEventAssociationObjectMetadata } from 'src/modules/calendar/standard-objects/calendar-channel-event-association.object-metadata';

@Injectable()
export class CalendarEventFindOnePreQueryHook implements WorkspacePreQueryHook {
constructor(
@InjectObjectMetadataRepository(
CalendarChannelEventAssociationObjectMetadata,
)
private readonly calendarChannelEventAssociationRepository: CalendarChannelEventAssociationRepository,
private readonly canAccessCalendarEventService: CanAccessCalendarEventService,
) {}

async execute(
userId: string,
workspaceId: string,
payload: FindOneResolverArgs,
): Promise<void> {
if (!payload?.filter?.id?.eq) {
throw new BadRequestException('id filter is required');
}

const calendarChannelCalendarEventAssociations =
await this.calendarChannelEventAssociationRepository.getByCalendarEventIds(
[payload?.filter?.id?.eq],
workspaceId,
);

if (calendarChannelCalendarEventAssociations.length === 0) {
throw new NotFoundException();
}

await this.canAccessCalendarEventService.canAccessCalendarEvent(
userId,
workspaceId,
calendarChannelCalendarEventAssociations,
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { ForbiddenException, Injectable } from '@nestjs/common';

import groupBy from 'lodash.groupby';

import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
import { ObjectRecord } from 'src/engine/workspace-manager/workspace-sync-metadata/types/object-record';
import { CalendarChannelRepository } from 'src/modules/calendar/repositories/calendar-channel.repository';
import { CalendarChannelEventAssociationObjectMetadata } from 'src/modules/calendar/standard-objects/calendar-channel-event-association.object-metadata';
import {
CalendarChannelObjectMetadata,
CalendarChannelVisibility,
} from 'src/modules/calendar/standard-objects/calendar-channel.object-metadata';
import { ConnectedAccountRepository } from 'src/modules/connected-account/repositories/connected-account.repository';
import { ConnectedAccountObjectMetadata } from 'src/modules/connected-account/standard-objects/connected-account.object-metadata';
import { WorkspaceMemberRepository } from 'src/modules/workspace-member/repositories/workspace-member.repository';
import { WorkspaceMemberObjectMetadata } from 'src/modules/workspace-member/standard-objects/workspace-member.object-metadata';

@Injectable()
export class CanAccessCalendarEventService {
constructor(
@InjectObjectMetadataRepository(CalendarChannelObjectMetadata)
private readonly calendarChannelRepository: CalendarChannelRepository,
@InjectObjectMetadataRepository(ConnectedAccountObjectMetadata)
private readonly connectedAccountRepository: ConnectedAccountRepository,
@InjectObjectMetadataRepository(WorkspaceMemberObjectMetadata)
private readonly workspaceMemberService: WorkspaceMemberRepository,
) {}

public async canAccessCalendarEvent(
userId: string,
workspaceId: string,
calendarChannelCalendarEventAssociations: ObjectRecord<CalendarChannelEventAssociationObjectMetadata>[],
) {
const calendarChannels = await this.calendarChannelRepository.getByIds(
calendarChannelCalendarEventAssociations.map(
(association) => association.calendarChannelId,
),
workspaceId,
);

const calendarChannelsGroupByVisibility = groupBy(
calendarChannels,
(channel) => channel.visibility,
);

if (
calendarChannelsGroupByVisibility[
CalendarChannelVisibility.SHARE_EVERYTHING
]
) {
return;
}

const currentWorkspaceMember =
await this.workspaceMemberService.getByIdOrFail(userId, workspaceId);

const calendarChannelsConnectedAccounts =
await this.connectedAccountRepository.getByIds(
calendarChannels.map((channel) => channel.connectedAccountId),
workspaceId,
);

const calendarChannelsWorkspaceMemberIds =
calendarChannelsConnectedAccounts.map(
(connectedAccount) => connectedAccount.accountOwnerId,
);

if (
calendarChannelsWorkspaceMemberIds.includes(currentWorkspaceMember.id)
) {
return;
}

throw new ForbiddenException();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Module } from '@nestjs/common';

import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module';
import { WorkspaceMemberObjectMetadata } from 'src/modules/workspace-member/standard-objects/workspace-member.object-metadata';
import { ConnectedAccountObjectMetadata } from 'src/modules/connected-account/standard-objects/connected-account.object-metadata';
import { CalendarChannelEventAssociationObjectMetadata } from 'src/modules/calendar/standard-objects/calendar-channel-event-association.object-metadata';
import { CalendarChannelObjectMetadata } from 'src/modules/calendar/standard-objects/calendar-channel.object-metadata';
import { CalendarEventFindManyPreQueryHook } from 'src/modules/calendar/query-hooks/calendar-event/calendar-event-find-many.pre-query.hook';
import { CalendarEventFindOnePreQueryHook } from 'src/modules/calendar/query-hooks/calendar-event/calendar-event-find-one.pre-query-hook';
import { CanAccessCalendarEventService } from 'src/modules/calendar/query-hooks/calendar-event/services/can-access-calendar-event.service';

@Module({
imports: [
ObjectMetadataRepositoryModule.forFeature([
CalendarChannelEventAssociationObjectMetadata,
CalendarChannelObjectMetadata,
ConnectedAccountObjectMetadata,
WorkspaceMemberObjectMetadata,
]),
],
providers: [
CanAccessCalendarEventService,
{
provide: CalendarEventFindOnePreQueryHook.name,
useClass: CalendarEventFindOnePreQueryHook,
},
{
provide: CalendarEventFindManyPreQueryHook.name,
useClass: CalendarEventFindManyPreQueryHook,
},
],
})
export class CalendarQueryHookModule {}

0 comments on commit d687523

Please sign in to comment.