From 4b38d7312059e5b2a388f1cf77966a3088470a8b Mon Sep 17 00:00:00 2001 From: Benny Joo Date: Wed, 6 Nov 2024 14:48:45 -0500 Subject: [PATCH] chore: Replace prisma actions Credential Repository / SelectedCalendar Repository methods (#17443) * create lib/google.ts * add updateAvatar method to UserRepository * refactor googlecalendar callback * add methods to CredentialRepository and SelectedCalendarRepository * refactor * remove not needed code * refactor * make credential repository and selected calendar repository more generic * create GoogleRepository --- .../app-store/googlecalendar/api/callback.ts | 72 +++++-------------- .../googlecalendar/lib/CalendarService.ts | 2 +- packages/lib/google.ts | 50 +++++++++++++ packages/lib/server/repository/credential.ts | 19 ++++- packages/lib/server/repository/google.ts | 38 ++++++++++ .../lib/server/repository/selectedCalendar.ts | 18 +++++ packages/lib/server/repository/user.ts | 16 +++++ 7 files changed, 156 insertions(+), 59 deletions(-) create mode 100644 packages/lib/google.ts create mode 100644 packages/lib/server/repository/google.ts create mode 100644 packages/lib/server/repository/selectedCalendar.ts diff --git a/packages/app-store/googlecalendar/api/callback.ts b/packages/app-store/googlecalendar/api/callback.ts index dfd1b24edc..3e78b11a15 100644 --- a/packages/app-store/googlecalendar/api/callback.ts +++ b/packages/app-store/googlecalendar/api/callback.ts @@ -1,21 +1,21 @@ -import type { Auth, calendar_v3 } from "googleapis"; import { google } from "googleapis"; import type { NextApiRequest, NextApiResponse } from "next"; import { renewSelectedCalendarCredentialId } from "@calcom/lib/connectedCalendar"; import { WEBAPP_URL, WEBAPP_URL_FOR_OAUTH } from "@calcom/lib/constants"; import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl"; +import { getAllCalendars, updateProfilePhoto } from "@calcom/lib/google"; import { HttpError } from "@calcom/lib/http-error"; -import logger from "@calcom/lib/logger"; import { defaultHandler, defaultResponder } from "@calcom/lib/server"; -import prisma from "@calcom/prisma"; +import { CredentialRepository } from "@calcom/lib/server/repository/credential"; +import { GoogleRepository } from "@calcom/lib/server/repository/google"; +import { SelectedCalendarRepository } from "@calcom/lib/server/repository/selectedCalendar"; import { Prisma } from "@calcom/prisma/client"; import getInstalledAppPath from "../../_utils/getInstalledAppPath"; import { decodeOAuthState } from "../../_utils/oauth/decodeOAuthState"; import { REQUIRED_SCOPES, SCOPE_USERINFO_PROFILE } from "../lib/constants"; import { getGoogleAppKeys } from "../lib/getGoogleAppKeys"; -import { getAllCalendars } from "../lib/utils"; async function getHandler(req: NextApiRequest, res: NextApiResponse) { const { code } = req.query; @@ -82,13 +82,9 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) { await updateProfilePhoto(oAuth2Client, req.session.user.id); } - const credential = await prisma.credential.create({ - data: { - type: "google_calendar", - key, - userId: req.session.user.id, - appId: "google-calendar", - }, + const gcalCredential = await GoogleRepository.createGoogleCalendarCredential({ + key, + userId: req.session.user.id, }); // If we still don't have a primary calendar skip creating the selected calendar. @@ -110,18 +106,17 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) { // Wrapping in a try/catch to reduce chance of race conditions- // also this improves performance for most of the happy-paths. try { - await prisma.selectedCalendar.create({ - data: { - credentialId: credential.id, - ...selectedCalendarWhereUnique, - }, + await GoogleRepository.createSelectedCalendar({ + credentialId: gcalCredential.id, + externalId: selectedCalendarWhereUnique.externalId, + userId: selectedCalendarWhereUnique.userId, }); } catch (error) { let errorMessage = "something_went_wrong"; if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2002") { // it is possible a selectedCalendar was orphaned, in this situation- // we want to recover by connecting the existing selectedCalendar to the new Credential. - if (await renewSelectedCalendarCredentialId(selectedCalendarWhereUnique, credential.id)) { + if (await renewSelectedCalendarCredentialId(selectedCalendarWhereUnique, gcalCredential.id)) { res.redirect( getSafeRedirectUrl(state?.returnTo) ?? getInstalledAppPath({ variant: "calendar", slug: "google-calendar" }) @@ -131,7 +126,7 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) { // else errorMessage = "account_already_linked"; } - await prisma.credential.delete({ where: { id: credential.id } }); + await CredentialRepository.deleteById({ id: gcalCredential.id }); res.redirect( `${ getSafeRedirectUrl(state?.onErrorReturnTo) ?? @@ -151,11 +146,8 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) { return; } - const existingGoogleMeetCredential = await prisma.credential.findFirst({ - where: { - userId: req.session.user.id, - type: "google_video", - }, + const existingGoogleMeetCredential = await GoogleRepository.findGoogleMeetCredential({ + userId: req.session.user.id, }); // If the user already has a google meet credential, there's nothing to do in here @@ -168,45 +160,13 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) { } // Create a new google meet credential - await prisma.credential.create({ - data: { - type: "google_video", - key: {}, - userId: req.session.user.id, - appId: "google-meet", - }, - }); - + await GoogleRepository.createGoogleMeetsCredential({ userId: req.session.user.id }); res.redirect( getSafeRedirectUrl(`${WEBAPP_URL}/apps/installed/conferencing?hl=google-meet`) ?? getInstalledAppPath({ variant: "conferencing", slug: "google-meet" }) ); } -async function updateProfilePhoto(oAuth2Client: Auth.OAuth2Client, userId: number) { - try { - const oauth2 = google.oauth2({ version: "v2", auth: oAuth2Client }); - const userDetails = await oauth2.userinfo.get(); - if (userDetails.data?.picture) { - // Using updateMany here since if the user already has a profile it would throw an error - // because no records were found to update the profile picture - await prisma.user.updateMany({ - where: { - id: userId, - avatarUrl: { - equals: null, - }, - }, - data: { - avatarUrl: userDetails.data.picture, - }, - }); - } - } catch (error) { - logger.error("Error updating avatarUrl from google calendar connect", error); - } -} - export default defaultHandler({ GET: Promise.resolve({ default: defaultResponder(getHandler) }), }); diff --git a/packages/app-store/googlecalendar/lib/CalendarService.ts b/packages/app-store/googlecalendar/lib/CalendarService.ts index 42ac3771cc..3cab66b0a9 100644 --- a/packages/app-store/googlecalendar/lib/CalendarService.ts +++ b/packages/app-store/googlecalendar/lib/CalendarService.ts @@ -16,6 +16,7 @@ import { CREDENTIAL_SYNC_SECRET_HEADER_NAME, } from "@calcom/lib/constants"; import { formatCalEvent } from "@calcom/lib/formatCalendarEvent"; +import { getAllCalendars } from "@calcom/lib/google"; import logger from "@calcom/lib/logger"; import { safeStringify } from "@calcom/lib/safeStringify"; import prisma from "@calcom/prisma"; @@ -36,7 +37,6 @@ import { markTokenAsExpired } from "../../_utils/oauth/markTokenAsExpired"; import { OAuth2UniversalSchema } from "../../_utils/oauth/universalSchema"; import { metadata } from "../_metadata"; import { getGoogleAppKeys } from "./getGoogleAppKeys"; -import { getAllCalendars } from "./utils"; const log = logger.getSubLogger({ prefix: ["app-store/googlecalendar/lib/CalendarService"] }); interface GoogleCalError extends Error { diff --git a/packages/lib/google.ts b/packages/lib/google.ts new file mode 100644 index 0000000000..7f1ca94424 --- /dev/null +++ b/packages/lib/google.ts @@ -0,0 +1,50 @@ +import type { OAuth2Client } from "google-auth-library"; +import type { calendar_v3 } from "googleapis"; +import { google } from "googleapis"; + +import logger from "@calcom/lib/logger"; + +import { UserRepository } from "./server/repository/user"; + +export async function getAllCalendars( + calendar: calendar_v3.Calendar, + fields: string[] = ["id", "summary", "primary", "accessRole"] +): Promise { + let allCalendars: calendar_v3.Schema$CalendarListEntry[] = []; + let pageToken: string | undefined; + + try { + do { + const response: any = await calendar.calendarList.list({ + fields: `items(${fields.join(",")}),nextPageToken`, + pageToken, + maxResults: 250, // 250 is max + }); + + allCalendars = [...allCalendars, ...(response.data.items ?? [])]; + pageToken = response.data.nextPageToken; + + logger.error("Google Calendar pagination failed in the middle", { + totalSoFar: allCalendars.length, + hasMorePages: !!pageToken, + }); + } while (pageToken); + + return allCalendars; + } catch (error) { + logger.error("Error fetching all Google Calendars", { error }); + throw error; + } +} + +export async function updateProfilePhoto(oAuth2Client: OAuth2Client, userId: number) { + try { + const oauth2 = google.oauth2({ version: "v2", auth: oAuth2Client }); + const userDetails = await oauth2.userinfo.get(); + if (userDetails.data?.picture) { + await UserRepository.updateAvatar({ id: userId, avatarUrl: userDetails.data.picture }); + } + } catch (error) { + logger.error("Error updating avatarUrl from google calendar connect", error); + } +} diff --git a/packages/lib/server/repository/credential.ts b/packages/lib/server/repository/credential.ts index 8f2d50a287..1475ce8d16 100644 --- a/packages/lib/server/repository/credential.ts +++ b/packages/lib/server/repository/credential.ts @@ -3,9 +3,16 @@ import type { Prisma } from "@prisma/client"; import { prisma } from "@calcom/prisma"; import { safeCredentialSelect } from "@calcom/prisma/selects/credential"; +type CredentialCreateInput = { + type: string; + key: any; + userId: number; + appId: string; +}; + export class CredentialRepository { - static async create(data: Prisma.CredentialCreateInput) { - return await prisma.credential.create({ data }); + static async create(data: CredentialCreateInput) { + return await prisma.credential.create({ data: { ...data } }); } /** @@ -24,4 +31,12 @@ export class CredentialRepository { select: { ...safeCredentialSelect, key: true }, }); } + + static async findFirstByUserIdAndType({ userId, type }: { userId: number; type: string }) { + return await prisma.credential.findFirst({ where: { userId, type } }); + } + + static async deleteById({ id }: { id: number }) { + await prisma.credential.delete({ where: { id } }); + } } diff --git a/packages/lib/server/repository/google.ts b/packages/lib/server/repository/google.ts new file mode 100644 index 0000000000..681e5f595a --- /dev/null +++ b/packages/lib/server/repository/google.ts @@ -0,0 +1,38 @@ +import type { Credentials } from "google-auth-library"; + +import { CredentialRepository } from "./credential"; +import { SelectedCalendarRepository } from "./selectedCalendar"; + +export class GoogleRepository { + static async createGoogleCalendarCredential({ userId, key }: { userId: number; key: Credentials }) { + return await CredentialRepository.create({ + type: "google_calendar", + key, + userId, + appId: "google-calendar", + }); + } + + static async createGoogleMeetsCredential({ userId }: { userId: number }) { + return await CredentialRepository.create({ + type: "google_video", + key: {}, + userId, + appId: "google-meet", + }); + } + + static async createSelectedCalendar(data: { credentialId: number; userId: number; externalId: string }) { + return await SelectedCalendarRepository.create({ + ...data, + integration: "google_calendar", + }); + } + + static async findGoogleMeetCredential({ userId }: { userId: number }) { + return await CredentialRepository.findFirstByUserIdAndType({ + userId, + type: "google_video", + }); + } +} diff --git a/packages/lib/server/repository/selectedCalendar.ts b/packages/lib/server/repository/selectedCalendar.ts new file mode 100644 index 0000000000..253f492fa5 --- /dev/null +++ b/packages/lib/server/repository/selectedCalendar.ts @@ -0,0 +1,18 @@ +import { prisma } from "@calcom/prisma"; + +type SelectedCalendarCreateInput = { + credentialId: number; + userId: number; + externalId: string; + integration: string; +}; + +export class SelectedCalendarRepository { + static async create(data: SelectedCalendarCreateInput) { + return await prisma.selectedCalendar.create({ + data: { + ...data, + }, + }); + } +} diff --git a/packages/lib/server/repository/user.ts b/packages/lib/server/repository/user.ts index d370d7e247..b1f5d9a310 100644 --- a/packages/lib/server/repository/user.ts +++ b/packages/lib/server/repository/user.ts @@ -731,4 +731,20 @@ export class UserRepository { } return user; } + + static async updateAvatar({ id, avatarUrl }: { id: number; avatarUrl: string }) { + // Using updateMany here since if the user already has a profile it would throw an error + // because no records were found to update the profile picture + await prisma.user.updateMany({ + where: { + id, + avatarUrl: { + equals: null, + }, + }, + data: { + avatarUrl, + }, + }); + } }