Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
53 changes: 51 additions & 2 deletions packages/das/src/api/miners/miners.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ import {
ApiQuery,
ApiTags,
} from "@nestjs/swagger";
import { ConfigService } from "@nestjs/config";
import { MinersService } from "./miners.service";
import { parsePaginationQuery } from "./pagination";

// GitHub owner/repo pattern: alphanum + `.`, `_`, `-`, reasonable length.
const REPO_FULL_NAME_PATTERN = /^[\w.-]{1,100}\/[\w.-]{1,100}$/;
Expand Down Expand Up @@ -104,7 +106,17 @@ const SINCE_BY_REPO_API_BODY = {
@ApiTags("Miners")
@Controller("api/v1/miners")
export class MinersController {
constructor(private readonly miners: MinersService) {}
private readonly defaultPaginationEnabled: boolean;

constructor(
private readonly miners: MinersService,
config: ConfigService,
) {
const raw = config.get<string>("MINERS_PAGINATION_ENABLED") ?? "";
this.defaultPaginationEnabled = ["1", "true", "yes", "on"].includes(
raw.toLowerCase(),
);
}

@Get(":githubId/pulls")
@ApiOperation({
Expand All @@ -122,13 +134,32 @@ export class MinersController {
description:
"ISO timestamp. Defaults to 35 days ago (midnight UTC) if omitted.",
})
@ApiQuery({
name: "cursor",
required: false,
description:
"Opaque pagination cursor from a previous response's next_cursor field.",
})
@ApiQuery({
name: "limit",
required: false,
description: "Page size (default 50, max 200).",
})
async getPullRequests(
@Param("githubId") githubId: string,
@Query("since") since?: string,
@Query("cursor") cursor?: string,
@Query("limit") limit?: string,
): Promise<unknown> {
const pagination = parsePaginationQuery(
limit,
cursor,
this.defaultPaginationEnabled,
);
return this.miners.getPullRequests(
githubId,
MinersService.resolveSince(since),
pagination,
);
}

Expand Down Expand Up @@ -169,11 +200,29 @@ export class MinersController {
"ISO timestamp. When omitted, the response contains all currently-" +
"OPEN issues with no time bound and no CLOSED history.",
})
@ApiQuery({
name: "cursor",
required: false,
description:
"Opaque pagination cursor from a previous response's next_cursor field.",
})
@ApiQuery({
name: "limit",
required: false,
description: "Page size (default 50, max 200).",
})
async getIssues(
@Param("githubId") githubId: string,
@Query("since") since?: string,
@Query("cursor") cursor?: string,
@Query("limit") limit?: string,
): Promise<unknown> {
return this.miners.getIssues(githubId, since ?? null);
const pagination = parsePaginationQuery(
limit,
cursor,
this.defaultPaginationEnabled,
);
return this.miners.getIssues(githubId, since ?? null, pagination);
}

@Post(":githubId/issues")
Expand Down
122 changes: 116 additions & 6 deletions packages/das/src/api/miners/miners.service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
/* eslint-disable @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-assignment */
import { Injectable } from "@nestjs/common";
import { DataSource } from "typeorm";
import {
buildPaginatedResponse,
keysetParams,
keysetSql,
PaginationParams,
} from "./pagination";

const DEFAULT_SINCE_DAYS = 35;

Expand Down Expand Up @@ -167,12 +173,54 @@ export class MinersService {
async getPullRequests(
githubId: string,
since: string,
pagination: PaginationParams | null,
): Promise<{
github_id: string;
since: string;
generated_at: string;
pull_requests: unknown[];
next_cursor?: string | null;
}> {
if (!pagination) {
const rows = await this.dataSource.query(
`
SELECT${PR_SELECT_COLUMNS}
FROM pull_requests p
LEFT JOIN pr_review_summary rs
ON rs.repo_full_name = p.repo_full_name
AND rs.pr_number = p.pr_number
LEFT JOIN repos r
ON r.repo_full_name = p.repo_full_name
WHERE p.author_github_id = $1
AND (
(p.state = 'OPEN' AND p.created_at >= $2)
OR (p.state = 'MERGED' AND p.merged_at >= $2)
OR (p.state = 'CLOSED' AND p.created_at >= $2)
)
ORDER BY p.created_at DESC
`,
[githubId, since],
);

return {
github_id: githubId,
since,
generated_at: new Date().toISOString(),
pull_requests: rows,
};
}

const { limit, cursor } = pagination;
const params: unknown[] = [githubId, since];
let keysetClause = "";
if (cursor) {
const startIdx = params.length + 1;
keysetClause = `AND ${keysetSql("p", "pr_number", startIdx)}`;
params.push(...keysetParams(cursor));
}
const limitIdx = params.length + 1;
params.push(limit + 1);

const rows = await this.dataSource.query(
`
SELECT${PR_SELECT_COLUMNS}
Expand All @@ -188,16 +236,29 @@ export class MinersService {
OR (p.state = 'MERGED' AND p.merged_at >= $2)
OR (p.state = 'CLOSED' AND p.created_at >= $2)
)
ORDER BY p.created_at DESC
${keysetClause}
ORDER BY p.created_at DESC, LOWER(p.repo_full_name) DESC, p.pr_number DESC
LIMIT $${limitIdx}
`,
[githubId, since],
params,
);

const page = buildPaginatedResponse(
rows as Record<string, unknown>[],
limit,
(row) => ({
created_at: row.created_at as string,
repo_full_name: row.repo_full_name as string,
pr_number: row.pr_number as number,
}),
);

return {
github_id: githubId,
since,
generated_at: new Date().toISOString(),
pull_requests: rows,
pull_requests: page.items,
next_cursor: page.nextCursor,
};
}

Expand Down Expand Up @@ -253,12 +314,48 @@ export class MinersService {
async getIssues(
githubId: string,
since: string | null,
pagination: PaginationParams | null,
): Promise<{
github_id: string;
since: string | null;
generated_at: string;
issues: unknown[];
next_cursor?: string | null;
}> {
if (!pagination) {
const rows = await this.dataSource.query(
`
SELECT${ISSUE_SELECT_COLUMNS}
FROM issues i
WHERE i.author_github_id = $1
AND (
(i.state = 'OPEN' AND ($2::timestamptz IS NULL OR i.created_at >= $2))
OR (i.state = 'CLOSED' AND i.closed_at >= $2)
)
ORDER BY i.created_at DESC
`,
[githubId, since],
);

return {
github_id: githubId,
since,
generated_at: new Date().toISOString(),
issues: rows,
};
}

const { limit, cursor } = pagination;
const params: unknown[] = [githubId, since];
let keysetClause = "";
if (cursor) {
const startIdx = params.length + 1;
keysetClause = `AND ${keysetSql("i", "issue_number", startIdx)}`;
params.push(...keysetParams(cursor));
}
const limitIdx = params.length + 1;
params.push(limit + 1);

const rows = await this.dataSource.query(
`
SELECT${ISSUE_SELECT_COLUMNS}
Expand All @@ -268,16 +365,29 @@ export class MinersService {
(i.state = 'OPEN' AND ($2::timestamptz IS NULL OR i.created_at >= $2))
OR (i.state = 'CLOSED' AND i.closed_at >= $2)
)
ORDER BY i.created_at DESC
${keysetClause}
ORDER BY i.created_at DESC, LOWER(i.repo_full_name) DESC, i.issue_number DESC
LIMIT $${limitIdx}
`,
[githubId, since],
params,
);

const page = buildPaginatedResponse(
rows as Record<string, unknown>[],
limit,
(row) => ({
created_at: row.created_at as string,
repo_full_name: row.repo_full_name as string,
issue_number: row.issue_number as number,
}),
);

return {
github_id: githubId,
since,
generated_at: new Date().toISOString(),
issues: rows,
issues: page.items,
next_cursor: page.nextCursor,
};
}

Expand Down
Loading