diff --git a/apps/web/lib/insights/getServerSideProps.tsx b/apps/web/lib/insights/getServerSideProps.tsx index d89a77bd5028d..a6108e44ac239 100644 --- a/apps/web/lib/insights/getServerSideProps.tsx +++ b/apps/web/lib/insights/getServerSideProps.tsx @@ -1,11 +1,11 @@ -import { getFeatureFlagMap } from "@calcom/features/flags/server/utils"; +import { getFeatureFlag } from "@calcom/features/flags/server/utils"; // If feature flag is disabled, return not found on getServerSideProps export const getServerSideProps = async () => { const prisma = await import("@calcom/prisma").then((mod) => mod.default); - const flags = await getFeatureFlagMap(prisma); + const insightsEnabled = await getFeatureFlag(prisma, "insights"); - if (flags.insights === false) { + if (!insightsEnabled) { return { notFound: true, } as const; diff --git a/apps/web/lib/settings/organizations/new/getServerSideProps.tsx b/apps/web/lib/settings/organizations/new/getServerSideProps.tsx index 5ba051296de73..f54bfc2f424d0 100644 --- a/apps/web/lib/settings/organizations/new/getServerSideProps.tsx +++ b/apps/web/lib/settings/organizations/new/getServerSideProps.tsx @@ -1,12 +1,12 @@ import type { GetServerSidePropsContext } from "next"; -import { getFeatureFlagMap } from "@calcom/features/flags/server/utils"; +import { getFeatureFlag } from "@calcom/features/flags/server/utils"; export const getServerSideProps = async (context: GetServerSidePropsContext) => { const prisma = await import("@calcom/prisma").then((mod) => mod.default); - const flags = await getFeatureFlagMap(prisma); + const organizations = await getFeatureFlag(prisma, "organizations"); // Check if organizations are enabled - if (flags["organizations"] !== true) { + if (!organizations) { return { notFound: true, } as const; diff --git a/apps/web/lib/signup/getServerSideProps.tsx b/apps/web/lib/signup/getServerSideProps.tsx index da32a050c0c9c..22a2149a9f05b 100644 --- a/apps/web/lib/signup/getServerSideProps.tsx +++ b/apps/web/lib/signup/getServerSideProps.tsx @@ -4,7 +4,7 @@ import { z } from "zod"; import { getOrgUsernameFromEmail } from "@calcom/features/auth/signup/utils/getOrgUsernameFromEmail"; import { checkPremiumUsername } from "@calcom/features/ee/common/lib/checkPremiumUsername"; import { isSAMLLoginEnabled } from "@calcom/features/ee/sso/lib/saml"; -import { getFeatureFlagMap } from "@calcom/features/flags/server/utils"; +import { getFeatureFlag } from "@calcom/features/flags/server/utils"; import { IS_SELF_HOSTED, WEBAPP_URL } from "@calcom/lib/constants"; import slugify from "@calcom/lib/slugify"; import { teamMetadataSchema } from "@calcom/prisma/zod-utils"; @@ -24,7 +24,7 @@ const querySchema = z.object({ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => { const prisma = await import("@calcom/prisma").then((mod) => mod.default); - const flags = await getFeatureFlagMap(prisma); + const signupDisabled = await getFeatureFlag(prisma, "disable-signup"); const ssr = await ssrInit(ctx); const token = z.string().optional().parse(ctx.query.token); @@ -38,7 +38,7 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => { // username + email prepopulated from query params const { username: preFillusername, email: prefilEmail } = querySchema.parse(ctx.query); - if ((process.env.NEXT_PUBLIC_DISABLE_SIGNUP === "true" && !token) || flags["disable-signup"]) { + if ((process.env.NEXT_PUBLIC_DISABLE_SIGNUP === "true" && !token) || signupDisabled) { return { notFound: true, } as const; diff --git a/apps/web/lib/team/[slug]/getServerSideProps.tsx b/apps/web/lib/team/[slug]/getServerSideProps.tsx index 76967e87c223f..fa68d09675092 100644 --- a/apps/web/lib/team/[slug]/getServerSideProps.tsx +++ b/apps/web/lib/team/[slug]/getServerSideProps.tsx @@ -1,7 +1,7 @@ import type { GetServerSidePropsContext } from "next"; import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains"; -import { getFeatureFlagMap } from "@calcom/features/flags/server/utils"; +import { getFeatureFlag } from "@calcom/features/flags/server/utils"; import { getBookerBaseUrlSync } from "@calcom/lib/getBookerUrl/client"; import logger from "@calcom/lib/logger"; import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML"; @@ -37,12 +37,11 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => // Provided by Rewrite from next.config.js const isOrgProfile = context.query?.isOrgProfile === "1"; - const flags = await getFeatureFlagMap(prisma); - const isOrganizationFeatureEnabled = flags["organizations"]; + const organizationsEnabled = await getFeatureFlag(prisma, "organizations"); log.debug("getServerSideProps", { isOrgProfile, - isOrganizationFeatureEnabled, + isOrganizationFeatureEnabled: organizationsEnabled, isValidOrgDomain, currentOrgDomain, }); @@ -74,7 +73,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => if ( (!isValidOrgDomain && team?.parent) || (!isValidOrgDomain && !!metadata?.isOrganization) || - !isOrganizationFeatureEnabled + !organizationsEnabled ) { return { notFound: true } as const; } diff --git a/packages/app-store/googlecalendar/lib/CalendarService.test.ts b/packages/app-store/googlecalendar/lib/CalendarService.test.ts index 8cf8f5b247930..776ac6d63e158 100644 --- a/packages/app-store/googlecalendar/lib/CalendarService.test.ts +++ b/packages/app-store/googlecalendar/lib/CalendarService.test.ts @@ -9,9 +9,7 @@ afterEach(() => { }); vi.mock("@calcom/features/flags/server/utils", () => ({ - getFeatureFlagMap: vi.fn().mockResolvedValue({ - "calendar-cache": true, - }), + getFeatureFlag: vi.fn().mockReturnValue(true), })); vi.mock("./getGoogleAppKeys", () => ({ diff --git a/packages/app-store/googlecalendar/lib/CalendarService.ts b/packages/app-store/googlecalendar/lib/CalendarService.ts index 079a9c420af4d..3ce9a8fa1b7f6 100644 --- a/packages/app-store/googlecalendar/lib/CalendarService.ts +++ b/packages/app-store/googlecalendar/lib/CalendarService.ts @@ -6,7 +6,7 @@ import { RRule } from "rrule"; import { MeetLocationType } from "@calcom/app-store/locations"; import dayjs from "@calcom/dayjs"; -import { getFeatureFlagMap } from "@calcom/features/flags/server/utils"; +import { getFeatureFlag } from "@calcom/features/flags/server/utils"; import { getLocation, getRichDescription } from "@calcom/lib/CalEventParser"; import type CalendarService from "@calcom/lib/CalendarService"; import logger from "@calcom/lib/logger"; @@ -461,10 +461,10 @@ export default class GoogleCalendarService implements Calendar { items: { id: string }[]; }): Promise { const calendar = await this.authedCalendar(); - const flags = await getFeatureFlagMap(prisma); + const calendarCacheEnabled = await getFeatureFlag(prisma, "calendar-cache"); let freeBusyResult: calendar_v3.Schema$FreeBusyResponse = {}; - if (!flags["calendar-cache"]) { + if (!calendarCacheEnabled) { this.log.warn("Calendar Cache is disabled - Skipping"); const { timeMin, timeMax, items } = args; const apires = await calendar.freebusy.query({ diff --git a/packages/emails/templates/_base-email.ts b/packages/emails/templates/_base-email.ts index 0fd65be9d7636..790f4b9210b32 100644 --- a/packages/emails/templates/_base-email.ts +++ b/packages/emails/templates/_base-email.ts @@ -3,7 +3,7 @@ import { createTransport } from "nodemailer"; import { z } from "zod"; import dayjs from "@calcom/dayjs"; -import { getFeatureFlagMap } from "@calcom/features/flags/server/utils"; +import { getFeatureFlag } from "@calcom/features/flags/server/utils"; import { getErrorFromUnknown } from "@calcom/lib/errors"; import { serverConfig } from "@calcom/lib/serverConfig"; import { setTestEmail } from "@calcom/lib/testEmails"; @@ -28,9 +28,9 @@ export default class BaseEmail { return {}; } public async sendEmail() { - const featureFlags = await getFeatureFlagMap(prisma); + const emailsDisabled = await getFeatureFlag(prisma, "emails"); /** If email kill switch exists and is active, we prevent emails being sent. */ - if (featureFlags.emails) { + if (emailsDisabled) { console.warn("Skipped Sending Email due to active Kill Switch"); return new Promise((r) => r("Skipped Sending Email due to active Kill Switch")); } diff --git a/packages/features/auth/lib/verifyEmail.ts b/packages/features/auth/lib/verifyEmail.ts index 10bf7b35cd08a..0f508181709d5 100644 --- a/packages/features/auth/lib/verifyEmail.ts +++ b/packages/features/auth/lib/verifyEmail.ts @@ -6,7 +6,7 @@ import { sendEmailVerificationLink, sendChangeOfEmailVerificationLink, } from "@calcom/emails/email-manager"; -import { getFeatureFlagMap } from "@calcom/features/flags/server/utils"; +import { getFeatureFlag } from "@calcom/features/flags/server/utils"; import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError"; import { WEBAPP_URL } from "@calcom/lib/constants"; import logger from "@calcom/lib/logger"; @@ -24,9 +24,9 @@ interface VerifyEmailType { export const sendEmailVerification = async ({ email, language, username }: VerifyEmailType) => { const token = randomBytes(32).toString("hex"); const translation = await getTranslation(language ?? "en", "common"); - const flags = await getFeatureFlagMap(prisma); + const emailVerification = await getFeatureFlag(prisma, "email-verification"); - if (!flags["email-verification"]) { + if (!emailVerification) { log.warn("Email verification is disabled - Skipping"); return { ok: true, skipped: true }; } @@ -93,9 +93,9 @@ interface ChangeOfEmail { export const sendChangeOfEmailVerification = async ({ user, language }: ChangeOfEmail) => { const token = randomBytes(32).toString("hex"); const translation = await getTranslation(language ?? "en", "common"); - const flags = await getFeatureFlagMap(prisma); + const emailVerification = await getFeatureFlag(prisma, "email-verification"); - if (!flags["email-verification"]) { + if (!emailVerification) { log.warn("Email verification is disabled - Skipping"); return { ok: true, skipped: true }; } diff --git a/packages/features/ee/organizations/pages/organization.tsx b/packages/features/ee/organizations/pages/organization.tsx index b9114d70da1ae..0699101f40978 100644 --- a/packages/features/ee/organizations/pages/organization.tsx +++ b/packages/features/ee/organizations/pages/organization.tsx @@ -1,14 +1,14 @@ import type { GetServerSidePropsContext } from "next"; import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; -import { getFeatureFlagMap } from "@calcom/features/flags/server/utils"; +import { getFeatureFlag } from "@calcom/features/flags/server/utils"; import { MembershipRole } from "@calcom/prisma/client"; export const getServerSideProps = async ({ req, res }: GetServerSidePropsContext) => { const prisma = await import("@calcom/prisma").then((mod) => mod.default); - const flags = await getFeatureFlagMap(prisma); + const organizationsEnabled = await getFeatureFlag(prisma, "organizations"); // Check if organizations are enabled - if (flags["organizations"] !== true) { + if (!organizationsEnabled) { return { notFound: true, }; diff --git a/packages/features/flags/server/utils.ts b/packages/features/flags/server/utils.ts index ddbf1dc491b32..001a814087101 100644 --- a/packages/features/flags/server/utils.ts +++ b/packages/features/flags/server/utils.ts @@ -12,3 +12,55 @@ export async function getFeatureFlagMap(prisma: PrismaClient) { return acc; }, {} as Partial); } + +interface CacheEntry { + value: boolean; // adapt to other supported value types in the future + expiry: number; +} + +interface CacheOptions { + ttl: number; // time in ms +} + +const featureFlagCache = new Map(); + +const isExpired = (entry: CacheEntry): boolean => { + return Date.now() > entry.expiry; +}; + +export const getFeatureFlag = async ( + prisma: PrismaClient, + slug: keyof AppFlags, + options: CacheOptions = { ttl: 5 * 60 * 1000 } +): Promise => { + // pre-compute all app flags, each one will independelty reload it's own state after expiry. + + if (featureFlagCache.size === 0) { + const flags = await prisma.feature.findMany({ orderBy: { slug: "asc" } }); + flags.forEach((flag) => { + featureFlagCache.set(flag.slug as keyof AppFlags, { + value: flag.enabled, + expiry: Date.now() + options.ttl, + }); + }); + } + + const cacheEntry = featureFlagCache.get(slug); + + if (cacheEntry && !isExpired(cacheEntry)) { + return cacheEntry.value; + } + + const flag = await prisma.feature.findUnique({ + where: { + slug, + }, + }); + + const isEnabled = Boolean(flag && flag.enabled); + const expiry = Date.now() + options.ttl; + + featureFlagCache.set(slug, { value: isEnabled, expiry }); + + return isEnabled; +}; diff --git a/packages/trpc/server/routers/loggedInViewer/updateProfile.handler.ts b/packages/trpc/server/routers/loggedInViewer/updateProfile.handler.ts index 0f0a444b05490..512bbe4096bd4 100644 --- a/packages/trpc/server/routers/loggedInViewer/updateProfile.handler.ts +++ b/packages/trpc/server/routers/loggedInViewer/updateProfile.handler.ts @@ -6,7 +6,7 @@ import stripe from "@calcom/app-store/stripepayment/lib/server"; import { getPremiumMonthlyPlanPriceId } from "@calcom/app-store/stripepayment/lib/utils"; import { passwordResetRequest } from "@calcom/features/auth/lib/passwordResetRequest"; import { sendChangeOfEmailVerification } from "@calcom/features/auth/lib/verifyEmail"; -import { getFeatureFlagMap } from "@calcom/features/flags/server/utils"; +import { getFeatureFlag } from "@calcom/features/flags/server/utils"; import hasKeyInMetadata from "@calcom/lib/hasKeyInMetadata"; import { HttpError } from "@calcom/lib/http-error"; import logger from "@calcom/lib/logger"; @@ -63,7 +63,7 @@ export const updateProfileHandler = async ({ ctx, input }: UpdateProfileOptions) const { user } = ctx; const userMetadata = handleUserMetadata({ ctx, input }); const locale = input.locale || user.locale; - const flags = await getFeatureFlagMap(prisma); + const emailVerification = await getFeatureFlag(prisma, "email-verification"); const data: Prisma.UserUpdateInput = { ...input, @@ -135,10 +135,8 @@ export const updateProfileHandler = async ({ ctx, input }: UpdateProfileOptions) hasEmailBeenChanged && user.identityProvider !== IdentityProvider.CAL; const hasEmailChangedOnCalProvider = hasEmailBeenChanged && user.identityProvider === IdentityProvider.CAL; - const sendEmailVerification = flags["email-verification"]; - if (hasEmailBeenChanged) { - if (sendEmailVerification && hasEmailChangedOnCalProvider) { + if (emailVerification && hasEmailChangedOnCalProvider) { // Set metadata of the user so we can set it to this updated email once it is confirmed data.metadata = { ...userMetadata, @@ -284,12 +282,12 @@ export const updateProfileHandler = async ({ ctx, input }: UpdateProfileOptions) return { ...input, - email: sendEmailVerification && hasEmailChangedOnCalProvider ? user.email : input.email, + email: emailVerification && hasEmailChangedOnCalProvider ? user.email : input.email, signOutUser, passwordReset, avatarUrl: updatedUser.avatarUrl, hasEmailBeenChanged, - sendEmailVerification, + sendEmailVerification: emailVerification, }; };