From 9bcbd10e36c3c0c6438232c717a7d57ce46cefb2 Mon Sep 17 00:00:00 2001 From: Chimdi Ejiogu Date: Sat, 11 Oct 2025 21:30:38 -0400 Subject: [PATCH 01/31] Update web scraping for printers --- .DS_Store | Bin 6148 -> 0 bytes src/data/scrapers/printers.py | 234 ++++++++++++++++++++++++++++++---- 2 files changed, 211 insertions(+), 23 deletions(-) delete mode 100644 .DS_Store diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 5b928bb719a93f6dc27edc3b82270d14a3109785..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHK!AiqG5S?wSO({wb3LY1{R*X?v#7n6914i_qQky1dFwK^vwTDv3S%1hc@q3)v z-Ac9U!Gnt3f!Q}ZJF{V4!fpltSZf&U0Mr1$LM1FzaQHxIopeqL+EYSga*q&F*n%zy z=*>kN$6sWC-rXt$_{O?$2ea?5kG>2-f0Xt8Soj$Ci8cYa8RSv)`!mJ=>Yo`FP*4(cW!LCKYRabNk?=eHZr<@t}xh_zg-~(Kv-iG`_6y z-b<5MBsUmGdla$m6uQ4%dDGdg^PT2Q<&C{>RIaOeSL2i$gN6aaz+y6>&r7SinB|+Q z4FiUOFBqWnfuj<-8gqs6=)gg?0EqM(DFtonB`Ak$bT#G*aRr5`R791^bc?}MI{LYe zb2a7)RXQ-;d@%iFraKfSpN{);84k=]+>") +BRACKET_CONTENT_RE = re.compile(r"[\(\[\{].*?[\)\]\}]") +MULTI_SPACE_RE = re.compile(r"\s+") +DELIMS_RE = re.compile(r"\s*[-–—:/|]\s*") +COORD_SPLIT_RE = re.compile(r"\s*,\s*") +ALL_CAPS_PHRASE_RE = re.compile(r"\b[A-Z]{2,}(?:\s+[A-Z]{2,})*\b") +TRAILING_CAPS_RE = re.compile(r"\b[A-Z]{2,}(?:\s+[A-Z]{2,})*\s*$") +LABEL_PHRASES_RE = re.compile( + r""" + \bresidents?\s*only\b | + \bstudents?\s*only\b | + \baa\s*&\s*p\b | + \baap\b + """, re.IGNORECASE | re.VERBOSE +) +RESIDUAL_TRAILING_LABEL_RE = re.compile( + r"\b(?:resident|residents|student|students|staff|public)\b\s*$", + re.IGNORECASE +) + +def _norm(s): + """ + Unicode/HTML/whitespace normalization. + """ + if s is None: + return "" + s = unicodedata.normalize('NFKC', s) # Normalizes unicode text + s = HTML_TAG_RE.sub(" ", s) + s = s.replace("*", " ") + s = BRACKET_CONTENT_RE.sub(" ", s) + s = MULTI_SPACE_RE.sub(" ", s).strip() + return s + +def _strip_trailing_allcaps(s): + """ + Remove trailing ALL-CAPS qualifiers (e.g., RESIDENTS ONLY). + """ + return TRAILING_CAPS_RE.sub("", s).strip() + +# def _title_clean(s: str) -> str: +# """ +# Nice display casing: keep acronyms as-is, titlecase other words. +# """ +# words = s.split() +# fixed = [w if w.isupper() else w.title() for w in words] +# return " ".join(fixed) + +def _pre_clean_for_match(s: str) -> str: + s = _norm(s) + s = LABEL_PHRASES_RE.sub(" ", s) # <— removes "Resident(s) only", "AA&P", etc. + s = _strip_trailing_allcaps(s) + s = RESIDUAL_TRAILING_LABEL_RE.sub(" ", s) # <— removes "Resident", "Students", etc. + + s = re.sub(r"[^\w\s\-’']", " ", s) # punctuation noise + s = re.sub(r"\s+", " ", s).strip() + return s + +def _token_sort(s): + tokens = s.lower().split() + tokens.sort() + return " ".join(tokens) + +def map_building(name, threshold=87): + if not name: + return None, 0 + + query = _token_sort(_pre_clean_for_match(name)) + canon_token_list = [_token_sort(_pre_clean_for_match(c)) for c in CANONICAL_BUILDINGS] + + best = get_close_matches(query, canon_token_list, n=1) # Returns a list of the (top-1) closest match to the cleaned name - # Locate the table - table = soup.find("table", {"id": "directoryTable"}) - rows = table.find("tbody").find_all("tr") + # If no matches (empty list), return the original name and 0 + if not best: + return name, 0 - # Extract data + # Return the closest match and its similarity score + match = best[0] + + # Calculate the similarity score of the match to the original name (for internal use, potential debugging purposes) + index = canon_token_list.index(match) + canon_raw = CANONICAL_BUILDINGS[index] + score = int(SequenceMatcher(None, query, match).ratio() * 100) + + # If the score is below the threshold, return the original name instead of the canonical name + return (canon_raw, score) if score >= threshold else (name, score) + +def map_labels(description): + """ + Extract label tokens from the description. + """ + if not description: + return [], description + + labels = LABEL_PHRASES_RE.findall(description) + labels = [label.title().replace("Aa&P", "AA&P").replace("Aap", "AA&P") for label in labels] + description = LABEL_PHRASES_RE.sub("", description).strip() + description = RESIDUAL_TRAILING_LABEL_RE.sub("", description).strip() + description = MULTI_SPACE_RE.sub(" ", description) + + return labels, description + +def fetch_printers_json(): + """ + Fetch printer data in JSON format from the CU Print directory endpoint. + """ + resp = requests.get(URL, headers=HEADERS, timeout=20) + resp.raise_for_status() + return resp.json() + +def scrape_printers(): + """ + Scrape CU Print printer locations from the Cornell directory page. + """ + payload = fetch_printers_json() data = [] - for row in rows: - cols = row.find_all("td") - if len(cols) < 3: # Ensure row has enough columns - continue - - location_name = cols[0].text.strip() - description = cols[1].text.strip() - - # Extract coordinates from the hyperlink tag inside - coordinates_link = cols[2].find("a") - coordinates_string = coordinates_link.text.strip() if coordinates_link else "" - coordinates = [float(x) for x in coordinates_string.split(', ')] + # payload['rows'] is a list of lists, where each inner list represents a row of data + for row in payload['rows']: + if len(row) < 3: # Ensure row has enough columns + continue # Skipping row with insufficient columns + + # Each row is of the structure ["Building", "Equipment & Location", "Coordinates (Lat, Lng)"] + [raw_building, raw_location, raw_coordinates] = row + + # Map raw building name to canonical building name + building, score = map_building(raw_building) + + # Map labels from description to canonical labels + # TODO: Handle description (parse for room number, etc.) + description = raw_location + + # Splits coordinates string into a list of floats + coordinates = [float(x) for x in raw_coordinates.split(', ')] data.append({ - "Location": location_name, + "Location": building, "Description": description, "Coordinates": coordinates }) - return data \ No newline at end of file + + return data \ No newline at end of file From 772a893f085b70a7c86991fd0d7e0813461fa616 Mon Sep 17 00:00:00 2001 From: Chimdi Ejiogu Date: Sat, 11 Oct 2025 23:21:54 -0400 Subject: [PATCH 02/31] Implement baseplate labeling for scraped data --- src/data/scrapers/printers.py | 63 +++++++++++++++++++++++++---------- 1 file changed, 45 insertions(+), 18 deletions(-) diff --git a/src/data/scrapers/printers.py b/src/data/scrapers/printers.py index df0161ba..acddffe2 100644 --- a/src/data/scrapers/printers.py +++ b/src/data/scrapers/printers.py @@ -89,6 +89,8 @@ COORD_SPLIT_RE = re.compile(r"\s*,\s*") ALL_CAPS_PHRASE_RE = re.compile(r"\b[A-Z]{2,}(?:\s+[A-Z]{2,})*\b") TRAILING_CAPS_RE = re.compile(r"\b[A-Z]{2,}(?:\s+[A-Z]{2,})*\s*$") + +# Used for stripping common label phrases from building names LABEL_PHRASES_RE = re.compile( r""" \bresidents?\s*only\b | @@ -97,6 +99,25 @@ \baap\b """, re.IGNORECASE | re.VERBOSE ) + +# Used to identify common variants of labels +LABEL_PATTERNS = { + # Residents Only (singular/plural + optional hyphen + any case) + "Residents Only": re.compile(r"\bresident[s]?[-\s]*only\b", re.IGNORECASE), + + # AA&P Students Only (accept AA&P or AAP; allow any junk in-between; optional hyphen) + "AA&P Students Only": re.compile( + r"\b(?:aa\s*&\s*p|aap)\b.*\bstudent[s]?[-\s]*only\b", + re.IGNORECASE + ), + + # Landscape Architecture Students Only (allow arbitrary whitespace; optional hyphen) + "Landscape Architecture Students Only": re.compile( + r"\blandscape\s+architecture\b.*\bstudent[s]?[-\s]*only\b", + re.IGNORECASE + ), +} + RESIDUAL_TRAILING_LABEL_RE = re.compile( r"\b(?:resident|residents|student|students|staff|public)\b\s*$", re.IGNORECASE @@ -121,14 +142,6 @@ def _strip_trailing_allcaps(s): """ return TRAILING_CAPS_RE.sub("", s).strip() -# def _title_clean(s: str) -> str: -# """ -# Nice display casing: keep acronyms as-is, titlecase other words. -# """ -# words = s.split() -# fixed = [w if w.isupper() else w.title() for w in words] -# return " ".join(fixed) - def _pre_clean_for_match(s: str) -> str: s = _norm(s) s = LABEL_PHRASES_RE.sub(" ", s) # <— removes "Resident(s) only", "AA&P", etc. @@ -168,20 +181,25 @@ def map_building(name, threshold=87): # If the score is below the threshold, return the original name instead of the canonical name return (canon_raw, score) if score >= threshold else (name, score) -def map_labels(description): +def map_labels(text): """ Extract label tokens from the description. """ - if not description: - return [], description + if not text: + return [] + + cleaned = _norm(text) + found_labels = [] - labels = LABEL_PHRASES_RE.findall(description) - labels = [label.title().replace("Aa&P", "AA&P").replace("Aap", "AA&P") for label in labels] - description = LABEL_PHRASES_RE.sub("", description).strip() - description = RESIDUAL_TRAILING_LABEL_RE.sub("", description).strip() - description = MULTI_SPACE_RE.sub(" ", description) + for canon, pattern in LABEL_PATTERNS.items(): + # Search for the pattern in the cleaned text + if pattern.search(cleaned): + found_labels.append(canon) - return labels, description + # Remove the found label from the text to avoid duplicates + cleaned = pattern.sub("", cleaned).strip() + + return sorted(set(found_labels)) def fetch_printers_json(): """ @@ -210,6 +228,14 @@ def scrape_printers(): building, score = map_building(raw_building) # Map labels from description to canonical labels + labels = [] + + labels.extend(map_labels(raw_building)) # Get labels from the building name (e.g., "Residents Only") + labels.extend(map_labels(raw_location)) # Get labels from the location description (e.g., "Landscape Architecture Student ONLY") + + # Deduplicate and sort labels + labels = sorted(set(labels)) + # TODO: Handle description (parse for room number, etc.) description = raw_location @@ -219,7 +245,8 @@ def scrape_printers(): data.append({ "Location": building, "Description": description, - "Coordinates": coordinates + "Coordinates": coordinates, + "Labels": labels }) return data \ No newline at end of file From 4b008e458302e3cb74a646ed71c3b53617b25730 Mon Sep 17 00:00:00 2001 From: Chimdi Ejiogu Date: Sat, 11 Oct 2025 23:28:45 -0400 Subject: [PATCH 03/31] Add labels for printer colors --- src/data/scrapers/printers.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/data/scrapers/printers.py b/src/data/scrapers/printers.py index acddffe2..5a568aa1 100644 --- a/src/data/scrapers/printers.py +++ b/src/data/scrapers/printers.py @@ -74,12 +74,7 @@ "White Hall", "Willard Student Center" ] - -CANONICAL_LABELS = [ - "Residents Only", - "AA&P Students Only", - "Landscape Architecture Students Only" -] +# Add more buildings as needed... # Regex helpers HTML_TAG_RE = re.compile(r"<[^>]+>") @@ -102,6 +97,7 @@ # Used to identify common variants of labels LABEL_PATTERNS = { + # --- Access restrictions --- # Residents Only (singular/plural + optional hyphen + any case) "Residents Only": re.compile(r"\bresident[s]?[-\s]*only\b", re.IGNORECASE), @@ -116,6 +112,15 @@ r"\blandscape\s+architecture\b.*\bstudent[s]?[-\s]*only\b", re.IGNORECASE ), + + # --- Printer capabilities --- + "Color": re.compile(r"\bcolor\b", re.IGNORECASE), + "Black & White": re.compile( + r"\b(?:black\s*(?:and|&)\s*white|b\s*&\s*w)\b", re.IGNORECASE + ), + "Color, Scan, & Copy": re.compile( + r"\bcolor[,/ &]*(scan|copy|print|copying)+\b", re.IGNORECASE + ), } RESIDUAL_TRAILING_LABEL_RE = re.compile( @@ -198,7 +203,8 @@ def map_labels(text): # Remove the found label from the text to avoid duplicates cleaned = pattern.sub("", cleaned).strip() - + + return sorted(set(found_labels)) def fetch_printers_json(): @@ -234,7 +240,7 @@ def scrape_printers(): labels.extend(map_labels(raw_location)) # Get labels from the location description (e.g., "Landscape Architecture Student ONLY") # Deduplicate and sort labels - labels = sorted(set(labels)) + labels = sorted(set(labels)) # TODO: Handle description (parse for room number, etc.) description = raw_location From 7f87078d85266b2a62dbbb8ea329558002b6dd25 Mon Sep 17 00:00:00 2001 From: Chimdi Ejiogu Date: Sat, 11 Oct 2025 23:38:29 -0400 Subject: [PATCH 04/31] Update description to exclude labels --- src/data/scrapers/printers.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/data/scrapers/printers.py b/src/data/scrapers/printers.py index 5a568aa1..8c76c214 100644 --- a/src/data/scrapers/printers.py +++ b/src/data/scrapers/printers.py @@ -191,7 +191,7 @@ def map_labels(text): Extract label tokens from the description. """ if not text: - return [] + return text, [] cleaned = _norm(text) found_labels = [] @@ -204,8 +204,8 @@ def map_labels(text): # Remove the found label from the text to avoid duplicates cleaned = pattern.sub("", cleaned).strip() - - return sorted(set(found_labels)) + cleaned = re.sub(r"\s+", " ", cleaned).strip() + return cleaned, sorted(set(found_labels)) def fetch_printers_json(): """ @@ -236,14 +236,16 @@ def scrape_printers(): # Map labels from description to canonical labels labels = [] - labels.extend(map_labels(raw_building)) # Get labels from the building name (e.g., "Residents Only") - labels.extend(map_labels(raw_location)) # Get labels from the location description (e.g., "Landscape Architecture Student ONLY") + _, building_labels = map_labels(raw_building) # Get labels from the building name (e.g., "Residents Only") + remainder, location_labels = map_labels(raw_location) # Get labels from the location description (e.g., "Landscape Architecture Student ONLY") # Deduplicate and sort labels + labels += building_labels + labels += location_labels labels = sorted(set(labels)) - - # TODO: Handle description (parse for room number, etc.) - description = raw_location + + cleaned = re.sub(r"^[\s\-–—:/|]+", "", remainder).strip() # Remove leftover delimiters at the start (like " - ", " / ", ": ", etc.) + description = cleaned # Final cleaned description text (with labels removed) — essentially, remainder of the location description # Splits coordinates string into a list of floats coordinates = [float(x) for x in raw_coordinates.split(', ')] From ff5d61be6d0d815a736547582ac545cd72213b93 Mon Sep 17 00:00:00 2001 From: Chimdi Ejiogu Date: Sat, 11 Oct 2025 23:42:50 -0400 Subject: [PATCH 05/31] Add comments/documentation and clean up code --- src/data/scrapers/printers.py | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/data/scrapers/printers.py b/src/data/scrapers/printers.py index 8c76c214..a71e1f71 100644 --- a/src/data/scrapers/printers.py +++ b/src/data/scrapers/printers.py @@ -10,6 +10,7 @@ URL = 'https://www.cornell.edu/about/maps/directory/text-data.cfm?layer=CUPrint&caption=%20CU%20Print%20Printers' +# HTTP headers to mimic a real browser request HEADERS = { "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36", "Referer": 'https://www.cornell.edu/about/maps/directory/', @@ -17,7 +18,8 @@ "Accept": 'application/json, text/javascript, */*', } -# Canonical list of Cornell buildings; NOTE: This list is not exhaustive. +# Canonical list of Cornell buildings +# NOTE: This list is not exhaustive. Add more buildings as needed... CANONICAL_BUILDINGS = [ "Akwe:kon", "Alice Cook House", @@ -74,15 +76,11 @@ "White Hall", "Willard Student Center" ] -# Add more buildings as needed... # Regex helpers HTML_TAG_RE = re.compile(r"<[^>]+>") BRACKET_CONTENT_RE = re.compile(r"[\(\[\{].*?[\)\]\}]") MULTI_SPACE_RE = re.compile(r"\s+") -DELIMS_RE = re.compile(r"\s*[-–—:/|]\s*") -COORD_SPLIT_RE = re.compile(r"\s*,\s*") -ALL_CAPS_PHRASE_RE = re.compile(r"\b[A-Z]{2,}(?:\s+[A-Z]{2,})*\b") TRAILING_CAPS_RE = re.compile(r"\b[A-Z]{2,}(?:\s+[A-Z]{2,})*\s*$") # Used for stripping common label phrases from building names @@ -123,6 +121,7 @@ ), } +# Used for stripping residual trailing labels from descriptions RESIDUAL_TRAILING_LABEL_RE = re.compile( r"\b(?:resident|residents|student|students|staff|public)\b\s*$", re.IGNORECASE @@ -148,6 +147,9 @@ def _strip_trailing_allcaps(s): return TRAILING_CAPS_RE.sub("", s).strip() def _pre_clean_for_match(s: str) -> str: + """ + Pre-clean a building name for matching against the canonical list. + """ s = _norm(s) s = LABEL_PHRASES_RE.sub(" ", s) # <— removes "Resident(s) only", "AA&P", etc. s = _strip_trailing_allcaps(s) @@ -158,18 +160,25 @@ def _pre_clean_for_match(s: str) -> str: return s def _token_sort(s): + """ + Tokenize a string, sort the tokens, and re-join them. + """ tokens = s.lower().split() tokens.sort() return " ".join(tokens) def map_building(name, threshold=87): + """ + Map a building name to a canonical building name using fuzzy matching. + """ if not name: return None, 0 query = _token_sort(_pre_clean_for_match(name)) canon_token_list = [_token_sort(_pre_clean_for_match(c)) for c in CANONICAL_BUILDINGS] - best = get_close_matches(query, canon_token_list, n=1) # Returns a list of the (top-1) closest match to the cleaned name + # Returns a list of the (top-1) closest match to the cleaned name + best = get_close_matches(query, canon_token_list, n=1) # If no matches (empty list), return the original name and 0 if not best: @@ -231,7 +240,7 @@ def scrape_printers(): [raw_building, raw_location, raw_coordinates] = row # Map raw building name to canonical building name - building, score = map_building(raw_building) + building, _ = map_building(raw_building) # Map labels from description to canonical labels labels = [] From 906c6331f54a5726b01a037b5a7d7b150a8b1e3a Mon Sep 17 00:00:00 2001 From: Chimdi Ejiogu Date: Sun, 12 Oct 2025 00:30:16 -0400 Subject: [PATCH 06/31] Include labels in database creation and population --- src/data/db/database.py | 40 ++++++++++++++++++++++++++++++++- src/data/db/models.py | 24 ++++++++++++++++++++ src/data/scripts/populate_db.py | 2 +- 3 files changed, 64 insertions(+), 2 deletions(-) diff --git a/src/data/db/database.py b/src/data/db/database.py index efdf541e..7aba2109 100644 --- a/src/data/db/database.py +++ b/src/data/db/database.py @@ -32,7 +32,7 @@ def insert_library(location, address, latitude, longitude): conn.close() -def insert_printer(location, description, latitude, longitude): +def insert_printer(location, description, labels, latitude, longitude): """Insert a printer into the database.""" conn = get_db_connection() cursor = conn.cursor() @@ -44,6 +44,44 @@ def insert_printer(location, description, latitude, longitude): """, (location, description, latitude, longitude), ) + + # Insert labels into the labels table and get their IDs + label_ids = [] + for label in labels: + cursor.execute( + """ + INSERT OR IGNORE INTO labels (label) + VALUES (?) + """, + (label,), + ) + cursor.execute( + """ + SELECT id FROM labels WHERE label = ? + """, + (label,), + ) + label_id = cursor.fetchone()[0] + label_ids.append(label_id) + + # Create entries in the junction table for printer-label relationships + cursor.execute( + """ + SELECT id FROM printers WHERE location = ? AND description = ? AND latitude = ? AND longitude = ? + """, + (location, description, latitude, longitude), + ) + printer_id = cursor.fetchone()[0] + + # Insert into junction table + for label_id in label_ids: + cursor.execute( + """ + INSERT OR IGNORE INTO printer_labels (printer_id, label_id) + VALUES (?, ?) + """, + (printer_id, label_id), + ) conn.commit() conn.close() diff --git a/src/data/db/models.py b/src/data/db/models.py index 7634fd0e..5499c307 100644 --- a/src/data/db/models.py +++ b/src/data/db/models.py @@ -15,6 +15,7 @@ def create_tables(): conn = sqlite3.connect(DB_PATH) cursor = conn.cursor() + #TODO: Remove UNIQUE constraint from location cursor.execute( """ CREATE TABLE IF NOT EXISTS libraries ( @@ -50,6 +51,29 @@ def create_tables(): ) """ ) + + # Table for storing unique labels + cursor.execute( + """ + CREATE TABLE IF NOT EXISTS labels ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + label TEXT UNIQUE NOT NULL + ) + """ + ) + + # Junction table for many-to-many relationship between printers and labels + cursor.execute( + """ + CREATE TABLE IF NOT EXISTS printer_labels ( + printer_id INTEGER NOT NULL, + label_id INTEGER NOT NULL, + PRIMARY KEY (printer_id, label_id), + FOREIGN KEY (printer_id) REFERENCES printers(id) ON DELETE CASCADE, + FOREIGN KEY (label_id) REFERENCES labels(id) ON DELETE CASCADE + ) + """ + ) conn.commit() conn.close() diff --git a/src/data/scripts/populate_db.py b/src/data/scripts/populate_db.py index fa6a23f4..c84cd1ba 100644 --- a/src/data/scripts/populate_db.py +++ b/src/data/scripts/populate_db.py @@ -18,7 +18,7 @@ def populate_db(): # Insert printers printers = scrape_printers() for printer in printers: - insert_printer(printer['Location'], printer['Description'], printer['Coordinates'][0], printer['Coordinates'][1]) + insert_printer(printer['Location'], printer['Description'], printers['Labels'], printer['Coordinates'][0], printer['Coordinates'][1]) if __name__ == "__main__": populate_db() \ No newline at end of file From de836bce1011263ea3b3082ba7334da688a518ac Mon Sep 17 00:00:00 2001 From: Chimdi Ejiogu Date: Tue, 14 Oct 2025 03:28:20 -0400 Subject: [PATCH 07/31] Update endpoint for fetching printer information and corresponding swagger documentation --- src/swagger.json | 2 +- src/utils/EcosystemUtils.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/swagger.json b/src/swagger.json index ff9b0afe..fc7f7348 100644 --- a/src/swagger.json +++ b/src/swagger.json @@ -66,7 +66,7 @@ ], "responses": { "200": { - "description": "{\"success\": true, \"data\": [{\"id\": 1, \"location\": \"Akwe:kon\", \"description\": \"Color - Room 115\", \"latitude\": 42.4563, \"longitude\": -76.4806}]}", + "description": "{\"success\": true, \"data\": [{\"id\": 1, \"location\": \"Akwe:kon\", \"description\": \"Room 115\", \"latitude\": 42.4563, \"longitude\": -76.4806, \"labels\": [\"Color\"]}]}", "schema": { "$ref": "#/components/schemas/BusStop" } diff --git a/src/utils/EcosystemUtils.js b/src/utils/EcosystemUtils.js index 5aadd2b8..a5e979ab 100644 --- a/src/utils/EcosystemUtils.js +++ b/src/utils/EcosystemUtils.js @@ -45,7 +45,7 @@ function fetchAllPrinters() { }); // Fetch printers - db.all("SELECT * FROM printers", (err, rows) => { + db.all("SELECT p.id, p.location, p.description, p.latitude, p.longitude, COALESCE(GROUP_CONCAT(DISTINCT l.label, ', '), '') AS labels FROM printers p LEFT JOIN printer_labels pl ON p.id = pl.printer_id LEFT JOIN labels l ON pl.label_id = l.id GROUP BY p.id", (err, rows) => { if (err) { console.error(err.message); return reject(err); From f23bcc5726b88691265d533aabe89d6af0950b42 Mon Sep 17 00:00:00 2001 From: Chimdi Ejiogu Date: Fri, 7 Nov 2025 19:30:35 -0500 Subject: [PATCH 08/31] Add script to run migrations on database --- package-lock.json | 15 +++++ package.json | 1 + src/.DS_Store | Bin 10244 -> 8196 bytes src/data/scripts/run-migrations.js | 100 +++++++++++++++++++++++++++++ 4 files changed, 116 insertions(+) create mode 100644 src/data/scripts/run-migrations.js diff --git a/package-lock.json b/package-lock.json index 826744f3..f6b08421 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "better-sqlite3": "^12.4.1", "dotenv": "^16.4.7", "express": "^4.21.2", "firebase-admin": "^13.1.0", @@ -791,6 +792,20 @@ "tweetnacl": "^0.14.3" } }, + "node_modules/better-sqlite3": { + "version": "12.4.1", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.4.1.tgz", + "integrity": "sha512-3yVdyZhklTiNrtg+4WqHpJpFDd+WHTg2oM7UcR80GqL05AOV0xEJzc6qNvFYoEtE+hRp1n9MpN6/+4yhlGkDXQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + }, + "engines": { + "node": "20.x || 22.x || 23.x || 24.x" + } + }, "node_modules/bignumber.js": { "version": "9.1.2", "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", diff --git a/package.json b/package.json index 057904ac..74e05ef6 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "author": "", "license": "ISC", "dependencies": { + "better-sqlite3": "^12.4.1", "dotenv": "^16.4.7", "express": "^4.21.2", "firebase-admin": "^13.1.0", diff --git a/src/.DS_Store b/src/.DS_Store index a39c96c47949260ec6c8cd9be8d9394560f28cf6..807e94451ef138fbec80fa0ae23bfb042a5ce57f 100644 GIT binary patch delta 96 zcmZn(XmOBWU|?W$DortDU;r^WfEYvza8E20o2aMA$gweCH$NlCWFCRdo6ie+unRH+ kWr09~8%Vf<6mBg1&ODi4C6I#=qMc!KJkPYvd}3_O03^Z?xc~qF delta 201 zcmZp1XbF&DU|?W$DortDU{C-uIe-{M3-C-V6q~50$jG%ZU^hP_*JK`n&9Zz9DGd1x z$qd;HsSHI7xM|c{%xc=|H;} zH}4VfX5ZNGflZJZ$OQrgZXn?ba>d5N@640=WdcQ*Aa2tDNi#Aq7=Y->1v0&xKMFH5 F0|3-|C*lAA diff --git a/src/data/scripts/run-migrations.js b/src/data/scripts/run-migrations.js new file mode 100644 index 00000000..25e2b1d0 --- /dev/null +++ b/src/data/scripts/run-migrations.js @@ -0,0 +1,100 @@ +// Imports necessary for data migrations +const fs = require('fs'); // Node's built-in file system module, which lets us read from disk +const path = require('path'); // Safer way to express file paths/path joining +const crypto = require('crypto'); +const Database = require('better-sqlite3'); + +const DB_PATH = path.join(__dirname, "../transit.db"); // Finds db file from current file's directory +const MIGRATIONS_DIR = path.join(__dirname, "../migrations"); + +/** + * Hashes a string using SHA-256 + * + * We use this to store the checksum of the migration file in the database. + * This allows us to track which migrations have been applied, as well as if a migration file has been modified since it was last applied. + * + * @param {string} s - The string to hash + * @returns {string} - The SHA-256 hash of the string + */ +function sha256(s) { + return crypto.createHash('sha256').update(s, 'utf8').digest('hex'); +} + +/** + * Runs the migrations + * + * This function reads all the migration files in the migrations directory, hashes them, and stores the checksum in the database. + * It then executes the migrations in the order of the files. + * + * @returns {void} + * @throws {Error} - If the migrations fail + */ +function runMigration() { + // Open the database using the better-sqlite3 library + const db = new Database(DB_PATH); + + // Set defaults for migrations + db.pragma('journal_mode = WAL'); + db.pragma('synchronous = NORMAL'); + db.pragma('foreign_keys = ON'); + + // Create the schema_migrations table if it doesn't exist for tracking migrations applied to the database + db.exec(` + CREATE TABLE IF NOT EXISTS schema_migrations ( + id INTEGER PRIMARY KEY, + filename TEXT NOT NULL UNIQUE, + checksum TEXT NOT NULL, + applied_at TEXT NOT NULL DEFAULT (datetime('now')) + ) + `); + + // Get the list of migrations that have already been applied to the database + const applied = new Set( + db.prepare('SELECT filename FROM schema_migrations').all().map(record => record.filename) + ); + + // Get the list of migration files in the migrations directory (keeping only .sql files and sorting them chronologically) + const files = fs.readdirSync(MIGRATIONS_DIR).filter(f => f.endsWith('.sql')).sort(); + + // Prepare the statement to insert a new migration into the schema_migrations table + const insertMig = db.prepare(` + INSERT INTO schema_migrations (filename, checksum) VALUES (?,?) + `); + + // Define a transaction to execute the migrations + const transaction = db.transaction(() => { + for (const file of files) { + // Skip if the migration has already been applied + if (applied.has(file)) { + continue; + } + + const full = path.join(MIGRATIONS_DIR, file); + const sql = fs.readFileSync(full, 'utf8').trim(); + if (!sql) { + continue; + } + + // Defensive: re-enable FKs inside each run (is already done in the migrations, but just in case) + db.exec('PRAGMA foreign_keys = ON;'); + + // Execute SQL commands in the migration file + db.exec(sql); + + // Records migration as applied to the database via its check + insertMig.run(file, sha256(sql)); + console.log(`Applied ${file}`); + } + }); + + try { + transaction(); + console.log('All migrations applied'); + } catch (e) { + console.error("Migration failed", e); + } finally { + db.close(); + } +} + +runMigration(); \ No newline at end of file From f8d95d5b4aac621931641cf5e4842da8bc22583a Mon Sep 17 00:00:00 2001 From: Chimdi Ejiogu Date: Fri, 7 Nov 2025 19:31:07 -0500 Subject: [PATCH 09/31] Add migration files to create labels and printer_label tables --- src/data/migrations/2025117_1854_create_labels.sql | 6 ++++++ .../migrations/2025117_1859_create_printer_labels.sql | 9 +++++++++ 2 files changed, 15 insertions(+) create mode 100644 src/data/migrations/2025117_1854_create_labels.sql create mode 100644 src/data/migrations/2025117_1859_create_printer_labels.sql diff --git a/src/data/migrations/2025117_1854_create_labels.sql b/src/data/migrations/2025117_1854_create_labels.sql new file mode 100644 index 00000000..3884e988 --- /dev/null +++ b/src/data/migrations/2025117_1854_create_labels.sql @@ -0,0 +1,6 @@ +PRAGMA foreign_keys = ON; + +CREATE TABLE IF NOT EXISTS labels ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + label TEXT UNIQUE NOT NULL +); \ No newline at end of file diff --git a/src/data/migrations/2025117_1859_create_printer_labels.sql b/src/data/migrations/2025117_1859_create_printer_labels.sql new file mode 100644 index 00000000..73fd9c06 --- /dev/null +++ b/src/data/migrations/2025117_1859_create_printer_labels.sql @@ -0,0 +1,9 @@ +PRAGMA foreign_keys = ON; + +CREATE TABLE IF NOT EXISTS printer_labels ( + printer_id INTEGER NOT NULL, + label_id INTEGER NOT NULL, + PRIMARY KEY (printer_id, label_id), + FOREIGN KEY (printer_id) REFERENCES printers(id) ON DELETE CASCADE, + FOREIGN KEY (label_id) REFERENCES labels(id) ON DELETE CASCADE +); \ No newline at end of file From 9fe37ff164f96a6c702f97c139d0445eb9b59656 Mon Sep 17 00:00:00 2001 From: Chimdi Ejiogu Date: Fri, 7 Nov 2025 19:31:52 -0500 Subject: [PATCH 10/31] Remove labels and printer_labels table from database initialization for migration --- src/data/db/models.py | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/src/data/db/models.py b/src/data/db/models.py index 5499c307..17db360b 100644 --- a/src/data/db/models.py +++ b/src/data/db/models.py @@ -51,29 +51,6 @@ def create_tables(): ) """ ) - - # Table for storing unique labels - cursor.execute( - """ - CREATE TABLE IF NOT EXISTS labels ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - label TEXT UNIQUE NOT NULL - ) - """ - ) - - # Junction table for many-to-many relationship between printers and labels - cursor.execute( - """ - CREATE TABLE IF NOT EXISTS printer_labels ( - printer_id INTEGER NOT NULL, - label_id INTEGER NOT NULL, - PRIMARY KEY (printer_id, label_id), - FOREIGN KEY (printer_id) REFERENCES printers(id) ON DELETE CASCADE, - FOREIGN KEY (label_id) REFERENCES labels(id) ON DELETE CASCADE - ) - """ - ) conn.commit() conn.close() From 919125c48d5c4d3d4b6fdedcd960aa0752f4403e Mon Sep 17 00:00:00 2001 From: Chimdi Ejiogu Date: Fri, 7 Nov 2025 20:15:22 -0500 Subject: [PATCH 11/31] Minor bug fix --- src/data/scripts/populate_db.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/data/scripts/populate_db.py b/src/data/scripts/populate_db.py index c84cd1ba..30ddc62f 100644 --- a/src/data/scripts/populate_db.py +++ b/src/data/scripts/populate_db.py @@ -18,7 +18,7 @@ def populate_db(): # Insert printers printers = scrape_printers() for printer in printers: - insert_printer(printer['Location'], printer['Description'], printers['Labels'], printer['Coordinates'][0], printer['Coordinates'][1]) + insert_printer(printer['Location'], printer['Description'], printer['Labels'], printer['Coordinates'][0], printer['Coordinates'][1]) if __name__ == "__main__": populate_db() \ No newline at end of file From aa664b75f5a1ac4cc1560456d096fcd9c1ab7b16 Mon Sep 17 00:00:00 2001 From: Chimdi Ejiogu Date: Fri, 7 Nov 2025 20:29:09 -0500 Subject: [PATCH 12/31] Export script to run migrations (and populate db) --- package.json | 4 +++- src/data/scripts/run-migrations.js | 8 +++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 74e05ef6..b4d1f5c2 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,9 @@ "type": "module", "scripts": { "start:dev": "nodemon --ignore src/data/notifRequests.json src/index.js", - "start": "node src/index.js" + "start": "node src/index.js", + "migrate": "node src/data/scripts/run-migrations.js", + "populate:db": "npm run migrate && python3 src/data/scripts/populate_db.py" }, "keywords": [], "author": "", diff --git a/src/data/scripts/run-migrations.js b/src/data/scripts/run-migrations.js index 25e2b1d0..47481934 100644 --- a/src/data/scripts/run-migrations.js +++ b/src/data/scripts/run-migrations.js @@ -97,4 +97,10 @@ function runMigration() { } } -runMigration(); \ No newline at end of file +module.exports = { + runMigration +}; + +if (require.main === module) { + runMigration(); +} \ No newline at end of file From 58917a8995e6d27341c1a0209e0719e299532685 Mon Sep 17 00:00:00 2001 From: Chimdi Ejiogu Date: Sat, 8 Nov 2025 10:07:46 -0500 Subject: [PATCH 13/31] Fix scraping and database bugs --- src/data/db/database.py | 22 +++++++++---------- src/data/db/models.py | 3 +-- src/data/scrapers/printers.py | 41 +++++++++++++++++++++++++++-------- 3 files changed, 43 insertions(+), 23 deletions(-) diff --git a/src/data/db/database.py b/src/data/db/database.py index 7aba2109..9b857b2a 100644 --- a/src/data/db/database.py +++ b/src/data/db/database.py @@ -37,14 +37,18 @@ def insert_printer(location, description, labels, latitude, longitude): conn = get_db_connection() cursor = conn.cursor() + # We remove the "OR IGNORE" because we acknoledge that several printers may have the same location and description (i.e., same building and room), so we rely on the unique printer_id to identify the printer cursor.execute( """ - INSERT OR IGNORE INTO printers (location, description, latitude, longitude) + INSERT INTO printers (location, description, latitude, longitude) VALUES (?, ?, ?, ?) """, (location, description, latitude, longitude), ) - + + # To get the printer_id, we do NOT rely on the location/description/coordinates, but rather on the printer_id that was just inserted (lastrowid), as several printers may have the same location and description (i.e., same building and room) + printer_id = cursor.lastrowid + # Insert labels into the labels table and get their IDs label_ids = [] for label in labels: @@ -61,17 +65,11 @@ def insert_printer(location, description, labels, latitude, longitude): """, (label,), ) - label_id = cursor.fetchone()[0] + result = cursor.fetchone() + if result is None: + raise ValueError(f"Failed to find label: {label}") + label_id = result[0] label_ids.append(label_id) - - # Create entries in the junction table for printer-label relationships - cursor.execute( - """ - SELECT id FROM printers WHERE location = ? AND description = ? AND latitude = ? AND longitude = ? - """, - (location, description, latitude, longitude), - ) - printer_id = cursor.fetchone()[0] # Insert into junction table for label_id in label_ids: diff --git a/src/data/db/models.py b/src/data/db/models.py index 17db360b..8183be91 100644 --- a/src/data/db/models.py +++ b/src/data/db/models.py @@ -15,7 +15,6 @@ def create_tables(): conn = sqlite3.connect(DB_PATH) cursor = conn.cursor() - #TODO: Remove UNIQUE constraint from location cursor.execute( """ CREATE TABLE IF NOT EXISTS libraries ( @@ -32,7 +31,7 @@ def create_tables(): """ CREATE TABLE IF NOT EXISTS printers ( id INTEGER PRIMARY KEY AUTOINCREMENT, - location TEXT UNIQUE, + location TEXT, description TEXT, latitude REAL, longitude REAL diff --git a/src/data/scrapers/printers.py b/src/data/scrapers/printers.py index a71e1f71..ea40cd69 100644 --- a/src/data/scrapers/printers.py +++ b/src/data/scrapers/printers.py @@ -1,5 +1,4 @@ import requests -from bs4 import BeautifulSoup from difflib import get_close_matches # For data scraping from difflib import SequenceMatcher import re # For using regex @@ -112,13 +111,14 @@ ), # --- Printer capabilities --- - "Color": re.compile(r"\bcolor\b", re.IGNORECASE), + "Color, Scan, & Copy": re.compile( + r"\bcolor\s*[,/&]?\s*(?:scan\s*[,/&]?\s*)?(?:and\s*)?\s*&?\s*(?:copy|print|copying)\b", re.IGNORECASE + ), "Black & White": re.compile( r"\b(?:black\s*(?:and|&)\s*white|b\s*&\s*w)\b", re.IGNORECASE ), - "Color, Scan, & Copy": re.compile( - r"\bcolor[,/ &]*(scan|copy|print|copying)+\b", re.IGNORECASE - ), + "Color": re.compile(r"\bcolor\b", re.IGNORECASE), + } # Used for stripping residual trailing labels from descriptions @@ -209,11 +209,20 @@ def map_labels(text): # Search for the pattern in the cleaned text if pattern.search(cleaned): found_labels.append(canon) + cleaned = pattern.sub("", cleaned, count=1).strip() + + # Collapse runs of punctuation-delimiters to a single space + cleaned = re.sub(r"\s*[,;/|&\-–—:]+\s*", " ", cleaned) - # Remove the found label from the text to avoid duplicates - cleaned = pattern.sub("", cleaned).strip() + # Remove any leftover leading delimiters/spaces (e.g., ", ", "- ") + cleaned = re.sub(r"^[\s,;/|&\-–—:]+", "", cleaned) + # Remove standalone "Copy", "Print", or "Scan" at the start (leftover from partial label removal) + cleaned = re.sub(r"^(?:copy|print|scan)\s+", "", cleaned, flags=re.IGNORECASE) + + # Final whitespace cleanup cleaned = re.sub(r"\s+", " ", cleaned).strip() + return cleaned, sorted(set(found_labels)) def fetch_printers_json(): @@ -242,12 +251,17 @@ def scrape_printers(): # Map raw building name to canonical building name building, _ = map_building(raw_building) + # If we weren't able to map the building to a canonical building, skip this row + # NOTE: This should prevent us from getting "None" as the location, which was happening earlier + if building not in CANONICAL_BUILDINGS: + continue + # Map labels from description to canonical labels labels = [] _, building_labels = map_labels(raw_building) # Get labels from the building name (e.g., "Residents Only") remainder, location_labels = map_labels(raw_location) # Get labels from the location description (e.g., "Landscape Architecture Student ONLY") - + # Deduplicate and sort labels labels += building_labels labels += location_labels @@ -266,4 +280,13 @@ def scrape_printers(): "Labels": labels }) - return data \ No newline at end of file + return data + +if __name__ == "__main__": + results = scrape_printers() + print(f"Scraped {len(results)} printers.\n") + + # Print a sample of the data + for row in results: + if row['Location'] == 'Vet Library': + print(row['Description'], row['Labels']) \ No newline at end of file From 8f8486c9c3e4b510a945609d1368244f63d7fe4e Mon Sep 17 00:00:00 2001 From: Chimdi Ejiogu Date: Sat, 8 Nov 2025 10:12:17 -0500 Subject: [PATCH 14/31] Fix imports --- src/data/scripts/run-migrations.js | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/src/data/scripts/run-migrations.js b/src/data/scripts/run-migrations.js index 47481934..5e499fc3 100644 --- a/src/data/scripts/run-migrations.js +++ b/src/data/scripts/run-migrations.js @@ -1,10 +1,15 @@ // Imports necessary for data migrations -const fs = require('fs'); // Node's built-in file system module, which lets us read from disk -const path = require('path'); // Safer way to express file paths/path joining -const crypto = require('crypto'); -const Database = require('better-sqlite3'); +import fs from 'fs' // Node's built-in file system module, which lets us read from disk +import path from 'path'; // Safer way to express file paths/path joining +import crypto from 'crypto'; +import Database from 'better-sqlite3'; +import { fileURLToPath } from 'url'; -const DB_PATH = path.join(__dirname, "../transit.db"); // Finds db file from current file's directory +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// || path.join(__dirname, "../transit.db") +const DB_PATH = process.env.DB_PATH; // Finds db file from current file's directory const MIGRATIONS_DIR = path.join(__dirname, "../migrations"); /** @@ -97,10 +102,11 @@ function runMigration() { } } -module.exports = { - runMigration -}; - -if (require.main === module) { +export function runMigrations() { runMigration(); -} \ No newline at end of file +} + +import { pathToFileURL } from 'url'; +if (import.meta.url === pathToFileURL(process.argv[1]).href) { + runMigrations(); + } \ No newline at end of file From 7e062ccee33396f7cd89ab36862438af7bafb692 Mon Sep 17 00:00:00 2001 From: Chimdi Ejiogu Date: Sat, 8 Nov 2025 10:12:34 -0500 Subject: [PATCH 15/31] Add pycache --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 41635879..a8fbbdc4 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ build/ logs/ node_modules/ +__pycache__/ # Specific Files config.json From ae3dd910e9f60605e5fc14044ee153c0883bf327 Mon Sep 17 00:00:00 2001 From: Chimdi Ejiogu Date: Wed, 3 Dec 2025 15:49:21 -0500 Subject: [PATCH 16/31] Add migration file to store event form submissions --- .../migrations/20251112_1755_create_event_form.sql | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 src/data/migrations/20251112_1755_create_event_form.sql diff --git a/src/data/migrations/20251112_1755_create_event_form.sql b/src/data/migrations/20251112_1755_create_event_form.sql new file mode 100644 index 00000000..64e23b08 --- /dev/null +++ b/src/data/migrations/20251112_1755_create_event_form.sql @@ -0,0 +1,14 @@ +PRAGMA foreign_keys = ON; + +CREATE TABLE IF NOT EXISTS event_forms ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + netid TEXT NOT NULL, + event_type TEXT NOT NULL, + start_date DATETIME, + end_date DATETIME, + organization_name TEXT, + about TEXT, + location TEXT NOT NULL + approval_status TEXT NOT NULL DEFAULT 'pending' CHECK(approval_status IN ('pending', 'approved', 'rejected')) +); \ No newline at end of file From 7d2b43987b73f50cf084aff596f378ecdd244ddd Mon Sep 17 00:00:00 2001 From: Chimdi Ejiogu Date: Wed, 3 Dec 2025 15:50:21 -0500 Subject: [PATCH 17/31] Implement simple HTTP routing logic for event form submission and retrieval --- src/controllers/EventFormsController.js | 29 +++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 src/controllers/EventFormsController.js diff --git a/src/controllers/EventFormsController.js b/src/controllers/EventFormsController.js new file mode 100644 index 00000000..b1694e05 --- /dev/null +++ b/src/controllers/EventFormsController.js @@ -0,0 +1,29 @@ +import express from "express"; +import EventFormsUtils from "../utils/EventFormsUtils.js"; + +const router = express.Router(); + +// Create an event form +router.post("/create-event", async (req, res) => { + try { + const { netid, name, eventType, startDate, endDate, organizationName, location, about } = req.body; + const eventForm = await EventFormsUtils.createEventForm({ netid, name, eventType, startDate, endDate, organizationName, location, about }); + res.status(201).json({ success: true, message: "Event request submitted successfully", data: eventForm }); + } catch (error) { + console.error("Error creating event form:", error.message); + res.status(500).json({ success: false, message: "Error submitting event request:", error: error.message }); + } +}); + +// Get all event forms +router.get("/all-events", async (req, res) => { + try { + const eventForms = await EventFormsUtils.getAllEventForms(); + res.status(200).json({ success: true, message: "All event requests retrieved successfully", data: eventForms }); + } catch (error) { + console.error("Error getting all event forms:", error.message); + res.status(500).json({ success: false, message: "Error getting all event requests:", error: error.message }); + } +}); + +export default router; \ No newline at end of file From 08b5987a75f3360fddaaba213ee813996ac68438 Mon Sep 17 00:00:00 2001 From: Chimdi Ejiogu Date: Wed, 3 Dec 2025 15:51:28 -0500 Subject: [PATCH 18/31] Implement logic to add to and retrieve forms from database --- src/utils/EventFormsUtils.js | 116 +++++++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 src/utils/EventFormsUtils.js diff --git a/src/utils/EventFormsUtils.js b/src/utils/EventFormsUtils.js new file mode 100644 index 00000000..e7ec1da0 --- /dev/null +++ b/src/utils/EventFormsUtils.js @@ -0,0 +1,116 @@ +import sqlite3 from "sqlite3"; +import { fileURLToPath } from "url"; +import path from "path"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const dbPath = path.join(__dirname, "..", "data", "event_forms.db"); + +const ALLOWED_EVENT_TYPES = ['temporary', 'permanent']; + +/** + * Creates an event form in the database. + * + * @param eventForm - The event form to create. + * @returns {Promise} - The created event form. + */ + +function createEventForm({ netid, name, eventType, startDate = null, endDate = null, organizationName = null, location, about = null }) { + // Safety checks - make sure the event form is valid + if (!netid || !name || !eventType || !location) { + throw new Error("Invalid event form — netid, name, event type, and location are required"); + }; + + // Ensures event type is valid + if (!ALLOWED_EVENT_TYPES.includes(eventType)) { + throw new Error('Invalid event form — event type invalid'); + } + + // Handle event types + if (eventType == 'temporary') { + // If the event is temporary (e.g., tabling), then we require event's date(s) and times, and name of the hosting organization + if (!startDate || !endDate) { + // NOTE: The start and end dates are required for temporary events + throw new Error("Invalid event form — start and end dates are required for temporary events"); + } + if (!organizationName) { + // NOTE: The organization name is required for temporary events + throw new Error("Invalid event form — organization name is required for temporary events"); + } + } + + // Create the event form + const eventForm = { + netid, + name, + eventType, + startDate, + endDate, + organizationName, + location, + about, + approvalStatus: 'pending', + }; + + return new Promise((resolve, reject) => { + // Open the database + const db = new sqlite3.Database(dbPath, (err) => { + if (err) { + console.error(err.message); + return reject(err); + } + }); + + // Prepare the query + const query = `INSERT INTO event_forms (netid, event_type, ${eventForm.startDate ? "start_date, " : ""}${eventForm.endDate ? "end_date, " : ""}organization_name, location, approval_status${eventForm.about ? ', about' : ''}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?${eventForm.about ? ', ?' : ''})`; + const values = [eventForm.netid, eventForm.eventType, eventForm.startDate ? eventForm.startDate : null, eventForm.endDate, eventForm.organizationName, eventForm.location, eventForm.approvalStatus, eventForm.about ? eventForm.about : null]; + + // Insert the event form into the database + db.run(query, values, function (err) { + if (err) { + console.error(err.message); + return reject(err); + } + resolve(eventForm); + }); + + // Close the database + db.close((err) => { + if (err) console.error(err.message); + }); + }); +} + +/** + * Gets all event forms from the database. + * + * @returns {Promise>} - The event forms. + */ +function getAllEventForms() { + return new Promise((resolve, reject) => { + const db = new sqlite3.Database(dbPath, (err) => { + if (err) { + console.error(err.message); + return reject(err); + } + }); + + // Prepare the query + const query = `SELECT * FROM event_forms`; + db.all(query, (err, rows) => { + if (err) { + console.error(err.message); + return reject(err); + } + resolve(rows); + }); + + // Close the database + db.close((err) => { + if (err) console.error(err.message); + }); + }); +} + + +export default { createEventForm, getAllEventForms }; \ No newline at end of file From 5c9eb69cb62be22d94be826e6917574e1f7180a0 Mon Sep 17 00:00:00 2001 From: Chimdi Ejiogu Date: Wed, 3 Dec 2025 16:17:08 -0500 Subject: [PATCH 19/31] Add API documentation for new event form endpoints --- src/controllers/EventFormsController.js | 4 +- src/index.js | 3 + src/swagger.json | 174 ++++++++++++++++++++++++ 3 files changed, 179 insertions(+), 2 deletions(-) diff --git a/src/controllers/EventFormsController.js b/src/controllers/EventFormsController.js index b1694e05..dbede446 100644 --- a/src/controllers/EventFormsController.js +++ b/src/controllers/EventFormsController.js @@ -11,7 +11,7 @@ router.post("/create-event", async (req, res) => { res.status(201).json({ success: true, message: "Event request submitted successfully", data: eventForm }); } catch (error) { console.error("Error creating event form:", error.message); - res.status(500).json({ success: false, message: "Error submitting event request:", error: error.message }); + res.status(400).json({ success: false, message: "Error submitting event request", error: error.message }); } }); @@ -22,7 +22,7 @@ router.get("/all-events", async (req, res) => { res.status(200).json({ success: true, message: "All event requests retrieved successfully", data: eventForms }); } catch (error) { console.error("Error getting all event forms:", error.message); - res.status(500).json({ success: false, message: "Error getting all event requests:", error: error.message }); + res.status(400).json({ success: false, message: "Error getting all event requests", error: error.message }); } }); diff --git a/src/index.js b/src/index.js index efef3dc0..b8ee58d0 100644 --- a/src/index.js +++ b/src/index.js @@ -10,6 +10,7 @@ import notifRoutes from "./controllers/NotificationController.js"; import reportingRoutes from "./controllers/RouteReportingController.js"; import stopsRoutes from "./controllers/StopsController.js"; import ecosystemRoutes from "./controllers/EcosystemController.js"; +import eventFormsRoutes from "./controllers/EventFormsController.js"; import NotificationUtils from "./utils/NotificationUtils.js"; import RealtimeFeedUtilsV3 from "./utils/RealtimeFeedUtilsV3.js"; @@ -43,6 +44,8 @@ app.use('/api/v1/', reportingRoutes); app.use('/api/v1/', ecosystemRoutes); +app.use('/api/v1/', eventFormsRoutes); + // Setup Swagger docs app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(swaggerDoc)); diff --git a/src/swagger.json b/src/swagger.json index fc7f7348..1004ef56 100644 --- a/src/swagger.json +++ b/src/swagger.json @@ -74,6 +74,135 @@ } } }, + "/api/v1/all-events": { + "get": { + "summary": "Returns a list of all event requests.", + "description": "Returns a list of all event requests with fields success, message, and data.", + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "Returns an object with fields success, message, and data.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "All event requests retrieved successfully" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/EventRequest" + } + } + } + } + } + } + }, + "400": { + "description": "Returns an object with fields success, message, and error.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": false + }, + "message": { + "type": "string", + "example": "Error getting all event requests" + }, + "error": { + "type": "string" + } + } + } + } + } + } + } + } + }, + "/api/v1/create-event": { + "post": { + "summary": "Submits an event request.", + "description": "Takes in a list of objects with fields netid, name, eventType, startDate, endDate, organizationName, location, and about and returns a list of objects with fields success, message, and data.", + "produces": [ + "application/json" + ], + "parameters": [ + { + "in": "body", + "name": "body", + "description": "The event request to submit with fields netid, name, eventType, startDate, endDate, organizationName, location, and about.", + "required": true, + "schema": { + "$ref": "#/components/schemas/EventRequest" + } + } + ], + "responses": { + "201": { + "description": "Returns an object with fields success, message, and data.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "Event request submitted successfully" + }, + "data": { + "$ref": "#/components/schemas/EventRequest" + } + } + } + } + } + }, + "400": { + "description": "Returns an object with fields success, message, and error. If the error is due to an invalid event form, the error message will be 'Invalid event form — netid, name, event type, and location are required'.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": false + }, + "message": { + "type": "string", + "example": "Error submitting event request" + }, + "error": { + "type": "string", + "example": "Invalid event form — netid, name, event type, and location are required" + } + } + } + } + } + } + } + } + }, "/api/v1/delays": { "post": { "summary": "Returns a list of bus delays for buses at a specific stop. **Recommended and most up-to-date.", @@ -1332,6 +1461,51 @@ "example": 34 } } + }, + "EventRequest": { + "type": "object", + "properties": { + "netid": { + "type": "string", + "description": "The netid of the user submitting the event request.", + "example": "jdoe123" + }, + "name": { + "type": "string", + "description": "The name of the event.", + "example": "Event Name" + }, + "eventType": { + "type": "string", + "description": "The type of event.", + "example": "temporary" + }, + "startDate": { + "type": "string", + "description": "The start date of the event.", + "example": "2025-01-01 12:00:00" + }, + "endDate": { + "type": "string", + "description": "The end date of the event.", + "example": "2025-01-02 12:00:00" + }, + "organizationName": { + "type": "string", + "description": "The name of the organization hosting the event.", + "example": "Organization Name" + }, + "location": { + "type": "string", + "description": "The location of the event.", + "example": "Location Name" + }, + "about": { + "type": "string", + "description": "The about section of the event.", + "example": "About the event." + } + } } } }, From 4ac8d38780694f335782bd1285cbe7233e5c6f7f Mon Sep 17 00:00:00 2001 From: Chimdi Ejiogu Date: Thu, 4 Dec 2025 03:06:49 -0500 Subject: [PATCH 20/31] Implement websocket logic into API networking, introduce new endpoints for handling form submissions --- package-lock.json | 239 ++++++++++++++++++++++++ package.json | 1 + src/controllers/EventFormsController.js | 64 ++++++- src/index.js | 65 ++++++- src/utils/EventFormsUtils.js | 106 ++++++++++- 5 files changed, 466 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index f6b08421..691feaa6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "lru-cache": "^11.0.2", "node-schedule": "^2.1.1", "request": "^2.88.2", + "socket.io": "^4.8.1", "sqlite": "^5.1.1", "sqlite3": "^5.1.7", "swagger-ui-express": "^5.0.1", @@ -400,6 +401,12 @@ "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", "hasInstallScript": true }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, "node_modules/@tootallnate/once": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", @@ -432,6 +439,15 @@ "@types/node": "*" } }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/express": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", @@ -784,6 +800,15 @@ } ] }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "license": "MIT", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, "node_modules/bcrypt-pbkdf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", @@ -1180,6 +1205,19 @@ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==" }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/cron-parser": { "version": "4.9.0", "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.9.0.tgz", @@ -1394,6 +1432,67 @@ "once": "^1.4.0" } }, + "node_modules/engine.io": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz", + "integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==", + "license": "MIT", + "dependencies": { + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/env-paths": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", @@ -3353,6 +3452,15 @@ "node": "*" } }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-inspect": { "version": "1.13.3", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz", @@ -4019,6 +4127,116 @@ "npm": ">= 3.0.0" } }, + "node_modules/socket.io": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", + "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", + "license": "MIT", + "dependencies": { + "debug": "~4.3.4", + "ws": "~8.17.1" + } + }, + "node_modules/socket.io-adapter/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-adapter/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/socket.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/socks": { "version": "2.8.4", "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.4.tgz", @@ -4697,6 +4915,27 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/xmlbuilder": { "version": "8.2.2", "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-8.2.2.tgz", diff --git a/package.json b/package.json index b4d1f5c2..cb0cc12d 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "lru-cache": "^11.0.2", "node-schedule": "^2.1.1", "request": "^2.88.2", + "socket.io": "^4.8.1", "sqlite": "^5.1.1", "sqlite3": "^5.1.7", "swagger-ui-express": "^5.0.1", diff --git a/src/controllers/EventFormsController.js b/src/controllers/EventFormsController.js index dbede446..147a5634 100644 --- a/src/controllers/EventFormsController.js +++ b/src/controllers/EventFormsController.js @@ -1,14 +1,20 @@ import express from "express"; -import EventFormsUtils from "../utils/EventFormsUtils.js"; +import { createEventForm, getAllEventForms, updateEventForm, getApprovedEventForms, toPublicEvent } from "../utils/EventFormsUtils.js"; const router = express.Router(); // Create an event form -router.post("/create-event", async (req, res) => { +router.post("/events/create-event", async (req, res) => { try { const { netid, name, eventType, startDate, endDate, organizationName, location, about } = req.body; - const eventForm = await EventFormsUtils.createEventForm({ netid, name, eventType, startDate, endDate, organizationName, location, about }); - res.status(201).json({ success: true, message: "Event request submitted successfully", data: eventForm }); + const eventForm = await createEventForm({ netid, name, eventType, startDate, endDate, organizationName, location, about }); + + // Broadcast a notification to all clients that the event form has been created + const io = req.app.get("io"); + io.to("admin").emit("eventForm:new", {message: "Event request submitted", event: eventForm}); + io.to(`netid:${netid}`).emit("eventForm:new", {message: "Your event request has been submitted", event: toPublicEvent(eventForm)}); + + res.status(201).json({ success: true, message: "Event request submitted successfully", data: toPublicEvent(eventForm) }); } catch (error) { console.error("Error creating event form:", error.message); res.status(400).json({ success: false, message: "Error submitting event request", error: error.message }); @@ -16,14 +22,58 @@ router.post("/create-event", async (req, res) => { }); // Get all event forms -router.get("/all-events", async (req, res) => { +router.get("/events/", async (req, res) => { try { - const eventForms = await EventFormsUtils.getAllEventForms(); - res.status(200).json({ success: true, message: "All event requests retrieved successfully", data: eventForms }); + const eventForms = await getAllEventForms(); + res.status(200).json({ success: true, message: "All event requests retrieved successfully", data: eventForms.map(toPublicEvent) }); } catch (error) { console.error("Error getting all event forms:", error.message); res.status(400).json({ success: false, message: "Error getting all event requests", error: error.message }); } }); +// Update an event form +// NOTE: Only admins can update event forms +// NOTE: id is the event form's id, stored as the primary key in the database +router.put("/events/:id", async (req, res) => { + try { + const { id } = req.params; + const { approvalStatus } = req.body; + + // Initalize the io instance + const io = req.app.get("io"); + + // Update the event form in the database + const eventForm = await updateEventForm({ id: parseInt(id), approvalStatus }); + + // Handle event approval + if (approvalStatus === "approved") { + // Send a notification to everyone (and the admin room) + io.to("public").emit("eventForm:update", {message: "Event approved", event: toPublicEvent(eventForm)}); + io.to("admin").emit("eventForm:update", {message: "Event approved", event: eventForm}); + io.to(`netid:${eventForm.netid}`).emit("eventForm:update", {message: "Your event request has been approved", event: toPublicEvent(eventForm)}); + } else { + // Send a notification to only the submitting user that the event was rejected + io.to(`netid:${eventForm.netid}`).emit("eventForm:update", {message: "Your event request has been rejected", event: toPublicEvent(eventForm)}); + io.to("admin").emit("eventForm:update", {message: "Event rejected", event: eventForm}); + } + + res.status(200).json({ success: true, message: "Event request updated successfully", data: eventForm }); + } catch (error) { + console.error("Error updating event form:", error.message); + res.status(400).json({ success: false, message: "Error updating event request", error: error.message }); + } +}); + +// Get all approved event forms +router.get("/events/approved", async (req, res) => { + try { + const eventForms = await getApprovedEventForms(); + res.status(200).json({ success: true, message: "All approved event requests retrieved successfully", data: eventForms }); + } catch (error) { + console.error("Error getting all approved event requests:", error.message); + res.status(400).json({ success: false, message: "Error getting all approved event requests", error: error.message }); + } +}); + export default router; \ No newline at end of file diff --git a/src/index.js b/src/index.js index b8ee58d0..bb8fdadb 100644 --- a/src/index.js +++ b/src/index.js @@ -22,10 +22,26 @@ import AlertsUtils from "./utils/AlertsUtils.js"; import AllStopUtils from "./utils/AllStopUtils.js"; import GTFSUtils from "./utils/GTFSUtils.js"; +import { createServer } from "http"; +import { Server as SocketIOServer } from "socket.io"; const app = express(); const port = process.env.PORT; +const httpServer = createServer(app); + +// Setup Socket.IO +const io = new SocketIOServer(httpServer, { + cors: { + origin: `http://localhost:${port}`, // Come back to, update URL + methods: ["GET", "POST"], + credentials: true, + }, +}); + +// Set the io instance to the app, so it can be accessed by the controllers +app.set("io", io); + app.use(express.json()); app.use('/api/v1/', delayRoutes); @@ -67,8 +83,55 @@ admin.initializeApp({ databaseURL: "https://ithaca-transit.firebaseio.com", }); +const ALLOWED_ROLES = ['admin', 'user']; + +// Handle a given socket's connections +io.on("connection", (socket) => { + // Log the socket connection + console.log("Client connected: ", socket.id); + + // Identify the socket's + socket.on("identify", ({role, netid}) => { + + // Safety checks - make sure the netid and role are valid + if (!netid || !role) { + console.error("Invalid netid or role - netid and role are required"); + socket.emit("identify:error", {message: "Invalid netid or role - netid and role are required"}); + return; + }; + + // Ensures role is valid + if (!ALLOWED_ROLES.includes(role)) { + console.error("Invalid role - role must be one of: " + ALLOWED_ROLES.join(", ")); + socket.emit("identify:error", {message: "Invalid role - role must be one of: " + ALLOWED_ROLES.join(", ")}); + return; + }; + + // Makes the socket a member of the public room — done only after all other checks pass + socket.join("public"); + + // Set the socket's data + socket.data.netid = netid; + socket.data.role = role; + console.log("Socket identified: ", socket.data); + + if (role === 'admin') { + socket.join("admin"); + } + + if (role === 'user') { + socket.join(`netid:${netid}`); + } + }); + + // Log the socket disconnection + socket.on("disconnect", () => { + console.log("Client disconnected: ", socket.id); + }); +}); + // Start the server -app.listen(port, () => { +httpServer.listen(port, () => { console.log(`[server]: Server is running at http://localhost:${port}`); console.log(`Swagger docs available at http://localhost:${port}/api-docs`); }); diff --git a/src/utils/EventFormsUtils.js b/src/utils/EventFormsUtils.js index e7ec1da0..edde08e1 100644 --- a/src/utils/EventFormsUtils.js +++ b/src/utils/EventFormsUtils.js @@ -13,6 +13,8 @@ const ALLOWED_EVENT_TYPES = ['temporary', 'permanent']; * * @param eventForm - The event form to create. * @returns {Promise} - The created event form. + * @throws {Error} - If the event form is invalid. + * @throws {Error} - If the database connection fails. */ function createEventForm({ netid, name, eventType, startDate = null, endDate = null, organizationName = null, location, about = null }) { @@ -85,6 +87,7 @@ function createEventForm({ netid, name, eventType, startDate = null, endDate = n * Gets all event forms from the database. * * @returns {Promise>} - The event forms. + * @throws {Error} - If the database connection fails. */ function getAllEventForms() { return new Promise((resolve, reject) => { @@ -112,5 +115,106 @@ function getAllEventForms() { }); } +/** + * Updates an event form in the database. + * + * Allowed approval statuses are: 'pending', 'approved', 'rejected'. + * + * @param {Object} payload - The payload containing the id and approval status of the event form to update. + * @param {string} payload.id - The id of the event form to update. + * @param {string} payload.approvalStatus - The approval status to update the event form to. + * @returns {Promise} - The updated event form. + * @throws {Error} - If the event form is invalid or the approval status is invalid. + * @throws {Error} - If the event form is not found. + */ +function updateEventForm(payload) { + const { id, approvalStatus } = payload; + + // Safety checks - make sure the event form is valid + if (!id || !approvalStatus) { + throw new Error("Invalid event form — id and approval status are required"); + } + + // Ensures approval status is valid + if (!ALLOWED_APPROVAL_STATUSES.includes(approvalStatus)) { + throw new Error('Invalid event form — approval status invalid'); + } + + return new Promise((resolve, reject) => { + const db = new sqlite3.Database(dbPath, (err) => { + if (err) { + console.error(err.message); + return reject(err); + } + }); + + // Prepare the query + const query = `UPDATE event_forms SET approval_status = ? WHERE id = ?`; + const values = [approvalStatus, id]; + + // Update the event form in the database + db.run(query, values, function (err) { + if (err) { + console.error(err.message); + return reject(err); + } + resolve(eventForm); + }); + + // Close the database + db.close((err) => { + if (err) console.error(err.message); + }); + }); +} + +/** + * Gets all approved event forms from the database. + * + * @returns {Promise>} - The approved event forms. + * @throws {Error} - If the database connection fails. + */ +function getApprovedEventForms() { + return new Promise((resolve, reject) => { + const db = new sqlite3.Database(dbPath, (err) => { + if (err) { + console.error(err.message); + return reject(err); + } + }); + + // Prepare the query + const query = `SELECT * FROM event_forms WHERE approval_status = 'approved'`; + db.all(query, (err, rows) => { + if (err) { + console.error(err.message); + return reject(err); + } + resolve(rows); + }); + + // Close the database + db.close((err) => { + if (err) console.error(err.message); + }); + }); +} + +/** + * Converts an event form to a public event object. + * + * @param {Object} eventForm - The event form to convert. + * @returns {Object} - The public event object. + */ +function toPublicEvent(eventForm) { + return { + id: eventForm.id, + name: eventForm.name, + eventType: eventForm.eventType, + startDate: eventForm.startDate, + endDate: eventForm.endDate, + organizationName: eventForm.organizationName, + }; +} -export default { createEventForm, getAllEventForms }; \ No newline at end of file +export default { createEventForm, getAllEventForms, updateEventForm, getApprovedEventForms, toPublicEvent }; \ No newline at end of file From 8fe085e6b72eb1b90e87d0a3406dc38242be9325 Mon Sep 17 00:00:00 2001 From: Chimdi Ejiogu Date: Thu, 4 Dec 2025 03:28:17 -0500 Subject: [PATCH 21/31] Remove extraneous function --- src/controllers/EventFormsController.js | 14 +++++++------- src/utils/EventFormsUtils.js | 20 ++------------------ 2 files changed, 9 insertions(+), 25 deletions(-) diff --git a/src/controllers/EventFormsController.js b/src/controllers/EventFormsController.js index 147a5634..e781c84c 100644 --- a/src/controllers/EventFormsController.js +++ b/src/controllers/EventFormsController.js @@ -1,5 +1,5 @@ import express from "express"; -import { createEventForm, getAllEventForms, updateEventForm, getApprovedEventForms, toPublicEvent } from "../utils/EventFormsUtils.js"; +import { createEventForm, getAllEventForms, updateEventForm, getApprovedEventForms } from "../utils/EventFormsUtils.js"; const router = express.Router(); @@ -12,9 +12,9 @@ router.post("/events/create-event", async (req, res) => { // Broadcast a notification to all clients that the event form has been created const io = req.app.get("io"); io.to("admin").emit("eventForm:new", {message: "Event request submitted", event: eventForm}); - io.to(`netid:${netid}`).emit("eventForm:new", {message: "Your event request has been submitted", event: toPublicEvent(eventForm)}); + io.to(`netid:${netid}`).emit("eventForm:new", {message: "Your event request has been submitted", event: eventForm}); - res.status(201).json({ success: true, message: "Event request submitted successfully", data: toPublicEvent(eventForm) }); + res.status(201).json({ success: true, message: "Event request submitted successfully", data: eventForm }); } catch (error) { console.error("Error creating event form:", error.message); res.status(400).json({ success: false, message: "Error submitting event request", error: error.message }); @@ -25,7 +25,7 @@ router.post("/events/create-event", async (req, res) => { router.get("/events/", async (req, res) => { try { const eventForms = await getAllEventForms(); - res.status(200).json({ success: true, message: "All event requests retrieved successfully", data: eventForms.map(toPublicEvent) }); + res.status(200).json({ success: true, message: "All event requests retrieved successfully", data: eventForms }); } catch (error) { console.error("Error getting all event forms:", error.message); res.status(400).json({ success: false, message: "Error getting all event requests", error: error.message }); @@ -49,12 +49,12 @@ router.put("/events/:id", async (req, res) => { // Handle event approval if (approvalStatus === "approved") { // Send a notification to everyone (and the admin room) - io.to("public").emit("eventForm:update", {message: "Event approved", event: toPublicEvent(eventForm)}); + io.to("public").emit("eventForm:update", {message: "Event approved", event: eventForm}); io.to("admin").emit("eventForm:update", {message: "Event approved", event: eventForm}); - io.to(`netid:${eventForm.netid}`).emit("eventForm:update", {message: "Your event request has been approved", event: toPublicEvent(eventForm)}); + io.to(`netid:${eventForm.netid}`).emit("eventForm:update", {message: "Your event request has been approved", event: eventForm}); } else { // Send a notification to only the submitting user that the event was rejected - io.to(`netid:${eventForm.netid}`).emit("eventForm:update", {message: "Your event request has been rejected", event: toPublicEvent(eventForm)}); + io.to(`netid:${eventForm.netid}`).emit("eventForm:update", {message: "Your event request has been rejected", event: eventForm}); io.to("admin").emit("eventForm:update", {message: "Event rejected", event: eventForm}); } diff --git a/src/utils/EventFormsUtils.js b/src/utils/EventFormsUtils.js index edde08e1..ea719c45 100644 --- a/src/utils/EventFormsUtils.js +++ b/src/utils/EventFormsUtils.js @@ -7,6 +7,7 @@ const __dirname = path.dirname(__filename); const dbPath = path.join(__dirname, "..", "data", "event_forms.db"); const ALLOWED_EVENT_TYPES = ['temporary', 'permanent']; +const ALLOWED_APPROVAL_STATUSES = ['pending', 'approved', 'rejected']; /** * Creates an event form in the database. @@ -200,21 +201,4 @@ function getApprovedEventForms() { }); } -/** - * Converts an event form to a public event object. - * - * @param {Object} eventForm - The event form to convert. - * @returns {Object} - The public event object. - */ -function toPublicEvent(eventForm) { - return { - id: eventForm.id, - name: eventForm.name, - eventType: eventForm.eventType, - startDate: eventForm.startDate, - endDate: eventForm.endDate, - organizationName: eventForm.organizationName, - }; -} - -export default { createEventForm, getAllEventForms, updateEventForm, getApprovedEventForms, toPublicEvent }; \ No newline at end of file +export { createEventForm, getAllEventForms, updateEventForm, getApprovedEventForms }; \ No newline at end of file From c9ff5a935fff80a265ebaf009917e4d8344171f1 Mon Sep 17 00:00:00 2001 From: Chimdi Ejiogu Date: Thu, 4 Dec 2025 03:28:46 -0500 Subject: [PATCH 22/31] Add documentation to new endpoints --- src/swagger.json | 136 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 132 insertions(+), 4 deletions(-) diff --git a/src/swagger.json b/src/swagger.json index 1004ef56..22457b7e 100644 --- a/src/swagger.json +++ b/src/swagger.json @@ -74,10 +74,10 @@ } } }, - "/api/v1/all-events": { + "/api/v1/events/": { "get": { - "summary": "Returns a list of all event requests.", - "description": "Returns a list of all event requests with fields success, message, and data.", + "summary": "Returns an object containing a list of all event requests.", + "description": "Returns an object with fields success, message, and data, where data is a list of all event requests.", "produces": [ "application/json" ], @@ -134,7 +134,7 @@ } } }, - "/api/v1/create-event": { + "/api/v1/events/create-event": { "post": { "summary": "Submits an event request.", "description": "Takes in a list of objects with fields netid, name, eventType, startDate, endDate, organizationName, location, and about and returns a list of objects with fields success, message, and data.", @@ -203,6 +203,134 @@ } } }, + "/api/v1/events/:id": { + "put": { + "summary": "Updates an event request.", + "description": "Takes in a list of objects with fields id and approvalStatus and returns a list of objects with fields success, message, and data.", + "produces": [ + "application/json" + ], + "parameters": [ + { + "in": "path", + "name": "id", + "description": "The id of the event request to update, as specified in the database.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Returns an object with fields success, message, and data.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "Event request updated successfully" + }, + "data": { + "$ref": "#/components/schemas/EventRequest" + } + } + } + } + } + }, + "400": { + "description": "Returns an object with fields success, message, and error.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": false + }, + "message": { + "type": "string", + "example": "Error updating event request" + }, + "error": { + "type": "string" + } + } + } + } + } + } + } + } + }, + "/api/v1/events/approved": { + "get": { + "summary": "Returns an object containing a list of all approved event requests.", + "description": "Returns an object with fields success, message, and data, where data is a list of all approved event requests.", + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "Returns an object with fields success, message, and data.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "All approved event requests retrieved successfully" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/EventRequest" + } + } + } + } + } + } + }, + "400": { + "description": "Returns an object with fields success, message, and error.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": false + }, + "message": { + "type": "string", + "example": "Error getting all approved event requests" + }, + "error": { + "type": "string" + } + } + } + } + } + } + } + } + }, "/api/v1/delays": { "post": { "summary": "Returns a list of bus delays for buses at a specific stop. **Recommended and most up-to-date.", From e27a3b4cc81a726148e9e9fef663ff9672d151d1 Mon Sep 17 00:00:00 2001 From: Chimdi Ejiogu Date: Tue, 23 Dec 2025 13:10:57 -0500 Subject: [PATCH 23/31] Minor change to database schema, store potentially important form metadata --- src/data/migrations/20251112_1755_create_event_form.sql | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/data/migrations/20251112_1755_create_event_form.sql b/src/data/migrations/20251112_1755_create_event_form.sql index 64e23b08..2582ff9c 100644 --- a/src/data/migrations/20251112_1755_create_event_form.sql +++ b/src/data/migrations/20251112_1755_create_event_form.sql @@ -9,6 +9,8 @@ CREATE TABLE IF NOT EXISTS event_forms ( end_date DATETIME, organization_name TEXT, about TEXT, - location TEXT NOT NULL - approval_status TEXT NOT NULL DEFAULT 'pending' CHECK(approval_status IN ('pending', 'approved', 'rejected')) + location TEXT NOT NULL, + approval_status TEXT NOT NULL DEFAULT 'pending' CHECK(approval_status IN ('pending', 'approved', 'rejected')), + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ); \ No newline at end of file From aedfaf39981ccf1cd33ea2764b846c6eb69f33b1 Mon Sep 17 00:00:00 2001 From: Chimdi Ejiogu Date: Fri, 26 Dec 2025 11:07:58 -0500 Subject: [PATCH 24/31] Add migration file to remove unique constraint from location attribute for printers table --- .gitignore | 4 +++ ...6_1106_remove_unique_location_printers.sql | 26 ++++++++++++++++++ src/data/scripts/run-migrations.js | 3 +- src/data/transit.db | Bin 28672 -> 65536 bytes 4 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 src/data/migrations/20251226_1106_remove_unique_location_printers.sql diff --git a/.gitignore b/.gitignore index a8fbbdc4..8cb6f767 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,10 @@ config.json service-account-credentials.json *.pem +# Database files +*.db-shm +*.db-wal + # File Types *.env *.zip diff --git a/src/data/migrations/20251226_1106_remove_unique_location_printers.sql b/src/data/migrations/20251226_1106_remove_unique_location_printers.sql new file mode 100644 index 00000000..ab373c41 --- /dev/null +++ b/src/data/migrations/20251226_1106_remove_unique_location_printers.sql @@ -0,0 +1,26 @@ +PRAGMA foreign_keys = OFF; +BEGIN; + +-- Step 1: Create a new printers table (match old schema except for the UNIQUE constraint on location) +CREATE TABLE printers_new ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + location TEXT, + description TEXT, + latitude REAL, + longitude REAL +); + +-- Step 2: Copy data from the old table to the new table (only if old table exists and has data) +-- Note: This will fail silently if printers doesn't exist, which is fine +INSERT INTO printers_new (id, location, description, latitude, longitude) +SELECT id, location, description, latitude, longitude +FROM printers; + +-- Step 3: Drop the old table +DROP TABLE IF EXISTS printers; + +-- Step 4: Rename the new table to the original name +ALTER TABLE printers_new RENAME TO printers; + +COMMIT; +PRAGMA foreign_keys = ON; \ No newline at end of file diff --git a/src/data/scripts/run-migrations.js b/src/data/scripts/run-migrations.js index 5e499fc3..428fbdee 100644 --- a/src/data/scripts/run-migrations.js +++ b/src/data/scripts/run-migrations.js @@ -8,8 +8,7 @@ import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); -// || path.join(__dirname, "../transit.db") -const DB_PATH = process.env.DB_PATH; // Finds db file from current file's directory +const DB_PATH = process.env.DB_PATH || path.join(__dirname, "../transit.db"); // Finds db file from current file's directory const MIGRATIONS_DIR = path.join(__dirname, "../migrations"); /** diff --git a/src/data/transit.db b/src/data/transit.db index 0ebc41a48a2178e97c8953c13cfef0361f175601..7558ce44dc44a4db830c7ee237122cbf471efb68 100644 GIT binary patch delta 10779 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 delta 326 zcmZo@U}<>3s1qFMlUb6gkd|MRn^>Y?%pkzP$jIQJpuoVusKvm*zz)PPz&ugMSW=5Y zuV@i3{|^Q>o*V|gyZlG_x_JxvqIhyP8!CL^-kih3%E;B2#=Pw z*&TlJ3vmJUnsWbP;Qqt^h5tVPPX3jfg#sq vs51LAPPXt@RALrk)@5|e$xKdFaL&)qhA3y^W|rl2%q~y0%FfSQlpp{A9p_iS From e3396c89d2a16080ddcbc3bd94f0eae7e27d559f Mon Sep 17 00:00:00 2001 From: Chimdi Ejiogu Date: Fri, 26 Dec 2025 18:48:31 -0500 Subject: [PATCH 25/31] Add file for testing WebSocket functionality --- src/data/scripts/socket_test_client.js | 32 ++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 src/data/scripts/socket_test_client.js diff --git a/src/data/scripts/socket_test_client.js b/src/data/scripts/socket_test_client.js new file mode 100644 index 00000000..06908a7b --- /dev/null +++ b/src/data/scripts/socket_test_client.js @@ -0,0 +1,32 @@ +import { io } from 'socket.io-client'; + +const URL = "http://localhost:3000"; +const role = process.argv[2] ?? "user"; +const netid = process.argv[3] ?? "ce123"; + +// Create a client to connect to the server +const socket = io(URL, { // io is a factory function that creates a socket instance + transports: ['websocket'], // Specifies the transport to use for the socket connection +}); + +socket.on('connect', () => { + console.log('Connected to server'); + socket.emit('identify', { role, netid }); +}); + +socket.on("identify:error", (payload) => { + console.log("identify:error", payload); + }); + +socket.on("eventForm:new", (payload) => { + console.log("eventForm:new", payload); +}); + +socket.on("eventForm:update", (payload) => { + console.log("eventForm:update", payload); +}); + +socket.on("disconnect", (reason) => { + console.log("disconnected", reason); +}); + From b50b6ca2b304335f98ffd95bfcf68d1c7e2068b3 Mon Sep 17 00:00:00 2001 From: Chimdi Ejiogu Date: Fri, 26 Dec 2025 18:49:28 -0500 Subject: [PATCH 26/31] Add migration file to remove UNIQUE constraint from location in printers table --- .../20251226_1106_remove_unique_location_printers.sql | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/data/migrations/20251226_1106_remove_unique_location_printers.sql b/src/data/migrations/20251226_1106_remove_unique_location_printers.sql index ab373c41..5c024c50 100644 --- a/src/data/migrations/20251226_1106_remove_unique_location_printers.sql +++ b/src/data/migrations/20251226_1106_remove_unique_location_printers.sql @@ -1,5 +1,4 @@ PRAGMA foreign_keys = OFF; -BEGIN; -- Step 1: Create a new printers table (match old schema except for the UNIQUE constraint on location) CREATE TABLE printers_new ( @@ -11,7 +10,6 @@ CREATE TABLE printers_new ( ); -- Step 2: Copy data from the old table to the new table (only if old table exists and has data) --- Note: This will fail silently if printers doesn't exist, which is fine INSERT INTO printers_new (id, location, description, latitude, longitude) SELECT id, location, description, latitude, longitude FROM printers; @@ -22,5 +20,4 @@ DROP TABLE IF EXISTS printers; -- Step 4: Rename the new table to the original name ALTER TABLE printers_new RENAME TO printers; -COMMIT; PRAGMA foreign_keys = ON; \ No newline at end of file From ccdbcb03e2a0143905a9cdb33b0c4c947b235b93 Mon Sep 17 00:00:00 2001 From: Chimdi Ejiogu Date: Fri, 26 Dec 2025 18:50:12 -0500 Subject: [PATCH 27/31] Bug fixes with WebSocket + DB functionality --- package-lock.json | 104 ++++++++++++++++++++++++ package.json | 1 + src/controllers/EventFormsController.js | 24 +++--- src/data/transit.db | Bin 65536 -> 98304 bytes src/utils/EventFormsUtils.js | 86 +++++++++++++------- 5 files changed, 173 insertions(+), 42 deletions(-) diff --git a/package-lock.json b/package-lock.json index 691feaa6..a0f6b385 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "node-schedule": "^2.1.1", "request": "^2.88.2", "socket.io": "^4.8.1", + "socket.io-client": "^4.8.3", "sqlite": "^5.1.1", "sqlite3": "^5.1.7", "swagger-ui-express": "^5.0.1", @@ -1452,6 +1453,63 @@ "node": ">=10.2.0" } }, + "node_modules/engine.io-client": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.4.tgz", + "integrity": "sha512-+kjUJnZGwzewFDw951CDWcwj35vMNf2fcj7xQWOctq1F2i1jkDdVvdFG9kM/BEChymCH36KgjnW0NsL58JYRxw==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.18.3", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-client/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io-client/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/engine.io-client/node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/engine.io-parser": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", @@ -4178,6 +4236,44 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/socket.io-client": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.3.tgz", + "integrity": "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1", + "engine.io-client": "~6.6.1", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-client/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-client/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/socket.io-parser": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", @@ -4944,6 +5040,14 @@ "node": ">=4.0" } }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", + "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index cb0cc12d..c3817400 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "node-schedule": "^2.1.1", "request": "^2.88.2", "socket.io": "^4.8.1", + "socket.io-client": "^4.8.3", "sqlite": "^5.1.1", "sqlite3": "^5.1.7", "swagger-ui-express": "^5.0.1", diff --git a/src/controllers/EventFormsController.js b/src/controllers/EventFormsController.js index e781c84c..d2ac75e9 100644 --- a/src/controllers/EventFormsController.js +++ b/src/controllers/EventFormsController.js @@ -1,5 +1,5 @@ import express from "express"; -import { createEventForm, getAllEventForms, updateEventForm, getApprovedEventForms } from "../utils/EventFormsUtils.js"; +import { createEventForm, getAllEventForms, updateEventForm, getApprovedEventForms, toPublicEvent } from "../utils/EventFormsUtils.js"; const router = express.Router(); @@ -9,12 +9,12 @@ router.post("/events/create-event", async (req, res) => { const { netid, name, eventType, startDate, endDate, organizationName, location, about } = req.body; const eventForm = await createEventForm({ netid, name, eventType, startDate, endDate, organizationName, location, about }); - // Broadcast a notification to all clients that the event form has been created + // Broadcast a notification to the admin room that the event form has been created const io = req.app.get("io"); io.to("admin").emit("eventForm:new", {message: "Event request submitted", event: eventForm}); - io.to(`netid:${netid}`).emit("eventForm:new", {message: "Your event request has been submitted", event: eventForm}); - res.status(201).json({ success: true, message: "Event request submitted successfully", data: eventForm }); + // Return the event form to the requesting client + res.status(201).json({ success: true, message: "Your event request has been submitted", data: toPublicEvent(eventForm)}); } catch (error) { console.error("Error creating event form:", error.message); res.status(400).json({ success: false, message: "Error submitting event request", error: error.message }); @@ -25,7 +25,7 @@ router.post("/events/create-event", async (req, res) => { router.get("/events/", async (req, res) => { try { const eventForms = await getAllEventForms(); - res.status(200).json({ success: true, message: "All event requests retrieved successfully", data: eventForms }); + res.status(200).json({ success: true, message: "All event requests retrieved successfully", data: eventForms.map(toPublicEvent)}); } catch (error) { console.error("Error getting all event forms:", error.message); res.status(400).json({ success: false, message: "Error getting all event requests", error: error.message }); @@ -46,19 +46,19 @@ router.put("/events/:id", async (req, res) => { // Update the event form in the database const eventForm = await updateEventForm({ id: parseInt(id), approvalStatus }); - // Handle event approval + // Handle event approval (currentl assumes that an update is only for approval or rejection, excludes pending) if (approvalStatus === "approved") { // Send a notification to everyone (and the admin room) - io.to("public").emit("eventForm:update", {message: "Event approved", event: eventForm}); + io.to("public").emit("eventForm:update", {message: "Event approved", event: toPublicEvent(eventForm)}); io.to("admin").emit("eventForm:update", {message: "Event approved", event: eventForm}); - io.to(`netid:${eventForm.netid}`).emit("eventForm:update", {message: "Your event request has been approved", event: eventForm}); } else { - // Send a notification to only the submitting user that the event was rejected - io.to(`netid:${eventForm.netid}`).emit("eventForm:update", {message: "Your event request has been rejected", event: eventForm}); + // Send a notification to the submitting user (and the admin room) that the event was rejected + io.to(`netid:${eventForm.netid}`).emit("eventForm:update", {message: "Your event request has been rejected", event: toPublicEvent(eventForm)}); io.to("admin").emit("eventForm:update", {message: "Event rejected", event: eventForm}); } - res.status(200).json({ success: true, message: "Event request updated successfully", data: eventForm }); + // Return the event form to the admin client that requested the update + res.status(200).json({ success: true, message: "Event request updated successfully", data: toPublicEvent(eventForm)}); } catch (error) { console.error("Error updating event form:", error.message); res.status(400).json({ success: false, message: "Error updating event request", error: error.message }); @@ -69,7 +69,7 @@ router.put("/events/:id", async (req, res) => { router.get("/events/approved", async (req, res) => { try { const eventForms = await getApprovedEventForms(); - res.status(200).json({ success: true, message: "All approved event requests retrieved successfully", data: eventForms }); + res.status(200).json({ success: true, message: "All approved event requests retrieved successfully", data: eventForms.map(toPublicEvent)}); } catch (error) { console.error("Error getting all approved event requests:", error.message); res.status(400).json({ success: false, message: "Error getting all approved event requests", error: error.message }); diff --git a/src/data/transit.db b/src/data/transit.db index 7558ce44dc44a4db830c7ee237122cbf471efb68..545375407dca2cc00c0f2a476e9abafc70426f32 100644 GIT binary patch delta 11407 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 delta 2915 zcmXw)cW{(d7l-#fyYI0qG!MXbB)BgiUBF9YT{% zkQPPC3IqftfJzmR4v}gU0YUi2!H@3`zxV8knaMoQ`Q0*iGnwCJWnos9XF<6VbzCl2 z!1?|6CwgJ6`c|EV{bNX7_6594RBlv^w^ukASd8JAsA*`aoK>oE6mQQC`f{$R@>X>C znTn1#=8nr__FLwOxoGyA6{qGZ8Igb-d6TixwF|$pAei{z69IW?_Yhf+0Hh8+m`902FGf_X+ z**YAId!lwt@c?;Dvdf$`Tg(iT9Ilwq(c~o0-xRQ&TfzLrcL&V>%wNv4j+lk(s(4Ly zYbVo_cdf%fsDaBoH~%pYO_8}_u9$P?xH;^6h+QVnA&@#`kub6uj+I9xGvE9^iI81=js)Dk)EZe>U5o|N9ch% zS$EeRb!**J*V8q0ybhJsC3Un0^;|tw57b?CU0qP8)KPU%?N-~=MzvZkQFGM{m8mAE zQEI5_tCCa~)lMa<#wtNoS8*yv1(a90=x_Ry?$Ir}LTBk19in{NL0c)8R?s4vMN=t* z(kO)nQy=O-bW-$K%OlLkMoWy(pIFWgu@I&VB zU{7#^&vg&S^Y|Os<9PfP(|9ZbOlAH8IF@-Aa18Sf*rR#dZF>~s7LIg>ocLxerttDj zzz>)=07o!?CLGQz1RTb^4){Lv8sSjpRlp(4D}aNUmjMScF98l@UIZM#ya3prc^?usicRz;4W=fL)nK2)i&31HQ#90PO6BkmWiA*olL0!G4qP`35`kco47y^8nfH zIsO{k@wgwbEps2)Z*ZKCZFt;E_&ReBU~A@X!dA?$2wO74go(^uge{mm0h=>JI{=%p zY$tq;`6Xae<~G7tnO^`lVSY~7IKm({V&)Mxj4+4|m|F?!M;OF<%q@h$2!oiw+zeQk zxryvL{O*ldo5#>+fVEgQ0M=yY0@h%zCwzsuj<7m&Enqd~8nCPK`PEp3$5nupnJWP+ zF;@V_Ge0G)$XpH>$6Q7j%Ul|3zs$#%Vg(+Tgb2&CECwvc%mFORTtrxgxezdhxd7~! z_}qMak;i#pm*#OUmf~>^;0w&zfF+r;z%IedpV$G$$7neH2yD&cOjJD10HnzO|8zn> z2h#wfnNz{`@xE;I@|Xp7l-sgG;6b)r$jeuME6{5np8B1jYY z1yV)s!dQ_zFvi&%a!%hKEr)K6l9ijWBZ~!6L~g(bB0tjzkwO?Qavg?=T%-3zuEJ1} zD=ms|Uwa8c0N+e7zMRrl5 z$WCY>vIClnY^P=-U&3o5+o-9?7x1dc=hUQFf{jH&dDKWOFW69ID>W#VV11D-RIgZq zL6OanAhL<-%IDcwM}p6ww#WvkC6Ws@Mb=Xdk#+Qn$XWs& zkrhx$FNmQkF@(%4wJX6efkETIY_i=n(o4wMsFL}k4m=gXZNw2VXxAV!YQ ze@TLQ@S+5BOG_{ZN{P&d7er=3N!jvA3E{^9S@;Ny1T(b+GeC(WOQ0J!pY!?Faz8Xrh`+rmd`h7PlDx%bJxb+%c8nky`7Hv VukCJ_J$jjQRinL&ldKfq{{cgL>E8eV diff --git a/src/utils/EventFormsUtils.js b/src/utils/EventFormsUtils.js index ea719c45..bc08097e 100644 --- a/src/utils/EventFormsUtils.js +++ b/src/utils/EventFormsUtils.js @@ -4,7 +4,7 @@ import path from "path"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); -const dbPath = path.join(__dirname, "..", "data", "event_forms.db"); +const dbPath = path.join(__dirname, "..", "data", "transit.db"); const ALLOWED_EVENT_TYPES = ['temporary', 'permanent']; const ALLOWED_APPROVAL_STATUSES = ['pending', 'approved', 'rejected']; @@ -65,21 +65,26 @@ function createEventForm({ netid, name, eventType, startDate = null, endDate = n }); // Prepare the query - const query = `INSERT INTO event_forms (netid, event_type, ${eventForm.startDate ? "start_date, " : ""}${eventForm.endDate ? "end_date, " : ""}organization_name, location, approval_status${eventForm.about ? ', about' : ''}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?${eventForm.about ? ', ?' : ''})`; - const values = [eventForm.netid, eventForm.eventType, eventForm.startDate ? eventForm.startDate : null, eventForm.endDate, eventForm.organizationName, eventForm.location, eventForm.approvalStatus, eventForm.about ? eventForm.about : null]; + const query = `INSERT INTO event_forms (name, netid, event_type, start_date, end_date, organization_name, location, approval_status, about) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`; + const values = [eventForm.name, eventForm.netid, eventForm.eventType, eventForm.startDate, eventForm.endDate, eventForm.organizationName, eventForm.location, eventForm.approvalStatus, eventForm.about]; // Insert the event form into the database db.run(query, values, function (err) { if (err) { + db.close(); console.error(err.message); return reject(err); } - resolve(eventForm); - }); - // Close the database - db.close((err) => { - if (err) console.error(err.message); + // Get the inserted event form + db.get(`SELECT * FROM event_forms WHERE id = ?`, [this.lastID], (err, row) => { + db.close(); + if (err) { + console.error(err.message); + return reject(err); + } + return resolve(row); + }); }); }); } @@ -102,16 +107,12 @@ function getAllEventForms() { // Prepare the query const query = `SELECT * FROM event_forms`; db.all(query, (err, rows) => { + db.close(); if (err) { console.error(err.message); return reject(err); } - resolve(rows); - }); - - // Close the database - db.close((err) => { - if (err) console.error(err.message); + return resolve(rows); }); }); } @@ -121,21 +122,15 @@ function getAllEventForms() { * * Allowed approval statuses are: 'pending', 'approved', 'rejected'. * - * @param {Object} payload - The payload containing the id and approval status of the event form to update. - * @param {string} payload.id - The id of the event form to update. - * @param {string} payload.approvalStatus - The approval status to update the event form to. + * @param integer id - The id of the event form to update. + * @param {Object} approvalStatus - The approval status to update the event form to. * @returns {Promise} - The updated event form. * @throws {Error} - If the event form is invalid or the approval status is invalid. * @throws {Error} - If the event form is not found. */ -function updateEventForm(payload) { - const { id, approvalStatus } = payload; - +function updateEventForm({ id, approvalStatus }) { // Safety checks - make sure the event form is valid - if (!id || !approvalStatus) { - throw new Error("Invalid event form — id and approval status are required"); - } - + if (!id || !approvalStatus) throw new Error("Invalid event form — id and approval status are required"); // Ensures approval status is valid if (!ALLOWED_APPROVAL_STATUSES.includes(approvalStatus)) { throw new Error('Invalid event form — approval status invalid'); @@ -156,15 +151,26 @@ function updateEventForm(payload) { // Update the event form in the database db.run(query, values, function (err) { if (err) { + db.close(); console.error(err.message); return reject(err); } - resolve(eventForm); - }); - // Close the database - db.close((err) => { - if (err) console.error(err.message); + // Checks if there were no updates to the event form (in which case, there was an error) + if (this.changes === 0) { + db.close(); + return reject(new Error("Event form not found")); + } + + // Get the updated event form + db.get(`SELECT * FROM event_forms WHERE id = ?`, [id], (err, row) => { + db.close(); + if (err) { + console.error(err.message); + return reject(err); + } + return resolve(row); + }); }); }); } @@ -201,4 +207,24 @@ function getApprovedEventForms() { }); } -export { createEventForm, getAllEventForms, updateEventForm, getApprovedEventForms }; \ No newline at end of file +/** + * Converts an event form to a public event. + * + * @param {Object} eventForm - The event form to convert. + * @returns {Object} - The public event. + */ +function toPublicEvent({ name, netid, eventType, startDate, endDate, organizationName, about, location, approvalStatus }) { + return { + name, + netid, + eventType, + startDate, + endDate, + organizationName, + about, + location, + approvalStatus, + } +} + +export { createEventForm, getAllEventForms, updateEventForm, getApprovedEventForms, toPublicEvent }; \ No newline at end of file From f13f9c572bb8341408529eac02edcbeacd647f66 Mon Sep 17 00:00:00 2001 From: Chimdi Ejiogu Date: Sat, 27 Dec 2025 23:07:57 -0500 Subject: [PATCH 28/31] Remove redundant notifications across clients --- src/controllers/EventFormsController.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/controllers/EventFormsController.js b/src/controllers/EventFormsController.js index d2ac75e9..8c52dbb7 100644 --- a/src/controllers/EventFormsController.js +++ b/src/controllers/EventFormsController.js @@ -50,15 +50,13 @@ router.put("/events/:id", async (req, res) => { if (approvalStatus === "approved") { // Send a notification to everyone (and the admin room) io.to("public").emit("eventForm:update", {message: "Event approved", event: toPublicEvent(eventForm)}); - io.to("admin").emit("eventForm:update", {message: "Event approved", event: eventForm}); } else { // Send a notification to the submitting user (and the admin room) that the event was rejected io.to(`netid:${eventForm.netid}`).emit("eventForm:update", {message: "Your event request has been rejected", event: toPublicEvent(eventForm)}); - io.to("admin").emit("eventForm:update", {message: "Event rejected", event: eventForm}); } // Return the event form to the admin client that requested the update - res.status(200).json({ success: true, message: "Event request updated successfully", data: toPublicEvent(eventForm)}); + res.status(200).json({ success: true, message: "Event request updated successfully", data: eventForm}); } catch (error) { console.error("Error updating event form:", error.message); res.status(400).json({ success: false, message: "Error updating event request", error: error.message }); From 66238a052ed50adc8ba5b8df02aa26e38e50d7f4 Mon Sep 17 00:00:00 2001 From: Chimdi Ejiogu Date: Sat, 27 Dec 2025 23:10:11 -0500 Subject: [PATCH 29/31] Bug fix: Ensure updated_at attribute actually updates --- src/utils/EventFormsUtils.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/EventFormsUtils.js b/src/utils/EventFormsUtils.js index bc08097e..58c77cb3 100644 --- a/src/utils/EventFormsUtils.js +++ b/src/utils/EventFormsUtils.js @@ -118,7 +118,7 @@ function getAllEventForms() { } /** - * Updates an event form in the database. + * Updates the approval status of a specified event form in the database. * * Allowed approval statuses are: 'pending', 'approved', 'rejected'. * @@ -145,7 +145,7 @@ function updateEventForm({ id, approvalStatus }) { }); // Prepare the query - const query = `UPDATE event_forms SET approval_status = ? WHERE id = ?`; + const query = `UPDATE event_forms SET approval_status = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`; const values = [approvalStatus, id]; // Update the event form in the database From 3424014ba5c9497046e3bda638ead7010f828c59 Mon Sep 17 00:00:00 2001 From: Chimdi Ejiogu Date: Sat, 27 Dec 2025 23:23:50 -0500 Subject: [PATCH 30/31] Refactor update_at logic to change timestamp for all updates to a given row in the event_forms table --- ...251227_2312_create_trigger_event_forms.sql | 8 ++++++++ src/data/transit.db | Bin 98304 -> 98304 bytes src/utils/EventFormsUtils.js | 2 +- 3 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 src/data/migrations/20251227_2312_create_trigger_event_forms.sql diff --git a/src/data/migrations/20251227_2312_create_trigger_event_forms.sql b/src/data/migrations/20251227_2312_create_trigger_event_forms.sql new file mode 100644 index 00000000..c8921ffc --- /dev/null +++ b/src/data/migrations/20251227_2312_create_trigger_event_forms.sql @@ -0,0 +1,8 @@ +CREATE TRIGGER trg_event_forms_updated_at +AFTER UPDATE ON event_forms +FOR EACH ROW +BEGIN + UPDATE event_forms + SET updated_at = CURRENT_TIMESTAMP + WHERE id = OLD.id; +END; \ No newline at end of file diff --git a/src/data/transit.db b/src/data/transit.db index 545375407dca2cc00c0f2a476e9abafc70426f32..6ad3c8afce1b4a68047b048fd0a5383cad707688 100644 GIT binary patch delta 723 zcma)3J8u&~7+e$i5-i^XDN-V=h>d`bShu_0dpP1@?2TiIoTJM{5fq$!zO`iquyZ~l z3ZmFZG&G6jhKd%7P(=41sOg~N2c$ttN5vTh*(ofgm@U4Y8O`@iuP5|+!g1yX0RSw< zP+}xwNP~MMO+fa;yeRGgQP2mwHxv5l;52#If3D1C3b*Ip)x)6GZ1Nxsnl9hxuR^!c z4qkTM*B!qWa^H=g@w8~Og2SL=SIT8(!|9q-Va4FntTUUzmLGptpOmsKe>KId zQnWbx)kp}#Zx0_Rf_MVtPx7ktQwk+TJc;HXeH1gEAcDIKx8g%BEG-e@@NNj%Xj!NY zkE~3k*69T8eePow(-nkQumROv9+`PcFORUAC%TuM=0-PtZErq3b-f20rlNkIU-FaFaZjCB_igxz|;MZX&NO+r${#&UJV(hhp}oH4b322&8a5hyiRpQ4UL)_)lJF?&y6!)zqkU%QTJeQ uO%bGXAb*#?$nWK6@*U|sT2_BaRGs08k>;xT1Ul1B4BsB0=;_ht-+uvDZO-Wc delta 232 zcmZo@U~6b#n;KNjx5aQ_MTm@?-xPOhmn K+#K1z`~v_#IYMaw diff --git a/src/utils/EventFormsUtils.js b/src/utils/EventFormsUtils.js index 58c77cb3..4b0e241f 100644 --- a/src/utils/EventFormsUtils.js +++ b/src/utils/EventFormsUtils.js @@ -145,7 +145,7 @@ function updateEventForm({ id, approvalStatus }) { }); // Prepare the query - const query = `UPDATE event_forms SET approval_status = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`; + const query = `UPDATE event_forms SET approval_status = ? WHERE id = ?`; const values = [approvalStatus, id]; // Update the event form in the database From d277bf6d5202e4aa684abdf01fe7c610cdf487c2 Mon Sep 17 00:00:00 2001 From: Chimdi Ejiogu Date: Sat, 27 Dec 2025 23:46:51 -0500 Subject: [PATCH 31/31] Minor bug fixes, minor updates to swagger --- src/controllers/EventFormsController.js | 2 +- src/swagger.json | 86 ++++++++++++++++++++++++- src/utils/EventFormsUtils.js | 3 +- 3 files changed, 85 insertions(+), 6 deletions(-) diff --git a/src/controllers/EventFormsController.js b/src/controllers/EventFormsController.js index 8c52dbb7..6d923756 100644 --- a/src/controllers/EventFormsController.js +++ b/src/controllers/EventFormsController.js @@ -37,7 +37,7 @@ router.get("/events/", async (req, res) => { // NOTE: id is the event form's id, stored as the primary key in the database router.put("/events/:id", async (req, res) => { try { - const { id } = req.params; + const { id } = req.params; // id is found in the url path const { approvalStatus } = req.body; // Initalize the io instance diff --git a/src/swagger.json b/src/swagger.json index 22457b7e..a4ef711b 100644 --- a/src/swagger.json +++ b/src/swagger.json @@ -219,6 +219,21 @@ "schema": { "type": "string" } + }, + { + "in": "body", + "name": "approvalStatus", + "description": "The approval status to update the event request to. Allowed approval statuses are: 'pending', 'approved', 'rejected'.", + "required": true, + "schema": { + "type": "string", + "enum": [ + "pending", + "approved", + "rejected" + ], + "example": "approved" + } } ], "responses": { @@ -238,7 +253,7 @@ "example": "Event request updated successfully" }, "data": { - "$ref": "#/components/schemas/EventRequest" + "$ref": "#/components/schemas/PrivateEventRequest" } } } @@ -297,7 +312,7 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/EventRequest" + "$ref": "#/components/schemas/PublicEventRequest" } } } @@ -1590,9 +1605,59 @@ } } }, - "EventRequest": { + "PublicEventRequest": { + "type": "object", + "properties": { + "netid": { + "type": "string", + "description": "The netid of the user submitting the event request.", + "example": "jdoe123" + }, + "name": { + "type": "string", + "description": "The name of the event.", + "example": "Event Name" + }, + "eventType": { + "type": "string", + "description": "The type of event.", + "example": "temporary" + }, + "startDate": { + "type": "string", + "description": "The start date of the event.", + "example": "2025-01-01 12:00:00" + }, + "endDate": { + "type": "string", + "description": "The end date of the event.", + "example": "2025-01-02 12:00:00" + }, + "organizationName": { + "type": "string", + "description": "The name of the organization hosting the event.", + "example": "Organization Name" + }, + "location": { + "type": "string", + "description": "The location of the event.", + "example": "Location Name" + }, + "about": { + "type": "string", + "description": "The about section of the event.", + "example": "About the event." + } + } + }, + "PrivateEventRequest": { "type": "object", "properties": { + "id": { + "type": "integer", + "description": "The id of the event request.", + "example": 1 + }, "netid": { "type": "string", "description": "The netid of the user submitting the event request.", @@ -1632,6 +1697,21 @@ "type": "string", "description": "The about section of the event.", "example": "About the event." + }, + "approvalStatus": { + "type": "string", + "description": "The approval status of the event request.", + "example": "pending" + }, + "createdAt": { + "type": "string", + "description": "The date and time the event request was created.", + "example": "2025-01-01 12:00:00" + }, + "updatedAt": { + "type": "string", + "description": "The date and time the event request was last updated.", + "example": "2025-01-01 12:00:00" } } } diff --git a/src/utils/EventFormsUtils.js b/src/utils/EventFormsUtils.js index 4b0e241f..68a362ae 100644 --- a/src/utils/EventFormsUtils.js +++ b/src/utils/EventFormsUtils.js @@ -213,7 +213,7 @@ function getApprovedEventForms() { * @param {Object} eventForm - The event form to convert. * @returns {Object} - The public event. */ -function toPublicEvent({ name, netid, eventType, startDate, endDate, organizationName, about, location, approvalStatus }) { +function toPublicEvent({ name, netid, eventType, startDate, endDate, organizationName, about, location }) { return { name, netid, @@ -223,7 +223,6 @@ function toPublicEvent({ name, netid, eventType, startDate, endDate, organizatio organizationName, about, location, - approvalStatus, } }