perf: handleChildrenEventTypes transaction (#25602)

* perf: handleChildrenEventTypes transaction

* fixup! perf: handleChildrenEventTypes transaction

* fix: e2e tests

* test: update handleChildrenEventTypes tests for createMany API

Co-Authored-By: morgan@cal.com <morgan@cal.com>

* fix: tests

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Morgan
2025-12-05 08:35:29 +00:00
committed by GitHub
co-authored by morgan@cal.com Devin AI
parent 8cace7f7a0
commit dea6ec5003
2 changed files with 278 additions and 117 deletions
@@ -9,6 +9,18 @@ import type { EventType, User, WorkflowsOnEventTypes } from "@calcom/prisma/clie
import type { Prisma } from "@calcom/prisma/client";
import { SchedulingType } from "@calcom/prisma/enums";
// Helper to setup transaction mock that executes the callback with the prisma mock
const setupTransactionMock = () => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
prismaMock.$transaction.mockImplementation(async (callback) => {
if (typeof callback === "function") {
return await callback(prismaMock);
}
return Promise.all(callback);
});
};
// create input does not allow ID
const mockFindFirstEventType = (
data?: Partial<EventType> & { workflows?: WorkflowsOnEventTypes[] } & { users?: User[] }
@@ -132,6 +144,13 @@ describe("handleChildrenEventTypes", () => {
metadata: { managedEventConfig: {} },
locations: [],
});
// Setup transaction mock to execute the callback
setupTransactionMock();
// Mock findMany to return empty array (no workflows to link)
prismaMock.eventType.findMany.mockResolvedValue([]);
const result = await updateChildrenEventTypes({
eventTypeId: 1,
oldEventType: { children: [], team: { name: "" } },
@@ -143,25 +162,27 @@ describe("handleChildrenEventTypes", () => {
updatedValues: {},
});
const { createdAt, updatedAt, ...expectedEvType } = evType;
expect(prismaMock.eventType.create).toHaveBeenCalledWith({
data: {
...expectedEvType,
parentId: 1,
users: { connect: [{ id: 4 }] },
lockTimeZoneToggleOnBookingPage: false,
requiresBookerEmailVerification: false,
bookingLimits: undefined,
durationLimits: undefined,
recurringEvent: undefined,
eventTypeColor: undefined,
customReplyToEmail: null,
userId: 4,
rrSegmentQueryValue: undefined,
assignRRMembersUsingSegment: false,
useBookerTimezone: false,
restrictionScheduleId: null,
allowReschedulingCancelledBookings: false,
},
expect(prismaMock.eventType.createMany).toHaveBeenCalledWith({
data: [
{
...expectedEvType,
parentId: 1,
lockTimeZoneToggleOnBookingPage: false,
requiresBookerEmailVerification: false,
bookingLimits: undefined,
durationLimits: undefined,
recurringEvent: undefined,
eventTypeColor: undefined,
customReplyToEmail: null,
userId: 4,
rrSegmentQueryValue: undefined,
assignRRMembersUsingSegment: false,
useBookerTimezone: false,
restrictionScheduleId: null,
allowReschedulingCancelledBookings: false,
},
],
skipDuplicates: true,
});
expect(result.newUserIds).toEqual([4]);
expect(result.oldUserIds).toEqual([]);
@@ -304,6 +325,13 @@ describe("handleChildrenEventTypes", () => {
metadata: { managedEventConfig: {} },
locations: [],
});
// Setup transaction mock to execute the callback
setupTransactionMock();
// Mock findMany to return empty array (no workflows to link)
prismaMock.eventType.findMany.mockResolvedValue([]);
prismaMock.eventType.deleteMany.mockResolvedValue([123] as unknown as Prisma.BatchPayload);
const result = await updateChildrenEventTypes({
eventTypeId: 1,
@@ -316,27 +344,28 @@ describe("handleChildrenEventTypes", () => {
updatedValues: {},
});
const { createdAt, updatedAt, ...expectedEvType } = evType;
expect(prismaMock.eventType.create).toHaveBeenCalledWith({
data: {
...expectedEvType,
parentId: 1,
users: { connect: [{ id: 4 }] },
bookingLimits: undefined,
durationLimits: undefined,
recurringEvent: undefined,
eventTypeColor: undefined,
customReplyToEmail: null,
instantMeetingScheduleId: undefined,
lockTimeZoneToggleOnBookingPage: false,
requiresBookerEmailVerification: false,
userId: 4,
workflows: undefined,
rrSegmentQueryValue: undefined,
assignRRMembersUsingSegment: false,
useBookerTimezone: false,
restrictionScheduleId: null,
allowReschedulingCancelledBookings: false,
},
expect(prismaMock.eventType.createMany).toHaveBeenCalledWith({
data: [
{
...expectedEvType,
parentId: 1,
bookingLimits: undefined,
durationLimits: undefined,
recurringEvent: undefined,
eventTypeColor: undefined,
customReplyToEmail: null,
instantMeetingScheduleId: undefined,
lockTimeZoneToggleOnBookingPage: false,
requiresBookerEmailVerification: false,
userId: 4,
rrSegmentQueryValue: undefined,
assignRRMembersUsingSegment: false,
useBookerTimezone: false,
restrictionScheduleId: null,
allowReschedulingCancelledBookings: false,
},
],
skipDuplicates: true,
});
expect(result.newUserIds).toEqual([4]);
expect(result.oldUserIds).toEqual([]);
@@ -439,6 +468,96 @@ describe("handleChildrenEventTypes", () => {
],
});
// Setup transaction mock to execute the callback
setupTransactionMock();
// Mock findMany to return the newly created event type for workflow linking
// This simulates the event type created for user 5
prismaMock.eventType.findMany.mockResolvedValue([
{
id: 3,
title: "test",
slug: "test",
description: null,
position: 0,
locations: [],
length: 30,
offsetStart: 0,
hidden: false,
userId: 5,
profileId: null,
teamId: null,
eventName: null,
parentId: 1,
bookingFields: null,
timeZone: null,
periodType: "UNLIMITED",
periodStartDate: null,
periodEndDate: null,
periodDays: null,
periodCountCalendarDays: null,
lockTimeZoneToggleOnBookingPage: false,
lockedTimeZone: null,
requiresConfirmation: false,
requiresConfirmationWillBlockSlot: false,
requiresConfirmationForFreeEmail: false,
requiresBookerEmailVerification: false,
canSendCalVideoTranscriptionEmails: true,
autoTranslateDescriptionEnabled: false,
recurringEvent: null,
disableGuests: false,
hideCalendarNotes: false,
hideCalendarEventDetails: false,
minimumBookingNotice: 120,
beforeEventBuffer: 0,
afterEventBuffer: 0,
seatsPerTimeSlot: null,
onlyShowFirstAvailableSlot: false,
showOptimizedSlots: false,
disableCancelling: false,
disableRescheduling: false,
seatsShowAttendees: false,
seatsShowAvailabilityCount: true,
schedulingType: null,
scheduleId: null,
price: 0,
currency: "usd",
slotInterval: null,
metadata: {},
successRedirectUrl: null,
forwardParamsSuccessRedirect: true,
bookingLimits: null,
durationLimits: null,
isInstantEvent: false,
instantMeetingExpiryTimeOffsetInSeconds: 90,
instantMeetingScheduleId: null,
assignAllTeamMembers: false,
useEventTypeDestinationCalendarEmail: false,
secondaryEmailId: null,
eventTypeColor: null,
rescheduleWithSameRoundRobinHost: false,
rrSegmentQueryValue: null,
assignRRMembersUsingSegment: false,
useEventLevelSelectedCalendars: false,
restrictionScheduleId: null,
useBookerTimezone: false,
allowReschedulingCancelledBookings: false,
includeNoShowInRRCalculation: false,
interfaceLanguage: null,
customReplyToEmail: null,
createdAt: new Date(),
updatedAt: new Date(),
instantMeetingParameters: [],
isRRWeightsEnabled: false,
maxLeadThreshold: null,
allowReschedulingPastBookings: false,
hideOrganizerEmail: false,
maxActiveBookingsPerBooker: null,
maxActiveBookingPerBookerOfferReschedule: false,
bookingRequiresAuthentication: false,
},
]);
// Mock the event type that will be returned for existing users
const mockUpdatedEventType = {
id: 2,
@@ -481,33 +600,41 @@ describe("handleChildrenEventTypes", () => {
});
const { createdAt, updatedAt, ...expectedEvType } = evType;
expect(prismaMock.eventType.create).toHaveBeenCalledWith({
data: {
...expectedEvType,
bookingLimits: undefined,
durationLimits: undefined,
recurringEvent: undefined,
eventTypeColor: undefined,
customReplyToEmail: null,
locations: [],
lockTimeZoneToggleOnBookingPage: false,
requiresBookerEmailVerification: false,
useBookerTimezone: false,
restrictionScheduleId: null,
parentId: 1,
userId: 5,
users: {
connect: [{ id: 5 }],
if ("workflows" in expectedEvType) delete expectedEvType.workflows;
// Verify createMany was called for new users (user 5)
// Note: createMany doesn't support nested relations like workflows, so they're handled separately
expect(prismaMock.eventType.createMany).toHaveBeenCalledWith({
data: [
{
...expectedEvType,
bookingLimits: undefined,
durationLimits: undefined,
recurringEvent: undefined,
eventTypeColor: undefined,
customReplyToEmail: null,
locations: [],
lockTimeZoneToggleOnBookingPage: false,
requiresBookerEmailVerification: false,
useBookerTimezone: false,
restrictionScheduleId: null,
instantMeetingScheduleId: undefined,
parentId: 1,
userId: 5,
rrSegmentQueryValue: undefined,
assignRRMembersUsingSegment: false,
useEventLevelSelectedCalendars: false,
allowReschedulingCancelledBookings: false,
},
workflows: {
create: [{ workflowId: 11 }],
},
rrSegmentQueryValue: undefined,
assignRRMembersUsingSegment: false,
useEventLevelSelectedCalendars: false,
allowReschedulingCancelledBookings: false,
},
],
skipDuplicates: true,
});
// Verify workflowsOnEventTypes.createMany was called for new users' workflows
expect(prismaMock.workflowsOnEventTypes.createMany).toHaveBeenCalledWith({
data: [{ eventTypeId: 3, workflowId: 11 }],
skipDuplicates: true,
});
const { profileId, rrSegmentQueryValue, createdAt: _, updatedAt: __, ...rest } = evType;
if ("workflows" in rest) delete rest.workflows;
expect(prismaMock.eventType.update).toHaveBeenCalledWith({
@@ -531,6 +658,7 @@ describe("handleChildrenEventTypes", () => {
},
},
});
// Verify workflowsOnEventTypes.upsert was called for existing users' workflows
expect(prismaMock.workflowsOnEventTypes.upsert).toHaveBeenCalledWith({
create: {
eventTypeId: 2,
@@ -1,4 +1,3 @@
import type { DeepMockProxy } from "vitest-mock-extended";
import { eventTypeMetaDataSchemaWithTypedApps } from "@calcom/app-store/zod-utils";
@@ -155,60 +154,94 @@ export default async function handleChildrenEventTypes({
userIds: newUserIds,
teamName: oldEventType.team?.name ?? null,
});
// Create event types for new users added
await prisma.$transaction(
newUserIds.map((userId) => {
// Exclude profileId and instantMeetingScheduleId from managed values to avoid duplication
const {
profileId: _,
instantMeetingScheduleId: __,
...managedValuesWithoutExplicit
} = managedEventTypeValues;
return prisma.eventType.create({
data: {
instantMeetingScheduleId: eventType.instantMeetingScheduleId ?? undefined,
profileId: profileId ?? null,
...managedValuesWithoutExplicit,
...{
...unlockedEventTypeValues,
// pre-genned as allowed null
locations: Array.isArray(unlockedEventTypeValues.locations)
? unlockedEventTypeValues.locations
: undefined,
// Create event types for new users added
const eventTypesToCreateData = newUserIds.map((userId) => {
// Exclude profileId and instantMeetingScheduleId from managed values to avoid duplication
const {
profileId: _,
instantMeetingScheduleId: __,
...managedValuesWithoutExplicit
} = managedEventTypeValues;
return {
instantMeetingScheduleId: eventType.instantMeetingScheduleId ?? undefined,
profileId: profileId ?? null,
...managedValuesWithoutExplicit,
...{
...unlockedEventTypeValues,
// pre-genned as allowed null
locations: Array.isArray(unlockedEventTypeValues.locations)
? unlockedEventTypeValues.locations
: undefined,
},
bookingLimits:
(managedEventTypeValues.bookingLimits as unknown as Prisma.InputJsonObject) ?? undefined,
recurringEvent:
(managedEventTypeValues.recurringEvent as unknown as Prisma.InputJsonValue) ?? undefined,
metadata: (managedEventTypeValues.metadata as Prisma.InputJsonValue) ?? undefined,
bookingFields: (managedEventTypeValues.bookingFields as Prisma.InputJsonValue) ?? undefined,
durationLimits: (managedEventTypeValues.durationLimits as Prisma.InputJsonValue) ?? undefined,
eventTypeColor: (managedEventTypeValues.eventTypeColor as Prisma.InputJsonValue) ?? undefined,
onlyShowFirstAvailableSlot: managedEventTypeValues.onlyShowFirstAvailableSlot ?? false,
userId,
parentId,
hidden: children?.find((ch) => ch.owner.id === userId)?.hidden ?? false,
/**
* RR Segment isn't applicable for managed event types.
*/
rrSegmentQueryValue: undefined,
assignRRMembersUsingSegment: false,
useEventLevelSelectedCalendars: false,
restrictionScheduleId: null,
useBookerTimezone: false,
allowReschedulingCancelledBookings:
managedEventTypeValues.allowReschedulingCancelledBookings ?? false,
};
});
await prisma.$transaction(async (tx) => {
await tx.eventType.createMany({
data: eventTypesToCreateData,
skipDuplicates: true,
});
// Fetch the newly created event types to connect users and workflows
const createdEvents = await tx.eventType.findMany({
where: { parentId: parentId, userId: { in: newUserIds } },
select: { id: true, userId: true },
});
// Connect users to their event types (many-to-many relation)
// This is needed because createMany doesn't support nested relations
await Promise.all(
createdEvents.map((event) =>
tx.eventType.update({
where: { id: event.id },
data: {
users: {
connect: [{ id: event.userId! }],
},
},
bookingLimits:
(managedEventTypeValues.bookingLimits as unknown as Prisma.InputJsonObject) ?? undefined,
recurringEvent:
(managedEventTypeValues.recurringEvent as unknown as Prisma.InputJsonValue) ?? undefined,
metadata: (managedEventTypeValues.metadata as Prisma.InputJsonValue) ?? undefined,
bookingFields: (managedEventTypeValues.bookingFields as Prisma.InputJsonValue) ?? undefined,
durationLimits: (managedEventTypeValues.durationLimits as Prisma.InputJsonValue) ?? undefined,
eventTypeColor: (managedEventTypeValues.eventTypeColor as Prisma.InputJsonValue) ?? undefined,
onlyShowFirstAvailableSlot: managedEventTypeValues.onlyShowFirstAvailableSlot ?? false,
userId,
users: {
connect: [{ id: userId }],
},
parentId,
hidden: children?.find((ch) => ch.owner.id === userId)?.hidden ?? false,
workflows: currentWorkflowIds && {
create: currentWorkflowIds.map((wfId) => ({ workflowId: wfId })),
},
/**
* RR Segment isn't applicable for managed event types.
*/
rrSegmentQueryValue: undefined,
assignRRMembersUsingSegment: false,
useEventLevelSelectedCalendars: false,
restrictionScheduleId: null,
useBookerTimezone: false,
allowReschedulingCancelledBookings:
managedEventTypeValues.allowReschedulingCancelledBookings ?? false,
},
})
)
);
// Link workflows if any exist
if (currentWorkflowIds && currentWorkflowIds.length > 0) {
const workflowConnections = createdEvents.flatMap((event) =>
currentWorkflowIds.map((wfId) => ({
eventTypeId: event.id,
workflowId: wfId,
}))
);
await tx.workflowsOnEventTypes.createMany({
data: workflowConnections,
skipDuplicates: true,
});
})
);
}
});
}
// Old users updated