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:
17
examples/data/ourdb_server.vsh
Executable file
17
examples/data/ourdb_server.vsh
Executable 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
98
lib/data/ourdb/SERVER.md
Normal 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
226
lib/data/ourdb/server.v
Normal 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'
|
||||
})
|
||||
}
|
||||
59
lib/data/ourdb/server_test.v
Normal file
59
lib/data/ourdb/server_test.v
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user