fix: Use Google recurring events protocol (#12651)
* working create event --wip-- [skip ci] fix bug related to adding event --wip-- [skip ci] --wip-- [skip ci] add delete all remove console.log remove console.log add patching of instances fix yarn.lock remove comments * fixed typing * use rrule * rollback tsconfig changes * fix comments * fix comments * rename db column * fix delete all booking * Update apps/web/pages/api/book/recurring-event.ts Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com> * remove non-null assertion * attempt to fix type errors * fix recurring event logic * remove skipAvailabilityCheck --------- Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com> Co-authored-by: CarinaWolli <wollencarina@gmail.com> Co-authored-by: Peer Richelsen <peeroke@gmail.com> Co-authored-by: Hariom <hariombalhara@gmail.com>
This commit is contained in:
12 files changed
+269
-101
No files matched your search
@@ -20,13 +20,16 @@ async function handler(req: NextApiRequest & { userId?: number }, res: NextApiRe
|
||||
const data: RecurringBookingCreateBody[] = req.body;
|
||||
const session = await getServerSession({ req, res });
|
||||
const createdBookings: BookingResponse[] = [];
|
||||
const allRecurringDates: string[] = data.map((booking) => booking.start);
|
||||
const allRecurringDates: { start: string | undefined; end: string | undefined }[] = data.map((booking) => {
|
||||
return { start: booking.start, end: booking.end };
|
||||
});
|
||||
const appsStatus: AppsStatus[] | undefined = undefined;
|
||||
|
||||
/* To mimic API behavior and comply with types */
|
||||
req.userId = session?.user?.id || -1;
|
||||
const numSlotsToCheckForAvailability = 2;
|
||||
|
||||
let thirdPartyRecurringEventId = null;
|
||||
for (let key = 0; key < data.length; key++) {
|
||||
const booking = data[key];
|
||||
// Disable AppStatus in Recurring Booking Email as it requires us to iterate backwards to be able to compute the AppsStatus for all the bookings except the very first slot and then send that slot's email with statuses
|
||||
@@ -53,16 +56,34 @@ async function handler(req: NextApiRequest & { userId?: number }, res: NextApiRe
|
||||
...booking,
|
||||
appsStatus,
|
||||
allRecurringDates,
|
||||
isFirstRecurringSlot: key == 0,
|
||||
thirdPartyRecurringEventId,
|
||||
numSlotsToCheckForAvailability,
|
||||
currentRecurringIndex: key,
|
||||
noEmail: key !== 0,
|
||||
};
|
||||
|
||||
const eachRecurringBooking = await handleNewBooking(recurringEventReq, {
|
||||
isNotAnApiCall: true,
|
||||
skipAvailabilityCheck: key >= numSlotsToCheckForAvailability,
|
||||
});
|
||||
const promiseEachRecurringBooking: ReturnType<typeof handleNewBooking> = handleNewBooking(
|
||||
recurringEventReq,
|
||||
{
|
||||
isNotAnApiCall: true,
|
||||
}
|
||||
);
|
||||
|
||||
const eachRecurringBooking = await promiseEachRecurringBooking;
|
||||
|
||||
createdBookings.push(eachRecurringBooking);
|
||||
|
||||
if (!thirdPartyRecurringEventId) {
|
||||
if (eachRecurringBooking.references && eachRecurringBooking.references.length > 0) {
|
||||
for (const reference of eachRecurringBooking.references!) {
|
||||
if (reference.thirdPartyRecurringEventId) {
|
||||
thirdPartyRecurringEventId = reference.thirdPartyRecurringEventId;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return createdBookings;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import type { Prisma } from "@prisma/client";
|
||||
import type { calendar_v3 } from "googleapis";
|
||||
import { google } from "googleapis";
|
||||
import { RRule } from "rrule";
|
||||
|
||||
import { MeetLocationType } from "@calcom/app-store/locations";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
@@ -209,6 +210,16 @@ export default class GoogleCalendarService implements Calendar {
|
||||
payload["location"] = getLocation(calEventRaw);
|
||||
}
|
||||
|
||||
if (calEventRaw.recurringEvent) {
|
||||
const rule = new RRule({
|
||||
freq: calEventRaw.recurringEvent.freq,
|
||||
interval: calEventRaw.recurringEvent.interval,
|
||||
count: calEventRaw.recurringEvent.count,
|
||||
});
|
||||
|
||||
payload["recurrence"] = [rule.toString()];
|
||||
}
|
||||
|
||||
if (calEventRaw.conferenceData && calEventRaw.location === MeetLocationType) {
|
||||
payload["conferenceData"] = calEventRaw.conferenceData;
|
||||
}
|
||||
@@ -220,22 +231,70 @@ export default class GoogleCalendarService implements Calendar {
|
||||
"primary";
|
||||
|
||||
try {
|
||||
const event = await calendar.events.insert({
|
||||
calendarId: selectedCalendar,
|
||||
requestBody: payload,
|
||||
conferenceDataVersion: 1,
|
||||
sendUpdates: "none",
|
||||
});
|
||||
let event;
|
||||
let recurringEventId = null;
|
||||
if (calEventRaw.existingRecurringEvent) {
|
||||
recurringEventId = calEventRaw.existingRecurringEvent.recurringEventId;
|
||||
const recurringEventInstances = await calendar.events.instances({
|
||||
calendarId: selectedCalendar,
|
||||
eventId: calEventRaw.existingRecurringEvent.recurringEventId,
|
||||
});
|
||||
if (recurringEventInstances.data.items) {
|
||||
const calComEventStartTime = dayjs(calEventRaw.startTime).format();
|
||||
for (let i = 0; i < recurringEventInstances.data.items.length; i++) {
|
||||
const instance = recurringEventInstances.data.items[i];
|
||||
const instanceStartTime = dayjs(instance.start?.dateTime)
|
||||
.tz(instance.start?.timeZone == null ? undefined : instance.start?.timeZone)
|
||||
.format();
|
||||
|
||||
if (event && event.data.id && event.data.hangoutLink) {
|
||||
if (instanceStartTime === calComEventStartTime) {
|
||||
event = instance;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!event) {
|
||||
event = recurringEventInstances.data.items[0];
|
||||
this.log.error(
|
||||
"Unable to find matching event amongst recurring event instances",
|
||||
safeStringify({ selectedCalendar, credentialId })
|
||||
);
|
||||
}
|
||||
await calendar.events.patch({
|
||||
calendarId: selectedCalendar,
|
||||
eventId: event.id || "",
|
||||
requestBody: {
|
||||
description: getRichDescription({
|
||||
...calEventRaw,
|
||||
}),
|
||||
},
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const eventResponse = await calendar.events.insert({
|
||||
calendarId: selectedCalendar,
|
||||
requestBody: payload,
|
||||
conferenceDataVersion: 1,
|
||||
sendUpdates: "none",
|
||||
});
|
||||
event = eventResponse.data;
|
||||
if (event.recurrence) {
|
||||
if (event.recurrence.length > 0) {
|
||||
recurringEventId = event.id;
|
||||
event = await this.getFirstEventInRecurrence(recurringEventId, selectedCalendar, calendar);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (event && event.id && event.hangoutLink) {
|
||||
await calendar.events.patch({
|
||||
// Update the same event but this time we know the hangout link
|
||||
calendarId: selectedCalendar,
|
||||
eventId: event.data.id || "",
|
||||
eventId: event.id || "",
|
||||
requestBody: {
|
||||
description: getRichDescription({
|
||||
...calEventRaw,
|
||||
additionalInformation: { hangoutLink: event.data.hangoutLink },
|
||||
additionalInformation: { hangoutLink: event.hangoutLink },
|
||||
}),
|
||||
},
|
||||
});
|
||||
@@ -243,14 +302,16 @@ export default class GoogleCalendarService implements Calendar {
|
||||
|
||||
return {
|
||||
uid: "",
|
||||
...event.data,
|
||||
id: event.data.id || "",
|
||||
...event,
|
||||
id: event?.id || "",
|
||||
thirdPartyRecurringEventId: recurringEventId,
|
||||
additionalInfo: {
|
||||
hangoutLink: event.data.hangoutLink || "",
|
||||
hangoutLink: event?.hangoutLink || "",
|
||||
},
|
||||
type: "google_calendar",
|
||||
password: "",
|
||||
url: "",
|
||||
iCalUID: event?.iCalUID,
|
||||
};
|
||||
} catch (error) {
|
||||
this.log.error(
|
||||
@@ -260,6 +321,22 @@ export default class GoogleCalendarService implements Calendar {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
async getFirstEventInRecurrence(
|
||||
recurringEventId: string | null | undefined,
|
||||
selectedCalendar: string,
|
||||
calendar: calendar_v3.Calendar
|
||||
): Promise<calendar_v3.Schema$Event> {
|
||||
const recurringEventInstances = await calendar.events.instances({
|
||||
calendarId: selectedCalendar,
|
||||
eventId: recurringEventId || "",
|
||||
});
|
||||
|
||||
if (recurringEventInstances.data.items) {
|
||||
return recurringEventInstances.data.items[0];
|
||||
} else {
|
||||
return {} as calendar_v3.Schema$Event;
|
||||
}
|
||||
}
|
||||
|
||||
async updateEvent(uid: string, event: CalendarEvent, externalCalendarId: string): Promise<any> {
|
||||
const payload: calendar_v3.Schema$Event = {
|
||||
|
||||
@@ -173,6 +173,7 @@ export default class EventManager {
|
||||
|
||||
// References can be any type: calendar/video
|
||||
const referencesToCreate = results.map((result) => {
|
||||
let thirdPartyRecurringEventId;
|
||||
let createdEventObj: createdEventSchema | null = null;
|
||||
if (typeof result?.createdEvent === "string") {
|
||||
createdEventObj = createdEventSchema.parse(JSON.parse(result.createdEvent));
|
||||
@@ -180,11 +181,13 @@ export default class EventManager {
|
||||
const isCalendarType = isCalendarResult(result);
|
||||
if (isCalendarType) {
|
||||
evt.iCalUID = result.iCalUID || event.iCalUID || undefined;
|
||||
thirdPartyRecurringEventId = result.createdEvent?.thirdPartyRecurringEventId;
|
||||
}
|
||||
|
||||
return {
|
||||
type: result.type,
|
||||
uid: createdEventObj ? createdEventObj.id : result.createdEvent?.id?.toString() ?? "",
|
||||
thirdPartyRecurringEventId: isCalendarType ? thirdPartyRecurringEventId : undefined,
|
||||
meetingId: createdEventObj ? createdEventObj.id : result.createdEvent?.id?.toString(),
|
||||
meetingPassword: createdEventObj ? createdEventObj.password : result.createdEvent?.password,
|
||||
meetingUrl: createdEventObj ? createdEventObj.onlineMeetingUrl : result.createdEvent?.url,
|
||||
|
||||
@@ -4,6 +4,7 @@ import type {
|
||||
AdditionalInformation,
|
||||
CalendarEvent,
|
||||
ConferenceData,
|
||||
ExistingRecurringEvent,
|
||||
Person,
|
||||
VideoCallData,
|
||||
} from "@calcom/types/Calendar";
|
||||
@@ -22,6 +23,7 @@ class CalendarEventClass implements CalendarEvent {
|
||||
conferenceData?: ConferenceData;
|
||||
additionalInformation?: AdditionalInformation;
|
||||
uid?: string | null;
|
||||
existingRecurringEvent?: ExistingRecurringEvent | null;
|
||||
videoCallData?: VideoCallData;
|
||||
paymentInfo?: any;
|
||||
destinationCalendar?: DestinationCalendar[] | null;
|
||||
|
||||
@@ -59,6 +59,7 @@ async function getBookingToDelete(id: number | undefined, uid: string | undefine
|
||||
type: true,
|
||||
externalCalendarId: true,
|
||||
credentialId: true,
|
||||
thirdPartyRecurringEventId: true,
|
||||
},
|
||||
},
|
||||
payment: true,
|
||||
@@ -458,24 +459,37 @@ async function handler(req: CustomRequest) {
|
||||
bookingToDelete.recurringEventId &&
|
||||
allRemainingBookings
|
||||
) {
|
||||
const promises = bookingToDelete.user.credentials
|
||||
.filter((credential) => credential.type.endsWith("_calendar"))
|
||||
.map(async (credential) => {
|
||||
const calendar = await getCalendar(credential);
|
||||
for (const updBooking of updatedBookings) {
|
||||
const bookingRef = updBooking.references.find((ref) => ref.type.includes("_calendar"));
|
||||
if (bookingRef) {
|
||||
const { uid, externalCalendarId } = bookingRef;
|
||||
const deletedEvent = await calendar?.deleteEvent(uid, evt, externalCalendarId);
|
||||
apiDeletes.push(deletedEvent);
|
||||
let thirdPartyRecurringEventId;
|
||||
for (const reference of bookingToDelete.references) {
|
||||
if (reference.thirdPartyRecurringEventId) {
|
||||
thirdPartyRecurringEventId = reference.thirdPartyRecurringEventId;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (thirdPartyRecurringEventId) {
|
||||
apiDeletes.push(
|
||||
calendar?.deleteEvent(thirdPartyRecurringEventId, evt, externalCalendarId) as Promise<unknown>
|
||||
);
|
||||
} else {
|
||||
const promises = bookingToDelete.user.credentials
|
||||
.filter((credential) => credential.type.endsWith("_calendar"))
|
||||
.map(async (credential) => {
|
||||
const calendar = await getCalendar(credential);
|
||||
for (const updBooking of updatedBookings) {
|
||||
const bookingRef = updBooking.references.find((ref) => ref.type.includes("_calendar"));
|
||||
if (bookingRef) {
|
||||
const { uid, externalCalendarId } = bookingRef;
|
||||
const deletedEvent = await calendar?.deleteEvent(uid, evt, externalCalendarId);
|
||||
apiDeletes.push(deletedEvent);
|
||||
}
|
||||
}
|
||||
});
|
||||
try {
|
||||
await Promise.all(promises);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
logger.error(error.message);
|
||||
}
|
||||
});
|
||||
try {
|
||||
await Promise.all(promises);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
logger.error(error.message);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -666,7 +680,8 @@ async function handler(req: CustomRequest) {
|
||||
const prismaPromises: Promise<unknown>[] = [bookingReferenceDeletes];
|
||||
|
||||
try {
|
||||
const settled = await Promise.allSettled(prismaPromises.concat(apiDeletes));
|
||||
const temp = prismaPromises.concat(apiDeletes);
|
||||
const settled = await Promise.allSettled(temp);
|
||||
const rejected = settled.filter(({ status }) => status === "rejected") as PromiseRejectedResult[];
|
||||
if (rejected.length) {
|
||||
throw new Error(`Reasons: ${rejected.map(({ reason }) => reason)}`);
|
||||
|
||||
@@ -985,13 +985,10 @@ async function handler(
|
||||
req: NextApiRequest & { userId?: number | undefined },
|
||||
{
|
||||
isNotAnApiCall = false,
|
||||
skipAvailabilityCheck = false,
|
||||
}: {
|
||||
isNotAnApiCall?: boolean;
|
||||
skipAvailabilityCheck?: boolean;
|
||||
} = {
|
||||
isNotAnApiCall: false,
|
||||
skipAvailabilityCheck: false,
|
||||
}
|
||||
) {
|
||||
const { userId } = req;
|
||||
@@ -1061,7 +1058,6 @@ async function handler(
|
||||
isTeamEventType,
|
||||
eventType: getPiiFreeEventType(eventType),
|
||||
dynamicUserList,
|
||||
skipAvailabilityCheck,
|
||||
paymentAppData: {
|
||||
enabled: paymentAppData.enabled,
|
||||
price: paymentAppData.price,
|
||||
@@ -1230,58 +1226,85 @@ async function handler(
|
||||
}
|
||||
|
||||
//checks what users are available
|
||||
if (!eventType.seatsPerTimeSlot && !skipAvailabilityCheck) {
|
||||
const availableUsers = await ensureAvailableUsers(
|
||||
{
|
||||
...eventType,
|
||||
users: users as IsFixedAwareUser[],
|
||||
...(eventType.recurringEvent && {
|
||||
recurringEvent: {
|
||||
...eventType.recurringEvent,
|
||||
count: recurringCount || eventType.recurringEvent.count,
|
||||
},
|
||||
}),
|
||||
},
|
||||
{
|
||||
dateFrom: dayjs(reqBody.start).tz(reqBody.timeZone).format(),
|
||||
dateTo: dayjs(reqBody.end).tz(reqBody.timeZone).format(),
|
||||
timeZone: reqBody.timeZone,
|
||||
originalRescheduledBooking,
|
||||
},
|
||||
loggerWithEventDetails
|
||||
);
|
||||
|
||||
const luckyUsers: typeof users = [];
|
||||
const luckyUserPool = availableUsers.filter((user) => !user.isFixed);
|
||||
loggerWithEventDetails.debug(
|
||||
"Computed available users",
|
||||
safeStringify({
|
||||
availableUsers: availableUsers.map((user) => user.id),
|
||||
luckyUserPool: luckyUserPool.map((user) => user.id),
|
||||
})
|
||||
);
|
||||
// loop through all non-fixed hosts and get the lucky users
|
||||
while (luckyUserPool.length > 0 && luckyUsers.length < 1 /* TODO: Add variable */) {
|
||||
const newLuckyUser = await getLuckyUser("MAXIMIZE_AVAILABILITY", {
|
||||
// find a lucky user that is not already in the luckyUsers array
|
||||
availableUsers: luckyUserPool.filter(
|
||||
(user) => !luckyUsers.find((existing) => existing.id === user.id)
|
||||
),
|
||||
eventTypeId: eventType.id,
|
||||
});
|
||||
if (!newLuckyUser) {
|
||||
break; // prevent infinite loop
|
||||
if (!eventType.seatsPerTimeSlot) {
|
||||
const eventTypeWithUsers: Awaited<ReturnType<typeof getEventTypesFromDB>> & {
|
||||
users: IsFixedAwareUser[];
|
||||
} = {
|
||||
...eventType,
|
||||
users: users as IsFixedAwareUser[],
|
||||
...(eventType.recurringEvent && {
|
||||
recurringEvent: {
|
||||
...eventType.recurringEvent,
|
||||
count: recurringCount || eventType.recurringEvent.count,
|
||||
},
|
||||
}),
|
||||
};
|
||||
if (req.body.allRecurringDates) {
|
||||
if (req.body.isFirstRecurringSlot) {
|
||||
for (
|
||||
let i = 0;
|
||||
i < req.body.allRecurringDates.length && i < req.body.numSlotsToCheckForAvailability;
|
||||
i++
|
||||
) {
|
||||
const start = req.body.allRecurringDates[i].start;
|
||||
const end = req.body.allRecurringDates[i].end;
|
||||
await ensureAvailableUsers(
|
||||
eventTypeWithUsers,
|
||||
{
|
||||
dateFrom: dayjs(start).tz(reqBody.timeZone).format(),
|
||||
dateTo: dayjs(end).tz(reqBody.timeZone).format(),
|
||||
timeZone: reqBody.timeZone,
|
||||
originalRescheduledBooking,
|
||||
},
|
||||
loggerWithEventDetails
|
||||
);
|
||||
}
|
||||
}
|
||||
luckyUsers.push(newLuckyUser);
|
||||
}
|
||||
// ALL fixed users must be available
|
||||
if (
|
||||
availableUsers.filter((user) => user.isFixed).length !== users.filter((user) => user.isFixed).length
|
||||
) {
|
||||
throw new Error(ErrorCode.HostsUnavailableForBooking);
|
||||
if (!req.body.allRecurringDates || req.body.isFirstRecurringSlot) {
|
||||
const availableUsers = await ensureAvailableUsers(
|
||||
eventTypeWithUsers,
|
||||
{
|
||||
dateFrom: dayjs(reqBody.start).tz(reqBody.timeZone).format(),
|
||||
dateTo: dayjs(reqBody.end).tz(reqBody.timeZone).format(),
|
||||
timeZone: reqBody.timeZone,
|
||||
originalRescheduledBooking,
|
||||
},
|
||||
loggerWithEventDetails
|
||||
);
|
||||
|
||||
const luckyUsers: typeof users = [];
|
||||
const luckyUserPool = availableUsers.filter((user) => !user.isFixed);
|
||||
loggerWithEventDetails.debug(
|
||||
"Computed available users",
|
||||
safeStringify({
|
||||
availableUsers: availableUsers.map((user) => user.id),
|
||||
luckyUserPool: luckyUserPool.map((user) => user.id),
|
||||
})
|
||||
);
|
||||
// loop through all non-fixed hosts and get the lucky users
|
||||
while (luckyUserPool.length > 0 && luckyUsers.length < 1 /* TODO: Add variable */) {
|
||||
const newLuckyUser = await getLuckyUser("MAXIMIZE_AVAILABILITY", {
|
||||
// find a lucky user that is not already in the luckyUsers array
|
||||
availableUsers: luckyUserPool.filter(
|
||||
(user) => !luckyUsers.find((existing) => existing.id === user.id)
|
||||
),
|
||||
eventTypeId: eventType.id,
|
||||
});
|
||||
if (!newLuckyUser) {
|
||||
break; // prevent infinite loop
|
||||
}
|
||||
luckyUsers.push(newLuckyUser);
|
||||
}
|
||||
// ALL fixed users must be available
|
||||
if (
|
||||
availableUsers.filter((user) => user.isFixed).length !== users.filter((user) => user.isFixed).length
|
||||
) {
|
||||
throw new Error(ErrorCode.HostsUnavailableForBooking);
|
||||
}
|
||||
// Pushing fixed user before the luckyUser guarantees the (first) fixed user as the organizer.
|
||||
users = [...availableUsers.filter((user) => user.isFixed), ...luckyUsers];
|
||||
}
|
||||
// Pushing fixed user before the luckyUser guarantees the (first) fixed user as the organizer.
|
||||
users = [...availableUsers.filter((user) => user.isFixed), ...luckyUsers];
|
||||
}
|
||||
|
||||
const [organizerUser] = users;
|
||||
@@ -1459,6 +1482,12 @@ async function handler(
|
||||
iCalSequence,
|
||||
};
|
||||
|
||||
if (req.body.thirdPartyRecurringEventId) {
|
||||
evt.existingRecurringEvent = {
|
||||
recurringEventId: req.body.thirdPartyRecurringEventId,
|
||||
};
|
||||
}
|
||||
|
||||
if (isTeamEventType && eventType.schedulingType === "COLLECTIVE") {
|
||||
evt.destinationCalendar?.push(...teamDestinationCalendars);
|
||||
}
|
||||
@@ -1563,7 +1592,9 @@ async function handler(
|
||||
let results: EventResult<AdditionalInformation & { url?: string; iCalUID?: string }>[] = [];
|
||||
let referencesToCreate: PartialReference[] = [];
|
||||
|
||||
let booking: (Booking & { appsStatus?: AppsStatus[] }) | null = null;
|
||||
let booking: (Booking & { appsStatus?: AppsStatus[]; paymentUid?: string; paymentId?: number }) | null =
|
||||
null;
|
||||
|
||||
loggerWithEventDetails.debug(
|
||||
"Going to create booking in DB now",
|
||||
safeStringify({
|
||||
@@ -2253,6 +2284,7 @@ async function handler(
|
||||
req.statusCode = 201;
|
||||
return {
|
||||
...booking,
|
||||
references: referencesToCreate,
|
||||
seatReferenceUid: evt.attendeeSeatId,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ export type RecurringBookingCreateBody = BookingCreateBody & {
|
||||
noEmail?: boolean;
|
||||
recurringCount?: number;
|
||||
appsStatus?: AppsStatus[] | undefined;
|
||||
allRecurringDates?: string[];
|
||||
allRecurringDates?: Record<string, string>[];
|
||||
currentRecurringIndex?: number;
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "BookingReference" ADD COLUMN "thirdPartyRecurringEventId" TEXT;
|
||||
@@ -373,19 +373,20 @@ model InstantMeetingToken {
|
||||
}
|
||||
|
||||
model BookingReference {
|
||||
id Int @id @default(autoincrement())
|
||||
id Int @id @default(autoincrement())
|
||||
/// @zod.min(1)
|
||||
type String
|
||||
type String
|
||||
/// @zod.min(1)
|
||||
uid String
|
||||
meetingId String?
|
||||
meetingPassword String?
|
||||
meetingUrl String?
|
||||
booking Booking? @relation(fields: [bookingId], references: [id], onDelete: Cascade)
|
||||
bookingId Int?
|
||||
externalCalendarId String?
|
||||
deleted Boolean?
|
||||
credentialId Int?
|
||||
uid String
|
||||
meetingId String?
|
||||
thirdPartyRecurringEventId String?
|
||||
meetingPassword String?
|
||||
meetingUrl String?
|
||||
booking Booking? @relation(fields: [bookingId], references: [id], onDelete: Cascade)
|
||||
bookingId Int?
|
||||
externalCalendarId String?
|
||||
deleted Boolean?
|
||||
credentialId Int?
|
||||
|
||||
@@index([bookingId])
|
||||
@@index([credentialId])
|
||||
|
||||
@@ -250,7 +250,14 @@ export const extendedBookingCreateBody = bookingCreateBodySchema.merge(
|
||||
z.object({
|
||||
noEmail: z.boolean().optional(),
|
||||
recurringCount: z.number().optional(),
|
||||
allRecurringDates: z.string().array().optional(),
|
||||
allRecurringDates: z
|
||||
.array(
|
||||
z.object({
|
||||
start: z.string(),
|
||||
end: z.string(),
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
currentRecurringIndex: z.number().optional(),
|
||||
appsStatus: z
|
||||
.array(
|
||||
@@ -301,6 +308,7 @@ export const vitalSettingsUpdateSchema = z.object({
|
||||
export const createdEventSchema = z
|
||||
.object({
|
||||
id: z.string(),
|
||||
thirdPartyRecurringEventId: z.string(),
|
||||
password: z.union([z.string(), z.undefined()]),
|
||||
onlineMeetingUrl: z.string().nullable(),
|
||||
iCalUID: z.string().optional(),
|
||||
|
||||
Vendored
+6
@@ -63,6 +63,7 @@ export type AdditionalInfo = Record<string, unknown> & { calWarnings?: string[]
|
||||
export type NewCalendarEventType = {
|
||||
uid: string;
|
||||
id: string;
|
||||
thirdPartyRecurringEventId?: string | null;
|
||||
type: string;
|
||||
password: string;
|
||||
url: string;
|
||||
@@ -143,6 +144,10 @@ export type CalEventResponses = Record<
|
||||
}
|
||||
>;
|
||||
|
||||
export interface ExistingRecurringEvent {
|
||||
recurringEventId: string;
|
||||
}
|
||||
|
||||
// If modifying this interface, probably should update builders/calendarEvent files
|
||||
export interface CalendarEvent {
|
||||
// Instead of sending this per event.
|
||||
@@ -167,6 +172,7 @@ export interface CalendarEvent {
|
||||
conferenceData?: ConferenceData;
|
||||
additionalInformation?: AdditionalInformation;
|
||||
uid?: string | null;
|
||||
existingRecurringEvent?: ExistingRecurringEvent | null;
|
||||
bookingId?: number;
|
||||
videoCallData?: VideoCallData;
|
||||
paymentInfo?: PaymentInfo | null;
|
||||
|
||||
Vendored
+1
@@ -6,6 +6,7 @@ export interface PartialReference {
|
||||
uid: string;
|
||||
meetingId?: string | null;
|
||||
meetingPassword?: string | null;
|
||||
thirdPartyRecurringEventId?: string | null;
|
||||
meetingUrl?: string | null;
|
||||
externalCalendarId?: string | null;
|
||||
credentialId?: number | null;
|
||||
|
||||
Reference in new issue
Block a user