Migrate from TinyDB to SQLite

- Replace TinyDB (JSON file) with sqlite3 for data persistence
- Add schema.py: table creation + data migration from db.json
- Rewrite database.py: all CRUD operations use sqlite3 directly
- All data retains original IDs via migration script
- Remove tinydb dependency from pyproject.toml
This commit is contained in:
Yuyao Huang 2026-05-08 16:18:26 +08:00
parent b060ba6bf8
commit a8fe6ed7b3
6 changed files with 289 additions and 92 deletions

24
app.py
View File

@ -66,13 +66,13 @@ def api_register():
password_hash = auth.hash_password(password)
user = database.create_user(username, password_hash)
session["user_id"] = user.doc_id
session["user_id"] = user["id"]
session["username"] = username
return jsonify({
"success": True,
"message": "Registration successful",
"user_id": user.doc_id
"user_id": user["id"]
}), 201
@ -89,14 +89,14 @@ def api_login():
if not user or not auth.check_password(password, user["password_hash"]):
return jsonify({"success": False, "message": "Invalid credentials"}), 401
session["user_id"] = user.doc_id
session["user_id"] = user["id"]
session["username"] = username
return jsonify({
"success": True,
"message": "Login successful",
"user": {
"user_id": user.doc_id,
"user_id": user["id"],
"username": user["username"],
"role": user["role"],
"max_goals": user["max_goals"]
@ -118,7 +118,7 @@ def api_me():
if not user:
return jsonify({"success": False, "message": "User not found"}), 404
return jsonify({
"user_id": user.doc_id,
"user_id": user["id"],
"username": user["username"],
"role": user["role"],
"max_goals": user["max_goals"]
@ -133,7 +133,7 @@ def api_get_goals():
result = []
for goal in goals:
goal_dict = dict(goal)
goal_dict["id"] = goal.doc_id
goal_dict["id"] = goal["id"]
result.append(goal_dict)
return jsonify(result)
@ -157,7 +157,7 @@ def api_create_goal():
}), 400
goal = database.create_goal(user_id, title)
return jsonify({"success": True, "goal_id": goal.doc_id}), 201
return jsonify({"success": True, "goal_id": goal["id"]}), 201
@app.route("/api/goals/<int:goal_id>", methods=["PUT"])
@ -221,7 +221,7 @@ def api_get_tasks():
result = []
for task in tasks:
task_dict = dict(task)
task_dict["id"] = task.doc_id
task_dict["id"] = task["id"]
result.append(task_dict)
return jsonify(result)
@ -245,7 +245,7 @@ def api_create_task():
return jsonify({"success": False, "message": "Cannot add task to deactivated goal"}), 400
task = database.create_task(goal_id, title, desc)
return jsonify({"success": True, "task_id": task.doc_id}), 201
return jsonify({"success": True, "task_id": task["id"]}), 201
@app.route("/api/tasks/<int:task_id>", methods=["PUT"])
@ -313,11 +313,11 @@ def api_update_task_status(task_id):
doing_task = None
all_tasks = database.get_tasks_by_goal(task["goal_id"])
for t in all_tasks:
if t.get("status") == "doing" and t.doc_id != task.doc_id:
if t.get("status") == "doing" and t["id"] != task["id"]:
doing_task = t
break
if doing_task:
database.update_task(doing_task.doc_id, status="todo")
database.update_task(doing_task["id"], status="todo")
updates["start_time"] = datetime.now().isoformat()
else:
updates["start_time"] = None
@ -359,7 +359,7 @@ def api_get_users():
result = []
for user in users:
result.append({
"user_id": user.doc_id,
"user_id": user["id"],
"username": user["username"],
"role": user["role"],
"max_goals": user["max_goals"]

View File

@ -1,6 +1,6 @@
import os
DB_PATH = os.path.join(os.path.dirname(__file__), "data", "db.json")
DB_PATH = os.path.join(os.path.dirname(__file__), "data", "db.sqlite")
DEFAULT_ADMIN_USERNAME = "admin"
DEFAULT_ADMIN_PASSWORD = "admin123"

View File

@ -1,137 +1,219 @@
import os
from tinydb import TinyDB, Query
from tinydb.operations import increment
import config
from schema import get_connection
os.makedirs(os.path.dirname(config.DB_PATH), exist_ok=True)
db = TinyDB(config.DB_PATH)
users_table = db.table("users")
goals_table = db.table("goals")
tasks_table = db.table("tasks")
User = Query()
Goal = Query()
Task = Query()
def row_to_dict(row):
if row is None:
return None
return dict(row)
def init_db():
if users_table.count(User.role == "admin") == 0:
import bcrypt
password_hash = bcrypt.hashpw(
config.DEFAULT_ADMIN_PASSWORD.encode("utf-8"),
bcrypt.gensalt()
).decode("utf-8")
users_table.insert({
"username": config.DEFAULT_ADMIN_USERNAME,
"password_hash": password_hash,
"role": "admin",
"max_goals": 100
})
from schema import init_db as _init_db, migrate_from_tinydb
_init_db()
migrate_from_tinydb()
def get_user_by_username(username):
result = users_table.search(User.username == username)
return result[0] if result else None
conn = get_connection()
try:
cur = conn.execute("SELECT * FROM users WHERE username = ?", (username,))
return row_to_dict(cur.fetchone())
finally:
conn.close()
def get_user_by_id(user_id):
return users_table.get(doc_id=user_id)
conn = get_connection()
try:
cur = conn.execute("SELECT * FROM users WHERE id = ?", (user_id,))
return row_to_dict(cur.fetchone())
finally:
conn.close()
def create_user(username, password_hash, role="user", max_goals=None):
if max_goals is None:
max_goals = config.DEFAULT_MAX_GOALS
doc_id = users_table.insert({
"username": username,
"password_hash": password_hash,
"role": role,
"max_goals": max_goals
})
return users_table.get(doc_id=doc_id)
max_goals = 5
conn = get_connection()
try:
cur = conn.execute(
"INSERT INTO users (username, password_hash, role, max_goals) VALUES (?, ?, ?, ?)",
(username, password_hash, role, max_goals)
)
conn.commit()
return row_to_dict(conn.execute("SELECT * FROM users WHERE id = ?", (cur.lastrowid,)).fetchone())
finally:
conn.close()
def update_user(user_id, **kwargs):
users_table.update(kwargs, doc_ids=[user_id])
if not kwargs:
return
sets = ", ".join(f"{k} = ?" for k in kwargs)
values = list(kwargs.values()) + [user_id]
conn = get_connection()
try:
conn.execute(f"UPDATE users SET {sets} WHERE id = ?", values)
conn.commit()
finally:
conn.close()
def get_all_users():
return users_table.all()
conn = get_connection()
try:
cur = conn.execute("SELECT * FROM users")
return [row_to_dict(r) for r in cur.fetchall()]
finally:
conn.close()
def get_goals_by_user(user_id):
return goals_table.search(Goal.user_id == user_id)
conn = get_connection()
try:
cur = conn.execute("SELECT * FROM goals WHERE user_id = ?", (user_id,))
return [row_to_dict(r) for r in cur.fetchall()]
finally:
conn.close()
def get_goal_by_id(goal_id):
return goals_table.get(doc_id=goal_id)
conn = get_connection()
try:
cur = conn.execute("SELECT * FROM goals WHERE id = ?", (goal_id,))
return row_to_dict(cur.fetchone())
finally:
conn.close()
def create_goal(user_id, title):
doc_id = goals_table.insert({
"user_id": user_id,
"title": title,
"activated": True
})
return goals_table.get(doc_id=doc_id)
conn = get_connection()
try:
cur = conn.execute(
"INSERT INTO goals (user_id, title, activated) VALUES (?, ?, 1)",
(user_id, title)
)
conn.commit()
return row_to_dict(conn.execute("SELECT * FROM goals WHERE id = ?", (cur.lastrowid,)).fetchone())
finally:
conn.close()
def update_goal(goal_id, **kwargs):
goals_table.update(kwargs, doc_ids=[goal_id])
if not kwargs:
return
sets = ", ".join(f"{k} = ?" for k in kwargs)
values = list(kwargs.values()) + [goal_id]
conn = get_connection()
try:
conn.execute(f"UPDATE goals SET {sets} WHERE id = ?", values)
conn.commit()
finally:
conn.close()
def delete_goal(goal_id):
tasks = tasks_table.search(Task.goal_id == goal_id)
for task in tasks:
tasks_table.remove(doc_ids=[task.doc_id])
goals_table.remove(doc_ids=[goal_id])
conn = get_connection()
try:
conn.execute("DELETE FROM tasks WHERE goal_id = ?", (goal_id,))
conn.execute("DELETE FROM goals WHERE id = ?", (goal_id,))
conn.commit()
finally:
conn.close()
def count_goals_by_user(user_id):
return len(goals_table.search(Goal.user_id == user_id))
conn = get_connection()
try:
cur = conn.execute("SELECT COUNT(*) FROM goals WHERE user_id = ?", (user_id,))
return cur.fetchone()[0]
finally:
conn.close()
def get_tasks_by_goal(goal_id):
return tasks_table.search(Task.goal_id == goal_id)
conn = get_connection()
try:
cur = conn.execute("SELECT * FROM tasks WHERE goal_id = ?", (goal_id,))
return [row_to_dict(r) for r in cur.fetchall()]
finally:
conn.close()
def get_task_by_id(task_id):
return tasks_table.get(doc_id=task_id)
conn = get_connection()
try:
cur = conn.execute("SELECT * FROM tasks WHERE id = ?", (task_id,))
return row_to_dict(cur.fetchone())
finally:
conn.close()
def create_task(goal_id, title, desc="", status="todo", order=None):
if order is None:
existing_tasks = tasks_table.search(Task.goal_id == goal_id)
unfinished = [t for t in existing_tasks if t.get("status") != "done"]
order = max([t.get("order", 0) for t in unfinished], default=0) + 1.0
conn = get_connection()
try:
cur = conn.execute(
"""SELECT COALESCE(MAX("order"), 0) + 1.0 FROM tasks
WHERE goal_id = ? AND status != 'done'""",
(goal_id,)
)
order = cur.fetchone()[0]
finally:
conn.close()
doc_id = tasks_table.insert({
"goal_id": goal_id,
"title": title,
"desc": desc,
"status": status,
"start_time": None,
"finished_time": None,
"order": order
})
return tasks_table.get(doc_id=doc_id)
conn = get_connection()
try:
cur = conn.execute(
"""INSERT INTO tasks (goal_id, title, desc, status, start_time, finished_time, "order")
VALUES (?, ?, ?, ?, ?, ?, ?)""",
(goal_id, title, desc, status, None, None, order)
)
conn.commit()
return row_to_dict(conn.execute("SELECT * FROM tasks WHERE id = ?", (cur.lastrowid,)).fetchone())
finally:
conn.close()
def update_task(task_id, **kwargs):
tasks_table.update(kwargs, doc_ids=[task_id])
if not kwargs:
return
sets = ", ".join(f"{k} = ?" for k in kwargs)
values = list(kwargs.values()) + [task_id]
conn = get_connection()
try:
conn.execute(f"UPDATE tasks SET {sets} WHERE id = ?", values)
conn.commit()
finally:
conn.close()
def delete_task(task_id):
tasks_table.remove(doc_ids=[task_id])
conn = get_connection()
try:
conn.execute("DELETE FROM tasks WHERE id = ?", (task_id,))
conn.commit()
finally:
conn.close()
def get_tasks_sorted(goal_id):
all_tasks = tasks_table.search(Task.goal_id == goal_id)
unfinished = [t for t in all_tasks if t.get("status") != "done"]
finished = [t for t in all_tasks if t.get("status") == "done"]
conn = get_connection()
try:
cur = conn.execute(
"""SELECT * FROM tasks WHERE goal_id = ? AND status = 'done'
ORDER BY finished_time DESC""",
(goal_id,)
)
finished = [row_to_dict(r) for r in cur.fetchall()]
unfinished.sort(key=lambda t: t.get("order", 0))
finished.sort(key=lambda t: t.get("finished_time", ""), reverse=True)
cur = conn.execute(
"""SELECT * FROM tasks WHERE goal_id = ? AND status != 'done'
ORDER BY "order" ASC""",
(goal_id,)
)
unfinished = [row_to_dict(r) for r in cur.fetchall()]
return finished + unfinished
finally:
conn.close()

View File

@ -6,5 +6,4 @@ requires-python = ">=3.13"
dependencies = [
"bcrypt>=5.0.0",
"flask>=3.1.3",
"tinydb>=4.8.2",
]

116
schema.py Normal file
View File

@ -0,0 +1,116 @@
import sqlite3
import os
import json
import config
CREATE_USERS = """
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
role TEXT NOT NULL DEFAULT 'user',
max_goals INTEGER NOT NULL DEFAULT 5
)
"""
CREATE_GOALS = """
CREATE TABLE IF NOT EXISTS goals (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
title TEXT NOT NULL,
activated INTEGER NOT NULL DEFAULT 1,
FOREIGN KEY (user_id) REFERENCES users(id)
)
"""
CREATE_TASKS = """
CREATE TABLE IF NOT EXISTS tasks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
goal_id INTEGER NOT NULL,
title TEXT NOT NULL,
desc TEXT NOT NULL DEFAULT '',
status TEXT NOT NULL DEFAULT 'todo',
start_time TEXT,
finished_time TEXT,
"order" REAL NOT NULL DEFAULT 0.0,
FOREIGN KEY (goal_id) REFERENCES goals(id)
)
"""
def get_connection():
os.makedirs(os.path.dirname(config.DB_PATH), exist_ok=True)
conn = sqlite3.connect(config.DB_PATH)
conn.row_factory = sqlite3.Row
conn.execute("PRAGMA foreign_keys = ON")
return conn
def init_db():
conn = get_connection()
try:
conn.execute(CREATE_USERS)
conn.execute(CREATE_GOALS)
conn.execute(CREATE_TASKS)
conn.commit()
import bcrypt
cur = conn.execute("SELECT COUNT(*) FROM users WHERE role = 'admin'")
if cur.fetchone()[0] == 0:
password_hash = bcrypt.hashpw(
config.DEFAULT_ADMIN_PASSWORD.encode("utf-8"),
bcrypt.gensalt()
).decode("utf-8")
conn.execute(
"INSERT INTO users (username, password_hash, role, max_goals) VALUES (?, ?, ?, ?)",
(config.DEFAULT_ADMIN_USERNAME, password_hash, "admin", 100)
)
conn.commit()
finally:
conn.close()
def migrate_from_tinydb():
json_path = os.path.join(os.path.dirname(config.DB_PATH), "db.json")
if not os.path.exists(json_path):
return
conn = get_connection()
try:
with open(json_path, "r") as f:
data = json.load(f)
if "users" in data:
for doc_id, record in data["users"].items():
record["id"] = int(doc_id)
if conn.execute("SELECT COUNT(*) FROM users WHERE id = ?", (record["id"],)).fetchone()[0] == 0:
conn.execute(
"INSERT INTO users (id, username, password_hash, role, max_goals) VALUES (?, ?, ?, ?, ?)",
(record["id"], record["username"], record["password_hash"], record["role"], record["max_goals"])
)
if "goals" in data:
for doc_id, record in data["goals"].items():
record["id"] = int(doc_id)
if conn.execute("SELECT COUNT(*) FROM goals WHERE id = ?", (record["id"],)).fetchone()[0] == 0:
conn.execute(
"INSERT INTO goals (id, user_id, title, activated) VALUES (?, ?, ?, ?)",
(record["id"], record["user_id"], record["title"], 1 if record.get("activated", True) else 0)
)
if "tasks" in data:
for doc_id, record in data["tasks"].items():
record["id"] = int(doc_id)
if conn.execute("SELECT COUNT(*) FROM tasks WHERE id = ?", (record["id"],)).fetchone()[0] == 0:
conn.execute(
"""INSERT INTO tasks (id, goal_id, title, desc, status, start_time, finished_time, "order")
VALUES (?, ?, ?, ?, ?, ?, ?, ?)""",
(record["id"], record["goal_id"], record["title"], record.get("desc", ""),
record.get("status", "todo"), record.get("start_time"), record.get("finished_time"),
record.get("order", 0.0))
)
conn.commit()
finally:
conn.close()

View File

@ -60,7 +60,7 @@ function openModal(goal = null) {
if (goal) {
modalTitle.textContent = "Edit Goal";
goalId.value = goal.doc_id;
goalId.value = goal.id;
goalTitle.value = goal.title;
} else {
modalTitle.textContent = "Create Goal";