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
This commit is contained in:
Benny Joo authored and GitHub committed 2024-11-06 14:48:45 -05:00
1 parent 8f92c9b884
commit 4b38d73120
7 files changed
+156 -59

No files matched your search

@@ -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) }),
});
@@ -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 {
+50
View File
@@ -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<calendar_v3.Schema$CalendarListEntry[]> {
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);
}
}
+17 -2
View File
@@ -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 } });
}
}
+38
View File
@@ -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",
});
}
}
@@ -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,
},
});
}
}
+16
View File
@@ -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,
},
});
}
}