supascan 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/.bun-version +1 -0
  2. package/.github/workflows/release-github.yml +70 -0
  3. package/.github/workflows/release-npm.yml +45 -0
  4. package/.github/workflows/tests.yml +36 -0
  5. package/LICENCE +22 -0
  6. package/README.md +115 -0
  7. package/apps/cli/build.ts +37 -0
  8. package/apps/cli/package.json +29 -0
  9. package/apps/cli/src/commands/analyze.ts +213 -0
  10. package/apps/cli/src/commands/dump.ts +68 -0
  11. package/apps/cli/src/commands/rpc.ts +67 -0
  12. package/apps/cli/src/context.ts +96 -0
  13. package/apps/cli/src/embedded-report.ts +1 -0
  14. package/apps/cli/src/formatters/console.ts +39 -0
  15. package/apps/cli/src/formatters/events.ts +95 -0
  16. package/apps/cli/src/index.ts +105 -0
  17. package/apps/cli/src/types.ts +9 -0
  18. package/apps/cli/src/utils/args.ts +46 -0
  19. package/apps/cli/src/utils/browser.ts +29 -0
  20. package/apps/cli/src/utils/files.ts +12 -0
  21. package/apps/cli/src/version.ts +3 -0
  22. package/apps/web/build.ts +68 -0
  23. package/apps/web/dev.ts +5 -0
  24. package/apps/web/index.html +75 -0
  25. package/apps/web/package.json +23 -0
  26. package/apps/web/src/App.tsx +129 -0
  27. package/apps/web/src/components/QueryBuilder.tsx +174 -0
  28. package/apps/web/src/components/QueryWindow.tsx +133 -0
  29. package/apps/web/src/components/RPCExecutor.tsx +176 -0
  30. package/apps/web/src/components/SchemaBrowser.tsx +269 -0
  31. package/apps/web/src/components/SmartTable.tsx +129 -0
  32. package/apps/web/src/components/TargetConfig.tsx +130 -0
  33. package/apps/web/src/components/TargetSummary.tsx +105 -0
  34. package/apps/web/src/hooks/useAnalysis.ts +54 -0
  35. package/apps/web/src/hooks/useNotification.ts +28 -0
  36. package/apps/web/src/hooks/useRPC.ts +53 -0
  37. package/apps/web/src/hooks/useSupabase.ts +46 -0
  38. package/apps/web/src/hooks/useTableQuery.ts +148 -0
  39. package/apps/web/src/index.tsx +18 -0
  40. package/apps/web/src/types.ts +16 -0
  41. package/apps/web/src/utils/hash.ts +27 -0
  42. package/context.test.ts +93 -0
  43. package/package.json +48 -0
  44. package/package.publish.json +18 -0
  45. package/packages/core/package.json +22 -0
  46. package/packages/core/src/analyzer.ts +212 -0
  47. package/packages/core/src/extractor.ts +233 -0
  48. package/packages/core/src/index.ts +9 -0
  49. package/packages/core/src/supabase.ts +316 -0
  50. package/packages/core/src/types/analyzer.types.ts +72 -0
  51. package/packages/core/src/types/event.types.ts +4 -0
  52. package/packages/core/src/types/events.types.ts +5 -0
  53. package/packages/core/src/types/extractor.types.ts +54 -0
  54. package/packages/core/src/types/result.types.ts +17 -0
  55. package/packages/core/src/types/supabase.types.ts +98 -0
  56. package/tsconfig.json +23 -0
  57. package/turbo.json +19 -0
  58. package/utils.test.ts +68 -0
  59. package/version.ts +3 -0
@@ -0,0 +1,174 @@
1
+ import { useState } from "react";
2
+ import { useNotification } from "../hooks/useNotification";
3
+ import { useTableQuery } from "../hooks/useTableQuery";
4
+ import type { SupabaseClient } from "../types";
5
+ import { SmartTable } from "./SmartTable";
6
+
7
+ interface QueryBuilderProps {
8
+ client: SupabaseClient | null;
9
+ schema: string;
10
+ table: string;
11
+ }
12
+
13
+ type Operation = "select" | "insert" | "update" | "delete";
14
+
15
+ export function QueryBuilder({ client, schema, table }: QueryBuilderProps) {
16
+ const [operation, setOperation] = useState<Operation>("select");
17
+ const [columns, setColumns] = useState("*");
18
+ const [limit, setLimit] = useState("10");
19
+ const [orderBy, setOrderBy] = useState("");
20
+ const [jsonData, setJsonData] = useState("{}");
21
+ const [filter, setFilter] = useState("");
22
+
23
+ const { state, execute } = useTableQuery(client, schema, table);
24
+ const notify = useNotification();
25
+
26
+ const handleExecute = async () => {
27
+ try {
28
+ const parsedLimit = parseInt(limit);
29
+ const [orderCol, orderDir] = orderBy.trim().split(" ");
30
+
31
+ let data: Record<string, unknown> | undefined;
32
+ if (operation !== "select") {
33
+ try {
34
+ data = JSON.parse(jsonData);
35
+ } catch (e) {
36
+ notify("Invalid JSON data", "error");
37
+ return;
38
+ }
39
+ }
40
+
41
+ await execute({
42
+ operation,
43
+ columns: columns.trim() || "*",
44
+ limit: parsedLimit > 0 ? parsedLimit : undefined,
45
+ orderBy: orderCol || undefined,
46
+ orderDir: orderDir === "desc" ? "desc" : "asc",
47
+ data,
48
+ filter: filter.trim() || undefined,
49
+ });
50
+
51
+ notify("Query executed successfully", "success");
52
+ } catch (err) {
53
+ const message = err instanceof Error ? err.message : String(err);
54
+ notify(message, "error");
55
+ }
56
+ };
57
+
58
+ return (
59
+ <div className="h-full flex flex-col">
60
+ <div className="p-4 space-y-3 border-b border-gray-200">
61
+ <select
62
+ value={operation}
63
+ onChange={(e) => setOperation(e.target.value as Operation)}
64
+ className="w-full p-2 text-sm border border-gray-300 rounded font-mono bg-white focus:ring-2 focus:ring-supabase-green focus:border-supabase-green"
65
+ >
66
+ <option value="select">SELECT</option>
67
+ <option value="insert">INSERT</option>
68
+ <option value="update">UPDATE</option>
69
+ <option value="delete">DELETE</option>
70
+ </select>
71
+
72
+ {operation === "select" && (
73
+ <>
74
+ <input
75
+ type="text"
76
+ value={columns}
77
+ onChange={(e) => setColumns(e.target.value)}
78
+ placeholder="* or column1, column2"
79
+ className="w-full p-2 text-sm border border-gray-300 rounded font-mono focus:ring-2 focus:ring-supabase-green focus:border-supabase-green"
80
+ />
81
+ <div className="grid grid-cols-2 gap-2">
82
+ <input
83
+ type="number"
84
+ value={limit}
85
+ onChange={(e) => setLimit(e.target.value)}
86
+ placeholder="Limit"
87
+ className="p-2 text-sm border border-gray-300 rounded font-mono focus:ring-2 focus:ring-supabase-green focus:border-supabase-green"
88
+ />
89
+ <input
90
+ type="text"
91
+ value={orderBy}
92
+ onChange={(e) => setOrderBy(e.target.value)}
93
+ placeholder="column asc/desc"
94
+ className="p-2 text-sm border border-gray-300 rounded font-mono focus:ring-2 focus:ring-supabase-green focus:border-supabase-green"
95
+ />
96
+ </div>
97
+ </>
98
+ )}
99
+
100
+ {operation !== "select" && (
101
+ <>
102
+ <textarea
103
+ value={jsonData}
104
+ onChange={(e) => setJsonData(e.target.value)}
105
+ placeholder={
106
+ operation === "insert"
107
+ ? '{"column": "value"}'
108
+ : '{"column": "new value"}'
109
+ }
110
+ rows={operation === "update" ? 2 : 3}
111
+ className="w-full p-2 text-sm border border-gray-300 rounded font-mono focus:ring-2 focus:ring-supabase-green focus:border-supabase-green"
112
+ />
113
+ {(operation === "update" || operation === "delete") && (
114
+ <input
115
+ type="text"
116
+ value={filter}
117
+ onChange={(e) => setFilter(e.target.value)}
118
+ placeholder="id = 1"
119
+ className="w-full p-2 text-sm border border-gray-300 rounded font-mono focus:ring-2 focus:ring-supabase-green focus:border-supabase-green"
120
+ />
121
+ )}
122
+ </>
123
+ )}
124
+
125
+ <button
126
+ onClick={handleExecute}
127
+ disabled={state.status === "loading" || !client}
128
+ className="w-full px-4 py-2 bg-slate-700 text-white rounded text-sm font-mono hover:bg-slate-800 disabled:opacity-50 transition-colors"
129
+ >
130
+ {state.status === "loading" ? "Executing..." : "Execute Query"}
131
+ </button>
132
+ </div>
133
+
134
+ <div className="flex-1 flex flex-col border-t border-gray-200 min-h-0">
135
+ <div className="px-4 py-2 bg-gray-50 border-b border-gray-200">
136
+ <h4 className="text-xs font-semibold text-gray-700 font-mono">
137
+ Results
138
+ </h4>
139
+ </div>
140
+ <div className="flex-1 overflow-auto">
141
+ {state.status === "loading" && (
142
+ <div className="p-8 text-center">
143
+ <div className="inline-flex items-center gap-3 text-slate-600 font-mono text-sm">
144
+ <div className="animate-spin rounded-full h-4 w-4 border-2 border-slate-300 border-t-slate-600"></div>
145
+ Executing query...
146
+ </div>
147
+ </div>
148
+ )}
149
+
150
+ {state.status === "error" && (
151
+ <div className="p-4 bg-red-50 border-b border-red-200 text-xs text-red-700 font-mono">
152
+ Error: {state.error.message}
153
+ </div>
154
+ )}
155
+
156
+ {state.status === "success" &&
157
+ (state.data.length > 0 ? (
158
+ <SmartTable data={state.data} />
159
+ ) : (
160
+ <div className="p-6 text-center text-gray-400 text-sm font-mono">
161
+ No data returned
162
+ </div>
163
+ ))}
164
+
165
+ {state.status === "idle" && (
166
+ <div className="p-6 text-center text-gray-400 text-sm font-mono">
167
+ Query results will appear here...
168
+ </div>
169
+ )}
170
+ </div>
171
+ </div>
172
+ </div>
173
+ );
174
+ }
@@ -0,0 +1,133 @@
1
+ import { useEffect, useRef, useState } from "react";
2
+
3
+ interface QueryWindowProps {
4
+ id: string;
5
+ title: string;
6
+ onClose: () => void;
7
+ onFocus: () => void;
8
+ zIndex: number;
9
+ initialPosition?: { x: number; y: number };
10
+ children: React.ReactNode;
11
+ }
12
+
13
+ export function QueryWindow({
14
+ id,
15
+ title,
16
+ onClose,
17
+ onFocus,
18
+ zIndex,
19
+ initialPosition = { x: 100, y: 100 },
20
+ children,
21
+ }: QueryWindowProps) {
22
+ const [position, setPosition] = useState(initialPosition);
23
+ const [size, setSize] = useState({ width: 800, height: 600 });
24
+ const [isDragging, setIsDragging] = useState(false);
25
+ const [isResizing, setIsResizing] = useState(false);
26
+ const [dragStart, setDragStart] = useState({ x: 0, y: 0 });
27
+ const windowRef = useRef<HTMLDivElement>(null);
28
+
29
+ useEffect(() => {
30
+ const handleMouseMove = (e: MouseEvent) => {
31
+ if (isDragging) {
32
+ setPosition({
33
+ x: Math.max(0, e.clientX - dragStart.x),
34
+ y: Math.max(0, e.clientY - dragStart.y),
35
+ });
36
+ } else if (isResizing) {
37
+ const newWidth = Math.max(400, e.clientX - position.x);
38
+ const newHeight = Math.max(300, e.clientY - position.y);
39
+ setSize({ width: newWidth, height: newHeight });
40
+ }
41
+ };
42
+
43
+ const handleMouseUp = () => {
44
+ setIsDragging(false);
45
+ setIsResizing(false);
46
+ };
47
+
48
+ if (isDragging || isResizing) {
49
+ document.addEventListener("mousemove", handleMouseMove);
50
+ document.addEventListener("mouseup", handleMouseUp);
51
+ return () => {
52
+ document.removeEventListener("mousemove", handleMouseMove);
53
+ document.removeEventListener("mouseup", handleMouseUp);
54
+ };
55
+ }
56
+ }, [isDragging, isResizing, dragStart, position.x, position.y]);
57
+
58
+ const handleHeaderMouseDown = (e: React.MouseEvent) => {
59
+ if ((e.target as HTMLElement).closest(".window-controls")) return;
60
+ onFocus();
61
+ setIsDragging(true);
62
+ setDragStart({
63
+ x: e.clientX - position.x,
64
+ y: e.clientY - position.y,
65
+ });
66
+ };
67
+
68
+ const handleResizeMouseDown = (e: React.MouseEvent) => {
69
+ e.stopPropagation();
70
+ onFocus();
71
+ setIsResizing(true);
72
+ };
73
+
74
+ const handleWindowClick = () => {
75
+ onFocus();
76
+ };
77
+
78
+ return (
79
+ <div
80
+ ref={windowRef}
81
+ className="fixed bg-white rounded-lg shadow-2xl border border-gray-300 flex flex-col overflow-hidden"
82
+ style={{
83
+ left: `${position.x}px`,
84
+ top: `${position.y}px`,
85
+ width: `${size.width}px`,
86
+ height: `${size.height}px`,
87
+ zIndex: 1000 + zIndex,
88
+ }}
89
+ onClick={handleWindowClick}
90
+ >
91
+ <div
92
+ className="bg-gradient-to-r from-slate-700 to-slate-600 px-4 py-3 flex items-center justify-between cursor-move select-none"
93
+ onMouseDown={handleHeaderMouseDown}
94
+ >
95
+ <div className="flex items-center gap-2">
96
+ <div className="flex gap-1.5 window-controls">
97
+ <button
98
+ onClick={onClose}
99
+ className="w-3 h-3 rounded-full bg-red-500 hover:bg-red-600 transition-colors"
100
+ title="Close"
101
+ />
102
+ <button
103
+ className="w-3 h-3 rounded-full bg-yellow-500 hover:bg-yellow-600 transition-colors opacity-50 cursor-default"
104
+ title="Minimize"
105
+ />
106
+ <button
107
+ className="w-3 h-3 rounded-full bg-green-500 hover:bg-green-600 transition-colors opacity-50 cursor-default"
108
+ title="Maximize"
109
+ />
110
+ </div>
111
+ <h3 className="text-sm font-semibold text-white font-mono ml-2 truncate">
112
+ {title}
113
+ </h3>
114
+ </div>
115
+ </div>
116
+
117
+ <div className="flex-1 overflow-auto">{children}</div>
118
+
119
+ <div
120
+ className="absolute bottom-0 right-0 w-4 h-4 cursor-se-resize"
121
+ onMouseDown={handleResizeMouseDown}
122
+ >
123
+ <svg
124
+ className="w-4 h-4 text-gray-400"
125
+ fill="currentColor"
126
+ viewBox="0 0 16 16"
127
+ >
128
+ <path d="M16 16V14l-2-2v4h4zm0-6V8l-4-4v4l4 4zm0-6V2l-6-2v4l6 2z" />
129
+ </svg>
130
+ </div>
131
+ </div>
132
+ );
133
+ }
@@ -0,0 +1,176 @@
1
+ import type { RPCFunction } from "@supascan/core";
2
+ import { useState } from "react";
3
+ import { useNotification } from "../hooks/useNotification";
4
+ import { useRPC } from "../hooks/useRPC";
5
+ import type { SupabaseClient } from "../types";
6
+ import { SmartTable } from "./SmartTable";
7
+
8
+ interface RPCExecutorProps {
9
+ client: SupabaseClient | null;
10
+ schema: string;
11
+ rpc: RPCFunction;
12
+ }
13
+
14
+ function parseParamValue(value: string, type: string): unknown {
15
+ if (value === "") return undefined;
16
+ if (type === "number" || type === "integer") return parseFloat(value);
17
+ if (type === "boolean") return value === "true";
18
+ if (type === "json" || type === "jsonb") return JSON.parse(value);
19
+ return value;
20
+ }
21
+
22
+ export function RPCExecutor({ client, schema, rpc }: RPCExecutorProps) {
23
+ const [params, setParams] = useState<Record<string, string>>({});
24
+ const { state, execute } = useRPC(client, schema, rpc.name);
25
+ const notify = useNotification();
26
+
27
+ const handleParamChange = (name: string, value: string) => {
28
+ if (value === "") {
29
+ const { [name]: _, ...rest } = params;
30
+ setParams(rest);
31
+ } else {
32
+ setParams({ ...params, [name]: value });
33
+ }
34
+ };
35
+
36
+ const handleExecute = async () => {
37
+ try {
38
+ const parsedParams = Object.entries(params).reduce(
39
+ (acc, [key, value]) => {
40
+ const param = rpc.parameters.find((p) => p.name === key);
41
+ if (param) {
42
+ try {
43
+ acc[key] = parseParamValue(value, param.type);
44
+ } catch (e) {
45
+ notify(`Invalid value for parameter "${key}"`, "error");
46
+ throw e;
47
+ }
48
+ }
49
+ return acc;
50
+ },
51
+ {} as Record<string, unknown>,
52
+ );
53
+
54
+ await execute(parsedParams);
55
+ notify(`RPC ${rpc.name} executed successfully`, "success");
56
+ } catch (err) {
57
+ const message = err instanceof Error ? err.message : String(err);
58
+ notify(message, "error");
59
+ }
60
+ };
61
+
62
+ const result = state.status === "success" ? state.data : null;
63
+
64
+ return (
65
+ <div className="h-full flex flex-col">
66
+ <div className="p-4 space-y-3 border-b border-gray-200">
67
+ {rpc.parameters.length > 0 && (
68
+ <div className="space-y-2">
69
+ {rpc.parameters.map((param) => (
70
+ <div key={param.name}>
71
+ <label className="block text-xs font-medium text-gray-600 mb-1 font-mono">
72
+ {param.name} ({param.type})
73
+ {param.required && <span className="text-red-600"> *</span>}
74
+ </label>
75
+ {param.type === "boolean" ? (
76
+ <select
77
+ value={params[param.name] || ""}
78
+ onChange={(e) =>
79
+ handleParamChange(param.name, e.target.value)
80
+ }
81
+ className="w-full p-2 text-sm border border-gray-300 rounded font-mono bg-white focus:ring-2 focus:ring-supabase-green focus:border-supabase-green"
82
+ >
83
+ <option value="">Select...</option>
84
+ <option value="true">true</option>
85
+ <option value="false">false</option>
86
+ </select>
87
+ ) : param.type === "json" || param.type === "jsonb" ? (
88
+ <textarea
89
+ value={params[param.name] || ""}
90
+ onChange={(e) =>
91
+ handleParamChange(param.name, e.target.value)
92
+ }
93
+ placeholder="{}"
94
+ rows={2}
95
+ className="w-full p-2 text-sm border border-gray-300 rounded font-mono focus:ring-2 focus:ring-supabase-green focus:border-supabase-green"
96
+ />
97
+ ) : (
98
+ <input
99
+ type={
100
+ param.type === "number" || param.type === "integer"
101
+ ? "number"
102
+ : "text"
103
+ }
104
+ value={params[param.name] || ""}
105
+ onChange={(e) =>
106
+ handleParamChange(param.name, e.target.value)
107
+ }
108
+ placeholder={param.description || `Enter ${param.type}`}
109
+ className="w-full p-2 text-sm border border-gray-300 rounded font-mono focus:ring-2 focus:ring-supabase-green focus:border-supabase-green"
110
+ />
111
+ )}
112
+ </div>
113
+ ))}
114
+ </div>
115
+ )}
116
+
117
+ <button
118
+ onClick={handleExecute}
119
+ disabled={state.status === "loading" || !client}
120
+ className="w-full px-4 py-2 bg-slate-700 text-white rounded text-sm font-mono hover:bg-slate-800 transition-colors disabled:opacity-50"
121
+ >
122
+ {state.status === "loading" ? "Executing..." : "Execute RPC"}
123
+ </button>
124
+ </div>
125
+
126
+ <div className="flex-1 flex flex-col border-t border-gray-200 min-h-0">
127
+ <div className="px-4 py-2 bg-gray-50 border-b border-gray-200">
128
+ <h4 className="text-xs font-semibold text-gray-700 font-mono">
129
+ Results
130
+ </h4>
131
+ </div>
132
+ <div className="flex-1 overflow-auto">
133
+ {state.status === "loading" && (
134
+ <div className="p-8 text-center">
135
+ <div className="inline-flex items-center gap-3 text-slate-600 font-mono text-sm">
136
+ <div className="animate-spin rounded-full h-4 w-4 border-2 border-slate-300 border-t-slate-600"></div>
137
+ Executing RPC...
138
+ </div>
139
+ </div>
140
+ )}
141
+
142
+ {state.status === "error" && (
143
+ <div className="p-4 bg-red-50 border-b border-red-200 text-xs text-red-700 font-mono">
144
+ Error: {state.error.message}
145
+ </div>
146
+ )}
147
+
148
+ {state.status === "success" &&
149
+ (Array.isArray(result) && result.length > 0 ? (
150
+ <SmartTable data={result as Record<string, unknown>[]} />
151
+ ) : Array.isArray(result) && result.length === 0 ? (
152
+ <div className="p-6 text-center text-gray-400 text-sm font-mono">
153
+ No data returned
154
+ </div>
155
+ ) : typeof result === "object" && result !== null ? (
156
+ <div className="p-4">
157
+ <pre className="text-xs font-mono whitespace-pre-wrap">
158
+ {JSON.stringify(result, null, 2)}
159
+ </pre>
160
+ </div>
161
+ ) : (
162
+ <div className="p-4">
163
+ <div className="text-xs font-mono">{String(result)}</div>
164
+ </div>
165
+ ))}
166
+
167
+ {state.status === "idle" && (
168
+ <div className="p-6 text-center text-gray-400 text-sm font-mono">
169
+ RPC results will appear here...
170
+ </div>
171
+ )}
172
+ </div>
173
+ </div>
174
+ </div>
175
+ );
176
+ }