parent
a9b45c1057
commit
5c4a9c32d1
4 changed files with 326 additions and 278 deletions
20
lib/logger.ts
Normal file
20
lib/logger.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { Logger } from "tslog";
|
||||
|
||||
const isProduction = process.env.NODE_ENV === "production";
|
||||
|
||||
const logger = new Logger({
|
||||
dateTimePattern: "hour:minute:second.millisecond timeZoneName",
|
||||
displayFunctionName: false,
|
||||
displayFilePath: "hidden",
|
||||
dateTimeTimezone: isProduction ? "utc" : Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
prettyInspectHighlightStyles: {
|
||||
name: "yellow",
|
||||
number: "blue",
|
||||
bigint: "blue",
|
||||
boolean: "blue",
|
||||
},
|
||||
maskValuesOfKeys: ["password", "passwordConfirmation", "credentials", "credential"],
|
||||
exposeErrorCodeFrame: !isProduction,
|
||||
});
|
||||
|
||||
export default logger;
|
|
@ -36,6 +36,7 @@
|
|||
"react-select": "^4.3.0",
|
||||
"react-timezone-select": "^1.0.2",
|
||||
"short-uuid": "^4.2.0",
|
||||
"tslog": "^3.2.0",
|
||||
"uuid": "^8.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
@ -10,8 +10,10 @@ import { getEventName } from "../../../lib/event";
|
|||
import { LocationType } from "../../../lib/location";
|
||||
import merge from "lodash.merge";
|
||||
import dayjs from "dayjs";
|
||||
import logger from "../../../lib/logger";
|
||||
|
||||
const translator = short();
|
||||
const log = logger.getChildLogger({ prefix: ["[api] book:user"] });
|
||||
|
||||
function isAvailable(busyTimes, time, length) {
|
||||
// Check for conflicts
|
||||
|
@ -69,304 +71,322 @@ const getLocationRequestFromIntegration = ({ location }: GetLocationRequestFromI
|
|||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse): Promise<void> {
|
||||
const { user } = req.query;
|
||||
|
||||
const isTimeInPast = (time) => {
|
||||
return dayjs(time).isBefore(new Date(), "day");
|
||||
};
|
||||
|
||||
if (isTimeInPast(req.body.start)) {
|
||||
return res
|
||||
.status(400)
|
||||
.json({ errorCode: "BookingDateInPast", message: "Attempting to create a meeting in the past." });
|
||||
}
|
||||
|
||||
let currentUser = await prisma.user.findFirst({
|
||||
where: {
|
||||
username: user,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
credentials: true,
|
||||
timeZone: true,
|
||||
email: true,
|
||||
name: true,
|
||||
},
|
||||
});
|
||||
|
||||
const selectedCalendars = await prisma.selectedCalendar.findMany({
|
||||
where: {
|
||||
userId: currentUser.id,
|
||||
},
|
||||
});
|
||||
|
||||
// Split credentials up into calendar credentials and video credentials
|
||||
let calendarCredentials = currentUser.credentials.filter((cred) => cred.type.endsWith("_calendar"));
|
||||
let videoCredentials = currentUser.credentials.filter((cred) => cred.type.endsWith("_video"));
|
||||
|
||||
const hasCalendarIntegrations =
|
||||
currentUser.credentials.filter((cred) => cred.type.endsWith("_calendar")).length > 0;
|
||||
const hasVideoIntegrations =
|
||||
currentUser.credentials.filter((cred) => cred.type.endsWith("_video")).length > 0;
|
||||
|
||||
const calendarAvailability = await getBusyCalendarTimes(
|
||||
currentUser.credentials,
|
||||
dayjs(req.body.start).startOf("day").utc().format(),
|
||||
dayjs(req.body.end).endOf("day").utc().format(),
|
||||
selectedCalendars
|
||||
);
|
||||
const videoAvailability = await getBusyVideoTimes(
|
||||
currentUser.credentials,
|
||||
dayjs(req.body.start).startOf("day").utc().format(),
|
||||
dayjs(req.body.end).endOf("day").utc().format()
|
||||
);
|
||||
let commonAvailability = [];
|
||||
|
||||
if (hasCalendarIntegrations && hasVideoIntegrations) {
|
||||
commonAvailability = calendarAvailability.filter((availability) =>
|
||||
videoAvailability.includes(availability)
|
||||
);
|
||||
} else if (hasVideoIntegrations) {
|
||||
commonAvailability = videoAvailability;
|
||||
} else if (hasCalendarIntegrations) {
|
||||
commonAvailability = calendarAvailability;
|
||||
}
|
||||
|
||||
// Now, get the newly stored credentials (new refresh token for example).
|
||||
currentUser = await prisma.user.findFirst({
|
||||
where: {
|
||||
username: user,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
credentials: true,
|
||||
timeZone: true,
|
||||
email: true,
|
||||
name: true,
|
||||
},
|
||||
});
|
||||
calendarCredentials = currentUser.credentials.filter((cred) => cred.type.endsWith("_calendar"));
|
||||
videoCredentials = currentUser.credentials.filter((cred) => cred.type.endsWith("_video"));
|
||||
|
||||
const rescheduleUid = req.body.rescheduleUid;
|
||||
|
||||
const selectedEventType = await prisma.eventType.findFirst({
|
||||
where: {
|
||||
userId: currentUser.id,
|
||||
id: req.body.eventTypeId,
|
||||
},
|
||||
select: {
|
||||
eventName: true,
|
||||
title: true,
|
||||
length: true,
|
||||
},
|
||||
});
|
||||
|
||||
const rawLocation = req.body.location;
|
||||
|
||||
let evt: CalendarEvent = {
|
||||
type: selectedEventType.title,
|
||||
title: getEventName(req.body.name, selectedEventType.title, selectedEventType.eventName),
|
||||
description: req.body.notes,
|
||||
startTime: req.body.start,
|
||||
endTime: req.body.end,
|
||||
organizer: { email: currentUser.email, name: currentUser.name, timeZone: currentUser.timeZone },
|
||||
attendees: [{ email: req.body.email, name: req.body.name, timeZone: req.body.timeZone }],
|
||||
};
|
||||
|
||||
// If phone or inPerson use raw location
|
||||
// set evt.location to req.body.location
|
||||
if (!rawLocation?.includes("integration")) {
|
||||
evt.location = rawLocation;
|
||||
}
|
||||
|
||||
// If location is set to an integration location
|
||||
// Build proper transforms for evt object
|
||||
// Extend evt object with those transformations
|
||||
if (rawLocation?.includes("integration")) {
|
||||
const maybeLocationRequestObject = getLocationRequestFromIntegration({
|
||||
location: rawLocation,
|
||||
});
|
||||
|
||||
evt = merge(evt, maybeLocationRequestObject);
|
||||
}
|
||||
|
||||
const eventType = await prisma.eventType.findFirst({
|
||||
where: {
|
||||
userId: currentUser.id,
|
||||
title: evt.type,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
|
||||
let isAvailableToBeBooked = true;
|
||||
log.debug(`Booking ${user} started`);
|
||||
|
||||
try {
|
||||
isAvailableToBeBooked = isAvailable(commonAvailability, req.body.start, selectedEventType.length);
|
||||
} catch {
|
||||
console.debug({
|
||||
message: "Unable set isAvailableToBeBooked. Using true. ",
|
||||
});
|
||||
}
|
||||
const isTimeInPast = (time) => {
|
||||
return dayjs(time).isBefore(new Date(), "day");
|
||||
};
|
||||
|
||||
if (!isAvailableToBeBooked) {
|
||||
return res.status(400).json({ message: `${currentUser.name} is unavailable at this time.` });
|
||||
}
|
||||
if (isTimeInPast(req.body.start)) {
|
||||
const error = {
|
||||
errorCode: "BookingDateInPast",
|
||||
message: "Attempting to create a meeting in the past.",
|
||||
};
|
||||
|
||||
let results = [];
|
||||
let referencesToCreate = [];
|
||||
log.error(`Booking ${user} failed`, error);
|
||||
return res.status(400).json(error);
|
||||
}
|
||||
|
||||
if (rescheduleUid) {
|
||||
// Reschedule event
|
||||
const booking = await prisma.booking.findFirst({
|
||||
let currentUser = await prisma.user.findFirst({
|
||||
where: {
|
||||
uid: rescheduleUid,
|
||||
username: user,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
references: {
|
||||
select: {
|
||||
id: true,
|
||||
type: true,
|
||||
uid: true,
|
||||
credentials: true,
|
||||
timeZone: true,
|
||||
email: true,
|
||||
name: true,
|
||||
},
|
||||
});
|
||||
|
||||
const selectedCalendars = await prisma.selectedCalendar.findMany({
|
||||
where: {
|
||||
userId: currentUser.id,
|
||||
},
|
||||
});
|
||||
|
||||
// Split credentials up into calendar credentials and video credentials
|
||||
let calendarCredentials = currentUser.credentials.filter((cred) => cred.type.endsWith("_calendar"));
|
||||
let videoCredentials = currentUser.credentials.filter((cred) => cred.type.endsWith("_video"));
|
||||
|
||||
const hasCalendarIntegrations =
|
||||
currentUser.credentials.filter((cred) => cred.type.endsWith("_calendar")).length > 0;
|
||||
const hasVideoIntegrations =
|
||||
currentUser.credentials.filter((cred) => cred.type.endsWith("_video")).length > 0;
|
||||
|
||||
const calendarAvailability = await getBusyCalendarTimes(
|
||||
currentUser.credentials,
|
||||
dayjs(req.body.start).startOf("day").utc().format(),
|
||||
dayjs(req.body.end).endOf("day").utc().format(),
|
||||
selectedCalendars
|
||||
);
|
||||
const videoAvailability = await getBusyVideoTimes(
|
||||
currentUser.credentials,
|
||||
dayjs(req.body.start).startOf("day").utc().format(),
|
||||
dayjs(req.body.end).endOf("day").utc().format()
|
||||
);
|
||||
let commonAvailability = [];
|
||||
|
||||
if (hasCalendarIntegrations && hasVideoIntegrations) {
|
||||
commonAvailability = calendarAvailability.filter((availability) =>
|
||||
videoAvailability.includes(availability)
|
||||
);
|
||||
} else if (hasVideoIntegrations) {
|
||||
commonAvailability = videoAvailability;
|
||||
} else if (hasCalendarIntegrations) {
|
||||
commonAvailability = calendarAvailability;
|
||||
}
|
||||
|
||||
// Now, get the newly stored credentials (new refresh token for example).
|
||||
currentUser = await prisma.user.findFirst({
|
||||
where: {
|
||||
username: user,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
credentials: true,
|
||||
timeZone: true,
|
||||
email: true,
|
||||
name: true,
|
||||
},
|
||||
});
|
||||
calendarCredentials = currentUser.credentials.filter((cred) => cred.type.endsWith("_calendar"));
|
||||
videoCredentials = currentUser.credentials.filter((cred) => cred.type.endsWith("_video"));
|
||||
|
||||
const rescheduleUid = req.body.rescheduleUid;
|
||||
|
||||
const selectedEventType = await prisma.eventType.findFirst({
|
||||
where: {
|
||||
userId: currentUser.id,
|
||||
id: req.body.eventTypeId,
|
||||
},
|
||||
select: {
|
||||
eventName: true,
|
||||
title: true,
|
||||
length: true,
|
||||
},
|
||||
});
|
||||
|
||||
const rawLocation = req.body.location;
|
||||
|
||||
let evt: CalendarEvent = {
|
||||
type: selectedEventType.title,
|
||||
title: getEventName(req.body.name, selectedEventType.title, selectedEventType.eventName),
|
||||
description: req.body.notes,
|
||||
startTime: req.body.start,
|
||||
endTime: req.body.end,
|
||||
organizer: { email: currentUser.email, name: currentUser.name, timeZone: currentUser.timeZone },
|
||||
attendees: [{ email: req.body.email, name: req.body.name, timeZone: req.body.timeZone }],
|
||||
};
|
||||
|
||||
// If phone or inPerson use raw location
|
||||
// set evt.location to req.body.location
|
||||
if (!rawLocation?.includes("integration")) {
|
||||
evt.location = rawLocation;
|
||||
}
|
||||
|
||||
// If location is set to an integration location
|
||||
// Build proper transforms for evt object
|
||||
// Extend evt object with those transformations
|
||||
if (rawLocation?.includes("integration")) {
|
||||
const maybeLocationRequestObject = getLocationRequestFromIntegration({
|
||||
location: rawLocation,
|
||||
});
|
||||
|
||||
evt = merge(evt, maybeLocationRequestObject);
|
||||
}
|
||||
|
||||
const eventType = await prisma.eventType.findFirst({
|
||||
where: {
|
||||
userId: currentUser.id,
|
||||
title: evt.type,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
|
||||
const isAvailableToBeBooked = true;
|
||||
|
||||
if (!isAvailableToBeBooked) {
|
||||
const error = {
|
||||
errorCode: "BookingUserUnAvailable",
|
||||
message: `${currentUser.name} is unavailable at this time.`,
|
||||
};
|
||||
|
||||
log.debug(`Booking ${user} failed`, error);
|
||||
return res.status(400).json(error);
|
||||
}
|
||||
|
||||
let results = [];
|
||||
let referencesToCreate = [];
|
||||
|
||||
if (rescheduleUid) {
|
||||
// Reschedule event
|
||||
const booking = await prisma.booking.findFirst({
|
||||
where: {
|
||||
uid: rescheduleUid,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
references: {
|
||||
select: {
|
||||
id: true,
|
||||
type: true,
|
||||
uid: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// Use all integrations
|
||||
results = results.concat(
|
||||
await async.mapLimit(calendarCredentials, 5, async (credential) => {
|
||||
const bookingRefUid = booking.references.filter((ref) => ref.type === credential.type)[0].uid;
|
||||
return updateEvent(credential, bookingRefUid, evt)
|
||||
.then((response) => ({ type: credential.type, success: true, response }))
|
||||
.catch((e) => {
|
||||
console.error("updateEvent failed", e);
|
||||
return { type: credential.type, success: false };
|
||||
});
|
||||
})
|
||||
);
|
||||
// Use all integrations
|
||||
results = results.concat(
|
||||
await async.mapLimit(calendarCredentials, 5, async (credential) => {
|
||||
const bookingRefUid = booking.references.filter((ref) => ref.type === credential.type)[0].uid;
|
||||
return updateEvent(credential, bookingRefUid, evt)
|
||||
.then((response) => ({ type: credential.type, success: true, response }))
|
||||
.catch((e) => {
|
||||
log.error("updateEvent failed", e, evt);
|
||||
return { type: credential.type, success: false };
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
results = results.concat(
|
||||
await async.mapLimit(videoCredentials, 5, async (credential) => {
|
||||
const bookingRefUid = booking.references.filter((ref) => ref.type === credential.type)[0].uid;
|
||||
return updateMeeting(credential, bookingRefUid, evt)
|
||||
.then((response) => ({ type: credential.type, success: true, response }))
|
||||
.catch((e) => {
|
||||
console.error("updateMeeting failed", e);
|
||||
return { type: credential.type, success: false };
|
||||
});
|
||||
})
|
||||
);
|
||||
results = results.concat(
|
||||
await async.mapLimit(videoCredentials, 5, async (credential) => {
|
||||
const bookingRefUid = booking.references.filter((ref) => ref.type === credential.type)[0].uid;
|
||||
return updateMeeting(credential, bookingRefUid, evt)
|
||||
.then((response) => ({ type: credential.type, success: true, response }))
|
||||
.catch((e) => {
|
||||
log.error("updateMeeting failed", e, evt);
|
||||
return { type: credential.type, success: false };
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
if (results.length > 0 && results.every((res) => !res.success)) {
|
||||
res.status(500).json({ message: "Rescheduling failed" });
|
||||
return;
|
||||
if (results.length > 0 && results.every((res) => !res.success)) {
|
||||
const error = {
|
||||
errorCode: "BookingReschedulingMeetingFailed",
|
||||
message: "Booking Rescheduling failed",
|
||||
};
|
||||
|
||||
log.error(`Booking ${user} failed`, error, results);
|
||||
return res.status(500).json(error);
|
||||
}
|
||||
|
||||
// Clone elements
|
||||
referencesToCreate = [...booking.references];
|
||||
|
||||
// Now we can delete the old booking and its references.
|
||||
const bookingReferenceDeletes = prisma.bookingReference.deleteMany({
|
||||
where: {
|
||||
bookingId: booking.id,
|
||||
},
|
||||
});
|
||||
const attendeeDeletes = prisma.attendee.deleteMany({
|
||||
where: {
|
||||
bookingId: booking.id,
|
||||
},
|
||||
});
|
||||
const bookingDeletes = prisma.booking.delete({
|
||||
where: {
|
||||
uid: rescheduleUid,
|
||||
},
|
||||
});
|
||||
|
||||
await Promise.all([bookingReferenceDeletes, attendeeDeletes, bookingDeletes]);
|
||||
} else {
|
||||
// Schedule event
|
||||
results = results.concat(
|
||||
await async.mapLimit(calendarCredentials, 5, async (credential) => {
|
||||
return createEvent(credential, evt)
|
||||
.then((response) => ({ type: credential.type, success: true, response }))
|
||||
.catch((e) => {
|
||||
log.error("createEvent failed", e, evt);
|
||||
return { type: credential.type, success: false };
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
results = results.concat(
|
||||
await async.mapLimit(videoCredentials, 5, async (credential) => {
|
||||
return createMeeting(credential, evt)
|
||||
.then((response) => ({ type: credential.type, success: true, response }))
|
||||
.catch((e) => {
|
||||
log.error("createMeeting failed", e, evt);
|
||||
return { type: credential.type, success: false };
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
if (results.length > 0 && results.every((res) => !res.success)) {
|
||||
const error = {
|
||||
errorCode: "BookingCreatingMeetingFailed",
|
||||
message: "Booking failed",
|
||||
};
|
||||
|
||||
log.error(`Booking ${user} failed`, error, results);
|
||||
return res.status(500).json(error);
|
||||
}
|
||||
|
||||
referencesToCreate = results.map((result) => {
|
||||
return {
|
||||
type: result.type,
|
||||
uid: result.response.createdEvent.id.toString(),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// Clone elements
|
||||
referencesToCreate = [...booking.references];
|
||||
|
||||
// Now we can delete the old booking and its references.
|
||||
const bookingReferenceDeletes = prisma.bookingReference.deleteMany({
|
||||
where: {
|
||||
bookingId: booking.id,
|
||||
},
|
||||
});
|
||||
const attendeeDeletes = prisma.attendee.deleteMany({
|
||||
where: {
|
||||
bookingId: booking.id,
|
||||
},
|
||||
});
|
||||
const bookingDeletes = prisma.booking.delete({
|
||||
where: {
|
||||
uid: rescheduleUid,
|
||||
},
|
||||
});
|
||||
|
||||
await Promise.all([bookingReferenceDeletes, attendeeDeletes, bookingDeletes]);
|
||||
} else {
|
||||
// Schedule event
|
||||
results = results.concat(
|
||||
await async.mapLimit(calendarCredentials, 5, async (credential) => {
|
||||
return createEvent(credential, evt)
|
||||
.then((response) => ({ type: credential.type, success: true, response }))
|
||||
.catch((e) => {
|
||||
console.error("createEvent failed", e);
|
||||
return { type: credential.type, success: false };
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
results = results.concat(
|
||||
await async.mapLimit(videoCredentials, 5, async (credential) => {
|
||||
return createMeeting(credential, evt)
|
||||
.then((response) => ({ type: credential.type, success: true, response }))
|
||||
.catch((e) => {
|
||||
console.error("createMeeting failed", e);
|
||||
return { type: credential.type, success: false };
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
if (results.length > 0 && results.every((res) => !res.success)) {
|
||||
res.status(500).json({ message: "Booking failed" });
|
||||
return;
|
||||
const hashUID =
|
||||
results.length > 0
|
||||
? results[0].response.uid
|
||||
: translator.fromUUID(uuidv5(JSON.stringify(evt), uuidv5.URL));
|
||||
// TODO Should just be set to the true case as soon as we have a "bare email" integration class.
|
||||
// UID generation should happen in the integration itself, not here.
|
||||
if (results.length === 0) {
|
||||
// Legacy as well, as soon as we have a separate email integration class. Just used
|
||||
// to send an email even if there is no integration at all.
|
||||
try {
|
||||
const mail = new EventAttendeeMail(evt, hashUID);
|
||||
await mail.sendEmail();
|
||||
} catch (e) {
|
||||
log.error("Sending legacy event mail failed", e);
|
||||
log.error(`Booking ${user} failed`);
|
||||
res.status(500).json({ message: "Booking failed" });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
referencesToCreate = results.map((result) => {
|
||||
return {
|
||||
type: result.type,
|
||||
uid: result.response.createdEvent.id.toString(),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const hashUID =
|
||||
results.length > 0
|
||||
? results[0].response.uid
|
||||
: translator.fromUUID(uuidv5(JSON.stringify(evt), uuidv5.URL));
|
||||
// TODO Should just be set to the true case as soon as we have a "bare email" integration class.
|
||||
// UID generation should happen in the integration itself, not here.
|
||||
if (results.length === 0) {
|
||||
// Legacy as well, as soon as we have a separate email integration class. Just used
|
||||
// to send an email even if there is no integration at all.
|
||||
try {
|
||||
const mail = new EventAttendeeMail(evt, hashUID);
|
||||
await mail.sendEmail();
|
||||
await prisma.booking.create({
|
||||
data: {
|
||||
uid: hashUID,
|
||||
userId: currentUser.id,
|
||||
references: {
|
||||
create: referencesToCreate,
|
||||
},
|
||||
eventTypeId: eventType.id,
|
||||
title: evt.title,
|
||||
description: evt.description,
|
||||
startTime: evt.startTime,
|
||||
endTime: evt.endTime,
|
||||
attendees: {
|
||||
create: evt.attendees,
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("Sending legacy event mail failed", e);
|
||||
res.status(500).json({ message: "Booking failed" });
|
||||
log.error(`Booking ${user} failed`, "Error when saving booking to db", e);
|
||||
res.status(500).json({ message: "Booking already exists" });
|
||||
return;
|
||||
}
|
||||
|
||||
log.debug(`Booking ${user} completed`);
|
||||
return res.status(204).json({ message: "Booking completed" });
|
||||
} catch (reason) {
|
||||
log.error(`Booking ${user} failed`, reason);
|
||||
return res.status(500).json({ message: "Booking failed for some unknown reason" });
|
||||
}
|
||||
|
||||
try {
|
||||
await prisma.booking.create({
|
||||
data: {
|
||||
uid: hashUID,
|
||||
userId: currentUser.id,
|
||||
references: {
|
||||
create: referencesToCreate,
|
||||
},
|
||||
eventTypeId: eventType.id,
|
||||
|
||||
title: evt.title,
|
||||
description: evt.description,
|
||||
startTime: evt.startTime,
|
||||
endTime: evt.endTime,
|
||||
|
||||
attendees: {
|
||||
create: evt.attendees,
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("Error when saving booking to db", e);
|
||||
res.status(500).json({ message: "Booking already exists" });
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(204).json({});
|
||||
}
|
||||
|
|
|
@ -5402,7 +5402,7 @@ source-map-js@^0.6.2:
|
|||
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-0.6.2.tgz#0bb5de631b41cfbda6cfba8bd05a80efdfd2385e"
|
||||
integrity sha512-/3GptzWzu0+0MBQFrDKzw/DvvMTUORvgY6k6jd/VS6iCR4RDTKWH6v6WPwQoUO8667uQEf9Oe38DxAYWY5F/Ug==
|
||||
|
||||
source-map-support@^0.5.6:
|
||||
source-map-support@^0.5.19, source-map-support@^0.5.6:
|
||||
version "0.5.19"
|
||||
resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61"
|
||||
integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==
|
||||
|
@ -5873,6 +5873,13 @@ tslib@^2.1.0:
|
|||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.0.tgz#803b8cdab3e12ba581a4ca41c8839bbb0dacb09e"
|
||||
integrity sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==
|
||||
|
||||
tslog@^3.2.0:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/tslog/-/tslog-3.2.0.tgz#4982c289a8948670d6a1c49c29977ae9f861adfa"
|
||||
integrity sha512-xOCghepl5w+wcI4qXI7vJy6c53loF8OoC/EuKz1ktAPMtltEDz00yo1poKuyBYIQaq4ZDYKYFPD9PfqVrFXh0A==
|
||||
dependencies:
|
||||
source-map-support "^0.5.19"
|
||||
|
||||
tsutils@^3.21.0:
|
||||
version "3.21.0"
|
||||
resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623"
|
||||
|
|
Loading…
Reference in a new issue