feat: Add OurDB key-value store server

- Adds a new lightweight key-value store server implemented in V.
- Includes basic CRUD operations (`set`, `get`, `delete`).
- Provides configurable host and operation restrictions for security.
- Offers middleware for logging and request validation.
- Supports incremental mode for automatic ID generation.
- Includes comprehensive documentation and example usage.
- Adds unit tests to ensure functionality and stability.
This commit is contained in:
Mahmoud Emad
2025-03-05 23:02:35 +02:00
parent fdf540cbd0
commit ae7e7ecb84
4 changed files with 400 additions and 0 deletions

17
examples/data/ourdb_server.vsh Executable file
View File

@@ -0,0 +1,17 @@
#!/usr/bin/env -S v -n -w -gc none -cc tcc -d use_openssl -enable-globals run
import freeflowuniverse.herolib.data.ourdb
import os
mut server := ourdb.new_server(
port: 9000
allowed_hosts: ['localhost']
allowed_operations: ['set', 'get', 'delete']
secret_key: 'secret'
config: ourdb.OurDBConfig{
path: '/tmp/ourdb'
incremental_mode: true
}
)!
server.run()

98
lib/data/ourdb/SERVER.md Normal file
View File

@@ -0,0 +1,98 @@
# OurDB Server
OurDBServer is a lightweight key-value store server built in V, designed for simplicity and performance. It provides basic CRUD operations with security features such as host and operation restrictions.
## Features
- Supports `set`, `get`, and `delete` operations.
- Allows configurable host restrictions.
- Middleware for logging and security.
- Incremental mode for automatic ID assignment.
- Configurable storage options.
## Installation
- Ensure you have V installed on your system:
## Usage
### Starting the Server
You can start the server using the following command:
```v
mut server := new_server(OurDBServerArgs{
port: 3000
allowed_hosts: ['localhost'] // Add more hosts as needed
allowed_operations: ['set', 'get', 'delete'] // Add more operations as needed, these are the current supported operations
secret_key: 'your-secret-key'
config: OurDBConfig{
path: '/tmp/ourdb'
incremental_mode: true
reset: true
}
}) or { panic(err) }
server.run(background: false )
```
### API Endpoints
#### 1. Set a Record
**Endpoint:**
```http
POST /set
```
**Request Body:**
```json
{
"id": 0, // ID is optional in incremental mode
"value": "Some data"
}
```
#### 2. Get a Record
**Endpoint:**
```http
GET /get/:id
```
#### 3. Delete a Record
**Endpoint:**
```http
DELETE /delete/:id
```
## Configuration
You can customize the server settings by modifying `OurDBServerArgs` when initializing the server.
| Parameter | Description | Default Value |
|---------------------|-----------------------------------|--------------|
| `port` | Server port | `3000` |
| `allowed_hosts` | List of allowed hosts | `['localhost']` |
| `allowed_operations` | List of permitted operations | `['set', 'get', 'delete']` |
| `secret_key` | Secret key for authentication | Auto-generated |
| `record_nr_max` | Max number of records | `100` |
| `record_size_max` | Max size per record (bytes) | `1024` |
| `file_size` | Max file storage size (bytes) | `10_000` |
| `incremental_mode` | Auto-generate IDs if enabled | `true` |
| `reset` | Clears storage on restart | `true` |
## Middleware
OurDB includes middleware for logging, host verification, and operation control:
- **Logger Middleware:** Logs incoming requests with details.
- **Allowed Hosts Middleware:** Blocks requests from unauthorized hosts.
- **Allowed Operations Middleware:** Blocks requests for unsupported operations.
## Running Tests
You can test the server using:
```sh
v -enable-globals -stats test lib/data/ourdb/server_test.v
```
## Running Server
You can run the server using:
```sh
v -enable-globals -stats run lib/data/ourdb/server.v
```
or use the created example
```sh
examples/data/ourdb_server.vsh
```

226
lib/data/ourdb/server.v Normal file
View File

@@ -0,0 +1,226 @@
module ourdb
import freeflowuniverse.herolib.ui.console
import veb
import rand
import time
import json
// Represents the server context, extending the veb.Context
pub struct ServerContext {
veb.Context
}
// Represents the OurDB server instance
@[heap]
pub struct OurDBServer {
veb.Middleware[ServerContext]
pub mut:
db &OurDB // Reference to the database instance
port int // Port on which the server runs
allowed_hosts []string // List of allowed hostnames
allowed_operations []string // List of allowed operations (e.g., set, get, delete)
secret_key string // Secret key for authentication
}
// Represents the arguments required to initialize the OurDB server
@[params]
pub struct OurDBServerArgs {
pub mut:
port int = 3000 // Server port, default is 3000
allowed_hosts []string = ['localhost'] // Allowed hosts
allowed_operations []string = ['set', 'get', 'delete'] // Allowed operations
secret_key string = rand.string_from_set('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789',
32) // Generated secret key
config OurDBConfig // Database configuration parameters
}
// Creates a new instance of the OurDB server
pub fn new_server(args OurDBServerArgs) !OurDBServer {
mut db := new(
record_nr_max: args.config.record_nr_max
record_size_max: args.config.record_size_max
file_size: args.config.file_size
path: args.config.path
incremental_mode: args.config.incremental_mode
reset: args.config.reset
) or { return error('Failed to create ourdb: ${err}') }
mut server := OurDBServer{
port: args.port
allowed_hosts: args.allowed_hosts
allowed_operations: args.allowed_operations
secret_key: args.secret_key
db: &db
}
server.use(handler: server.logger_handler)
server.use(handler: server.allowed_hosts_handler)
server.use(handler: server.allowed_operations_handler)
return server
}
// Middleware for logging incoming requests and responses
fn (self &OurDBServer) logger_handler(mut ctx ServerContext) bool {
start_time := time.now()
request := ctx.req
method := request.method.str().to_upper()
client_ip := ctx.req.header.get(.x_forwarded_for) or { ctx.req.host.str().split(':')[0] }
user_agent := ctx.req.header.get(.user_agent) or { 'Unknown' }
console.print_header('${start_time.format()} | [Request] IP: ${client_ip} | Method: ${method} | Path: ${request.url} | User-Agent: ${user_agent}')
return true
}
// Middleware to check if the client host is allowed
fn (self &OurDBServer) allowed_hosts_handler(mut ctx ServerContext) bool {
client_host := ctx.req.host.str().split(':')[0].to_lower()
if !self.allowed_hosts.contains(client_host) {
ctx.request_error('403 Forbidden: Host not allowed')
console.print_stderr('Unauthorized host: ${client_host}')
return false
}
return true
}
// Middleware to check if the requested operation is allowed
fn (self &OurDBServer) allowed_operations_handler(mut ctx ServerContext) bool {
url_parts := ctx.req.url.split('/')
operation := url_parts[1]
if operation !in self.allowed_operations {
ctx.request_error('403 Forbidden: Operation not allowed')
console.print_stderr('Unauthorized operation: ${operation}')
return false
}
return true
}
// Parameters for running the server
@[params]
pub struct RunParams {
pub mut:
background bool // If true, the server runs in the background
}
// Starts the OurDB server
pub fn (mut self OurDBServer) run(params RunParams) {
if params.background {
spawn veb.run[OurDBServer, ServerContext](mut self, self.port)
} else {
veb.run[OurDBServer, ServerContext](mut self, self.port)
}
}
// Represents a generic success response
@[params]
struct SuccessResponse[T] {
message string // Success message
data T // Response data
}
// Represents an error response
@[params]
struct ErrorResponse {
error string @[required] // Error type
message string @[required] // Error message
}
// Returns an error response
fn (server OurDBServer) error(args ErrorResponse) ErrorResponse {
return args
}
// Returns a success response
fn (server OurDBServer) success[T](args SuccessResponse[T]) SuccessResponse[T] {
return args
}
// Request body structure for the `/set` endpoint
struct SetRequestBody {
mut:
id u32 // Record ID
value string // Value to store
}
// API endpoint to set a key-value pair in the database
@['/set'; post]
pub fn (mut server OurDBServer) set(mut ctx ServerContext) veb.Result {
request_body := ctx.req.data.str()
mut decoded_body := json.decode(SetRequestBody, request_body) or {
ctx.res.set_status(.bad_request)
return ctx.json[ErrorResponse](server.error(
error: 'bad_request'
message: 'Invalid request body'
))
}
if server.db.incremental_mode && decoded_body.id > 0 {
ctx.res.set_status(.bad_request)
return ctx.json[ErrorResponse](server.error(
error: 'bad_request'
message: 'Cannot set id when incremental mode is enabled'
))
}
mut record := if server.db.incremental_mode {
server.db.set(data: decoded_body.value.bytes()) or {
ctx.res.set_status(.bad_request)
return ctx.json[ErrorResponse](server.error(
error: 'bad_request'
message: 'Failed to set key: ${err}'
))
}
} else {
server.db.set(id: decoded_body.id, data: decoded_body.value.bytes()) or {
ctx.res.set_status(.bad_request)
return ctx.json[ErrorResponse](server.error(
error: 'bad_request'
message: 'Failed to set key: ${err}'
))
}
}
decoded_body.id = record
ctx.res.set_status(.created)
return ctx.json(server.success(message: 'Successfully set the key', data: decoded_body))
}
// API endpoint to retrieve a record by ID
@['/get/:id'; get]
pub fn (mut server OurDBServer) get(mut ctx ServerContext, id string) veb.Result {
id_ := id.u32()
record := server.db.get(id_) or {
ctx.res.set_status(.not_found)
return ctx.json[ErrorResponse](server.error(
error: 'not_found'
message: 'Record does not exist: ${err}'
))
}
data := SetRequestBody{
id: id_
value: record.bytestr()
}
ctx.res.set_status(.ok)
return ctx.json(server.success(message: 'Successfully get record', data: data))
}
// API endpoint to delete a record by ID
@['/delete/:id'; delete]
pub fn (mut server OurDBServer) delete(mut ctx ServerContext, id string) veb.Result {
id_ := id.u32()
server.db.delete(id_) or {
ctx.res.set_status(.not_found)
return ctx.json[ErrorResponse](server.error(
error: 'not_found'
message: 'Failed to delete key: ${err}'
))
}
ctx.res.set_status(.no_content)
return ctx.json({
'message': 'Successfully deleted record'
})
}

View File

@@ -0,0 +1,59 @@
module ourdb
import time
import json
import rand
import net.http
fn test_ourdb_server() {
mut server := new_server(OurDBServerArgs{
port: 3000
allowed_hosts: ['localhost']
allowed_operations: ['set', 'get', 'delete']
secret_key: rand.string_from_set('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789',
32)
config: OurDBConfig{
record_nr_max: 100
record_size_max: 1024
file_size: 10_000
path: '/tmp/ourdb'
incremental_mode: true
reset: true
}
}) or { panic(err) }
server.run(RunParams{ background: true })
time.sleep(1 * time.second)
// Test set record
mut request_body := json.encode({
'value': 'Test Value'
})
mut req := http.new_request(.post, 'http://localhost:3000/set', request_body)
mut response := req.do()!
assert response.status_code == 201
mut decoded_response := json.decode(map[string]string, response.body)!
assert decoded_response['message'].str() == 'Successfully set the key'
// Test get record
time.sleep(500 * time.millisecond)
req = http.new_request(.get, 'http://localhost:3000/get/0', '')
response = req.do()!
assert response.status_code == 200
decoded_response = json.decode(map[string]string, response.body)!
assert decoded_response['message'].str() == 'Successfully get record'
// Test delete record
req = http.new_request(.delete, 'http://localhost:3000/delete/0', '')
response = req.do()!
assert response.status_code == 204
// Test invalid operation
req = http.new_request(.post, 'http://localhost:3000/invalid', '')
response = req.do()!
assert response.status_code == 400
}