Zapier Trigger: After Meeting ends (#3827)

* add new trigger with first simple job scheduling

* fix DB update

* use array to save scheduled jobs in booking

* cancel scheduled jobs when zap disabled or zapier disconnected

* schedule jobs only for confirmed bookings

* schedule jobs for already existing bookings

* fix bug to create workflow reminders when confirming recurring events

* delete remove all zapier webhooks when api key is deleted

* schedule job for all confirmed recurring bookings

* fix zapier trigger and workflow reminders when cancelling recurring events

* code clean up

* code clean up

* add migration

* add type package for node-schedule

* remove nodescheduler

* add updated nodescheduler

* move code to app-store

* add meeting ended to webhook constants

* udpate zapier README.md

* implement QA suggestions

* add default handler and fix imports

* Type fix

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
Co-authored-by: Omar López <zomars@me.com>
This commit is contained in:
authored and GitHub committed 2022-08-15 20:18:41 +00:00
1 parent 297a965508
commit 89510e6af6
18 files changed
+452 -83

No files matched your search

+3 -2
View File
@@ -7,11 +7,12 @@ export const WEBHOOK_TRIGGER_EVENTS_GROUPED_BY_APP = {
WebhookTriggerEvents.BOOKING_CANCELLED,
WebhookTriggerEvents.BOOKING_CREATED,
WebhookTriggerEvents.BOOKING_RESCHEDULED,
] as ["BOOKING_CANCELLED", "BOOKING_CREATED", "BOOKING_RESCHEDULED"],
WebhookTriggerEvents.MEETING_ENDED,
] as const,
routing_forms: [WebhookTriggerEvents.FORM_SUBMITTED] as ["FORM_SUBMITTED"],
};
export const WEBHOOK_TRIGGER_EVENTS = [
...WEBHOOK_TRIGGER_EVENTS_GROUPED_BY_APP.core,
...WEBHOOK_TRIGGER_EVENTS_GROUPED_BY_APP.routing_forms,
] as ["BOOKING_CANCELLED", "BOOKING_CREATED", "BOOKING_RESCHEDULED", "FORM_SUBMITTED"];
] as const;
+1
View File
@@ -30,6 +30,7 @@ const getWebhooks = async (options: GetSubscriberOptions) => {
},
},
select: {
id: true,
subscriberUrl: true,
payloadTemplate: true,
appId: true,
+108 -6
View File
@@ -1,8 +1,19 @@
import { Booking, BookingStatus, Prisma, SchedulingType, User } from "@prisma/client";
import {
Booking,
BookingStatus,
Prisma,
SchedulingType,
User,
WebhookTriggerEvents,
Workflow,
WorkflowsOnEventTypes,
WorkflowStep,
} from "@prisma/client";
import type { NextApiRequest } from "next";
import { z } from "zod";
import { refund } from "@calcom/app-store/stripepayment/lib/server";
import { scheduleTrigger } from "@calcom/app-store/zapier/lib/nodeScheduler";
import EventManager from "@calcom/core/EventManager";
import { sendDeclinedEmails, sendScheduledEmails } from "@calcom/emails";
import { scheduleWorkflowReminders } from "@calcom/features/ee/workflows/lib/reminders/reminderScheduler";
@@ -14,6 +25,7 @@ import type { AdditionalInformation, CalendarEvent } from "@calcom/types/Calenda
import { getSession } from "@lib/auth";
import { HttpError } from "@lib/core/http/error";
import getSubscribers from "@lib/webhooks/subscriptions";
import { getTranslation } from "@server/lib/i18n";
@@ -30,6 +42,7 @@ const authorized = async (
id: booking.eventTypeId || undefined,
},
select: {
id: true,
schedulingType: true,
users: true,
},
@@ -129,6 +142,7 @@ async function patchHandler(req: NextApiRequest) {
recurringEventId: true,
status: true,
smsReminderNumber: true,
scheduledJobs: true,
},
});
@@ -237,6 +251,21 @@ async function patchHandler(req: NextApiRequest) {
log.error(error);
}
}
let updatedBookings: {
scheduledJobs: string[];
id: number;
startTime: Date;
endTime: Date;
uid: string;
smsReminderNumber: string | null;
eventType: {
workflows: (WorkflowsOnEventTypes & {
workflow: Workflow & {
steps: WorkflowStep[];
};
})[];
} | null;
}[] = [];
if (recurringEventId) {
// The booking to confirm is a recurring event and comes from /booking/recurring, proceeding to mark all related
@@ -247,8 +276,9 @@ async function patchHandler(req: NextApiRequest) {
status: BookingStatus.PENDING,
},
});
unconfirmedRecurringBookings.map(async (recurringBooking) => {
await prisma.booking.update({
const updateBookingsPromise = unconfirmedRecurringBookings.map((recurringBooking) => {
return prisma.booking.update({
where: {
id: recurringBooking.id,
},
@@ -258,12 +288,35 @@ async function patchHandler(req: NextApiRequest) {
create: scheduleResult.referencesToCreate,
},
},
select: {
eventType: {
select: {
workflows: {
include: {
workflow: {
include: {
steps: true,
},
},
},
},
},
},
uid: true,
startTime: true,
endTime: true,
smsReminderNumber: true,
id: true,
scheduledJobs: true,
},
});
});
const updatedBookingsResult = await Promise.all(updateBookingsPromise);
updatedBookings = updatedBookings.concat(updatedBookingsResult);
} else {
// @NOTE: be careful with this as if any error occurs before this booking doesn't get confirmed
// Should perform update on booking (confirm) -> then trigger the rest handlers
await prisma.booking.update({
const updatedBooking = await prisma.booking.update({
where: {
id: bookingId,
},
@@ -273,13 +326,62 @@ async function patchHandler(req: NextApiRequest) {
create: scheduleResult.referencesToCreate,
},
},
select: {
eventType: {
select: {
workflows: {
include: {
workflow: {
include: {
steps: true,
},
},
},
},
},
},
uid: true,
startTime: true,
endTime: true,
smsReminderNumber: true,
id: true,
scheduledJobs: true,
},
});
updatedBookings.push(updatedBooking);
}
//Workflows - set reminders for confirmed events
if (booking.eventType?.workflows) {
await scheduleWorkflowReminders(booking.eventType.workflows, booking.smsReminderNumber, evt, false);
for (const updatedBooking of updatedBookings) {
if (updatedBooking.eventType?.workflows) {
const evtOfBooking = evt;
evtOfBooking.startTime = updatedBooking.startTime.toISOString();
evtOfBooking.endTime = updatedBooking.endTime.toISOString();
evtOfBooking.uid = updatedBooking.uid;
await scheduleWorkflowReminders(
updatedBooking.eventType.workflows,
updatedBooking.smsReminderNumber,
evtOfBooking,
false
);
}
}
// schedule job for zapier trigger 'when meeting ends'
const subscriberOptionsMeetingEnded = {
userId: booking.userId || 0,
eventTypeId: booking.eventTypeId || 0,
triggerEvent: WebhookTriggerEvents.MEETING_ENDED,
};
const subscribersMeetingEnded = await getSubscribers(subscriberOptionsMeetingEnded);
subscribersMeetingEnded.forEach((subscriber) => {
updatedBookings.forEach((booking) => {
scheduleTrigger(booking, subscriber.subscriberUrl, subscriber);
});
});
} else {
evt.rejectionReason = rejectionReason;
if (recurringEventId) {
+21 -1
View File
@@ -6,6 +6,7 @@ import short from "short-uuid";
import { v5 as uuidv5 } from "uuid";
import { handlePayment } from "@calcom/app-store/stripepayment/lib/server";
import { cancelScheduledJobs, scheduleTrigger } from "@calcom/app-store/zapier/lib/nodeScheduler";
import EventManager from "@calcom/core/EventManager";
import { getUserAvailability } from "@calcom/core/getUserAvailability";
import dayjs from "@calcom/dayjs";
@@ -791,13 +792,32 @@ async function handler(req: NextApiRequest) {
log.debug(`Booking ${organizerUser.username} completed`);
const eventTrigger: WebhookTriggerEvents = rescheduleUid ? "BOOKING_RESCHEDULED" : "BOOKING_CREATED";
const eventTrigger: WebhookTriggerEvents = rescheduleUid
? WebhookTriggerEvents.BOOKING_RESCHEDULED
: WebhookTriggerEvents.BOOKING_CREATED;
const subscriberOptions = {
userId: organizerUser.id,
eventTypeId,
triggerEvent: eventTrigger,
};
const subscriberOptionsMeetingEnded = {
userId: organizerUser.id,
eventTypeId,
triggerEvent: WebhookTriggerEvents.MEETING_ENDED,
};
const subscribersMeetingEnded = await getSubscribers(subscriberOptionsMeetingEnded);
subscribersMeetingEnded.forEach((subscriber) => {
if (rescheduleUid && originalRescheduledBooking) {
cancelScheduledJobs(originalRescheduledBooking);
}
if (booking && booking.status === BookingStatus.ACCEPTED) {
scheduleTrigger(booking, subscriber.subscriberUrl, subscriber);
}
});
// Send Webhook call if hooked to BOOKING_CREATED & BOOKING_RESCHEDULED
const subscribers = await getSubscribers(subscriberOptions);
console.log("evt:", {
+66 -24
View File
@@ -1,10 +1,18 @@
import { BookingStatus, Prisma, PrismaPromise, WebhookTriggerEvents, WorkflowMethods } from "@prisma/client";
import {
BookingStatus,
Prisma,
PrismaPromise,
WebhookTriggerEvents,
WorkflowMethods,
WorkflowReminder,
} from "@prisma/client";
import { NextApiRequest, NextApiResponse } from "next";
import z from "zod";
import { getCalendar } from "@calcom/app-store/_utils/getCalendar";
import { FAKE_DAILY_CREDENTIAL } from "@calcom/app-store/dailyvideo/lib/VideoApiAdapter";
import { refund } from "@calcom/app-store/stripepayment/lib/server";
import { cancelScheduledJobs } from "@calcom/app-store/zapier/lib/nodeScheduler";
import { deleteMeeting } from "@calcom/core/videoClient";
import dayjs from "@calcom/dayjs";
import { sendCancelledEmails } from "@calcom/emails";
@@ -82,6 +90,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
destinationCalendar: true,
smsReminderNumber: true,
workflowReminders: true,
scheduledJobs: true,
},
});
@@ -164,6 +173,21 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
);
await Promise.all(promises);
//Workflows - schedule reminders
if (bookingToDelete.eventType?.workflows) {
await sendCancelledReminders(
bookingToDelete.eventType?.workflows,
bookingToDelete.smsReminderNumber,
evt
);
}
let updatedBookings: {
uid: string;
workflowReminders: WorkflowReminder[];
scheduledJobs: string[];
}[] = [];
// by cancelling first, and blocking whilst doing so; we can ensure a cancel
// action always succeeds even if subsequent integrations fail cancellation.
if (bookingToDelete.eventType?.recurringEvent && bookingToDelete.recurringEventId && allRemainingBookings) {
@@ -181,8 +205,22 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
cancellationReason: cancellationReason,
},
});
const allUpdatedBookings = await prisma.booking.findMany({
where: {
recurringEventId: bookingToDelete.recurringEventId,
startTime: {
gte: new Date(),
},
},
select: {
workflowReminders: true,
uid: true,
scheduledJobs: true,
},
});
updatedBookings = updatedBookings.concat(allUpdatedBookings);
} else {
await prisma.booking.update({
const updatedBooking = await prisma.booking.update({
where: {
uid,
},
@@ -190,7 +228,13 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
status: BookingStatus.CANCELLED,
cancellationReason: cancellationReason,
},
select: {
workflowReminders: true,
uid: true,
scheduledJobs: true,
},
});
updatedBookings.push(updatedBooking);
}
/** TODO: Remove this without breaking functionality */
@@ -299,37 +343,35 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
},
});
//Workflows - delete all reminders for that booking
// delete scheduled jobs of cancelled bookings
updatedBookings.forEach((booking) => {
cancelScheduledJobs(booking);
});
//Workflows - delete all reminders for bookings
const remindersToDelete: PrismaPromise<Prisma.BatchPayload>[] = [];
bookingToDelete.workflowReminders.forEach((reminder) => {
if (reminder.scheduled && reminder.referenceId) {
if (reminder.method === WorkflowMethods.EMAIL) {
deleteScheduledEmailReminder(reminder.referenceId);
} else if (reminder.method === WorkflowMethods.SMS) {
deleteScheduledSMSReminder(reminder.referenceId);
updatedBookings.forEach((booking) => {
booking.workflowReminders.forEach((reminder) => {
if (reminder.scheduled && reminder.referenceId) {
if (reminder.method === WorkflowMethods.EMAIL) {
deleteScheduledEmailReminder(reminder.referenceId);
} else if (reminder.method === WorkflowMethods.SMS) {
deleteScheduledSMSReminder(reminder.referenceId);
}
}
}
const reminderToDelete = prisma.workflowReminder.deleteMany({
where: {
id: reminder.id,
},
const reminderToDelete = prisma.workflowReminder.deleteMany({
where: {
id: reminder.id,
},
});
remindersToDelete.push(reminderToDelete);
});
remindersToDelete.push(reminderToDelete);
});
await Promise.all([apiDeletes, attendeeDeletes, bookingReferenceDeletes].concat(remindersToDelete));
await sendCancelledEmails(evt);
//Workflows - schedule reminders
if (bookingToDelete.eventType?.workflows) {
await sendCancelledReminders(
bookingToDelete.eventType?.workflows,
bookingToDelete.smsReminderNumber,
evt
);
}
res.status(204).end();
}
+2 -2
View File
@@ -33,7 +33,7 @@ If you run it on localhost, check out the [additional information](https://githu
## Triggers
Booking created, Booking rescheduled, Booking cancelled
Booking created, Booking rescheduled, Booking cancelled, Meeting ended
### Booking created
@@ -54,7 +54,7 @@ Booking created, Booking rescheduled, Booking cancelled
- PerformList: GET `<baseUrl>`/api/integrations/zapier/listBookings
3. Test your API request
Create the other two triggers (booking rescheduled, booking cancelled) exactly like this one, just use the appropriate naming (e.g. booking_rescheduled instead of booking_created)
Create the other triggers (booking rescheduled, booking cancelled and meeting ended) exactly like this one, just use the appropriate naming (e.g. booking_rescheduled instead of booking_created)
### Set ZAPIER_INVITE_LINK
@@ -1,10 +1,13 @@
import { BookingStatus, WebhookTriggerEvents } from "@prisma/client";
import type { NextApiRequest, NextApiResponse } from "next";
import { v4 } from "uuid";
import { scheduleTrigger } from "@calcom/app-store/zapier/lib/nodeScheduler";
import findValidApiKey from "@calcom/features/ee/api-keys/lib/findValidApiKey";
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
import prisma from "@calcom/prisma";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
async function handler(req: NextApiRequest, res: NextApiResponse) {
const apiKey = req.query.apiKey as string;
if (!apiKey) {
@@ -19,21 +22,43 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const { subscriberUrl, triggerEvent } = req.body;
if (req.method === "POST") {
try {
const createSubscription = await prisma.webhook.create({
data: {
id: v4(),
try {
const createSubscription = await prisma.webhook.create({
data: {
id: v4(),
userId: validKey.userId,
eventTriggers: [triggerEvent],
subscriberUrl,
active: true,
appId: "zapier",
},
});
if (triggerEvent === WebhookTriggerEvents.MEETING_ENDED) {
//schedule job for already existing bookings
const bookings = await prisma.booking.findMany({
where: {
userId: validKey.userId,
eventTriggers: [triggerEvent],
subscriberUrl,
active: true,
appId: "zapier",
startTime: {
gte: new Date(),
},
status: BookingStatus.ACCEPTED,
},
});
res.status(200).json(createSubscription);
} catch (error) {
return res.status(500).json({ message: "Could not create subscription." });
for (const booking of bookings) {
scheduleTrigger(booking, createSubscription.subscriberUrl, {
id: createSubscription.id,
appId: createSubscription.appId,
});
}
}
res.status(200).json(createSubscription);
} catch (error) {
return res.status(500).json({ message: "Could not create subscription." });
}
}
export default defaultHandler({
POST: Promise.resolve({ default: defaultResponder(handler) }),
});
@@ -1,10 +1,18 @@
import { WebhookTriggerEvents } from "@prisma/client";
import type { NextApiRequest, NextApiResponse } from "next";
import z from "zod";
import findValidApiKey from "@calcom/features/ee/api-keys/lib/findValidApiKey";
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
import prisma from "@calcom/prisma";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const apiKey = req.query.apiKey as string;
const querySchema = z.object({
apiKey: z.string(),
id: z.string(),
});
async function handler(req: NextApiRequest, res: NextApiResponse) {
const { apiKey, id } = querySchema.parse(req.query);
if (!apiKey) {
return res.status(401).json({ message: "No API key provided" });
@@ -16,14 +24,44 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
return res.status(401).json({ message: "API key not valid" });
}
const id = req.query.id as string;
const webhook = await prisma.webhook.findFirst({
where: {
id,
},
});
if (req.method === "DELETE") {
await prisma.webhook.delete({
if (webhook?.eventTriggers.includes(WebhookTriggerEvents.MEETING_ENDED)) {
const bookingsWithScheduledJobs = await prisma.booking.findMany({
where: {
id,
userId: validKey.userId,
scheduledJobs: {
isEmpty: false,
},
},
});
res.status(204).json({ message: "Subscription is deleted." });
for (const booking of bookingsWithScheduledJobs) {
const updatedScheduledJobs = booking.scheduledJobs.filter(
(scheduledJob) => scheduledJob !== `zapier_${webhook.id}`
);
await prisma.booking.update({
where: {
id: booking.id,
},
data: {
scheduledJobs: updatedScheduledJobs,
},
});
}
}
await prisma.webhook.delete({
where: {
id,
},
});
res.status(204).json({ message: "Subscription is deleted." });
}
export default defaultHandler({
DELETE: Promise.resolve({ default: defaultResponder(handler) }),
});
@@ -1,9 +1,10 @@
import type { NextApiRequest, NextApiResponse } from "next";
import findValidApiKey from "@calcom/features/ee/api-keys/lib/findValidApiKey";
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
async function handler(req: NextApiRequest, res: NextApiResponse) {
const apiKey = req.query.apiKey as string;
if (!apiKey) {
@@ -16,30 +17,32 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
return res.status(401).json({ message: "API key not valid" });
}
if (req.method === "GET") {
try {
const bookings = await prisma.booking.findMany({
take: 3,
where: {
userId: validKey.userId,
},
select: {
...bookingMinimalSelect,
location: true,
attendees: {
select: {
name: true,
email: true,
timeZone: true,
},
try {
const bookings = await prisma.booking.findMany({
take: 3,
where: {
userId: validKey.userId,
},
select: {
...bookingMinimalSelect,
location: true,
attendees: {
select: {
name: true,
email: true,
timeZone: true,
},
},
});
},
});
res.status(201).json(bookings);
} catch (error) {
console.error(error);
return res.status(500).json({ message: "Unable to get bookings." });
}
res.status(201).json(bookings);
} catch (error) {
console.error(error);
return res.status(500).json({ message: "Unable to get bookings." });
}
}
export default defaultHandler({
GET: Promise.resolve({ default: defaultResponder(handler) }),
});
@@ -0,0 +1,83 @@
import schedule from "node-schedule";
import prisma from "@calcom/prisma";
export async function scheduleTrigger(
booking: { id: number; endTime: Date; scheduledJobs: string[] },
subscriberUrl: string,
subscriber: { id: string; appId: string | null }
) {
//schedule job to call subscriber url at the end of meeting
const job = schedule.scheduleJob(
`${subscriber.appId}_${subscriber.id}`,
booking.endTime,
async function () {
const body = JSON.stringify(booking);
await fetch(subscriberUrl, {
method: "POST",
body,
});
//remove scheduled job from bookings once triggered
const updatedScheduledJobs = booking.scheduledJobs.filter((scheduledJob) => {
return scheduledJob !== `${subscriber.appId}_${subscriber.id}`;
});
await prisma.booking.update({
where: {
id: booking.id,
},
data: {
scheduledJobs: updatedScheduledJobs,
},
});
}
);
//add scheduled job name to booking
await prisma.booking.update({
where: {
id: booking.id,
},
data: {
scheduledJobs: {
push: job.name,
},
},
});
}
export async function cancelScheduledJobs(
booking: { uid: string; scheduledJobs?: string[] },
appId?: string | null
) {
let scheduledJobs = booking.scheduledJobs || [];
if (booking.scheduledJobs) {
booking.scheduledJobs.forEach(async (scheduledJob) => {
if (appId) {
if (scheduledJob.startsWith(appId)) {
if (schedule.scheduledJobs[scheduledJob]) {
schedule.scheduledJobs[scheduledJob].cancel();
}
scheduledJobs = scheduledJobs?.filter((job) => scheduledJob !== job) || [];
}
} else {
//if no specific appId given, delete all scheduled jobs of booking
if (schedule.scheduledJobs[scheduledJob]) {
schedule.scheduledJobs[scheduledJob].cancel();
}
scheduledJobs = [];
}
await prisma.booking.update({
where: {
uid: booking.uid,
},
data: {
scheduledJobs: scheduledJobs,
},
});
});
}
}
+4 -2
View File
@@ -6,9 +6,11 @@
"main": "./index.ts",
"description": "Workflow automation for everyone. Use the Cal.com Zapier app to trigger your workflows when a booking was created, rescheduled or cancled.",
"dependencies": {
"@calcom/lib": "*"
"@calcom/lib": "*",
"node-schedule": "2.1.0"
},
"devDependencies": {
"@calcom/types": "*"
"@calcom/types": "*",
"@types/node-schedule": "^2.1.0"
}
}
@@ -1,4 +1,3 @@
import { WorkflowActions } from ".prisma/client";
import dayjs from "@calcom/dayjs";
const emailReminderTemplate = (
+3 -2
View File
@@ -7,11 +7,12 @@ export const WEBHOOK_TRIGGER_EVENTS_GROUPED_BY_APP = {
WebhookTriggerEvents.BOOKING_CANCELLED,
WebhookTriggerEvents.BOOKING_CREATED,
WebhookTriggerEvents.BOOKING_RESCHEDULED,
] as ["BOOKING_CANCELLED", "BOOKING_CREATED", "BOOKING_RESCHEDULED"],
WebhookTriggerEvents.MEETING_ENDED,
] as const,
routing_forms: [WebhookTriggerEvents.FORM_SUBMITTED] as ["FORM_SUBMITTED"],
};
export const WEBHOOK_TRIGGER_EVENTS = [
...WEBHOOK_TRIGGER_EVENTS_GROUPED_BY_APP.core,
...WEBHOOK_TRIGGER_EVENTS_GROUPED_BY_APP.routing_forms,
] as ["BOOKING_CANCELLED", "BOOKING_CREATED", "BOOKING_RESCHEDULED", "FORM_SUBMITTED"];
] as const;
+1
View File
@@ -30,6 +30,7 @@ const getWebhooks = async (options: GetSubscriberOptions) => {
},
},
select: {
id: true,
subscriberUrl: true,
payloadTemplate: true,
appId: true,
@@ -0,0 +1,5 @@
-- AlterEnum
ALTER TYPE "WebhookTriggerEvents" ADD VALUE 'MEETING_ENDED';
-- AlterTable
ALTER TABLE "Booking" ADD COLUMN "scheduledJobs" TEXT[];
+2
View File
@@ -291,6 +291,7 @@ model Booking {
recurringEventId String?
smsReminderNumber String?
workflowReminders WorkflowReminder[]
scheduledJobs String[]
}
model Schedule {
@@ -388,6 +389,7 @@ enum WebhookTriggerEvents {
BOOKING_RESCHEDULED
BOOKING_CANCELLED
FORM_SUBMITTED
MEETING_ENDED
}
model Webhook {
+27
View File
@@ -6,6 +6,7 @@ import { z } from "zod";
import app_RoutingForms from "@calcom/app-store/ee/routing_forms/trpc-router";
import stripe, { closePayments } from "@calcom/app-store/stripepayment/lib/server";
import getApps, { getLocationOptions } from "@calcom/app-store/utils";
import { cancelScheduledJobs } from "@calcom/app-store/zapier/lib/nodeScheduler";
import { getCalendarCredentials, getConnectedCalendars } from "@calcom/core/CalendarManager";
import dayjs from "@calcom/dayjs";
import { sendCancelledEmails, sendFeedbackEmail } from "@calcom/emails";
@@ -1226,6 +1227,32 @@ const loggedInViewerRouter = createProtectedRouter()
}
}
// if zapier get disconnected, delete zapier apiKey, delete zapier webhooks and cancel all scheduled jobs from zapier
if (credential.app?.slug === "zapier") {
await prisma.apiKey.deleteMany({
where: {
userId: ctx.user.id,
appId: "zapier",
},
});
await prisma.webhook.deleteMany({
where: {
appId: "zapier",
},
});
const bookingsWithScheduledJobs = await prisma.booking.findMany({
where: {
userId: ctx.user.id,
scheduledJobs: {
isEmpty: false,
},
},
});
for (const booking of bookingsWithScheduledJobs) {
cancelScheduledJobs(booking, credential.appId);
}
}
// Validated that credential is user's above
await prisma.credential.delete({
where: {
@@ -113,6 +113,13 @@ export const apiKeysRouter = createProtectedRouter()
}),
async resolve({ ctx, input }) {
const { id } = input;
const apiKeyToDelete = await ctx.prisma.apiKey.findFirst({
where: {
id
}
})
await ctx.prisma.user.update({
where: {
id: ctx.user.id,
@@ -125,6 +132,16 @@ export const apiKeysRouter = createProtectedRouter()
},
},
});
//remove all existing zapier webhooks, as we always have only one zapier API key and the running zaps won't work any more if this key is deleted
if(apiKeyToDelete && apiKeyToDelete.appId === 'zapier'){
await ctx.prisma.webhook.deleteMany({
where: {
appId: 'zapier'
}
})
}
return {
id,
};