Skip to content

feat(api): add V2 JSON:API relationship endpoint for user game list entry resources#5041

Merged
wescopeland merged 4 commits into
RetroAchievements:masterfrom
wescopeland:v2-user-game-list-entries
Jul 11, 2026
Merged

feat(api): add V2 JSON:API relationship endpoint for user game list entry resources#5041
wescopeland merged 4 commits into
RetroAchievements:masterfrom
wescopeland:v2-user-game-list-entries

Conversation

@wescopeland

Copy link
Copy Markdown
Member

This PR adds a V2 JSON:API relationship endpoint for UserGameListEntry resources.

The resource is accessible through a user relationship route:

  • GET /api/v2/users/{user}/user-game-list-entries

Things worth noting:

  1. User game list entries are accessed exclusively through the user relationship route. There is no standalone index or show route.
  2. The model's type column is exposed as kind to avoid colliding with JSON:API's top-level resource type.
  3. The endpoint supports filter[kind] and filter[gameId].
  4. filter[kind] accepts play, achievement_set_request, develop, or all. Unknown kinds return a 400.
  5. Visibility is still owned by UserGameListEntryPolicy. play entries are visible to self+friends. achievement_set_request entries are visible to all auth'd callers. develop entries are not currently exposed through this endpoint (though filter[kind] does recognize the value).
  6. Results default sort by -createdAt, and the game relationship can be included with ?include=game.
  7. This builds on refactor: dispatch UserGameListEntryPolicy::view by list kind #5008's per-kind UserGameListEntryPolicy::view() dispatch.

V1 to V2 mapping:

API_GetUserWantToPlayList?u={user} -> /api/v2/users/{user}/user-game-list-entries?filter[kind]=play&include=game
API_GetUserSetRequests?u={user} -> /api/v2/users/{user}/user-game-list-entries?filter[kind]=achievement_set_request&include=game

How to test this PR:

# Get a user's visible game list entries
curl --request GET \
  --url http://localhost:64000/api/v2/users/Scott/user-game-list-entries \
  --header 'X-API-Key: {redacted}'

# Get Want to Play entries
curl --request GET \
  --url 'http://localhost:64000/api/v2/users/Scott/user-game-list-entries?filter[kind]=play&include=game' \
  --header 'X-API-Key: {redacted}'

# Get achievement set request entries
curl --request GET \
  --url 'http://localhost:64000/api/v2/users/Scott/user-game-list-entries?filter[kind]=achievement_set_request&include=game' \
  --header 'X-API-Key: {redacted}'

# Filter to a specific game
curl --request GET \
  --url 'http://localhost:64000/api/v2/users/Scott/user-game-list-entries?filter[gameId]=1' \
  --header 'X-API-Key: {redacted}'

# Apply custom pagination
curl --request GET \
  --url 'http://localhost:64000/api/v2/users/Scott/user-game-list-entries?page[number]=2&page[size]=25' \
  --header 'X-API-Key: {redacted}'

@wescopeland
wescopeland requested a review from a team July 3, 2026 16:39
@greptile-apps

greptile-apps Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Confidence Score: 5/5

Safe to merge. Auth logic is correctly layered and all access paths are covered by tests.

The authorization design is sound: the top-level policy gate requires authentication, and relatableQuery enforces per-kind access before handing off to the framework filter pipeline. The default-to-Play fallback and the Develop self-only change are both tested end-to-end. No missing guards or logic gaps found.

No files require special attention.

Reviews (4): Last reviewed commit: "Merge branch 'master' into v2-user-game-..." | Re-trigger Greptile

Comment thread app/Api/V2/UserGameListEntries/UserGameListEntrySchema.php Outdated
Comment thread app/Api/V2/UserGameListEntries/UserGameListEntryKindFilter.php
Comment thread app/Api/V2/UserGameListEntries/UserGameListEntryKindFilter.php Outdated
Comment thread app/Api/V2/UserGameListEntries/UserGameListEntrySchema.php Outdated
Comment thread app/Api/V2/UserGameListEntries/UserGameListEntrySchema.php Outdated
'code' => 'invalid_filter',
'title' => 'Invalid Filter',
'detail' => "Unknown user game list kind [{$normalizedValue}].",
]);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Non-blocking question: Why 400 and not 422?

400 Bad Request: The server cannot understand the request because the syntax is broken (e.g., a missing comma in your JSON, or malformed text format).

422 Unprocessable Content: The JSON is structurally flawless and can be parsed, but the values inside fail server validation.

The Connect API uses 422 for invalid parameter values:

protected function invalidParameter(string $message): array
{
return [
'Success' => false,
'Status' => 422,
'Code' => 'invalid_parameter',
'Error' => $message,
];
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Good question. I think we should keep a 400 here, mainly because it follows the JSON:API spec's documented pattern. The spec never uses a 422 anywhere, and whenever it mentions a query param the server can't handle, it consistently calls for throwing a 400:

If a server is unable to identify a relationship path or does not support inclusion of resources from a path, it MUST respond with 400 Bad Request.

If the server does not support sorting as specified in the query parameter sort, it MUST return 400 Bad Request.

If a server encounters a query parameter that does not follow the naming conventions above, or the server does not know how to process it as a query parameter from this specification, it MUST return 400 Bad Request.

laravel-json-api's query validation also defaults to a 400. This seems to be a consistent theme with JSON:API endpoints in particular.

@Jamiras Jamiras Jul 11, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I asked AI:

The JSON:API specification requires 400 Bad Request for invalid filter values instead of 422 Unprocessable Entity. This is because 422 is reserved strictly for semantically invalid request bodies (data payload errors), whereas filters are treated as query parameters and fall under URL syntax or protocol errors.

I wonder if we should update the connect API. While most of the parameters are sent as POST data, they're still just URI query parameters.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

No strong feeling from me on the Connect API. If you think the 422 makes more sense for the Connect API, I think we should keep it. It's a custom RPC-style API, non-RESTful, that we can shape however we want.

@wescopeland
wescopeland merged commit b7df1d1 into RetroAchievements:master Jul 11, 2026
22 checks passed
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.

2 participants