-
Notifications
You must be signed in to change notification settings - Fork 5
feat(games): add filtering and pagination to GET /games endpoint #13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
e771176
5b085b1
96cda3d
5610883
230f469
f4874bf
85f190f
1718845
031295e
cf7a665
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,117 @@ | ||
| import { Hono } from "hono"; | ||
| import { db } from "../db/index"; | ||
| import { games } from "../db/schema"; | ||
| import { eq, like, gte, lte, lt, and } from "drizzle-orm"; | ||
| import { z } from "zod"; | ||
| import { requireSession } from "../lib/middleware"; | ||
|
|
||
| export const gamesRoute = new Hono(); | ||
|
|
||
| // zod validation | ||
| const gamesQuerySchema = z.object({ | ||
| limit: z.coerce.number().min(1).max(50).default(10), | ||
| cursor: z.coerce.number().optional(), | ||
| search: z.string().min(1).optional(), | ||
| maxLikes: z.coerce.number().min(0).optional(), | ||
| createdAfter: z.string().date().optional(), | ||
| createdBefore: z.string().date().optional(), | ||
| }); | ||
|
|
||
| /** | ||
| * GET /games | ||
| * | ||
| * Returns all active games with cursor-based (scroll) pagination. | ||
| * | ||
| * Requirements: | ||
| * - User must be authenticated | ||
| * | ||
| * Query parameters: | ||
| * - limit Number of results per page (default: 10, max: 50) | ||
| * - cursor createdAt timestamp of the last received item; omit for first page | ||
| * - search Filter by title | ||
| * - minLikes Filter by minimum like count | ||
| * - maxLikes Filter by maximum like count | ||
| * - createdAfter Return games created after this date (YYYY-MM-DD) | ||
| * - createdBefore Return games created before this date (YYYY-MM-DD) | ||
|
zia0307 marked this conversation as resolved.
|
||
| * | ||
| * Response: | ||
| * - games[] Array of game objects | ||
| * - nextCursor Pass as `cursor` in the next request; null means no more results | ||
| */ | ||
| gamesRoute.get("/", requireSession, async (c) => { | ||
| try { | ||
| const queryParse = gamesQuerySchema.safeParse(c.req.query()); | ||
|
|
||
| if (!queryParse.success) { | ||
| return c.json({ error: "Validation failed", details: queryParse.error.format() }, 400); | ||
| } | ||
|
|
||
| const { limit, cursor, search, maxLikes, createdAfter, createdBefore } = queryParse.data; | ||
|
|
||
| /* Filter conditions */ | ||
| const conditions = [eq(games.isActive, true)]; | ||
|
|
||
| // cursor pagination - only fetch records older than the last seen item | ||
| if (cursor !== undefined) { | ||
| conditions.push(lt(games.createdAt, cursor)); | ||
| } | ||
|
zia0307 marked this conversation as resolved.
|
||
|
|
||
| if (search) { | ||
| conditions.push(like(games.title, `%${search}%`)); | ||
| } | ||
|
|
||
| if (maxLikes !== undefined) { | ||
| conditions.push(lte(games.countLikes, maxLikes)); | ||
| } | ||
|
|
||
| if (createdAfter) { | ||
| const afterTimestamp = Math.floor(new Date(createdAfter).getTime() / 1000); | ||
| conditions.push(gte(games.createdAt, afterTimestamp)); | ||
| } | ||
|
|
||
| if (createdBefore) { | ||
| const beforeTimestamp = Math.floor(new Date(createdBefore).getTime() / 1000); | ||
| conditions.push(lte(games.createdAt, beforeTimestamp)); | ||
|
zia0307 marked this conversation as resolved.
|
||
| } | ||
|
Comment on lines
+67
to
+75
|
||
|
|
||
| const whereClause = and(...conditions); | ||
|
|
||
| /* Data retrieval - fetch one extra so we can tell if there's a next page */ | ||
| const gamesList = await db.query.games.findMany({ | ||
| where: whereClause, | ||
| orderBy: (g, { desc }) => [desc(g.createdAt)], | ||
| limit: limit + 1, | ||
|
zia0307 marked this conversation as resolved.
|
||
| columns: { | ||
| id: true, | ||
| title: true, | ||
| description: true, | ||
| gameUrl: true, | ||
| coverMediaId: true, | ||
| createdBy: true, | ||
| countLikes: true, | ||
| countDislikes: true, | ||
| countSuperlikes: true, | ||
| score: true, | ||
| viewCount: true, | ||
| createdAt: true, | ||
| updatedAt: true, | ||
| }, | ||
| }); | ||
|
|
||
| const hasNextPage = gamesList.length > limit; | ||
| const results = hasNextPage ? gamesList.slice(0, limit) : gamesList; | ||
| const nextCursor = hasNextPage ? results[results.length - 1].createdAt : null; | ||
|
|
||
| return c.json({ | ||
| success: true, | ||
| limit, | ||
| nextCursor, | ||
| games: results, | ||
|
Comment on lines
+101
to
+109
|
||
| }); | ||
| } catch (error) { | ||
| console.error("Get games error:", error); | ||
| return c.json({ error: "Failed to fetch games", message: "An error occurred while retrieving games." }, 500); | ||
| } | ||
| }); | ||
|
|
||
| export default gamesRoute; | ||
|
Comment on lines
+116
to
+117
|
||
Uh oh!
There was an error while loading. Please reload this page.