diff --git a/packages/das/src/api/api.module.ts b/packages/das/src/api/api.module.ts index 2f8dea2..14b6fcc 100644 --- a/packages/das/src/api/api.module.ts +++ b/packages/das/src/api/api.module.ts @@ -13,6 +13,8 @@ import { FETCH_QUEUE } from "../queue/constants"; import { AdminController } from "./admin.controller"; import { RequireApiKeyGuard } from "./require-api-key.guard"; import { HealthController } from "./health.controller"; +import { DashboardController } from "./dashboard/dashboard.controller"; +import { DashboardService } from "./dashboard/dashboard.service"; import { MinersController } from "./miners/miners.controller"; import { MinersService } from "./miners/miners.service"; import { PullsController } from "./pulls/pulls.controller"; @@ -31,11 +33,17 @@ import { PullsService } from "./pulls/pulls.service"; BullModule.registerQueue({ name: FETCH_QUEUE }), ], controllers: [ + DashboardController, MinersController, PullsController, AdminController, HealthController, ], - providers: [MinersService, PullsService, RequireApiKeyGuard], + providers: [ + DashboardService, + MinersService, + PullsService, + RequireApiKeyGuard, + ], }) export class ApiModule {} diff --git a/packages/das/src/api/dashboard/dashboard.controller.ts b/packages/das/src/api/dashboard/dashboard.controller.ts new file mode 100644 index 0000000..f332206 --- /dev/null +++ b/packages/das/src/api/dashboard/dashboard.controller.ts @@ -0,0 +1,37 @@ +import { BadRequestException, Controller, Get, Query } from "@nestjs/common"; +import { ApiOperation, ApiQuery, ApiTags } from "@nestjs/swagger"; +import { DashboardService } from "./dashboard.service"; + +@ApiTags("Dashboard") +@Controller("api/v1/dashboard") +export class DashboardController { + constructor(private readonly dashboard: DashboardService) {} + + @Get("issues") + @ApiOperation({ + summary: "Slim issue rows for dashboard trend aggregation", + description: + "Returns every issue with `created_at` on or after `since`, plus " + + "every CLOSED issue whose `closed_at` is on or after `since`. " + + "The mirror is intentionally roster-blind: every issue is returned " + + "regardless of author. The dashboard blends with the gittensor API " + + "miner roster client-side to filter to subnet authors. " + + "Designed as a single bulk replacement for the dashboard's per-miner " + + "fan-out against `/miners//issues` (one call instead of N).", + }) + @ApiQuery({ + name: "since", + required: true, + description: "ISO timestamp — earliest creation/close date to include.", + }) + async getIssues(@Query("since") since?: string): Promise { + if (!since) { + throw new BadRequestException("`since` query parameter is required"); + } + const parsed = new Date(since); + if (Number.isNaN(parsed.getTime())) { + throw new BadRequestException("`since` must be a valid ISO timestamp"); + } + return this.dashboard.getIssues(parsed.toISOString()); + } +} diff --git a/packages/das/src/api/dashboard/dashboard.service.ts b/packages/das/src/api/dashboard/dashboard.service.ts new file mode 100644 index 0000000..62a7ab6 --- /dev/null +++ b/packages/das/src/api/dashboard/dashboard.service.ts @@ -0,0 +1,58 @@ +/* eslint-disable @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-assignment */ +import { Injectable } from "@nestjs/common"; +import { DataSource } from "typeorm"; + +interface DashboardIssueRow { + repo_full_name: string; + issue_number: number; + author_github_id: string | null; + created_at: string; + closed_at: string | null; + state: string; + state_reason: string | null; + solving_pr: { merged_at: string } | null; +} + +@Injectable() +export class DashboardService { + constructor(private readonly dataSource: DataSource) {} + + async getIssues(since: string): Promise<{ + since: string; + generated_at: string; + issues: DashboardIssueRow[]; + }> { + const rows = await this.dataSource.query( + ` + SELECT + LOWER(i.repo_full_name) AS repo_full_name, + i.issue_number, + i.author_github_id, + i.created_at, + i.closed_at, + i.state, + i.state_reason, + ( + SELECT json_build_object('merged_at', sp.merged_at) + FROM pull_requests sp + WHERE sp.repo_full_name = i.repo_full_name + AND sp.pr_number = i.solved_by_pr + AND sp.merged_at IS NOT NULL + LIMIT 1 + ) AS solving_pr + FROM issues i + WHERE + i.created_at >= $1 + OR (i.state = 'CLOSED' AND i.closed_at >= $1) + ORDER BY i.created_at DESC + `, + [since], + ); + + return { + since, + generated_at: new Date().toISOString(), + issues: rows as DashboardIssueRow[], + }; + } +}