Compare commits

...
Author SHA1 Message Date
emrysal e33560ef82 chore: release v5.6.15
Post release / release (push) Canceled after 0s
2025-09-08 12:44:40 +00:00
devin-ai-integration[bot]andmorgan@cal.com 84f34b2244 feat: add 5000ms staleTime to useConnectedCalendars hook (#23674)
- Add staleTime: 5000 to useQuery configuration in useConnectedCalendars
- Prevents unnecessary refetches for 5 seconds after data becomes stale
- Improves performance by reducing redundant API calls

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: morgan@cal.com <morgan@cal.com>
2025-09-08 12:19:22 +00:00
Benny Joo 086bae03d3 refactor: move a constant to a dedicated file in App store package (#23671)
* define DailyLocationType in app-store/locations.ts

* update imports
2025-09-08 08:38:46 -03:00
4a0a429774 refactor: Migrate getRoutedUrl from @calcom/lib to @calcom/features (#23666)
* mkdir

* update imports

* chore: update platform libraries

---------

Co-authored-by: Lauris Skraucis <lauris.skraucis@gmail.com>
Co-authored-by: supalarry <laurisskraucis@gmail.com>
2025-09-08 11:28:22 +00:00
Benny Joo c4142ab4e7 refactor: Move handlePayment to Features Package (#23670)
* mv

* update imports
2025-09-08 07:32:29 -03:00
Benny Joo 95f8046db9 refactor: Error code - remove circular dep between app store <-> lib (#23669)
* remove stripe.types.ts

* use stripe-error defined in packages/lib

* refactor
2025-09-08 07:31:28 -03:00
Lauris Skraucis d8a87e9f04 fix: v2 seated bookings (#23514)
* fix: email manage link missing seat uid

* fix: toggle seated booking attendees

* fix: email manage link seat uid

* chore: update platform libraries
2025-09-08 09:50:36 +00:00
Keith Williams 86dcd6f0ff perf: Create 'heavy' eventTypes router (#23664)
* perf: Create 'heavy' eventTypes router

* Fixing type checks

* Fixing platform libraries imports
2025-09-08 08:24:30 +00:00
Anik Dhabal Babu 411a14dd3d fix: unit test flakiness (#23668)
* fix: unit test flakyness

* Update CalendarService.test.ts
2025-09-08 03:47:40 +00:00
Benny Joo 03bc8db64a refactor: Remove StripeService from @calcom/lib and define it in @calcom/trpc (#23655)
* remove packages/lib/server/stripe

* simplify metadata

* define business logic directly in trpc handler
2025-09-08 02:02:56 +00:00
Benny Joo fe7fd8bc79 chore: add an eslint rule to warn importing trpc package in app store package (#23665) 2025-09-08 10:36:34 +09:00
Benny Joo 92eb41c117 refactor: Migrate trpc routers in App store package to Trpc package (#23536)
* migrate trpc routers in app package to trpc package

* delete

* delete

* delete

* fix

* fix

* fix

* format

* format

* fix

* fix

* fix

* fix

* remove unused file

* fix

* fix

* wip
2025-09-07 22:17:33 -03:00
Benny Joo a1fca56aac refactor: Remove App Store's Type Dependency on Trpc Package (#23640)
* remove trpc type interface

* refactors

* refactors
2025-09-08 00:59:51 +00:00
107 changed files with 651 additions and 572 deletions
+12
View File
@@ -19,5 +19,17 @@ module.exports = {
],
},
},
{
files: ["packages/app-store/**/*.{ts,tsx,js,jsx}"],
rules: {
"no-restricted-imports": [
"warn",
{
paths: ["@calcom/trpc"],
patterns: ["@calcom/trpc/*"],
},
],
},
},
],
};
View File
+1 -1
View File
@@ -38,7 +38,7 @@
"@axiomhq/winston": "^1.2.0",
"@calcom/platform-constants": "*",
"@calcom/platform-enums": "*",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.344",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.347",
"@calcom/platform-types": "*",
"@calcom/platform-utils": "*",
"@calcom/prisma": "*",
@@ -117,6 +117,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
slug: seatedEventTypeSlug,
length: 60,
seatsPerTimeSlot: 3,
seatsShowAttendees: true,
},
user.id
);
@@ -53,8 +53,10 @@ describe("Bookings Endpoints 2024-08-13", () => {
let apiKeyString: string;
let seatedEventTypeId: number;
let seatedEventTypeIdAttendeesDisabledId: number;
const seatedEventSlug = `seated-bookings-event-type-${randomString()}`;
const seatedEventSlugAttendeesDisabled = `seated-bookings-event-type-attendees-disabled-${randomString()}`;
let createdSeatedBooking: CreateSeatedBookingOutput_2024_08_13;
let createdSeatedBooking2: CreateSeatedBookingOutput_2024_08_13;
@@ -121,6 +123,28 @@ describe("Bookings Endpoints 2024-08-13", () => {
);
seatedEventTypeId = seatedEvent.id;
const seatedEventAttendeesDisabled = await eventTypesRepositoryFixture.create(
{
title: `seated-bookings-2024-08-13-event-type-attendees-disabled-${randomString()}`,
slug: seatedEventSlugAttendeesDisabled,
length: 60,
seatsPerTimeSlot: 5,
seatsShowAttendees: false,
seatsShowAvailabilityCount: false,
locations: [{ type: "inPerson", address: "via 10, rome, italy" }],
metadata: {
disableStandardEmails: {
all: {
attendee: true,
host: true,
},
},
},
},
user.id
);
seatedEventTypeIdAttendeesDisabledId = seatedEventAttendeesDisabled.id;
app = moduleRef.createNestApplication();
bootstrap(app as NestExpressApplication);
@@ -414,6 +438,63 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
});
it("should book an event type with attendees disabled", async () => {
const body: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 0, 9, 14, 0, 0)).toISOString(),
eventTypeId: seatedEventTypeIdAttendeesDisabledId,
attendee: {
name: nameAttendeeOne,
email: emailAttendeeOne,
timeZone: "Europe/Rome",
language: "it",
},
bookingFieldsResponses: {
codingLanguage: "TypeScript",
},
metadata: {
userId: "100",
},
};
return request(app.getHttpServer())
.post("/v2/bookings")
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201)
.then(async (response) => {
const responseBody: CreateBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseDataIsCreateSeatedBooking(responseBody.data)).toBe(true);
if (responseDataIsCreateSeatedBooking(responseBody.data)) {
const data: CreateSeatedBookingOutput_2024_08_13 = responseBody.data;
expect(data.seatUid).toBeDefined();
expect(data.id).toBeDefined();
expect(data.uid).toBeDefined();
expect(data.hosts[0].id).toEqual(user.id);
expect(data.status).toEqual("accepted");
expect(data.start).toEqual(body.start);
expect(data.end).toEqual(
DateTime.fromISO(body.start, { zone: "utc" }).plus({ hours: 1 }).toISO()
);
expect(data.duration).toEqual(60);
expect(data.eventTypeId).toEqual(seatedEventTypeIdAttendeesDisabledId);
expect(data.eventType).toEqual({
id: seatedEventTypeIdAttendeesDisabledId,
slug: seatedEventSlugAttendeesDisabled,
});
expect(data.attendees.length).toEqual(0);
expect(data.location).toBeDefined();
expect(data.absentHost).toEqual(false);
} else {
throw new Error(
"Invalid response data - expected seated booking but received non array response"
);
}
});
});
describe("cancel seated booking", () => {
describe("cancel seated booking as attendee", () => {
it("should cancel seated booking", async () => {
@@ -576,10 +576,13 @@ export class BookingsService_2024_08_13 {
return this.outputService.getOutputRecurringBooking(booking);
}
if (isRecurring && isSeated) {
return this.outputService.getOutputRecurringSeatedBooking(booking);
return this.outputService.getOutputRecurringSeatedBooking(
booking,
!!booking.eventType?.seatsShowAttendees
);
}
if (isSeated) {
return this.outputService.getOutputSeatedBooking(booking);
return this.outputService.getOutputSeatedBooking(booking, !!booking.eventType?.seatsShowAttendees);
}
return this.outputService.getOutputBooking(booking);
}
@@ -591,7 +594,10 @@ export class BookingsService_2024_08_13 {
const ids = recurringBooking.map((booking) => booking.id);
const isRecurringSeated = !!recurringBooking[0].eventType?.seatsPerTimeSlot;
if (isRecurringSeated) {
return this.outputService.getOutputRecurringSeatedBookings(ids);
return this.outputService.getOutputRecurringSeatedBookings(
ids,
!!recurringBooking[0].eventType?.seatsShowAttendees
);
}
return this.outputService.getOutputRecurringBookings(ids);
@@ -657,9 +663,19 @@ export class BookingsService_2024_08_13 {
if (isRecurring && !isSeated) {
formattedBookings.push(this.outputService.getOutputRecurringBooking(formatted));
} else if (isRecurring && isSeated) {
formattedBookings.push(this.outputService.getOutputRecurringSeatedBooking(formatted));
formattedBookings.push(
this.outputService.getOutputRecurringSeatedBooking(
formatted,
!!formatted.eventType?.seatsShowAttendees
)
);
} else if (isSeated) {
formattedBookings.push(await this.outputService.getOutputSeatedBooking(formatted));
formattedBookings.push(
await this.outputService.getOutputSeatedBooking(
formatted,
!!formatted.eventType?.seatsShowAttendees
)
);
} else {
formattedBookings.push(await this.outputService.getOutputBooking(formatted));
}
@@ -68,6 +68,7 @@ type DatabaseBooking = Booking & {
eventType: {
id: number;
slug: string;
seatsShowAttendees?: boolean | null;
} | null;
attendees: {
name: string;
@@ -167,6 +168,7 @@ export class OutputBookingsService_2024_08_13 {
getUserDefinedMetadata(databaseMetadata: DatabaseMetadata) {
if (databaseMetadata === null) return {};
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { videoCallUrl, ...userDefinedMetadata } = databaseMetadata;
return userDefinedMetadata;
@@ -266,11 +268,14 @@ export class OutputBookingsService_2024_08_13 {
databaseBooking: DatabaseBooking,
seatUid: string
): Promise<CreateSeatedBookingOutput_2024_08_13> {
const getSeatedBookingOutput = await this.getOutputSeatedBooking(databaseBooking);
const getSeatedBookingOutput = await this.getOutputSeatedBooking(
databaseBooking,
!!databaseBooking.eventType?.seatsShowAttendees
);
return { ...getSeatedBookingOutput, seatUid };
}
async getOutputSeatedBooking(databaseBooking: DatabaseBooking) {
async getOutputSeatedBooking(databaseBooking: DatabaseBooking, showAttendees: boolean) {
const dateStart = DateTime.fromISO(databaseBooking.startTime.toISOString());
const dateEnd = DateTime.fromISO(databaseBooking.endTime.toISOString());
const duration = dateEnd.diff(dateStart, "minutes").minutes;
@@ -317,41 +322,43 @@ export class OutputBookingsService_2024_08_13 {
const parsed = plainToClass(GetSeatedBookingOutput_2024_08_13, booking, { strategy: "excludeAll" });
// note(Lauris): I don't know why plainToClass erases booking.attendees[n].responses so attaching manually
parsed.attendees = databaseBooking.attendees.map((attendee) => {
const { responses } = safeParse(
seatedBookingDataSchema,
attendee.bookingSeat?.data,
defaultSeatedBookingData,
false
);
parsed.attendees = showAttendees
? databaseBooking.attendees.map((attendee) => {
const { responses } = safeParse(
seatedBookingDataSchema,
attendee.bookingSeat?.data,
defaultSeatedBookingData,
false
);
const attendeeData = {
name: attendee.name,
email: attendee.email,
timeZone: attendee.timeZone,
language: attendee.locale,
absent: !!attendee.noShow,
seatUid: attendee.bookingSeat?.referenceUid,
bookingFieldsResponses: {},
};
const attendeeParsed = plainToClass(SeatedAttendee, attendeeData, { strategy: "excludeAll" });
attendeeParsed.bookingFieldsResponses = responses || {};
attendeeParsed.metadata = safeParse(
seatedBookingMetadataSchema,
attendee.bookingSeat?.metadata,
defaultSeatedBookingMetadata,
false
);
// note(Lauris): as of now email is not returned for privacy
delete attendeeParsed.bookingFieldsResponses.email;
const attendeeData = {
name: attendee.name,
email: attendee.email,
timeZone: attendee.timeZone,
language: attendee.locale,
absent: !!attendee.noShow,
seatUid: attendee.bookingSeat?.referenceUid,
bookingFieldsResponses: {},
};
const attendeeParsed = plainToClass(SeatedAttendee, attendeeData, { strategy: "excludeAll" });
attendeeParsed.bookingFieldsResponses = responses || {};
attendeeParsed.metadata = safeParse(
seatedBookingMetadataSchema,
attendee.bookingSeat?.metadata,
defaultSeatedBookingMetadata,
false
);
// note(Lauris): as of now email is not returned for privacy
delete attendeeParsed.bookingFieldsResponses.email;
return attendeeParsed;
});
return attendeeParsed;
})
: [];
return parsed;
}
async getOutputRecurringSeatedBookings(bookingsIds: number[]) {
async getOutputRecurringSeatedBookings(bookingsIds: number[], showAttendees: boolean) {
const transformed = [];
for (const bookingId of bookingsIds) {
@@ -361,7 +368,7 @@ export class OutputBookingsService_2024_08_13 {
throw new Error(`Booking with id=${bookingId} was not found in the database`);
}
transformed.push(this.getOutputRecurringSeatedBooking(databaseBooking));
transformed.push(this.getOutputRecurringSeatedBooking(databaseBooking, showAttendees));
}
return transformed.sort((a, b) => new Date(a.start).getTime() - new Date(b.start).getTime());
@@ -386,11 +393,14 @@ export class OutputBookingsService_2024_08_13 {
databaseBooking: DatabaseBooking,
seatUid: string
): CreateRecurringSeatedBookingOutput_2024_08_13 {
const getRecurringSeatedBookingOutput = this.getOutputRecurringSeatedBooking(databaseBooking);
const getRecurringSeatedBookingOutput = this.getOutputRecurringSeatedBooking(
databaseBooking,
!!databaseBooking.eventType?.seatsShowAttendees
);
return { ...getRecurringSeatedBookingOutput, seatUid };
}
getOutputRecurringSeatedBooking(databaseBooking: DatabaseBooking) {
getOutputRecurringSeatedBooking(databaseBooking: DatabaseBooking, showAttendees: boolean) {
const dateStart = DateTime.fromISO(databaseBooking.startTime.toISOString());
const dateEnd = DateTime.fromISO(databaseBooking.endTime.toISOString());
const duration = dateEnd.diff(dateStart, "minutes").minutes;
@@ -430,35 +440,37 @@ export class OutputBookingsService_2024_08_13 {
});
// note(Lauris): I don't know why plainToClass erases booking.attendees[n].responses so attaching manually
parsed.attendees = databaseBooking.attendees.map((attendee) => {
const { responses } = safeParse(
seatedBookingDataSchema,
attendee.bookingSeat?.data,
defaultSeatedBookingData,
false
);
parsed.attendees = showAttendees
? databaseBooking.attendees.map((attendee) => {
const { responses } = safeParse(
seatedBookingDataSchema,
attendee.bookingSeat?.data,
defaultSeatedBookingData,
false
);
const attendeeData = {
name: attendee.name,
email: attendee.email,
timeZone: attendee.timeZone,
language: attendee.locale,
absent: !!attendee.noShow,
seatUid: attendee.bookingSeat?.referenceUid,
bookingFieldsResponses: {},
};
const attendeeParsed = plainToClass(SeatedAttendee, attendeeData, { strategy: "excludeAll" });
attendeeParsed.bookingFieldsResponses = responses || {};
attendeeParsed.metadata = safeParse(
seatedBookingMetadataSchema,
attendee.bookingSeat?.metadata,
defaultSeatedBookingMetadata,
false
);
// note(Lauris): as of now email is not returned for privacy
delete attendeeParsed.bookingFieldsResponses.email;
return attendeeParsed;
});
const attendeeData = {
name: attendee.name,
email: attendee.email,
timeZone: attendee.timeZone,
language: attendee.locale,
absent: !!attendee.noShow,
seatUid: attendee.bookingSeat?.referenceUid,
bookingFieldsResponses: {},
};
const attendeeParsed = plainToClass(SeatedAttendee, attendeeData, { strategy: "excludeAll" });
attendeeParsed.bookingFieldsResponses = responses || {};
attendeeParsed.metadata = safeParse(
seatedBookingMetadataSchema,
attendee.bookingSeat?.metadata,
defaultSeatedBookingMetadata,
false
);
// note(Lauris): as of now email is not returned for privacy
delete attendeeParsed.bookingFieldsResponses.email;
return attendeeParsed;
})
: [];
return parsed;
}
@@ -2,8 +2,6 @@ import type { PageProps as _PageProps } from "app/_types";
import { _generateMetadata } from "app/_utils";
import { z } from "zod";
import { StripeService } from "@calcom/lib/server/service/stripe";
import VerifyPage from "~/auth/verify-view";
const querySchema = z.object({
@@ -14,18 +12,10 @@ const querySchema = z.object({
export const generateMetadata = async ({ params, searchParams }: _PageProps) => {
const p = { ...(await params), ...(await searchParams) };
const { sessionId, stripeCustomerId } = querySchema.parse(p);
const data = await StripeService.getCheckoutSession({
stripeCustomerId,
checkoutSessionId: sessionId,
});
const { hasPaymentFailed } = data;
const { sessionId } = querySchema.parse(p);
return await _generateMetadata(
() =>
hasPaymentFailed ? "Your payment failed" : sessionId ? "Payment successful!" : `Verify your email`,
() => (sessionId ? "Payment Page" : `Verify your email`),
() => "",
undefined,
undefined,
+6 -1
View File
@@ -15,6 +15,7 @@ const resolveEndpoint = (links: any) => {
// to the correct API endpoints.
// - viewer.me - 2 segment paths like this are for logged in requests
// - viewer.public.i18n - 3 segments paths can be public or authed
// - viewer.eventTypes.heavy.create - 4 segments paths for heavy sub-router
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return (ctx: any) => {
const parts = ctx.op.path.split(".");
@@ -23,7 +24,11 @@ const resolveEndpoint = (links: any) => {
if (parts.length == 2) {
endpoint = parts[0] as keyof typeof links;
path = parts[1];
} else {
} else if (parts.length >= 3 && parts[2] === "heavy") {
endpoint = parts[1] + "/heavy" as keyof typeof links;
path = parts[3];
}
else {
endpoint = parts[1] as keyof typeof links;
path = parts.splice(2, parts.length - 2).join(".");
}
@@ -37,7 +37,7 @@ const UserProfile = ({ user }: UserProfileProps) => {
const [imageSrc, setImageSrc] = useState<string>(user?.avatar || "");
const utils = trpc.useUtils();
const router = useRouter();
const createEventType = trpc.viewer.eventTypes.create.useMutation();
const createEventType = trpc.viewer.eventTypes.heavy.create.useMutation();
const telemetry = useTelemetry();
const [firstRender, setFirstRender] = useState(true);
@@ -177,7 +177,7 @@ const OnboardingPage = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [eventTypeGroups]);
const updateMutation = trpc.viewer.eventTypes.update.useMutation({
const updateMutation = trpc.viewer.eventTypes.heavy.update.useMutation({
onSuccess: async (data) => {
showToast(t("event_type_updated_successfully", { eventTypeTitle: data.eventType?.title }), "success");
},
@@ -268,7 +268,7 @@ export const InfiniteEventTypeList = ({
},
});
const setHiddenMutation = trpc.viewer.eventTypes.update.useMutation({
const setHiddenMutation = trpc.viewer.eventTypes.heavy.update.useMutation({
onMutate: async (data) => {
await utils.viewer.eventTypes.getEventTypesFromGroup.cancel();
const previousValue = utils.viewer.eventTypes.getEventTypesFromGroup.getInfiniteData({
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@calcom/web",
"version": "5.6.14",
"version": "5.6.15",
"private": true,
"scripts": {
"analyze": "ANALYZE=true next build",
+1 -1
View File
@@ -1,6 +1,6 @@
import { getRoutedUrl } from "@calcom/features/routing-forms/lib/getRoutedUrl";
import { defaultHandler } from "@calcom/lib/server/defaultHandler";
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
import { getRoutedUrl } from "@calcom/lib/server/getRoutedUrl";
export default defaultHandler({
OPTIONS: Promise.resolve({
@@ -1,4 +1,4 @@
import appBasecamp3 from "@calcom/app-store/basecamp3/trpc-router";
import { createNextApiHandler } from "@calcom/trpc/server/createNextApiHandler";
import appBasecamp3 from "@calcom/trpc/server/routers/apps/basecamp3/_router";
export default createNextApiHandler(appBasecamp3);
@@ -1,4 +1,4 @@
import appRoutingForms from "@calcom/app-store/routing-forms/trpc-router";
import { createNextApiHandler } from "@calcom/trpc/server/createNextApiHandler";
import appRoutingForms from "@calcom/trpc/server/routers/apps/routing-forms/_router";
export default createNextApiHandler(appRoutingForms);
@@ -0,0 +1,4 @@
import { createNextApiHandler } from "@calcom/trpc/server/createNextApiHandler";
import { eventTypesRouter } from "@calcom/trpc/server/routers/viewer/eventTypes/heavy/_router";
export default createNextApiHandler(eventTypesRouter);
@@ -113,7 +113,7 @@ test("multiple duration selection updates event length correctly", async ({ page
await page.getByTestId("vertical-tab-event_advanced_tab_title").click();
await page.fill('[name="eventName"]', "{Event duration} event btwn {Organiser} {Scheduler}");
await page.locator('[data-testid="update-eventtype"]').click();
await page.waitForResponse("/api/trpc/eventTypes/update?batch=1");
await page.waitForResponse("/api/trpc/eventTypes/heavy/update?batch=1");
});
await page.goto(`/${user.username}/multiple-duration`);
+5 -5
View File
@@ -131,7 +131,7 @@ test.describe("Event Types tests", () => {
expect(formTitle).toBe(firstTitle);
expect(formSlug).toContain(firstSlug);
const submitPromise = page.waitForResponse("/api/trpc/eventTypes/duplicate?batch=1");
const submitPromise = page.waitForResponse("/api/trpc/eventTypes/heavy/duplicate?batch=1");
await page.getByTestId("continue").click();
const response = await submitPromise;
expect(response.status()).toBe(200);
@@ -144,7 +144,7 @@ test.describe("Event Types tests", () => {
await page.waitForURL((url) => {
return !!url.pathname.match(/\/event-types\/.+/);
});
await submitAndWaitForResponse(page, "/api/trpc/eventTypes/update?batch=1", {
await submitAndWaitForResponse(page, "/api/trpc/eventTypes/heavy/update?batch=1", {
action: () => page.locator("[data-testid=update-eventtype]").click(),
});
});
@@ -167,7 +167,7 @@ test.describe("Event Types tests", () => {
await page.locator("[data-testid=add-location]").click();
await fillLocation(page, locationData[2], 2);
await submitAndWaitForResponse(page, "/api/trpc/eventTypes/update?batch=1", {
await submitAndWaitForResponse(page, "/api/trpc/eventTypes/heavy/update?batch=1", {
action: () => page.locator("[data-testid=update-eventtype]").click(),
});
@@ -314,7 +314,7 @@ test.describe("Event Types tests", () => {
const locationAddress = "New Delhi";
await fillLocation(page, locationAddress, 0, false);
await submitAndWaitForResponse(page, "/api/trpc/eventTypes/update?batch=1", {
await submitAndWaitForResponse(page, "/api/trpc/eventTypes/heavy/update?batch=1", {
action: () => page.locator("[data-testid=update-eventtype]").click(),
});
@@ -429,7 +429,7 @@ test.describe("Event Types tests", () => {
await page.locator('[aria-label="Timezone Select"]').fill("New York");
await page.keyboard.press("Enter");
await submitAndWaitForResponse(page, "/api/trpc/eventTypes/update?batch=1", {
await submitAndWaitForResponse(page, "/api/trpc/eventTypes/heavy/update?batch=1", {
action: () => page.locator("[data-testid=update-eventtype]").click(),
});
await page.goto("/event-types");
@@ -50,7 +50,7 @@ export function createBookingPageFixture(page: Page) {
await page.goto("/event-types");
},
updateEventType: async () => {
await submitAndWaitForResponse(page, "/api/trpc/eventTypes/update?batch=1", {
await submitAndWaitForResponse(page, "/api/trpc/eventTypes/heavy/update?batch=1", {
action: () => page.locator("[data-testid=update-eventtype]").click(),
});
},
+4 -4
View File
@@ -42,7 +42,7 @@ test.describe("private links creation and usage", () => {
const $url = await page.locator('[data-testid="private-link-url"]').inputValue();
// click update
await submitAndWaitForResponse(page, "/api/trpc/eventTypes/update?batch=1", {
await submitAndWaitForResponse(page, "/api/trpc/eventTypes/heavy/update?batch=1", {
action: () => page.locator("[data-testid=update-eventtype]").click(),
});
// book using generated url hash
@@ -74,7 +74,7 @@ test.describe("private links creation and usage", () => {
await page.locator("[data-testid=event-slug]").first().fill("somethingrandom");
await expect(page.locator('[data-testid="event-slug"]').first()).toHaveValue("somethingrandom");
await submitAndWaitForResponse(page, "/api/trpc/eventTypes/update?batch=1", {
await submitAndWaitForResponse(page, "/api/trpc/eventTypes/heavy/update?batch=1", {
action: () => page.locator("[data-testid=update-eventtype]").click(),
});
await page.locator("[data-testid=vertical-tab-event_advanced_tab_title]").click();
@@ -113,7 +113,7 @@ test.describe("private links creation and usage", () => {
await page.locator('[data-testid="private-link-expiration-settings-save"]').click();
await page.waitForLoadState("networkidle");
// click update
await submitAndWaitForResponse(page, "/api/trpc/eventTypes/update?batch=1", {
await submitAndWaitForResponse(page, "/api/trpc/eventTypes/heavy/update?batch=1", {
action: () => page.locator("[data-testid=update-eventtype]").click(),
});
// book using generated url hash
@@ -164,7 +164,7 @@ test.describe("private links creation and usage", () => {
await page.locator('[data-testid="private-link-expiration-settings-save"]').click();
await page.waitForLoadState("networkidle");
// click update
await submitAndWaitForResponse(page, "/api/trpc/eventTypes/update?batch=1", {
await submitAndWaitForResponse(page, "/api/trpc/eventTypes/heavy/update?batch=1", {
action: () => page.locator("[data-testid=update-eventtype]").click(),
});
// book using generated url hash
+1 -1
View File
@@ -476,7 +476,7 @@ export async function gotoBookingPage(page: Page) {
}
export async function saveEventType(page: Page) {
await submitAndWaitForResponse(page, "/api/trpc/eventTypes/update?batch=1", {
await submitAndWaitForResponse(page, "/api/trpc/eventTypes/heavy/update?batch=1", {
action: () => page.locator("[data-testid=update-eventtype]").click(),
});
}
@@ -677,7 +677,7 @@ async function openBookingFormInPreviewTab(context: PlaywrightTestArgs["context"
}
async function saveEventType(page: Page) {
await submitAndWaitForResponse(page, "/api/trpc/eventTypes/update?batch=1", {
await submitAndWaitForResponse(page, "/api/trpc/eventTypes/heavy/update?batch=1", {
action: () => page.locator("[data-testid=update-eventtype]").click(),
});
}
@@ -239,5 +239,5 @@ async function gotoBookingPage(page: Page) {
}
async function saveAndWaitForResponse(page: Page) {
await submitAndWaitForResponse(page, "/api/trpc/eventTypes/update?batch=1");
await submitAndWaitForResponse(page, "/api/trpc/eventTypes/heavy/update?batch=1");
}
@@ -407,7 +407,7 @@ test.describe("Bookings", () => {
await page.goto(`/event-types/${teamEvent.id}?tabName=advanced`);
await page.getByTestId("requires-confirmation").click();
await page.getByTestId("update-eventtype").click();
await page.waitForResponse((response) => response.url().includes("/api/trpc/eventTypes/update"));
await page.waitForResponse((response) => response.url().includes("/api/trpc/eventTypes/heavy/update"));
await doOnOrgDomain(
{
@@ -783,7 +783,7 @@ const markPhoneNumberAsRequiredAndEmailAsOptional = async (page: Page, eventId:
const emailRequiredFiled = await page.locator('[data-testid="field-required"]').first();
await emailRequiredFiled.click();
await page.getByTestId("field-add-save").click();
await submitAndWaitForResponse(page, "/api/trpc/eventTypes/update?batch=1", {
await submitAndWaitForResponse(page, "/api/trpc/eventTypes/heavy/update?batch=1", {
action: () => page.locator("[data-testid=update-eventtype]").click(),
});
};
@@ -797,7 +797,7 @@ const markPhoneNumberAsRequiredField = async (page: Page, eventId: number) => {
const phoneRequiredFiled = await page.locator('[data-testid="field-required"]').first();
await phoneRequiredFiled.click();
await page.getByTestId("field-add-save").click();
await submitAndWaitForResponse(page, "/api/trpc/eventTypes/update?batch=1", {
await submitAndWaitForResponse(page, "/api/trpc/eventTypes/heavy/update?batch=1", {
action: () => page.locator("[data-testid=update-eventtype]").click(),
});
};
@@ -458,7 +458,7 @@ test.describe("Organization", () => {
await page.locator('[data-testid="fixed-hosts-select"]').click();
await page.locator(`text="${invitedUserEmail}"`).click();
await page.locator('[data-testid="update-eventtype"]').click();
await page.waitForResponse("/api/trpc/eventTypes/update?batch=1");
await page.waitForResponse("/api/trpc/eventTypes/heavy/update?batch=1");
await expectPageToBeNotFound({ page, url: `/team/${team.slug}/${teamEvent.slug}` });
await doOnOrgDomain(
+1 -1
View File
@@ -35,7 +35,7 @@ test.describe("Payment app", () => {
await page.getByPlaceholder("Price").click();
await page.getByPlaceholder("Price").fill("200");
await page.getByText("SatoshissatsCurrencyBTCPayment optionCollect payment on booking").click();
await submitAndWaitForResponse(page, "/api/trpc/eventTypes/update?batch=1", {
await submitAndWaitForResponse(page, "/api/trpc/eventTypes/heavy/update?batch=1", {
action: () => page.locator("[data-testid=update-eventtype]").click(),
});
@@ -1,7 +1,7 @@
import { wrapGetServerSidePropsWithSentry } from "@sentry/nextjs";
import type { GetServerSidePropsContext } from "next";
import { getRoutedUrl, hasEmbedPath } from "@calcom/lib/server/getRoutedUrl";
import { getRoutedUrl, hasEmbedPath } from "@calcom/features/routing-forms/lib/getRoutedUrl";
import { TRPCError } from "@trpc/server";
@@ -4,7 +4,6 @@ import Link from "next/link";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { RouterOutputs } from "@calcom/trpc/react";
import classNames from "@calcom/ui/classNames";
import { Icon } from "@calcom/ui/components/icon";
@@ -13,7 +12,7 @@ export const AppDependencyComponent = ({
dependencyData,
}: {
appName: string;
dependencyData: RouterOutputs["viewer"]["apps"]["queryForDependencies"];
dependencyData?: { name: string; slug: string; installed: boolean }[];
}) => {
const { t } = useLocale();
+2 -5
View File
@@ -4,14 +4,13 @@ import Link from "next/link";
import { useAppContextWithSchema } from "@calcom/app-store/EventTypeAppContext";
import { useIsPlatform } from "@calcom/atoms/hooks/useIsPlatform";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { RouterOutputs } from "@calcom/trpc/react";
import classNames from "@calcom/ui/classNames";
import { Button } from "@calcom/ui/components/button";
import { Switch } from "@calcom/ui/components/form";
import { Icon } from "@calcom/ui/components/icon";
import { Section } from "@calcom/ui/components/section";
import type { CredentialOwner } from "../types";
import type { AppCardApp } from "../types";
import OmniInstallAppButton from "./OmniInstallAppButton";
export default function AppCard({
@@ -27,9 +26,7 @@ export default function AppCard({
hideSettingsIcon = false,
hideAppCardOptions = false,
}: {
app: RouterOutputs["viewer"]["apps"]["integrations"]["items"][number] & {
credentialOwner?: CredentialOwner;
};
app: AppCardApp;
description?: React.ReactNode;
switchChecked?: boolean;
switchOnClick?: (e: boolean) => void;
@@ -4,20 +4,15 @@ import type { GetAppData, SetAppData } from "@calcom/app-store/EventTypeAppConte
import EventTypeAppContext from "@calcom/app-store/EventTypeAppContext";
import { EventTypeAddonMap } from "@calcom/app-store/apps.browser.generated";
import type { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import type { RouterOutputs } from "@calcom/trpc/react";
import { ErrorBoundary } from "@calcom/ui/components/errorBoundary";
import type { EventTypeAppCardComponentProps, CredentialOwner } from "../types";
import type { EventTypeAppCardComponentProps, AppCardApp } from "../types";
import { DynamicComponent } from "./DynamicComponent";
export type EventTypeApp = RouterOutputs["viewer"]["apps"]["integrations"]["items"][number] & {
credentialOwner?: CredentialOwner;
};
export type EventTypeForAppCard = EventTypeAppCardComponentProps["eventType"];
export const EventTypeAppCard = (props: {
app: EventTypeApp;
app: AppCardApp;
eventType: EventTypeForAppCard;
getAppData: GetAppData;
setAppData: SetAppData;
@@ -1,9 +1,7 @@
import { render, screen } from "@testing-library/react";
import type { CredentialOwner } from "types";
import type { AppCardApp } from "types";
import { vi } from "vitest";
import type { RouterOutputs } from "@calcom/trpc";
import { DynamicComponent } from "./DynamicComponent";
import { EventTypeAppCard } from "./EventTypeAppCardInterface";
@@ -26,9 +24,7 @@ const mockProps = {
name: "TestApp",
slug: "testapp",
credentialOwner: {},
} as RouterOutputs["viewer"]["apps"]["integrations"]["items"][number] & {
credentialOwner?: CredentialOwner;
},
} as AppCardApp,
eventType: {},
getAppData: getAppDataMock,
setAppData: setAppDataMock,
-63
View File
@@ -1,63 +0,0 @@
import { z } from "zod";
// Copied from node_modules/stripe/types/Errors.d.ts because stripe lib doesn't expose these types
// Ref: https://github.com/stripe/stripe-node/blob/master/types/Errors.d.ts#L194
interface StripeInvalidRequestError extends Error {
readonly type: "StripeInvalidRequestError";
readonly rawType: "invalid_request_error";
/**
* A human-readable message giving more details about the error. For card errors, these messages can
* be shown to your users.
*/
readonly message: string;
/**
* For card errors, a short string describing the kind of card error that occurred.
*
* @docs https://stripe.com/docs/error-codes
*/
readonly code?: string;
/**
* A URL to more information about the error code reported.
*
* @docs https://stripe.com/docs/error-codes
*/
readonly doc_url?: string;
/**
* Typically a 4xx or 5xx.
*/
readonly statusCode?: number;
readonly raw: unknown;
readonly headers: {
[key: string]: string;
};
readonly requestId: string;
/**
* The parameter the error relates to if the error is parameter-specific. You can use this to display a
* message near the correct form field, for example.
*/
readonly param?: string;
}
const errorSchema = z.object({
name: z.string(),
message: z.string(),
stack: z.string().optional(),
});
export const stripeInvalidRequestErrorSchema = errorSchema.extend({
type: z.literal("StripeInvalidRequestError"),
rawType: z.literal("invalid_request_error"),
code: z.string().optional(),
doc_url: z.string().optional(),
statusCode: z.number().optional(),
raw: z.unknown(),
headers: z.record(z.string()),
requestId: z.string(),
param: z.string().optional(),
});
+1 -1
View File
@@ -16,7 +16,7 @@ function useIsAppEnabled(app: EventTypeAppCardApp) {
const credentialId = getAppData("credentialId");
const isAppEnabledForCredential =
isAppEnabled &&
(app.userCredentialIds.some((id) => id === credentialId) ||
(app.userCredentialIds.some((id: number) => id === credentialId) ||
app.credentialOwner.credentialId === credentialId);
return isAppEnabledForCredential ?? false; // Default to false if undefined
});
@@ -1 +0,0 @@
export { default } from "./trpc/_router";
+2
View File
@@ -1,3 +1,5 @@
export const MeetLocationType = "integrations:google:meet";
export const MSTeamsLocationType = "integrations:office365_video";
export const DailyLocationType = "integrations:daily";
@@ -1310,8 +1310,10 @@ describe("Date Optimization Benchmarks", () => {
)}ms, Speedup: ${speedupRatio.toFixed(1)}x`
);
const minSpeedup = process.env.CI ? 1.5 : 5; // Lower threshold for CI
expect(speedupRatio).toBeGreaterThan(minSpeedup);
if (!process.env.CI) {
const minSpeedup = 5; // Assert significant performance improvement (at least 5x faster)
expect(speedupRatio).toBeGreaterThan(minSpeedup);
}
}
});
+2 -1
View File
@@ -13,10 +13,12 @@ import type { EventLocationTypeFromAppMeta } from "../types/App";
import {
MeetLocationType as importedMeetLocationType,
MSTeamsLocationType as importedMSTeamsLocationType,
DailyLocationType as importedDailyLocationType,
} from "./constants";
export const MeetLocationType = importedMeetLocationType;
export const MSTeamsLocationType = importedMSTeamsLocationType;
export const DailyLocationType = importedDailyLocationType;
export type DefaultEventLocationType = {
default: true;
@@ -69,7 +71,6 @@ export type EventLocationTypeFromApp = Ensure<
export type EventLocationType = DefaultEventLocationType | EventLocationTypeFromApp;
export const DailyLocationType = "integrations:daily";
export const CalVideoLocationType = DailyLocationType;
/**
@@ -6,7 +6,7 @@ import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks";
import { sendGenericWebhookPayload } from "@calcom/features/webhooks/lib/sendPayload";
import { WebhookTriggerEvents } from "@calcom/prisma/enums";
import { _onFormSubmission } from "./utils";
import { _onFormSubmission } from "./formSubmissionUtils";
// Mock dependencies
vi.mock("@calcom/lib/getOrgIdFromMemberOrTeamId", () => ({
@@ -1,13 +1,244 @@
import type { Prisma } from "@prisma/client";
import type { App_RoutingForms_Form, User } from "@prisma/client";
import dayjs from "@calcom/dayjs";
import type { Tasker } from "@calcom/features/tasker/tasker";
import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks";
import { sendGenericWebhookPayload } from "@calcom/features/webhooks/lib/sendPayload";
import getOrgIdFromMemberOrTeamId from "@calcom/lib/getOrgIdFromMemberOrTeamId";
import logger from "@calcom/lib/logger";
import { withReporting } from "@calcom/lib/sentryWrapper";
import { prisma } from "@calcom/prisma";
import type { App_RoutingForms_Form } from "@calcom/prisma/client";
import { WebhookTriggerEvents } from "@calcom/prisma/enums";
import { RoutingFormSettings } from "@calcom/prisma/zod-utils";
import type { Ensure } from "@calcom/types/utils";
import { TRPCError } from "@trpc/server";
import { onFormSubmission } from "../trpc/utils";
import type { FormResponse, SerializableForm } from "../types/types";
import type { FormResponse, SerializableForm, SerializableField, OrderedResponses } from "../types/types";
const moduleLogger = logger.getSubLogger({ prefix: ["routing-forms/lib/formSubmissionUtils"] });
type SelectFieldWebhookResponse = string | number | string[] | { label: string; id: string | null };
export type FORM_SUBMITTED_WEBHOOK_RESPONSES = Record<
string,
{
/**
* Deprecates `value` prop as it now has both the id(that doesn't change) and the label(that can change but is human friendly)
*/
response: number | string | string[] | SelectFieldWebhookResponse | SelectFieldWebhookResponse[];
/**
* @deprecated Use `response` instead
*/
value: FormResponse[keyof FormResponse]["value"];
}
>;
function isOptionsField(field: Pick<SerializableField, "type" | "options">) {
return (field.type === "select" || field.type === "multiselect") && field.options;
}
export function getFieldResponse({
field,
fieldResponseValue,
}: {
fieldResponseValue: FormResponse[keyof FormResponse]["value"];
field: Pick<SerializableField, "type" | "options">;
}) {
if (!isOptionsField(field)) {
return {
value: fieldResponseValue,
response: fieldResponseValue,
};
}
if (!field.options) {
return {
value: fieldResponseValue,
response: fieldResponseValue,
};
}
const valueArray = fieldResponseValue instanceof Array ? fieldResponseValue : [fieldResponseValue];
const chosenOptions = valueArray.map((idOrLabel) => {
const foundOptionById = field.options?.find((option) => {
return option.id === idOrLabel;
});
if (foundOptionById) {
return {
label: foundOptionById.label,
id: foundOptionById.id,
};
} else {
return {
label: idOrLabel.toString(),
id: null,
};
}
});
return {
// value is a legacy prop that is just sending the labels which can change
value: chosenOptions.map((option) => option.label),
// response is new prop that is sending the label along with id(which doesn't change)
response: chosenOptions,
};
}
export const sendResponseEmail = async (
form: Pick<App_RoutingForms_Form, "id" | "name" | "fields">,
orderedResponses: OrderedResponses,
toAddresses: string[]
) => {
try {
if (typeof window === "undefined") {
const { default: ResponseEmail } = await import("../emails/templates/response-email");
const email = new ResponseEmail({ form: form, toAddresses, orderedResponses });
await email.sendEmail();
}
} catch (e) {
moduleLogger.error("Error sending response email", e);
}
};
function getWebhookTargetEntity(form: { teamId?: number | null; user: { id: number } }) {
// If it's a team form, the target must be team webhook
// If it's a user form, the target must be user webhook
const isTeamForm = form.teamId;
return { userId: isTeamForm ? null : form.user.id, teamId: isTeamForm ? form.teamId : null };
}
/**
* Not called in preview mode or dry run mode
* It takes care of sending webhooks and emails for form submissions
*/
export async function _onFormSubmission(
form: Ensure<
SerializableForm<App_RoutingForms_Form> & { user: Pick<User, "id" | "email">; userWithEmails?: string[] },
"fields"
>,
response: FormResponse,
responseId: number,
chosenAction?: {
type: "customPageMessage" | "externalRedirectUrl" | "eventTypeRedirectUrl";
value: string;
}
) {
const fieldResponsesByIdentifier: FORM_SUBMITTED_WEBHOOK_RESPONSES = {};
for (const [fieldId, fieldResponse] of Object.entries(response)) {
const field = form.fields.find((f) => f.id === fieldId);
if (!field) {
throw new Error(`Field with id ${fieldId} not found`);
}
// Use the label lowercased as the key to identify a field.
// TODO: We seem to be using label from the response, Can we not use the field.label
const key =
form.fields.find((f) => f.id === fieldId)?.identifier ||
(fieldResponse.label as keyof typeof fieldResponsesByIdentifier);
fieldResponsesByIdentifier[key] = getFieldResponse({
fieldResponseValue: fieldResponse.value,
field,
});
}
const { userId, teamId } = getWebhookTargetEntity(form);
const orgId = await getOrgIdFromMemberOrTeamId({ memberId: userId, teamId });
const subscriberOptionsFormSubmitted = {
userId,
teamId,
orgId,
triggerEvent: WebhookTriggerEvents.FORM_SUBMITTED,
};
const subscriberOptionsFormSubmittedNoEvent = {
userId,
teamId,
orgId,
triggerEvent: WebhookTriggerEvents.FORM_SUBMITTED_NO_EVENT,
};
const webhooksFormSubmitted = await getWebhooks(subscriberOptionsFormSubmitted);
const webhooksFormSubmittedNoEvent = await getWebhooks(subscriberOptionsFormSubmittedNoEvent);
const promisesFormSubmitted = webhooksFormSubmitted.map((webhook) => {
sendGenericWebhookPayload({
secretKey: webhook.secret,
triggerEvent: "FORM_SUBMITTED",
createdAt: new Date().toISOString(),
webhook,
data: {
formId: form.id,
formName: form.name,
teamId: form.teamId,
responses: fieldResponsesByIdentifier,
},
rootData: {
// Send responses unwrapped at root level for backwards compatibility
...Object.entries(fieldResponsesByIdentifier).reduce((acc, [key, value]) => {
acc[key] = value.value;
return acc;
}, {} as Record<string, FormResponse[keyof FormResponse]["value"]>),
},
}).catch((e) => {
console.error(`Error executing routing form webhook`, webhook, e);
});
});
if (typeof window === "undefined") {
try {
const tasker: Tasker = await (await import("@calcom/features/tasker")).default;
const promisesFormSubmittedNoEvent = webhooksFormSubmittedNoEvent.map((webhook) => {
const scheduledAt = dayjs().add(15, "minute").toDate();
return tasker.create(
"triggerFormSubmittedNoEventWebhook",
{
responseId,
form: {
id: form.id,
name: form.name,
teamId: form.teamId ?? null,
},
responses: fieldResponsesByIdentifier,
redirect: chosenAction,
webhook,
},
{ scheduledAt }
);
});
const promises = [...promisesFormSubmitted, ...promisesFormSubmittedNoEvent];
await Promise.all(promises);
const orderedResponses = form.fields.reduce((acc, field) => {
acc.push(response[field.id]);
return acc;
}, [] as OrderedResponses);
if (form.teamId) {
if (form.userWithEmails?.length) {
moduleLogger.debug(
`Preparing to send Form Response email for Form:${form.id} to users: ${form.userWithEmails.join(
","
)}`
);
await sendResponseEmail(form, orderedResponses, form.userWithEmails);
}
} else if (form.settings?.emailOwnerOnSubmission) {
moduleLogger.debug(
`Preparing to send Form Response email for Form:${form.id} to form owner: ${form.user.email}`
);
await sendResponseEmail(form, orderedResponses, [form.user.email]);
}
} catch (e) {
moduleLogger.error("Error triggering routing form response side effects", e);
}
}
}
export const onFormSubmission = withReporting(_onFormSubmission, "onFormSubmission");
export type TargetRoutingFormForResponse = SerializableForm<
App_RoutingForms_Form & {
@@ -13,10 +13,10 @@ import type { z } from "zod";
import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider";
import { areTheySiblingEntities } from "@calcom/lib/entityPermissionUtils.shared";
import type { EventTypesByViewer } from "@calcom/lib/event-types/getEventTypesByViewer";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { buildEmptyQueryValue, raqbQueryValueUtils } from "@calcom/lib/raqb/raqbUtils";
import { SchedulingType } from "@calcom/prisma/enums";
import type { RouterOutputs } from "@calcom/trpc/react";
import { trpc } from "@calcom/trpc/react";
import type { inferSSRProps } from "@calcom/types/inferSSRProps";
import classNames from "@calcom/ui/classNames";
@@ -63,8 +63,6 @@ import type {
import type { zodRoutes } from "../../zod";
import { RouteActionType } from "../../zod";
type EventTypesByGroup = RouterOutputs["viewer"]["eventTypes"]["getByViewer"];
type Form = inferSSRProps<typeof getServerSideProps>["form"];
type SetRoute = (id: string, route: Partial<EditFormRoute>) => void;
@@ -141,7 +139,7 @@ const buildEventsData = ({
form,
route,
}: {
eventTypesByGroup: EventTypesByGroup | undefined;
eventTypesByGroup: EventTypesByViewer | undefined;
form: Form;
route: EditFormRoute;
}) => {
@@ -372,7 +370,7 @@ const Route = ({
moveDown?: { fn: () => void; check: () => boolean } | null;
appUrl: string;
disabled?: boolean;
eventTypesByGroup: EventTypesByGroup;
eventTypesByGroup: EventTypesByViewer;
attributes?: Attribute[];
cardOptions?: {
collapsible?: boolean;
@@ -1129,7 +1127,7 @@ const Routes = ({
hookForm: UseFormReturn<RoutingFormWithResponseCount>;
appUrl: string;
attributes?: Attribute[];
eventTypesByGroup: EventTypesByGroup;
eventTypesByGroup: EventTypesByViewer;
}) => {
const { routes: serializedRoutes } = hookForm.getValues();
const { t } = useLocale();
@@ -1 +0,0 @@
export { default } from "./trpc/_router";
@@ -1,237 +0,0 @@
import type { App_RoutingForms_Form, User } from "@prisma/client";
import dayjs from "@calcom/dayjs";
import type { Tasker } from "@calcom/features/tasker/tasker";
import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks";
import { sendGenericWebhookPayload } from "@calcom/features/webhooks/lib/sendPayload";
import getOrgIdFromMemberOrTeamId from "@calcom/lib/getOrgIdFromMemberOrTeamId";
import logger from "@calcom/lib/logger";
import { withReporting } from "@calcom/lib/sentryWrapper";
import { WebhookTriggerEvents } from "@calcom/prisma/enums";
import type { Ensure } from "@calcom/types/utils";
import type { SerializableField, OrderedResponses } from "../types/types";
import type { FormResponse, SerializableForm } from "../types/types";
const moduleLogger = logger.getSubLogger({ prefix: ["routing-forms/trpc/utils"] });
type SelectFieldWebhookResponse = string | number | string[] | { label: string; id: string | null };
export type FORM_SUBMITTED_WEBHOOK_RESPONSES = Record<
string,
{
/**
* Deprecates `value` prop as it now has both the id(that doesn't change) and the label(that can change but is human friendly)
*/
response: number | string | string[] | SelectFieldWebhookResponse | SelectFieldWebhookResponse[];
/**
* @deprecated Use `response` instead
*/
value: FormResponse[keyof FormResponse]["value"];
}
>;
function isOptionsField(field: Pick<SerializableField, "type" | "options">) {
return (field.type === "select" || field.type === "multiselect") && field.options;
}
export function getFieldResponse({
field,
fieldResponseValue,
}: {
fieldResponseValue: FormResponse[keyof FormResponse]["value"];
field: Pick<SerializableField, "type" | "options">;
}) {
if (!isOptionsField(field)) {
return {
value: fieldResponseValue,
response: fieldResponseValue,
};
}
if (!field.options) {
return {
value: fieldResponseValue,
response: fieldResponseValue,
};
}
const valueArray = fieldResponseValue instanceof Array ? fieldResponseValue : [fieldResponseValue];
const chosenOptions = valueArray.map((idOrLabel) => {
const foundOptionById = field.options?.find((option) => {
return option.id === idOrLabel;
});
if (foundOptionById) {
return {
label: foundOptionById.label,
id: foundOptionById.id,
};
} else {
return {
label: idOrLabel.toString(),
id: null,
};
}
});
return {
// value is a legacy prop that is just sending the labels which can change
value: chosenOptions.map((option) => option.label),
// response is new prop that is sending the label along with id(which doesn't change)
response: chosenOptions,
};
}
/**
* Not called in preview mode or dry run mode
* It takes care of sending webhooks and emails for form submissions
*/
export async function _onFormSubmission(
form: Ensure<
SerializableForm<App_RoutingForms_Form> & { user: Pick<User, "id" | "email">; userWithEmails?: string[] },
"fields"
>,
response: FormResponse,
responseId: number,
chosenAction?: {
type: "customPageMessage" | "externalRedirectUrl" | "eventTypeRedirectUrl";
value: string;
}
) {
const fieldResponsesByIdentifier: FORM_SUBMITTED_WEBHOOK_RESPONSES = {};
for (const [fieldId, fieldResponse] of Object.entries(response)) {
const field = form.fields.find((f) => f.id === fieldId);
if (!field) {
throw new Error(`Field with id ${fieldId} not found`);
}
// Use the label lowercased as the key to identify a field.
// TODO: We seem to be using label from the response, Can we not use the field.label
const key =
form.fields.find((f) => f.id === fieldId)?.identifier ||
(fieldResponse.label as keyof typeof fieldResponsesByIdentifier);
fieldResponsesByIdentifier[key] = getFieldResponse({
fieldResponseValue: fieldResponse.value,
field,
});
}
const { userId, teamId } = getWebhookTargetEntity(form);
const orgId = await getOrgIdFromMemberOrTeamId({ memberId: userId, teamId });
const subscriberOptionsFormSubmitted = {
userId,
teamId,
orgId,
triggerEvent: WebhookTriggerEvents.FORM_SUBMITTED,
};
const subscriberOptionsFormSubmittedNoEvent = {
userId,
teamId,
orgId,
triggerEvent: WebhookTriggerEvents.FORM_SUBMITTED_NO_EVENT,
};
const webhooksFormSubmitted = await getWebhooks(subscriberOptionsFormSubmitted);
const webhooksFormSubmittedNoEvent = await getWebhooks(subscriberOptionsFormSubmittedNoEvent);
const promisesFormSubmitted = webhooksFormSubmitted.map((webhook) => {
sendGenericWebhookPayload({
secretKey: webhook.secret,
triggerEvent: "FORM_SUBMITTED",
createdAt: new Date().toISOString(),
webhook,
data: {
formId: form.id,
formName: form.name,
teamId: form.teamId,
responses: fieldResponsesByIdentifier,
},
rootData: {
// Send responses unwrapped at root level for backwards compatibility
...Object.entries(fieldResponsesByIdentifier).reduce((acc, [key, value]) => {
acc[key] = value.value;
return acc;
}, {} as Record<string, FormResponse[keyof FormResponse]["value"]>),
},
}).catch((e) => {
console.error(`Error executing routing form webhook`, webhook, e);
});
});
if (typeof window === "undefined") {
try {
const tasker: Tasker = await (await import("@calcom/features/tasker")).default;
const promisesFormSubmittedNoEvent = webhooksFormSubmittedNoEvent.map((webhook) => {
const scheduledAt = dayjs().add(15, "minute").toDate();
return tasker.create(
"triggerFormSubmittedNoEventWebhook",
{
responseId,
form: {
id: form.id,
name: form.name,
teamId: form.teamId ?? null,
},
responses: fieldResponsesByIdentifier,
redirect: chosenAction,
webhook,
},
{ scheduledAt }
);
});
const promises = [...promisesFormSubmitted, ...promisesFormSubmittedNoEvent];
await Promise.all(promises);
const orderedResponses = form.fields.reduce((acc, field) => {
acc.push(response[field.id]);
return acc;
}, [] as OrderedResponses);
if (form.teamId) {
if (form.userWithEmails?.length) {
moduleLogger.debug(
`Preparing to send Form Response email for Form:${form.id} to users: ${form.userWithEmails.join(
","
)}`
);
await sendResponseEmail(form, orderedResponses, form.userWithEmails);
}
} else if (form.settings?.emailOwnerOnSubmission) {
moduleLogger.debug(
`Preparing to send Form Response email for Form:${form.id} to form owner: ${form.user.email}`
);
await sendResponseEmail(form, orderedResponses, [form.user.email]);
}
} catch (e) {
moduleLogger.error("Error triggering routing form response side effects", e);
}
}
}
export const onFormSubmission = withReporting(_onFormSubmission, "onFormSubmission");
export const sendResponseEmail = async (
form: Pick<App_RoutingForms_Form, "id" | "name" | "fields">,
orderedResponses: OrderedResponses,
toAddresses: string[]
) => {
try {
if (typeof window === "undefined") {
const { default: ResponseEmail } = await import("../emails/templates/response-email");
const email = new ResponseEmail({ form: form, toAddresses, orderedResponses });
await email.sendEmail();
}
} catch (e) {
moduleLogger.error("Error sending response email", e);
}
};
function getWebhookTargetEntity(form: { teamId?: number | null; user: { id: number } }) {
// If it's a team form, the target must be team webhook
// If it's a user form, the target must be user webhook
const isTeamForm = form.teamId;
return { userId: isTeamForm ? null : form.user.id, teamId: isTeamForm ? form.teamId : null };
}
+5 -2
View File
@@ -1,8 +1,8 @@
import type React from "react";
import type { z } from "zod";
import type { ConnectedApps } from "@calcom/lib/getConnectedApps";
import type { EventTypeFormMetadataSchema } from "@calcom/prisma/zod-utils";
import type { RouterOutputs } from "@calcom/trpc/react";
import type { ButtonProps } from "@calcom/ui/components/button";
export type IntegrationOAuthCallbackState = {
@@ -22,8 +22,11 @@ export type CredentialOwner = {
readOnly?: boolean;
};
export type EventTypeAppCardApp = RouterOutputs["viewer"]["apps"]["integrations"]["items"][number] & {
export type AppCardApp = ConnectedApps[number] & {
credentialOwner?: CredentialOwner;
};
export type EventTypeAppCardApp = AppCardApp & {
credentialIds?: number[];
};
@@ -3,7 +3,7 @@ import { useCallback, useState } from "react";
import { InstallAppButton } from "@calcom/app-store/InstallAppButton";
import { AppSettings } from "@calcom/app-store/_components/AppSettings";
import { getLocationFromApp, type EventLocationType } from "@calcom/app-store/locations";
import type { CredentialOwner } from "@calcom/app-store/types";
import type { AppCardApp } from "@calcom/app-store/types";
import AppListCard from "@calcom/features/apps/components/AppListCard";
import type { UpdateUsersDefaultConferencingAppParams } from "@calcom/features/apps/components/AppSetDefaultLinkDialog";
import { AppSetDefaultLinkDialog } from "@calcom/features/apps/components/AppSetDefaultLinkDialog";
@@ -69,13 +69,7 @@ export const AppList = ({
showToast("Default app updated successfully", "success");
}, []);
const ChildAppCard = ({
item,
}: {
item: RouterOutputs["viewer"]["apps"]["integrations"]["items"][number] & {
credentialOwner?: CredentialOwner;
};
}) => {
const ChildAppCard = ({ item }: { item: AppCardApp }) => {
const appSlug = item?.slug;
const appIsDefault =
appSlug === defaultConferencingApp?.appSlug ||
@@ -1,8 +1,8 @@
import type { Prisma, WorkflowReminder } from "@prisma/client";
import type { z } from "zod";
import { DailyLocationType } from "@calcom/app-store/constants";
import { FAKE_DAILY_CREDENTIAL } from "@calcom/app-store/dailyvideo/lib/VideoApiAdapter";
import { DailyLocationType } from "@calcom/app-store/locations";
import dayjs from "@calcom/dayjs";
import { sendCancelledEmailsAndSMS } from "@calcom/emails";
import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses";
@@ -18,6 +18,7 @@ import { CalendarEventBuilder } from "@calcom/features/CalendarEventBuilder";
import type { BookingDataSchemaGetter } from "@calcom/features/bookings/lib/dto/types";
import type { CreateRegularBookingData, CreateBookingMeta } from "@calcom/features/bookings/lib/dto/types";
import type { CheckBookingAndDurationLimitsService } from "@calcom/features/bookings/lib/handleNewBooking/checkBookingAndDurationLimits";
import { handlePayment } from "@calcom/features/bookings/lib/handlePayment";
import { handleWebhookTrigger } from "@calcom/features/bookings/lib/handleWebhookTrigger";
import { isEventTypeLoggingEnabled } from "@calcom/features/bookings/lib/isEventTypeLoggingEnabled";
import type { CacheService } from "@calcom/features/calendar-cache/lib/getShouldServeCache";
@@ -57,7 +58,6 @@ import { getTeamIdFromEventType } from "@calcom/lib/getTeamIdFromEventType";
import { HttpError } from "@calcom/lib/http-error";
import type { CheckBookingLimitsService } from "@calcom/lib/intervalLimits/server/checkBookingLimits";
import logger from "@calcom/lib/logger";
import { handlePayment } from "@calcom/lib/payment/handlePayment";
import { getPiiFreeCalendarEvent, getPiiFreeEventType } from "@calcom/lib/piiFreeData";
import { safeStringify } from "@calcom/lib/safeStringify";
import { getTranslation } from "@calcom/lib/server/i18n";
@@ -1,7 +1,7 @@
import type { Booking, User, Webhook } from "@prisma/client";
import { afterAll, beforeAll, describe, expect, test, vi } from "vitest";
import { DailyLocationType } from "@calcom/app-store/locations";
import { DailyLocationType } from "@calcom/app-store/constants";
import { getMeetingSessionsFromRoomName } from "@calcom/features/tasker/tasks/triggerNoShow/getMeetingSessionsFromRoomName";
import { triggerHostNoShow } from "@calcom/features/tasker/tasks/triggerNoShow/triggerHostNoShow";
import { sendGenericWebhookPayload } from "@calcom/features/webhooks/lib/sendPayload";
@@ -1,4 +1,4 @@
import { DailyLocationType } from "@calcom/app-store/locations";
import { DailyLocationType } from "@calcom/app-store/constants";
import dayjs from "@calcom/dayjs";
import tasker from "@calcom/features/tasker";
import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks";
@@ -4,6 +4,7 @@ import { uuid } from "short-uuid";
import { sendScheduledSeatsEmailsAndSMS } from "@calcom/emails";
import { refreshCredentials } from "@calcom/features/bookings/lib/getAllCredentialsForUsersOnEvent/refreshCredentials";
import { handlePayment } from "@calcom/features/bookings/lib/handlePayment";
import {
allowDisablingAttendeeConfirmationEmails,
allowDisablingHostConfirmationEmails,
@@ -11,7 +12,6 @@ import {
import EventManager from "@calcom/lib/EventManager";
import { ErrorCode } from "@calcom/lib/errorCodes";
import { HttpError } from "@calcom/lib/http-error";
import { handlePayment } from "@calcom/lib/payment/handlePayment";
import prisma from "@calcom/prisma";
import { BookingStatus } from "@calcom/prisma/enums";
import { eventTypeAppMetadataOptionalSchema } from "@calcom/prisma/zod-utils";
@@ -1,16 +1,14 @@
import stripe from "@calcom/features/ee/payments/server/stripe";
import {
ZStripeCheckoutSessionInputSchema,
type TStripeCheckoutSessionInputSchema,
} from "@calcom/trpc/server/routers/publicViewer/stripeCheckoutSession.schema";
type StripeCheckoutSessionInput = {
checkoutSessionId?: string;
stripeCustomerId?: string;
};
export class StripeService {
static async getCheckoutSession(input: TStripeCheckoutSessionInputSchema) {
static async getCheckoutSession(input: StripeCheckoutSessionInput) {
const { checkoutSessionId, stripeCustomerId } = input;
// Moved the following data checks to superRefine
ZStripeCheckoutSessionInputSchema.parse(input);
let customerId: string;
let isPremiumUsername = false;
let hasPaymentFailed = false;
@@ -70,7 +70,7 @@ const DuplicateDialog = () => {
}
}, [searchParams?.get("dialog")]);
const duplicateMutation = trpc.viewer.eventTypes.duplicate.useMutation({
const duplicateMutation = trpc.viewer.eventTypes.heavy.duplicate.useMutation({
onSuccess: async ({ eventType }) => {
await router.replace(`/event-types/${eventType.id}`);
+1 -1
View File
@@ -178,7 +178,7 @@ export type LocationFormValues = Pick<FormValues, "id" | "locations" | "bookingF
export type EventTypeAssignedUsers = RouterOutputs["viewer"]["eventTypes"]["get"]["eventType"]["children"];
export type EventTypeHosts = RouterOutputs["viewer"]["eventTypes"]["get"]["eventType"]["hosts"];
export type EventTypeUpdateInput = RouterInputs["viewer"]["eventTypes"]["update"];
export type EventTypeUpdateInput = RouterInputs["viewer"]["eventTypes"]["heavy"]["update"];
export type TabMap = {
advanced: React.ReactNode;
ai?: React.ReactNode;
@@ -1,7 +1,7 @@
import { z } from "zod";
import type { FORM_SUBMITTED_WEBHOOK_RESPONSES } from "@calcom/app-store/routing-forms/lib/formSubmissionUtils";
import incompleteBookingActionFunctions from "@calcom/app-store/routing-forms/lib/incompleteBooking/actionFunctions";
import type { FORM_SUBMITTED_WEBHOOK_RESPONSES } from "@calcom/app-store/routing-forms/trpc/utils";
import { sendGenericWebhookPayload } from "@calcom/features/webhooks/lib/sendPayload";
import prisma from "@calcom/prisma";
+7 -4
View File
@@ -157,11 +157,12 @@ const getSeatReferenceId = (calEvent: Pick<CalendarEvent, "attendeeSeatId">): st
};
export const getBookingUrl = (calEvent: CalendarEvent) => {
const seatReferenceUid = getSeatReferenceId(calEvent);
if (calEvent.platformClientId) {
if (!calEvent.platformBookingUrl) return "";
return `${calEvent.platformBookingUrl}/${getUid(calEvent)}?slug=${calEvent.type}&username=${
calEvent.organizer.username
}&changes=true`;
}${seatReferenceUid ? `&seatReferenceUid=${seatReferenceUid}` : ""}&changes=true`;
}
return `${calEvent.bookerUrl ?? WEBAPP_URL}/booking/${getUid(calEvent)}?changes=true`;
@@ -174,6 +175,8 @@ export const getPlatformManageLink = (
t: TFunction
) => {
const shouldDisplayReschedule = !calEvent.recurringEvent && calEvent.platformRescheduleUrl;
const seatUid = getSeatReferenceId(calEvent);
let res =
calEvent.platformBookingUrl || shouldDisplayReschedule || calEvent.platformCancelUrl
? `${t("need_to_reschedule_or_cancel")} `
@@ -181,9 +184,9 @@ export const getPlatformManageLink = (
if (calEvent.platformBookingUrl) {
res += `Check Here: ${calEvent.platformBookingUrl}/${getUid(calEvent)}?slug=${calEvent.type}&username=${
calEvent.organizer.username
}${calEvent?.team ? `&teamId=${calEvent.team.id}` : ""}&changes=true${
calEvent.platformCancelUrl || shouldDisplayReschedule ? ` ${t("or_lowercase")} ` : ""
}`;
}${calEvent?.team ? `&teamId=${calEvent.team.id}` : ""}${
seatUid ? `&seatReferenceUid=${seatUid}` : ""
}&changes=true${calEvent.platformCancelUrl || shouldDisplayReschedule ? ` ${t("or_lowercase")} ` : ""}`;
}
if (calEvent.platformCancelUrl) {
res += `${t("cancel")}: ${getCancelLink(calEvent)}`;
+1 -1
View File
@@ -1,6 +1,6 @@
import type { Prisma, SelectedCalendar } from "@prisma/client";
import { DailyLocationType } from "@calcom/app-store/locations";
import { DailyLocationType } from "@calcom/app-store/constants";
import slugify from "@calcom/lib/slugify";
import { PeriodType, SchedulingType } from "@calcom/prisma/enums";
import type { userSelect } from "@calcom/prisma/selects";
+1 -1
View File
@@ -46,7 +46,7 @@ export const useCreateEventType = (
const [searchTerm, setSearchTerm] = useState("");
const debouncedSearchTerm = useDebounce(searchTerm, 500);
const createMutation = trpc.viewer.eventTypes.create.useMutation({
const createMutation = trpc.viewer.eventTypes.heavy.create.useMutation({
onSuccess: async ({ eventType }) => {
onSuccessMutation(eventType);
+1 -1
View File
@@ -1,5 +1,5 @@
import getAppKeysFromSlug from "@calcom/app-store/_utils/getAppKeysFromSlug";
import { DailyLocationType } from "@calcom/app-store/locations";
import { DailyLocationType } from "@calcom/app-store/constants";
import getApps from "@calcom/app-store/utils";
import { getUsersCredentialsIncludeServiceAccountKey } from "@calcom/lib/server/getUsersCredentials";
import { userMetadata as userMetadataSchema } from "@calcom/prisma/zod-utils";
@@ -2,7 +2,6 @@ import { Prisma } from "@prisma/client";
import type { ZodIssue } from "zod";
import { ZodError } from "zod";
import { stripeInvalidRequestErrorSchema } from "@calcom/app-store/_utils/stripe.types";
import { ErrorCode } from "@calcom/lib/errorCodes";
import { ErrorWithCode } from "@calcom/lib/errors";
@@ -11,6 +10,7 @@ import { getHTTPStatusCodeFromError } from "@trpc/server/http";
import { HttpError } from "../http-error";
import { redactError } from "../redactError";
import { stripeInvalidRequestErrorSchema } from "../stripe-error";
function hasName(cause: unknown): cause is { name: string } {
return !!cause && typeof cause === "object" && "name" in cause;
+19
View File
@@ -0,0 +1,19 @@
import { z } from "zod";
const errorSchema = z.object({
name: z.string(),
message: z.string(),
stack: z.string().optional(),
});
export const stripeInvalidRequestErrorSchema = errorSchema.extend({
type: z.literal("StripeInvalidRequestError"),
rawType: z.literal("invalid_request_error"),
code: z.string().optional(),
doc_url: z.string().optional(),
statusCode: z.number().optional(),
raw: z.unknown(),
headers: z.record(z.string()),
requestId: z.string(),
param: z.string().optional(),
});
+1 -1
View File
@@ -1,8 +1,8 @@
import short from "short-uuid";
import { v5 as uuidv5 } from "uuid";
import { DailyLocationType } from "@calcom/app-store/constants";
import { getDailyAppKeys } from "@calcom/app-store/dailyvideo/lib/getDailyAppKeys";
import { DailyLocationType } from "@calcom/app-store/locations";
import { VideoApiAdapterMap } from "@calcom/app-store/video.adapters.generated";
import { sendBrokenIntegrationEmail } from "@calcom/emails";
import { getUid } from "@calcom/lib/CalEventParser";
@@ -131,7 +131,7 @@ const EventTypeWeb = ({
teamId: eventType.team?.id || eventType.parent?.teamId,
onlyInstalled: true,
});
const updateMutation = trpc.viewer.eventTypes.update.useMutation({
const updateMutation = trpc.viewer.eventTypes.heavy.update.useMutation({
onSuccess: async () => {
const currentValues = form.getValues();
@@ -22,6 +22,7 @@ export const useConnectedCalendars = (props: { enabled?: boolean }) => {
});
},
enabled: props?.enabled !== undefined ? props.enabled && isInit : isInit,
staleTime: 5000,
});
return calendars;
+4 -4
View File
@@ -4,10 +4,10 @@ export { getPublicEvent, type PublicEventType } from "@calcom/features/eventtype
export { getBulkUserEventTypes, getBulkTeamEventTypes } from "@calcom/lib/event-types/getBulkEventTypes";
export { createHandler as createEventType } from "@calcom/trpc/server/routers/viewer/eventTypes/create.handler";
export { updateHandler as updateEventType } from "@calcom/trpc/server/routers/viewer/eventTypes/update.handler";
export { createHandler as createEventType } from "@calcom/trpc/server/routers/viewer/eventTypes/heavy/create.handler";
export { updateHandler as updateEventType } from "@calcom/trpc/server/routers/viewer/eventTypes/heavy/update.handler";
export type { TUpdateInputSchema as TUpdateEventTypeInputSchema } from "@calcom/trpc/server/routers/viewer/eventTypes/update.schema";
export type { TUpdateInputSchema as TUpdateEventTypeInputSchema } from "@calcom/trpc/server/routers/viewer/eventTypes/heavy/update.schema";
export type { EventTypesPublic } from "@calcom/lib/event-types/getEventTypesPublic";
export { getEventTypesPublic } from "@calcom/lib/event-types/getEventTypesPublic";
export { parseEventTypeColor } from "@calcom/lib/isEventTypeColor";
@@ -24,7 +24,7 @@ export { getEventTypeById } from "@calcom/lib/event-types/getEventTypeById";
export { getEventTypesByViewer } from "@calcom/lib/event-types/getEventTypesByViewer";
export type { EventType } from "@calcom/lib/event-types/getEventTypeById";
export type { EventTypesByViewer } from "@calcom/lib/event-types/getEventTypesByViewer";
export type { UpdateEventTypeReturn } from "@calcom/trpc/server/routers/viewer/eventTypes/update.handler";
export type { UpdateEventTypeReturn } from "@calcom/trpc/server/routers/viewer/eventTypes/heavy/update.handler";
export { updateNewTeamMemberEventTypes } from "@calcom/lib/server/queries/teams";
export { bulkUpdateEventsToDefaultLocation } from "@calcom/lib/bulkUpdateEventsToDefaultLocation";
+1 -1
View File
@@ -11,9 +11,9 @@ import {
import { handleCreatePhoneCall } from "@calcom/features/handleCreatePhoneCall";
import handleMarkNoShow from "@calcom/features/handleMarkNoShow";
import * as instantMeetingMethods from "@calcom/features/instant-meeting/handleInstantMeeting";
import { getRoutedUrl } from "@calcom/features/routing-forms/lib/getRoutedUrl";
import getAllUserBookings from "@calcom/lib/bookings/getAllUserBookings";
import { symmetricEncrypt, symmetricDecrypt } from "@calcom/lib/crypto";
import { getRoutedUrl } from "@calcom/lib/server/getRoutedUrl";
import { getTeamMemberEmailForResponseOrContactUsingUrlQuery } from "@calcom/lib/server/getTeamMemberEmailFromCrm";
import { getTranslation } from "@calcom/lib/server/i18n";
import type { Prisma } from "@calcom/prisma/client";
+1
View File
@@ -16,6 +16,7 @@ export const ENDPOINTS = [
"deploymentSetup",
"dsync",
"eventTypes",
"eventTypes/heavy",
"features",
"i18n",
"insights",
+4
View File
@@ -27,6 +27,7 @@ const resolveEndpoint = (links: any) => {
// to the correct API endpoints.
// - viewer.me - 2 segment paths like this are for logged in requests
// - viewer.public.i18n - 3 segments paths can be public or authed
// - viewer.eventTypes.heavy.create - 4 segments paths for heavy sub-router
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return (ctx: any) => {
const parts = ctx.op.path.split(".");
@@ -35,6 +36,9 @@ const resolveEndpoint = (links: any) => {
if (parts.length == 2) {
endpoint = parts[0] as keyof typeof links;
path = parts[1];
} else if (parts.length >= 3 && parts[2] === "heavy") {
endpoint = parts[1] + "/heavy" as keyof typeof links;
path = parts[3];
} else {
endpoint = parts[1] as keyof typeof links;
path = parts.splice(2, parts.length - 2).join(".");
@@ -1,12 +1,12 @@
import getAppKeysFromSlug from "@calcom/app-store/_utils/getAppKeysFromSlug";
import { refreshAccessToken } from "@calcom/app-store/basecamp3/lib/helpers";
import type { BasecampToken } from "@calcom/app-store/basecamp3/lib/types";
import type { PrismaClient } from "@calcom/prisma/client";
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
import type { TrpcSessionUser } from "@calcom/trpc/server/types";
import { TRPCError } from "@trpc/server";
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
import { refreshAccessToken } from "../lib/helpers";
import type { BasecampToken } from "../lib/types";
import type { TProjectMutationInputSchema } from "./projectMutation.schema";
interface ProjectMutationHandlerOptions {
@@ -1,13 +1,12 @@
import getAppKeysFromSlug from "@calcom/app-store/_utils/getAppKeysFromSlug";
import { refreshAccessToken } from "@calcom/app-store/basecamp3/lib/helpers";
import type { BasecampToken } from "@calcom/app-store/basecamp3/lib/types";
import type { PrismaClient } from "@calcom/prisma/client";
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
import type { TrpcSessionUser } from "@calcom/trpc/server/types";
import { TRPCError } from "@trpc/server";
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
import { refreshAccessToken } from "../lib/helpers";
import type { BasecampToken } from "../lib/types";
interface ProjectsHandlerOptions {
ctx: {
prisma: PrismaClient;
@@ -1,3 +1,4 @@
import getConnectedForms from "@calcom/app-store/routing-forms/lib/getConnectedForms";
import { entityPrismaWhereClause } from "@calcom/lib/entityPermissionUtils.server";
import type { PrismaClient } from "@calcom/prisma";
import { MembershipRole } from "@calcom/prisma/enums";
@@ -5,7 +6,6 @@ import type { TrpcSessionUser } from "@calcom/trpc/server/types";
import { TRPCError } from "@trpc/server";
import getConnectedForms from "../lib/getConnectedForms";
import type { TDeleteFormInputSchema } from "./deleteForm.schema";
import { checkPermissionOnExistingRoutingForm } from "./permissions";
@@ -1,6 +1,13 @@
import type { App_RoutingForms_Form } from "@prisma/client";
import { Prisma } from "@prisma/client";
import { createFallbackRoute } from "@calcom/app-store/routing-forms/lib/createFallbackRoute";
import { getSerializableForm } from "@calcom/app-store/routing-forms/lib/getSerializableForm";
import { isFallbackRoute } from "@calcom/app-store/routing-forms/lib/isFallbackRoute";
import isRouter from "@calcom/app-store/routing-forms/lib/isRouter";
import isRouterLinkedField from "@calcom/app-store/routing-forms/lib/isRouterLinkedField";
import type { SerializableForm } from "@calcom/app-store/routing-forms/types/types";
import { zodFields, zodRouterRoute, zodRoutes } from "@calcom/app-store/routing-forms/zod";
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
import { entityPrismaWhereClause, canEditEntity } from "@calcom/lib/entityPermissionUtils.server";
import type { PrismaClient } from "@calcom/prisma";
@@ -9,14 +16,6 @@ import type { TrpcSessionUser } from "@calcom/trpc/server/types";
import { TRPCError } from "@trpc/server";
import { createFallbackRoute } from "../lib/createFallbackRoute";
import { getSerializableForm } from "../lib/getSerializableForm";
import { isFallbackRoute } from "../lib/isFallbackRoute";
import { isFormCreateEditAllowed } from "../lib/isFormCreateEditAllowed";
import isRouter from "../lib/isRouter";
import isRouterLinkedField from "../lib/isRouterLinkedField";
import type { SerializableForm } from "../types/types";
import { zodFields, zodRouterRoute, zodRoutes } from "../zod";
import type { TFormMutationInputSchema } from "./formMutation.schema";
import { checkPermissionOnExistingRoutingForm } from "./permissions";
@@ -69,8 +68,8 @@ export const formMutationHandler = async ({ ctx, input }: FormMutationHandlerOpt
inputFields = inputFields || [];
inputRoutes = inputRoutes || [];
type InputFields = typeof inputFields;
type InputRoutes = typeof inputRoutes;
type InputFields = NonNullable<typeof inputFields>;
type InputRoutes = NonNullable<typeof inputRoutes>;
let routes: InputRoutes;
let fields: InputFields;
type DuplicateFrom = NonNullable<typeof duplicateFrom>;
@@ -1,9 +1,8 @@
import z from "zod";
import { zodFields, zodRoutes } from "@calcom/app-store/routing-forms/zod";
import { RoutingFormSettings } from "@calcom/prisma/zod-utils";
import { zodFields, zodRoutes } from "../zod";
export const ZFormMutationInputSchema = z.object({
id: z.string(),
name: z.string(),
@@ -1,11 +1,9 @@
import { getSerializableForm } from "@calcom/app-store/routing-forms/lib/getSerializableForm";
import { entityPrismaWhereClause } from "@calcom/lib/entityPermissionUtils.server";
import type { PrismaClient } from "@calcom/prisma";
import { MembershipRole } from "@calcom/prisma/enums";
import type { TrpcSessionUser } from "@calcom/trpc/server/types";
import { TRPCError } from "@trpc/server";
import { getSerializableForm } from "../lib/getSerializableForm";
import type { TFormQueryInputSchema } from "./formQuery.schema";
import { checkPermissionOnExistingRoutingForm } from "./permissions";
@@ -1,5 +1,7 @@
import type { z } from "zod";
import { getSerializableForm } from "@calcom/app-store/routing-forms/lib/getSerializableForm";
import type { zodFields, zodRoutes } from "@calcom/app-store/routing-forms/zod";
import { hasFilter } from "@calcom/features/filters/lib/hasFilter";
import { entityPrismaWhereClause, canEditEntity } from "@calcom/lib/entityPermissionUtils.server";
import logger from "@calcom/lib/logger";
@@ -9,8 +11,6 @@ import type { Prisma } from "@calcom/prisma/client";
import { entries } from "@calcom/prisma/zod-utils";
import type { TrpcSessionUser } from "@calcom/trpc/server/types";
import { getSerializableForm } from "../lib/getSerializableForm";
import type { zodFields, zodRoutes } from "../zod";
import type { TFormSchema } from "./forms.schema";
interface FormsHandlerOptions {
@@ -1,10 +1,10 @@
import { enabledIncompleteBookingApps } from "@calcom/app-store/routing-forms/lib/enabledIncompleteBookingApps";
import type { PrismaClient } from "@calcom/prisma";
import { safeCredentialSelect } from "@calcom/prisma/selects/credential";
import type { TrpcSessionUser } from "@calcom/trpc/server/types";
import { TRPCError } from "@trpc/server";
import { enabledIncompleteBookingApps } from "../lib/enabledIncompleteBookingApps";
import type { TGetIncompleteBookingSettingsInputSchema } from "./getIncompleteBookingSettings.schema";
interface GetIncompleteBookingSettingsOptions {
@@ -1,5 +1,8 @@
import type { z } from "zod";
import { enrichFormWithMigrationData } from "@calcom/app-store/routing-forms/enrichFormWithMigrationData";
import { getSerializableForm } from "@calcom/app-store/routing-forms/lib/getSerializableForm";
import type { FormResponse } from "@calcom/app-store/routing-forms/types/types";
import { canAccessEntity } from "@calcom/lib/entityPermissionUtils.server";
import { getTranslation } from "@calcom/lib/server/i18n";
import { prisma } from "@calcom/prisma";
@@ -7,9 +10,6 @@ import type { TrpcSessionUser } from "@calcom/trpc/server/types";
import { TRPCError } from "@trpc/server";
import { enrichFormWithMigrationData } from "../enrichFormWithMigrationData";
import { getSerializableForm } from "../lib/getSerializableForm";
import type { FormResponse } from "../types/types";
import type { ZFormByResponseIdInputSchema } from "./_router";
type GetResponseWithFormFieldsOptions = {
@@ -1,10 +1,10 @@
import incompleteBookingActionDataSchemas from "@calcom/app-store/routing-forms/lib/incompleteBooking/actionDataSchemas";
import logger from "@calcom/lib/logger";
import type { PrismaClient } from "@calcom/prisma";
import type { TrpcSessionUser } from "@calcom/trpc/server/types";
import { TRPCError } from "@trpc/server";
import incompleteBookingActionDataSchemas from "../lib/incompleteBooking/actionDataSchemas";
import type { TSaveIncompleteBookingSettingsInputSchema } from "./saveIncompleteBookingSettings.schema";
const log = logger.getSubLogger({ prefix: ["incomplete-booking"] });
@@ -1,5 +1,6 @@
import { StripeService } from "@calcom/lib/server/service/stripe";
import { StripeService } from "@calcom/features/ee/payments/server/stripe-service";
import { ZStripeCheckoutSessionInputSchema } from "./stripeCheckoutSession.schema";
import type { TStripeCheckoutSessionInputSchema } from "./stripeCheckoutSession.schema";
type StripeCheckoutSessionOptions = {
@@ -7,7 +8,9 @@ type StripeCheckoutSessionOptions = {
};
export const stripeCheckoutSessionHandler = async ({ input }: StripeCheckoutSessionOptions) => {
return await StripeService.getCheckoutSession(input);
const parsedInput = ZStripeCheckoutSessionInputSchema.parse(input);
return await StripeService.getCheckoutSession(parsedInput);
};
export default stripeCheckoutSessionHandler;
@@ -1,10 +1,10 @@
import app_Basecamp3 from "@calcom/app-store/basecamp3/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 { router } from "../../trpc";
import { router, mergeRouters } from "../../trpc";
import app_Basecamp3 from "../apps/basecamp3/_router";
import app_RoutingForms from "../apps/routing-forms/_router";
import { loggedInViewerRouter } from "../loggedInViewer/_router";
import { publicViewerRouter } from "../publicViewer/_router";
import { timezonesRouter } from "../publicViewer/timezones/_router";
@@ -24,6 +24,7 @@ import { delegationCredentialRouter } from "./delegationCredential/_router";
import { deploymentSetupRouter } from "./deploymentSetup/_router";
import { dsyncRouter } from "./dsync/_router";
import { eventTypesRouter } from "./eventTypes/_router";
import { eventTypesRouter as heavyEventTypesRouter } from "./eventTypes/heavy/_router";
import { filterSegmentsRouter } from "./filterSegments/_router";
import { googleWorkspaceRouter } from "./googleWorkspace/_router";
import { i18nRouter } from "./i18n/_router";
@@ -53,7 +54,12 @@ export const viewerRouter = router({
calendars: calendarsRouter,
calVideo: calVideoRouter,
credentials: credentialsRouter,
eventTypes: eventTypesRouter,
eventTypes: mergeRouters(
eventTypesRouter,
router({
heavy: heavyEventTypesRouter,
})
),
availability: availabilityRouter,
teams: viewerTeamsRouter,
timezones: timezonesRouter,
@@ -1,6 +1,6 @@
import { z } from "zod";
import { DailyLocationType } from "@calcom/app-store/locations";
import { DailyLocationType } from "@calcom/app-store/constants";
import { commonBookingSchema } from "./types";
@@ -4,15 +4,12 @@ import { logP } from "@calcom/lib/perf";
import authedProcedure from "../../../procedures/authedProcedure";
import { router } from "../../../trpc";
import { ZCreateInputSchema } from "./create.schema";
import { ZDeleteInputSchema } from "./delete.schema";
import { ZDuplicateInputSchema } from "./duplicate.schema";
import { ZEventTypeInputSchema, ZGetEventTypesFromGroupSchema } from "./getByViewer.schema";
import { ZGetHashedLinkInputSchema } from "./getHashedLink.schema";
import { ZGetHashedLinksInputSchema } from "./getHashedLinks.schema";
import { ZGetTeamAndEventTypeOptionsSchema } from "./getTeamAndEventTypeOptions.schema";
import { get } from "./procedures/get";
import { ZUpdateInputSchema } from "./update.schema";
import { eventOwnerProcedure } from "./util";
type BookingsRouterHandlerCache = {
@@ -22,11 +19,8 @@ type BookingsRouterHandlerCache = {
getTeamAndEventTypeOptions?: typeof import("./getTeamAndEventTypeOptions.handler").getTeamAndEventTypeOptions;
list?: typeof import("./list.handler").listHandler;
listWithTeam?: typeof import("./listWithTeam.handler").listWithTeamHandler;
create?: typeof import("./create.handler").createHandler;
get?: typeof import("./get.handler").getHandler;
update?: typeof import("./update.handler").updateHandler;
delete?: typeof import("./delete.handler").deleteHandler;
duplicate?: typeof import("./duplicate.handler").duplicateHandler;
bulkEventFetch?: typeof import("./bulkEventFetch.handler").bulkEventFetchHandler;
bulkUpdateToDefaultLocation?: typeof import("./bulkUpdateToDefaultLocation.handler").bulkUpdateToDefaultLocationHandler;
};
@@ -115,26 +109,8 @@ export const eventTypesRouter = router({
});
}),
create: authedProcedure.input(ZCreateInputSchema).mutation(async ({ ctx, input }) => {
const { createHandler } = await import("./create.handler");
return createHandler({
ctx,
input,
});
}),
get,
update: eventOwnerProcedure.input(ZUpdateInputSchema).mutation(async ({ ctx, input }) => {
const { updateHandler } = await import("./update.handler");
return updateHandler({
ctx,
input,
});
}),
delete: eventOwnerProcedure.input(ZDeleteInputSchema).mutation(async ({ ctx, input }) => {
const { deleteHandler } = await import("./delete.handler");
@@ -144,15 +120,6 @@ export const eventTypesRouter = router({
});
}),
duplicate: eventOwnerProcedure.input(ZDuplicateInputSchema).mutation(async ({ ctx, input }) => {
const { duplicateHandler } = await import("./duplicate.handler");
return duplicateHandler({
ctx,
input,
});
}),
bulkEventFetch: authedProcedure.query(async ({ ctx }) => {
const { bulkEventFetchHandler } = await import("./bulkEventFetch.handler");
@@ -0,0 +1,45 @@
import { z } from "zod";
import { logP } from "@calcom/lib/perf";
import authedProcedure from "../../../../procedures/authedProcedure";
import { router } from "../../../../trpc";
import { ZCreateInputSchema } from "./create.schema";
import { ZDuplicateInputSchema } from "./duplicate.schema";
import { eventOwnerProcedure } from "../util";
import { ZUpdateInputSchema } from "./update.schema";
type BookingsRouterHandlerCache = {
create?: typeof import("./create.handler").createHandler;
duplicate?: typeof import("./duplicate.handler").duplicateHandler;
update?: typeof import("./update.handler").updateHandler;
};
const UNSTABLE_HANDLER_CACHE: BookingsRouterHandlerCache = {};
export const eventTypesRouter = router({
create: authedProcedure.input(ZCreateInputSchema).mutation(async ({ ctx, input }) => {
const { createHandler } = await import("./create.handler");
return createHandler({
ctx,
input,
});
}),
duplicate: eventOwnerProcedure.input(ZDuplicateInputSchema).mutation(async ({ ctx, input }) => {
const { duplicateHandler } = await import("./duplicate.handler");
return duplicateHandler({
ctx,
input,
});
}),
update: eventOwnerProcedure.input(ZUpdateInputSchema).mutation(async ({ ctx, input }) => {
const { updateHandler } = await import("./update.handler");
return updateHandler({
ctx,
input,
});
})
});
@@ -1,6 +1,6 @@
import { Prisma } from "@prisma/client";
import { DailyLocationType } from "@calcom/app-store/locations";
import { DailyLocationType } from "@calcom/app-store/constants";
import { getDefaultLocations } from "@calcom/lib/server/getDefaultLocations";
import { EventTypeRepository } from "@calcom/lib/server/repository/eventTypeRepository";
import type { PrismaClient } from "@calcom/prisma";
@@ -9,7 +9,7 @@ import type { EventTypeLocation } from "@calcom/prisma/zod/custom/eventtype";
import { TRPCError } from "@trpc/server";
import type { TrpcSessionUser } from "../../../types";
import type { TrpcSessionUser } from "../../../../types";
import type { TCreateInputSchema } from "./create.schema";
type SessionUser = NonNullable<TrpcSessionUser>;
@@ -1,4 +1,4 @@
import prismaMock from "../../../../../../tests/libs/__mocks__/prismaMock";
import prismaMock from "../../../../../../../tests/libs/__mocks__/prismaMock";
import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library";
import { describe, it, expect, vi, beforeEach } from "vitest";

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