fastapi-sqlite-ui 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fastapi_sqlite_ui-1.0.0/.gitignore +6 -0
- fastapi_sqlite_ui-1.0.0/LICENSE +21 -0
- fastapi_sqlite_ui-1.0.0/PKG-INFO +106 -0
- fastapi_sqlite_ui-1.0.0/README.md +91 -0
- fastapi_sqlite_ui-1.0.0/fastapi_sqlite_ui/__init__.py +16 -0
- fastapi_sqlite_ui-1.0.0/fastapi_sqlite_ui/driver.py +174 -0
- fastapi_sqlite_ui-1.0.0/fastapi_sqlite_ui/router.py +120 -0
- fastapi_sqlite_ui-1.0.0/fastapi_sqlite_ui/ui.py +1131 -0
- fastapi_sqlite_ui-1.0.0/pyproject.toml +34 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 hoangtuvungcao
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fastapi-sqlite-ui
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: A lightweight, plug-and-play SQLite administration panel UI for FastAPI.
|
|
5
|
+
Author: hoangtuvungcao
|
|
6
|
+
License: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Classifier: Framework :: FastAPI
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Requires-Python: >=3.8
|
|
13
|
+
Requires-Dist: fastapi>=0.100.0
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# fastapi-sqlite-ui
|
|
17
|
+
|
|
18
|
+
A lightweight, plug-and-play SQLite administration panel UI for FastAPI applications.
|
|
19
|
+
|
|
20
|
+
*Giao diện quản lý SQLite Admin Panel gọn nhẹ, cắm-là-chạy dành cho các ứng dụng FastAPI.*
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Design & Layout (Thiết kế & Bố cục)
|
|
25
|
+
|
|
26
|
+
### English
|
|
27
|
+
The user interface utilizes standard modern design practices:
|
|
28
|
+
- **Dark Theme**: High-contrast dark mode (`#0b0f19`) featuring subtle borders and glassmorphism.
|
|
29
|
+
- **Icons & Typography**: Uses Google Font *Inter* for readability and *Lucide Icons* for user interface control mapping.
|
|
30
|
+
- **Transitions**: Smooth micro-interactions for modal triggers and status notifications.
|
|
31
|
+
|
|
32
|
+
### Tiếng Việt
|
|
33
|
+
Giao diện người dùng sử dụng các nguyên lý thiết kế hiện đại:
|
|
34
|
+
- **Chủ đề tối (Dark Theme)**: Chế độ tối độ tương phản cao (`#0b0f19`) kết hợp đường viền mỏng và hiệu ứng mờ kính (glassmorphism).
|
|
35
|
+
- **Biểu tượng & Font chữ**: Sử dụng font chữ *Inter* để tối ưu khả năng đọc và *Lucide Icons* để mô tả điều hướng trực quan.
|
|
36
|
+
- **Hiệu ứng chuyển động**: Các tương tác nhỏ (micro-interactions) mượt mà cho các sự kiện mở modal và thông báo trạng thái.
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Features (Tính năng)
|
|
41
|
+
|
|
42
|
+
### English
|
|
43
|
+
- **Plug & Play**: Single function call to mount the APIRouter.
|
|
44
|
+
- **Data Browser**: Paginated table view with search filters on text columns.
|
|
45
|
+
- **Schema Inspector**: Structured view of columns, data types, primary keys, and foreign keys.
|
|
46
|
+
- **Full CRUD**: Complete row insert, update, and delete support with schema-aware input fields.
|
|
47
|
+
- **Raw SQL Console**: Text-area input for executing arbitrary raw SQL statements with elapsed time metrics.
|
|
48
|
+
- **Read-Only Mode**: Toggle option to restrict database modifications.
|
|
49
|
+
- **Self-contained SPA**: No asset build steps required by the parent Python server.
|
|
50
|
+
|
|
51
|
+
### Tiếng Việt
|
|
52
|
+
- **Tích hợp nhanh**: Chỉ cần gọi một hàm để gắn APIRouter vào ứng dụng.
|
|
53
|
+
- **Duyệt dữ liệu**: Hiển thị bảng dạng phân trang kèm bộ lọc tìm kiếm trên các cột văn bản.
|
|
54
|
+
- **Kiểm tra cấu trúc**: Xem cấu trúc cột, kiểu dữ liệu, khóa chính và khóa ngoại.
|
|
55
|
+
- **Hỗ trợ CRUD**: Thao tác thêm, sửa, xóa các dòng dữ liệu thông qua các biểu mẫu tự động tương thích theo cấu trúc bảng.
|
|
56
|
+
- **Trình chạy SQL Raw**: Nhập và thực thi trực tiếp các câu lệnh SQL tự do có đo lường thời gian phản hồi.
|
|
57
|
+
- **Chế độ chỉ đọc (Read-Only)**: Cấu hình tùy chọn để chặn các thao tác ghi dữ liệu.
|
|
58
|
+
- **SPA tự đóng gói**: Không yêu cầu bước biên dịch tài nguyên tĩnh từ phía server Python.
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Installation (Cài đặt)
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
pip install fastapi-sqlite-ui
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Make sure you have `fastapi` and `uvicorn` installed.
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## Usage (Cách dùng)
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
from fastapi import FastAPI
|
|
76
|
+
from fastapi_sqlite_ui import mount_sqlite_ui
|
|
77
|
+
|
|
78
|
+
app = FastAPI()
|
|
79
|
+
|
|
80
|
+
# Mount the SQLite UI at /admin
|
|
81
|
+
mount_sqlite_ui(
|
|
82
|
+
app,
|
|
83
|
+
db_path="./mydb.sqlite",
|
|
84
|
+
mount_path="/admin",
|
|
85
|
+
read_only=False
|
|
86
|
+
)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Options (Cấu hình)
|
|
92
|
+
|
|
93
|
+
The `mount_sqlite_ui` function takes the following parameters:
|
|
94
|
+
|
|
95
|
+
| Parameter | Type | Default | Description / Mô tả |
|
|
96
|
+
| :--- | :--- | :--- | :--- |
|
|
97
|
+
| `app` | `FastAPI` | *Required* | The FastAPI application instance. / *Đối tượng ứng dụng FastAPI.* |
|
|
98
|
+
| `db_path` | `str` | *Required* | Path to the SQLite database file. / *Đường dẫn đến tệp cơ sở dữ liệu SQLite.* |
|
|
99
|
+
| `mount_path` | `str` | `"/admin"` | URL prefix for the administration dashboard. / *Tiền tố đường dẫn URL cho trang quản trị.* |
|
|
100
|
+
| `read_only` | `bool` | `False` | Hides CRUD actions and blocks write queries if True. / *Nếu là True, ẩn giao diện CRUD và chặn các câu lệnh ghi dữ liệu.* |
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## License
|
|
105
|
+
|
|
106
|
+
MIT License.
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# fastapi-sqlite-ui
|
|
2
|
+
|
|
3
|
+
A lightweight, plug-and-play SQLite administration panel UI for FastAPI applications.
|
|
4
|
+
|
|
5
|
+
*Giao diện quản lý SQLite Admin Panel gọn nhẹ, cắm-là-chạy dành cho các ứng dụng FastAPI.*
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Design & Layout (Thiết kế & Bố cục)
|
|
10
|
+
|
|
11
|
+
### English
|
|
12
|
+
The user interface utilizes standard modern design practices:
|
|
13
|
+
- **Dark Theme**: High-contrast dark mode (`#0b0f19`) featuring subtle borders and glassmorphism.
|
|
14
|
+
- **Icons & Typography**: Uses Google Font *Inter* for readability and *Lucide Icons* for user interface control mapping.
|
|
15
|
+
- **Transitions**: Smooth micro-interactions for modal triggers and status notifications.
|
|
16
|
+
|
|
17
|
+
### Tiếng Việt
|
|
18
|
+
Giao diện người dùng sử dụng các nguyên lý thiết kế hiện đại:
|
|
19
|
+
- **Chủ đề tối (Dark Theme)**: Chế độ tối độ tương phản cao (`#0b0f19`) kết hợp đường viền mỏng và hiệu ứng mờ kính (glassmorphism).
|
|
20
|
+
- **Biểu tượng & Font chữ**: Sử dụng font chữ *Inter* để tối ưu khả năng đọc và *Lucide Icons* để mô tả điều hướng trực quan.
|
|
21
|
+
- **Hiệu ứng chuyển động**: Các tương tác nhỏ (micro-interactions) mượt mà cho các sự kiện mở modal và thông báo trạng thái.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Features (Tính năng)
|
|
26
|
+
|
|
27
|
+
### English
|
|
28
|
+
- **Plug & Play**: Single function call to mount the APIRouter.
|
|
29
|
+
- **Data Browser**: Paginated table view with search filters on text columns.
|
|
30
|
+
- **Schema Inspector**: Structured view of columns, data types, primary keys, and foreign keys.
|
|
31
|
+
- **Full CRUD**: Complete row insert, update, and delete support with schema-aware input fields.
|
|
32
|
+
- **Raw SQL Console**: Text-area input for executing arbitrary raw SQL statements with elapsed time metrics.
|
|
33
|
+
- **Read-Only Mode**: Toggle option to restrict database modifications.
|
|
34
|
+
- **Self-contained SPA**: No asset build steps required by the parent Python server.
|
|
35
|
+
|
|
36
|
+
### Tiếng Việt
|
|
37
|
+
- **Tích hợp nhanh**: Chỉ cần gọi một hàm để gắn APIRouter vào ứng dụng.
|
|
38
|
+
- **Duyệt dữ liệu**: Hiển thị bảng dạng phân trang kèm bộ lọc tìm kiếm trên các cột văn bản.
|
|
39
|
+
- **Kiểm tra cấu trúc**: Xem cấu trúc cột, kiểu dữ liệu, khóa chính và khóa ngoại.
|
|
40
|
+
- **Hỗ trợ CRUD**: Thao tác thêm, sửa, xóa các dòng dữ liệu thông qua các biểu mẫu tự động tương thích theo cấu trúc bảng.
|
|
41
|
+
- **Trình chạy SQL Raw**: Nhập và thực thi trực tiếp các câu lệnh SQL tự do có đo lường thời gian phản hồi.
|
|
42
|
+
- **Chế độ chỉ đọc (Read-Only)**: Cấu hình tùy chọn để chặn các thao tác ghi dữ liệu.
|
|
43
|
+
- **SPA tự đóng gói**: Không yêu cầu bước biên dịch tài nguyên tĩnh từ phía server Python.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Installation (Cài đặt)
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pip install fastapi-sqlite-ui
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Make sure you have `fastapi` and `uvicorn` installed.
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Usage (Cách dùng)
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from fastapi import FastAPI
|
|
61
|
+
from fastapi_sqlite_ui import mount_sqlite_ui
|
|
62
|
+
|
|
63
|
+
app = FastAPI()
|
|
64
|
+
|
|
65
|
+
# Mount the SQLite UI at /admin
|
|
66
|
+
mount_sqlite_ui(
|
|
67
|
+
app,
|
|
68
|
+
db_path="./mydb.sqlite",
|
|
69
|
+
mount_path="/admin",
|
|
70
|
+
read_only=False
|
|
71
|
+
)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Options (Cấu hình)
|
|
77
|
+
|
|
78
|
+
The `mount_sqlite_ui` function takes the following parameters:
|
|
79
|
+
|
|
80
|
+
| Parameter | Type | Default | Description / Mô tả |
|
|
81
|
+
| :--- | :--- | :--- | :--- |
|
|
82
|
+
| `app` | `FastAPI` | *Required* | The FastAPI application instance. / *Đối tượng ứng dụng FastAPI.* |
|
|
83
|
+
| `db_path` | `str` | *Required* | Path to the SQLite database file. / *Đường dẫn đến tệp cơ sở dữ liệu SQLite.* |
|
|
84
|
+
| `mount_path` | `str` | `"/admin"` | URL prefix for the administration dashboard. / *Tiền tố đường dẫn URL cho trang quản trị.* |
|
|
85
|
+
| `read_only` | `bool` | `False` | Hides CRUD actions and blocks write queries if True. / *Nếu là True, ẩn giao diện CRUD và chặn các câu lệnh ghi dữ liệu.* |
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## License
|
|
90
|
+
|
|
91
|
+
MIT License.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from .router import sqlite_ui_router
|
|
2
|
+
|
|
3
|
+
def mount_sqlite_ui(app, db_path: str, mount_path: str = "/admin", read_only: bool = False):
|
|
4
|
+
"""
|
|
5
|
+
Mount the SQLite Admin UI APIRouter into your FastAPI application.
|
|
6
|
+
|
|
7
|
+
Args:
|
|
8
|
+
app: The FastAPI application instance.
|
|
9
|
+
db_path: Path to the SQLite database file.
|
|
10
|
+
mount_path: URL prefix under which the UI and API will be mounted (default: "/admin").
|
|
11
|
+
read_only: If True, hides mutation actions (add/edit/delete) and blocks write queries.
|
|
12
|
+
"""
|
|
13
|
+
router = sqlite_ui_router(db_path, read_only)
|
|
14
|
+
app.include_router(router, prefix=mount_path)
|
|
15
|
+
|
|
16
|
+
__all__ = ["sqlite_ui_router", "mount_sqlite_ui"]
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import sqlite3
|
|
2
|
+
import re
|
|
3
|
+
|
|
4
|
+
class SQLiteDriver:
|
|
5
|
+
def __init__(self, db_path: str, read_only: bool = False):
|
|
6
|
+
self.db_path = db_path
|
|
7
|
+
self.read_only = read_only
|
|
8
|
+
|
|
9
|
+
def _get_connection(self):
|
|
10
|
+
conn = sqlite3.connect(self.db_path)
|
|
11
|
+
conn.row_factory = sqlite3.Row
|
|
12
|
+
return conn
|
|
13
|
+
|
|
14
|
+
def _serialize_value(self, val):
|
|
15
|
+
if isinstance(val, bytes):
|
|
16
|
+
return f"<BLOB: {len(val)} bytes>"
|
|
17
|
+
return val
|
|
18
|
+
|
|
19
|
+
def execute(self, sql: str, params: tuple = ()) -> dict:
|
|
20
|
+
if self.read_only:
|
|
21
|
+
# Check if write statement
|
|
22
|
+
is_write = not re.match(r'^\s*(select|pragma|explain|show|desc)', sql, re.IGNORECASE)
|
|
23
|
+
if is_write:
|
|
24
|
+
return {"rows": [], "columns": [], "error": "Database is in read-only mode"}
|
|
25
|
+
|
|
26
|
+
conn = self._get_connection()
|
|
27
|
+
cursor = conn.cursor()
|
|
28
|
+
try:
|
|
29
|
+
cursor.execute(sql, params)
|
|
30
|
+
|
|
31
|
+
# Check if query returned rows (e.g. SELECT)
|
|
32
|
+
rows_data = []
|
|
33
|
+
columns = []
|
|
34
|
+
affected_rows = None
|
|
35
|
+
|
|
36
|
+
if cursor.description:
|
|
37
|
+
columns = [desc[0] for desc in cursor.description]
|
|
38
|
+
rows = cursor.fetchall()
|
|
39
|
+
rows_data = [
|
|
40
|
+
{col: self._serialize_value(row[col]) for col in columns}
|
|
41
|
+
for row in rows
|
|
42
|
+
]
|
|
43
|
+
else:
|
|
44
|
+
conn.commit()
|
|
45
|
+
affected_rows = cursor.rowcount
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
"rows": rows_data,
|
|
49
|
+
"columns": columns,
|
|
50
|
+
"affectedRows": affected_rows
|
|
51
|
+
}
|
|
52
|
+
except sqlite3.Error as e:
|
|
53
|
+
return {"rows": [], "columns": [], "error": str(e)}
|
|
54
|
+
finally:
|
|
55
|
+
conn.close()
|
|
56
|
+
|
|
57
|
+
def get_tables(self) -> list:
|
|
58
|
+
res = self.execute("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'")
|
|
59
|
+
if "error" in res:
|
|
60
|
+
raise Exception(res["error"])
|
|
61
|
+
return [row["name"] for row in res["rows"]]
|
|
62
|
+
|
|
63
|
+
def get_table_info(self, table: str) -> dict:
|
|
64
|
+
sanitized_table = re.sub(r'[^a-zA-Z0-9_]', '', table)
|
|
65
|
+
|
|
66
|
+
col_res = self.execute(f'PRAGMA table_info("{sanitized_table}")')
|
|
67
|
+
if "error" in col_res:
|
|
68
|
+
raise Exception(col_res["error"])
|
|
69
|
+
|
|
70
|
+
fk_res = self.execute(f'PRAGMA foreign_key_list("{sanitized_table}")')
|
|
71
|
+
if "error" in fk_res:
|
|
72
|
+
raise Exception(fk_res["error"])
|
|
73
|
+
|
|
74
|
+
columns = []
|
|
75
|
+
for r in col_res["rows"]:
|
|
76
|
+
columns.append({
|
|
77
|
+
"name": r["name"],
|
|
78
|
+
"type": r["type"],
|
|
79
|
+
"notNull": r["notnull"] == 1,
|
|
80
|
+
"defaultValue": r["dflt_value"],
|
|
81
|
+
"primaryKey": r["pk"] == 1 or r["pk"] is True
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
foreign_keys = []
|
|
85
|
+
for r in fk_res["rows"]:
|
|
86
|
+
foreign_keys.append({
|
|
87
|
+
"id": r["id"],
|
|
88
|
+
"seq": r["seq"],
|
|
89
|
+
"table": r["table"],
|
|
90
|
+
"from": r["from"],
|
|
91
|
+
"to": r["to"],
|
|
92
|
+
"onUpdate": r["on_update"],
|
|
93
|
+
"onDelete": r["on_delete"],
|
|
94
|
+
"match": r["match"]
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
"name": table,
|
|
99
|
+
"columns": columns,
|
|
100
|
+
"foreignKeys": foreign_keys
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
def get_rows(self, table: str, limit: int, offset: int, search: str = "") -> dict:
|
|
104
|
+
sanitized_table = re.sub(r'[^a-zA-Z0-9_]', '', table)
|
|
105
|
+
info = self.get_table_info(table)
|
|
106
|
+
|
|
107
|
+
where_clause = ""
|
|
108
|
+
params = []
|
|
109
|
+
|
|
110
|
+
if search and info["columns"]:
|
|
111
|
+
# Find columns that are of type text
|
|
112
|
+
search_terms = []
|
|
113
|
+
for c in info["columns"]:
|
|
114
|
+
c_type = c["type"].upper()
|
|
115
|
+
if any(t in c_type for t in ['TEXT', 'VARCHAR', 'CHAR', 'CLOB']) or c_type == '':
|
|
116
|
+
search_terms.append(f'"{c["name"].replace(chr(34), chr(34)+chr(34))}" LIKE ?')
|
|
117
|
+
|
|
118
|
+
if search_terms:
|
|
119
|
+
where_clause = f" WHERE {' OR '.join(search_terms)}"
|
|
120
|
+
wild_card_search = f"%{search}%"
|
|
121
|
+
for _ in search_terms:
|
|
122
|
+
params.append(wild_card_search)
|
|
123
|
+
|
|
124
|
+
# Count total rows
|
|
125
|
+
count_sql = f'SELECT COUNT(*) as count FROM "{sanitized_table}"{where_clause}'
|
|
126
|
+
count_res = self.execute(count_sql, tuple(params))
|
|
127
|
+
total = count_res["rows"][0]["count"] if count_res["rows"] else 0
|
|
128
|
+
|
|
129
|
+
# Fetch rows
|
|
130
|
+
select_sql = f'SELECT * FROM "{sanitized_table}"{where_clause} LIMIT ? OFFSET ?'
|
|
131
|
+
select_res = self.execute(select_sql, tuple(params + [limit, offset]))
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
"rows": select_res["rows"],
|
|
135
|
+
"total": total
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
def insert_row(self, table: str, data: dict) -> dict:
|
|
139
|
+
sanitized_table = re.sub(r'[^a-zA-Z0-9_]', '', table)
|
|
140
|
+
columns = [f'"{c.replace(chr(34), chr(34)+chr(34))}"' for c in data.keys()]
|
|
141
|
+
placeholders = [', '.join(['?'] * len(data))]
|
|
142
|
+
values = list(data.values())
|
|
143
|
+
|
|
144
|
+
sql = f'INSERT INTO "{sanitized_table}" ({", ".join(columns)}) VALUES ({", ".join(placeholders)})'
|
|
145
|
+
res = self.execute(sql, tuple(values))
|
|
146
|
+
if "error" in res:
|
|
147
|
+
raise Exception(res["error"])
|
|
148
|
+
return res
|
|
149
|
+
|
|
150
|
+
def update_row(self, table: str, pk: dict, data: dict) -> dict:
|
|
151
|
+
sanitized_table = re.sub(r'[^a-zA-Z0-9_]', '', table)
|
|
152
|
+
|
|
153
|
+
set_terms = [f'"{c.replace(chr(34), chr(34)+chr(34))}" = ?' for c in data.keys()]
|
|
154
|
+
set_values = list(data.values())
|
|
155
|
+
|
|
156
|
+
where_terms = [f'"{c.replace(chr(34), chr(34)+chr(34))}" = ?' for c in pk.keys()]
|
|
157
|
+
where_values = list(pk.values())
|
|
158
|
+
|
|
159
|
+
sql = f'UPDATE "{sanitized_table}" SET {", ".join(set_terms)} WHERE {" AND ".join(where_terms)}'
|
|
160
|
+
res = self.execute(sql, tuple(set_values + where_values))
|
|
161
|
+
if "error" in res:
|
|
162
|
+
raise Exception(res["error"])
|
|
163
|
+
return res
|
|
164
|
+
|
|
165
|
+
def delete_row(self, table: str, pk: dict) -> dict:
|
|
166
|
+
sanitized_table = re.sub(r'[^a-zA-Z0-9_]', '', table)
|
|
167
|
+
where_terms = [f'"{c.replace(chr(34), chr(34)+chr(34))}" = ?' for c in pk.keys()]
|
|
168
|
+
where_values = list(pk.values())
|
|
169
|
+
|
|
170
|
+
sql = f'DELETE FROM "{sanitized_table}" WHERE {" AND ".join(where_terms)}'
|
|
171
|
+
res = self.execute(sql, tuple(where_values))
|
|
172
|
+
if "error" in res:
|
|
173
|
+
raise Exception(res["error"])
|
|
174
|
+
return res
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# pyrefly: ignore [missing-import]
|
|
2
|
+
from fastapi import APIRouter, Request
|
|
3
|
+
# pyrefly: ignore [missing-import]
|
|
4
|
+
from fastapi.responses import HTMLResponse, JSONResponse
|
|
5
|
+
from .driver import SQLiteDriver
|
|
6
|
+
from .ui import get_html_template
|
|
7
|
+
|
|
8
|
+
def sqlite_ui_router(db_path: str, read_only: bool = False) -> APIRouter:
|
|
9
|
+
router = APIRouter()
|
|
10
|
+
driver = SQLiteDriver(db_path, read_only)
|
|
11
|
+
|
|
12
|
+
@router.get("/", response_class=HTMLResponse)
|
|
13
|
+
def index(request: Request):
|
|
14
|
+
path = request.url.path
|
|
15
|
+
base_path = path[:-1] if path.endswith('/') else path
|
|
16
|
+
return get_html_template(base_path)
|
|
17
|
+
|
|
18
|
+
@router.get("/index.html", response_class=HTMLResponse)
|
|
19
|
+
def index_html(request: Request):
|
|
20
|
+
path = request.url.path
|
|
21
|
+
base_path = path[:-11] if path.endswith('/index.html') else path
|
|
22
|
+
return get_html_template(base_path)
|
|
23
|
+
|
|
24
|
+
@router.get("/api/tables")
|
|
25
|
+
def list_tables():
|
|
26
|
+
try:
|
|
27
|
+
tables = driver.get_tables()
|
|
28
|
+
return {
|
|
29
|
+
"success": True,
|
|
30
|
+
"tables": tables,
|
|
31
|
+
"readOnly": read_only
|
|
32
|
+
}
|
|
33
|
+
except Exception as e:
|
|
34
|
+
return JSONResponse(status_code=500, content={"success": False, "error": str(e)})
|
|
35
|
+
|
|
36
|
+
@router.get("/api/tables/{table_name}")
|
|
37
|
+
def table_data(table_name: str, page: int = 1, limit: int = 10, search: str = ""):
|
|
38
|
+
try:
|
|
39
|
+
offset = (page - 1) * limit
|
|
40
|
+
info = driver.get_table_info(table_name)
|
|
41
|
+
data = driver.get_rows(table_name, limit, offset, search)
|
|
42
|
+
return {
|
|
43
|
+
"success": True,
|
|
44
|
+
"info": info,
|
|
45
|
+
"rows": data["rows"],
|
|
46
|
+
"total": data["total"]
|
|
47
|
+
}
|
|
48
|
+
except Exception as e:
|
|
49
|
+
return JSONResponse(status_code=500, content={"success": False, "error": str(e)})
|
|
50
|
+
|
|
51
|
+
@router.post("/api/query")
|
|
52
|
+
async def run_query(request: Request):
|
|
53
|
+
try:
|
|
54
|
+
body = await request.json()
|
|
55
|
+
sql = body.get("sql")
|
|
56
|
+
params = body.get("params", [])
|
|
57
|
+
|
|
58
|
+
if not sql:
|
|
59
|
+
return JSONResponse(status_code=400, content={"success": False, "error": "SQL query is required"})
|
|
60
|
+
|
|
61
|
+
res = driver.execute(sql, tuple(params))
|
|
62
|
+
if "error" in res:
|
|
63
|
+
return {"success": False, "error": res["error"]}
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
"success": True,
|
|
67
|
+
"rows": res["rows"],
|
|
68
|
+
"columns": res["columns"],
|
|
69
|
+
"affectedRows": res["affectedRows"]
|
|
70
|
+
}
|
|
71
|
+
except Exception as e:
|
|
72
|
+
return JSONResponse(status_code=500, content={"success": False, "error": str(e)})
|
|
73
|
+
|
|
74
|
+
@router.post("/api/tables/{table_name}")
|
|
75
|
+
async def add_row(table_name: str, request: Request):
|
|
76
|
+
if read_only:
|
|
77
|
+
return JSONResponse(status_code=403, content={"success": False, "error": "Database is in read-only mode"})
|
|
78
|
+
try:
|
|
79
|
+
body = await request.json()
|
|
80
|
+
data = body.get("data")
|
|
81
|
+
if not data:
|
|
82
|
+
return JSONResponse(status_code=400, content={"success": False, "error": "Data is required"})
|
|
83
|
+
|
|
84
|
+
driver.insert_row(table_name, data)
|
|
85
|
+
return {"success": True}
|
|
86
|
+
except Exception as e:
|
|
87
|
+
return JSONResponse(status_code=500, content={"success": False, "error": str(e)})
|
|
88
|
+
|
|
89
|
+
@router.put("/api/tables/{table_name}")
|
|
90
|
+
async def edit_row(table_name: str, request: Request):
|
|
91
|
+
if read_only:
|
|
92
|
+
return JSONResponse(status_code=403, content={"success": False, "error": "Database is in read-only mode"})
|
|
93
|
+
try:
|
|
94
|
+
body = await request.json()
|
|
95
|
+
pk = body.get("pk")
|
|
96
|
+
data = body.get("data")
|
|
97
|
+
if not pk or not data:
|
|
98
|
+
return JSONResponse(status_code=400, content={"success": False, "error": "pk and data are required"})
|
|
99
|
+
|
|
100
|
+
driver.update_row(table_name, pk, data)
|
|
101
|
+
return {"success": True}
|
|
102
|
+
except Exception as e:
|
|
103
|
+
return JSONResponse(status_code=500, content={"success": False, "error": str(e)})
|
|
104
|
+
|
|
105
|
+
@router.delete("/api/tables/{table_name}")
|
|
106
|
+
async def delete_row(table_name: str, request: Request):
|
|
107
|
+
if read_only:
|
|
108
|
+
return JSONResponse(status_code=403, content={"success": False, "error": "Database is in read-only mode"})
|
|
109
|
+
try:
|
|
110
|
+
body = await request.json()
|
|
111
|
+
pk = body.get("pk")
|
|
112
|
+
if not pk:
|
|
113
|
+
return JSONResponse(status_code=400, content={"success": False, "error": "pk is required"})
|
|
114
|
+
|
|
115
|
+
driver.delete_row(table_name, pk)
|
|
116
|
+
return {"success": True}
|
|
117
|
+
except Exception as e:
|
|
118
|
+
return JSONResponse(status_code=500, content={"success": False, "error": str(e)})
|
|
119
|
+
|
|
120
|
+
return router
|