diff --git a/apps/api/src/createApp.ts b/apps/api/src/createApp.ts index c219e45..72cb9a9 100644 --- a/apps/api/src/createApp.ts +++ b/apps/api/src/createApp.ts @@ -6,6 +6,7 @@ import { healthRoutes } from "./routes/health.ts"; import postImagesRoutes from "./routes/tasks/post-images.ts"; import urlMetadataRoutes from "./routes/tasks/url-metadata.ts"; import collectionsRoutes from "./routes/content/collections.ts"; +import profilesRoutes from "./routes/content/profiles.ts"; import fastify from "fastify"; import devRoutes from "./routes/dev/index.ts"; @@ -21,6 +22,7 @@ export const createApp = () => { app.register(postImagesRoutes); app.register(urlMetadataRoutes); app.register(collectionsRoutes); + app.register(profilesRoutes); if (env.ENVIRONMENT === "development") { app.register(devRoutes); diff --git a/apps/api/src/routes/content/profiles.test.ts b/apps/api/src/routes/content/profiles.test.ts new file mode 100644 index 0000000..00b98db --- /dev/null +++ b/apps/api/src/routes/content/profiles.test.ts @@ -0,0 +1,88 @@ +import fastify, { type FastifyInstance } from "fastify"; +import profilesRoutes from "./profiles.ts"; +import { db } from "@playfulprogramming/db"; + +describe("Profiles Routes Tests", () => { + let app: FastifyInstance; + beforeAll(async () => { + app = fastify(); + await app.register(profilesRoutes); + }); + + afterAll(async () => { + await app.close(); + }); + + describe("/content/profiles", () => { + test("returns all profiles", async () => { + vi.mocked(db.query.profiles.findMany).mockResolvedValue([ + { + slug: "crutchcorn", + name: "Corbin Crutchley", + description: "Project lead for Playful Programming.", + profileImage: "content/profile.png", + }, + { + slug: "fennifith", + name: "James Fenn", + description: "Backend lead for Playful Programming.", + profileImage: null, + }, + ] as never); + + const response = await app.inject({ + method: "GET", + url: "/content/profiles", + query: { page: "0", limit: "10" }, + }); + + expect(response.statusCode).toBe(200); + expect(response.json()).toMatchInlineSnapshot(` + [ + { + "description": "Project lead for Playful Programming.", + "id": "crutchcorn", + "name": "Corbin Crutchley", + "profileImageUrl": "https://s3_public_url.test/s3_bucket/content/profile.png", + }, + { + "description": "Backend lead for Playful Programming.", + "id": "fennifith", + "name": "James Fenn", + }, + ] + `); + }); + + test("returns an empty list when there are no profiles", async () => { + vi.mocked(db.query.profiles.findMany).mockResolvedValue([]); + + const response = await app.inject({ + method: "GET", + url: "/content/profiles", + query: { page: "0", limit: "10" }, + }); + + expect(response.statusCode).toBe(200); + expect(response.json()).toMatchInlineSnapshot(`[]`); + }); + + test("paginates using page and limit query params", async () => { + vi.mocked(db.query.profiles.findMany).mockResolvedValue([]); + + const response = await app.inject({ + method: "GET", + url: "/content/profiles", + query: { page: "2", limit: "10" }, + }); + + expect(response.statusCode).toBe(200); + expect(db.query.profiles.findMany).toBeCalledWith( + expect.objectContaining({ + offset: 20, + limit: 10, + }), + ); + }); + }); +}); diff --git a/apps/api/src/routes/content/profiles.ts b/apps/api/src/routes/content/profiles.ts new file mode 100644 index 0000000..4b1449d --- /dev/null +++ b/apps/api/src/routes/content/profiles.ts @@ -0,0 +1,91 @@ +import type { FastifyPluginAsync } from "fastify"; +import { db } from "@playfulprogramming/db"; +import { Type, type Static } from "typebox"; +import { createImageUrl } from "../../utils.ts"; + +const ProfilesQueryParamsSchema = Type.Object({ + page: Type.Number({ minimum: 0 }), + limit: Type.Number({ minimum: 1 }), +}); + +const ProfilesResponseSchema = Type.Array( + Type.Object( + { + id: Type.String(), + name: Type.String(), + description: Type.String(), + profileImageUrl: Type.Optional(Type.String()), + }, + { + examples: [ + { + id: "crutchcorn", + name: "Corbin Crutchley", + description: "Project lead for Playful Programming.", + profileImageUrl: "https://example.test/profile.jpg", + }, + { + id: "fennifith", + name: "James Fenn", + description: "Backend lead for Playful Programming.", + profileImageUrl: "https://example.test/profile.jpg", + }, + ], + }, + ), +); + +type ProfilesResponse = Static; + +const profilesRoutes: FastifyPluginAsync = async (fastify) => { + fastify.get<{ + Querystring: Static; + Reply: ProfilesResponse; + }>( + "/content/profiles", + { + schema: { + description: "Fetch a list of author profiles", + querystring: ProfilesQueryParamsSchema, + response: { + 200: { + description: "Successful", + content: { + "application/json": { + schema: ProfilesResponseSchema, + }, + }, + }, + }, + }, + }, + async (request, reply) => { + const queryParams = request.query; + + const profiles = await db.query.profiles.findMany({ + columns: { + slug: true, + name: true, + description: true, + profileImage: true, + }, + offset: queryParams.page * queryParams.limit, + limit: queryParams.limit, + }); + + const profilesResponse: ProfilesResponse = profiles.map((profile) => ({ + id: profile.slug, + name: profile.name, + description: profile.description, + profileImageUrl: profile.profileImage + ? createImageUrl(profile.profileImage) + : undefined, + })); + + reply.code(200); + reply.send(profilesResponse); + }, + ); +}; + +export default profilesRoutes; diff --git a/apps/api/test-utils/setup.ts b/apps/api/test-utils/setup.ts index 86a52d7..db958d6 100644 --- a/apps/api/test-utils/setup.ts +++ b/apps/api/test-utils/setup.ts @@ -32,6 +32,9 @@ vi.mock("@playfulprogramming/db", () => { collections: { findMany: vi.fn(), }, + profiles: { + findMany: vi.fn(), + }, }, }, };