From 6fa54aba463069649edae5f471457ce96efe8d56 Mon Sep 17 00:00:00 2001 From: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com> Date: Tue, 1 Aug 2023 07:10:52 -0400 Subject: [PATCH] perf: Refactor `getEnabledApps` to filter on credentials passed (#10353) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Omar López Co-authored-by: Hariom Balhara --- packages/app-store/applecalendar/api/add.ts | 1 + packages/app-store/caldavcalendar/api/add.ts | 1 + .../dailyvideo/lib/VideoApiAdapter.ts | 2 +- .../app-store/exchange2013calendar/api/add.ts | 1 + .../app-store/exchange2016calendar/api/add.ts | 1 + .../lib/CalendarService.ts | 1 - .../exchangecalendar/api/_postAdd.ts | 1 + packages/app-store/utils.ts | 30 ++++++++++------ packages/core/CalendarManager.ts | 2 +- packages/core/EventManager.ts | 2 +- packages/core/videoClient.ts | 3 ++ .../BookEventForm/BookFormAsModal.tsx | 2 +- packages/lib/apps/getEnabledApps.ts | 36 +++++++++++++++++-- packages/lib/getEventTypeById.ts | 3 +- .../loggedInViewer/integrations.handler.ts | 2 +- .../loggedInViewer/locationOptions.handler.ts | 3 +- ...pdateUserDefaultConferencingApp.handler.ts | 2 +- .../viewer/eventTypes/create.handler.ts | 6 ++-- packages/types/App.d.ts | 2 +- packages/types/Credential.d.ts | 2 +- 20 files changed, 76 insertions(+), 27 deletions(-) diff --git a/packages/app-store/applecalendar/api/add.ts b/packages/app-store/applecalendar/api/add.ts index 321ca75a00..3a3ee8f7e9 100644 --- a/packages/app-store/applecalendar/api/add.ts +++ b/packages/app-store/applecalendar/api/add.ts @@ -27,6 +27,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) process.env.CALENDSO_ENCRYPTION_KEY || "" ), userId: user.id, + teamId: null, appId: "apple-calendar", invalid: false, }; diff --git a/packages/app-store/caldavcalendar/api/add.ts b/packages/app-store/caldavcalendar/api/add.ts index b3e434b334..660723ffa8 100644 --- a/packages/app-store/caldavcalendar/api/add.ts +++ b/packages/app-store/caldavcalendar/api/add.ts @@ -27,6 +27,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) process.env.CALENDSO_ENCRYPTION_KEY || "" ), userId: user.id, + teamId: null, appId: "caldav-calendar", invalid: false, }; diff --git a/packages/app-store/dailyvideo/lib/VideoApiAdapter.ts b/packages/app-store/dailyvideo/lib/VideoApiAdapter.ts index 353fd020a3..31d447ed7c 100644 --- a/packages/app-store/dailyvideo/lib/VideoApiAdapter.ts +++ b/packages/app-store/dailyvideo/lib/VideoApiAdapter.ts @@ -62,7 +62,7 @@ export const FAKE_DAILY_CREDENTIAL: CredentialPayload & { invalid: boolean } = { userId: 0, appId: "daily-video", invalid: false, - teamId: undefined, + teamId: null, }; export const fetcher = async (endpoint: string, init?: RequestInit | undefined) => { diff --git a/packages/app-store/exchange2013calendar/api/add.ts b/packages/app-store/exchange2013calendar/api/add.ts index 52c325e14b..644103c135 100644 --- a/packages/app-store/exchange2013calendar/api/add.ts +++ b/packages/app-store/exchange2013calendar/api/add.ts @@ -33,6 +33,7 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) { type: "exchange2013_calendar", key: symmetricEncrypt(JSON.stringify(body), process.env.CALENDSO_ENCRYPTION_KEY || ""), userId: user.id, + teamId: null, appId: "exchange2013-calendar", invalid: false, }; diff --git a/packages/app-store/exchange2016calendar/api/add.ts b/packages/app-store/exchange2016calendar/api/add.ts index 683c9689c2..1e04e1cd55 100644 --- a/packages/app-store/exchange2016calendar/api/add.ts +++ b/packages/app-store/exchange2016calendar/api/add.ts @@ -33,6 +33,7 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) { type: "exchange2016_calendar", key: symmetricEncrypt(JSON.stringify(body), process.env.CALENDSO_ENCRYPTION_KEY || ""), userId: user.id, + teamId: null, appId: "exchange2016-calendar", invalid: false, }; diff --git a/packages/app-store/exchange2016calendar/lib/CalendarService.ts b/packages/app-store/exchange2016calendar/lib/CalendarService.ts index 3538fee521..ff1d20dd62 100644 --- a/packages/app-store/exchange2016calendar/lib/CalendarService.ts +++ b/packages/app-store/exchange2016calendar/lib/CalendarService.ts @@ -193,7 +193,6 @@ export default class ExchangeCalendarService implements Calendar { } async listCalendars(): Promise { - console.log("This triggers"); try { const allFolders: IntegrationCalendar[] = []; return this.getExchangeService() diff --git a/packages/app-store/exchangecalendar/api/_postAdd.ts b/packages/app-store/exchangecalendar/api/_postAdd.ts index 017a0837de..a408a29809 100644 --- a/packages/app-store/exchangecalendar/api/_postAdd.ts +++ b/packages/app-store/exchangecalendar/api/_postAdd.ts @@ -30,6 +30,7 @@ export async function getHandler(req: NextApiRequest, res: NextApiResponse) { type: "exchange_calendar", key: encrypted, userId: session.user?.id, + teamId: null, appId: "exchange", invalid: false, }; diff --git a/packages/app-store/utils.ts b/packages/app-store/utils.ts index 20b7b74822..6e57f7bf1a 100644 --- a/packages/app-store/utils.ts +++ b/packages/app-store/utils.ts @@ -1,3 +1,4 @@ +import type { Credential } from "@prisma/client"; import { Prisma } from "@prisma/client"; import type { TFunction } from "next-i18next"; @@ -33,7 +34,7 @@ const ALL_APPS_MAP = Object.keys(appStoreMetadata).reduce((store, key) => { }, {} as Record); const credentialData = Prisma.validator()({ - select: { id: true, type: true, key: true, userId: true, appId: true, invalid: true }, + select: { id: true, type: true, key: true, userId: true, teamId: true, appId: true, invalid: true }, }); export type CredentialData = Prisma.CredentialGetPayload; @@ -108,26 +109,30 @@ export function getLocationGroupedOptions(integrations: ReturnType { - const credentials = userCredentials.filter((credential) => credential.type === appMeta.type); +function getApps(credentials: CredentialData[], filterOnCredentials?: boolean) { + const apps = ALL_APPS.reduce((reducedArray, appMeta) => { + const appCredentials = credentials.filter((credential) => credential.type === appMeta.type); + + if (filterOnCredentials && !appCredentials.length && !appMeta.isGlobal) return reducedArray; + let locationOption: LocationOption | null = null; /** If the app is a globally installed one, let's inject it's key */ if (appMeta.isGlobal) { - credentials.push({ + appCredentials.push({ id: 0, type: appMeta.type, // eslint-disable-next-line @typescript-eslint/no-non-null-assertion key: appMeta.key!, userId: 0, + teamId: 0, appId: appMeta.slug, invalid: false, }); } /** Check if app has location option AND add it if user has credentials for it */ - if (credentials.length > 0 && appMeta?.appData?.location) { + if (appCredentials.length > 0 && appMeta?.appData?.location) { locationOption = { value: appMeta.appData.location.type, label: appMeta.appData.location.label || "No label set", @@ -135,18 +140,21 @@ function getApps(userCredentials: CredentialData[]) { }; } - const credential: (typeof credentials)[number] | null = credentials[0] || null; - return { + const credential: (typeof appCredentials)[number] | null = appCredentials[0] || null; + + reducedArray.push({ ...appMeta, /** * @deprecated use `credentials` */ credential, - credentials, + credentials: appCredentials, /** Option to display in `location` field while editing event types */ locationOption, - }; - }); + }); + + return reducedArray; + }, [] as (App & { credential: Credential; credentials: Credential[]; locationOption: LocationOption | null })[]); return apps; } diff --git a/packages/core/CalendarManager.ts b/packages/core/CalendarManager.ts index 1ae89be41a..aad2776834 100644 --- a/packages/core/CalendarManager.ts +++ b/packages/core/CalendarManager.ts @@ -21,7 +21,7 @@ import getCalendarsEvents from "./getCalendarsEvents"; const log = logger.getChildLogger({ prefix: ["CalendarManager"] }); export const getCalendarCredentials = (credentials: Array) => { - const calendarCredentials = getApps(credentials) + const calendarCredentials = getApps(credentials, true) .filter((app) => app.type.endsWith("_calendar")) .flatMap((app) => { const credentials = app.credentials.flatMap((credential) => { diff --git a/packages/core/EventManager.ts b/packages/core/EventManager.ts index 47b8c0114b..ff195f4196 100644 --- a/packages/core/EventManager.ts +++ b/packages/core/EventManager.ts @@ -75,7 +75,7 @@ export default class EventManager { * @param user */ constructor(user: EventManagerUser) { - const appCredentials = getApps(user.credentials).flatMap((app) => + const appCredentials = getApps(user.credentials, true).flatMap((app) => app.credentials.map((creds) => ({ ...creds, appName: app.name })) ); // This includes all calendar-related apps, traditional calendars such as Google Calendar diff --git a/packages/core/videoClient.ts b/packages/core/videoClient.ts index 098d98b0cd..1317b19fbb 100644 --- a/packages/core/videoClient.ts +++ b/packages/core/videoClient.ts @@ -176,6 +176,7 @@ const createMeetingWithCalVideo = async (calEvent: CalendarEvent) => { appId: "daily-video", type: "daily_video", userId: null, + teamId: null, key: dailyAppKeys, invalid: false, }, @@ -199,6 +200,7 @@ const getRecordingsOfCalVideoByRoomName = async ( appId: "daily-video", type: "daily_video", userId: null, + teamId: null, key: dailyAppKeys, invalid: false, }, @@ -220,6 +222,7 @@ const getDownloadLinkOfCalVideoByRecordingId = async (recordingId: string) => { appId: "daily-video", type: "daily_video", userId: null, + teamId: null, key: dailyAppKeys, invalid: false, }, diff --git a/packages/features/bookings/Booker/components/BookEventForm/BookFormAsModal.tsx b/packages/features/bookings/Booker/components/BookEventForm/BookFormAsModal.tsx index 7f3b04fc20..8a2a71a569 100644 --- a/packages/features/bookings/Booker/components/BookEventForm/BookFormAsModal.tsx +++ b/packages/features/bookings/Booker/components/BookEventForm/BookFormAsModal.tsx @@ -15,7 +15,7 @@ export function BookFormAsModal({ visible, onCancel }: { visible: boolean; onCan const selectedDuration = useBookerStore((state) => state.selectedDuration); const { data } = useEvent(); const parsedSelectedTimeslot = dayjs(selectedTimeslot); - const { timeFormat ,timezone} = useTimePreferences(); + const { timeFormat, timezone } = useTimePreferences(); return ( diff --git a/packages/lib/apps/getEnabledApps.ts b/packages/lib/apps/getEnabledApps.ts index 63f0bd4de3..2a695fef2b 100644 --- a/packages/lib/apps/getEnabledApps.ts +++ b/packages/lib/apps/getEnabledApps.ts @@ -2,12 +2,42 @@ import getApps from "@calcom/app-store/utils"; import type { CredentialData } from "@calcom/app-store/utils"; import { prisma } from "@calcom/prisma"; -const getEnabledApps = async (userCredentials: CredentialData[]) => { +import type { Prisma } from ".prisma/client"; + +/** + * + * @param credentials - Can be user or team credentials + * @param filterOnCredentials - Only include apps where credentials are present + * @returns A list of enabled app metadata & credentials tied to them + */ +const getEnabledApps = async (credentials: CredentialData[], filterOnCredentials?: boolean) => { + const filterOnIds = { + credentials: { + some: { + OR: [] as Prisma.CredentialWhereInput[], + }, + }, + } satisfies Prisma.AppWhereInput; + + if (filterOnCredentials) { + const userIds: number[] = [], + teamIds: number[] = []; + + for (const credential of credentials) { + if (credential.userId) userIds.push(credential.userId); + if (credential.teamId) teamIds.push(credential.teamId); + } + if (userIds.length) filterOnIds.credentials.some.OR.push({ userId: { in: userIds } }); + if (teamIds.length) filterOnIds.credentials.some.OR.push({ teamId: { in: teamIds } }); + } + const enabledApps = await prisma.app.findMany({ - where: { enabled: true }, + where: { + OR: [{ enabled: true, ...(filterOnIds.credentials.some.OR.length && filterOnIds) }], + }, select: { slug: true, enabled: true }, }); - const apps = getApps(userCredentials); + const apps = getApps(credentials, filterOnCredentials); const filteredApps = enabledApps.reduce((reducedArray, app) => { const appMetadata = apps.find((metadata) => metadata.slug === app.slug); diff --git a/packages/lib/getEventTypeById.ts b/packages/lib/getEventTypeById.ts index ddba0aabe7..517c6460ef 100644 --- a/packages/lib/getEventTypeById.ts +++ b/packages/lib/getEventTypeById.ts @@ -236,6 +236,7 @@ export default async function getEventTypeById({ type: true, key: true, userId: true, + teamId: true, appId: true, invalid: true, }, @@ -326,7 +327,7 @@ export default async function getEventTypeById({ const currentUser = eventType.users.find((u) => u.id === userId); const t = await getTranslation(currentUser?.locale ?? "en", "common"); - const integrations = await getEnabledApps(credentials); + const integrations = await getEnabledApps(credentials, true); const locationOptions = getLocationGroupedOptions(integrations, t); if (eventType.schedulingType === SchedulingType.MANAGED) { locationOptions.splice(0, 0, { diff --git a/packages/trpc/server/routers/loggedInViewer/integrations.handler.ts b/packages/trpc/server/routers/loggedInViewer/integrations.handler.ts index 09e711b657..53dc562fe1 100644 --- a/packages/trpc/server/routers/loggedInViewer/integrations.handler.ts +++ b/packages/trpc/server/routers/loggedInViewer/integrations.handler.ts @@ -114,7 +114,7 @@ export const integrationsHandler = async ({ ctx, input }: IntegrationsOptions) = } } - const enabledApps = await getEnabledApps(credentials); + const enabledApps = await getEnabledApps(credentials, onlyInstalled); //TODO: Refactor this to pick up only needed fields and prevent more leaking let apps = enabledApps.map( ({ credentials: _, credential: _1, key: _2 /* don't leak to frontend */, ...app }) => { diff --git a/packages/trpc/server/routers/loggedInViewer/locationOptions.handler.ts b/packages/trpc/server/routers/loggedInViewer/locationOptions.handler.ts index 155891c5fe..3ff0e76a24 100644 --- a/packages/trpc/server/routers/loggedInViewer/locationOptions.handler.ts +++ b/packages/trpc/server/routers/loggedInViewer/locationOptions.handler.ts @@ -29,12 +29,13 @@ export const locationOptionsHandler = async ({ ctx, input }: LocationOptionsOpti type: true, key: true, userId: true, + teamId: true, appId: true, invalid: true, }, }); - const integrations = await getEnabledApps(credentials); + const integrations = await getEnabledApps(credentials, true); const t = await getTranslation(ctx.user.locale ?? "en", "common"); diff --git a/packages/trpc/server/routers/loggedInViewer/updateUserDefaultConferencingApp.handler.ts b/packages/trpc/server/routers/loggedInViewer/updateUserDefaultConferencingApp.handler.ts index a34ba812d6..f23d5ef8f1 100644 --- a/packages/trpc/server/routers/loggedInViewer/updateUserDefaultConferencingApp.handler.ts +++ b/packages/trpc/server/routers/loggedInViewer/updateUserDefaultConferencingApp.handler.ts @@ -22,7 +22,7 @@ export const updateUserDefaultConferencingAppHandler = async ({ }: UpdateUserDefaultConferencingAppOptions) => { const currentMetadata = userMetadata.parse(ctx.user.metadata); const credentials = ctx.user.credentials; - const foundApp = getApps(credentials).filter((app) => app.slug === input.appSlug)[0]; + const foundApp = getApps(credentials, true).filter((app) => app.slug === input.appSlug)[0]; const appLocation = foundApp?.appData?.location; if (!foundApp || !appLocation) throw new TRPCError({ code: "BAD_REQUEST", message: "App not installed" }); diff --git a/packages/trpc/server/routers/viewer/eventTypes/create.handler.ts b/packages/trpc/server/routers/viewer/eventTypes/create.handler.ts index 0e86578220..7cfe4e1c35 100644 --- a/packages/trpc/server/routers/viewer/eventTypes/create.handler.ts +++ b/packages/trpc/server/routers/viewer/eventTypes/create.handler.ts @@ -25,7 +25,7 @@ export const createHandler = async ({ ctx, input }: CreateOptions) => { const { schedulingType, teamId, metadata, ...rest } = input; const userId = ctx.user.id; const isManagedEventType = schedulingType === SchedulingType.MANAGED; - // Get Users default conferncing app + // Get Users default conferencing app const defaultConferencingData = userMetadataSchema.parse(ctx.user.metadata)?.defaultConferencingApp; const appKeys = await getAppKeysFromSlug("daily-video"); @@ -42,7 +42,9 @@ export const createHandler = async ({ ctx, input }: CreateOptions) => { if (defaultConferencingData && defaultConferencingData.appSlug !== "daily-video") { const credentials = ctx.user.credentials; - const foundApp = getApps(credentials).filter((app) => app.slug === defaultConferencingData.appSlug)[0]; // There is only one possible install here so index [0] is the one we are looking for ; + const foundApp = getApps(credentials, true).filter( + (app) => app.slug === defaultConferencingData.appSlug + )[0]; // There is only one possible install here so index [0] is the one we are looking for ; const locationType = foundApp?.locationOption?.value ?? DailyLocationType; // Default to Daily if no location type is found locations = [{ type: locationType, link: defaultConferencingData.appLink }]; } diff --git a/packages/types/App.d.ts b/packages/types/App.d.ts index 61c4755301..e0a5a84257 100644 --- a/packages/types/App.d.ts +++ b/packages/types/App.d.ts @@ -111,7 +111,7 @@ export interface App { /** Number of reviews, harcoded for now. Should be fetched later on. */ reviews?: number; /** - * Wheter if the app is installed globally or needs user intervention. + * Whether if the app is installed globally or needs user intervention. * Used to show Connect/Disconnect buttons in App Store * */ isGlobal?: boolean; diff --git a/packages/types/Credential.d.ts b/packages/types/Credential.d.ts index 2737c06024..359b09b1b0 100644 --- a/packages/types/Credential.d.ts +++ b/packages/types/Credential.d.ts @@ -11,7 +11,7 @@ export type CredentialPayload = Prisma.CredentialGetPayload<{ appId: true; type: true; userId: true; - teamId?: true; + teamId: true; key: true; invalid: true; };