@rebasepro/studio 0.17.3 → 0.18.1
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/LICENSE +0 -1
- package/README.md +12 -4
- package/dist/{ApiKeysView-DiCurTEU.js → ApiKeysView-EIvegrUk.js} +54 -39
- package/dist/ApiKeysView-EIvegrUk.js.map +1 -0
- package/dist/{BackupsView-DNS6LdVg.js → BackupsView-DVkKzaPR.js} +29 -31
- package/dist/BackupsView-DVkKzaPR.js.map +1 -0
- package/dist/{BranchesView-CM3B-ER3.js → BranchesView-Df8hQ8wE.js} +85 -24
- package/dist/BranchesView-Df8hQ8wE.js.map +1 -0
- package/dist/{CronJobsView-BHtJZeJZ.js → CronJobsView-CLOGjb-L.js} +32 -27
- package/dist/CronJobsView-CLOGjb-L.js.map +1 -0
- package/dist/{RLSEditor-CBX4uD66.js → RLSEditor-Sa1D34ny.js} +724 -615
- package/dist/RLSEditor-Sa1D34ny.js.map +1 -0
- package/dist/{SQLEditor-ByecwX8B.js → SQLEditor-BfNA8xM3.js} +9 -4
- package/dist/{SQLEditor-ByecwX8B.js.map → SQLEditor-BfNA8xM3.js.map} +1 -1
- package/dist/{SchemaVisualizer-D6tMZgeC.js → SchemaVisualizer-DTDMYL6o.js} +84 -110
- package/dist/SchemaVisualizer-DTDMYL6o.js.map +1 -0
- package/dist/{StorageView-BGDeC5GI.js → StorageView-Ed9Q_VlB.js} +5 -32
- package/dist/StorageView-Ed9Q_VlB.js.map +1 -0
- package/dist/components/RLSEditor/saveRules.d.ts +38 -0
- package/dist/components/RebaseStudio.d.ts +1 -1
- package/dist/components/SchemaVisualizer/schema-visualizer.utils.d.ts +5 -2
- package/dist/components/SchemaVisualizer/useSchemaGraph.d.ts +0 -4
- package/dist/components/load-failure-view.d.ts +17 -0
- package/dist/components/load-failure.d.ts +47 -0
- package/dist/index.es.js +56 -36
- package/dist/index.es.js.map +1 -1
- package/dist/load-failure-C6OMJSGC.js +31 -0
- package/dist/load-failure-C6OMJSGC.js.map +1 -0
- package/dist/load-failure-view-CBTB_h-h.js +48 -0
- package/dist/load-failure-view-CBTB_h-h.js.map +1 -0
- package/package.json +36 -21
- package/dist/ApiKeysView-DiCurTEU.js.map +0 -1
- package/dist/BackupsView-DNS6LdVg.js.map +0 -1
- package/dist/BranchesView-CM3B-ER3.js.map +0 -1
- package/dist/CronJobsView-BHtJZeJZ.js.map +0 -1
- package/dist/RLSEditor-CBX4uD66.js.map +0 -1
- package/dist/SchemaVisualizer-D6tMZgeC.js.map +0 -1
- package/dist/StorageView-BGDeC5GI.js.map +0 -1
- package/dist/components/StorageView/storage-failure.d.ts +0 -34
- package/src/components/ApiExplorer/ApiExplorer.tsx +0 -299
- package/src/components/ApiExplorer/EndpointDetail.tsx +0 -279
- package/src/components/ApiExplorer/TryItPanel.tsx +0 -499
- package/src/components/ApiExplorer/parseSpec.ts +0 -104
- package/src/components/ApiExplorer/types.ts +0 -84
- package/src/components/ApiKeys/ApiKeysView.tsx +0 -479
- package/src/components/ApiKeys/CreateApiKeyDialog.tsx +0 -711
- package/src/components/ApiKeys/permissions.ts +0 -145
- package/src/components/AuthSimulationSelector.tsx +0 -72
- package/src/components/Backups/BackupsView.tsx +0 -185
- package/src/components/Branches/BranchesView.tsx +0 -386
- package/src/components/CronJobs/CronJobsView.tsx +0 -413
- package/src/components/JSEditor/JSEditor.tsx +0 -1100
- package/src/components/JSEditor/JSEditorSidebar.tsx +0 -350
- package/src/components/JSEditor/JSMonacoEditor.tsx +0 -410
- package/src/components/LogsExplorer/LogsExplorer.tsx +0 -273
- package/src/components/LogsExplorer/useLogTail.ts +0 -356
- package/src/components/RLSEditor/PolicyEditor.tsx +0 -399
- package/src/components/RLSEditor/RLSEditor.tsx +0 -1128
- package/src/components/RLSEditor/index.ts +0 -1
- package/src/components/RLSEditor/policy-presets.ts +0 -176
- package/src/components/RLSEditor/saveSecurityRules.ts +0 -48
- package/src/components/RebaseStudio.tsx +0 -174
- package/src/components/SQLEditor/ExplainVisualizer.tsx +0 -128
- package/src/components/SQLEditor/MonacoEditor.tsx +0 -203
- package/src/components/SQLEditor/SQLEditor.tsx +0 -1518
- package/src/components/SQLEditor/SQLEditorSidebar.tsx +0 -173
- package/src/components/SQLEditor/SchemaBrowser.tsx +0 -168
- package/src/components/SQLEditor/sql_editor_types.ts +0 -18
- package/src/components/SchemaVisualizer/RelationEdge.tsx +0 -102
- package/src/components/SchemaVisualizer/SchemaVisualizer.tsx +0 -710
- package/src/components/SchemaVisualizer/TableNode.tsx +0 -257
- package/src/components/SchemaVisualizer/schema-visualizer.utils.ts +0 -140
- package/src/components/SchemaVisualizer/useLiveRls.ts +0 -86
- package/src/components/SchemaVisualizer/useSchemaGraph.ts +0 -426
- package/src/components/StorageView/StorageView.tsx +0 -1588
- package/src/components/StorageView/storage-failure.ts +0 -63
- package/src/components/StudioHomePage.tsx +0 -430
- package/src/index.ts +0 -31
- package/src/utils/parseSpec.test.ts +0 -309
- package/src/utils/sql_utils.test.ts +0 -265
- package/src/utils/sql_utils.ts +0 -291
|
@@ -1,257 +0,0 @@
|
|
|
1
|
-
import React, { memo, useMemo } from "react";
|
|
2
|
-
import { Handle, Position } from "@xyflow/react";
|
|
3
|
-
import type { NodeProps } from "@xyflow/react";
|
|
4
|
-
import { Typography, Chip, Tooltip, cls } from "@rebasepro/ui";
|
|
5
|
-
import { IconForView } from "@rebasepro/app";
|
|
6
|
-
import type { TableNodeData, ColumnInfo } from "./useSchemaGraph";
|
|
7
|
-
import { getColumnRowY, getHeaderHeight } from "./schema-visualizer.utils";
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Custom React Flow node that renders a database table as a card.
|
|
11
|
-
*
|
|
12
|
-
* Every PK and FK column renders handles on BOTH Left and Right sides.
|
|
13
|
-
* The edge builder picks which side to use based on relative node position.
|
|
14
|
-
*/
|
|
15
|
-
const TableNodeInner = ({ data, selected }: NodeProps) => {
|
|
16
|
-
const {
|
|
17
|
-
tableName,
|
|
18
|
-
collectionName,
|
|
19
|
-
columns,
|
|
20
|
-
isJunction,
|
|
21
|
-
rlsEnabled,
|
|
22
|
-
historyEnabled,
|
|
23
|
-
icon
|
|
24
|
-
} = data as TableNodeData;
|
|
25
|
-
|
|
26
|
-
// Build handles: every PK/FK column gets Left + Right handles
|
|
27
|
-
const handles = useMemo(() => {
|
|
28
|
-
const result: { id: string; type: "source" | "target"; position: Position; top: number }[] = [];
|
|
29
|
-
const cols = columns as ColumnInfo[];
|
|
30
|
-
const headerH = getHeaderHeight({
|
|
31
|
-
isJunction: Boolean(isJunction),
|
|
32
|
-
collectionName: collectionName as string,
|
|
33
|
-
tableName: tableName as string
|
|
34
|
-
});
|
|
35
|
-
const midY = cols.length > 0 ? getColumnRowY(Math.floor(cols.length / 2), headerH) : 30;
|
|
36
|
-
|
|
37
|
-
cols.forEach((col, idx) => {
|
|
38
|
-
const y = getColumnRowY(idx, headerH);
|
|
39
|
-
|
|
40
|
-
if (col.isForeignKey && !col.isPrimaryKey) {
|
|
41
|
-
// FK: source handles on both sides
|
|
42
|
-
result.push({ id: `source-${col.name}-right`,
|
|
43
|
-
type: "source",
|
|
44
|
-
position: Position.Right,
|
|
45
|
-
top: y });
|
|
46
|
-
result.push({ id: `source-${col.name}-left`,
|
|
47
|
-
type: "source",
|
|
48
|
-
position: Position.Left,
|
|
49
|
-
top: y });
|
|
50
|
-
}
|
|
51
|
-
if (col.isPrimaryKey) {
|
|
52
|
-
// PK: target + source handles on both sides
|
|
53
|
-
result.push({ id: `target-${col.name}-right`,
|
|
54
|
-
type: "target",
|
|
55
|
-
position: Position.Right,
|
|
56
|
-
top: y });
|
|
57
|
-
result.push({ id: `target-${col.name}-left`,
|
|
58
|
-
type: "target",
|
|
59
|
-
position: Position.Left,
|
|
60
|
-
top: y });
|
|
61
|
-
result.push({ id: `source-${col.name}-right`,
|
|
62
|
-
type: "source",
|
|
63
|
-
position: Position.Right,
|
|
64
|
-
top: y });
|
|
65
|
-
result.push({ id: `source-${col.name}-left`,
|
|
66
|
-
type: "source",
|
|
67
|
-
position: Position.Left,
|
|
68
|
-
top: y });
|
|
69
|
-
}
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
// Default handles on both sides
|
|
73
|
-
result.push({ id: "target-default-right",
|
|
74
|
-
type: "target",
|
|
75
|
-
position: Position.Right,
|
|
76
|
-
top: midY });
|
|
77
|
-
result.push({ id: "target-default-left",
|
|
78
|
-
type: "target",
|
|
79
|
-
position: Position.Left,
|
|
80
|
-
top: midY });
|
|
81
|
-
result.push({ id: "source-default-right",
|
|
82
|
-
type: "source",
|
|
83
|
-
position: Position.Right,
|
|
84
|
-
top: midY });
|
|
85
|
-
result.push({ id: "source-default-left",
|
|
86
|
-
type: "source",
|
|
87
|
-
position: Position.Left,
|
|
88
|
-
top: midY });
|
|
89
|
-
|
|
90
|
-
return result;
|
|
91
|
-
}, [columns, isJunction, collectionName, tableName]);
|
|
92
|
-
|
|
93
|
-
return (
|
|
94
|
-
<div
|
|
95
|
-
className={cls(
|
|
96
|
-
"relative rounded-lg border bg-white dark:bg-surface-900 shadow-sm transition-all duration-200 min-w-[240px] max-w-[320px]",
|
|
97
|
-
selected
|
|
98
|
-
? "border-primary ring-2 ring-primary/20 shadow-md"
|
|
99
|
-
: "border-surface-200/40 dark:border-surface-700/40 hover:shadow-md hover:border-surface-300 dark:hover:border-surface-600",
|
|
100
|
-
isJunction && "border-dashed"
|
|
101
|
-
)}
|
|
102
|
-
>
|
|
103
|
-
{/* ── Header ── */}
|
|
104
|
-
<div
|
|
105
|
-
className={cls(
|
|
106
|
-
"flex items-center gap-2 px-3 py-2 border-b rounded-t-lg",
|
|
107
|
-
isJunction
|
|
108
|
-
? "bg-surface-50 dark:bg-surface-950/50 border-surface-200/30 dark:border-surface-700/30"
|
|
109
|
-
: "bg-surface-50 dark:bg-surface-950 border-surface-200/40 dark:border-surface-700/40"
|
|
110
|
-
)}
|
|
111
|
-
>
|
|
112
|
-
{icon && !isJunction && (
|
|
113
|
-
<div className="text-primary shrink-0">
|
|
114
|
-
<IconForView
|
|
115
|
-
collectionOrView={{ slug: tableName,
|
|
116
|
-
name: collectionName,
|
|
117
|
-
icon }}
|
|
118
|
-
size="smallest"
|
|
119
|
-
/>
|
|
120
|
-
</div>
|
|
121
|
-
)}
|
|
122
|
-
{isJunction && (
|
|
123
|
-
<svg
|
|
124
|
-
className="w-3.5 h-3.5 text-text-disabled dark:text-text-disabled-dark shrink-0"
|
|
125
|
-
fill="none"
|
|
126
|
-
stroke="currentColor"
|
|
127
|
-
viewBox="0 0 24 24"
|
|
128
|
-
>
|
|
129
|
-
<path
|
|
130
|
-
strokeLinecap="round"
|
|
131
|
-
strokeLinejoin="round"
|
|
132
|
-
strokeWidth={2}
|
|
133
|
-
d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4"
|
|
134
|
-
/>
|
|
135
|
-
</svg>
|
|
136
|
-
)}
|
|
137
|
-
<div className="flex flex-col min-w-0">
|
|
138
|
-
<Typography
|
|
139
|
-
variant="caption"
|
|
140
|
-
className={cls(
|
|
141
|
-
"font-semibold truncate text-[12px]",
|
|
142
|
-
isJunction
|
|
143
|
-
? "text-text-secondary dark:text-text-secondary-dark"
|
|
144
|
-
: "text-text-primary dark:text-text-primary-dark"
|
|
145
|
-
)}
|
|
146
|
-
>
|
|
147
|
-
{tableName}
|
|
148
|
-
</Typography>
|
|
149
|
-
{collectionName !== tableName && !isJunction && (
|
|
150
|
-
<Typography
|
|
151
|
-
variant="caption"
|
|
152
|
-
className="text-[10px] text-text-disabled dark:text-text-disabled-dark truncate"
|
|
153
|
-
>
|
|
154
|
-
{collectionName}
|
|
155
|
-
</Typography>
|
|
156
|
-
)}
|
|
157
|
-
</div>
|
|
158
|
-
|
|
159
|
-
{/* Badges */}
|
|
160
|
-
<div className="ml-auto flex items-center gap-1 shrink-0">
|
|
161
|
-
{rlsEnabled && (
|
|
162
|
-
<Tooltip title="RLS enabled">
|
|
163
|
-
<div className="w-1.5 h-1.5 rounded-full bg-green-500"/>
|
|
164
|
-
</Tooltip>
|
|
165
|
-
)}
|
|
166
|
-
{historyEnabled && (
|
|
167
|
-
<Tooltip title="History enabled">
|
|
168
|
-
<div className="w-1.5 h-1.5 rounded-full bg-blue-400"/>
|
|
169
|
-
</Tooltip>
|
|
170
|
-
)}
|
|
171
|
-
</div>
|
|
172
|
-
</div>
|
|
173
|
-
|
|
174
|
-
{/* ── Columns ── */}
|
|
175
|
-
<div className="divide-y divide-surface-100 dark:divide-surface-950/60">
|
|
176
|
-
{(columns as ColumnInfo[]).map((col: ColumnInfo) => (
|
|
177
|
-
<div
|
|
178
|
-
key={col.name}
|
|
179
|
-
className={cls(
|
|
180
|
-
"flex items-center gap-2 px-3 py-1.5 text-xs transition-colors",
|
|
181
|
-
col.isPrimaryKey && "bg-amber-50/50 dark:bg-amber-950/10",
|
|
182
|
-
col.isForeignKey &&
|
|
183
|
-
!col.isPrimaryKey &&
|
|
184
|
-
"bg-blue-50/40 dark:bg-blue-950/10"
|
|
185
|
-
)}
|
|
186
|
-
>
|
|
187
|
-
<span className="w-3 shrink-0 text-center">
|
|
188
|
-
{col.isPrimaryKey && (
|
|
189
|
-
<Tooltip title="Primary Key">
|
|
190
|
-
<span className="text-amber-500 text-[10px] font-semibold">🔑</span>
|
|
191
|
-
</Tooltip>
|
|
192
|
-
)}
|
|
193
|
-
{col.isForeignKey && !col.isPrimaryKey && (
|
|
194
|
-
<Tooltip title={`FK → ${col.relationName ?? "?"}`}>
|
|
195
|
-
<span className="text-blue-400 text-[10px] font-semibold">🔗</span>
|
|
196
|
-
</Tooltip>
|
|
197
|
-
)}
|
|
198
|
-
</span>
|
|
199
|
-
|
|
200
|
-
<Typography
|
|
201
|
-
variant="caption"
|
|
202
|
-
className={cls(
|
|
203
|
-
"font-mono text-[11px] truncate flex-1 min-w-0",
|
|
204
|
-
col.isPrimaryKey
|
|
205
|
-
? "font-semibold text-amber-700 dark:text-amber-400"
|
|
206
|
-
: col.isForeignKey
|
|
207
|
-
? "text-blue-600 dark:text-blue-400"
|
|
208
|
-
: "text-text-primary dark:text-text-primary-dark"
|
|
209
|
-
)}
|
|
210
|
-
>
|
|
211
|
-
{col.name}
|
|
212
|
-
</Typography>
|
|
213
|
-
|
|
214
|
-
{col.isEnum ? (
|
|
215
|
-
<Chip
|
|
216
|
-
size="smallest"
|
|
217
|
-
className="bg-violet-100 text-violet-700 dark:bg-violet-900/30 dark:text-violet-400 border-violet-200 dark:border-violet-800 text-[9px] py-0"
|
|
218
|
-
>
|
|
219
|
-
enum
|
|
220
|
-
</Chip>
|
|
221
|
-
) : (
|
|
222
|
-
<Typography
|
|
223
|
-
variant="caption"
|
|
224
|
-
className="text-[10px] text-text-disabled dark:text-text-disabled-dark font-mono shrink-0"
|
|
225
|
-
>
|
|
226
|
-
{col.typeLabel}
|
|
227
|
-
</Typography>
|
|
228
|
-
)}
|
|
229
|
-
|
|
230
|
-
{col.isRequired && !col.isPrimaryKey && (
|
|
231
|
-
<Tooltip title="Required">
|
|
232
|
-
<span className="text-red-400 text-[9px]">•</span>
|
|
233
|
-
</Tooltip>
|
|
234
|
-
)}
|
|
235
|
-
</div>
|
|
236
|
-
))}
|
|
237
|
-
</div>
|
|
238
|
-
|
|
239
|
-
{/* ── Handles at node root ── */}
|
|
240
|
-
{handles.map((h) => (
|
|
241
|
-
<Handle
|
|
242
|
-
key={h.id}
|
|
243
|
-
id={h.id}
|
|
244
|
-
type={h.type}
|
|
245
|
-
position={h.position}
|
|
246
|
-
style={{ top: h.top }}
|
|
247
|
-
className={cls(
|
|
248
|
-
"!w-2 !h-2 !border-2 !border-white dark:!border-surface-900",
|
|
249
|
-
h.type === "source" ? "!bg-blue-400" : "!bg-amber-400"
|
|
250
|
-
)}
|
|
251
|
-
/>
|
|
252
|
-
))}
|
|
253
|
-
</div>
|
|
254
|
-
);
|
|
255
|
-
};
|
|
256
|
-
|
|
257
|
-
export const TableNode = memo(TableNodeInner);
|
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
import dagre from "dagre";
|
|
2
|
-
import type { Node, Edge } from "@xyflow/react";
|
|
3
|
-
|
|
4
|
-
// ─── Layout Constants ─────────────────────────────────────────────────
|
|
5
|
-
export const NODE_WIDTH = 280;
|
|
6
|
-
/** Header with a single line of text (junction tables or tableName === collectionName). */
|
|
7
|
-
const HEADER_HEIGHT_SINGLE = 33;
|
|
8
|
-
/** Header with two lines (name + subtitle when collectionName !== tableName). */
|
|
9
|
-
const HEADER_HEIGHT_DOUBLE = 47;
|
|
10
|
-
const ROW_HEIGHT = 28; // height per column row
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Compute the correct header height for a table node.
|
|
14
|
-
*/
|
|
15
|
-
export const getHeaderHeight = (opts: {
|
|
16
|
-
isJunction: boolean;
|
|
17
|
-
collectionName: string;
|
|
18
|
-
tableName: string;
|
|
19
|
-
}): number => {
|
|
20
|
-
if (opts.isJunction) return HEADER_HEIGHT_SINGLE;
|
|
21
|
-
return opts.collectionName !== opts.tableName
|
|
22
|
-
? HEADER_HEIGHT_DOUBLE
|
|
23
|
-
: HEADER_HEIGHT_SINGLE;
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Estimate the pixel height of a table node based on column count.
|
|
28
|
-
*/
|
|
29
|
-
const estimateNodeHeight = (columnCount: number, headerHeight: number = HEADER_HEIGHT_DOUBLE): number =>
|
|
30
|
-
headerHeight + Math.max(columnCount, 1) * ROW_HEIGHT + 4; // +4 for bottom padding
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Get the vertical center Y of a specific column row (0-indexed)
|
|
34
|
-
* relative to the top of the node.
|
|
35
|
-
*/
|
|
36
|
-
export const getColumnRowY = (rowIndex: number, headerHeight: number = HEADER_HEIGHT_DOUBLE): number =>
|
|
37
|
-
headerHeight + rowIndex * ROW_HEIGHT + ROW_HEIGHT / 2;
|
|
38
|
-
|
|
39
|
-
// ─── Auto-Layout via Dagre ────────────────────────────────────────────
|
|
40
|
-
|
|
41
|
-
export type LayoutDirection = "TB" | "LR";
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Compute node positions using the dagre graph layout engine.
|
|
45
|
-
* Returns a new array of nodes with updated `position`.
|
|
46
|
-
*/
|
|
47
|
-
export const getLayoutedElements = (
|
|
48
|
-
nodes: Node[],
|
|
49
|
-
edges: Edge[],
|
|
50
|
-
direction: LayoutDirection = "LR"
|
|
51
|
-
): { nodes: Node[]; edges: Edge[] } => {
|
|
52
|
-
const g = new dagre.graphlib.Graph();
|
|
53
|
-
g.setGraph({
|
|
54
|
-
rankdir: direction,
|
|
55
|
-
nodesep: 100,
|
|
56
|
-
ranksep: 180,
|
|
57
|
-
edgesep: 60,
|
|
58
|
-
marginx: 60,
|
|
59
|
-
marginy: 60
|
|
60
|
-
});
|
|
61
|
-
g.setDefaultEdgeLabel(() => ({}));
|
|
62
|
-
|
|
63
|
-
const nodeHeights = new Map<string, number>();
|
|
64
|
-
|
|
65
|
-
nodes.forEach((node) => {
|
|
66
|
-
const data = node.data as { columns?: unknown[]; isJunction?: boolean; collectionName?: string; tableName?: string };
|
|
67
|
-
const columnCount = data.columns?.length ?? 3;
|
|
68
|
-
const headerH = getHeaderHeight({
|
|
69
|
-
isJunction: Boolean(data.isJunction),
|
|
70
|
-
collectionName: data.collectionName ?? "",
|
|
71
|
-
tableName: data.tableName ?? ""
|
|
72
|
-
});
|
|
73
|
-
const h = estimateNodeHeight(columnCount, headerH);
|
|
74
|
-
nodeHeights.set(node.id, h);
|
|
75
|
-
g.setNode(node.id, {
|
|
76
|
-
width: NODE_WIDTH,
|
|
77
|
-
height: h
|
|
78
|
-
});
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
edges.forEach((edge) => {
|
|
82
|
-
g.setEdge(edge.source, edge.target);
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
dagre.layout(g);
|
|
86
|
-
|
|
87
|
-
const layoutedNodes = nodes.map((node) => {
|
|
88
|
-
const nodeWithPosition = g.node(node.id);
|
|
89
|
-
const h = nodeHeights.get(node.id) ?? estimateNodeHeight(3);
|
|
90
|
-
return {
|
|
91
|
-
...node,
|
|
92
|
-
data: {
|
|
93
|
-
...node.data,
|
|
94
|
-
layoutDirection: direction
|
|
95
|
-
},
|
|
96
|
-
position: {
|
|
97
|
-
x: nodeWithPosition.x - NODE_WIDTH / 2,
|
|
98
|
-
y: nodeWithPosition.y - h / 2
|
|
99
|
-
}
|
|
100
|
-
};
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
return { nodes: layoutedNodes,
|
|
104
|
-
edges };
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
// ─── Column type → display label ──────────────────────────────────────
|
|
108
|
-
|
|
109
|
-
const TYPE_LABELS: Record<string, string> = {
|
|
110
|
-
string: "varchar",
|
|
111
|
-
number: "integer",
|
|
112
|
-
boolean: "boolean",
|
|
113
|
-
date: "timestamp",
|
|
114
|
-
map: "jsonb",
|
|
115
|
-
array: "jsonb",
|
|
116
|
-
relation: "FK"
|
|
117
|
-
};
|
|
118
|
-
|
|
119
|
-
export const getTypeLabel = (type: string): string =>
|
|
120
|
-
TYPE_LABELS[type] ?? type;
|
|
121
|
-
|
|
122
|
-
// ─── Edge styling by relation type ────────────────────────────────────
|
|
123
|
-
|
|
124
|
-
export interface RelationEdgeData {
|
|
125
|
-
cardinality: "one" | "many";
|
|
126
|
-
direction: "owning" | "inverse";
|
|
127
|
-
relationName: string;
|
|
128
|
-
hasJunction: boolean;
|
|
129
|
-
hasJoinPath: boolean;
|
|
130
|
-
label: string;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
export const getCardinalityLabel = (
|
|
134
|
-
cardinality: "one" | "many",
|
|
135
|
-
direction: "owning" | "inverse"
|
|
136
|
-
): string => {
|
|
137
|
-
if (cardinality === "many") return "M:N";
|
|
138
|
-
if (direction === "inverse") return "1:1 ←";
|
|
139
|
-
return "N:1";
|
|
140
|
-
};
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Which tables actually have row-level security switched on, according to the
|
|
3
|
-
* database.
|
|
4
|
-
*
|
|
5
|
-
* ## Why this is not read from the collection config
|
|
6
|
-
*
|
|
7
|
-
* The graph used to derive "RLS protected" from `collection.securityRules` —
|
|
8
|
-
* i.e. from what the *codebase* declares. Two problems with that:
|
|
9
|
-
*
|
|
10
|
-
* - **It is not the truth.** Rules in code are only live once a migration has
|
|
11
|
-
* applied them, and `ALTER TABLE … DISABLE ROW LEVEL SECURITY` in a psql
|
|
12
|
-
* session is invisible to it. A security indicator that reports intent
|
|
13
|
-
* rather than state is worse than none: it is green precisely when you most
|
|
14
|
-
* need it to be red.
|
|
15
|
-
* - **The hosted console has no codebase.** There, collections arrive from the
|
|
16
|
-
* tenant's `/api/meta/contract`, which deliberately strips `securityRules`
|
|
17
|
-
* (they carry the raw SQL of every policy guarding the project). So the
|
|
18
|
-
* console showed "RLS protected: 0" and no badge on any table, for every
|
|
19
|
-
* project, however well secured.
|
|
20
|
-
*
|
|
21
|
-
* `pg_tables.rowsecurity` answers the question directly and is available
|
|
22
|
-
* wherever `databaseAdmin` is — which is both hosts.
|
|
23
|
-
*
|
|
24
|
-
* When there is no SQL capability at all the hook reports `null`, and callers
|
|
25
|
-
* fall back to the config-derived flag rather than claiming everything is
|
|
26
|
-
* unprotected.
|
|
27
|
-
*/
|
|
28
|
-
import { useEffect, useState } from "react";
|
|
29
|
-
import { useRebaseContext } from "@rebasepro/app";
|
|
30
|
-
|
|
31
|
-
/** `schema.table` for every table with RLS enabled, or `null` if unknown. */
|
|
32
|
-
export type LiveRlsTables = Set<string> | null;
|
|
33
|
-
|
|
34
|
-
const RLS_TABLES_SQL = `
|
|
35
|
-
SELECT schemaname, tablename, rowsecurity
|
|
36
|
-
FROM pg_tables
|
|
37
|
-
WHERE schemaname NOT IN ('information_schema', 'pg_catalog');
|
|
38
|
-
`;
|
|
39
|
-
|
|
40
|
-
/** Postgres column names arrive lower-cased over the wire, but not on every driver. */
|
|
41
|
-
function pick<T>(row: Record<string, unknown>, name: string): T | undefined {
|
|
42
|
-
return (row[name] ?? row[name.toUpperCase()]) as T | undefined;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function extractRows(result: unknown): Record<string, unknown>[] {
|
|
46
|
-
if (result && typeof result === "object" && "rows" in result) {
|
|
47
|
-
const rows = (result as { rows: unknown }).rows;
|
|
48
|
-
if (Array.isArray(rows)) return rows as Record<string, unknown>[];
|
|
49
|
-
}
|
|
50
|
-
if (Array.isArray(result)) return result as Record<string, unknown>[];
|
|
51
|
-
return [];
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export function useLiveRlsTables(): LiveRlsTables {
|
|
55
|
-
const { databaseAdmin } = useRebaseContext();
|
|
56
|
-
const executeSql = databaseAdmin?.executeSql;
|
|
57
|
-
const [tables, setTables] = useState<LiveRlsTables>(null);
|
|
58
|
-
|
|
59
|
-
useEffect(() => {
|
|
60
|
-
if (!executeSql) {
|
|
61
|
-
setTables(null);
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
let cancelled = false;
|
|
65
|
-
executeSql(RLS_TABLES_SQL)
|
|
66
|
-
.then((result: unknown) => {
|
|
67
|
-
if (cancelled) return;
|
|
68
|
-
const enabled = new Set<string>();
|
|
69
|
-
for (const row of extractRows(result)) {
|
|
70
|
-
if (!pick<boolean>(row, "rowsecurity")) continue;
|
|
71
|
-
const schema = pick<string>(row, "schemaname") ?? "public";
|
|
72
|
-
const table = pick<string>(row, "tablename");
|
|
73
|
-
if (table) enabled.add(`${schema}.${table}`);
|
|
74
|
-
}
|
|
75
|
-
setTables(enabled);
|
|
76
|
-
})
|
|
77
|
-
.catch(() => {
|
|
78
|
-
// No answer is not the same as "nothing is protected" — leave it
|
|
79
|
-
// unknown so the caller keeps its config-derived fallback.
|
|
80
|
-
if (!cancelled) setTables(null);
|
|
81
|
-
});
|
|
82
|
-
return () => { cancelled = true; };
|
|
83
|
-
}, [executeSql]);
|
|
84
|
-
|
|
85
|
-
return tables;
|
|
86
|
-
}
|