dash-devtools 1.0.0__py3-none-any.whl
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.
- dash_devtools/__init__.py +8 -0
- dash_devtools/__main__.py +11 -0
- dash_devtools/ai_engine.py +441 -0
- dash_devtools/browser.py +541 -0
- dash_devtools/cli.py +1452 -0
- dash_devtools/database.py +338 -0
- dash_devtools/dbdiagram.py +183 -0
- dash_devtools/e2e.py +329 -0
- dash_devtools/fixers/__init__.py +57 -0
- dash_devtools/fixers/migration_fixer.py +115 -0
- dash_devtools/fixers/ux_fixer.py +106 -0
- dash_devtools/fixers/version_bumper.py +115 -0
- dash_devtools/gas_mes_test.py +1241 -0
- dash_devtools/generators/__init__.py +84 -0
- dash_devtools/health.py +476 -0
- dash_devtools/hooks/__init__.py +250 -0
- dash_devtools/hooks/pre_commit.py +161 -0
- dash_devtools/hooks/pre_push.py +275 -0
- dash_devtools/init_test.py +352 -0
- dash_devtools/markdown_report.py +309 -0
- dash_devtools/migrators/__init__.py +21 -0
- dash_devtools/perf.py +321 -0
- dash_devtools/report.py +667 -0
- dash_devtools/reporters/__init__.py +11 -0
- dash_devtools/spec.py +230 -0
- dash_devtools/stats.py +355 -0
- dash_devtools/test_suite.py +690 -0
- dash_devtools/testing.py +416 -0
- dash_devtools/validators/__init__.py +157 -0
- dash_devtools/validators/backend/__init__.py +12 -0
- dash_devtools/validators/backend/nodejs.py +245 -0
- dash_devtools/validators/backend/python.py +439 -0
- dash_devtools/validators/code_quality.py +243 -0
- dash_devtools/validators/common/__init__.py +11 -0
- dash_devtools/validators/common/quality.py +319 -0
- dash_devtools/validators/common/security.py +270 -0
- dash_devtools/validators/common/spec.py +273 -0
- dash_devtools/validators/detector.py +394 -0
- dash_devtools/validators/frontend/__init__.py +14 -0
- dash_devtools/validators/frontend/angular.py +245 -0
- dash_devtools/validators/frontend/gas.py +310 -0
- dash_devtools/validators/frontend/vite.py +539 -0
- dash_devtools/validators/migration.py +292 -0
- dash_devtools/validators/performance.py +167 -0
- dash_devtools/validators/security.py +205 -0
- dash_devtools/vision/__init__.py +368 -0
- dash_devtools/watch.py +266 -0
- dash_devtools/word_report.py +690 -0
- dash_devtools-1.0.0.dist-info/METADATA +834 -0
- dash_devtools-1.0.0.dist-info/RECORD +53 -0
- dash_devtools-1.0.0.dist-info/WHEEL +5 -0
- dash_devtools-1.0.0.dist-info/entry_points.txt +2 -0
- dash_devtools-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
"""
|
|
2
|
+
資料庫遷移管理模組
|
|
3
|
+
|
|
4
|
+
支援 Alembic + SQLModel 的自動遷移管理
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
import subprocess
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _run_alembic(project: str, args: list[str], capture: bool = True) -> dict:
|
|
15
|
+
"""執行 Alembic 指令
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
project: 專案路徑
|
|
19
|
+
args: Alembic 參數
|
|
20
|
+
capture: 是否擷取輸出
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
dict 包含 success, stdout, stderr
|
|
24
|
+
"""
|
|
25
|
+
project_path = Path(project).resolve()
|
|
26
|
+
|
|
27
|
+
# 檢查是否有 alembic.ini
|
|
28
|
+
alembic_ini = project_path / 'alembic.ini'
|
|
29
|
+
if not alembic_ini.exists():
|
|
30
|
+
return {
|
|
31
|
+
'success': False,
|
|
32
|
+
'error': '找不到 alembic.ini,請先執行 dash db init'
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
result = subprocess.run(
|
|
37
|
+
['alembic'] + args,
|
|
38
|
+
cwd=str(project_path),
|
|
39
|
+
capture_output=capture,
|
|
40
|
+
text=True,
|
|
41
|
+
timeout=60
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
'success': result.returncode == 0,
|
|
46
|
+
'stdout': result.stdout if capture else '',
|
|
47
|
+
'stderr': result.stderr if capture else '',
|
|
48
|
+
'returncode': result.returncode
|
|
49
|
+
}
|
|
50
|
+
except FileNotFoundError:
|
|
51
|
+
return {
|
|
52
|
+
'success': False,
|
|
53
|
+
'error': '找不到 alembic 指令,請安裝: pip install alembic'
|
|
54
|
+
}
|
|
55
|
+
except subprocess.TimeoutExpired:
|
|
56
|
+
return {
|
|
57
|
+
'success': False,
|
|
58
|
+
'error': '指令執行逾時'
|
|
59
|
+
}
|
|
60
|
+
except Exception as e:
|
|
61
|
+
return {
|
|
62
|
+
'success': False,
|
|
63
|
+
'error': str(e)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def init_alembic(project: str) -> dict:
|
|
68
|
+
"""初始化 Alembic 環境
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
project: 專案路徑
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
dict 包含 success, alembic_dir 或 error
|
|
75
|
+
"""
|
|
76
|
+
project_path = Path(project).resolve()
|
|
77
|
+
alembic_dir = project_path / 'alembic'
|
|
78
|
+
alembic_ini = project_path / 'alembic.ini'
|
|
79
|
+
|
|
80
|
+
# 檢查是否已初始化
|
|
81
|
+
if alembic_ini.exists():
|
|
82
|
+
return {
|
|
83
|
+
'success': False,
|
|
84
|
+
'error': 'Alembic 已初始化 (alembic.ini 已存在)'
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
# 執行 alembic init
|
|
88
|
+
try:
|
|
89
|
+
result = subprocess.run(
|
|
90
|
+
['alembic', 'init', 'alembic'],
|
|
91
|
+
cwd=str(project_path),
|
|
92
|
+
capture_output=True,
|
|
93
|
+
text=True,
|
|
94
|
+
timeout=30
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
if result.returncode != 0:
|
|
98
|
+
return {
|
|
99
|
+
'success': False,
|
|
100
|
+
'error': result.stderr or '初始化失敗'
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
'success': True,
|
|
105
|
+
'alembic_dir': str(alembic_dir)
|
|
106
|
+
}
|
|
107
|
+
except FileNotFoundError:
|
|
108
|
+
return {
|
|
109
|
+
'success': False,
|
|
110
|
+
'error': '找不到 alembic 指令,請安裝: pip install alembic'
|
|
111
|
+
}
|
|
112
|
+
except Exception as e:
|
|
113
|
+
return {
|
|
114
|
+
'success': False,
|
|
115
|
+
'error': str(e)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def get_migration_status(project: str) -> dict:
|
|
120
|
+
"""取得遷移狀態
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
project: 專案路徑
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
dict 包含 success, current, head, pending 或 error
|
|
127
|
+
"""
|
|
128
|
+
project_path = Path(project).resolve()
|
|
129
|
+
|
|
130
|
+
# 取得目前版本
|
|
131
|
+
current_result = _run_alembic(project, ['current'])
|
|
132
|
+
if not current_result['success']:
|
|
133
|
+
# 如果沒有 DATABASE_URL,嘗試讀取本地狀態
|
|
134
|
+
if 'DATABASE_URL' in current_result.get('stderr', ''):
|
|
135
|
+
return {
|
|
136
|
+
'success': False,
|
|
137
|
+
'error': 'DATABASE_URL 環境變數未設定'
|
|
138
|
+
}
|
|
139
|
+
return current_result
|
|
140
|
+
|
|
141
|
+
current = current_result['stdout'].strip()
|
|
142
|
+
if not current:
|
|
143
|
+
current = '(尚未套用任何遷移)'
|
|
144
|
+
|
|
145
|
+
# 取得最新版本
|
|
146
|
+
head_result = _run_alembic(project, ['heads'])
|
|
147
|
+
head = head_result['stdout'].strip() if head_result['success'] else 'N/A'
|
|
148
|
+
|
|
149
|
+
# 取得待套用的遷移
|
|
150
|
+
history_result = _run_alembic(project, ['history', '--indicate-current'])
|
|
151
|
+
pending = []
|
|
152
|
+
if history_result['success']:
|
|
153
|
+
lines = history_result['stdout'].strip().split('\n')
|
|
154
|
+
in_pending = True
|
|
155
|
+
for line in lines:
|
|
156
|
+
if '(current)' in line or '(head)' in line:
|
|
157
|
+
in_pending = False
|
|
158
|
+
elif in_pending and line.strip() and '->' in line:
|
|
159
|
+
pending.append(line.strip())
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
'success': True,
|
|
163
|
+
'current': current,
|
|
164
|
+
'head': head,
|
|
165
|
+
'pending': pending
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def generate_migration(project: str, message: str, autogenerate: bool = True) -> dict:
|
|
170
|
+
"""產生新的遷移檔
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
project: 專案路徑
|
|
174
|
+
message: 遷移訊息
|
|
175
|
+
autogenerate: 是否自動偵測變更
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
dict 包含 success, migration_file, warnings 或 error
|
|
179
|
+
"""
|
|
180
|
+
args = ['revision', '-m', message]
|
|
181
|
+
if autogenerate:
|
|
182
|
+
args.append('--autogenerate')
|
|
183
|
+
|
|
184
|
+
result = _run_alembic(project, args)
|
|
185
|
+
|
|
186
|
+
if not result['success']:
|
|
187
|
+
return {
|
|
188
|
+
'success': False,
|
|
189
|
+
'error': result.get('stderr') or result.get('error', '產生失敗')
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
# 解析輸出取得檔案路徑
|
|
193
|
+
output = result['stdout']
|
|
194
|
+
migration_file = None
|
|
195
|
+
|
|
196
|
+
# 尋找產生的檔案路徑
|
|
197
|
+
match = re.search(r'Generating (.+\.py)', output)
|
|
198
|
+
if match:
|
|
199
|
+
migration_file = match.group(1)
|
|
200
|
+
|
|
201
|
+
# 檢查危險操作
|
|
202
|
+
warnings = []
|
|
203
|
+
if migration_file and Path(migration_file).exists():
|
|
204
|
+
content = Path(migration_file).read_text()
|
|
205
|
+
dangerous_patterns = [
|
|
206
|
+
(r'\bop\.drop_table\b', 'DROP TABLE 操作'),
|
|
207
|
+
(r'\bop\.drop_column\b', 'DROP COLUMN 操作'),
|
|
208
|
+
(r'\bop\.drop_index\b', 'DROP INDEX 操作'),
|
|
209
|
+
]
|
|
210
|
+
for pattern, desc in dangerous_patterns:
|
|
211
|
+
if re.search(pattern, content, re.IGNORECASE):
|
|
212
|
+
warnings.append(desc)
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
'success': True,
|
|
216
|
+
'migration_file': migration_file,
|
|
217
|
+
'warnings': warnings
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def run_upgrade(project: str, revision: str = 'head', dry_run: bool = False) -> dict:
|
|
222
|
+
"""升級資料庫
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
project: 專案路徑
|
|
226
|
+
revision: 目標版本
|
|
227
|
+
dry_run: 預覽模式
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
dict 包含 success, current, sql 或 error
|
|
231
|
+
"""
|
|
232
|
+
if dry_run:
|
|
233
|
+
# SQL 預覽模式
|
|
234
|
+
result = _run_alembic(project, ['upgrade', revision, '--sql'])
|
|
235
|
+
if result['success']:
|
|
236
|
+
return {
|
|
237
|
+
'success': True,
|
|
238
|
+
'sql': result['stdout']
|
|
239
|
+
}
|
|
240
|
+
return {
|
|
241
|
+
'success': False,
|
|
242
|
+
'error': result.get('stderr') or result.get('error', '預覽失敗')
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
# 實際執行升級
|
|
246
|
+
result = _run_alembic(project, ['upgrade', revision])
|
|
247
|
+
|
|
248
|
+
if not result['success']:
|
|
249
|
+
return {
|
|
250
|
+
'success': False,
|
|
251
|
+
'error': result.get('stderr') or result.get('error', '升級失敗')
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
# 取得目前版本
|
|
255
|
+
status = get_migration_status(project)
|
|
256
|
+
|
|
257
|
+
return {
|
|
258
|
+
'success': True,
|
|
259
|
+
'current': status.get('current', 'N/A')
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def run_downgrade(project: str, revision: str) -> dict:
|
|
264
|
+
"""降級資料庫
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
project: 專案路徑
|
|
268
|
+
revision: 目標版本 (可用 -1 表示降一個版本)
|
|
269
|
+
|
|
270
|
+
Returns:
|
|
271
|
+
dict 包含 success, current 或 error
|
|
272
|
+
"""
|
|
273
|
+
result = _run_alembic(project, ['downgrade', revision])
|
|
274
|
+
|
|
275
|
+
if not result['success']:
|
|
276
|
+
return {
|
|
277
|
+
'success': False,
|
|
278
|
+
'error': result.get('stderr') or result.get('error', '降級失敗')
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
# 取得目前版本
|
|
282
|
+
status = get_migration_status(project)
|
|
283
|
+
|
|
284
|
+
return {
|
|
285
|
+
'success': True,
|
|
286
|
+
'current': status.get('current', 'N/A')
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def check_migration_sync(project: str) -> dict:
|
|
291
|
+
"""檢查 Model 與遷移是否同步
|
|
292
|
+
|
|
293
|
+
用於驗證器,檢查是否有未產生遷移的 Model 變更
|
|
294
|
+
|
|
295
|
+
Args:
|
|
296
|
+
project: 專案路徑
|
|
297
|
+
|
|
298
|
+
Returns:
|
|
299
|
+
dict 包含 success, synced, details
|
|
300
|
+
"""
|
|
301
|
+
project_path = Path(project).resolve()
|
|
302
|
+
|
|
303
|
+
# 檢查是否有 alembic
|
|
304
|
+
if not (project_path / 'alembic.ini').exists():
|
|
305
|
+
return {
|
|
306
|
+
'success': True,
|
|
307
|
+
'synced': None,
|
|
308
|
+
'details': 'Alembic 未初始化'
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
# 嘗試產生遷移預覽
|
|
312
|
+
result = _run_alembic(project, [
|
|
313
|
+
'check' # Alembic 1.9+ 的新功能
|
|
314
|
+
])
|
|
315
|
+
|
|
316
|
+
# alembic check 不存在時,改用其他方式
|
|
317
|
+
if 'No such command' in result.get('stderr', ''):
|
|
318
|
+
# 舊版 Alembic,使用 revision --autogenerate 加 --dry-run
|
|
319
|
+
# 這會比較複雜,先簡單回傳
|
|
320
|
+
return {
|
|
321
|
+
'success': True,
|
|
322
|
+
'synced': None,
|
|
323
|
+
'details': 'Alembic 版本過舊,無法自動檢查'
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if result['success']:
|
|
327
|
+
return {
|
|
328
|
+
'success': True,
|
|
329
|
+
'synced': True,
|
|
330
|
+
'details': 'Model 與遷移已同步'
|
|
331
|
+
}
|
|
332
|
+
else:
|
|
333
|
+
# 有差異
|
|
334
|
+
return {
|
|
335
|
+
'success': True,
|
|
336
|
+
'synced': False,
|
|
337
|
+
'details': result.get('stderr', 'Model 與遷移不同步')
|
|
338
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""
|
|
2
|
+
dbdiagram.io 整合工具
|
|
3
|
+
|
|
4
|
+
自動產生資料庫圖表連結,供外包廠商查看。
|
|
5
|
+
|
|
6
|
+
支援:
|
|
7
|
+
- Prisma schema → DBML → dbdiagram.io 連結
|
|
8
|
+
- DBML 檔案直接轉換
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import base64
|
|
12
|
+
import urllib.parse
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
import subprocess
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def encode_dbml_to_link(dbml_content: str) -> str:
|
|
18
|
+
"""將 DBML 編碼為 dbdiagram.io 分享連結
|
|
19
|
+
|
|
20
|
+
編碼步驟:
|
|
21
|
+
1. UTF-8 編碼
|
|
22
|
+
2. Base64 轉換
|
|
23
|
+
3. URL 編碼
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
dbml_content: DBML 文字內容
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
dbdiagram.io 分享連結
|
|
30
|
+
"""
|
|
31
|
+
# 移除自動產生的註解(節省 URL 長度)
|
|
32
|
+
clean_dbml = '\n'.join(
|
|
33
|
+
line for line in dbml_content.split('\n')
|
|
34
|
+
if not line.startswith('////')
|
|
35
|
+
).strip()
|
|
36
|
+
|
|
37
|
+
# UTF-8 → Base64 → URL encode
|
|
38
|
+
base64_bytes = base64.b64encode(clean_dbml.encode('utf-8'))
|
|
39
|
+
encoded = urllib.parse.quote(base64_bytes.decode('ascii'))
|
|
40
|
+
|
|
41
|
+
return f'https://dbdiagram.io/d?c={encoded}'
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def find_prisma_schema(project_path: str) -> Path | None:
|
|
45
|
+
"""尋找專案中的 Prisma schema 檔案"""
|
|
46
|
+
project = Path(project_path)
|
|
47
|
+
|
|
48
|
+
# 常見位置
|
|
49
|
+
possible_paths = [
|
|
50
|
+
project / 'prisma' / 'schema.prisma',
|
|
51
|
+
project / 'schema.prisma',
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
for path in possible_paths:
|
|
55
|
+
if path.exists():
|
|
56
|
+
return path
|
|
57
|
+
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def find_dbml_file(project_path: str) -> Path | None:
|
|
62
|
+
"""尋找專案中的 DBML 檔案"""
|
|
63
|
+
project = Path(project_path)
|
|
64
|
+
|
|
65
|
+
# 常見位置
|
|
66
|
+
possible_paths = [
|
|
67
|
+
project / 'docs' / 'schema.dbml',
|
|
68
|
+
project / 'prisma' / 'dbml' / 'schema.dbml',
|
|
69
|
+
project / 'schema.dbml',
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
for path in possible_paths:
|
|
73
|
+
if path.exists():
|
|
74
|
+
return path
|
|
75
|
+
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def generate_dbml_from_prisma(project_path: str) -> dict:
|
|
80
|
+
"""從 Prisma schema 產生 DBML
|
|
81
|
+
|
|
82
|
+
需要先安裝 prisma-dbml-generator:
|
|
83
|
+
npm install -D prisma-dbml-generator
|
|
84
|
+
|
|
85
|
+
並在 schema.prisma 加入:
|
|
86
|
+
generator dbml {
|
|
87
|
+
provider = "prisma-dbml-generator"
|
|
88
|
+
output = "../docs"
|
|
89
|
+
outputName = "schema.dbml"
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
{'success': bool, 'dbml_path': str, 'error': str}
|
|
94
|
+
"""
|
|
95
|
+
project = Path(project_path)
|
|
96
|
+
schema_path = find_prisma_schema(project_path)
|
|
97
|
+
|
|
98
|
+
if not schema_path:
|
|
99
|
+
return {'success': False, 'error': '找不到 schema.prisma'}
|
|
100
|
+
|
|
101
|
+
# 檢查是否有 generator dbml
|
|
102
|
+
schema_content = schema_path.read_text()
|
|
103
|
+
if 'prisma-dbml-generator' not in schema_content:
|
|
104
|
+
return {
|
|
105
|
+
'success': False,
|
|
106
|
+
'error': '請在 schema.prisma 加入 generator dbml { provider = "prisma-dbml-generator" }'
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
# 執行 prisma generate
|
|
110
|
+
try:
|
|
111
|
+
result = subprocess.run(
|
|
112
|
+
['npx', 'prisma', 'generate'],
|
|
113
|
+
cwd=project,
|
|
114
|
+
capture_output=True,
|
|
115
|
+
text=True
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
if result.returncode != 0:
|
|
119
|
+
return {'success': False, 'error': result.stderr}
|
|
120
|
+
|
|
121
|
+
# 尋找產生的 DBML 檔案
|
|
122
|
+
dbml_path = find_dbml_file(project_path)
|
|
123
|
+
if dbml_path:
|
|
124
|
+
return {'success': True, 'dbml_path': str(dbml_path)}
|
|
125
|
+
else:
|
|
126
|
+
return {'success': False, 'error': '產生失敗,找不到 DBML 檔案'}
|
|
127
|
+
|
|
128
|
+
except FileNotFoundError:
|
|
129
|
+
return {'success': False, 'error': '找不到 npx,請確認已安裝 Node.js'}
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def generate_dbdiagram_link(project_path: str) -> dict:
|
|
133
|
+
"""產生 dbdiagram.io 連結
|
|
134
|
+
|
|
135
|
+
流程:
|
|
136
|
+
1. 尋找現有的 DBML 檔案
|
|
137
|
+
2. 若無,嘗試從 Prisma schema 產生
|
|
138
|
+
3. 編碼為 dbdiagram.io 連結
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
{
|
|
142
|
+
'success': bool,
|
|
143
|
+
'link': str,
|
|
144
|
+
'embed_link': str,
|
|
145
|
+
'source': str, # 'dbml' or 'prisma'
|
|
146
|
+
'error': str
|
|
147
|
+
}
|
|
148
|
+
"""
|
|
149
|
+
# 尋找現有的 DBML
|
|
150
|
+
dbml_path = find_dbml_file(project_path)
|
|
151
|
+
|
|
152
|
+
if not dbml_path:
|
|
153
|
+
# 嘗試從 Prisma 產生
|
|
154
|
+
gen_result = generate_dbml_from_prisma(project_path)
|
|
155
|
+
if not gen_result['success']:
|
|
156
|
+
return {'success': False, 'error': gen_result['error']}
|
|
157
|
+
dbml_path = Path(gen_result['dbml_path'])
|
|
158
|
+
|
|
159
|
+
# 讀取 DBML
|
|
160
|
+
try:
|
|
161
|
+
dbml_content = dbml_path.read_text()
|
|
162
|
+
except Exception as e:
|
|
163
|
+
return {'success': False, 'error': f'讀取 DBML 失敗: {e}'}
|
|
164
|
+
|
|
165
|
+
# 編碼為連結
|
|
166
|
+
link = encode_dbml_to_link(dbml_content)
|
|
167
|
+
embed_link = link.replace('/d?', '/embed?')
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
'success': True,
|
|
171
|
+
'link': link,
|
|
172
|
+
'embed_link': embed_link,
|
|
173
|
+
'source': 'dbml',
|
|
174
|
+
'dbml_path': str(dbml_path)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def save_link_to_file(project_path: str, link: str) -> str:
|
|
179
|
+
"""儲存連結到檔案"""
|
|
180
|
+
output_path = Path(project_path) / 'docs' / 'dbdiagram-link.txt'
|
|
181
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
182
|
+
output_path.write_text(link)
|
|
183
|
+
return str(output_path)
|