@@ -4,31 +4,25 @@ import type { Database } from "elide:sqlite";
44import type { DiscoveredDatabase } from "./database.ts" ;
55import { getDatabaseInfo , getTables , getTableData } from "./database.ts" ;
66
7- /**
8- * HTTP Server Layer - JSON API Handler
9- *
10- * Provides RESTful JSON API endpoints for database operations.
11- * Used by the imperative Node.js HTTP server in index.tsx.
12- * The React UI is served separately via a static file server.
13- */
14-
157type ApiResponse = {
168 status : number ;
179 headers : Record < string , string > ;
1810 body : string ;
1911} ;
2012
21- /**
22- * Get database by index from the discovered list
23- */
24- function getDatabaseByIndex ( databases : DiscoveredDatabase [ ] , index : number ) : DiscoveredDatabase | null {
25- if ( index < 0 || index >= databases . length ) return null ;
26- return databases [ index ] ;
27- }
13+ type RouteContext = {
14+ databases : DiscoveredDatabase [ ] ;
15+ Database : typeof Database ;
16+ } ;
17+
18+ type RouteHandler = ( params : Record < string , string > , context : RouteContext ) => Promise < ApiResponse > ;
19+
20+ type Route = {
21+ method : string ;
22+ pattern : string ;
23+ handler : RouteHandler ;
24+ } ;
2825
29- /**
30- * Helper to create JSON response
31- */
3226function jsonResponse ( data : unknown , status : number = 200 ) : ApiResponse {
3327 return {
3428 status,
@@ -37,17 +31,27 @@ function jsonResponse(data: unknown, status: number = 200): ApiResponse {
3731 } ;
3832}
3933
40- /**
41- * Helper to create error response
42- */
4334function errorResponse ( message : string , status : number = 500 ) : ApiResponse {
4435 return jsonResponse ( { error : message } , status ) ;
4536}
4637
47- /**
48- * Match a route pattern with path parameters
49- * Returns null if no match, otherwise returns extracted parameters
50- */
38+ function validateAndGetDatabase (
39+ dbIndexStr : string ,
40+ databases : DiscoveredDatabase [ ]
41+ ) : { database : DiscoveredDatabase } | { error : ApiResponse } {
42+ const dbIndex = parseInt ( dbIndexStr , 10 ) ;
43+
44+ if ( isNaN ( dbIndex ) ) {
45+ return { error : errorResponse ( "Invalid database index" , 400 ) } ;
46+ }
47+
48+ if ( dbIndex < 0 || dbIndex >= databases . length ) {
49+ return { error : errorResponse ( `Database not found at index ${ dbIndex } ` , 404 ) } ;
50+ }
51+
52+ return { database : databases [ dbIndex ] } ;
53+ }
54+
5155function matchRoute ( pattern : string , path : string ) : Record < string , string > | null {
5256 const patternParts = pattern . split ( "/" ) . filter ( p => p ) ;
5357 const pathParts = path . split ( "/" ) . filter ( p => p ) ;
@@ -73,6 +77,70 @@ function matchRoute(pattern: string, path: string): Record<string, string> | nul
7377 return params ;
7478}
7579
80+ async function healthCheck ( ) : Promise < ApiResponse > {
81+ return jsonResponse ( { status : "ok" } ) ;
82+ }
83+
84+ async function listDatabases ( _params : Record < string , string > , context : RouteContext ) : Promise < ApiResponse > {
85+ return jsonResponse ( { databases : context . databases } ) ;
86+ }
87+
88+ async function getDatabaseInfoRoute ( params : Record < string , string > , context : RouteContext ) : Promise < ApiResponse > {
89+ const result = validateAndGetDatabase ( params . dbIndex , context . databases ) ;
90+ if ( "error" in result ) return result . error ;
91+
92+ const { database } = result ;
93+ const db = new context . Database ( database . path ) ;
94+ const info = getDatabaseInfo ( db , database . path ) ;
95+
96+ const fullInfo = {
97+ ...info ,
98+ size : database . size ,
99+ lastModified : database . lastModified ,
100+ isLocal : database . isLocal ,
101+ tableCount : info . tableCount ,
102+ } ;
103+
104+ return jsonResponse ( fullInfo ) ;
105+ }
106+
107+ async function getTablesRoute ( params : Record < string , string > , context : RouteContext ) : Promise < ApiResponse > {
108+ const result = validateAndGetDatabase ( params . dbIndex , context . databases ) ;
109+ if ( "error" in result ) return result . error ;
110+
111+ const { database } = result ;
112+ const db = new context . Database ( database . path ) ;
113+ const tables = getTables ( db ) ;
114+
115+ return jsonResponse ( { tables } ) ;
116+ }
117+
118+ async function getTableDataRoute ( params : Record < string , string > , context : RouteContext ) : Promise < ApiResponse > {
119+ const result = validateAndGetDatabase ( params . dbIndex , context . databases ) ;
120+ if ( "error" in result ) return result . error ;
121+
122+ const tableName = params . tableName ;
123+ if ( ! tableName ) {
124+ return errorResponse ( "Table name is required" , 400 ) ;
125+ }
126+
127+ const { database } = result ;
128+ const db = new context . Database ( database . path ) ;
129+ const tableData = getTableData ( db , tableName ) ;
130+
131+ return jsonResponse ( tableData ) ;
132+ }
133+
134+ /**
135+ * Route Registry
136+ */
137+ const routes : Route [ ] = [
138+ { method : "GET" , pattern : "/api/databases" , handler : listDatabases } ,
139+ { method : "GET" , pattern : "/api/databases/:dbIndex" , handler : getDatabaseInfoRoute } ,
140+ { method : "GET" , pattern : "/api/databases/:dbIndex/tables" , handler : getTablesRoute } ,
141+ { method : "GET" , pattern : "/api/databases/:dbIndex/tables/:tableName" , handler : getTableDataRoute } ,
142+ ] ;
143+
76144/**
77145 * Handle API requests using Node.js primitives
78146 */
@@ -87,100 +155,24 @@ export async function handleApiRequest(
87155 const path = url . split ( '?' ) [ 0 ] ;
88156
89157 try {
90- // ============================================================================
91- // Route: GET /health
92- // ============================================================================
158+ // Special case: health check endpoint (no params needed)
93159 if ( method === "GET" && path === "/health" ) {
94- return jsonResponse ( { status : "ok" } ) ;
95- }
96-
97- // ============================================================================
98- // Route: GET /api/databases
99- // ============================================================================
100- if ( method === "GET" && path === "/api/databases" ) {
101- return jsonResponse ( { databases } ) ;
102- }
103-
104- // ============================================================================
105- // Route: GET /api/databases/:dbIndex
106- // ============================================================================
107- const dbInfoMatch = matchRoute ( "/api/databases/:dbIndex" , path ) ;
108- if ( method === "GET" && dbInfoMatch ) {
109- const dbIndex = parseInt ( dbInfoMatch . dbIndex , 10 ) ;
110-
111- if ( isNaN ( dbIndex ) ) {
112- return errorResponse ( "Invalid database index" , 400 ) ;
113- }
114-
115- const database = getDatabaseByIndex ( databases , dbIndex ) ;
116- if ( ! database ) {
117- return errorResponse ( `Database not found at index ${ dbIndex } ` , 404 ) ;
118- }
119-
120- const db = new Database ( database . path ) ;
121- const info = getDatabaseInfo ( db , database . path ) ;
122-
123- const fullInfo = {
124- ...info ,
125- size : database . size ,
126- lastModified : database . lastModified ,
127- isLocal : database . isLocal ,
128- tableCount : info . tableCount ,
129- } ;
130-
131- return jsonResponse ( fullInfo ) ;
160+ return healthCheck ( ) ;
132161 }
133162
134- // ============================================================================
135- // Route: GET /api/databases/:dbIndex/tables
136- // ============================================================================
137- const tablesMatch = matchRoute ( "/api/databases/:dbIndex/tables" , path ) ;
138- if ( method === "GET" && tablesMatch ) {
139- const dbIndex = parseInt ( tablesMatch . dbIndex , 10 ) ;
163+ // Try to match against registered routes
164+ const context : RouteContext = { databases, Database } ;
140165
141- if ( isNaN ( dbIndex ) ) {
142- return errorResponse ( "Invalid database index" , 400 ) ;
143- }
166+ for ( const route of routes ) {
167+ if ( route . method !== method ) continue ;
144168
145- const database = getDatabaseByIndex ( databases , dbIndex ) ;
146- if ( ! database ) {
147- return errorResponse ( `Database not found at index ${ dbIndex } ` , 404 ) ;
169+ const params = matchRoute ( route . pattern , path ) ;
170+ if ( params ) {
171+ return await route . handler ( params , context ) ;
148172 }
149-
150- const db = new Database ( database . path ) ;
151- const tables = getTables ( db ) ;
152- return jsonResponse ( { tables } ) ;
153- }
154-
155- // ============================================================================
156- // Route: GET /api/databases/:dbIndex/tables/:tableName
157- // ============================================================================
158- const tableDataMatch = matchRoute ( "/api/databases/:dbIndex/tables/:tableName" , path ) ;
159- if ( method === "GET" && tableDataMatch ) {
160- const dbIndex = parseInt ( tableDataMatch . dbIndex , 10 ) ;
161-
162- if ( isNaN ( dbIndex ) ) {
163- return errorResponse ( "Invalid database index" , 400 ) ;
164- }
165-
166- const database = getDatabaseByIndex ( databases , dbIndex ) ;
167- if ( ! database ) {
168- return errorResponse ( `Database not found at index ${ dbIndex } ` , 404 ) ;
169- }
170-
171- const tableName = tableDataMatch . tableName ;
172- if ( ! tableName ) {
173- return errorResponse ( "Table name is required" , 400 ) ;
174- }
175-
176- const db = new Database ( database . path ) ;
177- const tableData = getTableData ( db , tableName ) ;
178- return jsonResponse ( tableData ) ;
179173 }
180174
181- // ============================================================================
182175 // No route matched - return 404
183- // ============================================================================
184176 return { status : 404 , headers : { } , body : '' } ;
185177
186178 } catch ( err ) {
0 commit comments