Skip to content

Commit 17a49a9

Browse files
committed
Add OpenAPI docs for Sourcebot public API
Generate and publish OpenAPI docs for the public search, repo, and file-browsing endpoints, add Mintlify integration, and serve the generated spec at /api/openapi.json. Note: /api/stream_search remains modeled as text/event-stream in OpenAPI. OpenAPI 3.0 does not provide a first-class way to describe SSE frames as a typed stream of JSON events.
1 parent 1ce1ec8 commit 17a49a9

File tree

16 files changed

+1617
-50
lines changed

16 files changed

+1617
-50
lines changed

docs/api-reference/sourcebot-public.openapi.json

Lines changed: 1111 additions & 0 deletions
Large diffs are not rendered by default.

docs/docs.json

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,17 @@
6666
}
6767
]
6868
},
69+
{
70+
"group": "API Reference",
71+
"pages": [
72+
"docs/api-reference/overview",
73+
{
74+
"group": "Public API",
75+
"openapi": "api-reference/sourcebot-public.openapi.json",
76+
"directory": "docs/api-reference/public-api"
77+
}
78+
]
79+
},
6980
{
7081
"group": "Configuration",
7182
"pages": [
@@ -160,4 +171,4 @@
160171
"default": "dark",
161172
"strict": false
162173
}
163-
}
174+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
---
2+
title: API Reference
3+
sidebarTitle: API Reference
4+
---
5+
6+
Sourcebot exposes a public REST API for code search, repository listing, and file browsing.
7+
8+
The endpoint reference in this section is generated from the web app's Zod schemas and OpenAPI registry, then rendered by Mintlify from [`api-reference/sourcebot-public.openapi.json`](/api-reference/sourcebot-public.openapi.json).
9+
10+
The first documented surface includes:
11+
12+
- `/api/search`
13+
- `/api/stream_search`
14+
- `/api/repos`
15+
- `/api/version`
16+
- `/api/source`
17+
- `/api/tree`
18+
- `/api/files`
19+
20+
To refresh the spec after changing those contracts:
21+
22+
```bash
23+
yarn openapi:generate
24+
```

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"dev:prisma:migrate:reset": "yarn with-env yarn workspace @sourcebot/db prisma:migrate:reset",
2020
"dev:prisma:db:push": "yarn with-env yarn workspace @sourcebot/db prisma:db:push",
2121
"build:deps": "yarn workspaces foreach --recursive --topological --from '{@sourcebot/schemas,@sourcebot/db,@sourcebot/shared,@sourcebot/query-language}' run build",
22+
"openapi:generate": "yarn workspace @sourcebot/web openapi:generate",
2223
"tool:decrypt-jwe": "yarn with-env yarn workspace @sourcebot/web tool:decrypt-jwe"
2324
},
2425
"devDependencies": {

packages/backend/src/api.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,14 @@ import { createLogger, env, hasEntitlement, PERMISSION_SYNC_SUPPORTED_IDENTITY_P
33
import express, { Request, Response } from 'express';
44
import 'express-async-errors';
55
import * as http from "http";
6-
import z from 'zod';
76
import { ConnectionManager } from './connectionManager.js';
87
import { AccountPermissionSyncer } from './ee/accountPermissionSyncer.js';
98
import { PromClient } from './promClient.js';
109
import { RepoIndexManager } from './repoIndexManager.js';
1110
import { createGitHubRepoRecord } from './repoCompileUtils.js';
1211
import { Octokit } from '@octokit/rest';
1312
import { SINGLE_TENANT_ORG_ID } from './constants.js';
13+
import z from 'zod';
1414

1515
const logger = createLogger('api');
1616
const PORT = 3060;
@@ -183,4 +183,4 @@ export class Api {
183183
});
184184
});
185185
}
186-
}
186+
}

packages/web/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"start": "next start",
99
"lint": "cross-env SKIP_ENV_VALIDATION=1 eslint .",
1010
"test": "cross-env SKIP_ENV_VALIDATION=1 vitest",
11+
"openapi:generate": "tsx tools/generateOpenApi.ts",
1112
"generate:protos": "proto-loader-gen-types --includeComments --longs=Number --enums=String --defaults --oneofs --grpcLib=@grpc/grpc-js --keepCase --includeDirs=../../vendor/zoekt/grpc/protos --outDir=src/proto zoekt/webserver/v1/webserver.proto zoekt/webserver/v1/query.proto",
1213
"dev:emails": "email dev --dir ./src/emails",
1314
"stripe:listen": "stripe listen --forward-to http://localhost:3000/api/stripe",
@@ -194,6 +195,7 @@
194195
"zod-to-json-schema": "^3.24.5"
195196
},
196197
"devDependencies": {
198+
"@asteasolutions/zod-to-openapi": "7.3.4",
197199
"@eslint/eslintrc": "^3",
198200
"@react-email/preview-server": "5.2.8",
199201
"@react-grab/mcp": "^0.1.23",
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import fs from 'node:fs/promises';
2+
import path from 'node:path';
3+
import { apiHandler } from '@/lib/apiHandler';
4+
5+
export const dynamic = 'force-dynamic';
6+
7+
const openApiPathCandidates = [
8+
path.resolve(process.cwd(), 'docs/api-reference/sourcebot-public.openapi.json'),
9+
path.resolve(process.cwd(), '../docs/api-reference/sourcebot-public.openapi.json'),
10+
path.resolve(process.cwd(), '../../docs/api-reference/sourcebot-public.openapi.json'),
11+
];
12+
13+
async function loadOpenApiDocument() {
14+
for (const candidate of openApiPathCandidates) {
15+
try {
16+
return JSON.parse(await fs.readFile(candidate, 'utf8'));
17+
} catch (error) {
18+
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
19+
throw error;
20+
}
21+
}
22+
}
23+
24+
throw new Error('OpenAPI spec file not found');
25+
}
26+
27+
export const GET = apiHandler(async () => {
28+
const document = await loadOpenApiDocument();
29+
30+
return Response.json(document, {
31+
headers: {
32+
'Content-Type': 'application/vnd.oai.openapi+json;version=3.0.3',
33+
},
34+
});
35+
}, { track: false });

packages/web/src/app/api/(server)/source/route.ts

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,20 @@
11
'use server';
22

33
import { getFileSource } from '@/features/git';
4+
import { fileSourceRequestSchema } from '@/features/git/schemas';
45
import { apiHandler } from "@/lib/apiHandler";
56
import { queryParamsSchemaValidationError, serviceErrorResponse } from "@/lib/serviceError";
67
import { isServiceError } from "@/lib/utils";
78
import { NextRequest } from "next/server";
8-
import { z } from "zod";
9-
10-
const querySchema = z.object({
11-
repo: z.string(),
12-
path: z.string(),
13-
ref: z.string().optional(),
14-
});
159

1610
export const GET = apiHandler(async (request: NextRequest) => {
1711
const rawParams = Object.fromEntries(
18-
Object.keys(querySchema.shape).map(key => [
12+
Object.keys(fileSourceRequestSchema.shape).map(key => [
1913
key,
2014
request.nextUrl.searchParams.get(key) ?? undefined
2115
])
2216
);
23-
const parsed = querySchema.safeParse(rawParams);
17+
const parsed = fileSourceRequestSchema.safeParse(rawParams);
2418

2519
if (!parsed.success) {
2620
return serviceErrorResponse(

packages/web/src/features/git/getFileSourceApi.ts

Lines changed: 3 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,28 +9,12 @@ import { withOptionalAuthV2 } from '@/withAuthV2';
99
import { getRepoPath } from '@sourcebot/shared';
1010
import { headers } from 'next/headers';
1111
import simpleGit from 'simple-git';
12-
import z from 'zod';
12+
import type z from 'zod';
1313
import { isGitRefValid, isPathValid } from './utils';
14-
import { CodeHostType } from '@sourcebot/db';
14+
import { fileSourceRequestSchema, fileSourceResponseSchema } from './schemas';
1515

16-
export const fileSourceRequestSchema = z.object({
17-
path: z.string(),
18-
repo: z.string(),
19-
ref: z.string().optional(),
20-
});
16+
export { fileSourceRequestSchema, fileSourceResponseSchema } from './schemas';
2117
export type FileSourceRequest = z.infer<typeof fileSourceRequestSchema>;
22-
23-
export const fileSourceResponseSchema = z.object({
24-
source: z.string(),
25-
language: z.string(),
26-
path: z.string(),
27-
repo: z.string(),
28-
repoCodeHostType: z.nativeEnum(CodeHostType),
29-
repoDisplayName: z.string().optional(),
30-
repoExternalWebUrl: z.string().optional(),
31-
webUrl: z.string(),
32-
externalWebUrl: z.string().optional(),
33-
});
3418
export type FileSourceResponse = z.infer<typeof fileSourceResponseSchema>;
3519

3620
export const getFileSource = async ({ path: filePath, repo: repoName, ref }: FileSourceRequest, { source }: { source?: string } = {}): Promise<FileSourceResponse | ServiceError> => sew(() => withOptionalAuthV2(async ({ org, prisma, user }) => {

packages/web/src/features/git/getFilesApi.ts

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,15 @@
11
import { sew } from '@/actions';
2-
import { FileTreeItem, fileTreeItemSchema } from "./types";
2+
import { FileTreeItem } from "./types";
33
import { notFound, ServiceError, unexpectedError } from '@/lib/serviceError';
44
import { withOptionalAuthV2 } from "@/withAuthV2";
55
import { getRepoPath } from '@sourcebot/shared';
66
import simpleGit from 'simple-git';
7-
import z from 'zod';
7+
import type z from 'zod';
8+
import { getFilesRequestSchema, getFilesResponseSchema } from './schemas';
89
import { logger } from './utils';
910

10-
export const getFilesRequestSchema = z.object({
11-
repoName: z.string(),
12-
revisionName: z.string(),
13-
});
11+
export { getFilesRequestSchema, getFilesResponseSchema } from './schemas';
1412
export type GetFilesRequest = z.infer<typeof getFilesRequestSchema>;
15-
16-
export const getFilesResponseSchema = z.array(fileTreeItemSchema);
1713
export type GetFilesResponse = z.infer<typeof getFilesResponseSchema>;
1814

1915
export const getFiles = async ({ repoName, revisionName }: GetFilesRequest): Promise<GetFilesResponse | ServiceError> => sew(() =>

0 commit comments

Comments
 (0)