fix: update transaction timeout for large managed event-types (#25944)
* fix: update transaction timeout for large managed event-types * test: add findMany mock for batch update optimization in handleChildrenEventTypes tests Co-Authored-By: morgan@cal.com <morgan@cal.com> * fix: reduce batch size to 50 and use skipDuplicates for workflow linkage Co-Authored-By: morgan@cal.com <morgan@cal.com> * test: add comprehensive tests for batch execution and retry behavior Co-Authored-By: keith@cal.com <keithwillcode@gmail.com> * fix: use logger instead of console.log and fix type error Co-Authored-By: morgan@cal.com <morgan@cal.com> * test: update test file to mock logger instead of console.log Co-Authored-By: morgan@cal.com <morgan@cal.com> * fix: type issues * fixup! fix: type issues --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: keith@cal.com <keithwillcode@gmail.com>
This commit is contained in:
co-authored by
morgan@cal.com <morgan@cal.com>
morgan@cal.com <morgan@cal.com>
keith@cal.com <keithwillcode@gmail.com>
morgan@cal.com <morgan@cal.com>
morgan@cal.com <morgan@cal.com>
Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
keith@cal.com <keithwillcode@gmail.com>
parent
f319b662c5
commit
a4f8571f59
@@ -1,18 +1,24 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import prismaMock from "@calcom/testing/lib/__mocks__/prismaMock";
|
||||
|
||||
import updateChildrenEventTypes from "@calcom/features/ee/managed-event-types/lib/handleChildrenEventTypes";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { buildEventType } from "@calcom/lib/test/builder";
|
||||
import type { EventType, Prisma, User, WorkflowsOnEventTypes } from "@calcom/prisma/client";
|
||||
import { SchedulingType } from "@calcom/prisma/enums";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import updateChildrenEventTypes from "@calcom/features/ee/managed-event-types/lib/handleChildrenEventTypes";
|
||||
import { buildEventType } from "@calcom/lib/test/builder";
|
||||
import type { EventType, User, WorkflowsOnEventTypes } from "@calcom/prisma/client";
|
||||
import type { Prisma } from "@calcom/prisma/client";
|
||||
import { SchedulingType } from "@calcom/prisma/enums";
|
||||
// Mock the logger module
|
||||
vi.mock("@calcom/lib/logger", () => ({
|
||||
default: {
|
||||
info: vi.fn(),
|
||||
error: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
// 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);
|
||||
@@ -98,7 +104,7 @@ describe("handleChildrenEventTypes", () => {
|
||||
|
||||
it("Returns message 'Missing event type'", async () => {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
// @ts-expect-error
|
||||
prismaMock.eventType.findFirst.mockImplementation(() => {
|
||||
return new Promise((resolve) => {
|
||||
resolve(null);
|
||||
@@ -124,8 +130,6 @@ describe("handleChildrenEventTypes", () => {
|
||||
|
||||
describe("Happy paths", () => {
|
||||
it("Adds new users", async () => {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
const {
|
||||
schedulingType,
|
||||
id,
|
||||
@@ -209,8 +213,6 @@ describe("handleChildrenEventTypes", () => {
|
||||
});
|
||||
|
||||
it("Updates old users", async () => {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
const {
|
||||
schedulingType,
|
||||
id,
|
||||
@@ -233,6 +235,11 @@ describe("handleChildrenEventTypes", () => {
|
||||
metadata: { managedEventConfig: {} },
|
||||
locations: [],
|
||||
});
|
||||
|
||||
// Mock findMany for existing records lookup (new batch update optimization)
|
||||
// @ts-expect-error - partial mock for test purposes
|
||||
prismaMock.eventType.findMany.mockResolvedValue([{ userId: 4, metadata: {} }]);
|
||||
|
||||
const result = await updateChildrenEventTypes({
|
||||
eventTypeId: 1,
|
||||
oldEventType: { children: [{ userId: 4 }], team: { name: "" } },
|
||||
@@ -301,6 +308,11 @@ describe("handleChildrenEventTypes", () => {
|
||||
metadata: { managedEventConfig: {} },
|
||||
locations: [],
|
||||
});
|
||||
|
||||
// Mock findMany for existing records lookup (new batch update optimization)
|
||||
// @ts-expect-error - partial mock for test purposes
|
||||
prismaMock.eventType.findMany.mockResolvedValue([{ userId: 4, metadata: {} }]);
|
||||
|
||||
const result = await updateChildrenEventTypes({
|
||||
eventTypeId: 1,
|
||||
oldEventType: { children: [{ userId: 4 }, { userId: 1 }], team: { name: "" } },
|
||||
@@ -324,8 +336,6 @@ describe("handleChildrenEventTypes", () => {
|
||||
|
||||
describe("Slug conflicts", () => {
|
||||
it("Deletes existent event types for new users added", async () => {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
const {
|
||||
schedulingType,
|
||||
id,
|
||||
@@ -412,8 +422,6 @@ describe("handleChildrenEventTypes", () => {
|
||||
expect(result.deletedExistentEventTypes).toEqual([123]);
|
||||
});
|
||||
it("Deletes existent event types for old users updated", async () => {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
const {
|
||||
schedulingType,
|
||||
id,
|
||||
@@ -437,6 +445,11 @@ describe("handleChildrenEventTypes", () => {
|
||||
metadata: { managedEventConfig: {} },
|
||||
locations: [],
|
||||
});
|
||||
|
||||
// Mock findMany for existing records lookup (new batch update optimization)
|
||||
// @ts-expect-error - partial mock for test purposes
|
||||
prismaMock.eventType.findMany.mockResolvedValue([{ userId: 4, metadata: {} }]);
|
||||
|
||||
prismaMock.eventType.deleteMany.mockResolvedValue([123] as unknown as Prisma.BatchPayload);
|
||||
const result = await updateChildrenEventTypes({
|
||||
eventTypeId: 1,
|
||||
@@ -535,6 +548,9 @@ describe("handleChildrenEventTypes", () => {
|
||||
};
|
||||
prismaMock.eventType.createManyAndReturn.mockResolvedValue([createdEventType]);
|
||||
|
||||
// Mock findMany for existing records lookup (new batch update optimization)
|
||||
prismaMock.eventType.findMany.mockResolvedValue([{ userId: 4, metadata: {} } as EventType]);
|
||||
|
||||
// Mock the event type that will be returned for existing users
|
||||
const mockUpdatedEventType = {
|
||||
id: 2,
|
||||
@@ -657,7 +673,290 @@ describe("handleChildrenEventTypes", () => {
|
||||
// Since findMany returned empty array, all workflow-eventType pairs should be created
|
||||
expect(prismaMock.workflowsOnEventTypes.createMany).toHaveBeenCalledWith({
|
||||
data: [{ eventTypeId: 2, workflowId: 11 }],
|
||||
skipDuplicates: false,
|
||||
skipDuplicates: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Batch execution and retry behavior", () => {
|
||||
it("processes updates in batches of 50", async () => {
|
||||
// Create 120 old users to test batching (should result in 3 batches: 50, 50, 20)
|
||||
const oldUserIds = Array.from({ length: 120 }, (_, i) => i + 1);
|
||||
const children = oldUserIds.map((id) => ({
|
||||
hidden: false,
|
||||
owner: { id, name: `User ${id}`, email: `user${id}@test.com`, eventTypeSlugs: [] },
|
||||
}));
|
||||
|
||||
mockFindFirstEventType({
|
||||
metadata: { managedEventConfig: {} },
|
||||
locations: [],
|
||||
});
|
||||
|
||||
// Mock findMany for existing records lookup
|
||||
// @ts-expect-error - partial mock for test purposes
|
||||
prismaMock.eventType.findMany.mockResolvedValue(oldUserIds.map((userId) => ({ userId, metadata: {} })));
|
||||
|
||||
// Track update calls to verify batching
|
||||
let updateCallCount = 0;
|
||||
// @ts-expect-error - partial mock for test purposes
|
||||
prismaMock.eventType.update.mockImplementation(async () => {
|
||||
updateCallCount++;
|
||||
return {
|
||||
id: updateCallCount,
|
||||
userId: updateCallCount,
|
||||
title: "Test",
|
||||
slug: "test",
|
||||
length: 30,
|
||||
hidden: false,
|
||||
position: 0,
|
||||
teamId: null,
|
||||
schedulingType: SchedulingType.MANAGED,
|
||||
scheduleId: null,
|
||||
price: 0,
|
||||
currency: "usd",
|
||||
slotInterval: null,
|
||||
metadata: {},
|
||||
successRedirectUrl: null,
|
||||
bookingFields: [],
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
});
|
||||
|
||||
const result = await updateChildrenEventTypes({
|
||||
eventTypeId: 1,
|
||||
oldEventType: { children: oldUserIds.map((userId) => ({ userId })), team: { name: "" } },
|
||||
children,
|
||||
updatedEventType: { schedulingType: "MANAGED", slug: "something" },
|
||||
currentUserId: 1,
|
||||
prisma: prismaMock,
|
||||
profileId: null,
|
||||
updatedValues: {},
|
||||
});
|
||||
|
||||
// Verify all 120 users were processed
|
||||
expect(result.oldUserIds).toHaveLength(120);
|
||||
expect(updateCallCount).toBe(120);
|
||||
// Verify findMany was called once for bulk fetch optimization
|
||||
expect(prismaMock.eventType.findMany).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("retries failed updates once", async () => {
|
||||
// Reset logger mocks before test
|
||||
vi.mocked(logger.info).mockClear();
|
||||
vi.mocked(logger.error).mockClear();
|
||||
|
||||
mockFindFirstEventType({
|
||||
metadata: { managedEventConfig: {} },
|
||||
locations: [],
|
||||
});
|
||||
|
||||
prismaMock.eventType.findMany.mockResolvedValue([
|
||||
{ userId: 1, metadata: {} } as EventType,
|
||||
{ userId: 2, metadata: {} } as EventType,
|
||||
{ userId: 3, metadata: {} } as EventType,
|
||||
]);
|
||||
|
||||
// Track calls per userId to simulate failure on first attempt, success on retry
|
||||
const callCountByUser: Record<number, number> = {};
|
||||
|
||||
// @ts-expect-error - partial mock for test purposes
|
||||
prismaMock.eventType.update.mockImplementation(async (args) => {
|
||||
const userId = (args.where as { userId_parentId: { userId: number } }).userId_parentId.userId;
|
||||
callCountByUser[userId] = (callCountByUser[userId] || 0) + 1;
|
||||
|
||||
// User 2 fails on first attempt, succeeds on retry
|
||||
if (userId === 2 && callCountByUser[userId] === 1) {
|
||||
throw new Error("Simulated database error");
|
||||
}
|
||||
|
||||
return {
|
||||
id: userId,
|
||||
userId,
|
||||
title: "Test",
|
||||
slug: "test",
|
||||
length: 30,
|
||||
hidden: false,
|
||||
position: 0,
|
||||
teamId: null,
|
||||
schedulingType: SchedulingType.MANAGED,
|
||||
scheduleId: null,
|
||||
price: 0,
|
||||
currency: "usd",
|
||||
slotInterval: null,
|
||||
metadata: {},
|
||||
successRedirectUrl: null,
|
||||
bookingFields: [],
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
} as unknown as EventType;
|
||||
});
|
||||
|
||||
const result = await updateChildrenEventTypes({
|
||||
eventTypeId: 1,
|
||||
oldEventType: { children: [{ userId: 1 }, { userId: 2 }, { userId: 3 }], team: { name: "" } },
|
||||
children: [
|
||||
{ hidden: false, owner: { id: 1, name: "", email: "", eventTypeSlugs: [] } },
|
||||
{ hidden: false, owner: { id: 2, name: "", email: "", eventTypeSlugs: [] } },
|
||||
{ hidden: false, owner: { id: 3, name: "", email: "", eventTypeSlugs: [] } },
|
||||
],
|
||||
updatedEventType: { schedulingType: "MANAGED", slug: "something" },
|
||||
currentUserId: 1,
|
||||
prisma: prismaMock,
|
||||
profileId: null,
|
||||
updatedValues: {},
|
||||
});
|
||||
|
||||
// All 3 users should be in oldUserIds (successful after retry)
|
||||
expect(result.oldUserIds).toEqual([1, 2, 3]);
|
||||
// User 2 should have been called twice (initial + retry)
|
||||
expect(callCountByUser[2]).toBe(2);
|
||||
// Verify retry log was called
|
||||
expect(logger.info).toHaveBeenCalledWith("Retrying 1 failed updates...");
|
||||
});
|
||||
|
||||
it("continues processing when some updates fail permanently", async () => {
|
||||
// Reset logger mocks before test
|
||||
vi.mocked(logger.info).mockClear();
|
||||
vi.mocked(logger.error).mockClear();
|
||||
|
||||
mockFindFirstEventType({
|
||||
metadata: { managedEventConfig: {} },
|
||||
locations: [],
|
||||
});
|
||||
|
||||
// Mock findMany for existing records lookup
|
||||
prismaMock.eventType.findMany.mockResolvedValue([
|
||||
{ userId: 1, metadata: {} } as EventType,
|
||||
{ userId: 2, metadata: {} } as EventType,
|
||||
{ userId: 3, metadata: {} } as EventType,
|
||||
]);
|
||||
|
||||
// @ts-expect-error - partial mock for test purposes
|
||||
prismaMock.eventType.update.mockImplementation(async (args: Prisma.EventTypeUpdateArgs) => {
|
||||
const userId = (args.where as { userId_parentId: { userId: number } }).userId_parentId.userId;
|
||||
|
||||
// User 2 always fails (permanent failure)
|
||||
if (userId === 2) {
|
||||
throw new Error("Permanent database error");
|
||||
}
|
||||
|
||||
return {
|
||||
id: userId,
|
||||
userId,
|
||||
title: "Test",
|
||||
slug: "test",
|
||||
length: 30,
|
||||
hidden: false,
|
||||
position: 0,
|
||||
teamId: null,
|
||||
schedulingType: SchedulingType.MANAGED,
|
||||
scheduleId: null,
|
||||
price: 0,
|
||||
currency: "usd",
|
||||
slotInterval: null,
|
||||
metadata: {},
|
||||
successRedirectUrl: null,
|
||||
bookingFields: [],
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
} as unknown as EventType;
|
||||
});
|
||||
|
||||
const result = await updateChildrenEventTypes({
|
||||
eventTypeId: 1,
|
||||
oldEventType: { children: [{ userId: 1 }, { userId: 2 }, { userId: 3 }], team: { name: "" } },
|
||||
children: [
|
||||
{ hidden: false, owner: { id: 1, name: "", email: "", eventTypeSlugs: [] } },
|
||||
{ hidden: false, owner: { id: 2, name: "", email: "", eventTypeSlugs: [] } },
|
||||
{ hidden: false, owner: { id: 3, name: "", email: "", eventTypeSlugs: [] } },
|
||||
],
|
||||
updatedEventType: { schedulingType: "MANAGED", slug: "something" },
|
||||
currentUserId: 1,
|
||||
prisma: prismaMock,
|
||||
profileId: null,
|
||||
updatedValues: {},
|
||||
});
|
||||
|
||||
// oldUserIds should still contain all 3 (the function returns the input, not successful ones)
|
||||
expect(result.oldUserIds).toEqual([1, 2, 3]);
|
||||
// Verify permanent failure was logged
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
"handleChildrenEventType - Could not update managed event-type",
|
||||
{
|
||||
parentId: 1,
|
||||
userIds: [2],
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it("handles large scale scenarios with 100+ users efficiently", async () => {
|
||||
// Create 150 old users to test large scale handling
|
||||
const oldUserIds = Array.from({ length: 150 }, (_, i) => i + 1);
|
||||
const children = oldUserIds.map((id) => ({
|
||||
hidden: false,
|
||||
owner: { id, name: `User ${id}`, email: `user${id}@test.com`, eventTypeSlugs: [] },
|
||||
}));
|
||||
|
||||
mockFindFirstEventType({
|
||||
metadata: { managedEventConfig: {} },
|
||||
locations: [],
|
||||
});
|
||||
|
||||
// Mock findMany for existing records lookup - should be called ONCE (bulk optimization)
|
||||
// @ts-expect-error - partial mock for test purposes
|
||||
prismaMock.eventType.findMany.mockResolvedValue(oldUserIds.map((userId) => ({ userId, metadata: {} })));
|
||||
|
||||
// Track timing to ensure batching doesn't cause sequential delays
|
||||
const updateResults: number[] = [];
|
||||
// @ts-expect-error - partial mock for test purposes
|
||||
prismaMock.eventType.update.mockImplementation(async (args) => {
|
||||
const userId = (args.where as { userId_parentId: { userId: number } }).userId_parentId.userId;
|
||||
updateResults.push(userId);
|
||||
|
||||
return {
|
||||
id: userId,
|
||||
userId,
|
||||
title: "Test",
|
||||
slug: "test",
|
||||
length: 30,
|
||||
hidden: false,
|
||||
position: 0,
|
||||
teamId: null,
|
||||
schedulingType: SchedulingType.MANAGED,
|
||||
scheduleId: null,
|
||||
price: 0,
|
||||
currency: "usd",
|
||||
slotInterval: null,
|
||||
metadata: {},
|
||||
successRedirectUrl: null,
|
||||
bookingFields: [],
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
} as unknown as EventType;
|
||||
});
|
||||
|
||||
const result = await updateChildrenEventTypes({
|
||||
eventTypeId: 1,
|
||||
oldEventType: { children: oldUserIds.map((userId) => ({ userId })), team: { name: "" } },
|
||||
children,
|
||||
updatedEventType: { schedulingType: "MANAGED", slug: "something" },
|
||||
currentUserId: 1,
|
||||
prisma: prismaMock,
|
||||
profileId: null,
|
||||
updatedValues: {},
|
||||
});
|
||||
|
||||
// Verify all 150 users were processed
|
||||
expect(result.oldUserIds).toHaveLength(150);
|
||||
expect(updateResults).toHaveLength(150);
|
||||
|
||||
// Verify bulk fetch optimization: findMany should be called only ONCE
|
||||
// (not 150 individual findUnique calls as in the old implementation)
|
||||
expect(prismaMock.eventType.findMany).toHaveBeenCalledTimes(1);
|
||||
expect(prismaMock.eventType.findMany).toHaveBeenCalledWith({
|
||||
where: { parentId: 1, userId: { in: oldUserIds } },
|
||||
select: { userId: true, metadata: true },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,9 +2,10 @@ import type { DeepMockProxy } from "vitest-mock-extended";
|
||||
|
||||
import { eventTypeMetaDataSchemaWithTypedApps } from "@calcom/app-store/zod-utils";
|
||||
import { sendSlugReplacementEmail } from "@calcom/emails/integration-email-service";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { getTranslation } from "@calcom/lib/server/i18n";
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
import type { Prisma } from "@calcom/prisma/client";
|
||||
import type { EventType, Prisma } from "@calcom/prisma/client";
|
||||
import { SchedulingType } from "@calcom/prisma/enums";
|
||||
import {
|
||||
allManagedEventTypeProps,
|
||||
@@ -273,7 +274,7 @@ export default async function handleChildrenEventTypes({
|
||||
|
||||
// Old users updated
|
||||
if (oldUserIds?.length) {
|
||||
// Check if there are children with existent homonym event types to send notifications
|
||||
// 1. Initial Checks and Setup
|
||||
deletedExistentEventTypes = await checkExistentEventTypes({
|
||||
updatedEventType,
|
||||
children,
|
||||
@@ -283,50 +284,42 @@ export default async function handleChildrenEventTypes({
|
||||
});
|
||||
|
||||
const { unlockedFields } = managedEventTypeValues.metadata?.managedEventConfig ?? {};
|
||||
const unlockedFieldProps = !unlockedFields
|
||||
? {}
|
||||
: Object.keys(unlockedFields).reduce((acc, key) => {
|
||||
const filteredKey =
|
||||
key === "afterBufferTime"
|
||||
? "afterEventBuffer"
|
||||
: key === "beforeBufferTime"
|
||||
? "beforeEventBuffer"
|
||||
: key;
|
||||
// @ts-expect-error Element implicitly has any type
|
||||
acc[filteredKey] = true;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
// Add to payload all eventType values that belong to locked fields, changed or unchanged
|
||||
// Ignore from payload any eventType values that belong to unlocked fields
|
||||
// Transform unlocked fields into a lookup object with mapped keys
|
||||
const unlockedFieldProps = Object.keys(unlockedFields ?? {}).reduce((acc, key) => {
|
||||
const mappedKey =
|
||||
key === "afterBufferTime"
|
||||
? "afterEventBuffer"
|
||||
: key === "beforeBufferTime"
|
||||
? "beforeEventBuffer"
|
||||
: key;
|
||||
// @ts-expect-error Element implicitly has any type
|
||||
acc[mappedKey] = true;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
// Prepare payload: Omit unlocked fields and the "children" property
|
||||
const updatePayload = allManagedEventTypePropsZod.omit(unlockedFieldProps).parse(eventType);
|
||||
const updatePayloadFiltered = Object.entries(updatePayload)
|
||||
.filter(([key, _]) => key !== "children")
|
||||
.reduce((newObj, [key, value]) => ({ ...newObj, [key]: value }), {});
|
||||
// Update event types for old users
|
||||
const oldEventTypes = await Promise.all(
|
||||
oldUserIds.map(async (userId) => {
|
||||
const existingEventType = await prisma.eventType.findUnique({
|
||||
where: {
|
||||
userId_parentId: {
|
||||
userId,
|
||||
parentId,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
metadata: true,
|
||||
},
|
||||
});
|
||||
|
||||
const metadata = eventTypeMetaDataSchemaWithTypedApps.parse(existingEventType?.metadata || {});
|
||||
// 2. Data Fetching Optimization
|
||||
const existingRecords = await prisma.eventType.findMany({
|
||||
where: { parentId, userId: { in: oldUserIds } },
|
||||
select: { userId: true, metadata: true },
|
||||
});
|
||||
|
||||
const metadataMap = new Map(existingRecords.map((rec) => [rec.userId, rec.metadata]));
|
||||
|
||||
// 3. Define Reusable Update Logic
|
||||
const performUpdate = async (userId: number) => {
|
||||
try {
|
||||
const rawMetadata = metadataMap.get(userId);
|
||||
const metadata = eventTypeMetaDataSchemaWithTypedApps.parse(rawMetadata || {});
|
||||
|
||||
return await prisma.eventType.update({
|
||||
where: {
|
||||
userId_parentId: {
|
||||
userId,
|
||||
parentId,
|
||||
},
|
||||
},
|
||||
where: { userId_parentId: { userId, parentId } },
|
||||
data: {
|
||||
...updatePayloadFiltered,
|
||||
rrHostSubsetEnabled: false,
|
||||
@@ -334,12 +327,7 @@ export default async function handleChildrenEventTypes({
|
||||
...("schedule" in unlockedFieldProps ? {} : { scheduleId: eventType.scheduleId || null }),
|
||||
restrictionScheduleId: null,
|
||||
useBookerTimezone: false,
|
||||
hashedLink:
|
||||
"multiplePrivateLinks" in unlockedFieldProps
|
||||
? undefined
|
||||
: {
|
||||
deleteMany: {},
|
||||
},
|
||||
hashedLink: "multiplePrivateLinks" in unlockedFieldProps ? undefined : { deleteMany: {} },
|
||||
allowReschedulingCancelledBookings:
|
||||
managedEventTypeValues.allowReschedulingCancelledBookings ?? false,
|
||||
metadata: {
|
||||
@@ -351,10 +339,46 @@ export default async function handleChildrenEventTypes({
|
||||
},
|
||||
},
|
||||
});
|
||||
})
|
||||
);
|
||||
} catch (error) {
|
||||
throw { userId, error };
|
||||
}
|
||||
};
|
||||
|
||||
// Link workflows with old users' event types if new workflows were added
|
||||
// 4. Batch Execution Handler
|
||||
const executeBatch = async (ids: number[]) => {
|
||||
const successes: EventType[] = [];
|
||||
const failures: { userId: number; error: Error }[] = [];
|
||||
const BATCH_SIZE = 50;
|
||||
|
||||
for (let i = 0; i < ids.length; i += BATCH_SIZE) {
|
||||
const batch = ids.slice(i, i + BATCH_SIZE);
|
||||
const results = await Promise.allSettled(batch.map(performUpdate));
|
||||
|
||||
results.forEach((res) => {
|
||||
if (res.status === "fulfilled") successes.push(res.value);
|
||||
else failures.push(res.reason);
|
||||
});
|
||||
}
|
||||
return { successes, failures };
|
||||
};
|
||||
|
||||
// 5. Run Updates & Retries
|
||||
const { successes: oldEventTypes, failures: failedUpdates } = await executeBatch(oldUserIds);
|
||||
|
||||
if (failedUpdates.length > 0) {
|
||||
logger.info(`Retrying ${failedUpdates.length} failed updates...`);
|
||||
const retry = await executeBatch(failedUpdates.map((f) => f.userId));
|
||||
oldEventTypes.push(...retry.successes);
|
||||
// Any remaining failures in retry.failures are permanent
|
||||
if (retry.failures.length > 0) {
|
||||
logger.error("handleChildrenEventType - Could not update managed event-type", {
|
||||
parentId,
|
||||
userIds: retry.failures.map((failure) => failure.userId),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Workflow Linkage
|
||||
if (currentWorkflowIds?.length && oldEventTypes.length) {
|
||||
await prisma.$transaction(async (tx) => {
|
||||
const eventTypeIds = oldEventTypes.map((e) => e.id);
|
||||
@@ -393,7 +417,7 @@ export default async function handleChildrenEventTypes({
|
||||
if (newRelationshipsToCreate.length > 0) {
|
||||
await tx.workflowsOnEventTypes.createMany({
|
||||
data: newRelationshipsToCreate,
|
||||
skipDuplicates: false,
|
||||
skipDuplicates: true,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user