@rebasepro/studio 0.17.3 → 0.18.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 (81) hide show
  1. package/LICENSE +0 -1
  2. package/README.md +12 -4
  3. package/dist/{ApiKeysView-DiCurTEU.js → ApiKeysView-EIvegrUk.js} +54 -39
  4. package/dist/ApiKeysView-EIvegrUk.js.map +1 -0
  5. package/dist/{BackupsView-DNS6LdVg.js → BackupsView-DVkKzaPR.js} +29 -31
  6. package/dist/BackupsView-DVkKzaPR.js.map +1 -0
  7. package/dist/{BranchesView-CM3B-ER3.js → BranchesView-Df8hQ8wE.js} +85 -24
  8. package/dist/BranchesView-Df8hQ8wE.js.map +1 -0
  9. package/dist/{CronJobsView-BHtJZeJZ.js → CronJobsView-CLOGjb-L.js} +32 -27
  10. package/dist/CronJobsView-CLOGjb-L.js.map +1 -0
  11. package/dist/{RLSEditor-CBX4uD66.js → RLSEditor-Sa1D34ny.js} +724 -615
  12. package/dist/RLSEditor-Sa1D34ny.js.map +1 -0
  13. package/dist/{SQLEditor-ByecwX8B.js → SQLEditor-BfNA8xM3.js} +9 -4
  14. package/dist/{SQLEditor-ByecwX8B.js.map → SQLEditor-BfNA8xM3.js.map} +1 -1
  15. package/dist/{SchemaVisualizer-D6tMZgeC.js → SchemaVisualizer-DTDMYL6o.js} +84 -110
  16. package/dist/SchemaVisualizer-DTDMYL6o.js.map +1 -0
  17. package/dist/{StorageView-BGDeC5GI.js → StorageView-Ed9Q_VlB.js} +5 -32
  18. package/dist/StorageView-Ed9Q_VlB.js.map +1 -0
  19. package/dist/components/RLSEditor/saveRules.d.ts +38 -0
  20. package/dist/components/RebaseStudio.d.ts +1 -1
  21. package/dist/components/SchemaVisualizer/schema-visualizer.utils.d.ts +5 -2
  22. package/dist/components/SchemaVisualizer/useSchemaGraph.d.ts +0 -4
  23. package/dist/components/load-failure-view.d.ts +17 -0
  24. package/dist/components/load-failure.d.ts +47 -0
  25. package/dist/index.es.js +56 -36
  26. package/dist/index.es.js.map +1 -1
  27. package/dist/load-failure-C6OMJSGC.js +31 -0
  28. package/dist/load-failure-C6OMJSGC.js.map +1 -0
  29. package/dist/load-failure-view-CBTB_h-h.js +48 -0
  30. package/dist/load-failure-view-CBTB_h-h.js.map +1 -0
  31. package/package.json +36 -21
  32. package/dist/ApiKeysView-DiCurTEU.js.map +0 -1
  33. package/dist/BackupsView-DNS6LdVg.js.map +0 -1
  34. package/dist/BranchesView-CM3B-ER3.js.map +0 -1
  35. package/dist/CronJobsView-BHtJZeJZ.js.map +0 -1
  36. package/dist/RLSEditor-CBX4uD66.js.map +0 -1
  37. package/dist/SchemaVisualizer-D6tMZgeC.js.map +0 -1
  38. package/dist/StorageView-BGDeC5GI.js.map +0 -1
  39. package/dist/components/StorageView/storage-failure.d.ts +0 -34
  40. package/src/components/ApiExplorer/ApiExplorer.tsx +0 -299
  41. package/src/components/ApiExplorer/EndpointDetail.tsx +0 -279
  42. package/src/components/ApiExplorer/TryItPanel.tsx +0 -499
  43. package/src/components/ApiExplorer/parseSpec.ts +0 -104
  44. package/src/components/ApiExplorer/types.ts +0 -84
  45. package/src/components/ApiKeys/ApiKeysView.tsx +0 -479
  46. package/src/components/ApiKeys/CreateApiKeyDialog.tsx +0 -711
  47. package/src/components/ApiKeys/permissions.ts +0 -145
  48. package/src/components/AuthSimulationSelector.tsx +0 -72
  49. package/src/components/Backups/BackupsView.tsx +0 -185
  50. package/src/components/Branches/BranchesView.tsx +0 -386
  51. package/src/components/CronJobs/CronJobsView.tsx +0 -413
  52. package/src/components/JSEditor/JSEditor.tsx +0 -1100
  53. package/src/components/JSEditor/JSEditorSidebar.tsx +0 -350
  54. package/src/components/JSEditor/JSMonacoEditor.tsx +0 -410
  55. package/src/components/LogsExplorer/LogsExplorer.tsx +0 -273
  56. package/src/components/LogsExplorer/useLogTail.ts +0 -356
  57. package/src/components/RLSEditor/PolicyEditor.tsx +0 -399
  58. package/src/components/RLSEditor/RLSEditor.tsx +0 -1128
  59. package/src/components/RLSEditor/index.ts +0 -1
  60. package/src/components/RLSEditor/policy-presets.ts +0 -176
  61. package/src/components/RLSEditor/saveSecurityRules.ts +0 -48
  62. package/src/components/RebaseStudio.tsx +0 -174
  63. package/src/components/SQLEditor/ExplainVisualizer.tsx +0 -128
  64. package/src/components/SQLEditor/MonacoEditor.tsx +0 -203
  65. package/src/components/SQLEditor/SQLEditor.tsx +0 -1518
  66. package/src/components/SQLEditor/SQLEditorSidebar.tsx +0 -173
  67. package/src/components/SQLEditor/SchemaBrowser.tsx +0 -168
  68. package/src/components/SQLEditor/sql_editor_types.ts +0 -18
  69. package/src/components/SchemaVisualizer/RelationEdge.tsx +0 -102
  70. package/src/components/SchemaVisualizer/SchemaVisualizer.tsx +0 -710
  71. package/src/components/SchemaVisualizer/TableNode.tsx +0 -257
  72. package/src/components/SchemaVisualizer/schema-visualizer.utils.ts +0 -140
  73. package/src/components/SchemaVisualizer/useLiveRls.ts +0 -86
  74. package/src/components/SchemaVisualizer/useSchemaGraph.ts +0 -426
  75. package/src/components/StorageView/StorageView.tsx +0 -1588
  76. package/src/components/StorageView/storage-failure.ts +0 -63
  77. package/src/components/StudioHomePage.tsx +0 -430
  78. package/src/index.ts +0 -31
  79. package/src/utils/parseSpec.test.ts +0 -309
  80. package/src/utils/sql_utils.test.ts +0 -265
  81. package/src/utils/sql_utils.ts +0 -291
@@ -1,426 +0,0 @@
1
- import { useMemo, useState, useCallback, useEffect } from "react";
2
- import type { Node, Edge } from "@xyflow/react";
3
- import { MarkerType } from "@xyflow/react";
4
- import { isPostgresCollectionConfig } from "@rebasepro/types";
5
- import type { Relation, ResolvedRelation } from "@rebasepro/types";
6
- import { resolveCollectionRelations } from "@rebasepro/common";
7
- import { getLayoutedElements, getCardinalityLabel, getTypeLabel, NODE_WIDTH } from "./schema-visualizer.utils";
8
- import type { LayoutDirection, RelationEdgeData } from "./schema-visualizer.utils";
9
- import type { AdminCollection, AdminPostgresCollection } from "@rebasepro/cms-types";
10
-
11
- // ─── Column info extracted from a collection ──────────────────────────
12
-
13
- export interface ColumnInfo {
14
- name: string;
15
- type: string;
16
- typeLabel: string;
17
- isPrimaryKey: boolean;
18
- isForeignKey: boolean;
19
- isRequired: boolean;
20
- isEnum: boolean;
21
- enumValues?: string[];
22
- relationName?: string;
23
- }
24
-
25
- // ─── Node data shape ──────────────────────────────────────────────────
26
-
27
- export interface TableNodeData {
28
- tableName: string;
29
- collectionName: string;
30
- slug: string;
31
- columns: ColumnInfo[];
32
- isJunction: boolean;
33
- isUnmanaged: boolean;
34
- rlsEnabled: boolean;
35
- historyEnabled: boolean;
36
- icon?: string;
37
- [key: string]: unknown;
38
- }
39
-
40
- // ─── Extract columns from a AdminCollection ─────────────────────────
41
-
42
- const extractColumns = (collection: AdminCollection): ColumnInfo[] => {
43
- const columns: ColumnInfo[] = [];
44
- const properties = collection.properties ?? {};
45
-
46
- for (const [propName, prop] of Object.entries(properties)) {
47
- if (prop.type === "relation") continue; // Relations are shown as edges, not columns
48
-
49
- const isPk =
50
- ("isId" in prop && Boolean(prop.isId)) ||
51
- (!Object.values(properties).some(
52
- (p) => "isId" in p && Boolean(p.isId)
53
- ) &&
54
- propName === "id");
55
-
56
- const isEnum = prop.type === "string" && "enum" in prop && Boolean(prop.enum);
57
-
58
- let enumValues: string[] | undefined;
59
- if (isEnum && "enum" in prop && prop.enum) {
60
- const enumProp = prop.enum;
61
- if (Array.isArray(enumProp)) {
62
- enumValues = enumProp.map((v: unknown) =>
63
- typeof v === "string" ? v : String((v as Record<string, unknown>)?.id ?? v)
64
- );
65
- } else if (typeof enumProp === "object" && enumProp !== null) {
66
- enumValues = Object.keys(enumProp);
67
- }
68
- }
69
-
70
- columns.push({
71
- name: propName,
72
- type: prop.type,
73
- typeLabel: getTypeLabel(prop.type),
74
- isPrimaryKey: isPk,
75
- isForeignKey: false,
76
- isRequired: Boolean(prop.validation?.required),
77
- isEnum,
78
- enumValues
79
- });
80
- }
81
-
82
- // Add FK columns from owning relations
83
- if (isPostgresCollectionConfig(collection)) {
84
- try {
85
- const resolvedRelations = resolveCollectionRelations(collection);
86
- for (const rel of Object.values(resolvedRelations)) {
87
- if (rel.kind === "belongsTo" && rel.localKey) {
88
- // Only add if not already present as a regular column
89
- if (!columns.some((c) => c.name === rel.localKey)) {
90
- columns.push({
91
- name: rel.localKey,
92
- type: "number",
93
- typeLabel: "FK",
94
- isPrimaryKey: false,
95
- isForeignKey: true,
96
- isRequired: false,
97
- isEnum: false,
98
- relationName: rel.relationName
99
- });
100
- } else {
101
- // Mark existing column as FK
102
- const existing = columns.find((c) => c.name === rel.localKey);
103
- if (existing) {
104
- existing.isForeignKey = true;
105
- existing.relationName = rel.relationName;
106
- }
107
- }
108
- }
109
- }
110
- } catch {
111
- // Ignore resolution errors
112
- }
113
- }
114
-
115
- return columns;
116
- };
117
-
118
- // ─── Build graph from collections ─────────────────────────────────────
119
-
120
- const buildGraph = (
121
- collections: AdminCollection[],
122
- direction: LayoutDirection,
123
- liveRls: Set<string> | null
124
- ): { nodes: Node[]; edges: Edge[] } => {
125
- /**
126
- * Is RLS on for this table?
127
- *
128
- * The database is the authority whenever we could reach it. Declaring rules
129
- * in code is not the same as having them applied, and the hosted console
130
- * never sees the declarations at all — see `useLiveRlsTables`. Only when
131
- * there is no SQL capability do we fall back to what the config says.
132
- */
133
- const isRlsEnabled = (schema: string, tableName: string, declaresRules: boolean): boolean =>
134
- liveRls ? liveRls.has(`${schema}.${tableName}`) : declaresRules;
135
-
136
- const nodes: Node[] = [];
137
- const edges: Edge[] = [];
138
- const tableToNodeId = new Map<string, string>();
139
- // Track columns per node so we can resolve handle IDs for edges
140
- const nodeColumns = new Map<string, ColumnInfo[]>();
141
-
142
- // Helper: find the PK column name for a node (falls back to "id")
143
- const getPkHandle = (nodeId: string): string => {
144
- const cols = nodeColumns.get(nodeId);
145
- const pk = cols?.find((c) => c.isPrimaryKey);
146
- return pk ? `target-${pk.name}` : "target-default";
147
- };
148
-
149
- // Helper: find the FK column name for a node pointing to a relation
150
- const getFkHandle = (nodeId: string, localKey: string | undefined): string => {
151
- if (!localKey) return "source-default";
152
- const cols = nodeColumns.get(nodeId);
153
- const fk = cols?.find((c) => c.name === localKey);
154
- return fk ? `source-${fk.name}` : "source-default";
155
- };
156
-
157
- // 1. Create nodes for each collection
158
- for (const collection of collections) {
159
- if (!isPostgresCollectionConfig(collection)) continue;
160
-
161
- const tableName = collection.table ?? collection.slug;
162
- const nodeId = `table-${tableName}`;
163
- tableToNodeId.set(tableName, nodeId);
164
-
165
- const columns = extractColumns(collection);
166
- nodeColumns.set(nodeId, columns);
167
-
168
- const nodeData: TableNodeData = {
169
- tableName,
170
- collectionName: collection.name,
171
- slug: collection.slug,
172
- columns,
173
- isJunction: false,
174
- isUnmanaged: false,
175
- rlsEnabled: isRlsEnabled(
176
- collection.schema ?? "public",
177
- tableName,
178
- Boolean(collection.securityRules && collection.securityRules.length > 0)
179
- ),
180
- historyEnabled: Boolean(collection.history),
181
- icon: typeof collection.icon === "string" ? collection.icon : undefined
182
- };
183
-
184
- nodes.push({
185
- id: nodeId,
186
- type: "tableNode",
187
- position: { x: 0,
188
- y: 0 },
189
- data: nodeData
190
- });
191
- }
192
-
193
- // 2. Create junction table nodes and edges
194
- const processedJunctions = new Set<string>();
195
-
196
- for (const collection of collections) {
197
- if (!isPostgresCollectionConfig(collection)) continue;
198
-
199
- const tableName = collection.table ?? collection.slug;
200
- const sourceNodeId = tableToNodeId.get(tableName);
201
- if (!sourceNodeId) continue;
202
-
203
- let resolvedRelations: Record<string, ResolvedRelation>;
204
- try {
205
- resolvedRelations = resolveCollectionRelations(collection);
206
- } catch {
207
- continue;
208
- }
209
-
210
- for (const [relationKey, rel] of Object.entries(resolvedRelations)) {
211
- let targetCollection: AdminCollection;
212
- try {
213
- targetCollection = rel.target();
214
- } catch {
215
- continue;
216
- }
217
-
218
- if (!isPostgresCollectionConfig(targetCollection)) continue;
219
-
220
- const targetTable = targetCollection.table ?? targetCollection.slug;
221
- const targetNodeId = tableToNodeId.get(targetTable);
222
- if (!targetNodeId) continue;
223
-
224
- // Draw the side that owns the storage: a foreign key on this table,
225
- // or the junction. `hasOne`/`hasMany` are the mirror image of a
226
- // `belongsTo` that is drawn from the other end, and `via` has no
227
- // single edge to draw.
228
- if (rel.kind !== "belongsTo" && rel.kind !== "manyToMany") continue;
229
-
230
- const direction = "owning" as const;
231
- const edgeData: RelationEdgeData = {
232
- cardinality: rel.cardinality,
233
- direction,
234
- relationName: rel.relationName ?? relationKey,
235
- hasJunction: rel.kind === "manyToMany",
236
- hasJoinPath: false,
237
- label: getCardinalityLabel(rel.cardinality, direction)
238
- };
239
-
240
- if (rel.kind === "manyToMany" && !processedJunctions.has(rel.through.table)) {
241
- // Many-to-many: create junction node + two edges
242
- processedJunctions.add(rel.through.table);
243
- const junctionNodeId = `junction-${rel.through.table}`;
244
-
245
- const junctionColumns: ColumnInfo[] = [
246
- {
247
- name: rel.through.sourceColumn,
248
- type: "number",
249
- typeLabel: "FK",
250
- isPrimaryKey: true,
251
- isForeignKey: true,
252
- isRequired: true,
253
- isEnum: false
254
- },
255
- {
256
- name: rel.through.targetColumn,
257
- type: "number",
258
- typeLabel: "FK",
259
- isPrimaryKey: true,
260
- isForeignKey: true,
261
- isRequired: true,
262
- isEnum: false
263
- }
264
- ];
265
- nodeColumns.set(junctionNodeId, junctionColumns);
266
-
267
- nodes.push({
268
- id: junctionNodeId,
269
- type: "tableNode",
270
- position: { x: 0,
271
- y: 0 },
272
- data: {
273
- tableName: rel.through.table,
274
- collectionName: rel.through.table,
275
- slug: rel.through.table,
276
- columns: junctionColumns,
277
- isJunction: true,
278
- isUnmanaged: false,
279
- // Junction tables get derived policies rather than
280
- // declared ones, so the config can never say — but the
281
- // database can, and usually says yes.
282
- rlsEnabled: isRlsEnabled("public", rel.through.table, false),
283
- historyEnabled: false
284
- } satisfies TableNodeData
285
- });
286
-
287
- // Source table → junction (junction references source PK)
288
- const sourcePk = nodeColumns.get(sourceNodeId)?.find((c) => c.isPrimaryKey);
289
- edges.push({
290
- id: `edge-${sourceNodeId}-${junctionNodeId}`,
291
- source: sourceNodeId,
292
- target: junctionNodeId,
293
- sourceHandle: sourcePk ? `source-${sourcePk.name}` : "source-default",
294
- targetHandle: `target-${rel.through.sourceColumn}`,
295
- type: "relationEdge",
296
- data: { ...edgeData,
297
- label: "1:N" } as Record<string, unknown>,
298
- markerEnd: { type: MarkerType.ArrowClosed,
299
- width: 16,
300
- height: 16 }
301
- });
302
-
303
- // Junction → target table
304
- edges.push({
305
- id: `edge-${junctionNodeId}-${targetNodeId}`,
306
- source: junctionNodeId,
307
- target: targetNodeId,
308
- sourceHandle: `source-${rel.through.targetColumn}`,
309
- targetHandle: getPkHandle(targetNodeId),
310
- type: "relationEdge",
311
- data: { ...edgeData,
312
- label: "N:1" } as Record<string, unknown>,
313
- markerEnd: { type: MarkerType.ArrowClosed,
314
- width: 16,
315
- height: 16 }
316
- });
317
- } else if (rel.kind === "belongsTo") {
318
- // A foreign key on this table.
319
- edges.push({
320
- id: `edge-${sourceNodeId}-${targetNodeId}-${relationKey}`,
321
- source: sourceNodeId,
322
- target: targetNodeId,
323
- sourceHandle: getFkHandle(sourceNodeId, rel.localKey),
324
- targetHandle: getPkHandle(targetNodeId),
325
- type: "relationEdge",
326
- data: { ...edgeData } as Record<string, unknown>,
327
- markerEnd: { type: MarkerType.ArrowClosed,
328
- width: 16,
329
- height: 16 }
330
- });
331
- }
332
- }
333
- }
334
- // 3. Apply dagre layout first to get node positions
335
- const layoutResult = getLayoutedElements(nodes, edges, direction);
336
-
337
- // 4. Build a position lookup from the laid-out nodes
338
- const nodePositions = new Map<string, { x: number }>();
339
- for (const node of layoutResult.nodes) {
340
- nodePositions.set(node.id, { x: node.position.x });
341
- }
342
-
343
- // 5. Resolve handle sides based on relative node positions.
344
- // If source is left of target → source exits Right, target enters Left.
345
- // If source is right of target → source exits Left, target enters Right.
346
- for (const edge of layoutResult.edges) {
347
- const srcPos = nodePositions.get(edge.source);
348
- const tgtPos = nodePositions.get(edge.target);
349
-
350
- const srcBaseHandle = (edge.sourceHandle ?? "source-default") as string;
351
- const tgtBaseHandle = (edge.targetHandle ?? "target-default") as string;
352
-
353
- if (srcPos && tgtPos) {
354
- const srcCenterX = srcPos.x + NODE_WIDTH / 2;
355
- const tgtCenterX = tgtPos.x + NODE_WIDTH / 2;
356
- const sourceIsLeft = srcCenterX <= tgtCenterX;
357
-
358
- edge.sourceHandle = `${srcBaseHandle}-${sourceIsLeft ? "right" : "left"}`;
359
- edge.targetHandle = `${tgtBaseHandle}-${sourceIsLeft ? "left" : "right"}`;
360
- } else {
361
- edge.sourceHandle = `${srcBaseHandle}-right`;
362
- edge.targetHandle = `${tgtBaseHandle}-left`;
363
- }
364
- }
365
-
366
- return layoutResult;
367
- };
368
-
369
- // ─── Hook ─────────────────────────────────────────────────────────────
370
-
371
- export interface UseSchemaGraphResult {
372
- nodes: Node[];
373
- edges: Edge[];
374
- direction: LayoutDirection;
375
- setDirection: (dir: LayoutDirection) => void;
376
- relayout: () => void;
377
- isLoading: boolean;
378
- tableCount: number;
379
- relationCount: number;
380
- }
381
-
382
- export const useSchemaGraph = (
383
- collections: AdminCollection[] | undefined,
384
- /**
385
- * `schema.table` for every table the database reports RLS on, or `null`
386
- * when there is no SQL capability to ask. See {@link useLiveRlsTables}.
387
- */
388
- liveRls: Set<string> | null = null
389
- ): UseSchemaGraphResult => {
390
- const [direction, setDirection] = useState<LayoutDirection>("LR");
391
- const [version, setVersion] = useState(0);
392
-
393
- const relayout = useCallback(() => setVersion((v) => v + 1), []);
394
-
395
- // Re-layout on direction change
396
- useEffect(() => {
397
- setVersion((v) => v + 1);
398
- }, [direction]);
399
-
400
- const { nodes, edges, tableCount, relationCount } = useMemo(() => {
401
- if (!collections || collections.length === 0) {
402
- return { nodes: [],
403
- edges: [],
404
- tableCount: 0,
405
- relationCount: 0 };
406
- }
407
- const result = buildGraph(collections, direction, liveRls);
408
- return {
409
- ...result,
410
- tableCount: result.nodes.length,
411
- relationCount: result.edges.length
412
- };
413
- // eslint-disable-next-line react-hooks/exhaustive-deps
414
- }, [collections, direction, version, liveRls]);
415
-
416
- return {
417
- nodes,
418
- edges,
419
- direction,
420
- setDirection,
421
- relayout,
422
- isLoading: !collections,
423
- tableCount,
424
- relationCount
425
- };
426
- };