From 3b08a91de3874de85e79db0decf17b90175b6d04 Mon Sep 17 00:00:00 2001 From: anonpenguin23 Date: Tue, 23 Sep 2025 07:42:34 +0300 Subject: [PATCH] Fixed wrong URL /v1/db to /v1/rqlite --- CHANGELOG.md | 10 +- README.md | 126 ++++++++++++++++---------- e2e/gateway_e2e_test.go | 24 ++--- examples/sdk-typescript/src/client.ts | 114 +++++++++++++++-------- openapi/gateway.yaml | 10 +- pkg/gateway/routes.go | 2 +- 6 files changed, 180 insertions(+), 106 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6459e39..dca5daa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,8 @@ The format is based on [Keep a Changelog][keepachangelog] and adheres to [Semant ### Fixed +- Fixed wrong URL /v1/db to /v1/rqlite + ### Security ## [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 readme - ### Deprecated ### Removed @@ -65,7 +66,6 @@ The format is based on [Keep a Changelog][keepachangelog] and adheres to [Semant ### Security - ## [0.43.6] - 2025-09-20 ### Added @@ -88,11 +88,13 @@ The format is based on [Keep a Changelog][keepachangelog] and adheres to [Semant ## [0.43.4] - 2025-09-18 ### Added + - Added extra comments on main.go - Remove backoff_test.go and associated backoff tests - Created node_test, write tests for CalculateNextBackoff, AddJitter, GetPeerId, LoadOrCreateIdentity, hasBootstrapConnections ### Changed + - replaced git.debros.io with github.com ### Deprecated @@ -106,20 +108,24 @@ The format is based on [Keep a Changelog][keepachangelog] and adheres to [Semant ## [0.43.3] - 2025-09-15 ### Added + - User authentication module with OAuth2 support. ### Changed + - Make file version to 0.43.2 ### Deprecated ### Removed + - Removed cli, network-cli binaries from project - Removed AI_CONTEXT.md - Removed Network.md - Removed unused log from monitoring.go ### Fixed + - Resolved race condition when saving settings. ### Security diff --git a/README.md b/README.md index ddf6f03..ef538c1 100644 --- a/README.md +++ b/README.md @@ -143,6 +143,7 @@ curl -sSL https://github.com/DeBrosOfficial/network/raw/main/scripts/install-deb ``` **What the Script Does:** + - Detects OS, installs Go, RQLite, dependencies - Creates `debros` system user, secure directory structure - 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` **Directory Structure:** + ``` /opt/debros/ ├── bin/ # Binaries @@ -163,6 +165,7 @@ curl -sSL https://github.com/DeBrosOfficial/network/raw/main/scripts/install-deb ``` **Service Management:** + ```bash sudo systemctl status 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. node: + - id (string) Optional node ID. Auto-generated if empty. - type (string) "bootstrap" or "node". Default: "node". - 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. database: + - data_dir (string) Directory for database files. Default: "./data/db". - replication_factor (int) Number of replicas. Default: 3. - 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. discovery: + - bootstrap_peers (string[]) List of LibP2P multiaddrs of bootstrap peers. - discovery_interval (duration) How often to announce/discover peers. Default: 15s. - bootstrap_port (int) Default port for bootstrap nodes. Default: 4001. @@ -286,11 +292,13 @@ discovery: - node_namespace (string) Namespace for node identifiers. Default: "default". security: + - enable_tls (bool) Enable TLS for externally exposed services. Default: false. - private_key_file (string) Path to TLS private key (if TLS enabled). - certificate_file (string) Path to TLS certificate (if TLS enabled). logging: + - level (string) one of "debug", "info", "warn", "error". Default: "info". - format (string) "json" or "console". Default: "console". - output_file (string) Empty for stdout; otherwise path to log file. @@ -347,6 +355,7 @@ logging: Precedence (gateway): Flags > Environment Variables > YAML > Defaults. Environment variables: + - GATEWAY_ADDR - GATEWAY_NAMESPACE - GATEWAY_BOOTSTRAP_PEERS (comma-separated) @@ -385,8 +394,6 @@ bootstrap_peers: ./bin/network-cli peers # List connected peers ``` - - ### Database Operations ```bash @@ -414,27 +421,27 @@ bootstrap_peers: ### Database Operations (Gateway REST) ```http -POST /v1/db/exec # Body: {"sql": "INSERT/UPDATE/DELETE/DDL ...", "args": [...]} -POST /v1/db/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/db/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/db/query # Body: {"sql": "SELECT ...", "args": [..]} (legacy-friendly SELECT) -GET /v1/db/schema # Returns tables/views + create SQL -POST /v1/db/create-table # Body: {"schema": "CREATE TABLE ..."} -POST /v1/db/drop-table # Body: {"table": "table_name"} +POST /v1/rqlite/exec # Body: {"sql": "INSERT/UPDATE/DELETE/DDL ...", "args": [...]} +POST /v1/rqlite/find # Body: {"table":"...", "criteria":{"col":val,...}, "options":{...}} +POST /v1/rqlite/find-one # Body: same as /find, returns a single row (404 if not found) +POST /v1/rqlite/select # Body: {"table":"...", "select":[...], "where":[...], "joins":[...], "order_by":[...], "limit":N, "offset":N, "one":false} +POST /v1/rqlite/transaction # Body: {"ops":[{"kind":"exec|query","sql":"...","args":[...]}], "return_results": true} +POST /v1/rqlite/query # Body: {"sql": "SELECT ...", "args": [..]} (legacy-friendly SELECT) +GET /v1/rqlite/schema # Returns tables/views + create SQL +POST /v1/rqlite/create-table # Body: {"schema": "CREATE TABLE ..."} +POST /v1/rqlite/drop-table # Body: {"table": "table_name"} ``` Common workflows: ```bash # 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' \ -d '{"sql":"INSERT INTO users(name,email) VALUES(?,?)","args":["Alice","alice@example.com"]}' # 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' \ -d '{ "table":"users", @@ -443,7 +450,7 @@ curl -X POST "$GW/v1/db/find" \ }' # 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' \ -d '{ "table":"orders o", @@ -455,7 +462,7 @@ curl -X POST "$GW/v1/db/select" \ }' # 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' \ -d '{ "return_results": true, @@ -466,12 +473,12 @@ curl -X POST "$GW/v1/db/transaction" \ }' # Schema -curl "$GW/v1/db/schema" -H "Authorization: Bearer $API_KEY" +curl "$GW/v1/rqlite/schema" -H "Authorization: Bearer $API_KEY" # 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)"}' -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"}' ``` @@ -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 When using operations that require authentication (storage, database, pubsub), the CLI will automatically: + 1. Check for existing valid credentials 2. Prompt for wallet authentication if needed 3. Handle signature verification 4. Persist credentials for future use **Example with automatic authentication:** + ```bash # First time - will prompt for wallet authentication when needed ./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: #### Key Features + - **Automatic Authentication:** No manual auth commands required - authentication happens automatically when needed - **Multi-Wallet Support:** Seamlessly manage multiple wallet credentials with automatic switching - **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 **Wallet-Based Authentication (Ethereum EIP-191)** + - Uses `personal_sign` for secure wallet verification - Supports multiple wallets with automatic detection - Addresses are case-insensitive with normalized signature handling **JWT Tokens** + - Issued by the gateway with configurable expiration - JWKS endpoints available at `/v1/auth/jwks` and `/.well-known/jwks.json` - Automatic refresh capability **API Keys** + - Support for pre-configured API keys via `Authorization: Bearer ` or `X-API-Key` headers - Optional namespace mapping for multi-tenant applications ### API Endpoints #### Health & Status + ```http GET /health # Basic health check GET /v1/health # Detailed health status @@ -562,6 +576,7 @@ GET /v1/version # Version information ``` #### Authentication (Public Endpoints) + ```http POST /v1/auth/challenge # Generate wallet challenge 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` - Endpoints: - - `POST /v1/db/exec` — Execute write/DDL SQL; returns `{ rows_affected, last_insert_id }` - - `POST /v1/db/find` — Map-based criteria; returns `{ items: [...], count: N }` - - `POST /v1/db/find-one` — Single row; 404 if not found - - `POST /v1/db/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/db/query` — Arbitrary SELECT (legacy-friendly), returns `items` - - `GET /v1/db/schema` — List user tables/views + create SQL - - `POST /v1/db/create-table` — Convenience for DDL - - `POST /v1/db/drop-table` — Safe drop (identifier validated) + - `POST /v1/rqlite/exec` — Execute write/DDL SQL; returns `{ rows_affected, last_insert_id }` + - `POST /v1/rqlite/find` — Map-based criteria; returns `{ items: [...], count: N }` + - `POST /v1/rqlite/find-one` — Single row; 404 if not found + - `POST /v1/rqlite/select` — Fluent SELECT via JSON (joins, where, order, group, limit, offset) + - `POST /v1/rqlite/transaction` — Atomic batch of exec/query ops, optional per-op results + - `POST /v1/rqlite/query` — Arbitrary SELECT (legacy-friendly), returns `items` + - `GET /v1/rqlite/schema` — List user tables/views + create SQL + - `POST /v1/rqlite/create-table` — Convenience for DDL + - `POST /v1/rqlite/drop-table` — Safe drop (identifier validated) Payload examples are shown in the [Database Operations (Gateway REST)](#database-operations-gateway-rest) section. #### Network Operations + ```http GET /v1/network/status # Network status GET /v1/network/peers # Connected peers @@ -601,11 +617,13 @@ POST /v1/network/disconnect # Disconnect from peer #### Pub/Sub Messaging **WebSocket Interface** + ```http GET /v1/pubsub/ws?topic= # WebSocket connection for real-time messaging ``` **REST Interface** + ```http POST /v1/pubsub/publish # Publish message to topic GET /v1/pubsub/topics # List active topics @@ -616,31 +634,34 @@ GET /v1/pubsub/topics # List active topics ## SDK Authoring Guide ### Base concepts + - OpenAPI: a machine-readable spec is available at `openapi/gateway.yaml` for SDK code generation. - **Auth**: send `X-API-Key: ` or `Authorization: Bearer ` with every request. - **Versioning**: all endpoints are under `/v1/`. - **Responses**: mutations return `{status:"ok"}`; queries/lists return JSON; errors return `{ "error": "message" }` with proper HTTP status. ### Key HTTP endpoints for SDKs + - **Database** - - Exec: `POST /v1/db/exec` `{sql, args?}` → `{rows_affected,last_insert_id}` - - Find: `POST /v1/db/find` `{table, criteria, options?}` → `{items,count}` - - FindOne: `POST /v1/db/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?}` - - Transaction: `POST /v1/db/transaction` `{ops:[{kind,sql,args?}], return_results?}` - - Query: `POST /v1/db/query` `{sql, args?}` → `{items,count}` - - Schema: `GET /v1/db/schema` - - Create Table: `POST /v1/db/create-table` `{schema}` - - Drop Table: `POST /v1/db/drop-table` `{table}` + - Exec: `POST /v1/rqlite/exec` `{sql, args?}` → `{rows_affected,last_insert_id}` + - Find: `POST /v1/rqlite/find` `{table, criteria, options?}` → `{items,count}` + - FindOne: `POST /v1/rqlite/find-one` `{table, criteria, options?}` → single object or 404 + - Select: `POST /v1/rqlite/select` `{table, select?, joins?, where?, order_by?, group_by?, limit?, offset?, one?}` + - Transaction: `POST /v1/rqlite/transaction` `{ops:[{kind,sql,args?}], return_results?}` + - Query: `POST /v1/rqlite/query` `{sql, args?}` → `{items,count}` + - Schema: `GET /v1/rqlite/schema` + - Create Table: `POST /v1/rqlite/create-table` `{schema}` + - Drop Table: `POST /v1/rqlite/drop-table` `{table}` - **PubSub** - WS Subscribe: `GET /v1/pubsub/ws?topic=` - Publish: `POST /v1/pubsub/publish` `{topic, data_base64}` → `{status:"ok"}` - Topics: `GET /v1/pubsub/topics` → `{topics:[...]}` ### Migrations + - Add column: `ALTER TABLE users ADD COLUMN age INTEGER` - 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 @@ -649,8 +670,13 @@ TypeScript (Node) ```ts import { GatewayClient } from "../examples/sdk-typescript/src/client"; -const client = new GatewayClient(process.env.GATEWAY_BASE_URL!, process.env.GATEWAY_API_KEY!); -await client.createTable("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)"); +const client = new GatewayClient( + 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]); ``` @@ -664,7 +690,7 @@ KEY = os.environ['GATEWAY_API_KEY'] H = { 'X-API-Key': KEY, 'Content-Type': 'application/json' } 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() return r.json()['rows'] ``` @@ -672,7 +698,7 @@ def query(sql, args=None): 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("Content-Type", "application/json") resp, err := http.DefaultClient.Do(req) @@ -688,6 +714,7 @@ resp, err := http.DefaultClient.Do(req) ### Usage Examples #### Wallet Authentication Flow + ```bash # 1. Get challenge (automatic) 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..."}' ``` - - #### Real-time Messaging + ```javascript // 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) => { - console.log('Received:', event.data); + console.log("Received:", event.data); }; // Send message -ws.send('Hello, network!'); +ws.send("Hello, network!"); ``` --- ## Development - + ### Project Structure @@ -758,6 +784,7 @@ scripts/test-multinode.sh ## Database Client (Go ORM-like) A lightweight ORM-like client over rqlite using Go’s `database/sql`. It provides: + - Query/Exec for raw SQL - A fluent QueryBuilder (`Where`, `InnerJoin`, `LeftJoin`, `OrderBy`, `GroupBy`, `Limit`, `Offset`) - Simple repositories with `Find`/`FindOne` @@ -772,7 +799,7 @@ A lightweight ORM-like client over rqlite using Go’s `database/sql`. It provid ### Quick Start -```go +````go package main import ( @@ -834,7 +861,7 @@ type Post struct { CreatedAt time.Time `db:"created_at"` } func (Post) TableName() string { return "posts" } -``` +```` ### Basic queries @@ -988,7 +1015,6 @@ if err := rqlite.ApplyMigrationsDirs(ctx, db, dirs, logger); err != nil { } ``` - --- ## Troubleshooting diff --git a/e2e/gateway_e2e_test.go b/e2e/gateway_e2e_test.go index 6a99e98..82e7f27 100644 --- a/e2e/gateway_e2e_test.go +++ b/e2e/gateway_e2e_test.go @@ -170,7 +170,7 @@ func TestGateway_Database_CreateQueryMigrate(t *testing.T) { // Create table 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) - 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) resp, err := httpClient().Do(req) if err != nil { @@ -183,7 +183,7 @@ func TestGateway_Database_CreateQueryMigrate(t *testing.T) { // Insert via transaction (simulate migration/data seed) 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) resp, err = httpClient().Do(req) if err != nil { @@ -196,7 +196,7 @@ func TestGateway_Database_CreateQueryMigrate(t *testing.T) { // Query rows 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) resp, err = httpClient().Do(req) if err != nil { @@ -219,7 +219,7 @@ func TestGateway_Database_CreateQueryMigrate(t *testing.T) { } // 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) resp2, err := httpClient().Do(req) 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) // create 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) resp, err := httpClient().Do(req) if err != nil { @@ -251,7 +251,7 @@ func TestGateway_Database_DropTable(t *testing.T) { } // drop 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) resp, err = httpClient().Do(req) if err != nil { @@ -262,7 +262,7 @@ func TestGateway_Database_DropTable(t *testing.T) { t.Fatalf("drop-table status: %d", resp.StatusCode) } // 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) resp2, err := httpClient().Do(req) 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)) 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) resp, err := httpClient().Do(req) if err != nil { @@ -311,7 +311,7 @@ func TestGateway_Database_RecreateWithFK(t *testing.T) { } // 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) - 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) resp, err := httpClient().Do(req) if err != nil { @@ -331,7 +331,7 @@ func TestGateway_Database_RecreateWithFK(t *testing.T) { "DROP TABLE %s", "ALTER TABLE %s_new RENAME TO %s" ]}`, 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) resp, err = httpClient().Do(req) if err != nil { @@ -344,7 +344,7 @@ func TestGateway_Database_RecreateWithFK(t *testing.T) { // verify schema type change 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) resp, err = httpClient().Do(req) if err != nil { @@ -375,7 +375,7 @@ func TestGateway_Database_RecreateWithFK(t *testing.T) { if !ageIsInt { // Fallback: inspect CREATE TABLE SQL from sqlite_master 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) resp3, err := httpClient().Do(req2) if err != nil { diff --git a/examples/sdk-typescript/src/client.ts b/examples/sdk-typescript/src/client.ts index 154efe7..bf80606 100644 --- a/examples/sdk-typescript/src/client.ts +++ b/examples/sdk-typescript/src/client.ts @@ -1,81 +1,110 @@ -import WebSocket from 'isomorphic-ws'; +import WebSocket from "isomorphic-ws"; 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 { - const h: Record = { 'X-API-Key': this.apiKey }; - if (json) h['Content-Type'] = 'application/json'; + const h: Record = { "X-API-Key": this.apiKey }; + if (json) h["Content-Type"] = "application/json"; return h; } // Database async createTable(schema: string): Promise { - const r = await this.http(`${this.baseUrl}/v1/db/create-table`, { - method: 'POST', headers: this.headers(), body: JSON.stringify({ schema }) + const r = await this.http(`${this.baseUrl}/v1/rqlite/create-table`, { + method: "POST", + headers: this.headers(), + body: JSON.stringify({ schema }), }); if (!r.ok) throw new Error(`createTable failed: ${r.status}`); } async dropTable(table: string): Promise { - const r = await this.http(`${this.baseUrl}/v1/db/drop-table`, { - method: 'POST', headers: this.headers(), body: JSON.stringify({ table }) + const r = await this.http(`${this.baseUrl}/v1/rqlite/drop-table`, { + method: "POST", + headers: this.headers(), + body: JSON.stringify({ table }), }); if (!r.ok) throw new Error(`dropTable failed: ${r.status}`); } async query(sql: string, args: any[] = []): Promise<{ rows: T[] }> { - const r = await this.http(`${this.baseUrl}/v1/db/query`, { - method: 'POST', headers: this.headers(), body: JSON.stringify({ sql, args }) + const r = await this.http(`${this.baseUrl}/v1/rqlite/query`, { + method: "POST", + headers: this.headers(), + body: JSON.stringify({ sql, args }), }); if (!r.ok) throw new Error(`query failed: ${r.status}`); return r.json(); } async transaction(statements: string[]): Promise { - const r = await this.http(`${this.baseUrl}/v1/db/transaction`, { - method: 'POST', headers: this.headers(), body: JSON.stringify({ statements }) + const r = await this.http(`${this.baseUrl}/v1/rqlite/transaction`, { + method: "POST", + headers: this.headers(), + body: JSON.stringify({ statements }), }); if (!r.ok) throw new Error(`transaction failed: ${r.status}`); } async schema(): Promise { - 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}`); return r.json(); } // Storage async put(key: string, value: Uint8Array | string): Promise { - const body = typeof value === 'string' ? new TextEncoder().encode(value) : value; - const r = await this.http(`${this.baseUrl}/v1/storage/put?key=${encodeURIComponent(key)}`, { - method: 'POST', headers: { 'X-API-Key': this.apiKey }, body - }); + const body = + typeof value === "string" ? new TextEncoder().encode(value) : value; + 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}`); } async get(key: string): Promise { - const r = await this.http(`${this.baseUrl}/v1/storage/get?key=${encodeURIComponent(key)}`, { - headers: { 'X-API-Key': this.apiKey } - }); + const r = await this.http( + `${this.baseUrl}/v1/storage/get?key=${encodeURIComponent(key)}`, + { + headers: { "X-API-Key": this.apiKey }, + } + ); if (!r.ok) throw new Error(`get failed: ${r.status}`); const buf = new Uint8Array(await r.arrayBuffer()); return buf; } async exists(key: string): Promise { - const r = await this.http(`${this.baseUrl}/v1/storage/exists?key=${encodeURIComponent(key)}`, { - headers: this.headers(false) - }); + const r = await this.http( + `${this.baseUrl}/v1/storage/exists?key=${encodeURIComponent(key)}`, + { + headers: this.headers(false), + } + ); if (!r.ok) throw new Error(`exists failed: ${r.status}`); const j = await r.json(); return !!j.exists; } async list(prefix = ""): Promise { - const r = await this.http(`${this.baseUrl}/v1/storage/list?prefix=${encodeURIComponent(prefix)}`, { - headers: this.headers(false) - }); + const r = await this.http( + `${this.baseUrl}/v1/storage/list?prefix=${encodeURIComponent(prefix)}`, + { + headers: this.headers(false), + } + ); if (!r.ok) throw new Error(`list failed: ${r.status}`); const j = await r.json(); return j.keys || []; @@ -83,29 +112,42 @@ export class GatewayClient { async delete(key: string): Promise { 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}`); } // PubSub (minimal) - subscribe(topic: string, onMessage: (data: Uint8Array) => void): { close: () => void } { - 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'; + subscribe( + topic: string, + onMessage: (data: Uint8Array) => void + ): { close: () => void } { + 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) => { - 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); }; return { close: () => ws.close() }; } async publish(topic: string, data: Uint8Array | string): Promise { - const bytes = typeof data === 'string' ? new TextEncoder().encode(data) : data; - const b64 = Buffer.from(bytes).toString('base64'); + const bytes = + 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`, { - 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}`); } diff --git a/openapi/gateway.yaml b/openapi/gateway.yaml index 6150344..489f26e 100644 --- a/openapi/gateway.yaml +++ b/openapi/gateway.yaml @@ -192,7 +192,7 @@ paths: key: { type: string } responses: "200": { description: OK } - /v1/db/create-table: + /v1/rqlite/create-table: post: summary: Create tables via SQL DDL requestBody: @@ -220,7 +220,7 @@ paths: { schema: { $ref: "#/components/schemas/Error" } }, }, } - /v1/db/drop-table: + /v1/rqlite/drop-table: post: summary: Drop a table requestBody: @@ -230,7 +230,7 @@ paths: schema: { $ref: "#/components/schemas/DropTableRequest" } responses: "200": { description: OK } - /v1/db/query: + /v1/rqlite/query: post: summary: Execute a single SQL query requestBody: @@ -262,7 +262,7 @@ paths: { schema: { $ref: "#/components/schemas/Error" } }, }, } - /v1/db/transaction: + /v1/rqlite/transaction: post: summary: Execute multiple SQL statements atomically requestBody: @@ -290,7 +290,7 @@ paths: { schema: { $ref: "#/components/schemas/Error" } }, }, } - /v1/db/schema: + /v1/rqlite/schema: get: summary: Get current database schema responses: diff --git a/pkg/gateway/routes.go b/pkg/gateway/routes.go index 46154f1..4ad7cc9 100644 --- a/pkg/gateway/routes.go +++ b/pkg/gateway/routes.go @@ -27,7 +27,7 @@ func (g *Gateway) Routes() http.Handler { mux.HandleFunc("/v1/auth/logout", g.logoutHandler) 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 { g.ormHTTP.BasePath = "/v1/rqlite" g.ormHTTP.RegisterRoutes(mux)