@hobenakicoffee/libraries 8.0.0 → 8.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/package.json +1 -1
- package/src/App.tsx +20 -3
- package/src/SchemaViewer.tsx +616 -0
- package/src/types/supabase.ts +57 -0
- package/src/vite-env.d.ts +35 -0
package/package.json
CHANGED
package/src/App.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useState } from "react";
|
|
2
2
|
import { productInfo } from "@/constants/legal";
|
|
3
|
+
import SchemaViewer from "@/SchemaViewer";
|
|
3
4
|
import logoSvg from "/favicon.svg";
|
|
4
5
|
|
|
5
6
|
declare const __LATEST_VERSION__: string;
|
|
@@ -39,7 +40,10 @@ const EXPORTS = [
|
|
|
39
40
|
{ path: "/docs", name: "docs", desc: "Documentation & guides" },
|
|
40
41
|
];
|
|
41
42
|
|
|
43
|
+
type Page = "home" | "schema";
|
|
44
|
+
|
|
42
45
|
const App = () => {
|
|
46
|
+
const [page, setPage] = useState<Page>("home");
|
|
43
47
|
const [copied, setCopied] = useState(false);
|
|
44
48
|
|
|
45
49
|
const handleCopy = () => {
|
|
@@ -48,6 +52,10 @@ const App = () => {
|
|
|
48
52
|
setTimeout(() => setCopied(false), 2000);
|
|
49
53
|
};
|
|
50
54
|
|
|
55
|
+
if (page === "schema") {
|
|
56
|
+
return <SchemaViewer onBack={() => setPage("home")} />;
|
|
57
|
+
}
|
|
58
|
+
|
|
51
59
|
return (
|
|
52
60
|
<div className="min-dvh relative flex flex-col overflow-hidden bg-[#0a0c10] text-slate-200">
|
|
53
61
|
<div className="pointer-events-none absolute inset-0 overflow-hidden">
|
|
@@ -76,9 +84,18 @@ const App = () => {
|
|
|
76
84
|
</p>
|
|
77
85
|
</div>
|
|
78
86
|
</div>
|
|
79
|
-
<div className="flex items-center gap-
|
|
80
|
-
<
|
|
81
|
-
|
|
87
|
+
<div className="flex items-center gap-3">
|
|
88
|
+
<button
|
|
89
|
+
className="cursor-pointer rounded-md border border-slate-700 bg-slate-900/50 px-3 py-1.5 font-mono text-slate-400 text-xs transition-colors hover:border-slate-500 hover:text-slate-200"
|
|
90
|
+
onClick={() => setPage("schema")}
|
|
91
|
+
type="button"
|
|
92
|
+
>
|
|
93
|
+
schema
|
|
94
|
+
</button>
|
|
95
|
+
<div className="flex items-center gap-2 rounded-md border border-slate-800 bg-slate-900/50 px-2 py-1 font-mono text-slate-400 text-xs">
|
|
96
|
+
<span className="h-2 w-2 rounded-full bg-green-500" />
|
|
97
|
+
TypeScript
|
|
98
|
+
</div>
|
|
82
99
|
</div>
|
|
83
100
|
</header>
|
|
84
101
|
|
|
@@ -0,0 +1,616 @@
|
|
|
1
|
+
import { schema } from "virtual:supabase-schema";
|
|
2
|
+
import { useMemo, useState } from "react";
|
|
3
|
+
|
|
4
|
+
type SelectedItem =
|
|
5
|
+
| { type: "table"; name: string }
|
|
6
|
+
| { type: "view"; name: string }
|
|
7
|
+
| { type: "enum"; name: string };
|
|
8
|
+
|
|
9
|
+
const tableNames = Object.keys(schema.tables);
|
|
10
|
+
const viewNames = Object.keys(schema.views);
|
|
11
|
+
const enumNames = Object.keys(schema.enums);
|
|
12
|
+
|
|
13
|
+
const SectionIcon = ({ label }: { label: string }) => {
|
|
14
|
+
if (label === "Tables") return <span className="text-blue-400">▶</span>;
|
|
15
|
+
if (label === "Views")
|
|
16
|
+
return <span className="text-emerald-400">▶</span>;
|
|
17
|
+
if (label === "Enums") return <span className="text-amber-400">▶</span>;
|
|
18
|
+
return null;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const EnumBadge = ({ values }: { values: string[] }) => (
|
|
22
|
+
<span
|
|
23
|
+
className="cursor-help rounded border border-amber-700/40 bg-amber-950/40 px-1.5 py-0.5 font-mono text-[10px] text-amber-300"
|
|
24
|
+
title={values.join(", ")}
|
|
25
|
+
>
|
|
26
|
+
enum
|
|
27
|
+
</span>
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
const SerialBadge = () => (
|
|
31
|
+
<span className="rounded border border-purple-700/40 bg-purple-950/40 px-1.5 py-0.5 font-mono text-[10px] text-purple-300">
|
|
32
|
+
serial
|
|
33
|
+
</span>
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const NullableBadge = () => (
|
|
37
|
+
<span className="rounded border border-slate-600/40 bg-slate-800/40 px-1.5 py-0.5 font-mono text-[10px] text-slate-400">
|
|
38
|
+
null
|
|
39
|
+
</span>
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
const BackButton = ({ onClick }: { onClick: () => void }) => (
|
|
43
|
+
<button
|
|
44
|
+
className="flex cursor-pointer items-center gap-1.5 rounded-lg border border-slate-700 bg-slate-900 px-3 py-1.5 font-mono text-slate-400 text-xs transition-colors hover:border-slate-500 hover:text-slate-200"
|
|
45
|
+
onClick={onClick}
|
|
46
|
+
type="button"
|
|
47
|
+
>
|
|
48
|
+
<span>←</span>
|
|
49
|
+
<span>Back</span>
|
|
50
|
+
</button>
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
const SchemaViewer = ({ onBack }: { onBack: () => void }) => {
|
|
54
|
+
const [selected, setSelected] = useState<SelectedItem | null>(null);
|
|
55
|
+
const [search, setSearch] = useState("");
|
|
56
|
+
const [collapsed, setCollapsed] = useState<Record<string, boolean>>({
|
|
57
|
+
Tables: false,
|
|
58
|
+
Views: false,
|
|
59
|
+
Enums: false,
|
|
60
|
+
});
|
|
61
|
+
const [sidebarOpen, setSidebarOpen] = useState(false);
|
|
62
|
+
|
|
63
|
+
const query = search.toLowerCase();
|
|
64
|
+
|
|
65
|
+
const filteredTables = useMemo(
|
|
66
|
+
() => tableNames.filter((n) => n.includes(query)),
|
|
67
|
+
[query]
|
|
68
|
+
);
|
|
69
|
+
const filteredViews = useMemo(
|
|
70
|
+
() => viewNames.filter((n) => n.includes(query)),
|
|
71
|
+
[query]
|
|
72
|
+
);
|
|
73
|
+
const filteredEnums = useMemo(
|
|
74
|
+
() => enumNames.filter((n) => n.includes(query)),
|
|
75
|
+
[query]
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
const hasTables = filteredTables.length > 0;
|
|
79
|
+
const hasViews = filteredViews.length > 0;
|
|
80
|
+
const hasEnums = filteredEnums.length > 0;
|
|
81
|
+
|
|
82
|
+
const toggleSection = (section: string) => {
|
|
83
|
+
setCollapsed((prev) => ({ ...prev, [section]: !prev[section] }));
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const handleSelect = (item: SelectedItem) => {
|
|
87
|
+
setSelected(item);
|
|
88
|
+
setSidebarOpen(false);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const handleFKClick = (tableName: string) => {
|
|
92
|
+
setSelected({ type: "table", name: tableName });
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const sel = selected;
|
|
96
|
+
const selectedTable =
|
|
97
|
+
sel !== null && sel.type === "table" ? schema.tables[sel.name] : undefined;
|
|
98
|
+
const selectedView =
|
|
99
|
+
sel !== null && sel.type === "view" ? schema.views[sel.name] : undefined;
|
|
100
|
+
const selectedEnum =
|
|
101
|
+
sel !== null && sel.type === "enum"
|
|
102
|
+
? { name: sel.name, values: schema.enums[sel.name] ?? [] }
|
|
103
|
+
: undefined;
|
|
104
|
+
|
|
105
|
+
const getEnumValues = (enumName: string): string[] | undefined =>
|
|
106
|
+
schema.enums[enumName];
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<div className="flex h-dvh flex-col bg-[#0a0c10] text-slate-200">
|
|
110
|
+
<header className="flex shrink-0 items-center justify-between border-slate-800 border-b px-6 py-3">
|
|
111
|
+
<div className="flex items-center gap-4">
|
|
112
|
+
<BackButton onClick={onBack} />
|
|
113
|
+
<h1 className="font-mono font-semibold text-sm">
|
|
114
|
+
<span className="text-brand">$</span> supabase schema
|
|
115
|
+
</h1>
|
|
116
|
+
</div>
|
|
117
|
+
<div className="flex items-center gap-3">
|
|
118
|
+
{selected && (
|
|
119
|
+
<span className="hidden font-mono text-slate-500 text-xs sm:inline">
|
|
120
|
+
{selected.type} / {selected.name}
|
|
121
|
+
</span>
|
|
122
|
+
)}
|
|
123
|
+
<button
|
|
124
|
+
className="flex cursor-pointer items-center gap-1.5 rounded-lg border border-slate-700 bg-slate-900 px-3 py-1.5 font-mono text-slate-400 text-xs transition-colors hover:border-slate-500 hover:text-slate-200 lg:hidden"
|
|
125
|
+
onClick={() => setSidebarOpen(true)}
|
|
126
|
+
type="button"
|
|
127
|
+
>
|
|
128
|
+
<svg
|
|
129
|
+
aria-label="Menu"
|
|
130
|
+
className="h-4 w-4"
|
|
131
|
+
fill="none"
|
|
132
|
+
role="img"
|
|
133
|
+
stroke="currentColor"
|
|
134
|
+
strokeWidth="2"
|
|
135
|
+
viewBox="0 0 24 24"
|
|
136
|
+
>
|
|
137
|
+
<title>Open sidebar</title>
|
|
138
|
+
<path d="M4 6h16M4 12h16M4 18h16" strokeLinecap="round" />
|
|
139
|
+
</svg>
|
|
140
|
+
<span>Tables</span>
|
|
141
|
+
</button>
|
|
142
|
+
</div>
|
|
143
|
+
</header>
|
|
144
|
+
|
|
145
|
+
<div className="flex min-h-0 flex-1">
|
|
146
|
+
{sidebarOpen && (
|
|
147
|
+
<button
|
|
148
|
+
aria-label="Close sidebar"
|
|
149
|
+
className="fixed inset-0 z-30 cursor-default bg-black/60 lg:hidden"
|
|
150
|
+
onClick={() => setSidebarOpen(false)}
|
|
151
|
+
type="button"
|
|
152
|
+
/>
|
|
153
|
+
)}
|
|
154
|
+
|
|
155
|
+
<aside
|
|
156
|
+
className={`w-72 shrink-0 overflow-y-auto border-slate-800 border-r bg-slate-950 p-4 ${
|
|
157
|
+
sidebarOpen
|
|
158
|
+
? "fixed inset-y-0 left-0 z-40 block"
|
|
159
|
+
: "hidden lg:block"
|
|
160
|
+
}`}
|
|
161
|
+
>
|
|
162
|
+
<SidebarContent
|
|
163
|
+
collapsed={collapsed}
|
|
164
|
+
filteredEnums={filteredEnums}
|
|
165
|
+
filteredTables={filteredTables}
|
|
166
|
+
filteredViews={filteredViews}
|
|
167
|
+
handleSelect={handleSelect}
|
|
168
|
+
hasEnums={hasEnums}
|
|
169
|
+
hasTables={hasTables}
|
|
170
|
+
hasViews={hasViews}
|
|
171
|
+
search={search}
|
|
172
|
+
selected={selected}
|
|
173
|
+
setSearch={setSearch}
|
|
174
|
+
toggleSection={toggleSection}
|
|
175
|
+
/>
|
|
176
|
+
</aside>
|
|
177
|
+
|
|
178
|
+
<main className="flex-1 overflow-y-auto p-6">
|
|
179
|
+
{!selected && (
|
|
180
|
+
<div className="flex items-center justify-center pt-24">
|
|
181
|
+
<div className="text-center">
|
|
182
|
+
<div className="mb-4 font-mono text-4xl text-slate-600">
|
|
183
|
+
{"{ : }"}
|
|
184
|
+
</div>
|
|
185
|
+
<p className="font-mono text-slate-500 text-sm">
|
|
186
|
+
select a table, view, or enum from the sidebar
|
|
187
|
+
</p>
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
)}
|
|
191
|
+
|
|
192
|
+
{selectedTable && sel && (
|
|
193
|
+
<TableDetail
|
|
194
|
+
enumValues={getEnumValues}
|
|
195
|
+
onFKClick={handleFKClick}
|
|
196
|
+
relationships={selectedTable.relationships}
|
|
197
|
+
tableName={sel.name}
|
|
198
|
+
>
|
|
199
|
+
<ColumnsTable
|
|
200
|
+
columns={selectedTable.columns}
|
|
201
|
+
enumValues={getEnumValues}
|
|
202
|
+
/>
|
|
203
|
+
</TableDetail>
|
|
204
|
+
)}
|
|
205
|
+
|
|
206
|
+
{selectedView && sel && (
|
|
207
|
+
<ViewDetail viewName={sel.name}>
|
|
208
|
+
<ColumnsTable
|
|
209
|
+
columns={selectedView.columns}
|
|
210
|
+
enumValues={getEnumValues}
|
|
211
|
+
/>
|
|
212
|
+
</ViewDetail>
|
|
213
|
+
)}
|
|
214
|
+
|
|
215
|
+
{selectedEnum && (
|
|
216
|
+
<EnumDetail name={selectedEnum.name} values={selectedEnum.values} />
|
|
217
|
+
)}
|
|
218
|
+
</main>
|
|
219
|
+
</div>
|
|
220
|
+
</div>
|
|
221
|
+
);
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const SidebarSection = ({
|
|
225
|
+
children,
|
|
226
|
+
collapsed,
|
|
227
|
+
count,
|
|
228
|
+
label,
|
|
229
|
+
onToggle,
|
|
230
|
+
}: {
|
|
231
|
+
children: React.ReactNode;
|
|
232
|
+
collapsed: boolean;
|
|
233
|
+
count: number;
|
|
234
|
+
label: string;
|
|
235
|
+
onToggle: () => void;
|
|
236
|
+
}) => (
|
|
237
|
+
<div className="mb-3">
|
|
238
|
+
<button
|
|
239
|
+
className="flex w-full cursor-pointer items-center gap-2 rounded px-1 py-1.5 font-mono text-slate-400 text-xs transition-colors hover:text-slate-200"
|
|
240
|
+
onClick={onToggle}
|
|
241
|
+
type="button"
|
|
242
|
+
>
|
|
243
|
+
<span className={`transition-transform ${collapsed ? "-rotate-90" : ""}`}>
|
|
244
|
+
<SectionIcon label={label} />
|
|
245
|
+
</span>
|
|
246
|
+
<span>{label}</span>
|
|
247
|
+
<span className="ml-auto rounded bg-slate-800 px-1.5 py-0.5 text-slate-500">
|
|
248
|
+
{count}
|
|
249
|
+
</span>
|
|
250
|
+
</button>
|
|
251
|
+
{!collapsed && <div className="mt-1 ml-2 space-y-0.5">{children}</div>}
|
|
252
|
+
</div>
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
const SidebarItem = ({
|
|
256
|
+
isActive,
|
|
257
|
+
label,
|
|
258
|
+
onClick,
|
|
259
|
+
}: {
|
|
260
|
+
isActive: boolean;
|
|
261
|
+
label: string;
|
|
262
|
+
onClick: () => void;
|
|
263
|
+
}) => (
|
|
264
|
+
<button
|
|
265
|
+
className={`w-full cursor-pointer rounded px-3 py-1.5 text-left font-mono text-xs transition-colors ${
|
|
266
|
+
isActive
|
|
267
|
+
? "bg-brand/10 text-brand"
|
|
268
|
+
: "text-slate-400 hover:bg-slate-800/50 hover:text-slate-200"
|
|
269
|
+
}`}
|
|
270
|
+
onClick={onClick}
|
|
271
|
+
type="button"
|
|
272
|
+
>
|
|
273
|
+
{label}
|
|
274
|
+
</button>
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
const SidebarContent = ({
|
|
278
|
+
collapsed,
|
|
279
|
+
filteredEnums,
|
|
280
|
+
filteredTables,
|
|
281
|
+
filteredViews,
|
|
282
|
+
handleSelect,
|
|
283
|
+
hasEnums,
|
|
284
|
+
hasTables,
|
|
285
|
+
hasViews,
|
|
286
|
+
search,
|
|
287
|
+
selected,
|
|
288
|
+
setSearch,
|
|
289
|
+
toggleSection,
|
|
290
|
+
}: {
|
|
291
|
+
collapsed: Record<string, boolean>;
|
|
292
|
+
filteredEnums: string[];
|
|
293
|
+
filteredTables: string[];
|
|
294
|
+
filteredViews: string[];
|
|
295
|
+
handleSelect: (item: SelectedItem) => void;
|
|
296
|
+
hasEnums: boolean;
|
|
297
|
+
hasTables: boolean;
|
|
298
|
+
hasViews: boolean;
|
|
299
|
+
search: string;
|
|
300
|
+
selected: SelectedItem | null;
|
|
301
|
+
setSearch: (v: string) => void;
|
|
302
|
+
toggleSection: (section: string) => void;
|
|
303
|
+
}) => (
|
|
304
|
+
<>
|
|
305
|
+
<input
|
|
306
|
+
className="mb-4 w-full rounded-lg border border-slate-700 bg-slate-900 px-3 py-2 font-mono text-slate-200 text-xs placeholder-slate-500 outline-none focus:border-brand/50"
|
|
307
|
+
onChange={(e) => setSearch(e.target.value)}
|
|
308
|
+
placeholder="filter tables, views, enums..."
|
|
309
|
+
type="text"
|
|
310
|
+
value={search}
|
|
311
|
+
/>
|
|
312
|
+
|
|
313
|
+
{!(hasTables || hasViews || hasEnums) && (
|
|
314
|
+
<p className="font-mono text-slate-500 text-xs">no results</p>
|
|
315
|
+
)}
|
|
316
|
+
|
|
317
|
+
{hasTables && (
|
|
318
|
+
<SidebarSection
|
|
319
|
+
collapsed={collapsed.Tables ?? false}
|
|
320
|
+
count={filteredTables.length}
|
|
321
|
+
label="Tables"
|
|
322
|
+
onToggle={() => toggleSection("Tables")}
|
|
323
|
+
>
|
|
324
|
+
{filteredTables.map((name) => (
|
|
325
|
+
<SidebarItem
|
|
326
|
+
isActive={selected?.type === "table" && selected.name === name}
|
|
327
|
+
key={name}
|
|
328
|
+
label={name}
|
|
329
|
+
onClick={() => handleSelect({ type: "table", name })}
|
|
330
|
+
/>
|
|
331
|
+
))}
|
|
332
|
+
</SidebarSection>
|
|
333
|
+
)}
|
|
334
|
+
|
|
335
|
+
{hasViews && (
|
|
336
|
+
<SidebarSection
|
|
337
|
+
collapsed={collapsed.Views ?? false}
|
|
338
|
+
count={filteredViews.length}
|
|
339
|
+
label="Views"
|
|
340
|
+
onToggle={() => toggleSection("Views")}
|
|
341
|
+
>
|
|
342
|
+
{filteredViews.map((name) => (
|
|
343
|
+
<SidebarItem
|
|
344
|
+
isActive={selected?.type === "view" && selected.name === name}
|
|
345
|
+
key={name}
|
|
346
|
+
label={name}
|
|
347
|
+
onClick={() => handleSelect({ type: "view", name })}
|
|
348
|
+
/>
|
|
349
|
+
))}
|
|
350
|
+
</SidebarSection>
|
|
351
|
+
)}
|
|
352
|
+
|
|
353
|
+
{hasEnums && (
|
|
354
|
+
<SidebarSection
|
|
355
|
+
collapsed={collapsed.Enums ?? false}
|
|
356
|
+
count={filteredEnums.length}
|
|
357
|
+
label="Enums"
|
|
358
|
+
onToggle={() => toggleSection("Enums")}
|
|
359
|
+
>
|
|
360
|
+
{filteredEnums.map((name) => (
|
|
361
|
+
<SidebarItem
|
|
362
|
+
isActive={selected?.type === "enum" && selected.name === name}
|
|
363
|
+
key={name}
|
|
364
|
+
label={name}
|
|
365
|
+
onClick={() => handleSelect({ type: "enum", name })}
|
|
366
|
+
/>
|
|
367
|
+
))}
|
|
368
|
+
</SidebarSection>
|
|
369
|
+
)}
|
|
370
|
+
</>
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
const ColumnsTable = ({
|
|
374
|
+
columns,
|
|
375
|
+
enumValues,
|
|
376
|
+
}: {
|
|
377
|
+
columns: {
|
|
378
|
+
name: string;
|
|
379
|
+
type: string;
|
|
380
|
+
rawType: string;
|
|
381
|
+
nullable: boolean;
|
|
382
|
+
enumName: string | null;
|
|
383
|
+
}[];
|
|
384
|
+
enumValues: (name: string) => string[] | undefined;
|
|
385
|
+
}) => {
|
|
386
|
+
if (columns.length === 0) {
|
|
387
|
+
return (
|
|
388
|
+
<p className="py-4 font-mono text-slate-500 text-xs">
|
|
389
|
+
no columns defined
|
|
390
|
+
</p>
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return (
|
|
395
|
+
<div className="overflow-x-auto rounded-lg border border-slate-800">
|
|
396
|
+
<table className="w-full border-collapse font-mono text-xs">
|
|
397
|
+
<thead>
|
|
398
|
+
<tr className="border-slate-800 border-b bg-slate-900/80">
|
|
399
|
+
<Th>Column</Th>
|
|
400
|
+
<Th>Type</Th>
|
|
401
|
+
<Th>Attributes</Th>
|
|
402
|
+
</tr>
|
|
403
|
+
</thead>
|
|
404
|
+
<tbody>
|
|
405
|
+
{columns.map((col) => {
|
|
406
|
+
const enumName: string | null = col.enumName;
|
|
407
|
+
const isEnum = enumName !== null;
|
|
408
|
+
const isSerial = col.rawType === "never";
|
|
409
|
+
const hasEnumValues = isEnum
|
|
410
|
+
? (enumValues(enumName) ?? [])
|
|
411
|
+
: undefined;
|
|
412
|
+
|
|
413
|
+
return (
|
|
414
|
+
<tr
|
|
415
|
+
className="border-slate-800/50 border-b transition-colors hover:bg-slate-800/30"
|
|
416
|
+
key={col.name}
|
|
417
|
+
>
|
|
418
|
+
<Td>{col.name}</Td>
|
|
419
|
+
<Td>
|
|
420
|
+
<span
|
|
421
|
+
className={
|
|
422
|
+
isEnum
|
|
423
|
+
? "text-amber-300"
|
|
424
|
+
: isSerial
|
|
425
|
+
? "text-purple-300"
|
|
426
|
+
: "text-cyan-300"
|
|
427
|
+
}
|
|
428
|
+
title={col.rawType}
|
|
429
|
+
>
|
|
430
|
+
{col.type}
|
|
431
|
+
</span>
|
|
432
|
+
</Td>
|
|
433
|
+
<Td>
|
|
434
|
+
<div className="flex flex-wrap gap-1">
|
|
435
|
+
{isSerial && <SerialBadge />}
|
|
436
|
+
{col.nullable && <NullableBadge />}
|
|
437
|
+
{isEnum && <EnumBadge values={hasEnumValues ?? []} />}
|
|
438
|
+
</div>
|
|
439
|
+
</Td>
|
|
440
|
+
</tr>
|
|
441
|
+
);
|
|
442
|
+
})}
|
|
443
|
+
</tbody>
|
|
444
|
+
</table>
|
|
445
|
+
</div>
|
|
446
|
+
);
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
const Th = ({ children }: { children: React.ReactNode }) => (
|
|
450
|
+
<th className="px-4 py-2.5 text-left font-semibold text-slate-400">
|
|
451
|
+
{children}
|
|
452
|
+
</th>
|
|
453
|
+
);
|
|
454
|
+
|
|
455
|
+
const Td = ({ children }: { children: React.ReactNode }) => (
|
|
456
|
+
<td className="px-4 py-2.5 text-slate-300">{children}</td>
|
|
457
|
+
);
|
|
458
|
+
|
|
459
|
+
const TableDetail = ({
|
|
460
|
+
children,
|
|
461
|
+
enumValues,
|
|
462
|
+
onFKClick,
|
|
463
|
+
relationships,
|
|
464
|
+
tableName,
|
|
465
|
+
}: {
|
|
466
|
+
children: React.ReactNode;
|
|
467
|
+
enumValues: (name: string) => string[] | undefined;
|
|
468
|
+
onFKClick: (tableName: string) => void;
|
|
469
|
+
relationships: {
|
|
470
|
+
column: string;
|
|
471
|
+
referencedTable: string;
|
|
472
|
+
referencedColumn: string;
|
|
473
|
+
foreignKeyName: string;
|
|
474
|
+
isOneToOne: boolean;
|
|
475
|
+
}[];
|
|
476
|
+
tableName: string;
|
|
477
|
+
}) => (
|
|
478
|
+
<div>
|
|
479
|
+
<div className="mb-6">
|
|
480
|
+
<h2 className="font-mono font-semibold text-lg text-slate-100">
|
|
481
|
+
{tableName}
|
|
482
|
+
</h2>
|
|
483
|
+
<p className="font-mono text-slate-500 text-xs">table</p>
|
|
484
|
+
</div>
|
|
485
|
+
|
|
486
|
+
<div className="mb-8">
|
|
487
|
+
<h3 className="mb-3 font-mono text-slate-500 text-xs uppercase tracking-wider">
|
|
488
|
+
Columns
|
|
489
|
+
</h3>
|
|
490
|
+
{children}
|
|
491
|
+
</div>
|
|
492
|
+
|
|
493
|
+
{relationships.length > 0 && (
|
|
494
|
+
<div>
|
|
495
|
+
<h3 className="mb-3 font-mono text-slate-500 text-xs uppercase tracking-wider">
|
|
496
|
+
Relationships
|
|
497
|
+
</h3>
|
|
498
|
+
<div className="space-y-1.5">
|
|
499
|
+
{relationships.map((rel) => (
|
|
500
|
+
<RelationshipRow
|
|
501
|
+
enumValues={enumValues}
|
|
502
|
+
isOneToOne={rel.isOneToOne}
|
|
503
|
+
key={rel.foreignKeyName}
|
|
504
|
+
onTableClick={onFKClick}
|
|
505
|
+
rel={rel}
|
|
506
|
+
/>
|
|
507
|
+
))}
|
|
508
|
+
</div>
|
|
509
|
+
</div>
|
|
510
|
+
)}
|
|
511
|
+
</div>
|
|
512
|
+
);
|
|
513
|
+
|
|
514
|
+
const RelationshipRow = ({
|
|
515
|
+
enumValues,
|
|
516
|
+
isOneToOne,
|
|
517
|
+
onTableClick,
|
|
518
|
+
rel,
|
|
519
|
+
}: {
|
|
520
|
+
enumValues: (name: string) => string[] | undefined;
|
|
521
|
+
isOneToOne: boolean;
|
|
522
|
+
onTableClick: (tableName: string) => void;
|
|
523
|
+
rel: {
|
|
524
|
+
column: string;
|
|
525
|
+
referencedTable: string;
|
|
526
|
+
referencedColumn: string;
|
|
527
|
+
foreignKeyName: string;
|
|
528
|
+
isOneToOne: boolean;
|
|
529
|
+
};
|
|
530
|
+
}) => {
|
|
531
|
+
const hasEnumOnRef = enumValues(rel.referencedTable) !== undefined;
|
|
532
|
+
return (
|
|
533
|
+
<div
|
|
534
|
+
className="group rounded-lg border border-slate-800 bg-slate-900/30 p-3 transition-colors hover:border-slate-700"
|
|
535
|
+
key={rel.foreignKeyName}
|
|
536
|
+
>
|
|
537
|
+
<div className="mb-1 flex items-center gap-2 font-mono text-xs">
|
|
538
|
+
<code className="text-cyan-300">{rel.column}</code>
|
|
539
|
+
<span className="text-slate-600">
|
|
540
|
+
{isOneToOne ? "\u2192" : "\u2192\u2192"}
|
|
541
|
+
</span>
|
|
542
|
+
<button
|
|
543
|
+
className="cursor-pointer text-brand transition-colors hover:text-brand/80"
|
|
544
|
+
onClick={() => onTableClick(rel.referencedTable)}
|
|
545
|
+
title={`Jump to ${rel.referencedTable}`}
|
|
546
|
+
type="button"
|
|
547
|
+
>
|
|
548
|
+
{rel.referencedTable}
|
|
549
|
+
</button>
|
|
550
|
+
<code className="text-slate-500">.{rel.referencedColumn}</code>
|
|
551
|
+
{isOneToOne && (
|
|
552
|
+
<span className="rounded border border-purple-700/30 bg-purple-950/30 px-1.5 py-0.5 text-[10px] text-purple-300">
|
|
553
|
+
1:1
|
|
554
|
+
</span>
|
|
555
|
+
)}
|
|
556
|
+
{hasEnumOnRef && (
|
|
557
|
+
<EnumBadge values={enumValues(rel.referencedTable) ?? []} />
|
|
558
|
+
)}
|
|
559
|
+
</div>
|
|
560
|
+
<div className="font-mono text-[10px] text-slate-600">
|
|
561
|
+
{rel.foreignKeyName.replace(/_fkey$/, "")}
|
|
562
|
+
</div>
|
|
563
|
+
</div>
|
|
564
|
+
);
|
|
565
|
+
};
|
|
566
|
+
|
|
567
|
+
const ViewDetail = ({
|
|
568
|
+
children,
|
|
569
|
+
viewName,
|
|
570
|
+
}: {
|
|
571
|
+
children: React.ReactNode;
|
|
572
|
+
viewName: string;
|
|
573
|
+
}) => (
|
|
574
|
+
<div>
|
|
575
|
+
<div className="mb-6">
|
|
576
|
+
<h2 className="font-mono font-semibold text-lg text-slate-100">
|
|
577
|
+
{viewName}
|
|
578
|
+
</h2>
|
|
579
|
+
<p className="font-mono text-slate-500 text-xs">view</p>
|
|
580
|
+
</div>
|
|
581
|
+
|
|
582
|
+
<div className="mb-8">
|
|
583
|
+
<h3 className="mb-3 font-mono text-slate-500 text-xs uppercase tracking-wider">
|
|
584
|
+
Columns
|
|
585
|
+
</h3>
|
|
586
|
+
{children}
|
|
587
|
+
</div>
|
|
588
|
+
</div>
|
|
589
|
+
);
|
|
590
|
+
|
|
591
|
+
const EnumDetail = ({ name, values }: { name: string; values: string[] }) => (
|
|
592
|
+
<div>
|
|
593
|
+
<div className="mb-6">
|
|
594
|
+
<h2 className="font-mono font-semibold text-lg text-slate-100">{name}</h2>
|
|
595
|
+
<p className="font-mono text-slate-500 text-xs">enum</p>
|
|
596
|
+
</div>
|
|
597
|
+
|
|
598
|
+
<div className="mb-8">
|
|
599
|
+
<h3 className="mb-3 font-mono text-slate-500 text-xs uppercase tracking-wider">
|
|
600
|
+
Values
|
|
601
|
+
</h3>
|
|
602
|
+
<div className="flex flex-wrap gap-2">
|
|
603
|
+
{values.map((v) => (
|
|
604
|
+
<span
|
|
605
|
+
className="rounded-lg border border-slate-700 bg-slate-900 px-3 py-1.5 font-mono text-slate-300 text-xs"
|
|
606
|
+
key={v}
|
|
607
|
+
>
|
|
608
|
+
{v}
|
|
609
|
+
</span>
|
|
610
|
+
))}
|
|
611
|
+
</div>
|
|
612
|
+
</div>
|
|
613
|
+
</div>
|
|
614
|
+
);
|
|
615
|
+
|
|
616
|
+
export default SchemaViewer;
|
package/src/types/supabase.ts
CHANGED
|
@@ -1381,6 +1381,30 @@ export type Database = {
|
|
|
1381
1381
|
};
|
|
1382
1382
|
Relationships: [];
|
|
1383
1383
|
};
|
|
1384
|
+
messages_2026_08: {
|
|
1385
|
+
Row: {
|
|
1386
|
+
content: string;
|
|
1387
|
+
conversation_id: string;
|
|
1388
|
+
created_at: string;
|
|
1389
|
+
id: number;
|
|
1390
|
+
sender_id: string;
|
|
1391
|
+
};
|
|
1392
|
+
Insert: {
|
|
1393
|
+
content: string;
|
|
1394
|
+
conversation_id: string;
|
|
1395
|
+
created_at?: string;
|
|
1396
|
+
id?: number;
|
|
1397
|
+
sender_id: string;
|
|
1398
|
+
};
|
|
1399
|
+
Update: {
|
|
1400
|
+
content?: string;
|
|
1401
|
+
conversation_id?: string;
|
|
1402
|
+
created_at?: string;
|
|
1403
|
+
id?: number;
|
|
1404
|
+
sender_id?: string;
|
|
1405
|
+
};
|
|
1406
|
+
Relationships: [];
|
|
1407
|
+
};
|
|
1384
1408
|
messages_default: {
|
|
1385
1409
|
Row: {
|
|
1386
1410
|
content: string;
|
|
@@ -4587,6 +4611,39 @@ export type Database = {
|
|
|
4587
4611
|
total_spent: number;
|
|
4588
4612
|
}[];
|
|
4589
4613
|
};
|
|
4614
|
+
get_supporters_with_profiles: {
|
|
4615
|
+
Args: {
|
|
4616
|
+
p_from_date?: string;
|
|
4617
|
+
p_limit?: number;
|
|
4618
|
+
p_offset?: number;
|
|
4619
|
+
p_search?: string;
|
|
4620
|
+
p_to_date?: string;
|
|
4621
|
+
p_type?: string;
|
|
4622
|
+
};
|
|
4623
|
+
Returns: {
|
|
4624
|
+
conversation_id: string;
|
|
4625
|
+
created_at: string;
|
|
4626
|
+
creator_id: string;
|
|
4627
|
+
first_supported_at: string;
|
|
4628
|
+
id: string;
|
|
4629
|
+
identity_hash: string;
|
|
4630
|
+
is_monthly: boolean;
|
|
4631
|
+
last_supported_at: string;
|
|
4632
|
+
last_supported_service: string;
|
|
4633
|
+
metadata: Json;
|
|
4634
|
+
name: string;
|
|
4635
|
+
profile_avatar_url: string;
|
|
4636
|
+
profile_display_name: string;
|
|
4637
|
+
profile_id: string;
|
|
4638
|
+
profile_username: string;
|
|
4639
|
+
social_platform: Database["public"]["Enums"]["supporter_platform_enum"];
|
|
4640
|
+
support_count: number;
|
|
4641
|
+
total_amount: number;
|
|
4642
|
+
total_count: number;
|
|
4643
|
+
updated_at: string;
|
|
4644
|
+
user_profile_id: string;
|
|
4645
|
+
}[];
|
|
4646
|
+
};
|
|
4590
4647
|
get_total_supports_count: {
|
|
4591
4648
|
Args: { p_creator_id: string; p_from_date: string; p_to_date: string };
|
|
4592
4649
|
Returns: number;
|
package/src/vite-env.d.ts
CHANGED
|
@@ -1 +1,36 @@
|
|
|
1
1
|
/// <reference types="vite/client" />
|
|
2
|
+
|
|
3
|
+
declare module "virtual:supabase-schema" {
|
|
4
|
+
export type ColumnInfo = {
|
|
5
|
+
name: string;
|
|
6
|
+
type: string;
|
|
7
|
+
rawType: string;
|
|
8
|
+
nullable: boolean;
|
|
9
|
+
enumName: string | null;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type RelationshipInfo = {
|
|
13
|
+
foreignKeyName: string;
|
|
14
|
+
column: string;
|
|
15
|
+
referencedTable: string;
|
|
16
|
+
referencedColumn: string;
|
|
17
|
+
isOneToOne: boolean;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type TableInfo = {
|
|
21
|
+
columns: ColumnInfo[];
|
|
22
|
+
relationships: RelationshipInfo[];
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type ViewInfo = {
|
|
26
|
+
columns: ColumnInfo[];
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type Schema = {
|
|
30
|
+
tables: Record<string, TableInfo>;
|
|
31
|
+
views: Record<string, ViewInfo>;
|
|
32
|
+
enums: Record<string, string[]>;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const schema: Schema;
|
|
36
|
+
}
|