From 5c4a9c32d1d37dc7bb37aedab6eb43b1396e778b Mon Sep 17 00:00:00 2001 From: Femi Odugbesan Date: Fri, 9 Jul 2021 10:49:42 -0500 Subject: [PATCH] Add application logger (#332) * add application logger * use logger --- lib/logger.ts | 20 ++ package.json | 1 + pages/api/book/[user].ts | 574 ++++++++++++++++++++------------------- yarn.lock | 9 +- 4 files changed, 326 insertions(+), 278 deletions(-) create mode 100644 lib/logger.ts diff --git a/lib/logger.ts b/lib/logger.ts new file mode 100644 index 00000000..a4ce3e7c --- /dev/null +++ b/lib/logger.ts @@ -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; diff --git a/package.json b/package.json index 4b8836be..99e3462e 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/pages/api/book/[user].ts b/pages/api/book/[user].ts index 32ae584d..b088f647 100644 --- a/pages/api/book/[user].ts +++ b/pages/api/book/[user].ts @@ -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 { 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({}); } diff --git a/yarn.lock b/yarn.lock index 5448eb4f..478c781d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"