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