Skip to content

Add collections route#120

Merged
fennifith merged 27 commits intoplayfulprogramming:mainfrom
femincan:feat/add-collections-route
Mar 10, 2026
Merged

Add collections route#120
fennifith merged 27 commits intoplayfulprogramming:mainfrom
femincan:feat/add-collections-route

Conversation

@femincan
Copy link
Contributor

@femincan femincan commented Mar 1, 2026

TODO:

  • The collections route needs to be registered in createApp
  • Create a unit test for the collections route
  • Write an example response on CollectionsResponseSchema
  • The image paths (coverUrl and authors[].profileImage) need to be combined with the base URL, like in mapPostImages (tasks/post-images.ts)

Note: While working on the PR there was an error in tsconfig.json because of the misname of the eslint config and missing allowJs property. I updated the config to suppress the error.

Closes: #84

Summary by CodeRabbit

  • New Features
    • Added a Collections API endpoint returning collections with images, titles, descriptions, chapter counts and authors; supports author filtering, pagination and locale.
  • Tests
    • Added test coverage validating full/empty results, author filtering and image presence/absence.
  • Chores
    • Enabled JS file support in TypeScript, updated ESLint config entry, added test mocks, and introduced a backend dependency to support data handling.

@coderabbitai
Copy link

coderabbitai bot commented Mar 1, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a new Fastify GET route at /content/collections, registers it on app startup, implements DB queries (with optional author/pagination), formats collection objects (including full image URLs via createImageUrl), adds tests and test mocks, and updates tsconfig to allow JS and rename ESLint config file.

Changes

Cohort / File(s) Summary
Collections API Implementation
apps/api/src/routes/content/collections.ts, apps/api/src/createApp.ts
New Fastify plugin collectionsRoutes implemented and registered. Handler accepts locale, page, limit, author, queries DB for collections with authors and chapter counts, formats response objects, and converts image paths to full URLs using createImageUrl.
Collections API Tests
apps/api/src/routes/content/collections.test.ts
New test suite that spins up a Fastify app, registers the collections routes, and verifies responses for full results, empty results, author-filtering, and no-collections scenarios using mocked DB responses.
Test Infrastructure
apps/api/test-utils/setup.ts
Added vi.fn() mock declaration for db.query.urlMetadata.collections.findMany to support the new tests.
TypeScript Configuration
tsconfig.json
Enabled "allowJs": true and updated included ESLint config filename from eslint.config.js to eslint.config.mjs.
Dependencies
apps/api/package.json
Added drizzle-orm to dependencies.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Fastify
  participant DB
  participant S3

  Client->>Fastify: GET /content/collections?author=&page=&limit=
  Fastify->>DB: Query collections with authors and chapter counts
  DB-->>Fastify: Collection rows
  Fastify->>S3: createImageUrl for cover/profile images
  S3-->>Fastify: Full image URLs
  Fastify-->>Client: 200 OK with collections array
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~18 minutes

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning Changes in tsconfig.json (allowJs flag and eslint.config.mjs rename) and the drizzle-orm dependency addition are build/configuration fixes unrelated to the collections route feature itself. Move tsconfig.json and package.json changes to a separate PR focused on build configuration updates, or clarify their necessity for the collections route implementation.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Add collections route' directly describes the main change—adding a new collections API route with route registration and tests.
Linked Issues check ✅ Passed The PR implements the GET /collections endpoint [#84] with required query parameters (page, limit, author) and returns all fields needed to render collection cards (cover URL, title, description, author id/name/profile image, chapter count).
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@femincan
Copy link
Contributor Author

femincan commented Mar 5, 2026

Hey @fennifith, should the query parameters be optional or required? Because currently they are all required and I am not sure about this.

Also can you look at the example profileImage and coverUrl values if they are in the correct format:

https://github.com/playfulprogramming/hoof/pull/120/changes#diff-3419d39e54474279f3c506a983b01cbfe7abe3d3e508f54d94672284fa705880R29-R63

(I tried adding changes as a diff with previews but I couldn't find how can I do. I also tried added as a link with highlight and it didn't work and only points to the beginning of the highlight.)

@fennifith
Copy link
Member

@femincan I would make author optional! I think page and limit make sense to be required.

For the example URLs, I'd just use a fake value e.g. "https://example.test/profile.jpg" for both (these should be full URLs once they go through your createImageUrl function)

@femincan
Copy link
Contributor Author

femincan commented Mar 8, 2026

The referenced issue says:

returns the full data required to display the collection card (cover URL, title, description, author id/name/profile image, chapter count)

To be able to display the collection card we also need the slug of the collection. Therefore I added the slug property to the response.


Also we are returning the author's slug mapped as id. Can't we directly return it with the name slug? If not then should I update the slug property of collection as id too for consistency?

@femincan femincan marked this pull request as ready for review March 9, 2026 22:04
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (3)
apps/api/src/routes/content/collections.test.ts (2)

18-43: Use mockResolvedValue for async database queries.

db.query.collections.findMany is an async function, but mockReturnValue is used instead of mockResolvedValue. While this may work due to how the code awaits the result, mockResolvedValue is the correct semantic choice for async functions and makes the test intent clearer.

♻️ Proposed fix
-      vi.mocked(db.query.collections.findMany).mockReturnValue([
+      vi.mocked(db.query.collections.findMany).mockResolvedValue([
         {
           slug: "harsh-leadership-truths",
           // ... rest of mock data
         },
-      ] as never);
+      ] as never);

Apply the same change to all test cases (lines 78, 91, 139).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/routes/content/collections.test.ts` around lines 18 - 43, The
test is mocking an async DB call using
vi.mocked(db.query.collections.findMany).mockReturnValue(...); replace
mockReturnValue with mockResolvedValue so the promise resolves correctly: update
the mock calls for db.query.collections.findMany (and any other async mocks in
this file) to use mockResolvedValue instead of mockReturnValue, including the
other occurrences noted (lines with similar mocks for collections.findMany at
other test cases).

17-74: Consider verifying the database query receives correct parameters.

The tests verify response shapes but don't assert that findMany was called with the expected arguments (where, limit, offset). Adding such assertions would catch parameter-passing bugs.

💡 Example assertion to add
expect(db.query.collections.findMany).toHaveBeenCalledWith(
  expect.objectContaining({
    limit: 10,
    offset: 0,
    where: expect.objectContaining({ authors: { slug: undefined } }),
  })
);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/routes/content/collections.test.ts` around lines 17 - 74, The
test currently only validates the response body but doesn't assert that
db.query.collections.findMany was invoked with the expected parameters; add an
assertion after the request to check that db.query.collections.findMany was
called with an object containing limit: 10, offset: 0 (derived from page/limit),
and the expected where clause (e.g., where: expect.objectContaining({ authors: {
slug: undefined } }) or the actual filter used by the route); reference the
mocked function db.query.collections.findMany in the test to locate where to add
this expect(...) call so parameter-passing regressions are caught.
apps/api/src/routes/content/collections.ts (1)

6-11: Consider adding bounds validation for pagination parameters.

The page and limit parameters have no constraints, allowing negative values or excessively large limits that could cause unexpected database behavior or resource exhaustion.

♻️ Proposed fix with validation constraints
 const CollectionsQueryParamsSchema = Type.Object({
   locale: Type.String({ default: "en" }),
-  page: Type.Number(),
-  limit: Type.Number(),
+  page: Type.Number({ minimum: 0 }),
+  limit: Type.Number({ minimum: 1, maximum: 100 }),
   author: Type.Optional(Type.String()),
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/routes/content/collections.ts` around lines 6 - 11, The
CollectionsQueryParamsSchema allows unbounded page and limit values; update the
schema (CollectionsQueryParamsSchema) to validate pagination by enforcing page
>= 1 and a sane range for limit (e.g., >=1 and a reasonable maximum like 100 or
1000), and consider adding defaults for page and limit to avoid missing values;
adjust the Type.Number declarations for the page and limit fields to include
minimum (and maximum for limit) constraints so invalid or abusive values are
rejected before reaching DB/query logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/api/src/routes/content/collections.ts`:
- Around line 116-127: The loop building formattedCollection reads
collection.data[0] unsafely which can be undefined; update the loop in the
collections processing code (the for...of over collections that constructs
formattedCollection) to guard against missing locale data by checking if
collection.data && collection.data[0] exist (or use optional chaining) before
accessing properties like coverImage, title, and description; if data is
missing, either skip that collection or populate safe defaults (e.g.,
undefined/empty strings and chapterCount 0) so accessing
collectionData.coverImage, collectionData.title, and collectionData.description
cannot throw.
- Around line 30-64: The examples in the response schema are inconsistent with
the defined model: fix each example object inside the examples array by (1)
removing the stray "n" from the first example's description, (2) adding the
required slug field to each example (e.g., slug: "<appropriate-slug>") and (3)
renaming each author property profileImage to profileImageUrl so authors match
the schema; update the example objects that include coverUrl, title,
description, authors, chapterCount, and slug accordingly.
- Line 4: Replace the incorrect TypeBox import with the correct package import
(`@sinclair/typebox`) by updating the import of Type and Static, add a null-safety
guard before accessing collection.data[0] in the code that iterates collections
(e.g., assign const collectionData = collection.data[0]; if (!collectionData)
continue; before using collectionData), and update the schema examples to match
the schema fields used by the Type.Object/Type.String definitions (use
profileImageUrl instead of profileImage, include the required slug field, and
remove the stray typo "...n") so examples align with the declared schema.
- Around line 101-113: The query in db.query.collections.findMany currently
always supplies where: { authors: { slug: queryParams.author } }, which passes
slug: undefined when queryParams.author is absent; update the where to
conditionally include the author filter only when queryParams.author is defined
by using the Drizzle callback style (e.g., use and() and eq() on the
authors.slug column inside the where callback) so that when queryParams.author
is undefined the authors.slug predicate is omitted; ensure this change is
applied where db.query.collections.findMany is called and continue to include
the other with:, limit:, and offset: options unchanged.

---

Nitpick comments:
In `@apps/api/src/routes/content/collections.test.ts`:
- Around line 18-43: The test is mocking an async DB call using
vi.mocked(db.query.collections.findMany).mockReturnValue(...); replace
mockReturnValue with mockResolvedValue so the promise resolves correctly: update
the mock calls for db.query.collections.findMany (and any other async mocks in
this file) to use mockResolvedValue instead of mockReturnValue, including the
other occurrences noted (lines with similar mocks for collections.findMany at
other test cases).
- Around line 17-74: The test currently only validates the response body but
doesn't assert that db.query.collections.findMany was invoked with the expected
parameters; add an assertion after the request to check that
db.query.collections.findMany was called with an object containing limit: 10,
offset: 0 (derived from page/limit), and the expected where clause (e.g., where:
expect.objectContaining({ authors: { slug: undefined } }) or the actual filter
used by the route); reference the mocked function db.query.collections.findMany
in the test to locate where to add this expect(...) call so parameter-passing
regressions are caught.

In `@apps/api/src/routes/content/collections.ts`:
- Around line 6-11: The CollectionsQueryParamsSchema allows unbounded page and
limit values; update the schema (CollectionsQueryParamsSchema) to validate
pagination by enforcing page >= 1 and a sane range for limit (e.g., >=1 and a
reasonable maximum like 100 or 1000), and consider adding defaults for page and
limit to avoid missing values; adjust the Type.Number declarations for the page
and limit fields to include minimum (and maximum for limit) constraints so
invalid or abusive values are rejected before reaching DB/query logic.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a52417fb-4095-4083-b5c9-e5eaca4cf991

📥 Commits

Reviewing files that changed from the base of the PR and between f37a454 and 2811597.

📒 Files selected for processing (5)
  • apps/api/src/createApp.ts
  • apps/api/src/routes/content/collections.test.ts
  • apps/api/src/routes/content/collections.ts
  • apps/api/test-utils/setup.ts
  • tsconfig.json

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
apps/api/src/routes/content/collections.ts (1)

6-11: Consider adding minimum value constraints for pagination parameters.

page and limit accept any number, including negative values. A negative page would produce a negative offset (line 116), and a zero or negative limit could cause unexpected behavior or DB errors.

♻️ Proposed fix with minimum constraints
 const CollectionsQueryParamsSchema = Type.Object({
 	locale: Type.String({ default: "en" }),
-	page: Type.Number(),
-	limit: Type.Number(),
+	page: Type.Number({ minimum: 0 }),
+	limit: Type.Number({ minimum: 1 }),
 	author: Type.Optional(Type.String()),
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/routes/content/collections.ts` around lines 6 - 11, The
CollectionsQueryParamsSchema currently allows any numbers for page and limit
which can produce negative offsets and DB errors; update the schema's page and
limit entries in CollectionsQueryParamsSchema to enforce minimum values (e.g.,
page minimum 1 and limit minimum 1, and consider sensible defaults) so
validation rejects negatives/zero, and ensure the code that computes the offset
from page and limit (the offset calculation that uses page and limit) relies on
these validated values before querying the DB.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@apps/api/src/routes/content/collections.ts`:
- Around line 6-11: The CollectionsQueryParamsSchema currently allows any
numbers for page and limit which can produce negative offsets and DB errors;
update the schema's page and limit entries in CollectionsQueryParamsSchema to
enforce minimum values (e.g., page minimum 1 and limit minimum 1, and consider
sensible defaults) so validation rejects negatives/zero, and ensure the code
that computes the offset from page and limit (the offset calculation that uses
page and limit) relies on these validated values before querying the DB.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 992f474c-924b-4beb-a483-c79fae274484

📥 Commits

Reviewing files that changed from the base of the PR and between c6cf552 and b0340fe.

📒 Files selected for processing (1)
  • apps/api/src/routes/content/collections.ts

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
apps/api/src/routes/content/collections.ts (1)

7-12: Consider adding a maximum limit and sort parameter.

  1. The limit parameter has no maximum, allowing clients to request very large result sets which could impact performance.

  2. Issue #84 mentions supporting a sort parameter (e.g., by date), but it's not included in this schema. Was this intentionally deferred?

💡 Suggested addition for limit cap and sort
 const CollectionsQueryParamsSchema = Type.Object({
 	locale: Type.String({ default: "en" }),
 	page: Type.Number({ minimum: 0 }),
-	limit: Type.Number({ minimum: 1 }),
+	limit: Type.Number({ minimum: 1, maximum: 100 }),
 	author: Type.Optional(Type.String()),
+	sort: Type.Optional(Type.Union([Type.Literal("date"), Type.Literal("title")])),
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/routes/content/collections.ts` around lines 7 - 12,
CollectionsQueryParamsSchema lacks a max cap on limit and is missing the
requested sort parameter; update the schema (CollectionsQueryParamsSchema) to
set a reasonable maximum for the limit property (e.g., Type.Number with minimum
and maximum) to prevent unbounded requests, and add an optional sort property
(e.g., Type.Optional(Type.Union([...]) or Type.String with an enum of allowed
sort keys like "date:asc", "date:desc" or "createdAt:asc/desc")) so callers can
request ordered results; ensure any default behavior is documented or set via
Type.Default if desired and validate the chosen sort values match the backend
sort handling.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@apps/api/src/routes/content/collections.ts`:
- Around line 7-12: CollectionsQueryParamsSchema lacks a max cap on limit and is
missing the requested sort parameter; update the schema
(CollectionsQueryParamsSchema) to set a reasonable maximum for the limit
property (e.g., Type.Number with minimum and maximum) to prevent unbounded
requests, and add an optional sort property (e.g.,
Type.Optional(Type.Union([...]) or Type.String with an enum of allowed sort keys
like "date:asc", "date:desc" or "createdAt:asc/desc")) so callers can request
ordered results; ensure any default behavior is documented or set via
Type.Default if desired and validate the chosen sort values match the backend
sort handling.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9b97876f-af8b-4bd1-8c2e-07fafba7a94a

📥 Commits

Reviewing files that changed from the base of the PR and between 7ddc5aa and 3dc14d0.

📒 Files selected for processing (2)
  • apps/api/src/routes/content/collections.test.ts
  • apps/api/src/routes/content/collections.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/api/src/routes/content/collections.test.ts

@femincan femincan requested a review from fennifith March 10, 2026 00:20
Copy link
Member

@fennifith fennifith left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good! The tests are super thorough - great work :D

@fennifith fennifith added this pull request to the merge queue Mar 10, 2026
Merged via the queue into playfulprogramming:main with commit 43125fe Mar 10, 2026
3 checks passed
@femincan femincan deleted the feat/add-collections-route branch March 10, 2026 02:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Endpoint to return a list of collections

2 participants