Skip to content

Commit cee455f

Browse files
chore(database-ui): cleanup responses and router code
1 parent 75d0e5d commit cee455f

File tree

5 files changed

+175
-87
lines changed

5 files changed

+175
-87
lines changed

packages/cli/src/db-studio/api/http/responses.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
import type { ApiResponse } from "./types.ts";
22

3+
const SUCCESS_STATUS = 200;
4+
const ERROR_STATUS = 500;
5+
const NOT_FOUND_STATUS = 404;
6+
37
/**
48
* Create a JSON response
59
*/
6-
export function jsonResponse(data: unknown, status: number = 200): ApiResponse {
10+
export function jsonResponse(data: unknown, status: number = SUCCESS_STATUS): ApiResponse {
711
console.log("returning json response", data);
812
return {
913
status,
@@ -15,7 +19,7 @@ export function jsonResponse(data: unknown, status: number = 200): ApiResponse {
1519
/**
1620
* Create an error response
1721
*/
18-
export function errorResponse(message: string, status: number = 500): ApiResponse {
22+
export function errorResponse(message: string, status: number = ERROR_STATUS): ApiResponse {
1923
return jsonResponse({ error: message }, status);
2024
}
2125

@@ -24,6 +28,12 @@ export function errorResponse(message: string, status: number = 500): ApiRespons
2428
*/
2529
export function handleDatabaseError(err: unknown, operation: string): ApiResponse {
2630
const errorMessage = err instanceof Error ? err.message : "Unknown error";
27-
return errorResponse(`Failed to ${operation}: ${errorMessage}`, 500);
31+
return errorResponse(`Failed to ${operation}: ${errorMessage}`, ERROR_STATUS);
2832
}
2933

34+
/**
35+
* Creates a 404 Not Found response
36+
*/
37+
export function notFoundResponse(): ApiResponse {
38+
return jsonResponse({ error: "Not Found" }, NOT_FOUND_STATUS);
39+
}

packages/cli/src/db-studio/api/http/router.ts

Lines changed: 83 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,95 @@
11
/**
2-
* Match a route pattern against a path and extract parameters
2+
* Represents a captured route parameter
3+
*/
4+
type RouteParameter = {
5+
name: string;
6+
value: string;
7+
};
8+
9+
/**
10+
* Result of matching a single path segment
11+
*/
12+
type SegmentMatchResult =
13+
| { matched: true; parameter: RouteParameter | null }
14+
| { matched: false };
15+
16+
/**
17+
* Splits a URL path into segments, filtering out empty strings
18+
*/
19+
function splitPathIntoSegments(path: string): string[] {
20+
return path.split("/").filter(segment => segment.length > 0);
21+
}
22+
23+
/**
24+
* Checks if a pattern segment is a parameter (starts with ':')
25+
*/
26+
function isParameterSegment(segment: string): boolean {
27+
return segment.startsWith(":");
28+
}
29+
30+
/**
31+
* Extracts the parameter name from a pattern segment (removes the ':' prefix)
32+
*/
33+
function extractParameterName(segment: string): string {
34+
return segment.slice(1);
35+
}
36+
37+
/**
38+
* Matches a single pattern segment against a path segment
39+
* Returns match result indicating success and any captured parameter
40+
*/
41+
function matchPathSegment(
42+
patternSegment: string,
43+
pathSegment: string
44+
): SegmentMatchResult {
45+
if (isParameterSegment(patternSegment)) {
46+
return {
47+
matched: true,
48+
parameter: {
49+
name: extractParameterName(patternSegment),
50+
value: pathSegment
51+
}
52+
};
53+
}
54+
55+
// Literal segment - must match exactly
56+
if (patternSegment === pathSegment) {
57+
return { matched: true, parameter: null };
58+
}
59+
60+
return { matched: false };
61+
}
62+
63+
/**
64+
* Matches a route pattern against a path and extracts parameters
65+
*
66+
* Pattern segments starting with ':' are treated as parameters.
67+
*
68+
* @example
69+
* matchRoute("/api/:dbIndex/query", "/api/0/query")
70+
* // Returns: { dbIndex: "0" }
371
*/
472
export function matchRoute(pattern: string, path: string): Record<string, string> | null {
5-
const patternParts = pattern.split("/").filter(p => p);
6-
const pathParts = path.split("/").filter(p => p);
73+
const patternSegments = splitPathIntoSegments(pattern);
74+
const pathSegments = splitPathIntoSegments(path);
775

8-
if (patternParts.length !== pathParts.length) return null;
76+
// Paths must have the same number of segments to match
77+
if (patternSegments.length !== pathSegments.length) {
78+
return null;
79+
}
980

1081
const params: Record<string, string> = {};
1182

12-
for (let i = 0; i < patternParts.length; i++) {
13-
const patternPart = patternParts[i];
14-
const pathPart = pathParts[i];
15-
16-
if (patternPart.startsWith(":")) {
17-
// Parameter segment
18-
const paramName = patternPart.slice(1);
19-
params[paramName] = pathPart;
20-
} else if (patternPart !== pathPart) {
21-
// Literal segment doesn't match
83+
for (let i = 0; i < patternSegments.length; i++) {
84+
const result = matchPathSegment(patternSegments[i], pathSegments[i]);
85+
86+
if (!result.matched) {
2287
return null;
2388
}
89+
90+
if (result.parameter) {
91+
params[result.parameter.name] = result.parameter.value;
92+
}
2493
}
2594

2695
return params;

packages/cli/src/db-studio/api/http/server.ts

Lines changed: 34 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,38 @@ import type { DiscoveredDatabase } from "../database.ts";
22
import type { ApiResponse, RouteContext, DatabaseConstructor } from "./types.ts";
33
import { matchRoute } from "./router.ts";
44
import { routes } from "../routes/index.ts";
5-
import { errorResponse } from "./responses.ts";
5+
import { errorResponse, notFoundResponse } from "./responses.ts";
66

77
/**
8-
* Handle API requests using Node.js primitives
8+
* Extracts the path from a URL, removing query strings
9+
*/
10+
function parseUrlPath(url: string): string {
11+
return url.split('?')[0];
12+
}
13+
14+
/**
15+
* Attempts to find and execute a matching route handler
16+
*/
17+
async function executeMatchingRoute(
18+
path: string,
19+
method: string,
20+
body: string,
21+
context: RouteContext
22+
): Promise<ApiResponse | null> {
23+
for (const route of routes) {
24+
if (route.method !== method) continue;
25+
26+
const params = matchRoute(route.pattern, path);
27+
if (params) {
28+
return await route.handler(params, context, body);
29+
}
30+
}
31+
32+
return null;
33+
}
34+
35+
/**
36+
* Main API request handler - routes requests to appropriate handlers
937
*/
1038
export async function handleApiRequest(
1139
url: string,
@@ -14,29 +42,17 @@ export async function handleApiRequest(
1442
databases: DiscoveredDatabase[],
1543
Database: DatabaseConstructor
1644
): Promise<ApiResponse> {
17-
// Parse URL path (remove query string if present)
18-
const path = url.split('?')[0];
19-
2045
try {
21-
// Try to match against registered routes
46+
const path = parseUrlPath(url);
2247
const context: RouteContext = { databases, Database };
2348

24-
for (const route of routes) {
25-
if (route.method !== method) continue;
26-
27-
const params = matchRoute(route.pattern, path);
28-
if (params) {
29-
return await route.handler(params, context, body);
30-
}
31-
}
32-
33-
// No route matched - return 404
34-
return { status: 404, headers: {}, body: '' };
49+
const response = await executeMatchingRoute(path, method, body, context);
50+
return response ?? notFoundResponse();
3551

3652
} catch (err) {
3753
const errorMessage = err instanceof Error ? err.message : "Unknown error";
3854
console.error("Error handling request:", err);
39-
return errorResponse(errorMessage, 500);
55+
return errorResponse(errorMessage);
4056
}
4157
}
4258

packages/cli/src/db-studio/api/index.ts

Lines changed: 45 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
import { createServer } from "http";
1+
import { createServer, IncomingMessage, ServerResponse } from "http";
22
import { Database } from "elide:sqlite";
33
import { handleApiRequest } from "./http/server.ts";
4+
import { errorResponse } from "./http/responses.ts";
5+
import type { ApiResponse } from "./http/types.ts";
46
import config from "./config.ts";
57

68
/**
@@ -13,50 +15,53 @@ export { Database };
1315

1416
const { port, databases } = config;
1517

16-
// Server options with self-signed certificate for HTTPS/HTTP3
17-
const certificate = {
18-
kind: 'selfSigned',
19-
subject: 'localhost'
20-
};
21-
22-
const options = {
23-
elide: {
24-
https: { certificate },
25-
http3: { certificate },
26-
}
27-
};
28-
29-
// Create HTTP server
30-
const server = createServer(options, (req, res) => {
31-
let body = '';
18+
/**
19+
* Parses the request body from an incoming HTTP request
20+
*/
21+
function parseRequestBody(req: IncomingMessage): Promise<string> {
22+
return new Promise((resolve, reject) => {
23+
let body = '';
24+
25+
req.on('data', (chunk) => {
26+
body += chunk.toString('utf8');
27+
});
28+
29+
req.on('end', () => resolve(body));
30+
req.on('error', (err) => reject(err));
31+
});
32+
}
3233

33-
req.on('data', (chunk) => {
34-
body += chunk.toString('utf8');
34+
/**
35+
* Writes an ApiResponse to the HTTP ServerResponse
36+
*/
37+
function writeResponse(res: ServerResponse, response: ApiResponse): void {
38+
res.writeHead(response.status, {
39+
...response.headers,
40+
'Content-Length': Buffer.byteLength(response.body, 'utf8')
3541
});
42+
res.end(response.body);
43+
}
3644

37-
req.on('end', async () => {
38-
try {
39-
const url = req.url || '/';
40-
const method = req.method || 'GET';
45+
/**
46+
* Main request handler - processes incoming HTTP requests
47+
*/
48+
async function handleRequest(req: IncomingMessage, res: ServerResponse): Promise<void> {
49+
try {
50+
const body = await parseRequestBody(req);
51+
const url = req.url || '/';
52+
const method = req.method || 'GET';
4153

42-
const response = await handleApiRequest(url, method, body, databases, Database);
54+
const response = await handleApiRequest(url, method, body, databases, Database);
55+
writeResponse(res, response);
56+
} catch (err) {
57+
console.error("Error handling request:", err);
58+
const response = errorResponse('Internal server error', 500);
59+
writeResponse(res, response);
60+
}
61+
}
4362

44-
res.writeHead(response.status, {
45-
...response.headers,
46-
'Content-Length': Buffer.byteLength(response.body, 'utf8')
47-
});
48-
res.end(response.body);
49-
} catch (err) {
50-
console.error("Error handling request:", err);
51-
const errorBody = JSON.stringify({ error: 'Internal server error' });
52-
res.writeHead(500, {
53-
'Content-Type': 'application/json',
54-
'Content-Length': Buffer.byteLength(errorBody, 'utf8')
55-
});
56-
res.end(errorBody);
57-
}
58-
});
59-
});
63+
// Create and configure HTTP server
64+
const server = createServer(handleRequest);
6065

6166
// Start listening on configured port
6267
server.listen(port, () => {

packages/cli/src/db-studio/api/index2.ts

Lines changed: 0 additions & 12 deletions
This file was deleted.

0 commit comments

Comments
 (0)