Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
6d1f805
feat: add 30 research API endpoints, 28 MCP tools, Zod validation, an…
sidneyswift Apr 6, 2026
9b9f60a
feat: add GET /api/research/track/playlists endpoint
sidneyswift Apr 7, 2026
ed94ece
fix: use Spotify-powered track search for reliable q= resolution
sidneyswift Apr 7, 2026
c62fc77
fix: resolve tracks via ISRC for reliable Chartmetric ID mapping
sidneyswift Apr 7, 2026
e65ef6a
fix: resolve track ID via artist playlists + tracks matching
sidneyswift Apr 7, 2026
46d9701
fix: use Chartmetric /track/:type/:id/get-ids for track resolution
sidneyswift Apr 7, 2026
0dcd869
Merge branch 'test' into feature/research-endpoints
recoupableorg Apr 9, 2026
ae185ef
style: fix formatting in research track playlists files
sidneyswift Apr 9, 2026
6561d80
Merge remote-tracking branch 'origin/test' into feature/research-endp…
sweetmantech Apr 15, 2026
de07d4b
fix(lint): remove unused getCorsHeaders import
sweetmantech Apr 15, 2026
ac7865d
refactor(review): SRP on chartmetric token cache; drop MCP research t…
sweetmantech Apr 15, 2026
87a8cdc
refactor: replace handleArtistResearch orchestrator with small compos…
sweetmantech Apr 15, 2026
d24785d
refactor: rename getArtistResearch → handleArtistResearch (no functio…
sweetmantech Apr 15, 2026
aae9670
refactor: consolidate JSON response helpers (DRY)
sweetmantech Apr 15, 2026
66fa96f
refactor: align research handler naming + shape with codebase convent…
sweetmantech Apr 15, 2026
7bed815
refactor(kiss): spread validated into handleArtistResearch call
sweetmantech Apr 15, 2026
3d932e9
refactor: align charts + discover handlers with validator/helper conv…
sweetmantech Apr 15, 2026
99bb543
refactor: port final 4 research handlers; delete handleResearchRequest
sweetmantech Apr 15, 2026
218d4ed
refactor(kiss): rename handleResearchProxy → handleResearch, proxyToC…
sweetmantech Apr 15, 2026
4c0b4e6
refactor: move fetchChartmetric to lib/chartmetric
sweetmantech Apr 15, 2026
147dff6
refactor(dry): share CHARTMETRIC_BASE between token exchange and fetc…
sweetmantech Apr 15, 2026
f3dca50
refactor: port 5 non-artist research GET handlers to validator+handle…
sweetmantech Apr 15, 2026
81871c4
refactor: charge credits for each Chartmetric hop in track+playlist l…
sweetmantech Apr 15, 2026
aa0048b
refactor(review): address 9 review comments
sweetmantech Apr 15, 2026
fef9d7c
fix(review): tighten validateGetResearchCuratorRequest with clear errors
sweetmantech Apr 15, 2026
fda5592
fix(research/track): add artist disambiguation + better match ranking
sweetmantech Apr 16, 2026
a28deb2
feat(research/search): pass through beta, platforms, offset to Chartm…
sweetmantech Apr 16, 2026
c787d05
refactor(research): /track and /playlist become pure ID proxies (KISS)
sweetmantech Apr 16, 2026
cb6152b
fix(research): /albums id-proxy + /charts enum validation (KISS)
sweetmantech Apr 16, 2026
969f5f8
fix(research/albums): default isPrimary=true to exclude features
sweetmantech Apr 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions app/api/research/albums/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { NextRequest, NextResponse } from "next/server";
import { getCorsHeaders } from "@/lib/networking/getCorsHeaders";
import { getResearchAlbumsHandler } from "@/lib/research/getResearchAlbumsHandler";

/**
* OPTIONS /api/research/albums — CORS preflight.
*
* @returns CORS-enabled 200 response
*/
export async function OPTIONS() {
return new NextResponse(null, { status: 200, headers: getCorsHeaders() });
}

/**
* GET /api/research/albums — Album discography for a Chartmetric artist id.
* Discovery by name is the caller's job via `GET /api/research`.
*
* @param request - must include numeric `artist_id` query param
* @returns JSON album list or error
*/
export async function GET(request: NextRequest) {
return getResearchAlbumsHandler(request);
}
22 changes: 22 additions & 0 deletions app/api/research/audience/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { NextRequest, NextResponse } from "next/server";
import { getCorsHeaders } from "@/lib/networking/getCorsHeaders";
import { getResearchAudienceHandler } from "@/lib/research/getResearchAudienceHandler";

/**
* OPTIONS /api/research/audience — CORS preflight.
*
* @returns CORS-enabled 200 response
*/
export async function OPTIONS() {
return new NextResponse(null, { status: 200, headers: getCorsHeaders() });
}

/**
* GET /api/research/audience — Audience demographics by platform. Requires `?artist=` query param.
*
* @param request - must include `artist` query param
* @returns JSON audience demographics or error
*/
export async function GET(request: NextRequest) {
return getResearchAudienceHandler(request);
}
22 changes: 22 additions & 0 deletions app/api/research/career/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { NextRequest, NextResponse } from "next/server";
import { getCorsHeaders } from "@/lib/networking/getCorsHeaders";
import { getResearchCareerHandler } from "@/lib/research/getResearchCareerHandler";

/**
* OPTIONS /api/research/career — CORS preflight.
*
* @returns CORS-enabled 200 response
*/
export async function OPTIONS() {
return new NextResponse(null, { status: 200, headers: getCorsHeaders() });
}

/**
* GET /api/research/career — Artist career history and milestones. Requires `?artist=` query param.
*
* @param request - must include `artist` query param
* @returns JSON career timeline or error
*/
export async function GET(request: NextRequest) {
return getResearchCareerHandler(request);
}
22 changes: 22 additions & 0 deletions app/api/research/charts/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { NextRequest, NextResponse } from "next/server";
import { getCorsHeaders } from "@/lib/networking/getCorsHeaders";
import { getResearchChartsHandler } from "@/lib/research/getResearchChartsHandler";

/**
* OPTIONS /api/research/charts — CORS preflight.
*
* @returns CORS-enabled 200 response
*/
export async function OPTIONS() {
return new NextResponse(null, { status: 200, headers: getCorsHeaders() });
}

/**
* GET /api/research/charts — Global chart positions by platform and country. Requires `?artist=` query param.
*
* @param request - must include `artist` query param
* @returns JSON chart positions or error
*/
export async function GET(request: NextRequest) {
return getResearchChartsHandler(request);
}
22 changes: 22 additions & 0 deletions app/api/research/cities/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { NextRequest, NextResponse } from "next/server";
import { getCorsHeaders } from "@/lib/networking/getCorsHeaders";
import { getResearchCitiesHandler } from "@/lib/research/getResearchCitiesHandler";

/**
* OPTIONS /api/research/cities — CORS preflight.
*
* @returns CORS-enabled 200 response
*/
export async function OPTIONS() {
return new NextResponse(null, { status: 200, headers: getCorsHeaders() });
}

/**
* GET /api/research/cities — Geographic listening data for an artist. Requires `?artist=` query param.
*
* @param request - must include `artist` query param
* @returns JSON city-level listener data or error
*/
export async function GET(request: NextRequest) {
return getResearchCitiesHandler(request);
}
22 changes: 22 additions & 0 deletions app/api/research/curator/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { NextRequest, NextResponse } from "next/server";
import { getCorsHeaders } from "@/lib/networking/getCorsHeaders";
import { getResearchCuratorHandler } from "@/lib/research/getResearchCuratorHandler";

/**
* OPTIONS /api/research/curator — CORS preflight.
*
* @returns CORS-enabled 200 response
*/
export async function OPTIONS() {
return new NextResponse(null, { status: 200, headers: getCorsHeaders() });
}

/**
* GET /api/research/curator — Playlist curator details. Requires `?platform=` and `?id=` query params.
*
* @param request - must include `platform` and `id` query params
* @returns JSON curator profile or error
*/
export async function GET(request: NextRequest) {
return getResearchCuratorHandler(request);
}
22 changes: 22 additions & 0 deletions app/api/research/deep/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { NextRequest, NextResponse } from "next/server";
import { getCorsHeaders } from "@/lib/networking/getCorsHeaders";
import { postResearchDeepHandler } from "@/lib/research/postResearchDeepHandler";

/**
* OPTIONS /api/research/deep — CORS preflight.
*
* @returns CORS-enabled 200 response
*/
export async function OPTIONS() {
return new NextResponse(null, { status: 200, headers: getCorsHeaders() });
}

/**
* POST /api/research/deep — Deep, comprehensive research with citations. Body: `{ query }`.
*
* @param request - JSON body with `query` string
* @returns JSON research report with citations or error
*/
export async function POST(request: NextRequest) {
return postResearchDeepHandler(request);
}
22 changes: 22 additions & 0 deletions app/api/research/discover/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { NextRequest, NextResponse } from "next/server";
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
import { getCorsHeaders } from "@/lib/networking/getCorsHeaders";
import { getResearchDiscoverHandler } from "@/lib/research/getResearchDiscoverHandler";

/**
* OPTIONS /api/research/discover — CORS preflight.
*
* @returns CORS-enabled 200 response
*/
export async function OPTIONS() {
return new NextResponse(null, { status: 200, headers: getCorsHeaders() });
}

/**
* GET /api/research/discover — Discover artists by genre, country, and growth criteria. Supports `?genre=`, `?country=`, `?sort=`, `?limit=` filters.
*
* @param request - filter criteria via query params
* @returns JSON array of matching artists or error
*/
export async function GET(request: NextRequest) {
return getResearchDiscoverHandler(request);
}
Comment on lines +1 to +22
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot Mar 28, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 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.ts

Repository: 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.ts file exporting a Zod schema and inferred type for country, genre, sort, limit, sp_monthly_listeners_min, sp_monthly_listeners_max parameters
  • 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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed — JSDoc rewritten with meaningful one-liner descriptions.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@sidneyswift, thanks for updating the JSDoc! That addresses the documentation concern.

Two items from the original review are still outstanding:

  1. Zod-based query validation — query parameters (country, genre, sort, limit, sp_monthly_listeners_min, sp_monthly_listeners_max) are currently parsed manually in getResearchDiscoverHandler. Per the coding guidelines and the learning from AGENTS.md, all endpoints should use Zod for input parsing. A lib/research/validateDiscoverQuery.ts exporting a schema would make validation consistent and type-safe.

  2. Test coverage — the learning for this repo requires tests covering all success and error paths (auth failure, insufficient credits, proxy failure, and happy path). The discover endpoint currently has no tests.

Would you like to address these in this PR, or shall I open a follow-up issue to track them?


🧠 Learnings used
Learnt from: CR
Repo: recoupable/api PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-17T19:09:32.948Z
Learning: Applies to app/api/**/route.ts : Write tests for new API endpoints covering all success and error paths

Learnt from: CR
Repo: recoupable/api PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-17T19:09:32.948Z
Learning: Applies to app/api/**/route.ts : All API routes should have JSDoc comments

Learnt from: CR
Repo: recoupable/api PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-17T19:09:32.948Z
Learning: Applies to app/api/**/route.ts : All API endpoints should use a validate function for input parsing using Zod for schema validation

22 changes: 22 additions & 0 deletions app/api/research/enrich/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { NextRequest, NextResponse } from "next/server";
import { getCorsHeaders } from "@/lib/networking/getCorsHeaders";
import { postResearchEnrichHandler } from "@/lib/research/postResearchEnrichHandler";

/**
* OPTIONS /api/research/enrich — CORS preflight.
*
* @returns CORS-enabled 200 response
*/
export async function OPTIONS() {
return new NextResponse(null, { status: 200, headers: getCorsHeaders() });
}

/**
* POST /api/research/enrich — Enrich an entity with structured web research data. Body: `{ url, prompt? }`.
*
* @param request - JSON body with `url` and optional `prompt`
* @returns JSON enriched entity data or error
*/
export async function POST(request: NextRequest) {
return postResearchEnrichHandler(request);
}
22 changes: 22 additions & 0 deletions app/api/research/extract/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { NextRequest, NextResponse } from "next/server";
import { getCorsHeaders } from "@/lib/networking/getCorsHeaders";
import { postResearchExtractHandler } from "@/lib/research/postResearchExtractHandler";

/**
* OPTIONS /api/research/extract — CORS preflight.
*
* @returns CORS-enabled 200 response
*/
export async function OPTIONS() {
return new NextResponse(null, { status: 200, headers: getCorsHeaders() });
}

/**
* POST /api/research/extract — Extract clean markdown from URLs. Body: `{ urls }`.
*
* @param request - JSON body with `urls` array
* @returns JSON extracted markdown content or error
*/
export async function POST(request: NextRequest) {
return postResearchExtractHandler(request);
}
22 changes: 22 additions & 0 deletions app/api/research/festivals/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { NextRequest, NextResponse } from "next/server";
import { getCorsHeaders } from "@/lib/networking/getCorsHeaders";
import { getResearchFestivalsHandler } from "@/lib/research/getResearchFestivalsHandler";

/**
* OPTIONS /api/research/festivals — CORS preflight.
*
* @returns CORS-enabled 200 response
*/
export async function OPTIONS() {
return new NextResponse(null, { status: 200, headers: getCorsHeaders() });
}

/**
* GET /api/research/festivals — List of music festivals.
*
* @param request - optional filter query params
* @returns JSON festival list or error
*/
export async function GET(request: NextRequest) {
return getResearchFestivalsHandler(request);
}
22 changes: 22 additions & 0 deletions app/api/research/genres/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { NextRequest, NextResponse } from "next/server";
import { getCorsHeaders } from "@/lib/networking/getCorsHeaders";
import { getResearchGenresHandler } from "@/lib/research/getResearchGenresHandler";

/**
* OPTIONS /api/research/genres — CORS preflight.
*
* @returns CORS-enabled 200 response
*/
export async function OPTIONS() {
return new NextResponse(null, { status: 200, headers: getCorsHeaders() });
}

/**
* GET /api/research/genres — All available genre IDs and names.
*
* @param request - no required query params
* @returns JSON genre list or error
*/
export async function GET(request: NextRequest) {
return getResearchGenresHandler(request);
}
22 changes: 22 additions & 0 deletions app/api/research/insights/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { NextRequest, NextResponse } from "next/server";
import { getCorsHeaders } from "@/lib/networking/getCorsHeaders";
import { getResearchInsightsHandler } from "@/lib/research/getResearchInsightsHandler";

/**
* OPTIONS /api/research/insights — CORS preflight.
*
* @returns CORS-enabled 200 response
*/
export async function OPTIONS() {
return new NextResponse(null, { status: 200, headers: getCorsHeaders() });
}

/**
* GET /api/research/insights — Noteworthy highlights and trending metrics for an artist. Requires `?artist=` query param.
*
* @param request - must include `artist` query param
* @returns JSON insights data or error
*/
export async function GET(request: NextRequest) {
return getResearchInsightsHandler(request);
}
22 changes: 22 additions & 0 deletions app/api/research/instagram-posts/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { NextRequest, NextResponse } from "next/server";
import { getCorsHeaders } from "@/lib/networking/getCorsHeaders";
import { getResearchInstagramPostsHandler } from "@/lib/research/getResearchInstagramPostsHandler";

/**
* OPTIONS /api/research/instagram-posts — CORS preflight.
*
* @returns CORS-enabled 200 response
*/
export async function OPTIONS() {
return new NextResponse(null, { status: 200, headers: getCorsHeaders() });
}

/**
* GET /api/research/instagram-posts — Recent Instagram posts for an artist. Requires `?artist=` query param.
*
* @param request - must include `artist` query param
* @returns JSON Instagram posts or error
*/
export async function GET(request: NextRequest) {
return getResearchInstagramPostsHandler(request);
}
22 changes: 22 additions & 0 deletions app/api/research/lookup/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { NextRequest, NextResponse } from "next/server";
import { getCorsHeaders } from "@/lib/networking/getCorsHeaders";
import { getResearchLookupHandler } from "@/lib/research/getResearchLookupHandler";

/**
* OPTIONS /api/research/lookup — CORS preflight.
*
* @returns CORS-enabled 200 response
*/
export async function OPTIONS() {
return new NextResponse(null, { status: 200, headers: getCorsHeaders() });
}

/**
* GET /api/research/lookup — Resolve a Spotify artist URL to cross-platform IDs. Requires `?url=` query param.
*
* @param request - must include `url` query param (Spotify URL)
* @returns JSON cross-platform IDs or error
*/
export async function GET(request: NextRequest) {
return getResearchLookupHandler(request);
}
22 changes: 22 additions & 0 deletions app/api/research/metrics/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { NextRequest, NextResponse } from "next/server";
import { getCorsHeaders } from "@/lib/networking/getCorsHeaders";
import { getResearchMetricsHandler } from "@/lib/research/getResearchMetricsHandler";

/**
* OPTIONS /api/research/metrics — CORS preflight.
*
* @returns CORS-enabled 200 response
*/
export async function OPTIONS() {
return new NextResponse(null, { status: 200, headers: getCorsHeaders() });
}

/**
* GET /api/research/metrics — Platform-specific streaming and social metrics. Requires `?artist=` and `?source=` query params.
*
* @param request - must include `artist` and `source` query params
* @returns JSON metrics data or error
*/
export async function GET(request: NextRequest) {
return getResearchMetricsHandler(request);
}
22 changes: 22 additions & 0 deletions app/api/research/milestones/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { NextRequest, NextResponse } from "next/server";
import { getCorsHeaders } from "@/lib/networking/getCorsHeaders";
import { getResearchMilestonesHandler } from "@/lib/research/getResearchMilestonesHandler";

/**
* OPTIONS /api/research/milestones — CORS preflight.
*
* @returns CORS-enabled 200 response
*/
export async function OPTIONS() {
return new NextResponse(null, { status: 200, headers: getCorsHeaders() });
}

/**
* GET /api/research/milestones — Artist activity feed: playlist adds, chart entries, events. Requires `?artist=` query param.
*
* @param request - must include `artist` query param
* @returns JSON milestone activity feed or error
*/
export async function GET(request: NextRequest) {
return getResearchMilestonesHandler(request);
}
Loading
Loading