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:
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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 } });
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user