diff --git a/backend/app.js b/backend/app.js index af91fb691..1c302e05a 100644 --- a/backend/app.js +++ b/backend/app.js @@ -1,10 +1,11 @@ + // app.js - Entry point for our application // Load in all of our node modules. Their uses are explained below as they are called. const express = require('express'); +const morgan = require('morgan'); const cron = require('node-cron'); const fetch = require('node-fetch'); -const morgan = require('morgan'); const cookieParser = require('cookie-parser'); const customRequestHeaderName = 'x-customrequired-header'; @@ -52,12 +53,14 @@ app.use(cookieParser()); app.use(morgan('dev')); // WORKERS -const runOpenCheckinWorker = require('./workers/openCheckins')(cron, fetch); -const runCloseCheckinWorker = require('./workers/closeCheckins')(cron, fetch); +const runOpenCheckinWorker = require('./workers/openCheckins'); +runOpenCheckinWorker(cron, fetch); -const { createRecurringEvents } = require('./workers/createRecurringEvents'); -const runCreateRecurringEventsWorker = createRecurringEvents(cron, fetch); +const runCloseCheckinWorker = require('./workers/closeCheckins'); +runCloseCheckinWorker(cron, fetch); +const { createRecurringEvents } = require('./workers/createRecurringEvents'); +createRecurringEvents(cron, fetch); // const runSlackBot = require("./workers/slackbot")(fetch); // MIDDLEWARE diff --git a/backend/controllers/event.controller.js b/backend/controllers/event.controller.js index e0e52af2d..ca6b275ba 100644 --- a/backend/controllers/event.controller.js +++ b/backend/controllers/event.controller.js @@ -9,6 +9,7 @@ EventController.event_list = async function (req, res) { const events = await Event.find(query).populate('project'); return res.status(200).send(events); } catch (err) { + console.error('[event.controller]', err); return res.sendStatus(400); } }; @@ -20,6 +21,7 @@ EventController.event_by_id = async function (req, res) { const events = await Event.findById(EventId).populate('project'); return res.status(200).send(events); } catch (err) { + console.error('[event.controller]', err); return res.sendStatus(400); } }; @@ -28,9 +30,15 @@ EventController.create = async function (req, res) { const { body } = req; try { - const event = await Event.create(body); - return res.status(201).send(event); + if (Array.isArray(body)) { + const events = await Event.insertMany(body); + return res.status(201).send(events); + } else { + const event = await Event.create(body); + return res.status(201).send(event); + } } catch (err) { + console.error('[event.controller]', err); return res.sendStatus(400); } }; @@ -42,17 +50,31 @@ EventController.destroy = async function (req, res) { const event = await Event.findByIdAndDelete(EventId); return res.status(200).send(event); } catch (err) { + console.error('[event.controller]', err); return res.sendStatus(400); } }; EventController.update = async function (req, res) { - const { EventId } = req.params; + const { body } = req; try { - const event = await Event.findByIdAndUpdate(EventId, req.body); - return res.status(200).send(event); + if (Array.isArray(body)) { + const ops = body.map((e) => ({ + updateOne: { + filter: { _id: e._id }, + update: { $set: { checkInReady: e.checkInReady } }, + }, + })); + const events = await Event.bulkWrite(ops); + return res.status(200).send(events); + } else { + const { EventId } = req.params; + const event = await Event.findByIdAndUpdate(EventId, req.body); + return res.status(200).send(event); + } } catch (err) { + console.error('[event.controller]', err); return res.sendStatus(400); } }; diff --git a/backend/routers/events.router.js b/backend/routers/events.router.js index 7614b2012..7a81e1938 100644 --- a/backend/routers/events.router.js +++ b/backend/routers/events.router.js @@ -1,4 +1,4 @@ -const express = require("express"); +const express = require('express'); const router = express.Router(); const { Event } = require('../models/event.model'); @@ -13,12 +13,14 @@ router.get('/:EventId', EventController.event_by_id); router.delete('/:EventId', EventController.destroy); +router.patch('/batchUpdate', EventController.update); + router.patch('/:EventId', EventController.update); // TODO: Refactor and remove -router.get("/nexteventbyproject/:id", (req, res) => { +router.get('/nexteventbyproject/:id', (req, res) => { Event.find({ project: req.params.id }) - .populate("project") + .populate('project') .then((events) => { res.status(200).json(events[events.length - 1]); }) diff --git a/backend/workers/closeCheckins.js b/backend/workers/closeCheckins.js index 9d04cd7da..4a5141521 100644 --- a/backend/workers/closeCheckins.js +++ b/backend/workers/closeCheckins.js @@ -1,86 +1,99 @@ module.exports = (cron, fetch) => { + // Check to see if any events are about to start, + // and if so, open their respective check-ins + + const url = + process.env.NODE_ENV === 'prod' + ? 'https://www.vrms.io' + : `http://localhost:${process.env.BACKEND_PORT}`; + const headerToSend = process.env.CUSTOM_REQUEST_HEADER; + + async function fetchEvents() { + try { + const res = await fetch(`${url}/api/events`, { + headers: { + 'x-customrequired-header': headerToSend, + }, + }); + const resJson = await res.json(); + + return resJson; + } catch (error) { + console.log(error); + } + } + + async function updateEvents(eventsToUpdate) { + try { + const res = await fetch(`${url}/api/events/batchUpdate`, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + 'x-customrequired-header': headerToSend, + }, + body: JSON.stringify(eventsToUpdate), + }); + if (!res.ok) throw new Error('Failed to update event'); + return await res.json(); + } catch (error) { + console.error('Error updating event:', error); + return null; + } + } + async function sortAndFilterEvents() { + const events = await fetchEvents(); + + // Get current time and set to date variable + const now = Date.now(); + + // Filter events if event date is after now but before thirty minutes from now + if (events && events.length > 0) { + const sortedEvents = events.filter((event) => { + if (!event.date) { + // handle if event date is null/undefined + // false meaning don't include in sortedEvents + return false; + } + // Calculate three hours from now + const threeHoursFromStartTime = new Date(event.date).getTime() + 10800000; + if (Number.isNaN(threeHoursFromStartTime)) return false; + return now >= threeHoursFromStartTime && event.checkInReady === true; + }); + + // console.log('Sorted events: ', sortedEvents); + return sortedEvents; + } + } + + async function closeCheckins(events) { + if (events && events.length > 0) { + console.log('Closing check-ins'); + // console.log('Closing event: ', event); + const batchEventsToUpdate = events.map((e) => ({ + _id: e._id, + checkInReady: false, + })); + const updatedEvents = await updateEvents(batchEventsToUpdate); + if (updatedEvents) console.log('Updated events:', updatedEvents); + console.log('Check-ins closed'); + } else { + console.log('No open events to close'); + } + } + + async function runTask() { + const eventsToClose = await sortAndFilterEvents().catch((err) => { + console.log(err); + }); - // Check to see if any events are about to start, - // and if so, open their respective check-ins - - const url = process.env.NODE_ENV === 'prod' ? 'https://www.vrms.io' : `http://localhost:${process.env.BACKEND_PORT}`; - const headerToSend = process.env.CUSTOM_REQUEST_HEADER; - - async function fetchEvents() { - try { - const res = await fetch(`${url}/api/events`, { - headers: { - "x-customrequired-header": headerToSend - } - }); - const resJson = await res.json(); - - return resJson; - } catch(error) { - console.log(error); - }; - }; - - async function sortAndFilterEvents() { - const events = await fetchEvents(); - - // Filter events if event date is after now but before thirty minutes from now - if (events && events.length > 0) { - - const sortedEvents = events.filter(event => { - if (!event.date) { - // handle if event date is null/undefined - // false meaning don't include in sortedEvents - return false - } - - const currentTimeISO = new Date().toISOString(); - const threeHoursFromStartTime = new Date(event.date).getTime() + 10800000; - const threeHoursISO = new Date(threeHoursFromStartTime).toISOString(); - - return (currentTimeISO > threeHoursISO) && (event.checkInReady === true); - }); - - // console.log('Sorted events: ', sortedEvents); - return sortedEvents; - }; - }; - - async function closeCheckins(events) { - if(events && events.length > 0) { - events.forEach(async event => { - // console.log('Closing event: ', event); - - await fetch(`${url}/api/events/${event._id}`, { - method: "PATCH", - headers: { - "Content-Type": "application/json", - "x-customrequired-header": headerToSend - }, - body: JSON.stringify({ checkInReady: false }) - }) - .catch(err => { - console.log(err); - }); - }); - }; - }; - - async function runTask() { - console.log("Closing check-ins"); - - const eventsToClose = await sortAndFilterEvents() - .catch(err => {console.log(err)}); - - await closeCheckins(eventsToClose) - .catch(err => {console.log(err)}); - - console.log("Check-ins closed"); - }; - - const scheduledTask = cron.schedule('*/30 * * * *', () => { - runTask(); + await closeCheckins(eventsToClose).catch((err) => { + console.log(err); }); + } + + const scheduledTask = cron.schedule('*/30 * * * *', () => { + runTask(); + }); - return scheduledTask; -}; \ No newline at end of file + return scheduledTask; +}; diff --git a/backend/workers/createRecurringEvents.js b/backend/workers/createRecurringEvents.js index 0516f1b12..d5fce7dff 100644 --- a/backend/workers/createRecurringEvents.js +++ b/backend/workers/createRecurringEvents.js @@ -1,6 +1,7 @@ const { generateEventData } = require('./lib/generateEventData'); -/** +//API CALLS to GET and POST +/** GET * Utility to fetch data from an API endpoint. * @param {string} endpoint - The API endpoint to fetch data from. * @param {string} URL - The base URL for API requests. @@ -20,6 +21,31 @@ const fetchData = async (endpoint, URL, headerToSend, fetch) => { } }; +/** POST + * Creates a new event by making a POST request to the events API. + * @param {Object} eventArray - The events array data to create. + * @returns {Promise} - The created event data or null on failure. + */ +const createEvents = async (eventArray, URL, headerToSend, fetch) => { + if (!eventArray) return null; + + try { + const res = await fetch(`${URL}/api/events/`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-customrequired-header': headerToSend, + }, + body: JSON.stringify(eventArray), + }); + if (!res.ok) throw new Error('Failed to create event'); + return await res.json(); + } catch (error) { + console.error('Error creating event:', error); + return null; + } +}; + /** * Checks if two dates are on the same day in UTC. * @param {Date} eventDate - Event date. @@ -47,28 +73,21 @@ const doesEventExist = (recurringEventName, today, events) => }); /** - * Creates a new event by making a POST request to the events API. - * @param {Object} event - The event data to create. - * @returns {Promise} - The created event data or null on failure. + * Adjusts an event date to Los_Angeles time, accounting for DST offsets. + * @param {Date} eventDate - The event date to adjust. + * @returns {Date} - The adjusted event date. */ -const createEvent = async (event, URL, headerToSend, fetch) => { - if (!event) return null; - - try { - const res = await fetch(`${URL}/api/events/`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'x-customrequired-header': headerToSend, - }, - body: JSON.stringify(event), - }); - if (!res.ok) throw new Error('Failed to create event'); - return await res.json(); - } catch (error) { - console.error('Error creating event:', error); - return null; - } +const adjustToLosAngelesTime = (eventDate) => { + const tempDate = new Date(eventDate); + const losAngelesOffsetHours = new Intl.DateTimeFormat('en-US', { + timeZone: 'America/Los_Angeles', + timeZoneName: 'shortOffset', + }) + .formatToParts(tempDate) + .find((part) => part.type === 'timeZoneName') + .value.slice(3); + const offsetMinutes = parseInt(losAngelesOffsetHours, 10) * 60; + return new Date(tempDate.getTime() + offsetMinutes * 60000); }; /** @@ -84,51 +103,62 @@ const createEvent = async (event, URL, headerToSend, fetch) => { const filterAndCreateEvents = async (events, recurringEvents, URL, headerToSend, fetch) => { const today = new Date(); const todayUTCDay = today.getUTCDay(); - // filter recurring events for today and not already existing - const eventsToCreate = recurringEvents.filter((recurringEvent) => { + //2025-11-25 + // const allLocalDays = []; + // const dateCheck = []; + // const eventNameExist = []; + // // filter recurring events for today and not already existing + const eventsToCreate = recurringEvents?.filter((recurringEvent) => { // we're converting the stored UTC event date to local time to compare the system DOW with the event DOW const localEventDate = adjustToLosAngelesTime(recurringEvent.date); + //Logs for checking + // allLocalDays.push(localEventDate.getUTCDay()); + // dateCheck.push(localEventDate.getUTCDay() === todayUTCDay); + // eventNameExist.push(!doesEventExist(recurringEvent.name, today, events)); return ( localEventDate.getUTCDay() === todayUTCDay && !doesEventExist(recurringEvent.name, today, events) ); }); + // console.log( + // 'Event date\n', + // today, + // '\nToday\n', + // todayUTCDay, + // '\nAll days\n', + // allLocalDays, + // '\nDay vs All Days comparison (Bool)\n', + // dateCheck, + // '\nEvent exist or not (Bool)\n', + // eventNameExist, + // '\nList of events to create\n', + // eventsToCreate, + // ); - for (const event of eventsToCreate) { - // convert to local time for DST correction... - const correctedStartTime = adjustToLosAngelesTime(event.startTime); - const timeCorrectedEvent = { - ...event, - // ... then back to UTC for DB - date: correctedStartTime.toISOString(), - startTime: correctedStartTime.toISOString(), - }; - // map/generate all event data with adjusted date, startTime - const eventToCreate = generateEventData(timeCorrectedEvent); - - const createdEvent = await createEvent(eventToCreate, URL, headerToSend, fetch); - if (createdEvent) console.log('Created event:', createdEvent); + //Check if event exists + if (!eventsToCreate || eventsToCreate?.length === 0) { + return 'No events for today.'; + } else { + const batchEvents = []; + for (const event of eventsToCreate) { + // convert to local time for DST correction... + const correctedStartTime = adjustToLosAngelesTime(event.startTime); + const timeCorrectedEvent = { + ...event, + // ... then back to UTC for DB + date: correctedStartTime.toISOString(), + startTime: correctedStartTime.toISOString(), + }; + // map/generate all event data with adjusted date, startTime + const eventToCreate = generateEventData(timeCorrectedEvent); + batchEvents.push(eventToCreate); + } + const createdEvents = await createEvents(batchEvents, URL, headerToSend, fetch); + if (createdEvents) console.log('Created events:', createdEvents); + return "Today's events have been created."; } }; -/** - * Adjusts an event date to Los_Angeles time, accounting for DST offsets. - * @param {Date} eventDate - The event date to adjust. - * @returns {Date} - The adjusted event date. - */ -const adjustToLosAngelesTime = (eventDate) => { - const tempDate = new Date(eventDate); - const losAngelesOffsetHours = new Intl.DateTimeFormat('en-US', { - timeZone: 'America/Los_Angeles', - timeZoneName: 'shortOffset', - }) - .formatToParts(tempDate) - .find((part) => part.type === 'timeZoneName') - .value.slice(3); - const offsetMinutes = parseInt(losAngelesOffsetHours, 10) * 60; - return new Date(tempDate.getTime() + offsetMinutes * 60000); -}; - /** * Executes the task of fetching existing events and recurring events, * filtering those that should occur today, and creating them if needed. @@ -144,8 +174,14 @@ const runTask = async (fetch, URL, headerToSend) => { fetchData('/api/recurringevents/', URL, headerToSend, fetch), ]); - await filterAndCreateEvents(events, recurringEvents, URL, headerToSend, fetch); - console.log("Today's events have been created."); + const checkAndCreateEvents = await filterAndCreateEvents( + events, + recurringEvents, + URL, + headerToSend, + fetch, + ); + console.log(checkAndCreateEvents); }; /** @@ -184,7 +220,7 @@ module.exports = { adjustToLosAngelesTime, isSameUTCDate, doesEventExist, - createEvent, + createEvents, filterAndCreateEvents, runTask, scheduleTask, diff --git a/backend/workers/createRecurringEvents.test.js b/backend/workers/createRecurringEvents.test.js index 479a8d969..2f905ef48 100644 --- a/backend/workers/createRecurringEvents.test.js +++ b/backend/workers/createRecurringEvents.test.js @@ -3,7 +3,7 @@ const { adjustToLosAngelesTime, isSameUTCDate, doesEventExist, - createEvent, + createEvents, filterAndCreateEvents, runTask, scheduleTask, @@ -189,7 +189,7 @@ describe('createRecurringEvents Module Tests', () => { expect(fetch).toHaveBeenCalledWith( `${mockURL}/api/events/`, expect.objectContaining({ - body: JSON.stringify(expectedEvent), + body: JSON.stringify([expectedEvent]), }), ); @@ -222,7 +222,7 @@ describe('createRecurringEvents Module Tests', () => { expect(fetch).toHaveBeenCalledWith( `${mockURL}/api/events/`, expect.objectContaining({ - body: JSON.stringify(expectedEvent), + body: JSON.stringify([expectedEvent]), }), ); @@ -256,7 +256,7 @@ describe('createRecurringEvents Module Tests', () => { expect(fetch).toHaveBeenCalledWith( `${mockURL}/api/events/`, expect.objectContaining({ - body: JSON.stringify(expectedEvent), + body: JSON.stringify([expectedEvent]), }), ); @@ -289,7 +289,7 @@ describe('createRecurringEvents Module Tests', () => { expect(fetch).toHaveBeenCalledWith( `${mockURL}/api/events/`, expect.objectContaining({ - body: JSON.stringify(expectedEvent), + body: JSON.stringify([expectedEvent]), }), ); @@ -330,15 +330,16 @@ describe('createRecurringEvents Module Tests', () => { }); }); - describe('createEvent', () => { + describe('createEvents', () => { it('should create a new event via POST request', async () => { const mockEvent = { name: 'Event 1', date: '2023-11-02T19:00:00Z' }; + const mockEventArray = [mockEvent]; fetch.mockResolvedValueOnce({ ok: true, json: jest.fn().mockResolvedValue({ id: 1, ...mockEvent }), }); - const result = await createEvent(mockEvent, mockURL, mockHeader, fetch); + const result = await createEvents(mockEventArray, mockURL, mockHeader, fetch); expect(fetch).toHaveBeenCalledWith(`${mockURL}/api/events/`, { method: 'POST', @@ -346,7 +347,7 @@ describe('createRecurringEvents Module Tests', () => { 'Content-Type': 'application/json', 'x-customrequired-header': mockHeader, }, - body: JSON.stringify(mockEvent), + body: JSON.stringify(mockEventArray), }); expect(result).toEqual({ id: 1, ...mockEvent }); }); @@ -354,7 +355,7 @@ describe('createRecurringEvents Module Tests', () => { it('should return null if event creation fails', async () => { fetch.mockRejectedValueOnce(new Error('Network error')); - const result = await createEvent(null, mockURL, mockHeader, fetch); + const result = await createEvents(null, mockURL, mockHeader, fetch); expect(result).toBeNull(); }); diff --git a/backend/workers/openCheckins.js b/backend/workers/openCheckins.js index 18d917d92..6bbb74111 100644 --- a/backend/workers/openCheckins.js +++ b/backend/workers/openCheckins.js @@ -1,81 +1,101 @@ module.exports = (cron, fetch) => { + // Check to see if any events are about to start, + // and if so, open their respective check-ins - // Check to see if any events are about to start, - // and if so, open their respective check-ins + const url = + process.env.NODE_ENV === 'prod' + ? 'https://www.vrms.io' + : `http://localhost:${process.env.BACKEND_PORT}`; + const headerToSend = process.env.CUSTOM_REQUEST_HEADER; - const url = process.env.NODE_ENV === 'prod' ? 'https://www.vrms.io' : `http://localhost:${process.env.BACKEND_PORT}`; - const headerToSend = process.env.CUSTOM_REQUEST_HEADER; + async function fetchEvents() { + try { + const res = await fetch(`${url}/api/events`, { + headers: { + 'x-customrequired-header': headerToSend, + }, + }); + const resJson = await res.json(); - async function fetchEvents() { - try { - const res = await fetch(`${url}/api/events`, { - headers: { - "x-customrequired-header": headerToSend - } - }); - const resJson = await res.json(); + return resJson; + } catch (error) { + console.log(error); + } + } - return resJson; - } catch(error) { - console.log(error); - }; - }; + async function updateEvents(eventsToUpdate) { + try { + const res = await fetch(`${url}/api/events/batchUpdate`, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + 'x-customrequired-header': headerToSend, + }, + body: JSON.stringify(eventsToUpdate), + }); + if (!res.ok) throw new Error('Failed to update event'); + return await res.json(); + } catch (error) { + console.error('Error updating event:', error); + return null; + } + } - async function sortAndFilterEvents(currentTime, thirtyMinutes) { - const events = await fetchEvents(); + async function sortAndFilterEvents() { + const events = await fetchEvents(); - // Filter events if event date is after now but before thirty minutes from now - if (events && events.length > 0) { - const sortedEvents = events.filter(event => { - return (event.date >= currentTime) && (event.date <= thirtyMinutes) && (event.checkInReady === false); - }) - // console.log('Sorted events: ', sortedEvents); - return sortedEvents; - }; - }; + // Get current time and set to date variable + const now = Date.now(); - async function openCheckins(events) { - if(events && events.length > 0) { - events.forEach(event => { - // console.log('Opening event: ', event); + // Calculate thirty minutes from now + const thirtyMinutesFromNow = now + 1800000; - fetch(`${url}/api/events/${event._id}`, { - method: "PATCH", - headers: { - "Content-Type": "application/json", - "x-customrequired-header": headerToSend - }, - body: JSON.stringify({ checkInReady: true }) - }) - .then(res => { - const response = res; - }) - .catch(err => { - console.log(err); - }); - }); - }; - }; + // Filter events if event date is after now but before thirty minutes from now + if (events && events.length > 0) { + const sortedEvents = events.filter((event) => { + if (!event.date) { + // handle if event date is null/undefined + // false meaning don't include in sortedEvents + return false; + } + const startMs = new Date(event.date).getTime(); + if (Number.isNaN(startMs)) return false; + return startMs >= now && startMs <= thirtyMinutesFromNow && event.checkInReady === false; + }); + // console.log('Sorted events: ', sortedEvents); + return sortedEvents; + } + } - async function runTask() { - console.log("Opening check-ins"); + async function openCheckins(events) { + if (events && events.length > 0) { + console.log('Opening check-ins'); + // console.log('Opening event: ', event); + const batchEventsToUpdate = events.map((e) => ({ + _id: e._id, + checkInReady: true, + })); + const updatedEvents = await updateEvents(batchEventsToUpdate); + if (updatedEvents) console.log('Updated events:', updatedEvents); + console.log('Check-ins opened'); + } else { + console.log('No scheduled events to open'); + } + } - // Get current time and set to date variable - const currentTimeISO = new Date().toISOString(); - - // Calculate thirty minutes from now - const thirtyMinutesFromNow = new Date().getTime() + 1800000; - const thirtyMinutesISO = new Date(thirtyMinutesFromNow).toISOString(); - - const eventsToOpen = await sortAndFilterEvents(currentTimeISO, thirtyMinutesISO); - await openCheckins(eventsToOpen); - - console.log("Check-ins opened"); - }; + async function runTask() { + const eventsToOpen = await sortAndFilterEvents().catch((err) => { + console.log(err); + }); - const scheduledTask = cron.schedule('*/30 * * * *', () => { - runTask(); + await openCheckins(eventsToOpen).catch((err) => { + console.log(err); }); + } + + const scheduledTask = cron.schedule('*/30 * * * *', () => { + runTask(); + }); - return scheduledTask; -}; \ No newline at end of file + return scheduledTask; +};