sql-dev-inspector 0.0.0-stage → 1.0.0
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.
- package/CHANGELOG.md +3 -0
- package/LICENSE +21 -0
- package/README.VI.md +341 -0
- package/README.md +267 -2
- package/dist/client/nuxt-plugin.client.d.ts +2 -0
- package/dist/client/nuxt-plugin.client.js +11 -0
- package/dist/client/widget.d.ts +40 -0
- package/dist/client/widget.js +485 -0
- package/dist/i18n/index.d.ts +26 -0
- package/dist/i18n/index.js +48 -0
- package/dist/i18n/messages.d.ts +119 -0
- package/dist/i18n/messages.js +331 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +7 -0
- package/dist/inspector-server.d.ts +11 -0
- package/dist/inspector-server.js +95 -0
- package/dist/nitro-plugin.d.ts +19 -0
- package/dist/nitro-plugin.js +30 -0
- package/dist/node.d.ts +27 -0
- package/dist/node.js +52 -0
- package/dist/nuxt.d.ts +20 -0
- package/dist/nuxt.js +21 -0
- package/dist/report.d.ts +24 -0
- package/dist/report.js +182 -0
- package/dist/runtime.d.ts +21 -0
- package/dist/runtime.js +42 -0
- package/dist/setup.d.ts +8 -0
- package/dist/setup.js +44 -0
- package/dist/sql-analyzer.d.ts +12 -0
- package/dist/sql-analyzer.js +214 -0
- package/dist/store.d.ts +36 -0
- package/dist/store.js +86 -0
- package/dist/terminal.d.ts +5 -0
- package/dist/terminal.js +64 -0
- package/dist/tracer.d.ts +36 -0
- package/dist/tracer.js +143 -0
- package/dist/types.d.ts +79 -0
- package/dist/types.js +1 -0
- package/dist/ui/client.d.ts +1 -0
- package/dist/ui/client.js +472 -0
- package/dist/ui/page.d.ts +6 -0
- package/dist/ui/page.js +73 -0
- package/dist/ui/styles.d.ts +7 -0
- package/dist/ui/styles.js +207 -0
- package/docs/PUBLISH-AND-INSTALL.md +267 -0
- package/package.json +80 -4
package/CHANGELOG.md
ADDED
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nhu Duy Thin
|
|
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.
|
package/README.VI.md
ADDED
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
# sql-dev-inspector
|
|
2
|
+
|
|
3
|
+
> 🇬🇧 [English README](README.md) — bản chung chung cho cộng đồng. File này (`README.VI.md`) là bản tiếng Việt chi tiết, dành cho tác giả / người bảo trì.
|
|
4
|
+
|
|
5
|
+
> Công cụ **chỉ dùng khi dev** cho mọi ứng dụng Node.js nói chuyện với MySQL qua [`mysql2`](https://github.com/sidorares/node-mysql2).
|
|
6
|
+
> Khi client gọi một API, bạn thấy ngay backend đã chạy **những câu SQL nào, theo thứ tự nào, nằm trong transaction / savepoint nào, subquery lồng mấy tầng, mỗi câu mất bao lâu, trả về bao nhiêu dòng** — kèm cảnh báo N+1, query lặp, query chậm.
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
GET /api/orders/42 → 200 163ms total · 12 queries · 148ms in SQL
|
|
10
|
+
├─ SELECT order_items 3.86ms · 2 rows
|
|
11
|
+
│ select `id`, `sku` from `order_items` where `order_id` = 11
|
|
12
|
+
├─ SELECT customers, orders ⤷subquery×3 (depth 3) 1.50ms · 2 rows
|
|
13
|
+
├─ TRANSACTION [conn 101] committed 10.2ms
|
|
14
|
+
│ ├─ INSERT orders 1.39ms · 1 affected
|
|
15
|
+
│ ├─ SAVEPOINT sp1 [conn 101] rolled-back 4.30ms
|
|
16
|
+
│ │ └─ SELECT 0.18ms ✖ You have an error in your SQL syntax …
|
|
17
|
+
│ └─ SELECT orders 1.33ms · 2 rows
|
|
18
|
+
⚠ Nghi N+1: 4 lần, tổng 8.4ms — select `id`, `sku` from `order_items` where `order_id` = ?
|
|
19
|
+
✖ Lỗi SQL: You have an error in your SQL syntax …
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Mục lục
|
|
23
|
+
|
|
24
|
+
1. [Tính năng](#tính-năng)
|
|
25
|
+
2. [Yêu cầu](#yêu-cầu)
|
|
26
|
+
3. [Cài đặt](#cài-đặt)
|
|
27
|
+
4. [Bắt đầu nhanh](#bắt-đầu-nhanh) — Nuxt · Nitro · Express/Connect · `http` thuần · job/cron
|
|
28
|
+
5. [Cách xem kết quả](#cách-xem-kết-quả) — terminal · trang web · nút nổi trong app
|
|
29
|
+
6. [Cảnh báo tự động](#cảnh-báo-tự-động)
|
|
30
|
+
7. [Cấu hình (biến môi trường)](#cấu-hình-biến-môi-trường)
|
|
31
|
+
8. [API](#api)
|
|
32
|
+
9. [Cách hoạt động](#cách-hoạt-động)
|
|
33
|
+
10. [An toàn & quyền riêng tư](#an-toàn--quyền-riêng-tư)
|
|
34
|
+
11. [Giới hạn đã biết](#giới-hạn-đã-biết)
|
|
35
|
+
12. [Xử lý sự cố](#xử-lý-sự-cố)
|
|
36
|
+
13. [Phát triển & test](#phát-triển--test)
|
|
37
|
+
14. [Giấy phép](#giấy-phép)
|
|
38
|
+
|
|
39
|
+
Hướng dẫn đưa lên git public, publish npm và cài vào dự án khác: [docs/PUBLISH-AND-INSTALL.md](docs/PUBLISH-AND-INSTALL.md).
|
|
40
|
+
|
|
41
|
+
## Tính năng
|
|
42
|
+
|
|
43
|
+
- **Gắn câu SQL vào đúng request** (kể cả khi chạy song song, kể cả khi pool connection bị bão hòa) bằng `AsyncLocalStorage`.
|
|
44
|
+
- **Cây lồng nhau**: `begin … commit` thành node transaction, `savepoint … release / rollback to` thành node con, trạng thái committed / rolled-back / open. Hai transaction chạy song song trong cùng request không bị trộn.
|
|
45
|
+
- **Subquery**: đo độ sâu `(SELECT …)` và số subquery; SQL được format theo cấp lồng để đọc như một cái cây.
|
|
46
|
+
- **Cảnh báo**: N+1, query lặp y hệt, query chậm, lỗi SQL, `SELECT *`, subquery sâu, quá nhiều query (và tuỳ chọn: quy ước "SELECT ngoài transaction").
|
|
47
|
+
- **Đa ngôn ngữ**: English, Tiếng Việt, 日本語 — terminal, trang web và nút nổi — thêm ngôn ngữ mới chỉ cần thêm một khối văn bản (xem [Ngôn ngữ](#ngôn-ngữ-i18n)).
|
|
48
|
+
- **Giao diện chuyên nghiệp**: sáng / tối / theo hệ thống, responsive tới cả panel dock hẹp, điều hướng bằng bàn phím, SQL có tô màu cú pháp, biểu đồ waterfall cho từng câu.
|
|
49
|
+
- **Ba cách xem**: cây in ra terminal · trang web live (`/__sql-inspector/`) · **nút nổi** trong chính ứng dụng của bạn (Nuxt tự gắn; ứng dụng khác gắn bằng 1 dòng).
|
|
50
|
+
- **Không sửa code truy vấn**: hoạt động với mọi thứ đi qua `mysql2` — mysql2 thuần, Drizzle (đã kiểm chứng), và về nguyên lý cả Knex / Sequelize / TypeORM… (xem [Giới hạn](#giới-hạn-đã-biết)).
|
|
51
|
+
- **Không phụ thuộc runtime nào** ngoài `mysql2` (peer). Trang web một file, không tải gì từ CDN.
|
|
52
|
+
- **An toàn mặc định**: không bao giờ chạy khi `NODE_ENV=production`; production là no-op hoàn toàn.
|
|
53
|
+
|
|
54
|
+
## Yêu cầu
|
|
55
|
+
|
|
56
|
+
- Node.js **≥ 20**
|
|
57
|
+
- `mysql2` **≥ 3** (peer dependency; đã kiểm chứng với 3.20)
|
|
58
|
+
- Ứng dụng của bạn phải gửi SQL qua `mysql2` (pool, connection, hoặc ORM dùng mysql2 bên dưới)
|
|
59
|
+
|
|
60
|
+
## Cài đặt
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
npm install --save-dev sql-dev-inspector
|
|
64
|
+
# hoặc
|
|
65
|
+
pnpm add -D sql-dev-inspector
|
|
66
|
+
yarn add -D sql-dev-inspector
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
> Tên package và các cách cài khác (từ GitHub, từ file `.tgz`) xem [docs/PUBLISH-AND-INSTALL.md](docs/PUBLISH-AND-INSTALL.md).
|
|
70
|
+
> Nếu ứng dụng của bạn import tool trong code chạy cả ở production (ví dụ Express), hãy cài vào `dependencies` thay vì `devDependencies` — tool tự thành no-op khi `NODE_ENV=production`.
|
|
71
|
+
|
|
72
|
+
## Bắt đầu nhanh
|
|
73
|
+
|
|
74
|
+
Trong mọi trường hợp, tool chỉ bật khi `NODE_ENV=development` (hoặc `SQL_INSPECTOR=1`) — xem [Cấu hình](#cấu-hình-biến-môi-trường).
|
|
75
|
+
|
|
76
|
+
### Nuxt 3 / 4
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
// nuxt.config.ts
|
|
80
|
+
export default defineNuxtConfig({
|
|
81
|
+
modules: ["sql-dev-inspector/nuxt"]
|
|
82
|
+
})
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Xong. Module chỉ đăng ký trong `nuxt dev` (không có gì trong `build` / `generate`): plugin Nitro phía server, và nút nổi phía client. Mở `http://localhost:3000/__sql-inspector/`, hoặc bấm nút **SQL** ở góc phải dưới.
|
|
86
|
+
|
|
87
|
+
Nếu app dùng `app.baseURL` (ví dụ `/alps`), đường dẫn là `/alps/__sql-inspector/`.
|
|
88
|
+
|
|
89
|
+
### Nitro thuần (không Nuxt)
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
// nitro.config.ts
|
|
93
|
+
export default defineNitroConfig({
|
|
94
|
+
plugins: ["sql-dev-inspector/nitro"]
|
|
95
|
+
})
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Express / Connect (và framework nhận middleware kiểu Connect)
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
import express from "express"
|
|
102
|
+
import { createSqlInspector } from "sql-dev-inspector"
|
|
103
|
+
|
|
104
|
+
const app = express()
|
|
105
|
+
const sqlInspector = createSqlInspector()
|
|
106
|
+
|
|
107
|
+
app.use(sqlInspector.middleware) // đặt TRƯỚC mọi route / middleware có chạy SQL
|
|
108
|
+
|
|
109
|
+
app.get("/orders/:id", async (req, res) => {
|
|
110
|
+
const [rows] = await pool.query("select * from orders where id = ?", [req.params.id])
|
|
111
|
+
res.json(rows)
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
app.listen(3000)
|
|
115
|
+
// mở http://localhost:3000/__sql-inspector/
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Node `http` thuần (hoặc framework khác)
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
import http from "node:http"
|
|
122
|
+
import { createSqlInspector } from "sql-dev-inspector"
|
|
123
|
+
|
|
124
|
+
const sqlInspector = createSqlInspector()
|
|
125
|
+
|
|
126
|
+
http.createServer((req, res) => {
|
|
127
|
+
if (sqlInspector.handle(req, res)) return // trang /__sql-inspector do tool phục vụ
|
|
128
|
+
sqlInspector.track(req, res, () => myApp(req, res)) // mọi await bên trong thuộc về request này
|
|
129
|
+
}).listen(3000)
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Job / cron / script (không phải HTTP request)
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
await sqlInspector.task("nightly-sync", async () => {
|
|
136
|
+
await syncCustomers()
|
|
137
|
+
})
|
|
138
|
+
// hiện trong danh sách với method TASK, tên "nightly-sync"
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Nút nổi cho ứng dụng không phải Nuxt
|
|
142
|
+
|
|
143
|
+
Ở phía trình duyệt (cùng origin với server), gọi một lần trong entry của frontend:
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
if (import.meta.env.DEV) {
|
|
147
|
+
const { mountSqlInspectorWidget } = await import("sql-dev-inspector/client")
|
|
148
|
+
mountSqlInspectorWidget({ baseURL: "/" }) // baseURL = tiền tố đường dẫn của server
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Nếu frontend chạy ở origin khác server (ví dụ Vite :5173, API :3000) thì dùng trang `http://localhost:3000/__sql-inspector/` trực tiếp, hoặc proxy `/__sql-inspector` qua dev server của frontend.
|
|
153
|
+
|
|
154
|
+
## Cách xem kết quả
|
|
155
|
+
|
|
156
|
+
### 1. Terminal
|
|
157
|
+
|
|
158
|
+
Mỗi request có chạy SQL in một cây ngay khi response kết thúc (không in request không chạy SQL). Tắt bằng `SQL_INSPECTOR_LOG=0`.
|
|
159
|
+
|
|
160
|
+
### 2. Trang web `/__sql-inspector/`
|
|
161
|
+
|
|
162
|
+
Danh sách request cập nhật live (SSE), có ô tìm kiếm và bộ lọc *Tất cả / Có vấn đề / Lỗi*. Bấm vào request để xem KPI (tổng thời gian, số query, thời gian SQL và tỉ lệ, số transaction, độ sâu subquery), các cảnh báo, và dòng thời gian các câu SQL kèm thanh waterfall. Bấm vào từng câu để xem SQL đã tô màu + format, SQL gốc, parameters, nút copy; bấm vào một cảnh báo để highlight các câu liên quan; đóng/mở từng transaction. "Theo dõi mới nhất" tự nhảy sang request mới. Bàn phím: `/` tìm kiếm, `↑` `↓` (hoặc `j` `k`) request trước/sau, `e` mở/đóng tất cả. Ngôn ngữ và giao diện (hệ thống / sáng / tối) nằm ở thanh trên.
|
|
163
|
+
|
|
164
|
+
### 3. Nút nổi trong app
|
|
165
|
+
|
|
166
|
+
Nút **SQL** nổi ở góc phải dưới. Số trên nút = số query của request gần nhất; vàng khi có cảnh báo, đỏ khi có lỗi.
|
|
167
|
+
|
|
168
|
+
| Thao tác | Cách làm |
|
|
169
|
+
|---|---|
|
|
170
|
+
| Mở / đóng panel | Bấm nút, hoặc `Alt+Shift+S`; `Esc` đóng khi đang focus trong panel |
|
|
171
|
+
| Ẩn / hiện nút | `Alt+Shift+D` (phím mở panel vẫn dùng được khi nút đã ẩn) |
|
|
172
|
+
| Đổi chỗ nút | Kéo nút |
|
|
173
|
+
| Đổi kích thước panel | Kéo mép panel |
|
|
174
|
+
| Dock đáy / dock phải | Hai biểu tượng dock ở đầu panel |
|
|
175
|
+
| Tự mở khi có cảnh báo / lỗi | Menu "⚙ Tuỳ chỉnh" (mặc định tắt) |
|
|
176
|
+
| Mở tab riêng · xoá lịch sử | Biểu tượng ↗ ở đầu panel · "Xoá lịch sử" trong menu tuỳ chỉnh |
|
|
177
|
+
| Tắt hẳn | Menu "⚙ Tuỳ chỉnh" → "Tắt SQL Inspector" (`Alt+Shift+S` bật lại) |
|
|
178
|
+
|
|
179
|
+
Cài đặt lưu trong `localStorage` (`sqlDevInspector.widget`). Panel chỉ tải khi mở lần đầu. Nút dựng bằng DOM thuần trong Shadow DOM: không phụ thuộc Vue/React và không bị CSS của app ảnh hưởng.
|
|
180
|
+
|
|
181
|
+
## Cảnh báo tự động
|
|
182
|
+
|
|
183
|
+
| Loại | Điều kiện | Mức |
|
|
184
|
+
|---|---|---|
|
|
185
|
+
| `n-plus-one` | ≥ 3 câu SELECT cùng "hình dạng" (khác giá trị) trong một request | warn |
|
|
186
|
+
| `duplicate` | cùng một câu SQL y hệt chạy ≥ 2 lần | warn |
|
|
187
|
+
| `slow` | ≥ 100ms (`SQL_INSPECTOR_SLOW_MS`) | warn |
|
|
188
|
+
| `error` | câu bị MySQL từ chối | error |
|
|
189
|
+
| `many-queries` | ≥ 30 câu trong một request | warn |
|
|
190
|
+
| `select-star` | `SELECT *` | info |
|
|
191
|
+
| `deep-subquery` | subquery lồng ≥ 3 tầng | info |
|
|
192
|
+
| `select-in-transaction`, `write-outside-transaction` | **tuỳ chọn** `SQL_INSPECTOR_TX_CONVENTION=1`: quy ước "SELECT ngoài transaction, transaction chỉ cho câu ghi" | info |
|
|
193
|
+
|
|
194
|
+
N+1 so sánh "fingerprint": SQL đã che giá trị literal và gộp `IN (…)` / `VALUES (…)`.
|
|
195
|
+
|
|
196
|
+
## Cấu hình (biến môi trường)
|
|
197
|
+
|
|
198
|
+
| Biến | Mặc định | Ý nghĩa |
|
|
199
|
+
|---|---|---|
|
|
200
|
+
| `NODE_ENV` | — | `production` ⇒ **luôn tắt** (kể cả `SQL_INSPECTOR=1`). `development` ⇒ bật |
|
|
201
|
+
| `SQL_INSPECTOR` | — | `1` bật cả khi `NODE_ENV` không phải development (ví dụ `test`); `0` tắt hẳn |
|
|
202
|
+
| `SQL_INSPECTOR_LANG` | `en` | Ngôn ngữ của terminal (`en`, `vi`, `ja`) và ngôn ngữ khởi đầu của trang web. Chỉ biến này có tác dụng — `LANG` của máy cố ý bị bỏ qua |
|
|
203
|
+
| `SQL_INSPECTOR_LOG` | bật | `0`: không in cây ra terminal |
|
|
204
|
+
| `SQL_INSPECTOR_SLOW_MS` | `100` | Ngưỡng cảnh báo chậm |
|
|
205
|
+
| `SQL_INSPECTOR_MAX_REQUESTS` | `200` | Số request giữ trong bộ nhớ |
|
|
206
|
+
| `SQL_INSPECTOR_MAX_QUERIES` | `1000` | Số câu tối đa giữ cho mỗi request |
|
|
207
|
+
| `SQL_INSPECTOR_TX_CONVENTION` | tắt | `1`: bật 2 cảnh báo quy ước transaction |
|
|
208
|
+
| `NO_COLOR` | — | Tắt màu ở terminal |
|
|
209
|
+
|
|
210
|
+
## API
|
|
211
|
+
|
|
212
|
+
Entry chính `sql-dev-inspector`:
|
|
213
|
+
|
|
214
|
+
```ts
|
|
215
|
+
createSqlInspector(): SqlInspector
|
|
216
|
+
interface SqlInspector {
|
|
217
|
+
enabled: boolean // false ở production / khi tắt: mọi thứ dưới đây là no-op
|
|
218
|
+
middleware(req, res, next): void // Connect/Express
|
|
219
|
+
handle(req, res): boolean // true nếu là URL của tool (đã phục vụ)
|
|
220
|
+
track<T>(req, res, fn: () => T): T // chạy fn như một request được theo dõi
|
|
221
|
+
task<T>(name: string, fn: () => T | Promise<T>): Promise<T> // đơn vị công việc không phải HTTP
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Mức thấp (cho tích hợp riêng): `startInspector()`, `getInspector()`, `isInspectorEnabled()`, `installTracer(store)`, `runInRequest(trace, fn)`, `TraceStore`, `buildReport(trace, options)`, `buildTree(queries)`, `analyzeSql(sql)`, `fingerprintOf(sql)`, `formatSql(sql)` và toàn bộ kiểu (`QueryEvent`, `RequestTrace`, `RequestReport`, `Insight`…).
|
|
226
|
+
|
|
227
|
+
Các entry khác:
|
|
228
|
+
|
|
229
|
+
| Import | Dùng cho |
|
|
230
|
+
|---|---|
|
|
231
|
+
| `sql-dev-inspector/nuxt` | Nuxt module (`modules: [...]`) |
|
|
232
|
+
| `sql-dev-inspector/nitro` | Nitro plugin (`plugins: [...]`) |
|
|
233
|
+
| `sql-dev-inspector/client` | `mountSqlInspectorWidget({ baseURL })` — nút nổi cho frontend bất kỳ |
|
|
234
|
+
|
|
235
|
+
Các route của tool (đều dưới `/__sql-inspector/`): `GET /` trang web · `GET /api/requests` · `GET /api/requests/:id` · `GET /api/stream` (SSE) · `POST /api/clear`.
|
|
236
|
+
|
|
237
|
+
## Cách hoạt động
|
|
238
|
+
|
|
239
|
+
Mọi câu SQL — dù đến từ `pool.query`, transaction, hay ORM — đều đi qua `Connection.prototype.query/execute` của mysql2. Tool patch đúng điểm đó (một lần, gỡ được) để ghi lại SQL, parameters, thời gian, số dòng, lỗi và **connection nào đã chạy câu lệnh**.
|
|
240
|
+
|
|
241
|
+
Vì sao không dùng cách khác:
|
|
242
|
+
|
|
243
|
+
| Phương án | Hạn chế |
|
|
244
|
+
|---|---|
|
|
245
|
+
| `logger` của ORM (ví dụ Drizzle) | Chỉ có SQL + params; không có thời gian, kết quả, connection ⇒ không dựng được cây transaction/savepoint, không biết request nào |
|
|
246
|
+
| `diagnostics_channel` của mysql2 ≥ 3.20 | Có thời gian nhưng **không cho biết connection** ⇒ hai transaction song song trong một request bị trộn |
|
|
247
|
+
| `general_log` của MySQL | Không biết request nào, không có thời gian/cây lồng, cần quyền trên DB |
|
|
248
|
+
|
|
249
|
+
Gắn query vào request:
|
|
250
|
+
|
|
251
|
+
1. Adapter bọc request handler bằng `AsyncLocalStorage`, nên mọi `await` bên dưới đều thuộc request đó.
|
|
252
|
+
2. `pool.query()` đưa câu lệnh cho connection qua callback; khi mọi connection bận, callback chạy từ context của *request khác*. Tool bọc callback của `Pool.getConnection` bằng `AsyncLocalStorage.bind` để không gán nhầm.
|
|
253
|
+
3. Cây transaction gom theo connection; "subquery lồng" đo bằng độ sâu `(SELECT …)` trên SQL đã che literal (không đếm ngoặc thường).
|
|
254
|
+
|
|
255
|
+
## Ngôn ngữ (i18n)
|
|
256
|
+
|
|
257
|
+
Có sẵn English, Tiếng Việt (`vi`), 日本語 (`ja`).
|
|
258
|
+
|
|
259
|
+
- **Trang web**: ưu tiên lựa chọn đã lưu → `SQL_INSPECTOR_LANG` (nếu đặt) → ngôn ngữ trình duyệt → English. Đổi bất cứ lúc nào ở thanh trên; `?lang=ja` trên URL ghi đè cho lần xem đó.
|
|
260
|
+
- **Nút nổi**: theo ngôn ngữ trình duyệt, hoặc chọn trong menu tuỳ chỉnh (panel tải lại bằng ngôn ngữ đó).
|
|
261
|
+
- **Terminal và trường `message` của API**: `SQL_INSPECTOR_LANG`, mặc định English.
|
|
262
|
+
|
|
263
|
+
Cảnh báo (insight) được trả về dạng dữ liệu (`type` + `params`) cùng với `message` đã dịch sẵn, nên client nào cũng tự diễn đạt lại được.
|
|
264
|
+
|
|
265
|
+
**Thêm ngôn ngữ mới**: sao chép khối `en` trong `src/i18n/messages.ts`, dịch các giá trị (giữ nguyên mọi `{placeholder}`; thêm biến thể `.one` khi số ít khác số nhiều, ví dụ `"term.queries.one"`), thêm mã vào kiểu `Locale` và `LOCALES` trong `src/i18n/index.ts`. `npm test` sẽ fail nếu thiếu key hoặc placeholder.
|
|
266
|
+
|
|
267
|
+
## Style & cô lập
|
|
268
|
+
|
|
269
|
+
Tool không bao giờ được làm giao diện của ứng dụng xấu đi, nên:
|
|
270
|
+
|
|
271
|
+
- **Nút nổi và panel** nằm trong **shadow root**, reset bằng `all: initial`, host là custom element (không phải `<div>`), các reset có độ ưu tiên bằng 0 (`:where`). CSS toàn cục của app (`button { … }`, `* { … }`, `select`, `body > *`) không đụng được vào chúng, và chúng không thêm CSS nào vào trang. Chúng là `position: fixed`, không làm xô lệch layout, không cướp focus.
|
|
272
|
+
- **Trang inspector** là một document riêng (nằm trong `<iframe>` khi dock) nên cũng không chạm được vào trang của bạn. Mọi control gốc (nút, switch, thanh cuộn) đều được vẽ lại, và bộ chọn ngôn ngữ là listbox tự vẽ — popup của `<select>` gốc do hệ điều hành vẽ, CSS không đụng được nên không dùng `<select>` ở bất kỳ đâu (có test kiểm tra), khai báo `color-scheme` để dark mode không rơi về giao diện mặc định của trình duyệt.
|
|
273
|
+
- **Không gọi ra ngoài**: không web font, không CDN, không analytics — chỉ dùng font hệ thống (có cả tiếng Nhật, tiếng Việt).
|
|
274
|
+
- Tôn trọng `prefers-color-scheme`, `prefers-reduced-motion`, focus bàn phím (`:focus-visible`).
|
|
275
|
+
- Nếu app đặt Content-Security-Policy chặt, nút nổi cần `style-src 'unsafe-inline'` (nó chèn một `<style>` vào shadow root) và `frame-src 'self'`. Tool chỉ dùng khi dev; hãy nới chính sách chỉ cho môi trường dev.
|
|
276
|
+
|
|
277
|
+
## An toàn & quyền riêng tư
|
|
278
|
+
|
|
279
|
+
- `NODE_ENV=production` là **hard stop**: không patch gì, middleware chỉ gọi `next()`.
|
|
280
|
+
- Nuxt module chỉ đăng ký trong `nuxt dev`; `build` / `generate` không chứa gì của tool.
|
|
281
|
+
- Dữ liệu chỉ ở **bộ nhớ** của tiến trình dev, không ghi file, không gửi đi đâu, không gọi mạng.
|
|
282
|
+
- ⚠️ **SQL hiển thị gồm cả giá trị thật** (kể cả dữ liệu cá nhân của DB bạn đang trỏ tới). Đừng chia sẻ màn hình / ảnh chụp khi trỏ vào DB staging hoặc production-like.
|
|
283
|
+
- Trang `/__sql-inspector/` không có xác thực: chỉ để chạy ở `localhost`. Đừng mở cổng dev ra mạng chung khi đang bật tool (ví dụ `--host 0.0.0.0`).
|
|
284
|
+
- Trong Nuxt/Nitro, trang được phục vụ trước mọi middleware (không cần đăng nhập, và query xác thực của app không làm nhiễu kết quả).
|
|
285
|
+
|
|
286
|
+
## Giới hạn đã biết
|
|
287
|
+
|
|
288
|
+
- **Chỉ MySQL qua `mysql2`.** PostgreSQL, SQLite, hoặc driver MySQL khác (ví dụ `mariadb`) không được hỗ trợ. ORM không dùng mysql2 bên dưới (ví dụ engine riêng của Prisma) không đi qua điểm patch.
|
|
289
|
+
- **Đã kiểm chứng**: mysql2 thuần (pool, connection, transaction thủ công) và Drizzle ORM. Knex / Sequelize / TypeORM dùng mysql2 nên *về nguyên lý* chạy được, nhưng chưa có test riêng — nếu gặp vấn đề hãy mở issue.
|
|
290
|
+
- **Không biết dòng code nào gọi query**: ORM như Drizzle trả builder dạng thenable nên khi query thật sự chạy, call stack không còn frame của code gọi. (Đã thử, nên không có tính năng "caller".)
|
|
291
|
+
- Một builder tạo trong scope request nhưng `await` ở ngoài scope (ví dụ trả về rồi await sau khi response đóng) sẽ không gắn về request.
|
|
292
|
+
- Câu lệnh chạy ngoài request/task (startup, timer) được gom vào mục `(ngoài request …)`.
|
|
293
|
+
- Request không chạy SQL (asset, HMR) không được lưu.
|
|
294
|
+
- Adapter Express/Connect đã được test với `req`/`res` giả đúng giao diện Connect (chưa có test chạy trên `express` thật); nút nổi ngoài Nuxt (`mountSqlInspectorWidget`) mới được test trong happy-dom.
|
|
295
|
+
- Đã test với `mysql2` 3.20 và Node 24; Node 20+ theo `engines`.
|
|
296
|
+
|
|
297
|
+
## Xử lý sự cố
|
|
298
|
+
|
|
299
|
+
| Triệu chứng | Nguyên nhân thường gặp |
|
|
300
|
+
|---|---|
|
|
301
|
+
| Không thấy gì, không có dòng `[sql-inspector] bật` | `NODE_ENV` không phải `development`: chạy với `NODE_ENV=development` hoặc `SQL_INSPECTOR=1` (và không phải `production`) |
|
|
302
|
+
| Có terminal log nhưng trang `/__sql-inspector/` 404 | Express: `sqlInspector.middleware` phải đặt trước router; Nitro/Nuxt: kiểm tra `app.baseURL` |
|
|
303
|
+
| Query nằm hết trong `(ngoài request …)` | Middleware đặt sau route đã chạy SQL; hoặc SQL chạy trong scope không `await` từ handler (timer, queue) — bọc bằng `sqlInspector.task()` |
|
|
304
|
+
| Nút nổi không hiện | Không phải Nuxt: chưa gọi `mountSqlInspectorWidget`. Nuxt: chỉ có trong `nuxt dev`. Thử `Alt+Shift+D` (có thể đã ẩn) hoặc xoá key `sqlDevInspector.widget` trong localStorage |
|
|
305
|
+
| Query của ORM X không hiện | ORM đó không dùng `mysql2`. Kiểm tra driver bên dưới |
|
|
306
|
+
| Hai bản `mysql2` trong `node_modules` | Tool patch bản mà nó `import` được. Đảm bảo app và tool dùng chung một bản (peer dependency được hoist) |
|
|
307
|
+
|
|
308
|
+
## Phát triển & test
|
|
309
|
+
|
|
310
|
+
```bash
|
|
311
|
+
npm install
|
|
312
|
+
npm run typecheck # tsc --noEmit (src + test)
|
|
313
|
+
npm test # vitest: 131 test
|
|
314
|
+
npm run build # tsc -> dist/
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
Test tích hợp chạy **mysql2 thật + Drizzle thật** với một MySQL giả nói đúng wire protocol (`test/mock-mysql-server.ts`, nối bằng stream trong bộ nhớ nên không cần mở cổng hay cài MySQL): transaction, savepoint lồng, rollback, hai transaction song song, 8 request đồng thời trên pool 3 connection, lỗi SQL, N+1, SSE, gỡ patch, adapter Express/`http`, nút nổi và trang web (happy-dom).
|
|
318
|
+
|
|
319
|
+
Cấu trúc:
|
|
320
|
+
|
|
321
|
+
```
|
|
322
|
+
src/
|
|
323
|
+
├─ index.ts # API công khai
|
|
324
|
+
├─ node.ts # createSqlInspector: Express / http / task
|
|
325
|
+
├─ nitro-plugin.ts # Nitro / Nuxt (server)
|
|
326
|
+
├─ nuxt.ts # Nuxt module (chỉ đăng ký trong dev)
|
|
327
|
+
├─ setup.ts # bật tracer + in terminal, một lần mỗi process
|
|
328
|
+
├─ tracer.ts # patch mysql2 + AsyncLocalStorage
|
|
329
|
+
├─ store.ts # ring buffer request/query + pub/sub
|
|
330
|
+
├─ sql-analyzer.ts # loại câu lệnh, bảng, subquery, fingerprint, format
|
|
331
|
+
├─ report.ts # cây transaction/savepoint + cảnh báo
|
|
332
|
+
├─ terminal.ts # in cây ra terminal
|
|
333
|
+
├─ inspector-server.ts # route /__sql-inspector (UI, JSON, SSE)
|
|
334
|
+
├─ ui.ts # trang web một file
|
|
335
|
+
├─ runtime.ts # cờ môi trường + singleton
|
|
336
|
+
└─ client/ # nút nổi (widget.ts) + plugin client của Nuxt
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
## Giấy phép
|
|
340
|
+
|
|
341
|
+
MIT — xem [LICENSE](LICENSE).
|
package/README.md
CHANGED
|
@@ -1,3 +1,268 @@
|
|
|
1
|
-
#
|
|
1
|
+
# sql-dev-inspector
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**See exactly which SQL your backend ran for each request — in development only.**
|
|
4
|
+
|
|
5
|
+
When a client calls your API, `sql-dev-inspector` shows every statement your server sent to MySQL: in what order, inside which transaction or savepoint, how deeply subqueries nest, how long each one took and how many rows it returned. It also flags N+1 loops, repeated queries, slow queries and SQL errors.
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
GET /api/orders/42 → 200 163ms total · 12 queries · 148ms in SQL
|
|
9
|
+
├─ SELECT order_items 3.86ms · 2 rows
|
|
10
|
+
│ select `id`, `sku` from `order_items` where `order_id` = 11
|
|
11
|
+
├─ SELECT customers, orders ⤷subquery×3 (depth 3) 1.50ms · 2 rows
|
|
12
|
+
├─ TRANSACTION [conn 101] committed 10.2ms
|
|
13
|
+
│ ├─ INSERT orders 1.39ms · 1 affected
|
|
14
|
+
│ ├─ SAVEPOINT sp1 [conn 101] rolled-back 4.30ms
|
|
15
|
+
│ │ └─ SELECT 0.18ms ✖ You have an error in your SQL syntax …
|
|
16
|
+
│ └─ SELECT orders 1.33ms · 2 rows
|
|
17
|
+
⚠ N+1 suspected: 4×, 8.4ms total — select `id`, `sku` from `order_items` where `order_id` = ?
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Features
|
|
21
|
+
|
|
22
|
+
- **Per-request attribution** — every statement is tied to the request that caused it (concurrent requests and a saturated connection pool included), using `AsyncLocalStorage`.
|
|
23
|
+
- **Nesting tree** — transactions and savepoints, with committed / rolled-back / open status. Two transactions running in parallel inside one request are kept apart.
|
|
24
|
+
- **Subquery depth** and pretty-printed SQL.
|
|
25
|
+
- **Multilingual**: English, Vietnamese and Japanese — terminal, web page and floating button — and adding a language is one block of text (see [Languages](#languages)).
|
|
26
|
+
- **Professional UI**: light / dark / system theme, responsive down to a narrow docked panel, keyboard navigation, syntax-highlighted SQL, timeline waterfall per statement.
|
|
27
|
+
- **Three ways to look**: a tree in the terminal, a live web page (`/__sql-inspector/`), and a floating in-app launcher button with keyboard shortcuts.
|
|
28
|
+
- **No changes to your queries** — it works at the `mysql2` driver level, so it covers plain `mysql2` and Drizzle ORM (both tested), and in principle anything else built on `mysql2`.
|
|
29
|
+
- **Zero runtime dependencies** besides the `mysql2` peer. The web UI is a single self-contained page; nothing is loaded from a CDN.
|
|
30
|
+
- **Safe by default** — never active when `NODE_ENV=production`; there it is a complete no-op.
|
|
31
|
+
|
|
32
|
+
## Requirements
|
|
33
|
+
|
|
34
|
+
- Node.js **20+**
|
|
35
|
+
- `mysql2` **3+** (peer dependency; tested with 3.20)
|
|
36
|
+
- Your app must send SQL through `mysql2` (pool, connection, or an ORM that uses it)
|
|
37
|
+
|
|
38
|
+
## Install
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npm install --save-dev sql-dev-inspector
|
|
42
|
+
# or
|
|
43
|
+
pnpm add -D sql-dev-inspector
|
|
44
|
+
yarn add -D sql-dev-inspector
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Other ways to install (from GitHub, from a `.tgz` file): see [docs/PUBLISH-AND-INSTALL.md](docs/PUBLISH-AND-INSTALL.md) *(Vietnamese)*.
|
|
48
|
+
|
|
49
|
+
## Quick start
|
|
50
|
+
|
|
51
|
+
The inspector turns on when `NODE_ENV=development` (or `SQL_INSPECTOR=1`). Most dev servers (`nuxt dev`, `next dev`, `vite`) set `NODE_ENV` for you; for a plain Node script run `NODE_ENV=development node server.js`.
|
|
52
|
+
|
|
53
|
+
### Nuxt 3 / 4
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
// nuxt.config.ts
|
|
57
|
+
export default defineNuxtConfig({
|
|
58
|
+
modules: ["sql-dev-inspector/nuxt"]
|
|
59
|
+
})
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
That's it. The module registers itself in `nuxt dev` only (nothing is added to `build` / `generate`): a Nitro plugin on the server and a floating **SQL** button in the app. Open `http://localhost:3000/__sql-inspector/` or click the button. If you set `app.baseURL`, prefix the path with it.
|
|
63
|
+
|
|
64
|
+
### Nitro (without Nuxt)
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
// nitro.config.ts
|
|
68
|
+
export default defineNitroConfig({
|
|
69
|
+
plugins: ["sql-dev-inspector/nitro"]
|
|
70
|
+
})
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Express / Connect
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
import express from "express"
|
|
77
|
+
import { createSqlInspector } from "sql-dev-inspector"
|
|
78
|
+
|
|
79
|
+
const app = express()
|
|
80
|
+
const sqlInspector = createSqlInspector()
|
|
81
|
+
|
|
82
|
+
app.use(sqlInspector.middleware) // mount BEFORE any route that runs SQL
|
|
83
|
+
|
|
84
|
+
app.get("/orders/:id", async (req, res) => {
|
|
85
|
+
const [rows] = await pool.query("select * from orders where id = ?", [req.params.id])
|
|
86
|
+
res.json(rows)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
app.listen(3000)
|
|
90
|
+
// open http://localhost:3000/__sql-inspector/
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Plain Node `http` (or any other framework)
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
import http from "node:http"
|
|
97
|
+
import { createSqlInspector } from "sql-dev-inspector"
|
|
98
|
+
|
|
99
|
+
const sqlInspector = createSqlInspector()
|
|
100
|
+
|
|
101
|
+
http.createServer((req, res) => {
|
|
102
|
+
if (sqlInspector.handle(req, res)) return // serves the inspector's own pages
|
|
103
|
+
sqlInspector.track(req, res, () => myApp(req, res)) // everything awaited inside belongs to this request
|
|
104
|
+
}).listen(3000)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Jobs, cron, scripts (not HTTP requests)
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
await sqlInspector.task("nightly-sync", async () => {
|
|
111
|
+
await syncCustomers()
|
|
112
|
+
})
|
|
113
|
+
// shows up in the list as a TASK named "nightly-sync"
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Floating button in a non-Nuxt frontend
|
|
117
|
+
|
|
118
|
+
In your browser entry point (same origin as the server):
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
if (import.meta.env.DEV) {
|
|
122
|
+
const { mountSqlInspectorWidget } = await import("sql-dev-inspector/client")
|
|
123
|
+
mountSqlInspectorWidget({ baseURL: "/" }) // baseURL = path prefix of your server
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
If your frontend runs on a different origin than the API (e.g. Vite on :5173, API on :3000), open `http://localhost:3000/__sql-inspector/` directly, or proxy `/__sql-inspector` through the frontend dev server.
|
|
128
|
+
|
|
129
|
+
## Viewing the results
|
|
130
|
+
|
|
131
|
+
1. **Terminal** — each request that ran SQL prints a tree when its response finishes. Silence with `SQL_INSPECTOR_LOG=0`.
|
|
132
|
+
2. **Web page** `/__sql-inspector/` — live list of requests with search and *All / Issues / Errors* filters; click one for KPIs (total time, queries, SQL time and its share, transactions, subquery depth), insights, and a timeline of statements with a waterfall bar each. Click a statement for highlighted + formatted SQL, raw SQL, parameters and a copy button; click an insight to highlight the statements it is about; collapse a transaction. *Follow latest* jumps to each new request. Keyboard: `/` search, `↑` `↓` (or `j` `k`) previous / next request, `e` expand / collapse all. Language and theme (system / light / dark) are in the top bar.
|
|
133
|
+
3. **Floating button** — the number on it is the query count of the latest request (amber with warnings, red with errors).
|
|
134
|
+
|
|
135
|
+
| Action | How |
|
|
136
|
+
|---|---|
|
|
137
|
+
| Open / close the panel | Click the button, or `Alt+Shift+S`; `Esc` closes it while focus is inside |
|
|
138
|
+
| Hide / show the button | `Alt+Shift+D` (the panel shortcut keeps working while hidden) |
|
|
139
|
+
| Move the button | Drag it |
|
|
140
|
+
| Resize the panel | Drag its edge |
|
|
141
|
+
| Dock bottom / right | the two dock icons in the panel header |
|
|
142
|
+
| Auto-open on warnings or errors | Settings menu (off by default) |
|
|
143
|
+
| Open in a new tab · clear history | the ↗ icon in the header · "Clear history" in the settings menu |
|
|
144
|
+
| Turn it off completely | Settings menu → disable (`Alt+Shift+S` brings it back) |
|
|
145
|
+
|
|
146
|
+
Settings (including language) are stored in `localStorage`. The panel loads only the first time it is opened.
|
|
147
|
+
|
|
148
|
+
## Insights
|
|
149
|
+
|
|
150
|
+
| Insight | Triggered by | Level |
|
|
151
|
+
|---|---|---|
|
|
152
|
+
| N+1 | ≥ 3 SELECTs with the same shape (different values) in one request | warn |
|
|
153
|
+
| Duplicate | the exact same statement ≥ 2 times | warn |
|
|
154
|
+
| Slow | ≥ 100 ms (`SQL_INSPECTOR_SLOW_MS`) | warn |
|
|
155
|
+
| SQL error | the statement was rejected | error |
|
|
156
|
+
| Many queries | ≥ 30 statements in one request | warn |
|
|
157
|
+
| `SELECT *` · deep subquery (≥ 3 levels) | | info |
|
|
158
|
+
| Transaction convention *(opt-in via `SQL_INSPECTOR_TX_CONVENTION=1`)* | reads inside a transaction; several writes outside one | info |
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
## Configuration
|
|
162
|
+
|
|
163
|
+
| Variable | Default | Meaning |
|
|
164
|
+
|---|---|---|
|
|
165
|
+
| `NODE_ENV` | — | `production` ⇒ **always off** (even with `SQL_INSPECTOR=1`); `development` ⇒ on |
|
|
166
|
+
| `SQL_INSPECTOR` | — | `1` forces on outside production (e.g. `test`); `0` forces off |
|
|
167
|
+
| `SQL_INSPECTOR_LANG` | `en` | Language of terminal output (`en`, `vi`, `ja`) and the web page's starting language. Only this variable counts — your machine's `LANG` is deliberately ignored |
|
|
168
|
+
| `SQL_INSPECTOR_LOG` | on | `0` disables the terminal output |
|
|
169
|
+
| `SQL_INSPECTOR_SLOW_MS` | `100` | slow-query threshold |
|
|
170
|
+
| `SQL_INSPECTOR_MAX_REQUESTS` | `200` | requests kept in memory |
|
|
171
|
+
| `SQL_INSPECTOR_MAX_QUERIES` | `1000` | statements kept per request |
|
|
172
|
+
| `SQL_INSPECTOR_TX_CONVENTION` | off | `1` enables the transaction-convention insights |
|
|
173
|
+
| `NO_COLOR` | — | disables terminal colors |
|
|
174
|
+
|
|
175
|
+
## API
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
createSqlInspector(): SqlInspector
|
|
179
|
+
|
|
180
|
+
interface SqlInspector {
|
|
181
|
+
enabled: boolean // false in production / when switched off: everything below is a no-op
|
|
182
|
+
middleware(req, res, next): void // Connect / Express
|
|
183
|
+
handle(req, res): boolean // true if the URL belongs to the inspector (already served)
|
|
184
|
+
track<T>(req, res, fn: () => T): T // run fn as one tracked request
|
|
185
|
+
task<T>(name: string, fn: () => T | Promise<T>): Promise<T> // a unit of work that is not HTTP
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Lower level, for custom integrations: `startInspector`, `getInspector`, `isInspectorEnabled`, `installTracer`, `runInRequest`, `TraceStore`, `buildReport`, `buildTree`, `analyzeSql`, `fingerprintOf`, `formatSql`, plus all types.
|
|
190
|
+
|
|
191
|
+
| Import | For |
|
|
192
|
+
|---|---|
|
|
193
|
+
| `sql-dev-inspector` | main API above |
|
|
194
|
+
| `sql-dev-inspector/nuxt` | Nuxt module |
|
|
195
|
+
| `sql-dev-inspector/nitro` | Nitro plugin |
|
|
196
|
+
| `sql-dev-inspector/client` | `mountSqlInspectorWidget({ baseURL })` — floating button for any frontend |
|
|
197
|
+
|
|
198
|
+
HTTP routes, all under `/__sql-inspector/`: `GET /` (page), `GET /api/requests`, `GET /api/requests/:id`, `GET /api/stream` (SSE), `POST /api/clear`.
|
|
199
|
+
|
|
200
|
+
## How it works
|
|
201
|
+
|
|
202
|
+
Every statement — from `pool.query`, a transaction or an ORM — goes through `Connection.prototype.query/execute` in `mysql2`. The inspector patches exactly that point (once, reversibly) to record the SQL, parameters, duration, row count, error and **which connection ran it**. The connection is what lets it rebuild transaction and savepoint nesting, which neither an ORM logger nor mysql2's `diagnostics_channel` can do. Each adapter wraps your request handler in an `AsyncLocalStorage` scope; the pool's `getConnection` callback is bound to the caller's context so statements queued behind a busy pool are not attributed to the wrong request.
|
|
203
|
+
|
|
204
|
+
## Languages
|
|
205
|
+
|
|
206
|
+
English, Tiếng Việt (`vi`) and 日本語 (`ja`) are built in.
|
|
207
|
+
|
|
208
|
+
- **Web page**: first the saved choice, then `SQL_INSPECTOR_LANG` if set, then the browser language, then English. Change it any time from the top bar; `?lang=ja` in the URL overrides for that visit.
|
|
209
|
+
- **Floating button**: follows the browser language, or pick one in its settings menu (the panel reloads in that language).
|
|
210
|
+
- **Terminal and API `message` fields**: `SQL_INSPECTOR_LANG`, default English.
|
|
211
|
+
|
|
212
|
+
Insights are returned as data (`type` + `params`) next to the ready-made `message`, so any client can phrase them itself.
|
|
213
|
+
|
|
214
|
+
**Adding a language**: copy the `en` block in `src/i18n/messages.ts`, translate the values (keep every `{placeholder}`; add a `.one` variant where the singular differs, e.g. `"term.queries.one"`), add the code to the `Locale` type and to `LOCALES` in `src/i18n/index.ts`. `npm test` fails if a key or placeholder is missing. Pull requests welcome.
|
|
215
|
+
|
|
216
|
+
## Styling & isolation
|
|
217
|
+
|
|
218
|
+
The tool must never make your app look worse, so:
|
|
219
|
+
|
|
220
|
+
- **Floating button and panel** live in a **shadow root**, reset with `all: initial`, use a custom-element host (not a `<div>`) and zero-specificity resets. Your global CSS (`button { … }`, `* { … }`, `select`, `body > *`) cannot restyle them, and they add no CSS to your page. They are `position: fixed`, never shift your layout, and don't steal focus.
|
|
221
|
+
- **Inspector page** is its own document (inside an `<iframe>` when docked), so it cannot touch your page either. Every native control (buttons, switch, scrollbars) is restyled, and the language picker is a custom listbox — a native `<select>` popup is drawn by the OS and cannot be styled, so none is used anywhere (a test enforces it), with `color-scheme` declared so nothing falls back to a browser-default look in dark mode.
|
|
222
|
+
- **No external requests**: no web fonts, no CDN, no analytics — system font stacks only (Latin, Japanese and Vietnamese included).
|
|
223
|
+
- Respects `prefers-color-scheme`, `prefers-reduced-motion`, and keyboard focus (`:focus-visible`).
|
|
224
|
+
- If your app sets a strict Content-Security-Policy, the floating button needs `style-src 'unsafe-inline'` (it injects one `<style>` into its shadow root) and `frame-src 'self'`. This is a dev-only tool; relax the policy for development only.
|
|
225
|
+
|
|
226
|
+
## Security & privacy
|
|
227
|
+
|
|
228
|
+
- `NODE_ENV=production` is a hard stop: nothing is patched and the middleware only calls `next()`.
|
|
229
|
+
- The Nuxt module registers only in `nuxt dev`; `build` / `generate` contain nothing of this tool.
|
|
230
|
+
- Data lives in the memory of the dev process only: no files written, no network calls.
|
|
231
|
+
- ⚠️ **Displayed SQL contains real values**, possibly personal data from the database you point at. Don't share your screen or screenshots while connected to staging or production-like data.
|
|
232
|
+
- `/__sql-inspector/` has **no authentication** — it is meant for `localhost`. Don't expose your dev server to a shared network (e.g. `--host 0.0.0.0`) while it is on.
|
|
233
|
+
|
|
234
|
+
## Limitations
|
|
235
|
+
|
|
236
|
+
- **MySQL via `mysql2` only.** Not PostgreSQL, SQLite, or other MySQL drivers (e.g. `mariadb`). ORMs that don't use `mysql2` underneath (e.g. Prisma's own engine) bypass the patched point.
|
|
237
|
+
- **Tested with**: plain `mysql2` (pool, connection, manual transactions) and Drizzle ORM. Knex / Sequelize / TypeORM use `mysql2` and should work, but have no dedicated tests — please open an issue if they don't.
|
|
238
|
+
- The Express/Connect adapter is tested with a faithful fake `req`/`res`, not with `express` itself; the floating button outside Nuxt is tested in happy-dom only.
|
|
239
|
+
- **No "caller" line**: ORMs like Drizzle return lazy thenables, so by the time the query runs the call stack no longer contains your code.
|
|
240
|
+
- A lazy builder created inside a request scope but awaited outside it is not attributed to that request.
|
|
241
|
+
- Statements outside any request or task are grouped under one "(outside request)" entry.
|
|
242
|
+
- Requests that run no SQL (assets, HMR) are not kept.
|
|
243
|
+
|
|
244
|
+
## Troubleshooting
|
|
245
|
+
|
|
246
|
+
| Symptom | Likely cause |
|
|
247
|
+
|---|---|
|
|
248
|
+
| Nothing shows up | `NODE_ENV` isn't `development`; run with `NODE_ENV=development` or `SQL_INSPECTOR=1` (and never `production`) |
|
|
249
|
+
| Terminal output but `/__sql-inspector/` is 404 | Express: mount the middleware before your router. Nuxt/Nitro: check `app.baseURL` |
|
|
250
|
+
| All queries under "(outside request)" | middleware mounted after the route that ran SQL, or SQL ran outside an awaited handler (timers, queues) — wrap it in `sqlInspector.task()` |
|
|
251
|
+
| Floating button missing | Non-Nuxt: you haven't called `mountSqlInspectorWidget`. Nuxt: it exists in `nuxt dev` only. Try `Alt+Shift+D`, or clear the `sqlDevInspector.widget` key in `localStorage` |
|
|
252
|
+
| An ORM's queries are missing | It doesn't use `mysql2` underneath |
|
|
253
|
+
| Two copies of `mysql2` in `node_modules` | The inspector patches the copy it can import; make sure app and inspector share one (hoisted peer) |
|
|
254
|
+
|
|
255
|
+
## Development
|
|
256
|
+
|
|
257
|
+
```bash
|
|
258
|
+
npm install
|
|
259
|
+
npm run typecheck
|
|
260
|
+
npm test # 131 tests: real mysql2 + Drizzle against an in-memory MySQL protocol server
|
|
261
|
+
npm run build
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
Tests need no MySQL installation and open no ports: `test/mock-mysql-server.ts` speaks the real wire protocol over in-memory streams.
|
|
265
|
+
|
|
266
|
+
## License
|
|
267
|
+
|
|
268
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { defineNuxtPlugin, useRuntimeConfig } from "#app";
|
|
2
|
+
import { mountSqlInspectorWidget } from "./widget.js";
|
|
3
|
+
// Dev only, twice over: the module registers this plugin in `nuxt dev` only, and the plugin
|
|
4
|
+
// itself does nothing outside dev.
|
|
5
|
+
export default defineNuxtPlugin(() => {
|
|
6
|
+
const config = useRuntimeConfig();
|
|
7
|
+
if (!import.meta.dev) {
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
mountSqlInspectorWidget({ baseURL: config.app.baseURL });
|
|
11
|
+
});
|