Skip to content

Conversation

@rob-ghost
Copy link
Contributor

@rob-ghost rob-ghost commented Dec 11, 2025

Context

Site owners with active communities need a way to moderate comments at scale. Currently, the Admin API only supports browsing comments for a specific post (GET /comments/post/:post_id), which works for the embedded Comments-UI but doesn't support a dedicated moderation interface where admins need to see and act on comments across their entire site.

This is the backend foundation for the upcoming comment moderation feature (FEA-485), which will allow admins to browse, filter, and bulk-moderate comments from a central location in Ghost Admin.

What this PR does

Adds a new Admin API endpoint GET /ghost/api/admin/comments/ that:

  • Returns all comments across the site (no post_id required)
  • Supports pagination, filtering by status/member/post, and ordering
  • Returns a flat list with parent_id references (rather than nested trees) to enable efficient pagination and future bulk operations
  • Always includes member and post relations (no include param needed)
  • Supports include_nested query param to control whether replies appear in the flat list (default: true) or only top-level comments are returned
  • Returns deleted comments as tombstones (just id, post_id, parent_id, status) when they have visible replies, so the UI can show "Reply to deleted comment" without leaking member/content data

The existing per-post endpoint (GET /comments/post/:post_id) is unchanged.

How to test

# Browse all comments (newest first by default)
GET /ghost/api/admin/comments/

# Filter by status
GET /ghost/api/admin/comments/?filter=status:hidden

# Only top-level comments (no replies)
GET /ghost/api/admin/comments/?include_nested=false

# Paginate
GET /ghost/api/admin/comments/?page=2&limit=30

ref https://linear.app/ghost/issue/FEA-485

@rob-ghost rob-ghost marked this pull request as draft December 11, 2025 12:55
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 11, 2025

Walkthrough

Adds an admin-facing "browseAll" comments endpoint and controller handler, a CommentsService.getAdminAllComments method, and an admin GET /comments route (placed before /comments/:id). Serializers for comments now include parent_id and return a minimal tombstone object for comments with status "deleted" (optionally including post relation and recursively mapping replies). E2E tests and matchers were updated to assert parent_id, tombstone shapes, pagination, filtering, ordering, and related behaviors.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Areas requiring extra attention:

  • ghost/core/core/server/services/comments/CommentsService.js — new getAdminAllComments signature, admin context, includeNested → parentId behavior.
  • ghost/core/core/server/services/comments/CommentsController.js — include_nested normalization and delegation.
  • ghost/core/core/server/api/endpoints/comments.js — new browseAll handler, options, permissions, and headers.
  • ghost/core/core/server/web/api/endpoints/admin/routes.js — new GET /comments route placement relative to /comments/:id.
  • ghost/core/core/server/api/endpoints/utils/serializers/output/mappers/comments.js — parent_id addition, tombstone branch, and recursive replies mapping.
  • ghost/core/test/e2e-api/** — extensive test updates, new matchers, snapshot changes, and added Browse All test suite.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: adding a new Admin API endpoint for browsing all comments across the site, which is the primary feature introduced in this PR.
Description check ✅ Passed The description comprehensively explains the context, what the PR does, and provides testing examples. It directly relates to the changeset by detailing the new endpoint functionality and its features.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch rob/comments-browse-all-api

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ast-grep (0.40.0)
ghost/core/test/e2e-api/members-comments/comments.test.js

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.

Copy link
Contributor

@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: 0

🧹 Nitpick comments (4)
ghost/core/core/server/services/comments/CommentsService.js (1)

212-234: Consider explicitly setting isAdmin: true for consistency.

Unlike getAdminAllComments which explicitly sets isAdmin: true, this method relies on the caller to pass it via options. For clarity and defensive coding, consider making this explicit.

     async getAdminPostComments(options) {
         const findPageOptions = {
             ...options,
-            parentId: null // Only top-level comments for per-post browse
+            parentId: null, // Only top-level comments for per-post browse
+            isAdmin: true   // Admin context allows viewing hidden comments
         };
 
         return await this.models.Comment.findPage(findPageOptions);
     }
ghost/core/test/e2e-api/admin/comments.test.js (1)

1151-1388: Browse All admin tests give solid coverage; a couple of edge cases are optional follow‑ups

The new “Browse All” suite does a good job exercising the critical behaviours: cross‑post browsing, include_nested semantics, status/deleted filtering, member filters, pagination, ordering, and relations (member, post). The setup uses the existing helpers consistently, so it stays in line with the rest of the file.

If you want to tighten things further (optional), you could:

  • Add a case combining include_nested=false with a filter (e.g. status:hidden or member_id:) to ensure filters always apply to the top‑level subset.
  • Add a small assertion that the member relation is still present when include=post is used, to guard against future regressions in withRelated handling.

These are nice-to-haves; the current tests are already strong.

ghost/core/core/server/services/comments/CommentsController.js (2)

66-99: adminBrowsePost correctly scopes by post_id and preserves admin impersonation behaviour

The method cleanly builds a post‑scoped filter (or mongoTransformer), deletes post_id from options once encoded in the filter (while safely capturing it in a local postId), sets isAdmin, and reuses #setImpersonationContext before delegating to getAdminPostComments. This keeps behaviour aligned with the existing admin browse‑by‑post flow.

Optionally, you could add a defensive guard (or assertion) for a missing frame.options.post_id to fail fast if the route wiring ever changes, but it’s not strictly required given current routing.


101-123: adminBrowseAll cleanly parses frame options; consider mirroring admin flags if needed later

The new adminBrowseAll method nicely isolates controller concerns: it normalizes include_nested (handling both 'false' and false), provides a default order = 'created_at desc', and passes a trimmed parameter object into getAdminAllComments, avoiding leaking the whole frame.

If, in future, the browse‑all endpoint needs the same semantics as other admin routes (e.g. exposing liked for an impersonate_member_uuid or relying on an isAdmin hint in the service), you might want to:

  • Set frame.options.isAdmin = true (or pass an explicit isAdmin: true flag in the options object), and/or
  • Call await this.#setImpersonationContext(frame.options) and thread the resulting context through.

For the current moderation use case, the implementation looks sound as‑is.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2546ddd and 0f5ba29.

⛔ Files ignored due to path filters (3)
  • ghost/core/test/e2e-api/admin/__snapshots__/activity-feed.test.js.snap is excluded by !**/*.snap
  • ghost/core/test/e2e-api/admin/__snapshots__/comments.test.js.snap is excluded by !**/*.snap
  • ghost/core/test/e2e-api/members-comments/__snapshots__/comments.test.js.snap is excluded by !**/*.snap
📒 Files selected for processing (7)
  • ghost/core/core/server/api/endpoints/comments.js (1 hunks)
  • ghost/core/core/server/api/endpoints/utils/serializers/output/mappers/comments.js (1 hunks)
  • ghost/core/core/server/services/comments/CommentsController.js (1 hunks)
  • ghost/core/core/server/services/comments/CommentsService.js (1 hunks)
  • ghost/core/core/server/web/api/endpoints/admin/routes.js (1 hunks)
  • ghost/core/test/e2e-api/admin/comments.test.js (2 hunks)
  • ghost/core/test/e2e-api/members-comments/comments.test.js (1 hunks)
🧰 Additional context used
🧠 Learnings (4)
📚 Learning: 2025-11-25T14:28:50.351Z
Learnt from: CR
Repo: TryGhost/Ghost PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-25T14:28:50.351Z
Learning: API routes should be defined in `ghost/core/core/server/api/`

Applied to files:

  • ghost/core/core/server/web/api/endpoints/admin/routes.js
📚 Learning: 2025-06-18T10:56:19.906Z
Learnt from: 55sketch
Repo: TryGhost/Ghost PR: 23894
File: ghost/core/core/server/api/endpoints/comments.js:16-22
Timestamp: 2025-06-18T10:56:19.906Z
Learning: The validateCommentData function in ghost/core/core/server/api/endpoints/comments.js is not duplicated elsewhere in the codebase - it's a unique validation function that ensures either post_id or parent_id is provided for comment creation.

Applied to files:

  • ghost/core/core/server/web/api/endpoints/admin/routes.js
  • ghost/core/test/e2e-api/admin/comments.test.js
  • ghost/core/test/e2e-api/members-comments/comments.test.js
  • ghost/core/core/server/api/endpoints/utils/serializers/output/mappers/comments.js
  • ghost/core/core/server/api/endpoints/comments.js
📚 Learning: 2025-11-24T17:29:43.865Z
Learnt from: CR
Repo: TryGhost/Ghost PR: 0
File: e2e/AGENTS.md:0-0
Timestamp: 2025-11-24T17:29:43.865Z
Learning: Applies to e2e/**/*.{ts,tsx} : Prefer less comments and give things clear names

Applied to files:

  • ghost/core/test/e2e-api/admin/comments.test.js
📚 Learning: 2025-11-24T17:29:43.865Z
Learnt from: CR
Repo: TryGhost/Ghost PR: 0
File: e2e/AGENTS.md:0-0
Timestamp: 2025-11-24T17:29:43.865Z
Learning: Applies to e2e/**/*.test.ts : Use factory pattern for all test data creation instead of hard-coded data or direct database manipulation

Applied to files:

  • ghost/core/test/e2e-api/admin/comments.test.js
🧬 Code graph analysis (3)
ghost/core/core/server/web/api/endpoints/admin/routes.js (3)
ghost/core/core/server/web/api/endpoints/content/routes.js (3)
  • router (12-12)
  • mw (5-5)
  • api (3-3)
ghost/core/test/legacy/mock-express-style/utils/mock-express.js (1)
  • http (1-1)
ghost/core/core/server/api/endpoints/session.js (1)
  • api (5-5)
ghost/core/core/server/services/comments/CommentsController.js (2)
ghost/core/core/server/api/endpoints/comments.js (1)
  • postId (6-6)
ghost/core/test/e2e-api/admin/comments.test.js (1)
  • postId (27-27)
ghost/core/core/server/services/comments/CommentsService.js (1)
ghost/core/core/server/services/comments/CommentsController.js (1)
  • includeNested (113-113)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: Ghost-CLI tests
  • GitHub Check: Acceptance tests (Node 22.18.0, mysql8)
  • GitHub Check: Legacy tests (Node 22.18.0, mysql8)
  • GitHub Check: Acceptance tests (Node 22.18.0, sqlite3)
  • GitHub Check: Legacy tests (Node 22.18.0, sqlite3)
  • GitHub Check: Unit tests (Node 22.18.0)
  • GitHub Check: Lint
  • GitHub Check: Build & Push Docker Image
🔇 Additional comments (7)
ghost/core/core/server/services/comments/CommentsService.js (1)

179-210: Well-structured admin browsing method with clear documentation.

The getAdminAllComments method correctly:

  • Skips the checkEnabled() call to allow admins to moderate existing comments
  • Uses parentId: undefined vs null to control nested inclusion
  • Sets isAdmin: true to enable viewing hidden comments
ghost/core/core/server/web/api/endpoints/admin/routes.js (1)

44-45: Correct route ordering and authentication.

The comment clearly explains why GET /comments must be defined before GET /comments/:id. This prevents the parametrized route from incorrectly capturing requests to the browse-all endpoint. The mw.authAdminApi middleware is correctly applied.

ghost/core/core/server/api/endpoints/utils/serializers/output/mappers/comments.js (1)

6-16: Fields correctly added to support flat list admin browsing.

Adding post_id and parent_id to the serialized output enables the flat list response structure mentioned in the PR objectives, supporting efficient pagination and bulk operations in the moderation interface.

ghost/core/test/e2e-api/members-comments/comments.test.js (1)

104-117: Test matcher correctly updated for new fields.

The commentMatcher appropriately defines:

  • post_id: anyObjectId — always present on comments
  • parent_id: nullable(anyObjectId) — null for top-level, ObjectId for replies

This correctly reflects the serializer changes and maintains test coverage for the new admin browsing functionality.

ghost/core/core/server/api/endpoints/comments.js (2)

96-100: Clean separation of post-specific and site-wide browsing.

The rename from adminBrowse to adminBrowsePost clearly communicates intent and maintains the existing per-post functionality while the new browseAll handles site-wide browsing.


101-123: Well-designed browseAll endpoint configuration.

The endpoint correctly:

  • Exposes include_nested option for controlling reply inclusion in the flat list
  • Reuses browse permission method to leverage existing admin authorization
  • Uses empty validation since no required parameters (unlike post-specific browse which requires post_id)
ghost/core/test/e2e-api/admin/comments.test.js (1)

9-16: Extending membersCommentMatcher to include post_id and nullable parent_id is consistent with the new API shape

Asserting post_id: anyObjectId and parent_id: nullable(anyObjectId) on member-facing comment payloads matches the new serializer contract and keeps snapshots honest about the new fields. No issues here.

@rob-ghost rob-ghost force-pushed the rob/comments-browse-all-api branch 10 times, most recently from 12b7a27 to 9e5a55c Compare December 11, 2025 16:43
@rob-ghost rob-ghost self-assigned this Dec 11, 2025
@github-actions
Copy link
Contributor

React E2E Tests Failed

To view the Playwright test report locally, run:

REPORT_DIR=$(mktemp -d) && gh run download 20140535270 -n playwright-report-react -D "$REPORT_DIR" && npx playwright show-report "$REPORT_DIR"

1 similar comment
@github-actions
Copy link
Contributor

React E2E Tests Failed

To view the Playwright test report locally, run:

REPORT_DIR=$(mktemp -d) && gh run download 20140535270 -n playwright-report-react -D "$REPORT_DIR" && npx playwright show-report "$REPORT_DIR"

@rob-ghost rob-ghost marked this pull request as ready for review December 11, 2025 19:39
@rob-ghost rob-ghost force-pushed the rob/comments-browse-all-api branch from 9e5a55c to 30d820f Compare December 11, 2025 20:03
Copy link
Contributor

@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: 0

🧹 Nitpick comments (2)
ghost/core/core/server/services/comments/CommentsService.js (1)

212-217: Consider consistency with getAdminAllComments regarding checkEnabled().

Note that getAdminComments still calls checkEnabled() (Line 213) while the new getAdminAllComments intentionally skips it. If the reasoning "admins can moderate existing comments" applies, you may want to align getAdminComments in a follow-up. This is not blocking for this PR.

ghost/core/test/e2e-api/admin/comments.test.js (1)

1219-1431: Comprehensive test coverage for the new Browse All endpoint.

The test suite thoroughly covers:

  • Cross-post browsing
  • Flat list with/without nested replies
  • Hidden and deleted comment handling
  • Tombstone semantics
  • Status and member filtering
  • Pagination and ordering

One enhancement to consider: the PR objectives mention filtering by post but there's no explicit test for filter=post_id:'...'. This may be covered implicitly by the NQL filter tests.

Consider adding an explicit test for post filtering:

it('Can filter by post_id', async function () {
    await dbFns.addComment({
        post_id: fixtureManager.get('posts', 0).id,
        member_id: fixtureManager.get('members', 0).id
    });
    await dbFns.addComment({
        post_id: fixtureManager.get('posts', 1).id,
        member_id: fixtureManager.get('members', 0).id
    });

    await adminApi.get('/comments/?filter=' + encodeURIComponent(`post_id:'${fixtureManager.get('posts', 0).id}'`))
        .expectStatus(200)
        .matchBodySnapshot({
            comments: [commentMatcher]
        });
});
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9e5a55c and 30d820f.

⛔ Files ignored due to path filters (3)
  • ghost/core/test/e2e-api/admin/__snapshots__/activity-feed.test.js.snap is excluded by !**/*.snap
  • ghost/core/test/e2e-api/admin/__snapshots__/comments.test.js.snap is excluded by !**/*.snap
  • ghost/core/test/e2e-api/members-comments/__snapshots__/comments.test.js.snap is excluded by !**/*.snap
📒 Files selected for processing (7)
  • ghost/core/core/server/api/endpoints/comments.js (1 hunks)
  • ghost/core/core/server/api/endpoints/utils/serializers/output/mappers/comments.js (2 hunks)
  • ghost/core/core/server/services/comments/CommentsController.js (1 hunks)
  • ghost/core/core/server/services/comments/CommentsService.js (1 hunks)
  • ghost/core/core/server/web/api/endpoints/admin/routes.js (1 hunks)
  • ghost/core/test/e2e-api/admin/comments.test.js (8 hunks)
  • ghost/core/test/e2e-api/members-comments/comments.test.js (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • ghost/core/core/server/api/endpoints/utils/serializers/output/mappers/comments.js
  • ghost/core/test/e2e-api/members-comments/comments.test.js
🧰 Additional context used
🧠 Learnings (6)
📚 Learning: 2025-06-18T10:56:19.906Z
Learnt from: 55sketch
Repo: TryGhost/Ghost PR: 23894
File: ghost/core/core/server/api/endpoints/comments.js:16-22
Timestamp: 2025-06-18T10:56:19.906Z
Learning: The validateCommentData function in ghost/core/core/server/api/endpoints/comments.js is not duplicated elsewhere in the codebase - it's a unique validation function that ensures either post_id or parent_id is provided for comment creation.

Applied to files:

  • ghost/core/core/server/web/api/endpoints/admin/routes.js
  • ghost/core/core/server/api/endpoints/comments.js
  • ghost/core/test/e2e-api/admin/comments.test.js
📚 Learning: 2025-11-24T17:29:43.865Z
Learnt from: CR
Repo: TryGhost/Ghost PR: 0
File: e2e/AGENTS.md:0-0
Timestamp: 2025-11-24T17:29:43.865Z
Learning: Applies to e2e/**/*.test.ts : Test suite names should follow the format 'Ghost Admin - Feature' or 'Ghost Public - Feature'

Applied to files:

  • ghost/core/test/e2e-api/admin/comments.test.js
📚 Learning: 2025-03-13T09:00:20.205Z
Learnt from: mike182uk
Repo: TryGhost/Ghost PR: 22471
File: apps/admin-x-activitypub/src/utils/pending-activity.ts:13-71
Timestamp: 2025-03-13T09:00:20.205Z
Learning: The pending activity utilities in the Ghost ActivityPub module are covered by tests in the file `apps/admin-x-activitypub/test/unit/utils/pending-activity.ts`.

Applied to files:

  • ghost/core/test/e2e-api/admin/comments.test.js
📚 Learning: 2025-11-24T17:29:43.865Z
Learnt from: CR
Repo: TryGhost/Ghost PR: 0
File: e2e/AGENTS.md:0-0
Timestamp: 2025-11-24T17:29:43.865Z
Learning: Applies to e2e/**/*.{ts,tsx} : Prefer less comments and give things clear names

Applied to files:

  • ghost/core/test/e2e-api/admin/comments.test.js
📚 Learning: 2025-05-29T10:37:26.369Z
Learnt from: ErisDS
Repo: TryGhost/Ghost PR: 23588
File: ghost/core/test/e2e-api/admin/backup.test.js:136-148
Timestamp: 2025-05-29T10:37:26.369Z
Learning: In the Ghost test framework, when testing CSV exports via the admin API, the response `body` field is empty while the actual CSV data is provided through the `text` field. Tests should use `expectEmptyBody()` and then validate the CSV content via `.expect(({text}) => ...)` - this is not contradictory behavior.

Applied to files:

  • ghost/core/test/e2e-api/admin/comments.test.js
📚 Learning: 2025-11-24T17:29:43.865Z
Learnt from: CR
Repo: TryGhost/Ghost PR: 0
File: e2e/AGENTS.md:0-0
Timestamp: 2025-11-24T17:29:43.865Z
Learning: Applies to e2e/**/*.test.ts : Use factory pattern for all test data creation instead of hard-coded data or direct database manipulation

Applied to files:

  • ghost/core/test/e2e-api/admin/comments.test.js
🧬 Code graph analysis (2)
ghost/core/core/server/web/api/endpoints/admin/routes.js (2)
ghost/core/core/server/web/api/endpoints/content/routes.js (3)
  • router (12-12)
  • mw (5-5)
  • api (3-3)
ghost/core/core/server/api/endpoints/session.js (1)
  • api (5-5)
ghost/core/core/server/services/comments/CommentsService.js (1)
ghost/core/core/server/services/comments/CommentsController.js (1)
  • includeNested (105-105)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: Legacy tests (Node 22.18.0, sqlite3)
  • GitHub Check: Legacy tests (Node 22.18.0, mysql8)
  • GitHub Check: Acceptance tests (Node 22.18.0, mysql8)
  • GitHub Check: Ghost-CLI tests
  • GitHub Check: Lint
  • GitHub Check: Unit tests (Node 22.18.0)
  • GitHub Check: Build & Push Docker Image
🔇 Additional comments (6)
ghost/core/core/server/web/api/endpoints/admin/routes.js (1)

44-45: LGTM! Route ordering is correct.

The new /comments route is correctly placed before the /comments/:id route to ensure Express matches the collection endpoint first. The authentication middleware follows the established pattern.

ghost/core/core/server/services/comments/CommentsController.js (1)

104-114: Clean controller implementation with proper separation of concerns.

The boolean parsing (!== 'false') correctly handles query string values. The controller appropriately transforms frame options into domain parameters before delegating to the service.

One observation: unlike adminBrowse, this method doesn't call #setImpersonationContext. This appears intentional since the browse-all endpoint is for moderation purposes and doesn't need member-specific data like "liked" status.

ghost/core/core/server/api/endpoints/comments.js (1)

101-120: Endpoint definition follows established patterns.

The empty validation object is appropriate since no parameters are required. Reusing browse permission (permissions: {method: 'browse'}) correctly leverages existing authorization without duplication.

ghost/core/core/server/services/comments/CommentsService.js (1)

189-210: Well-documented intentional behavior difference.

The explicit decision to skip checkEnabled() is correctly documented - admins should be able to moderate existing comments even when commenting is disabled site-wide. This differs from getAdminComments (Line 212-217) which still checks if comments are enabled.

The parentId logic is correct: undefined returns all comments, null filters to top-level only.

ghost/core/test/e2e-api/admin/comments.test.js (2)

12-24: Matcher updated correctly to include parent_id.

The addition of parent_id: nullable(anyObjectId) aligns with the serializer changes and correctly uses nullable() since top-level comments have null parent_id.


454-476: Tombstone shape is context-appropriate for per-post browse.

The tombstone matcher for per-post browse correctly omits the post relation since the endpoint is already post-scoped. The Browse All tombstone (Lines 1333-1340) correctly includes post since comments span multiple posts.

ref https://linear.app/ghost/issue/FEA-485

Adds GET /api/admin/comments/ endpoint for browsing all comments across
posts, supporting filtering, pagination, and include_nested parameter.
@rob-ghost rob-ghost force-pushed the rob/comments-browse-all-api branch from 30d820f to d6660f0 Compare December 11, 2025 20:09
Copy link
Contributor

@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: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 30d820f and d6660f0.

⛔ Files ignored due to path filters (3)
  • ghost/core/test/e2e-api/admin/__snapshots__/activity-feed.test.js.snap is excluded by !**/*.snap
  • ghost/core/test/e2e-api/admin/__snapshots__/comments.test.js.snap is excluded by !**/*.snap
  • ghost/core/test/e2e-api/members-comments/__snapshots__/comments.test.js.snap is excluded by !**/*.snap
📒 Files selected for processing (7)
  • ghost/core/core/server/api/endpoints/comments.js (1 hunks)
  • ghost/core/core/server/api/endpoints/utils/serializers/output/mappers/comments.js (2 hunks)
  • ghost/core/core/server/services/comments/CommentsController.js (1 hunks)
  • ghost/core/core/server/services/comments/CommentsService.js (1 hunks)
  • ghost/core/core/server/web/api/endpoints/admin/routes.js (1 hunks)
  • ghost/core/test/e2e-api/admin/comments.test.js (8 hunks)
  • ghost/core/test/e2e-api/members-comments/comments.test.js (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
  • ghost/core/core/server/services/comments/CommentsController.js
  • ghost/core/core/server/api/endpoints/comments.js
  • ghost/core/core/server/services/comments/CommentsService.js
  • ghost/core/core/server/web/api/endpoints/admin/routes.js
🧰 Additional context used
🧠 Learnings (6)
📚 Learning: 2025-06-18T10:56:19.906Z
Learnt from: 55sketch
Repo: TryGhost/Ghost PR: 23894
File: ghost/core/core/server/api/endpoints/comments.js:16-22
Timestamp: 2025-06-18T10:56:19.906Z
Learning: The validateCommentData function in ghost/core/core/server/api/endpoints/comments.js is not duplicated elsewhere in the codebase - it's a unique validation function that ensures either post_id or parent_id is provided for comment creation.

Applied to files:

  • ghost/core/test/e2e-api/members-comments/comments.test.js
  • ghost/core/test/e2e-api/admin/comments.test.js
  • ghost/core/core/server/api/endpoints/utils/serializers/output/mappers/comments.js
📚 Learning: 2025-11-24T17:29:43.865Z
Learnt from: CR
Repo: TryGhost/Ghost PR: 0
File: e2e/AGENTS.md:0-0
Timestamp: 2025-11-24T17:29:43.865Z
Learning: Applies to e2e/**/*.test.ts : Test suite names should follow the format 'Ghost Admin - Feature' or 'Ghost Public - Feature'

Applied to files:

  • ghost/core/test/e2e-api/admin/comments.test.js
📚 Learning: 2025-03-13T09:00:20.205Z
Learnt from: mike182uk
Repo: TryGhost/Ghost PR: 22471
File: apps/admin-x-activitypub/src/utils/pending-activity.ts:13-71
Timestamp: 2025-03-13T09:00:20.205Z
Learning: The pending activity utilities in the Ghost ActivityPub module are covered by tests in the file `apps/admin-x-activitypub/test/unit/utils/pending-activity.ts`.

Applied to files:

  • ghost/core/test/e2e-api/admin/comments.test.js
📚 Learning: 2025-11-24T17:29:43.865Z
Learnt from: CR
Repo: TryGhost/Ghost PR: 0
File: e2e/AGENTS.md:0-0
Timestamp: 2025-11-24T17:29:43.865Z
Learning: Applies to e2e/**/*.{ts,tsx} : Prefer less comments and give things clear names

Applied to files:

  • ghost/core/test/e2e-api/admin/comments.test.js
📚 Learning: 2025-05-29T10:37:26.369Z
Learnt from: ErisDS
Repo: TryGhost/Ghost PR: 23588
File: ghost/core/test/e2e-api/admin/backup.test.js:136-148
Timestamp: 2025-05-29T10:37:26.369Z
Learning: In the Ghost test framework, when testing CSV exports via the admin API, the response `body` field is empty while the actual CSV data is provided through the `text` field. Tests should use `expectEmptyBody()` and then validate the CSV content via `.expect(({text}) => ...)` - this is not contradictory behavior.

Applied to files:

  • ghost/core/test/e2e-api/admin/comments.test.js
📚 Learning: 2025-11-24T17:29:43.865Z
Learnt from: CR
Repo: TryGhost/Ghost PR: 0
File: e2e/AGENTS.md:0-0
Timestamp: 2025-11-24T17:29:43.865Z
Learning: Applies to e2e/**/*.test.ts : Use factory pattern for all test data creation instead of hard-coded data or direct database manipulation

Applied to files:

  • ghost/core/test/e2e-api/admin/comments.test.js
🧬 Code graph analysis (2)
ghost/core/test/e2e-api/members-comments/comments.test.js (2)
ghost/core/test/e2e-api/admin/comments.test.js (4)
  • tombstoneMatcher (455-457)
  • tombstoneMatcher (639-641)
  • tombstoneMatcher (1333-1340)
  • commentMatcher (1221-1235)
ghost/core/core/server/api/endpoints/comments.js (4)
  • result (63-66)
  • result (97-97)
  • result (117-117)
  • result (157-172)
ghost/core/core/server/api/endpoints/utils/serializers/output/mappers/comments.js (1)
ghost/core/core/server/services/comments/CommentsController.js (1)
  • _ (1-1)
🔇 Additional comments (12)
ghost/core/core/server/api/endpoints/utils/serializers/output/mappers/comments.js (2)

51-68: Well-implemented tombstone handling for deleted comments.

The early return pattern correctly short-circuits the standard mapping flow for deleted comments. The tombstone contains the minimal required information (id, parent_id, status) while preserving the post relation and recursively mapping any replies.

One consideration: the recursive commentMapper(reply, frame) call will correctly handle deleted replies within a deleted parent, returning tombstones for those as well.


6-15: LGTM - parent_id now included in serialized output.

Adding parent_id to commentFields enables the flat list representation for the new admin browse endpoint, allowing the UI to reconstruct the comment hierarchy.

ghost/core/test/e2e-api/members-comments/comments.test.js (3)

123-134: Tombstone matchers align with mapper output.

The tombstoneMatcher correctly reflects the minimal fields returned for deleted comments (id, parent_id). The tombstoneMatcherWithReplies helper properly expects full commentMatcher objects for replies, since published replies under deleted parents should have complete data.


680-686: Test correctly updated for tombstone semantics.

The test now properly verifies that a deleted parent with published replies is returned as a tombstone (status='deleted') with nested replies visible.


104-116: LGTM - commentMatcher updated with parent_id.

The addition of parent_id: nullable(anyObjectId) correctly matches the new serialization behavior where top-level comments have parent_id: null and replies have the parent's ObjectId.

ghost/core/test/e2e-api/admin/comments.test.js (7)

1219-1235: Well-structured commentMatcher for Browse All endpoint.

The matcher correctly includes:

  • parent_id: nullable(anyObjectId) for flat list representation
  • edited_at: nullable(anyISODateTime) for edit tracking
  • post relation (always included per PR requirements)

This aligns with the PR objective that the browse-all endpoint "always includes member and post relations."


1237-1254: Good test for cross-post comment browsing.

This test verifies the core functionality of the new endpoint: browsing comments across multiple posts in a single request.


1271-1284: Good test for include_nested=false behavior.

This test verifies that the include_nested=false query parameter correctly filters out replies, returning only top-level comments for efficient moderation views.


1372-1401: LGTM - Pagination and ordering tests.

Tests properly verify:

  • Pagination with limit and page parameters
  • Default ordering by created_at desc (newest first)
  • Explicit ordering with created_at asc

1350-1370: LGTM - Filter tests cover key use cases.

Filtering by status enables moderators to find hidden comments, and filtering by member_id allows viewing all comments from a specific member.


428-440: LGTM - Migrated to snapshot-based assertions.

The tests now use .matchBodySnapshot() for consistent validation, which improves maintainability and catches unexpected response changes.


9-24: LGTM - Updated imports and matcher.

The additions of anyString and nullable matchers, along with parent_id in membersCommentMatcher, properly support the new tombstone and flat-list functionality.

Comment on lines +454 to 476
// Tombstone - just id, parent_id, status (per-post browse doesn't load post relation)
const tombstoneMatcher = {
id: anyObjectId
};

const deletedComment = res.body.comments[0];
assert.equal(deletedComment.html, 'Comment 1');
// Reply has full content including member
const replyMatcher = {
id: anyObjectId,
parent_id: anyObjectId,
created_at: anyISODateTime,
member: {
id: anyObjectId,
uuid: anyUuid
}
};

const publishedReply = res.body.comments[0].replies[0];
assert.equal(publishedReply.html, 'Reply 1');
// Parent is tombstone with nested reply that has full content
await adminApi.get('/comments/post/' + postId + '/')
.expectStatus(200)
.matchBodySnapshot({
comments: [{...tombstoneMatcher, replies: [replyMatcher]}]
});
});
Copy link
Contributor

@coderabbitai coderabbitai bot Dec 11, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, locate the comments.js file that contains the mapper
find . -name "comments.js" -type f | head -20

Repository: TryGhost/Ghost

Length of output: 390


🏁 Script executed:

# Read the mapper file to see the tombstone implementation
cat -n ./ghost/core/core/server/api/endpoints/utils/serializers/output/mappers/comments.js | head -100

Repository: TryGhost/Ghost

Length of output: 3536


🏁 Script executed:

# Read the test file around lines 454-476 and beyond to see the full test context
sed -n '440,500p' ./ghost/core/test/e2e-api/admin/comments.test.js

Repository: TryGhost/Ghost

Length of output: 2311


🏁 Script executed:

# Search for other tombstoneMatcher definitions in the test file
rg -n "tombstoneMatcher" ./ghost/core/test/e2e-api/admin/comments.test.js -A 5

Repository: TryGhost/Ghost

Length of output: 1510


🏁 Script executed:

# Search for matchBodySnapshot implementation or usage patterns
rg -n "matchBodySnapshot" ./ghost/core/test/e2e-api/admin/comments.test.js | head -20

Repository: TryGhost/Ghost

Length of output: 880


🏁 Script executed:

# Search for matchBodySnapshot definition
rg -n "matchBodySnapshot" ./ghost/core/test/e2e-api/ -l | head -5

Repository: TryGhost/Ghost

Length of output: 319


🏁 Script executed:

# Look for the test setup and where matchBodySnapshot is defined
find ./ghost/core/test -name "*.js" -path "*/e2e-api/*" | xargs grep -l "matchBodySnapshot\s*=" | head -5

Repository: TryGhost/Ghost

Length of output: 40


🏁 Script executed:

# Look for the test setup file in e2e-api
ls -la ./ghost/core/test/e2e-api/ | head -20

Repository: TryGhost/Ghost

Length of output: 471


🏁 Script executed:

# Search for adminApi definition
rg -n "const adminApi" ./ghost/core/test/e2e-api/admin/comments.test.js | head -5

Repository: TryGhost/Ghost

Length of output: 40


🏁 Script executed:

# Look at the top of the test file to see imports and setup
head -50 ./ghost/core/test/e2e-api/admin/comments.test.js

Repository: TryGhost/Ghost

Length of output: 1436


🏁 Script executed:

# Look at the e2e-framework to understand how matchBodySnapshot works
find ./ghost/core/test/utils -name "*.js" -type f | xargs grep -l "matchBodySnapshot" | head -5

Repository: TryGhost/Ghost

Length of output: 40


🏁 Script executed:

# Find the e2e-framework setup file
find ./ghost/core/test/utils -name "*framework*" -o -name "*agent*" | head -10

Repository: TryGhost/Ghost

Length of output: 452


🏁 Script executed:

# Search for matchBodySnapshot in test-agent.js
grep -n "matchBodySnapshot" ./ghost/core/test/utils/agents/test-agent.js -A 30 | head -50

Repository: TryGhost/Ghost

Length of output: 40


🏁 Script executed:

# Search for matchBodySnapshot in admin-api-test-agent
grep -n "matchBodySnapshot" ./ghost/core/test/utils/agents/admin-api-test-agent.js -B 5 -A 30

Repository: TryGhost/Ghost

Length of output: 40


🏁 Script executed:

# Search more broadly for matchBodySnapshot in all test utilities
rg "matchBodySnapshot" ./ghost/core/test/utils/ -B 3 -A 10 | head -80

Repository: TryGhost/Ghost

Length of output: 40


🏁 Script executed:

# Look for snapshot files for the comments test
find ./ghost/core/test -name "*comments*" -path "*snapshot*" -o -path "*__snapshots__*" | head -10

Repository: TryGhost/Ghost

Length of output: 735


Add parent_id to tombstoneMatcher to match mapper output.

The tombstoneMatcher on lines 455-457 is incomplete. The mapper always includes parent_id in deleted comments (see comments.js lines 53-56), returning either the actual parent_id or null. The matcher should include parent_id: nullable(anyObjectId) to fully verify the API response, consistent with how other matchers like membersCommentMatcher handle this field. The same applies to the tombstoneMatcher on lines 639-641.

🤖 Prompt for AI Agents
In ghost/core/test/e2e-api/admin/comments.test.js around lines 454-476 (and also
update the similar tombstoneMatcher at lines 639-641), the tombstoneMatcher is
missing parent_id which the mapper always includes (either actual parent_id or
null); update both tombstoneMatcher objects to include parent_id:
nullable(anyObjectId) so the tests match the mapper output and mirror the
pattern used in membersCommentMatcher.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

parent_id is always null and stored in the snapshot so it doesn't need to be in the matcher. The matcher is to handle dynamic values which can't be stored in the snapshots, like ids.

Copy link
Contributor

Choose a reason for hiding this comment

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

@rob-ghost, thank you for the clarification! You're absolutely right—since parent_id is always null for these deleted comment tombstones, it's a static value that belongs in the snapshot rather than the matcher. The matcher should only handle dynamic values like IDs that change between test runs. I appreciate you explaining the distinction between what goes in matchers versus snapshots in your test framework.


🧠 Learnings used
Learnt from: 55sketch
Repo: TryGhost/Ghost PR: 23894
File: ghost/core/core/server/api/endpoints/comments.js:16-22
Timestamp: 2025-06-18T10:56:19.906Z
Learning: The validateCommentData function in ghost/core/core/server/api/endpoints/comments.js is not duplicated elsewhere in the codebase - it's a unique validation function that ensures either post_id or parent_id is provided for comment creation.

Learnt from: ErisDS
Repo: TryGhost/Ghost PR: 23588
File: ghost/core/test/e2e-api/admin/backup.test.js:136-148
Timestamp: 2025-05-29T10:37:26.369Z
Learning: In the Ghost test framework, when testing CSV exports via the admin API, the response `body` field is empty while the actual CSV data is provided through the `text` field. Tests should use `expectEmptyBody()` and then validate the CSV content via `.expect(({text}) => ...)` - this is not contradictory behavior.

Learnt from: CR
Repo: TryGhost/Ghost PR: 0
File: e2e/AGENTS.md:0-0
Timestamp: 2025-11-24T17:29:43.865Z
Learning: Applies to e2e/**/*.test.ts : Test suite names should follow the format 'Ghost Admin - Feature' or 'Ghost Public - Feature'

Learnt from: mike182uk
Repo: TryGhost/Ghost PR: 22471
File: apps/admin-x-activitypub/src/utils/pending-activity.ts:13-71
Timestamp: 2025-03-13T09:00:20.205Z
Learning: The pending activity utilities in the Ghost ActivityPub module are covered by tests in the file `apps/admin-x-activitypub/test/unit/utils/pending-activity.ts`.

Copy link
Member

@jonatansberg jonatansberg left a comment

Choose a reason for hiding this comment

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

From my point of view, this looks more than good enough. The duplication between the admin and non-admin implementations at the service and model layers feels a bit iffy to me, but that seems to be in line with the pre-existing patterns.

The comment by the rabbit on the test seems valid. Maybe you want to address that.

I'm also curious to hear what @kevinansfield thinks. I'm only able to do a somewhat superficial review at this point without having dove deeper into the backend.

@rob-ghost
Copy link
Contributor Author

Thanks @jonatansberg!

I agree, the duplication isn't ideal but I'm trying to carefully avoid pulling on too many threads given the speed we want to move at for this work in particular. I'd be keen to iterate on this whole backend section once we've unblocked the immediate work.

The code rabbit comment is not a problem because parent_id is in the snapshot and its null so it doesn't need to be in that specific matcher (the matchers are for dynamic things like IDs which we can't snapshot).

@rob-ghost rob-ghost merged commit ae8fbf2 into main Dec 15, 2025
37 checks passed
@rob-ghost rob-ghost deleted the rob/comments-browse-all-api branch December 15, 2025 10:18
rob-ghost added a commit that referenced this pull request Dec 15, 2025
ref #25700

Tombstones were introduced to return minimal data for deleted comments
that still had replies, but this approach had issues - the serialized
tombstones were missing expected attributes like `member` and `replies`.

Instead of returning partial data, the admin API now simply excludes
deleted comments entirely. The public API continues to include deleted
comments when they have replies (to preserve thread structure), but
returns them with full attributes and `html: null` so the UI can show
appropriate placeholder text.
rob-ghost added a commit that referenced this pull request Dec 15, 2025
ref #25700

Tombstones were introduced to return minimal data for deleted comments
that still had replies, but this approach had issues - the serialized
tombstones were missing expected attributes like `member` and `replies`.

Instead of returning partial data:
- Both Admin and Public APIs preserve thread structure by including
  deleted parents when they have published replies
- Deleted comments return full attributes with `html: null` so the UI
  can show appropriate placeholder text
- The new Browse All endpoint (`/comments`) uses `browseAll: true` to
  exclude deleted comments entirely (flat list, no thread preservation)
rob-ghost added a commit that referenced this pull request Dec 15, 2025
ref #25700

Tombstones were introduced to return minimal data for deleted comments
that still had replies, but this approach had issues - the serialized
tombstones were missing expected attributes like `member` and `replies`.

Instead of returning partial data:
- Both Admin and Public APIs preserve thread structure by including
  deleted parents when they have published replies
- Deleted comments return full attributes with `html: null` so the UI
  can show appropriate placeholder text
- The new Browse All endpoint (`/comments`) uses `browseAll: true` to
  exclude deleted comments entirely (flat list, no thread preservation)
rob-ghost added a commit that referenced this pull request Dec 15, 2025
ref #25700

Tombstones were introduced to return minimal data for deleted comments
that still had replies, but this approach had issues - the serialized
tombstones were missing expected attributes like `member` and `replies`.

Instead of returning partial data:
- Both Admin and Public APIs preserve thread structure by including
  deleted parents when they have published replies
- Deleted comments return full attributes with `html: null` so the UI
  can show appropriate placeholder text
- The new Browse All endpoint (`/comments`) uses `browseAll: true` to
  exclude deleted comments entirely (flat list, no thread preservation)
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.

3 participants