fix: replace mock data with Prisma DB queries in 6 API routes#52
fix: replace mock data with Prisma DB queries in 6 API routes#52qianghan wants to merge 7 commits into
Conversation
|
@seanhanca is attempting to deploy a commit to the Livepeer Foundation Team on Vercel. A member of the Team first needs to authorize it. |
|
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:
📝 WalkthroughWalkthroughMultiple Next.js API routes were migrated from in-memory/mock data to Prisma-backed database queries, added JSON body parsing/validation and DB existence checks, introduced a serialiseModel helper, and adjusted some route signatures and typings (params/return types) where applicable. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant Client
participant API as "Next.js API Route"
participant Auth as "Auth / Session"
participant DB as "Prisma (Database)"
Client->>API: HTTP request (GET/POST)
API->>Auth: validate session / token / CSRF
Auth-->>API: session (ok / unauthorized)
alt session ok
API->>DB: Prisma query (models, gatewayOffers, integrations, packages)
DB-->>API: rows/results
API->>API: parse & validate JSON, serialise rows to response shape
API-->>Client: 200 + JSON
else session missing/invalid
API-->>Client: 401 / unauthorized
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Suggested labels
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Fix all issues with AI agents
In `@apps/web-next/src/app/api/v1/integrations/route.ts`:
- Around line 23-39: The response for the DB path is missing the category and
description fields that the fallback catalogue includes; update the
prisma.integrationConfig.findMany call to select category and description (add
them to the select payload) and include them in the mapping where rows are
transformed (the rows.map(...) block) so the returned object includes type,
displayName, configured, category, and description, ensuring parity with the
fallback; alternatively, if you prefer to always omit these fields, remove them
from the fallback or normalize clients, but implement the select + mapping
change in prisma.integrationConfig.findMany and the mapping function to fix the
immediate mismatch.
In `@apps/web-next/src/app/api/v1/plugin-publisher/stats/`[packageName]/route.ts:
- Around line 58-71: The code mixes local-time midnight
(thirtyDaysAgo.setHours(0,0,0,0)) with UTC reads
(getUTCFullYear/getUTCMonth/getUTCDate) causing off-by-one-day errors; update
the date creation to use UTC consistently (e.g., set thirtyDaysAgo to UTC
midnight with setUTCHours(0,0,0,0) or construct via Date.UTC) and keep the
timeline loop using getUTCFullYear/getUTCMonth/getUTCDate so the generated date
strings and bucket boundaries are all computed in UTC (adjust the thirtyDaysAgo
initialization and leave the timeline formatting using the existing UTC
getters).
- Around line 38-40: The installations relation in the query currently selects
all installations; restrict it to the last 30 days by adding a where clause on
installations.installAt/installedAt (use the exact field name installedAt) such
as installedAt >= thirtyDaysAgoDate and keep the select { installedAt: true }
inside installations. Also remove the redundant recalculation of thirtyDaysAgo
later and reuse the existing thirtyDaysAgoDate variable used in the route
handler (e.g., in the GET handler/function where installations is queried) so
the query only returns 30 days of records and the same thirtyDaysAgoDate is used
throughout.
🧹 Nitpick comments (5)
apps/web-next/src/app/api/v1/developer/keys/route.ts (2)
82-93: Consider adding runtime type validation for request body fields.The type assertion on lines 89-93 assumes the parsed JSON has the expected shape. While downstream checks verify required fields exist, adding explicit type guards or a schema validation library (e.g., Zod) would catch malformed input earlier and provide more specific error messages.
18-20: Clarify the truncation rationale forhashApiKey.The hash is truncated to 16 hex characters (64 bits), which significantly reduces the entropy of SHA-256. If this is intentional for storage/display purposes and the full key is validated elsewhere, consider adding a brief comment explaining this design choice.
apps/web-next/src/app/api/v1/developer/models/route.ts (1)
77-89: Consider adding server-side pagination for scalability.The current implementation fetches all matching models from the database. While this works for small datasets, it may become a performance concern as the model catalog grows. The
parsePaginationutility is available (used inkeys/route.ts) and could be applied here for consistency.apps/web-next/src/app/api/v1/developer/models/[id]/route.ts (1)
10-10: Consider extractingserialiseModelto a shared utility.Importing from
../routecreates coupling between route files. MovingserialiseModelto a shared location (e.g.,@/lib/api/serializersor a dedicatedmodels.utils.ts) would improve modularity and make it easier to reuse across the codebase.apps/web-next/src/app/api/v1/plugin-publisher/stats/[packageName]/route.ts (1)
70-74: Consider removing unuseddownloadsfield from timeline or adding a clarifying comment.The
downloadsfield in timeline entries is always 0 since only installs are bucketed. If per-day download data isn't available, consider either removing the field to avoid confusion or adding a comment explaining the limitation.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@apps/web-next/src/app/api/v1/plugin-publisher/stats/`[packageName]/route.ts:
- Around line 80-88: The current loop over pkg.installations uses Math.min(29,
Math.floor(...)) which folds out-of-range dayIndex values into bucket 29; change
it to compute dayIndex = Math.floor((instDate.getTime() -
thirtyDaysAgo.getTime()) / (24*60*60*1000)) and then only increment
timeline[dayIndex].installs when dayIndex >= 0 && dayIndex <= 29, removing the
Math.min clamp; ensure you still skip installs with no installedAt and keep
using timeline and pkg.installations as before.
🧹 Nitpick comments (1)
apps/web-next/src/app/api/v1/integrations/route.ts (1)
25-45: Consider simplifying the two-step array initialization.The current approach creates
DEFAULT_INTEGRATIONSwith simple capitalization, then mutates it viaforEachto apply display name overrides. This can be consolidated into a single pass.♻️ Proposed refactor to eliminate the mutation
+const DISPLAY_NAME_OVERRIDES: Record<string, string> = { + openai: 'OpenAI', + anthropic: 'Anthropic', + 'aws-s3': 'AWS S3', + sendgrid: 'SendGrid', + stripe: 'Stripe', + twilio: 'Twilio', +}; + // Fallback catalogue — returned only when the IntegrationConfig table is empty // (e.g. fresh deployment before seed). Each entry carries `configured: false`. const DEFAULT_INTEGRATIONS = Object.entries(INTEGRATION_META).map(([type, meta]) => ({ type, - displayName: type.charAt(0).toUpperCase() + type.slice(1), + displayName: DISPLAY_NAME_OVERRIDES[type] ?? type.charAt(0).toUpperCase() + type.slice(1), configured: false, ...meta, })); - -// Override display names for entries that need special casing -const DISPLAY_NAME_OVERRIDES: Record<string, string> = { - openai: 'OpenAI', - anthropic: 'Anthropic', - 'aws-s3': 'AWS S3', - sendgrid: 'SendGrid', - stripe: 'Stripe', - twilio: 'Twilio', -}; -DEFAULT_INTEGRATIONS.forEach((i) => { - if (DISPLAY_NAME_OVERRIDES[i.type]) i.displayName = DISPLAY_NAME_OVERRIDES[i.type]; -});
Migrate developer/models, developer/keys, integrations, and plugin-publisher/stats routes from hardcoded mock data to real database queries using Prisma. Routes fixed: - developer/models/route.ts: query DevApiAIModel table - developer/models/[id]/route.ts: query DevApiAIModel by id - developer/models/[id]/gateways/route.ts: query DevApiGatewayOffer - developer/keys/route.ts: validate model/gateway against DB - integrations/route.ts: query IntegrationConfig table - plugin-publisher/stats/[packageName]/route.ts: compute stats from DB Closes #51 Co-authored-by: Cursor <cursoragent@cursor.com>
- integrations/route.ts: ensure consistent response shape by enriching DB rows with category/description from static metadata map - plugin-publisher/stats: filter installations to last 30 days in the Prisma query for better performance - plugin-publisher/stats: use UTC consistently (setUTCDate/setUTCHours) to prevent off-by-one day errors from timezone mismatch Co-authored-by: Cursor <cursoragent@cursor.com>
25a1db6 to
66bed62
Compare
|
@coderabbitai resolve |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@apps/web-next/src/app/api/v1/developer/keys/route.ts`:
- Around line 82-97: The request body parsing accepts truthy non-string values
for projectName, modelId, and gatewayId which can surface as Prisma errors;
validate each of body.projectName, body.modelId, and body.gatewayId are strings
and non-empty before proceeding (after body = await request.json()), and return
errors.badRequest with a clear message if any are missing or not a non-empty
string; update the validation logic around the variables projectName, modelId,
gatewayId (used later before Prisma calls) to enforce typeof === 'string' &&
value.trim().length > 0.
In `@apps/web-next/src/app/api/v1/plugin-publisher/stats/`[packageName]/route.ts:
- Around line 59-62: The summary "Active Installs" currently uses the all-time
counter pkg._count.installations which mismatches the 30-day timeline; change
totalInstalls to use the filtered installs array instead (replace the usage of
pkg._count.installations with the 30-day filtered collection pkg.installations)
so totalInstalls = pkg.installations.length and the summary aligns with the
timeline shown by versions and installations.
🧹 Nitpick comments (2)
apps/web-next/src/app/api/v1/integrations/route.ts (1)
27-45: Consider constructingDEFAULT_INTEGRATIONSwith overrides in one pass to avoid mutation.The current approach mutates
DEFAULT_INTEGRATIONSafter creation (line 43-45), which works but violates the expectation that aconstarray's contents remain stable. Consolidating the logic avoids the mutation and is slightly cleaner.♻️ Proposed refactor
-// Fallback catalogue — returned only when the IntegrationConfig table is empty -// (e.g. fresh deployment before seed). Each entry carries `configured: false`. -const DEFAULT_INTEGRATIONS = Object.entries(INTEGRATION_META).map(([type, meta]) => ({ - type, - displayName: type.charAt(0).toUpperCase() + type.slice(1), - configured: false, - ...meta, -})); - // Override display names for entries that need special casing const DISPLAY_NAME_OVERRIDES: Record<string, string> = { openai: 'OpenAI', anthropic: 'Anthropic', 'aws-s3': 'AWS S3', sendgrid: 'SendGrid', stripe: 'Stripe', twilio: 'Twilio', }; -DEFAULT_INTEGRATIONS.forEach((i) => { - if (DISPLAY_NAME_OVERRIDES[i.type]) i.displayName = DISPLAY_NAME_OVERRIDES[i.type]; -}); + +// Fallback catalogue — returned only when the IntegrationConfig table is empty +// (e.g. fresh deployment before seed). Each entry carries `configured: false`. +const DEFAULT_INTEGRATIONS = Object.entries(INTEGRATION_META).map(([type, meta]) => ({ + type, + displayName: DISPLAY_NAME_OVERRIDES[type] ?? type.charAt(0).toUpperCase() + type.slice(1), + configured: false, + ...meta, +}));apps/web-next/src/app/api/v1/developer/models/route.ts (1)
77-90: Consider adding pagination for scalability.The query fetches all matching models without pagination. If the number of models grows significantly, this could impact response times and memory usage. Consider adding
skipandtakeparameters.If the dataset is expected to remain small, this is acceptable—but worth noting for future scalability.
Optional: Add pagination support
const searchParams = request.nextUrl.searchParams; const type = searchParams.get('type'); const featured = searchParams.get('featured'); const realtime = searchParams.get('realtime'); + const page = Math.max(1, parseInt(searchParams.get('page') ?? '1', 10)); + const limit = Math.min(100, Math.max(1, parseInt(searchParams.get('limit') ?? '50', 10))); // ... where clause building ... const rows = await prisma.devApiAIModel.findMany({ where, orderBy: [{ featured: 'desc' }, { name: 'asc' }], include: { _count: { select: { gatewayOffers: true } }, }, + skip: (page - 1) * limit, + take: limit, }); + const total = await prisma.devApiAIModel.count({ where }); const models = rows.map(serialiseModel); return success({ models, - total: models.length, + total, + page, + limit, });
✅ Actions performedComments resolved and changes approved. |
- stats route: replace Math.min(29, dayIndex) clamp with explicit bounds check (dayIndex >= 0 && dayIndex <= 29) to avoid folding out-of-range installs into the last bucket - integrations route: consolidate DEFAULT_INTEGRATIONS array init into a single pass using DISPLAY_NAME_OVERRIDES inline Co-authored-by: Cursor <cursoragent@cursor.com>
- keys route: enforce typeof string + non-empty validation for projectName, modelId, gatewayId before Prisma calls - stats route: use 30-day filtered installations.length for totalInstalls instead of all-time _count to align with timeline - stats route: remove unused _count.installations from query Co-authored-by: Cursor <cursoragent@cursor.com>
|
@github-copilot review |
There was a problem hiding this comment.
Pull request overview
This PR replaces hardcoded mock data with real Prisma database queries across 6 API routes, ensuring all developer-facing endpoints return live data instead of static fixtures.
Changes:
- Migrated developer models, gateways, and API key routes to query
DevApiAIModelandDevApiGatewayOffertables - Replaced mock integration list with
IntegrationConfigtable queries, falling back to a default catalogue when empty - Implemented real plugin statistics by aggregating
PluginVersion.downloadsandPluginInstallationdata instead of usingMath.random()
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
apps/web-next/src/app/api/v1/plugin-publisher/stats/[packageName]/route.ts |
Replaced random mock statistics with real 30-day installation timeline and download counts from database |
apps/web-next/src/app/api/v1/integrations/route.ts |
Query IntegrationConfig table with static metadata enrichment, fallback to default catalogue |
apps/web-next/src/app/api/v1/developer/models/route.ts |
Replaced in-memory models array with Prisma queries using type-safe WhereInput filters |
apps/web-next/src/app/api/v1/developer/models/[id]/route.ts |
Fetch single model by ID from database with gateway count |
apps/web-next/src/app/api/v1/developer/models/[id]/gateways/route.ts |
Query DevApiGatewayOffer table for model's gateway offers |
apps/web-next/src/app/api/v1/developer/keys/route.ts |
Validate model and gateway existence against database instead of mock helpers |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const instDate = new Date(inst.installedAt); | ||
| const diffMs = instDate.getTime() - thirtyDaysAgo.getTime(); | ||
| const dayIndex = Math.floor(diffMs / (24 * 60 * 60 * 1000)); | ||
| if (dayIndex >= 0 && dayIndex <= 29) { |
There was a problem hiding this comment.
The upper bound check should be dayIndex < 30 instead of dayIndex <= 29. While mathematically equivalent, using < 30 is clearer and matches the loop condition on line 65 that creates 30 buckets (indices 0-29). This makes the boundary check more consistent and readable.
| if (dayIndex >= 0 && dayIndex <= 29) { | |
| if (dayIndex >= 0 && dayIndex < 30) { |
There was a problem hiding this comment.
Fixed in d6f3997 — changed to dayIndex < 30 for consistency with the loop creating 30 buckets.
| }; | ||
| } | ||
|
|
||
| export { serialiseModel }; |
There was a problem hiding this comment.
The serialiseModel function is exported but defined within this route file. If this is intended for reuse (as evidenced by the import in [id]/route.ts), consider moving it to a shared utility module like @/lib/api/models to better separate business logic from route handlers and improve maintainability.
There was a problem hiding this comment.
Fixed in d6f3997 — moved serialiseModel to @/lib/api/models.ts shared utility. Both models/route.ts and [id]/route.ts now import from there.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const instDate = new Date(inst.installedAt); | ||
| const diffMs = instDate.getTime() - thirtyDaysAgo.getTime(); | ||
| const dayIndex = Math.floor(diffMs / (24 * 60 * 60 * 1000)); | ||
| if (dayIndex >= 0 && dayIndex <= 29) { |
There was a problem hiding this comment.
The condition dayIndex <= 29 should be dayIndex < 30 for consistency with the loop that creates 30 buckets (indices 0-29). While both work, using < 30 matches the loop boundary and is clearer.
| if (dayIndex >= 0 && dayIndex <= 29) { | |
| if (dayIndex >= 0 && dayIndex < 30) { |
| projectName, | ||
| modelId, | ||
| gatewayOfferId: gatewayId || undefined, | ||
| gatewayOfferId: gateway.id, |
There was a problem hiding this comment.
The gatewayOfferId field is being set to gateway.id, but the query selects only { id: true } from DevApiGatewayOffer. This id represents the offer's unique identifier, not the gatewayId. The field should likely be set to the offer's ID (which appears correct) but verify the schema expects the offer ID rather than the gateway's string ID. If the schema expects the gateway's string identifier, this should be gatewayId instead.
- Move serialiseModel to shared @/lib/api/models utility module - Use dayIndex < 30 instead of dayIndex <= 29 for consistency with the loop that creates 30 buckets (indices 0-29) Co-authored-by: Cursor <cursoragent@cursor.com>
|
@copilot review |
Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const timeline: { date: string; downloads: number; installs: number }[] = []; | ||
| for (let i = 0; i < 30; i++) { | ||
| const d = new Date(thirtyDaysAgo); | ||
| d.setUTCDate(d.getUTCDate() + i); |
There was a problem hiding this comment.
Date mutation inside the loop reuses the same Date object, causing all 30 days to reference the final date. Create a new Date instance for each iteration: const d = new Date(thirtyDaysAgo.getTime() + i * 24 * 60 * 60 * 1000); or clone the date before mutation.
There was a problem hiding this comment.
Fixed in e0aedd6 — now uses timestamp arithmetic new Date(thirtyDaysAgo.getTime() + i * MS_PER_DAY) instead of mutating a Date clone.
| const DISPLAY_NAME_OVERRIDES: Record<string, string> = { | ||
| openai: 'OpenAI', | ||
| anthropic: 'Anthropic', | ||
| 'aws-s3': 'AWS S3', | ||
| sendgrid: 'SendGrid', | ||
| stripe: 'Stripe', | ||
| twilio: 'Twilio', | ||
| }; |
There was a problem hiding this comment.
The DISPLAY_NAME_OVERRIDES constant duplicates display names that are already defined in DEFAULT_INTEGRATIONS (line 37). This creates a maintenance burden where display names must be updated in two places. Consider removing DISPLAY_NAME_OVERRIDES and deriving display names directly from INTEGRATION_META entries or storing them in a single source.
There was a problem hiding this comment.
Fixed in e0aedd6 — merged DISPLAY_NAME_OVERRIDES into INTEGRATION_META as a displayName field. Single source of truth now.
| const { projectName, modelId, gatewayId } = body as { | ||
| projectName?: string; | ||
| modelId?: string; | ||
| gatewayId?: string; | ||
| }; |
There was a problem hiding this comment.
Type assertion bypasses TypeScript's type checking. Use a type guard or validation library (e.g., zod) to safely parse and validate the request body shape before destructuring.
There was a problem hiding this comment.
Fixed in e0aedd6 — removed the type assertion. Now extracts fields from body as unknown and validates with typeof checks before use.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@apps/web-next/src/app/api/v1/plugin-publisher/stats/`[packageName]/route.ts:
- Around line 45-48: The installations subquery currently filters only with
installedAt: { gte: thirtyDaysAgo } which can include future-dated rows; update
the query in route.ts (the installations select used to compute totalInstalls
and the similar block at lines ~56-60) to add an upper bound by also requiring
installedAt: { lte: now } (or Date.now()/new Date()) so the where becomes a
range between thirtyDaysAgo and now, ensuring future timestamps aren’t counted.
🧹 Nitpick comments (2)
apps/web-next/src/app/api/v1/developer/models/route.ts (2)
35-40: Consider supporting explicitfalsefilter values.Currently,
featured=falseandrealtime=falsequery params are ignored since only'true'is checked. If clients need to filter for non-featured/non-realtime models, this pattern won't work.If intentional (filter only when requesting featured items), the current behavior is fine. Otherwise:
♻️ Optional: Support explicit false filters
if (featured === 'true') { where.featured = true; + } else if (featured === 'false') { + where.featured = false; } if (realtime === 'true') { where.realtime = true; + } else if (realtime === 'false') { + where.realtime = false; }
50-55: Consider adding pagination for scalability.The route fetches all matching models without limit. If the model catalog grows large, this could impact response times and memory usage. Pagination (limit/offset or cursor-based) would be beneficial.
This is not blocking for the current migration but worth considering as a follow-up.
| installations: { | ||
| where: { installedAt: { gte: thirtyDaysAgo } }, | ||
| select: { installedAt: true }, | ||
| }, |
There was a problem hiding this comment.
Add an upper bound to the installations query to avoid future-dated inflation.
totalInstalls is derived from the filtered installations list, but the query only enforces gte: thirtyDaysAgo. Any future-dated rows (clock skew or bad data) will be counted, even though the timeline excludes them. Consider bounding the query to now.
🛠️ Suggested fix
installations: {
- where: { installedAt: { gte: thirtyDaysAgo } },
+ where: { installedAt: { gte: thirtyDaysAgo, lte: now } },
select: { installedAt: true },
},Also applies to: 56-60
🤖 Prompt for AI Agents
In `@apps/web-next/src/app/api/v1/plugin-publisher/stats/`[packageName]/route.ts
around lines 45 - 48, The installations subquery currently filters only with
installedAt: { gte: thirtyDaysAgo } which can include future-dated rows; update
the query in route.ts (the installations select used to compute totalInstalls
and the similar block at lines ~56-60) to add an upper bound by also requiring
installedAt: { lte: now } (or Date.now()/new Date()) so the where becomes a
range between thirtyDaysAgo and now, ensuring future timestamps aren’t counted.
- stats route: use timestamp arithmetic instead of Date mutation to avoid potential issues with reused Date objects - integrations route: merge DISPLAY_NAME_OVERRIDES into INTEGRATION_META to eliminate duplicate display name source - keys route: remove type assertion, use runtime typeof checks instead to validate body fields safely Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated 7 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Aggregate real download counts from versions | ||
| const totalDownloads = pkg.versions.reduce((sum, v) => sum + (v.downloads ?? 0), 0); | ||
| // Use the filtered (30-day) installations count to align with the timeline | ||
| const totalInstalls = pkg.installations.length; | ||
| const versionsCount = pkg.versions.length; | ||
|
|
||
| for (let i = 29; i >= 0; i--) { | ||
| const date = new Date(now); | ||
| date.setDate(date.getDate() - i); | ||
| // Build a 30-day installation timeline from real installation data | ||
| // Create day buckets using UTC consistently | ||
| const MS_PER_DAY = 24 * 60 * 60 * 1000; | ||
| const timeline: { date: string; downloads: number; installs: number }[] = []; | ||
| for (let i = 0; i < 30; i++) { | ||
| const d = new Date(thirtyDaysAgo.getTime() + i * MS_PER_DAY); | ||
| const y = d.getUTCFullYear(); | ||
| const m = String(d.getUTCMonth() + 1).padStart(2, '0'); | ||
| const day = String(d.getUTCDate()).padStart(2, '0'); | ||
| timeline.push({ | ||
| date: date.toISOString().split('T')[0], | ||
| downloads: Math.floor(Math.random() * (baseDownloads / 30) + 1), | ||
| installs: Math.floor(Math.random() * (baseDownloads / 60) + 1), | ||
| date: `${y}-${m}-${day}`, | ||
| downloads: 0, | ||
| installs: 0, | ||
| }); | ||
| } |
There was a problem hiding this comment.
The response now returns timeline.downloads: 0 for every day (no code ever increments it), which is a behavior regression if clients expect a non-zero downloads series. Either (a) compute a meaningful per-day downloads series (requires per-day download data, or an intentional allocation strategy), or (b) remove/omit downloads from the timeline and/or rename the field to avoid implying it is populated.
| return success({ | ||
| totalDownloads: pkg.downloads || 0, | ||
| totalInstalls: Math.floor((pkg.downloads || 0) * 0.3), | ||
| versionsCount: pkg.versions?.length || 1, | ||
| totalDownloads, | ||
| totalInstalls, | ||
| versionsCount, | ||
| timeline, | ||
| }); |
There was a problem hiding this comment.
The response now returns timeline.downloads: 0 for every day (no code ever increments it), which is a behavior regression if clients expect a non-zero downloads series. Either (a) compute a meaningful per-day downloads series (requires per-day download data, or an intentional allocation strategy), or (b) remove/omit downloads from the timeline and/or rename the field to avoid implying it is populated.
| installations: { | ||
| where: { installedAt: { gte: thirtyDaysAgo } }, | ||
| select: { installedAt: true }, | ||
| }, |
There was a problem hiding this comment.
This pulls every installation row within the 30-day window into memory and then buckets in JS. For packages with high volume, this will create large payloads and slow route execution. Prefer aggregating in the database (e.g., a Prisma groupBy on date truncation via raw SQL, or maintaining a daily rollup table) so the API only transfers 30 buckets rather than N installations.
| // Bucket installations into days | ||
| for (const inst of pkg.installations) { | ||
| if (!inst.installedAt) continue; | ||
| const instDate = new Date(inst.installedAt); | ||
| const diffMs = instDate.getTime() - thirtyDaysAgo.getTime(); | ||
| const dayIndex = Math.floor(diffMs / (24 * 60 * 60 * 1000)); | ||
| if (dayIndex >= 0 && dayIndex < 30) { | ||
| timeline[dayIndex].installs += 1; | ||
| } | ||
| } |
There was a problem hiding this comment.
This pulls every installation row within the 30-day window into memory and then buckets in JS. For packages with high volume, this will create large payloads and slow route execution. Prefer aggregating in the database (e.g., a Prisma groupBy on date truncation via raw SQL, or maintaining a daily rollup table) so the API only transfers 30 buckets rather than N installations.
| /** | ||
| * Single source of truth for known integration metadata. | ||
| * The IntegrationConfig DB model stores only type, displayName, and configured. | ||
| * displayName, category, and description are enriched from this map so the | ||
| * response shape is consistent regardless of whether DB rows exist. | ||
| */ | ||
| const INTEGRATION_META: Record<string, { displayName: string; category: string; description: string }> = { | ||
| openai: { displayName: 'OpenAI', category: 'ai', description: 'GPT models for AI-powered features' }, | ||
| anthropic: { displayName: 'Anthropic', category: 'ai', description: 'Claude AI models' }, | ||
| 'aws-s3': { displayName: 'AWS S3', category: 'storage', description: 'Amazon S3 for file storage' }, | ||
| sendgrid: { displayName: 'SendGrid', category: 'email', description: 'Email delivery service' }, | ||
| stripe: { displayName: 'Stripe', category: 'payments', description: 'Payment processing' }, | ||
| twilio: { displayName: 'Twilio', category: 'communications', description: 'SMS and voice services' }, | ||
| }; |
There was a problem hiding this comment.
The comment says metadata is the “single source of truth”, but displayName comes from the DB row while category/description come from the metadata map. This can produce inconsistent results (e.g., DB displayName differs from meta displayName, or meta is updated but DB lags). Consider making one authoritative: either always use meta.displayName when present (and only fall back to r.displayName when missing), or remove displayName from INTEGRATION_META if DB should own it.
| type: r.type, | ||
| displayName: r.displayName, | ||
| configured: r.configured, | ||
| category: meta?.category ?? 'other', | ||
| description: meta?.description ?? '', | ||
| }; |
There was a problem hiding this comment.
The comment says metadata is the “single source of truth”, but displayName comes from the DB row while category/description come from the metadata map. This can produce inconsistent results (e.g., DB displayName differs from meta displayName, or meta is updated but DB lags). Consider making one authoritative: either always use meta.displayName when present (and only fall back to r.displayName when missing), or remove displayName from INTEGRATION_META if DB should own it.
| */ | ||
|
|
||
| /** Map a Prisma DevApiAIModel row to the shape the frontend expects. */ | ||
| export function serialiseModel(m: { |
There was a problem hiding this comment.
The new helper is named serialiseModel (British spelling). If the codebase predominantly uses serialize* (American spelling) elsewhere, this will be easy to miss and can lead to inconsistent naming/searchability. Consider renaming to serializeModel for consistency.
| export function serialiseModel(m: { | |
| export function serializeModel(m: { |
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@apps/web-next/src/app/api/v1/developer/keys/route.ts`:
- Around line 82-100: The handler currently assumes request.json() returns an
object, which throws when body is null or an array; after parsing (body = await
request.json()), add an explicit guard: if (typeof body !== 'object' || body ===
null || Array.isArray(body)) return errors.badRequest('Invalid JSON payload:
expected object'); then proceed to read projectName, modelId, gatewayId (the
existing variables) so property access on body is safe; keep body typed as
Record<string, unknown> only after this guard or cast after validating its
shape.
🧹 Nitpick comments (2)
apps/web-next/src/app/api/v1/integrations/route.ts (1)
25-33: Ordering mismatch between fallback and DB-backed responses.
DEFAULT_INTEGRATIONSpreservesINTEGRATION_METAinsertion order, but the DB query on line 38 orders bydisplayName: 'asc'. This creates inconsistent ordering between fresh deployments (fallback) and seeded databases, which could cause subtle UI shifts when the DB is first populated.🔧 Proposed fix to sort DEFAULT_INTEGRATIONS
// Fallback catalogue — returned only when the IntegrationConfig table is empty // (e.g. fresh deployment before seed). Each entry carries `configured: false`. -const DEFAULT_INTEGRATIONS = Object.entries(INTEGRATION_META).map(([type, meta]) => ({ - type, - displayName: meta.displayName, - configured: false, - category: meta.category, - description: meta.description, -})); +const DEFAULT_INTEGRATIONS = Object.entries(INTEGRATION_META) + .map(([type, meta]) => ({ + type, + displayName: meta.displayName, + configured: false, + category: meta.category, + description: meta.description, + })) + .sort((a, b) => a.displayName.localeCompare(b.displayName));apps/web-next/src/app/api/v1/plugin-publisher/stats/[packageName]/route.ts (1)
73-74: Consider documenting whydownloadsis always zero in the timeline.The
downloadsfield in each timeline entry is initialized to 0 but never populated. If this is intentional due to the data model only tracking total downloads per version (not per-day), a brief comment would clarify this for future maintainers.
| let body: Record<string, unknown>; | ||
| try { | ||
| body = await request.json(); | ||
| } catch { | ||
| return errors.badRequest('Invalid JSON in request body'); | ||
| } | ||
|
|
||
| if (!projectName || !modelId || !gatewayId) { | ||
| const projectName = body.projectName; | ||
| const modelId = body.modelId; | ||
| const gatewayId = body.gatewayId; | ||
|
|
||
| if ( | ||
| typeof projectName !== 'string' || | ||
| typeof modelId !== 'string' || | ||
| typeof gatewayId !== 'string' || | ||
| projectName.trim() === '' || | ||
| modelId.trim() === '' || | ||
| gatewayId.trim() === '' | ||
| ) { |
There was a problem hiding this comment.
Guard against non-object JSON bodies to avoid 500s.
If the JSON body is null or an array, property access throws and the handler returns a 500. Reject non-object payloads explicitly with a 400 before reading fields.
🛠️ Proposed fix
- let body: Record<string, unknown>;
+ let body: unknown;
try {
body = await request.json();
} catch {
return errors.badRequest('Invalid JSON in request body');
}
- const projectName = body.projectName;
- const modelId = body.modelId;
- const gatewayId = body.gatewayId;
+ if (body === null || typeof body !== 'object' || Array.isArray(body)) {
+ return errors.badRequest('Request body must be a JSON object');
+ }
+
+ const { projectName, modelId, gatewayId } = body as Record<string, unknown>;🤖 Prompt for AI Agents
In `@apps/web-next/src/app/api/v1/developer/keys/route.ts` around lines 82 - 100,
The handler currently assumes request.json() returns an object, which throws
when body is null or an array; after parsing (body = await request.json()), add
an explicit guard: if (typeof body !== 'object' || body === null ||
Array.isArray(body)) return errors.badRequest('Invalid JSON payload: expected
object'); then proceed to read projectName, modelId, gatewayId (the existing
variables) so property access on body is safe; keep body typed as Record<string,
unknown> only after this guard or cast after validating its shape.
|
Closing in favor of same-repo PR to fix label/merge permissions. |
Address 103 high-severity CodeQL alerts across 34 files: **Insecure temporary files (js/insecure-temporary-file) — 20 alerts:** - Replace predictable temp paths with crypto.randomUUID() and fs.mkdtempSync() **User-controlled bypass (js/user-controlled-bypass) — 14 alerts:** - Add typeof checks for auth tokens, codes, emails, passwords - Remove user-controlled skipVerification flag from publish endpoints - Validate GitHub webhook headers with typeof guards - Add OAuth error code allowlist in callback handler **Remote property injection (js/remote-property-injection) — 6 alerts:** - Block __proto__, constructor, prototype keys in config merge/assignment **File system race (js/file-system-race) — 3 alerts:** - Replace TOCTOU check-then-read with try/catch around direct operations **Tainted format string (js/tainted-format-string) — 5 alerts:** - Sanitize user input (strip control chars/newlines) before logging **Incomplete sanitization + bad HTML filter (js/bad-tag-filter) — 2 alerts:** - Replace single-pass regex HTML stripping with iterative loop **Path injection (js/path-injection) — 20 alerts:** - Add safeResolvePath() for storage paths - Add sanitizePathComponent() for plugin/version path segments - Add validateFilePath() for uploaded file paths **SQL injection (js/sql-injection) — 5 alerts:** - Add validateIdentifier() strict allowlist for DDL identifiers - Sanitize database names, usernames, and passwords **XSS through DOM (js/xss-through-dom) — 2 alerts:** - Validate avatar URLs with URL constructor, block javascript: URIs **Reflected XSS (js/reflected-xss) — 1 alert:** - Validate plugin name against allowlist before HTML interpolation **Insecure download (js/insecure-download) — 2 alerts:** - Enforce HTTPS protocol validation before fetch calls **Insecure randomness (js/insecure-randomness) — 1 alert:** - Replace Math.random() with crypto.randomInt() **Insufficient password hash (js/insufficient-password-hash) — 2 alerts:** - Replace SHA-256 with crypto.scryptSync for API key hashing **Missing rate limiting (js/missing-rate-limiting) — 13 alerts:** - Add in-memory rate limiting to auth, registry, embed, and config routes **Insecure Helmet configuration (js/insecure-helmet-configuration) — 2 alerts:** - Replace disabled CSP with proper CSP directives **Incomplete URL sanitization (js/incomplete-url-substring-sanitization) — 5 alerts:** - Replace string includes/startsWith with URL constructor parsing Alerts fixed: #1-50, #52-53, #57-61, #76-77, #82-97, #102-128, #279-280 Co-authored-by: Cursor <cursoragent@cursor.com>
Address 103 high-severity CodeQL alerts across 34 files: **Insecure temporary files (js/insecure-temporary-file) — 20 alerts:** - Replace predictable temp paths with crypto.randomUUID() and fs.mkdtempSync() **User-controlled bypass (js/user-controlled-bypass) — 14 alerts:** - Add typeof checks for auth tokens, codes, emails, passwords - Remove user-controlled skipVerification flag from publish endpoints - Validate GitHub webhook headers with typeof guards - Add OAuth error code allowlist in callback handler **Remote property injection (js/remote-property-injection) — 6 alerts:** - Block __proto__, constructor, prototype keys in config merge/assignment **File system race (js/file-system-race) — 3 alerts:** - Replace TOCTOU check-then-read with try/catch around direct operations **Tainted format string (js/tainted-format-string) — 5 alerts:** - Sanitize user input (strip control chars/newlines) before logging **Incomplete sanitization + bad HTML filter (js/bad-tag-filter) — 2 alerts:** - Replace single-pass regex HTML stripping with iterative loop **Path injection (js/path-injection) — 20 alerts:** - Add safeResolvePath() for storage paths - Add sanitizePathComponent() for plugin/version path segments - Add validateFilePath() for uploaded file paths **SQL injection (js/sql-injection) — 5 alerts:** - Add validateIdentifier() strict allowlist for DDL identifiers - Sanitize database names, usernames, and passwords **XSS through DOM (js/xss-through-dom) — 2 alerts:** - Validate avatar URLs with URL constructor, block javascript: URIs **Reflected XSS (js/reflected-xss) — 1 alert:** - Validate plugin name against allowlist before HTML interpolation **Insecure download (js/insecure-download) — 2 alerts:** - Enforce HTTPS protocol validation before fetch calls **Insecure randomness (js/insecure-randomness) — 1 alert:** - Replace Math.random() with crypto.randomInt() **Insufficient password hash (js/insufficient-password-hash) — 2 alerts:** - Replace SHA-256 with crypto.scryptSync for API key hashing **Missing rate limiting (js/missing-rate-limiting) — 13 alerts:** - Add in-memory rate limiting to auth, registry, embed, and config routes **Insecure Helmet configuration (js/insecure-helmet-configuration) — 2 alerts:** - Replace disabled CSP with proper CSP directives **Incomplete URL sanitization (js/incomplete-url-substring-sanitization) — 5 alerts:** - Replace string includes/startsWith with URL constructor parsing Alerts fixed: #1-50, #52-53, #57-61, #76-77, #82-97, #102-128, #279-280 Co-authored-by: Cursor <cursoragent@cursor.com>
Address 103 high-severity CodeQL alerts across 34 files: **Insecure temporary files (js/insecure-temporary-file) — 20 alerts:** - Replace predictable temp paths with crypto.randomUUID() and fs.mkdtempSync() **User-controlled bypass (js/user-controlled-bypass) — 14 alerts:** - Add typeof checks for auth tokens, codes, emails, passwords - Remove user-controlled skipVerification flag from publish endpoints - Validate GitHub webhook headers with typeof guards - Add OAuth error code allowlist in callback handler **Remote property injection (js/remote-property-injection) — 6 alerts:** - Block __proto__, constructor, prototype keys in config merge/assignment **File system race (js/file-system-race) — 3 alerts:** - Replace TOCTOU check-then-read with try/catch around direct operations **Tainted format string (js/tainted-format-string) — 5 alerts:** - Sanitize user input (strip control chars/newlines) before logging **Incomplete sanitization + bad HTML filter (js/bad-tag-filter) — 2 alerts:** - Replace single-pass regex HTML stripping with iterative loop **Path injection (js/path-injection) — 20 alerts:** - Add safeResolvePath() for storage paths - Add sanitizePathComponent() for plugin/version path segments - Add validateFilePath() for uploaded file paths **SQL injection (js/sql-injection) — 5 alerts:** - Add validateIdentifier() strict allowlist for DDL identifiers - Sanitize database names, usernames, and passwords **XSS through DOM (js/xss-through-dom) — 2 alerts:** - Validate avatar URLs with URL constructor, block javascript: URIs **Reflected XSS (js/reflected-xss) — 1 alert:** - Validate plugin name against allowlist before HTML interpolation **Insecure download (js/insecure-download) — 2 alerts:** - Enforce HTTPS protocol validation before fetch calls **Insecure randomness (js/insecure-randomness) — 1 alert:** - Replace Math.random() with crypto.randomInt() **Insufficient password hash (js/insufficient-password-hash) — 2 alerts:** - Replace SHA-256 with crypto.scryptSync for API key hashing **Missing rate limiting (js/missing-rate-limiting) — 13 alerts:** - Add in-memory rate limiting to auth, registry, embed, and config routes **Insecure Helmet configuration (js/insecure-helmet-configuration) — 2 alerts:** - Replace disabled CSP with proper CSP directives **Incomplete URL sanitization (js/incomplete-url-substring-sanitization) — 5 alerts:** - Replace string includes/startsWith with URL constructor parsing Alerts fixed: #1-50, #52-53, #57-61, #76-77, #82-97, #102-128, #279-280 Co-authored-by: Cursor <cursoragent@cursor.com>
Summary
Several Next.js API routes returned hardcoded mock data instead of querying the database, even though Prisma models existed for the data. This is the same class of bug as #48 (capacity planner persistence).
Routes migrated to Prisma:
developer/models/route.ts→ queriesDevApiAIModeltable with type-safePrisma.DevApiAIModelWhereInputdeveloper/models/[id]/route.ts→ queriesDevApiAIModelby ID with gateway countdeveloper/models/[id]/gateways/route.ts→ queriesDevApiGatewayOffertabledeveloper/keys/route.ts→ validates model/gateway existence against DB (was using mockgetModel/getGatewayOffer)integrations/route.ts→ queriesIntegrationConfigtable (falls back to default catalogue if empty)plugin-publisher/stats/[packageName]/route.ts→ computes real stats fromPluginVersion.downloadsandPluginInstallationdata instead ofMath.random()Also identified (lower priority, no DB model):
network-analytics/*routes (4 routes) — pure display/analytics, no Prisma modelorchestrator-manager/orchestrators/route.ts— no Prisma modelreleases/route.ts— no Prisma model (future: GitHub API)Closes #51
Test plan
Made with Cursor
Summary by CodeRabbit
Refactor
New Features
Bug Fixes