@ailnaf/zoddb 0.0.0-stage → 0.2.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 +73 -0
- package/LICENSE +15 -0
- package/README.md +440 -2
- package/dist/aggregate.d.ts +42 -0
- package/dist/aggregate.d.ts.map +1 -0
- package/dist/aggregate.js +77 -0
- package/dist/aggregate.js.map +1 -0
- package/dist/auth-schema.d.ts +26 -0
- package/dist/auth-schema.d.ts.map +1 -0
- package/dist/auth-schema.js +139 -0
- package/dist/auth-schema.js.map +1 -0
- package/dist/better-auth-adapter.d.ts +21 -0
- package/dist/better-auth-adapter.d.ts.map +1 -0
- package/dist/better-auth-adapter.js +175 -0
- package/dist/better-auth-adapter.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +341 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +22 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +52 -0
- package/dist/config.js.map +1 -0
- package/dist/db.d.ts +183 -0
- package/dist/db.d.ts.map +1 -0
- package/dist/db.js +1063 -0
- package/dist/db.js.map +1 -0
- package/dist/ddl.d.ts +21 -0
- package/dist/ddl.d.ts.map +1 -0
- package/dist/ddl.js +127 -0
- package/dist/ddl.js.map +1 -0
- package/dist/dev.d.ts +9 -0
- package/dist/dev.d.ts.map +1 -0
- package/dist/dev.js +21 -0
- package/dist/dev.js.map +1 -0
- package/dist/errors.d.ts +63 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +122 -0
- package/dist/errors.js.map +1 -0
- package/dist/filter.d.ts +67 -0
- package/dist/filter.d.ts.map +1 -0
- package/dist/filter.js +230 -0
- package/dist/filter.js.map +1 -0
- package/dist/graph.d.ts +54 -0
- package/dist/graph.d.ts.map +1 -0
- package/dist/graph.js +52 -0
- package/dist/graph.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/init.d.ts +12 -0
- package/dist/init.d.ts.map +1 -0
- package/dist/init.js +62 -0
- package/dist/init.js.map +1 -0
- package/dist/migrate.d.ts +58 -0
- package/dist/migrate.d.ts.map +1 -0
- package/dist/migrate.js +447 -0
- package/dist/migrate.js.map +1 -0
- package/dist/migrations.d.ts +80 -0
- package/dist/migrations.d.ts.map +1 -0
- package/dist/migrations.js +281 -0
- package/dist/migrations.js.map +1 -0
- package/dist/relation-graph.d.ts +50 -0
- package/dist/relation-graph.d.ts.map +1 -0
- package/dist/relation-graph.js +93 -0
- package/dist/relation-graph.js.map +1 -0
- package/dist/relations.d.ts +128 -0
- package/dist/relations.d.ts.map +1 -0
- package/dist/relations.js +21 -0
- package/dist/relations.js.map +1 -0
- package/dist/schema.d.ts +247 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +584 -0
- package/dist/schema.js.map +1 -0
- package/dist/studio/page.d.ts +3 -0
- package/dist/studio/page.d.ts.map +1 -0
- package/dist/studio/page.js +708 -0
- package/dist/studio/page.js.map +1 -0
- package/dist/studio/server.d.ts +26 -0
- package/dist/studio/server.d.ts.map +1 -0
- package/dist/studio/server.js +415 -0
- package/dist/studio/server.js.map +1 -0
- package/dist/table.d.ts +156 -0
- package/dist/table.d.ts.map +1 -0
- package/dist/table.js +165 -0
- package/dist/table.js.map +1 -0
- package/dist/where.d.ts +18 -0
- package/dist/where.d.ts.map +1 -0
- package/dist/where.js +76 -0
- package/dist/where.js.map +1 -0
- package/docs/api.md +476 -0
- package/package.json +70 -4
|
@@ -0,0 +1,708 @@
|
|
|
1
|
+
/** Studio 切片 0:零依赖、只读的 schema explorer。 */
|
|
2
|
+
export const STUDIO_HTML = String.raw `<!doctype html>
|
|
3
|
+
<html lang="zh-CN">
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="utf-8">
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
7
|
+
<title>zoddb studio</title>
|
|
8
|
+
<style>
|
|
9
|
+
:root {
|
|
10
|
+
color-scheme: dark;
|
|
11
|
+
--bg: #0b1020;
|
|
12
|
+
--panel: #121a2f;
|
|
13
|
+
--panel-2: #18233d;
|
|
14
|
+
--line: #26324f;
|
|
15
|
+
--text: #e8edf8;
|
|
16
|
+
--muted: #92a0bd;
|
|
17
|
+
--accent: #7dd3fc;
|
|
18
|
+
--accent-2: #a7f3d0;
|
|
19
|
+
--danger: #fca5a5;
|
|
20
|
+
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
21
|
+
}
|
|
22
|
+
* { box-sizing: border-box; }
|
|
23
|
+
body { margin: 0; background: var(--bg); color: var(--text); min-height: 100vh; }
|
|
24
|
+
header { display: flex; align-items: center; gap: 12px; padding: 18px 24px; border-bottom: 1px solid var(--line); background: #0e1528; }
|
|
25
|
+
header h1 { font-size: 18px; margin: 0; letter-spacing: .02em; }
|
|
26
|
+
.badge { border: 1px solid var(--line); color: var(--accent-2); padding: 3px 8px; border-radius: 999px; font-size: 12px; }
|
|
27
|
+
.status { color: var(--muted); font-size: 13px; margin-left: auto; }
|
|
28
|
+
.status.error { color: var(--danger); }
|
|
29
|
+
main { display: grid; grid-template-columns: 250px minmax(0, 1fr) 330px; min-height: calc(100vh - 62px); }
|
|
30
|
+
aside, section { min-width: 0; padding: 18px; }
|
|
31
|
+
aside { background: #0e1528; }
|
|
32
|
+
#tables { border-right: 1px solid var(--line); }
|
|
33
|
+
#relations { border-left: 1px solid var(--line); overflow: auto; }
|
|
34
|
+
h2 { font-size: 12px; text-transform: uppercase; letter-spacing: .12em; color: var(--muted); margin: 0 0 12px; }
|
|
35
|
+
.table-item { display: block; width: 100%; text-align: left; border: 1px solid transparent; background: transparent; color: var(--text); padding: 10px 12px; border-radius: 8px; cursor: pointer; font: inherit; }
|
|
36
|
+
.table-item:hover { background: var(--panel-2); }
|
|
37
|
+
.table-item.active { background: var(--panel-2); border-color: var(--line); color: var(--accent); }
|
|
38
|
+
.table-item small { display: block; color: var(--muted); font-size: 11px; margin-top: 3px; }
|
|
39
|
+
.migrations { margin-top: 24px; padding-top: 16px; border-top: 1px solid var(--line); }
|
|
40
|
+
.migrations-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 10px; }
|
|
41
|
+
.migrations-head h2 { margin: 0; }
|
|
42
|
+
.migrations-head button { background: transparent; color: var(--accent); border: 1px solid var(--line); border-radius: 6px; padding: 3px 7px; font: inherit; font-size: 11px; cursor: pointer; }
|
|
43
|
+
.migrations-head button:hover { border-color: var(--accent); }
|
|
44
|
+
.migration-state { border: 1px solid var(--line); border-radius: 999px; padding: 2px 7px; color: var(--muted); font-size: 10px; white-space: nowrap; }
|
|
45
|
+
.migration-state.ok { color: var(--accent-2); border-color: #2f6b57; }
|
|
46
|
+
.migration-state.warn { color: #fbbf24; border-color: #7c5a12; }
|
|
47
|
+
.migration-state.error { color: var(--danger); border-color: #7f3333; }
|
|
48
|
+
.migration-grid { display: grid; grid-template-columns: 1fr auto; gap: 5px 10px; font-size: 12px; }
|
|
49
|
+
.migration-grid .label { color: var(--muted); }
|
|
50
|
+
.migration-grid .count { text-align: right; font-variant-numeric: tabular-nums; }
|
|
51
|
+
.migration-grid .count.bad { color: #fbbf24; }
|
|
52
|
+
.migration-meta { color: var(--muted); font-size: 10px; line-height: 1.55; margin-top: 9px; overflow-wrap: anywhere; }
|
|
53
|
+
.migration-error { color: var(--danger); font-size: 11px; line-height: 1.55; margin-top: 8px; overflow-wrap: anywhere; white-space: pre-wrap; }
|
|
54
|
+
.empty { color: var(--muted); font-size: 13px; line-height: 1.6; }
|
|
55
|
+
.detail-head { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; margin-bottom: 4px; }
|
|
56
|
+
.detail-head h2 { font-size: 24px; text-transform: none; letter-spacing: 0; color: var(--text); margin: 0; }
|
|
57
|
+
.mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
|
|
58
|
+
.sql-name { color: var(--muted); font-size: 13px; }
|
|
59
|
+
table { width: 100%; border-collapse: collapse; margin-top: 18px; font-size: 13px; }
|
|
60
|
+
th, td { text-align: left; padding: 9px 10px; border-bottom: 1px solid var(--line); vertical-align: top; }
|
|
61
|
+
th { color: var(--muted); font-weight: 500; font-size: 11px; text-transform: uppercase; letter-spacing: .08em; }
|
|
62
|
+
tr:last-child td { border-bottom: 0; }
|
|
63
|
+
.pill { display: inline-block; border: 1px solid var(--line); border-radius: 999px; padding: 1px 7px; color: var(--muted); font-size: 10px; margin-right: 4px; white-space: nowrap; }
|
|
64
|
+
.pill.pk { color: var(--accent-2); border-color: #2f6b57; }
|
|
65
|
+
.pill.unique { color: var(--accent); border-color: #2c5b72; }
|
|
66
|
+
.edge { background: var(--panel); border: 1px solid var(--line); border-radius: 10px; padding: 11px; margin-bottom: 9px; }
|
|
67
|
+
.edge.active { border-color: var(--accent); }
|
|
68
|
+
.edge-title { font-size: 13px; margin-bottom: 5px; }
|
|
69
|
+
.edge-meta { color: var(--muted); font-size: 11px; line-height: 1.55; }
|
|
70
|
+
.edge-meta .mono { color: var(--text); }
|
|
71
|
+
footer { color: var(--muted); font-size: 11px; margin-top: 18px; line-height: 1.6; }
|
|
72
|
+
.data { margin-top: 28px; border-top: 1px solid var(--line); padding-top: 18px; }
|
|
73
|
+
.data-head { display: flex; align-items: baseline; gap: 10px; margin-bottom: 12px; }
|
|
74
|
+
.data-head h3 { margin: 0; font-size: 15px; }
|
|
75
|
+
.data-status { color: var(--muted); font-size: 12px; }
|
|
76
|
+
.toolbar { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin-bottom: 12px; }
|
|
77
|
+
.toolbar select, .toolbar input, .toolbar button { background: var(--panel); color: var(--text); border: 1px solid var(--line); border-radius: 7px; padding: 7px 9px; font: inherit; font-size: 12px; }
|
|
78
|
+
.toolbar input { min-width: 150px; }
|
|
79
|
+
.toolbar button { cursor: pointer; }
|
|
80
|
+
.toolbar button:hover:not(:disabled) { border-color: var(--accent); }
|
|
81
|
+
.toolbar button:disabled { opacity: .45; cursor: not-allowed; }
|
|
82
|
+
.toolbar .spacer { flex: 1; }
|
|
83
|
+
.rows-wrap { overflow: auto; border: 1px solid var(--line); border-radius: 10px; }
|
|
84
|
+
.rows-wrap table { margin: 0; }
|
|
85
|
+
.rows-wrap th { position: sticky; top: 0; background: var(--panel); z-index: 1; }
|
|
86
|
+
.rows-wrap td { max-width: 340px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
87
|
+
.null { color: var(--muted); font-style: italic; }
|
|
88
|
+
.cell-link { background: transparent; border: 0; padding: 0; color: var(--accent); cursor: pointer; font: inherit; text-decoration: underline; text-underline-offset: 2px; }
|
|
89
|
+
.relation-toolbar { margin-top: -2px; padding-top: 8px; border-top: 1px dashed var(--line); }
|
|
90
|
+
.relation-toolbar label { color: var(--muted); font-size: 12px; }
|
|
91
|
+
.relation-preview td { background: #101a30; color: var(--muted); padding-left: 24px; }
|
|
92
|
+
.relation-preview details { white-space: normal; }
|
|
93
|
+
.relation-preview summary { color: var(--accent); cursor: pointer; }
|
|
94
|
+
.relation-preview pre { margin: 8px 0 0; max-height: 220px; overflow: auto; color: var(--text); font-size: 11px; white-space: pre-wrap; word-break: break-word; }
|
|
95
|
+
.edge button { margin-top: 8px; background: transparent; color: var(--accent); border: 1px solid var(--line); border-radius: 6px; padding: 4px 8px; font: inherit; font-size: 11px; cursor: pointer; }
|
|
96
|
+
.edge button + button { margin-left: 5px; }
|
|
97
|
+
.edge button:hover { border-color: var(--accent); }
|
|
98
|
+
.badge.write { color: #fbbf24; border-color: #7c5a12; }
|
|
99
|
+
.detail-actions { display: flex; gap: 8px; margin: 14px 0 2px; }
|
|
100
|
+
.detail-actions button, .row-actions button { background: var(--panel); color: var(--text); border: 1px solid var(--line); border-radius: 7px; padding: 6px 10px; font: inherit; font-size: 12px; cursor: pointer; }
|
|
101
|
+
.detail-actions button:hover, .row-actions button:hover { border-color: var(--accent); }
|
|
102
|
+
.row-actions { white-space: nowrap; }
|
|
103
|
+
.row-actions button { padding: 4px 7px; font-size: 11px; }
|
|
104
|
+
.row-actions button.danger { color: var(--danger); }
|
|
105
|
+
.write-dialog { background: var(--panel); color: var(--text); border: 1px solid var(--line); border-radius: 12px; padding: 18px; width: min(720px, calc(100vw - 32px)); box-shadow: 0 24px 80px #0008; }
|
|
106
|
+
.write-dialog::backdrop { background: #020617cc; }
|
|
107
|
+
.write-dialog h3 { margin: 0 0 8px; font-size: 16px; }
|
|
108
|
+
.write-dialog p { color: var(--muted); font-size: 12px; margin: 0 0 10px; line-height: 1.5; }
|
|
109
|
+
.write-dialog textarea { width: 100%; min-height: 300px; resize: vertical; background: #0b1020; color: var(--text); border: 1px solid var(--line); border-radius: 8px; padding: 12px; font: 12px/1.5 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
|
|
110
|
+
.write-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 12px; }
|
|
111
|
+
.write-actions button { background: var(--panel-2); color: var(--text); border: 1px solid var(--line); border-radius: 7px; padding: 7px 12px; font: inherit; font-size: 12px; cursor: pointer; }
|
|
112
|
+
.write-actions button.primary { border-color: var(--accent); color: var(--accent); }
|
|
113
|
+
.write-error { color: var(--danger); font-size: 12px; min-height: 18px; margin-top: 8px; white-space: pre-wrap; }
|
|
114
|
+
@media (max-width: 980px) {
|
|
115
|
+
main { grid-template-columns: 210px minmax(0, 1fr); }
|
|
116
|
+
#relations { grid-column: 1 / -1; border-left: 0; border-top: 1px solid var(--line); }
|
|
117
|
+
}
|
|
118
|
+
</style>
|
|
119
|
+
</head>
|
|
120
|
+
<body>
|
|
121
|
+
<header>
|
|
122
|
+
<h1>zoddb <span style="color: var(--accent)">studio</span></h1>
|
|
123
|
+
<span class="badge" id="mode-badge">read-only</span>
|
|
124
|
+
<span class="status" id="status">加载 schema…</span>
|
|
125
|
+
</header>
|
|
126
|
+
<main>
|
|
127
|
+
<aside id="tables">
|
|
128
|
+
<h2>Tables</h2><div id="table-list" class="empty">加载中…</div>
|
|
129
|
+
<div id="migrations" class="migrations">
|
|
130
|
+
<div class="migrations-head"><h2>Migrations</h2><span id="migration-state" class="migration-state">…</span><button id="migration-refresh" type="button">刷新</button></div>
|
|
131
|
+
<div id="migration-body" class="empty">加载中…</div>
|
|
132
|
+
</div>
|
|
133
|
+
</aside>
|
|
134
|
+
<section id="detail"><h2>Table</h2><div id="table-detail" class="empty">选择左侧的表查看列与主键。</div></section>
|
|
135
|
+
<aside id="relations"><h2>Relations</h2><div id="edge-list" class="empty">加载中…</div></aside>
|
|
136
|
+
</main>
|
|
137
|
+
<script>
|
|
138
|
+
(() => {
|
|
139
|
+
'use strict'
|
|
140
|
+
const token = new URLSearchParams(location.search).get('token') || ''
|
|
141
|
+
const state = { graph: null, selected: null, page: 0, total: 0, limit: 20, filter: { column: '', op: 'eq', value: '' }, sort: '', with: '', relation: { name: '', mode: 'some', column: '', op: 'eq', value: '' }, writeEnabled: false }
|
|
142
|
+
const status = document.getElementById('status')
|
|
143
|
+
const tableList = document.getElementById('table-list')
|
|
144
|
+
const tableDetail = document.getElementById('table-detail')
|
|
145
|
+
const edgeList = document.getElementById('edge-list')
|
|
146
|
+
const migrationBody = document.getElementById('migration-body')
|
|
147
|
+
const migrationState = document.getElementById('migration-state')
|
|
148
|
+
const migrationRefresh = document.getElementById('migration-refresh')
|
|
149
|
+
|
|
150
|
+
function setStatus(text, isError) {
|
|
151
|
+
status.textContent = text
|
|
152
|
+
status.className = isError ? 'status error' : 'status'
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function make(tag, className, text) {
|
|
156
|
+
const el = document.createElement(tag)
|
|
157
|
+
if (className) el.className = className
|
|
158
|
+
if (text !== undefined) el.textContent = text
|
|
159
|
+
return el
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function clear(el) { el.replaceChildren() }
|
|
163
|
+
|
|
164
|
+
function pill(text, className) {
|
|
165
|
+
return make('span', className ? 'pill ' + className : 'pill', text)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function formatCell(value) {
|
|
169
|
+
if (value === null || value === undefined) return 'NULL'
|
|
170
|
+
if (value instanceof Date) return value.toISOString()
|
|
171
|
+
if (typeof value === 'object') return JSON.stringify(value)
|
|
172
|
+
return String(value)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function relationSummary(edge, value) {
|
|
176
|
+
if (value === null || value === undefined) return edge.name + ': NULL'
|
|
177
|
+
const target = state.graph.nodes.find(n => n.key === edge.to)
|
|
178
|
+
const label = target && target.primaryKey.length ? target.primaryKey[0] : 'id'
|
|
179
|
+
if (Array.isArray(value)) {
|
|
180
|
+
const sample = value.slice(0, 3).map(row => String(row[label] ?? row.title ?? row.name ?? row.slug ?? JSON.stringify(row)))
|
|
181
|
+
return edge.name + ': ' + value.length + ' 行' + (sample.length ? ' · ' + sample.join(', ') : '')
|
|
182
|
+
}
|
|
183
|
+
return edge.name + ': ' + formatCell(value)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function renderRowTable(node, rows) {
|
|
187
|
+
const wrap = document.getElementById('rows')
|
|
188
|
+
clear(wrap)
|
|
189
|
+
if (rows.length === 0) {
|
|
190
|
+
wrap.append(make('div', 'empty', '没有行。'))
|
|
191
|
+
return
|
|
192
|
+
}
|
|
193
|
+
const table = make('table', 'rows-table')
|
|
194
|
+
const thead = document.createElement('thead')
|
|
195
|
+
const headerRow = document.createElement('tr')
|
|
196
|
+
for (const column of node.columns) headerRow.append(make('th', 'mono', column.key))
|
|
197
|
+
if (state.with) headerRow.append(make('th', '', 'relation'))
|
|
198
|
+
if (state.writeEnabled) headerRow.append(make('th', '', 'actions'))
|
|
199
|
+
thead.append(headerRow)
|
|
200
|
+
table.append(thead)
|
|
201
|
+
const tbody = document.createElement('tbody')
|
|
202
|
+
const edgesFrom = state.graph.edges.filter(e => e.from === node.key)
|
|
203
|
+
for (const row of rows) {
|
|
204
|
+
const tr = document.createElement('tr')
|
|
205
|
+
for (const column of node.columns) {
|
|
206
|
+
const value = row[column.key]
|
|
207
|
+
const td = make('td', 'mono')
|
|
208
|
+
const edge = edgesFrom.find(e => !e.through && e.fromColumn === column.key)
|
|
209
|
+
if (value === null || value === undefined) {
|
|
210
|
+
td.append(make('span', 'null', 'NULL'))
|
|
211
|
+
} else if (edge) {
|
|
212
|
+
const link = make('button', 'cell-link', formatCell(value))
|
|
213
|
+
link.type = 'button'
|
|
214
|
+
link.title = '打开 ' + edge.to + ' where ' + edge.toColumn + ' = ' + formatCell(value)
|
|
215
|
+
link.addEventListener('click', () => openTable(edge.to, edge.toColumn, value))
|
|
216
|
+
td.append(link)
|
|
217
|
+
} else {
|
|
218
|
+
td.textContent = formatCell(value)
|
|
219
|
+
}
|
|
220
|
+
tr.append(td)
|
|
221
|
+
}
|
|
222
|
+
if (state.with) {
|
|
223
|
+
const td = make('td', 'relation-preview')
|
|
224
|
+
const edge = state.graph.edges.find(e => e.from === node.key && e.name === state.with)
|
|
225
|
+
const value = row[state.with]
|
|
226
|
+
if (edge && value !== undefined) {
|
|
227
|
+
const text = relationSummary(edge, value)
|
|
228
|
+
if (value === null || value === undefined) {
|
|
229
|
+
td.textContent = text
|
|
230
|
+
} else {
|
|
231
|
+
const details = document.createElement('details')
|
|
232
|
+
details.append(make('summary', '', text))
|
|
233
|
+
details.append(make('pre', 'mono', JSON.stringify(value, null, 2)))
|
|
234
|
+
td.append(details)
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
tr.append(td)
|
|
238
|
+
}
|
|
239
|
+
if (state.writeEnabled) {
|
|
240
|
+
const actions = make('td', 'row-actions')
|
|
241
|
+
const edit = make('button', '', '编辑')
|
|
242
|
+
edit.type = 'button'
|
|
243
|
+
edit.addEventListener('click', () => openEditor('update', node, row))
|
|
244
|
+
const del = make('button', 'danger', '删除')
|
|
245
|
+
del.type = 'button'
|
|
246
|
+
del.addEventListener('click', () => {
|
|
247
|
+
if (!window.confirm('确认删除这行?此操作不可撤销。')) return
|
|
248
|
+
postWrite({ op: 'delete', table: node.key, where: pkWhere(node, row) })
|
|
249
|
+
.then(() => loadRows())
|
|
250
|
+
.catch(err => window.alert(err instanceof Error ? err.message : String(err)))
|
|
251
|
+
})
|
|
252
|
+
actions.append(edit, del)
|
|
253
|
+
tr.append(actions)
|
|
254
|
+
}
|
|
255
|
+
tbody.append(tr)
|
|
256
|
+
}
|
|
257
|
+
table.append(tbody)
|
|
258
|
+
wrap.append(table)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function buildDataPanel(node) {
|
|
262
|
+
const wrap = make('div', 'data')
|
|
263
|
+
const head = make('div', 'data-head')
|
|
264
|
+
head.append(make('h3', '', 'Rows'))
|
|
265
|
+
head.append(make('span', 'data-status', ''))
|
|
266
|
+
wrap.append(head)
|
|
267
|
+
|
|
268
|
+
const toolbar = make('div', 'toolbar')
|
|
269
|
+
const filterColumn = document.createElement('select')
|
|
270
|
+
filterColumn.id = 'filter-column'
|
|
271
|
+
filterColumn.append(new Option('无过滤', ''))
|
|
272
|
+
for (const column of node.columns) filterColumn.append(new Option(column.key, column.key))
|
|
273
|
+
const filterOp = document.createElement('select')
|
|
274
|
+
filterOp.id = 'filter-op'
|
|
275
|
+
for (const [value, label] of [['eq', '等于'], ['contains', '包含'], ['startsWith', '开头'], ['endsWith', '结尾'], ['gt', '>'], ['gte', '>='], ['lt', '<'], ['lte', '<='], ['ne', '不等于'], ['isNull', '为 NULL']]) {
|
|
276
|
+
filterOp.append(new Option(label, value))
|
|
277
|
+
}
|
|
278
|
+
const filterValue = document.createElement('input')
|
|
279
|
+
filterValue.id = 'filter-value'
|
|
280
|
+
filterValue.placeholder = '过滤值'
|
|
281
|
+
const withSelect = document.createElement('select')
|
|
282
|
+
withSelect.id = 'with-relation'
|
|
283
|
+
withSelect.append(new Option('不预览关系', ''))
|
|
284
|
+
for (const edge of state.graph.edges.filter(e => e.from === node.key)) withSelect.append(new Option('预览 ' + edge.name, edge.name))
|
|
285
|
+
const sort = document.createElement('select')
|
|
286
|
+
sort.id = 'sort-column'
|
|
287
|
+
sort.append(new Option('不排序', ''))
|
|
288
|
+
for (const column of node.columns) sort.append(new Option(column.key, column.key))
|
|
289
|
+
const dir = document.createElement('select')
|
|
290
|
+
dir.id = 'sort-dir'
|
|
291
|
+
dir.append(new Option('升序', 'asc'))
|
|
292
|
+
dir.append(new Option('降序', 'desc'))
|
|
293
|
+
const limit = document.createElement('select')
|
|
294
|
+
limit.id = 'row-limit'
|
|
295
|
+
for (const n of [20, 50, 100]) limit.append(new Option(String(n) + ' 行', String(n)))
|
|
296
|
+
const apply = make('button', '', '应用')
|
|
297
|
+
apply.type = 'button'
|
|
298
|
+
const prev = make('button', '', '上一页')
|
|
299
|
+
prev.type = 'button'
|
|
300
|
+
prev.id = 'rows-prev'
|
|
301
|
+
const next = make('button', '', '下一页')
|
|
302
|
+
next.type = 'button'
|
|
303
|
+
next.id = 'rows-next'
|
|
304
|
+
const spacer = make('span', 'spacer')
|
|
305
|
+
toolbar.append(filterColumn, filterOp, filterValue, withSelect, sort, dir, limit, apply, spacer, prev, next)
|
|
306
|
+
wrap.append(toolbar)
|
|
307
|
+
|
|
308
|
+
const relationToolbar = make('div', 'toolbar relation-toolbar')
|
|
309
|
+
const relationSelect = document.createElement('select')
|
|
310
|
+
relationSelect.id = 'relation-name'
|
|
311
|
+
relationSelect.append(new Option('不过滤关系', ''))
|
|
312
|
+
for (const edge of state.graph.edges.filter(e => e.from === node.key)) relationSelect.append(new Option(edge.name, edge.name))
|
|
313
|
+
const relationMode = document.createElement('select')
|
|
314
|
+
relationMode.id = 'relation-mode'
|
|
315
|
+
relationMode.append(new Option('some(存在)', 'some'))
|
|
316
|
+
relationMode.append(new Option('none(不存在)', 'none'))
|
|
317
|
+
relationToolbar.append(make('label', '', '关系'), relationSelect, relationMode)
|
|
318
|
+
wrap.append(relationToolbar)
|
|
319
|
+
|
|
320
|
+
const rows = make('div', 'rows-wrap')
|
|
321
|
+
rows.id = 'rows'
|
|
322
|
+
rows.append(make('div', 'empty', '加载中…'))
|
|
323
|
+
wrap.append(rows)
|
|
324
|
+
|
|
325
|
+
filterColumn.value = state.filter.column
|
|
326
|
+
filterOp.value = state.filter.op
|
|
327
|
+
filterValue.value = state.filter.value
|
|
328
|
+
withSelect.value = state.with
|
|
329
|
+
sort.value = state.sort
|
|
330
|
+
relationSelect.value = state.relation.name
|
|
331
|
+
relationMode.value = state.relation.mode
|
|
332
|
+
filterColumn.disabled = !state.graph?.dataEnabled
|
|
333
|
+
filterOp.disabled = !state.graph?.dataEnabled
|
|
334
|
+
filterValue.disabled = !state.graph?.dataEnabled
|
|
335
|
+
withSelect.disabled = !state.graph?.dataEnabled
|
|
336
|
+
sort.disabled = !state.graph?.dataEnabled
|
|
337
|
+
dir.disabled = !state.graph?.dataEnabled
|
|
338
|
+
limit.disabled = !state.graph?.dataEnabled
|
|
339
|
+
relationSelect.disabled = !state.graph?.dataEnabled
|
|
340
|
+
relationMode.disabled = !state.graph?.dataEnabled
|
|
341
|
+
apply.disabled = !state.graph?.dataEnabled
|
|
342
|
+
prev.disabled = true
|
|
343
|
+
next.disabled = true
|
|
344
|
+
|
|
345
|
+
apply.addEventListener('click', () => {
|
|
346
|
+
state.filter = { column: filterColumn.value, op: filterOp.value, value: filterValue.value }
|
|
347
|
+
state.with = withSelect.value
|
|
348
|
+
state.sort = sort.value
|
|
349
|
+
state.relation = { name: relationSelect.value, mode: relationMode.value, column: '', op: 'eq', value: '' }
|
|
350
|
+
state.limit = Number(limit.value) || 20
|
|
351
|
+
state.page = 0
|
|
352
|
+
void loadRows()
|
|
353
|
+
})
|
|
354
|
+
prev.addEventListener('click', () => {
|
|
355
|
+
if (state.page > 0) {
|
|
356
|
+
state.page -= 1
|
|
357
|
+
void loadRows()
|
|
358
|
+
}
|
|
359
|
+
})
|
|
360
|
+
next.addEventListener('click', () => {
|
|
361
|
+
if ((state.page + 1) * state.limit < state.total) {
|
|
362
|
+
state.page += 1
|
|
363
|
+
void loadRows()
|
|
364
|
+
}
|
|
365
|
+
})
|
|
366
|
+
return { wrap, dir, prev, next, status: head.querySelector('.data-status') }
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
async function loadRows() {
|
|
370
|
+
const node = state.graph && state.graph.nodes.find(n => n.key === state.selected)
|
|
371
|
+
const status = document.querySelector('.data-status')
|
|
372
|
+
const rows = document.getElementById('rows')
|
|
373
|
+
if (!node || !rows) return
|
|
374
|
+
if (!state.graph.dataEnabled) {
|
|
375
|
+
rows.replaceChildren(make('div', 'empty', '未配置数据库;使用 --url 或 DATABASE_URL 启动 Studio 后可浏览数据。'))
|
|
376
|
+
if (status) status.textContent = 'read-only schema'
|
|
377
|
+
return
|
|
378
|
+
}
|
|
379
|
+
const params = new URLSearchParams({ table: node.key, limit: String(state.limit), offset: String(state.page * state.limit) })
|
|
380
|
+
if (state.filter.column) {
|
|
381
|
+
params.set('filterColumn', state.filter.column)
|
|
382
|
+
params.set('filterOp', state.filter.op)
|
|
383
|
+
params.set('filterValue', state.filter.value)
|
|
384
|
+
}
|
|
385
|
+
if (state.sort) {
|
|
386
|
+
params.set('sort', state.sort)
|
|
387
|
+
params.set('dir', document.getElementById('sort-dir') ? document.getElementById('sort-dir').value : 'asc')
|
|
388
|
+
}
|
|
389
|
+
if (state.with) params.set('with', state.with)
|
|
390
|
+
if (state.relation.name) {
|
|
391
|
+
params.set('relation', state.relation.name)
|
|
392
|
+
params.set('relationMode', state.relation.mode)
|
|
393
|
+
}
|
|
394
|
+
rows.replaceChildren(make('div', 'empty', '加载中…'))
|
|
395
|
+
try {
|
|
396
|
+
const res = await fetch('/api/rows?' + params.toString(), { headers: { 'x-zoddb-studio-token': token } })
|
|
397
|
+
const body = await res.json()
|
|
398
|
+
if (!res.ok) throw new Error((body.error || ('HTTP ' + res.status)) + (body.code ? ' [' + body.code + ']' : ''))
|
|
399
|
+
state.total = body.total
|
|
400
|
+
if (status) status.textContent = body.total + ' rows · page ' + (state.page + 1) + ' / ' + Math.max(1, Math.ceil(body.total / state.limit))
|
|
401
|
+
renderRowTable(node, body.rows)
|
|
402
|
+
const prev = document.getElementById('rows-prev')
|
|
403
|
+
const next = document.getElementById('rows-next')
|
|
404
|
+
if (prev) prev.disabled = state.page === 0
|
|
405
|
+
if (next) next.disabled = (state.page + 1) * state.limit >= state.total
|
|
406
|
+
} catch (err) {
|
|
407
|
+
if (status) status.textContent = '读取失败'
|
|
408
|
+
rows.replaceChildren(make('div', 'empty', '读取失败:' + (err instanceof Error ? err.message : String(err))))
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function pkWhere(node, row) {
|
|
413
|
+
const where = {}
|
|
414
|
+
for (const key of node.primaryKey) where[key] = row[key]
|
|
415
|
+
if (Object.keys(where).length === 0) throw new Error('表没有主键,无法安全定位行')
|
|
416
|
+
return where
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function editableValues(node, row) {
|
|
420
|
+
const values = {}
|
|
421
|
+
for (const column of node.columns) {
|
|
422
|
+
if (!column.primaryKey) values[column.key] = row[column.key]
|
|
423
|
+
}
|
|
424
|
+
return values
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
async function postWrite(body) {
|
|
428
|
+
const res = await fetch('/api/write', {
|
|
429
|
+
method: 'POST',
|
|
430
|
+
headers: { 'content-type': 'application/json', 'x-zoddb-studio-token': token },
|
|
431
|
+
body: JSON.stringify(body),
|
|
432
|
+
})
|
|
433
|
+
let data = null
|
|
434
|
+
try { data = await res.json() } catch { /* ignore */ }
|
|
435
|
+
if (!res.ok) {
|
|
436
|
+
const message = data && data.error ? data.error : 'HTTP ' + res.status
|
|
437
|
+
throw new Error(message + (data && data.code ? ' [' + data.code + ']' : ''))
|
|
438
|
+
}
|
|
439
|
+
return data
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function openEditor(mode, node, row) {
|
|
443
|
+
const dialog = document.createElement('dialog')
|
|
444
|
+
dialog.className = 'write-dialog'
|
|
445
|
+
const title = make('h3', '', mode === 'create' ? '新建 ' + node.key : mode === 'update' ? '编辑 ' + node.key : '事务')
|
|
446
|
+
const hint = make('p', '', mode === 'transaction'
|
|
447
|
+
? '输入 { "ops": [...] }。所有操作在同一个 db.transaction 中执行,任一失败全部回滚。'
|
|
448
|
+
: mode === 'create'
|
|
449
|
+
? '输入 create input。嵌套 create 与 M2M connect 可以直接写在这里。'
|
|
450
|
+
: '输入 update values。where 由主键自动生成;M2M 可写 tags: { set|connect|disconnect: [...] }。')
|
|
451
|
+
const area = document.createElement('textarea')
|
|
452
|
+
area.spellcheck = false
|
|
453
|
+
if (mode === 'create') area.value = '{}'
|
|
454
|
+
else if (mode === 'update') area.value = JSON.stringify(editableValues(node, row), null, 2)
|
|
455
|
+
else area.value = JSON.stringify({ ops: [] }, null, 2)
|
|
456
|
+
const error = make('div', 'write-error', '')
|
|
457
|
+
const actions = make('div', 'write-actions')
|
|
458
|
+
const cancel = make('button', '', '取消')
|
|
459
|
+
cancel.type = 'button'
|
|
460
|
+
const save = make('button', 'primary', mode === 'delete' ? '删除' : '执行')
|
|
461
|
+
save.type = 'button'
|
|
462
|
+
actions.append(cancel, save)
|
|
463
|
+
dialog.append(title, hint, area, error, actions)
|
|
464
|
+
document.body.append(dialog)
|
|
465
|
+
dialog.addEventListener('close', () => dialog.remove())
|
|
466
|
+
cancel.addEventListener('click', () => dialog.close())
|
|
467
|
+
save.addEventListener('click', async () => {
|
|
468
|
+
let parsed
|
|
469
|
+
try {
|
|
470
|
+
parsed = JSON.parse(area.value)
|
|
471
|
+
} catch (err) {
|
|
472
|
+
error.textContent = 'JSON 无效:' + (err instanceof Error ? err.message : String(err))
|
|
473
|
+
return
|
|
474
|
+
}
|
|
475
|
+
let body
|
|
476
|
+
if (mode === 'create') body = { op: 'create', table: node.key, input: parsed }
|
|
477
|
+
else if (mode === 'update') body = { op: 'update', table: node.key, where: pkWhere(node, row), values: parsed }
|
|
478
|
+
else body = { op: 'transaction', ops: Array.isArray(parsed) ? parsed : parsed.ops }
|
|
479
|
+
save.disabled = true
|
|
480
|
+
error.textContent = ''
|
|
481
|
+
try {
|
|
482
|
+
await postWrite(body)
|
|
483
|
+
dialog.close()
|
|
484
|
+
await loadRows()
|
|
485
|
+
} catch (err) {
|
|
486
|
+
error.textContent = err instanceof Error ? err.message : String(err)
|
|
487
|
+
} finally {
|
|
488
|
+
save.disabled = false
|
|
489
|
+
}
|
|
490
|
+
})
|
|
491
|
+
dialog.showModal()
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function renderTables() {
|
|
495
|
+
clear(tableList)
|
|
496
|
+
if (!state.graph || state.graph.nodes.length === 0) {
|
|
497
|
+
tableList.append(make('div', 'empty', 'schema 中没有表。'))
|
|
498
|
+
return
|
|
499
|
+
}
|
|
500
|
+
for (const node of state.graph.nodes) {
|
|
501
|
+
const button = make('button', 'table-item' + (state.selected === node.key ? ' active' : ''))
|
|
502
|
+
button.type = 'button'
|
|
503
|
+
button.append(make('span', 'mono', node.key))
|
|
504
|
+
button.append(make('small', '', node.table + ' · ' + node.columns.length + ' columns'))
|
|
505
|
+
button.addEventListener('click', () => select(node.key))
|
|
506
|
+
tableList.append(button)
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function renderDetail() {
|
|
511
|
+
clear(tableDetail)
|
|
512
|
+
const node = state.graph && state.graph.nodes.find(n => n.key === state.selected)
|
|
513
|
+
if (!node) {
|
|
514
|
+
tableDetail.append(make('div', 'empty', '选择左侧的表查看列与主键。'))
|
|
515
|
+
return
|
|
516
|
+
}
|
|
517
|
+
const head = make('div', 'detail-head')
|
|
518
|
+
head.append(make('h2', '', node.key))
|
|
519
|
+
head.append(make('span', 'sql-name mono', node.table))
|
|
520
|
+
tableDetail.append(head)
|
|
521
|
+
tableDetail.append(make('div', 'sql-name', 'primary key: ' + (node.primaryKey.length ? node.primaryKey.join(', ') : '无')))
|
|
522
|
+
|
|
523
|
+
if (state.writeEnabled) {
|
|
524
|
+
const actions = make('div', 'detail-actions')
|
|
525
|
+
const add = make('button', '', '新建行')
|
|
526
|
+
add.type = 'button'
|
|
527
|
+
add.addEventListener('click', () => openEditor('create', node))
|
|
528
|
+
const tx = make('button', '', '事务')
|
|
529
|
+
tx.type = 'button'
|
|
530
|
+
tx.addEventListener('click', () => openEditor('transaction', node))
|
|
531
|
+
actions.append(add, tx)
|
|
532
|
+
tableDetail.append(actions)
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const table = make('table')
|
|
536
|
+
const thead = document.createElement('thead')
|
|
537
|
+
const headerRow = document.createElement('tr')
|
|
538
|
+
for (const label of ['Column', 'SQL name', 'Type', 'Null', 'Flags']) headerRow.append(make('th', '', label))
|
|
539
|
+
thead.append(headerRow)
|
|
540
|
+
table.append(thead)
|
|
541
|
+
const tbody = document.createElement('tbody')
|
|
542
|
+
for (const column of node.columns) {
|
|
543
|
+
const row = document.createElement('tr')
|
|
544
|
+
row.append(make('td', 'mono', column.key))
|
|
545
|
+
row.append(make('td', 'mono', column.column))
|
|
546
|
+
row.append(make('td', 'mono', column.type))
|
|
547
|
+
row.append(make('td', '', column.nullable ? 'YES' : 'NO'))
|
|
548
|
+
const flags = document.createElement('td')
|
|
549
|
+
if (column.primaryKey) flags.append(pill('PK', 'pk'))
|
|
550
|
+
if (column.unique) flags.append(pill('UNIQUE', 'unique'))
|
|
551
|
+
if (!column.primaryKey && !column.unique) flags.append(make('span', 'empty', '—'))
|
|
552
|
+
row.append(flags)
|
|
553
|
+
tbody.append(row)
|
|
554
|
+
}
|
|
555
|
+
table.append(tbody)
|
|
556
|
+
tableDetail.append(table)
|
|
557
|
+
tableDetail.append(buildDataPanel(node).wrap)
|
|
558
|
+
void loadRows()
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function edgeText(edge) {
|
|
562
|
+
const base = edge.from + '.' + edge.name + ' → ' + edge.to
|
|
563
|
+
const kind = edge.kind + ' / ' + edge.fromColumn + ' → ' + edge.toColumn
|
|
564
|
+
return { base, kind }
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function renderEdges() {
|
|
568
|
+
clear(edgeList)
|
|
569
|
+
if (!state.graph || state.graph.edges.length === 0) {
|
|
570
|
+
edgeList.append(make('div', 'empty', 'schema 中没有关系。'))
|
|
571
|
+
return
|
|
572
|
+
}
|
|
573
|
+
for (const edge of state.graph.edges) {
|
|
574
|
+
const text = edgeText(edge)
|
|
575
|
+
const active = edge.from === state.selected || edge.to === state.selected
|
|
576
|
+
const card = make('div', 'edge' + (active ? ' active' : ''))
|
|
577
|
+
card.append(make('div', 'edge-title mono', text.base))
|
|
578
|
+
const meta = make('div', 'edge-meta')
|
|
579
|
+
meta.append(make('div', '', text.kind))
|
|
580
|
+
meta.append(make('div', '', edge.foreignKey ? 'foreign key' : 'read-only relation'))
|
|
581
|
+
if (edge.onDelete) meta.append(make('div', '', 'on delete: ' + edge.onDelete))
|
|
582
|
+
if (edge.through) meta.append(make('div', 'mono', 'through: ' + edge.through.table + ' (' + edge.through.from + ' → ' + edge.through.to + ')'))
|
|
583
|
+
card.append(meta)
|
|
584
|
+
const other = edge.from === state.selected ? edge.to : edge.from
|
|
585
|
+
const open = make('button', '', '打开 ' + other)
|
|
586
|
+
open.type = 'button'
|
|
587
|
+
open.addEventListener('click', () => openTable(other))
|
|
588
|
+
card.append(open)
|
|
589
|
+
if (edge.through) {
|
|
590
|
+
const through = make('button', '', '打开 through ' + edge.through.table)
|
|
591
|
+
through.type = 'button'
|
|
592
|
+
through.addEventListener('click', () => openTable(edge.through.table))
|
|
593
|
+
card.append(through)
|
|
594
|
+
}
|
|
595
|
+
edgeList.append(card)
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function setMigrationState(text, kind) {
|
|
600
|
+
migrationState.textContent = text
|
|
601
|
+
migrationState.className = 'migration-state' + (kind ? ' ' + kind : '')
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
async function loadMigrations() {
|
|
605
|
+
if (!token) {
|
|
606
|
+
setMigrationState('不可用', 'error')
|
|
607
|
+
migrationBody.textContent = '缺少访问 token。'
|
|
608
|
+
return
|
|
609
|
+
}
|
|
610
|
+
migrationBody.textContent = '读取中…'
|
|
611
|
+
try {
|
|
612
|
+
const res = await fetch('/api/migrations', { headers: { 'x-zoddb-studio-token': token } })
|
|
613
|
+
let data = null
|
|
614
|
+
try { data = await res.json() } catch { /* ignore */ }
|
|
615
|
+
if (!res.ok) {
|
|
616
|
+
const message = data && data.error ? data.error : 'HTTP ' + res.status
|
|
617
|
+
const code = data && data.code ? ' [' + data.code + ']' : ''
|
|
618
|
+
throw new Error(message + code)
|
|
619
|
+
}
|
|
620
|
+
const status = data.status
|
|
621
|
+
const keys = [
|
|
622
|
+
['applied', 'applied'],
|
|
623
|
+
['pending', 'pending'],
|
|
624
|
+
['missing', 'missing'],
|
|
625
|
+
['outOfOrder', 'out-of-order'],
|
|
626
|
+
['empty', 'empty'],
|
|
627
|
+
['tampered', 'tampered'],
|
|
628
|
+
['drift', 'drift'],
|
|
629
|
+
]
|
|
630
|
+
const issues = keys.filter(([key]) => key !== 'applied').reduce((n, [key]) => n + status[key].length, 0)
|
|
631
|
+
setMigrationState(issues === 0 ? 'up to date' : 'needs attention', issues === 0 ? 'ok' : 'warn')
|
|
632
|
+
const grid = make('div', 'migration-grid')
|
|
633
|
+
for (const [key, label] of keys) {
|
|
634
|
+
const count = status[key].length
|
|
635
|
+
const value = make('div', 'count' + (key !== 'applied' && count > 0 ? ' bad' : ''), String(count))
|
|
636
|
+
grid.append(make('div', 'label', label), value)
|
|
637
|
+
}
|
|
638
|
+
const meta = make('div', 'migration-meta')
|
|
639
|
+
meta.textContent = 'exit ' + data.exitCode + ' · ' + data.migrations + ' · ' + new Date(data.checkedAt).toLocaleString()
|
|
640
|
+
migrationBody.className = ''
|
|
641
|
+
migrationBody.replaceChildren(grid, meta)
|
|
642
|
+
} catch (err) {
|
|
643
|
+
setMigrationState('不可用', 'error')
|
|
644
|
+
migrationBody.className = 'empty'
|
|
645
|
+
migrationBody.replaceChildren(make('div', 'migration-error', err instanceof Error ? err.message : String(err)))
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
migrationRefresh.addEventListener('click', () => { void loadMigrations() })
|
|
650
|
+
|
|
651
|
+
function select(key, initialFilter) {
|
|
652
|
+
state.selected = key
|
|
653
|
+
state.page = 0
|
|
654
|
+
state.total = 0
|
|
655
|
+
state.filter = initialFilter ? { ...initialFilter } : { column: '', op: 'eq', value: '' }
|
|
656
|
+
state.sort = ''
|
|
657
|
+
state.with = ''
|
|
658
|
+
state.relation = { name: '', mode: 'some', column: '', op: 'eq', value: '' }
|
|
659
|
+
renderTables()
|
|
660
|
+
renderDetail()
|
|
661
|
+
renderEdges()
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function openTable(key, column, value) {
|
|
665
|
+
select(key, column ? { column, op: 'eq', value: String(value) } : undefined)
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
async function load() {
|
|
669
|
+
if (!token) {
|
|
670
|
+
setStatus('缺少访问 token;请使用启动时输出的完整 URL。', true)
|
|
671
|
+
tableList.textContent = '无法读取 schema。'
|
|
672
|
+
edgeList.textContent = '无法读取 schema。'
|
|
673
|
+
setMigrationState('不可用', 'error')
|
|
674
|
+
migrationBody.textContent = '缺少访问 token。'
|
|
675
|
+
return
|
|
676
|
+
}
|
|
677
|
+
try {
|
|
678
|
+
const res = await fetch('/api/schema', { headers: { 'x-zoddb-studio-token': token } })
|
|
679
|
+
if (!res.ok) throw new Error('HTTP ' + res.status)
|
|
680
|
+
state.graph = await res.json()
|
|
681
|
+
state.writeEnabled = Boolean(state.graph.writeEnabled)
|
|
682
|
+
const badge = document.getElementById('mode-badge')
|
|
683
|
+
if (badge) {
|
|
684
|
+
badge.textContent = state.writeEnabled ? 'write' : 'read-only'
|
|
685
|
+
badge.className = state.writeEnabled ? 'badge write' : 'badge'
|
|
686
|
+
}
|
|
687
|
+
setStatus(state.graph.nodes.length + ' tables · ' + state.graph.edges.length + ' relations')
|
|
688
|
+
renderTables()
|
|
689
|
+
renderEdges()
|
|
690
|
+
if (state.graph.nodes[0]) select(state.graph.nodes[0].key)
|
|
691
|
+
else renderDetail()
|
|
692
|
+
document.title = 'zoddb studio · ' + state.graph.nodes.length + ' tables'
|
|
693
|
+
void loadMigrations()
|
|
694
|
+
} catch (err) {
|
|
695
|
+
setStatus('读取 schema 失败:' + (err instanceof Error ? err.message : String(err)), true)
|
|
696
|
+
tableList.textContent = '读取失败。'
|
|
697
|
+
edgeList.textContent = '读取失败。'
|
|
698
|
+
setMigrationState('不可用', 'error')
|
|
699
|
+
migrationBody.textContent = '读取 schema 失败。'
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
load()
|
|
704
|
+
})()
|
|
705
|
+
</script>
|
|
706
|
+
</body>
|
|
707
|
+
</html>`;
|
|
708
|
+
//# sourceMappingURL=page.js.map
|