perf: lazy load tRPC routes (#8167)

* experiment: cold start perf

* fix: update failing test

* chore: add database indexes

* chore: use json protocol and add query batching back

* Update [status].tsx

* Update [trpc].ts

* Delete getSlimSession.ts

* Update createContext.ts

* remove trpc caller

* correctly import Prisma

* lazy ethRouter

* replace crypto with md5

* import fixes

* public event endpoint refactor

* Update yarn.lock

* Update yarn.lock

* Using yarn.lock from main

---------

Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Efraín Rochín <roae.85@gmail.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
This commit is contained in:
Lucas Smith 2023-04-26 08:39:47 +10:00 committed by GitHub
parent a4725920ff
commit 1eeb91a793
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
275 changed files with 11251 additions and 7614 deletions

View File

@ -6,6 +6,6 @@
},
"typescript.preferences.importModuleSpecifier": "non-relative",
"spellright.language": ["en"],
"spellright.documentTypes": ["markdown", "typescript"],
"spellright.documentTypes": ["markdown", "typescript", "typescriptreact"],
"tailwindCSS.experimental.classRegex": [["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"]]
}

View File

@ -71,5 +71,5 @@ module.exports = {
return config;
},
typescript: { reactDocgen: 'react-docgen' }
typescript: { reactDocgen: "react-docgen" },
};

View File

@ -11,7 +11,7 @@ import useMediaQuery from "@calcom/lib/hooks/useMediaQuery";
import { TimeFormat } from "@calcom/lib/timeFormat";
import { nameOfDay } from "@calcom/lib/weekday";
import { trpc } from "@calcom/trpc/react";
import type { Slot } from "@calcom/trpc/server/routers/viewer/slots";
import type { Slot } from "@calcom/trpc/server/routers/viewer/slots/types";
import { SkeletonContainer, SkeletonText, ToggleGroup } from "@calcom/ui";
import classNames from "@lib/classNames";

View File

@ -483,12 +483,12 @@ const RecurringBookingsTooltip = ({ booking, recurringDates }: RecurringBookings
i18n: { language },
} = useLocale();
const now = new Date();
const recurringCount = recurringDates.filter((date) => {
const recurringCount = recurringDates.filter((recurringDate) => {
return (
date >= now &&
recurringDate >= now &&
!booking.recurringInfo?.bookings[BookingStatus.CANCELLED]
.map((date) => date.toDateString())
.includes(date.toDateString())
.includes(recurringDate.toDateString())
);
}).length;

View File

@ -7,7 +7,7 @@ import { defaultResponder } from "@calcom/lib/server";
import prisma from "@calcom/prisma";
import { TRPCError } from "@calcom/trpc/server";
import { createContext } from "@calcom/trpc/server/createContext";
import { viewerRouter } from "@calcom/trpc/server/routers/viewer";
import { viewerRouter } from "@calcom/trpc/server/routers/viewer/_router";
enum DirectAction {
ACCEPT = "accept",
@ -51,7 +51,8 @@ async function handler(req: NextApiRequest, res: NextApiResponse<Response>) {
try {
/** @see https://trpc.io/docs/server-side-calls */
const ctx = await createContext({ req, res }, sessionGetter);
const caller = viewerRouter.createCaller(ctx);
const caller = viewerRouter.createCaller({ ...ctx, req, res });
await caller.bookings.confirm({
bookingId: booking.id,
recurringEventId: booking.recurringEventId || undefined,

View File

@ -15,15 +15,14 @@ import { v4 as uuidv4 } from "uuid";
import logger from "@calcom/lib/logger";
import prisma from "@calcom/prisma";
import type { BookingStatus } from "@calcom/prisma/client";
import type { Slot } from "@calcom/trpc/server/routers/viewer/slots";
import { getSchedule } from "@calcom/trpc/server/routers/viewer/slots";
import type { Slot } from "@calcom/trpc/server/routers/viewer/slots/types";
import { getSchedule } from "@calcom/trpc/server/routers/viewer/slots/util";
import { prismaMock, CalendarManagerMock } from "../../../../tests/config/singleton";
// TODO: Mock properly
prismaMock.eventType.findUnique.mockResolvedValue(null);
prismaMock.user.findMany.mockResolvedValue([]);
prismaMock.selectedSlots.findMany.mockResolvedValue([]);
jest.mock("@calcom/lib/constants", () => ({
IS_PRODUCTION: true,
@ -271,16 +270,13 @@ describe("getSchedule", () => {
end: `${plus2DateString}T23:00:00.000Z`,
},
]);
const scheduleForDayWithAGoogleCalendarBooking = await getSchedule(
{
eventTypeId: 1,
eventTypeSlug: "",
startTime: `${plus1DateString}T18:30:00.000Z`,
endTime: `${plus2DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
},
ctx
);
const scheduleForDayWithAGoogleCalendarBooking = await getSchedule({
eventTypeId: 1,
eventTypeSlug: "",
startTime: `${plus1DateString}T18:30:00.000Z`,
endTime: `${plus2DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
});
// As per Google Calendar Availability, only 4PM(4-4:45PM) GMT slot would be available
expect(scheduleForDayWithAGoogleCalendarBooking).toHaveTimeSlots([`04:00:00.000Z`], {
@ -357,17 +353,14 @@ describe("getSchedule", () => {
});
// Day Plus 2 is completely free - It only has non accepted bookings
const scheduleOnCompletelyFreeDay = await getSchedule(
{
eventTypeId: 1,
// EventTypeSlug doesn't matter for non-dynamic events
eventTypeSlug: "",
startTime: `${plus1DateString}T18:30:00.000Z`,
endTime: `${plus2DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
},
ctx
);
const scheduleOnCompletelyFreeDay = await getSchedule({
eventTypeId: 1,
// EventTypeSlug doesn't matter for non-dynamic events
eventTypeSlug: "",
startTime: `${plus1DateString}T18:30:00.000Z`,
endTime: `${plus2DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
});
// getSchedule returns timeslots in GMT
expect(scheduleOnCompletelyFreeDay).toHaveTimeSlots(
@ -390,16 +383,13 @@ describe("getSchedule", () => {
);
// Day plus 3
const scheduleForDayWithOneBooking = await getSchedule(
{
eventTypeId: 1,
eventTypeSlug: "",
startTime: `${plus2DateString}T18:30:00.000Z`,
endTime: `${plus3DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
},
ctx
);
const scheduleForDayWithOneBooking = await getSchedule({
eventTypeId: 1,
eventTypeSlug: "",
startTime: `${plus2DateString}T18:30:00.000Z`,
endTime: `${plus3DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
});
expect(scheduleForDayWithOneBooking).toHaveTimeSlots(
[
@ -455,16 +445,13 @@ describe("getSchedule", () => {
});
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
const { dateString: plus2DateString } = getDate({ dateIncrement: 2 });
const scheduleForEventWith30Length = await getSchedule(
{
eventTypeId: 1,
eventTypeSlug: "",
startTime: `${plus1DateString}T18:30:00.000Z`,
endTime: `${plus2DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
},
ctx
);
const scheduleForEventWith30Length = await getSchedule({
eventTypeId: 1,
eventTypeSlug: "",
startTime: `${plus1DateString}T18:30:00.000Z`,
endTime: `${plus2DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
});
expect(scheduleForEventWith30Length).toHaveTimeSlots(
[
`04:00:00.000Z`,
@ -490,16 +477,13 @@ describe("getSchedule", () => {
}
);
const scheduleForEventWith30minsLengthAndSlotInterval2hrs = await getSchedule(
{
eventTypeId: 2,
eventTypeSlug: "",
startTime: `${plus1DateString}T18:30:00.000Z`,
endTime: `${plus2DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
},
ctx
);
const scheduleForEventWith30minsLengthAndSlotInterval2hrs = await getSchedule({
eventTypeId: 2,
eventTypeSlug: "",
startTime: `${plus1DateString}T18:30:00.000Z`,
endTime: `${plus2DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
});
// `slotInterval` takes precedence over `length`
expect(scheduleForEventWith30minsLengthAndSlotInterval2hrs).toHaveTimeSlots(
[`04:00:00.000Z`, `06:00:00.000Z`, `08:00:00.000Z`, `10:00:00.000Z`, `12:00:00.000Z`],
@ -553,16 +537,13 @@ describe("getSchedule", () => {
});
const { dateString: todayDateString } = getDate();
const { dateString: minus1DateString } = getDate({ dateIncrement: -1 });
const scheduleForEventWithBookingNotice13Hrs = await getSchedule(
{
eventTypeId: 1,
eventTypeSlug: "",
startTime: `${minus1DateString}T18:30:00.000Z`,
endTime: `${todayDateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
},
ctx
);
const scheduleForEventWithBookingNotice13Hrs = await getSchedule({
eventTypeId: 1,
eventTypeSlug: "",
startTime: `${minus1DateString}T18:30:00.000Z`,
endTime: `${todayDateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
});
expect(scheduleForEventWithBookingNotice13Hrs).toHaveTimeSlots(
[
/*`04:00:00.000Z`, `06:00:00.000Z`, - Minimum time slot is 07:30 UTC*/ `08:00:00.000Z`,
@ -574,16 +555,13 @@ describe("getSchedule", () => {
}
);
const scheduleForEventWithBookingNotice10Hrs = await getSchedule(
{
eventTypeId: 2,
eventTypeSlug: "",
startTime: `${minus1DateString}T18:30:00.000Z`,
endTime: `${todayDateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
},
ctx
);
const scheduleForEventWithBookingNotice10Hrs = await getSchedule({
eventTypeId: 2,
eventTypeSlug: "",
startTime: `${minus1DateString}T18:30:00.000Z`,
endTime: `${todayDateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
});
expect(scheduleForEventWithBookingNotice10Hrs).toHaveTimeSlots(
[
/*`04:00:00.000Z`, - Minimum bookable time slot is 04:30 UTC but next available is 06:00*/
@ -639,16 +617,13 @@ describe("getSchedule", () => {
},
]);
const scheduleForEventOnADayWithNonCalBooking = await getSchedule(
{
eventTypeId: 1,
eventTypeSlug: "",
startTime: `${plus2DateString}T18:30:00.000Z`,
endTime: `${plus3DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
},
ctx
);
const scheduleForEventOnADayWithNonCalBooking = await getSchedule({
eventTypeId: 1,
eventTypeSlug: "",
startTime: `${plus2DateString}T18:30:00.000Z`,
endTime: `${plus3DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
});
expect(scheduleForEventOnADayWithNonCalBooking).toHaveTimeSlots(
[
@ -714,16 +689,13 @@ describe("getSchedule", () => {
},
]);
const scheduleForEventOnADayWithCalBooking = await getSchedule(
{
eventTypeId: 1,
eventTypeSlug: "",
startTime: `${plus1DateString}T18:30:00.000Z`,
endTime: `${plus2DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
},
ctx
);
const scheduleForEventOnADayWithCalBooking = await getSchedule({
eventTypeId: 1,
eventTypeSlug: "",
startTime: `${plus1DateString}T18:30:00.000Z`,
endTime: `${plus2DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
});
expect(scheduleForEventOnADayWithCalBooking).toHaveTimeSlots(
[
@ -767,16 +739,13 @@ describe("getSchedule", () => {
createBookingScenario(scenarioData);
const scheduleForEventOnADayWithDateOverride = await getSchedule(
{
eventTypeId: 1,
eventTypeSlug: "",
startTime: `${plus1DateString}T18:30:00.000Z`,
endTime: `${plus2DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
},
ctx
);
const scheduleForEventOnADayWithDateOverride = await getSchedule({
eventTypeId: 1,
eventTypeSlug: "",
startTime: `${plus1DateString}T18:30:00.000Z`,
endTime: `${plus2DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
});
expect(scheduleForEventOnADayWithDateOverride).toHaveTimeSlots(
["08:30:00.000Z", "09:30:00.000Z", "10:30:00.000Z", "11:30:00.000Z"],
@ -853,16 +822,13 @@ describe("getSchedule", () => {
// Requesting this user's availability for their
// individual Event Type
const thisUserAvailability = await getSchedule(
{
eventTypeId: 2,
eventTypeSlug: "",
startTime: `${plus1DateString}T18:30:00.000Z`,
endTime: `${plus2DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
},
ctx
);
const thisUserAvailability = await getSchedule({
eventTypeId: 2,
eventTypeSlug: "",
startTime: `${plus1DateString}T18:30:00.000Z`,
endTime: `${plus2DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
});
expect(thisUserAvailability).toHaveTimeSlots(
[
@ -951,16 +917,13 @@ describe("getSchedule", () => {
hosts: [],
});
const scheduleForTeamEventOnADayWithNoBooking = await getSchedule(
{
eventTypeId: 1,
eventTypeSlug: "",
startTime: `${todayDateString}T18:30:00.000Z`,
endTime: `${plus1DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
},
ctx
);
const scheduleForTeamEventOnADayWithNoBooking = await getSchedule({
eventTypeId: 1,
eventTypeSlug: "",
startTime: `${todayDateString}T18:30:00.000Z`,
endTime: `${plus1DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
});
expect(scheduleForTeamEventOnADayWithNoBooking).toHaveTimeSlots(
[
@ -981,16 +944,13 @@ describe("getSchedule", () => {
}
);
const scheduleForTeamEventOnADayWithOneBookingForEachUser = await getSchedule(
{
eventTypeId: 1,
eventTypeSlug: "",
startTime: `${plus1DateString}T18:30:00.000Z`,
endTime: `${plus2DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
},
ctx
);
const scheduleForTeamEventOnADayWithOneBookingForEachUser = await getSchedule({
eventTypeId: 1,
eventTypeSlug: "",
startTime: `${plus1DateString}T18:30:00.000Z`,
endTime: `${plus2DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
});
// A user with blocked time in another event, still affects Team Event availability
// It's a collective availability, so both user 101 and 102 are considered for timeslots
expect(scheduleForTeamEventOnADayWithOneBookingForEachUser).toHaveTimeSlots(
@ -1088,16 +1048,13 @@ describe("getSchedule", () => {
],
hosts: [],
});
const scheduleForTeamEventOnADayWithOneBookingForEachUserButOnDifferentTimeslots = await getSchedule(
{
eventTypeId: 1,
eventTypeSlug: "",
startTime: `${plus1DateString}T18:30:00.000Z`,
endTime: `${plus2DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
},
ctx
);
const scheduleForTeamEventOnADayWithOneBookingForEachUserButOnDifferentTimeslots = await getSchedule({
eventTypeId: 1,
eventTypeSlug: "",
startTime: `${plus1DateString}T18:30:00.000Z`,
endTime: `${plus2DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
});
// A user with blocked time in another event, still affects Team Event availability
expect(scheduleForTeamEventOnADayWithOneBookingForEachUserButOnDifferentTimeslots).toHaveTimeSlots(
[
@ -1116,16 +1073,13 @@ describe("getSchedule", () => {
{ dateString: plus2DateString }
);
const scheduleForTeamEventOnADayWithOneBookingForEachUserOnSameTimeSlot = await getSchedule(
{
eventTypeId: 1,
eventTypeSlug: "",
startTime: `${plus2DateString}T18:30:00.000Z`,
endTime: `${plus3DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
},
ctx
);
const scheduleForTeamEventOnADayWithOneBookingForEachUserOnSameTimeSlot = await getSchedule({
eventTypeId: 1,
eventTypeSlug: "",
startTime: `${plus2DateString}T18:30:00.000Z`,
endTime: `${plus3DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
});
// A user with blocked time in another event, still affects Team Event availability
expect(scheduleForTeamEventOnADayWithOneBookingForEachUserOnSameTimeSlot).toHaveTimeSlots(
[

View File

@ -0,0 +1,25 @@
import { checkBalance } from "../utils/ethereum";
import type { TBalanceInputSchema } from "./balance.schema";
interface BalanceHandlerOptions {
input: TBalanceInputSchema;
}
export const balanceHandler = async ({ input }: BalanceHandlerOptions) => {
const { address, tokenAddress, chainId } = input;
try {
const hasBalance = await checkBalance(address, tokenAddress, chainId);
return {
data: {
hasBalance,
},
};
} catch (e) {
return {
data: {
hasBalance: false,
},
};
}
};

View File

@ -0,0 +1,19 @@
import z from "zod";
export const ZBalanceInputSchema = z.object({
address: z.string(),
tokenAddress: z.string(),
chainId: z.number(),
});
export const ZBalanceOutputSchema = z.object({
data: z
.object({
hasBalance: z.boolean(),
})
.nullish(),
error: z.string().nullish(),
});
export type TBalanceOutputSchema = z.infer<typeof ZBalanceOutputSchema>;
export type TBalanceInputSchema = z.infer<typeof ZBalanceInputSchema>;

View File

@ -0,0 +1,42 @@
import { ethers } from "ethers";
import { configureChains, createClient } from "wagmi";
import abi from "../utils/abi.json";
import { getProviders, SUPPORTED_CHAINS } from "../utils/ethereum";
import type { TContractInputSchema } from "./contract.schema";
interface ContractHandlerOptions {
input: TContractInputSchema;
}
export const contractHandler = async ({ input }: ContractHandlerOptions) => {
const { address, chainId } = input;
const { provider } = configureChains(
SUPPORTED_CHAINS.filter((chain) => chain.id === chainId),
getProviders()
);
const client = createClient({
provider,
});
const contract = new ethers.Contract(address, abi, client.provider);
try {
const name = await contract.name();
const symbol = await contract.symbol();
return {
data: {
name,
symbol: `$${symbol}`,
},
};
} catch (e) {
return {
data: {
name: address,
symbol: "$UNKNOWN",
},
};
}
};

View File

@ -0,0 +1,19 @@
import z from "zod";
export const ZContractInputSchema = z.object({
address: z.string(),
chainId: z.number(),
});
export const ZContractOutputSchema = z.object({
data: z
.object({
name: z.string(),
symbol: z.string(),
})
.nullish(),
error: z.string().nullish(),
});
export type TContractInputSchema = z.infer<typeof ZContractInputSchema>;
export type TContractOutputSchema = z.infer<typeof ZContractOutputSchema>;

View File

@ -1,100 +1,53 @@
import { ethers } from "ethers";
import { configureChains, createClient } from "wagmi";
import { z } from "zod";
import { router, publicProcedure } from "@calcom/trpc/server/trpc";
import abi from "../utils/abi.json";
import { checkBalance, getProviders, SUPPORTED_CHAINS } from "../utils/ethereum";
import { ZBalanceInputSchema, ZBalanceOutputSchema } from "./balance.schema";
import { ZContractInputSchema, ZContractOutputSchema } from "./contract.schema";
interface EthRouterHandlersCache {
contract?: typeof import("./contract.handler").contractHandler;
balance?: typeof import("./balance.handler").balanceHandler;
}
const UNSTABLE_HANDLER_CACHE: EthRouterHandlersCache = {};
const ethRouter = router({
// Fetch contract `name` and `symbol` or error
contract: publicProcedure
.input(
z.object({
address: z.string(),
chainId: z.number(),
})
)
.output(
z.object({
data: z
.object({
name: z.string(),
symbol: z.string(),
})
.nullish(),
error: z.string().nullish(),
})
)
.input(ZContractInputSchema)
.output(ZContractOutputSchema)
.query(async ({ input }) => {
const { address, chainId } = input;
const { provider } = configureChains(
SUPPORTED_CHAINS.filter((chain) => chain.id === chainId),
getProviders()
);
const client = createClient({
provider,
});
const contract = new ethers.Contract(address, abi, client.provider);
try {
const name = await contract.name();
const symbol = await contract.symbol();
return {
data: {
name,
symbol: `$${symbol}`,
},
};
} catch (e) {
return {
data: {
name: address,
symbol: "$UNKNOWN",
},
};
if (!UNSTABLE_HANDLER_CACHE.contract) {
UNSTABLE_HANDLER_CACHE.contract = await import("./contract.handler").then(
(mod) => mod.contractHandler
);
}
// Unreachable code but required for type safety
if (!UNSTABLE_HANDLER_CACHE.contract) {
throw new Error("Failed to load handler");
}
return UNSTABLE_HANDLER_CACHE.contract({
input,
});
}),
// Fetch user's `balance` of either ERC-20 or ERC-721 compliant token or error
balance: publicProcedure
.input(
z.object({
address: z.string(),
tokenAddress: z.string(),
chainId: z.number(),
})
)
.output(
z.object({
data: z
.object({
hasBalance: z.boolean(),
})
.nullish(),
error: z.string().nullish(),
})
)
.input(ZBalanceInputSchema)
.output(ZBalanceOutputSchema)
.query(async ({ input }) => {
const { address, tokenAddress, chainId } = input;
try {
const hasBalance = await checkBalance(address, tokenAddress, chainId);
return {
data: {
hasBalance,
},
};
} catch (e) {
return {
data: {
hasBalance: false,
},
};
if (!UNSTABLE_HANDLER_CACHE.balance) {
UNSTABLE_HANDLER_CACHE.balance = await import("./balance.handler").then((mod) => mod.balanceHandler);
}
// Unreachable code but required for type safety
if (!UNSTABLE_HANDLER_CACHE.balance) {
throw new Error("Failed to load handler");
}
return UNSTABLE_HANDLER_CACHE.balance({
input,
});
}),
});

View File

@ -1,4 +1,4 @@
import { RouterOutputs } from "@calcom/trpc/react";
import type { RouterOutputs } from "@calcom/trpc/react";
export const mockEvent: RouterOutputs["viewer"]["public"]["event"] = {
id: 1,

View File

@ -1,8 +1,9 @@
import React, { ComponentProps } from "react";
import type { ComponentProps } from "react";
import React from "react";
import Shell from "@calcom/features/shell/Shell";
import { HorizontalTabs } from "@calcom/ui";
import { VerticalTabItemProps, HorizontalTabItemProps } from "@calcom/ui";
import type { VerticalTabItemProps, HorizontalTabItemProps } from "@calcom/ui";
import { FiltersContainer } from "../components/FiltersContainer";

View File

@ -14,7 +14,7 @@ import { IS_PRODUCTION } from "@calcom/lib/constants";
import { getErrorFromUnknown } from "@calcom/lib/errors";
import { HttpError as HttpCode } from "@calcom/lib/http-error";
import { getTranslation } from "@calcom/lib/server/i18n";
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
import { prisma, bookingMinimalSelect } from "@calcom/prisma";
import type { CalendarEvent } from "@calcom/types/Calendar";
export const config = {

View File

@ -1,5 +1,5 @@
import type { Prisma } from "@prisma/client";
import crypto from "crypto";
import md5 from "md5";
import { z } from "zod";
import dayjs from "@calcom/dayjs";
@ -715,7 +715,7 @@ export const insightsRouter = router({
return {
userId: booking.userId,
user: userHashMap.get(booking.userId),
emailMd5: crypto.createHash("md5").update(user?.email).digest("hex"),
emailMd5: md5(user?.email),
count: booking._count.id,
};
});
@ -806,7 +806,7 @@ export const insightsRouter = router({
return {
userId: booking.userId,
user: userHashMap.get(booking.userId),
emailMd5: crypto.createHash("md5").update(user?.email).digest("hex"),
emailMd5: md5(user?.email),
count: booking._count.id,
};
});

View File

@ -1,4 +1,4 @@
export { useSchedule } from "./useSchedule";
export { useSlotsForDate } from "./useSlotsForDate";
export { useNonEmptyScheduleDays } from "./useNonEmptyScheduleDays";
export type { Slots } from "./types";
export type { Slots } from "./types";

View File

@ -1,3 +1,3 @@
import { RouterOutputs } from "@calcom/trpc/react";
import type { RouterOutputs } from "@calcom/trpc/react";
export type Slots = RouterOutputs["viewer"]["public"]["slots"]["getSchedule"]["slots"];

View File

@ -7,7 +7,8 @@ import { trpc } from "@calcom/trpc/react";
import { Meta, showToast, SkeletonContainer } from "@calcom/ui";
import { getLayout } from "../../settings/layouts/SettingsLayout";
import WebhookForm, { WebhookFormSubmitData } from "../components/WebhookForm";
import type { WebhookFormSubmitData } from "../components/WebhookForm";
import WebhookForm from "../components/WebhookForm";
const querySchema = z.object({ id: z.string() });

8
packages/lib/perf.ts Normal file
View File

@ -0,0 +1,8 @@
export const logP = (message: string) => {
const start = performance.now();
return () => {
const end = performance.now();
console.log(`[PERF]: ${message} took ${end - start}ms`);
};
};

View File

@ -1,6 +1,7 @@
import type { NextPageContext } from "next/types";
import superjson from "superjson";
import { httpBatchLink } from "../client/links/httpBatchLink";
import { httpLink } from "../client/links/httpLink";
import { loggerLink } from "../client/links/loggerLink";
import { splitLink } from "../client/links/splitLink";
@ -8,7 +9,6 @@ import { createTRPCNext } from "../next";
// Type-only import:
// https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-8.html#type-only-imports-and-export
import type { TRPCClientErrorLike } from "../react";
import { httpBatchLink } from "../react";
import type { inferRouterInputs, inferRouterOutputs, Maybe } from "../server";
import type { AppRouter } from "../server/routers/_app";

View File

@ -77,3 +77,5 @@ export const createContext = async (
res,
};
};
export type TRPCContext = Awaited<ReturnType<typeof createContext>>;

View File

@ -2,7 +2,7 @@
* This file contains the root router of your tRPC-backend
*/
import { router } from "../trpc";
import { viewerRouter } from "./viewer";
import { viewerRouter } from "./viewer/_router";
/**
* Create your application's root router

View File

@ -0,0 +1,369 @@
import { authedProcedure, router } from "../../trpc";
import { ZAppByIdInputSchema } from "./appById.schema";
import { ZAppCredentialsByTypeInputSchema } from "./appCredentialsByType.schema";
import { ZAppsInputSchema } from "./apps.schema";
import { ZAwayInputSchema } from "./away.schema";
import { ZDeleteCredentialInputSchema } from "./deleteCredential.schema";
import { ZDeleteMeInputSchema } from "./deleteMe.schema";
import { ZEventTypeOrderInputSchema } from "./eventTypeOrder.schema";
import { ZGetCalVideoRecordingsInputSchema } from "./getCalVideoRecordings.schema";
import { ZGetDownloadLinkOfCalVideoRecordingsInputSchema } from "./getDownloadLinkOfCalVideoRecordings.schema";
import { ZIntegrationsInputSchema } from "./integrations.schema";
import { ZSetDestinationCalendarInputSchema } from "./setDestinationCalendar.schema";
import { ZSubmitFeedbackInputSchema } from "./submitFeedback.schema";
import { ZUpdateProfileInputSchema } from "./updateProfile.schema";
import { ZUpdateUserDefaultConferencingAppInputSchema } from "./updateUserDefaultConferencingApp.schema";
type AppsRouterHandlerCache = {
me?: typeof import("./me.handler").meHandler;
avatar?: typeof import("./avatar.handler").avatarHandler;
deleteMe?: typeof import("./deleteMe.handler").deleteMeHandler;
deleteMeWithoutPassword?: typeof import("./deleteMeWithoutPassword.handler").deleteMeWithoutPasswordHandler;
away?: typeof import("./away.handler").awayHandler;
connectedCalendars?: typeof import("./connectedCalendars.handler").connectedCalendarsHandler;
setDestinationCalendar?: typeof import("./setDestinationCalendar.handler").setDestinationCalendarHandler;
integrations?: typeof import("./integrations.handler").integrationsHandler;
appById?: typeof import("./appById.handler").appByIdHandler;
apps?: typeof import("./apps.handler").appsHandler;
appCredentialsByType?: typeof import("./appCredentialsByType.handler").appCredentialsByTypeHandler;
stripeCustomer?: typeof import("./stripeCustomer.handler").stripeCustomerHandler;
updateProfile?: typeof import("./updateProfile.handler").updateProfileHandler;
eventTypeOrder?: typeof import("./eventTypeOrder.handler").eventTypeOrderHandler;
submitFeedback?: typeof import("./submitFeedback.handler").submitFeedbackHandler;
locationOptions?: typeof import("./locationOptions.handler").locationOptionsHandler;
deleteCredential?: typeof import("./deleteCredential.handler").deleteCredentialHandler;
bookingUnconfirmedCount?: typeof import("./bookingUnconfirmedCount.handler").bookingUnconfirmedCountHandler;
getCalVideoRecordings?: typeof import("./getCalVideoRecordings.handler").getCalVideoRecordingsHandler;
getDownloadLinkOfCalVideoRecordings?: typeof import("./getDownloadLinkOfCalVideoRecordings.handler").getDownloadLinkOfCalVideoRecordingsHandler;
getUsersDefaultConferencingApp?: typeof import("./getUsersDefaultConferencingApp.handler").getUsersDefaultConferencingAppHandler;
updateUserDefaultConferencingApp?: typeof import("./updateUserDefaultConferencingApp.handler").updateUserDefaultConferencingAppHandler;
};
const UNSTABLE_HANDLER_CACHE: AppsRouterHandlerCache = {};
export const loggedInViewerRouter = router({
me: authedProcedure.query(async ({ ctx }) => {
if (!UNSTABLE_HANDLER_CACHE.me) {
UNSTABLE_HANDLER_CACHE.me = (await import("./me.handler")).meHandler;
}
// Unreachable code but required for type safety
if (!UNSTABLE_HANDLER_CACHE.me) {
throw new Error("Failed to load handler");
}
return UNSTABLE_HANDLER_CACHE.me({ ctx });
}),
avatar: authedProcedure.query(async ({ ctx }) => {
if (!UNSTABLE_HANDLER_CACHE.avatar) {
UNSTABLE_HANDLER_CACHE.avatar = (await import("./avatar.handler")).avatarHandler;
}
// Unreachable code but required for type safety
if (!UNSTABLE_HANDLER_CACHE.avatar) {
throw new Error("Failed to load handler");
}
return UNSTABLE_HANDLER_CACHE.avatar({ ctx });
}),
deleteMe: authedProcedure.input(ZDeleteMeInputSchema).mutation(async ({ ctx, input }) => {
if (!UNSTABLE_HANDLER_CACHE.deleteMe) {
UNSTABLE_HANDLER_CACHE.deleteMe = (await import("./deleteMe.handler")).deleteMeHandler;
}
// Unreachable code but required for type safety
if (!UNSTABLE_HANDLER_CACHE.deleteMe) {
throw new Error("Failed to load handler");
}
return UNSTABLE_HANDLER_CACHE.deleteMe({ ctx, input });
}),
deleteMeWithoutPassword: authedProcedure.mutation(async ({ ctx }) => {
if (!UNSTABLE_HANDLER_CACHE.deleteMeWithoutPassword) {
UNSTABLE_HANDLER_CACHE.deleteMeWithoutPassword = (
await import("./deleteMeWithoutPassword.handler")
).deleteMeWithoutPasswordHandler;
}
// Unreachable code but required for type safety
if (!UNSTABLE_HANDLER_CACHE.deleteMeWithoutPassword) {
throw new Error("Failed to load handler");
}
return UNSTABLE_HANDLER_CACHE.deleteMeWithoutPassword({ ctx });
}),
away: authedProcedure.input(ZAwayInputSchema).mutation(async ({ ctx, input }) => {
if (!UNSTABLE_HANDLER_CACHE.away) {
UNSTABLE_HANDLER_CACHE.away = (await import("./away.handler")).awayHandler;
}
// Unreachable code but required for type safety
if (!UNSTABLE_HANDLER_CACHE.away) {
throw new Error("Failed to load handler");
}
return UNSTABLE_HANDLER_CACHE.away({ ctx, input });
}),
connectedCalendars: authedProcedure.query(async ({ ctx }) => {
if (!UNSTABLE_HANDLER_CACHE.connectedCalendars) {
UNSTABLE_HANDLER_CACHE.connectedCalendars = (
await import("./connectedCalendars.handler")
).connectedCalendarsHandler;
}
// Unreachable code but required for type safety
if (!UNSTABLE_HANDLER_CACHE.connectedCalendars) {
throw new Error("Failed to load handler");
}
return UNSTABLE_HANDLER_CACHE.connectedCalendars({ ctx });
}),
setDestinationCalendar: authedProcedure
.input(ZSetDestinationCalendarInputSchema)
.mutation(async ({ ctx, input }) => {
if (!UNSTABLE_HANDLER_CACHE.setDestinationCalendar) {
UNSTABLE_HANDLER_CACHE.setDestinationCalendar = (
await import("./setDestinationCalendar.handler")
).setDestinationCalendarHandler;
}
// Unreachable code but required for type safety
if (!UNSTABLE_HANDLER_CACHE.setDestinationCalendar) {
throw new Error("Failed to load handler");
}
return UNSTABLE_HANDLER_CACHE.setDestinationCalendar({ ctx, input });
}),
integrations: authedProcedure.input(ZIntegrationsInputSchema).query(async ({ ctx, input }) => {
if (!UNSTABLE_HANDLER_CACHE.integrations) {
UNSTABLE_HANDLER_CACHE.integrations = (await import("./integrations.handler")).integrationsHandler;
}
// Unreachable code but required for type safety
if (!UNSTABLE_HANDLER_CACHE.integrations) {
throw new Error("Failed to load handler");
}
return UNSTABLE_HANDLER_CACHE.integrations({ ctx, input });
}),
appById: authedProcedure.input(ZAppByIdInputSchema).query(async ({ ctx, input }) => {
if (!UNSTABLE_HANDLER_CACHE.appById) {
UNSTABLE_HANDLER_CACHE.appById = (await import("./appById.handler")).appByIdHandler;
}
// Unreachable code but required for type safety
if (!UNSTABLE_HANDLER_CACHE.appById) {
throw new Error("Failed to load handler");
}
return UNSTABLE_HANDLER_CACHE.appById({ ctx, input });
}),
apps: authedProcedure.input(ZAppsInputSchema).query(async ({ ctx, input }) => {
if (!UNSTABLE_HANDLER_CACHE.apps) {
UNSTABLE_HANDLER_CACHE.apps = (await import("./apps.handler")).appsHandler;
}
// Unreachable code but required for type safety
if (!UNSTABLE_HANDLER_CACHE.apps) {
throw new Error("Failed to load handler");
}
return UNSTABLE_HANDLER_CACHE.apps({ ctx, input });
}),
appCredentialsByType: authedProcedure
.input(ZAppCredentialsByTypeInputSchema)
.query(async ({ ctx, input }) => {
if (!UNSTABLE_HANDLER_CACHE.appCredentialsByType) {
UNSTABLE_HANDLER_CACHE.appCredentialsByType = (
await import("./appCredentialsByType.handler")
).appCredentialsByTypeHandler;
}
// Unreachable code but required for type safety
if (!UNSTABLE_HANDLER_CACHE.appCredentialsByType) {
throw new Error("Failed to load handler");
}
return UNSTABLE_HANDLER_CACHE.appCredentialsByType({ ctx, input });
}),
stripeCustomer: authedProcedure.query(async ({ ctx }) => {
if (!UNSTABLE_HANDLER_CACHE.stripeCustomer) {
UNSTABLE_HANDLER_CACHE.stripeCustomer = (
await import("./stripeCustomer.handler")
).stripeCustomerHandler;
}
// Unreachable code but required for type safety
if (!UNSTABLE_HANDLER_CACHE.stripeCustomer) {
throw new Error("Failed to load handler");
}
return UNSTABLE_HANDLER_CACHE.stripeCustomer({ ctx });
}),
updateProfile: authedProcedure.input(ZUpdateProfileInputSchema).mutation(async ({ ctx, input }) => {
if (!UNSTABLE_HANDLER_CACHE.updateProfile) {
UNSTABLE_HANDLER_CACHE.updateProfile = (await import("./updateProfile.handler")).updateProfileHandler;
}
// Unreachable code but required for type safety
if (!UNSTABLE_HANDLER_CACHE.updateProfile) {
throw new Error("Failed to load handler");
}
return UNSTABLE_HANDLER_CACHE.updateProfile({ ctx, input });
}),
eventTypeOrder: authedProcedure.input(ZEventTypeOrderInputSchema).mutation(async ({ ctx, input }) => {
if (!UNSTABLE_HANDLER_CACHE.eventTypeOrder) {
UNSTABLE_HANDLER_CACHE.eventTypeOrder = (
await import("./eventTypeOrder.handler")
).eventTypeOrderHandler;
}
// Unreachable code but required for type safety
if (!UNSTABLE_HANDLER_CACHE.eventTypeOrder) {
throw new Error("Failed to load handler");
}
return UNSTABLE_HANDLER_CACHE.eventTypeOrder({ ctx, input });
}),
//Comment for PR: eventTypePosition is not used anywhere
submitFeedback: authedProcedure.input(ZSubmitFeedbackInputSchema).mutation(async ({ ctx, input }) => {
if (!UNSTABLE_HANDLER_CACHE.submitFeedback) {
UNSTABLE_HANDLER_CACHE.submitFeedback = (
await import("./submitFeedback.handler")
).submitFeedbackHandler;
}
// Unreachable code but required for type safety
if (!UNSTABLE_HANDLER_CACHE.submitFeedback) {
throw new Error("Failed to load handler");
}
return UNSTABLE_HANDLER_CACHE.submitFeedback({ ctx, input });
}),
locationOptions: authedProcedure.query(async ({ ctx }) => {
if (!UNSTABLE_HANDLER_CACHE.locationOptions) {
UNSTABLE_HANDLER_CACHE.locationOptions = (
await import("./locationOptions.handler")
).locationOptionsHandler;
}
// Unreachable code but required for type safety
if (!UNSTABLE_HANDLER_CACHE.locationOptions) {
throw new Error("Failed to load handler");
}
return UNSTABLE_HANDLER_CACHE.locationOptions({ ctx });
}),
deleteCredential: authedProcedure.input(ZDeleteCredentialInputSchema).mutation(async ({ ctx, input }) => {
if (!UNSTABLE_HANDLER_CACHE.deleteCredential) {
UNSTABLE_HANDLER_CACHE.deleteCredential = (
await import("./deleteCredential.handler")
).deleteCredentialHandler;
}
// Unreachable code but required for type safety
if (!UNSTABLE_HANDLER_CACHE.deleteCredential) {
throw new Error("Failed to load handler");
}
return UNSTABLE_HANDLER_CACHE.deleteCredential({ ctx, input });
}),
bookingUnconfirmedCount: authedProcedure.query(async ({ ctx }) => {
if (!UNSTABLE_HANDLER_CACHE.bookingUnconfirmedCount) {
UNSTABLE_HANDLER_CACHE.bookingUnconfirmedCount = (
await import("./bookingUnconfirmedCount.handler")
).bookingUnconfirmedCountHandler;
}
// Unreachable code but required for type safety
if (!UNSTABLE_HANDLER_CACHE.bookingUnconfirmedCount) {
throw new Error("Failed to load handler");
}
return UNSTABLE_HANDLER_CACHE.bookingUnconfirmedCount({ ctx });
}),
getCalVideoRecordings: authedProcedure
.input(ZGetCalVideoRecordingsInputSchema)
.query(async ({ ctx, input }) => {
if (!UNSTABLE_HANDLER_CACHE.getCalVideoRecordings) {
UNSTABLE_HANDLER_CACHE.getCalVideoRecordings = (
await import("./getCalVideoRecordings.handler")
).getCalVideoRecordingsHandler;
}
// Unreachable code but required for type safety
if (!UNSTABLE_HANDLER_CACHE.getCalVideoRecordings) {
throw new Error("Failed to load handler");
}
return UNSTABLE_HANDLER_CACHE.getCalVideoRecordings({ ctx, input });
}),
getDownloadLinkOfCalVideoRecordings: authedProcedure
.input(ZGetDownloadLinkOfCalVideoRecordingsInputSchema)
.query(async ({ ctx, input }) => {
if (!UNSTABLE_HANDLER_CACHE.getDownloadLinkOfCalVideoRecordings) {
UNSTABLE_HANDLER_CACHE.getDownloadLinkOfCalVideoRecordings = (
await import("./getDownloadLinkOfCalVideoRecordings.handler")
).getDownloadLinkOfCalVideoRecordingsHandler;
}
// Unreachable code but required for type safety
if (!UNSTABLE_HANDLER_CACHE.getDownloadLinkOfCalVideoRecordings) {
throw new Error("Failed to load handler");
}
return UNSTABLE_HANDLER_CACHE.getDownloadLinkOfCalVideoRecordings({ ctx, input });
}),
getUsersDefaultConferencingApp: authedProcedure.query(async ({ ctx }) => {
if (!UNSTABLE_HANDLER_CACHE.getUsersDefaultConferencingApp) {
UNSTABLE_HANDLER_CACHE.getUsersDefaultConferencingApp = (
await import("./getUsersDefaultConferencingApp.handler")
).getUsersDefaultConferencingAppHandler;
}
// Unreachable code but required for type safety
if (!UNSTABLE_HANDLER_CACHE.getUsersDefaultConferencingApp) {
throw new Error("Failed to load handler");
}
return UNSTABLE_HANDLER_CACHE.getUsersDefaultConferencingApp({ ctx });
}),
updateUserDefaultConferencingApp: authedProcedure
.input(ZUpdateUserDefaultConferencingAppInputSchema)
.mutation(async ({ ctx, input }) => {
if (!UNSTABLE_HANDLER_CACHE.updateUserDefaultConferencingApp) {
UNSTABLE_HANDLER_CACHE.updateUserDefaultConferencingApp = (
await import("./updateUserDefaultConferencingApp.handler")
).updateUserDefaultConferencingAppHandler;
}
// Unreachable code but required for type safety
if (!UNSTABLE_HANDLER_CACHE.updateUserDefaultConferencingApp) {
throw new Error("Failed to load handler");
}
return UNSTABLE_HANDLER_CACHE.updateUserDefaultConferencingApp({ ctx, input });
}),
});

View File

@ -0,0 +1,31 @@
import getApps from "@calcom/app-store/utils";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
import { TRPCError } from "@trpc/server";
import type { TAppByIdInputSchema } from "./appById.schema";
type AppByIdOptions = {
ctx: {
user: NonNullable<TrpcSessionUser>;
};
input: TAppByIdInputSchema;
};
export const appByIdHandler = async ({ ctx, input }: AppByIdOptions) => {
const { user } = ctx;
const appId = input.appId;
const { credentials } = user;
const apps = getApps(credentials);
const appFromDb = apps.find((app) => app.slug === appId);
if (!appFromDb) {
throw new TRPCError({ code: "BAD_REQUEST", message: `Could not find app ${appId}` });
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { credential: _, credentials: _1, ...app } = appFromDb;
return {
isInstalled: appFromDb.credentials.length,
...app,
};
};

View File

@ -0,0 +1,7 @@
import { z } from "zod";
export const ZAppByIdInputSchema = z.object({
appId: z.string(),
});
export type TAppByIdInputSchema = z.infer<typeof ZAppByIdInputSchema>;

View File

@ -0,0 +1,15 @@
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
import type { TAppCredentialsByTypeInputSchema } from "./appCredentialsByType.schema";
type AppCredentialsByTypeOptions = {
ctx: {
user: NonNullable<TrpcSessionUser>;
};
input: TAppCredentialsByTypeInputSchema;
};
export const appCredentialsByTypeHandler = async ({ ctx, input }: AppCredentialsByTypeOptions) => {
const { user } = ctx;
return user.credentials.filter((app) => app.type == input.appType).map((credential) => credential.id);
};

View File

@ -0,0 +1,7 @@
import { z } from "zod";
export const ZAppCredentialsByTypeInputSchema = z.object({
appType: z.string(),
});
export type TAppCredentialsByTypeInputSchema = z.infer<typeof ZAppCredentialsByTypeInputSchema>;

View File

@ -0,0 +1,24 @@
import getEnabledApps from "@calcom/lib/apps/getEnabledApps";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
import type { TAppsInputSchema } from "./apps.schema";
type AppsOptions = {
ctx: {
user: NonNullable<TrpcSessionUser>;
};
input: TAppsInputSchema;
};
export const appsHandler = async ({ ctx, input }: AppsOptions) => {
const { user } = ctx;
const { credentials } = user;
const apps = await getEnabledApps(credentials);
return apps
.filter((app) => app.extendsFeature?.includes(input.extendsFeature))
.map((app) => ({
...app,
isInstalled: !!app.credentials?.length,
}));
};

View File

@ -0,0 +1,7 @@
import { z } from "zod";
export const ZAppsInputSchema = z.object({
extendsFeature: z.literal("EventType"),
});
export type TAppsInputSchema = z.infer<typeof ZAppsInputSchema>;

View File

@ -0,0 +1,13 @@
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
type AvatarOptions = {
ctx: {
user: NonNullable<TrpcSessionUser>;
};
};
export const avatarHandler = async ({ ctx }: AvatarOptions) => {
return {
avatar: ctx.user.rawAvatar,
};
};

View File

@ -0,0 +1 @@
export {};

View File

@ -0,0 +1,22 @@
import { prisma } from "@calcom/prisma";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
import type { TAwayInputSchema } from "./away.schema";
type AwayOptions = {
ctx: {
user: NonNullable<TrpcSessionUser>;
};
input: TAwayInputSchema;
};
export const awayHandler = async ({ ctx, input }: AwayOptions) => {
await prisma.user.update({
where: {
email: ctx.user.email,
},
data: {
away: input.away,
},
});
};

View File

@ -0,0 +1,7 @@
import { z } from "zod";
export const ZAwayInputSchema = z.object({
away: z.boolean(),
});
export type TAwayInputSchema = z.infer<typeof ZAwayInputSchema>;

View File

@ -0,0 +1,38 @@
import { BookingStatus } from "@prisma/client";
import { prisma } from "@calcom/prisma";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
type BookingUnconfirmedCountOptions = {
ctx: {
user: NonNullable<TrpcSessionUser>;
};
};
export const bookingUnconfirmedCountHandler = async ({ ctx }: BookingUnconfirmedCountOptions) => {
const { user } = ctx;
const count = await prisma.booking.count({
where: {
status: BookingStatus.PENDING,
userId: user.id,
endTime: { gt: new Date() },
},
});
const recurringGrouping = await prisma.booking.groupBy({
by: ["recurringEventId"],
_count: {
recurringEventId: true,
},
where: {
recurringEventId: { not: { equals: null } },
status: { equals: "PENDING" },
userId: user.id,
endTime: { gt: new Date() },
},
});
return recurringGrouping.reduce((prev, current) => {
// recurringEventId is the total number of recurring instances for a booking
// we need to subtract all but one, to represent a single recurring booking
return prev - (current._count?.recurringEventId - 1);
}, count);
};

View File

@ -0,0 +1,90 @@
import type { DestinationCalendar } from "@prisma/client";
import { AppCategories } from "@prisma/client";
import { getCalendarCredentials, getConnectedCalendars } from "@calcom/core/CalendarManager";
import { prisma } from "@calcom/prisma";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
type ConnectedCalendarsOptions = {
ctx: {
user: NonNullable<TrpcSessionUser>;
};
};
export const connectedCalendarsHandler = async ({ ctx }: ConnectedCalendarsOptions) => {
const { user } = ctx;
const userCredentials = await prisma.credential.findMany({
where: {
userId: ctx.user.id,
app: {
categories: { has: AppCategories.calendar },
enabled: true,
},
},
});
// get user's credentials + their connected integrations
const calendarCredentials = getCalendarCredentials(userCredentials);
// get all the connected integrations' calendars (from third party)
const { connectedCalendars, destinationCalendar } = await getConnectedCalendars(
calendarCredentials,
user.selectedCalendars,
user.destinationCalendar?.externalId
);
if (connectedCalendars.length === 0) {
/* As there are no connected calendars, delete the destination calendar if it exists */
if (user.destinationCalendar) {
await prisma.destinationCalendar.delete({
where: { userId: user.id },
});
user.destinationCalendar = null;
}
} else if (!user.destinationCalendar) {
/*
There are connected calendars, but no destination calendar
So create a default destination calendar with the first primary connected calendar
*/
const { integration = "", externalId = "", credentialId } = connectedCalendars[0].primary ?? {};
user.destinationCalendar = await prisma.destinationCalendar.create({
data: {
userId: user.id,
integration,
externalId,
credentialId,
},
});
} else {
/* There are connected calendars and a destination calendar */
// Check if destinationCalendar exists in connectedCalendars
const allCals = connectedCalendars.map((cal) => cal.calendars ?? []).flat();
const destinationCal = allCals.find(
(cal) =>
cal.externalId === user.destinationCalendar?.externalId &&
cal.integration === user.destinationCalendar?.integration
);
if (!destinationCal) {
// If destinationCalendar is out of date, update it with the first primary connected calendar
const { integration = "", externalId = "" } = connectedCalendars[0].primary ?? {};
user.destinationCalendar = await prisma.destinationCalendar.update({
where: { userId: user.id },
data: {
integration,
externalId,
},
});
}
}
return {
connectedCalendars,
destinationCalendar: {
...(user.destinationCalendar as DestinationCalendar),
...destinationCalendar,
},
};
};

View File

@ -0,0 +1 @@
export {};

View File

@ -0,0 +1,345 @@
import { AppCategories, BookingStatus } from "@prisma/client";
import z from "zod";
import { cancelScheduledJobs } from "@calcom/app-store/zapier/lib/nodeScheduler";
import { DailyLocationType } from "@calcom/core/location";
import { sendCancelledEmails } from "@calcom/emails";
import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses";
import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib";
import { WEBAPP_URL } from "@calcom/lib/constants";
import getPaymentAppData from "@calcom/lib/getPaymentAppData";
import { deletePayment } from "@calcom/lib/payment/deletePayment";
import { getTranslation } from "@calcom/lib/server/i18n";
import { bookingMinimalSelect } from "@calcom/prisma";
import { prisma } from "@calcom/prisma";
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
import { TRPCError } from "@trpc/server";
import type { TDeleteCredentialInputSchema } from "./deleteCredential.schema";
type DeleteCredentialOptions = {
ctx: {
user: NonNullable<TrpcSessionUser>;
};
input: TDeleteCredentialInputSchema;
};
export const deleteCredentialHandler = async ({ ctx, input }: DeleteCredentialOptions) => {
const { id, externalId } = input;
const credential = await prisma.credential.findFirst({
where: {
id: id,
userId: ctx.user.id,
},
select: {
key: true,
appId: true,
app: {
select: {
slug: true,
categories: true,
dirName: true,
},
},
},
});
if (!credential) {
throw new TRPCError({ code: "NOT_FOUND" });
}
const eventTypes = await prisma.eventType.findMany({
where: {
userId: ctx.user.id,
},
select: {
id: true,
locations: true,
destinationCalendar: {
include: {
credential: true,
},
},
price: true,
currency: true,
metadata: true,
},
});
// TODO: Improve this uninstallation cleanup per event by keeping a relation of EventType to App which has the data.
for (const eventType of eventTypes) {
if (eventType.locations) {
// If it's a video, replace the location with Cal video
if (credential.app?.categories.includes(AppCategories.video)) {
// Find the user's event types
// Look for integration name from app slug
const integrationQuery =
credential.app?.slug === "msteams" ? "office365_video" : credential.app?.slug.split("-")[0];
// Check if the event type uses the deleted integration
// To avoid type errors, need to stringify and parse JSON to use array methods
const locationsSchema = z.array(z.object({ type: z.string() }));
const locations = locationsSchema.parse(eventType.locations);
const updatedLocations = locations.map((location: { type: string }) => {
if (location.type.includes(integrationQuery)) {
return { type: DailyLocationType };
}
return location;
});
await prisma.eventType.update({
where: {
id: eventType.id,
},
data: {
locations: updatedLocations,
},
});
}
}
// If it's a calendar, remove the destination calendar from the event type
if (credential.app?.categories.includes(AppCategories.calendar)) {
if (eventType.destinationCalendar?.credential?.appId === credential.appId) {
const destinationCalendar = await prisma.destinationCalendar.findFirst({
where: {
id: eventType.destinationCalendar?.id,
},
});
if (destinationCalendar) {
await prisma.destinationCalendar.delete({
where: {
id: destinationCalendar.id,
},
});
}
}
if (externalId) {
const existingSelectedCalendar = await prisma.selectedCalendar.findFirst({
where: {
externalId: externalId,
},
});
// @TODO: SelectedCalendar doesn't have unique ID so we should only delete one item
if (existingSelectedCalendar) {
await prisma.selectedCalendar.delete({
where: {
userId_integration_externalId: {
userId: existingSelectedCalendar.userId,
externalId: existingSelectedCalendar.externalId,
integration: existingSelectedCalendar.integration,
},
},
});
}
}
}
const metadata = EventTypeMetaDataSchema.parse(eventType.metadata);
const stripeAppData = getPaymentAppData({ ...eventType, metadata });
// If it's a payment, hide the event type and set the price to 0. Also cancel all pending bookings
if (credential.app?.categories.includes(AppCategories.payment)) {
if (stripeAppData.price) {
await prisma.$transaction(async () => {
await prisma.eventType.update({
where: {
id: eventType.id,
},
data: {
hidden: true,
metadata: {
...metadata,
apps: {
...metadata?.apps,
stripe: {
...metadata?.apps?.stripe,
price: 0,
},
},
},
},
});
// Assuming that all bookings under this eventType need to be paid
const unpaidBookings = await prisma.booking.findMany({
where: {
userId: ctx.user.id,
eventTypeId: eventType.id,
status: "PENDING",
paid: false,
payment: {
every: {
success: false,
},
},
},
select: {
...bookingMinimalSelect,
recurringEventId: true,
userId: true,
responses: true,
user: {
select: {
id: true,
credentials: true,
email: true,
timeZone: true,
name: true,
destinationCalendar: true,
locale: true,
},
},
location: true,
references: {
select: {
uid: true,
type: true,
externalCalendarId: true,
},
},
payment: true,
paid: true,
eventType: {
select: {
recurringEvent: true,
title: true,
bookingFields: true,
seatsPerTimeSlot: true,
seatsShowAttendees: true,
},
},
uid: true,
eventTypeId: true,
destinationCalendar: true,
},
});
for (const booking of unpaidBookings) {
await prisma.booking.update({
where: {
id: booking.id,
},
data: {
status: BookingStatus.CANCELLED,
cancellationReason: "Payment method removed",
},
});
for (const payment of booking.payment) {
try {
await deletePayment(payment.id, credential);
} catch (e) {
console.error(e);
}
await prisma.payment.delete({
where: {
id: payment.id,
},
});
}
await prisma.attendee.deleteMany({
where: {
bookingId: booking.id,
},
});
await prisma.bookingReference.deleteMany({
where: {
bookingId: booking.id,
},
});
const attendeesListPromises = booking.attendees.map(async (attendee) => {
return {
name: attendee.name,
email: attendee.email,
timeZone: attendee.timeZone,
language: {
translate: await getTranslation(attendee.locale ?? "en", "common"),
locale: attendee.locale ?? "en",
},
};
});
const attendeesList = await Promise.all(attendeesListPromises);
const tOrganizer = await getTranslation(booking?.user?.locale ?? "en", "common");
await sendCancelledEmails({
type: booking?.eventType?.title as string,
title: booking.title,
description: booking.description,
customInputs: isPrismaObjOrUndefined(booking.customInputs),
...getCalEventResponses({
bookingFields: booking.eventType?.bookingFields ?? null,
booking,
}),
startTime: booking.startTime.toISOString(),
endTime: booking.endTime.toISOString(),
organizer: {
email: booking?.user?.email as string,
name: booking?.user?.name ?? "Nameless",
timeZone: booking?.user?.timeZone as string,
language: { translate: tOrganizer, locale: booking?.user?.locale ?? "en" },
},
attendees: attendeesList,
uid: booking.uid,
recurringEvent: parseRecurringEvent(booking.eventType?.recurringEvent),
location: booking.location,
destinationCalendar: booking.destinationCalendar || booking.user?.destinationCalendar,
cancellationReason: "Payment method removed by organizer",
seatsPerTimeSlot: booking.eventType?.seatsPerTimeSlot,
seatsShowAttendees: booking.eventType?.seatsShowAttendees,
});
}
});
}
}
}
// if zapier get disconnected, delete zapier apiKey, delete zapier webhooks and cancel all scheduled jobs from zapier
if (credential.app?.slug === "zapier") {
await prisma.apiKey.deleteMany({
where: {
userId: ctx.user.id,
appId: "zapier",
},
});
await prisma.webhook.deleteMany({
where: {
userId: ctx.user.id,
appId: "zapier",
},
});
const bookingsWithScheduledJobs = await prisma.booking.findMany({
where: {
userId: ctx.user.id,
scheduledJobs: {
isEmpty: false,
},
},
});
for (const booking of bookingsWithScheduledJobs) {
cancelScheduledJobs(booking, credential.appId);
}
}
// Validated that credential is user's above
await prisma.credential.delete({
where: {
id: id,
},
});
// Revalidate user calendar cache.
if (credential.app?.slug.includes("calendar")) {
await fetch(`${WEBAPP_URL}/api/revalidate-calendar-cache/${ctx?.user?.username}`);
}
};

View File

@ -0,0 +1,8 @@
import { z } from "zod";
export const ZDeleteCredentialInputSchema = z.object({
id: z.number(),
externalId: z.string().optional(),
});
export type TDeleteCredentialInputSchema = z.infer<typeof ZDeleteCredentialInputSchema>;

View File

@ -0,0 +1,87 @@
import { IdentityProvider } from "@prisma/client";
import { authenticator } from "otplib";
import { deleteStripeCustomer } from "@calcom/app-store/stripepayment/lib/customer";
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
import { verifyPassword } from "@calcom/features/auth/lib/verifyPassword";
import { symmetricDecrypt } from "@calcom/lib/crypto";
import { deleteWebUser as syncServicesDeleteWebUser } from "@calcom/lib/sync/SyncServiceManager";
import { prisma } from "@calcom/prisma";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
import type { TDeleteMeInputSchema } from "./deleteMe.schema";
type DeleteMeOptions = {
ctx: {
user: NonNullable<TrpcSessionUser>;
};
input: TDeleteMeInputSchema;
};
export const deleteMeHandler = async ({ ctx, input }: DeleteMeOptions) => {
// Check if input.password is correct
const user = await prisma.user.findUnique({
where: {
email: ctx.user.email.toLowerCase(),
},
});
if (!user) {
throw new Error(ErrorCode.UserNotFound);
}
if (user.identityProvider !== IdentityProvider.CAL) {
throw new Error(ErrorCode.ThirdPartyIdentityProviderEnabled);
}
if (!user.password) {
throw new Error(ErrorCode.UserMissingPassword);
}
const isCorrectPassword = await verifyPassword(input.password, user.password);
if (!isCorrectPassword) {
throw new Error(ErrorCode.IncorrectPassword);
}
if (user.twoFactorEnabled) {
if (!input.totpCode) {
throw new Error(ErrorCode.SecondFactorRequired);
}
if (!user.twoFactorSecret) {
console.error(`Two factor is enabled for user ${user.id} but they have no secret`);
throw new Error(ErrorCode.InternalServerError);
}
if (!process.env.CALENDSO_ENCRYPTION_KEY) {
console.error(`"Missing encryption key; cannot proceed with two factor login."`);
throw new Error(ErrorCode.InternalServerError);
}
const secret = symmetricDecrypt(user.twoFactorSecret, process.env.CALENDSO_ENCRYPTION_KEY);
if (secret.length !== 32) {
console.error(
`Two factor secret decryption failed. Expected key with length 32 but got ${secret.length}`
);
throw new Error(ErrorCode.InternalServerError);
}
// If user has 2fa enabled, check if input.totpCode is correct
const isValidToken = authenticator.check(input.totpCode, secret);
if (!isValidToken) {
throw new Error(ErrorCode.IncorrectTwoFactorCode);
}
}
// If 2FA is disabled or totpCode is valid then delete the user from stripe and database
await deleteStripeCustomer(user).catch(console.warn);
// Remove my account
const deletedUser = await prisma.user.delete({
where: {
id: ctx.user.id,
},
});
// Sync Services
syncServicesDeleteWebUser(deletedUser);
return;
};

View File

@ -0,0 +1,8 @@
import { z } from "zod";
export const ZDeleteMeInputSchema = z.object({
password: z.string(),
totpCode: z.string().optional(),
});
export type TDeleteMeInputSchema = z.infer<typeof ZDeleteMeInputSchema>;

View File

@ -0,0 +1,46 @@
import { IdentityProvider } from "@prisma/client";
import { deleteStripeCustomer } from "@calcom/app-store/stripepayment/lib/customer";
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
import { deleteWebUser as syncServicesDeleteWebUser } from "@calcom/lib/sync/SyncServiceManager";
import { prisma } from "@calcom/prisma";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
type DeleteMeWithoutPasswordOptions = {
ctx: {
user: NonNullable<TrpcSessionUser>;
};
};
export const deleteMeWithoutPasswordHandler = async ({ ctx }: DeleteMeWithoutPasswordOptions) => {
const user = await prisma.user.findUnique({
where: {
email: ctx.user.email.toLowerCase(),
},
});
if (!user) {
throw new Error(ErrorCode.UserNotFound);
}
if (user.identityProvider === IdentityProvider.CAL) {
throw new Error(ErrorCode.SocialIdentityProviderRequired);
}
if (user.twoFactorEnabled) {
throw new Error(ErrorCode.SocialIdentityProviderRequired);
}
// Remove me from Stripe
await deleteStripeCustomer(user).catch(console.warn);
// Remove my account
const deletedUser = await prisma.user.delete({
where: {
id: ctx.user.id,
},
});
// Sync Services
syncServicesDeleteWebUser(deletedUser);
return;
};

View File

@ -0,0 +1,69 @@
import { reverse } from "lodash";
import { prisma } from "@calcom/prisma";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
import { TRPCError } from "@trpc/server";
import type { TEventTypeOrderInputSchema } from "./eventTypeOrder.schema";
type EventTypeOrderOptions = {
ctx: {
user: NonNullable<TrpcSessionUser>;
};
input: TEventTypeOrderInputSchema;
};
export const eventTypeOrderHandler = async ({ ctx, input }: EventTypeOrderOptions) => {
const { user } = ctx;
const allEventTypes = await prisma.eventType.findMany({
select: {
id: true,
},
where: {
id: {
in: input.ids,
},
OR: [
{
userId: user.id,
},
{
users: {
some: {
id: user.id,
},
},
},
{
team: {
members: {
some: {
userId: user.id,
},
},
},
},
],
},
});
const allEventTypeIds = new Set(allEventTypes.map((type) => type.id));
if (input.ids.some((id) => !allEventTypeIds.has(id))) {
throw new TRPCError({
code: "UNAUTHORIZED",
});
}
await Promise.all(
reverse(input.ids).map((id, position) => {
return prisma.eventType.update({
where: {
id,
},
data: {
position,
},
});
})
);
};

View File

@ -0,0 +1,7 @@
import { z } from "zod";
export const ZEventTypeOrderInputSchema = z.object({
ids: z.array(z.number()),
});
export type TEventTypeOrderInputSchema = z.infer<typeof ZEventTypeOrderInputSchema>;

View File

@ -0,0 +1,26 @@
import { getRecordingsOfCalVideoByRoomName } from "@calcom/core/videoClient";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
import { TRPCError } from "@trpc/server";
import type { TGetCalVideoRecordingsInputSchema } from "./getCalVideoRecordings.schema";
type GetCalVideoRecordingsOptions = {
ctx: {
user: NonNullable<TrpcSessionUser>;
};
input: TGetCalVideoRecordingsInputSchema;
};
export const getCalVideoRecordingsHandler = async ({ ctx: _ctx, input }: GetCalVideoRecordingsOptions) => {
const { roomName } = input;
try {
const res = await getRecordingsOfCalVideoByRoomName(roomName);
return res;
} catch (err) {
throw new TRPCError({
code: "BAD_REQUEST",
});
}
};

View File

@ -0,0 +1,7 @@
import { z } from "zod";
export const ZGetCalVideoRecordingsInputSchema = z.object({
roomName: z.string(),
});
export type TGetCalVideoRecordingsInputSchema = z.infer<typeof ZGetCalVideoRecordingsInputSchema>;

View File

@ -0,0 +1,38 @@
/// <reference types="@calcom/types/next-auth" />
import { getDownloadLinkOfCalVideoByRecordingId } from "@calcom/core/videoClient";
import { IS_SELF_HOSTED } from "@calcom/lib/constants";
import { TRPCError } from "@trpc/server";
import type { CreateInnerContextOptions } from "../../createContext";
import type { TGetDownloadLinkOfCalVideoRecordingsInputSchema } from "./getDownloadLinkOfCalVideoRecordings.schema";
type GetDownloadLinkOfCalVideoRecordingsHandlerOptions = {
ctx: CreateInnerContextOptions;
input: TGetDownloadLinkOfCalVideoRecordingsInputSchema;
};
export const getDownloadLinkOfCalVideoRecordingsHandler = async ({
input,
ctx,
}: GetDownloadLinkOfCalVideoRecordingsHandlerOptions) => {
const { recordingId } = input;
const { session } = ctx;
const isDownloadAllowed = IS_SELF_HOSTED || session?.user?.belongsToActiveTeam;
if (!isDownloadAllowed) {
throw new TRPCError({
code: "FORBIDDEN",
});
}
try {
const res = await getDownloadLinkOfCalVideoByRecordingId(recordingId);
return res;
} catch (err) {
throw new TRPCError({
code: "BAD_REQUEST",
});
}
};

View File

@ -0,0 +1,9 @@
import { z } from "zod";
export const ZGetDownloadLinkOfCalVideoRecordingsInputSchema = z.object({
recordingId: z.string(),
});
export type TGetDownloadLinkOfCalVideoRecordingsInputSchema = z.infer<
typeof ZGetDownloadLinkOfCalVideoRecordingsInputSchema
>;

View File

@ -0,0 +1,14 @@
import { userMetadata } from "@calcom/prisma/zod-utils";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
type GetUsersDefaultConferencingAppOptions = {
ctx: {
user: NonNullable<TrpcSessionUser>;
};
};
export const getUsersDefaultConferencingAppHandler = async ({
ctx,
}: GetUsersDefaultConferencingAppOptions) => {
return userMetadata.parse(ctx.user.metadata)?.defaultConferencingApp;
};

View File

@ -0,0 +1,52 @@
import getEnabledApps from "@calcom/lib/apps/getEnabledApps";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
import type { TIntegrationsInputSchema } from "./integrations.schema";
type IntegrationsOptions = {
ctx: {
user: NonNullable<TrpcSessionUser>;
};
input: TIntegrationsInputSchema;
};
export const integrationsHandler = async ({ ctx, input }: IntegrationsOptions) => {
const { user } = ctx;
const { variant, exclude, onlyInstalled } = input;
const { credentials } = user;
const enabledApps = await getEnabledApps(credentials);
//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 }) => {
const credentialIds = credentials.filter((c) => c.type === app.type).map((c) => c.id);
const invalidCredentialIds = credentials
.filter((c) => c.type === app.type && c.invalid)
.map((c) => c.id);
return {
...app,
credentialIds,
invalidCredentialIds,
};
}
);
if (variant) {
// `flatMap()` these work like `.filter()` but infers the types correctly
apps = apps
// variant check
.flatMap((item) => (item.variant.startsWith(variant) ? [item] : []));
}
if (exclude) {
// exclusion filter
apps = apps.filter((item) => (exclude ? !exclude.includes(item.variant) : true));
}
if (onlyInstalled) {
apps = apps.flatMap((item) => (item.credentialIds.length > 0 || item.isGlobal ? [item] : []));
}
return {
items: apps,
};
};

View File

@ -0,0 +1,9 @@
import { z } from "zod";
export const ZIntegrationsInputSchema = z.object({
variant: z.string().optional(),
exclude: z.array(z.string()).optional(),
onlyInstalled: z.boolean().optional(),
});
export type TIntegrationsInputSchema = z.infer<typeof ZIntegrationsInputSchema>;

View File

@ -0,0 +1,35 @@
import { getLocationGroupedOptions } from "@calcom/app-store/utils";
import getEnabledApps from "@calcom/lib/apps/getEnabledApps";
import { getTranslation } from "@calcom/lib/server/i18n";
import { prisma } from "@calcom/prisma";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
type LocationOptionsOptions = {
ctx: {
user: NonNullable<TrpcSessionUser>;
};
};
export const locationOptionsHandler = async ({ ctx }: LocationOptionsOptions) => {
const credentials = await prisma.credential.findMany({
where: {
userId: ctx.user.id,
},
select: {
id: true,
type: true,
key: true,
userId: true,
appId: true,
invalid: true,
},
});
const integrations = await getEnabledApps(credentials);
const t = await getTranslation(ctx.user.locale ?? "en", "common");
const locationOptions = getLocationGroupedOptions(integrations, t);
return locationOptions;
};

View File

@ -0,0 +1 @@
export {};

View File

@ -0,0 +1,41 @@
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
type MeOptions = {
ctx: {
user: NonNullable<TrpcSessionUser>;
};
};
export const meHandler = async ({ ctx }: MeOptions) => {
const { user } = ctx;
// Destructuring here only makes it more illegible
// pick only the part we want to expose in the API
return {
id: user.id,
name: user.name,
username: user.username,
email: user.email,
startTime: user.startTime,
endTime: user.endTime,
bufferTime: user.bufferTime,
locale: user.locale,
timeFormat: user.timeFormat,
timeZone: user.timeZone,
avatar: user.avatar,
createdDate: user.createdDate,
trialEndsAt: user.trialEndsAt,
defaultScheduleId: user.defaultScheduleId,
completedOnboarding: user.completedOnboarding,
twoFactorEnabled: user.twoFactorEnabled,
disableImpersonation: user.disableImpersonation,
identityProvider: user.identityProvider,
brandColor: user.brandColor,
darkBrandColor: user.darkBrandColor,
away: user.away,
bio: user.bio,
weekStart: user.weekStart,
theme: user.theme,
hideBranding: user.hideBranding,
metadata: user.metadata,
};
};

View File

@ -0,0 +1 @@
export {};

View File

@ -0,0 +1,65 @@
import { getCalendarCredentials, getConnectedCalendars } from "@calcom/core/CalendarManager";
import { prisma } from "@calcom/prisma";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
import { TRPCError } from "@trpc/server";
import type { TSetDestinationCalendarInputSchema } from "./setDestinationCalendar.schema";
type SetDestinationCalendarOptions = {
ctx: {
user: NonNullable<TrpcSessionUser>;
};
input: TSetDestinationCalendarInputSchema;
};
export const setDestinationCalendarHandler = async ({ ctx, input }: SetDestinationCalendarOptions) => {
const { user } = ctx;
const { integration, externalId, eventTypeId } = input;
const calendarCredentials = getCalendarCredentials(user.credentials);
const { connectedCalendars } = await getConnectedCalendars(calendarCredentials, user.selectedCalendars);
const allCals = connectedCalendars.map((cal) => cal.calendars ?? []).flat();
const credentialId = allCals.find(
(cal) => cal.externalId === externalId && cal.integration === integration && cal.readOnly === false
)?.credentialId;
if (!credentialId) {
throw new TRPCError({ code: "BAD_REQUEST", message: `Could not find calendar ${input.externalId}` });
}
let where;
if (eventTypeId) {
if (
!(await prisma.eventType.findFirst({
where: {
id: eventTypeId,
userId: user.id,
},
}))
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: `You don't have access to event type ${eventTypeId}`,
});
}
where = { eventTypeId };
} else where = { userId: user.id };
await prisma.destinationCalendar.upsert({
where,
update: {
integration,
externalId,
credentialId,
},
create: {
...where,
integration,
externalId,
credentialId,
},
});
};

View File

@ -0,0 +1,10 @@
import { z } from "zod";
export const ZSetDestinationCalendarInputSchema = z.object({
integration: z.string(),
externalId: z.string(),
eventTypeId: z.number().nullish(),
bookingId: z.number().nullish(),
});
export type TSetDestinationCalendarInputSchema = z.infer<typeof ZSetDestinationCalendarInputSchema>;

View File

@ -0,0 +1,50 @@
import stripe from "@calcom/app-store/stripepayment/lib/server";
import { prisma } from "@calcom/prisma";
import { userMetadata } from "@calcom/prisma/zod-utils";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
import { TRPCError } from "@trpc/server";
type StripeCustomerOptions = {
ctx: {
user: NonNullable<TrpcSessionUser>;
};
};
export const stripeCustomerHandler = async ({ ctx }: StripeCustomerOptions) => {
const {
user: { id: userId },
} = ctx;
const user = await prisma.user.findUnique({
where: {
id: userId,
},
select: {
metadata: true,
},
});
if (!user) {
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "User not found" });
}
const metadata = userMetadata.parse(user.metadata);
if (!metadata?.stripeCustomerId) {
throw new TRPCError({ code: "BAD_REQUEST", message: "No stripe customer id" });
}
// Fetch stripe customer
const stripeCustomerId = metadata?.stripeCustomerId;
const customer = await stripe.customers.retrieve(stripeCustomerId);
if (customer.deleted) {
throw new TRPCError({ code: "BAD_REQUEST", message: "No stripe customer found" });
}
const username = customer?.metadata?.username || null;
return {
isPremium: !!metadata?.isPremium,
username,
};
};

View File

@ -0,0 +1 @@
export {};

View File

@ -0,0 +1,35 @@
import dayjs from "@calcom/dayjs";
import { sendFeedbackEmail } from "@calcom/emails";
import { prisma } from "@calcom/prisma";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
import type { TSubmitFeedbackInputSchema } from "./submitFeedback.schema";
type SubmitFeedbackOptions = {
ctx: {
user: NonNullable<TrpcSessionUser>;
};
input: TSubmitFeedbackInputSchema;
};
export const submitFeedbackHandler = async ({ ctx, input }: SubmitFeedbackOptions) => {
const { rating, comment } = input;
const feedback = {
username: ctx.user.username || "Nameless",
email: ctx.user.email || "No email address",
rating: rating,
comment: comment,
};
await prisma.feedback.create({
data: {
date: dayjs().toISOString(),
userId: ctx.user.id,
rating: rating,
comment: comment,
},
});
if (process.env.SEND_FEEDBACK_EMAIL && comment) sendFeedbackEmail(feedback);
};

View File

@ -0,0 +1,8 @@
import { z } from "zod";
export const ZSubmitFeedbackInputSchema = z.object({
rating: z.string(),
comment: z.string(),
});
export type TSubmitFeedbackInputSchema = z.infer<typeof ZSubmitFeedbackInputSchema>;

View File

@ -0,0 +1,140 @@
import type { Prisma } from "@prisma/client";
import type { NextApiResponse, GetServerSidePropsContext } from "next";
import stripe from "@calcom/app-store/stripepayment/lib/server";
import { getPremiumPlanProductId } from "@calcom/app-store/stripepayment/lib/utils";
import hasKeyInMetadata from "@calcom/lib/hasKeyInMetadata";
import { checkUsername } from "@calcom/lib/server/checkUsername";
import { resizeBase64Image } from "@calcom/lib/server/resizeBase64Image";
import slugify from "@calcom/lib/slugify";
import { updateWebUser as syncServicesUpdateWebUser } from "@calcom/lib/sync/SyncServiceManager";
import { prisma } from "@calcom/prisma";
import { userMetadata } from "@calcom/prisma/zod-utils";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
import { TRPCError } from "@trpc/server";
import type { TUpdateProfileInputSchema } from "./updateProfile.schema";
type UpdateProfileOptions = {
ctx: {
user: NonNullable<TrpcSessionUser>;
res?: NextApiResponse | GetServerSidePropsContext["res"];
};
input: TUpdateProfileInputSchema;
};
export const updateProfileHandler = async ({ ctx, input }: UpdateProfileOptions) => {
const { user } = ctx;
const data: Prisma.UserUpdateInput = {
...input,
metadata: input.metadata as Prisma.InputJsonValue,
};
let isPremiumUsername = false;
if (input.username) {
const username = slugify(input.username);
// Only validate if we're changing usernames
if (username !== user.username) {
data.username = username;
const response = await checkUsername(username);
isPremiumUsername = response.premium;
if (!response.available) {
throw new TRPCError({ code: "BAD_REQUEST", message: response.message });
}
}
}
if (input.avatar) {
data.avatar = await resizeBase64Image(input.avatar);
}
const userToUpdate = await prisma.user.findUnique({
where: {
id: user.id,
},
});
if (!userToUpdate) {
throw new TRPCError({ code: "NOT_FOUND", message: "User not found" });
}
const metadata = userMetadata.parse(userToUpdate.metadata);
const isPremium = metadata?.isPremium;
if (isPremiumUsername) {
const stripeCustomerId = metadata?.stripeCustomerId;
if (!isPremium || !stripeCustomerId) {
throw new TRPCError({ code: "BAD_REQUEST", message: "User is not premium" });
}
const stripeSubscriptions = await stripe.subscriptions.list({ customer: stripeCustomerId });
if (!stripeSubscriptions || !stripeSubscriptions.data.length) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "No stripeSubscription found",
});
}
// Iterate over subscriptions and look for premium product id and status active
// @TODO: iterate if stripeSubscriptions.hasMore is true
const isPremiumUsernameSubscriptionActive = stripeSubscriptions.data.some(
(subscription) =>
subscription.items.data[0].price.product === getPremiumPlanProductId() &&
subscription.status === "active"
);
if (!isPremiumUsernameSubscriptionActive) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "You need to pay for premium username",
});
}
}
const updatedUser = await prisma.user.update({
where: {
id: user.id,
},
data,
select: {
id: true,
username: true,
email: true,
metadata: true,
name: true,
createdDate: true,
},
});
// Sync Services
await syncServicesUpdateWebUser(updatedUser);
// Notify stripe about the change
if (updatedUser && updatedUser.metadata && hasKeyInMetadata(updatedUser, "stripeCustomerId")) {
const stripeCustomerId = `${updatedUser.metadata.stripeCustomerId}`;
await stripe.customers.update(stripeCustomerId, {
metadata: {
username: updatedUser.username,
email: updatedUser.email,
userId: updatedUser.id,
},
});
}
// Revalidate booking pages
const res = ctx.res as NextApiResponse;
if (typeof res?.revalidate !== "undefined") {
const eventTypes = await prisma.eventType.findMany({
where: {
userId: user.id,
team: null,
hidden: false,
},
select: {
id: true,
slug: true,
},
});
// waiting for this isn't needed
Promise.all(eventTypes.map((eventType) => res?.revalidate(`/${ctx.user.username}/${eventType.slug}`)))
.then(() => console.info("Booking pages revalidated"))
.catch((e) => console.error(e));
}
};

View File

@ -0,0 +1,26 @@
import { z } from "zod";
import { FULL_NAME_LENGTH_MAX_LIMIT } from "@calcom/lib/constants";
import { userMetadata } from "@calcom/prisma/zod-utils";
export const ZUpdateProfileInputSchema = z.object({
username: z.string().optional(),
name: z.string().max(FULL_NAME_LENGTH_MAX_LIMIT).optional(),
email: z.string().optional(),
bio: z.string().optional(),
avatar: z.string().optional(),
timeZone: z.string().optional(),
weekStart: z.string().optional(),
hideBranding: z.boolean().optional(),
allowDynamicBooking: z.boolean().optional(),
brandColor: z.string().optional(),
darkBrandColor: z.string().optional(),
theme: z.string().optional().nullable(),
completedOnboarding: z.boolean().optional(),
locale: z.string().optional(),
timeFormat: z.number().optional(),
disableImpersonation: z.boolean().optional(),
metadata: userMetadata.optional(),
});
export type TUpdateProfileInputSchema = z.infer<typeof ZUpdateProfileInputSchema>;

View File

@ -0,0 +1,59 @@
import z from "zod";
import getApps from "@calcom/app-store/utils";
import { prisma } from "@calcom/prisma";
import { userMetadata } from "@calcom/prisma/zod-utils";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
import { TRPCError } from "@trpc/server";
import type { TUpdateUserDefaultConferencingAppInputSchema } from "./updateUserDefaultConferencingApp.schema";
type UpdateUserDefaultConferencingAppOptions = {
ctx: {
user: NonNullable<TrpcSessionUser>;
};
input: TUpdateUserDefaultConferencingAppInputSchema;
};
export const updateUserDefaultConferencingAppHandler = async ({
ctx,
input,
}: 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 appLocation = foundApp?.appData?.location;
if (!foundApp || !appLocation) throw new TRPCError({ code: "BAD_REQUEST", message: "App not installed" });
if (appLocation.linkType === "static" && !input.appLink) {
throw new TRPCError({ code: "BAD_REQUEST", message: "App link is required" });
}
if (appLocation.linkType === "static" && appLocation.urlRegExp) {
const validLink = z
.string()
.regex(new RegExp(appLocation.urlRegExp), "Invalid App Link")
.parse(input.appLink);
if (!validLink) {
throw new TRPCError({ code: "BAD_REQUEST", message: "Invalid app link" });
}
}
await prisma.user.update({
where: {
id: ctx.user.id,
},
data: {
metadata: {
...currentMetadata,
defaultConferencingApp: {
appSlug: input.appSlug,
appLink: input.appLink,
},
},
},
});
return input;
};

View File

@ -0,0 +1,10 @@
import { z } from "zod";
export const ZUpdateUserDefaultConferencingAppInputSchema = z.object({
appSlug: z.string().optional(),
appLink: z.string().optional(),
});
export type TUpdateUserDefaultConferencingAppInputSchema = z.infer<
typeof ZUpdateUserDefaultConferencingAppInputSchema
>;

View File

@ -0,0 +1,140 @@
import { publicProcedure, router } from "../../trpc";
import { slotsRouter } from "../viewer/slots/_router";
import { ZEventInputSchema } from "./event.schema";
import { ZSamlTenantProductInputSchema } from "./samlTenantProduct.schema";
import { ZStripeCheckoutSessionInputSchema } from "./stripeCheckoutSession.schema";
type PublicViewerRouterHandlerCache = {
session?: typeof import("./session.handler").sessionHandler;
i18n?: typeof import("./i18n.handler").i18nHandler;
countryCode?: typeof import("./countryCode.handler").countryCodeHandler;
samlTenantProduct?: typeof import("./samlTenantProduct.handler").samlTenantProductHandler;
stripeCheckoutSession?: typeof import("./stripeCheckoutSession.handler").stripeCheckoutSessionHandler;
cityTimezones?: typeof import("./cityTimezones.handler").cityTimezonesHandler;
event?: typeof import("./event.handler").eventHandler;
};
const UNSTABLE_HANDLER_CACHE: PublicViewerRouterHandlerCache = {};
// things that unauthenticated users can query about themselves
export const publicViewerRouter = router({
session: publicProcedure.query(async ({ ctx }) => {
if (!UNSTABLE_HANDLER_CACHE.session) {
UNSTABLE_HANDLER_CACHE.session = await import("./session.handler").then((mod) => mod.sessionHandler);
}
// Unreachable code but required for type safety
if (!UNSTABLE_HANDLER_CACHE.session) {
throw new Error("Failed to load handler");
}
return UNSTABLE_HANDLER_CACHE.session({
ctx,
});
}),
i18n: publicProcedure.query(async ({ ctx }) => {
if (!UNSTABLE_HANDLER_CACHE.i18n) {
UNSTABLE_HANDLER_CACHE.i18n = await import("./i18n.handler").then((mod) => mod.i18nHandler);
}
// Unreachable code but required for type safety
if (!UNSTABLE_HANDLER_CACHE.i18n) {
throw new Error("Failed to load handler");
}
return UNSTABLE_HANDLER_CACHE.i18n({
ctx,
});
}),
countryCode: publicProcedure.query(async ({ ctx }) => {
if (!UNSTABLE_HANDLER_CACHE.countryCode) {
UNSTABLE_HANDLER_CACHE.countryCode = await import("./countryCode.handler").then(
(mod) => mod.countryCodeHandler
);
}
// Unreachable code but required for type safety
if (!UNSTABLE_HANDLER_CACHE.countryCode) {
throw new Error("Failed to load handler");
}
return UNSTABLE_HANDLER_CACHE.countryCode({
ctx,
});
}),
samlTenantProduct: publicProcedure.input(ZSamlTenantProductInputSchema).mutation(async ({ ctx, input }) => {
if (!UNSTABLE_HANDLER_CACHE.samlTenantProduct) {
UNSTABLE_HANDLER_CACHE.samlTenantProduct = await import("./samlTenantProduct.handler").then(
(mod) => mod.samlTenantProductHandler
);
}
// Unreachable code but required for type safety
if (!UNSTABLE_HANDLER_CACHE.samlTenantProduct) {
throw new Error("Failed to load handler");
}
return UNSTABLE_HANDLER_CACHE.samlTenantProduct({
ctx,
input,
});
}),
stripeCheckoutSession: publicProcedure
.input(ZStripeCheckoutSessionInputSchema)
.query(async ({ ctx, input }) => {
if (!UNSTABLE_HANDLER_CACHE.stripeCheckoutSession) {
UNSTABLE_HANDLER_CACHE.stripeCheckoutSession = await import("./stripeCheckoutSession.handler").then(
(mod) => mod.stripeCheckoutSessionHandler
);
}
// Unreachable code but required for type safety
if (!UNSTABLE_HANDLER_CACHE.stripeCheckoutSession) {
throw new Error("Failed to load handler");
}
return UNSTABLE_HANDLER_CACHE.stripeCheckoutSession({
ctx,
input,
});
}),
cityTimezones: publicProcedure.query(async ({ ctx }) => {
if (!UNSTABLE_HANDLER_CACHE.cityTimezones) {
UNSTABLE_HANDLER_CACHE.cityTimezones = await import("./cityTimezones.handler").then(
(mod) => mod.cityTimezonesHandler
);
}
// Unreachable code but required for type safety
if (!UNSTABLE_HANDLER_CACHE.cityTimezones) {
throw new Error("Failed to load handler");
}
return UNSTABLE_HANDLER_CACHE.cityTimezones({
ctx,
});
}),
// REVIEW: This router is part of both the public and private viewer router?
slots: slotsRouter,
event: publicProcedure.input(ZEventInputSchema).query(async ({ ctx, input }) => {
if (!UNSTABLE_HANDLER_CACHE.event) {
UNSTABLE_HANDLER_CACHE.event = await import("./event.handler").then((mod) => mod.eventHandler);
}
// Unreachable code but required for type safety
if (!UNSTABLE_HANDLER_CACHE.event) {
throw new Error("Failed to load handler");
}
return UNSTABLE_HANDLER_CACHE.event({
ctx,
input,
});
}),
});

View File

@ -0,0 +1,38 @@
import type { Session } from "next-auth";
type CityTimezonesOptions = {
ctx: {
session: Session | null;
};
};
export const cityTimezonesHandler = async ({ ctx: _ctx }: CityTimezonesOptions) => {
/**
* Lazy loads third party dependency to avoid loading 1.5Mb for ALL tRPC procedures.
* Thanks @roae for the tip 🙏
**/
const allCities = await import("city-timezones").then((mod) => mod.cityMapping);
/**
* Filter out all cities that have the same "city" key and only use the one with the highest population.
* This way we return a new array of cities without running the risk of having more than one city
* with the same name on the dropdown and prevent users from mistaking the time zone of the desired city.
*/
const topPopulatedCities: { [key: string]: { city: string; timezone: string; pop: number } } = {};
allCities.forEach((city) => {
const cityPopulationCount = city.pop;
if (
topPopulatedCities[city.city]?.pop === undefined ||
cityPopulationCount > topPopulatedCities[city.city].pop
) {
topPopulatedCities[city.city] = { city: city.city, timezone: city.timezone, pop: city.pop };
}
});
const uniqueCities = Object.values(topPopulatedCities);
/** Add specific overries in here */
uniqueCities.forEach((city) => {
if (city.city === "London") city.timezone = "Europe/London";
if (city.city === "Londonderry") city.city = "London";
});
return uniqueCities;
};

View File

@ -0,0 +1 @@
export {};

View File

@ -0,0 +1,12 @@
import type { CreateInnerContextOptions } from "../../createContext";
type CountryCodeOptions = {
ctx: CreateInnerContextOptions;
};
export const countryCodeHandler = async ({ ctx }: CountryCodeOptions) => {
const { req } = ctx;
const countryCode: string | string[] = req?.headers?.["x-vercel-ip-country"] ?? "";
return { countryCode: Array.isArray(countryCode) ? countryCode[0] : countryCode };
};

View File

@ -0,0 +1 @@
export {};

View File

@ -0,0 +1,15 @@
import type { PrismaClient } from "@prisma/client";
import { getPublicEvent } from "@calcom/features/eventtypes/lib/getPublicEvent";
import type { TEventInputSchema } from "./event.schema";
interface EventHandlerOptions {
ctx: { prisma: PrismaClient };
input: TEventInputSchema;
}
export const eventHandler = async ({ ctx, input }: EventHandlerOptions) => {
const event = await getPublicEvent(input.username, input.eventSlug, ctx.prisma);
return event;
};

View File

@ -0,0 +1,8 @@
import z from "zod";
export const ZEventInputSchema = z.object({
username: z.string(),
eventSlug: z.string(),
});
export type TEventInputSchema = z.infer<typeof ZEventInputSchema>;

View File

@ -0,0 +1,15 @@
import type { CreateInnerContextOptions } from "../../createContext";
import { getLocale } from "../../trpc";
type I18nOptions = {
ctx: CreateInnerContextOptions;
};
export const i18nHandler = async ({ ctx }: I18nOptions) => {
const { locale, i18n } = await getLocale(ctx);
return {
i18n,
locale,
};
};

View File

@ -0,0 +1 @@
export {};

View File

@ -0,0 +1,19 @@
import type { Session } from "next-auth";
import { samlTenantProduct } from "@calcom/features/ee/sso/lib/saml";
import { prisma } from "@calcom/prisma";
import type { TSamlTenantProductInputSchema } from "./samlTenantProduct.schema";
type SamlTenantProductOptions = {
ctx: {
session: Session | null;
};
input: TSamlTenantProductInputSchema;
};
export const samlTenantProductHandler = async ({ ctx: _ctx, input }: SamlTenantProductOptions) => {
const { email } = input;
return await samlTenantProduct(prisma, email);
};

View File

@ -0,0 +1,7 @@
import { z } from "zod";
export const ZSamlTenantProductInputSchema = z.object({
email: z.string().email(),
});
export type TSamlTenantProductInputSchema = z.infer<typeof ZSamlTenantProductInputSchema>;

View File

@ -0,0 +1,11 @@
import type { Session } from "next-auth";
type SessionOptions = {
ctx: {
session: Session | null;
};
};
export const sessionHandler = async ({ ctx }: SessionOptions) => {
return ctx.session;
};

View File

@ -0,0 +1 @@
export {};

View File

@ -0,0 +1,72 @@
import type { Session } from "next-auth";
import stripe from "@calcom/app-store/stripepayment/lib/server";
import type { TStripeCheckoutSessionInputSchema } from "./stripeCheckoutSession.schema";
type StripeCheckoutSessionOptions = {
ctx: {
session: Session | null;
};
input: TStripeCheckoutSessionInputSchema;
};
export const stripeCheckoutSessionHandler = async ({ input }: StripeCheckoutSessionOptions) => {
const { checkoutSessionId, stripeCustomerId } = input;
// TODO: Move the following data checks to superRefine
if (!checkoutSessionId && !stripeCustomerId) {
throw new Error("Missing checkoutSessionId or stripeCustomerId");
}
if (checkoutSessionId && stripeCustomerId) {
throw new Error("Both checkoutSessionId and stripeCustomerId provided");
}
let customerId: string;
let isPremiumUsername = false;
let hasPaymentFailed = false;
if (checkoutSessionId) {
try {
const session = await stripe.checkout.sessions.retrieve(checkoutSessionId);
if (typeof session.customer !== "string") {
return {
valid: false,
};
}
customerId = session.customer;
isPremiumUsername = true;
hasPaymentFailed = session.payment_status !== "paid";
} catch (e) {
return {
valid: false,
};
}
} else {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
customerId = stripeCustomerId!;
}
try {
const customer = await stripe.customers.retrieve(customerId);
if (customer.deleted) {
return {
valid: false,
};
}
return {
valid: true,
hasPaymentFailed,
isPremiumUsername,
customer: {
username: customer.metadata.username,
email: customer.metadata.email,
stripeCustomerId: customerId,
},
};
} catch (e) {
return {
valid: false,
};
}
};

View File

@ -0,0 +1,8 @@
import { z } from "zod";
export const ZStripeCheckoutSessionInputSchema = z.object({
stripeCustomerId: z.string().optional(),
checkoutSessionId: z.string().optional(),
});
export type TStripeCheckoutSessionInputSchema = z.infer<typeof ZStripeCheckoutSessionInputSchema>;

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,50 @@
import ethRouter from "@calcom/app-store/rainbow/trpc/router";
import app_RoutingForms from "@calcom/app-store/routing-forms/trpc-router";
import { userAdminRouter } from "@calcom/features/ee/users/server/trpc-router";
import { featureFlagRouter } from "@calcom/features/flags/server/router";
import { insightsRouter } from "@calcom/features/insights/server/trpc-router";
import { mergeRouters, router } from "../../trpc";
import { loggedInViewerRouter } from "../loggedInViewer/_router";
import { publicViewerRouter } from "../publicViewer/_router";
import { apiKeysRouter } from "./apiKeys/_router";
import { appsRouter } from "./apps/_router";
import { authRouter } from "./auth/_router";
import { availabilityRouter } from "./availability/_router";
import { bookingsRouter } from "./bookings/_router";
import { deploymentSetupRouter } from "./deploymentSetup/_router";
import { eventTypesRouter } from "./eventTypes/_router";
import { paymentsRouter } from "./payments/_router";
import { slotsRouter } from "./slots/_router";
import { ssoRouter } from "./sso/_router";
import { viewerTeamsRouter } from "./teams/_router";
import { webhookRouter } from "./webhook/_router";
import { workflowsRouter } from "./workflows/_router";
export const viewerRouter = mergeRouters(
loggedInViewerRouter,
router({
loggedInViewerRouter,
public: publicViewerRouter,
auth: authRouter,
deploymentSetup: deploymentSetupRouter,
bookings: bookingsRouter,
eventTypes: eventTypesRouter,
availability: availabilityRouter,
teams: viewerTeamsRouter,
webhook: webhookRouter,
apiKeys: apiKeysRouter,
slots: slotsRouter,
workflows: workflowsRouter,
saml: ssoRouter,
insights: insightsRouter,
payments: paymentsRouter,
// NOTE: Add all app related routes in the bottom till the problem described in @calcom/app-store/trpc-routers.ts is solved.
// After that there would just one merge call here for all the apps.
appRoutingForms: app_RoutingForms,
eth: ethRouter,
features: featureFlagRouter,
appsRouter,
users: userAdminRouter,
})
);

View File

@ -1,153 +0,0 @@
import { v4 } from "uuid";
import { z } from "zod";
import { generateUniqueAPIKey } from "@calcom/features/ee/api-keys/lib/apiKeys";
import { router, authedProcedure } from "../../trpc";
export const apiKeysRouter = router({
list: authedProcedure.query(async ({ ctx }) => {
return await ctx.prisma.apiKey.findMany({
where: {
userId: ctx.user.id,
OR: [
{
NOT: {
appId: "zapier",
},
},
{
appId: null,
},
],
},
orderBy: { createdAt: "desc" },
});
}),
findKeyOfType: authedProcedure
.input(
z.object({
appId: z.string().optional().nullable(),
})
)
.query(async ({ ctx, input }) => {
return await ctx.prisma.apiKey.findFirst({
where: {
AND: [
{
userId: ctx.user.id,
},
{
appId: input.appId,
},
],
},
});
}),
create: authedProcedure
.input(
z.object({
note: z.string().optional().nullish(),
expiresAt: z.date().optional().nullable(),
neverExpires: z.boolean().optional(),
appId: z.string().optional().nullable(),
})
)
.mutation(async ({ ctx, input }) => {
const [hashedApiKey, apiKey] = generateUniqueAPIKey();
// Here we snap never expires before deleting it so it's not passed to prisma create call.
const neverExpires = input.neverExpires;
delete input.neverExpires;
await ctx.prisma.apiKey.create({
data: {
id: v4(),
userId: ctx.user.id,
...input,
// And here we pass a null to expiresAt if never expires is true. otherwise just pass expiresAt from input
expiresAt: neverExpires ? null : input.expiresAt,
hashedKey: hashedApiKey,
},
});
const prefixedApiKey = `${process.env.API_KEY_PREFIX ?? "cal_"}${apiKey}`;
return prefixedApiKey;
}),
edit: authedProcedure
.input(
z.object({
id: z.string(),
note: z.string().optional().nullish(),
expiresAt: z.date().optional(),
})
)
.mutation(async ({ ctx, input }) => {
const { id, ...data } = input;
const {
apiKeys: [updatedApiKey],
} = await ctx.prisma.user.update({
where: {
id: ctx.user.id,
},
data: {
apiKeys: {
update: {
where: {
id,
},
data,
},
},
},
select: {
apiKeys: {
where: {
id,
},
},
},
});
return updatedApiKey;
}),
delete: authedProcedure
.input(
z.object({
id: z.string(),
eventTypeId: z.number().optional(),
})
)
.mutation(async ({ ctx, input }) => {
const { id } = input;
const apiKeyToDelete = await ctx.prisma.apiKey.findFirst({
where: {
id,
},
});
await ctx.prisma.user.update({
where: {
id: ctx.user.id,
},
data: {
apiKeys: {
delete: {
id,
},
},
},
});
//remove all existing zapier webhooks, as we always have only one zapier API key and the running zaps won't work any more if this key is deleted
if (apiKeyToDelete && apiKeyToDelete.appId === "zapier") {
await ctx.prisma.webhook.deleteMany({
where: {
userId: ctx.user.id,
appId: "zapier",
},
});
}
return {
id,
};
}),
});

View File

@ -0,0 +1,101 @@
import { authedProcedure, router } from "../../../trpc";
import { ZCreateInputSchema } from "./create.schema";
import { ZDeleteInputSchema } from "./delete.schema";
import { ZEditInputSchema } from "./edit.schema";
import { ZFindKeyOfTypeInputSchema } from "./findKeyOfType.schema";
type ApiKeysRouterHandlerCache = {
list?: typeof import("./list.handler").listHandler;
findKeyOfType?: typeof import("./findKeyOfType.handler").findKeyOfTypeHandler;
create?: typeof import("./create.handler").createHandler;
edit?: typeof import("./edit.handler").editHandler;
delete?: typeof import("./delete.handler").deleteHandler;
};
const UNSTABLE_HANDLER_CACHE: ApiKeysRouterHandlerCache = {};
export const apiKeysRouter = router({
// List keys
list: authedProcedure.query(async ({ ctx }) => {
if (!UNSTABLE_HANDLER_CACHE.list) {
UNSTABLE_HANDLER_CACHE.list = await import("./list.handler").then((mod) => mod.listHandler);
}
// Unreachable code but required for type safety
if (!UNSTABLE_HANDLER_CACHE.list) {
throw new Error("Failed to load handler");
}
return UNSTABLE_HANDLER_CACHE.list({
ctx,
});
}),
// Find key of type
findKeyOfType: authedProcedure.input(ZFindKeyOfTypeInputSchema).query(async ({ ctx, input }) => {
if (!UNSTABLE_HANDLER_CACHE.findKeyOfType) {
UNSTABLE_HANDLER_CACHE.findKeyOfType = await import("./findKeyOfType.handler").then(
(mod) => mod.findKeyOfTypeHandler
);
}
// Unreachable code but required for type safety
if (!UNSTABLE_HANDLER_CACHE.findKeyOfType) {
throw new Error("Failed to load handler");
}
return UNSTABLE_HANDLER_CACHE.findKeyOfType({
ctx,
input,
});
}),
// Create a new key
create: authedProcedure.input(ZCreateInputSchema).mutation(async ({ ctx, input }) => {
if (!UNSTABLE_HANDLER_CACHE.create) {
UNSTABLE_HANDLER_CACHE.create = await import("./create.handler").then((mod) => mod.createHandler);
}
// Unreachable code but required for type safety
if (!UNSTABLE_HANDLER_CACHE.create) {
throw new Error("Failed to load handler");
}
return UNSTABLE_HANDLER_CACHE.create({
ctx,
input,
});
}),
edit: authedProcedure.input(ZEditInputSchema).mutation(async ({ ctx, input }) => {
if (!UNSTABLE_HANDLER_CACHE.edit) {
UNSTABLE_HANDLER_CACHE.edit = await import("./edit.handler").then((mod) => mod.editHandler);
}
// Unreachable code but required for type safety
if (!UNSTABLE_HANDLER_CACHE.edit) {
throw new Error("Failed to load handler");
}
return UNSTABLE_HANDLER_CACHE.edit({
ctx,
input,
});
}),
delete: authedProcedure.input(ZDeleteInputSchema).mutation(async ({ ctx, input }) => {
if (!UNSTABLE_HANDLER_CACHE.delete) {
UNSTABLE_HANDLER_CACHE.delete = await import("./delete.handler").then((mod) => mod.deleteHandler);
}
// Unreachable code but required for type safety
if (!UNSTABLE_HANDLER_CACHE.delete) {
throw new Error("Failed to load handler");
}
return UNSTABLE_HANDLER_CACHE.delete({
ctx,
input,
});
}),
});

View File

@ -0,0 +1,38 @@
import { v4 } from "uuid";
import { generateUniqueAPIKey } from "@calcom/ee/api-keys/lib/apiKeys";
import prisma from "@calcom/prisma";
import type { TrpcSessionUser } from "../../../trpc";
import type { TCreateInputSchema } from "./create.schema";
type CreateHandlerOptions = {
ctx: {
user: NonNullable<TrpcSessionUser>;
};
input: TCreateInputSchema;
};
export const createHandler = async ({ ctx, input }: CreateHandlerOptions) => {
const [hashedApiKey, apiKey] = generateUniqueAPIKey();
// Here we snap never expires before deleting it so it's not passed to prisma create call.
const { neverExpires, ...rest } = input;
await prisma.apiKey.create({
data: {
id: v4(),
userId: ctx.user.id,
...rest,
// And here we pass a null to expiresAt if never expires is true. otherwise just pass expiresAt from input
expiresAt: neverExpires ? null : rest.expiresAt,
hashedKey: hashedApiKey,
},
});
const apiKeyPrefix = process.env.API_KEY_PREFIX ?? "cal_";
const prefixedApiKey = `${apiKeyPrefix}${apiKey}`;
return prefixedApiKey;
};

View File

@ -0,0 +1,10 @@
import { z } from "zod";
export const ZCreateInputSchema = z.object({
note: z.string().optional().nullish(),
expiresAt: z.date().optional().nullable(),
neverExpires: z.boolean().optional(),
appId: z.string().optional().nullable(),
});
export type TCreateInputSchema = z.infer<typeof ZCreateInputSchema>;

View File

@ -0,0 +1,48 @@
import prisma from "@calcom/prisma";
import type { TrpcSessionUser } from "../../../trpc";
import type { TDeleteInputSchema } from "./delete.schema";
type DeleteOptions = {
ctx: {
user: NonNullable<TrpcSessionUser>;
};
input: TDeleteInputSchema;
};
export const deleteHandler = async ({ ctx, input }: DeleteOptions) => {
const { id } = input;
const apiKeyToDelete = await prisma.apiKey.findFirst({
where: {
id,
},
});
await prisma.user.update({
where: {
id: ctx.user.id,
},
data: {
apiKeys: {
delete: {
id,
},
},
},
});
//remove all existing zapier webhooks, as we always have only one zapier API key and the running zaps won't work any more if this key is deleted
if (apiKeyToDelete && apiKeyToDelete.appId === "zapier") {
await prisma.webhook.deleteMany({
where: {
userId: ctx.user.id,
appId: "zapier",
},
});
}
return {
id,
};
};

View File

@ -0,0 +1,8 @@
import { z } from "zod";
export const ZDeleteInputSchema = z.object({
id: z.string(),
eventTypeId: z.number().optional(),
});
export type TDeleteInputSchema = z.infer<typeof ZDeleteInputSchema>;

View File

@ -0,0 +1,42 @@
import prisma from "@calcom/prisma";
import type { TrpcSessionUser } from "../../../trpc";
import type { TEditInputSchema } from "./edit.schema";
type EditOptions = {
ctx: {
user: NonNullable<TrpcSessionUser>;
};
input: TEditInputSchema;
};
export const editHandler = async ({ ctx, input }: EditOptions) => {
const { id, ...data } = input;
const {
apiKeys: [updatedApiKey],
} = await prisma.user.update({
where: {
id: ctx.user.id,
},
data: {
apiKeys: {
update: {
where: {
id,
},
data,
},
},
},
select: {
apiKeys: {
where: {
id,
},
},
},
});
return updatedApiKey;
};

View File

@ -0,0 +1,9 @@
import { z } from "zod";
export const ZEditInputSchema = z.object({
id: z.string(),
note: z.string().optional().nullish(),
expiresAt: z.date().optional(),
});
export type TEditInputSchema = z.infer<typeof ZEditInputSchema>;

View File

@ -0,0 +1,26 @@
import prisma from "@calcom/prisma";
import type { TrpcSessionUser } from "../../../trpc";
import type { TFindKeyOfTypeInputSchema } from "./findKeyOfType.schema";
type FindKeyOfTypeOptions = {
ctx: {
user: NonNullable<TrpcSessionUser>;
};
input: TFindKeyOfTypeInputSchema;
};
export const findKeyOfTypeHandler = async ({ ctx, input }: FindKeyOfTypeOptions) => {
return await prisma.apiKey.findFirst({
where: {
AND: [
{
userId: ctx.user.id,
},
{
appId: input.appId,
},
],
},
});
};

View File

@ -0,0 +1,7 @@
import { z } from "zod";
export const ZFindKeyOfTypeInputSchema = z.object({
appId: z.string().optional(),
});
export type TFindKeyOfTypeInputSchema = z.infer<typeof ZFindKeyOfTypeInputSchema>;

View File

@ -0,0 +1,28 @@
import prisma from "@calcom/prisma";
import type { TrpcSessionUser } from "../../../trpc";
type ListOptions = {
ctx: {
user: NonNullable<TrpcSessionUser>;
};
};
export const listHandler = async ({ ctx }: ListOptions) => {
return await prisma.apiKey.findMany({
where: {
userId: ctx.user.id,
OR: [
{
NOT: {
appId: "zapier",
},
},
{
appId: null,
},
],
},
orderBy: { createdAt: "desc" },
});
};

View File

@ -0,0 +1 @@
export {};

View File

@ -1,357 +0,0 @@
import { AppCategories } from "@prisma/client";
import type { Prisma } from "@prisma/client";
import z from "zod";
import { appKeysSchemas } from "@calcom/app-store/apps.keys-schemas.generated";
import { getLocalAppMetadata, getAppFromSlug } from "@calcom/app-store/utils";
import { sendDisabledAppEmail } from "@calcom/emails";
import { deriveAppDictKeyFromType } from "@calcom/lib/deriveAppDictKeyFromType";
import { getTranslation } from "@calcom/lib/server/i18n";
import { TRPCError } from "@trpc/server";
import { authedAdminProcedure, authedProcedure, router } from "../../trpc";
export const appsRouter = router({
listLocal: authedAdminProcedure
.input(
z.object({
category: z.nativeEnum({ ...AppCategories, conferencing: "conferencing" }),
})
)
.query(async ({ ctx, input }) => {
const category = input.category === "conferencing" ? "video" : input.category;
const localApps = getLocalAppMetadata();
const dbApps = await ctx.prisma.app.findMany({
where: {
categories: {
has: AppCategories[category as keyof typeof AppCategories],
},
},
select: {
slug: true,
keys: true,
enabled: true,
dirName: true,
},
});
return localApps.flatMap((app) => {
// Filter applications that does not belong to the current requested category.
if (!(app.category === category || app.categories?.some((appCategory) => appCategory === category))) {
return [];
}
// Find app metadata
const dbData = dbApps.find((dbApp) => dbApp.slug === app.slug);
// If the app already contains keys then return
if (dbData?.keys) {
return {
name: app.name,
slug: app.slug,
logo: app.logo,
title: app.title,
type: app.type,
description: app.description,
// We know that keys are going to be an object or null. Prisma can not type check against JSON fields
keys: dbData.keys as Prisma.JsonObject | null,
dirName: app.dirName || app.slug,
enabled: dbData?.enabled || false,
isTemplate: app.isTemplate,
};
}
const keysSchema = appKeysSchemas[app.dirName as keyof typeof appKeysSchemas];
const keys: Record<string, string> = {};
// `typeof val === 'undefined'` is always slower than !== undefined comparison
// it is important to avoid string to string comparisons as much as we can
if (keysSchema !== undefined) {
// TODO: Remove the Object.values and reduce to improve the performance.
Object.values(keysSchema.keyof()._def.values).reduce((keysObject, key) => {
keys[key as string] = "";
return keysObject;
}, {} as Record<string, string>);
}
return {
name: app.name,
slug: app.slug,
logo: app.logo,
type: app.type,
title: app.title,
description: app.description,
enabled: dbData?.enabled ?? false,
dirName: app.dirName ?? app.slug,
keys: Object.keys(keys).length === 0 ? null : keys,
};
});
}),
toggle: authedAdminProcedure
.input(
z.object({
slug: z.string(),
enabled: z.boolean(),
})
)
.mutation(async ({ ctx, input }) => {
const { prisma } = ctx;
const { enabled } = input;
// Get app name from metadata
const localApps = getLocalAppMetadata();
const appMetadata = localApps.find((localApp) => localApp.slug === input.slug);
if (!appMetadata)
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "App metadata could not be found" });
const app = await prisma.app.upsert({
where: {
slug: input.slug,
},
update: {
enabled,
dirName: appMetadata?.dirName || appMetadata?.slug || "",
},
create: {
slug: input.slug,
dirName: appMetadata?.dirName || appMetadata?.slug || "",
categories:
(appMetadata?.categories as AppCategories[]) ||
([appMetadata?.category] as AppCategories[]) ||
undefined,
keys: undefined,
enabled,
},
});
// If disabling an app then we need to alert users based on the app type
if (!enabled) {
const translations = new Map();
if (app.categories.some((category) => ["calendar", "video"].includes(category))) {
// Find all users with the app credentials
const appCredentials = await prisma.credential.findMany({
where: {
appId: app.slug,
},
select: {
user: {
select: {
email: true,
locale: true,
},
},
},
});
// TODO: This should be done async probably using a queue.
Promise.all(
appCredentials.map(async (credential) => {
// No need to continue if credential does not have a user
if (!credential.user || !credential.user.email) return;
const locale = credential.user.locale ?? "en";
let t = translations.get(locale);
if (!t) {
t = await getTranslation(locale, "common");
translations.set(locale, t);
}
await sendDisabledAppEmail({
email: credential.user.email,
appName: appMetadata?.name || app.slug,
appType: app.categories,
t,
});
})
);
} else {
const eventTypesWithApp = await prisma.eventType.findMany({
where: {
metadata: {
path: ["apps", app.slug as string, "enabled"],
equals: true,
},
},
select: {
id: true,
title: true,
users: {
select: {
email: true,
locale: true,
},
},
metadata: true,
},
});
// TODO: This should be done async probably using a queue.
Promise.all(
eventTypesWithApp.map(async (eventType) => {
// TODO: This update query can be removed by merging it with
// the previous `findMany` query, if that query returns certain values.
await prisma.eventType.update({
where: {
id: eventType.id,
},
data: {
metadata: {
...(eventType.metadata as object),
apps: {
// From this comment we can not type JSON fields in Prisma https://github.com/prisma/prisma/issues/3219#issuecomment-670202980
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
...eventType.metadata?.apps,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
[app.slug]: { ...eventType.metadata?.apps[app.slug], enabled: false },
},
},
},
});
return Promise.all(
eventType.users.map(async (user) => {
const locale = user.locale ?? "en";
let t = translations.get(locale);
if (!t) {
t = await getTranslation(locale, "common");
translations.set(locale, t);
}
await sendDisabledAppEmail({
email: user.email,
appName: appMetadata?.name || app.slug,
appType: app.categories,
t,
title: eventType.title,
eventTypeId: eventType.id,
});
})
);
})
);
}
}
return app.enabled;
}),
saveKeys: authedAdminProcedure
.input(
z.object({
slug: z.string(),
dirName: z.string(),
type: z.string(),
// Validate w/ app specific schema
keys: z.unknown(),
fromEnabled: z.boolean().optional(),
})
)
.mutation(async ({ ctx, input }) => {
const keysSchema = appKeysSchemas[input.dirName as keyof typeof appKeysSchemas];
const keys = keysSchema.parse(input.keys);
// Get app name from metadata
const localApps = getLocalAppMetadata();
const appMetadata = localApps.find((localApp) => localApp.slug === input.slug);
if (!appMetadata?.dirName && appMetadata?.categories)
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "App metadata could not be found" });
await ctx.prisma.app.upsert({
where: {
slug: input.slug,
},
update: { keys, ...(input.fromEnabled && { enabled: true }) },
create: {
slug: input.slug,
dirName: appMetadata?.dirName || appMetadata?.slug || "",
categories:
(appMetadata?.categories as AppCategories[]) ||
([appMetadata?.category] as AppCategories[]) ||
undefined,
keys: (input.keys as Prisma.InputJsonObject) || undefined,
...(input.fromEnabled && { enabled: true }),
},
});
}),
checkForGCal: authedProcedure.query(async ({ ctx }) => {
const gCalPresent = await ctx.prisma.credential.findFirst({
where: {
type: "google_calendar",
userId: ctx.user.id,
},
});
return !!gCalPresent;
}),
updateAppCredentials: authedProcedure
.input(
z.object({
credentialId: z.number(),
key: z.object({}).passthrough(),
})
)
.mutation(async ({ ctx, input }) => {
const { user } = ctx;
const { key } = input;
// Find user credential
const credential = await ctx.prisma.credential.findFirst({
where: {
id: input.credentialId,
userId: user.id,
},
});
// Check if credential exists
if (!credential) {
throw new TRPCError({
code: "BAD_REQUEST",
message: `Could not find credential ${input.credentialId}`,
});
}
const updated = await ctx.prisma.credential.update({
where: {
id: credential.id,
},
data: {
key: {
...(credential.key as Prisma.JsonObject),
...(key as Prisma.JsonObject),
},
},
});
return !!updated;
}),
queryForDependencies: authedProcedure.input(z.string().array().optional()).query(async ({ ctx, input }) => {
if (!input) return;
const dependencyData: { name: string; slug: string; installed: boolean }[] = [];
await Promise.all(
input.map(async (dependency) => {
const appInstalled = await ctx.prisma.credential.findFirst({
where: {
appId: dependency,
userId: ctx.user.id,
},
});
const app = await getAppFromSlug(dependency);
dependencyData.push({ name: app?.name || dependency, slug: dependency, installed: !!appInstalled });
})
);
return dependencyData;
}),
});

View File

@ -0,0 +1,126 @@
import { authedAdminProcedure, authedProcedure, router } from "../../../trpc";
import { ZListLocalInputSchema } from "./listLocal.schema";
import { ZQueryForDependenciesInputSchema } from "./queryForDependencies.schema";
import { ZSaveKeysInputSchema } from "./saveKeys.schema";
import { ZToggleInputSchema } from "./toggle.schema";
import { ZUpdateAppCredentialsInputSchema } from "./updateAppCredentials.schema";
type AppsRouterHandlerCache = {
listLocal?: typeof import("./listLocal.handler").listLocalHandler;
toggle?: typeof import("./toggle.handler").toggleHandler;
saveKeys?: typeof import("./saveKeys.handler").saveKeysHandler;
checkForGCal?: typeof import("./checkForGCal.handler").checkForGCalHandler;
updateAppCredentials?: typeof import("./updateAppCredentials.handler").updateAppCredentialsHandler;
queryForDependencies?: typeof import("./queryForDependencies.handler").queryForDependenciesHandler;
};
const UNSTABLE_HANDLER_CACHE: AppsRouterHandlerCache = {};
export const appsRouter = router({
listLocal: authedAdminProcedure.input(ZListLocalInputSchema).query(async ({ ctx, input }) => {
if (!UNSTABLE_HANDLER_CACHE.listLocal) {
UNSTABLE_HANDLER_CACHE.listLocal = await import("./listLocal.handler").then(
(mod) => mod.listLocalHandler
);
}
// Unreachable code but required for type safety
if (!UNSTABLE_HANDLER_CACHE.listLocal) {
throw new Error("Failed to load handler");
}
return UNSTABLE_HANDLER_CACHE.listLocal({
ctx,
input,
});
}),
toggle: authedAdminProcedure.input(ZToggleInputSchema).mutation(async ({ ctx, input }) => {
if (!UNSTABLE_HANDLER_CACHE.toggle) {
UNSTABLE_HANDLER_CACHE.toggle = await import("./toggle.handler").then((mod) => mod.toggleHandler);
}
// Unreachable code but required for type safety
if (!UNSTABLE_HANDLER_CACHE.toggle) {
throw new Error("Failed to load handler");
}
return UNSTABLE_HANDLER_CACHE.toggle({
ctx,
input,
});
}),
saveKeys: authedAdminProcedure.input(ZSaveKeysInputSchema).mutation(async ({ ctx, input }) => {
if (!UNSTABLE_HANDLER_CACHE.saveKeys) {
UNSTABLE_HANDLER_CACHE.saveKeys = await import("./saveKeys.handler").then((mod) => mod.saveKeysHandler);
}
// Unreachable code but required for type safety
if (!UNSTABLE_HANDLER_CACHE.saveKeys) {
throw new Error("Failed to load handler");
}
return UNSTABLE_HANDLER_CACHE.saveKeys({
ctx,
input,
});
}),
checkForGCal: authedProcedure.query(async ({ ctx }) => {
if (!UNSTABLE_HANDLER_CACHE.checkForGCal) {
UNSTABLE_HANDLER_CACHE.checkForGCal = await import("./checkForGCal.handler").then(
(mod) => mod.checkForGCalHandler
);
}
// Unreachable code but required for type safety
if (!UNSTABLE_HANDLER_CACHE.checkForGCal) {
throw new Error("Failed to load handler");
}
return UNSTABLE_HANDLER_CACHE.checkForGCal({
ctx,
});
}),
updateAppCredentials: authedProcedure
.input(ZUpdateAppCredentialsInputSchema)
.mutation(async ({ ctx, input }) => {
if (!UNSTABLE_HANDLER_CACHE.updateAppCredentials) {
UNSTABLE_HANDLER_CACHE.updateAppCredentials = await import("./updateAppCredentials.handler").then(
(mod) => mod.updateAppCredentialsHandler
);
}
// Unreachable code but required for type safety
if (!UNSTABLE_HANDLER_CACHE.updateAppCredentials) {
throw new Error("Failed to load handler");
}
return UNSTABLE_HANDLER_CACHE.updateAppCredentials({
ctx,
input,
});
}),
queryForDependencies: authedProcedure
.input(ZQueryForDependenciesInputSchema)
.query(async ({ ctx, input }) => {
if (!UNSTABLE_HANDLER_CACHE.queryForDependencies) {
UNSTABLE_HANDLER_CACHE.queryForDependencies = await import("./queryForDependencies.handler").then(
(mod) => mod.queryForDependenciesHandler
);
}
// Unreachable code but required for type safety
if (!UNSTABLE_HANDLER_CACHE.queryForDependencies) {
throw new Error("Failed to load handler");
}
return UNSTABLE_HANDLER_CACHE.queryForDependencies({
ctx,
input,
});
}),
});

View File

@ -0,0 +1,20 @@
import { prisma } from "@calcom/prisma";
import type { TrpcSessionUser } from "../../../trpc";
type CheckForGCalOptions = {
ctx: {
user: NonNullable<TrpcSessionUser>;
};
};
export const checkForGCalHandler = async ({ ctx }: CheckForGCalOptions) => {
const gCalPresent = await prisma.credential.findFirst({
where: {
type: "google_calendar",
userId: ctx.user.id,
},
});
return !!gCalPresent;
};

View File

@ -0,0 +1 @@
export {};

Some files were not shown because too many files have changed in this diff Show More