import type { GitLabAPI, GitLabAuthStatus, GitLabBranchesResult, GitLabBranchesSearchResult, GitLabIssueCommentResult, GitLabIssueCommentsResult, GitLabIssueCommentInput, GitLabIssueCreateInput, GitLabIssueCreateResult, GitLabIssueGetResult, GitLabIssuesListResult, GitLabIssueUpdateInput, GitLabIssueUpdateResult, GitLabLabelsSearchResult, GitLabMergeRequest, GitLabMergeRequestCommitsResult, GitLabMergeRequestContextResult, GitLabMergeRequestCreateInput, GitLabMergeRequestCreateResult, GitLabMergeRequestMergeInput, GitLabMergeRequestMergeResult, GitLabMergeRequestsListResult, GitLabMergeRequestTimelineResult, GitLabMergeRequestUpdateInput, GitLabMergeRequestUpdateResult, GitLabMilestonesSearchResult, GitLabMrApproveInput, GitLabMrApproveResult, GitLabMrNoteInput, GitLabMrNoteResult, GitLabTagsSearchResult, GitLabUserSummary, GitLabUsersSearchResult, } from '@openchamber/ui/lib/api/types'; import { runtimeFetch } from '@openchamber/ui/lib/runtime-fetch'; import type { RuntimeUrlResolver } from '@openchamber/ui/lib/runtime-url'; interface WebGitLabAPIOptions { urls: RuntimeUrlResolver; } const jsonOrNull = async (response: Response): Promise => { return (await response.json().catch(() => null)) as T | null; }; export const createWebGitLabAPI = ({ urls }: WebGitLabAPIOptions): GitLabAPI => ({ async authStatus(): Promise { const response = await runtimeFetch('/api/gitlab/auth/status', { method: 'GET', headers: { Accept: 'application/json' } }); const payload = await jsonOrNull(response); if (!response.ok || !payload) { throw new Error(payload?.error || response.statusText || 'Failed to load GitLab status'); } return payload; }, async authConnect(input: { accessToken: string; baseUrl?: string }): Promise { const response = await runtimeFetch('/api/gitlab/auth/connect', { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body: JSON.stringify(input), }); const payload = await jsonOrNull(response); if (!response.ok || !payload) { throw new Error(payload?.error || response.statusText || 'Failed to connect GitLab'); } return payload; }, async authActivate(accountId: string): Promise { const response = await runtimeFetch('/api/gitlab/auth/activate', { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body: JSON.stringify({ accountId }), }); const payload = await jsonOrNull(response); if (!response.ok || !payload) { throw new Error(payload?.error || response.statusText || 'Failed to activate GitLab account'); } return payload; }, async authDisconnect(): Promise<{ removed: boolean }> { const response = await runtimeFetch('/api/gitlab/auth', { method: 'DELETE', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, }); const payload = await jsonOrNull<{ removed?: boolean; error?: string }>(response); if (!response.ok) { throw new Error(payload?.error || response.statusText || 'Failed to disconnect GitLab'); } return { removed: Boolean(payload?.removed) }; }, async me(): Promise { const response = await runtimeFetch('/api/gitlab/me', { method: 'GET', headers: { Accept: 'application/json' } }); const payload = await jsonOrNull(response); if (!response.ok || !payload) { throw new Error(payload?.error || response.statusText || 'Failed to fetch GitLab user'); } return payload; }, async searchUsers(directory, query, options): Promise { const params = new URLSearchParams({ directory, query }); if (options?.namespace) params.set('namespace', options.namespace); if (options?.project) params.set('project', options.project); const response = await runtimeFetch(urls.api('/api/gitlab/users/search', params), { method: 'GET', headers: { Accept: 'application/json' } }); const body = await jsonOrNull(response); if (!response.ok || !body) { throw new Error(body?.error || response.statusText || 'Failed to search GitLab users'); } return { connected: body.connected, repo: body.repo ?? null, users: body.users ?? [] }; }, async searchLabels(directory, query, options): Promise { const params = new URLSearchParams({ directory, query }); if (options?.namespace) params.set('namespace', options.namespace); if (options?.project) params.set('project', options.project); const response = await runtimeFetch(urls.api('/api/gitlab/labels/search', params), { method: 'GET', headers: { Accept: 'application/json' } }); const body = await jsonOrNull(response); if (!response.ok || !body) { throw new Error(body?.error || response.statusText || 'Failed to search GitLab labels'); } return { connected: body.connected, repo: body.repo ?? null, labels: body.labels ?? [] }; }, async searchMilestones(directory, query, options): Promise { const params = new URLSearchParams({ directory, query }); if (options?.namespace) params.set('namespace', options.namespace); if (options?.project) params.set('project', options.project); const response = await runtimeFetch(urls.api('/api/gitlab/milestones/search', params), { method: 'GET', headers: { Accept: 'application/json' } }); const body = await jsonOrNull(response); if (!response.ok || !body) { throw new Error(body?.error || response.statusText || 'Failed to search GitLab milestones'); } return { connected: body.connected, repo: body.repo ?? null, milestones: body.milestones ?? [] }; }, async searchBranches(directory, query, options): Promise { const params = new URLSearchParams({ directory, query }); if (options?.namespace) params.set('namespace', options.namespace); if (options?.project) params.set('project', options.project); const response = await runtimeFetch(urls.api('/api/gitlab/branches/search', params), { method: 'GET', headers: { Accept: 'application/json' } }); const body = await jsonOrNull(response); if (!response.ok || !body) { throw new Error(body?.error || response.statusText || 'Failed to search GitLab branches'); } return { connected: body.connected, repo: body.repo ?? null, branches: body.branches ?? [] }; }, async searchTags(directory, query, options): Promise { const params = new URLSearchParams({ directory, query }); if (options?.namespace) params.set('namespace', options.namespace); if (options?.project) params.set('project', options.project); const response = await runtimeFetch(urls.api('/api/gitlab/tags/search', params), { method: 'GET', headers: { Accept: 'application/json' } }); const body = await jsonOrNull(response); if (!response.ok || !body) { throw new Error(body?.error || response.statusText || 'Failed to search GitLab tags'); } return { connected: body.connected, repo: body.repo ?? null, tags: body.tags ?? [] }; }, async issuesList(directory: string, options?: { page?: number; query?: string }): Promise { const page = options?.page ?? 1; const params = new URLSearchParams({ directory, page: String(page), }); if (options?.query) { params.set('query', options.query); } const response = await runtimeFetch( `/api/gitlab/issues/list?${params.toString()}`, { method: 'GET', headers: { Accept: 'application/json' } } ); const payload = await jsonOrNull(response); if (!response.ok || !payload) { throw new Error(payload?.error || response.statusText || 'Failed to load GitLab issues'); } return payload; }, async issueGet(directory: string, number: number, options?: { namespace?: string; project?: string }): Promise { const params = new URLSearchParams({ directory, number: String(number) }); if (options?.namespace) { params.set('namespace', options.namespace); } if (options?.project) { params.set('project', options.project); } const response = await runtimeFetch(urls.api('/api/gitlab/issues/get', params), { method: 'GET', headers: { Accept: 'application/json' } }); const payload = await jsonOrNull(response); if (!response.ok || !payload) { throw new Error(payload?.error || response.statusText || 'Failed to load GitLab issue'); } return payload; }, async issueComments(directory: string, number: number, options?: { namespace?: string; project?: string }): Promise { const params = new URLSearchParams({ directory, number: String(number) }); if (options?.namespace) { params.set('namespace', options.namespace); } if (options?.project) { params.set('project', options.project); } const response = await runtimeFetch(urls.api('/api/gitlab/issues/comments', params), { method: 'GET', headers: { Accept: 'application/json' } }); const payload = await jsonOrNull(response); if (!response.ok || !payload) { throw new Error(payload?.error || response.statusText || 'Failed to load GitLab issue comments'); } return payload; }, async mrsList(directory: string, options?: { page?: number; query?: string; sourceBranch?: string }): Promise { const page = options?.page ?? 1; const params = new URLSearchParams({ directory, page: String(page), }); if (options?.query) { params.set('query', options.query); } if (options?.sourceBranch) { params.set('sourceBranch', options.sourceBranch); } const response = await runtimeFetch( `/api/gitlab/mrs/list?${params.toString()}`, { method: 'GET', headers: { Accept: 'application/json' } } ); const payload = await jsonOrNull(response); if (!response.ok || !payload) { throw new Error(payload?.error || response.statusText || 'Failed to load GitLab merge requests'); } return payload; }, async mrContext( directory: string, number: number, options?: { includeDiff?: boolean; namespace?: string; project?: string } ): Promise { const params = new URLSearchParams({ directory, number: String(number) }); if (options?.includeDiff) { params.set('diff', '1'); } if (options?.namespace) { params.set('namespace', options.namespace); } if (options?.project) { params.set('project', options.project); } const response = await runtimeFetch(urls.api('/api/gitlab/mrs/context', params), { method: 'GET', headers: { Accept: 'application/json' } }); const payload = await jsonOrNull(response); if (!response.ok || !payload) { throw new Error(payload?.error || response.statusText || 'Failed to load GitLab merge request context'); } return payload; }, async mrCommits(directory: string, number: number, options?: { namespace?: string; project?: string }): Promise { const params = new URLSearchParams({ directory, number: String(number) }); if (options?.namespace) { params.set('namespace', options.namespace); } if (options?.project) { params.set('project', options.project); } const response = await runtimeFetch(urls.api('/api/gitlab/mrs/commits', params), { method: 'GET', headers: { Accept: 'application/json' } }); const payload = await jsonOrNull(response); if (!response.ok || !payload) { throw new Error(payload?.error || response.statusText || 'Failed to load GitLab merge request commits'); } return payload; }, async mrTimeline(directory: string, number: number, options?: { namespace?: string; project?: string }): Promise { const params = new URLSearchParams({ directory, number: String(number) }); if (options?.namespace) { params.set('namespace', options.namespace); } if (options?.project) { params.set('project', options.project); } const response = await runtimeFetch(urls.api('/api/gitlab/mrs/timeline', params), { method: 'GET', headers: { Accept: 'application/json' } }); const payload = await jsonOrNull(response); if (!response.ok || !payload) { throw new Error(payload?.error || response.statusText || 'Failed to load GitLab merge request timeline'); } return payload; }, async mrCreate(input: GitLabMergeRequestCreateInput): Promise { const response = await runtimeFetch('/api/gitlab/mrs/create', { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body: JSON.stringify(input), }); const payload = await jsonOrNull(response); if (!response.ok || !payload?.mr) { throw new Error(payload?.error || response.statusText || 'Failed to create GitLab merge request'); } return payload.mr; }, async mrUpdate(input: GitLabMergeRequestUpdateInput): Promise { const response = await runtimeFetch('/api/gitlab/mrs/update', { method: 'PUT', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body: JSON.stringify(input), }); const payload = await jsonOrNull(response); if (!response.ok || !payload?.mr) { throw new Error(payload?.error || response.statusText || 'Failed to update GitLab merge request'); } return payload.mr; }, async mrMerge(input: GitLabMergeRequestMergeInput): Promise { const response = await runtimeFetch('/api/gitlab/mrs/merge', { method: 'PUT', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body: JSON.stringify(input), }); const payload = await jsonOrNull(response); // The server rejects non-mergeable requests with 405/409/422 and a // `{ connected, merged: false, message }` body — parse it and return it // instead of throwing. Only throw when there is no parseable payload // (network failure) or the server surfaced a real `{ error }`. if (!payload) { throw new Error(response.statusText || 'Failed to merge GitLab merge request'); } if (payload.error) { throw new Error(payload.error); } return { connected: Boolean(payload.connected), merged: Boolean(payload.merged), ...(payload.message ? { message: payload.message } : {}), }; }, async issueComment(input: GitLabIssueCommentInput): Promise { const response = await runtimeFetch('/api/gitlab/issues/comment', { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body: JSON.stringify(input), }); const body = await jsonOrNull(response); if (!response.ok || !body) { throw new Error(body?.error || response.statusText || 'Failed to post GitLab issue comment'); } return body; }, async issueCreate(input: GitLabIssueCreateInput): Promise { const response = await runtimeFetch('/api/gitlab/issues/create', { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body: JSON.stringify(input), }); const body = await jsonOrNull(response); if (!response.ok || !body) { throw new Error(body?.error || response.statusText || 'Failed to create GitLab issue'); } return body; }, async issueUpdate(input: GitLabIssueUpdateInput): Promise { const response = await runtimeFetch('/api/gitlab/issues/update', { method: 'PUT', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body: JSON.stringify(input), }); const body = await jsonOrNull(response); if (!response.ok || !body) { throw new Error(body?.error || response.statusText || 'Failed to update GitLab issue'); } return body; }, async mrComment(input: GitLabMrNoteInput): Promise { const response = await runtimeFetch('/api/gitlab/mrs/comment', { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body: JSON.stringify(input), }); const body = await jsonOrNull(response); if (!response.ok || !body) { throw new Error(body?.error || response.statusText || 'Failed to post GitLab merge request comment'); } return body; }, async mrApprove(input: GitLabMrApproveInput): Promise { const response = await runtimeFetch('/api/gitlab/mrs/approve', { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body: JSON.stringify(input), }); const body = await jsonOrNull(response); if (!response.ok || !body) { throw new Error(body?.error || response.statusText || 'Failed to approve GitLab merge request'); } return body; }, async repoBranches(namespace: string, project: string): Promise { const response = await runtimeFetch( `/api/gitlab/repo/branches?namespace=${encodeURIComponent(namespace)}&project=${encodeURIComponent(project)}`, { method: 'GET', headers: { Accept: 'application/json' } } ); const body = await jsonOrNull(response); if (!response.ok || !body) { throw new Error(body?.error || response.statusText || 'Failed to fetch GitLab repo branches'); } return { branches: body.branches ?? [], defaultBranch: body.defaultBranch ?? null, }; }, });