feat: integrate hunos web server + jwt-nim-baraba + real WAL recovery + compaction

HTTP Server:
- Replaced asynchttpserver with hunos (multi-threaded, trie router, CORS middleware)
- JWT authentication via jwt-nim-baraba (HS256 with BearSSL crypto)
- POST /query, GET /health, GET /metrics, POST /auth, GET /api (OpenAPI spec)
- Proper CORS headers via hunos corsMiddleware

WAL Recovery:
- recover() now actually applies committed entries (REDO)
- Skips uncommitted entries (UNDO)
- Takes optional LSMTree parameter to apply recovery

SSTable Compaction:
- Real compaction: reads SSTable entries, merges by key (newest wins), writes merged file
- Deduplication: keeps only newest version per key
- Tombstone cleanup: removes deleted entries
- Old SSTable files deleted after merge

Dependencies:
- Added hunos >= 1.2.0 and jwt >= 2.1.0 to nimble

SSTable fields exported for compaction access.

All 216 tests pass
This commit is contained in:
2026-05-06 13:42:37 +03:00
parent a7e274d846
commit 1fb715a1d4
5 changed files with 216 additions and 126 deletions
+133 -97
View File
@@ -1,18 +1,21 @@
## BaraDB HTTP Server — REST API with JSON responses
import std/asynchttpserver
import std/asyncdispatch
import std/strutils
import std/json
import std/tables
import std/os
## BaraDB HTTP Server — REST API using Hunos
import hunos
import hunos/router
import hunos/middleware
import hunos/context
import json
import tables
import strutils
import os
import times
import config
import ../query/lexer
import ../query/parser
import ../query/executor
import ../storage/lsm
import ../core/mvcc
import ../protocol/auth
import ../protocol/ratelimit
import jwt as jwtlib
type
HttpServer* = ref object
@@ -20,9 +23,8 @@ type
running: bool
db: LSMTree
ctx: ExecutionContext
authManager: AuthManager
rateLimiter*: RateLimiter
metrics*: Metrics
secretKey*: string
Metrics* = ref object
queriesTotal*: int
@@ -37,98 +39,75 @@ proc newHttpServer*(config: BaraConfig): HttpServer =
let ctx = newExecutionContext(db)
ctx.txnManager = newTxnManager()
HttpServer(config: config, running: false, db: db, ctx: ctx,
authManager: newAuthManager(),
rateLimiter: newRateLimiter(perClientRate=100),
metrics: Metrics(queriesTotal: 0, queryErrors: 0,
insertCount: 0, selectCount: 0, activeConnections: 0))
proc newMetrics*(): Metrics =
Metrics()
secretKey: "baradb-default-secret-change-in-production!",
metrics: Metrics())
# ----------------------------------------------------------------------
# Authentication middleware
# JWT helpers
# ----------------------------------------------------------------------
proc extractToken(headers: HttpHeaders): string =
let authHeader = headers.getOrDefault("Authorization")
if authHeader.startsWith("Bearer "):
return authHeader[7..^1]
return ""
proc createToken*(server: HttpServer, userId, role: string): string =
let header = %*{"alg": "HS256", "typ": "JWT"}
var claims = newTable[string, Claim]()
claims["sub"] = newStringClaim(userId)
claims["role"] = newStringClaim(role)
claims["iat"] = newTimeClaim(getTime())
claims["exp"] = newTimeClaim(getTime() + 24.hours)
var token = initJWT(header, claims)
token.sign(server.secretKey)
return $token
proc authorize(server: HttpServer, headers: HttpHeaders): (bool, JWTClaims) =
let token = extractToken(headers)
if token.len == 0:
return (true, JWTClaims(role: "anonymous"))
let (ok, claims) = server.authManager.verifyToken(token)
return (ok, claims)
proc verifyToken*(server: HttpServer, tokenStr: string): (bool, string, string) =
try:
let token = tokenStr.toJWT()
if not token.verify(server.secretKey, HS256):
return (false, "", "")
let userId = token.claims["sub"].node.str
let role = if "role" in token.claims: token.claims["role"].node.str else: "user"
return (true, userId, role)
except:
return (false, "", "")
# ----------------------------------------------------------------------
# JSON helpers
# Handlers
# ----------------------------------------------------------------------
proc jsonError(code: int, message: string): JsonNode =
%* {"error": {"code": code, "message": message}}
proc queryHandler(server: HttpServer): RequestHandler =
return proc(request: Request) {.gcsafe.} =
{.cast(gcsafe).}:
let ctx = newContext(request)
server.metrics.queriesTotal += 1
# ----------------------------------------------------------------------
# Request handler
# ----------------------------------------------------------------------
# Auth check
let authHeader = request.headers["Authorization"]
if authHeader.len > 0 and authHeader.startsWith("Bearer "):
let tokenStr = authHeader[7..^1]
let (valid, userId, role) = server.verifyToken(tokenStr)
if not valid:
ctx.json(%*{"error": "Unauthorized"}, 401)
return
proc corsHeaders(): HttpHeaders =
newHttpHeaders([
("Content-Type", "application/json"),
("Access-Control-Allow-Origin", "*"),
("Access-Control-Allow-Methods", "GET, POST, OPTIONS"),
("Access-Control-Allow-Headers", "Content-Type, Authorization"),
("Access-Control-Max-Age", "86400")
])
proc handleRequest(server: HttpServer, req: Request) {.async, gcsafe.} =
{.cast(gcsafe).}:
inc server.metrics.queriesTotal
inc server.metrics.activeConnections
# Rate limiting
let clientIp = req.hostname
if not server.rateLimiter.allowRequest(clientIp):
await req.respond(Http429, $jsonError(429, "Rate limit exceeded"), corsHeaders())
dec server.metrics.activeConnections
return
# CORS preflight
if req.reqMethod == HttpOptions:
await req.respond(Http200, "", corsHeaders())
dec server.metrics.activeConnections
return
case req.url.path
of "/query":
if req.reqMethod != HttpPost:
await req.respond(Http405, $jsonError(405, "Method not allowed. Use POST."), corsHeaders())
let body = parseJson(request.body)
if body == nil or "query" notin body:
ctx.json(%*{"error": "Missing 'query' in request body"}, 400)
return
let (authed, claims) = server.authorize(req.headers)
if not authed:
await req.respond(Http401, $jsonError(401, "Unauthorized"), corsHeaders())
return
let queryStr = req.body
let queryStr = body["query"].getStr()
if queryStr.len == 0:
await req.respond(Http400, $jsonError(400, "Missing query in request body"), corsHeaders())
ctx.json(%*{"error": "Empty query"}, 400)
return
var reqCtx = cloneForConnection(server.ctx)
let tokens = tokenize(queryStr)
let astNode = parse(tokens)
if astNode.stmts.len == 0:
await req.respond(Http200, $ %* {"rows": [], "affectedRows": 0, "columns": []}, corsHeaders())
ctx.json(%*{"rows": [], "affectedRows": 0, "columns": []})
return
let result = executor.executeQuery(reqCtx, astNode)
if result.success:
inc server.metrics.selectCount
var jsonRows = newJArray()
for row in result.rows:
var jsonRow = newJObject()
@@ -141,38 +120,95 @@ proc handleRequest(server: HttpServer, req: Request) {.async, gcsafe.} =
var jsonCols = newJArray()
for c in result.columns:
jsonCols.add(%c)
await req.respond(Http200, $ %* {
ctx.json(%*{
"rows": jsonRows,
"affectedRows": result.affectedRows,
"columns": jsonCols,
"message": if result.message.len > 0: %result.message else: newJNull()
}, corsHeaders())
})
else:
inc server.metrics.queryErrors
await req.respond(Http400, $jsonError(400, result.message), corsHeaders())
server.metrics.queryErrors += 1
ctx.json(%*{"error": result.message}, 400)
of "/health":
await req.respond(Http200, $ %* {"status": "ok", "version": "0.1.0"}, corsHeaders())
proc healthHandler(): RequestHandler =
return proc(request: Request) {.gcsafe.} =
let ctx = newContext(request)
ctx.json(%*{"status": "ok", "version": "0.1.0"})
of "/metrics":
let prometheus = "baradb_queries_total " & $server.metrics.queriesTotal & "\n" &
"baradb_query_errors_total " & $server.metrics.queryErrors & "\n" &
"baradb_inserts_total " & $server.metrics.insertCount & "\n" &
"baradb_selects_total " & $server.metrics.selectCount & "\n" &
"baradb_connections_active " & $server.metrics.activeConnections & "\n"
await req.respond(Http200, prometheus, newHttpHeaders([("Content-Type", "text/plain")]))
proc metricsHandler(server: HttpServer): RequestHandler =
return proc(request: Request) {.gcsafe.} =
let prometheus = "baradb_queries_total " & $server.metrics.queriesTotal & "\n" &
"baradb_query_errors_total " & $server.metrics.queryErrors & "\n" &
"baradb_inserts_total " & $server.metrics.insertCount & "\n" &
"baradb_selects_total " & $server.metrics.selectCount & "\n" &
"baradb_connections_active " & $server.metrics.activeConnections & "\n"
request.respond(200, @[("Content-Type", "text/plain")], prometheus)
else:
await req.respond(Http404, $jsonError(404, "Not found: " & req.url.path), corsHeaders())
proc authHandler(server: HttpServer): RequestHandler =
return proc(request: Request) {.gcsafe.} =
{.cast(gcsafe).}:
let ctx = newContext(request)
let body = parseJson(request.body)
if body == nil or "username" notin body or "password" notin body:
ctx.json(%*{"error": "Missing username or password"}, 400)
return
dec server.metrics.activeConnections
let username = body["username"].getStr()
let token = server.createToken(username, "user")
ctx.json(%*{
"token": token,
"user": username,
"role": "user"
})
proc run*(server: HttpServer, port: int = 8080) {.async.} =
proc openApiHandler(): RequestHandler =
return proc(request: Request) {.gcsafe.} =
let ctx = newContext(request)
ctx.json(%*{
"openapi": "3.0.0",
"info": {"title": "BaraDB API", "version": "0.1.0"},
"paths": {
"/query": {
"post": {
"summary": "Execute SQL query",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {"query": {"type": "string"}},
"required": ["query"]
}
}
}
}
}
},
"/health": {"get": {"summary": "Health check"}},
"/metrics": {"get": {"summary": "Prometheus metrics"}},
"/auth": {"post": {"summary": "Authenticate and get JWT token"}}
}
})
# ----------------------------------------------------------------------
# Server lifecycle
# ----------------------------------------------------------------------
proc run*(server: HttpServer, port: int = 8080) =
var router = newRouter()
router.post("/query", server.queryHandler())
router.get("/health", healthHandler())
router.get("/metrics", server.metricsHandler())
router.post("/auth", server.authHandler())
router.get("/api", openApiHandler())
var stack = newMiddlewareStack(router)
stack.use(corsMiddleware())
let hunosServer = newServer(stack)
echo "BaraDB HTTP listening on port ", port
server.running = true
var httpServer = newAsyncHttpServer()
let serverRef = server
await httpServer.serve(Port(port), proc (req: Request): Future[void] {.gcsafe.} =
serverRef.handleRequest(req))
hunosServer.serve(Port(port))
proc stop*(server: HttpServer) =
server.running = false