@hobenakicoffee/libraries 8.1.0 → 8.3.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 +114 -2
- 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
|
@@ -1333,7 +1333,7 @@ export type Database = {
|
|
|
1333
1333
|
},
|
|
1334
1334
|
];
|
|
1335
1335
|
};
|
|
1336
|
-
|
|
1336
|
+
messages_2026_06: {
|
|
1337
1337
|
Row: {
|
|
1338
1338
|
content: string;
|
|
1339
1339
|
conversation_id: string;
|
|
@@ -1357,7 +1357,7 @@ export type Database = {
|
|
|
1357
1357
|
};
|
|
1358
1358
|
Relationships: [];
|
|
1359
1359
|
};
|
|
1360
|
-
|
|
1360
|
+
messages_2026_08: {
|
|
1361
1361
|
Row: {
|
|
1362
1362
|
content: string;
|
|
1363
1363
|
conversation_id: string;
|
|
@@ -1818,6 +1818,95 @@ export type Database = {
|
|
|
1818
1818
|
},
|
|
1819
1819
|
];
|
|
1820
1820
|
};
|
|
1821
|
+
payment_sessions: {
|
|
1822
|
+
Row: {
|
|
1823
|
+
amount: number;
|
|
1824
|
+
bank_tran_id: string | null;
|
|
1825
|
+
created_at: string;
|
|
1826
|
+
creator_profile_id: string | null;
|
|
1827
|
+
currency: string;
|
|
1828
|
+
error: string | null;
|
|
1829
|
+
expires_at: string;
|
|
1830
|
+
gateway_response: Json | null;
|
|
1831
|
+
id: string;
|
|
1832
|
+
payload: Json;
|
|
1833
|
+
rpc_result: Json | null;
|
|
1834
|
+
service_type: string;
|
|
1835
|
+
status: Database["public"]["Enums"]["payment_session_status_enum"];
|
|
1836
|
+
supporter_profile_id: string | null;
|
|
1837
|
+
tran_id: string;
|
|
1838
|
+
updated_at: string;
|
|
1839
|
+
val_id: string | null;
|
|
1840
|
+
};
|
|
1841
|
+
Insert: {
|
|
1842
|
+
amount: number;
|
|
1843
|
+
bank_tran_id?: string | null;
|
|
1844
|
+
created_at?: string;
|
|
1845
|
+
creator_profile_id?: string | null;
|
|
1846
|
+
currency?: string;
|
|
1847
|
+
error?: string | null;
|
|
1848
|
+
expires_at?: string;
|
|
1849
|
+
gateway_response?: Json | null;
|
|
1850
|
+
id?: string;
|
|
1851
|
+
payload: Json;
|
|
1852
|
+
rpc_result?: Json | null;
|
|
1853
|
+
service_type: string;
|
|
1854
|
+
status?: Database["public"]["Enums"]["payment_session_status_enum"];
|
|
1855
|
+
supporter_profile_id?: string | null;
|
|
1856
|
+
tran_id: string;
|
|
1857
|
+
updated_at?: string;
|
|
1858
|
+
val_id?: string | null;
|
|
1859
|
+
};
|
|
1860
|
+
Update: {
|
|
1861
|
+
amount?: number;
|
|
1862
|
+
bank_tran_id?: string | null;
|
|
1863
|
+
created_at?: string;
|
|
1864
|
+
creator_profile_id?: string | null;
|
|
1865
|
+
currency?: string;
|
|
1866
|
+
error?: string | null;
|
|
1867
|
+
expires_at?: string;
|
|
1868
|
+
gateway_response?: Json | null;
|
|
1869
|
+
id?: string;
|
|
1870
|
+
payload?: Json;
|
|
1871
|
+
rpc_result?: Json | null;
|
|
1872
|
+
service_type?: string;
|
|
1873
|
+
status?: Database["public"]["Enums"]["payment_session_status_enum"];
|
|
1874
|
+
supporter_profile_id?: string | null;
|
|
1875
|
+
tran_id?: string;
|
|
1876
|
+
updated_at?: string;
|
|
1877
|
+
val_id?: string | null;
|
|
1878
|
+
};
|
|
1879
|
+
Relationships: [
|
|
1880
|
+
{
|
|
1881
|
+
foreignKeyName: "payment_sessions_creator_profile_id_fkey";
|
|
1882
|
+
columns: ["creator_profile_id"];
|
|
1883
|
+
isOneToOne: false;
|
|
1884
|
+
referencedRelation: "profiles";
|
|
1885
|
+
referencedColumns: ["id"];
|
|
1886
|
+
},
|
|
1887
|
+
{
|
|
1888
|
+
foreignKeyName: "payment_sessions_creator_profile_id_fkey";
|
|
1889
|
+
columns: ["creator_profile_id"];
|
|
1890
|
+
isOneToOne: false;
|
|
1891
|
+
referencedRelation: "public_profiles";
|
|
1892
|
+
referencedColumns: ["id"];
|
|
1893
|
+
},
|
|
1894
|
+
{
|
|
1895
|
+
foreignKeyName: "payment_sessions_supporter_profile_id_fkey";
|
|
1896
|
+
columns: ["supporter_profile_id"];
|
|
1897
|
+
isOneToOne: false;
|
|
1898
|
+
referencedRelation: "profiles";
|
|
1899
|
+
referencedColumns: ["id"];
|
|
1900
|
+
},
|
|
1901
|
+
{
|
|
1902
|
+
foreignKeyName: "payment_sessions_supporter_profile_id_fkey";
|
|
1903
|
+
columns: ["supporter_profile_id"];
|
|
1904
|
+
isOneToOne: false;
|
|
1905
|
+
referencedRelation: "public_profiles";
|
|
1906
|
+
referencedColumns: ["id"];
|
|
1907
|
+
},
|
|
1908
|
+
];
|
|
1909
|
+
};
|
|
1821
1910
|
payout_methods: {
|
|
1822
1911
|
Row: {
|
|
1823
1912
|
created_at: string;
|
|
@@ -4184,6 +4273,7 @@ export type Database = {
|
|
|
4184
4273
|
dismiss_all_activities: { Args: never; Returns: undefined };
|
|
4185
4274
|
dispatch_pending_email_notifications: { Args: never; Returns: undefined };
|
|
4186
4275
|
drop_old_partitions: { Args: never; Returns: undefined };
|
|
4276
|
+
expire_stale_payment_sessions: { Args: never; Returns: number };
|
|
4187
4277
|
flag_transaction_disputed: {
|
|
4188
4278
|
Args: { p_is_disputed?: boolean; p_transaction_id: string };
|
|
4189
4279
|
Returns: Json;
|
|
@@ -4418,6 +4508,15 @@ export type Database = {
|
|
|
4418
4508
|
Args: never;
|
|
4419
4509
|
Returns: Database["public"]["Enums"]["user_role"];
|
|
4420
4510
|
};
|
|
4511
|
+
get_payment_session_status: {
|
|
4512
|
+
Args: { p_tran_id: string };
|
|
4513
|
+
Returns: {
|
|
4514
|
+
amount: number;
|
|
4515
|
+
error: string;
|
|
4516
|
+
service_type: string;
|
|
4517
|
+
status: Database["public"]["Enums"]["payment_session_status_enum"];
|
|
4518
|
+
}[];
|
|
4519
|
+
};
|
|
4421
4520
|
get_platform_setting: { Args: { p_key: string }; Returns: number };
|
|
4422
4521
|
get_platform_setting_jsonb: { Args: { p_key: string }; Returns: Json };
|
|
4423
4522
|
get_popular_content: {
|
|
@@ -5218,6 +5317,12 @@ export type Database = {
|
|
|
5218
5317
|
| "expired"
|
|
5219
5318
|
| "paused"
|
|
5220
5319
|
| "past_due";
|
|
5320
|
+
payment_session_status_enum:
|
|
5321
|
+
| "pending"
|
|
5322
|
+
| "completed"
|
|
5323
|
+
| "failed"
|
|
5324
|
+
| "cancelled"
|
|
5325
|
+
| "expired";
|
|
5221
5326
|
payment_status_enum:
|
|
5222
5327
|
| "pending"
|
|
5223
5328
|
| "processing"
|
|
@@ -5512,6 +5617,13 @@ export const Constants = {
|
|
|
5512
5617
|
"paused",
|
|
5513
5618
|
"past_due",
|
|
5514
5619
|
],
|
|
5620
|
+
payment_session_status_enum: [
|
|
5621
|
+
"pending",
|
|
5622
|
+
"completed",
|
|
5623
|
+
"failed",
|
|
5624
|
+
"cancelled",
|
|
5625
|
+
"expired",
|
|
5626
|
+
],
|
|
5515
5627
|
payment_status_enum: [
|
|
5516
5628
|
"pending",
|
|
5517
5629
|
"processing",
|
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
|
+
}
|