Skip to content

Commit db9b22c

Browse files
mpywclaude
andcommitted
feat: restructure config and add database.init support
BREAKING CHANGE: Config structure changed from flat to nested format - Change `dsn` to `database.dsn` - Change `cors` to `http.cors` with extended options - Add `database.init` for startup SQL execution (inline or sql_files) - Add CORS configuration: allowed_origins, allow_credentials, max_age - Support `mock: true` for type: none mutations - Add 500 error logging in responder - Update stupid_todolist example to use database.init Config migration: Before: dsn: "..." / cors: true After: database: { dsn: "..." } / http: { cors: true } Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent fc30b16 commit db9b22c

99 files changed

Lines changed: 1048 additions & 245 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CLAUDE.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,11 @@ go test ./...
4444
Example `sql-http-proxy.yaml`:
4545

4646
```yaml
47-
dsn: postgres://user:pass@localhost:5432/db?sslmode=disable
47+
database:
48+
dsn: postgres://user:pass@localhost:5432/db?sslmode=disable
49+
50+
http:
51+
cors: true # Enable permissive CORS, or use object for detailed config
4852

4953
queries:
5054
- type: one
@@ -56,7 +60,8 @@ queries:
5660
sql: SELECT * FROM users LIMIT :limit
5761
```
5862
59-
- `dsn`: Database connection string (or use `SQL_PROXY_DSN` environment variable)
63+
- `database.dsn`: Database connection string (supports `${VAR}` env expansion)
64+
- `http.cors`: CORS config - `true` for permissive, or object with `allowed_origins`, `allow_credentials`, `max_age`
6065
- `queries[].type`: `one` for single row, `many` for multiple rows
6166
- `queries[].path`: HTTP endpoint path
6267
- `queries[].sql`: SQL query with named placeholders (`:name`)

README.md

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,8 @@ go install -tags mock github.com/mpyw/sql-http-proxy/cmd/sql-http-proxy@latest
8484
Create `.sql-http-proxy.yaml`:
8585

8686
```yaml
87-
dsn: postgres://${DB_USER}:${DB_PASSWORD}@${DB_HOST:-localhost}:${DB_PORT:-5432}/mydb
87+
database:
88+
dsn: postgres://${DB_USER}:${DB_PASSWORD}@${DB_HOST:-localhost}:${DB_PORT:-5432}/mydb
8889

8990
queries:
9091
- type: many
@@ -147,12 +148,16 @@ queries:
147148

148149
See [SCHEMA.md](SCHEMA.md) for complete reference and [sql-http-proxy.example.yaml](sql-http-proxy.example.yaml) for examples.
149150

150-
## DSN
151+
## Database & HTTP Configuration
151152

152-
Supports `${VAR}` environment variable expansion:
153+
Database connection with `${VAR}` environment variable expansion:
153154

154155
```yaml
155-
dsn: postgres://${DB_USER}:${DB_PASSWORD}@${DB_HOST:-localhost}:${DB_PORT:-5432}/mydb
156+
database:
157+
dsn: postgres://${DB_USER}:${DB_PASSWORD}@${DB_HOST:-localhost}:${DB_PORT:-5432}/mydb
158+
159+
http:
160+
cors: true # or: { allowed_origins: [...], allow_credentials: true, max_age: 86400 }
156161
```
157162

158163
## Queries & Mutations

SCHEMA.md

Lines changed: 78 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,11 @@ This document describes all configuration options for sql-http-proxy. For usage
55
## Table of Contents
66

77
- [Top-Level Options](#top-level-options)
8-
- [DSN](#dsn)
8+
- [Database Configuration](#database-configuration)
9+
- [Database Init](#database-init)
910
- [Driver Examples](#driver-examples)
11+
- [HTTP Configuration](#http-configuration)
12+
- [CORS](#cors)
1013
- [Global Helpers](#global-helpers)
1114
- [CSV Config](#csv-config)
1215
- [Queries](#queries)
@@ -40,22 +43,60 @@ This document describes all configuration options for sql-http-proxy. For usage
4043

4144
| Option | Type | Required | Description |
4245
|--------|------|----------|-------------|
43-
| [`dsn`](#dsn) | string | No* | Database connection string. Supports `${VAR}`, `${VAR:-default}` env expansion |
46+
| [`database`](#database-configuration) | object | No* | Database connection configuration |
47+
| [`http`](#http-configuration) | object | No | HTTP server configuration (CORS, etc.) |
4448
| [`global_helpers`](#global-helpers) | object/string | No | JavaScript helpers for all transforms |
4549
| [`csv`](#csv-config) | object | No | CSV parsing options |
4650
| [`queries`](#queries) | array | No | Query endpoints (SELECT) |
4751
| [`mutations`](#mutations) | array | No | Mutation endpoints (INSERT/UPDATE/DELETE) |
4852

4953
> *Required unless all endpoints use [mock](#mock)
5054
51-
## DSN
55+
## Database Configuration
5256

53-
Database connection string with `${VAR}`, `$VAR`, or `${VAR:-default}` environment variable expansion.
57+
Database connection configuration with `${VAR}`, `$VAR`, or `${VAR:-default}` environment variable expansion.
5458

5559
```yaml
56-
dsn: postgres://${DB_USER}:${DB_PASSWORD}@${DB_HOST:-localhost}:${DB_PORT:-5432}/mydb
60+
database:
61+
dsn: postgres://${DB_USER}:${DB_PASSWORD}@${DB_HOST:-localhost}:${DB_PORT:-5432}/mydb
5762
```
5863
64+
| Property | Type | Required | Description |
65+
|----------|------|----------|-------------|
66+
| `dsn` | string | Yes | Database connection string with env var expansion |
67+
| `init` | string/object | No | SQL to execute on startup (e.g., schema creation) |
68+
69+
### Database Init
70+
71+
Execute SQL when the database connection is established. Useful for creating tables, seeding data, or SQLite in-memory databases.
72+
73+
```yaml
74+
# Shorthand (inline SQL only)
75+
database:
76+
dsn: "sqlite::memory:"
77+
init: |
78+
CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT);
79+
INSERT INTO users (id, name) VALUES (1, 'Alice');
80+
81+
# Full form (with sql_files)
82+
database:
83+
dsn: "sqlite::memory:"
84+
init:
85+
sql: |
86+
CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT);
87+
sql_files:
88+
- ./migrations/001_init.sql
89+
- ./migrations/002_seed.sql
90+
```
91+
92+
| Property | Type | Description |
93+
|----------|------|-------------|
94+
| `sql` | string | Inline SQL code to execute |
95+
| `sql_files` | string[] | Paths to SQL files (relative to config file) |
96+
97+
> [!TIP]
98+
> **Shorthand:** `init: |` is equivalent to `init: { sql: | }`
99+
59100
### Driver Examples
60101

61102
| Database | DSN Format |
@@ -65,6 +106,38 @@ dsn: postgres://${DB_USER}:${DB_PASSWORD}@${DB_HOST:-localhost}:${DB_PORT:-5432}
65106
| SQLite | `file:./data.db` or `sqlite:./data.db` |
66107
| SQL Server | `sqlserver://user:pass@localhost:1433?database=db` |
67108

109+
## HTTP Configuration
110+
111+
HTTP server configuration including CORS settings.
112+
113+
```yaml
114+
http:
115+
cors: true # Permissive CORS (Access-Control-Allow-Origin: *)
116+
```
117+
118+
Or with detailed configuration:
119+
120+
```yaml
121+
http:
122+
cors:
123+
allowed_origins:
124+
- https://example.com
125+
- https://app.example.com
126+
allow_credentials: true
127+
max_age: 86400
128+
```
129+
130+
### CORS
131+
132+
| Property | Type | Required | Description |
133+
|----------|------|----------|-------------|
134+
| `allowed_origins` | string[] | Yes (if object) | List of allowed origins. Use `["*"]` for all origins |
135+
| `allow_credentials` | boolean | No | Allow credentials (cookies, auth headers). Default: `false` |
136+
| `max_age` | integer | No | Preflight cache duration in seconds. Default: `0` |
137+
138+
> [!TIP]
139+
> Use `cors: true` for permissive development mode. For production, specify `allowed_origins` explicitly.
140+
68141
## Global Helpers
69142

70143
JavaScript functions available in all [`pre`](#pre-transform), [`post`](#post-transform) transforms, [mock JS sources](#mock-js-variables), [`filter`](#filter), and [`csv.value_parser`](#csv-config).

e2e/body_parse_edge_test.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
dsn: postgres://localhost/test
1+
database:
2+
dsn: postgres://localhost/test
23

34
mutations:
45
- type: one

e2e/body_parse_error_test.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
dsn: postgres://localhost/test
1+
database:
2+
dsn: postgres://localhost/test
23

34
mutations:
45
- type: one

e2e/content_type_default_test.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
dsn: "sqlite::memory:"
1+
database:
2+
dsn: "sqlite::memory:"
23

34
mutations:
45
- type: one

e2e/content_type_form_test.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
dsn: "sqlite::memory:"
1+
database:
2+
dsn: "sqlite::memory:"
23

34
mutations:
45
- type: one

e2e/content_type_json_test.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
dsn: "sqlite::memory:"
1+
database:
2+
dsn: "sqlite::memory:"
23

34
mutations:
45
- type: one

e2e/database_init_test.go

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
package e2e
2+
3+
import (
4+
"encoding/json"
5+
"net/http"
6+
"net/http/httptest"
7+
"os"
8+
"path/filepath"
9+
"testing"
10+
11+
"github.com/stretchr/testify/assert"
12+
"github.com/stretchr/testify/require"
13+
14+
"github.com/mpyw/sql-http-proxy/internal/config"
15+
"github.com/mpyw/sql-http-proxy/internal/db"
16+
"github.com/mpyw/sql-http-proxy/internal/server"
17+
)
18+
19+
func TestDatabaseInit_InlineSQL(t *testing.T) {
20+
cfg := loadConfig(t, "database_init_test.yaml")
21+
22+
// Connect to database - this should execute init SQL
23+
configDir, _ := filepath.Abs(".")
24+
conn, err := db.Connect(cfg, configDir)
25+
require.NoError(t, err)
26+
require.NotNil(t, conn)
27+
defer conn.Close()
28+
29+
// Create server handler
30+
mux, err := server.NewServeMux(conn, cfg, configDir)
31+
require.NoError(t, err)
32+
33+
// Test that data was initialized
34+
req := httptest.NewRequest("GET", "/products", nil)
35+
w := httptest.NewRecorder()
36+
mux.ServeHTTP(w, req)
37+
38+
require.Equal(t, http.StatusOK, w.Code)
39+
40+
var result []map[string]any
41+
require.NoError(t, json.NewDecoder(w.Body).Decode(&result))
42+
43+
// Verify init SQL created table and inserted data
44+
require.Len(t, result, 2)
45+
assert.Equal(t, float64(1), result[0]["id"])
46+
assert.Equal(t, "Widget", result[0]["name"])
47+
assert.Equal(t, 19.99, result[0]["price"])
48+
assert.Equal(t, float64(2), result[1]["id"])
49+
assert.Equal(t, "Gadget", result[1]["name"])
50+
assert.Equal(t, 29.99, result[1]["price"])
51+
}
52+
53+
func TestDatabaseInit_SQLFile(t *testing.T) {
54+
// Create test config and SQL file in temp directory
55+
tmpDir := t.TempDir()
56+
57+
// Create SQL init file
58+
sqlFile := filepath.Join(tmpDir, "init.sql")
59+
err := os.WriteFile(sqlFile, []byte(`
60+
CREATE TABLE IF NOT EXISTS categories (
61+
id INTEGER PRIMARY KEY,
62+
name TEXT NOT NULL
63+
);
64+
DELETE FROM categories;
65+
INSERT INTO categories (id, name) VALUES (1, 'Electronics');
66+
INSERT INTO categories (id, name) VALUES (2, 'Books');
67+
`), 0644)
68+
require.NoError(t, err)
69+
70+
// Create config file
71+
configFile := filepath.Join(tmpDir, "config.yaml")
72+
err = os.WriteFile(configFile, []byte(`
73+
database:
74+
dsn: "sqlite::memory:"
75+
init:
76+
sql_files:
77+
- ./init.sql
78+
79+
queries:
80+
- type: many
81+
path: /categories
82+
sql: SELECT * FROM categories ORDER BY id
83+
`), 0644)
84+
require.NoError(t, err)
85+
86+
// Parse config
87+
cfg, err := config.ParseFile(configFile)
88+
require.NoError(t, err)
89+
90+
// Connect to database - this should execute init SQL files
91+
conn, err := db.Connect(cfg, tmpDir)
92+
require.NoError(t, err)
93+
require.NotNil(t, conn)
94+
defer conn.Close()
95+
96+
// Create server handler
97+
mux, err := server.NewServeMux(conn, cfg, tmpDir)
98+
require.NoError(t, err)
99+
100+
// Test that data was initialized
101+
req := httptest.NewRequest("GET", "/categories", nil)
102+
w := httptest.NewRecorder()
103+
mux.ServeHTTP(w, req)
104+
105+
require.Equal(t, http.StatusOK, w.Code)
106+
107+
var result []map[string]any
108+
require.NoError(t, json.NewDecoder(w.Body).Decode(&result))
109+
110+
// Verify init SQL file created table and inserted data
111+
require.Len(t, result, 2)
112+
assert.Equal(t, float64(1), result[0]["id"])
113+
assert.Equal(t, "Electronics", result[0]["name"])
114+
assert.Equal(t, float64(2), result[1]["id"])
115+
assert.Equal(t, "Books", result[1]["name"])
116+
}

e2e/database_init_test.yaml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
database:
2+
dsn: "sqlite::memory:"
3+
init: |
4+
CREATE TABLE IF NOT EXISTS products (
5+
id INTEGER PRIMARY KEY,
6+
name TEXT NOT NULL,
7+
price REAL NOT NULL
8+
);
9+
DELETE FROM products;
10+
INSERT INTO products (id, name, price) VALUES (1, 'Widget', 19.99);
11+
INSERT INTO products (id, name, price) VALUES (2, 'Gadget', 29.99);
12+
13+
queries:
14+
- type: many
15+
path: /products
16+
sql: SELECT * FROM products ORDER BY id

0 commit comments

Comments
 (0)