feat: wip server folder structure (#4573)

* feat: wip server folder structure

* fix: merge

* fix: wrong merge

* fix: remove unused file

* fix: comment

* fix: lint

* fix: merge

* fix: remove console.log

* fix: metadata graphql arguments broken
This commit is contained in:
Jérémy M
2024-03-20 16:23:46 +01:00
committed by GitHub
parent da12710fe9
commit e5c1309e8c
461 changed files with 1396 additions and 1322 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/core-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/core-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,13 @@
import { Module } from '@nestjs/common';
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
import { UserModule } from 'src/engine/core-modules/user/user.module';
import { TimelineCalendarEventResolver } from 'src/engine/core-modules/calendar/timeline-calendar-event.resolver';
import { TimelineCalendarEventService } from 'src/engine/core-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/core-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/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 { UserService } from 'src/engine/core-modules/user/services/user.service';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { NotFoundError } from 'src/engine/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/core-modules/calendar/constants/calendar.constants';
import { TimelineCalendarEventAttendee } from 'src/engine/core-modules/calendar/dtos/timeline-calendar-event-attendee.dto';
import {
TimelineCalendarEvent,
TimelineCalendarEventVisibility,
} from 'src/engine/core-modules/calendar/dtos/timeline-calendar-event.dto';
import { TimelineCalendarEventsWithTotal } from 'src/engine/core-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;
}
}