4643 create a pre hook for calendar events (#4666)

* 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 <corentin@twenty.com>
This commit is contained in:
bosiraphael 2024-03-27 19:44:35 +01:00 committed by GitHub
parent c3cc0f651c
commit d687523e22
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 257 additions and 38 deletions

View File

@ -860,6 +860,7 @@ export type Object = {
imageIdentifierFieldMetadataId?: Maybe<Scalars['String']>; imageIdentifierFieldMetadataId?: Maybe<Scalars['String']>;
isActive: Scalars['Boolean']; isActive: Scalars['Boolean'];
isCustom: Scalars['Boolean']; isCustom: Scalars['Boolean'];
isRemote: Scalars['Boolean'];
isSystem: Scalars['Boolean']; isSystem: Scalars['Boolean'];
labelIdentifierFieldMetadataId?: Maybe<Scalars['String']>; labelIdentifierFieldMetadataId?: Maybe<Scalars['String']>;
labelPlural: Scalars['String']; labelPlural: Scalars['String'];
@ -905,9 +906,9 @@ export type RelationEdge = {
node: Relation; 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 }> }> }; 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 }> }> };
@ -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 type GetWorkspaceFromInviteHashQuery = { __typename?: 'Query', findWorkspaceFromInviteHash: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, allowImpersonation: boolean } };
export const AttendeeFragmentFragmentDoc = gql` export const TimelineCalendarEventAttendeeFragmentFragmentDoc = gql`
fragment AttendeeFragment on TimelineCalendarEventAttendee { fragment TimelineCalendarEventAttendeeFragment on TimelineCalendarEventAttendee {
personId personId
workspaceMemberId workspaceMemberId
firstName firstName
@ -1165,8 +1166,8 @@ export const AttendeeFragmentFragmentDoc = gql`
handle handle
} }
`; `;
export const CalendarEventFragmentFragmentDoc = gql` export const TimelineCalendarEventFragmentFragmentDoc = gql`
fragment CalendarEventFragment on TimelineCalendarEvent { fragment TimelineCalendarEventFragment on TimelineCalendarEvent {
id id
title title
description description
@ -1176,18 +1177,18 @@ export const CalendarEventFragmentFragmentDoc = gql`
isFullDay isFullDay
visibility visibility
attendees { attendees {
...AttendeeFragment ...TimelineCalendarEventAttendeeFragment
} }
} }
${AttendeeFragmentFragmentDoc}`; ${TimelineCalendarEventAttendeeFragmentFragmentDoc}`;
export const TimelineCalendarEventsWithTotalFragmentFragmentDoc = gql` export const TimelineCalendarEventsWithTotalFragmentFragmentDoc = gql`
fragment TimelineCalendarEventsWithTotalFragment on TimelineCalendarEventsWithTotal { fragment TimelineCalendarEventsWithTotalFragment on TimelineCalendarEventsWithTotal {
totalNumberOfCalendarEvents totalNumberOfCalendarEvents
timelineCalendarEvents { timelineCalendarEvents {
...CalendarEventFragment ...TimelineCalendarEventFragment
} }
} }
${CalendarEventFragmentFragmentDoc}`; ${TimelineCalendarEventFragmentFragmentDoc}`;
export const ParticipantFragmentFragmentDoc = gql` export const ParticipantFragmentFragmentDoc = gql`
fragment ParticipantFragment on TimelineThreadParticipant { fragment ParticipantFragment on TimelineThreadParticipant {
personId personId

View File

@ -1,20 +0,0 @@
import { gql } from '@apollo/client';
import { attendeeFragment } from '@/activities/calendar/queries/fragments/attendeeFragment';
export const calendarEventFragment = gql`
fragment CalendarEventFragment on TimelineCalendarEvent {
id
title
description
location
startsAt
endsAt
isFullDay
visibility
attendees {
...AttendeeFragment
}
}
${attendeeFragment}
`;

View File

@ -1,7 +1,7 @@
import { gql } from '@apollo/client'; import { gql } from '@apollo/client';
export const attendeeFragment = gql` export const timelineCalendarEventAttendeeFragment = gql`
fragment AttendeeFragment on TimelineCalendarEventAttendee { fragment TimelineCalendarEventAttendeeFragment on TimelineCalendarEventAttendee {
personId personId
workspaceMemberId workspaceMemberId
firstName firstName

View File

@ -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}
`;

View File

@ -1,13 +1,13 @@
import { gql } from '@apollo/client'; 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` export const timelineCalendarEventWithTotalFragment = gql`
fragment TimelineCalendarEventsWithTotalFragment on TimelineCalendarEventsWithTotal { fragment TimelineCalendarEventsWithTotalFragment on TimelineCalendarEventsWithTotal {
totalNumberOfCalendarEvents totalNumberOfCalendarEvents
timelineCalendarEvents { timelineCalendarEvents {
...CalendarEventFragment ...TimelineCalendarEventFragment
} }
} }
${calendarEventFragment} ${timelineCalendarEventFragment}
`; `;

View File

@ -1,6 +1,6 @@
import { gql } from '@apollo/client'; 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` export const getTimelineCalendarEventsFromCompanyId = gql`
query GetTimelineCalendarEventsFromCompanyId( query GetTimelineCalendarEventsFromCompanyId(

View File

@ -1,6 +1,6 @@
import { gql } from '@apollo/client'; 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` export const getTimelineCalendarEventsFromPersonId = gql`
query GetTimelineCalendarEventsFromPersonId( query GetTimelineCalendarEventsFromPersonId(

View File

@ -1,6 +1,8 @@
import { MessageFindManyPreQueryHook } from 'src/modules/messaging/query-hooks/message/message-find-many.pre-query.hook'; 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 { 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 { 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 // TODO: move to a decorator
export const workspacePreQueryHooks: WorkspaceQueryHook = { export const workspacePreQueryHooks: WorkspaceQueryHook = {
@ -8,4 +10,8 @@ export const workspacePreQueryHooks: WorkspaceQueryHook = {
findOne: [MessageFindOnePreQueryHook.name], findOne: [MessageFindOnePreQueryHook.name],
findMany: [MessageFindManyPreQueryHook.name], findMany: [MessageFindManyPreQueryHook.name],
}, },
calendarEvent: {
findOne: [CalendarEventFindOnePreQueryHook.name],
findMany: [CalendarEventFindManyPreQueryHook.name],
},
}; };

View File

@ -2,9 +2,10 @@ import { Module } from '@nestjs/common';
import { MessagingQueryHookModule } from 'src/modules/messaging/query-hooks/messaging-query-hook.module'; 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 { 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({ @Module({
imports: [MessagingQueryHookModule], imports: [MessagingQueryHookModule, CalendarQueryHookModule],
providers: [WorkspacePreQueryHookService], providers: [WorkspacePreQueryHookService],
exports: [WorkspacePreQueryHookService], exports: [WorkspacePreQueryHookService],
}) })

View File

@ -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,
);
}
}

View File

@ -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,
);
}
}

View File

@ -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();
}
}

View File

@ -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 {}