feat(api): add GET /content/post/:slug endpoint#174
Conversation
Closes playfulprogramming#80. Returns a single post's data, author list, and collection chapter list (sibling posts ordered by collectionOrder, with the current post flagged via isCurrent), following collections.ts's schema and query conventions. Filters out unpublished posts/chapters (post_data.publishedAt: null) at both the main lookup and the chapter list, to avoid leaking draft slugs and titles via the chapter list to anyone who doesn't already know they exist. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
📝 WalkthroughWalkthroughAdds a new Fastify GET ChangesPost Endpoint
Sequence Diagram(s)sequenceDiagram
participant Client
participant postRoutes
participant Database
Client->>postRoutes: GET /content/post/:slug?locale
postRoutes->>Database: db.query.posts.findFirst (authors, collection posts)
Database-->>postRoutes: post record or undefined
alt post missing or unpublished
postRoutes-->>Client: 404 { error: "Post not found" }
else post found
postRoutes->>postRoutes: filter/sort collection posts into chapters
postRoutes->>postRoutes: build image URLs, format publishedAt
postRoutes-->>Client: 200 with post, authors, chapters
end
Estimated code review effort🎯 2 (Simple) | ⏱️ ~15 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/api/src/routes/content/post.ts`:
- Line 128: The authors mapping in the post response is currently relying on
database return order, which is not stable. Update the post retrieval/mapping in
the post route to add an explicit orderBy for authors (or otherwise define a
deterministic sort) when populating post.authors, using the existing authors
relation in the response shape so the author order is consistent.
- Around line 114-143: The post query is pulling `data` rows only by locale,
which can return multiple versions for the same slug and make
`post.data[0]`/`chapter.data[0]` nondeterministic. Update the
`db.query.posts.findFirst` relations in `post.ts` so both the top-level `data`
and nested `collection.posts.data` filters include the intended `version`
alongside `locale`. Use the existing `post.data` and `chapter.data` relation
selectors to locate the fix.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 165375d1-9075-4126-b152-8e80943bf7e4
📒 Files selected for processing (4)
apps/api/src/createApp.tsapps/api/src/routes/content/post.test.tsapps/api/src/routes/content/post.tsapps/api/test-utils/setup.ts
| const post = await db.query.posts.findFirst({ | ||
| where: { slug }, | ||
| with: { | ||
| data: { | ||
| columns: { | ||
| title: true, | ||
| description: true, | ||
| bannerImage: true, | ||
| socialImage: true, | ||
| wordCount: true, | ||
| publishedAt: true, | ||
| }, | ||
| where: { locale }, | ||
| }, | ||
| authors: { columns: { slug: true, name: true, profileImage: true } }, | ||
| collection: { | ||
| with: { | ||
| posts: { | ||
| columns: { slug: true, collectionOrder: true }, | ||
| with: { | ||
| data: { | ||
| columns: { title: true, publishedAt: true }, | ||
| where: { locale }, | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| }); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Show the relevant route file with line numbers
sed -n '1,240p' apps/api/src/routes/content/post.ts
echo '--- FILES ---'
# Locate post_data schema and related usages
git ls-files | rg 'apps/api/src/.*(schema|db|table|drizzle|content).*|apps/api/src/routes/content|post_data|postData|version'Repository: playfulprogramming/hoof
Length of output: 5021
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Map the route file structure to identify relevant functions/relations
ast-grep outline apps/api/src/routes/content/post.ts --view expanded || true
echo '--- SEARCH version filters/usages ---'
rg -n --hidden --glob '!**/node_modules/**' --glob '!**/dist/**' --glob '!**/build/**' \
'\bversion\b|post_data|postData|published version|draft' apps/api/src || true
echo '--- SEARCH data relation patterns ---'
rg -n --hidden --glob '!**/node_modules/**' --glob '!**/dist/**' --glob '!**/build/**' \
'with:\s*\{\s*data:\s*\{' apps/api/src || trueRepository: playfulprogramming/hoof
Length of output: 1863
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the schema and the route body in smaller slices.
echo '--- post.ts lines 100-170 ---'
sed -n '100,170p' apps/api/src/routes/content/post.ts
echo '--- schema candidates ---'
fd -a 'post*' apps/api/src || true
fd -a '*schema*' apps/api/src || true
fd -a '*table*' apps/api/src || trueRepository: playfulprogramming/hoof
Length of output: 2748
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- locate db schema and relation definitions ---'
git ls-files | rg '(^|/)(schema|schemas|table|tables|db|drizzle).*|packages/.*/src/.*(schema|table|db).*|.*post_data.*|.*postData.*|.*version.*'
echo '--- search for post_data / version definitions ---'
rg -n --hidden --glob '!**/node_modules/**' --glob '!**/dist/**' --glob '!**/build/**' \
'post_data|postData|version: text\("version"\)|primaryKey\(\{ columns: \[table\.slug, table\.locale, table\.version\]\}\)|where: \{ locale \}|orderBy' .
echo '--- search for data relation in db package ---'
rg -n --hidden --glob '!**/node_modules/**' --glob '!**/dist/**' --glob '!**/build/**' \
'relations\(|data:\s*\(|many\(.*post_data|one\(.*post_data|posts.*data' .Repository: playfulprogramming/hoof
Length of output: 12920
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- packages/db/src/schema/posts.ts ---'
sed -n '1,220p' packages/db/src/schema/posts.ts
echo '--- packages/db/src/relations.ts ---'
sed -n '1,220p' packages/db/src/relations.ts
echo '--- apps/worker/src/tasks/sync-post/processor.ts (relevant slice) ---'
sed -n '120,220p' apps/worker/src/tasks/sync-post/processor.ts
echo '--- version assignments across repo ---'
rg -n --hidden --glob '!**/node_modules/**' --glob '!**/dist/**' --glob '!**/build/**' \
'\bversion\s*:\s*["'\'']' apps packages | sed -n '1,200p'Repository: playfulprogramming/hoof
Length of output: 5752
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- post.test.ts slices around fixtures and assertions ---'
sed -n '1,340p' apps/api/src/routes/content/post.test.ts
echo '--- search for postData version in tests/fixtures ---'
rg -n --hidden --glob '!**/node_modules/**' --glob '!**/dist/**' --glob '!**/build/**' \
'postData|version:' apps/api/src/routes/content/post.test.ts apps/worker/src/tasks/sync-post/processor.test.ts apps/worker/test-utils/setup.tsRepository: playfulprogramming/hoof
Length of output: 7989
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- search for parsed.version and content version semantics ---'
rg -n --hidden --glob '!**/node_modules/**' --glob '!**/dist/**' --glob '!**/build/**' \
'parsed\.version|frontmatter.*version|version\s*:' apps/worker apps/api packages/post-images packages/db | sed -n '1,220p'
echo '--- inspect sync-post parser-related code ---'
fd -a 'processor.ts' apps/worker/src/tasks/sync-post
fd -a '*parse*' apps/worker/src/tasks/sync-post apps/worker/src | sed -n '1,120p'Repository: playfulprogramming/hoof
Length of output: 1297
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- locate markdown parser / sync-post inputs ---'
rg -n --hidden --glob '!**/node_modules/**' --glob '!**/dist/**' --glob '!**/build/**' \
'sync-post|parsed\.title|parsed\.published|parsed\.version|upToDateSlug|noindex|license' apps/worker/src | sed -n '1,260p'Repository: playfulprogramming/hoof
Length of output: 1905
Filter post_data by version here. The worker writes parsed.version into post_data, so where: { locale } can return multiple rows for the same slug/locale. post.data[0] and chapter.data[0] then become order-dependent; add version: "" (or the published version you intend) to both data relations.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/api/src/routes/content/post.ts` around lines 114 - 143, The post query
is pulling `data` rows only by locale, which can return multiple versions for
the same slug and make `post.data[0]`/`chapter.data[0]` nondeterministic. Update
the `db.query.posts.findFirst` relations in `post.ts` so both the top-level
`data` and nested `collection.posts.data` filters include the intended `version`
alongside `locale`. Use the existing `post.data` and `chapter.data` relation
selectors to locate the fix.
| }, | ||
| where: { locale }, | ||
| }, | ||
| authors: { columns: { slug: true, name: true, profileImage: true } }, |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check if there's an order/position column on the post_authors join table
ast-grep run --pattern 'export const postAuthors = pgTable($_, $_, $_)' --lang typescript packages/db
rg -n -A 15 'postAuthors' packages/db/src/schema --type tsRepository: playfulprogramming/hoof
Length of output: 2118
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== apps/api/src/routes/content/post.ts =="
ast-grep outline apps/api/src/routes/content/post.ts --view expanded
echo
sed -n '1,240p' apps/api/src/routes/content/post.ts
echo
echo "== packages/db/src/schema/posts.ts =="
ast-grep outline packages/db/src/schema/posts.ts --view expanded
echo
sed -n '1,220p' packages/db/src/schema/posts.ts
echo
echo "== search for authors ordering / post.authors usage =="
rg -n "post\.authors|authors: \{|orderBy:|author.*order" apps packages --type tsRepository: playfulprogramming/hoof
Length of output: 7859
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== relation definitions around posts =="
rg -n -A 80 -B 20 'relations\(|postAuthors|authors:' packages/db/src/schema/posts.ts packages/db/src/schema/*.ts
echo
echo "== relation helpers / docs references to orderBy in db schema =="
rg -n "orderBy" packages/db/src/schema packages/db/src -g '*.ts'Repository: playfulprogramming/hoof
Length of output: 6930
Add an explicit author sort If author order matters in the response, add an explicit orderBy for authors; post.authors is mapped in whatever order the DB returns, and post_authors has no position column to make that stable.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/api/src/routes/content/post.ts` at line 128, The authors mapping in the
post response is currently relying on database return order, which is not
stable. Update the post retrieval/mapping in the post route to add an explicit
orderBy for authors (or otherwise define a deterministic sort) when populating
post.authors, using the existing authors relation in the response shape so the
author order is consistent.
| collectionOrder: Type.Number(), | ||
| isCurrent: Type.Boolean(), |
There was a problem hiding this comment.
Two requests:
- Could we remove these two properties (
collectionOrderandisCurrent)? The frontend should be able to infer both from the data. - Can we add the collection slug & title to the response? (I would group these in an object; i.e.
collection.slug,collection.title,collection.chaptersare all required when the collection is present)
Otherwise, this looks great!
Closes #80.
Adds
GET /content/post/:slug?locale=en(defaulten), returning a single post's data along with its author list and "chapter list."Response shape
authors: id, name, profile image URL (joined viapost_authors)chapters: sibling posts in the same collection, ordered bycollectionOrder, each flagged withisCurrentThere's no
chapterstable in the schema — "chapters" are just sibling posts sharing acollectionSlug, which is howcollections.tsalready treats them (chapterCount).publishedAtfilteringpost_data.publishedAtis nullable at the schema level, but every current write path (sync-post) always sets it, so there's no existing filtering precedent anywhere else in the codebase. I added filtering here anyway:post_datarow haspublishedAt: nullfor the requested locale (same 404 as "no row for locale").chapterslist excludes any sibling whose ownpost_datarow haspublishedAt: null— otherwise a published chapter's response would leak the title/slug of unpublished siblings in the same collection, which nothing else in the codebase currently exposes.This is filtered at the app level (post-fetch) rather than in the Drizzle
whereclause — fine for a single-row + small-sibling-list endpoint like this; could move to a DB-level filter later if collections grow large enough for it to matter.Flagging this as a judgment call in case the intended gate is different (e.g. drafts visible to authenticated authors) — happy to adjust based on feedback.
Testing
post.test.ts: 6 tests covering happy path (with chapters +isCurrent), no-collection case, missing-locale-data 404, unpublished-main-post 404, unpublished-sibling-excluded, and post-not-found 404.pnpm test:unit: all 24 targets pass.pnpm prettier(check): clean except a pre-existing, unrelated flag on.claude/settings.local.json.Summary by CodeRabbit
New Features
Bug Fixes