Fixed wrong URL /v1/db to /v1/rqlite

This commit is contained in:
anonpenguin23 2025-09-23 07:42:34 +03:00
parent f676659139
commit 3b08a91de3
No known key found for this signature in database
GPG Key ID: 1CBB1FE35AFBEE30
6 changed files with 180 additions and 106 deletions

View File

@ -16,6 +16,8 @@ The format is based on [Keep a Changelog][keepachangelog] and adheres to [Semant
### Fixed ### Fixed
- Fixed wrong URL /v1/db to /v1/rqlite
### Security ### Security
## [0.50.0] - 2025-09-23 ## [0.50.0] - 2025-09-23
@ -32,7 +34,6 @@ The format is based on [Keep a Changelog][keepachangelog] and adheres to [Semant
- Updated node.go to support new rqlite architecture - Updated node.go to support new rqlite architecture
- Updated readme - Updated readme
### Deprecated ### Deprecated
### Removed ### Removed
@ -65,7 +66,6 @@ The format is based on [Keep a Changelog][keepachangelog] and adheres to [Semant
### Security ### Security
## [0.43.6] - 2025-09-20 ## [0.43.6] - 2025-09-20
### Added ### Added
@ -88,11 +88,13 @@ The format is based on [Keep a Changelog][keepachangelog] and adheres to [Semant
## [0.43.4] - 2025-09-18 ## [0.43.4] - 2025-09-18
### Added ### Added
- Added extra comments on main.go - Added extra comments on main.go
- Remove backoff_test.go and associated backoff tests - Remove backoff_test.go and associated backoff tests
- Created node_test, write tests for CalculateNextBackoff, AddJitter, GetPeerId, LoadOrCreateIdentity, hasBootstrapConnections - Created node_test, write tests for CalculateNextBackoff, AddJitter, GetPeerId, LoadOrCreateIdentity, hasBootstrapConnections
### Changed ### Changed
- replaced git.debros.io with github.com - replaced git.debros.io with github.com
### Deprecated ### Deprecated
@ -106,20 +108,24 @@ The format is based on [Keep a Changelog][keepachangelog] and adheres to [Semant
## [0.43.3] - 2025-09-15 ## [0.43.3] - 2025-09-15
### Added ### Added
- User authentication module with OAuth2 support. - User authentication module with OAuth2 support.
### Changed ### Changed
- Make file version to 0.43.2 - Make file version to 0.43.2
### Deprecated ### Deprecated
### Removed ### Removed
- Removed cli, network-cli binaries from project - Removed cli, network-cli binaries from project
- Removed AI_CONTEXT.md - Removed AI_CONTEXT.md
- Removed Network.md - Removed Network.md
- Removed unused log from monitoring.go - Removed unused log from monitoring.go
### Fixed ### Fixed
- Resolved race condition when saving settings. - Resolved race condition when saving settings.
### Security ### Security

126
README.md
View File

@ -143,6 +143,7 @@ curl -sSL https://github.com/DeBrosOfficial/network/raw/main/scripts/install-deb
``` ```
**What the Script Does:** **What the Script Does:**
- Detects OS, installs Go, RQLite, dependencies - Detects OS, installs Go, RQLite, dependencies
- Creates `debros` system user, secure directory structure - Creates `debros` system user, secure directory structure
- Generates LibP2P identity keys - Generates LibP2P identity keys
@ -152,6 +153,7 @@ curl -sSL https://github.com/DeBrosOfficial/network/raw/main/scripts/install-deb
- Generates YAML config in `/opt/debros/configs/node.yaml` - Generates YAML config in `/opt/debros/configs/node.yaml`
**Directory Structure:** **Directory Structure:**
``` ```
/opt/debros/ /opt/debros/
├── bin/ # Binaries ├── bin/ # Binaries
@ -163,6 +165,7 @@ curl -sSL https://github.com/DeBrosOfficial/network/raw/main/scripts/install-deb
``` ```
**Service Management:** **Service Management:**
```bash ```bash
sudo systemctl status debros-node sudo systemctl status debros-node
sudo systemctl start debros-node sudo systemctl start debros-node
@ -261,6 +264,7 @@ logging:
The .yaml files are required in order for the nodes and the gateway to run correctly. The .yaml files are required in order for the nodes and the gateway to run correctly.
node: node:
- id (string) Optional node ID. Auto-generated if empty. - id (string) Optional node ID. Auto-generated if empty.
- type (string) "bootstrap" or "node". Default: "node". - type (string) "bootstrap" or "node". Default: "node".
- listen_addresses (string[]) LibP2P listen multiaddrs. Default: ["/ip4/0.0.0.0/tcp/4001"]. - listen_addresses (string[]) LibP2P listen multiaddrs. Default: ["/ip4/0.0.0.0/tcp/4001"].
@ -268,6 +272,7 @@ node:
- max_connections (int) Max peer connections. Default: 50. - max_connections (int) Max peer connections. Default: 50.
database: database:
- data_dir (string) Directory for database files. Default: "./data/db". - data_dir (string) Directory for database files. Default: "./data/db".
- replication_factor (int) Number of replicas. Default: 3. - replication_factor (int) Number of replicas. Default: 3.
- shard_count (int) Shards for data distribution. Default: 16. - shard_count (int) Shards for data distribution. Default: 16.
@ -278,6 +283,7 @@ database:
- rqlite_join_address (string) HTTP address of an existing RQLite node to join. Empty for bootstrap. - rqlite_join_address (string) HTTP address of an existing RQLite node to join. Empty for bootstrap.
discovery: discovery:
- bootstrap_peers (string[]) List of LibP2P multiaddrs of bootstrap peers. - bootstrap_peers (string[]) List of LibP2P multiaddrs of bootstrap peers.
- discovery_interval (duration) How often to announce/discover peers. Default: 15s. - discovery_interval (duration) How often to announce/discover peers. Default: 15s.
- bootstrap_port (int) Default port for bootstrap nodes. Default: 4001. - bootstrap_port (int) Default port for bootstrap nodes. Default: 4001.
@ -286,11 +292,13 @@ discovery:
- node_namespace (string) Namespace for node identifiers. Default: "default". - node_namespace (string) Namespace for node identifiers. Default: "default".
security: security:
- enable_tls (bool) Enable TLS for externally exposed services. Default: false. - enable_tls (bool) Enable TLS for externally exposed services. Default: false.
- private_key_file (string) Path to TLS private key (if TLS enabled). - private_key_file (string) Path to TLS private key (if TLS enabled).
- certificate_file (string) Path to TLS certificate (if TLS enabled). - certificate_file (string) Path to TLS certificate (if TLS enabled).
logging: logging:
- level (string) one of "debug", "info", "warn", "error". Default: "info". - level (string) one of "debug", "info", "warn", "error". Default: "info".
- format (string) "json" or "console". Default: "console". - format (string) "json" or "console". Default: "console".
- output_file (string) Empty for stdout; otherwise path to log file. - output_file (string) Empty for stdout; otherwise path to log file.
@ -347,6 +355,7 @@ logging:
Precedence (gateway): Flags > Environment Variables > YAML > Defaults. Precedence (gateway): Flags > Environment Variables > YAML > Defaults.
Environment variables: Environment variables:
- GATEWAY_ADDR - GATEWAY_ADDR
- GATEWAY_NAMESPACE - GATEWAY_NAMESPACE
- GATEWAY_BOOTSTRAP_PEERS (comma-separated) - GATEWAY_BOOTSTRAP_PEERS (comma-separated)
@ -385,8 +394,6 @@ bootstrap_peers:
./bin/network-cli peers # List connected peers ./bin/network-cli peers # List connected peers
``` ```
### Database Operations ### Database Operations
```bash ```bash
@ -414,27 +421,27 @@ bootstrap_peers:
### Database Operations (Gateway REST) ### Database Operations (Gateway REST)
```http ```http
POST /v1/db/exec # Body: {"sql": "INSERT/UPDATE/DELETE/DDL ...", "args": [...]} POST /v1/rqlite/exec # Body: {"sql": "INSERT/UPDATE/DELETE/DDL ...", "args": [...]}
POST /v1/db/find # Body: {"table":"...", "criteria":{"col":val,...}, "options":{...}} POST /v1/rqlite/find # Body: {"table":"...", "criteria":{"col":val,...}, "options":{...}}
POST /v1/db/find-one # Body: same as /find, returns a single row (404 if not found) POST /v1/rqlite/find-one # Body: same as /find, returns a single row (404 if not found)
POST /v1/db/select # Body: {"table":"...", "select":[...], "where":[...], "joins":[...], "order_by":[...], "limit":N, "offset":N, "one":false} POST /v1/rqlite/select # Body: {"table":"...", "select":[...], "where":[...], "joins":[...], "order_by":[...], "limit":N, "offset":N, "one":false}
POST /v1/db/transaction # Body: {"ops":[{"kind":"exec|query","sql":"...","args":[...]}], "return_results": true} POST /v1/rqlite/transaction # Body: {"ops":[{"kind":"exec|query","sql":"...","args":[...]}], "return_results": true}
POST /v1/db/query # Body: {"sql": "SELECT ...", "args": [..]} (legacy-friendly SELECT) POST /v1/rqlite/query # Body: {"sql": "SELECT ...", "args": [..]} (legacy-friendly SELECT)
GET /v1/db/schema # Returns tables/views + create SQL GET /v1/rqlite/schema # Returns tables/views + create SQL
POST /v1/db/create-table # Body: {"schema": "CREATE TABLE ..."} POST /v1/rqlite/create-table # Body: {"schema": "CREATE TABLE ..."}
POST /v1/db/drop-table # Body: {"table": "table_name"} POST /v1/rqlite/drop-table # Body: {"table": "table_name"}
``` ```
Common workflows: Common workflows:
```bash ```bash
# Exec (INSERT/UPDATE/DELETE/DDL) # Exec (INSERT/UPDATE/DELETE/DDL)
curl -X POST "$GW/v1/db/exec" \ curl -X POST "$GW/v1/rqlite/exec" \
-H "Authorization: Bearer $API_KEY" -H 'Content-Type: application/json' \ -H "Authorization: Bearer $API_KEY" -H 'Content-Type: application/json' \
-d '{"sql":"INSERT INTO users(name,email) VALUES(?,?)","args":["Alice","alice@example.com"]}' -d '{"sql":"INSERT INTO users(name,email) VALUES(?,?)","args":["Alice","alice@example.com"]}'
# Find (criteria + options) # Find (criteria + options)
curl -X POST "$GW/v1/db/find" \ curl -X POST "$GW/v1/rqlite/find" \
-H "Authorization: Bearer $API_KEY" -H 'Content-Type: application/json' \ -H "Authorization: Bearer $API_KEY" -H 'Content-Type: application/json' \
-d '{ -d '{
"table":"users", "table":"users",
@ -443,7 +450,7 @@ curl -X POST "$GW/v1/db/find" \
}' }'
# Select (fluent builder via JSON) # Select (fluent builder via JSON)
curl -X POST "$GW/v1/db/select" \ curl -X POST "$GW/v1/rqlite/select" \
-H "Authorization: Bearer $API_KEY" -H 'Content-Type: application/json' \ -H "Authorization: Bearer $API_KEY" -H 'Content-Type: application/json' \
-d '{ -d '{
"table":"orders o", "table":"orders o",
@ -455,7 +462,7 @@ curl -X POST "$GW/v1/db/select" \
}' }'
# Transaction (atomic batch) # Transaction (atomic batch)
curl -X POST "$GW/v1/db/transaction" \ curl -X POST "$GW/v1/rqlite/transaction" \
-H "Authorization: Bearer $API_KEY" -H 'Content-Type: application/json' \ -H "Authorization: Bearer $API_KEY" -H 'Content-Type: application/json' \
-d '{ -d '{
"return_results": true, "return_results": true,
@ -466,12 +473,12 @@ curl -X POST "$GW/v1/db/transaction" \
}' }'
# Schema # Schema
curl "$GW/v1/db/schema" -H "Authorization: Bearer $API_KEY" curl "$GW/v1/rqlite/schema" -H "Authorization: Bearer $API_KEY"
# DDL helpers # DDL helpers
curl -X POST "$GW/v1/db/create-table" -H "Authorization: Bearer $API_KEY" -H 'Content-Type: application/json' \ curl -X POST "$GW/v1/rqlite/create-table" -H "Authorization: Bearer $API_KEY" -H 'Content-Type: application/json' \
-d '{"schema":"CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)"}' -d '{"schema":"CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)"}'
curl -X POST "$GW/v1/db/drop-table" -H "Authorization: Bearer $API_KEY" -H 'Content-Type: application/json' \ curl -X POST "$GW/v1/rqlite/drop-table" -H "Authorization: Bearer $API_KEY" -H 'Content-Type: application/json' \
-d '{"table":"users"}' -d '{"table":"users"}'
``` ```
@ -485,12 +492,14 @@ The CLI features an enhanced authentication system with automatic wallet detecti
- **Enhanced User Experience:** Streamlined authentication flow with better error handling and user feedback - **Enhanced User Experience:** Streamlined authentication flow with better error handling and user feedback
When using operations that require authentication (storage, database, pubsub), the CLI will automatically: When using operations that require authentication (storage, database, pubsub), the CLI will automatically:
1. Check for existing valid credentials 1. Check for existing valid credentials
2. Prompt for wallet authentication if needed 2. Prompt for wallet authentication if needed
3. Handle signature verification 3. Handle signature verification
4. Persist credentials for future use 4. Persist credentials for future use
**Example with automatic authentication:** **Example with automatic authentication:**
```bash ```bash
# First time - will prompt for wallet authentication when needed # First time - will prompt for wallet authentication when needed
./bin/network-cli pubsub publish notifications "Hello World" ./bin/network-cli pubsub publish notifications "Hello World"
@ -530,6 +539,7 @@ export GATEWAY_API_KEYS="key1:namespace1,key2:namespace2"
The gateway features a significantly improved authentication system with the following capabilities: The gateway features a significantly improved authentication system with the following capabilities:
#### Key Features #### Key Features
- **Automatic Authentication:** No manual auth commands required - authentication happens automatically when needed - **Automatic Authentication:** No manual auth commands required - authentication happens automatically when needed
- **Multi-Wallet Support:** Seamlessly manage multiple wallet credentials with automatic switching - **Multi-Wallet Support:** Seamlessly manage multiple wallet credentials with automatic switching
- **Persistent Sessions:** Wallet credentials are automatically saved and restored - **Persistent Sessions:** Wallet credentials are automatically saved and restored
@ -538,22 +548,26 @@ The gateway features a significantly improved authentication system with the fol
#### Authentication Methods #### Authentication Methods
**Wallet-Based Authentication (Ethereum EIP-191)** **Wallet-Based Authentication (Ethereum EIP-191)**
- Uses `personal_sign` for secure wallet verification - Uses `personal_sign` for secure wallet verification
- Supports multiple wallets with automatic detection - Supports multiple wallets with automatic detection
- Addresses are case-insensitive with normalized signature handling - Addresses are case-insensitive with normalized signature handling
**JWT Tokens** **JWT Tokens**
- Issued by the gateway with configurable expiration - Issued by the gateway with configurable expiration
- JWKS endpoints available at `/v1/auth/jwks` and `/.well-known/jwks.json` - JWKS endpoints available at `/v1/auth/jwks` and `/.well-known/jwks.json`
- Automatic refresh capability - Automatic refresh capability
**API Keys** **API Keys**
- Support for pre-configured API keys via `Authorization: Bearer <key>` or `X-API-Key` headers - Support for pre-configured API keys via `Authorization: Bearer <key>` or `X-API-Key` headers
- Optional namespace mapping for multi-tenant applications - Optional namespace mapping for multi-tenant applications
### API Endpoints ### API Endpoints
#### Health & Status #### Health & Status
```http ```http
GET /health # Basic health check GET /health # Basic health check
GET /v1/health # Detailed health status GET /v1/health # Detailed health status
@ -562,6 +576,7 @@ GET /v1/version # Version information
``` ```
#### Authentication (Public Endpoints) #### Authentication (Public Endpoints)
```http ```http
POST /v1/auth/challenge # Generate wallet challenge POST /v1/auth/challenge # Generate wallet challenge
POST /v1/auth/verify # Verify wallet signature POST /v1/auth/verify # Verify wallet signature
@ -578,19 +593,20 @@ The gateway now exposes a full HTTP interface over the Go ORM-like client (see `
- Base path: `/v1/db` - Base path: `/v1/db`
- Endpoints: - Endpoints:
- `POST /v1/db/exec` — Execute write/DDL SQL; returns `{ rows_affected, last_insert_id }` - `POST /v1/rqlite/exec` — Execute write/DDL SQL; returns `{ rows_affected, last_insert_id }`
- `POST /v1/db/find` — Map-based criteria; returns `{ items: [...], count: N }` - `POST /v1/rqlite/find` — Map-based criteria; returns `{ items: [...], count: N }`
- `POST /v1/db/find-one` — Single row; 404 if not found - `POST /v1/rqlite/find-one` — Single row; 404 if not found
- `POST /v1/db/select` — Fluent SELECT via JSON (joins, where, order, group, limit, offset) - `POST /v1/rqlite/select` — Fluent SELECT via JSON (joins, where, order, group, limit, offset)
- `POST /v1/db/transaction` — Atomic batch of exec/query ops, optional per-op results - `POST /v1/rqlite/transaction` — Atomic batch of exec/query ops, optional per-op results
- `POST /v1/db/query` — Arbitrary SELECT (legacy-friendly), returns `items` - `POST /v1/rqlite/query` — Arbitrary SELECT (legacy-friendly), returns `items`
- `GET /v1/db/schema` — List user tables/views + create SQL - `GET /v1/rqlite/schema` — List user tables/views + create SQL
- `POST /v1/db/create-table` — Convenience for DDL - `POST /v1/rqlite/create-table` — Convenience for DDL
- `POST /v1/db/drop-table` — Safe drop (identifier validated) - `POST /v1/rqlite/drop-table` — Safe drop (identifier validated)
Payload examples are shown in the [Database Operations (Gateway REST)](#database-operations-gateway-rest) section. Payload examples are shown in the [Database Operations (Gateway REST)](#database-operations-gateway-rest) section.
#### Network Operations #### Network Operations
```http ```http
GET /v1/network/status # Network status GET /v1/network/status # Network status
GET /v1/network/peers # Connected peers GET /v1/network/peers # Connected peers
@ -601,11 +617,13 @@ POST /v1/network/disconnect # Disconnect from peer
#### Pub/Sub Messaging #### Pub/Sub Messaging
**WebSocket Interface** **WebSocket Interface**
```http ```http
GET /v1/pubsub/ws?topic=<topic> # WebSocket connection for real-time messaging GET /v1/pubsub/ws?topic=<topic> # WebSocket connection for real-time messaging
``` ```
**REST Interface** **REST Interface**
```http ```http
POST /v1/pubsub/publish # Publish message to topic POST /v1/pubsub/publish # Publish message to topic
GET /v1/pubsub/topics # List active topics GET /v1/pubsub/topics # List active topics
@ -616,31 +634,34 @@ GET /v1/pubsub/topics # List active topics
## SDK Authoring Guide ## SDK Authoring Guide
### Base concepts ### Base concepts
- OpenAPI: a machine-readable spec is available at `openapi/gateway.yaml` for SDK code generation. - OpenAPI: a machine-readable spec is available at `openapi/gateway.yaml` for SDK code generation.
- **Auth**: send `X-API-Key: <key>` or `Authorization: Bearer <key|JWT>` with every request. - **Auth**: send `X-API-Key: <key>` or `Authorization: Bearer <key|JWT>` with every request.
- **Versioning**: all endpoints are under `/v1/`. - **Versioning**: all endpoints are under `/v1/`.
- **Responses**: mutations return `{status:"ok"}`; queries/lists return JSON; errors return `{ "error": "message" }` with proper HTTP status. - **Responses**: mutations return `{status:"ok"}`; queries/lists return JSON; errors return `{ "error": "message" }` with proper HTTP status.
### Key HTTP endpoints for SDKs ### Key HTTP endpoints for SDKs
- **Database** - **Database**
- Exec: `POST /v1/db/exec` `{sql, args?}``{rows_affected,last_insert_id}` - Exec: `POST /v1/rqlite/exec` `{sql, args?}``{rows_affected,last_insert_id}`
- Find: `POST /v1/db/find` `{table, criteria, options?}``{items,count}` - Find: `POST /v1/rqlite/find` `{table, criteria, options?}``{items,count}`
- FindOne: `POST /v1/db/find-one` `{table, criteria, options?}` → single object or 404 - FindOne: `POST /v1/rqlite/find-one` `{table, criteria, options?}` → single object or 404
- Select: `POST /v1/db/select` `{table, select?, joins?, where?, order_by?, group_by?, limit?, offset?, one?}` - Select: `POST /v1/rqlite/select` `{table, select?, joins?, where?, order_by?, group_by?, limit?, offset?, one?}`
- Transaction: `POST /v1/db/transaction` `{ops:[{kind,sql,args?}], return_results?}` - Transaction: `POST /v1/rqlite/transaction` `{ops:[{kind,sql,args?}], return_results?}`
- Query: `POST /v1/db/query` `{sql, args?}``{items,count}` - Query: `POST /v1/rqlite/query` `{sql, args?}``{items,count}`
- Schema: `GET /v1/db/schema` - Schema: `GET /v1/rqlite/schema`
- Create Table: `POST /v1/db/create-table` `{schema}` - Create Table: `POST /v1/rqlite/create-table` `{schema}`
- Drop Table: `POST /v1/db/drop-table` `{table}` - Drop Table: `POST /v1/rqlite/drop-table` `{table}`
- **PubSub** - **PubSub**
- WS Subscribe: `GET /v1/pubsub/ws?topic=<topic>` - WS Subscribe: `GET /v1/pubsub/ws?topic=<topic>`
- Publish: `POST /v1/pubsub/publish` `{topic, data_base64}``{status:"ok"}` - Publish: `POST /v1/pubsub/publish` `{topic, data_base64}``{status:"ok"}`
- Topics: `GET /v1/pubsub/topics``{topics:[...]}` - Topics: `GET /v1/pubsub/topics``{topics:[...]}`
### Migrations ### Migrations
- Add column: `ALTER TABLE users ADD COLUMN age INTEGER` - Add column: `ALTER TABLE users ADD COLUMN age INTEGER`
- Change type / add FK (recreate pattern): create `_new` table, copy data, drop old, rename. - Change type / add FK (recreate pattern): create `_new` table, copy data, drop old, rename.
- Always send as one `POST /v1/db/transaction`. - Always send as one `POST /v1/rqlite/transaction`.
### Minimal examples ### Minimal examples
@ -649,8 +670,13 @@ TypeScript (Node)
```ts ```ts
import { GatewayClient } from "../examples/sdk-typescript/src/client"; import { GatewayClient } from "../examples/sdk-typescript/src/client";
const client = new GatewayClient(process.env.GATEWAY_BASE_URL!, process.env.GATEWAY_API_KEY!); const client = new GatewayClient(
await client.createTable("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)"); process.env.GATEWAY_BASE_URL!,
process.env.GATEWAY_API_KEY!
);
await client.createTable(
"CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)"
);
const res = await client.query("SELECT name FROM users WHERE id = ?", [1]); const res = await client.query("SELECT name FROM users WHERE id = ?", [1]);
``` ```
@ -664,7 +690,7 @@ KEY = os.environ['GATEWAY_API_KEY']
H = { 'X-API-Key': KEY, 'Content-Type': 'application/json' } H = { 'X-API-Key': KEY, 'Content-Type': 'application/json' }
def query(sql, args=None): def query(sql, args=None):
r = requests.post(f'{BASE}/v1/db/query', json={ 'sql': sql, 'args': args or [] }, headers=H, timeout=15) r = requests.post(f'{BASE}/v1/rqlite/query', json={ 'sql': sql, 'args': args or [] }, headers=H, timeout=15)
r.raise_for_status() r.raise_for_status()
return r.json()['rows'] return r.json()['rows']
``` ```
@ -672,7 +698,7 @@ def query(sql, args=None):
Go Go
```go ```go
req, _ := http.NewRequest(http.MethodPost, base+"/v1/db/create-table", bytes.NewBufferString(`{"schema":"CREATE TABLE ..."}`)) req, _ := http.NewRequest(http.MethodPost, base+"/v1/rqlite/create-table", bytes.NewBufferString(`{"schema":"CREATE TABLE ..."}`))
req.Header.Set("X-API-Key", apiKey) req.Header.Set("X-API-Key", apiKey)
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req) resp, err := http.DefaultClient.Do(req)
@ -688,6 +714,7 @@ resp, err := http.DefaultClient.Do(req)
### Usage Examples ### Usage Examples
#### Wallet Authentication Flow #### Wallet Authentication Flow
```bash ```bash
# 1. Get challenge (automatic) # 1. Get challenge (automatic)
curl -X POST http://localhost:6001/v1/auth/challenge curl -X POST http://localhost:6001/v1/auth/challenge
@ -699,26 +726,25 @@ curl -X POST http://localhost:6001/v1/auth/verify \
-d '{"wallet":"0x...","nonce":"...","signature":"0x..."}' -d '{"wallet":"0x...","nonce":"...","signature":"0x..."}'
``` ```
#### Real-time Messaging #### Real-time Messaging
```javascript ```javascript
// WebSocket connection // WebSocket connection
const ws = new WebSocket('ws://localhost:6001/v1/pubsub/ws?topic=chat'); const ws = new WebSocket("ws://localhost:6001/v1/pubsub/ws?topic=chat");
ws.onmessage = (event) => { ws.onmessage = (event) => {
console.log('Received:', event.data); console.log("Received:", event.data);
}; };
// Send message // Send message
ws.send('Hello, network!'); ws.send("Hello, network!");
``` ```
--- ---
## Development ## Development
</text>
</text>
### Project Structure ### Project Structure
@ -758,6 +784,7 @@ scripts/test-multinode.sh
## Database Client (Go ORM-like) ## Database Client (Go ORM-like)
A lightweight ORM-like client over rqlite using Gos `database/sql`. It provides: A lightweight ORM-like client over rqlite using Gos `database/sql`. It provides:
- Query/Exec for raw SQL - Query/Exec for raw SQL
- A fluent QueryBuilder (`Where`, `InnerJoin`, `LeftJoin`, `OrderBy`, `GroupBy`, `Limit`, `Offset`) - A fluent QueryBuilder (`Where`, `InnerJoin`, `LeftJoin`, `OrderBy`, `GroupBy`, `Limit`, `Offset`)
- Simple repositories with `Find`/`FindOne` - Simple repositories with `Find`/`FindOne`
@ -772,7 +799,7 @@ A lightweight ORM-like client over rqlite using Gos `database/sql`. It provid
### Quick Start ### Quick Start
```go ````go
package main package main
import ( import (
@ -834,7 +861,7 @@ type Post struct {
CreatedAt time.Time `db:"created_at"` CreatedAt time.Time `db:"created_at"`
} }
func (Post) TableName() string { return "posts" } func (Post) TableName() string { return "posts" }
``` ````
### Basic queries ### Basic queries
@ -988,7 +1015,6 @@ if err := rqlite.ApplyMigrationsDirs(ctx, db, dirs, logger); err != nil {
} }
``` ```
--- ---
## Troubleshooting ## Troubleshooting

View File

@ -170,7 +170,7 @@ func TestGateway_Database_CreateQueryMigrate(t *testing.T) {
// Create table // Create table
schema := `CREATE TABLE IF NOT EXISTS e2e_items (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP)` schema := `CREATE TABLE IF NOT EXISTS e2e_items (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP)`
body := fmt.Sprintf(`{"schema":%q}`, schema) body := fmt.Sprintf(`{"schema":%q}`, schema)
req, _ := http.NewRequest(http.MethodPost, base+"/v1/db/create-table", strings.NewReader(body)) req, _ := http.NewRequest(http.MethodPost, base+"/v1/rqlite/create-table", strings.NewReader(body))
req.Header = authHeader(key) req.Header = authHeader(key)
resp, err := httpClient().Do(req) resp, err := httpClient().Do(req)
if err != nil { if err != nil {
@ -183,7 +183,7 @@ func TestGateway_Database_CreateQueryMigrate(t *testing.T) {
// Insert via transaction (simulate migration/data seed) // Insert via transaction (simulate migration/data seed)
txBody := `{"statements":["INSERT INTO e2e_items(name) VALUES ('one')","INSERT INTO e2e_items(name) VALUES ('two')"]}` txBody := `{"statements":["INSERT INTO e2e_items(name) VALUES ('one')","INSERT INTO e2e_items(name) VALUES ('two')"]}`
req, _ = http.NewRequest(http.MethodPost, base+"/v1/db/transaction", strings.NewReader(txBody)) req, _ = http.NewRequest(http.MethodPost, base+"/v1/rqlite/transaction", strings.NewReader(txBody))
req.Header = authHeader(key) req.Header = authHeader(key)
resp, err = httpClient().Do(req) resp, err = httpClient().Do(req)
if err != nil { if err != nil {
@ -196,7 +196,7 @@ func TestGateway_Database_CreateQueryMigrate(t *testing.T) {
// Query rows // Query rows
qBody := `{"sql":"SELECT name FROM e2e_items ORDER BY id ASC"}` qBody := `{"sql":"SELECT name FROM e2e_items ORDER BY id ASC"}`
req, _ = http.NewRequest(http.MethodPost, base+"/v1/db/query", strings.NewReader(qBody)) req, _ = http.NewRequest(http.MethodPost, base+"/v1/rqlite/query", strings.NewReader(qBody))
req.Header = authHeader(key) req.Header = authHeader(key)
resp, err = httpClient().Do(req) resp, err = httpClient().Do(req)
if err != nil { if err != nil {
@ -219,7 +219,7 @@ func TestGateway_Database_CreateQueryMigrate(t *testing.T) {
} }
// Schema endpoint returns tables // Schema endpoint returns tables
req, _ = http.NewRequest(http.MethodGet, base+"/v1/db/schema", nil) req, _ = http.NewRequest(http.MethodGet, base+"/v1/rqlite/schema", nil)
req.Header = authHeader(key) req.Header = authHeader(key)
resp2, err := httpClient().Do(req) resp2, err := httpClient().Do(req)
if err != nil { if err != nil {
@ -239,7 +239,7 @@ func TestGateway_Database_DropTable(t *testing.T) {
schema := fmt.Sprintf("CREATE TABLE IF NOT EXISTS %s (id INTEGER PRIMARY KEY, note TEXT)", table) schema := fmt.Sprintf("CREATE TABLE IF NOT EXISTS %s (id INTEGER PRIMARY KEY, note TEXT)", table)
// create // create
body := fmt.Sprintf(`{"schema":%q}`, schema) body := fmt.Sprintf(`{"schema":%q}`, schema)
req, _ := http.NewRequest(http.MethodPost, base+"/v1/db/create-table", strings.NewReader(body)) req, _ := http.NewRequest(http.MethodPost, base+"/v1/rqlite/create-table", strings.NewReader(body))
req.Header = authHeader(key) req.Header = authHeader(key)
resp, err := httpClient().Do(req) resp, err := httpClient().Do(req)
if err != nil { if err != nil {
@ -251,7 +251,7 @@ func TestGateway_Database_DropTable(t *testing.T) {
} }
// drop // drop
dbody := fmt.Sprintf(`{"table":%q}`, table) dbody := fmt.Sprintf(`{"table":%q}`, table)
req, _ = http.NewRequest(http.MethodPost, base+"/v1/db/drop-table", strings.NewReader(dbody)) req, _ = http.NewRequest(http.MethodPost, base+"/v1/rqlite/drop-table", strings.NewReader(dbody))
req.Header = authHeader(key) req.Header = authHeader(key)
resp, err = httpClient().Do(req) resp, err = httpClient().Do(req)
if err != nil { if err != nil {
@ -262,7 +262,7 @@ func TestGateway_Database_DropTable(t *testing.T) {
t.Fatalf("drop-table status: %d", resp.StatusCode) t.Fatalf("drop-table status: %d", resp.StatusCode)
} }
// verify not in schema // verify not in schema
req, _ = http.NewRequest(http.MethodGet, base+"/v1/db/schema", nil) req, _ = http.NewRequest(http.MethodGet, base+"/v1/rqlite/schema", nil)
req.Header = authHeader(key) req.Header = authHeader(key)
resp2, err := httpClient().Do(req) resp2, err := httpClient().Do(req)
if err != nil { if err != nil {
@ -298,7 +298,7 @@ func TestGateway_Database_RecreateWithFK(t *testing.T) {
createUsers := fmt.Sprintf(`{"schema":%q}`, fmt.Sprintf("CREATE TABLE IF NOT EXISTS %s (id INTEGER PRIMARY KEY, name TEXT, org_id INTEGER, age TEXT)", users)) createUsers := fmt.Sprintf(`{"schema":%q}`, fmt.Sprintf("CREATE TABLE IF NOT EXISTS %s (id INTEGER PRIMARY KEY, name TEXT, org_id INTEGER, age TEXT)", users))
for _, body := range []string{createOrgs, createUsers} { for _, body := range []string{createOrgs, createUsers} {
req, _ := http.NewRequest(http.MethodPost, base+"/v1/db/create-table", strings.NewReader(body)) req, _ := http.NewRequest(http.MethodPost, base+"/v1/rqlite/create-table", strings.NewReader(body))
req.Header = authHeader(key) req.Header = authHeader(key)
resp, err := httpClient().Do(req) resp, err := httpClient().Do(req)
if err != nil { if err != nil {
@ -311,7 +311,7 @@ func TestGateway_Database_RecreateWithFK(t *testing.T) {
} }
// seed data // seed data
txSeed := fmt.Sprintf(`{"statements":["INSERT INTO %s(id,name) VALUES (1,'org')","INSERT INTO %s(id,name,org_id,age) VALUES (1,'alice',1,'30')"]}`, orgs, users) txSeed := fmt.Sprintf(`{"statements":["INSERT INTO %s(id,name) VALUES (1,'org')","INSERT INTO %s(id,name,org_id,age) VALUES (1,'alice',1,'30')"]}`, orgs, users)
req, _ := http.NewRequest(http.MethodPost, base+"/v1/db/transaction", strings.NewReader(txSeed)) req, _ := http.NewRequest(http.MethodPost, base+"/v1/rqlite/transaction", strings.NewReader(txSeed))
req.Header = authHeader(key) req.Header = authHeader(key)
resp, err := httpClient().Do(req) resp, err := httpClient().Do(req)
if err != nil { if err != nil {
@ -331,7 +331,7 @@ func TestGateway_Database_RecreateWithFK(t *testing.T) {
"DROP TABLE %s", "DROP TABLE %s",
"ALTER TABLE %s_new RENAME TO %s" "ALTER TABLE %s_new RENAME TO %s"
]}`, users, orgs, users, users, users, users, users) ]}`, users, orgs, users, users, users, users, users)
req, _ = http.NewRequest(http.MethodPost, base+"/v1/db/transaction", strings.NewReader(txMig)) req, _ = http.NewRequest(http.MethodPost, base+"/v1/rqlite/transaction", strings.NewReader(txMig))
req.Header = authHeader(key) req.Header = authHeader(key)
resp, err = httpClient().Do(req) resp, err = httpClient().Do(req)
if err != nil { if err != nil {
@ -344,7 +344,7 @@ func TestGateway_Database_RecreateWithFK(t *testing.T) {
// verify schema type change // verify schema type change
qBody := fmt.Sprintf(`{"sql":"PRAGMA table_info(%s)"}`, users) qBody := fmt.Sprintf(`{"sql":"PRAGMA table_info(%s)"}`, users)
req, _ = http.NewRequest(http.MethodPost, base+"/v1/db/query", strings.NewReader(qBody)) req, _ = http.NewRequest(http.MethodPost, base+"/v1/rqlite/query", strings.NewReader(qBody))
req.Header = authHeader(key) req.Header = authHeader(key)
resp, err = httpClient().Do(req) resp, err = httpClient().Do(req)
if err != nil { if err != nil {
@ -375,7 +375,7 @@ func TestGateway_Database_RecreateWithFK(t *testing.T) {
if !ageIsInt { if !ageIsInt {
// Fallback: inspect CREATE TABLE SQL from sqlite_master // Fallback: inspect CREATE TABLE SQL from sqlite_master
qBody2 := fmt.Sprintf(`{"sql":"SELECT sql FROM sqlite_master WHERE type='table' AND name='%s'"}`, users) qBody2 := fmt.Sprintf(`{"sql":"SELECT sql FROM sqlite_master WHERE type='table' AND name='%s'"}`, users)
req2, _ := http.NewRequest(http.MethodPost, base+"/v1/db/query", strings.NewReader(qBody2)) req2, _ := http.NewRequest(http.MethodPost, base+"/v1/rqlite/query", strings.NewReader(qBody2))
req2.Header = authHeader(key) req2.Header = authHeader(key)
resp3, err := httpClient().Do(req2) resp3, err := httpClient().Do(req2)
if err != nil { if err != nil {

View File

@ -1,81 +1,110 @@
import WebSocket from 'isomorphic-ws'; import WebSocket from "isomorphic-ws";
export class GatewayClient { export class GatewayClient {
constructor(private baseUrl: string, private apiKey: string, private http = fetch) {} constructor(
private baseUrl: string,
private apiKey: string,
private http = fetch
) {}
private headers(json = true): Record<string, string> { private headers(json = true): Record<string, string> {
const h: Record<string, string> = { 'X-API-Key': this.apiKey }; const h: Record<string, string> = { "X-API-Key": this.apiKey };
if (json) h['Content-Type'] = 'application/json'; if (json) h["Content-Type"] = "application/json";
return h; return h;
} }
// Database // Database
async createTable(schema: string): Promise<void> { async createTable(schema: string): Promise<void> {
const r = await this.http(`${this.baseUrl}/v1/db/create-table`, { const r = await this.http(`${this.baseUrl}/v1/rqlite/create-table`, {
method: 'POST', headers: this.headers(), body: JSON.stringify({ schema }) method: "POST",
headers: this.headers(),
body: JSON.stringify({ schema }),
}); });
if (!r.ok) throw new Error(`createTable failed: ${r.status}`); if (!r.ok) throw new Error(`createTable failed: ${r.status}`);
} }
async dropTable(table: string): Promise<void> { async dropTable(table: string): Promise<void> {
const r = await this.http(`${this.baseUrl}/v1/db/drop-table`, { const r = await this.http(`${this.baseUrl}/v1/rqlite/drop-table`, {
method: 'POST', headers: this.headers(), body: JSON.stringify({ table }) method: "POST",
headers: this.headers(),
body: JSON.stringify({ table }),
}); });
if (!r.ok) throw new Error(`dropTable failed: ${r.status}`); if (!r.ok) throw new Error(`dropTable failed: ${r.status}`);
} }
async query<T = any>(sql: string, args: any[] = []): Promise<{ rows: T[] }> { async query<T = any>(sql: string, args: any[] = []): Promise<{ rows: T[] }> {
const r = await this.http(`${this.baseUrl}/v1/db/query`, { const r = await this.http(`${this.baseUrl}/v1/rqlite/query`, {
method: 'POST', headers: this.headers(), body: JSON.stringify({ sql, args }) method: "POST",
headers: this.headers(),
body: JSON.stringify({ sql, args }),
}); });
if (!r.ok) throw new Error(`query failed: ${r.status}`); if (!r.ok) throw new Error(`query failed: ${r.status}`);
return r.json(); return r.json();
} }
async transaction(statements: string[]): Promise<void> { async transaction(statements: string[]): Promise<void> {
const r = await this.http(`${this.baseUrl}/v1/db/transaction`, { const r = await this.http(`${this.baseUrl}/v1/rqlite/transaction`, {
method: 'POST', headers: this.headers(), body: JSON.stringify({ statements }) method: "POST",
headers: this.headers(),
body: JSON.stringify({ statements }),
}); });
if (!r.ok) throw new Error(`transaction failed: ${r.status}`); if (!r.ok) throw new Error(`transaction failed: ${r.status}`);
} }
async schema(): Promise<any> { async schema(): Promise<any> {
const r = await this.http(`${this.baseUrl}/v1/db/schema`, { headers: this.headers(false) }); const r = await this.http(`${this.baseUrl}/v1/rqlite/schema`, {
headers: this.headers(false),
});
if (!r.ok) throw new Error(`schema failed: ${r.status}`); if (!r.ok) throw new Error(`schema failed: ${r.status}`);
return r.json(); return r.json();
} }
// Storage // Storage
async put(key: string, value: Uint8Array | string): Promise<void> { async put(key: string, value: Uint8Array | string): Promise<void> {
const body = typeof value === 'string' ? new TextEncoder().encode(value) : value; const body =
const r = await this.http(`${this.baseUrl}/v1/storage/put?key=${encodeURIComponent(key)}`, { typeof value === "string" ? new TextEncoder().encode(value) : value;
method: 'POST', headers: { 'X-API-Key': this.apiKey }, body const r = await this.http(
}); `${this.baseUrl}/v1/storage/put?key=${encodeURIComponent(key)}`,
{
method: "POST",
headers: { "X-API-Key": this.apiKey },
body,
}
);
if (!r.ok) throw new Error(`put failed: ${r.status}`); if (!r.ok) throw new Error(`put failed: ${r.status}`);
} }
async get(key: string): Promise<Uint8Array> { async get(key: string): Promise<Uint8Array> {
const r = await this.http(`${this.baseUrl}/v1/storage/get?key=${encodeURIComponent(key)}`, { const r = await this.http(
headers: { 'X-API-Key': this.apiKey } `${this.baseUrl}/v1/storage/get?key=${encodeURIComponent(key)}`,
}); {
headers: { "X-API-Key": this.apiKey },
}
);
if (!r.ok) throw new Error(`get failed: ${r.status}`); if (!r.ok) throw new Error(`get failed: ${r.status}`);
const buf = new Uint8Array(await r.arrayBuffer()); const buf = new Uint8Array(await r.arrayBuffer());
return buf; return buf;
} }
async exists(key: string): Promise<boolean> { async exists(key: string): Promise<boolean> {
const r = await this.http(`${this.baseUrl}/v1/storage/exists?key=${encodeURIComponent(key)}`, { const r = await this.http(
headers: this.headers(false) `${this.baseUrl}/v1/storage/exists?key=${encodeURIComponent(key)}`,
}); {
headers: this.headers(false),
}
);
if (!r.ok) throw new Error(`exists failed: ${r.status}`); if (!r.ok) throw new Error(`exists failed: ${r.status}`);
const j = await r.json(); const j = await r.json();
return !!j.exists; return !!j.exists;
} }
async list(prefix = ""): Promise<string[]> { async list(prefix = ""): Promise<string[]> {
const r = await this.http(`${this.baseUrl}/v1/storage/list?prefix=${encodeURIComponent(prefix)}`, { const r = await this.http(
headers: this.headers(false) `${this.baseUrl}/v1/storage/list?prefix=${encodeURIComponent(prefix)}`,
}); {
headers: this.headers(false),
}
);
if (!r.ok) throw new Error(`list failed: ${r.status}`); if (!r.ok) throw new Error(`list failed: ${r.status}`);
const j = await r.json(); const j = await r.json();
return j.keys || []; return j.keys || [];
@ -83,29 +112,42 @@ export class GatewayClient {
async delete(key: string): Promise<void> { async delete(key: string): Promise<void> {
const r = await this.http(`${this.baseUrl}/v1/storage/delete`, { const r = await this.http(`${this.baseUrl}/v1/storage/delete`, {
method: 'POST', headers: this.headers(), body: JSON.stringify({ key }) method: "POST",
headers: this.headers(),
body: JSON.stringify({ key }),
}); });
if (!r.ok) throw new Error(`delete failed: ${r.status}`); if (!r.ok) throw new Error(`delete failed: ${r.status}`);
} }
// PubSub (minimal) // PubSub (minimal)
subscribe(topic: string, onMessage: (data: Uint8Array) => void): { close: () => void } { subscribe(
const url = new URL(`${this.baseUrl.replace(/^http/, 'ws')}/v1/pubsub/ws`); topic: string,
url.searchParams.set('topic', topic); onMessage: (data: Uint8Array) => void
const ws = new WebSocket(url.toString(), { headers: { 'X-API-Key': this.apiKey } } as any); ): { close: () => void } {
ws.binaryType = 'arraybuffer'; const url = new URL(`${this.baseUrl.replace(/^http/, "ws")}/v1/pubsub/ws`);
url.searchParams.set("topic", topic);
const ws = new WebSocket(url.toString(), {
headers: { "X-API-Key": this.apiKey },
} as any);
ws.binaryType = "arraybuffer";
ws.onmessage = (ev: any) => { ws.onmessage = (ev: any) => {
const data = ev.data instanceof ArrayBuffer ? new Uint8Array(ev.data) : new TextEncoder().encode(String(ev.data)); const data =
ev.data instanceof ArrayBuffer
? new Uint8Array(ev.data)
: new TextEncoder().encode(String(ev.data));
onMessage(data); onMessage(data);
}; };
return { close: () => ws.close() }; return { close: () => ws.close() };
} }
async publish(topic: string, data: Uint8Array | string): Promise<void> { async publish(topic: string, data: Uint8Array | string): Promise<void> {
const bytes = typeof data === 'string' ? new TextEncoder().encode(data) : data; const bytes =
const b64 = Buffer.from(bytes).toString('base64'); typeof data === "string" ? new TextEncoder().encode(data) : data;
const b64 = Buffer.from(bytes).toString("base64");
const r = await this.http(`${this.baseUrl}/v1/pubsub/publish`, { const r = await this.http(`${this.baseUrl}/v1/pubsub/publish`, {
method: 'POST', headers: this.headers(), body: JSON.stringify({ topic, data_base64: b64 }) method: "POST",
headers: this.headers(),
body: JSON.stringify({ topic, data_base64: b64 }),
}); });
if (!r.ok) throw new Error(`publish failed: ${r.status}`); if (!r.ok) throw new Error(`publish failed: ${r.status}`);
} }

View File

@ -192,7 +192,7 @@ paths:
key: { type: string } key: { type: string }
responses: responses:
"200": { description: OK } "200": { description: OK }
/v1/db/create-table: /v1/rqlite/create-table:
post: post:
summary: Create tables via SQL DDL summary: Create tables via SQL DDL
requestBody: requestBody:
@ -220,7 +220,7 @@ paths:
{ schema: { $ref: "#/components/schemas/Error" } }, { schema: { $ref: "#/components/schemas/Error" } },
}, },
} }
/v1/db/drop-table: /v1/rqlite/drop-table:
post: post:
summary: Drop a table summary: Drop a table
requestBody: requestBody:
@ -230,7 +230,7 @@ paths:
schema: { $ref: "#/components/schemas/DropTableRequest" } schema: { $ref: "#/components/schemas/DropTableRequest" }
responses: responses:
"200": { description: OK } "200": { description: OK }
/v1/db/query: /v1/rqlite/query:
post: post:
summary: Execute a single SQL query summary: Execute a single SQL query
requestBody: requestBody:
@ -262,7 +262,7 @@ paths:
{ schema: { $ref: "#/components/schemas/Error" } }, { schema: { $ref: "#/components/schemas/Error" } },
}, },
} }
/v1/db/transaction: /v1/rqlite/transaction:
post: post:
summary: Execute multiple SQL statements atomically summary: Execute multiple SQL statements atomically
requestBody: requestBody:
@ -290,7 +290,7 @@ paths:
{ schema: { $ref: "#/components/schemas/Error" } }, { schema: { $ref: "#/components/schemas/Error" } },
}, },
} }
/v1/db/schema: /v1/rqlite/schema:
get: get:
summary: Get current database schema summary: Get current database schema
responses: responses:

View File

@ -27,7 +27,7 @@ func (g *Gateway) Routes() http.Handler {
mux.HandleFunc("/v1/auth/logout", g.logoutHandler) mux.HandleFunc("/v1/auth/logout", g.logoutHandler)
mux.HandleFunc("/v1/auth/whoami", g.whoamiHandler) mux.HandleFunc("/v1/auth/whoami", g.whoamiHandler)
// rqlite ORM HTTP gateway (mounts /v1/db/* endpoints) // rqlite ORM HTTP gateway (mounts /v1/rqlite/* endpoints)
if g.ormHTTP != nil { if g.ormHTTP != nil {
g.ormHTTP.BasePath = "/v1/rqlite" g.ormHTTP.BasePath = "/v1/rqlite"
g.ormHTTP.RegisterRoutes(mux) g.ormHTTP.RegisterRoutes(mux)