refactor: improve pull request schemas and validation

- Add proper state enum validation
- Add title and body length validation
- Consolidate request schemas
- Add consistent parameter handling
- Improve type safety
- Add proper JSDoc documentation
This commit is contained in:
Peter M. Elias 2024-12-28 03:07:34 -08:00
commit 835be7f855

View file

@ -5,71 +5,122 @@ import {
GitHubRepositorySchema GitHubRepositorySchema
} from "../common/types.js"; } from "../common/types.js";
// Schema definitions // Constants for GitHub limits and constraints
export const GitHubPullRequestHeadSchema = z.object({ const GITHUB_TITLE_MAX_LENGTH = 256;
const GITHUB_BODY_MAX_LENGTH = 65536;
// Base schema for repository identification
export const RepositoryParamsSchema = z.object({
owner: z.string().min(1).describe("Repository owner (username or organization)"),
repo: z.string().min(1).describe("Repository name"),
});
// Common validation schemas
export const GitHubPullRequestStateSchema = z.enum([
"open",
"closed",
"merged",
"draft"
]).describe("The current state of the pull request");
export const GitHubPullRequestSortSchema = z.enum([
"created",
"updated",
"popularity",
"long-running"
]).describe("The sorting field for pull requests");
export const GitHubDirectionSchema = z.enum([
"asc",
"desc"
]).describe("The sort direction");
// Pull request head/base schema
export const GitHubPullRequestRefSchema = z.object({
label: z.string(), label: z.string(),
ref: z.string(), ref: z.string().min(1),
sha: z.string(), sha: z.string().length(40),
user: GitHubIssueAssigneeSchema, user: GitHubIssueAssigneeSchema,
repo: GitHubRepositorySchema, repo: GitHubRepositorySchema,
}); }).describe("Reference information for pull request head or base");
// Main pull request schema
export const GitHubPullRequestSchema = z.object({ export const GitHubPullRequestSchema = z.object({
url: z.string(), url: z.string().url(),
id: z.number(), id: z.number().positive(),
node_id: z.string(), node_id: z.string(),
html_url: z.string(), html_url: z.string().url(),
diff_url: z.string(), diff_url: z.string().url(),
patch_url: z.string(), patch_url: z.string().url(),
issue_url: z.string(), issue_url: z.string().url(),
number: z.number(), number: z.number().positive(),
state: z.string(), state: GitHubPullRequestStateSchema,
locked: z.boolean(), locked: z.boolean(),
title: z.string(), title: z.string().max(GITHUB_TITLE_MAX_LENGTH),
user: GitHubIssueAssigneeSchema, user: GitHubIssueAssigneeSchema,
body: z.string(), body: z.string().max(GITHUB_BODY_MAX_LENGTH).nullable(),
created_at: z.string(), created_at: z.string().datetime(),
updated_at: z.string(), updated_at: z.string().datetime(),
closed_at: z.string().nullable(), closed_at: z.string().datetime().nullable(),
merged_at: z.string().nullable(), merged_at: z.string().datetime().nullable(),
merge_commit_sha: z.string().nullable(), merge_commit_sha: z.string().length(40).nullable(),
assignee: GitHubIssueAssigneeSchema.nullable(), assignee: GitHubIssueAssigneeSchema.nullable(),
assignees: z.array(GitHubIssueAssigneeSchema), assignees: z.array(GitHubIssueAssigneeSchema),
head: GitHubPullRequestHeadSchema, requested_reviewers: z.array(GitHubIssueAssigneeSchema),
base: GitHubPullRequestHeadSchema, labels: z.array(z.object({
name: z.string(),
color: z.string().regex(/^[0-9a-fA-F]{6}$/),
description: z.string().nullable(),
})),
head: GitHubPullRequestRefSchema,
base: GitHubPullRequestRefSchema,
}); });
// Request schemas
export const ListPullRequestsOptionsSchema = z.object({
state: GitHubPullRequestStateSchema.optional(),
head: z.string().optional(),
base: z.string().optional(),
sort: GitHubPullRequestSortSchema.optional(),
direction: GitHubDirectionSchema.optional(),
per_page: z.number().min(1).max(100).optional(),
page: z.number().min(1).optional(),
}).describe("Options for listing pull requests");
export const CreatePullRequestOptionsSchema = z.object({ export const CreatePullRequestOptionsSchema = z.object({
title: z.string().describe("Pull request title"), title: z.string().max(GITHUB_TITLE_MAX_LENGTH).describe("Pull request title"),
body: z.string().optional().describe("Pull request body/description"), body: z.string().max(GITHUB_BODY_MAX_LENGTH).optional().describe("Pull request body/description"),
head: z.string().describe("The name of the branch where your changes are implemented"), head: z.string().min(1).describe("The name of the branch where your changes are implemented"),
base: z.string().describe("The name of the branch you want the changes pulled into"), base: z.string().min(1).describe("The name of the branch you want the changes pulled into"),
maintainer_can_modify: z.boolean().optional().describe("Whether maintainers can modify the pull request"), maintainer_can_modify: z.boolean().optional().describe("Whether maintainers can modify the pull request"),
draft: z.boolean().optional().describe("Whether to create the pull request as a draft"), draft: z.boolean().optional().describe("Whether to create the pull request as a draft"),
}); }).describe("Options for creating a pull request");
export const CreatePullRequestSchema = z.object({ // Combine repository params with operation options
owner: z.string().describe("Repository owner (username or organization)"), export const CreatePullRequestSchema = RepositoryParamsSchema.extend({
repo: z.string().describe("Repository name"), ...CreatePullRequestOptionsSchema.shape,
title: z.string().describe("Pull request title"),
body: z.string().optional().describe("Pull request body/description"),
head: z.string().describe("The name of the branch where your changes are implemented"),
base: z.string().describe("The name of the branch you want the changes pulled into"),
draft: z.boolean().optional().describe("Whether to create the pull request as a draft"),
maintainer_can_modify: z.boolean().optional().describe("Whether maintainers can modify the pull request"),
}); });
// Type exports // Type exports
export type RepositoryParams = z.infer<typeof RepositoryParamsSchema>;
export type CreatePullRequestOptions = z.infer<typeof CreatePullRequestOptionsSchema>; export type CreatePullRequestOptions = z.infer<typeof CreatePullRequestOptionsSchema>;
export type ListPullRequestsOptions = z.infer<typeof ListPullRequestsOptionsSchema>;
export type GitHubPullRequest = z.infer<typeof GitHubPullRequestSchema>; export type GitHubPullRequest = z.infer<typeof GitHubPullRequestSchema>;
export type GitHubPullRequestHead = z.infer<typeof GitHubPullRequestHeadSchema>; export type GitHubPullRequestRef = z.infer<typeof GitHubPullRequestRefSchema>;
// Function implementations /**
* Creates a new pull request in a repository.
*
* @param params Repository identification and pull request creation options
* @returns Promise resolving to the created pull request
* @throws {ZodError} If the input parameters fail validation
* @throws {Error} If the GitHub API request fails
*/
export async function createPullRequest( export async function createPullRequest(
owner: string, params: z.infer<typeof CreatePullRequestSchema>
repo: string, ): Promise<GitHubPullRequest> {
options: CreatePullRequestOptions const { owner, repo, ...options } = CreatePullRequestSchema.parse(params);
): Promise<z.infer<typeof GitHubPullRequestSchema>> {
const response = await githubRequest( const response = await githubRequest(
`https://api.github.com/repos/${owner}/${repo}/pulls`, `https://api.github.com/repos/${owner}/${repo}/pulls`,
{ {
@ -81,11 +132,21 @@ export async function createPullRequest(
return GitHubPullRequestSchema.parse(response); return GitHubPullRequestSchema.parse(response);
} }
/**
* Retrieves a specific pull request by its number.
*
* @param params Repository parameters and pull request number
* @returns Promise resolving to the pull request details
* @throws {Error} If the pull request is not found or the request fails
*/
export async function getPullRequest( export async function getPullRequest(
owner: string, params: RepositoryParams & { pullNumber: number }
repo: string, ): Promise<GitHubPullRequest> {
pullNumber: number const { owner, repo, pullNumber } = z.object({
): Promise<z.infer<typeof GitHubPullRequestSchema>> { ...RepositoryParamsSchema.shape,
pullNumber: z.number().positive(),
}).parse(params);
const response = await githubRequest( const response = await githubRequest(
`https://api.github.com/repos/${owner}/${repo}/pulls/${pullNumber}` `https://api.github.com/repos/${owner}/${repo}/pulls/${pullNumber}`
); );
@ -93,20 +154,24 @@ export async function getPullRequest(
return GitHubPullRequestSchema.parse(response); return GitHubPullRequestSchema.parse(response);
} }
/**
* Lists pull requests in a repository with optional filtering.
*
* @param params Repository parameters and listing options
* @returns Promise resolving to an array of pull requests
* @throws {ZodError} If the input parameters fail validation
* @throws {Error} If the GitHub API request fails
*/
export async function listPullRequests( export async function listPullRequests(
owner: string, params: RepositoryParams & Partial<ListPullRequestsOptions>
repo: string, ): Promise<GitHubPullRequest[]> {
options: { const { owner, repo, ...options } = z.object({
state?: "open" | "closed" | "all"; ...RepositoryParamsSchema.shape,
head?: string; ...ListPullRequestsOptionsSchema.partial().shape,
base?: string; }).parse(params);
sort?: "created" | "updated" | "popularity" | "long-running";
direction?: "asc" | "desc";
per_page?: number;
page?: number;
} = {}
): Promise<z.infer<typeof GitHubPullRequestSchema>[]> {
const url = new URL(`https://api.github.com/repos/${owner}/${repo}/pulls`); const url = new URL(`https://api.github.com/repos/${owner}/${repo}/pulls`);
Object.entries(options).forEach(([key, value]) => { Object.entries(options).forEach(([key, value]) => {
if (value !== undefined) { if (value !== undefined) {
url.searchParams.append(key, value.toString()); url.searchParams.append(key, value.toString());