Conversation
Made-with: Cursor
20 flat routes under /api/research/ with query params (matching Recoup API pattern). Shared infrastructure: - getChartmetricToken: token exchange - proxyToChartmetric: auth + proxy + strip obj wrapper - resolveArtist: name/UUID → Chartmetric ID resolution - handleArtistResearch: DRY handler for artist-scoped endpoints Endpoints: search, lookup, profile, metrics, audience, cities, similar, urls, instagram-posts, playlists, albums, tracks, career, insights, track, playlist, curator, discover, genres, festivals. 17 tests passing (token: 4, proxy: 6, resolve: 7). Web/deep research endpoints deferred to separate PR. Made-with: Cursor
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds ~20 Next.js API routes under app/api/research/*, ~40 research helpers and MCP tool registrations in lib/research and lib/mcp/tools/research, Chartmetric/Parallel/Exa client utilities, a Chartmetric token helper, and a new content template entry "album-record-store". Routes handle CORS and delegate logic to new lib handlers. Changes
Sequence Diagram(s)sequenceDiagram
rect rgba(220,220,255,0.5)
participant Client
participant Route as "API Route"
participant Handler as "Research Handler"
end
rect rgba(220,255,220,0.5)
participant Auth as "validateAuthContext"
participant Credits as "deductCredits"
end
rect rgba(255,220,220,0.5)
participant Chartmetric
participant Token as "getChartmetricToken"
end
Client->>Route: GET /api/research/{endpoint}?artist=...
Route->>Handler: delegate request to getResearch*Handler(request)
Handler->>Auth: validateAuthContext(request)
Auth-->>Handler: accountId or NextResponse(401)
alt authenticated
Handler->>Credits: deductCredits(accountId, cost)
Credits-->>Handler: success or throw
Handler->>Token: getChartmetricToken()
Token-->>Handler: access_token
Handler->>Chartmetric: GET /{built-path}?{params} (Bearer token)
Chartmetric-->>Handler: { status, data }
Handler-->>Route: NextResponse(200, payload) + CORS
else unauthenticated
Auth-->>Route: NextResponse(401) + CORS
end
Route-->>Client: HTTP response + CORS
sequenceDiagram
participant Client
participant MCP as "MCP Server"
participant Register as "registerAllResearchTools"
participant Tool as "research_* Tool"
participant Resolve as "resolveArtist"
participant Proxy as "proxyToChartmetric"
MCP->>Register: registerAllResearchTools(server)
Register->>Tool: server.registerTool('research_artist'...)
Client->>MCP: invoke research_artist({ artist })
MCP->>Tool: Tool.handler(args)
Tool->>Resolve: resolveArtist(args.artist)
Resolve->>Proxy: GET /search?q=...&type=artists
Proxy-->>Resolve: { artists: [...] }
Tool->>Proxy: GET /artist/{id}/...
Proxy-->>Tool: { data }
Tool-->>MCP: getToolResultSuccess(data)
MCP-->>Client: tool result
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ❌ 1❌ Failed checks (1 warning)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ 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: 9
🧹 Nitpick comments (31)
app/api/research/career/route.ts (1)
5-7: Upgrade placeholder JSDoc to actual API documentation.Line 5-Line 7 and Line 12-Line 15 should describe endpoint behavior and request expectations instead of empty placeholders.
As per coding guidelines, "
app/api/**/route.ts: All API routes should have JSDoc comments".Also applies to: 12-15
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/api/research/career/route.ts` around lines 5 - 7, Replace the empty JSDoc blocks with real API documentation describing the route behavior and request expectations: document the exported route handler functions (e.g., GET/POST handlers in this file), the endpoint purpose, accepted request parameters/body shape, expected response shape and status codes, and any authorization or error cases; update both JSDoc blocks (lines around the top of the file and the later block) so they clearly state input/output contracts and side effects to satisfy the "app/api/**/route.ts: All API routes should have JSDoc comments" guideline.app/api/research/curator/route.ts (2)
1-18: Consider a shared route-wrapper utility for research endpoints.Line 8-Line 10 and Line 16-Line 17 follow the same passthrough pattern repeated across these new research routes. A small factory/helper would reduce repetition and keep future endpoint additions safer.
As per coding guidelines, "
**/*.{ts,tsx}: Extract shared logic into reusable utilities following Don't Repeat Yourself (DRY) principle".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/api/research/curator/route.ts` around lines 1 - 18, The OPTIONS and GET handlers in this file duplicate passthrough logic (calling getCorsHeaders and delegating to getResearchCuratorHandler); factor this pattern into a reusable route wrapper (e.g., a utility that builds standard handlers for research endpoints) and replace the explicit OPTIONS and GET implementations with calls to that wrapper; reference the existing symbols OPTIONS, GET, getCorsHeaders and getResearchCuratorHandler when implementing the factory so it returns a standard OPTIONS response with CORS headers and a GET handler that delegates to the provided handler function.
5-7: Replace placeholder JSDoc with endpoint-level contract details.The blocks at Line 5-Line 7 and Line 12-Line 15 are empty placeholders. Please document method intent, required query params, and key error cases.
Proposed doc-only update
-/** - * - */ +/** + * OPTIONS /api/research/curator + * Handles CORS preflight for the curator research endpoint. + */ export async function OPTIONS() { return new NextResponse(null, { status: 200, headers: getCorsHeaders() }); } -/** - * - * `@param` request - */ +/** + * GET /api/research/curator + * Returns curator research data for the provided query parameters. + * + * `@param` request Incoming request with query params. + */ export async function GET(request: NextRequest) { return getResearchCuratorHandler(request); }As per coding guidelines, "
app/api/**/route.ts: All API routes should have JSDoc comments".Also applies to: 12-15
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/api/research/curator/route.ts` around lines 5 - 7, The file has empty JSDoc placeholders for the API endpoint; replace each placeholder block with an endpoint-level contract: a short description of intent, the HTTP method(s) handled (e.g., GET/POST), required query parameters or body schema (names and types), expected success response shape and status code, and key error cases with their status codes (validation, auth, not-found, server errors). Update the JSDoc immediately above the route handler functions (the exported GET/POST handlers in app/api/research/curator/route.ts) so it documents required params, example requests/responses, and any thrown errors.app/api/research/lookup/route.ts (1)
5-7: Add substantive JSDoc for route contract clarity.At Line 5-Line 7 and Line 12-Line 15, please replace empty placeholders with method intent, required params, and key error responses.
As per coding guidelines, "
app/api/**/route.ts: All API routes should have JSDoc comments".Also applies to: 12-15
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/api/research/lookup/route.ts` around lines 5 - 7, Add substantive JSDoc above the exported route handler functions (e.g., the exported async GET/POST handlers in this file) replacing the empty blocks: state the HTTP method and high-level intent, list required query or body parameters (names, types, whether optional), describe the successful response shape and status code (e.g., 200 with JSON { ... }), and enumerate key error responses and their status codes (e.g., 400 for validation errors, 401/403 for auth, 404 if no resource, 500 for server errors). Include any auth/permission requirements and examples of expected input/output where helpful so the route contract is clear to callers and maintainers.lib/research/getResearchProfileHandler.ts (1)
7-9: Prefer provider-agnostic wording in endpoint docs.Line 7 references “Chartmetric artist profile.” Since this API layer is intended to stay provider-agnostic, consider generic wording in the handler JSDoc.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/research/getResearchProfileHandler.ts` around lines 7 - 9, Update the JSDoc for getResearchProfileHandler to remove provider-specific wording (remove "Chartmetric") and use generic phrasing like "Returns the full artist profile" or "Returns the full research artist profile" while keeping the note about requiring the `artist` query param (name, numeric ID, or UUID); locate the JSDoc comment above the getResearchProfileHandler function and replace the provider-specific sentence with the provider-agnostic wording.app/api/research/cities/route.ts (1)
5-7: Provide meaningful JSDoc for both handlers.At Line 5-Line 7 and Line 12-Line 15, the comments are placeholders; add concrete method/param/response documentation.
Based on learnings, "Applies to app/api/**/route.ts : All API routes should have JSDoc comments".
Also applies to: 12-15
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/api/research/cities/route.ts` around lines 5 - 7, Replace the placeholder comment blocks with meaningful JSDoc for each exported route handler: add a JSDoc above export async function GET describing the endpoint purpose, accepted query params, the shape of the returned JSON and possible status codes; add a JSDoc above export async function POST describing the request body schema, validation rules, created resource format, status codes (e.g., 201/400/500), and any auth/permission requirements or thrown errors; ensure each JSDoc includes `@param` for request/query/body, `@returns` for response shape and status, and `@throws` (or `@throws`) for error cases so the handlers are fully documented.app/api/research/similar/route.ts (1)
5-7: Replace placeholder comments with concrete JSDoc.Line 5-Line 7 and Line 12-Line 15 should document endpoint behavior and required query semantics.
As per coding guidelines, "
app/api/**/route.ts: All API routes should have JSDoc comments".Also applies to: 12-15
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/api/research/similar/route.ts` around lines 5 - 7, The file contains placeholder block comments; replace them with concrete JSDoc for the API route: add a top-level JSDoc that describes the endpoint purpose (what "similar" returns), accepted HTTP method(s) and example usage, and for the exported GET handler (or any exported function named GET) add JSDoc describing required query parameters (names, types, whether required), expected response shape and status codes, and any error conditions; ensure the JSDoc explicitly documents required query semantics (e.g., "q: string, required", "limit: number, optional, default 10") and update both comment blocks referenced (the header comment and the block above the GET handler) accordingly.app/api/research/discover/route.ts (1)
5-7: Fill in the JSDoc placeholders with real route docs.Line 5-Line 7 and Line 12-Line 15 currently provide no contract detail. Please include endpoint purpose, required query params, and expected error statuses.
Based on learnings, "Applies to app/api/**/route.ts : All API routes should have JSDoc comments".
Also applies to: 12-15
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/api/research/discover/route.ts` around lines 5 - 7, Replace the empty JSDoc blocks with a real route contract for the exported route handler (e.g., the GET handler or exported default function in this file): document the endpoint purpose (what "discover" returns), list required and optional query parameters by name and type (e.g., q: string, page: number, limit: number), describe the successful response shape and status code (200 + JSON schema), and enumerate expected error statuses and conditions (400 for bad request/missing params, 401 for unauthorized, 500 for server errors) including example request and response snippets; ensure the JSDoc tags `@route`, `@param`, `@returns`, and `@throws/`@errors are present and match the handler function name (GET or default export) so consumers know the contract.app/api/research/route.ts (1)
8-10: Consider centralizing preflight response creation to reduce repeated route boilerplate.
OPTIONS()is duplicated across the new research routes; extracting a tiny helper/factory would reduce maintenance overhead and keep behavior uniform.As per coding guidelines:
**/*.{ts,tsx}: Extract shared logic into reusable utilities following Don't Repeat Yourself (DRY) principle.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/api/research/route.ts` around lines 8 - 10, Extract the duplicated preflight response creation into a reusable helper (e.g., preflightResponse or createPreflightResponse) that returns new NextResponse(null, { status: 200, headers: getCorsHeaders() }) and replace each route's OPTIONS() implementation with a single call to that helper; ensure the helper imports/uses getCorsHeaders and NextResponse so all research routes call the same function for uniform CORS preflight handling.app/api/research/profile/route.ts (1)
5-7: Document this route with meaningful JSDoc (not placeholder blocks).Lines 5-7 and Lines 12-15 should describe preflight behavior and the GET contract so the route is self-explanatory.
📝 Proposed doc update
/** - * + * OPTIONS /api/research/profile + * + * Handles CORS preflight for the profile research endpoint. */ export async function OPTIONS() { return new NextResponse(null, { status: 200, headers: getCorsHeaders() }); } /** - * + * GET /api/research/profile + * + * Returns profile-level research data for the requested artist. + * * `@param` request */ export async function GET(request: NextRequest) { return getResearchProfileHandler(request); }Based on learnings: Applies to app/api/**/route.ts : All API routes should have JSDoc comments.
Also applies to: 12-15
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/api/research/profile/route.ts` around lines 5 - 7, Replace the placeholder JSDoc in app/api/research/profile/route.ts with a concise, meaningful comment that documents preflight behavior and the GET contract: state that the route responds to OPTIONS preflight with appropriate CORS headers (if applicable) and that the exported GET function accepts any query or auth requirements (e.g., required headers, session) and returns JSON describing the user profile, list the expected response shape (fields and types), possible status codes (200, 401, 400, 500) and error conditions, and note content-type and any caching or rate-limit behavior; reference the exported GET handler name and any helper functions used so a reader can find the implementation quickly.app/api/research/playlist/route.ts (1)
5-7: Replace empty JSDoc blocks with endpoint-specific descriptions.Lines 5-7 and Lines 12-15 currently don’t document behavior, params, or intent.
📝 Proposed doc update
/** - * + * OPTIONS /api/research/playlist + * + * Handles CORS preflight for the playlist research endpoint. */ export async function OPTIONS() { return new NextResponse(null, { status: 200, headers: getCorsHeaders() }); } /** - * + * GET /api/research/playlist + * + * Returns playlist-level research data for the requested playlist context. + * * `@param` request */ export async function GET(request: NextRequest) { return getResearchPlaylistHandler(request); }As per coding guidelines:
app/api/**/route.ts: All API routes should have JSDoc comments.Also applies to: 12-15
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/api/research/playlist/route.ts` around lines 5 - 7, Replace the empty JSDoc blocks above the route handler functions with concise, endpoint-specific JSDoc for the exported handlers (e.g., GET and POST functions) that describes the endpoint purpose, accepted parameters or request body shape, expected response shape/status codes, authentication/authorization requirements, and possible error conditions; update the two blank comment blocks to include summary, `@param` for Request/NextRequest where applicable, `@returns` for Response/NextResponse, and any side effects so future readers and automated linters have meaningful documentation.app/api/research/genres/route.ts (1)
5-7: Fill in the JSDoc placeholders with actual route contract details.Lines 5-7 and Lines 12-15 are empty blocks today, which makes the route self-documentation effectively missing.
📝 Proposed doc update
/** - * + * OPTIONS /api/research/genres + * + * Handles CORS preflight for the genres research endpoint. */ export async function OPTIONS() { return new NextResponse(null, { status: 200, headers: getCorsHeaders() }); } /** - * + * GET /api/research/genres + * + * Returns available genres from the research provider. + * * `@param` request */ export async function GET(request: NextRequest) { return getResearchGenresHandler(request); }As per coding guidelines:
app/api/**/route.ts: All API routes should have JSDoc comments.Also applies to: 12-15
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/api/research/genres/route.ts` around lines 5 - 7, Replace the empty JSDoc blocks in this route with concrete contract details: update the top-of-file JSDoc (first empty block) to include a short description of the route purpose, HTTP method(s) it supports, expected path, authorization requirements, and primary response shape; then update the second empty JSDoc (lines 12-15) immediately above the exported handler/function to document parameters (path/query/body), example request/response types or status codes, and any error conditions. Reference the route's exported handler (the default export or the named GET/POST function in this file) and ensure the JSDoc contains method, input contract, and output contract so the route is fully self-documented.app/api/research/metrics/route.ts (1)
1-18: Consider extracting a shared research route factory to reduce boilerplate.This route repeats the same
OPTIONS+ delegatedGETstructure used across many files; centralizing it will reduce copy/paste drift.♻️ Refactor sketch
-import { NextRequest, NextResponse } from "next/server"; -import { getCorsHeaders } from "@/lib/networking/getCorsHeaders"; +import { createResearchGetRoute } from "@/lib/research/createResearchGetRoute"; import { getResearchMetricsHandler } from "@/lib/research/getResearchMetricsHandler"; -/** - * - */ -export async function OPTIONS() { - return new NextResponse(null, { status: 200, headers: getCorsHeaders() }); -} - -/** - * - * `@param` request - */ -export async function GET(request: NextRequest) { - return getResearchMetricsHandler(request); -} +export const { OPTIONS, GET } = createResearchGetRoute(getResearchMetricsHandler);// lib/research/createResearchGetRoute.ts import { NextRequest, NextResponse } from "next/server"; import { getCorsHeaders } from "@/lib/networking/getCorsHeaders"; export function createResearchGetRoute( handler: (request: NextRequest) => Promise<NextResponse>, ) { return { async OPTIONS() { return new NextResponse(null, { status: 200, headers: getCorsHeaders() }); }, async GET(request: NextRequest) { return handler(request); }, }; }As per coding guidelines "
**/*.{ts,tsx}: Extract shared logic into reusable utilities following Don't Repeat Yourself (DRY) principle".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/api/research/metrics/route.ts` around lines 1 - 18, This route duplicates the common OPTIONS + delegated GET pattern; extract a reusable factory (e.g., createResearchGetRoute) that returns { async OPTIONS() { return new NextResponse(null, { status: 200, headers: getCorsHeaders() }); }, async GET(request) { return handler(request); } } and update this file to export the result of createResearchGetRoute(getResearchMetricsHandler) so OPTIONS and GET behavior is centralized; reference getCorsHeaders, createResearchGetRoute, and getResearchMetricsHandler when making the change.app/api/research/audience/route.ts (1)
5-17: Empty JSDoc comments need documentation.Same pattern as other route files. Add meaningful descriptions for the audience endpoint's purpose and parameters.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/api/research/audience/route.ts` around lines 5 - 17, Add meaningful JSDoc comments above the OPTIONS and GET exports to describe the endpoint's purpose and behavior: document that OPTIONS returns CORS preflight with getCorsHeaders and that GET accepts a NextRequest and delegates to getResearchAudienceHandler to retrieve research audience data; include descriptions of parameters (request: NextRequest), return values (NextResponse), and any side effects or headers so the comments match the style used in other route files.app/api/research/albums/route.ts (1)
5-17: Empty JSDoc comments need meaningful descriptions.The JSDoc blocks are present but lack any actual documentation. As per coding guidelines, all API routes should have descriptive JSDoc comments explaining the endpoint's purpose, expected parameters, and response structure.
📝 Suggested JSDoc improvements
-/** - * - */ +/** + * Handles CORS preflight requests for the albums endpoint. + * `@returns` Empty response with CORS headers. + */ export async function OPTIONS() { return new NextResponse(null, { status: 200, headers: getCorsHeaders() }); } -/** - * - * `@param` request - */ +/** + * GET /api/research/albums + * Retrieves album data for a given artist from Chartmetric. + * Requires `artist` query parameter. + * + * `@param` request - The incoming Next.js request object + * `@returns` JSON response with album data or error + */ export async function GET(request: NextRequest) { return getResearchAlbumsHandler(request); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/api/research/albums/route.ts` around lines 5 - 17, Replace the empty JSDoc blocks above the exported OPTIONS and GET functions with descriptive comments: document OPTIONS to explain it responds to preflight CORS checks, returns a 200 with headers from getCorsHeaders(), and mention any headers set; document GET to describe that it accepts a NextRequest, calls getResearchAlbumsHandler(request) to fetch research album data, outline expected query parameters (if any), possible response shapes and status codes, and note that it uses NextRequest/NextResponse types; place these JSDoc comments immediately above the OPTIONS and GET function declarations to satisfy documentation guidelines.app/api/research/festivals/route.ts (1)
5-17: Empty JSDoc comments—same pattern as other routes.Populate the JSDoc blocks with meaningful endpoint documentation describing the festivals endpoint purpose and required parameters.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/api/research/festivals/route.ts` around lines 5 - 17, Replace the empty JSDoc blocks above the OPTIONS and GET functions with descriptive endpoint documentation: for OPTIONS describe this endpoint supports CORS preflight and returns 200 with CORS headers; for GET document the festivals endpoint purpose (returns research festivals list), accepted inputs (the NextRequest/query parameters like page, limit, filters or any expected headers/auth token), response shape (summary of returned data items and status codes), and any errors; reference the GET function and getResearchFestivalsHandler to ensure the documented parameters match what that handler reads.app/api/research/instagram-posts/route.ts (1)
5-17: Empty JSDoc comments need meaningful descriptions.Same issue as other route files—the JSDoc blocks are scaffolded but empty. Consider documenting the endpoint's purpose and the
artistquery parameter requirement.📝 Suggested JSDoc improvements
-/** - * - */ +/** + * Handles CORS preflight requests for the Instagram posts endpoint. + */ export async function OPTIONS() { return new NextResponse(null, { status: 200, headers: getCorsHeaders() }); } -/** - * - * `@param` request - */ +/** + * GET /api/research/instagram-posts + * Returns recent Instagram posts for the given artist via Chartmetric's DeepSocial integration. + * Requires `artist` query parameter. + * + * `@param` request - The incoming Next.js request object + */ export async function GET(request: NextRequest) { return getResearchInstagramPostsHandler(request); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/api/research/instagram-posts/route.ts` around lines 5 - 17, The JSDoc blocks above the OPTIONS and GET handlers are empty; update them to document the endpoint purpose and parameters: add a meaningful description for the OPTIONS() function stating it returns CORS preflight responses, and for GET(request: NextRequest) describe that it handles fetching research Instagram posts, requires an "artist" query parameter (describe expected format and behavior), and that it delegates to getResearchInstagramPostsHandler; reference the functions OPTIONS, GET, and getResearchInstagramPostsHandler in the docs so readers know where the logic lives.app/api/research/insights/route.ts (1)
5-17: Empty JSDoc comments require documentation.Fill in the JSDoc blocks to describe the insights endpoint—what noteworthy insights it returns and the required
artistparameter.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/api/research/insights/route.ts` around lines 5 - 17, The file contains empty JSDoc blocks above the exported handlers OPTIONS and GET; update them to document the insights endpoint by describing what notable insights the GET handler returns (e.g., trend summaries, top tracks, listener demographics, or other research insights) and the required query or route parameter "artist" that the endpoint expects; add brief descriptions for the OPTIONS function (returns CORS headers and 200) and for GET (accepts a NextRequest, validates the required artist parameter, and delegates to getResearchInsightsHandler), referencing the functions OPTIONS, GET, getResearchInsightsHandler, and getCorsHeaders so maintainers can locate and understand the behavior.lib/research/proxyToChartmetric.ts (1)
48-50: Consider wrapping JSON parsing in try/catch.
response.json()can throw if the response body isn't valid JSON, which would result in an unhandled exception rather than a structured error response.🛡️ Defensive JSON parsing
- const json = await response.json(); - - const data = json.obj !== undefined ? json.obj : json; + let json: unknown; + try { + json = await response.json(); + } catch { + return { + data: { error: "Invalid JSON response from Chartmetric" }, + status: 502, + }; + } + + const data = typeof json === "object" && json !== null && "obj" in json + ? (json as { obj: unknown }).obj + : json;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/research/proxyToChartmetric.ts` around lines 48 - 50, Wrap the call to response.json() in a try/catch inside proxyToChartmetric (around the lines creating json and data) to avoid unhandled exceptions; if JSON parsing fails, catch the error (e.g., SyntaxError), log or attach the parsing error and the raw response text, and return or throw a structured error object/response instead of letting the exception bubble (ensure downstream code that uses json/data handles this error shape). Make sure to preserve existing behavior when parsing succeeds by assigning json to data as currently done.lib/research/resolveArtist.ts (1)
12-13: JSDoc return description doesn't match actual return type.The docstring says "or null if not found" but the function returns
{ error: string }on failure, notnull. The actual discriminated union return type is correct and well-designed.📝 Fix documentation
- * `@returns` The Chartmetric artist ID, or null if not found + * `@returns` Object with `id` on success, or `error` message on failure🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/research/resolveArtist.ts` around lines 12 - 13, The JSDoc for resolveArtist is inaccurate: it states the function returns "The Chartmetric artist ID, or null if not found" but the implementation returns a discriminated union that includes an error object ({ error: string }). Update the `@returns` description in the resolveArtist JSDoc to accurately describe the actual return shape (e.g., success result containing the Chartmetric artist ID or a failure object with an error string), referencing the resolveArtist function and its union return type so the docs match the implementation.lib/research/getResearchCuratorHandler.ts (2)
39-39: Validateplatformandidparameters before path interpolation.User-controlled values are directly embedded in the API path. While
idshould be alphanumeric andplatformshould match known values, there's no validation enforcing this. Consider validatingplatformagainst an allowlist and ensuringidcontains only expected characters.🛡️ Suggested validation
+const VALID_CURATOR_PLATFORMS = ["spotify", "applemusic", "deezer"] as const; + export async function getResearchCuratorHandler(request: NextRequest): Promise<NextResponse> { // ... auth validation ... const { searchParams } = new URL(request.url); const platform = searchParams.get("platform"); const id = searchParams.get("id"); if (!platform || !id) { return NextResponse.json( { status: "error", error: "platform and id parameters are required" }, { status: 400, headers: getCorsHeaders() }, ); } + if (!VALID_CURATOR_PLATFORMS.includes(platform as typeof VALID_CURATOR_PLATFORMS[number])) { + return NextResponse.json( + { status: "error", error: `Invalid platform. Must be one of: ${VALID_CURATOR_PLATFORMS.join(", ")}` }, + { status: 400, headers: getCorsHeaders() }, + ); + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/research/getResearchCuratorHandler.ts` at line 39, In getResearchCuratorHandler, validate the incoming platform and id before interpolating them into the path passed to proxyToChartmetric: enforce platform against a small allowlist (e.g., known enum values) and ensure id matches an expected regex (alphanumeric, hyphen/underscore if allowed) and reject or return a 400 error for invalid inputs; only after validation construct the path `/curator/${platform}/${id}` and call proxyToChartmetric so no unvalidated user-controlled characters reach the backend.
14-57: Consider extracting shared handler logic for non-artist lookups.This handler duplicates the auth → credits → proxy → response pattern found in
getResearchGenresHandler,getResearchFestivalsHandler, and others. A shared utility similar tohandleArtistResearchbut for non-artist lookups could reduce boilerplate. As per coding guidelines, "Extract shared logic into reusable utilities following Don't Repeat Yourself (DRY) principle."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/research/getResearchCuratorHandler.ts` around lines 14 - 57, getResearchCuratorHandler duplicates the auth→credits→proxy→response flow used by getResearchGenresHandler, getResearchFestivalsHandler, etc.; extract that shared flow into a new utility (e.g., handleNonArtistResearch or handleResearchLookup) that accepts parameters for building the proxy path (or a path template), creditsToDeduct, and any response shaping, then update getResearchCuratorHandler to call this utility instead of inlining validateAuthContext, deductCredits, proxyToChartmetric and getCorsHeaders logic; reuse the same utility from getResearchGenresHandler/getResearchFestivalsHandler and leave handleArtistResearch untouched for artist-specific differences.lib/research/getResearchPlaylistsHandler.ts (1)
20-25: Validateplatformandstatusagainst an allowlist before path interpolation.User-controlled values are directly embedded into the Chartmetric API path without validation. This pattern (also present in
getResearchMetricsHandler.ts) could allow path injection if unexpected characters are passed. While the URL constructor provides some protection, explicit validation is safer and more defensive.🛡️ Suggested validation
+const VALID_PLATFORMS = ["spotify", "applemusic", "deezer", "amazon", "itunes"] as const; +const VALID_STATUSES = ["current", "past"] as const; + export async function getResearchPlaylistsHandler(request: NextRequest) { const { searchParams } = new URL(request.url); - const platform = searchParams.get("platform") || "spotify"; - const status = searchParams.get("status") || "current"; + const platformParam = searchParams.get("platform") || "spotify"; + const statusParam = searchParams.get("status") || "current"; + + const platform = VALID_PLATFORMS.includes(platformParam as typeof VALID_PLATFORMS[number]) + ? platformParam + : "spotify"; + const status = VALID_STATUSES.includes(statusParam as typeof VALID_STATUSES[number]) + ? statusParam + : "current";🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/research/getResearchPlaylistsHandler.ts` around lines 20 - 25, Validate the user-controlled platform and status before interpolating them into the Chartmetric path: in getResearchPlaylistsHandler.ts (and similarly in getResearchMetricsHandler.ts) replace the current unvalidated platform and status usage by checking the platform and status variables against explicit allowlists (e.g., ALLOWED_PLATFORMS and ALLOWED_STATUSES), and if a value is not allowed either coerce to a safe default (e.g., "spotify"/"current") or return a 400 error; do this check before calling handleArtistResearch and use the validated values when building the cmId => `/artist/${cmId}/${platform}/${status}/playlists` path. Ensure the validation uses the exact variable names platform and status so the substitution is always from trusted values.lib/research/getResearchPlaylistHandler.ts (1)
19-39: Validateplatformandidparameters to prevent path injection.Same concern as
getResearchCuratorHandler- user-controlled values are directly interpolated into the API path on line 39. Theplatformshould be validated against known streaming platforms, andidshould be validated to contain only alphanumeric characters or hyphens.🛡️ Suggested validation
+const VALID_PLAYLIST_PLATFORMS = ["spotify", "applemusic", "deezer", "amazon"] as const; +const PLAYLIST_ID_PATTERN = /^[\w-]+$/; + // After presence check: + if (!VALID_PLAYLIST_PLATFORMS.includes(platform as typeof VALID_PLAYLIST_PLATFORMS[number])) { + return NextResponse.json( + { status: "error", error: "Invalid platform" }, + { status: 400, headers: getCorsHeaders() }, + ); + } + + if (!PLAYLIST_ID_PATTERN.test(id)) { + return NextResponse.json( + { status: "error", error: "Invalid playlist ID format" }, + { status: 400, headers: getCorsHeaders() }, + ); + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/research/getResearchPlaylistHandler.ts` around lines 19 - 39, Validate incoming query params in getResearchPlaylistHandler before calling proxyToChartmetric: ensure platform matches a whitelist of known streaming platforms (e.g., an array like ["spotify","apple","youtube","deezer"]) and ensure id matches a strict pattern (only alphanumerics, hyphens, underscores or whatever spec you choose, e.g., /^[A-Za-z0-9_-]+$/). If either check fails return a 400 NextResponse with an error message and CORS headers (same shape used elsewhere). Only after both validations pass call proxyToChartmetric(`/playlist/${platform}/${id}`) so untrusted input cannot inject paths. Ensure the validation is done prior to deductCredits or bail out early as appropriate.lib/research/getResearchLookupHandler.ts (2)
61-69: Response spreading pattern is duplicated across handlers.This exact pattern (
typeof result.data === "object" && result.data !== null ? result.data : { data: result.data }) appears in multiple handlers. Consider extracting a shared utility likeformatProxyResponse(result)to consolidate this logic.♻️ Suggested utility extraction
Create a shared utility in
lib/research/formatProxyResponse.ts:export function formatProxyResponse(data: unknown): Record<string, unknown> { return typeof data === "object" && data !== null ? (data as Record<string, unknown>) : { data }; }Then simplify handlers:
+import { formatProxyResponse } from "@/lib/research/formatProxyResponse"; return NextResponse.json( - { - status: "success", - ...(typeof result.data === "object" && result.data !== null - ? result.data - : { data: result.data }), - }, + { status: "success", ...formatProxyResponse(result.data) }, { status: 200, headers: getCorsHeaders() }, );🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/research/getResearchLookupHandler.ts` around lines 61 - 69, The response-spreading logic repeated in getResearchLookupHandler (the ternary checking typeof result.data === "object" && result.data !== null and spreading result.data or wrapping it as { data: result.data }) should be extracted to a shared utility (e.g., formatProxyResponse) and used where NextResponse.json is called; create a function like formatProxyResponse(data: unknown): Record<string, unknown> that returns data if it's a non-null object or { data } otherwise, then replace the inline ternary in the NextResponse.json call (and other handlers using the same pattern) with formatProxyResponse(result.data) to centralize the logic.
43-50: Same catch block observation as the search handler.Consider differentiating between "No credits usage found" and "Insufficient credits" errors from
deductCreditsfor better error messaging.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/research/getResearchLookupHandler.ts` around lines 43 - 50, The catch block calling deductCredits in getResearchLookupHandler should capture the thrown error (catch (err)) and distinguish between a "No credits usage found" error and an "Insufficient credits" error (by checking err.code or err.message or a custom error class from deductCredits); return a 404/appropriate status and JSON {status:"error", error:"No credits usage found"} when the no-usage condition is detected, otherwise return the 402 JSON {status:"error", error:"Insufficient credits"} for insufficient funds—update deductCredits usage handling accordingly so responses accurately reflect the error type.lib/research/getResearchSearchHandler.ts (1)
31-38: Catch block masks error details.The
catchblock assumes all errors fromdeductCreditsmean "Insufficient credits," but perlib/credits/deductCredits.ts, it may also throw "No credits usage found for this account." Logging or differentiating the error message would improve debuggability.💡 Suggested improvement
try { await deductCredits({ accountId, creditsToDeduct: 5 }); - } catch { + } catch (error) { + const message = error instanceof Error ? error.message : "Insufficient credits"; return NextResponse.json( - { status: "error", error: "Insufficient credits" }, + { status: "error", error: message }, { status: 402, headers: getCorsHeaders() }, ); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/research/getResearchSearchHandler.ts` around lines 31 - 38, The catch block in getResearchSearchHandler around the call to deductCredits currently swallows the thrown error and always returns "Insufficient credits"; change it to catch the error object (e), log or include e.message, and branch on e.message (or error type) from deductCredits to return an appropriate response (e.g., 402 for insufficient credits, a 4xx/404 for "No credits usage found for this account") while ensuring getCorsHeaders() is preserved; update the handler's error response to include the actual error message for better debuggability.lib/research/getResearchDiscoverHandler.ts (2)
26-33: Same catch block observation applies here.The catch block masks the specific error from
deductCredits. Consider preserving error details as suggested in the other handlers.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/research/getResearchDiscoverHandler.ts` around lines 26 - 33, Change the catch to capture the thrown error from deductCredits (e.g., catch (err)) and include the error details when returning the NextResponse.json so the client and logs preserve the underlying reason; update the response payload returned by NextResponse.json in getResearchDiscoverHandler to include the actual error message (err.message or String(err)) alongside "Insufficient credits" and, if available in this module, log the full error via the existing logger before returning, keeping the CORS headers from getCorsHeaders().
64-72: Same response spreading pattern—consolidation opportunity.This is the same spreading logic seen in the other handlers. A shared
formatProxyResponseutility would reduce duplication.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/research/getResearchDiscoverHandler.ts` around lines 64 - 72, The response construction in getResearchDiscoverHandler.ts duplicates the spreading logic used elsewhere when returning NextResponse.json (specifically the conditional spread of result.data vs { data: result.data }); extract this into a shared utility (e.g., formatProxyResponse) that accepts the proxy result and returns the normalized response body, then replace the inline spreading in getResearchDiscoverHandler (and other handlers) to call formatProxyResponse and pass its result into NextResponse.json with the existing status and headers.lib/research/getResearchTrackHandler.ts (2)
16-82: Handler exceeds 50-line guideline and performs two distinct operations.This handler orchestrates search + detail fetch, making it longer than the 50-line limit and arguably violating SRP. Consider splitting into smaller functions or creating a
searchAndFetchTrackhelper.♻️ Suggested decomposition
// In a separate file or as local helpers: async function searchTrackByName(q: string): Promise<{ trackId: number } | null> { const result = await proxyToChartmetric("/search", { q, type: "tracks", limit: "1" }); if (result.status !== 200) return null; const data = result.data as { tracks?: Array<{ id: number }> }; return data?.tracks?.[0] ? { trackId: data.tracks[0].id } : null; } async function fetchTrackDetails(trackId: number): Promise<ProxyResult> { return proxyToChartmetric(`/track/${trackId}`); }This keeps the main handler focused on orchestration and error responses.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/research/getResearchTrackHandler.ts` around lines 16 - 82, The getResearchTrackHandler is too long and mixes two responsibilities (search + detail fetch); extract the search and detail-fetch logic into helpers (e.g., create searchTrackByName(q: string) that calls proxyToChartmetric("/search", { q, type: "tracks", limit: "1" }) and returns a trackId or null, and fetchTrackDetails(trackId: number) that calls proxyToChartmetric(`/track/${trackId}`) ), then simplify getResearchTrackHandler to validate auth, call deductCredits, call searchTrackByName to get trackId, handle not-found/error responses, then call fetchTrackDetails and return the final response; keep existing error responses and headers (getCorsHeaders) and preserve calls to validateAuthContext and deductCredits.
31-38: Credits deducted before API calls complete.Credits are deducted at line 32, but two sequential API calls follow. If the search succeeds but the detail fetch fails (rate limit, timeout, etc.), the user loses credits without receiving the track details. This is a known tradeoff (per context snippets), but worth documenting or considering a deferred-deduction pattern for multi-call handlers.
Also applies to: 40-51, 63-71
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/research/getResearchTrackHandler.ts` around lines 31 - 38, deductCredits is called before the subsequent external API calls, so if the search succeeds but the detail fetch fails the user still loses credits; change getResearchTrackHandler to either defer calling deductCredits until after all dependent API calls (e.g., after search+detail fetch succeed) or implement a compensating refund flow by calling a refundCredits/{creditRefund} helper when the detail fetch fails, and ensure the error path that returns NextResponse.json({ status: "error", ... }, { status: 402, headers: getCorsHeaders() }) triggers that refund; locate usages of deductCredits and the error return blocks (the try/catch around deductCredits and the later failure branches) and update them to use the deferred-deduction or refund approach consistently for the other similar blocks noted (lines 40-51, 63-71).
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 48c2e50f-a8f0-4ff7-b53c-96694a3a874d
⛔ Files ignored due to path filters (3)
lib/chartmetric/__tests__/getChartmetricToken.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/research/__tests__/proxyToChartmetric.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/research/__tests__/resolveArtist.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**
📒 Files selected for processing (45)
app/api/research/albums/route.tsapp/api/research/audience/route.tsapp/api/research/career/route.tsapp/api/research/cities/route.tsapp/api/research/curator/route.tsapp/api/research/discover/route.tsapp/api/research/festivals/route.tsapp/api/research/genres/route.tsapp/api/research/insights/route.tsapp/api/research/instagram-posts/route.tsapp/api/research/lookup/route.tsapp/api/research/metrics/route.tsapp/api/research/playlist/route.tsapp/api/research/playlists/route.tsapp/api/research/profile/route.tsapp/api/research/route.tsapp/api/research/similar/route.tsapp/api/research/track/route.tsapp/api/research/tracks/route.tsapp/api/research/urls/route.tslib/chartmetric/getChartmetricToken.tslib/content/contentTemplates.tslib/research/getResearchAlbumsHandler.tslib/research/getResearchAudienceHandler.tslib/research/getResearchCareerHandler.tslib/research/getResearchCitiesHandler.tslib/research/getResearchCuratorHandler.tslib/research/getResearchDiscoverHandler.tslib/research/getResearchFestivalsHandler.tslib/research/getResearchGenresHandler.tslib/research/getResearchInsightsHandler.tslib/research/getResearchInstagramPostsHandler.tslib/research/getResearchLookupHandler.tslib/research/getResearchMetricsHandler.tslib/research/getResearchPlaylistHandler.tslib/research/getResearchPlaylistsHandler.tslib/research/getResearchProfileHandler.tslib/research/getResearchSearchHandler.tslib/research/getResearchSimilarHandler.tslib/research/getResearchTrackHandler.tslib/research/getResearchTracksHandler.tslib/research/getResearchUrlsHandler.tslib/research/handleArtistResearch.tslib/research/proxyToChartmetric.tslib/research/resolveArtist.ts
| import { NextRequest, NextResponse } from "next/server"; | ||
| import { getCorsHeaders } from "@/lib/networking/getCorsHeaders"; | ||
| import { getResearchDiscoverHandler } from "@/lib/research/getResearchDiscoverHandler"; | ||
|
|
||
| /** | ||
| * | ||
| */ | ||
| export async function OPTIONS() { | ||
| return new NextResponse(null, { status: 200, headers: getCorsHeaders() }); | ||
| } | ||
|
|
||
| /** | ||
| * | ||
| * @param request | ||
| */ | ||
| export async function GET(request: NextRequest) { | ||
| return getResearchDiscoverHandler(request); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Locate route and handler symbols
fd --type f 'route.ts' app/api/research | sort
rg -n 'getResearchDiscoverHandler|/api/research/discover'
# Find tests referencing discover endpoint/handler
rg -n --iglob '*{test,spec}*.{ts,tsx,js,jsx}' \
'/api/research/discover|getResearchDiscoverHandler'
# Check for explicit error-path assertions (status codes/messages)
rg -n --iglob '*{test,spec}*.{ts,tsx,js,jsx}' \
'discover.*(400|401|402|500)|status\s*[:=]\s*(400|401|402|500)|insufficient|invalid|missing'Repository: recoupable/api
Length of output: 1160
🏁 Script executed:
cat -n lib/research/getResearchDiscoverHandler.tsRepository: recoupable/api
Length of output: 2924
Add comprehensive test coverage and implement Zod query validation for this endpoint.
Missing test coverage for success and error paths (credit insufficiency, auth failure, proxy failures). Additionally, query parameters should be validated using a Zod schema (validateDiscoverQuery.ts) rather than manually parsed. The route file's JSDoc comments are empty and should document the endpoint's parameters and behavior.
The handler correctly uses validateAuthContext() and includes proper error handling, but these should be covered by tests. Create:
- Tests covering auth success/failure, credit success/failure, and proxy success/failure paths
- A
lib/research/validateDiscoverQuery.tsfile exporting a Zod schema and inferred type forcountry,genre,sort,limit,sp_monthly_listeners_min,sp_monthly_listeners_maxparameters - JSDoc comments in the route file documenting the endpoint
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/api/research/discover/route.ts` around lines 1 - 18, Add Zod-based query
validation and comprehensive tests: create lib/research/validateDiscoverQuery.ts
exporting a Zod schema and inferred TypeScript type for the query params
(country, genre, sort, limit, sp_monthly_listeners_min,
sp_monthly_listeners_max) and update the discover handler to use that schema
instead of manual parsing (reference getResearchDiscoverHandler and the exported
schema name). Add JSDoc to the route file (the exported OPTIONS and GET
handlers) documenting accepted query parameters, auth requirements, and possible
responses. Add tests that exercise success and error paths for auth
(validateAuthContext), credit checks, and proxy failures by invoking the GET
route or directly calling getResearchDiscoverHandler with mocked dependencies,
and include tests validating the schema rejectsbad inputs and accepts valid
ones.
| export async function GET(request: NextRequest) { | ||
| return getResearchSearchHandler(request); |
There was a problem hiding this comment.
Add endpoint-level tests for this new route’s success and error paths before merge.
The PR coverage described is for shared internals (token/proxy/artist resolution), but this new route also needs direct API-route tests (e.g., auth failure, missing/invalid query input, upstream error propagation, happy path).
Based on learnings: Applies to app/api/**/route.ts : Write tests for new API endpoints covering all success and error paths.
I can draft a test matrix + starter test file for this route (and the shared pattern for sibling research routes) if you want.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/api/research/route.ts` around lines 19 - 20, Add endpoint-level tests for
the new API route by exercising the exported GET handler and its underlying
getResearchSearchHandler: write tests that cover the happy path (valid auth and
query returning expected data), auth failures (missing/invalid credentials),
input validation (missing or invalid query params/body), and upstream/error
propagation (simulate proxy/token/artist resolution failures or upstream 5xx and
assert correct status and message). Use the same test harness/HTTP invocation
used for other app/api routes in the repo, stub or mock the shared internals
(token/proxy/artist resolution) to force success and failure scenarios, and
create assertions against GET/getResearchSearchHandler responses for status
codes, response bodies, and error logs.
| /** | ||
| * | ||
| */ | ||
| export async function OPTIONS() { | ||
| return new NextResponse(null, { status: 200, headers: getCorsHeaders() }); | ||
| } | ||
|
|
||
| /** | ||
| * | ||
| * @param request | ||
| */ |
There was a problem hiding this comment.
Complete the route JSDoc blocks (currently placeholders).
Line 5 and Line 12 use empty JSDoc stubs, which makes route intent and contract harder to maintain across this endpoint family.
📝 Suggested update
-/**
- *
- */
+/**
+ * OPTIONS /api/research/track
+ *
+ * Handles CORS preflight for the track research endpoint.
+ */
export async function OPTIONS() {
return new NextResponse(null, { status: 200, headers: getCorsHeaders() });
}
-/**
- *
- * `@param` request
- */
+/**
+ * GET /api/research/track
+ *
+ * Proxies track-level research data for a requested artist.
+ *
+ * `@param` request Incoming request with query params consumed by the handler.
+ */
export async function GET(request: NextRequest) {
return getResearchTrackHandler(request);
}As per coding guidelines "app/api/**/route.ts: All API routes should have JSDoc comments".
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| /** | |
| * | |
| */ | |
| export async function OPTIONS() { | |
| return new NextResponse(null, { status: 200, headers: getCorsHeaders() }); | |
| } | |
| /** | |
| * | |
| * @param request | |
| */ | |
| /** | |
| * OPTIONS /api/research/track | |
| * | |
| * Handles CORS preflight for the track research endpoint. | |
| */ | |
| export async function OPTIONS() { | |
| return new NextResponse(null, { status: 200, headers: getCorsHeaders() }); | |
| } | |
| /** | |
| * GET /api/research/track | |
| * | |
| * Proxies track-level research data for a requested artist. | |
| * | |
| * `@param` request Incoming request with query params consumed by the handler. | |
| */ | |
| export async function GET(request: NextRequest) { | |
| return getResearchTrackHandler(request); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/api/research/track/route.ts` around lines 5 - 15, Replace the empty JSDoc
stubs with meaningful route documentation: add a JSDoc for the exported OPTIONS
function describing its purpose (responds to CORS preflight), list parameters
(none) and return value (NextResponse with CORS headers and 200 status), and add
a JSDoc for the subsequent HTTP handler (the function accepting request)
describing the endpoint intent, accepted request shape (query/body), expected
responses and status codes, and any side effects; reference the exported
function name OPTIONS and the forthcoming request handler name in the comments
so maintainers can quickly understand contract and behavior.
| export async function GET(request: NextRequest) { | ||
| return getResearchTrackHandler(request); |
There was a problem hiding this comment.
Add endpoint-level tests for success and error paths.
I don’t see route-level tests for this new endpoint surface in the provided changes; please add coverage for happy path, missing required params, auth failure, and provider error propagation.
I can draft a reusable test matrix for all new /api/research/* routes if you want.
Based on learnings "Write tests for new API endpoints covering all success and error paths".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/api/research/track/route.ts` around lines 16 - 17, Add route-level tests
for the new GET endpoint that calls getResearchTrackHandler: create tests that
exercise the happy path (valid auth + required query params returning expected
body/status), missing required params (expect 400), auth failure (simulate
missing/invalid auth and expect 401/403), and provider error propagation (mock
the underlying provider/service used by getResearchTrackHandler to throw and
assert the route returns the propagated error status and message). Use the same
test harness used elsewhere (mock request objects or supertest against the
route), spy/mock getResearchTrackHandler's dependencies to simulate success and
failures, and include assertions on status codes and response bodies for each
case.
| export async function getChartmetricToken(): Promise<string> { | ||
| const refreshToken = process.env.CHARTMETRIC_REFRESH_TOKEN; | ||
|
|
||
| if (!refreshToken) { | ||
| throw new Error("CHARTMETRIC_REFRESH_TOKEN environment variable is not set"); | ||
| } | ||
|
|
||
| const response = await fetch("https://api.chartmetric.com/api/token", { | ||
| method: "POST", | ||
| headers: { | ||
| "Content-Type": "application/json", | ||
| }, | ||
| body: JSON.stringify({ refreshtoken: refreshToken }), | ||
| }); | ||
|
|
||
| if (!response.ok) { | ||
| throw new Error(`Chartmetric token exchange failed with status ${response.status}`); | ||
| } | ||
|
|
||
| const data = (await response.json()) as { access_token: string; expires_in: number }; | ||
|
|
||
| if (!data.access_token) { | ||
| throw new Error("Chartmetric token response did not include an access_token"); | ||
| } | ||
|
|
||
| return data.access_token; | ||
| } |
There was a problem hiding this comment.
Consider caching the access token to avoid redundant API calls.
Based on the relevant code snippet from proxyToChartmetric.ts, every request calls getChartmetricToken() without any caching mechanism. With 20 research endpoints, this means each user request triggers a fresh token exchange with Chartmetric, which could:
- Hit rate limits on Chartmetric's token endpoint
- Add latency (~100-300ms per token exchange)
- Waste resources since the response includes
expires_in(currently ignored)
🔧 Suggested caching implementation
+let cachedToken: string | null = null;
+let tokenExpiresAt: number = 0;
+
export async function getChartmetricToken(): Promise<string> {
+ // Return cached token if still valid (with 60s buffer)
+ if (cachedToken && Date.now() < tokenExpiresAt - 60_000) {
+ return cachedToken;
+ }
+
const refreshToken = process.env.CHARTMETRIC_REFRESH_TOKEN;
if (!refreshToken) {
throw new Error("CHARTMETRIC_REFRESH_TOKEN environment variable is not set");
}
const response = await fetch("https://api.chartmetric.com/api/token", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ refreshtoken: refreshToken }),
});
if (!response.ok) {
throw new Error(`Chartmetric token exchange failed with status ${response.status}`);
}
const data = (await response.json()) as { access_token: string; expires_in: number };
if (!data.access_token) {
throw new Error("Chartmetric token response did not include an access_token");
}
+ cachedToken = data.access_token;
+ tokenExpiresAt = Date.now() + data.expires_in * 1000;
+
return data.access_token;
}Note: For serverless deployments where in-memory caching is less effective, consider using Redis or a similar distributed cache if high concurrency is expected.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/chartmetric/getChartmetricToken.ts` around lines 7 - 33, The
getChartmetricToken function currently fetches a new token on every call;
implement caching by storing the token and its expiry at module scope (e.g.,
cachedToken and expiresAt) and return the cached token if Date.now() <
expiresAt; when calling fetch in getChartmetricToken, read expires_in from the
response, compute expiresAt (Date.now() + expires_in*1000 - smallBufferMs) and
save both token and expiresAt before returning; ensure fallback logic still
fetches a new token if none cached or token expired, and consider pluggable
support to use an external cache (Redis) for serverless environments by
abstracting the cache read/write behind functions used by getChartmetricToken.
| const mlMin = searchParams.get("sp_monthly_listeners_min"); | ||
| const mlMax = searchParams.get("sp_monthly_listeners_max"); | ||
| if (mlMin) params["sp_ml[]"] = mlMin; | ||
| if (mlMax) params["sp_ml[]"] = mlMax; | ||
|
|
||
| const result = await proxyToChartmetric("/artist/list/filter", params); |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Chartmetric API filter endpoint sp_ml parameter format
💡 Result:
No public documentation found for the Chartmetric API 'sp_ml' parameter format in the filter endpoint. Official API docs are at https://api.chartmetric.com/apidoc/ (confirmed by Chartmetric Help Center), but specific details on 'sp_ml' (likely Spotify ML-related filter) are not surfaced in web search results. Check the official apidoc directly for endpoint parameters, as it may require authentication or be behind login. Third-party wrappers like py-chartmetric-api and Chartmetric CLI mention filters (e.g., sp_followers, platform=spotify) but no 'sp_ml'.
Citations:
- 1: https://help.chartmetric.com/en/articles/6591743-where-can-i-find-api-documentation
- 2: https://api.chartmetric.com/apidoc/
- 3: https://pypi.org/project/py-chartmetric-api/
- 4: https://mixturelabs.com/projects/chartmetric-cli/cm_artists_fan-metrics.html
🏁 Script executed:
# Find the file mentioned in review
find . -name "getResearchDiscoverHandler.ts" -type fRepository: recoupable/api
Length of output: 102
🏁 Script executed:
# Search for proxyToChartmetric to understand its implementation
rg "proxyToChartmetric" --type ts --type tsx -A 10 -B 2Repository: recoupable/api
Length of output: 85
🏁 Script executed:
# Look for similar parameter patterns with array notation in the codebase
rg 'params\[.*\[\]' --type ts --type tsxRepository: recoupable/api
Length of output: 85
🏁 Script executed:
# Search for proxyToChartmetric implementation
rg "proxyToChartmetric" -A 10 -B 2Repository: recoupable/api
Length of output: 33020
🏁 Script executed:
# Look for similar parameter patterns with array notation
rg 'params\[.*\[\]' -g '*.ts'Repository: recoupable/api
Length of output: 223
🏁 Script executed:
# Read the file to understand the full context
cat -n ./lib/research/getResearchDiscoverHandler.tsRepository: recoupable/api
Length of output: 2924
Bug: Assigning to params["sp_ml[]"] twice causes min value to be lost.
When both mlMin and mlMax are provided, the second assignment at line 53 overwrites the first. Since proxyToChartmetric uses url.searchParams.set() (which replaces rather than appends duplicate keys), only mlMax is sent to Chartmetric. The listener range filter is effectively broken when both bounds are specified.
To fix, you need to either:
- Accumulate both values into a single parameter (if Chartmetric accepts comma-separated or another array format)
- Extend
proxyToChartmetricto accept array values and useappend()for duplicate keys
Verify the correct format in Chartmetric's API docs before implementing.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/research/getResearchDiscoverHandler.ts` around lines 50 - 55, The current
code in getResearchDiscoverHandler sets params["sp_ml[]"] twice (using mlMin and
mlMax), causing the second assignment to overwrite the first so only the max is
sent; update the code so when both mlMin and mlMax are present you send both
values together instead of reassigning: either (A) combine them into the single
format Chartmetric expects (e.g., comma-separated or a single array-like string)
and assign that to params["sp_ml[]"], or (B) modify proxyToChartmetric to accept
array values for params and when a param value is an array call
url.searchParams.append(...) for each element so both mlMin and mlMax are sent;
locate the mlMin/mlMax handling and the params["sp_ml[]"] usage as well as the
proxyToChartmetric function to implement the appropriate change after confirming
Chartmetric’s expected format.
| const { searchParams } = new URL(request.url); | ||
| const source = searchParams.get("source"); | ||
|
|
||
| if (!source) { | ||
| return NextResponse.json( | ||
| { status: "error", error: "source parameter is required" }, | ||
| { status: 400, headers: getCorsHeaders() }, | ||
| ); | ||
| } | ||
|
|
||
| return handleArtistResearch(request, cmId => `/artist/${cmId}/stat/${source}`); |
There was a problem hiding this comment.
Validate source before embedding it in the upstream path.
Line 25 interpolates raw query input into a path segment. That allows path tampering (/, %2f, ..) and bypasses endpoint intent.
🔒 Suggested hardening with Zod
import { type NextRequest, NextResponse } from "next/server";
+import { z } from "zod";
import { getCorsHeaders } from "@/lib/networking/getCorsHeaders";
import { handleArtistResearch } from "@/lib/research/handleArtistResearch";
+const sourceSchema = z
+ .string()
+ .trim()
+ .min(1)
+ .regex(/^[a-z0-9_-]+$/i, "invalid source");
+
export async function getResearchMetricsHandler(request: NextRequest) {
const { searchParams } = new URL(request.url);
- const source = searchParams.get("source");
+ const sourceResult = sourceSchema.safeParse(searchParams.get("source"));
- if (!source) {
+ if (!sourceResult.success) {
return NextResponse.json(
- { status: "error", error: "source parameter is required" },
+ { status: "error", error: "valid source parameter is required" },
{ status: 400, headers: getCorsHeaders() },
);
}
+ const source = sourceResult.data.toLowerCase();
return handleArtistResearch(request, cmId => `/artist/${cmId}/stat/${source}`);
}As per coding guidelines "app/api/**/*.ts: Validate input parameters using Zod schemas" and "Proper error handling and validation".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/research/getResearchMetricsHandler.ts` around lines 15 - 25, Validate and
sanitize the "source" query param before embedding it into the upstream path:
replace the current raw usage of source (the variable retrieved from
searchParams) with a Zod-validated value or a strict whitelist/regex (e.g.,
/^[A-Za-z0-9_-]+$/ or an enum of allowed sources) and reject requests that fail
validation with a 400 response; specifically, update the block that reads source
and the call to handleArtistResearch so you only interpolate the
validated/sanitizedSource (and not the raw source) into the path
`/artist/${cmId}/stat/${source}` and ensure percent-encoded slashes, "..", and
other path separators are rejected.
lib/research/handleArtistResearch.ts
Outdated
| try { | ||
| await deductCredits({ accountId, creditsToDeduct: 5 }); | ||
| } catch { | ||
| return NextResponse.json( | ||
| { status: "error", error: "Insufficient credits" }, | ||
| { status: 402, headers: getCorsHeaders() }, | ||
| ); | ||
| } |
There was a problem hiding this comment.
Error handling masks different failure modes from deductCredits.
The catch block treats all errors as "Insufficient credits" with HTTP 402, but deductCredits can throw different error types (per lib/credits/deductCredits.ts):
"No credits usage found for this account"— account config/data issue"Insufficient credits..."— actual billing issue- Database/network errors from
updateCreditsUsage
Returning 402 for infrastructure failures is misleading. Consider inspecting the error message to return appropriate status codes.
🔧 Suggested improvement
try {
await deductCredits({ accountId, creditsToDeduct: 5 });
- } catch {
+ } catch (err) {
+ const message = err instanceof Error ? err.message : "Credit deduction failed";
+ const isInsufficientCredits = message.toLowerCase().includes("insufficient credits");
return NextResponse.json(
- { status: "error", error: "Insufficient credits" },
- { status: 402, headers: getCorsHeaders() },
+ { status: "error", error: isInsufficientCredits ? "Insufficient credits" : "Credit check failed" },
+ { status: isInsufficientCredits ? 402 : 500, headers: getCorsHeaders() },
);
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| try { | |
| await deductCredits({ accountId, creditsToDeduct: 5 }); | |
| } catch { | |
| return NextResponse.json( | |
| { status: "error", error: "Insufficient credits" }, | |
| { status: 402, headers: getCorsHeaders() }, | |
| ); | |
| } | |
| try { | |
| await deductCredits({ accountId, creditsToDeduct: 5 }); | |
| } catch (err) { | |
| const message = err instanceof Error ? err.message : "Credit deduction failed"; | |
| const isInsufficientCredits = message.toLowerCase().includes("insufficient credits"); | |
| return NextResponse.json( | |
| { status: "error", error: isInsufficientCredits ? "Insufficient credits" : "Credit check failed" }, | |
| { status: isInsufficientCredits ? 402 : 500, headers: getCorsHeaders() }, | |
| ); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/research/handleArtistResearch.ts` around lines 45 - 52, The catch block
around deductCredits incorrectly maps all failures to a 402; change the catch to
capture the thrown error (e.g., catch (err)) and inspect err.message (or
stringified err) to map to appropriate responses: if it contains "Insufficient
credits" return NextResponse.json({...}, { status: 402, headers:
getCorsHeaders() }); if it contains "No credits usage found" return a 400 (or
404) with the error text; otherwise treat as an infrastructure/db error and
return a 500 including the actual error message. Update the error responses in
handleArtistResearch.ts so they include the original error message and correct
status codes when calling NextResponse.json.
| export async function proxyToChartmetric( | ||
| path: string, | ||
| queryParams?: Record<string, string>, | ||
| ): Promise<ProxyResult> { | ||
| const accessToken = await getChartmetricToken(); | ||
|
|
||
| const url = new URL(`${CHARTMETRIC_BASE}${path}`); | ||
| if (queryParams) { | ||
| for (const [key, value] of Object.entries(queryParams)) { | ||
| if (value !== undefined && value !== "") { | ||
| url.searchParams.set(key, value); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| const response = await fetch(url.toString(), { | ||
| method: "GET", | ||
| headers: { | ||
| Authorization: `Bearer ${accessToken}`, | ||
| "Content-Type": "application/json", | ||
| }, | ||
| }); | ||
|
|
||
| if (!response.ok) { | ||
| return { | ||
| data: { error: `Chartmetric API returned ${response.status}` }, | ||
| status: response.status, | ||
| }; | ||
| } | ||
|
|
||
| const json = await response.json(); | ||
|
|
||
| const data = json.obj !== undefined ? json.obj : json; | ||
|
|
||
| return { data, status: response.status }; | ||
| } |
There was a problem hiding this comment.
Unhandled exceptions from getChartmetricToken() break the consistent error response pattern.
The function returns a structured ProxyResult on HTTP errors (lines 41-46), but getChartmetricToken() on line 22 throws exceptions that propagate unhandled. This creates an inconsistent error contract—callers expect ProxyResult, but may receive thrown exceptions instead.
From lib/chartmetric/getChartmetricToken.ts, exceptions are thrown for:
- Missing
CHARTMETRIC_REFRESH_TOKENenv var - Non-ok response from token endpoint
- Missing
access_tokenin response
🔧 Wrap token exchange in try/catch for consistent error handling
export async function proxyToChartmetric(
path: string,
queryParams?: Record<string, string>,
): Promise<ProxyResult> {
- const accessToken = await getChartmetricToken();
+ let accessToken: string;
+ try {
+ accessToken = await getChartmetricToken();
+ } catch (error) {
+ return {
+ data: { error: error instanceof Error ? error.message : "Token exchange failed" },
+ status: 500,
+ };
+ }
const url = new URL(`${CHARTMETRIC_BASE}${path}`);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/research/proxyToChartmetric.ts` around lines 18 - 53, proxyToChartmetric
currently calls getChartmetricToken() directly and lets its exceptions escape,
breaking the ProxyResult contract; wrap the await getChartmetricToken() call in
a try/catch inside proxyToChartmetric and on error return a ProxyResult with
data: { error: <descriptive error.message> } and an appropriate status (e.g.,
500) so callers always receive a structured response; ensure you reference the
function name proxyToChartmetric and the token helper getChartmetricToken in
your change.
Discovered via live API test — the token exchange response uses the field name 'token', not 'access_token'. Now accepts both for resilience. Made-with: Cursor
When a Spotify playlist ID (string) is passed instead of a numeric Chartmetric ID, the handler searches for the playlist by ID string and uses the top match. Numeric IDs are passed through directly. Made-with: Cursor
Arrays were being spread as { "0": item, "1": item } instead
of wrapped under named keys. Fixed all handlers:
- albums → { albums: [...] }
- tracks → { tracks: [...] }
- insights → { insights: [...] }
- career → { career: [...] }
- similar → { artists: [...], total: N }
- playlists → { placements: [...] }
- cities → { cities: [{ name, country, listeners }] } (normalized from Chartmetric dict)
- urls → { urls: [...] }
- genres → { genres: [...] }
- festivals → { festivals: [...] }
- discover → { artists: [...] }
All 20 endpoints verified E2E against live Chartmetric API.
Made-with: Cursor
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
lib/research/getResearchPlaylistHandler.ts (1)
14-81: This handler is doing too much in one place.Auth, param validation, credit deduction, non-numeric ID resolution, upstream proxying, and response shaping are all packed into one 68-line function. Extracting something like
resolvePlaylistId()plus a small shared error-response helper would make the branches easier to test and keep this closer to the repo's handler conventions.As per coding guidelines,
lib/**/*.ts:Apply Single Responsibility Principle (SRP): one exported function per file; each file should do one thing wellandFor domain functions, ensure: Single responsibility per function,Keep functions under 50 lines, andDRY: Consolidate similar logic into shared utilities.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/research/getResearchPlaylistHandler.ts` around lines 14 - 81, getResearchPlaylistHandler is doing too many responsibilities; split its logic by extracting a resolvePlaylistId(requestPlatform, id, proxyToChartmetric) function to encapsulate the non-numeric ID lookup and playlistId resolution, and add a small helper like makeErrorResponse(payload, status) to centralize JSON error responses (used instead of repeating NextResponse.json(..., { headers: getCorsHeaders() })). Move the existing credit check (deductCredits), auth (validateAuthContext), and final proxy call (proxyToChartmetric(`/playlist/${platform}/${playlistId}`)) to orchestrate these smaller functions so getResearchPlaylistHandler becomes a thin coordinator under 50 lines, reusing resolvePlaylistId and makeErrorResponse for testable, single-responsibility units while preserving existing symbols: validateAuthContext, deductCredits, proxyToChartmetric, getCorsHeaders.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@lib/research/getResearchPlaylistHandler.ts`:
- Around line 30-37: The current try/catch around deductCredits treats any
thrown error as "Insufficient credits"; update the handler so it inspects the
error thrown by deductCredits and only returns NextResponse.json({ status:
"error", error: "Insufficient credits" }, { status: 402, headers:
getCorsHeaders() }) when the error explicitly indicates a
low-balance/insufficient-credits condition, while for other errors (e.g.,
missing credits record or unexpected failures) log the error and return a 500
response (NextResponse.json with status 500 and getCorsHeaders()). Locate the
deductCredits call in getResearchPlaylistHandler and implement error branching
based on the error type/message or a custom error class returned by
deductCredits. Ensure error logging includes the original error for non-402
cases.
- Around line 41-58: The search branch that calls proxyToChartmetric("/search",
...) must check the upstream response status before assuming playlists is
missing; update the logic in getResearchPlaylistHandler to inspect
searchResult.status (or equivalent) and if it is not a successful 2xx code,
return a NextResponse.json containing the upstream error message/body and use
the same status code (and include getCorsHeaders()), instead of falling through
to the 404 “No playlist found” message; keep using the existing
proxyToChartmetric call and playlists extraction but short-circuit on non-OK
searchResult.status to propagate provider errors correctly.
---
Nitpick comments:
In `@lib/research/getResearchPlaylistHandler.ts`:
- Around line 14-81: getResearchPlaylistHandler is doing too many
responsibilities; split its logic by extracting a
resolvePlaylistId(requestPlatform, id, proxyToChartmetric) function to
encapsulate the non-numeric ID lookup and playlistId resolution, and add a small
helper like makeErrorResponse(payload, status) to centralize JSON error
responses (used instead of repeating NextResponse.json(..., { headers:
getCorsHeaders() })). Move the existing credit check (deductCredits), auth
(validateAuthContext), and final proxy call
(proxyToChartmetric(`/playlist/${platform}/${playlistId}`)) to orchestrate these
smaller functions so getResearchPlaylistHandler becomes a thin coordinator under
50 lines, reusing resolvePlaylistId and makeErrorResponse for testable,
single-responsibility units while preserving existing symbols:
validateAuthContext, deductCredits, proxyToChartmetric, getCorsHeaders.
🪄 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: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 4bdd1005-5f24-465d-b876-8895669f56f2
📒 Files selected for processing (1)
lib/research/getResearchPlaylistHandler.ts
| try { | ||
| await deductCredits({ accountId, creditsToDeduct: 5 }); | ||
| } catch { | ||
| return NextResponse.json( | ||
| { status: "error", error: "Insufficient credits" }, | ||
| { status: 402, headers: getCorsHeaders() }, | ||
| ); | ||
| } |
There was a problem hiding this comment.
Only return 402 for real low-balance failures.
deductCredits also throws when the credits record is missing, so Line 32 currently turns that server-side failure into a false "Insufficient credits" response. Please branch on the thrown error and reserve 402 for the actual insufficient-balance case.
🛠️ Suggested fix
try {
await deductCredits({ accountId, creditsToDeduct: 5 });
- } catch {
+ } catch (error) {
+ const message = error instanceof Error ? error.message : "Credit deduction failed";
+ const insufficientCredits = message.startsWith("Insufficient credits");
+
return NextResponse.json(
- { status: "error", error: "Insufficient credits" },
- { status: 402, headers: getCorsHeaders() },
+ {
+ status: "error",
+ error: insufficientCredits ? "Insufficient credits" : "Credit deduction failed",
+ },
+ { status: insufficientCredits ? 402 : 500, headers: getCorsHeaders() },
);
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| try { | |
| await deductCredits({ accountId, creditsToDeduct: 5 }); | |
| } catch { | |
| return NextResponse.json( | |
| { status: "error", error: "Insufficient credits" }, | |
| { status: 402, headers: getCorsHeaders() }, | |
| ); | |
| } | |
| try { | |
| await deductCredits({ accountId, creditsToDeduct: 5 }); | |
| } catch (error) { | |
| const message = error instanceof Error ? error.message : "Credit deduction failed"; | |
| const insufficientCredits = message.startsWith("Insufficient credits"); | |
| return NextResponse.json( | |
| { | |
| status: "error", | |
| error: insufficientCredits ? "Insufficient credits" : "Credit deduction failed", | |
| }, | |
| { status: insufficientCredits ? 402 : 500, headers: getCorsHeaders() }, | |
| ); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/research/getResearchPlaylistHandler.ts` around lines 30 - 37, The current
try/catch around deductCredits treats any thrown error as "Insufficient
credits"; update the handler so it inspects the error thrown by deductCredits
and only returns NextResponse.json({ status: "error", error: "Insufficient
credits" }, { status: 402, headers: getCorsHeaders() }) when the error
explicitly indicates a low-balance/insufficient-credits condition, while for
other errors (e.g., missing credits record or unexpected failures) log the error
and return a 500 response (NextResponse.json with status 500 and
getCorsHeaders()). Locate the deductCredits call in getResearchPlaylistHandler
and implement error branching based on the error type/message or a custom error
class returned by deductCredits. Ensure error logging includes the original
error for non-402 cases.
| if (!/^\d+$/.test(id)) { | ||
| const searchResult = await proxyToChartmetric("/search", { | ||
| q: id, | ||
| type: "playlists", | ||
| limit: "1", | ||
| }); | ||
|
|
||
| const playlists = | ||
| (searchResult.data as { playlists?: { [key: string]: Array<{ id: number }> } })?.playlists?.[ | ||
| platform | ||
| ]; | ||
|
|
||
| if (!playlists || playlists.length === 0) { | ||
| return NextResponse.json( | ||
| { status: "error", error: `No playlist found matching "${id}" on ${platform}` }, | ||
| { status: 404, headers: getCorsHeaders() }, | ||
| ); | ||
| } |
There was a problem hiding this comment.
Handle /search failures before falling through to 404.
proxyToChartmetric preserves upstream status codes in its return value. If the search request fails, this branch just sees playlists === undefined and reports "No playlist found", which is the wrong outcome for provider errors.
🛠️ Suggested fix
if (!/^\d+$/.test(id)) {
const searchResult = await proxyToChartmetric("/search", {
q: id,
type: "playlists",
limit: "1",
});
+
+ if (searchResult.status !== 200) {
+ return NextResponse.json(
+ { status: "error", error: "Playlist search failed" },
+ { status: searchResult.status, headers: getCorsHeaders() },
+ );
+ }
const playlists =
(searchResult.data as { playlists?: { [key: string]: Array<{ id: number }> } })?.playlists?.[
platform
];📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (!/^\d+$/.test(id)) { | |
| const searchResult = await proxyToChartmetric("/search", { | |
| q: id, | |
| type: "playlists", | |
| limit: "1", | |
| }); | |
| const playlists = | |
| (searchResult.data as { playlists?: { [key: string]: Array<{ id: number }> } })?.playlists?.[ | |
| platform | |
| ]; | |
| if (!playlists || playlists.length === 0) { | |
| return NextResponse.json( | |
| { status: "error", error: `No playlist found matching "${id}" on ${platform}` }, | |
| { status: 404, headers: getCorsHeaders() }, | |
| ); | |
| } | |
| if (!/^\d+$/.test(id)) { | |
| const searchResult = await proxyToChartmetric("/search", { | |
| q: id, | |
| type: "playlists", | |
| limit: "1", | |
| }); | |
| if (searchResult.status !== 200) { | |
| return NextResponse.json( | |
| { status: "error", error: "Playlist search failed" }, | |
| { status: searchResult.status, headers: getCorsHeaders() }, | |
| ); | |
| } | |
| const playlists = | |
| (searchResult.data as { playlists?: { [key: string]: Array<{ id: number }> } })?.playlists?.[ | |
| platform | |
| ]; | |
| if (!playlists || playlists.length === 0) { | |
| return NextResponse.json( | |
| { status: "error", error: `No playlist found matching "${id}" on ${platform}` }, | |
| { status: 404, headers: getCorsHeaders() }, | |
| ); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/research/getResearchPlaylistHandler.ts` around lines 41 - 58, The search
branch that calls proxyToChartmetric("/search", ...) must check the upstream
response status before assuming playlists is missing; update the logic in
getResearchPlaylistHandler to inspect searchResult.status (or equivalent) and if
it is not a successful 2xx code, return a NextResponse.json containing the
upstream error message/body and use the same status code (and include
getCorsHeaders()), instead of falling through to the 404 “No playlist found”
message; keep using the existing proxyToChartmetric call and playlists
extraction but short-circuit on non-OK searchResult.status to propagate provider
errors correctly.
Chartmetric returns nearly empty results without filter params. Now defaults to editorial + indie + majorCurator + popularIndie when no explicit filters are passed. Users can still override with specific filter params. Made-with: Cursor
Three new POST endpoints: - /api/research/web — Perplexity web search (5 credits) - /api/research/deep — Perplexity sonar-deep-research (25 credits) - /api/research/people — Exa people search with LinkedIn profiles (5 credits) All three tested E2E: - Web: returns Wikipedia, press, Apple Music results - Deep: returns 34K char cited report with 50 citations - People: returns LinkedIn profiles of music industry professionals Total research endpoints: 23 (20 Chartmetric + 3 web/deep/people) Made-with: Cursor
There was a problem hiding this comment.
Actionable comments posted: 8
♻️ Duplicate comments (1)
lib/research/getResearchDiscoverHandler.ts (1)
50-53:⚠️ Potential issue | 🟠 MajorBug:
sp_ml[]min value is lost when both bounds are specified.This issue was previously flagged and remains unresolved. When both
mlMinandmlMaxare provided, line 53 overwrites line 52's assignment becauseproxyToChartmetricusesurl.searchParams.set()(which replaces rather than appends). Only the max listener bound reaches Chartmetric, effectively breaking the range filter.Two options to fix:
- Modify
proxyToChartmetricto acceptRecord<string, string | string[]>and useappend()for array values- Build the URL differently in this handler if Chartmetric expects a different format (comma-separated, etc.)
🔧 Option 1: Update proxyToChartmetric signature
In
lib/research/proxyToChartmetric.ts:export async function proxyToChartmetric( path: string, - queryParams?: Record<string, string>, + queryParams?: Record<string, string | string[]>, ): Promise<ProxyResult> { // ... if (queryParams) { for (const [key, value] of Object.entries(queryParams)) { - if (value !== undefined && value !== "") { - url.searchParams.set(key, value); + if (value !== undefined && value !== "") { + if (Array.isArray(value)) { + for (const v of value) { + url.searchParams.append(key, v); + } + } else { + url.searchParams.set(key, value); + } } } }Then in this handler:
const mlMin = searchParams.get("sp_monthly_listeners_min"); const mlMax = searchParams.get("sp_monthly_listeners_max"); - if (mlMin) params["sp_ml[]"] = mlMin; - if (mlMax) params["sp_ml[]"] = mlMax; + const mlValues = [mlMin, mlMax].filter(Boolean) as string[]; + if (mlValues.length > 0) params["sp_ml[]"] = mlValues;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/research/getResearchDiscoverHandler.ts` around lines 50 - 53, The handler is overwriting the sp_ml[] param when both mlMin and mlMax are present because proxyToChartmetric uses url.searchParams.set(); update proxyToChartmetric to accept Record<string, string | string[]> and, when a value is an array, call url.searchParams.append() for each element, then in getResearchDiscoverHandler (symbols: mlMin, mlMax, params and params["sp_ml[]"]) set params["sp_ml[]"] = [mlMin, mlMax] (or a single string when only one bound exists) so both bounds are sent to Chartmetric.
🧹 Nitpick comments (1)
lib/research/postResearchDeepHandler.ts (1)
15-71: Extract shared POST research orchestration into a reusable utility.This handler repeats the same auth → parse → validate → deduct → execute → map-error pattern used in other research POST handlers. Consolidating this flow will reduce drift and fix bugs once.
As per coding guidelines: "Extract shared logic into reusable utilities following Don't Repeat Yourself (DRY) principle."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/research/postResearchDeepHandler.ts` around lines 15 - 71, Extract the repeated auth→parse→validate→deduct→execute→map-error flow from postResearchDeepHandler into a reusable utility (e.g., handlePostResearchRequest) that accepts the NextRequest, creditsToDeduct, an executor callback (the core execution like chatWithPerplexity), and any operation name for logs; move the calls to validateAuthContext, request.json parsing, query presence check, deductCredits, and unified NextResponse creation (preserving getCorsHeaders and the same status codes for 400/402/500/200) into that utility, then refactor postResearchDeepHandler to call this utility with creditsToDeduct=25 and an executor that invokes chatWithPerplexity([{ role: "user", content: body.query }], "sonar-deep-research") and maps the executor result to the { status:"success", content, citations } shape while letting the utility handle error mapping.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@app/api/research/web/route.ts`:
- Around line 12-19: Add endpoint tests covering the new POST /api/research/web
route by exercising the exported POST(request: NextRequest) wrapper and the
underlying postResearchWebHandler; create tests for (1) auth failure (mock
invalid/absent auth and assert 401), (2) invalid JSON/body validation (send
malformed or missing fields and assert 400 with validation errors), (3)
insufficient credits (mock user/credits check to return insufficient and assert
the appropriate 402/403 response), (4) provider failure (mock the external
provider call used in postResearchWebHandler to throw or return an error and
assert the handler returns a 5xx or mapped error response), and (5) successful
path (mock auth, credits, and provider to return expected data and assert the
success status and response shape). Ensure you mock dependencies used by
postResearchWebHandler (auth, credits check, and provider client) and verify
response status codes and JSON schema for each scenario.
In `@lib/exa/searchPeople.ts`:
- Around line 27-47: The function searchPeople currently documents a "max 100"
for numResults but doesn't enforce it; clamp numResults to a safe range (e.g.,
Math.max(1, Math.min(numResults, 100))) inside the searchPeople function before
composing the request body so the POST always sends a value between 1 and 100
(use the clamped variable in the body instead of the raw numResults).
In `@lib/research/getResearchDiscoverHandler.ts`:
- Around line 26-33: The catch block in getResearchDiscoverHandler currently
hides the real failure from deductCredits; change the catch to capture the
thrown error (e.g., catch (err)) and return the original error message in the
JSON response while keeping the 402 status and CORS headers (use
getCorsHeaders()); reference deductCredits to locate the source and ensure you
use err.message or String(err) so the actual message like "No credits usage
found for this account" is preserved instead of always returning "Insufficient
credits".
In `@lib/research/getResearchPlaylistsHandler.ts`:
- Line 35: The long boolean expression assigned to hasFilters is causing
Prettier to fail; refactor the assignment in getResearchPlaylistsHandler by
replacing the long OR chain (sp.get("editorial") || sp.get("indie") ||
sp.get("majorCurator") || sp.get("personalized") || sp.get("chart")) with a
shorter, formatted expression such as using an array and Array.prototype.some:
e.g., const hasFilters =
["editorial","indie","majorCurator","personalized","chart"].some(k =>
sp.get(k)); this keeps the logic identical (calls sp.get for each key) while
allowing the formatter to wrap lines cleanly.
- Around line 35-47: The code ignores a caller-provided popularIndie value
because popularIndie isn't included in the hasFilters check or the
explicit-filter branch; update the logic so popularIndie is treated like the
other filters: add sp.get("popularIndie") to the hasFilters expression and
inside the explicit branch set params.popularIndie = sp.get("popularIndie")!
when present (leaving the else block to still set params.popularIndie = "true"
as the default).
In `@lib/research/postResearchDeepHandler.ts`:
- Around line 39-46: The code currently calls deductCredits({ accountId,
creditsToDeduct: 25 }) before the external provider call and returns a blanket
402 on any failure; move the deductCredits call to after the provider call
succeeds (so failed research doesn't consume credits), and in the deductCredits
try/catch inspect the thrown error to distinguish an "insufficient credits"
condition from other operational/storage errors — return
NextResponse.json({status:"error", error:"Insufficient credits"}, {status:402,
headers:getCorsHeaders()}) only for the genuine insufficient-balance error and
return a 5xx NextResponse.json with getCorsHeaders() for other errors; apply the
same change for the second deduction site referenced around lines 48-52 and use
the existing deductCredits, NextResponse.json and getCorsHeaders symbols to
locate and update the code paths.
- Around line 22-37: The handler currently parses request.json() into body and
only checks body.query truthiness; replace this with Zod schema validation by
creating a schema (e.g., const PostResearchSchema = z.object({ query:
z.string().min(1).transform(s => s.trim()).refine(s => s.length > 0) })) and run
it via the shared validate function (validate(PostResearchSchema, await
request.json())) instead of the try/catch + if (!body.query) block; on
validation failure return the same NextResponse.json error with getCorsHeaders
and on success use the validated.value.query (trimmed) for downstream logic so
the input shape and non-empty trimmed query are enforced.
In `@lib/research/postResearchPeopleHandler.ts`:
- Around line 23-24: The request body is not being schema-validated so
num_results can exceed allowed bounds; add a Zod validation step in
postResearchPeopleHandler (or a shared validate function) that defines body: {
query: string().optional(), num_results:
number().int().min(1).max(50).optional().default(10) } (use the documented max
of 50), call validate/parse before using the body variable, and replace usages
of the raw body.num_results with the validated value so Exa always receives a
bounded integer.
---
Duplicate comments:
In `@lib/research/getResearchDiscoverHandler.ts`:
- Around line 50-53: The handler is overwriting the sp_ml[] param when both
mlMin and mlMax are present because proxyToChartmetric uses
url.searchParams.set(); update proxyToChartmetric to accept Record<string,
string | string[]> and, when a value is an array, call url.searchParams.append()
for each element, then in getResearchDiscoverHandler (symbols: mlMin, mlMax,
params and params["sp_ml[]"]) set params["sp_ml[]"] = [mlMin, mlMax] (or a
single string when only one bound exists) so both bounds are sent to
Chartmetric.
---
Nitpick comments:
In `@lib/research/postResearchDeepHandler.ts`:
- Around line 15-71: Extract the repeated
auth→parse→validate→deduct→execute→map-error flow from postResearchDeepHandler
into a reusable utility (e.g., handlePostResearchRequest) that accepts the
NextRequest, creditsToDeduct, an executor callback (the core execution like
chatWithPerplexity), and any operation name for logs; move the calls to
validateAuthContext, request.json parsing, query presence check, deductCredits,
and unified NextResponse creation (preserving getCorsHeaders and the same status
codes for 400/402/500/200) into that utility, then refactor
postResearchDeepHandler to call this utility with creditsToDeduct=25 and an
executor that invokes chatWithPerplexity([{ role: "user", content: body.query
}], "sonar-deep-research") and maps the executor result to the {
status:"success", content, citations } shape while letting the utility handle
error mapping.
🪄 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: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: f201516b-5a30-466b-acc5-77dcbd3f7562
📒 Files selected for processing (18)
app/api/research/deep/route.tsapp/api/research/people/route.tsapp/api/research/web/route.tslib/exa/searchPeople.tslib/research/getResearchAlbumsHandler.tslib/research/getResearchCareerHandler.tslib/research/getResearchCitiesHandler.tslib/research/getResearchDiscoverHandler.tslib/research/getResearchFestivalsHandler.tslib/research/getResearchGenresHandler.tslib/research/getResearchInsightsHandler.tslib/research/getResearchPlaylistsHandler.tslib/research/getResearchSimilarHandler.tslib/research/getResearchTracksHandler.tslib/research/getResearchUrlsHandler.tslib/research/postResearchDeepHandler.tslib/research/postResearchPeopleHandler.tslib/research/postResearchWebHandler.ts
🚧 Files skipped from review as they are similar to previous changes (9)
- lib/research/getResearchAlbumsHandler.ts
- lib/research/getResearchInsightsHandler.ts
- lib/research/getResearchUrlsHandler.ts
- lib/research/getResearchCareerHandler.ts
- lib/research/getResearchTracksHandler.ts
- lib/research/getResearchCitiesHandler.ts
- lib/research/getResearchGenresHandler.ts
- lib/research/getResearchSimilarHandler.ts
- lib/research/getResearchFestivalsHandler.ts
| /** | ||
| * POST /api/research/web | ||
| * | ||
| * Search the web for real-time information. | ||
| */ | ||
| export async function POST(request: NextRequest) { | ||
| return postResearchWebHandler(request); | ||
| } |
There was a problem hiding this comment.
Add endpoint tests for success and error paths.
This new route is added without corresponding endpoint tests in the provided changes. Please add coverage for at least auth failure, invalid JSON/body validation, insufficient credits, provider failure, and success response shape.
Based on learnings: "Applies to app/api/**/route.ts : Write tests for new API endpoints covering all success and error paths".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/api/research/web/route.ts` around lines 12 - 19, Add endpoint tests
covering the new POST /api/research/web route by exercising the exported
POST(request: NextRequest) wrapper and the underlying postResearchWebHandler;
create tests for (1) auth failure (mock invalid/absent auth and assert 401), (2)
invalid JSON/body validation (send malformed or missing fields and assert 400
with validation errors), (3) insufficient credits (mock user/credits check to
return insufficient and assert the appropriate 402/403 response), (4) provider
failure (mock the external provider call used in postResearchWebHandler to throw
or return an error and assert the handler returns a 5xx or mapped error
response), and (5) successful path (mock auth, credits, and provider to return
expected data and assert the success status and response shape). Ensure you mock
dependencies used by postResearchWebHandler (auth, credits check, and provider
client) and verify response status codes and JSON schema for each scenario.
| export async function searchPeople( | ||
| query: string, | ||
| numResults: number = 10, | ||
| ): Promise<ExaPeopleResponse> { | ||
| const apiKey = process.env.EXA_API_KEY; | ||
|
|
||
| if (!apiKey) { | ||
| throw new Error("EXA_API_KEY environment variable is not set"); | ||
| } | ||
|
|
||
| const response = await fetch(`${EXA_BASE_URL}/search`, { | ||
| method: "POST", | ||
| headers: { | ||
| "Content-Type": "application/json", | ||
| "x-api-key": apiKey, | ||
| }, | ||
| body: JSON.stringify({ | ||
| query, | ||
| category: "people", | ||
| numResults, | ||
| contents: { |
There was a problem hiding this comment.
Enforce numResults contract in code, not only docs.
The function documents max 100 but does not enforce bounds. Guard/clamp before sending the request to prevent predictable upstream failures.
Proposed guard
export async function searchPeople(
query: string,
numResults: number = 10,
): Promise<ExaPeopleResponse> {
+ const safeNumResults = Math.min(100, Math.max(1, Math.floor(numResults)));
+
const apiKey = process.env.EXA_API_KEY;
...
body: JSON.stringify({
query,
category: "people",
- numResults,
+ numResults: safeNumResults,
contents: {
highlights: { maxCharacters: 4000 },
summary: true,
},
}),🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/exa/searchPeople.ts` around lines 27 - 47, The function searchPeople
currently documents a "max 100" for numResults but doesn't enforce it; clamp
numResults to a safe range (e.g., Math.max(1, Math.min(numResults, 100))) inside
the searchPeople function before composing the request body so the POST always
sends a value between 1 and 100 (use the clamped variable in the body instead of
the raw numResults).
| const since = sp.get("since"); | ||
| if (since) params.since = since; | ||
|
|
||
| const hasFilters = sp.get("editorial") || sp.get("indie") || sp.get("majorCurator") || sp.get("personalized") || sp.get("chart"); |
There was a problem hiding this comment.
Formatting check is currently failing CI.
Line 35 is very long and matches the reported Prettier failure. Please run formatting (or split this expression) so the pipeline passes.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/research/getResearchPlaylistsHandler.ts` at line 35, The long boolean
expression assigned to hasFilters is causing Prettier to fail; refactor the
assignment in getResearchPlaylistsHandler by replacing the long OR chain
(sp.get("editorial") || sp.get("indie") || sp.get("majorCurator") ||
sp.get("personalized") || sp.get("chart")) with a shorter, formatted expression
such as using an array and Array.prototype.some: e.g., const hasFilters =
["editorial","indie","majorCurator","personalized","chart"].some(k =>
sp.get(k)); this keeps the logic identical (calls sp.get for each key) while
allowing the formatter to wrap lines cleanly.
- POST /api/research/extract — scrape specific URLs into clean
markdown. Handles JS-heavy pages and PDFs. Accepts objective
for focused extraction. (5 credits per URL)
- POST /api/research/enrich — structured data enrichment from
web research. Pass a JSON schema, get typed data back with
citations. Uses Parallel's blocking /result endpoint.
Both tested E2E:
- Extract: Wikipedia page → title + excerpts
- Enrich: "Kaash Paige" → { real_name: "D'Kyla Paige Woolen",
hometown: "Dallas, Texas", label: "Rostrum Records",
biggest_song: "Love Songs" }
Total research endpoints: 25
Made-with: Cursor
New MCP tools registered automatically — chat app picks them up via the existing MCP connection: - research_artist — search + full profile by name - research_metrics — streaming/social metrics (14 platforms) - research_audience — demographics (Instagram/TikTok/YouTube) - research_cities — top listener cities, normalized - research_similar — competitive landscape with career stages - research_playlists — playlist placements with default filters - research_people — find industry contacts via Exa - research_extract — scrape URLs to markdown via Parallel - research_enrich — structured data enrichment via Parallel All tools use the same underlying functions as the REST endpoints. No changes needed in the chat repo. Made-with: Cursor
All 25 REST endpoints now have MCP tool equivalents (23 new + 2 existing). Chat agent has full access to research data. Added: research_profile, research_urls, research_instagram_posts, research_albums, research_tracks, research_career, research_insights, research_lookup, research_track, research_playlist, research_curator, research_discover, research_genres, research_festivals. Made-with: Cursor
…descriptions
- Removed research_profile — identical to research_artist (both
call /artist/{id} on Chartmetric). research_artist is the
single entry point for artist profiles.
- Updated research_albums description to note get_spotify_artist_albums
exists for Spotify-specific data.
- Updated research_urls description to note get_artist_socials
exists for Recoup-connected socials.
22 research MCP tools total.
Made-with: Cursor
5 new endpoints (REST + MCP): - milestones: artist activity feed (playlist adds, chart entries) - venues: performance venue history with cities and capacity - rank: global artist ranking (single number) - charts: global chart positions by platform/country - radio: radio station list (3083 stations) All tested E2E. Charts requires interval+type params for Spotify. Total: 30 REST endpoints, 27 MCP tools. Made-with: Cursor
6 non-artist handlers (genres, festivals, radio, discover, charts, curator) now use shared handleResearchRequest instead of duplicating auth + deductCredits + proxyToChartmetric boilerplate. Two shared handlers: - handleArtistResearch: 15 artist-scoped handlers - handleResearchRequest: 6 non-artist handlers - 4 handlers with unique logic keep their own (search, track, playlist, lookup — each has custom resolution/parsing) Made-with: Cursor
There was a problem hiding this comment.
Actionable comments posted: 10
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
🟡 Minor comments (14)
app/api/research/venues/route.ts-5-15 (1)
5-15:⚠️ Potential issue | 🟡 MinorEmpty JSDoc comments need meaningful documentation.
The JSDoc blocks are present but contain no actual documentation. Per coding guidelines, all API routes should have JSDoc comments that describe the endpoint's purpose, parameters, and response.
📝 Proposed documentation
/** - * + * Handles CORS preflight requests for the venues endpoint. + * `@returns` Empty response with CORS headers */ export async function OPTIONS() { return new NextResponse(null, { status: 200, headers: getCorsHeaders() }); } /** - * - * `@param` request + * Returns venues the artist has performed at, including capacity and location. + * Requires `artist` query parameter (name or UUID). + * `@param` request - The incoming Next.js request + * `@returns` Venue data for the specified artist */ export async function GET(request: NextRequest) {As per coding guidelines: "All API routes should have JSDoc comments".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/api/research/venues/route.ts` around lines 5 - 15, The empty JSDoc blocks above the OPTIONS export (function OPTIONS) and the following handler parameter should be replaced with meaningful documentation: add a brief description of the endpoint's purpose, document the request parameter (type and expected shape or headers), and describe the response (status codes and headers, e.g., CORS preflight 200 with getCorsHeaders()). Update the JSDoc for function OPTIONS and the subsequent handler's JSDoc to follow the project's JSDoc style and include `@param` and `@returns` tags so the route's behavior is clear.lib/research/getResearchRankHandler.ts-16-16 (1)
16-16:⚠️ Potential issue | 🟡 MinorUse nullish coalescing and a typed payload for
rank.Line 16 uses
|| null, which can coerce valid falsy values, andas anydrops type safety.Proposed fix
- (data) => ({ rank: (data as any)?.artist_rank || null }), + (data) => { + const rank = (data as { artist_rank?: number | null })?.artist_rank ?? null; + return { rank }; + },As per coding guidelines
lib/**/*.ts: "Use TypeScript for type safety".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/research/getResearchRankHandler.ts` at line 16, Replace the unsafe cast and falsy-coercing fallback in the mapping that returns { rank: ... } by introducing a typed payload (e.g., an interface like ResearchRankPayload with artist_rank?: number | null) and using a safe nullish coalescing fallback; specifically, change the occurrence of (data as any)?.artist_rank || null to a typed access (data as ResearchRankPayload)?.artist_rank ?? null inside the mapping used by getResearchRankHandler so valid falsy values (0, '') are preserved and you regain compile-time type safety.lib/mcp/tools/research/registerResearchPeopleTool.ts-9-13 (1)
9-13:⚠️ Potential issue | 🟡 MinorAdd integer bounds to the
num_resultsschema.The Zod schema accepts any number, but
searchPeopledocuments a maximum of 100 results. Without bounds validation, invalid or oversized requests could reach the upstream Exa API. Add.int(),.min(1), and.max(100)constraints to enforce valid input at the MCP tool boundary.Proposed fix
num_results: z .number() + .int() + .min(1) + .max(100) .optional() .default(10) .describe("Number of results to return (default: 10)"),🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/mcp/tools/research/registerResearchPeopleTool.ts` around lines 9 - 13, Update the Zod schema for num_results to enforce integer bounds so oversized or invalid requests are blocked at the MCP boundary: change the num_results definition that currently uses z.number() to include .int().min(1).max(100) (keeping .optional().default(10) and .describe("Number of results to return (default: 10)") intact) so the schema validates integer values between 1 and 100 for num_results before calling searchPeople.app/api/research/radio/route.ts-5-10 (1)
5-10:⚠️ Potential issue | 🟡 MinorEmpty JSDoc comments should be populated.
Per coding guidelines, all API routes should have JSDoc comments. These are empty and don't provide documentation value.
📝 Suggested documentation
/** - * + * CORS preflight handler for /api/research/radio */ export async function OPTIONS() { return new NextResponse(null, { status: 200, headers: getCorsHeaders() }); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/api/research/radio/route.ts` around lines 5 - 10, Populate the empty JSDoc for the OPTIONS handler by adding a concise description of its purpose and behavior, parameters (none), and return type; update the comment above export async function OPTIONS() to explain that it responds to CORS preflight requests, returns a 200 NextResponse with CORS headers from getCorsHeaders(), and note any side effects or relevant headers used so callers and maintainers understand the route.app/api/research/radio/route.ts-12-18 (1)
12-18:⚠️ Potential issue | 🟡 MinorPopulate JSDoc with endpoint description.
📝 Suggested documentation
/** - * - * `@param` request + * GET /api/research/radio + * Returns the list of radio stations tracked by Chartmetric. + * Requires authentication and deducts 5 credits. + * + * `@param` request - The incoming Next.js request + * `@returns` JSON response with { status: "success", stations: Array } or error */ export async function GET(request: NextRequest) { return getResearchRadioHandler(request); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/api/research/radio/route.ts` around lines 12 - 18, Add a descriptive JSDoc block above the exported async function GET that explains this endpoint's purpose (e.g., "Handles GET requests for the research radio endpoint"), describes the request parameter (NextRequest) and any expected query params or headers, and documents the return value (the Response returned by getResearchRadioHandler). Update the JSDoc to reference the handler function name getResearchRadioHandler and include tags like `@param` and `@returns` so readers and editors can quickly understand inputs/outputs.lib/mcp/tools/research/registerResearchCitiesTool.ts-1-1 (1)
1-1:⚠️ Potential issue | 🟡 MinorAddress Prettier formatting failure.
The pipeline indicates a Prettier formatting check failure. Run
prettier --writeon this file to resolve.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/mcp/tools/research/registerResearchCitiesTool.ts` at line 1, Prettier formatting failed for this file; run the formatter (e.g., prettier --write) on the file containing the import of McpServer (the import statement "import { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\"") or apply Prettier rules to fix whitespace/quoting/newline issues so the file conforms to project Prettier settings; after formatting, re-run the pipeline to confirm the Prettier check passes.lib/mcp/tools/research/registerResearchSimilarTool.ts-75-79 (1)
75-79:⚠️ Potential issue | 🟡 MinorResponse normalization differs from the HTTP handler.
The HTTP handler (
getResearchSimilarHandler.tslines 37-42) returnstotalunconditionally from the data object, while this MCP tool conditionally omitstotalwhendatais an array. This inconsistency could cause confusion for consumers expecting identical shapes from both interfaces.Consider aligning the normalization logic:
♻️ Suggested alignment with HTTP handler
const data = result.data as Record<string, unknown>; return getToolResultSuccess({ artists: Array.isArray(data) ? data : data?.data || [], - total: Array.isArray(data) ? undefined : data?.total, + total: (data as Record<string, unknown>)?.total, });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/mcp/tools/research/registerResearchSimilarTool.ts` around lines 75 - 79, The tool's response normalization in registerResearchSimilarTool (around the getToolResultSuccess call) diverges from the HTTP handler getResearchSimilarHandler by conditionally omitting total when data is an array; change the normalization to match the HTTP handler by always returning total from the data object (use data?.total) and keep artists computed as Array.isArray(data) ? data : data?.data || [], so getToolResultSuccess returns the same shape as the HTTP handler.lib/mcp/tools/research/registerResearchChartsTool.ts-44-62 (1)
44-62:⚠️ Potential issue | 🟡 MinorMCP tools lack credit deduction—a pattern across all tools, not just charts.
The HTTP handler deducts 5 credits via
handleResearchRequest, but this MCP tool (and all others inlib/mcp/tools/) skip credit deduction entirely. This architectural inconsistency between HTTP and MCP interfaces creates a potential credit-bypass path. Consider whether MCP tools should share the same credit deduction logic as their HTTP counterparts for billing consistency.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/mcp/tools/research/registerResearchChartsTool.ts` around lines 44 - 62, The MCP tool handler in registerResearchChartsTool.ts currently calls proxyToChartmetric and returns getToolResultSuccess/getToolResultError without deducting credits; add a call to the shared credit-deduction routine (reuse handleResearchRequest or its underlying deductCredits function) at the start of the async (args) handler (or wrap the proxy call in handleResearchRequest) so the same 5-credit deduction occurs for MCP requests; locate the anonymous async handler, proxyToChartmetric, and the getToolResultSuccess/getToolResultError calls and ensure the credit deduction runs before the external call and that failures still return getToolResultError while not double-deducting.lib/mcp/tools/research/registerResearchUrlsTool.ts-34-35 (1)
34-35:⚠️ Potential issue | 🟡 MinorMissing status check on
proxyToChartmetricresult.Per the
proxyToChartmetriccontract (seelib/research/proxyToChartmetric.ts:5-52), non-200 responses return{ data: { error: string }, status: number }without throwing. The current implementation returnsresult.dataunconditionally, which could surface Chartmetric error payloads as successful tool results.🛡️ Suggested fix to check status
const result = await proxyToChartmetric(`/artist/${resolved.id}/urls`); + if (result.status !== 200) { + return getToolResultError( + `Request failed with status ${result.status}`, + ); + } return getToolResultSuccess(result.data);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/mcp/tools/research/registerResearchUrlsTool.ts` around lines 34 - 35, The call to proxyToChartmetric in registerResearchUrlsTool returns a non-throwing result object with status and data; update the code in the function that calls proxyToChartmetric(`/artist/${resolved.id}/urls`) to check result.status === 200 before returning getToolResultSuccess(result.data), and when status is not 200 return a failure via getToolResultError (or similar error-return helper) including the Chartmetric error payload (e.g., result.data.error) and the status so callers don't treat error responses as successes.lib/mcp/tools/research/registerResearchAudienceTool.ts-41-44 (1)
41-44:⚠️ Potential issue | 🟡 MinorMissing status check on
proxyToChartmetricresult.Same issue as in
registerResearchUrlsTool.ts— the result status isn't checked before returning data, which could propagate Chartmetric error payloads as successful tool responses.🛡️ Suggested fix
const result = await proxyToChartmetric( `/artist/${resolved.id}/${platform}-audience-stats`, ); + if (result.status !== 200) { + return getToolResultError( + `Request failed with status ${result.status}`, + ); + } return getToolResultSuccess(result.data);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/mcp/tools/research/registerResearchAudienceTool.ts` around lines 41 - 44, The call to proxyToChartmetric in registerResearchAudienceTool.ts returns a response object whose HTTP status isn't checked, so Chartmetric errors can be returned as successes; update the code after calling proxyToChartmetric(`/artist/${resolved.id}/${platform}-audience-stats`) to inspect result.status (or equivalent), and if it indicates failure return a failure tool response (e.g., via getToolResultFailure with the status and error payload) otherwise return getToolResultSuccess(result.data); mirror the same status-check pattern used in registerResearchUrlsTool.ts and reference proxyToChartmetric and getToolResultSuccess/getToolResultFailure to locate where to change.lib/mcp/tools/research/registerResearchCareerTool.ts-34-37 (1)
34-37:⚠️ Potential issue | 🟡 MinorMissing status check on
proxyToChartmetricresult.Consistent with the pattern in other MCP tools in this PR, the status code isn't validated before returning data. Add a status check for robustness.
🛡️ Suggested fix
const result = await proxyToChartmetric( `/artist/${resolved.id}/career`, ); + if (result.status !== 200) { + return getToolResultError( + `Request failed with status ${result.status}`, + ); + } return getToolResultSuccess(result.data);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/mcp/tools/research/registerResearchCareerTool.ts` around lines 34 - 37, The call to proxyToChartmetric in registerResearchCareerTool returns a response whose HTTP status is not being validated before using result.data; update the handler to check the response status (e.g., ensure result.status === 200 or a success-range) and only call getToolResultSuccess(result.data) on success, otherwise return an error path (e.g., getToolResultFailure or a similar failure helper) including the status and error payload; locate the proxyToChartmetric call and the return of getToolResultSuccess in registerResearchCareerTool to add this status check and appropriate failure return.lib/research/postResearchEnrichHandler.ts-23-47 (1)
23-47:⚠️ Potential issue | 🟡 MinorAdd validation for
processorparameter.The
body.processortype annotation suggests"base" | "core" | "ultra", but there's no runtime validation. Invalid values silently default to 5 credits (line 47). The MCP tool counterpart usesz.enum(["base", "core", "ultra"])for strict validation—this handler should be consistent.🛡️ Suggested validation
+ const validProcessors = ["base", "core", "ultra"] as const; + const processor = body.processor && validProcessors.includes(body.processor) + ? body.processor + : "base"; + - const creditCost = body.processor === "ultra" ? 25 : body.processor === "core" ? 10 : 5; + const creditCost = processor === "ultra" ? 25 : processor === "core" ? 10 : 5; try { await deductCredits({ accountId, creditsToDeduct: creditCost });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/research/postResearchEnrichHandler.ts` around lines 23 - 47, Add runtime validation for body.processor to ensure it is one of "base" | "core" | "ultra" before computing creditCost: check the parsed request body (body.processor) and if it's missing or not one of the allowed strings return a 400 JSON response (similar style to the input/schema checks). Then compute creditCost using the validated value (the existing creditCost expression referencing body.processor) so invalid values don't silently fall back to 5. Use the same error/response pattern and headers as the other early-return validations in postResearchEnrichHandler.lib/parallel/enrichEntity.ts-1-1 (1)
1-1:⚠️ Potential issue | 🟡 MinorRun Prettier to fix formatting.
Pipeline indicates Prettier check failed. Run
prettier --writeon this file to resolve.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/parallel/enrichEntity.ts` at line 1, Reformat this file with the project's Prettier settings (run `prettier --write`) to fix the formatting error reported by CI; specifically ensure the top-level constant PARALLEL_BASE_URL and surrounding code are reformatted to match the repo style so Prettier checks pass.lib/mcp/tools/research/registerResearchPlaylistsTool.ts-1-1 (1)
1-1:⚠️ Potential issue | 🟡 MinorRun Prettier to fix formatting.
Pipeline indicates Prettier check failed. Run
prettier --writeon this file to resolve.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/mcp/tools/research/registerResearchPlaylistsTool.ts` at line 1, Run Prettier to fix formatting for this file: reformat the file (e.g., run `prettier --write`) so the import line "import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";" and the rest of registerResearchPlaylistsTool.ts conform to the project's Prettier rules; commit the reformatted file to resolve the pipeline Prettier check failure.
🧹 Nitpick comments (21)
app/api/research/rank/route.ts (1)
5-7: Replace empty JSDoc blocks with meaningful route documentation.Line 5 and Line 12 docblocks are empty; add concise endpoint/method/param descriptions.
As per coding guidelines
app/api/**/route.ts: "All API routes should have JSDoc comments".Also applies to: 12-15
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/api/research/rank/route.ts` around lines 5 - 7, The empty JSDoc blocks above the route handler need to be replaced with concise route documentation: add a short JSDoc before the exported route handler(s) (e.g., the GET/POST/handler function name used in this file) describing the endpoint path, HTTP method, expected query/body parameters, and the response shape/status codes; also update the second empty block (lines ~12-15) with param descriptions and an example response or error conditions so the file complies with the API route documentation guideline.lib/mcp/tools/research/registerResearchInstagramPostsTool.ts (1)
34-37: Consider normalizing response shape for consistency.Other collection-returning tools (
research_tracks,research_albums,research_insights) normalize responses into{ [collection]: Array }. This tool returnsresult.datadirectly, which may yield inconsistent response shapes across the research tool family.If the Instagram data structure permits, consider normalizing to
{ posts: Array.isArray(data) ? data : [] }for a more predictable API.♻️ Optional normalization
const result = await proxyToChartmetric( `/SNS/deepSocial/cm_artist/${resolved.id}/instagram`, ); - return getToolResultSuccess(result.data); + const data = result.data; + return getToolResultSuccess({ + posts: Array.isArray(data) ? data : [], + });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/mcp/tools/research/registerResearchInstagramPostsTool.ts` around lines 34 - 37, The Instagram tool currently returns result.data directly which causes inconsistent shapes vs other research tools; inside the registerResearchInstagramPostsTool handler (the block calling proxyToChartmetric and getToolResultSuccess) normalize the response to an object like { posts: Array.isArray(result.data) ? result.data : [] } (or appropriate property name if Instagram uses a different key) and pass that normalized object into getToolResultSuccess so the tool returns a consistent collection-shaped payload.lib/research/getResearchRadioHandler.ts (1)
20-27: Consider distinguishing credit deduction errors.The current implementation catches all exceptions from
deductCreditsand returns 402 with "Insufficient credits". IfdeductCreditscan throw for other reasons (e.g., database errors), this may mask the actual failure.♻️ More precise error handling
try { await deductCredits({ accountId, creditsToDeduct: 5 }); - } catch { + } catch (error) { + const isInsufficientCredits = error instanceof Error && + error.message.includes("Insufficient"); return NextResponse.json( - { status: "error", error: "Insufficient credits" }, - { status: 402, headers: getCorsHeaders() }, + { status: "error", error: isInsufficientCredits ? "Insufficient credits" : "Credit deduction failed" }, + { status: isInsufficientCredits ? 402 : 500, headers: getCorsHeaders() }, ); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/research/getResearchRadioHandler.ts` around lines 20 - 27, Catch the error thrown by deductCredits({ accountId, creditsToDeduct: 5 }) into a variable and distinguish insufficient-credit failures from other failures: check for a specific error marker (e.g., error instanceof InsufficientCreditsError, error.name === 'InsufficientCreditsError', or error.code === 'INSUFFICIENT_CREDITS') and return the 402 NextResponse.json({ status: "error", error: "Insufficient credits" }, ...) only in that case; for other errors, log or capture the error and return a 500 NextResponse.json({ status: "error", error: "Internal server error" }, ...) (or rethrow) so database/unknown errors are not masked. Ensure references to deductCredits and the surrounding try/catch in getResearchRadioHandler.ts are updated accordingly.lib/mcp/tools/research/index.ts (1)
28-29: Minor: Missing blank line before JSDoc.There's no blank line between the last import (line 28) and the JSDoc comment (line 29). This is a minor formatting inconsistency but may be caught by the linter.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/mcp/tools/research/index.ts` around lines 28 - 29, Minor formatting: add a single blank line between the last import statement (import { registerResearchRadioTool } from "./registerResearchRadioTool";) and the following JSDoc block so the JSDoc is separated from imports; update the top of lib/mcp/tools/research/index.ts by inserting one empty line between that import and the /** JSDoc comment to satisfy linter/style rules.app/api/research/charts/route.ts (1)
5-10: JSDoc comments are incomplete.The JSDoc blocks are present but lack descriptions. Per coding guidelines, all API routes should have JSDoc comments. Consider adding meaningful descriptions for both handlers.
📝 Suggested documentation
/** - * + * OPTIONS handler for CORS preflight requests. */ export async function OPTIONS() { return new NextResponse(null, { status: 200, headers: getCorsHeaders() }); } /** - * - * `@param` request + * GET /api/research/charts + * + * Returns global chart positions for a given platform (Spotify, Apple Music, etc.). + * + * `@param` request - The incoming Next.js request object */ export async function GET(request: NextRequest) { return getResearchChartsHandler(request); }Also applies to: 12-18
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/api/research/charts/route.ts` around lines 5 - 10, The JSDoc blocks for the API route handlers (e.g., the exported async function OPTIONS and the other handlers around that block) are empty—add concise JSDoc comments above each exported handler describing what the route/handler does, its expected inputs or purpose, and the response (e.g., "Handles CORS preflight requests and returns 200 with CORS headers" for OPTIONS); apply the same pattern to the other handlers in this file so each exported function has a meaningful description, parameters/returns notes if applicable, and any relevant behavior details.lib/research/getResearchChartsHandler.ts (2)
20-51: Consider using Zod schema for input validation.Per coding guidelines, all API endpoints should use a validate function with Zod for input parsing. Currently, parameters are manually extracted without schema validation. This would improve type safety and provide better error messages.
♻️ Suggested Zod schema approach
import { z } from "zod"; const chartsQuerySchema = z.object({ platform: z.string().min(1, "platform parameter is required"), country: z.string().optional(), interval: z.string().optional(), type: z.string().optional(), latest: z.string().default("true"), }); // Then in handler: const parseResult = chartsQuerySchema.safeParse(Object.fromEntries(searchParams)); if (!parseResult.success) { return NextResponse.json( { status: "error", error: parseResult.error.issues[0].message }, { status: 400, headers: getCorsHeaders() }, ); } const { platform, country, interval, type, latest } = parseResult.data;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/research/getResearchChartsHandler.ts` around lines 20 - 51, Replace manual query extraction with Zod-based validation: define a chartsQuerySchema (using z.object with platform: z.string().min(1), country/interval/type optional, latest default "true") and run safeParse/validate on Object.fromEntries(searchParams) at the top of getResearchChartsHandler; if validation fails return a 400 NextResponse.json with the validation message and getCorsHeaders(), and only after successful parse call deductCredits({ accountId, creditsToDeduct: 5 }); then use parsed fields (platform, country, interval, type, latest) to populate params instead of reading searchParams directly. Ensure you reference chartsQuerySchema, searchParams, NextResponse, getCorsHeaders, and deductCredits when making the change.
62-65: Response structure uses nesteddatawrapper.The coding guidelines specify to keep API response bodies flat with fields at the root level, not nested inside a
datawrapper. However, this pattern appears consistent with other research handlers in this PR. If this is intentional for this endpoint family, consider documenting the exception.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/research/getResearchChartsHandler.ts` around lines 62 - 65, The response body currently nests the payload under a data property (in the return inside getResearchChartsHandler), violating the flat-root response convention; change the NextResponse.json call to place fields from result.data at the root (e.g., return { status: "success", ...result.data }) instead of { status: "success", data: result.data }, and if the nested pattern is intentional for the research handlers, update or add a short note in docs or a comment to justify the exception and make the pattern consistent across related handlers.lib/mcp/tools/research/registerResearchAudienceTool.ts (1)
40-40: Redundant fallback — Zod default already provides"instagram".The schema at line 13 uses
.default("instagram"), soargs.platformwill never beundefinedafter Zod parsing. The?? "instagram"fallback is defensive but unnecessary.✂️ Simplification
- const platform = args.platform ?? "instagram"; + const platform = args.platform;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/mcp/tools/research/registerResearchAudienceTool.ts` at line 40, The fallback is redundant because the Zod schema already sets a default; replace the defensive expression by using args.platform directly (remove the "?? 'instagram'" from the const assignment) in the registerResearchAudienceTool code so the variable simply reads the parsed value from args.platform; ensure no other code assumes undefined for platform.lib/mcp/tools/research/registerResearchEnrichTool.ts (1)
43-43: Redundant fallback — Zod default already provides"base".The schema at line 15 uses
.default("base"), soargs.processorwill always be defined after parsing. The?? "base"fallback is unnecessary.✂️ Simplification
- args.processor ?? "base", + args.processor,🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/mcp/tools/research/registerResearchEnrichTool.ts` at line 43, Remove the redundant fallback by deleting the `?? "base"` after `args.processor` in the call site (the `args.processor ?? "base"` expression) since the Zod schema already sets a default of "base"; update the code in the registerResearchEnrichTool usage to pass `args.processor` directly (referencing the `args.processor` symbol and the surrounding function `registerResearchEnrichTool`) so the value comes from the parsed args without an extra fallback.app/api/research/milestones/route.ts (1)
5-15: Empty JSDoc comments provide no documentation value.The JSDoc blocks for both
OPTIONSandGEThandlers are empty placeholders. As per coding guidelines, all API routes should have meaningful JSDoc comments describing the endpoint's purpose, parameters, and response.📝 Suggested JSDoc improvements
/** - * + * OPTIONS handler for CORS preflight requests. */ export async function OPTIONS() { return new NextResponse(null, { status: 200, headers: getCorsHeaders() }); } /** - * - * `@param` request + * GET /api/research/milestones + * + * Returns an artist's activity feed — playlist adds, chart entries, + * and other notable events tracked by Chartmetric. + * + * `@param` request - The incoming request with artist query parameter */ export async function GET(request: NextRequest) {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/api/research/milestones/route.ts` around lines 5 - 15, The file contains empty JSDoc blocks for the API route handlers; update the JSDoc for the exported handlers (OPTIONS and GET in route.ts) to provide a concise description of the endpoint's purpose, the incoming parameters (e.g., Request or query params), and the expected response shape/status codes; reference the OPTIONS and GET functions and include notes about CORS headers (getCorsHeaders) in the docs so consumers and maintainers understand behavior and responses.lib/research/getResearchMilestonesHandler.ts (1)
12-18: Consider defining a type for the Chartmetric response.The
(data as any)?.insightscast works but loses type safety. A minimal interface would improve maintainability and IDE support without significant effort.🔧 Optional type improvement
+interface MilestonesResponse { + insights?: unknown[]; +} + export async function getResearchMilestonesHandler(request: NextRequest) { return handleArtistResearch( request, (cmId) => `/artist/${cmId}/milestones`, undefined, - (data) => ({ milestones: (data as any)?.insights || [] }), + (data) => ({ milestones: (data as MilestonesResponse)?.insights || [] }), ); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/research/getResearchMilestonesHandler.ts` around lines 12 - 18, Replace the untyped cast in getResearchMilestonesHandler by introducing a minimal interface for the Chartmetric response (e.g. ChartmetricMilestonesResponse with an insights: Milestone[] field) and use that type instead of any; update the mapping callback passed to handleArtistResearch to expect ChartmetricMilestonesResponse and return { milestones: response.insights || [] } (or make handleArtistResearch generic so you can call handleArtistResearch<ChartmetricMilestonesResponse>(...)). This preserves type safety while keeping the transformation logic in getResearchMilestonesHandler and requires adding the new interface (and optional Milestone type) and, if necessary, a generic parameter on handleArtistResearch.lib/mcp/tools/research/registerResearchMetricsTool.ts (1)
10-14: Consider usingz.enum()for platform validation consistency.The sibling tool
registerResearchAudienceToolusesz.enum(["instagram", "tiktok", "youtube"])to constrain platform values at the schema level. Here,sourceaccepts any string and is interpolated directly into the URL path at line 42. While Chartmetric will reject invalid platforms, schema-level validation provides better error messages and consistency across tools.♻️ Suggested improvement
const schema = z.object({ artist: z.string().describe("Artist name to research"), source: z - .string() + .enum([ + "spotify", + "instagram", + "tiktok", + "youtube_channel", + "soundcloud", + "deezer", + "twitter", + "facebook", + ]) .describe( "Platform: spotify, instagram, tiktok, youtube_channel, soundcloud, deezer, twitter, facebook, etc.", ), });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/mcp/tools/research/registerResearchMetricsTool.ts` around lines 10 - 14, The schema for `source` in `registerResearchMetricsTool` currently uses `z.string()` and should be tightened to a `z.enum()` that lists the allowed platforms (the same set used by `registerResearchAudienceTool`, e.g., "instagram", "tiktok", "youtube", etc.) so invalid platform values are rejected at schema validation instead of only by the downstream Chartmetric call; update the `source` schema to `z.enum([...])`, keep the descriptive text, and ensure any code that reads `source` (the URL path interpolation) continues to work with the enum values.lib/research/postResearchExtractHandler.ts (1)
46-53: Consider preserving the original error message fromdeductCredits.The
deductCreditsfunction throws with specific messages (e.g., "No credits usage found for this account" vs "Insufficient credits. Required: X, Available: Y"). Catching all exceptions as "Insufficient credits" loses diagnostic information.♻️ Preserve error detail
try { await deductCredits({ accountId, creditsToDeduct: 5 * body.urls.length }); } catch { + } catch (error) { return NextResponse.json( - { status: "error", error: "Insufficient credits" }, + { status: "error", error: error instanceof Error ? error.message : "Insufficient credits" }, { status: 402, headers: getCorsHeaders() }, ); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/research/postResearchExtractHandler.ts` around lines 46 - 53, The catch for deductCredits currently swallows the original error; change the try/catch to catch the error (e.g., catch (err)) and return the NextResponse.json error payload using the original message (e.g., error: err?.message || String(err) || "Insufficient credits") while keeping the 402 status and getCorsHeaders; update references in this block (deductCredits, accountId, creditsToDeduct, NextResponse.json, getCorsHeaders) so diagnostic details from deductCredits are preserved safely.lib/mcp/tools/research/registerResearchRankTool.ts (1)
38-38: Consider using nullish coalescing for rank extraction.Using
|| nullwill convert a rank of0tonull. While Chartmetric ranks are typically 1-indexed, using?? nullis more defensive and semantically correct for "missing value" scenarios.♻️ Minor fix
- const rank = (result.data as any)?.artist_rank || null; + const rank = (result.data as any)?.artist_rank ?? null;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/mcp/tools/research/registerResearchRankTool.ts` at line 38, The current extraction uses const rank = (result.data as any)?.artist_rank || null which will coerce a valid 0 rank to null; change the fallback to the nullish coalescing operator so use (result.data as any)?.artist_rank ?? null instead — update the declaration of rank in registerResearchRankTool (the const rank binding that reads result.data.artist_rank) to use ?? null so only null/undefined become null while preserving numeric 0.lib/parallel/enrichEntity.ts (3)
80-86: Consider defensive type check foroutput.basis.The
output?.basis || []handles undefined, but ifbasisexists but isn't an array (API contract change),flatMapwill throw. A defensive check improves resilience:🛡️ Defensive type guard
- const citations = (output?.basis || []).flatMap( + const basisArray = Array.isArray(output?.basis) ? output.basis : []; + const citations = basisArray.flatMap(🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/parallel/enrichEntity.ts` around lines 80 - 86, The code assumes output.basis is an array when building citations from resultData.output; add a defensive type guard before using flatMap: verify output exists and Array.isArray(output.basis) (or coerce to an empty array) to avoid runtime errors if basis is present but not an array. Update the citations computation (referencing resultData, output, and the citations variable) to use the guarded/normalized basis value so flatMap is only called on a true array.
55-60: Add type safety for the task creation response.The response from
createResponse.json()is untyped. Consider adding a minimal type assertion or validation to ensurerun_idexists with the expected type.♻️ Type-safe response handling
- const taskRun = await createResponse.json(); - const runId = taskRun.run_id; + const taskRun = (await createResponse.json()) as { run_id?: string }; + const runId = taskRun.run_id;Or for stricter validation:
interface TaskRunResponse { run_id: string; } const taskRun: TaskRunResponse = await createResponse.json();🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/parallel/enrichEntity.ts` around lines 55 - 60, The code reads an untyped JSON from createResponse.json() and then accesses run_id (taskRun and runId); add type safety by defining a minimal interface (e.g., TaskRunResponse { run_id: string }) and assert/validate the parsed JSON against it, or perform a runtime check that taskRun has a string run_id before assigning runId, throwing a clear error if validation fails; apply this change around the createResponse.json() call and the taskRun/runId variables to ensure type-safe handling.
20-94: Function exceeds 50-line guideline.At ~74 lines, this function exceeds the coding guideline suggesting functions stay under 50 lines. Consider extracting the task creation (lines 32-60) and result polling (lines 62-93) into separate private helpers if this module grows.
As per coding guidelines: "Keep functions under 50 lines."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/parallel/enrichEntity.ts` around lines 20 - 94, The enrichEntity function is over the 50-line guideline—extract the task creation and result fetching logic into two private helpers to reduce size: move the POST-to-/tasks/runs block (currently creating createResponse, checking createResponse.ok, parsing taskRun and runId) into a helper named e.g. createParallelTaskRun(input, outputSchema, processor, apiKey) and move the GET-to-/tasks/runs/{runId}/result logic (timeout/404/other error handling, parsing resultData, building output and citations) into a helper named e.g. fetchParallelTaskResult(runId, timeout, apiKey); then have enrichEntity call those two helpers and return the final EnrichResult, preserving existing error messages and behavior.lib/parallel/extractUrl.ts (2)
56-56: Redundantawaitin return statement.The
awaitbeforeresponse.json()is unnecessary when immediately returning. The async function already wraps the return value in a Promise.♻️ Minor cleanup
- return await response.json(); + return response.json();Note: Some style guides prefer explicit
awaitfor consistency in error stack traces. This is a minor preference.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/parallel/extractUrl.ts` at line 56, Remove the redundant await in the return statement: replace the explicit "return await response.json()" inside the async function (the function containing the response.json() call in lib/parallel/extractUrl.ts) with "return response.json()" so the async function directly returns the Promise from response.json().
22-24: Input constraints documented but not validated.The JSDoc mentions "max 10 per request" for URLs and "max 3000 chars" for objective, but these aren't validated before the API call. Consider adding validation to fail fast with clear errors:
🛡️ Add input validation
export async function extractUrl( urls: string[], objective?: string, fullContent: boolean = false, ): Promise<ExtractResponse> { + if (urls.length === 0) { + throw new Error("At least one URL is required"); + } + if (urls.length > 10) { + throw new Error("Maximum 10 URLs allowed per request"); + } + if (objective && objective.length > 3000) { + throw new Error("Objective must be 3000 characters or fewer"); + } + const apiKey = process.env.PARALLEL_API_KEY;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/parallel/extractUrl.ts` around lines 22 - 24, The JSDoc limits (max 10 URLs and max 3000 chars for objective) are not enforced; update the exported function (e.g., extractUrl or extractUrls in lib/parallel/extractUrl.ts) to validate inputs up-front: check that urls is an array with 1–10 items, that each item is a string (optionally validate via new URL(...) to ensure well-formed URLs), and that objective, if provided, is a string of length ≤3000; if any check fails, throw a clear, synchronous error (or return a rejected Promise) with a descriptive message before making the API call so callers fail fast.lib/mcp/tools/research/registerResearchPlaylistsTool.ts (2)
60-67: Filter behavior differs wheneditorialis explicitly provided.When
editorialis explicitly set (true or false), only that filter is sent. This means:
editorial: true→ only editorial playlists (loses indie, majorCurator, popularIndie)editorial: false→ sendseditorial=falsewithout other filtersCompare with the REST handler in
getResearchPlaylistsHandler.tswhich supportsindie,majorCurator,personalized, andchartfilters individually. If this is intentional simplification for MCP, consider documenting it. Otherwise, exposing additional filter options would provide parity.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/mcp/tools/research/registerResearchPlaylistsTool.ts` around lines 60 - 67, Current behavior in registerResearchPlaylistsTool (the args -> queryParams block) only sends the editorial filter when args.editorial is defined, dropping indie/majorCurator/popularIndie; update the logic so that when args.editorial is provided you still populate other filter query params from args if present (e.g., args.indie, args.majorCurator, args.popularIndie, and any supported personalized/chart flags) instead of overwriting them, mirroring the behavior in getResearchPlaylistsHandler.ts; locate the conditional around args.editorial and change it to set queryParams.editorial = String(args.editorial) while also conditionally setting queryParams.indie, queryParams.majorCurator, queryParams.popularIndie (and personalized/chart if supported) from args when those fields are present, or document the intentional simplification if parity is not desired.
24-28: Thelimitdefault makes the conditional on line 58 always true.Since
limithas.default(20),args.limitwill always be defined and truthy (20). The conditionalif (args.limit)on line 58 will always evaluate to true, solimitis always included inqueryParams.This works correctly but is misleading. Consider removing the conditional for clarity:
♻️ Clearer implementation
const queryParams: Record<string, string> = {}; - if (args.limit) queryParams.limit = String(args.limit); + queryParams.limit = String(args.limit);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/mcp/tools/research/registerResearchPlaylistsTool.ts` around lines 24 - 28, The conditional "if (args.limit)" is misleading because args.limit always has a default 20; update registerResearchPlaylistsTool to always include the limit param instead of conditionally adding it: remove the "if (args.limit)" guard and assign queryParams.limit = args.limit (or otherwise ensure queryParams includes args.limit unconditionally). This keeps the z schema default behavior and makes the intent explicit by using args.limit and queryParams directly.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 1868c788-3137-4922-9193-f96aacc7789b
📒 Files selected for processing (45)
app/api/research/charts/route.tsapp/api/research/enrich/route.tsapp/api/research/extract/route.tsapp/api/research/milestones/route.tsapp/api/research/radio/route.tsapp/api/research/rank/route.tsapp/api/research/venues/route.tslib/mcp/tools/index.tslib/mcp/tools/research/index.tslib/mcp/tools/research/registerResearchAlbumsTool.tslib/mcp/tools/research/registerResearchArtistTool.tslib/mcp/tools/research/registerResearchAudienceTool.tslib/mcp/tools/research/registerResearchCareerTool.tslib/mcp/tools/research/registerResearchChartsTool.tslib/mcp/tools/research/registerResearchCitiesTool.tslib/mcp/tools/research/registerResearchCuratorTool.tslib/mcp/tools/research/registerResearchDiscoverTool.tslib/mcp/tools/research/registerResearchEnrichTool.tslib/mcp/tools/research/registerResearchExtractTool.tslib/mcp/tools/research/registerResearchFestivalsTool.tslib/mcp/tools/research/registerResearchGenresTool.tslib/mcp/tools/research/registerResearchInsightsTool.tslib/mcp/tools/research/registerResearchInstagramPostsTool.tslib/mcp/tools/research/registerResearchLookupTool.tslib/mcp/tools/research/registerResearchMetricsTool.tslib/mcp/tools/research/registerResearchMilestonesTool.tslib/mcp/tools/research/registerResearchPeopleTool.tslib/mcp/tools/research/registerResearchPlaylistTool.tslib/mcp/tools/research/registerResearchPlaylistsTool.tslib/mcp/tools/research/registerResearchRadioTool.tslib/mcp/tools/research/registerResearchRankTool.tslib/mcp/tools/research/registerResearchSimilarTool.tslib/mcp/tools/research/registerResearchTrackTool.tslib/mcp/tools/research/registerResearchTracksTool.tslib/mcp/tools/research/registerResearchUrlsTool.tslib/mcp/tools/research/registerResearchVenuesTool.tslib/parallel/enrichEntity.tslib/parallel/extractUrl.tslib/research/getResearchChartsHandler.tslib/research/getResearchMilestonesHandler.tslib/research/getResearchRadioHandler.tslib/research/getResearchRankHandler.tslib/research/getResearchVenuesHandler.tslib/research/postResearchEnrichHandler.tslib/research/postResearchExtractHandler.ts
| const result = await proxyToChartmetric( | ||
| `/curator/${args.platform}/${args.id}`, | ||
| ); | ||
| return getToolResultSuccess(result.data); | ||
| } catch (error) { |
There was a problem hiding this comment.
Encode path params and handle non-2xx proxy results before returning success.
Line 29 interpolates raw user input into the path, and Line 31 returns success even when upstream returned an HTTP error payload.
Proposed fix
- const result = await proxyToChartmetric(
- `/curator/${args.platform}/${args.id}`,
- );
+ const platform = encodeURIComponent(args.platform);
+ const id = encodeURIComponent(args.id);
+ const result = await proxyToChartmetric(`/curator/${platform}/${id}`);
+ if (result.status >= 400) {
+ return getToolResultError(
+ (result.data as { error?: string })?.error ?? "Failed to fetch curator",
+ );
+ }
return getToolResultSuccess(result.data);Based on learnings: "Use getToolResultSuccess(data) to wrap successful MCP tool responses and getToolResultError(message) to wrap error responses".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/mcp/tools/research/registerResearchCuratorTool.ts` around lines 28 - 32,
The code should URL-encode user-supplied path params and return an error wrapper
for non-2xx upstream responses: encode args.platform and args.id with
encodeURIComponent when building the path passed to proxyToChartmetric (the call
in proxyToChartmetric(`/curator/${args.platform}/${args.id}`)), then inspect the
proxyToChartmetric response (e.g., result.status or result.ok) and if it is not
a success status return getToolResultError(...) with a clear message (use
result.data?.message or include result.status), otherwise return
getToolResultSuccess(result.data).
| const spotifyId = args.url | ||
| .split("/") | ||
| .pop() | ||
| ?.split("?")[0]; | ||
|
|
||
| if (!spotifyId) { | ||
| return getToolResultError("Could not extract Spotify ID from URL"); | ||
| } | ||
|
|
||
| const result = await proxyToChartmetric( | ||
| `/artist/spotify/${spotifyId}/get-ids`, | ||
| ); | ||
| return getToolResultSuccess(result.data); |
There was a problem hiding this comment.
Harden Spotify ID extraction and map proxy HTTP errors to MCP error responses.
Line 27-30 fails on valid trailing-slash URLs, and Line 39 returns success without checking upstream status.
Proposed fix
- const spotifyId = args.url
- .split("/")
- .pop()
- ?.split("?")[0];
+ const raw = args.url.trim();
+ const spotifyId = raw.startsWith("http")
+ ? new URL(raw).pathname.split("/").filter(Boolean).pop()
+ : raw;
if (!spotifyId) {
return getToolResultError("Could not extract Spotify ID from URL");
}
const result = await proxyToChartmetric(
- `/artist/spotify/${spotifyId}/get-ids`,
+ `/artist/spotify/${encodeURIComponent(spotifyId)}/get-ids`,
);
+ if (result.status >= 400) {
+ return getToolResultError(
+ (result.data as { error?: string })?.error ?? "Failed to look up artist",
+ );
+ }
return getToolResultSuccess(result.data);Based on learnings: "Use getToolResultSuccess(data) to wrap successful MCP tool responses and getToolResultError(message) to wrap error responses".
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const spotifyId = args.url | |
| .split("/") | |
| .pop() | |
| ?.split("?")[0]; | |
| if (!spotifyId) { | |
| return getToolResultError("Could not extract Spotify ID from URL"); | |
| } | |
| const result = await proxyToChartmetric( | |
| `/artist/spotify/${spotifyId}/get-ids`, | |
| ); | |
| return getToolResultSuccess(result.data); | |
| const raw = args.url.trim(); | |
| const spotifyId = raw.startsWith("http") | |
| ? new URL(raw).pathname.split("/").filter(Boolean).pop() | |
| : raw; | |
| if (!spotifyId) { | |
| return getToolResultError("Could not extract Spotify ID from URL"); | |
| } | |
| const result = await proxyToChartmetric( | |
| `/artist/spotify/${encodeURIComponent(spotifyId)}/get-ids`, | |
| ); | |
| if (result.status >= 400) { | |
| return getToolResultError( | |
| (result.data as { error?: string })?.error ?? "Failed to look up artist", | |
| ); | |
| } | |
| return getToolResultSuccess(result.data); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/mcp/tools/research/registerResearchLookupTool.ts` around lines 27 - 39,
The Spotify ID extraction is fragile and the proxy response isn't validated;
update the logic around args.url to robustly extract spotifyId (trim trailing
slashes, guard empty segments, e.g., compute spotifyId by splitting on "/" and
taking the last non-empty segment then stripping query params) and return
getToolResultError(...) if extraction fails; call proxyToChartmetric(...) inside
a try/catch, validate the upstream response (e.g., check result && result.status
=== 200 and result.data) and on non-200 or missing data return
getToolResultError with a mapped message (include upstream error or status), and
only call getToolResultSuccess(result.data) when the response is present and
valid.
| const playlists = searchResult.data as Record<string, unknown>[]; | ||
| if (!Array.isArray(playlists) || playlists.length === 0) { | ||
| return getToolResultError( | ||
| `No playlist found for "${args.id}"`, | ||
| ); | ||
| } | ||
|
|
||
| numericId = String((playlists[0] as Record<string, unknown>).id); |
There was a problem hiding this comment.
Response structure mismatch will cause search-by-name to fail.
The Chartmetric /search?type=playlists endpoint returns playlists nested by platform (e.g., { playlists: { spotify: [...], applemusic: [...] } }). The HTTP handler at lib/research/getResearchPlaylistHandler.ts:52-53 correctly accesses searchResult.data.playlists?.[platform], but this MCP tool treats searchResult.data as a flat array.
When a user provides a playlist name instead of numeric ID, this code will fail to find the playlist.
🐛 Proposed fix to match HTTP handler pattern
const searchResult = await proxyToChartmetric("/search", {
q: numericId,
type: "playlists",
limit: "1",
});
- const playlists = searchResult.data as Record<string, unknown>[];
- if (!Array.isArray(playlists) || playlists.length === 0) {
+ const playlistsByPlatform = (searchResult.data as { playlists?: Record<string, Array<{ id: number }>> })?.playlists;
+ const playlists = playlistsByPlatform?.[args.platform];
+ if (!playlists || playlists.length === 0) {
return getToolResultError(
- `No playlist found for "${args.id}"`,
+ `No playlist found for "${args.id}" on ${args.platform}`,
);
}
- numericId = String((playlists[0] as Record<string, unknown>).id);
+ numericId = String(playlists[0].id);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const playlists = searchResult.data as Record<string, unknown>[]; | |
| if (!Array.isArray(playlists) || playlists.length === 0) { | |
| return getToolResultError( | |
| `No playlist found for "${args.id}"`, | |
| ); | |
| } | |
| numericId = String((playlists[0] as Record<string, unknown>).id); | |
| const playlistsByPlatform = (searchResult.data as { playlists?: Record<string, Array<{ id: number }>> })?.playlists; | |
| const playlists = playlistsByPlatform?.[args.platform]; | |
| if (!playlists || playlists.length === 0) { | |
| return getToolResultError( | |
| `No playlist found for "${args.id}" on ${args.platform}`, | |
| ); | |
| } | |
| numericId = String(playlists[0].id); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/mcp/tools/research/registerResearchPlaylistTool.ts` around lines 39 - 46,
The code treats searchResult.data as a flat array but Chartmetric returns
playlists nested by platform; update the logic in
registerResearchPlaylistTool.ts to first look up the platform bucket (e.g.,
const platform = args.platform || 'spotify'; const playlists =
(searchResult.data as any).playlists?.[platform] ?? (searchResult.data as any));
then validate Array.isArray(playlists) and length, returning getToolResultError
when empty, and set numericId = String((playlists[0] as Record<string,
unknown>).id); ensuring you reference searchResult, playlists, numericId,
args.id and args.platform so the tool matches getResearchPlaylistHandler.ts
behavior.
| const result = await proxyToChartmetric("/radio/station-list"); | ||
| const stations = Array.isArray(result.data) ? result.data : []; | ||
| return getToolResultSuccess({ stations }); |
There was a problem hiding this comment.
Avoid masking upstream failures as an empty station list.
Line 27 converts non-array responses (including error payloads) to [], then Line 28 returns success. This hides provider failures.
Proposed fix
const result = await proxyToChartmetric("/radio/station-list");
- const stations = Array.isArray(result.data) ? result.data : [];
+ if (result.status >= 400) {
+ return getToolResultError(
+ (result.data as { error?: string })?.error ??
+ "Failed to fetch radio stations",
+ );
+ }
+ const stations = Array.isArray(result.data) ? result.data : [];
return getToolResultSuccess({ stations });Based on learnings: "Use getToolResultSuccess(data) to wrap successful MCP tool responses and getToolResultError(message) to wrap error responses".
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const result = await proxyToChartmetric("/radio/station-list"); | |
| const stations = Array.isArray(result.data) ? result.data : []; | |
| return getToolResultSuccess({ stations }); | |
| const result = await proxyToChartmetric("/radio/station-list"); | |
| if (result.status >= 400) { | |
| return getToolResultError( | |
| (result.data as { error?: string })?.error ?? | |
| "Failed to fetch radio stations", | |
| ); | |
| } | |
| const stations = Array.isArray(result.data) ? result.data : []; | |
| return getToolResultSuccess({ stations }); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/mcp/tools/research/registerResearchRadioTool.ts` around lines 26 - 28,
The current logic in registerResearchRadioTool masks upstream failures by
treating any non-array result.data as an empty stations array and returning
getToolResultSuccess; instead, after calling
proxyToChartmetric("/radio/station-list") check the shape and status of the
response (e.g., ensure result.data is an Array and/or inspect any error fields),
and if it’s not an array return getToolResultError with a clear message
(including upstream error text if present) rather than calling
getToolResultSuccess; update the code paths around proxyToChartmetric,
getToolResultSuccess, and getToolResultError so only truly valid array responses
are wrapped in getToolResultSuccess({ stations }) and all other cases return
getToolResultError(...) to surface provider failures.
| const tracks = searchResult.data as Record<string, unknown>[]; | ||
| if (!Array.isArray(tracks) || tracks.length === 0) { | ||
| return getToolResultError(`No track found for "${args.q}"`); | ||
| } |
There was a problem hiding this comment.
Incorrect data extraction from search response — this will always fail.
The Chartmetric /search endpoint returns { tracks: Array<...> }, not a direct array. As implemented, Array.isArray(tracks) will always be false because searchResult.data is an object with a tracks property.
Compare with lib/research/getResearchTrackHandler.ts:53-54 which correctly accesses:
const searchData = searchResult.data as { tracks?: Array<{ id: number }> };
const tracks = searchData?.tracks;🐛 Proposed fix
- const tracks = searchResult.data as Record<string, unknown>[];
- if (!Array.isArray(tracks) || tracks.length === 0) {
+ const searchData = searchResult.data as { tracks?: Array<{ id: number }> };
+ const tracks = searchData?.tracks;
+ if (!tracks || tracks.length === 0) {
return getToolResultError(`No track found for "${args.q}"`);
}
- const trackId = (tracks[0] as Record<string, unknown>).id;
+ const trackId = tracks[0].id;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/mcp/tools/research/registerResearchTrackTool.ts` around lines 33 - 36,
The code incorrectly treats searchResult.data as an array (variable tracks) but
Chartmetric returns an object with a tracks property; update
registerResearchTrackTool.ts to cast searchResult.data to a shape like {
tracks?: Array<{ id: number }> } (e.g., const searchData = searchResult.data as
{ tracks?: Array<{ id: number }> }), then set const tracks = searchData?.tracks
and perform the existing Array.isArray/tracks.length checks and error return
using args.q; this mirrors the approach used in getResearchTrackHandler.ts and
ensures proper extraction and validation of the tracks array.
| const { accountId } = authResult; | ||
|
|
||
| const { searchParams } = new URL(request.url); | ||
| const platform = searchParams.get("platform"); |
There was a problem hiding this comment.
Validate and sanitize the platform parameter to prevent path traversal.
The platform parameter is interpolated directly into the Chartmetric API path (/charts/${platform}) without validation. A malicious value like ../token or spotify/../../../admin could potentially reach unintended endpoints on the upstream API. Consider validating against an allowlist of known platforms or at minimum rejecting values containing path separators.
🛡️ Proposed fix: add platform validation
+ const VALID_PLATFORMS = ["spotify", "applemusic", "tiktok", "youtube", "itunes", "shazam", "deezer", "amazon"];
+
const platform = searchParams.get("platform");
if (!platform) {
return NextResponse.json(
{ status: "error", error: "platform parameter is required" },
{ status: 400, headers: getCorsHeaders() },
);
}
+ if (platform.includes("/") || platform.includes("\\") || !VALID_PLATFORMS.includes(platform.toLowerCase())) {
+ return NextResponse.json(
+ { status: "error", error: "Invalid platform" },
+ { status: 400, headers: getCorsHeaders() },
+ );
+ }Also applies to: 53-53
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/research/getResearchChartsHandler.ts` at line 21, The platform value read
via searchParams.get("platform") in getResearchChartsHandler is used directly in
the Chartmetric path (/charts/${platform}); validate and sanitize it before use
by implementing an allowlist of supported platform slugs (e.g., known strings)
or at minimum reject any values containing path separators like "/" or ".." and
non-alphanumeric characters; if validation fails, return a 400/validation error.
Ensure the check is applied wherever platform is used (including the other
occurrence at the same file) and replace direct interpolation with the
validated/sanitized variable.
Tool names now describe what they do, not what category they're in: - research_artist → get_artist_profile - research_metrics → get_artist_metrics - research_similar → get_similar_artists - research_albums → get_artist_discography - research_people → find_industry_people - research_extract → extract_url_content - research_enrich → enrich_entity - etc. Updated overlap descriptions to differentiate from existing tools: - get_artist_discography vs get_spotify_artist_albums - get_artist_tracks vs get_spotify_artist_top_tracks - get_artist_urls vs get_artist_socials Made-with: Cursor
Critical: - Credits deducted AFTER provider success, not before - Path traversal prevention — platform params validated against allowlist Major: - MCP tools return error on proxy failure instead of success - Zod validation on all POST body params (web, deep, people, extract, enrich) - popularIndie filter now overrideable in playlists handler All 1,611 tests pass. Made-with: Cursor
Summary
/api/research/backed by Chartmetric, matching the Recoup API query-param patternobjwrapper stripped from responses?artist=Drakeresolves internally, no provider IDs exposedproxyToChartmetric,resolveArtist,handleArtistResearchEndpoints
Search, Lookup, Profile, Metrics (14 platforms), Audience, Cities, Similar, URLs, Instagram Posts, Playlists, Albums, Tracks, Career, Insights, Track, Playlist, Curator, Discover, Genres, Festivals
Deferred
POST /api/research/web) and Deep research (POST /api/research/deep) — separate PRobjstripping (field renames per OpenAPI schema)Docs spec
recoupable/docs#85
Test plan
CHARTMETRIC_REFRESH_TOKENto Vercel env and test live endpointsMade with Cursor
Summary by CodeRabbit