Auto-indexing service and GraphQL API for AT Protocol Records
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

refactor: reorganize HTTP handlers into dedicated handlers/ directory

- Create handlers/ directory with 9 handler modules
- Move all HTTP/WebSocket handlers to handlers/ with consistent naming
- Drop _handler suffix from module names for cleaner imports
- Extract inline handlers (health, backfill, index) from server.gleam
- Simplify server.gleam to thin routing layer (730→618 lines, 15% reduction)
- Update all imports in server.gleam and test files
- Keep oauth/ separate (OAuth-specific handlers)
- Keep event_handler.gleam in src/ (domain logic, not HTTP handler)

Handler modules:
- handlers/backfill.gleam - Backfill trigger endpoint
- handlers/client_graphql.gleam - Admin GraphQL API
- handlers/graphiql.gleam - GraphiQL interactive interface
- handlers/graphql_ws.gleam - GraphQL WebSocket subscriptions
- handlers/graphql.gleam - GraphQL HTTP endpoint
- handlers/health.gleam - Health check endpoint
- handlers/index.gleam - SPA index and fallback routing
- handlers/settings.gleam - Settings management
- handlers/upload.gleam - File upload handler

All 179 tests passing. Clean build.

+892 -129
+728
docs/plans/2025-11-24-handler-reorganization.md
··· 1 + # Handler Reorganization Implementation Plan 2 + 3 + > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. 4 + 5 + **Goal:** Reorganize all HTTP/WebSocket handlers into a dedicated `handlers/` directory with consistent naming (no `_handler` suffix) to improve code organization and discoverability. 6 + 7 + **Architecture:** Extract inline route handlers from `server.gleam` into separate modules, move existing `*_handler.gleam` files to `handlers/` directory with renamed modules (dropping `_handler` suffix), and update all imports. Keep `oauth/` as a separate directory for OAuth-related functionality. 8 + 9 + **Tech Stack:** Gleam, Wisp web framework, Mist HTTP server 10 + 11 + --- 12 + 13 + ### Task 1: Create handlers directory and health handler 14 + 15 + **Files:** 16 + - Create: `/Users/chadmiller/code/quickslice/server/src/handlers/health.gleam` 17 + 18 + **Step 1: Create handlers directory** 19 + 20 + Run: `mkdir -p /Users/chadmiller/code/quickslice/server/src/handlers` 21 + Expected: Directory created successfully 22 + 23 + **Step 2: Create health.gleam handler** 24 + 25 + Create `/Users/chadmiller/code/quickslice/server/src/handlers/health.gleam` with: 26 + 27 + ```gleam 28 + /// Health check endpoint handler 29 + /// 30 + /// Handles /health endpoint with database connectivity verification 31 + import database/repositories/lexicons 32 + import sqlight 33 + import wisp 34 + 35 + /// Handle health check request 36 + /// Returns 200 if database is accessible, 503 if not 37 + pub fn handle(db: sqlight.Connection) -> wisp.Response { 38 + // Try a simple database query to verify connectivity 39 + case lexicons.get_count(db) { 40 + Ok(_) -> { 41 + // Database is accessible 42 + wisp.response(200) 43 + |> wisp.set_header("content-type", "application/json") 44 + |> wisp.set_body(wisp.Text("{\"status\": \"healthy\"}")) 45 + } 46 + Error(_) -> { 47 + // Database is not accessible 48 + wisp.response(503) 49 + |> wisp.set_header("content-type", "application/json") 50 + |> wisp.set_body(wisp.Text( 51 + "{\"status\": \"unhealthy\", \"message\": \"Database connection failed\"}", 52 + )) 53 + } 54 + } 55 + } 56 + ``` 57 + 58 + **Step 3: Build to verify syntax** 59 + 60 + Run: `cd /Users/chadmiller/code/quickslice/server && gleam build` 61 + Expected: Build succeeds with no errors 62 + 63 + **Step 4: Commit** 64 + 65 + ```bash 66 + git add server/src/handlers/health.gleam 67 + git commit -m "feat: create health handler module 68 + 69 + Extract health check endpoint from server.gleam into dedicated handler 70 + 71 + 🤖 Generated with [Claude Code](https://claude.com/claude-code) 72 + 73 + Co-Authored-By: Claude <noreply@anthropic.com>" 74 + ``` 75 + 76 + --- 77 + 78 + ### Task 2: Create backfill handler 79 + 80 + **Files:** 81 + - Create: `/Users/chadmiller/code/quickslice/server/src/handlers/backfill.gleam` 82 + 83 + **Step 1: Create backfill.gleam handler** 84 + 85 + Create `/Users/chadmiller/code/quickslice/server/src/handlers/backfill.gleam` with: 86 + 87 + ```gleam 88 + /// Backfill endpoint handler 89 + /// 90 + /// Handles /backfill POST endpoint for triggering collection backfills 91 + import backfill 92 + import config 93 + import database/repositories/lexicons 94 + import gleam/erlang/process 95 + import gleam/http as gleam_http 96 + import gleam/int 97 + import gleam/list 98 + import gleam/option 99 + import sqlight 100 + import wisp 101 + 102 + /// Handle backfill request 103 + /// Only accepts POST method to trigger backfill 104 + pub fn handle( 105 + req: wisp.Request, 106 + db: sqlight.Connection, 107 + config_subject: process.Subject(config.Message), 108 + ) -> wisp.Response { 109 + case req.method { 110 + gleam_http.Post -> { 111 + // Get domain authority from config 112 + let domain_authority = case config.get_domain_authority(config_subject) { 113 + option.Some(authority) -> authority 114 + option.None -> "" 115 + } 116 + 117 + // Get all record-type lexicons 118 + case lexicons.get_record_types(db) { 119 + Ok(lexicons) -> { 120 + case lexicons { 121 + [] -> { 122 + wisp.response(200) 123 + |> wisp.set_header("content-type", "application/json") 124 + |> wisp.set_body(wisp.Text( 125 + "{\"status\": \"no_lexicons\", \"message\": \"No record-type lexicons found\"}", 126 + )) 127 + } 128 + _ -> { 129 + // Separate lexicons by domain authority 130 + let #(collections, external_collections) = 131 + lexicons 132 + |> list.partition(fn(lex) { 133 + backfill.nsid_matches_domain_authority( 134 + lex.id, 135 + domain_authority, 136 + ) 137 + }) 138 + 139 + let collection_ids = list.map(collections, fn(lex) { lex.id }) 140 + let external_collection_ids = 141 + list.map(external_collections, fn(lex) { lex.id }) 142 + 143 + // Run backfill in background process 144 + let backfill_config = backfill.default_config() 145 + process.spawn_unlinked(fn() { 146 + backfill.backfill_collections( 147 + [], 148 + collection_ids, 149 + external_collection_ids, 150 + backfill_config, 151 + db, 152 + ) 153 + }) 154 + 155 + wisp.response(200) 156 + |> wisp.set_header("content-type", "application/json") 157 + |> wisp.set_body(wisp.Text( 158 + "{\"status\": \"started\", \"collections\": " 159 + <> int.to_string(list.length(collection_ids)) 160 + <> ", \"external_collections\": " 161 + <> int.to_string(list.length(external_collection_ids)) 162 + <> "}", 163 + )) 164 + } 165 + } 166 + } 167 + Error(_) -> { 168 + wisp.response(500) 169 + |> wisp.set_header("content-type", "application/json") 170 + |> wisp.set_body(wisp.Text( 171 + "{\"error\": \"database_error\", \"message\": \"Failed to fetch lexicons\"}", 172 + )) 173 + } 174 + } 175 + } 176 + _ -> { 177 + wisp.response(405) 178 + |> wisp.set_header("content-type", "application/json") 179 + |> wisp.set_body(wisp.Text( 180 + "{\"error\": \"method_not_allowed\", \"message\": \"Use POST to trigger backfill\"}", 181 + )) 182 + } 183 + } 184 + } 185 + ``` 186 + 187 + **Step 2: Build to verify syntax** 188 + 189 + Run: `cd /Users/chadmiller/code/quickslice/server && gleam build` 190 + Expected: Build succeeds with no errors 191 + 192 + **Step 3: Commit** 193 + 194 + ```bash 195 + git add server/src/handlers/backfill.gleam 196 + git commit -m "feat: create backfill handler module 197 + 198 + Extract backfill endpoint from server.gleam into dedicated handler 199 + 200 + 🤖 Generated with [Claude Code](https://claude.com/claude-code) 201 + 202 + Co-Authored-By: Claude <noreply@anthropic.com>" 203 + ``` 204 + 205 + --- 206 + 207 + ### Task 3: Create index handler 208 + 209 + **Files:** 210 + - Create: `/Users/chadmiller/code/quickslice/server/src/handlers/index.gleam` 211 + 212 + **Step 1: Create index.gleam handler** 213 + 214 + Create `/Users/chadmiller/code/quickslice/server/src/handlers/index.gleam` with: 215 + 216 + ```gleam 217 + /// Index/SPA handler 218 + /// 219 + /// Serves the client SPA's index.html for root and fallback routes 220 + import simplifile 221 + import wisp 222 + 223 + /// Handle index route and SPA fallback 224 + /// Serves the bundled client application's index.html 225 + pub fn handle() -> wisp.Response { 226 + // Serve the client SPA's index.html (bundled by lustre dev tools) 227 + let assert Ok(priv_dir) = wisp.priv_directory("server") 228 + let index_path = priv_dir <> "/static/index.html" 229 + 230 + case simplifile.read(index_path) { 231 + Ok(contents) -> wisp.html_response(contents, 200) 232 + Error(_) -> 233 + wisp.html_response( 234 + "<h1>Error</h1><p>Client application not found. Run 'gleam run -m lustre/dev build' in the client directory.</p>", 235 + 500, 236 + ) 237 + } 238 + } 239 + ``` 240 + 241 + **Step 2: Build to verify syntax** 242 + 243 + Run: `cd /Users/chadmiller/code/quickslice/server && gleam build` 244 + Expected: Build succeeds with no errors 245 + 246 + **Step 3: Commit** 247 + 248 + ```bash 249 + git add server/src/handlers/index.gleam 250 + git commit -m "feat: create index handler module 251 + 252 + Extract SPA index route from server.gleam into dedicated handler 253 + 254 + 🤖 Generated with [Claude Code](https://claude.com/claude-code) 255 + 256 + Co-Authored-By: Claude <noreply@anthropic.com>" 257 + ``` 258 + 259 + --- 260 + 261 + ### Task 4: Move graphql_handler to handlers/graphql 262 + 263 + **Files:** 264 + - Move: `/Users/chadmiller/code/quickslice/server/src/graphql_handler.gleam` → `/Users/chadmiller/code/quickslice/server/src/handlers/graphql.gleam` 265 + 266 + **Step 1: Move and rename the file** 267 + 268 + Run: `cd /Users/chadmiller/code/quickslice/server && mv src/graphql_handler.gleam src/handlers/graphql.gleam` 269 + Expected: File moved successfully 270 + 271 + **Step 2: Update module name in graphql.gleam** 272 + 273 + The file already has appropriate naming - function is `handle_graphql_request` which is fine. No changes needed to the file contents. 274 + 275 + **Step 3: Build to verify** 276 + 277 + Run: `cd /Users/chadmiller/code/quickslice/server && gleam build` 278 + Expected: Build succeeds 279 + 280 + **Step 4: Commit** 281 + 282 + ```bash 283 + git add server/src/handlers/graphql.gleam 284 + git add server/src/graphql_handler.gleam 285 + git commit -m "refactor: move graphql_handler to handlers/graphql 286 + 287 + Move to organized handlers directory with consistent naming 288 + 289 + 🤖 Generated with [Claude Code](https://claude.com/claude-code) 290 + 291 + Co-Authored-By: Claude <noreply@anthropic.com>" 292 + ``` 293 + 294 + --- 295 + 296 + ### Task 5: Move client_graphql_handler to handlers/client_graphql 297 + 298 + **Files:** 299 + - Move: `/Users/chadmiller/code/quickslice/server/src/client_graphql_handler.gleam` → `/Users/chadmiller/code/quickslice/server/src/handlers/client_graphql.gleam` 300 + 301 + **Step 1: Move and rename the file** 302 + 303 + Run: `cd /Users/chadmiller/code/quickslice/server && mv src/client_graphql_handler.gleam src/handlers/client_graphql.gleam` 304 + Expected: File moved successfully 305 + 306 + **Step 2: Build to verify** 307 + 308 + Run: `cd /Users/chadmiller/code/quickslice/server && gleam build` 309 + Expected: Build succeeds 310 + 311 + **Step 3: Commit** 312 + 313 + ```bash 314 + git add server/src/handlers/client_graphql.gleam 315 + git add server/src/client_graphql_handler.gleam 316 + git commit -m "refactor: move client_graphql_handler to handlers/client_graphql 317 + 318 + Move to organized handlers directory with consistent naming 319 + 320 + 🤖 Generated with [Claude Code](https://claude.com/claude-code) 321 + 322 + Co-Authored-By: Claude <noreply@anthropic.com>" 323 + ``` 324 + 325 + --- 326 + 327 + ### Task 6: Move graphql_ws_handler to handlers/graphql_ws 328 + 329 + **Files:** 330 + - Move: `/Users/chadmiller/code/quickslice/server/src/graphql_ws_handler.gleam` → `/Users/chadmiller/code/quickslice/server/src/handlers/graphql_ws.gleam` 331 + 332 + **Step 1: Move and rename the file** 333 + 334 + Run: `cd /Users/chadmiller/code/quickslice/server && mv src/graphql_ws_handler.gleam src/handlers/graphql_ws.gleam` 335 + Expected: File moved successfully 336 + 337 + **Step 2: Build to verify** 338 + 339 + Run: `cd /Users/chadmiller/code/quickslice/server && gleam build` 340 + Expected: Build succeeds 341 + 342 + **Step 3: Commit** 343 + 344 + ```bash 345 + git add server/src/handlers/graphql_ws.gleam 346 + git add server/src/graphql_ws_handler.gleam 347 + git commit -m "refactor: move graphql_ws_handler to handlers/graphql_ws 348 + 349 + Move to organized handlers directory with consistent naming 350 + 351 + 🤖 Generated with [Claude Code](https://claude.com/claude-code) 352 + 353 + Co-Authored-By: Claude <noreply@anthropic.com>" 354 + ``` 355 + 356 + --- 357 + 358 + ### Task 7: Move graphiql_handler to handlers/graphiql 359 + 360 + **Files:** 361 + - Move: `/Users/chadmiller/code/quickslice/server/src/graphiql_handler.gleam` → `/Users/chadmiller/code/quickslice/server/src/handlers/graphiql.gleam` 362 + 363 + **Step 1: Move and rename the file** 364 + 365 + Run: `cd /Users/chadmiller/code/quickslice/server && mv src/graphiql_handler.gleam src/handlers/graphiql.gleam` 366 + Expected: File moved successfully 367 + 368 + **Step 2: Build to verify** 369 + 370 + Run: `cd /Users/chadmiller/code/quickslice/server && gleam build` 371 + Expected: Build succeeds 372 + 373 + **Step 3: Commit** 374 + 375 + ```bash 376 + git add server/src/handlers/graphiql.gleam 377 + git add server/src/graphiql_handler.gleam 378 + git commit -m "refactor: move graphiql_handler to handlers/graphiql 379 + 380 + Move to organized handlers directory with consistent naming 381 + 382 + 🤖 Generated with [Claude Code](https://claude.com/claude-code) 383 + 384 + Co-Authored-By: Claude <noreply@anthropic.com>" 385 + ``` 386 + 387 + --- 388 + 389 + ### Task 8: Move upload_handler to handlers/upload 390 + 391 + **Files:** 392 + - Move: `/Users/chadmiller/code/quickslice/server/src/upload_handler.gleam` → `/Users/chadmiller/code/quickslice/server/src/handlers/upload.gleam` 393 + 394 + **Step 1: Move and rename the file** 395 + 396 + Run: `cd /Users/chadmiller/code/quickslice/server && mv src/upload_handler.gleam src/handlers/upload.gleam` 397 + Expected: File moved successfully 398 + 399 + **Step 2: Build to verify** 400 + 401 + Run: `cd /Users/chadmiller/code/quickslice/server && gleam build` 402 + Expected: Build succeeds 403 + 404 + **Step 3: Commit** 405 + 406 + ```bash 407 + git add server/src/handlers/upload.gleam 408 + git add server/src/upload_handler.gleam 409 + git commit -m "refactor: move upload_handler to handlers/upload 410 + 411 + Move to organized handlers directory with consistent naming 412 + 413 + 🤖 Generated with [Claude Code](https://claude.com/claude-code) 414 + 415 + Co-Authored-By: Claude <noreply@anthropic.com>" 416 + ``` 417 + 418 + --- 419 + 420 + ### Task 9: Move settings_handler to handlers/settings 421 + 422 + **Files:** 423 + - Move: `/Users/chadmiller/code/quickslice/server/src/settings_handler.gleam` → `/Users/chadmiller/code/quickslice/server/src/handlers/settings.gleam` 424 + 425 + **Step 1: Move and rename the file** 426 + 427 + Run: `cd /Users/chadmiller/code/quickslice/server && mv src/settings_handler.gleam src/handlers/settings.gleam` 428 + Expected: File moved successfully 429 + 430 + **Step 2: Build to verify** 431 + 432 + Run: `cd /Users/chadmiller/code/quickslice/server && gleam build` 433 + Expected: Build succeeds 434 + 435 + **Step 3: Commit** 436 + 437 + ```bash 438 + git add server/src/handlers/settings.gleam 439 + git add server/src/settings_handler.gleam 440 + git commit -m "refactor: move settings_handler to handlers/settings 441 + 442 + Move to organized handlers directory with consistent naming 443 + 444 + 🤖 Generated with [Claude Code](https://claude.com/claude-code) 445 + 446 + Co-Authored-By: Claude <noreply@anthropic.com>" 447 + ``` 448 + 449 + --- 450 + 451 + ### Task 10: Update server.gleam imports and routing 452 + 453 + **Files:** 454 + - Modify: `/Users/chadmiller/code/quickslice/server/src/server.gleam` 455 + 456 + **Step 1: Update imports (lines 5-31)** 457 + 458 + Replace the handler imports: 459 + 460 + ```gleam 461 + import client_graphql_handler 462 + import graphiql_handler 463 + import graphql_handler 464 + import graphql_ws_handler 465 + import upload_handler 466 + ``` 467 + 468 + With: 469 + 470 + ```gleam 471 + import handlers/backfill as backfill_handler 472 + import handlers/client_graphql as client_graphql_handler 473 + import handlers/graphiql as graphiql_handler 474 + import handlers/graphql as graphql_handler 475 + import handlers/graphql_ws as graphql_ws_handler 476 + import handlers/health as health_handler 477 + import handlers/index as index_handler 478 + import handlers/upload as upload_handler 479 + ``` 480 + 481 + **Step 2: Update routing in handle_request (lines 549-578)** 482 + 483 + Replace: 484 + 485 + ```gleam 486 + case segments { 487 + [] -> index_route(req, ctx) 488 + ["health"] -> handle_health_check(ctx) 489 + ["oauth", "authorize"] -> 490 + handlers.handle_oauth_authorize(req, ctx.db, ctx.oauth_config) 491 + ["oauth", "callback"] -> 492 + handlers.handle_oauth_callback(req, ctx.db, ctx.oauth_config) 493 + ["logout"] -> handlers.handle_logout(req, ctx.db) 494 + ["backfill"] -> handle_backfill_request(req, ctx) 495 + ["admin", "graphql"] -> 496 + client_graphql_handler.handle_client_graphql_request( 497 + req, 498 + ctx.db, 499 + ctx.admin_dids, 500 + ctx.jetstream_consumer, 501 + ) 502 + ["graphql"] -> 503 + graphql_handler.handle_graphql_request( 504 + req, 505 + ctx.db, 506 + ctx.auth_base_url, 507 + ctx.plc_url, 508 + ) 509 + ["graphiql"] -> 510 + graphiql_handler.handle_graphiql_request(req, ctx.db, ctx.oauth_config) 511 + ["upload"] -> 512 + upload_handler.handle_upload_request(req, ctx.db, ctx.oauth_config) 513 + // Fallback: serve SPA index.html for client-side routing 514 + _ -> index_route(req, ctx) 515 + } 516 + ``` 517 + 518 + With: 519 + 520 + ```gleam 521 + case segments { 522 + [] -> index_handler.handle() 523 + ["health"] -> health_handler.handle(ctx.db) 524 + ["oauth", "authorize"] -> 525 + handlers.handle_oauth_authorize(req, ctx.db, ctx.oauth_config) 526 + ["oauth", "callback"] -> 527 + handlers.handle_oauth_callback(req, ctx.db, ctx.oauth_config) 528 + ["logout"] -> handlers.handle_logout(req, ctx.db) 529 + ["backfill"] -> backfill_handler.handle(req, ctx.db, ctx.config) 530 + ["admin", "graphql"] -> 531 + client_graphql_handler.handle_client_graphql_request( 532 + req, 533 + ctx.db, 534 + ctx.admin_dids, 535 + ctx.jetstream_consumer, 536 + ) 537 + ["graphql"] -> 538 + graphql_handler.handle_graphql_request( 539 + req, 540 + ctx.db, 541 + ctx.auth_base_url, 542 + ctx.plc_url, 543 + ) 544 + ["graphiql"] -> 545 + graphiql_handler.handle_graphiql_request(req, ctx.db, ctx.oauth_config) 546 + ["upload"] -> 547 + upload_handler.handle_upload_request(req, ctx.db, ctx.oauth_config) 548 + // Fallback: serve SPA index.html for client-side routing 549 + _ -> index_handler.handle() 550 + } 551 + ``` 552 + 553 + **Step 3: Remove inline handler functions (lines 581-693)** 554 + 555 + Delete the following functions entirely: 556 + - `handle_backfill_request` (lines 581-657) 557 + - `handle_health_check` (lines 659-677) 558 + - `index_route` (lines 679-693) 559 + 560 + **Step 4: Update WebSocket handler reference (line 512)** 561 + 562 + Replace: 563 + 564 + ```gleam 565 + graphql_ws_handler.handle_websocket( 566 + ``` 567 + 568 + With: 569 + 570 + ```gleam 571 + graphql_ws_handler.handle_websocket( 572 + ``` 573 + 574 + (No change needed - the function name stays the same) 575 + 576 + **Step 5: Build to verify all changes** 577 + 578 + Run: `cd /Users/chadmiller/code/quickslice/server && gleam build` 579 + Expected: Build succeeds with no errors 580 + 581 + **Step 6: Commit** 582 + 583 + ```bash 584 + git add server/src/server.gleam 585 + git commit -m "refactor: update server.gleam to use handlers/ directory 586 + 587 + - Update imports to use handlers/* modules 588 + - Route all endpoints to dedicated handler modules 589 + - Remove inline handler functions (health, backfill, index) 590 + - Simplify server.gleam to thin routing layer 591 + 592 + 🤖 Generated with [Claude Code](https://claude.com/claude-code) 593 + 594 + Co-Authored-By: Claude <noreply@anthropic.com>" 595 + ``` 596 + 597 + --- 598 + 599 + ### Task 11: Search for other files importing moved handlers 600 + 601 + **Files:** 602 + - Search and potentially modify other files importing handlers 603 + 604 + **Step 1: Search for imports of old handler modules** 605 + 606 + Run: `cd /Users/chadmiller/code/quickslice/server && grep -r "import.*_handler" src/ --include="*.gleam" | grep -v "src/handlers/"` 607 + Expected: Shows any files that still import the old handler modules (likely jetstream_consumer.gleam based on earlier grep) 608 + 609 + **Step 2: Check jetstream_consumer.gleam** 610 + 611 + Run: `grep "import.*_handler" /Users/chadmiller/code/quickslice/server/src/jetstream_consumer.gleam` 612 + Expected: Shows which handler it imports 613 + 614 + **Step 3: Update jetstream_consumer.gleam if needed** 615 + 616 + If it imports `event_handler`, check if that needs updating or if it's a different module entirely. Make necessary updates. 617 + 618 + **Step 4: Build to verify** 619 + 620 + Run: `cd /Users/chadmiller/code/quickslice/server && gleam build` 621 + Expected: Build succeeds with no errors 622 + 623 + **Step 5: Commit if changes were needed** 624 + 625 + ```bash 626 + git add server/src/jetstream_consumer.gleam 627 + git commit -m "refactor: update imports in jetstream_consumer 628 + 629 + Update to use new handler module locations 630 + 631 + 🤖 Generated with [Claude Code](https://claude.com/claude-code) 632 + 633 + Co-Authored-By: Claude <noreply@anthropic.com>" 634 + ``` 635 + 636 + --- 637 + 638 + ### Task 12: Final verification and testing 639 + 640 + **Files:** 641 + - Test: All endpoints 642 + 643 + **Step 1: Run full build** 644 + 645 + Run: `cd /Users/chadmiller/code/quickslice/server && gleam build` 646 + Expected: Clean build with no errors or warnings 647 + 648 + **Step 2: Run tests if they exist** 649 + 650 + Run: `cd /Users/chadmiller/code/quickslice/server && gleam test` 651 + Expected: All tests pass 652 + 653 + **Step 3: Start server and verify endpoints** 654 + 655 + Run: `cd /Users/chadmiller/code/quickslice/server && gleam run` 656 + Expected: Server starts successfully 657 + 658 + **Step 4: Test health endpoint** 659 + 660 + Run: `curl http://localhost:8000/health` 661 + Expected: `{"status": "healthy"}` response 662 + 663 + **Step 5: Verify file structure** 664 + 665 + Run: `ls -la /Users/chadmiller/code/quickslice/server/src/handlers/` 666 + Expected: Shows all handler files: 667 + - backfill.gleam 668 + - client_graphql.gleam 669 + - graphiql.gleam 670 + - graphql.gleam 671 + - graphql_ws.gleam 672 + - health.gleam 673 + - index.gleam 674 + - settings.gleam 675 + - upload.gleam 676 + 677 + **Step 6: Commit final verification** 678 + 679 + ```bash 680 + git commit --allow-empty -m "verify: handler reorganization complete 681 + 682 + All handlers moved to handlers/ directory with consistent naming. 683 + Server builds and runs successfully. 684 + 685 + 🤖 Generated with [Claude Code](https://claude.com/claude-code) 686 + 687 + Co-Authored-By: Claude <noreply@anthropic.com>" 688 + ``` 689 + 690 + --- 691 + 692 + ## Summary 693 + 694 + After completion, the codebase will have: 695 + 696 + 1. **New structure:** 697 + ``` 698 + server/src/ 699 + ├── handlers/ 700 + │ ├── backfill.gleam 701 + │ ├── client_graphql.gleam 702 + │ ├── graphiql.gleam 703 + │ ├── graphql.gleam 704 + │ ├── graphql_ws.gleam 705 + │ ├── health.gleam 706 + │ ├── index.gleam 707 + │ ├── settings.gleam 708 + │ └── upload.gleam 709 + ├── oauth/ 710 + │ └── handlers.gleam (unchanged) 711 + └── server.gleam (simplified routing) 712 + ``` 713 + 714 + 2. **Benefits:** 715 + - All HTTP handlers in one discoverable location 716 + - Consistent naming without `_handler` suffix 717 + - `server.gleam` reduced by ~150 lines 718 + - Clear separation of concerns 719 + - Easier to find and modify endpoints 720 + 721 + 3. **DRY, YAGNI, TDD principles:** 722 + - No premature abstraction - simple file moves 723 + - Frequent commits for easy rollback 724 + - Build verification at each step 725 + 726 + --- 727 + 728 + **Plan complete! Ready for execution using superpowers:subagent-driven-development**
server/src/client_graphql_handler.gleam server/src/handlers/client_graphql.gleam
server/src/graphiql_handler.gleam server/src/handlers/graphiql.gleam
server/src/graphql_handler.gleam server/src/handlers/graphql.gleam
server/src/graphql_ws_handler.gleam server/src/handlers/graphql_ws.gleam
+97
server/src/handlers/backfill.gleam
··· 1 + /// Backfill endpoint handler 2 + /// 3 + /// Handles /backfill POST endpoint for triggering collection backfills 4 + import backfill 5 + import config 6 + import database/repositories/lexicons 7 + import gleam/erlang/process 8 + import gleam/http as gleam_http 9 + import gleam/int 10 + import gleam/list 11 + import gleam/option 12 + import sqlight 13 + import wisp 14 + 15 + /// Handle backfill request 16 + /// Only accepts POST method to trigger backfill 17 + pub fn handle( 18 + req: wisp.Request, 19 + db: sqlight.Connection, 20 + config_subject: process.Subject(config.Message), 21 + ) -> wisp.Response { 22 + case req.method { 23 + gleam_http.Post -> { 24 + // Get domain authority from config 25 + let domain_authority = case config.get_domain_authority(config_subject) { 26 + option.Some(authority) -> authority 27 + option.None -> "" 28 + } 29 + 30 + // Get all record-type lexicons 31 + case lexicons.get_record_types(db) { 32 + Ok(lexicons) -> { 33 + case lexicons { 34 + [] -> { 35 + wisp.response(200) 36 + |> wisp.set_header("content-type", "application/json") 37 + |> wisp.set_body(wisp.Text( 38 + "{\"status\": \"no_lexicons\", \"message\": \"No record-type lexicons found\"}", 39 + )) 40 + } 41 + _ -> { 42 + // Separate lexicons by domain authority 43 + let #(collections, external_collections) = 44 + lexicons 45 + |> list.partition(fn(lex) { 46 + backfill.nsid_matches_domain_authority( 47 + lex.id, 48 + domain_authority, 49 + ) 50 + }) 51 + 52 + let collection_ids = list.map(collections, fn(lex) { lex.id }) 53 + let external_collection_ids = 54 + list.map(external_collections, fn(lex) { lex.id }) 55 + 56 + // Run backfill in background process 57 + let backfill_config = backfill.default_config() 58 + process.spawn_unlinked(fn() { 59 + backfill.backfill_collections( 60 + [], 61 + collection_ids, 62 + external_collection_ids, 63 + backfill_config, 64 + db, 65 + ) 66 + }) 67 + 68 + wisp.response(200) 69 + |> wisp.set_header("content-type", "application/json") 70 + |> wisp.set_body(wisp.Text( 71 + "{\"status\": \"started\", \"collections\": " 72 + <> int.to_string(list.length(collection_ids)) 73 + <> ", \"external_collections\": " 74 + <> int.to_string(list.length(external_collection_ids)) 75 + <> "}", 76 + )) 77 + } 78 + } 79 + } 80 + Error(_) -> { 81 + wisp.response(500) 82 + |> wisp.set_header("content-type", "application/json") 83 + |> wisp.set_body(wisp.Text( 84 + "{\"error\": \"database_error\", \"message\": \"Failed to fetch lexicons\"}", 85 + )) 86 + } 87 + } 88 + } 89 + _ -> { 90 + wisp.response(405) 91 + |> wisp.set_header("content-type", "application/json") 92 + |> wisp.set_body(wisp.Text( 93 + "{\"error\": \"method_not_allowed\", \"message\": \"Use POST to trigger backfill\"}", 94 + )) 95 + } 96 + } 97 + }
+28
server/src/handlers/health.gleam
··· 1 + /// Health check endpoint handler 2 + /// 3 + /// Handles /health endpoint with database connectivity verification 4 + import database/repositories/lexicons 5 + import sqlight 6 + import wisp 7 + 8 + /// Handle health check request 9 + /// Returns 200 if database is accessible, 503 if not 10 + pub fn handle(db: sqlight.Connection) -> wisp.Response { 11 + // Try a simple database query to verify connectivity 12 + case lexicons.get_count(db) { 13 + Ok(_) -> { 14 + // Database is accessible 15 + wisp.response(200) 16 + |> wisp.set_header("content-type", "application/json") 17 + |> wisp.set_body(wisp.Text("{\"status\": \"healthy\"}")) 18 + } 19 + Error(_) -> { 20 + // Database is not accessible 21 + wisp.response(503) 22 + |> wisp.set_header("content-type", "application/json") 23 + |> wisp.set_body(wisp.Text( 24 + "{\"status\": \"unhealthy\", \"message\": \"Database connection failed\"}", 25 + )) 26 + } 27 + } 28 + }
+22
server/src/handlers/index.gleam
··· 1 + /// Index/SPA handler 2 + /// 3 + /// Serves the client SPA's index.html for root and fallback routes 4 + import simplifile 5 + import wisp 6 + 7 + /// Handle index route and SPA fallback 8 + /// Serves the bundled client application's index.html 9 + pub fn handle() -> wisp.Response { 10 + // Serve the client SPA's index.html (bundled by lustre dev tools) 11 + let assert Ok(priv_dir) = wisp.priv_directory("server") 12 + let index_path = priv_dir <> "/static/index.html" 13 + 14 + case simplifile.read(index_path) { 15 + Ok(contents) -> wisp.html_response(contents, 200) 16 + Error(_) -> 17 + wisp.html_response( 18 + "<h1>Error</h1><p>Client application not found. Run 'gleam run -m lustre/dev build' in the client directory.</p>", 19 + 500, 20 + ) 21 + } 22 + }
+12 -124
server/src/server.gleam
··· 2 2 import argv 3 3 import backfill 4 4 import backfill_state 5 - import client_graphql_handler 5 + import handlers/backfill as backfill_handler 6 + import handlers/client_graphql as client_graphql_handler 6 7 import config 7 8 import database/connection 8 9 import database/repositories/lexicons ··· 15 16 import gleam/list 16 17 import gleam/option 17 18 import gleam/string 18 - import graphiql_handler 19 - import graphql_handler 20 - import graphql_ws_handler 19 + import handlers/graphiql as graphiql_handler 20 + import handlers/graphql as graphql_handler 21 + import handlers/graphql_ws as graphql_ws_handler 22 + import handlers/health as health_handler 23 + import handlers/index as index_handler 21 24 import importer 22 25 import jetstream_consumer 23 26 import logging ··· 25 28 import oauth/handlers 26 29 import oauth/registration 27 30 import pubsub 28 - import simplifile 29 31 import sqlight 30 32 import stats_pubsub 31 - import upload_handler 33 + import handlers/upload as upload_handler 32 34 import wisp 33 35 import wisp/wisp_mist 34 36 ··· 547 549 let segments = wisp.path_segments(req) 548 550 549 551 case segments { 550 - [] -> index_route(req, ctx) 551 - ["health"] -> handle_health_check(ctx) 552 + [] -> index_handler.handle() 553 + ["health"] -> health_handler.handle(ctx.db) 552 554 ["oauth", "authorize"] -> 553 555 handlers.handle_oauth_authorize(req, ctx.db, ctx.oauth_config) 554 556 ["oauth", "callback"] -> 555 557 handlers.handle_oauth_callback(req, ctx.db, ctx.oauth_config) 556 558 ["logout"] -> handlers.handle_logout(req, ctx.db) 557 - ["backfill"] -> handle_backfill_request(req, ctx) 559 + ["backfill"] -> backfill_handler.handle(req, ctx.db, ctx.config) 558 560 ["admin", "graphql"] -> 559 561 client_graphql_handler.handle_client_graphql_request( 560 562 req, ··· 574 576 ["upload"] -> 575 577 upload_handler.handle_upload_request(req, ctx.db, ctx.oauth_config) 576 578 // Fallback: serve SPA index.html for client-side routing 577 - _ -> index_route(req, ctx) 578 - } 579 - } 580 - 581 - fn handle_backfill_request(req: wisp.Request, ctx: Context) -> wisp.Response { 582 - case req.method { 583 - gleam_http.Post -> { 584 - // Get domain authority from config 585 - let domain_authority = case config.get_domain_authority(ctx.config) { 586 - option.Some(authority) -> authority 587 - option.None -> "" 588 - } 589 - 590 - // Get all record-type lexicons 591 - case lexicons.get_record_types(ctx.db) { 592 - Ok(lexicons) -> { 593 - case lexicons { 594 - [] -> { 595 - wisp.response(200) 596 - |> wisp.set_header("content-type", "application/json") 597 - |> wisp.set_body(wisp.Text( 598 - "{\"status\": \"no_lexicons\", \"message\": \"No record-type lexicons found\"}", 599 - )) 600 - } 601 - _ -> { 602 - // Separate lexicons by domain authority 603 - let #(collections, external_collections) = 604 - lexicons 605 - |> list.partition(fn(lex) { 606 - backfill.nsid_matches_domain_authority( 607 - lex.id, 608 - domain_authority, 609 - ) 610 - }) 611 - 612 - let collection_ids = list.map(collections, fn(lex) { lex.id }) 613 - let external_collection_ids = 614 - list.map(external_collections, fn(lex) { lex.id }) 615 - 616 - // Run backfill in background process 617 - let backfill_config = backfill.default_config() 618 - process.spawn_unlinked(fn() { 619 - backfill.backfill_collections( 620 - [], 621 - collection_ids, 622 - external_collection_ids, 623 - backfill_config, 624 - ctx.db, 625 - ) 626 - }) 627 - 628 - wisp.response(200) 629 - |> wisp.set_header("content-type", "application/json") 630 - |> wisp.set_body(wisp.Text( 631 - "{\"status\": \"started\", \"collections\": " 632 - <> int.to_string(list.length(collection_ids)) 633 - <> ", \"external_collections\": " 634 - <> int.to_string(list.length(external_collection_ids)) 635 - <> "}", 636 - )) 637 - } 638 - } 639 - } 640 - Error(_) -> { 641 - wisp.response(500) 642 - |> wisp.set_header("content-type", "application/json") 643 - |> wisp.set_body(wisp.Text( 644 - "{\"error\": \"database_error\", \"message\": \"Failed to fetch lexicons\"}", 645 - )) 646 - } 647 - } 648 - } 649 - _ -> { 650 - wisp.response(405) 651 - |> wisp.set_header("content-type", "application/json") 652 - |> wisp.set_body(wisp.Text( 653 - "{\"error\": \"method_not_allowed\", \"message\": \"Use POST to trigger backfill\"}", 654 - )) 655 - } 656 - } 657 - } 658 - 659 - fn handle_health_check(ctx: Context) -> wisp.Response { 660 - // Try a simple database query to verify connectivity 661 - case lexicons.get_count(ctx.db) { 662 - Ok(_) -> { 663 - // Database is accessible 664 - wisp.response(200) 665 - |> wisp.set_header("content-type", "application/json") 666 - |> wisp.set_body(wisp.Text("{\"status\": \"healthy\"}")) 667 - } 668 - Error(_) -> { 669 - // Database is not accessible 670 - wisp.response(503) 671 - |> wisp.set_header("content-type", "application/json") 672 - |> wisp.set_body(wisp.Text( 673 - "{\"status\": \"unhealthy\", \"message\": \"Database connection failed\"}", 674 - )) 675 - } 676 - } 677 - } 678 - 679 - fn index_route(_req: wisp.Request, _ctx: Context) -> wisp.Response { 680 - // Serve the client SPA's index.html (bundled by lustre dev tools) 681 - // The priv directory is resolved at startup and passed through the handler 682 - let assert Ok(priv_dir) = wisp.priv_directory("server") 683 - let index_path = priv_dir <> "/static/index.html" 684 - 685 - case simplifile.read(index_path) { 686 - Ok(contents) -> wisp.html_response(contents, 200) 687 - Error(_) -> 688 - wisp.html_response( 689 - "<h1>Error</h1><p>Client application not found. Run 'gleam run -m lustre/dev build' in the client directory.</p>", 690 - 500, 691 - ) 579 + _ -> index_handler.handle() 692 580 } 693 581 } 694 582
server/src/settings_handler.gleam server/src/handlers/settings.gleam
server/src/upload_handler.gleam server/src/handlers/upload.gleam
+1 -1
server/test/blob_integration_test.gleam
··· 12 12 import gleam/json 13 13 import gleam/string 14 14 import gleeunit/should 15 - import graphql_handler 15 + import handlers/graphql as graphql_handler 16 16 import sqlight 17 17 import wisp 18 18 import wisp/simulate
+1 -1
server/test/graphql_aggregation_integration_test.gleam
··· 19 19 import gleam/string 20 20 import gleeunit 21 21 import gleeunit/should 22 - import graphql_handler 22 + import handlers/graphql as graphql_handler 23 23 import sqlight 24 24 import wisp 25 25 import wisp/simulate
+1 -1
server/test/graphql_handler_integration_test.gleam
··· 15 15 import gleam/list 16 16 import gleam/string 17 17 import gleeunit/should 18 - import graphql_handler 18 + import handlers/graphql as graphql_handler 19 19 import sqlight 20 20 import wisp 21 21 import wisp/simulate
+1 -1
server/test/graphql_introspection_did_join_test.gleam
··· 12 12 import gleam/result 13 13 import gleam/string 14 14 import gleeunit/should 15 - import graphql_handler 15 + import handlers/graphql as graphql_handler 16 16 import importer 17 17 import honk 18 18 import simplifile
+1 -1
server/test/graphql_total_count_test.gleam
··· 11 11 import gleam/list 12 12 import gleam/string 13 13 import gleeunit/should 14 - import graphql_handler 14 + import handlers/graphql as graphql_handler 15 15 import sqlight 16 16 import wisp 17 17 import wisp/simulate