4485 create a custom resolver for calendar events (#4568)

* create timeline calendar event resolver

* working on getCalendarEventsFromPersonIds

* add count query

* add calendarEventVisibility and add typing

* update calendarEvent dto

* modify calendarEvent dto

* compute calendar event visibility

* fix types

* add FieldMetadata in timeline calendar dtos and create queries and fragments

* remove fieldMatadata

* fix naming

* update resolver

* add getCalendarEventsFromCompanyId

* fix queries

* refactor queries

* fix visibility

* fix calendar event attendees bug

* visibility is working

* remove @IDField

* update gql queries

* update dto

* add error

* add enum

* throw http exception

* modify error

* Refactor calendar event visibility check

* use enum
This commit is contained in:
bosiraphael
2024-03-19 18:34:00 +01:00
committed by GitHub
parent e579554d47
commit 4ab426c52a
15 changed files with 785 additions and 5 deletions

View File

@ -0,0 +1,2 @@
export const TIMELINE_CALENDAR_EVENTS_DEFAULT_PAGE_SIZE = 20;
export const TIMELINE_CALENDAR_EVENTS_MAX_PAGE_SIZE = 50;

View File

@ -0,0 +1,25 @@
import { ObjectType, Field, ID } from '@nestjs/graphql';
@ObjectType('TimelineCalendarEventAttendee')
export class TimelineCalendarEventAttendee {
@Field(() => ID, { nullable: true })
personId: string;
@Field(() => ID, { nullable: true })
workspaceMemberId: string;
@Field()
firstName: string;
@Field()
lastName: string;
@Field()
displayName: string;
@Field()
avatarUrl: string;
@Field()
handle: string;
}

View File

@ -0,0 +1,52 @@
import { ObjectType, ID, Field, registerEnumType } from '@nestjs/graphql';
import { TimelineCalendarEventAttendee } from 'src/engine/modules/calendar/dtos/timeline-calendar-event-attendee.dto';
export enum TimelineCalendarEventVisibility {
METADATA = 'METADATA',
SHARE_EVERYTHING = 'SHARE_EVERYTHING',
}
registerEnumType(TimelineCalendarEventVisibility, {
name: 'TimelineCalendarEventVisibility',
description: 'Visibility of the calendar event',
});
@ObjectType('TimelineCalendarEvent')
export class TimelineCalendarEvent {
@Field(() => ID)
id: string;
@Field()
title: string;
@Field()
isCanceled: boolean;
@Field()
isFullDay: boolean;
@Field()
startsAt: Date;
@Field()
endsAt: Date;
@Field()
description: string;
@Field()
location: string;
@Field()
conferenceSolution: string;
@Field()
conferenceUri: string;
@Field(() => [TimelineCalendarEventAttendee])
attendees: TimelineCalendarEventAttendee[];
@Field(() => TimelineCalendarEventVisibility)
visibility: TimelineCalendarEventVisibility;
}

View File

@ -0,0 +1,12 @@
import { Field, Int, ObjectType } from '@nestjs/graphql';
import { TimelineCalendarEvent } from 'src/engine/modules/calendar/dtos/timeline-calendar-event.dto';
@ObjectType('TimelineCalendarEventsWithTotal')
export class TimelineCalendarEventsWithTotal {
@Field(() => Int)
totalNumberOfCalendarEvents: number;
@Field(() => [TimelineCalendarEvent])
timelineCalendarEvents: TimelineCalendarEvent[];
}

View File

@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
import { UserModule } from 'src/engine/modules/user/user.module';
import { TimelineCalendarEventResolver } from 'src/engine/modules/calendar/timeline-calendar-event.resolver';
import { TimelineCalendarEventService } from 'src/engine/modules/calendar/timeline-calendar-event.service';
@Module({
imports: [WorkspaceDataSourceModule, UserModule],
exports: [],
providers: [TimelineCalendarEventResolver, TimelineCalendarEventService],
})
export class TimelineCalendarEventModule {}

View File

@ -0,0 +1,108 @@
import { UseGuards } from '@nestjs/common';
import {
Query,
Args,
ArgsType,
Field,
ID,
Int,
Resolver,
} from '@nestjs/graphql';
import { Max } from 'class-validator';
import { User } from 'src/engine/modules/user/user.entity';
import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { JwtAuthGuard } from 'src/engine/guards/jwt.auth.guard';
import { TIMELINE_CALENDAR_EVENTS_MAX_PAGE_SIZE } from 'src/engine/modules/calendar/constants/calendar.constants';
import { TimelineCalendarEventsWithTotal } from 'src/engine/modules/calendar/dtos/timeline-calendar-events-with-total.dto';
import { TimelineCalendarEventService } from 'src/engine/modules/calendar/timeline-calendar-event.service';
import { UserService } from 'src/engine/modules/user/services/user.service';
import { Workspace } from 'src/engine/modules/workspace/workspace.entity';
import { NotFoundError } from 'src/engine/filters/utils/graphql-errors.util';
@ArgsType()
class GetTimelineCalendarEventsFromPersonIdArgs {
@Field(() => ID)
personId: string;
@Field(() => Int)
page: number;
@Field(() => Int)
@Max(TIMELINE_CALENDAR_EVENTS_MAX_PAGE_SIZE)
pageSize: number;
}
@ArgsType()
class GetTimelineCalendarEventsFromCompanyIdArgs {
@Field(() => ID)
companyId: string;
@Field(() => Int)
page: number;
@Field(() => Int)
@Max(TIMELINE_CALENDAR_EVENTS_MAX_PAGE_SIZE)
pageSize: number;
}
@UseGuards(JwtAuthGuard)
@Resolver(() => TimelineCalendarEventsWithTotal)
export class TimelineCalendarEventResolver {
constructor(
private readonly timelineCalendarEventService: TimelineCalendarEventService,
private readonly userService: UserService,
) {}
@Query(() => TimelineCalendarEventsWithTotal)
async getTimelineCalendarEventsFromPersonId(
@AuthWorkspace() { id: workspaceId }: Workspace,
@AuthUser() user: User,
@Args()
{ personId, page, pageSize }: GetTimelineCalendarEventsFromPersonIdArgs,
) {
const workspaceMember = await this.userService.loadWorkspaceMember(user);
if (!workspaceMember) {
throw new NotFoundError('Workspace member not found');
}
const timelineCalendarEvents =
await this.timelineCalendarEventService.getCalendarEventsFromPersonIds(
workspaceMember.id,
workspaceId,
[personId],
page,
pageSize,
);
return timelineCalendarEvents;
}
@Query(() => TimelineCalendarEventsWithTotal)
async getTimelineCalendarEventsFromCompanyId(
@AuthWorkspace() { id: workspaceId }: Workspace,
@AuthUser() user: User,
@Args()
{ companyId, page, pageSize }: GetTimelineCalendarEventsFromCompanyIdArgs,
) {
const workspaceMember = await this.userService.loadWorkspaceMember(user);
if (!workspaceMember) {
throw new NotFoundError('Workspace member not found');
}
const timelineCalendarEvents =
await this.timelineCalendarEventService.getCalendarEventsFromCompanyId(
workspaceMember.id,
workspaceId,
companyId,
page,
pageSize,
);
return timelineCalendarEvents;
}
}

View File

@ -0,0 +1,271 @@
import { Injectable } from '@nestjs/common';
import groupBy from 'lodash.groupBy';
import { TIMELINE_CALENDAR_EVENTS_DEFAULT_PAGE_SIZE } from 'src/engine/modules/calendar/constants/calendar.constants';
import { TimelineCalendarEventAttendee } from 'src/engine/modules/calendar/dtos/timeline-calendar-event-attendee.dto';
import {
TimelineCalendarEvent,
TimelineCalendarEventVisibility,
} from 'src/engine/modules/calendar/dtos/timeline-calendar-event.dto';
import { TimelineCalendarEventsWithTotal } from 'src/engine/modules/calendar/dtos/timeline-calendar-events-with-total.dto';
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
import { ObjectRecord } from 'src/engine/workspace-manager/workspace-sync-metadata/types/object-record';
import { CalendarEventAttendeeObjectMetadata } from 'src/modules/calendar/standard-objects/calendar-event-attendee.object-metadata';
type TimelineCalendarEventAttendeeWithPersonInformation =
ObjectRecord<CalendarEventAttendeeObjectMetadata> & {
personFirstName: string;
personLastName: string;
personAvatarUrl: string;
workspaceMemberFirstName: string;
workspaceMemberLastName: string;
workspaceMemberAvatarUrl: string;
};
@Injectable()
export class TimelineCalendarEventService {
constructor(
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
) {}
async getCalendarEventsFromPersonIds(
workspaceMemberId: string,
workspaceId: string,
personIds: string[],
page: number = 1,
pageSize: number = TIMELINE_CALENDAR_EVENTS_DEFAULT_PAGE_SIZE,
): Promise<TimelineCalendarEventsWithTotal> {
const offset = (page - 1) * pageSize;
const dataSourceSchema =
this.workspaceDataSourceService.getSchemaName(workspaceId);
const calendarEvents: Omit<TimelineCalendarEvent, 'attendees'>[] =
await this.workspaceDataSourceService.executeRawQuery(
`SELECT
"calendarEvent".*
FROM
${dataSourceSchema}."calendarEvent" "calendarEvent"
LEFT JOIN
${dataSourceSchema}."calendarEventAttendee" "calendarEventAttendee" ON "calendarEvent".id = "calendarEventAttendee"."calendarEventId"
LEFT JOIN
${dataSourceSchema}."person" "person" ON "calendarEventAttendee"."personId" = "person".id
WHERE
"calendarEventAttendee"."personId" = ANY($1)
GROUP BY
"calendarEvent".id
ORDER BY
"calendarEvent"."startsAt" DESC
LIMIT $2
OFFSET $3`,
[personIds, pageSize, offset],
workspaceId,
);
if (!calendarEvents) {
return {
totalNumberOfCalendarEvents: 0,
timelineCalendarEvents: [],
};
}
const calendarEventAttendees: TimelineCalendarEventAttendeeWithPersonInformation[] =
await this.workspaceDataSourceService.executeRawQuery(
`SELECT
"calendarEventAttendee".*,
"person"."nameFirstName" as "personFirstName",
"person"."nameLastName" as "personLastName",
"person"."avatarUrl" as "personAvatarUrl",
"workspaceMember"."nameFirstName" as "workspaceMemberFirstName",
"workspaceMember"."nameLastName" as "workspaceMemberLastName",
"workspaceMember"."avatarUrl" as "workspaceMemberAvatarUrl"
FROM
${dataSourceSchema}."calendarEventAttendee" "calendarEventAttendee"
LEFT JOIN
${dataSourceSchema}."person" "person" ON "calendarEventAttendee"."personId" = "person".id
LEFT JOIN
${dataSourceSchema}."workspaceMember" "workspaceMember" ON "calendarEventAttendee"."workspaceMemberId" = "workspaceMember".id
WHERE
"calendarEventAttendee"."calendarEventId" = ANY($1)`,
[calendarEvents.map((event) => event.id)],
workspaceId,
);
const formattedCalendarEventAttendees: TimelineCalendarEventAttendee[] =
calendarEventAttendees.map((attendee) => {
const firstName =
attendee.personFirstName || attendee.workspaceMemberFirstName || '';
const lastName =
attendee.personLastName || attendee.workspaceMemberLastName || '';
const displayName =
firstName || attendee.displayName || attendee.handle;
const avatarUrl =
attendee.personAvatarUrl || attendee.workspaceMemberAvatarUrl || '';
return {
calendarEventId: attendee.calendarEventId,
personId: attendee.personId,
workspaceMemberId: attendee.workspaceMemberId,
firstName,
lastName,
displayName,
avatarUrl,
handle: attendee.handle,
};
});
const calendarEventAttendeesByEventId: {
[calendarEventId: string]: TimelineCalendarEventAttendee[];
} = groupBy(formattedCalendarEventAttendees, 'calendarEventId');
const totalNumberOfCalendarEvents: { count: number }[] =
await this.workspaceDataSourceService.executeRawQuery(
`
SELECT
COUNT(DISTINCT "calendarEventId")
FROM
${dataSourceSchema}."calendarEventAttendee" "calendarEventAttendee"
WHERE
"calendarEventAttendee"."personId" = ANY($1)
`,
[personIds],
workspaceId,
);
const timelineCalendarEvents = calendarEvents.map((event) => {
const attendees = calendarEventAttendeesByEventId[event.id] || [];
return {
...event,
attendees,
};
});
const calendarEventIdsWithWorkspaceMemberInAttendees =
await this.workspaceDataSourceService.executeRawQuery(
`
SELECT
"calendarEventId"
FROM
${dataSourceSchema}."calendarEventAttendee" "calendarEventAttendee"
WHERE
"calendarEventAttendee"."workspaceMemberId" = $1
`,
[workspaceMemberId],
workspaceId,
);
const calendarEventIdsWithWorkspaceMemberInAttendeesFormatted =
calendarEventIdsWithWorkspaceMemberInAttendees.map(
(event: { calendarEventId: string }) => event.calendarEventId,
);
const calendarEventIdsToFetchVisibilityFor = timelineCalendarEvents
.filter(
(event) =>
!calendarEventIdsWithWorkspaceMemberInAttendeesFormatted.includes(
event.id,
),
)
.map((event) => event.id);
const calendarEventIdsForWhichVisibilityIsMetadata:
| {
id: string;
}[]
| undefined = await this.workspaceDataSourceService.executeRawQuery(
`
SELECT
"calendarChannelEventAssociation"."calendarEventId" AS "id"
FROM
${dataSourceSchema}."calendarChannel" "calendarChannel"
LEFT JOIN
${dataSourceSchema}."calendarChannelEventAssociation" "calendarChannelEventAssociation" ON "calendarChannel".id = "calendarChannelEventAssociation"."calendarChannelId"
WHERE
"calendarChannelEventAssociation"."calendarEventId" = ANY($1)
AND
"calendarChannel"."visibility" = 'METADATA'
`,
[calendarEventIdsToFetchVisibilityFor],
workspaceId,
);
if (!calendarEventIdsForWhichVisibilityIsMetadata) {
throw new Error('Failed to fetch calendar event visibility');
}
const calendarEventIdsForWhichVisibilityIsMetadataMap = new Map(
calendarEventIdsForWhichVisibilityIsMetadata.map((event) => [
event.id,
TimelineCalendarEventVisibility.METADATA,
]),
);
timelineCalendarEvents.forEach((event) => {
event.visibility =
calendarEventIdsForWhichVisibilityIsMetadataMap.get(event.id) ??
TimelineCalendarEventVisibility.SHARE_EVERYTHING;
if (event.visibility === TimelineCalendarEventVisibility.METADATA) {
event.title = '';
event.description = '';
event.location = '';
event.conferenceSolution = '';
event.conferenceUri = '';
}
});
return {
totalNumberOfCalendarEvents: totalNumberOfCalendarEvents[0].count,
timelineCalendarEvents,
};
}
async getCalendarEventsFromCompanyId(
workspaceMemberId: string,
workspaceId: string,
companyId: string,
page: number = 1,
pageSize: number = TIMELINE_CALENDAR_EVENTS_DEFAULT_PAGE_SIZE,
): Promise<TimelineCalendarEventsWithTotal> {
const dataSourceSchema =
this.workspaceDataSourceService.getSchemaName(workspaceId);
const personIds = await this.workspaceDataSourceService.executeRawQuery(
`
SELECT
p."id"
FROM
${dataSourceSchema}."person" p
WHERE
p."companyId" = $1
`,
[companyId],
workspaceId,
);
if (!personIds) {
return {
totalNumberOfCalendarEvents: 0,
timelineCalendarEvents: [],
};
}
const formattedPersonIds = personIds.map(
(personId: { id: string }) => personId.id,
);
const messageThreads = await this.getCalendarEventsFromPersonIds(
workspaceMemberId,
workspaceId,
formattedPersonIds,
page,
pageSize,
);
return messageThreads;
}
}

View File

@ -9,6 +9,7 @@ import { OpenApiModule } from 'src/engine/modules/open-api/open-api.module';
import { TimelineMessagingModule } from 'src/engine/modules/messaging/timeline-messaging.module';
import { BillingModule } from 'src/engine/modules/billing/billing.module';
import { HealthModule } from 'src/engine/modules/health/health.module';
import { TimelineCalendarEventModule } from 'src/engine/modules/calendar/timeline-calendar-event.module';
import { AnalyticsModule } from './analytics/analytics.module';
import { FileModule } from './file/file.module';
@ -26,6 +27,7 @@ import { ClientConfigModule } from './client-config/client-config.module';
OpenApiModule,
RefreshTokenModule,
TimelineMessagingModule,
TimelineCalendarEventModule,
UserModule,
WorkspaceModule,
],
@ -34,6 +36,7 @@ import { ClientConfigModule } from './client-config/client-config.module';
AuthModule,
FeatureFlagModule,
TimelineMessagingModule,
TimelineCalendarEventModule,
UserModule,
WorkspaceModule,
],

View File

@ -1,12 +1,10 @@
import { ObjectType, Field, ID } from '@nestjs/graphql';
import { IDField } from '@ptc-org/nestjs-query-graphql';
import { TimelineThreadParticipant } from 'src/engine/modules/messaging/dtos/timeline-thread-participant.dto';
@ObjectType('TimelineThread')
export class TimelineThread {
@IDField(() => ID)
@Field(() => ID)
id: string;
@Field()