feat: switch from MySQL to SQLite for zero-dependency deployment

- Rewrite db.php to use SQLite via PDO (no external DB server needed)
- Rewrite schema.sql for SQLite (AUTOINCREMENT, CHECK constraints,
  triggers for updated_at, ON CONFLICT instead of ON DUPLICATE KEY)
- Fix config.php upsert to use ON CONFLICT syntax
- Fix index.php: tag filter via json_each, vote upsert via ON CONFLICT,
  and fix protocol create route (was returning 405 for POST /api/protocols)
- Fix yaml.php seed import: replace invalid false callbacks with fn() => null,
  fix yaml_parse pos argument (-1 -> 0)
- Update Dockerfile: drop pdo_mysql/mysqli (pdo_sqlite is built in)
- Update CloudronManifest.json: remove mysql addon, bump to v0.3.0
- Update README for SQLite deployment
This commit is contained in:
Protocolbot
2026-07-06 14:42:45 -06:00
parent 7379d0c540
commit 3386ac6542
8 changed files with 155 additions and 132 deletions
Executable → Regular
+45 -43
View File
@@ -169,29 +169,56 @@ if ($parts[0] === 'protocols') {
if (!$slug) {
// List protocols
if ($method !== 'GET') respond(405, ['error' => 'Method not allowed']);
if ($method === 'GET') {
$where = "WHERE p.visibility = 'public'";
$params = [];
$where = "WHERE p.visibility = 'public'";
$params = [];
$tag = $_GET['tag'] ?? null;
if ($tag) {
// Match tag within the JSON array string
$where .= " AND EXISTS (SELECT 1 FROM json_each(p.tags) WHERE value = ?)";
$params[] = $tag;
}
$tag = $_GET['tag'] ?? null;
if ($tag) {
// Use JSON_CONTAINS for accurate tag matching (MySQL 5.7+)
$where .= " AND JSON_CONTAINS(p.tags, JSON_QUOTE(?))";
$params[] = $tag;
$q = $_GET['q'] ?? null;
if ($q) {
$where .= " AND (p.title LIKE ? OR p.description LIKE ? OR p.source LIKE ?)";
$params = array_merge($params, ["%$q%", "%$q%", "%$q%"]);
}
$sql = "SELECT p.*, u.username AS author_username
FROM protocols p LEFT JOIN users u ON p.author_id = u.id
$where ORDER BY p.created_at DESC";
$rows = db_all($sql, $params);
respond(200, array_map('format_protocol', $rows));
}
$q = $_GET['q'] ?? null;
if ($q) {
$where .= " AND (p.title LIKE ? OR p.description LIKE ? OR p.source LIKE ?)";
$params = array_merge($params, ["%$q%", "%$q%", "%$q%"]);
// Create protocol
if ($method === 'POST') {
$user = require_member();
$data = json_body();
$new_slug = ensure_unique_slug($data['slug'] ?? slugify($data['title'] ?? 'untitled'));
$id = db_insert('protocols', [
'slug' => $new_slug,
'title' => substr($data['title'] ?? '', 0, 255),
'description' => $data['description'] ?? '',
'source' => $data['source'] ?? ('@' . $user['username']),
'source_url' => substr($data['source_url'] ?? '', 0, 500),
'tags' => json_encode(array_slice($data['tags'] ?? [], 0, 20)),
'forked_from' => $data['forked_from'] ?? null,
'image' => $data['image'] ?? null,
'steps' => json_encode(array_slice($data['steps'] ?? [], 0, 50)),
'outcome' => $data['outcome'] ?? '',
'practice' => $data['practice'] ?? '',
'author_id' => $user['id'],
'visibility' => validate_visibility($data['visibility'] ?? 'public'),
]);
$row = db_one("SELECT p.*, u.username AS author_username FROM protocols p LEFT JOIN users u ON p.author_id = u.id WHERE p.id = ?", [$id]);
respond(201, format_protocol($row));
}
$sql = "SELECT p.*, u.username AS author_username
FROM protocols p LEFT JOIN users u ON p.author_id = u.id
$where ORDER BY p.created_at DESC";
$rows = db_all($sql, $params);
respond(200, array_map('format_protocol', $rows));
respond(405, ['error' => 'Method not allowed']);
}
// YAML export
@@ -236,31 +263,6 @@ if ($parts[0] === 'protocols') {
respond(200, format_protocol($row));
}
// Create protocol
if ($method === 'POST') {
$user = require_member();
$data = json_body();
$new_slug = ensure_unique_slug($data['slug'] ?? slugify($data['title'] ?? 'untitled'));
$id = db_insert('protocols', [
'slug' => $new_slug,
'title' => substr($data['title'] ?? '', 0, 255),
'description' => $data['description'] ?? '',
'source' => $data['source'] ?? ('@' . $user['username']),
'source_url' => substr($data['source_url'] ?? '', 0, 500),
'tags' => json_encode(array_slice($data['tags'] ?? [], 0, 20)),
'forked_from' => $data['forked_from'] ?? null,
'image' => $data['image'] ?? null,
'steps' => json_encode(array_slice($data['steps'] ?? [], 0, 50)),
'outcome' => $data['outcome'] ?? '',
'practice' => $data['practice'] ?? '',
'author_id' => $user['id'],
'visibility' => validate_visibility($data['visibility'] ?? 'public'),
]);
$row = db_one("SELECT p.*, u.username AS author_username FROM protocols p LEFT JOIN users u ON p.author_id = u.id WHERE p.id = ?", [$id]);
respond(201, format_protocol($row));
}
// Update protocol — auth before existence check
if ($method === 'PUT') {
$user = require_auth();
@@ -459,7 +461,7 @@ if ($parts[0] === 'votes' && $method === 'POST') {
// Upsert vote
db_query(
"INSERT INTO votes (user_id, protocol_slug, value, comment) VALUES (?, ?, ?, ?)
ON DUPLICATE KEY UPDATE value = VALUES(value), comment = VALUES(comment)",
ON CONFLICT(user_id, protocol_slug) DO UPDATE SET value = excluded.value, comment = excluded.comment",
[$user['id'], $protocol_slug, $value, $comment]
);
respond(200, ['ok' => true]);