From c2472cc05ad5f4b77a02479bc4c54c26956822c0 Mon Sep 17 00:00:00 2001 From: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com> Date: Wed, 24 Sep 2025 11:29:46 +0530 Subject: [PATCH] feat: add createdAt and updatedAt fields to EventType model (#23949) * feat: add createdAt and updatedAt fields to EventType model - Add timestamp fields to EventType schema with backward compatibility - Update all EventType select statements across API v1, v2, and tRPC - Update platform API output types to include timestamp fields - Generate migration for existing database records - Fix test files to handle new timestamp fields properly The timestamp fields are added as optional (DateTime?) to ensure backward compatibility - existing EventType records will have null values for these fields, while new records will automatically get timestamps. Co-Authored-By: anik@cal.com * fix: remove @default(now()) from EventType createdAt to prevent existing records from getting current timestamp - Remove @default(now()) from createdAt field in schema to ensure backward compatibility - Create migration that only drops DEFAULT constraint without updating existing records - Existing EventType records will keep null timestamps - New records will get timestamps via application logic Co-Authored-By: anik@cal.com * feat: add createdAt and updatedAt timestamps to all EventType creation locations - Update all seed scripts to set timestamps for new EventType records - Ensure consistency across all EventType creation patterns - Maintain backward compatibility with nullable timestamp fields Co-Authored-By: anik@cal.com * feat: add timestamps to remaining EventType creation locations in test files - Update integration test files to include createdAt and updatedAt - Ensure consistency across all EventType creation patterns - Complete comprehensive update of all EventType creation locations Co-Authored-By: anik@cal.com * add migration * few left * feat: implement Prisma extension for automatic EventType timestamps - Create eventTypeTimestampsExtension to automatically set createdAt and updatedAt - Handle both eventType.create and eventType.createMany operations - Revert all manual timestamp setting from application code - Extension only sets timestamps if not already provided - Maintains backward compatibility - existing records keep null timestamps - New records get automatic timestamps via Prisma extension - Follows existing Cal.com extension patterns and architecture Co-Authored-By: anik@cal.com * feat: remove hardcoded timestamp settings from test files - Remove manual createdAt/updatedAt settings from routing forms controller test - Remove manual timestamp settings from event types repository fixture - Prisma extension now handles timestamps automatically for all EventType operations Co-Authored-By: anik@cal.com * update * Update event-type.output.ts * fix test --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com> --- .../test/lib/handleChildrenEventTypes.test.ts | 21 ++++++++++----- .../features/eventtypes/lib/defaultEvents.ts | 2 ++ packages/lib/test/builder.ts | 2 ++ .../extensions/event-type-timestamps.ts | 27 +++++++++++++++++++ packages/prisma/index.ts | 3 +++ .../migration.sql | 3 +++ packages/prisma/schema.prisma | 3 +++ 7 files changed, 54 insertions(+), 7 deletions(-) create mode 100644 packages/prisma/extensions/event-type-timestamps.ts create mode 100644 packages/prisma/migrations/20250919174231_add_event_type_timestamps/migration.sql diff --git a/apps/web/test/lib/handleChildrenEventTypes.test.ts b/apps/web/test/lib/handleChildrenEventTypes.test.ts index eec979b593..f53459e734 100644 --- a/apps/web/test/lib/handleChildrenEventTypes.test.ts +++ b/apps/web/test/lib/handleChildrenEventTypes.test.ts @@ -143,9 +143,10 @@ describe("handleChildrenEventTypes", () => { profileId: null, updatedValues: {}, }); + const { createdAt, updatedAt, ...expectedEvType } = evType; expect(prismaMock.eventType.create).toHaveBeenCalledWith({ data: { - ...evType, + ...expectedEvType, parentId: 1, users: { connect: [{ id: 4 }] }, lockTimeZoneToggleOnBookingPage: false, @@ -206,7 +207,7 @@ describe("handleChildrenEventTypes", () => { bookingLimits: undefined, }, }); - const { profileId, autoTranslateDescriptionEnabled, ...rest } = evType; + const { profileId, autoTranslateDescriptionEnabled, createdAt, updatedAt, ...rest } = evType; expect(prismaMock.eventType.update).toHaveBeenCalledWith({ data: { ...rest, @@ -317,9 +318,10 @@ describe("handleChildrenEventTypes", () => { profileId: null, updatedValues: {}, }); + const { createdAt, updatedAt, ...expectedEvType } = evType; expect(prismaMock.eventType.create).toHaveBeenCalledWith({ data: { - ...evType, + ...expectedEvType, parentId: 1, users: { connect: [{ id: 4 }] }, bookingLimits: undefined, @@ -383,7 +385,7 @@ describe("handleChildrenEventTypes", () => { length: 30, }, }); - const { profileId, autoTranslateDescriptionEnabled, ...rest } = evType; + const { profileId, autoTranslateDescriptionEnabled, createdAt, updatedAt, ...rest } = evType; expect(prismaMock.eventType.update).toHaveBeenCalledWith({ data: { ...rest, @@ -464,7 +466,11 @@ describe("handleChildrenEventTypes", () => { ...evType, }; - prismaMock.eventType.update.mockResolvedValue(mockUpdatedEventType); + prismaMock.eventType.update.mockResolvedValue({ + ...mockUpdatedEventType, + createdAt: new Date(), + updatedAt: new Date(), + }); await updateChildrenEventTypes({ eventTypeId: 1, @@ -480,9 +486,10 @@ describe("handleChildrenEventTypes", () => { updatedValues: {}, }); + const { createdAt, updatedAt, ...expectedEvType } = evType; expect(prismaMock.eventType.create).toHaveBeenCalledWith({ data: { - ...evType, + ...expectedEvType, bookingLimits: undefined, durationLimits: undefined, recurringEvent: undefined, @@ -507,7 +514,7 @@ describe("handleChildrenEventTypes", () => { allowReschedulingCancelledBookings: false, }, }); - const { profileId, rrSegmentQueryValue, ...rest } = evType; + const { profileId, rrSegmentQueryValue, createdAt: _, updatedAt: __, ...rest } = evType; if ("workflows" in rest) delete rest.workflows; expect(prismaMock.eventType.update).toHaveBeenCalledWith({ data: { diff --git a/packages/features/eventtypes/lib/defaultEvents.ts b/packages/features/eventtypes/lib/defaultEvents.ts index 40d4dd8243..b50efc84f1 100644 --- a/packages/features/eventtypes/lib/defaultEvents.ts +++ b/packages/features/eventtypes/lib/defaultEvents.ts @@ -149,6 +149,8 @@ const commons = { eventTypeColor: null, hostGroups: [], bookingRequiresAuthentication: false, + createdAt: null, + updatedAt: null, }; export const dynamicEvent = { diff --git a/packages/lib/test/builder.ts b/packages/lib/test/builder.ts index 4db4ff722c..119dff6bdb 100644 --- a/packages/lib/test/builder.ts +++ b/packages/lib/test/builder.ts @@ -162,6 +162,8 @@ export const buildEventType = (eventType?: Partial): EventType => { restrictionScheduleId: null, useBookerTimezone: false, bookingRequiresAuthentication: false, + createdAt: null, + updatedAt: null, ...eventType, }; }; diff --git a/packages/prisma/extensions/event-type-timestamps.ts b/packages/prisma/extensions/event-type-timestamps.ts new file mode 100644 index 0000000000..9f1f8d5388 --- /dev/null +++ b/packages/prisma/extensions/event-type-timestamps.ts @@ -0,0 +1,27 @@ +import { Prisma } from "@calcom/prisma/client"; + +export function eventTypeTimestampsExtension() { + return Prisma.defineExtension({ + query: { + eventType: { + async create({ args, query }) { + const now = new Date(); + args.data.createdAt = now; + return query(args); + }, + async createMany({ args, query }) { + const now = new Date(); + if (Array.isArray(args.data)) { + args.data = args.data.map((item) => ({ + ...item, + createdAt: now, + })); + } else { + args.data.createdAt = now; + } + return query(args); + }, + }, + }, + }); +} diff --git a/packages/prisma/index.ts b/packages/prisma/index.ts index c014de6c7d..03a4456ab4 100644 --- a/packages/prisma/index.ts +++ b/packages/prisma/index.ts @@ -2,6 +2,7 @@ import { PrismaClient, type Prisma } from "@calcom/prisma/client"; import { bookingIdempotencyKeyExtension } from "./extensions/booking-idempotency-key"; import { disallowUndefinedDeleteUpdateManyExtension } from "./extensions/disallow-undefined-delete-update-many"; +import { eventTypeTimestampsExtension } from "./extensions/event-type-timestamps"; import { excludeLockedUsersExtension } from "./extensions/exclude-locked-users"; import { excludePendingPaymentsExtension } from "./extensions/exclude-pending-payment-teams"; import { usageTrackingExtention } from "./extensions/usage-tracking"; @@ -42,6 +43,7 @@ export const customPrisma = (options?: Prisma.PrismaClientOptions) => .$extends(excludeLockedUsersExtension()) .$extends(excludePendingPaymentsExtension()) .$extends(bookingIdempotencyKeyExtension()) + .$extends(eventTypeTimestampsExtension()) .$extends(disallowUndefinedDeleteUpdateManyExtension()) as unknown as PrismaClient; // If any changed on middleware server restart is required @@ -58,6 +60,7 @@ export const prisma: PrismaClient = baseClient .$extends(excludeLockedUsersExtension()) .$extends(excludePendingPaymentsExtension()) .$extends(bookingIdempotencyKeyExtension()) + .$extends(eventTypeTimestampsExtension()) .$extends(disallowUndefinedDeleteUpdateManyExtension()) as unknown as PrismaClient; // This prisma instance is meant to be used only for READ operations. diff --git a/packages/prisma/migrations/20250919174231_add_event_type_timestamps/migration.sql b/packages/prisma/migrations/20250919174231_add_event_type_timestamps/migration.sql new file mode 100644 index 0000000000..46ad7995c0 --- /dev/null +++ b/packages/prisma/migrations/20250919174231_add_event_type_timestamps/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable +ALTER TABLE "EventType" ADD COLUMN "createdAt" TIMESTAMP(3), +ADD COLUMN "updatedAt" TIMESTAMP(3); diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index 71a60d98d6..2bf272ff2c 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -244,6 +244,9 @@ model EventType { bookingRequiresAuthentication Boolean @default(false) + createdAt DateTime? + updatedAt DateTime? @updatedAt + @@unique([userId, slug]) @@unique([teamId, slug]) @@unique([userId, parentId])