import { Router } from "express"; import type { Request } from "express"; import { graphqlClient } from "../lib/graphqlClient"; import { respondWithError } from "../lib/error"; import { gql } from "graphql-request"; import type { Work } from "../../shared/generated/graphql"; const router = Router(); interface GqlRequest extends Request { gql?: typeof graphqlClient; } // Define a type that includes fields we need for filtering interface WorkWithFilters extends Pick { tags?: { id: string; name: string }[]; authors?: { id: string; name: string }[]; } const WORKS_WITH_FILTERS_QUERY = gql` query WorksWithFilters($search: String, $limit: Int) { works(search: $search, limit: $limit) { id name language createdAt tags { id name } authors { id name } } } `; // GET /api/filter router.get("/", async (req: GqlRequest, res) => { try { const q = req.query.q as string | undefined; const language = req.query.language as string | undefined; const type = req.query.type as string | undefined; const yearStart = req.query.yearStart ? parseInt(req.query.yearStart as string) : undefined; const yearEnd = req.query.yearEnd ? parseInt(req.query.yearEnd as string) : undefined; const tags = req.query.tags ? (req.query.tags as string).split(",") : undefined; const authorIds = req.query.authorIds ? (req.query.authorIds as string).split(",") : undefined; // We ignore page param because we are doing client-side pagination for now, // returning all matching results (up to limit). // const page = req.query.page ? parseInt(req.query.page as string) : 1; const client = req.gql || graphqlClient; // Fetch a larger set to filter in memory. const variables = { search: q, limit: 1000, }; const response = await client.request<{ works: WorkWithFilters[] }>( WORKS_WITH_FILTERS_QUERY, variables ); let filteredWorks = response.works; // Filter by language if (language) { filteredWorks = filteredWorks.filter(w => w.language === language); } // Filter by type if (type) { filteredWorks = filteredWorks.filter(w => { const typeLower = type.toLowerCase(); const hasTag = w.tags?.some(t => t.name.toLowerCase().includes(typeLower)); return hasTag; }); } // Filter by year if (yearStart !== undefined || yearEnd !== undefined) { filteredWorks = filteredWorks.filter(w => { const year = new Date(w.createdAt).getFullYear(); if (yearStart !== undefined && year < yearStart) return false; if (yearEnd !== undefined && year > yearEnd) return false; return true; }); } // Filter by tags if (tags && tags.length > 0) { filteredWorks = filteredWorks.filter(w => { if (!w.tags) return false; const workTagIds = w.tags.map(t => t.id); return tags.every(tagId => workTagIds.includes(tagId)); }); } // Filter by authors if (authorIds && authorIds.length > 0) { filteredWorks = filteredWorks.filter(w => { if (!w.authors) return false; const workAuthorIds = w.authors.map(a => a.id); return authorIds.some(authorId => workAuthorIds.includes(authorId)); }); } // Return all results for client-side pagination res.json(filteredWorks); } catch (error) { respondWithError(res, error, "Failed to filter works"); } }); export default router;