Skip to content

Commit fc30b16

Browse files
mpywclaude
andcommitted
examples: add stupid_todolist demo app
A minimal todo list demonstrating sql-http-proxy with: - SQLite in-memory database - Pure HTML/CSS/JavaScript frontend - CORS handling via global_helpers - Full CRUD operations Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 961142a commit fc30b16

5 files changed

Lines changed: 511 additions & 0 deletions

File tree

examples/stupid_todolist/README.md

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# Stupid Todo List
2+
3+
A minimal todo list application demonstrating sql-http-proxy with SQLite in-memory database.
4+
5+
## Requirements
6+
7+
- sql-http-proxy binary
8+
- A web browser
9+
10+
## How to Run
11+
12+
### 1. Start the API server
13+
14+
```bash
15+
cd examples/stupid_todolist
16+
sql-http-proxy -c config.yaml -l :8080
17+
```
18+
19+
### 2. Open the frontend
20+
21+
Open `index.html` in your browser:
22+
23+
```bash
24+
# macOS
25+
open index.html
26+
27+
# Linux
28+
xdg-open index.html
29+
30+
# Windows
31+
start index.html
32+
```
33+
34+
Or use a simple HTTP server:
35+
36+
```bash
37+
# Python 3
38+
python -m http.server 3000
39+
40+
# Then open http://localhost:3000
41+
```
42+
43+
### 3. Initialize the database
44+
45+
Click the "Initialize Database" button to create the todos table.
46+
47+
## Architecture
48+
49+
```
50+
Browser (index.html)
51+
|
52+
| fetch() with CORS
53+
v
54+
sql-http-proxy (:8080)
55+
|
56+
| SQL
57+
v
58+
SQLite (in-memory)
59+
```
60+
61+
## API Endpoints
62+
63+
| Method | Path | Description |
64+
|--------|------|-------------|
65+
| POST | /api/init | Initialize database (create table) |
66+
| GET | /api/todos | List all todos |
67+
| GET | /api/todos/:id | Get single todo |
68+
| POST | /api/todos | Create todo |
69+
| PUT | /api/todos/:id | Update todo |
70+
| DELETE | /api/todos/:id | Delete todo |
71+
72+
## Notes
73+
74+
- Uses SQLite in-memory database (`file::memory:?cache=shared`)
75+
- Data is lost when the server stops
76+
- CORS headers are added via `global_helpers` and `response.headers.set()`

examples/stupid_todolist/app.js

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
const API_BASE = 'http://localhost:8080/api';
2+
3+
const todoForm = document.getElementById('todo-form');
4+
const todoInput = document.getElementById('todo-input');
5+
const todoList = document.getElementById('todo-list');
6+
const statusDiv = document.getElementById('status');
7+
const initBtn = document.getElementById('init-btn');
8+
9+
// Show status message
10+
function showStatus(message, isError = false) {
11+
statusDiv.textContent = message;
12+
statusDiv.className = 'status ' + (isError ? 'error' : 'success');
13+
setTimeout(() => {
14+
statusDiv.textContent = '';
15+
statusDiv.className = 'status';
16+
}, 3000);
17+
}
18+
19+
// API helpers
20+
async function api(endpoint, options = {}) {
21+
const res = await fetch(`${API_BASE}${endpoint}`, {
22+
headers: { 'Content-Type': 'application/json' },
23+
...options,
24+
});
25+
if (!res.ok && res.status !== 204) {
26+
const error = await res.json().catch(() => ({ error: res.statusText }));
27+
throw new Error(error.error || error.message || 'Request failed');
28+
}
29+
if (res.status === 204) return null;
30+
return res.json();
31+
}
32+
33+
// Initialize database
34+
async function initDatabase() {
35+
try {
36+
initBtn.disabled = true;
37+
await api('/init', { method: 'POST' });
38+
showStatus('Database initialized!');
39+
loadTodos();
40+
} catch (err) {
41+
showStatus('Init failed: ' + err.message, true);
42+
} finally {
43+
initBtn.disabled = false;
44+
}
45+
}
46+
47+
// Load all todos
48+
async function loadTodos() {
49+
try {
50+
todoList.classList.add('loading');
51+
const todos = await api('/todos');
52+
renderTodos(todos);
53+
} catch (err) {
54+
showStatus('Failed to load: ' + err.message, true);
55+
} finally {
56+
todoList.classList.remove('loading');
57+
}
58+
}
59+
60+
// Render todos
61+
function renderTodos(todos) {
62+
todoList.innerHTML = todos.map(todo => `
63+
<li class="todo-item ${todo.completed ? 'completed' : ''}" data-id="${todo.id}">
64+
<input type="checkbox" ${todo.completed ? 'checked' : ''} onchange="toggleTodo(${todo.id}, this.checked)">
65+
<span class="title">${escapeHtml(todo.title)}</span>
66+
<button class="delete-btn" onclick="deleteTodo(${todo.id})">Delete</button>
67+
</li>
68+
`).join('');
69+
}
70+
71+
// Escape HTML
72+
function escapeHtml(text) {
73+
const div = document.createElement('div');
74+
div.textContent = text;
75+
return div.innerHTML;
76+
}
77+
78+
// Add todo
79+
async function addTodo(title) {
80+
try {
81+
todoInput.disabled = true;
82+
await api('/todos', {
83+
method: 'POST',
84+
body: JSON.stringify({ title }),
85+
});
86+
todoInput.value = '';
87+
loadTodos();
88+
} catch (err) {
89+
showStatus('Failed to add: ' + err.message, true);
90+
} finally {
91+
todoInput.disabled = false;
92+
todoInput.focus();
93+
}
94+
}
95+
96+
// Toggle todo
97+
async function toggleTodo(id, completed) {
98+
try {
99+
const todo = await api(`/todos/${id}`);
100+
await api(`/todos/${id}`, {
101+
method: 'PUT',
102+
body: JSON.stringify({ ...todo, completed }),
103+
});
104+
loadTodos();
105+
} catch (err) {
106+
showStatus('Failed to update: ' + err.message, true);
107+
loadTodos();
108+
}
109+
}
110+
111+
// Delete todo
112+
async function deleteTodo(id) {
113+
try {
114+
await api(`/todos/${id}`, { method: 'DELETE' });
115+
loadTodos();
116+
} catch (err) {
117+
showStatus('Failed to delete: ' + err.message, true);
118+
}
119+
}
120+
121+
// Event listeners
122+
todoForm.addEventListener('submit', (e) => {
123+
e.preventDefault();
124+
const title = todoInput.value.trim();
125+
if (title) addTodo(title);
126+
});
127+
128+
initBtn.addEventListener('click', initDatabase);
129+
130+
// Initial load
131+
loadTodos();
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
# Stupid Todo List - sql-http-proxy example
2+
# Uses SQLite in-memory database
3+
4+
dsn: "file::memory:?cache=shared"
5+
6+
global_helpers: |
7+
function cors() {
8+
response.headers.set('Access-Control-Allow-Origin', '*');
9+
response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
10+
response.headers.set('Access-Control-Allow-Headers', 'Content-Type');
11+
}
12+
13+
queries:
14+
# Initialize database (call once on startup)
15+
- type: none
16+
path: /api/init
17+
sql: |
18+
CREATE TABLE IF NOT EXISTS todos (
19+
id INTEGER PRIMARY KEY AUTOINCREMENT,
20+
title TEXT NOT NULL,
21+
completed INTEGER DEFAULT 0,
22+
created_at TEXT DEFAULT (datetime('now'))
23+
)
24+
transform:
25+
post: |
26+
cors();
27+
return { ok: true };
28+
29+
# List all todos
30+
- type: many
31+
path: /api/todos
32+
sql: SELECT id, title, completed, created_at FROM todos ORDER BY created_at DESC
33+
transform:
34+
post: |
35+
cors();
36+
return output.map(row => ({
37+
...row,
38+
completed: Boolean(row.completed)
39+
}));
40+
41+
# Get single todo
42+
- type: one
43+
path: /api/todos/{id:[0-9]+}
44+
sql: SELECT id, title, completed, created_at FROM todos WHERE id = :id
45+
transform:
46+
post: |
47+
cors();
48+
return { ...output, completed: Boolean(output.completed) };
49+
50+
mutations:
51+
# Create todo
52+
- type: one
53+
method: POST
54+
path: /api/todos
55+
sql: |
56+
INSERT INTO todos (title) VALUES (:title)
57+
RETURNING id, title, completed, created_at
58+
transform:
59+
post: |
60+
cors();
61+
response.status = 201;
62+
return { ...output, completed: Boolean(output.completed) };
63+
64+
# Update todo
65+
- type: one
66+
method: PUT
67+
path: /api/todos/{id:[0-9]+}
68+
sql: |
69+
UPDATE todos SET title = :title, completed = :completed WHERE id = :id
70+
RETURNING id, title, completed, created_at
71+
transform:
72+
pre: |
73+
return {
74+
...input,
75+
completed: input.completed ? 1 : 0
76+
};
77+
post: |
78+
cors();
79+
return { ...output, completed: Boolean(output.completed) };
80+
81+
# Delete todo
82+
- type: none
83+
method: DELETE
84+
path: /api/todos/{id:[0-9]+}
85+
sql: DELETE FROM todos WHERE id = :id
86+
transform:
87+
post: |
88+
cors();
89+
response.status = 204;
90+
return null;
91+
92+
# CORS preflight
93+
- type: none
94+
method: OPTIONS
95+
path: /api/todos
96+
mock:
97+
object: {}
98+
transform:
99+
post: |
100+
cors();
101+
response.status = 204;
102+
return null;
103+
104+
- type: none
105+
method: OPTIONS
106+
path: /api/todos/{id:[0-9]+}
107+
mock:
108+
object: {}
109+
transform:
110+
post: |
111+
cors();
112+
response.status = 204;
113+
return null;
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>Stupid Todo List</title>
7+
<link rel="stylesheet" href="style.css">
8+
</head>
9+
<body>
10+
<div class="container">
11+
<h1>Stupid Todo List</h1>
12+
<p class="subtitle">Powered by sql-http-proxy + SQLite</p>
13+
14+
<form id="todo-form">
15+
<input type="text" id="todo-input" placeholder="What needs to be done?" required>
16+
<button type="submit">Add</button>
17+
</form>
18+
19+
<div id="status" class="status"></div>
20+
21+
<ul id="todo-list"></ul>
22+
23+
<div class="footer">
24+
<button id="init-btn" class="init-btn">Initialize Database</button>
25+
</div>
26+
</div>
27+
28+
<script src="app.js"></script>
29+
</body>
30+
</html>

0 commit comments

Comments
 (0)