From 65db8214c3c612a00675242ebee146d368ab8050 Mon Sep 17 00:00:00 2001 From: Michelle Date: Sat, 22 Nov 2025 14:02:32 -0500 Subject: [PATCH 1/9] created filter by role endpoint handler --- package-lock.json | 35 +++---- src/app/volunteers/filter_by_role/route.ts | 20 ++++ src/lib/api/getRolesByFilter.ts | 106 +++++++++++++++++++++ src/lib/api/index.ts | 1 + 4 files changed, 141 insertions(+), 21 deletions(-) create mode 100644 src/app/volunteers/filter_by_role/route.ts create mode 100644 src/lib/api/getRolesByFilter.ts diff --git a/package-lock.json b/package-lock.json index 192080d..a86eb04 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1154,7 +1154,6 @@ "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", "dev": true, - "license": "ISC", "dependencies": { "minipass": "^7.0.4" }, @@ -3133,7 +3132,6 @@ "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", "dev": true, - "license": "BlueOak-1.0.0", "engines": { "node": ">=18" } @@ -5296,7 +5294,6 @@ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", "dev": true, - "license": "ISC", "engines": { "node": ">=16 || 14 >=14.17" } @@ -5306,7 +5303,6 @@ "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", "dev": true, - "license": "MIT", "dependencies": { "minipass": "^7.1.2" }, @@ -6539,17 +6535,16 @@ } }, "node_modules/supabase": { - "version": "2.54.11", - "resolved": "https://registry.npmjs.org/supabase/-/supabase-2.54.11.tgz", - "integrity": "sha512-KuDDVi1s2fhfun81LNPaCLpW4/LFsP3G2LKUQimzEoj64sP1DtZ/d97uw6GFYbNMPW9JC2Ruuei53qHInOwJLA==", + "version": "2.58.5", + "resolved": "https://registry.npmjs.org/supabase/-/supabase-2.58.5.tgz", + "integrity": "sha512-mYZSkUIePTdmwlHd26Pff8wpmjfre8gcuWzrc5QqhZgZvCXugVzAQQhcjaQisw5kusbPQWNIjUwcHYEKmejhPw==", "dev": true, "hasInstallScript": true, - "license": "MIT", "dependencies": { "bin-links": "^6.0.0", "https-proxy-agent": "^7.0.2", "node-fetch": "^3.3.2", - "tar": "7.5.1" + "tar": "7.5.2" }, "bin": { "supabase": "bin/supabase" @@ -6599,11 +6594,10 @@ } }, "node_modules/tar": { - "version": "7.5.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.1.tgz", - "integrity": "sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g==", + "version": "7.5.2", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.2.tgz", + "integrity": "sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg==", "dev": true, - "license": "ISC", "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", @@ -7346,7 +7340,6 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", "dev": true, - "license": "BlueOak-1.0.0", "engines": { "node": ">=18" } @@ -11501,15 +11494,15 @@ } }, "supabase": { - "version": "2.54.11", - "resolved": "https://registry.npmjs.org/supabase/-/supabase-2.54.11.tgz", - "integrity": "sha512-KuDDVi1s2fhfun81LNPaCLpW4/LFsP3G2LKUQimzEoj64sP1DtZ/d97uw6GFYbNMPW9JC2Ruuei53qHInOwJLA==", + "version": "2.58.5", + "resolved": "https://registry.npmjs.org/supabase/-/supabase-2.58.5.tgz", + "integrity": "sha512-mYZSkUIePTdmwlHd26Pff8wpmjfre8gcuWzrc5QqhZgZvCXugVzAQQhcjaQisw5kusbPQWNIjUwcHYEKmejhPw==", "dev": true, "requires": { "bin-links": "^6.0.0", "https-proxy-agent": "^7.0.2", "node-fetch": "^3.3.2", - "tar": "7.5.1" + "tar": "7.5.2" } }, "supports-color": { @@ -11537,9 +11530,9 @@ } }, "tar": { - "version": "7.5.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.1.tgz", - "integrity": "sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g==", + "version": "7.5.2", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.2.tgz", + "integrity": "sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg==", "dev": true, "requires": { "@isaacs/fs-minipass": "^4.0.0", diff --git a/src/app/volunteers/filter_by_role/route.ts b/src/app/volunteers/filter_by_role/route.ts new file mode 100644 index 0000000..f928f71 --- /dev/null +++ b/src/app/volunteers/filter_by_role/route.ts @@ -0,0 +1,20 @@ +import { NextRequest } from "next/server"; +import { getRolesByFilter } from "@/lib/api/index"; + +export async function GET(request: NextRequest) { + + + const response = await getRolesByFilter("OR", ["Role 1"]); + + if (response.status == 200) { + return new Response(JSON.stringify(response), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } else { + return new Response(JSON.stringify(response), { + status: response.status, + headers: { "Content-Type": "application/json" }, + }); + } +} diff --git a/src/lib/api/getRolesByFilter.ts b/src/lib/api/getRolesByFilter.ts new file mode 100644 index 0000000..0381955 --- /dev/null +++ b/src/lib/api/getRolesByFilter.ts @@ -0,0 +1,106 @@ +import { createClient } from "../client/supabase/server"; + +const ALLOWED_OPERATORS = ["OR", "AND"]; + +type Volunteer = { + id: number; + created_at: string; + updated_at: string; + email: string | null; + phone: string | null; + name_org: string; + notes: string | null; + opt_in_communication: boolean | null; + position: string | null; + pronouns: string | null; + pseudonym: string | null; +}; + +function areAllStrings(arr: unknown[]): arr is string[] { + return arr.every((item) => typeof item === "string"); +} + +export async function getRolesByFilter( + operator: string, + filters: string[] +) { + if ( + typeof operator !== "string" || + !ALLOWED_OPERATORS.includes(operator.toUpperCase()) + ) { + return { status: 400, error: "Operator is not AND or OR" }; + } + + if (!areAllStrings(filters)) { + return { + status: 400, + error: "Roles to filter by are not all strings", + }; + } + + const client = await createClient(); + const { data: allRows, error } = await client + .from("VolunteerRoles") + .select( + ` + Roles!inner (name), + Volunteers!inner (*) + ` + ) + .in("Roles.name", filters); + + if (error) { + console.error("Supabase error:", error.message); + console.error("Details:", error.details); + console.error("Hint:", error.hint); + + return { + status: 500, + error: `Failed to query Supabase database: ${error.message}`, + }; + } + + const volunteerRoleMap = new Map< + number, + { + row: Volunteer; + roleNames: Set; + } + >(); + + for (const row of allRows) { + const volunteerId = row.Volunteers.id; + const roleName = row.Roles.name; + + let volunteerData = volunteerRoleMap.get(volunteerId); + if (!volunteerData) { + volunteerData = { row: row.Volunteers, roleNames: new Set() }; + volunteerRoleMap.set(volunteerId, volunteerData); + } + + volunteerData.roleNames.add(roleName); + } + + const filteredVolunteers = []; + if (operator == "OR") { + for (const volunteer of volunteerRoleMap.values()) { + + filteredVolunteers.push({ + ...volunteer.row, + role_names: Array.from(volunteer.roleNames), + }); + } + } else { + for (const volunteer of volunteerRoleMap.values()) { + + if (volunteer.roleNames.size == filters.length) { + filteredVolunteers.push({ + ...volunteer.row, + roles: Array.from(volunteer.roleNames), + }); + } + } + } + + return { data: filteredVolunteers, status: 200 }; +} diff --git a/src/lib/api/index.ts b/src/lib/api/index.ts index 53e202a..6e7dd54 100644 --- a/src/lib/api/index.ts +++ b/src/lib/api/index.ts @@ -1,3 +1,4 @@ // Add any exports from your API files here export { getExample } from "./getExample"; +export { getRolesByFilter } from "./getRolesByFilter"; \ No newline at end of file From 6bc18e1b768e647714a921a762f68b30bb366d82 Mon Sep 17 00:00:00 2001 From: Michelle Date: Sat, 22 Nov 2025 15:21:36 -0500 Subject: [PATCH 2/9] made file name and function more specific for filtering volunteers by role --- src/app/volunteers/filter_by_role/route.ts | 37 ++++++++++++------- ...lesByFilter.ts => getVolunteersByRoles.ts} | 30 ++++++++------- src/lib/api/index.ts | 6 ++- tests/lib/api/getVolunteersByRoles.ts | 33 +++++++++++++++++ 4 files changed, 78 insertions(+), 28 deletions(-) rename src/lib/api/{getRolesByFilter.ts => getVolunteersByRoles.ts} (83%) create mode 100644 tests/lib/api/getVolunteersByRoles.ts diff --git a/src/app/volunteers/filter_by_role/route.ts b/src/app/volunteers/filter_by_role/route.ts index f928f71..904a78c 100644 --- a/src/app/volunteers/filter_by_role/route.ts +++ b/src/app/volunteers/filter_by_role/route.ts @@ -1,20 +1,31 @@ -import { NextRequest } from "next/server"; -import { getRolesByFilter } from "@/lib/api/index"; +import { NextRequest, NextResponse } from "next/server"; +import { getVolunteersByRoles, isAllStrings, isValidOperator } from "@/lib/api/index"; export async function GET(request: NextRequest) { - + const { searchParams } = new URL(request.url); + const operator = searchParams.get("operator"); + const rolesParam = searchParams.get("roles"); - const response = await getRolesByFilter("OR", ["Role 1"]); + if (!operator || !rolesParam) { + return NextResponse.json( + { error: "Missing operator or filters" }, + { status: 400 } + ); + } - if (response.status == 200) { - return new Response(JSON.stringify(response), { - status: 200, - headers: { "Content-Type": "application/json" }, - }); - } else { - return new Response(JSON.stringify(response), { + console.log(operator); + const rolesArray = rolesParam.split(","); + console.log(rolesArray); + if (!isAllStrings(rolesArray) || !isValidOperator(operator)) { + return NextResponse.json( + { error: "Malformed operator or filters" }, + { status: 400 } + ); + } + + const response = await getVolunteersByRoles(operator as "OR" | "AND", rolesArray); + + return NextResponse.json(response, { status: response.status, - headers: { "Content-Type": "application/json" }, }); - } } diff --git a/src/lib/api/getRolesByFilter.ts b/src/lib/api/getVolunteersByRoles.ts similarity index 83% rename from src/lib/api/getRolesByFilter.ts rename to src/lib/api/getVolunteersByRoles.ts index 0381955..3e6649b 100644 --- a/src/lib/api/getRolesByFilter.ts +++ b/src/lib/api/getVolunteersByRoles.ts @@ -1,7 +1,5 @@ import { createClient } from "../client/supabase/server"; -const ALLOWED_OPERATORS = ["OR", "AND"]; - type Volunteer = { id: number; created_at: string; @@ -16,22 +14,28 @@ type Volunteer = { pseudonym: string | null; }; -function areAllStrings(arr: unknown[]): arr is string[] { +type Operator = "OR" | "AND"; + +export function isAllStrings(arr: unknown[]): arr is string[] { return arr.every((item) => typeof item === "string"); } -export async function getRolesByFilter( - operator: string, +export function isValidOperator(operator: string) { + return ( + typeof operator === "string" && + ["OR", "AND"].includes(operator.toUpperCase()) + ); +} + +export async function getVolunteersByRoles( + operator: Operator, filters: string[] ) { - if ( - typeof operator !== "string" || - !ALLOWED_OPERATORS.includes(operator.toUpperCase()) - ) { + if (!isValidOperator(operator)) { return { status: 400, error: "Operator is not AND or OR" }; } - if (!areAllStrings(filters)) { + if (!isAllStrings(filters)) { return { status: 400, error: "Roles to filter by are not all strings", @@ -53,7 +57,7 @@ export async function getRolesByFilter( console.error("Supabase error:", error.message); console.error("Details:", error.details); console.error("Hint:", error.hint); - + return { status: 500, error: `Failed to query Supabase database: ${error.message}`, @@ -82,9 +86,8 @@ export async function getRolesByFilter( } const filteredVolunteers = []; - if (operator == "OR") { + if (operator.toUpperCase() == "OR") { for (const volunteer of volunteerRoleMap.values()) { - filteredVolunteers.push({ ...volunteer.row, role_names: Array.from(volunteer.roleNames), @@ -92,7 +95,6 @@ export async function getRolesByFilter( } } else { for (const volunteer of volunteerRoleMap.values()) { - if (volunteer.roleNames.size == filters.length) { filteredVolunteers.push({ ...volunteer.row, diff --git a/src/lib/api/index.ts b/src/lib/api/index.ts index 6e7dd54..a12822d 100644 --- a/src/lib/api/index.ts +++ b/src/lib/api/index.ts @@ -1,4 +1,8 @@ // Add any exports from your API files here export { getExample } from "./getExample"; -export { getRolesByFilter } from "./getRolesByFilter"; \ No newline at end of file +export { + getVolunteersByRoles, + isValidOperator, + isAllStrings, +} from "./getVolunteersByRoles"; diff --git a/tests/lib/api/getVolunteersByRoles.ts b/tests/lib/api/getVolunteersByRoles.ts new file mode 100644 index 0000000..cffc061 --- /dev/null +++ b/tests/lib/api/getVolunteersByRoles.ts @@ -0,0 +1,33 @@ +// Example test for getExample function +// This test is not meaningful as is, but serves as a template +// You should modify it to fit your actual implementation and testing needs + +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { getExample } from "@/lib/api/getExample"; +import { createClient } from "@/lib/client/supabase/server"; + +// Mock the Supabase client +vi.mock("@/lib/client/supabase/server", () => ({ + createClient: vi.fn(), +})); + +describe("getExample", () => { + const mockSelect = vi.fn(); + const mockFrom = vi.fn(() => ({ select: mockSelect })); + const mockClient = { from: mockFrom }; + + beforeEach(() => { + vi.clearAllMocks(); + // @ts-expect-error - Partial mock of SupabaseClient for testing + vi.mocked(createClient).mockResolvedValue(mockClient); + mockSelect.mockResolvedValue({ data: [{ id: 1, name: "Test Volunteer" }] }); + }); + + it("should fetch volunteers data successfully", async () => { + const result = await getExample("test"); + + expect(createClient).toHaveBeenCalled(); + expect(mockSelect).toHaveBeenCalled(); + expect(result).toEqual({ data: [{ id: 1, name: "Test Volunteer" }] }); + }); +}); From f20928c8efebd54c99cd5037d0e9d676ea205153 Mon Sep 17 00:00:00 2001 From: Michelle Date: Sat, 22 Nov 2025 19:05:51 -0500 Subject: [PATCH 3/9] Revert "made file name and function more specific for filtering volunteers by role" This reverts commit 6bc18e1b768e647714a921a762f68b30bb366d82. --- src/app/volunteers/filter_by_role/route.ts | 37 +++++++------------ ...lunteersByRoles.ts => getRolesByFilter.ts} | 30 +++++++-------- src/lib/api/index.ts | 6 +-- tests/lib/api/getVolunteersByRoles.ts | 33 ----------------- 4 files changed, 28 insertions(+), 78 deletions(-) rename src/lib/api/{getVolunteersByRoles.ts => getRolesByFilter.ts} (83%) delete mode 100644 tests/lib/api/getVolunteersByRoles.ts diff --git a/src/app/volunteers/filter_by_role/route.ts b/src/app/volunteers/filter_by_role/route.ts index 904a78c..f928f71 100644 --- a/src/app/volunteers/filter_by_role/route.ts +++ b/src/app/volunteers/filter_by_role/route.ts @@ -1,31 +1,20 @@ -import { NextRequest, NextResponse } from "next/server"; -import { getVolunteersByRoles, isAllStrings, isValidOperator } from "@/lib/api/index"; +import { NextRequest } from "next/server"; +import { getRolesByFilter } from "@/lib/api/index"; export async function GET(request: NextRequest) { - const { searchParams } = new URL(request.url); - const operator = searchParams.get("operator"); - const rolesParam = searchParams.get("roles"); + - if (!operator || !rolesParam) { - return NextResponse.json( - { error: "Missing operator or filters" }, - { status: 400 } - ); - } - - console.log(operator); - const rolesArray = rolesParam.split(","); - console.log(rolesArray); - if (!isAllStrings(rolesArray) || !isValidOperator(operator)) { - return NextResponse.json( - { error: "Malformed operator or filters" }, - { status: 400 } - ); - } + const response = await getRolesByFilter("OR", ["Role 1"]); - const response = await getVolunteersByRoles(operator as "OR" | "AND", rolesArray); - - return NextResponse.json(response, { + if (response.status == 200) { + return new Response(JSON.stringify(response), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } else { + return new Response(JSON.stringify(response), { status: response.status, + headers: { "Content-Type": "application/json" }, }); + } } diff --git a/src/lib/api/getVolunteersByRoles.ts b/src/lib/api/getRolesByFilter.ts similarity index 83% rename from src/lib/api/getVolunteersByRoles.ts rename to src/lib/api/getRolesByFilter.ts index 3e6649b..0381955 100644 --- a/src/lib/api/getVolunteersByRoles.ts +++ b/src/lib/api/getRolesByFilter.ts @@ -1,5 +1,7 @@ import { createClient } from "../client/supabase/server"; +const ALLOWED_OPERATORS = ["OR", "AND"]; + type Volunteer = { id: number; created_at: string; @@ -14,28 +16,22 @@ type Volunteer = { pseudonym: string | null; }; -type Operator = "OR" | "AND"; - -export function isAllStrings(arr: unknown[]): arr is string[] { +function areAllStrings(arr: unknown[]): arr is string[] { return arr.every((item) => typeof item === "string"); } -export function isValidOperator(operator: string) { - return ( - typeof operator === "string" && - ["OR", "AND"].includes(operator.toUpperCase()) - ); -} - -export async function getVolunteersByRoles( - operator: Operator, +export async function getRolesByFilter( + operator: string, filters: string[] ) { - if (!isValidOperator(operator)) { + if ( + typeof operator !== "string" || + !ALLOWED_OPERATORS.includes(operator.toUpperCase()) + ) { return { status: 400, error: "Operator is not AND or OR" }; } - if (!isAllStrings(filters)) { + if (!areAllStrings(filters)) { return { status: 400, error: "Roles to filter by are not all strings", @@ -57,7 +53,7 @@ export async function getVolunteersByRoles( console.error("Supabase error:", error.message); console.error("Details:", error.details); console.error("Hint:", error.hint); - + return { status: 500, error: `Failed to query Supabase database: ${error.message}`, @@ -86,8 +82,9 @@ export async function getVolunteersByRoles( } const filteredVolunteers = []; - if (operator.toUpperCase() == "OR") { + if (operator == "OR") { for (const volunteer of volunteerRoleMap.values()) { + filteredVolunteers.push({ ...volunteer.row, role_names: Array.from(volunteer.roleNames), @@ -95,6 +92,7 @@ export async function getVolunteersByRoles( } } else { for (const volunteer of volunteerRoleMap.values()) { + if (volunteer.roleNames.size == filters.length) { filteredVolunteers.push({ ...volunteer.row, diff --git a/src/lib/api/index.ts b/src/lib/api/index.ts index a12822d..6e7dd54 100644 --- a/src/lib/api/index.ts +++ b/src/lib/api/index.ts @@ -1,8 +1,4 @@ // Add any exports from your API files here export { getExample } from "./getExample"; -export { - getVolunteersByRoles, - isValidOperator, - isAllStrings, -} from "./getVolunteersByRoles"; +export { getRolesByFilter } from "./getRolesByFilter"; \ No newline at end of file diff --git a/tests/lib/api/getVolunteersByRoles.ts b/tests/lib/api/getVolunteersByRoles.ts deleted file mode 100644 index cffc061..0000000 --- a/tests/lib/api/getVolunteersByRoles.ts +++ /dev/null @@ -1,33 +0,0 @@ -// Example test for getExample function -// This test is not meaningful as is, but serves as a template -// You should modify it to fit your actual implementation and testing needs - -import { describe, it, expect, vi, beforeEach } from "vitest"; -import { getExample } from "@/lib/api/getExample"; -import { createClient } from "@/lib/client/supabase/server"; - -// Mock the Supabase client -vi.mock("@/lib/client/supabase/server", () => ({ - createClient: vi.fn(), -})); - -describe("getExample", () => { - const mockSelect = vi.fn(); - const mockFrom = vi.fn(() => ({ select: mockSelect })); - const mockClient = { from: mockFrom }; - - beforeEach(() => { - vi.clearAllMocks(); - // @ts-expect-error - Partial mock of SupabaseClient for testing - vi.mocked(createClient).mockResolvedValue(mockClient); - mockSelect.mockResolvedValue({ data: [{ id: 1, name: "Test Volunteer" }] }); - }); - - it("should fetch volunteers data successfully", async () => { - const result = await getExample("test"); - - expect(createClient).toHaveBeenCalled(); - expect(mockSelect).toHaveBeenCalled(); - expect(result).toEqual({ data: [{ id: 1, name: "Test Volunteer" }] }); - }); -}); From 0dc2eca7cb6c0fffb5a3a2863d351139ebe117d2 Mon Sep 17 00:00:00 2001 From: Michelle Date: Sat, 22 Nov 2025 19:21:28 -0500 Subject: [PATCH 4/9] renamed filter file to something more specific --- src/app/volunteers/filter_by_role/route.ts | 44 ++++++--- src/lib/api/getVolunteersByRoles.ts | 108 +++++++++++++++++++++ src/lib/client/supabase/index.ts | 5 + 3 files changed, 144 insertions(+), 13 deletions(-) create mode 100644 src/lib/api/getVolunteersByRoles.ts diff --git a/src/app/volunteers/filter_by_role/route.ts b/src/app/volunteers/filter_by_role/route.ts index f928f71..1ea38a6 100644 --- a/src/app/volunteers/filter_by_role/route.ts +++ b/src/app/volunteers/filter_by_role/route.ts @@ -1,20 +1,38 @@ -import { NextRequest } from "next/server"; -import { getRolesByFilter } from "@/lib/api/index"; +import { NextRequest, NextResponse } from "next/server"; +import { + getVolunteersByRoles, + isAllStrings, + isValidOperator, +} from "@/lib/client/supabase"; export async function GET(request: NextRequest) { - + const { searchParams } = request.nextUrl; + const operator = searchParams.get("operator"); + const roleParams = searchParams.get("roles"); - const response = await getRolesByFilter("OR", ["Role 1"]); + if (!operator || !roleParams) { + return NextResponse.json({ + status: 400, + error: "Missing operator or role filter values", + }); + } - if (response.status == 200) { - return new Response(JSON.stringify(response), { - status: 200, - headers: { "Content-Type": "application/json" }, - }); - } else { - return new Response(JSON.stringify(response), { + const roles = roleParams.split(","); + + if (!isValidOperator(operator)) { + return { status: 400, error: "Operator is not AND or OR" }; + } + + if (!isAllStrings(roles)) { + return { + status: 400, + error: "Roles to filter by are not all strings", + }; + } + + const response = await getVolunteersByRoles(operator as "OR" | "AND", roles); + + return NextResponse.json(response, { status: response.status, - headers: { "Content-Type": "application/json" }, }); - } } diff --git a/src/lib/api/getVolunteersByRoles.ts b/src/lib/api/getVolunteersByRoles.ts new file mode 100644 index 0000000..d9a0785 --- /dev/null +++ b/src/lib/api/getVolunteersByRoles.ts @@ -0,0 +1,108 @@ +import { createClient } from "../client/supabase/server"; + +type Volunteer = { + id: number; + created_at: string; + updated_at: string; + email: string | null; + phone: string | null; + name_org: string; + notes: string | null; + opt_in_communication: boolean | null; + position: string | null; + pronouns: string | null; + pseudonym: string | null; +}; + +type Operator = "OR" | "AND"; + +export function isAllStrings(arr: unknown[]): arr is string[] { + return arr.every((item) => typeof item === "string"); +} + +export function isValidOperator(operator: string) { + return ( + typeof operator === "string" && + ["OR", "AND"].includes(operator.toUpperCase()) + ); +} + +export async function getVolunteersByRoles( + operator: Operator, + filters: string[] +) { + if (!isValidOperator(operator)) { + return { status: 400, error: "Operator is not AND or OR" }; + } + + if (!isAllStrings(filters)) { + return { + status: 400, + error: "Roles to filter by are not all strings", + }; + } + + const client = await createClient(); + const { data: allRows, error } = await client + .from("VolunteerRoles") + .select( + ` + Roles!inner (name), + Volunteers!inner (*) + ` + ) + .in("Roles.name", filters); + + if (error) { + console.error("Supabase error:", error.message); + console.error("Details:", error.details); + console.error("Hint:", error.hint); + + return { + status: 500, + error: `Failed to query Supabase database: ${error.message}`, + }; + } + + const volunteerRoleMap = new Map< + number, + { + row: Volunteer; + roleNames: Set; + } + >(); + + for (const row of allRows) { + const volunteerId = row.Volunteers.id; + const roleName = row.Roles.name; + + let volunteerData = volunteerRoleMap.get(volunteerId); + if (!volunteerData) { + volunteerData = { row: row.Volunteers, roleNames: new Set() }; + volunteerRoleMap.set(volunteerId, volunteerData); + } + + volunteerData.roleNames.add(roleName); + } + + const filteredVolunteers = []; + if (operator.toUpperCase() == "OR") { + for (const volunteer of volunteerRoleMap.values()) { + filteredVolunteers.push({ + ...volunteer.row, + filtered_roles: Array.from(volunteer.roleNames), + }); + } + } else { + for (const volunteer of volunteerRoleMap.values()) { + if (volunteer.roleNames.size == filters.length) { + filteredVolunteers.push({ + ...volunteer.row, + filtered_roles: Array.from(volunteer.roleNames), + }); + } + } + } + + return { data: filteredVolunteers, status: 200 }; +} diff --git a/src/lib/client/supabase/index.ts b/src/lib/client/supabase/index.ts index 3e17ca8..c18a316 100644 --- a/src/lib/client/supabase/index.ts +++ b/src/lib/client/supabase/index.ts @@ -1,3 +1,8 @@ // Exports for supabase client export { createClient } from "./server"; +export { + getVolunteersByRoles, + isAllStrings, + isValidOperator, +} from "../../api/getVolunteersByRoles"; From ccc9f4a9133704b9bf788f5ac949436cfb308413 Mon Sep 17 00:00:00 2001 From: Michelle Date: Sat, 22 Nov 2025 19:21:50 -0500 Subject: [PATCH 5/9] added type: module so that unit tests can run w/o error --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 84ee71a..bc94da7 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,7 @@ "name": "trcc", "version": "0.1.0", "private": true, + "type": "module", "scripts": { "dev": "next dev --turbopack", "build": "next build --turbopack", From ca064168f2a41fe6568d596716945e77609a85d8 Mon Sep 17 00:00:00 2001 From: Michelle Date: Sat, 22 Nov 2025 19:21:59 -0500 Subject: [PATCH 6/9] creating test file --- tests/lib/api/getVolunteersByRoles.ts | 200 ++++++++++++++++++++++++++ 1 file changed, 200 insertions(+) create mode 100644 tests/lib/api/getVolunteersByRoles.ts diff --git a/tests/lib/api/getVolunteersByRoles.ts b/tests/lib/api/getVolunteersByRoles.ts new file mode 100644 index 0000000..14ecb0a --- /dev/null +++ b/tests/lib/api/getVolunteersByRoles.ts @@ -0,0 +1,200 @@ +// Example test for getExample function +// This test is not meaningful as is, but serves as a template +// You should modify it to fit your actual implementation and testing needs + +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { + getVolunteersByRoles, + isAllStrings, + isValidOperator, +} from "@/lib/api/index"; +import { createClient } from "@/lib/client/supabase/server"; + +const volunteerTestData = [ + { + name_org: "Volunteer1", + pseudonym: "V1", + pronouns: "He/him", + email: "v1@mail.com", + phone: "123 456 7890", + position: "member", + opt_in_communication: true, + notes: "Notes for volunteer 1", + created_at: "2025-11-10T01:26:20.619465+00:00", + updated_at: "2025-11-10T01:26:20.619465+00:00", + id: 1, + }, + { + name_org: "Volunteer2", + pseudonym: "V2", + pronouns: "She/her", + email: "v2@mail.com", + phone: "098 765 4321", + position: "member", + opt_in_communication: false, + notes: "Notes for volunteer 2", + created_at: "2025-11-10T01:26:20.619465+00:00", + updated_at: "2025-11-10T01:26:20.619465+00:00", + id: 2, + }, + { + name_org: "Volunteer3", + pseudonym: "V3", + pronouns: null, + email: null, + phone: "123 456 7890", + position: null, + opt_in_communication: true, + notes: null, + created_at: "2025-11-10T01:26:20.619465+00:00", + updated_at: "2025-11-10T01:26:20.619465+00:00", + id: 3, + }, + { + name_org: "Jiji", + pseudonym: null, + pronouns: null, + email: null, + phone: null, + position: null, + opt_in_communication: true, + notes: null, + created_at: "2025-11-22T22:52:18.24417+00:00", + updated_at: "2025-11-22T22:52:18.24417+00:00", + id: 23, + }, +]; + +const volunteerRolesTestData = [ + { + created_at: "2025-11-10T01:27:18.139166+00:00", + role_id: 1, + volunteer_id: 1, + }, + { + created_at: "2025-11-10T01:27:18.139166+00:00", + role_id: 1, + volunteer_id: 3, + }, + { + created_at: "2025-11-10T01:27:18.139166+00:00", + role_id: 2, + volunteer_id: 2, + }, + { + created_at: "2025-11-10T01:27:18.139166+00:00", + role_id: 2, + volunteer_id: 3, + }, +]; + +const RolesTestData = [ + { + name: "Role 1", + type: "current", + is_active: true, + created_at: "2025-11-10T01:26:45.632811+00:00", + id: 1, + }, + { + name: "Role 2", + type: "", + is_active: false, + created_at: "2025-11-10T01:26:45.632811+00:00", + id: 2, + }, +]; + +const JoinedData = [ + { + Roles: { name: "Role 1" }, + Volunteers: { + id: 1, + email: "v1@mail.com", + notes: "Notes for volunteer 1", + phone: "123 456 7890", + name_org: "Volunteer1", + position: "member", + pronouns: "He/him", + pseudonym: "V1", + created_at: "2025-11-10T01:26:20.619465+00:00", + updated_at: "2025-11-10T01:26:20.619465+00:00", + opt_in_communication: true, + }, + }, + { + Roles: { name: "Role 1" }, + Volunteers: { + id: 3, + email: null, + notes: null, + phone: "123 456 7890", + name_org: "Volunteer3", + position: null, + pronouns: null, + pseudonym: "V3", + created_at: "2025-11-10T01:26:20.619465+00:00", + updated_at: "2025-11-10T01:26:20.619465+00:00", + opt_in_communication: true, + }, + }, + { + Roles: { name: "Role 2" }, + Volunteers: { + id: 2, + email: "v2@mail.com", + notes: "Notes for volunteer 2", + phone: "098 765 4321", + name_org: "Volunteer2", + position: "member", + pronouns: "She/her", + pseudonym: "V2", + created_at: "2025-11-10T01:26:20.619465+00:00", + updated_at: "2025-11-10T01:26:20.619465+00:00", + opt_in_communication: false, + }, + }, + { + Roles: { name: "Role 2" }, + Volunteers: { + id: 3, + email: null, + notes: null, + phone: "123 456 7890", + name_org: "Volunteer3", + position: null, + pronouns: null, + pseudonym: "V3", + created_at: "2025-11-10T01:26:20.619465+00:00", + updated_at: "2025-11-10T01:26:20.619465+00:00", + opt_in_communication: true, + }, + }, +]; + +// Mock the Supabase client +vi.mock("@/lib/client/supabase/server", () => ({ + createClient: vi.fn(), +})); + +describe("getVolunteersByRoles", () => { + const mockIn = vi.fn(); + const mockSelect = vi.fn(() => ({ in: mockIn })); + const mockFrom = vi.fn(() => ({ select: mockSelect })); + const mockClient = { from: mockFrom }; + + beforeEach(() => { + vi.clearAllMocks(); + // @ts-expect-error - Partial mock of SupabaseClient for testing + vi.mocked(createClient).mockResolvedValue(mockClient); + mockIn.mockResolvedValue({ data: JoinedData, error: null }); + }); + + it("returns error response for an invalid operator", async () => { + const result = await getVolunteersByRoles("INVALID", ["Role 1"]); + + expect(result.status).toBe(400); + expect(mockSelect).toHaveBeenCalled(); + expect(result).toEqual({ data: [{ id: 1, name: "Test Volunteer" }] }); + }); +}); From 5266bf9304eb86390757978f144905b3b8c33da4 Mon Sep 17 00:00:00 2001 From: Michelle Date: Sat, 22 Nov 2025 19:22:50 -0500 Subject: [PATCH 7/9] moved filter exports to correct index file --- src/lib/api/index.ts | 6 +++++- src/lib/client/supabase/index.ts | 7 +------ tests/lib/api/getVolunteersByRoles.ts | 2 +- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/lib/api/index.ts b/src/lib/api/index.ts index 6e7dd54..2b997d4 100644 --- a/src/lib/api/index.ts +++ b/src/lib/api/index.ts @@ -1,4 +1,8 @@ // Add any exports from your API files here export { getExample } from "./getExample"; -export { getRolesByFilter } from "./getRolesByFilter"; \ No newline at end of file +export { + getVolunteersByRoles, + isAllStrings, + isValidOperator, +} from "./getVolunteersByRoles"; diff --git a/src/lib/client/supabase/index.ts b/src/lib/client/supabase/index.ts index c18a316..7228a59 100644 --- a/src/lib/client/supabase/index.ts +++ b/src/lib/client/supabase/index.ts @@ -1,8 +1,3 @@ // Exports for supabase client -export { createClient } from "./server"; -export { - getVolunteersByRoles, - isAllStrings, - isValidOperator, -} from "../../api/getVolunteersByRoles"; +export { createClient } from "./server"; \ No newline at end of file diff --git a/tests/lib/api/getVolunteersByRoles.ts b/tests/lib/api/getVolunteersByRoles.ts index 14ecb0a..99941d2 100644 --- a/tests/lib/api/getVolunteersByRoles.ts +++ b/tests/lib/api/getVolunteersByRoles.ts @@ -7,7 +7,7 @@ import { getVolunteersByRoles, isAllStrings, isValidOperator, -} from "@/lib/api/index"; +} from "@/lib/client/supabase/index"; import { createClient } from "@/lib/client/supabase/server"; const volunteerTestData = [ From cd9bf447008c8a518b8a995dbde8cf9d337ec1c8 Mon Sep 17 00:00:00 2001 From: Michelle Date: Sat, 22 Nov 2025 19:55:06 -0500 Subject: [PATCH 8/9] linted and formatted --- src/app/volunteers/filter_by_role/route.ts | 2 +- src/lib/api/getRolesByFilter.ts | 9 +- src/lib/api/getVolunteersByRoles.ts | 6 +- src/lib/client/supabase/index.ts | 2 +- tests/lib/api/getVolunteersByRoles.test.ts | 103 +++++++++++ tests/lib/api/getVolunteersByRoles.ts | 200 --------------------- 6 files changed, 110 insertions(+), 212 deletions(-) create mode 100644 tests/lib/api/getVolunteersByRoles.test.ts delete mode 100644 tests/lib/api/getVolunteersByRoles.ts diff --git a/src/app/volunteers/filter_by_role/route.ts b/src/app/volunteers/filter_by_role/route.ts index 1ea38a6..8ab5341 100644 --- a/src/app/volunteers/filter_by_role/route.ts +++ b/src/app/volunteers/filter_by_role/route.ts @@ -3,7 +3,7 @@ import { getVolunteersByRoles, isAllStrings, isValidOperator, -} from "@/lib/client/supabase"; +} from "@/lib/api/getVolunteersByRoles"; export async function GET(request: NextRequest) { const { searchParams } = request.nextUrl; diff --git a/src/lib/api/getRolesByFilter.ts b/src/lib/api/getRolesByFilter.ts index 0381955..6777643 100644 --- a/src/lib/api/getRolesByFilter.ts +++ b/src/lib/api/getRolesByFilter.ts @@ -20,10 +20,7 @@ function areAllStrings(arr: unknown[]): arr is string[] { return arr.every((item) => typeof item === "string"); } -export async function getRolesByFilter( - operator: string, - filters: string[] -) { +export async function getRolesByFilter(operator: string, filters: string[]) { if ( typeof operator !== "string" || !ALLOWED_OPERATORS.includes(operator.toUpperCase()) @@ -53,7 +50,7 @@ export async function getRolesByFilter( console.error("Supabase error:", error.message); console.error("Details:", error.details); console.error("Hint:", error.hint); - + return { status: 500, error: `Failed to query Supabase database: ${error.message}`, @@ -84,7 +81,6 @@ export async function getRolesByFilter( const filteredVolunteers = []; if (operator == "OR") { for (const volunteer of volunteerRoleMap.values()) { - filteredVolunteers.push({ ...volunteer.row, role_names: Array.from(volunteer.roleNames), @@ -92,7 +88,6 @@ export async function getRolesByFilter( } } else { for (const volunteer of volunteerRoleMap.values()) { - if (volunteer.roleNames.size == filters.length) { filteredVolunteers.push({ ...volunteer.row, diff --git a/src/lib/api/getVolunteersByRoles.ts b/src/lib/api/getVolunteersByRoles.ts index d9a0785..877bad0 100644 --- a/src/lib/api/getVolunteersByRoles.ts +++ b/src/lib/api/getVolunteersByRoles.ts @@ -54,9 +54,9 @@ export async function getVolunteersByRoles( .in("Roles.name", filters); if (error) { - console.error("Supabase error:", error.message); - console.error("Details:", error.details); - console.error("Hint:", error.hint); + // console.error("Supabase error:", error.message); + // console.error("Details:", error.details); + // console.error("Hint:", error.hint); return { status: 500, diff --git a/src/lib/client/supabase/index.ts b/src/lib/client/supabase/index.ts index 7228a59..3e17ca8 100644 --- a/src/lib/client/supabase/index.ts +++ b/src/lib/client/supabase/index.ts @@ -1,3 +1,3 @@ // Exports for supabase client -export { createClient } from "./server"; \ No newline at end of file +export { createClient } from "./server"; diff --git a/tests/lib/api/getVolunteersByRoles.test.ts b/tests/lib/api/getVolunteersByRoles.test.ts new file mode 100644 index 0000000..b670fbb --- /dev/null +++ b/tests/lib/api/getVolunteersByRoles.test.ts @@ -0,0 +1,103 @@ +// Example test for getExample function +// This test is not meaningful as is, but serves as a template +// You should modify it to fit your actual implementation and testing needs + +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { getVolunteersByRoles } from "@/lib/api/getVolunteersByRoles"; +import { createClient } from "@/lib/client/supabase/server"; + +const Roles1 = [ + { + Roles: { name: "Role 1" }, + Volunteers: { + id: 1, + }, + }, + { + Roles: { name: "Role 1" }, + Volunteers: { + id: 3, + }, + }, +]; + +const Roles2 = [ + { + Roles: { name: "Role 2" }, + Volunteers: { + id: 2, + }, + }, + { + Roles: { name: "Role 2" }, + Volunteers: { + id: 3, + }, + }, +]; + +// Mock the Supabase client +vi.mock("@/lib/client/supabase/server", () => ({ + createClient: vi.fn(), +})); + +describe("getVolunteersByRoles", () => { + const mockIn = vi.fn(); + const mockSelect = vi.fn(() => ({ in: mockIn })); + const mockFrom = vi.fn(() => ({ select: mockSelect })); + const mockClient = { from: mockFrom }; + + beforeEach(() => { + vi.clearAllMocks(); + // @ts-expect-error - Partial mock of SupabaseClient for testing + vi.mocked(createClient).mockResolvedValue(mockClient); + }); + + it("returns error response for an invalid operator", async () => { + const result = await getVolunteersByRoles("INVALID", ["Role 1"]); + expect(result.status).toBe(400); + expect(result).toEqual({ status: 400, error: "Operator is not AND or OR" }); + }); + + it("returns error response if the filters array is malformed", async () => { + const result = await getVolunteersByRoles("OR", [1]); + expect(result.status).toBe(400); + expect(result).toEqual({ + status: 400, + error: "Roles to filter by are not all strings", + }); + }); + + it("returns all volunteers with Role 1 or Role 2", async () => { + mockIn.mockResolvedValueOnce({ data: [...Roles1, ...Roles2], error: null }); + + const result = await getVolunteersByRoles("OR", ["Role 1", "Role 2"]); + expect(result.status).toBe(200); + expect(result).toHaveProperty("data"); + + const ids = result.data?.map((volunteer) => volunteer.id).sort(); + expect(ids).toEqual([1, 2, 3]); + }); + + it("returns all volunteers with Role 1", async () => { + mockIn.mockResolvedValueOnce({ data: Roles1, error: null }); + + const result = await getVolunteersByRoles("OR", ["Role 1"]); + expect(result.status).toBe(200); + expect(result).toHaveProperty("data"); + + const ids = result.data?.map((volunteer) => volunteer.id).sort(); + expect(ids).toEqual([1, 3]); + }); + + it("returns all volunteers with Role 1 AND Role2", async () => { + mockIn.mockResolvedValueOnce({ data: [...Roles1, ...Roles2], error: null }); + + const result = await getVolunteersByRoles("AND", ["Role 1", "Role 2"]); + expect(result.status).toBe(200); + expect(result).toHaveProperty("data"); + + const ids = result.data?.map((volunteer) => volunteer.id).sort(); + expect(ids).toEqual([3]); + }); +}); diff --git a/tests/lib/api/getVolunteersByRoles.ts b/tests/lib/api/getVolunteersByRoles.ts deleted file mode 100644 index 99941d2..0000000 --- a/tests/lib/api/getVolunteersByRoles.ts +++ /dev/null @@ -1,200 +0,0 @@ -// Example test for getExample function -// This test is not meaningful as is, but serves as a template -// You should modify it to fit your actual implementation and testing needs - -import { describe, it, expect, vi, beforeEach } from "vitest"; -import { - getVolunteersByRoles, - isAllStrings, - isValidOperator, -} from "@/lib/client/supabase/index"; -import { createClient } from "@/lib/client/supabase/server"; - -const volunteerTestData = [ - { - name_org: "Volunteer1", - pseudonym: "V1", - pronouns: "He/him", - email: "v1@mail.com", - phone: "123 456 7890", - position: "member", - opt_in_communication: true, - notes: "Notes for volunteer 1", - created_at: "2025-11-10T01:26:20.619465+00:00", - updated_at: "2025-11-10T01:26:20.619465+00:00", - id: 1, - }, - { - name_org: "Volunteer2", - pseudonym: "V2", - pronouns: "She/her", - email: "v2@mail.com", - phone: "098 765 4321", - position: "member", - opt_in_communication: false, - notes: "Notes for volunteer 2", - created_at: "2025-11-10T01:26:20.619465+00:00", - updated_at: "2025-11-10T01:26:20.619465+00:00", - id: 2, - }, - { - name_org: "Volunteer3", - pseudonym: "V3", - pronouns: null, - email: null, - phone: "123 456 7890", - position: null, - opt_in_communication: true, - notes: null, - created_at: "2025-11-10T01:26:20.619465+00:00", - updated_at: "2025-11-10T01:26:20.619465+00:00", - id: 3, - }, - { - name_org: "Jiji", - pseudonym: null, - pronouns: null, - email: null, - phone: null, - position: null, - opt_in_communication: true, - notes: null, - created_at: "2025-11-22T22:52:18.24417+00:00", - updated_at: "2025-11-22T22:52:18.24417+00:00", - id: 23, - }, -]; - -const volunteerRolesTestData = [ - { - created_at: "2025-11-10T01:27:18.139166+00:00", - role_id: 1, - volunteer_id: 1, - }, - { - created_at: "2025-11-10T01:27:18.139166+00:00", - role_id: 1, - volunteer_id: 3, - }, - { - created_at: "2025-11-10T01:27:18.139166+00:00", - role_id: 2, - volunteer_id: 2, - }, - { - created_at: "2025-11-10T01:27:18.139166+00:00", - role_id: 2, - volunteer_id: 3, - }, -]; - -const RolesTestData = [ - { - name: "Role 1", - type: "current", - is_active: true, - created_at: "2025-11-10T01:26:45.632811+00:00", - id: 1, - }, - { - name: "Role 2", - type: "", - is_active: false, - created_at: "2025-11-10T01:26:45.632811+00:00", - id: 2, - }, -]; - -const JoinedData = [ - { - Roles: { name: "Role 1" }, - Volunteers: { - id: 1, - email: "v1@mail.com", - notes: "Notes for volunteer 1", - phone: "123 456 7890", - name_org: "Volunteer1", - position: "member", - pronouns: "He/him", - pseudonym: "V1", - created_at: "2025-11-10T01:26:20.619465+00:00", - updated_at: "2025-11-10T01:26:20.619465+00:00", - opt_in_communication: true, - }, - }, - { - Roles: { name: "Role 1" }, - Volunteers: { - id: 3, - email: null, - notes: null, - phone: "123 456 7890", - name_org: "Volunteer3", - position: null, - pronouns: null, - pseudonym: "V3", - created_at: "2025-11-10T01:26:20.619465+00:00", - updated_at: "2025-11-10T01:26:20.619465+00:00", - opt_in_communication: true, - }, - }, - { - Roles: { name: "Role 2" }, - Volunteers: { - id: 2, - email: "v2@mail.com", - notes: "Notes for volunteer 2", - phone: "098 765 4321", - name_org: "Volunteer2", - position: "member", - pronouns: "She/her", - pseudonym: "V2", - created_at: "2025-11-10T01:26:20.619465+00:00", - updated_at: "2025-11-10T01:26:20.619465+00:00", - opt_in_communication: false, - }, - }, - { - Roles: { name: "Role 2" }, - Volunteers: { - id: 3, - email: null, - notes: null, - phone: "123 456 7890", - name_org: "Volunteer3", - position: null, - pronouns: null, - pseudonym: "V3", - created_at: "2025-11-10T01:26:20.619465+00:00", - updated_at: "2025-11-10T01:26:20.619465+00:00", - opt_in_communication: true, - }, - }, -]; - -// Mock the Supabase client -vi.mock("@/lib/client/supabase/server", () => ({ - createClient: vi.fn(), -})); - -describe("getVolunteersByRoles", () => { - const mockIn = vi.fn(); - const mockSelect = vi.fn(() => ({ in: mockIn })); - const mockFrom = vi.fn(() => ({ select: mockSelect })); - const mockClient = { from: mockFrom }; - - beforeEach(() => { - vi.clearAllMocks(); - // @ts-expect-error - Partial mock of SupabaseClient for testing - vi.mocked(createClient).mockResolvedValue(mockClient); - mockIn.mockResolvedValue({ data: JoinedData, error: null }); - }); - - it("returns error response for an invalid operator", async () => { - const result = await getVolunteersByRoles("INVALID", ["Role 1"]); - - expect(result.status).toBe(400); - expect(mockSelect).toHaveBeenCalled(); - expect(result).toEqual({ data: [{ id: 1, name: "Test Volunteer" }] }); - }); -}); From f71c6fc70658acedc4fe8e129afefd5cc12ccad1 Mon Sep 17 00:00:00 2001 From: Michelle Date: Sun, 23 Nov 2025 15:49:59 -0500 Subject: [PATCH 9/9] deleted duplicate file --- src/lib/api/getRolesByFilter.ts | 101 -------------------------------- 1 file changed, 101 deletions(-) delete mode 100644 src/lib/api/getRolesByFilter.ts diff --git a/src/lib/api/getRolesByFilter.ts b/src/lib/api/getRolesByFilter.ts deleted file mode 100644 index 6777643..0000000 --- a/src/lib/api/getRolesByFilter.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { createClient } from "../client/supabase/server"; - -const ALLOWED_OPERATORS = ["OR", "AND"]; - -type Volunteer = { - id: number; - created_at: string; - updated_at: string; - email: string | null; - phone: string | null; - name_org: string; - notes: string | null; - opt_in_communication: boolean | null; - position: string | null; - pronouns: string | null; - pseudonym: string | null; -}; - -function areAllStrings(arr: unknown[]): arr is string[] { - return arr.every((item) => typeof item === "string"); -} - -export async function getRolesByFilter(operator: string, filters: string[]) { - if ( - typeof operator !== "string" || - !ALLOWED_OPERATORS.includes(operator.toUpperCase()) - ) { - return { status: 400, error: "Operator is not AND or OR" }; - } - - if (!areAllStrings(filters)) { - return { - status: 400, - error: "Roles to filter by are not all strings", - }; - } - - const client = await createClient(); - const { data: allRows, error } = await client - .from("VolunteerRoles") - .select( - ` - Roles!inner (name), - Volunteers!inner (*) - ` - ) - .in("Roles.name", filters); - - if (error) { - console.error("Supabase error:", error.message); - console.error("Details:", error.details); - console.error("Hint:", error.hint); - - return { - status: 500, - error: `Failed to query Supabase database: ${error.message}`, - }; - } - - const volunteerRoleMap = new Map< - number, - { - row: Volunteer; - roleNames: Set; - } - >(); - - for (const row of allRows) { - const volunteerId = row.Volunteers.id; - const roleName = row.Roles.name; - - let volunteerData = volunteerRoleMap.get(volunteerId); - if (!volunteerData) { - volunteerData = { row: row.Volunteers, roleNames: new Set() }; - volunteerRoleMap.set(volunteerId, volunteerData); - } - - volunteerData.roleNames.add(roleName); - } - - const filteredVolunteers = []; - if (operator == "OR") { - for (const volunteer of volunteerRoleMap.values()) { - filteredVolunteers.push({ - ...volunteer.row, - role_names: Array.from(volunteer.roleNames), - }); - } - } else { - for (const volunteer of volunteerRoleMap.values()) { - if (volunteer.roleNames.size == filters.length) { - filteredVolunteers.push({ - ...volunteer.row, - roles: Array.from(volunteer.roleNames), - }); - } - } - } - - return { data: filteredVolunteers, status: 200 }; -}